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

Process: Allow None for input ports that are not required #5722

Merged

Conversation

sphuber
Copy link
Contributor

@sphuber sphuber commented Oct 26, 2022

Fixes #5720

Python functions and process functions accept None for arguments
that define that as a default without any issues. That is to say,
the following works just fine:

from aiida.engine import calcfunction

@calcfunction
def accepts_none(a=None):
    pass

accepts_none()
accepts_none(None)

For a Process, however, None is not accepted even if the port is not
required. This can be annoying for users because when writing wrappers
around a process launcher, they will have to manually check for one of
the inputs being None and remove it before submitting the input
dictionary.

Here we update the InputPort constructor to automatically add the
NoneType to the valid_type argument if the port is marked as
required=False. This way, non-required ports will accept None
without raising an exception during input validation.

@sphuber sphuber requested a review from unkcpz October 26, 2022 19:47
@sphuber sphuber force-pushed the feature/5720/allow-none-for-non-required-ports branch from 22df578 to b747f05 Compare October 27, 2022 07:15
Copy link
Member

@unkcpz unkcpz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sphuber thanks!
Maybe better to add a nightly daemon test for the process function with the parameter default set to None as what you describe in the commit message?

from aiida.manage import get_manager

runner = get_manager().get_runner()
process = instantiate_process(runner, TestNotRequiredNoneProcess, not_required=None)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the process function with input parameter defaulting to None is not covered by this test, correct? Can you add to nightly tests which are exactly the process function manifest the case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is already being tested:

def test_function_with_none_default():

Here we are adding the same behavior for any other process, such as CalcJob and WorkChain.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. The FunctionProcess is subclass of Process I thought this PR is going to fix the bug introduced by the previously added feature for None supported input.
It is all good to me! Thanks!

@sphuber sphuber requested a review from unkcpz November 2, 2022 08:59
The tests for the Sphinx extension for process specifications worked
using a direct comparison of the parsed XML file. Although this worked
fine, we have the `pytest-regressions` plugin for this which handles
this automatically, including a useful switch `--force-regen` to
regenerate the reference file if it is supposed to change.
@sphuber sphuber force-pushed the feature/5720/allow-none-for-non-required-ports branch from b72c237 to 9bbc1dc Compare November 2, 2022 10:32
Python functions and process functions accept `None` for arguments
that define that as a default without any issues. That is to say,
the following works just fine:

    from aiida.engine import calcfunction

    @calcfunction
    def accepts_none(a=None):
        pass

    accepts_none()
    accepts_none(None)

For a `Process`, however, `None` is not accepted even if the port is not
required. This can be annoying for users because when writing wrappers
around a process launcher, they will have to manually check for one of
the inputs being `None` and remove it before submitting the input
dictionary.

Here we update the `InputPort` constructor to automatically add the
`NoneType` to the `valid_type` argument if the port is marked as
`required=False`. This way, non-required ports will accept `None`
without raising an exception during input validation.
@sphuber sphuber force-pushed the feature/5720/allow-none-for-non-required-ports branch from 9bbc1dc to fa9fd9e Compare November 2, 2022 10:52
@sphuber sphuber merged commit ae79c89 into aiidateam:main Nov 2, 2022
@sphuber sphuber deleted the feature/5720/allow-none-for-non-required-ports branch November 2, 2022 11:16
@mbercx
Copy link
Member

mbercx commented Nov 11, 2022

@sphuber unfortunately I think this change has had some unintended backwards-incompatible side effects. When trying to run a PwBaseWorkChain with aiida-core==2.1.0, the process excepts with the following traceback:

$ verdi process report 12099
2022-11-11 16:33:57 [4372 | REPORT]: [12099|PwBaseWorkChain|on_except]: Traceback (most recent call last):
  File "/home/mbercx/.virtualenvs/aiida-dev/lib/python3.8/site-packages/plumpy/process_states.py", line 228, in execute
    result = self.run_fn(*self.args, **self.kwargs)
  File "/home/mbercx/envs/aiida-dev/code/aiida-core/aiida/engine/processes/workchains/workchain.py", line 252, in _do_step
    finished, stepper_result = self._stepper.step()
  File "/home/mbercx/.virtualenvs/aiida-dev/lib/python3.8/site-packages/plumpy/workchains.py", line 295, in step
    finished, result = self._child_stepper.step()
  File "/home/mbercx/.virtualenvs/aiida-dev/lib/python3.8/site-packages/plumpy/workchains.py", line 528, in step
    finished, result = self._child_stepper.step()
  File "/home/mbercx/.virtualenvs/aiida-dev/lib/python3.8/site-packages/plumpy/workchains.py", line 295, in step
    finished, result = self._child_stepper.step()
  File "/home/mbercx/.virtualenvs/aiida-dev/lib/python3.8/site-packages/plumpy/workchains.py", line 246, in step
    return True, self._fn(self._workchain)
  File "/home/mbercx/envs/aiida-dev/code/aiida-core/aiida/engine/processes/workchains/restart.py", line 197, in run_process
    node = self.submit(self.process_class, **inputs)
  File "/home/mbercx/envs/aiida-dev/code/aiida-core/aiida/engine/processes/process.py", line 523, in submit
    return self.runner.submit(process, *args, **kwargs)
  File "/home/mbercx/envs/aiida-dev/code/aiida-core/aiida/engine/runners.py", line 183, in submit
    process_inited = self.instantiate_process(process, *args, **inputs)
  File "/home/mbercx/envs/aiida-dev/code/aiida-core/aiida/engine/runners.py", line 169, in instantiate_process
    return instantiate_process(self, process, *args, **inputs)
  File "/home/mbercx/envs/aiida-dev/code/aiida-core/aiida/engine/utils.py", line 64, in instantiate_process
    process = process_class(runner=runner, inputs=inputs)
  File "/home/mbercx/.virtualenvs/aiida-dev/lib/python3.8/site-packages/plumpy/base/state_machine.py", line 194, in __call__
    inst.transition_to(inst.create_initial_state())
  File "/home/mbercx/.virtualenvs/aiida-dev/lib/python3.8/site-packages/plumpy/base/state_machine.py", line 336, in transition_to
    self.transition_failed(initial_state_label, label, *sys.exc_info()[1:])
  File "/home/mbercx/.virtualenvs/aiida-dev/lib/python3.8/site-packages/plumpy/base/state_machine.py", line 352, in transition_failed
    raise exception.with_traceback(trace)
  File "/home/mbercx/.virtualenvs/aiida-dev/lib/python3.8/site-packages/plumpy/base/state_machine.py", line 321, in transition_to
    self._enter_next_state(new_state)
  File "/home/mbercx/.virtualenvs/aiida-dev/lib/python3.8/site-packages/plumpy/base/state_machine.py", line 383, in _enter_next_state
    self._fire_state_event(StateEventHook.ENTERING_STATE, next_state)
  File "/home/mbercx/.virtualenvs/aiida-dev/lib/python3.8/site-packages/plumpy/base/state_machine.py", line 300, in _fire_state_event
    callback(self, hook, state)
  File "/home/mbercx/.virtualenvs/aiida-dev/lib/python3.8/site-packages/plumpy/processes.py", line 329, in <lambda>
    lambda _s, _h, state: self.on_entering(cast(process_states.State, state)),
  File "/home/mbercx/envs/aiida-dev/code/aiida-core/aiida/engine/processes/process.py", line 405, in on_entering
    super().on_entering(state)
  File "/home/mbercx/.virtualenvs/aiida-dev/lib/python3.8/site-packages/plumpy/processes.py", line 675, in on_entering
    call_with_super_check(self.on_create)
  File "/home/mbercx/.virtualenvs/aiida-dev/lib/python3.8/site-packages/plumpy/base/utils.py", line 29, in call_with_super_check
    wrapped(*args, **kwargs)
  File "/home/mbercx/envs/aiida-dev/code/aiida-core/aiida/engine/processes/process.py", line 395, in on_create
    super().on_create()
  File "/home/mbercx/.virtualenvs/aiida-dev/lib/python3.8/site-packages/plumpy/base/utils.py", line 16, in wrapper
    wrapped(self, *args, **kwargs)
  File "/home/mbercx/.virtualenvs/aiida-dev/lib/python3.8/site-packages/plumpy/processes.py", line 732, in on_create
    raise ValueError(result)
ValueError: Error occurred validating port 'inputs.settings': value 'settings' is not of the right type. Got '<class 'aiida.common.extendeddicts.AttributeDict'>', expected '(<class 'aiida.orm.nodes.data.dict.Dict'>, <class 'NoneType'>)'

2022-11-11 16:33:57 [4373 | REPORT]: [12099|PwBaseWorkChain|on_terminated]: remote folders will not be cleaned

The problem is that the PwBaseWorkChain uses the prepare_process_inputs function to convert e.g. the settings input into a Dict node, which in turn checks is valid_type == Dict. Since the valid_type is now converted in a list on the fly, this breaks the PwBaseWorkChain.

I was looking into fixing this in the aiida-quantumespresso plugin, but perhaps it is better to revert this commit? Other plugins might also rely on checking the valid_type of an input port during runtime, and hence break as well.

EDIT: Actually, it's the BaseRestartWorkChain._wrap_bare_dict_inputs method that normally would convert the settings into a Dict node here, but the issue is the same.

@sphuber
Copy link
Contributor Author

sphuber commented Nov 11, 2022

Thanks for letting us know @mbercx .

I think this was actually really an inaccuracy that was anyway hiding in the BaseRestartWorkChain. So why don't we just fix that by changing

elif port.valid_type == orm.Dict and isinstance(value, dict):

to

valid_types = port.valid_type if isinstance(port.valid_type, (list, tuple)) else (port.valid_type,)

...
elif orm.Dict in valid_types and isinstance(value, dict):

Do you really think there is other code out there that might break due to this?

@mbercx
Copy link
Member

mbercx commented Nov 14, 2022

Well, the QE plugin also does ^^

https://github.com/aiidateam/aiida-quantumespresso/blob/d15f8cd1032e37ae59a19d733ee5ae1ed59e1f10/src/aiida_quantumespresso/utils/mapping.py#L94-L95

As I said in #5757, I'm not sure if there is a lot of other plugins that rely on checking the valid_type on the fly. I suppose it's a rather rare use case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

InputPort: allow explicitly passed None for ports that are not required
3 participants