Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix autocompletion for LinkManager and AttributeManager #3985

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions aiida/common/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
"""Module that define the exceptions that are thrown by AiiDA's internal code."""

__all__ = (
'AiidaException', 'NotExistent', 'MultipleObjectsError', 'RemoteOperationError', 'ContentNotExistent',
'FailedError', 'StoringNotAllowed', 'ModificationNotAllowed', 'IntegrityError', 'UniquenessError',
'EntryPointError', 'MissingEntryPointError', 'MultipleEntryPointError', 'LoadingEntryPointError',
'InvalidEntryPointTypeError', 'InvalidOperation', 'ParsingError', 'InternalError', 'PluginInternalError',
'ValidationError', 'ConfigurationError', 'ProfileConfigurationError', 'MissingConfigurationError',
'ConfigurationVersionError', 'IncompatibleDatabaseSchema', 'DbContentError', 'InputValidationError',
'FeatureNotAvailable', 'FeatureDisabled', 'LicensingException', 'TestsNotAllowedError', 'UnsupportedSpeciesError',
'TransportTaskException', 'OutputParsingError'
'AiidaException', 'NotExistent', 'NotExistentAttributeError', 'NotExistentKeyError', 'MultipleObjectsError',
'RemoteOperationError', 'ContentNotExistent', 'FailedError', 'StoringNotAllowed', 'ModificationNotAllowed',
'IntegrityError', 'UniquenessError', 'EntryPointError', 'MissingEntryPointError', 'MultipleEntryPointError',
'LoadingEntryPointError', 'InvalidEntryPointTypeError', 'InvalidOperation', 'ParsingError', 'InternalError',
'PluginInternalError', 'ValidationError', 'ConfigurationError', 'ProfileConfigurationError',
'MissingConfigurationError', 'ConfigurationVersionError', 'IncompatibleDatabaseSchema', 'DbContentError',
'InputValidationError', 'FeatureNotAvailable', 'FeatureDisabled', 'LicensingException', 'TestsNotAllowedError',
'UnsupportedSpeciesError', 'TransportTaskException', 'OutputParsingError'
)


Expand All @@ -36,6 +36,18 @@ class NotExistent(AiidaException):
"""


class NotExistentAttributeError(AttributeError, NotExistent):
"""
Raised when the required entity does not exist, when fetched as an attribute.
"""


class NotExistentKeyError(KeyError, NotExistent):
"""
Raised when the required entity does not exist, when fetched as a dictionary key.
"""


class MultipleObjectsError(AiidaException):
"""
Raised when more than one entity is found in the DB, but only one was
Expand Down
22 changes: 17 additions & 5 deletions aiida/orm/utils/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""

from aiida.common.links import LinkType
from aiida.common.exceptions import NotExistent, NotExistentAttributeError, NotExistentKeyError

__all__ = ('NodeLinksManager', 'AttributeManager')

Expand Down Expand Up @@ -80,8 +81,15 @@ def __getattr__(self, name):
"""
try:
return self._get_node_by_link_label(label=name)
except KeyError:
raise AttributeError("Node '{}' does not have an input with link '{}'".format(self._node.pk, name))
except NotExistent:
# Note: in order for TAB-completion to work, we need to raise an
# exception that also inherits from AttributeError, so that
# `getattr(node.inputs, 'some_label', some_default)` returns
# `some_default`. Otherwise, the exception is not caught by
# `getattr` and is just propagated, instead of returning the default.
raise NotExistentAttributeError(
"Node '{}' does not have an input with link '{}'".format(self._node.pk, name)
)

def __getitem__(self, name):
"""
Expand All @@ -91,8 +99,12 @@ def __getitem__(self, name):
"""
try:
return self._get_node_by_link_label(label=name)
except KeyError:
raise KeyError("Node '{}' does not have an input with link '{}'".format(self._node.pk, name))
except NotExistent:
# Note: in order for this class to behave as a dictionary, we raise
# an exception that also inherits from KeyError - in this way, users
# can use the standard construct `try/except KeyError` and this will
# behave like a standard dictionary.
raise NotExistentKeyError("Node '{}' does not have an input with link '{}'".format(self._node.pk, name))

def __str__(self):
"""Return a string representation of the manager"""
Expand Down Expand Up @@ -125,7 +137,7 @@ def __dir__(self):
"""
Allow to list the keys of the dictionary
"""
return sorted(self._node.attributes_items())
return sorted(self._node.attributes_keys())

def __iter__(self):
"""
Expand Down
8 changes: 4 additions & 4 deletions aiida/parsers/plugins/arithmetic/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@
# For further information on the license, see the LICENSE.txt file #
# For further information please visit http://www.aiida.net #
###########################################################################
from aiida.common import exceptions
from aiida.orm import Int
from aiida.parsers.parser import Parser


class ArithmeticAddParser(Parser):
"""Parser for an ArithmeticAdd calculation."""

def parse(self, **kwargs):
def parse(self, **kwargs): # pylint: disable=inconsistent-return-statements
"""Parse the contents of the output files retrieved in the `FolderData`."""
try:
output_folder = self.retrieved
except exceptions.NotExistent:
except AttributeError:
return self.exit_codes.ERROR_NO_RETRIEVED_FOLDER

try:
Expand All @@ -32,7 +32,7 @@ def parse(self, **kwargs):

try:
allow_negative = self.node.inputs.settings.get_attribute('allow_negative', True)
except exceptions.NotExistent:
except AttributeError:
allow_negative = True

if not allow_negative and result < 0:
Expand Down
8 changes: 4 additions & 4 deletions tests/orm/node/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -706,29 +706,29 @@ def test_tab_completable_properties(self):
# .inputs for calculations
self.assertEqual(calc1.inputs.input_value.pk, input1.pk)
self.assertEqual(calc2.inputs.input_value.pk, input2.pk)
with self.assertRaises(exceptions.NotExistent):
with self.assertRaises(AttributeError):
_ = calc1.inputs.some_label

# .inputs for workflows
self.assertEqual(top_workflow.inputs.a.pk, input1.pk)
self.assertEqual(top_workflow.inputs.b.pk, input2.pk)
self.assertEqual(workflow.inputs.a.pk, input1.pk)
self.assertEqual(workflow.inputs.b.pk, input2.pk)
with self.assertRaises(exceptions.NotExistent):
with self.assertRaises(AttributeError):
_ = workflow.inputs.some_label

# .outputs for calculations
self.assertEqual(calc1.outputs.result.pk, output1.pk)
self.assertEqual(calc2.outputs.result.pk, output2.pk)
with self.assertRaises(exceptions.NotExistent):
with self.assertRaises(AttributeError):
_ = calc1.outputs.some_label

# .outputs for workflows
self.assertEqual(top_workflow.outputs.result_a.pk, output1.pk)
self.assertEqual(top_workflow.outputs.result_b.pk, output2.pk)
self.assertEqual(workflow.outputs.result_a.pk, output1.pk)
self.assertEqual(workflow.outputs.result_b.pk, output2.pk)
with self.assertRaises(exceptions.NotExistent):
with self.assertRaises(AttributeError):
_ = workflow.outputs.some_label


Expand Down
137 changes: 137 additions & 0 deletions tests/orm/utils/test_managers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# -*- coding: utf-8 -*-
###########################################################################
# Copyright (c), The AiiDA team. All rights reserved. #
# This file is part of the AiiDA code. #
# #
# The code is hosted on GitHub at https://github.com/aiidateam/aiida-core #
# For further information on the license, see the LICENSE.txt file #
# For further information please visit http://www.aiida.net #
###########################################################################
"""Tests for the various node managers (.inputs, .outputs, .dict, ...)."""
# pylint: disable=unused-argument

import pytest

from aiida import orm
from aiida.common.exceptions import NotExistent, NotExistentAttributeError, NotExistentKeyError
from aiida.common import LinkType


def test_dot_dict_manager(clear_database_before_test):
"""Verify that the Dict.dict manager behaves as intended."""
dict_content = {'a': True, 'b': 1, 'c': 'Some string'}
dict_node = orm.Dict(dict=dict_content)

# Check that dir() return all keys and nothing else, important
# for tab competion
assert len(dir(dict_node.dict)) == len(dict_content)
assert set(dir(dict_node.dict)) == set(dict_content)
# Check that it works also as an iterator
assert len(list(dict_node.dict)) == len(dict_content)
assert set(dict_node.dict) == set(dict_content)

for key, val in dict_content.items():
# dict_node.dict.a == True, ...
assert getattr(dict_node.dict, key) == val
# dict_node.dict['a'] == True, ...
assert dict_node.dict[key] == val

# I check the attribute fetching directly
assert dict_node.dict.b == 1

# Must raise a AttributeError, otherwise tab competion will not work
with pytest.raises(AttributeError):
getattr(dict_node.dict, 'NotExistentKey')

# Must raise a KeyError
with pytest.raises(KeyError):
_ = dict_node.dict['NotExistentKey']


def test_link_manager(clear_database_before_test):
"""Test the LinkManager via .inputs and .outputs from a ProcessNode."""
# I first create a calculation with two inputs and two outputs

# Create inputs
inp1 = orm.Data()
inp1.store()
inp2 = orm.Data()
inp2.store()

# Create calc with inputs
calc = orm.CalculationNode()
calc.add_incoming(inp1, link_type=LinkType.INPUT_CALC, link_label='inp1label')
calc.add_incoming(inp2, link_type=LinkType.INPUT_CALC, link_label='inp2label')
calc.store()

# Attach outputs
out1 = orm.Data()
out2 = orm.Data()
out1.add_incoming(calc, link_type=LinkType.CREATE, link_label='out1label')
out1.store()
out2.add_incoming(calc, link_type=LinkType.CREATE, link_label='out2label')
out2.store()

expected_inputs = {'inp1label': inp1.uuid, 'inp2label': inp2.uuid}
expected_outputs = {'out1label': out1.uuid, 'out2label': out2.uuid}

#### Check the 'inputs' manager ###
# Check that dir() return all keys and nothing else, important
# for tab competion (we skip anything that starts with an underscore)
assert len([key for key in dir(calc.inputs) if not key.startswith('_')]) == len(expected_inputs)
assert set(key for key in dir(calc.inputs) if not key.startswith('_')) == set(expected_inputs)
# Check that it works also as an iterator
assert len(list(calc.inputs)) == len(expected_inputs)
assert set(calc.inputs) == set(expected_inputs)

for key, val in expected_inputs.items():
# calc.inputs.a.uuid == ..., ...
assert getattr(calc.inputs, key).uuid == val
# calc.inputs['a'].uuid == ..., ...
assert calc.inputs[key].uuid == val

# I check the attribute fetching directly
assert calc.inputs.inp1label.uuid == expected_inputs['inp1label']

## Check for not-existing links
# - Must raise a AttributeError, otherwise tab competion will not work
# - Actually raises a NotExistentAttributeError
# - NotExistentAttributeError should also be caught by NotExistent,
# for backwards-compatibility for AiiDA 1.0, 1.1, 1.2
for exception in [AttributeError, NotExistent, NotExistentAttributeError]:
with pytest.raises(exception):
getattr(calc.inputs, 'NotExistentLabel')

# - Must raise a KeyError to behave like a dictionary
# - Actually raises a NotExistentKeyError
# - NotExistentKeyError should also be caught by NotExistent,
# for backwards-compatibility for AiiDA 1.0, 1.1, 1.2
for exception in [KeyError, NotExistent, NotExistentKeyError]:
with pytest.raises(exception):
_ = calc.inputs['NotExistentLabel']

#### Check the 'outputs' manager ###
# Check that dir() return all keys and nothing else, important
# for tab competion (we skip anything that starts with an underscore)
assert len([key for key in dir(calc.outputs) if not key.startswith('_')]) == len(expected_outputs)
assert set(key for key in dir(calc.outputs) if not key.startswith('_')) == set(expected_outputs)
# Check that it works also as an iterator
assert len(list(calc.outputs)) == len(expected_outputs)
assert set(calc.outputs) == set(expected_outputs)

for key, val in expected_outputs.items():
# calc.outputs.a.uuid == ..., ...
assert getattr(calc.outputs, key).uuid == val
# calc.outputs['a'].uuid == ..., ...
assert calc.outputs[key].uuid == val

# I check the attribute fetching directly
assert calc.outputs.out1label.uuid == expected_outputs['out1label']

# Must raise a AttributeError, otherwise tab competion will not work
with pytest.raises(AttributeError):
getattr(calc.outputs, 'NotExistentLabel')

# Must raise a KeyError
with pytest.raises(KeyError):
_ = calc.outputs['NotExistentLabel']