Skip to content

Commit

Permalink
PortNamespace: Make dynamic apply recursively (#263)
Browse files Browse the repository at this point in the history
The `dynamic` attribute of a port namespace indicates whether it should
accept ports that are not explicitly defined. This was mostly used
during validation, when a dictionary of port values was matched against
a given `PortNamespace`.

This was, however, not being applied recursively _and_ only during
validation. For example, given a dynamic portnamespace, validating a
dictionary:

    {
        'nested': {
	    'output': 'some_value'
        }
    }

would pass validation without problems. However, the `Process.out` call
that would actually attempt to attach the output to the process instance
would call:

    self.spec().outputs.get_port(namespace_separator.join(namespace))

which would raise, since `get_port` would raise a `ValueError`:

    ValueError: port 'output' does not exist in port namespace 'nested'

The problem is that the `nested` namespace is expected, because the top
level namespace was marked as dynamic, however, it itself would not also
be treated as dynamic and so attempting to retrieve `some_value` from
the `nested` namespace, would trigger a `KeyError`.

Here the logic in `PortNamespace.get_port` is updated to check in
advance whether the port exists in the namespace, and if not the case
_and_ the namespace is dynamic, the nested port namespace is created.
The attributes of the new namespace are inherited from its parent
namespace, making the `dynamic` attribute act recursively.

Cherry-pick: 4c29f44
  • Loading branch information
sphuber committed Apr 3, 2023
1 parent 2f324d8 commit f9ccdd7
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 1 deletion.
13 changes: 12 additions & 1 deletion src/plumpy/ports.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,9 +446,20 @@ def get_port(self, name: str) -> Union[Port, 'PortNamespace']:
namespace = name.split(self.NAMESPACE_SEPARATOR)
port_name = namespace.pop(0)

if port_name not in self:
if port_name not in self and not self.dynamic:
raise ValueError(f"port '{port_name}' does not exist in port namespace '{self.name}'")

if port_name not in self and self.dynamic:
self[port_name] = self.__class__(
name=port_name,
required=self.required,
validator=self.validator,
valid_type=self.valid_type,
default=self.default,
dynamic=self.dynamic,
populate_defaults=self.populate_defaults
)

if namespace:
portnamespace = cast(PortNamespace, self[port_name])
return portnamespace.get_port(self.NAMESPACE_SEPARATOR.join(namespace))
Expand Down
21 changes: 21 additions & 0 deletions test/test_port.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,27 @@ def test_port_namespace_get_port(self):
port = self.port_namespace.get_port('sub.name.space.' + self.BASE_PORT_NAME)
self.assertEqual(port, self.port)

def test_port_namespace_get_port_dynamic(self):
"""Test that ``get_port`` does not raise if a port does not exist as long as the namespace is dynamic.
In this case, the method should create the subnamespace on-the-fly with the same stats as the host namespace.
"""
port_namespace = PortNamespace(self.BASE_PORT_NAMESPACE_NAME, dynamic=True)

name = 'undefined'
sub_namespace = port_namespace.get_port(name)

assert isinstance(sub_namespace, PortNamespace)
assert sub_namespace.dynamic
assert sub_namespace.name == name

name = 'nested.undefined'
sub_namespace = port_namespace.get_port(name)

assert isinstance(sub_namespace, PortNamespace)
assert sub_namespace.dynamic
assert sub_namespace.name == 'undefined'

def test_port_namespace_create_port_namespace(self):
"""
Test the create_port_namespace function of the PortNamespace class
Expand Down

0 comments on commit f9ccdd7

Please sign in to comment.