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

Allow the usage of lambdas for InputPort default values #3465

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
30 changes: 29 additions & 1 deletion aiida/backends/tests/engine/test_ports.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from aiida.backends.testbase import AiidaTestCase
from aiida.engine.processes.ports import InputPort, PortNamespace
from aiida.orm import Dict
from aiida.orm import Dict, Int


class TestInputPort(AiidaTestCase):
Expand Down Expand Up @@ -98,3 +98,31 @@ def test_serialize_type_check(self):
# The `assertRaisesRegexp` method is deprecated in python 3 but assertRaisesRegex` does not exist in python 2
with self.assertRaisesRegexp(TypeError, '.*{}.*{}.*'.format(base_namespace, nested_namespace)):
port_namespace.serialize({'some': {'nested': {'namespace': {Dict()}}}})

def test_lambda_default(self):
"""Test that an input port can specify a lambda as a default."""
port_namespace = PortNamespace('base')

# Defining lambda for default that returns incorrect type should not except at construction
port_namespace['port'] = InputPort('port', valid_type=Int, default=lambda: 'string')

# However, pre processing the namespace, which shall evaluate the default followed by validation will fail
inputs = port_namespace.pre_process({})
self.assertIsNotNone(port_namespace.validate(inputs))

# Passing an explicit value for the port will forego the default and validation on returned inputs should pass
inputs = port_namespace.pre_process({'port': Int(5)})
self.assertIsNone(port_namespace.validate(inputs))

# Redefining the port, this time with a correct default
port_namespace['port'] = InputPort('port', valid_type=Int, default=lambda: Int(5))

# Pre processing the namespace shall evaluate the default and return the int node
inputs = port_namespace.pre_process({})
self.assertIsInstance(inputs['port'], Int)
self.assertEqual(inputs['port'].value, 5)

# Passing an explicit value for the port will forego the default
inputs = port_namespace.pre_process({'port': Int(3)})
self.assertIsInstance(inputs['port'], Int)
self.assertEqual(inputs['port'].value, 3)
2 changes: 1 addition & 1 deletion docs/requirements_for_rtd.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ passlib==1.7.1
pg8000<1.13.0
pgtest==1.3.1
pika==1.1.0
plumpy==0.14.2
plumpy==0.14.3
psutil==5.6.3
psycopg2-binary==2.8.3
pyblake2==1.1.2; python_version<'3.6'
Expand Down
14 changes: 12 additions & 2 deletions docs/source/working/processes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,19 @@ An example input port that explicitly sets all these attributes is the following

.. code:: python

spec.input('positive_number', required=False, default=Int(1), valid_type=(Int, Float), validator=is_number_positive)
spec.input('positive_number', required=False, default=lambda: Int(1), valid_type=(Int, Float), validator=is_number_positive)

Here we define an input named ``positive_number`` that should be of type ``Int`` or ``Float`` and should pass the test of the ``is_number_positive`` validator.
If no value is passed, the default will be used.

.. warning::

In python, it is good practice to avoid mutable defaults for function arguments, `since they are instantiated at function definition and reused for each invocation <https://docs.python.org/3/reference/compound_stmts.html#function-definitions>`_.
This can lead to unexpected results when the default value is changed between function calls.
In the context of AiiDA, nodes (both stored and unstored) are considered *mutable* and should therefore *not* be used as default values for process ports.
However, it is possible to use a lambda that returns a node instance as done in the example above.
This will return a new instance of the node with the given value, each time the process is instantiated.

Here we define an input named ``positive_number`` that is not required, if a value is not explicitly passed, the default ``Int(1)`` will be used and if a value *is* passed, it should be of type ``Int`` or ``Float`` and it should be valid according to the ``is_number_positive`` validator.
Note that the validator is nothing more than a free function which takes a single argument, being the value that is to be validated.
If nothing is returned, the value is considered to be valid.
To signal that the value is invalid and to have a validation error raised, simply return a string with the validation error message, for example:
Expand Down
2 changes: 1 addition & 1 deletion environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ dependencies:
- paramiko==2.6.0
- passlib==1.7.1
- pika==1.1.0
- plumpy==0.14.2
- plumpy==0.14.3
- psutil==5.6.3
- psycopg2==2.8.3
- python-dateutil==2.8.0
Expand Down
2 changes: 1 addition & 1 deletion setup.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"paramiko==2.6.0",
"passlib==1.7.1",
"pika==1.1.0",
"plumpy==0.14.2",
"plumpy==0.14.3",
"psutil==5.6.3",
"psycopg2-binary==2.8.3",
"pyblake2==1.1.2; python_version<'3.6'",
Expand Down