Skip to content

Commit

Permalink
Allow the usage of lambdas for InputPort default values (#3465)
Browse files Browse the repository at this point in the history
This is made possible by upgrading `plumpy` to version `0.14.3`. This
functionality is useful to define defaults that should change at each
invocation, for example the generation of a seed. However, it also
provides a way to avoid the issues that arise when defining a default to
be an instance of a `Node`. Since a node instance is always regarded as
a mutable type by python, regardless whether it is stored or not, it can
cause unexpected behavior. For example when the node appointed to the
default value of a port is deleted in the database, the memory instance
remains and points to a no longer existing database row. The next time
it is accessed an exception will be thrown. Using a lambda avoids this
problem.
  • Loading branch information
sphuber authored Oct 25, 2019
1 parent 4ff2829 commit 6b62fa3
Show file tree
Hide file tree
Showing 5 changed files with 44 additions and 6 deletions.
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

0 comments on commit 6b62fa3

Please sign in to comment.