procfunc 0.30.2__tar.gz → 0.33.0__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 (112) hide show
  1. {procfunc-0.30.2/src/procfunc.egg-info → procfunc-0.33.0}/PKG-INFO +4 -4
  2. {procfunc-0.30.2 → procfunc-0.33.0}/README.md +1 -2
  3. {procfunc-0.30.2 → procfunc-0.33.0}/pyproject.toml +9 -2
  4. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/__init__.py +2 -1
  5. procfunc-0.33.0/src/procfunc/cli/__init__.py +3 -0
  6. procfunc-0.33.0/src/procfunc/cli/main.py +35 -0
  7. procfunc-0.33.0/src/procfunc/codegen/__init__.py +8 -0
  8. {procfunc-0.30.2/src/procfunc/transpiler → procfunc-0.33.0/src/procfunc/codegen}/codegen.py +103 -150
  9. {procfunc-0.30.2/src/procfunc/transpiler → procfunc-0.33.0/src/procfunc/codegen}/identifiers.py +10 -0
  10. procfunc-0.33.0/src/procfunc/codegen/repr.py +121 -0
  11. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/compute_graph/__init__.py +26 -0
  12. procfunc-0.33.0/src/procfunc/compute_graph/compute_graph.py +32 -0
  13. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/compute_graph/node.py +2 -17
  14. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/compute_graph/operators_info.py +0 -2
  15. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/compute_graph/util.py +26 -23
  16. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/context.py +8 -7
  17. procfunc-0.30.2/src/procfunc/control.py → procfunc-0.33.0/src/procfunc/control/__init__.py +1 -1
  18. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/nodes/__init__.py +8 -4
  19. procfunc-0.33.0/src/procfunc/nodes/color.py +345 -0
  20. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/nodes/compositor.py +238 -400
  21. procfunc-0.33.0/src/procfunc/nodes/execute/construct_nodes.py +268 -0
  22. procfunc-0.33.0/src/procfunc/nodes/execute/construct_operator.py +277 -0
  23. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/nodes/execute/construct_special_cases.py +125 -18
  24. procfunc-0.33.0/src/procfunc/nodes/execute/construct_standard.py +357 -0
  25. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/nodes/execute/execute.py +17 -218
  26. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/nodes/execute/infer_runtime_data_type.py +70 -32
  27. procfunc-0.33.0/src/procfunc/nodes/execute/realize.py +225 -0
  28. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/nodes/execute/util.py +109 -8
  29. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/nodes/func.py +227 -533
  30. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/nodes/geo.py +584 -264
  31. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/nodes/manifest.json +2710 -839
  32. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/nodes/math.py +330 -143
  33. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/nodes/shader.py +121 -940
  34. procfunc-0.33.0/src/procfunc/nodes/texture.py +720 -0
  35. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/nodes/types.py +5 -27
  36. procfunc-0.33.0/src/procfunc/nodes/util/bindings_util.py +427 -0
  37. {procfunc-0.30.2/src/procfunc/nodes → procfunc-0.33.0/src/procfunc/nodes/util}/bpy_node_info.py +90 -7
  38. {procfunc-0.30.2/src/procfunc/nodes → procfunc-0.33.0/src/procfunc/nodes/util}/node_function.py +13 -2
  39. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/ops/__init__.py +1 -0
  40. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/ops/_util.py +1 -1
  41. procfunc-0.33.0/src/procfunc/ops/file.py +279 -0
  42. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/ops/mesh.py +5 -1
  43. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/ops/modifier.py +1 -0
  44. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/ops/primitives/mesh.py +2 -2
  45. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/random.py +4 -2
  46. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/tracer/__init__.py +2 -10
  47. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/tracer/decorator.py +6 -9
  48. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/tracer/patch.py +0 -17
  49. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/tracer/proxy.py +1 -1
  50. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/transforms/__init__.py +2 -0
  51. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/transforms/cleanup.py +4 -1
  52. procfunc-0.33.0/src/procfunc/transforms/convert.py +20 -0
  53. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/transforms/distribution.py +25 -26
  54. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/transforms/infer_distribution.py +37 -7
  55. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/transpiler/__init__.py +1 -8
  56. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/transpiler/bpy_to_computegraph.py +161 -167
  57. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/transpiler/main.py +105 -73
  58. procfunc-0.33.0/src/procfunc/transpiler/parse_default_values.py +35 -0
  59. procfunc-0.33.0/src/procfunc/transpiler/parse_special_cases.py +141 -0
  60. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/types.py +21 -73
  61. procfunc-0.33.0/src/procfunc/util/bpy_data.py +39 -0
  62. procfunc-0.33.0/src/procfunc/util/camera.py +0 -0
  63. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/util/pytree.py +30 -24
  64. {procfunc-0.30.2 → procfunc-0.33.0/src/procfunc.egg-info}/PKG-INFO +4 -4
  65. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc.egg-info/SOURCES.txt +28 -8
  66. procfunc-0.33.0/src/procfunc.egg-info/entry_points.txt +2 -0
  67. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc.egg-info/requires.txt +1 -0
  68. procfunc-0.33.0/tests/test_bpy_data_cleanup.py +50 -0
  69. procfunc-0.33.0/tests/test_cli_transpile.py +81 -0
  70. procfunc-0.30.2/tests/test_dedup_names.py → procfunc-0.33.0/tests/test_codegen.py +1 -1
  71. procfunc-0.33.0/tests/test_codegen_matrix.py +110 -0
  72. {procfunc-0.30.2 → procfunc-0.33.0}/tests/test_compute_graph.py +25 -0
  73. procfunc-0.33.0/tests/test_node_function.py +28 -0
  74. {procfunc-0.30.2 → procfunc-0.33.0}/tests/test_ops.py +60 -0
  75. {procfunc-0.30.2 → procfunc-0.33.0}/tests/test_pytree.py +10 -0
  76. procfunc-0.33.0/tests/test_random.py +29 -0
  77. {procfunc-0.30.2 → procfunc-0.33.0}/tests/test_trace.py +1 -1
  78. procfunc-0.33.0/tests/test_transforms.py +389 -0
  79. procfunc-0.30.2/src/procfunc/compute_graph/compute_graph.py +0 -115
  80. procfunc-0.30.2/src/procfunc/nodes/bindings_util.py +0 -196
  81. procfunc-0.30.2/src/procfunc/nodes/execute/construct_nodes.py +0 -571
  82. procfunc-0.30.2/src/procfunc/ops/file.py +0 -126
  83. procfunc-0.30.2/src/procfunc/transforms/convert.py +0 -20
  84. {procfunc-0.30.2 → procfunc-0.33.0}/LICENSE.md +0 -0
  85. {procfunc-0.30.2 → procfunc-0.33.0}/setup.cfg +0 -0
  86. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/color.py +0 -0
  87. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/compute_graph/proxy.py +0 -0
  88. {procfunc-0.30.2/src/procfunc → procfunc-0.33.0/src/procfunc/nodes}/util/__init__.py +0 -0
  89. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/ops/addons.py +0 -0
  90. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/ops/attr.py +0 -0
  91. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/ops/collection.py +0 -0
  92. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/ops/curve.py +0 -0
  93. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/ops/manifest.json +0 -0
  94. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/ops/object.py +0 -0
  95. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/ops/primitives/__init__.py +0 -0
  96. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/ops/primitives/camera.py +0 -0
  97. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/ops/primitives/curve.py +0 -0
  98. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/ops/primitives/light.py +0 -0
  99. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/ops/uv.py +0 -0
  100. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/tracer/trace.py +0 -0
  101. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/transforms/extract_materials.py +0 -0
  102. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/transforms/parameters.py +0 -0
  103. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/transforms/util.py +0 -0
  104. /procfunc-0.30.2/src/procfunc/util/camera.py → /procfunc-0.33.0/src/procfunc/util/__init__.py +0 -0
  105. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/util/bpy_info.py +0 -0
  106. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/util/keyframe.py +0 -0
  107. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/util/log.py +0 -0
  108. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/util/manifest.py +0 -0
  109. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc/util/teardown.py +0 -0
  110. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc.egg-info/dependency_links.txt +0 -0
  111. {procfunc-0.30.2 → procfunc-0.33.0}/src/procfunc.egg-info/top_level.txt +0 -0
  112. {procfunc-0.30.2 → procfunc-0.33.0}/tests/test_asset.py +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: procfunc
3
- Version: 0.30.2
3
+ Version: 0.33.0
4
4
  Summary: Function-Oriented Abstractions for Procedural 3D Generation in Python
5
5
  License-Expression: BSD-3-Clause
6
6
  Project-URL: Homepage, https://github.com/princeton-vl/procfunc
7
7
  Project-URL: Repository, https://github.com/princeton-vl/procfunc
8
- Requires-Python: >=3.11
8
+ Requires-Python: <3.12,>=3.11
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE.md
11
11
  Requires-Dist: bpy==4.2.0
@@ -19,13 +19,13 @@ Requires-Dist: ty; extra == "dev"
19
19
  Provides-Extra: docs
20
20
  Requires-Dist: sphinx; extra == "docs"
21
21
  Requires-Dist: sphinx-rtd-theme; extra == "docs"
22
+ Requires-Dist: sphinx-argparse; extra == "docs"
22
23
  Dynamic: license-file
23
24
 
24
25
  # ProcFunc: Function-Oriented Abstractions for Procedural 3D Generation in Python
25
26
 
26
27
  [**Documentation**](#documentation)
27
28
  | [**Research Paper**](https://arxiv.org/abs/2604.26943)
28
- | [**Documentation**](https://procfunc.readthedocs.io)
29
29
  | [**Transpiling**](#transpile-a-blender-file-to-procfunc-code)
30
30
  | [**Experiments**](#experiments)
31
31
  | [**Contributing**](#contributing)
@@ -62,7 +62,7 @@ Please create Github Issues for any bugs or unclear interfaces!
62
62
  Convert a Blender geometry node tree into procfunc Python code by downloading our example blend and executing the transpiler:
63
63
  ```bash
64
64
  wget https://raw.githubusercontent.com/princeton-vl/procfunc/main/examples/transpile_simple_chair/simple_chair.blend
65
- uv run python -m procfunc.transpiler.main simple_chair.blend --node_trees simple_chair --output transpiled_code.py
65
+ uv run procfunc transpile simple_chair.blend --node_trees simple_chair --output transpiled_code.py
66
66
  ```
67
67
 
68
68
  See the expected output in [`examples/transpile_simple_chair/transpiled_code.py`](examples/transpile_simple_chair/transpiled_code.py).
@@ -2,7 +2,6 @@
2
2
 
3
3
  [**Documentation**](#documentation)
4
4
  | [**Research Paper**](https://arxiv.org/abs/2604.26943)
5
- | [**Documentation**](https://procfunc.readthedocs.io)
6
5
  | [**Transpiling**](#transpile-a-blender-file-to-procfunc-code)
7
6
  | [**Experiments**](#experiments)
8
7
  | [**Contributing**](#contributing)
@@ -39,7 +38,7 @@ Please create Github Issues for any bugs or unclear interfaces!
39
38
  Convert a Blender geometry node tree into procfunc Python code by downloading our example blend and executing the transpiler:
40
39
  ```bash
41
40
  wget https://raw.githubusercontent.com/princeton-vl/procfunc/main/examples/transpile_simple_chair/simple_chair.blend
42
- uv run python -m procfunc.transpiler.main simple_chair.blend --node_trees simple_chair --output transpiled_code.py
41
+ uv run procfunc transpile simple_chair.blend --node_trees simple_chair --output transpiled_code.py
43
42
  ```
44
43
 
45
44
  See the expected output in [`examples/transpile_simple_chair/transpiled_code.py`](examples/transpile_simple_chair/transpiled_code.py).
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "procfunc"
7
- version = "0.30.2"
7
+ dynamic = ["version"]
8
8
  description = "Function-Oriented Abstractions for Procedural 3D Generation in Python"
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
11
- requires-python = ">=3.11"
11
+ requires-python = ">=3.11,<3.12"
12
12
  dependencies = [
13
13
  "bpy==4.2.0",
14
14
  "numpy<2",
@@ -19,6 +19,9 @@ dependencies = [
19
19
  Homepage = "https://github.com/princeton-vl/procfunc"
20
20
  Repository = "https://github.com/princeton-vl/procfunc"
21
21
 
22
+ [project.scripts]
23
+ procfunc = "procfunc.cli:cli"
24
+
22
25
  [project.optional-dependencies]
23
26
  dev = [
24
27
  "pytest",
@@ -29,8 +32,12 @@ dev = [
29
32
  docs = [
30
33
  "sphinx",
31
34
  "sphinx-rtd-theme",
35
+ "sphinx-argparse",
32
36
  ]
33
37
 
38
+ [tool.setuptools.dynamic]
39
+ version = {attr = "procfunc.__version__"}
40
+
34
41
  [tool.setuptools.packages.find]
35
42
  where = ["src"]
36
43
 
@@ -2,7 +2,7 @@
2
2
  # ensure this gets imported first so that mathutils etc is available even if later modules dont import bpy
3
3
  import bpy
4
4
 
5
- __version__ = "0.30.2"
5
+ __version__ = "0.33.0"
6
6
 
7
7
  from numpy.random import Generator as RNG
8
8
 
@@ -41,6 +41,7 @@ from .types import (
41
41
  LightObject,
42
42
  LightProbeObject,
43
43
  MetaObject,
44
+ PointCloudObject,
44
45
  Material,
45
46
  Texture,
46
47
  Collection,
@@ -0,0 +1,3 @@
1
+ from procfunc.cli.main import cli
2
+
3
+ __all__ = ["cli"]
@@ -0,0 +1,35 @@
1
+ import argparse
2
+
3
+ from procfunc.transpiler import main as transpiler_main
4
+ from procfunc.util.teardown import skip_teardown_on_exit
5
+
6
+
7
+ def get_parser() -> argparse.ArgumentParser:
8
+ parser = argparse.ArgumentParser(prog="procfunc")
9
+ subparsers = parser.add_subparsers(dest="command", required=True)
10
+
11
+ transpile_parser = subparsers.add_parser(
12
+ "transpile", help="Transpile a Blender file to procfunc Python code."
13
+ )
14
+ transpiler_main.add_transpile_arguments(transpile_parser)
15
+
16
+ return parser
17
+
18
+
19
+ def main():
20
+ parser = get_parser()
21
+ args = parser.parse_args()
22
+
23
+ if args.command == "transpile":
24
+ transpiler_main.run(args)
25
+ else:
26
+ parser.error(f"Unknown command: {args.command}")
27
+
28
+
29
+ def cli():
30
+ with skip_teardown_on_exit():
31
+ main()
32
+
33
+
34
+ if __name__ == "__main__":
35
+ cli()
@@ -0,0 +1,8 @@
1
+ from .codegen import default_func_resolution_map, to_python
2
+ from .identifiers import dedup_names_with_suffix
3
+
4
+ __all__ = [
5
+ "default_func_resolution_map",
6
+ "to_python",
7
+ "dedup_names_with_suffix",
8
+ ]
@@ -5,19 +5,19 @@ import itertools
5
5
  import logging
6
6
  from collections import OrderedDict, defaultdict
7
7
  from pathlib import Path
8
- from typing import Any, Callable, Generator, Union, get_args, get_origin
8
+ from typing import Any, Callable, Generator
9
9
 
10
10
  import numpy as np
11
11
 
12
12
  import procfunc as pf
13
13
  from procfunc import compute_graph as cg
14
+ from procfunc.codegen import identifiers
15
+ from procfunc.codegen.repr import repr_type, repr_value
14
16
  from procfunc.compute_graph.operators_info import (
15
17
  FUNCTIONS_TO_OPERATORS,
16
18
  OPERATOR_TEMPLATES,
17
19
  OperatorType,
18
20
  )
19
- from procfunc.nodes import types as nt
20
- from procfunc.transpiler import identifiers
21
21
  from procfunc.util import pytree
22
22
 
23
23
  logger = logging.getLogger(__name__)
@@ -29,92 +29,6 @@ def indent_lines(lines: list[str], indent: str = INDENT) -> list[str]:
29
29
  return [indent + line for line in lines]
30
30
 
31
31
 
32
- def _repr_type(x: Any) -> str:
33
- # TODO: make the user pass in special resolutions for types, or else we will just do verbose types
34
-
35
- if isinstance(x, str):
36
- return x
37
-
38
- if x.__name__ == "NoneType":
39
- return "None"
40
-
41
- origin = get_origin(x)
42
- args = get_args(x)
43
-
44
- if x.__name__ == "ProcNode":
45
- if len(args) == 1:
46
- return f"pf.ProcNode[{_repr_type(args[0])}]"
47
- elif len(args) == 0:
48
- return "pf.ProcNode"
49
- else:
50
- raise ValueError(f"Unsupported ProcNode type: {x} {args=}")
51
-
52
- if hasattr(pf, x.__name__):
53
- if len(args):
54
- raise ValueError(f"procfunc type had unhandled annotations: {x} {args=}")
55
- return f"pf.{x.__name__}"
56
-
57
- if x.__module__ == "builtins":
58
- return x.__name__
59
-
60
- origin = get_origin(x)
61
- args = get_args(x)
62
-
63
- if origin is Union:
64
- args_0 = get_args(args[0])
65
- if get_origin(args[0]) is nt.ProcNode and args_0[0] is args[1]:
66
- return f"t.SocketOrVal[{_repr_type(args_0[0])}]"
67
- else:
68
- return " | ".join([_repr_type(a) for a in args])
69
-
70
- if getattr(x, "__module__", None) == "procfunc.nodes.types":
71
- return f"t.{x.__name__}"
72
-
73
- return x.__name__
74
-
75
-
76
- def _repr_value(value: Any) -> str:
77
- if hasattr(value, "__wrapped__"):
78
- value = value.__wrapped__
79
-
80
- if isinstance(value, cg.Proxy):
81
- logger.warning(
82
- f"Proxy object {value} should never appear as a raw value in codegen - "
83
- f"its underlying node {value.node} was not resolved to a variable"
84
- )
85
- if isinstance(value, nt.ProcNode):
86
- logger.warning(
87
- f"Procnode object {value} should never be treated as a raw value in codegen"
88
- )
89
-
90
- if isinstance(value, np.random.Generator):
91
- return "np.random.default_rng()"
92
- elif isinstance(value, type):
93
- return _repr_type(value)
94
- elif isinstance(value, np.ndarray):
95
- nprepr = repr(value).replace("\n", "")
96
- return f"np.{nprepr}"
97
- elif isinstance(value, np.dtype):
98
- return f"np.dtype('{value}')"
99
- elif isinstance(value, (pf.Color, pf.Vector, pf.Euler, pf.Quaternion, pf.Matrix)):
100
- x = tuple(round(x, 6) for x in value)
101
- return f"pf.{value.__class__.__name__}({x})"
102
- elif isinstance(value, enum.Enum):
103
- return f"{type(value).__name__}.{value.name}"
104
- elif isinstance(value, Path):
105
- return f"Path({str(value)!r})"
106
- elif dataclasses.is_dataclass(value) and not isinstance(value, type):
107
- args_str = ", ".join(
108
- f"{f.name}={_repr_value(getattr(value, f.name))}"
109
- for f in dataclasses.fields(value)
110
- )
111
- return f"{type(value).__name__}({args_str})"
112
- elif isinstance(value, list):
113
- return f"[{', '.join([_repr_value(x) for x in value])}]"
114
- else:
115
- return repr(value)
116
-
117
-
118
32
  def _repr_inp(
119
33
  arg: Any,
120
34
  scope_expressions: dict[int, str | list[str]],
@@ -127,7 +41,7 @@ def _repr_inp(
127
41
  )
128
42
  expr = scope_expressions[id(arg)]
129
43
  else:
130
- expr = _repr_value(arg)
44
+ expr = repr_value(arg)
131
45
 
132
46
  if isinstance(expr, list):
133
47
  if len(expr) > 1:
@@ -184,7 +98,7 @@ def _repr_args(
184
98
 
185
99
  argreprs = pytree.PyTree(args).map(lambda x: _repr_inp(x, scope_expressions))
186
100
  argreprs = [
187
- pytree.repr_tree_to_str(v, type_namer=_repr_type)
101
+ pytree.repr_tree_to_str(v, type_namer=repr_type)
188
102
  for v in argreprs.unflatten_one_level()
189
103
  ]
190
104
 
@@ -194,7 +108,7 @@ def _repr_args(
194
108
  .unflatten_one_level()
195
109
  )
196
110
  kwargreprs = {
197
- k: pytree.repr_tree_to_str(v, type_namer=_repr_type)
111
+ k: pytree.repr_tree_to_str(v, type_namer=repr_type)
198
112
  for k, v in kwargreprs.items()
199
113
  }
200
114
 
@@ -222,11 +136,12 @@ def _repr_function_call(
222
136
  node: cg.FunctionCallNode | cg.MethodCallNode | cg.SubgraphCallNode,
223
137
  scope_expressions: dict[int, str | list[str]],
224
138
  line_limit: int = 80,
139
+ func_str: str | None = None,
225
140
  ) -> list[str]:
226
141
  match node:
227
142
  case cg.FunctionCallNode():
228
143
  func = node.func
229
- func_str = scope_expressions[id(func)]
144
+ func_str = func_str or scope_expressions[id(func)]
230
145
  case cg.MethodCallNode(args=(target, *_), method_name=method_name):
231
146
  if not isinstance(target, cg.Node):
232
147
  raise ValueError(f"Method call {node=} has non-node target {target=}")
@@ -261,22 +176,44 @@ def _repr_function_call(
261
176
  return [f"{func_str}({', '.join(arg_reprs)})"]
262
177
 
263
178
 
264
- def _repr_operator_call(
179
+ def _operator_call_operands(
265
180
  node: cg.FunctionCallNode,
181
+ template: str,
182
+ ) -> list | None:
183
+ """Operand values for rendering `node` in infix/operator form, or None to
184
+ decline it. The operator template only has slots for the operands, so any
185
+ extra argument beyond them (e.g. a non-default epsilon on func.equal) would
186
+ be silently dropped by the infix form - decline unless it merely restates
187
+ the signature default, in which case it is redundant and dropped."""
188
+ n_slots = template.count("{}")
189
+ try:
190
+ sig = inspect.signature(node.func)
191
+ bound = sig.bind(*node.args, **node.kwargs)
192
+ except (TypeError, ValueError):
193
+ return None
194
+
195
+ operand_names = list(sig.parameters)[:n_slots]
196
+ if any(name not in bound.arguments for name in operand_names):
197
+ return None
198
+
199
+ for name, value in bound.arguments.items():
200
+ if name in operand_names:
201
+ continue
202
+ if not _kwarg_matches_default(sig, name, value):
203
+ return None
204
+
205
+ return [bound.arguments[name] for name in operand_names]
206
+
207
+
208
+ def _repr_operator_call(
209
+ operands: list,
210
+ template: str,
266
211
  scope_expressions: dict[int, str | list[str]],
267
212
  ) -> list[str]:
268
- assert isinstance(node, cg.FunctionCallNode), node
269
-
270
- # Support both positional args and kwargs for operator templates
271
- all_args = [
272
- _repr_inp(v, scope_expressions, extra_parens=True) for v in node.args
273
- ] + [
274
- _repr_inp(v, scope_expressions, extra_parens=True) for v in node.kwargs.values()
213
+ operand_reprs = [
214
+ _repr_inp(v, scope_expressions, extra_parens=True) for v in operands
275
215
  ]
276
-
277
- operator_template = scope_expressions[id(node.func)]
278
- assert isinstance(operator_template, str), operator_template
279
- return [operator_template.format(*all_args)]
216
+ return [template.format(*operand_reprs)]
280
217
 
281
218
 
282
219
  def _codegen_for_node(
@@ -293,7 +230,17 @@ def _codegen_for_node(
293
230
  elif funcres == OperatorType.NOOP:
294
231
  return [] # no code needed
295
232
  elif "{}" in funcres:
296
- return _repr_operator_call(node, scope_expressions)
233
+ operands = _operator_call_operands(node, funcres)
234
+ if operands is None:
235
+ # args the infix form cannot express: emit a named call.
236
+ # scope_expressions holds the operator template, so re-derive
237
+ # the callsite name (import already present via
238
+ # default_func_resolution_map)
239
+ _, callsite = _resolve_func(func)
240
+ return _repr_function_call(
241
+ node, scope_expressions, func_str=callsite
242
+ )
243
+ return _repr_operator_call(operands, funcres, scope_expressions)
297
244
  else:
298
245
  return _repr_function_call(node, scope_expressions)
299
246
  case cg.MethodCallNode() if node.method_name == "__getitem__":
@@ -320,7 +267,7 @@ def _codegen_for_node(
320
267
  )
321
268
  return [f"{arg_expr}.{attribute_name}"]
322
269
  case cg.ConstantNode(value=value):
323
- return [_repr_value(value)]
270
+ return [repr_value(value)]
324
271
  case _:
325
272
  raise TypeError(f"Unsupported {node=}")
326
273
 
@@ -353,17 +300,17 @@ def _codegen_graph_inputs(
353
300
 
354
301
  known_value_type = node.metadata.get("known_value_type", None)
355
302
  line = (
356
- f"{name}: {_repr_type(known_value_type)}"
303
+ f"{name}: {repr_type(known_value_type)}"
357
304
  if known_value_type is not None
358
305
  else f"{name}"
359
306
  )
360
307
 
361
308
  if (default := node.kwargs.get("default_value")) is not None:
362
- line += f" = {_repr_value(default)}"
309
+ line += f" = {repr_value(default)}"
363
310
 
364
311
  args_lines.append(line + ",")
365
312
 
366
- end_statement = "):" if typename is None else f") -> {typename}: "
313
+ end_statement = "):" if typename is None else f") -> {typename}:"
367
314
 
368
315
  return [f"def {func_name}("] + indent_lines(args_lines) + [end_statement]
369
316
 
@@ -379,7 +326,7 @@ def _codegen_namedtuple_def(outputs: pytree.PyTree):
379
326
  if vt is None:
380
327
  type_lines.append(f"{name}: Any")
381
328
  else:
382
- type_lines.append(f"{name}: {_repr_type(vt)}")
329
+ type_lines.append(f"{name}: {repr_type(vt)}")
383
330
 
384
331
  return [f"class {tupletype.__name__}(NamedTuple):"] + indent_lines(type_lines)
385
332
 
@@ -389,15 +336,17 @@ def _codegen_for_outputs(
389
336
  scope_expressions: dict[int, str | list[str]],
390
337
  ) -> tuple[str | None, list[str], list[str]]:
391
338
  if len(graph.outputs) == 0:
392
- return None, [], []
339
+ # Sink graphs (no return values) still need a body statement so the
340
+ # generated function parses; emit `pass` when nothing else fills it.
341
+ return None, [], ["pass"]
393
342
  if len(graph.outputs) == 1:
394
343
  single_output = next(graph.outputs.values())
395
344
  vt = single_output.metadata.get("known_value_type", None)
396
- type_name = _repr_type(vt) if vt is not None else None
345
+ type_name = repr_type(vt) if vt is not None else None
397
346
  return type_name, [], [f"return {_repr_inp(single_output, scope_expressions)}"]
398
347
 
399
348
  graph_output_type = graph.outputs.toplevel_type()
400
- type_name = _repr_type(graph_output_type)
349
+ type_name = repr_type(graph_output_type)
401
350
 
402
351
  is_pf_type = hasattr(pf, graph_output_type.__name__)
403
352
  if is_pf_type:
@@ -416,7 +365,7 @@ def _codegen_for_outputs(
416
365
 
417
366
  reprs_tree = graph.outputs.map(lambda node: _repr_inp(node, scope_expressions))
418
367
  return_lines = [
419
- f"return {pytree.repr_tree_to_str(reprs_tree, type_namer=_repr_type)}"
368
+ f"return {pytree.repr_tree_to_str(reprs_tree, type_namer=repr_type)}"
420
369
  ]
421
370
 
422
371
  return type_name, type_def, return_lines
@@ -572,6 +521,7 @@ def _code_paragraphing_predicate(
572
521
  def _codegen_for_assignment(
573
522
  assign_varname: str,
574
523
  node_code: list[str] | str,
524
+ node: cg.Node,
575
525
  add_line_comments: bool,
576
526
  ) -> list[str]:
577
527
  assert isinstance(assign_varname, str)
@@ -582,7 +532,7 @@ def _codegen_for_assignment(
582
532
  else:
583
533
  node_code = [f"{assign_varname} = {node_code}"]
584
534
  if add_line_comments:
585
- node_code[0] += f" # {node}" # noqa: F821
535
+ node_code[-1] += f" # {str(node).replace(chr(10), ' ')}"
586
536
 
587
537
  return node_code
588
538
 
@@ -662,7 +612,7 @@ def _codegen_for_graph(
662
612
  continue
663
613
 
664
614
  varname = expressions[id(node)]
665
- node_code = _codegen_for_assignment(varname, node_code, add_line_comments)
615
+ node_code = _codegen_for_assignment(varname, node_code, node, add_line_comments)
666
616
  code_lines.extend(node_code)
667
617
 
668
618
  if last_varname.split("_")[0] != varname.split("_")[0]:
@@ -720,7 +670,7 @@ def default_func_resolution_map(
720
670
  skip_funcs: set | None = None,
721
671
  ) -> tuple[dict[Any, str | OperatorType], list[str]]:
722
672
  func_resolution = {}
723
- import_lines = set()
673
+ import_lines = {"import procfunc as pf"}
724
674
 
725
675
  for graph in cg.traverse_nested_graphs(toplevel_graph):
726
676
  assert isinstance(graph, cg.ComputeGraph), graph
@@ -771,43 +721,46 @@ def graphs_to_python_functions(
771
721
  np_linewidth = np.get_printoptions()["linewidth"]
772
722
  np.set_printoptions(linewidth=100000)
773
723
 
774
- targets = _topo_sort_subgraphs(graph)
724
+ try:
725
+ targets = _topo_sort_subgraphs(graph)
775
726
 
776
- def _clean_graph_name(name: str) -> str:
777
- for suffix in identifiers.NONDESCRIPTIVE_NODE_NAME_PARTS:
778
- if name.endswith("_" + suffix):
779
- name = name[: -(len(suffix) + 1)]
780
- return name
727
+ def _clean_graph_name(name: str) -> str:
728
+ for suffix in identifiers.NONDESCRIPTIVE_NODE_NAME_PARTS:
729
+ if name.endswith("_" + suffix):
730
+ name = name[: -(len(suffix) + 1)]
731
+ return name
781
732
 
782
- for subgraph in cg.traverse_nested_graphs(graph):
783
- subgraph.name = _clean_graph_name(subgraph.name)
733
+ for subgraph in cg.traverse_nested_graphs(graph):
734
+ subgraph.name = _clean_graph_name(subgraph.name)
784
735
 
785
- subgraph_names = {
786
- id(subgraph): subgraph.name for subgraph in cg.traverse_nested_graphs(graph)
787
- }
788
- subgraph_names = identifiers.dedup_names_with_suffix(subgraph_names, separator="_")
789
-
790
- scope_expressions = subgraph_names.copy()
791
- for k, v in func_resolution.items():
792
- if isinstance(v, OperatorType):
793
- scope_expressions[id(k)] = OPERATOR_TEMPLATES[v]
794
- else:
795
- scope_expressions[id(k)] = v
796
-
797
- lines_for_modules = []
798
- for subgraph in targets:
799
- func_name = subgraph_names[id(subgraph)]
800
- result = _codegen_for_graph(
801
- subgraph,
802
- scope_expressions=scope_expressions.copy(),
803
- as_maincall=(subgraph is graph and toplevel_as_maincall),
804
- add_version_comment=add_version_comment,
805
- add_line_comments=add_line_comments,
806
- func_name=func_name,
736
+ subgraph_names = {
737
+ id(subgraph): subgraph.name for subgraph in cg.traverse_nested_graphs(graph)
738
+ }
739
+ subgraph_names = identifiers.dedup_names_with_suffix(
740
+ subgraph_names, separator="_"
807
741
  )
808
- lines_for_modules.append((subgraph_names[id(subgraph)], result))
809
742
 
810
- np.set_printoptions(linewidth=np_linewidth)
743
+ scope_expressions = subgraph_names.copy()
744
+ for k, v in func_resolution.items():
745
+ if isinstance(v, OperatorType):
746
+ scope_expressions[id(k)] = OPERATOR_TEMPLATES[v]
747
+ else:
748
+ scope_expressions[id(k)] = v
749
+
750
+ lines_for_modules = []
751
+ for subgraph in targets:
752
+ func_name = subgraph_names[id(subgraph)]
753
+ result = _codegen_for_graph(
754
+ subgraph,
755
+ scope_expressions=scope_expressions.copy(),
756
+ as_maincall=(subgraph is graph and toplevel_as_maincall),
757
+ add_version_comment=add_version_comment,
758
+ add_line_comments=add_line_comments,
759
+ func_name=func_name,
760
+ )
761
+ lines_for_modules.append((subgraph_names[id(subgraph)], result))
762
+ finally:
763
+ np.set_printoptions(linewidth=np_linewidth)
811
764
 
812
765
  return OrderedDict(lines_for_modules)
813
766
 
@@ -1,3 +1,4 @@
1
+ import keyword
1
2
  import logging
2
3
  import re
3
4
  from collections import Counter, defaultdict
@@ -36,6 +37,10 @@ def bpy_name_to_pythonid(name: str) -> str:
36
37
 
37
38
  name = name.lower()
38
39
 
40
+ # drop anything that isn't valid in a python identifier (e.g. parentheses
41
+ # in socket names like "Joint ID (do not set)")
42
+ name = re.sub(r"[^0-9a-z_]", "_", name)
43
+
39
44
  name = re.sub(r"_+", "_", name).strip("_")
40
45
 
41
46
  # move number terms to end
@@ -46,6 +51,9 @@ def bpy_name_to_pythonid(name: str) -> str:
46
51
 
47
52
  name = "_".join(parts)
48
53
 
54
+ if keyword.iskeyword(name):
55
+ name = name + "_"
56
+
49
57
  return name
50
58
 
51
59
 
@@ -60,6 +68,8 @@ def is_valid_snake_identifier(name: str) -> bool:
60
68
  return False
61
69
  if name != name.lower():
62
70
  return False # this is opinionated
71
+ if not name.isidentifier() or keyword.iskeyword(name):
72
+ return False
63
73
  return True
64
74
 
65
75