flowrep 0.6.2__tar.gz → 0.6.4__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 (57) hide show
  1. {flowrep-0.6.2 → flowrep-0.6.4}/PKG-INFO +3 -3
  2. {flowrep-0.6.2 → flowrep-0.6.4}/pyproject.toml +2 -2
  3. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/_version.py +2 -2
  4. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/api/tools.py +3 -0
  5. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/converters/python_workflow_definition.py +131 -31
  6. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/object_scope.py +2 -1
  7. {flowrep-0.6.2 → flowrep-0.6.4}/.gitignore +0 -0
  8. {flowrep-0.6.2 → flowrep-0.6.4}/LICENSE +0 -0
  9. {flowrep-0.6.2 → flowrep-0.6.4}/docs/README.md +0 -0
  10. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/__init__.py +0 -0
  11. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/api/__init__.py +0 -0
  12. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/api/schemas.py +0 -0
  13. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/base_models.py +0 -0
  14. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/compiler/__init__.py +0 -0
  15. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/compiler/annotate.py +0 -0
  16. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/compiler/flow_control.py +0 -0
  17. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/compiler/function.py +0 -0
  18. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/compiler/source.py +0 -0
  19. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/compiler/statements.py +0 -0
  20. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/compiler/sugar.py +0 -0
  21. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/converters/__init__.py +0 -0
  22. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/edge_models.py +0 -0
  23. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/__init__.py +0 -0
  24. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/atomic_parser.py +0 -0
  25. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/attribute_parser.py +0 -0
  26. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/case_helpers.py +0 -0
  27. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/chain_parser.py +0 -0
  28. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/constant_parser.py +0 -0
  29. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/dataclass_parser.py +0 -0
  30. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/for_parser.py +0 -0
  31. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/if_parser.py +0 -0
  32. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/item_parser.py +0 -0
  33. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/label_helpers.py +0 -0
  34. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/parser_helpers.py +0 -0
  35. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/parser_protocol.py +0 -0
  36. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/symbol_scope.py +0 -0
  37. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/try_parser.py +0 -0
  38. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/while_parser.py +0 -0
  39. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/workflow_parser.py +0 -0
  40. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/prospective/__init__.py +0 -0
  41. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/prospective/atomic_recipe.py +0 -0
  42. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/prospective/constant_recipe.py +0 -0
  43. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/prospective/for_recipe.py +0 -0
  44. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/prospective/helper_models.py +0 -0
  45. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/prospective/if_recipe.py +0 -0
  46. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/prospective/try_recipe.py +0 -0
  47. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/prospective/union_types.py +0 -0
  48. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/prospective/while_recipe.py +0 -0
  49. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/prospective/workflow_recipe.py +0 -0
  50. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/retrospective/__init__.py +0 -0
  51. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/retrospective/datastructures.py +0 -0
  52. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/retrospective/storage.py +0 -0
  53. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/retrospective/storage_widget.py +0 -0
  54. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/retrospective/viewer.py +0 -0
  55. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/std.py +0 -0
  56. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/subgraph_validation.py +0 -0
  57. {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/wfms.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: flowrep
3
- Version: 0.6.2
3
+ Version: 0.6.4
4
4
  Summary: flowrep - Your premier tool for workflow representations
5
5
  Project-URL: Homepage, https://pyiron.org/
6
6
  Project-URL: Documentation, https://flowrep.readthedocs.io
@@ -47,7 +47,7 @@ Classifier: Programming Language :: Python :: 3.13
47
47
  Classifier: Programming Language :: Python :: 3.14
48
48
  Classifier: Topic :: Scientific/Engineering
49
49
  Requires-Python: <3.15,>=3.11
50
- Requires-Dist: packaging<=26.2,>=24.2
50
+ Requires-Dist: packaging<=26.3,>=24.2
51
51
  Requires-Dist: pydantic<2.13.0,>=2.12.0
52
52
  Requires-Dist: pyiron-snippets<2.0.0,>=1.3.0
53
53
  Requires-Dist: typing-extensions<4.17.0,>=4.14.1
@@ -1,5 +1,5 @@
1
1
  [build-system]
2
- requires = [ "hatchling==1.31.0", "hatch-vcs==0.5.0",]
2
+ requires = [ "hatchling==1.32.0", "hatch-vcs==0.5.0",]
3
3
  build-backend = "hatchling.build"
4
4
 
5
5
  [project]
@@ -9,7 +9,7 @@ readme = "docs/README.md"
9
9
  keywords = [ "pyiron",]
10
10
  requires-python = ">=3.11, <3.15"
11
11
  classifiers = [ "Development Status :: 4 - Beta", "Topic :: Scientific/Engineering", "License :: OSI Approved :: BSD License", "Intended Audience :: Science/Research", "Operating System :: OS Independent", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14",]
12
- dependencies = [ "packaging>=24.2,<=26.2", "pydantic>=2.12.0,<2.13.0", "pyiron_snippets>=1.3.0,<2.0.0", "typing-extensions>=4.14.1,<4.17.0",]
12
+ dependencies = [ "packaging>=24.2,<=26.3", "pydantic>=2.12.0,<2.13.0", "pyiron_snippets>=1.3.0,<2.0.0", "typing-extensions>=4.14.1,<4.17.0",]
13
13
  dynamic = [ "version",]
14
14
  [[project.authors]]
15
15
  name = "Sam Waseda"
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.6.2'
22
- __version_tuple__ = version_tuple = (0, 6, 2)
21
+ __version__ = version = '0.6.4'
22
+ __version_tuple__ = version_tuple = (0, 6, 4)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -6,6 +6,9 @@ for downstream library developers.
6
6
  """
7
7
 
8
8
  from flowrep.compiler.source import flowrep2python as flowrep2python
9
+ from flowrep.converters.python_workflow_definition import (
10
+ OutputContractError as OutputContractError,
11
+ )
9
12
  from flowrep.converters.python_workflow_definition import flowrep2pwd as flowrep2pwd
10
13
  from flowrep.converters.python_workflow_definition import pwd2flowrep as pwd2flowrep
11
14
  from flowrep.parsers.atomic_parser import atomic as atomic
@@ -4,6 +4,24 @@ Bidirectional converter between flowrep and python-workflow-definition formats.
4
4
  The ``python_workflow_definition`` (pwd) package is an **optional** dependency.
5
5
  It represents workflows as flat, non-nested DAGs of atomic function calls with
6
6
  explicit input/output nodes carrying JSON-serializable default values.
7
+
8
+ The two formats hold different axioms about how a function's return value maps
9
+ onto a node's output ports:
10
+
11
+ =========== ========================== ================================
12
+ whole return value component of return
13
+ =========== ========================== ================================
14
+ pwd ``sourcePort: null`` ``sourcePort: "<key>"`` →
15
+ ``result["<key>"]``
16
+ flowrep exactly one output port N>1 ports unpack an N-tuple
17
+ =========== ========================== ================================
18
+
19
+ They overlap only on *exactly one output, meaning the whole return value*.
20
+ Anything outside that overlap raises :class:`OutputContractError` rather than
21
+ being mistranslated into a recipe that computes the wrong answer. As a
22
+ corollary, a flowrep node's sole output port name is not preserved across a
23
+ round-trip: it becomes pwd's unnamed ``sourcePort`` and returns as
24
+ :data:`_DEFAULT_OUTPUT_PORT`.
7
25
  """
8
26
 
9
27
  from __future__ import annotations
@@ -36,6 +54,23 @@ _DEFAULT_OUTPUT_PORT: str = "__result__"
36
54
  _PORT_SANITIZE_PREFIX: str = "flowrep_sanitized_"
37
55
 
38
56
 
57
+ class OutputContractError(ValueError):
58
+ """
59
+ A node's output contract is not representable in the target format.
60
+
61
+ flowrep and pwd disagree about how a function's return value maps onto
62
+ output ports. pwd says ``sourcePort: null`` is the entire return value and
63
+ a named ``sourcePort`` is a key of a returned dict; flowrep says a single
64
+ output port is the entire return value and N>1 ports unpack an N-tuple.
65
+ The two overlap only on *exactly one output, meaning the whole return
66
+ value*. Converting a node outside that overlap would produce a recipe that
67
+ executes incorrectly, so we refuse rather than mistranslate.
68
+
69
+ Subclasses :class:`ValueError` so that callers already handling the
70
+ converter's other validation failures keep working.
71
+ """
72
+
73
+
39
74
  def _needs_sanitization(port: str) -> bool:
40
75
  """Return ``True`` if *port* is not a valid flowrep :class:`Label`."""
41
76
  return not base_models.is_valid_label(port)
@@ -55,7 +90,12 @@ def _sanitize_port(port: str) -> str:
55
90
 
56
91
 
57
92
  def _desanitize_port(port: str) -> str:
58
- """Reverse :func:`_sanitize_port` when converting back to pwd."""
93
+ """
94
+ Reverse :func:`_sanitize_port` when converting back to pwd.
95
+
96
+ Only input (``targetPort``) names need this; output ports never round-trip
97
+ through pwd by name — see :func:`_build_pwd_edges`.
98
+ """
59
99
  if port.startswith(_PORT_SANITIZE_PREFIX):
60
100
  candidate = port[len(_PORT_SANITIZE_PREFIX) :]
61
101
  if candidate and _needs_sanitization(candidate):
@@ -77,8 +117,13 @@ def pwd2flowrep(
77
117
  A ``(WorkflowRecipe, defaults)`` pair where *defaults* maps each
78
118
  workflow-input name to the default value carried by the corresponding
79
119
  PWD input node.
120
+
121
+ Raises:
122
+ OutputContractError: If any function node is consumed via a named
123
+ ``sourcePort``.
80
124
  """
81
125
  input_nodes, output_nodes, function_nodes = _categorize_pwd_nodes(wf.nodes)
126
+ _validate_pwd_output_contracts(wf.edges, function_nodes)
82
127
 
83
128
  label_map = _build_label_map(function_nodes)
84
129
  node_inputs, node_outputs = _collect_function_node_ports(
@@ -136,8 +181,10 @@ def flowrep2pwd(
136
181
  Raises:
137
182
  ValueError: If any child is non-atomic, or if *terminal_inputs* does
138
183
  not exactly cover the workflow's inputs.
184
+ OutputContractError: If any child has more than one output port.
139
185
  """
140
186
  _validate_flat_workflow(wf)
187
+ _validate_flowrep_output_contract(wf)
141
188
  _validate_terminal_inputs(wf, terminal_inputs)
142
189
 
143
190
  id_counter = _IdCounter()
@@ -216,6 +263,45 @@ def _categorize_pwd_nodes(
216
263
  return input_nodes, output_nodes, function_nodes
217
264
 
218
265
 
266
+ def _validate_pwd_output_contracts(
267
+ edges: list[pwd.PythonWorkflowDefinitionEdge],
268
+ function_nodes: dict[int, pwd.PythonWorkflowDefinitionFunctionNode],
269
+ ) -> None:
270
+ """
271
+ Raise :class:`OutputContractError` for function nodes consumed by key.
272
+
273
+ A named ``sourcePort`` tells pwd to subscript the node's single return value
274
+ with that key. A flowrep atomic node cannot express that: its outputs are
275
+ either the whole return value (one port) or a tuple unpacking (many ports).
276
+ Inspects raw ``sourcePort`` values, before sanitisation, so the message
277
+ quotes the author's own port names.
278
+ """
279
+ named: dict[int, list[str]] = {}
280
+ for edge in edges:
281
+ if edge.source not in function_nodes:
282
+ continue
283
+ port = edge.sourcePort
284
+ if port is None or port == pwd.INTERNAL_DEFAULT_HANDLE:
285
+ continue
286
+ ports = named.setdefault(edge.source, [])
287
+ if port not in ports:
288
+ ports.append(port)
289
+
290
+ if named:
291
+ offenders = [
292
+ f"node {nid} ({function_nodes[nid].value}) is consumed via "
293
+ f"sourcePort(s) {sorted(named[nid])}"
294
+ for nid in sorted(named)
295
+ ]
296
+ raise OutputContractError(
297
+ "pwd2flowrep requires every function node to be consumed via a null "
298
+ "sourcePort. pwd reads a named sourcePort as a key of a single "
299
+ "returned dict, which a flowrep atomic node cannot express — its "
300
+ "outputs are either the whole return value (one port) or a tuple "
301
+ "unpacking (many). Offending nodes: " + "; ".join(offenders) + "."
302
+ )
303
+
304
+
219
305
  def _build_label_map(
220
306
  function_nodes: dict[int, pwd.PythonWorkflowDefinitionFunctionNode],
221
307
  ) -> dict[int, str]:
@@ -237,13 +323,6 @@ def _build_label_map(
237
323
  return label_map
238
324
 
239
325
 
240
- def _resolve_source_port(port: str | None) -> str:
241
- """Map pwd's internal sourcePort to a flowrep output-port name."""
242
- if port is None or port == pwd.INTERNAL_DEFAULT_HANDLE:
243
- return _DEFAULT_OUTPUT_PORT
244
- return _sanitize_port(port)
245
-
246
-
247
326
  def _collect_function_node_ports(
248
327
  edges: list[pwd.PythonWorkflowDefinitionEdge],
249
328
  function_nodes: dict[int, pwd.PythonWorkflowDefinitionFunctionNode],
@@ -251,8 +330,9 @@ def _collect_function_node_ports(
251
330
  """
252
331
  Collect ordered, unique input/output port names for each function node.
253
332
 
254
- Port names that are not valid flowrep Labels are sanitized via
255
- :func:`_sanitize_port`.
333
+ Input port names that are not valid flowrep Labels are sanitized via
334
+ :func:`_sanitize_port`. Output ports are always the single
335
+ :data:`_DEFAULT_OUTPUT_PORT`.
256
336
  """
257
337
  node_inputs: dict[int, list[str]] = {nid: [] for nid in function_nodes}
258
338
  node_outputs: dict[int, list[str]] = {nid: [] for nid in function_nodes}
@@ -265,12 +345,13 @@ def _collect_function_node_ports(
265
345
  if port not in ports:
266
346
  ports.append(port)
267
347
 
268
- # Source ports (outputs of the function node)
348
+ # Outputs of the function node. Guaranteed null-sourced by
349
+ # _validate_pwd_output_contracts, so every such node has exactly the
350
+ # one default-named output port.
269
351
  if edge.source in function_nodes:
270
- port = _resolve_source_port(edge.sourcePort)
271
352
  ports = node_outputs[edge.source]
272
- if port not in ports:
273
- ports.append(port)
353
+ if _DEFAULT_OUTPUT_PORT not in ports:
354
+ ports.append(_DEFAULT_OUTPUT_PORT)
274
355
 
275
356
  return node_inputs, node_outputs
276
357
 
@@ -345,7 +426,7 @@ def _build_flowrep_edges(
345
426
  # Function node → workflow output
346
427
  out_name = output_nodes[edge.target].name
347
428
  source_label = label_map[edge.source]
348
- source_port = _resolve_source_port(edge.sourcePort)
429
+ source_port = _DEFAULT_OUTPUT_PORT
349
430
  fr_output_edges[edge_models.OutputTarget(port=out_name)] = (
350
431
  edge_models.SourceHandle(node=source_label, port=source_port)
351
432
  )
@@ -354,7 +435,7 @@ def _build_flowrep_edges(
354
435
  # Function node → function node (sibling edge)
355
436
  source_label = label_map[edge.source]
356
437
  target_label = label_map[edge.target]
357
- source_port = _resolve_source_port(edge.sourcePort)
438
+ source_port = _DEFAULT_OUTPUT_PORT
358
439
  target_port = _sanitize_port(edge.targetPort)
359
440
  fr_edges[edge_models.TargetHandle(node=target_label, port=target_port)] = (
360
441
  edge_models.SourceHandle(node=source_label, port=source_port)
@@ -385,6 +466,33 @@ def _validate_flat_workflow(wf: workflow_recipe.WorkflowRecipe) -> None:
385
466
  )
386
467
 
387
468
 
469
+ def _validate_flowrep_output_contract(wf: workflow_recipe.WorkflowRecipe) -> None:
470
+ """
471
+ Raise :class:`OutputContractError` if any child has more than one output.
472
+
473
+ Must be called *after* :func:`_validate_flat_workflow`, which guarantees
474
+ every child is an :class:`AtomicRecipe`.
475
+ """
476
+ offenders: list[str] = []
477
+ for label, node in wf.nodes.items():
478
+ # Guaranteed by _validate_flat_workflow
479
+ assert isinstance(node, atomic_recipe.AtomicRecipe)
480
+ if len(node.outputs) > 1:
481
+ offenders.append(
482
+ f"'{label}' ({node.fully_qualified_name}) has "
483
+ f"{len(node.outputs)} outputs {node.outputs}"
484
+ )
485
+ if offenders:
486
+ raise OutputContractError(
487
+ "flowrep2pwd requires every child to have exactly one output. "
488
+ "flowrep reads multiple output ports as unpacking a tuple, whereas "
489
+ "pwd reads them as keys of a single returned dict, so the converted "
490
+ "workflow would not execute correctly. Offending nodes: "
491
+ + "; ".join(offenders)
492
+ + "."
493
+ )
494
+
495
+
388
496
  def _validate_terminal_inputs(
389
497
  wf: workflow_recipe.WorkflowRecipe,
390
498
  terminal_inputs: dict[str, Any],
@@ -405,19 +513,6 @@ def _validate_terminal_inputs(
405
513
  )
406
514
 
407
515
 
408
- def _flowrep_port_to_pwd_source_port(port: str) -> str | None:
409
- """
410
- Map a flowrep output-port name back to a pwd ``sourcePort`` value.
411
-
412
- Returns ``None`` for the default-output sentinel so that the PWD edge
413
- validator stores it as :data:`pwd.INTERNAL_DEFAULT_HANDLE`. Otherwise
414
- reverses any sanitisation applied by :func:`_sanitize_port`.
415
- """
416
- if port == _DEFAULT_OUTPUT_PORT:
417
- return None
418
- return _desanitize_port(port)
419
-
420
-
421
516
  def _build_pwd_edges(
422
517
  wf: workflow_recipe.WorkflowRecipe,
423
518
  input_node_ids: dict[str, int],
@@ -430,6 +525,11 @@ def _build_pwd_edges(
430
525
  Edges are emitted in a deterministic order that preserves the input-port
431
526
  ordering of each child node — this is important for consumers that rely on
432
527
  edge-list order (e.g. ``get_list``).
528
+
529
+ Every function-node source emits ``sourcePort=None``. A flowrep child has
530
+ exactly one output (enforced by :func:`_validate_flowrep_output_contract`) and that
531
+ output *is* the whole return value, which is precisely what pwd spells as a
532
+ null ``sourcePort``. The flowrep port's name is therefore not carried over.
433
533
  """
434
534
  pwd_edges: list[pwd.PythonWorkflowDefinitionEdge] = []
435
535
 
@@ -456,7 +556,7 @@ def _build_pwd_edges(
456
556
  pwd_edges.append(
457
557
  pwd.PythonWorkflowDefinitionEdge(
458
558
  source=func_node_ids[source.node],
459
- sourcePort=_flowrep_port_to_pwd_source_port(source.port),
559
+ sourcePort=None,
460
560
  target=func_node_ids[label],
461
561
  targetPort=target_port_pwd,
462
562
  )
@@ -480,7 +580,7 @@ def _build_pwd_edges(
480
580
  pwd_edges.append(
481
581
  pwd.PythonWorkflowDefinitionEdge(
482
582
  source=func_node_ids[source.node],
483
- sourcePort=_flowrep_port_to_pwd_source_port(source.port),
583
+ sourcePort=None,
484
584
  target=output_node_ids[port],
485
585
  targetPort=None,
486
586
  )
@@ -94,7 +94,8 @@ def get_scope(func: Callable[..., Any] | type[Any]) -> ScopeProxy:
94
94
  "inspect.getmodule() returned None and no resolvable __module__ "
95
95
  "attribute was found."
96
96
  )
97
- return ScopeProxy(module.__dict__ | vars(builtins))
97
+ scope = module.__dict__ | getattr(func, "__globals__", {})
98
+ return ScopeProxy(scope | vars(builtins))
98
99
 
99
100
 
100
101
  def resolve_attribute_to_object(attribute: str, scope: ScopeProxy | object) -> object:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes