aiida-pythonjob 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/PKG-INFO +1 -1
  2. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/__init__.py +1 -1
  3. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/calculations/pyfunction.py +32 -12
  4. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/calculations/pythonjob.py +15 -20
  5. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/utils.py +2 -4
  6. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/tests/test_pyfunction.py +13 -0
  7. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/tests/test_pythonjob.py +19 -0
  8. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/tests/test_utils.py +2 -9
  9. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/.github/workflows/ci.yml +0 -0
  10. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/.github/workflows/python-publish.yml +0 -0
  11. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/.gitignore +0 -0
  12. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/.pre-commit-config.yaml +0 -0
  13. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/.readthedocs.yml +0 -0
  14. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/LICENSE +0 -0
  15. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/README.md +0 -0
  16. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/Makefile +0 -0
  17. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/environment.yml +0 -0
  18. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/gallery/autogen/GALLERY_HEADER.rst +0 -0
  19. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/gallery/autogen/pyfunction.py +0 -0
  20. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/gallery/autogen/pythonjob.py +0 -0
  21. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/make.bat +0 -0
  22. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/requirements.txt +0 -0
  23. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/source/conf.py +0 -0
  24. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/source/index.rst +0 -0
  25. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/source/installation.rst +0 -0
  26. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/source/tutorial/dft.ipynb +0 -0
  27. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/source/tutorial/html/atomization_energy.html +0 -0
  28. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/source/tutorial/html/pythonjob_eos_emt.html +0 -0
  29. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/source/tutorial/index.rst +0 -0
  30. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/examples/test_add.py +0 -0
  31. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/pyproject.toml +0 -0
  32. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/calculations/__init__.py +0 -0
  33. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/calculations/utils.py +0 -0
  34. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/config.py +0 -0
  35. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/data/__init__.py +0 -0
  36. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/data/atoms.py +0 -0
  37. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/data/deserializer.py +0 -0
  38. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/data/pickled_data.py +0 -0
  39. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/data/serializer.py +0 -0
  40. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/decorator.py +0 -0
  41. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/launch.py +0 -0
  42. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/parsers/__init__.py +0 -0
  43. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/parsers/pythonjob.py +0 -0
  44. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/tests/conftest.py +0 -0
  45. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/tests/inputs_folder/another_input.txt +0 -0
  46. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/tests/test_create_env.py +0 -0
  47. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/tests/test_data.py +0 -0
  48. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/tests/test_entry_points.py +0 -0
  49. {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/tests/test_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiida-pythonjob
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Run Python functions on a remote computer.
5
5
  Project-URL: Source, https://github.com/aiidateam/aiida-pythonjob
6
6
  Author-email: Xing Wang <xingwang1991@gmail.com>
@@ -1,6 +1,6 @@
1
1
  """AiiDA plugin that run Python function on remote computers."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.2.2"
4
4
 
5
5
  from .calculations import PythonJob
6
6
  from .decorator import pyfunction
@@ -38,20 +38,18 @@ class PyFunction(Process):
38
38
 
39
39
  @property
40
40
  def func(self) -> t.Callable[..., t.Any]:
41
+ import cloudpickle
42
+
41
43
  if self._func is None:
42
- self._func = self.inputs.function_data.pickled_function.value
44
+ self._func = cloudpickle.loads(self.inputs.function_data.pickled_function)
43
45
  return self._func
44
46
 
45
47
  @classmethod
46
48
  def define(cls, spec: ProcessSpec) -> None: # type: ignore[override]
47
49
  """Define the process specification, including its inputs, outputs and known exit codes."""
48
50
  super().define(spec)
49
- spec.input_namespace("function_data")
50
- spec.input("function_data.name", valid_type=Str, serializer=to_aiida_type)
51
- spec.input("function_data.source_code", valid_type=Str, serializer=to_aiida_type, required=False)
51
+ spec.input_namespace("function_data", dynamic=True, required=True)
52
52
  spec.input("function_data.outputs", valid_type=List, serializer=to_aiida_type, required=False)
53
- spec.input("function_data.pickled_function", valid_type=Data, required=False)
54
- spec.input("function_data.mode", valid_type=Str, serializer=to_aiida_type, required=False)
55
53
  spec.input("process_label", valid_type=Str, serializer=to_aiida_type, required=False)
56
54
  spec.input_namespace("function_inputs", valid_type=Data, required=False)
57
55
  spec.input(
@@ -88,10 +86,10 @@ class PyFunction(Process):
88
86
  def get_function_name(self) -> str:
89
87
  """Return the name of the function to run."""
90
88
  if "name" in self.inputs.function_data:
91
- name = self.inputs.function_data.name.value
89
+ name = self.inputs.function_data.name
92
90
  else:
93
91
  try:
94
- name = self.inputs.function_data.pickled_function.value.__name__
92
+ name = self.func.__name__
95
93
  except AttributeError:
96
94
  # If a user doesn't specify name, fallback to something generic
97
95
  name = "anonymous_function"
@@ -202,8 +200,10 @@ class PyFunction(Process):
202
200
  if exit_code.status != 0:
203
201
  return exit_code
204
202
  if len(top_level_output_list) == 1:
205
- # If output name in results, use it
206
- if top_level_output_list[0]["name"] in results:
203
+ # User returned a single (nested) dict with AiiDA data nodes as values
204
+ if self.already_serialized(results):
205
+ top_level_output_list = [{"name": key, "value": value} for key, value in results.items()]
206
+ elif top_level_output_list[0]["name"] in results:
207
207
  top_level_output_list[0]["value"] = self.serialize_output(
208
208
  results.pop(top_level_output_list[0]["name"]),
209
209
  top_level_output_list[0],
@@ -228,8 +228,14 @@ class PyFunction(Process):
228
228
  if len(results) > 0:
229
229
  self.logger.warning(f"Found extra results that are not included in the output: {results.keys()}")
230
230
  elif len(top_level_output_list) == 1:
231
- # Single top-level output, single result
232
- top_level_output_list[0]["value"] = self.serialize_output(results, top_level_output_list[0])
231
+ # Single top-level output
232
+ # There are two cases:
233
+ # 1. The output as a whole will be serialized as the single output
234
+ # 2. The output is a mapping with already AiiDA data nodes as values, no need to serialize
235
+ if self.already_serialized(results):
236
+ top_level_output_list[0]["value"] = results
237
+ else:
238
+ top_level_output_list[0]["value"] = self.serialize_output(results, top_level_output_list[0])
233
239
  else:
234
240
  return self.exit_codes.ERROR_RESULT_OUTPUT_MISMATCH
235
241
  # Store the outputs
@@ -238,6 +244,20 @@ class PyFunction(Process):
238
244
 
239
245
  return ExitCode()
240
246
 
247
+ def already_serialized(self, results):
248
+ """Check if the results are already serialized."""
249
+ import collections
250
+
251
+ if isinstance(results, Data):
252
+ return True
253
+ elif isinstance(results, collections.abc.Mapping):
254
+ for value in results.values():
255
+ if not self.already_serialized(value):
256
+ return False
257
+ return True
258
+ else:
259
+ return False
260
+
241
261
  def find_output(self, name):
242
262
  """Find the output spec with the given name."""
243
263
  for output in self.output_list:
@@ -7,6 +7,7 @@ import typing as t
7
7
 
8
8
  from aiida.common.datastructures import CalcInfo, CodeInfo
9
9
  from aiida.common.folders import Folder
10
+ from aiida.common.lang import override
10
11
  from aiida.engine import CalcJob, CalcJobProcessSpec
11
12
  from aiida.orm import (
12
13
  Data,
@@ -37,17 +38,14 @@ class PythonJob(CalcJob):
37
38
  _DEFAULT_INPUT_FILE = "script.py"
38
39
  _DEFAULT_OUTPUT_FILE = "aiida.out"
39
40
  _DEFAULT_PARENT_FOLDER_NAME = "./parent_folder/"
41
+ _SOURCE_CODE_KEY = "source_code"
40
42
 
41
43
  @classmethod
42
44
  def define(cls, spec: CalcJobProcessSpec) -> None: # type: ignore[override]
43
45
  """Define the process specification, including its inputs, outputs and known exit codes."""
44
46
  super().define(spec)
45
- spec.input_namespace("function_data")
46
- spec.input("function_data.name", valid_type=Str, serializer=to_aiida_type)
47
- spec.input("function_data.source_code", valid_type=Str, serializer=to_aiida_type, required=False)
47
+ spec.input_namespace("function_data", dynamic=True, required=True)
48
48
  spec.input("function_data.outputs", valid_type=List, serializer=to_aiida_type, required=False)
49
- spec.input("function_data.pickled_function", valid_type=Data, required=False)
50
- spec.input("function_data.mode", valid_type=Str, serializer=to_aiida_type, required=False)
51
49
  spec.input("process_label", valid_type=Str, serializer=to_aiida_type, required=False)
52
50
  spec.input_namespace("function_inputs", valid_type=Data, required=False)
53
51
  spec.input(
@@ -175,13 +173,9 @@ class PythonJob(CalcJob):
175
173
  def get_function_name(self) -> str:
176
174
  """Return the name of the function to run."""
177
175
  if "name" in self.inputs.function_data:
178
- name = self.inputs.function_data.name.value
176
+ name = self.inputs.function_data.name
179
177
  else:
180
- try:
181
- name = self.inputs.function_data.pickled_function.value.__name__
182
- except AttributeError:
183
- # If a user doesn't specify name, fallback to something generic
184
- name = "anonymous_function"
178
+ name = "anonymous_function"
185
179
  return name
186
180
 
187
181
  def _build_process_label(self) -> str:
@@ -192,6 +186,13 @@ class PythonJob(CalcJob):
192
186
  name = self.get_function_name()
193
187
  return f"PythonJob<{name}>"
194
188
 
189
+ @override
190
+ def _setup_db_record(self) -> None:
191
+ """Set up the database record for the process."""
192
+ super()._setup_db_record()
193
+ if "source_code" in self.inputs.function_data:
194
+ self.node.base.attributes.set(self._SOURCE_CODE_KEY, self.inputs.function_data.source_code)
195
+
195
196
  def on_create(self) -> None:
196
197
  """Called when a Process is created."""
197
198
  super().on_create()
@@ -223,19 +224,13 @@ class PythonJob(CalcJob):
223
224
  else:
224
225
  parent_folder_name = self._DEFAULT_PARENT_FOLDER_NAME
225
226
 
226
- function_data = self.inputs.function_data
227
-
228
227
  # Build the Python script
229
- source_code = function_data.get("source_code")
230
- if "pickled_function" in self.inputs.function_data:
231
- pickled_function = self.inputs.function_data.pickled_function.get_serialized_value()
232
- else:
233
- pickled_function = None
234
- # Generate script.py content
228
+ source_code = self.node.base.attributes.get(self._SOURCE_CODE_KEY, None)
229
+ pickled_function = self.inputs.function_data.pickled_function
235
230
  function_name = self.get_function_name() # or some user-defined name
236
231
  script_content = generate_script_py(
237
232
  pickled_function=pickled_function,
238
- source_code=source_code.value if source_code else None,
233
+ source_code=source_code,
239
234
  function_name=function_name,
240
235
  )
241
236
 
@@ -54,8 +54,6 @@ def inspect_function(
54
54
  # the source code is not saved in the pickle file
55
55
  import cloudpickle
56
56
 
57
- from aiida_pythonjob.data.pickled_data import PickledData
58
-
59
57
  if inspect_source:
60
58
  try:
61
59
  source_code = inspect.getsource(func)
@@ -70,10 +68,10 @@ def inspect_function(
70
68
  if register_pickle_by_value:
71
69
  module = importlib.import_module(func.__module__)
72
70
  cloudpickle.register_pickle_by_value(module)
73
- pickled_function = PickledData(value=func)
71
+ pickled_function = cloudpickle.dumps(func)
74
72
  cloudpickle.unregister_pickle_by_value(module)
75
73
  else:
76
- pickled_function = PickledData(value=func)
74
+ pickled_function = cloudpickle.dumps(func)
77
75
 
78
76
  return {"source_code": source_code, "mode": "use_pickled_function", "pickled_function": pickled_function}
79
77
 
@@ -1,3 +1,4 @@
1
+ from aiida import orm
1
2
  from aiida.engine import run_get_node
2
3
  from aiida_pythonjob import pyfunction
3
4
 
@@ -118,3 +119,15 @@ def test_override_outputs():
118
119
  assert result["add_multiply"]["add"]["order1"].value == 3
119
120
  assert result["add_multiply"]["add"]["order2"].value == 5
120
121
  assert result["add_multiply"]["multiply"].value == 2
122
+
123
+
124
+ def test_aiida_node_as_inputs_outputs():
125
+ """Test function with AiiDA nodes as inputs and outputs."""
126
+
127
+ @pyfunction()
128
+ def add(x, y):
129
+ return {"sum": orm.Int(x + y), "diff": orm.Int(x - y)}
130
+
131
+ result, node = run_get_node(add, x=orm.Int(1), y=orm.Int(2))
132
+ assert set(result.keys()) == {"sum", "diff"}
133
+ assert result["sum"].value == 3
@@ -310,3 +310,22 @@ def test_local_function(fixture_localhost):
310
310
  )
311
311
  result, node = run_get_node(PythonJob, **inputs)
312
312
  assert result["result"].value == 8
313
+
314
+
315
+ @pytest.mark.usefixtures("started_daemon_client")
316
+ def test_submit(fixture_localhost):
317
+ """Test decorator."""
318
+ from aiida.engine import submit
319
+
320
+ def add(x, y):
321
+ return x + y
322
+
323
+ inputs = prepare_pythonjob_inputs(
324
+ add,
325
+ function_inputs={"x": 1, "y": 2},
326
+ process_label="add",
327
+ )
328
+ node = submit(PythonJob, **inputs, wait=True)
329
+
330
+ assert node.outputs.result.value == 3
331
+ assert node.process_label == "add"
@@ -12,19 +12,12 @@ def test_build_function_data():
12
12
  assert function_data["name"] == "build_function_data"
13
13
  assert "source_code" in function_data
14
14
  assert "pickled_function" in function_data
15
- node = function_data["pickled_function"]
16
- with node.base.repository.open(node.FILENAME, mode="rb") as f:
17
- text = f.read()
18
- assert b"cloudpickle" not in text
19
-
15
+ assert b"cloudpickle" not in function_data["pickled_function"]
20
16
  function_data = build_function_data(build_function_data, register_pickle_by_value=True)
21
17
  assert function_data["name"] == "build_function_data"
22
18
  assert "source_code" in function_data
23
19
  assert "pickled_function" in function_data
24
- node = function_data["pickled_function"]
25
- with node.base.repository.open(node.FILENAME, mode="rb") as f:
26
- text = f.read()
27
- assert b"cloudpickle" in text
20
+ assert b"cloudpickle" in function_data["pickled_function"]
28
21
 
29
22
  def local_function(x, y):
30
23
  return x + y
File without changes