procfunc 0.31.0__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 (110) hide show
  1. {procfunc-0.31.0/src/procfunc.egg-info → procfunc-0.33.0}/PKG-INFO +2 -2
  2. {procfunc-0.31.0 → procfunc-0.33.0}/pyproject.toml +5 -2
  3. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/__init__.py +1 -1
  4. procfunc-0.33.0/src/procfunc/cli/__init__.py +3 -0
  5. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/codegen/codegen.py +98 -152
  6. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/codegen/identifiers.py +10 -0
  7. procfunc-0.33.0/src/procfunc/codegen/repr.py +121 -0
  8. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/compute_graph/__init__.py +26 -0
  9. procfunc-0.33.0/src/procfunc/compute_graph/compute_graph.py +32 -0
  10. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/compute_graph/node.py +0 -8
  11. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/compute_graph/operators_info.py +0 -2
  12. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/compute_graph/util.py +26 -23
  13. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/context.py +8 -7
  14. procfunc-0.31.0/src/procfunc/control.py → procfunc-0.33.0/src/procfunc/control/__init__.py +1 -1
  15. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/nodes/__init__.py +3 -3
  16. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/nodes/color.py +20 -9
  17. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/nodes/compositor.py +119 -53
  18. procfunc-0.33.0/src/procfunc/nodes/execute/construct_nodes.py +268 -0
  19. procfunc-0.33.0/src/procfunc/nodes/execute/construct_operator.py +277 -0
  20. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/nodes/execute/construct_special_cases.py +108 -4
  21. procfunc-0.33.0/src/procfunc/nodes/execute/construct_standard.py +357 -0
  22. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/nodes/execute/execute.py +17 -218
  23. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/nodes/execute/infer_runtime_data_type.py +70 -32
  24. procfunc-0.33.0/src/procfunc/nodes/execute/realize.py +225 -0
  25. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/nodes/execute/util.py +107 -6
  26. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/nodes/func.py +183 -76
  27. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/nodes/geo.py +394 -85
  28. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/nodes/manifest.json +292 -629
  29. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/nodes/math.py +36 -30
  30. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/nodes/shader.py +120 -62
  31. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/nodes/texture.py +80 -46
  32. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/nodes/types.py +5 -9
  33. {procfunc-0.31.0/src/procfunc/nodes → procfunc-0.33.0/src/procfunc/nodes/util}/bindings_util.py +161 -40
  34. {procfunc-0.31.0/src/procfunc/nodes → procfunc-0.33.0/src/procfunc/nodes/util}/bpy_node_info.py +83 -7
  35. {procfunc-0.31.0/src/procfunc/nodes → procfunc-0.33.0/src/procfunc/nodes/util}/node_function.py +13 -2
  36. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/ops/__init__.py +1 -0
  37. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/ops/_util.py +1 -1
  38. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/ops/mesh.py +5 -1
  39. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/ops/modifier.py +1 -0
  40. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/ops/primitives/mesh.py +2 -2
  41. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/random.py +4 -2
  42. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/tracer/__init__.py +2 -10
  43. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/tracer/decorator.py +6 -9
  44. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/tracer/patch.py +0 -17
  45. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/tracer/proxy.py +1 -1
  46. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/transforms/cleanup.py +4 -1
  47. procfunc-0.33.0/src/procfunc/transforms/convert.py +20 -0
  48. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/transforms/distribution.py +25 -26
  49. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/transforms/infer_distribution.py +37 -7
  50. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/transpiler/__init__.py +1 -2
  51. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/transpiler/bpy_to_computegraph.py +89 -153
  52. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/transpiler/main.py +21 -53
  53. procfunc-0.33.0/src/procfunc/transpiler/parse_default_values.py +35 -0
  54. procfunc-0.33.0/src/procfunc/transpiler/parse_special_cases.py +141 -0
  55. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/types.py +21 -73
  56. procfunc-0.33.0/src/procfunc/util/bpy_data.py +39 -0
  57. procfunc-0.33.0/src/procfunc/util/camera.py +0 -0
  58. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/util/pytree.py +30 -24
  59. {procfunc-0.31.0 → procfunc-0.33.0/src/procfunc.egg-info}/PKG-INFO +2 -2
  60. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc.egg-info/SOURCES.txt +21 -6
  61. procfunc-0.33.0/tests/test_bpy_data_cleanup.py +50 -0
  62. procfunc-0.33.0/tests/test_cli_transpile.py +81 -0
  63. procfunc-0.33.0/tests/test_codegen_matrix.py +110 -0
  64. {procfunc-0.31.0 → procfunc-0.33.0}/tests/test_compute_graph.py +25 -0
  65. procfunc-0.33.0/tests/test_node_function.py +28 -0
  66. {procfunc-0.31.0 → procfunc-0.33.0}/tests/test_ops.py +29 -0
  67. {procfunc-0.31.0 → procfunc-0.33.0}/tests/test_pytree.py +10 -0
  68. procfunc-0.33.0/tests/test_random.py +29 -0
  69. procfunc-0.33.0/tests/test_transforms.py +389 -0
  70. procfunc-0.31.0/src/procfunc/compute_graph/compute_graph.py +0 -115
  71. procfunc-0.31.0/src/procfunc/nodes/execute/construct_nodes.py +0 -602
  72. procfunc-0.31.0/src/procfunc/transforms/convert.py +0 -20
  73. {procfunc-0.31.0 → procfunc-0.33.0}/LICENSE.md +0 -0
  74. {procfunc-0.31.0 → procfunc-0.33.0}/README.md +0 -0
  75. {procfunc-0.31.0 → procfunc-0.33.0}/setup.cfg +0 -0
  76. /procfunc-0.31.0/src/procfunc/cli.py → /procfunc-0.33.0/src/procfunc/cli/main.py +0 -0
  77. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/codegen/__init__.py +0 -0
  78. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/color.py +0 -0
  79. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/compute_graph/proxy.py +0 -0
  80. {procfunc-0.31.0/src/procfunc → procfunc-0.33.0/src/procfunc/nodes}/util/__init__.py +0 -0
  81. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/ops/addons.py +0 -0
  82. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/ops/attr.py +0 -0
  83. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/ops/collection.py +0 -0
  84. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/ops/curve.py +0 -0
  85. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/ops/file.py +0 -0
  86. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/ops/manifest.json +0 -0
  87. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/ops/object.py +0 -0
  88. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/ops/primitives/__init__.py +0 -0
  89. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/ops/primitives/camera.py +0 -0
  90. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/ops/primitives/curve.py +0 -0
  91. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/ops/primitives/light.py +0 -0
  92. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/ops/uv.py +0 -0
  93. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/tracer/trace.py +0 -0
  94. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/transforms/__init__.py +0 -0
  95. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/transforms/extract_materials.py +0 -0
  96. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/transforms/parameters.py +0 -0
  97. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/transforms/util.py +0 -0
  98. /procfunc-0.31.0/src/procfunc/util/camera.py → /procfunc-0.33.0/src/procfunc/util/__init__.py +0 -0
  99. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/util/bpy_info.py +0 -0
  100. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/util/keyframe.py +0 -0
  101. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/util/log.py +0 -0
  102. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/util/manifest.py +0 -0
  103. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc/util/teardown.py +0 -0
  104. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc.egg-info/dependency_links.txt +0 -0
  105. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc.egg-info/entry_points.txt +0 -0
  106. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc.egg-info/requires.txt +0 -0
  107. {procfunc-0.31.0 → procfunc-0.33.0}/src/procfunc.egg-info/top_level.txt +0 -0
  108. {procfunc-0.31.0 → procfunc-0.33.0}/tests/test_asset.py +0 -0
  109. {procfunc-0.31.0 → procfunc-0.33.0}/tests/test_codegen.py +0 -0
  110. {procfunc-0.31.0 → procfunc-0.33.0}/tests/test_trace.py +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: procfunc
3
- Version: 0.31.0
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
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "procfunc"
7
- version = "0.31.0"
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",
@@ -35,6 +35,9 @@ docs = [
35
35
  "sphinx-argparse",
36
36
  ]
37
37
 
38
+ [tool.setuptools.dynamic]
39
+ version = {attr = "procfunc.__version__"}
40
+
38
41
  [tool.setuptools.packages.find]
39
42
  where = ["src"]
40
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
 
@@ -0,0 +1,3 @@
1
+ from procfunc.cli.main import cli
2
+
3
+ __all__ = ["cli"]
@@ -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
14
  from procfunc.codegen import identifiers
15
+ from procfunc.codegen.repr import repr_type, repr_value
15
16
  from procfunc.compute_graph.operators_info import (
16
17
  FUNCTIONS_TO_OPERATORS,
17
18
  OPERATOR_TEMPLATES,
18
19
  OperatorType,
19
20
  )
20
- from procfunc.nodes import types as nt
21
21
  from procfunc.util import pytree
22
22
 
23
23
  logger = logging.getLogger(__name__)
@@ -29,97 +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, 8) 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
- elif isinstance(value, tuple):
115
- inner = ", ".join(_repr_value(x) for x in value)
116
- return f"({inner},)" if len(value) == 1 else f"({inner})"
117
- elif isinstance(value, float) and not isinstance(value, bool):
118
- return repr(round(value, 8))
119
- else:
120
- return repr(value)
121
-
122
-
123
32
  def _repr_inp(
124
33
  arg: Any,
125
34
  scope_expressions: dict[int, str | list[str]],
@@ -132,7 +41,7 @@ def _repr_inp(
132
41
  )
133
42
  expr = scope_expressions[id(arg)]
134
43
  else:
135
- expr = _repr_value(arg)
44
+ expr = repr_value(arg)
136
45
 
137
46
  if isinstance(expr, list):
138
47
  if len(expr) > 1:
@@ -189,7 +98,7 @@ def _repr_args(
189
98
 
190
99
  argreprs = pytree.PyTree(args).map(lambda x: _repr_inp(x, scope_expressions))
191
100
  argreprs = [
192
- pytree.repr_tree_to_str(v, type_namer=_repr_type)
101
+ pytree.repr_tree_to_str(v, type_namer=repr_type)
193
102
  for v in argreprs.unflatten_one_level()
194
103
  ]
195
104
 
@@ -199,7 +108,7 @@ def _repr_args(
199
108
  .unflatten_one_level()
200
109
  )
201
110
  kwargreprs = {
202
- k: pytree.repr_tree_to_str(v, type_namer=_repr_type)
111
+ k: pytree.repr_tree_to_str(v, type_namer=repr_type)
203
112
  for k, v in kwargreprs.items()
204
113
  }
205
114
 
@@ -227,11 +136,12 @@ def _repr_function_call(
227
136
  node: cg.FunctionCallNode | cg.MethodCallNode | cg.SubgraphCallNode,
228
137
  scope_expressions: dict[int, str | list[str]],
229
138
  line_limit: int = 80,
139
+ func_str: str | None = None,
230
140
  ) -> list[str]:
231
141
  match node:
232
142
  case cg.FunctionCallNode():
233
143
  func = node.func
234
- func_str = scope_expressions[id(func)]
144
+ func_str = func_str or scope_expressions[id(func)]
235
145
  case cg.MethodCallNode(args=(target, *_), method_name=method_name):
236
146
  if not isinstance(target, cg.Node):
237
147
  raise ValueError(f"Method call {node=} has non-node target {target=}")
@@ -266,22 +176,44 @@ def _repr_function_call(
266
176
  return [f"{func_str}({', '.join(arg_reprs)})"]
267
177
 
268
178
 
269
- def _repr_operator_call(
179
+ def _operator_call_operands(
270
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,
271
211
  scope_expressions: dict[int, str | list[str]],
272
212
  ) -> list[str]:
273
- assert isinstance(node, cg.FunctionCallNode), node
274
-
275
- # Support both positional args and kwargs for operator templates
276
- all_args = [
277
- _repr_inp(v, scope_expressions, extra_parens=True) for v in node.args
278
- ] + [
279
- _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
280
215
  ]
281
-
282
- operator_template = scope_expressions[id(node.func)]
283
- assert isinstance(operator_template, str), operator_template
284
- return [operator_template.format(*all_args)]
216
+ return [template.format(*operand_reprs)]
285
217
 
286
218
 
287
219
  def _codegen_for_node(
@@ -298,7 +230,17 @@ def _codegen_for_node(
298
230
  elif funcres == OperatorType.NOOP:
299
231
  return [] # no code needed
300
232
  elif "{}" in funcres:
301
- 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)
302
244
  else:
303
245
  return _repr_function_call(node, scope_expressions)
304
246
  case cg.MethodCallNode() if node.method_name == "__getitem__":
@@ -325,7 +267,7 @@ def _codegen_for_node(
325
267
  )
326
268
  return [f"{arg_expr}.{attribute_name}"]
327
269
  case cg.ConstantNode(value=value):
328
- return [_repr_value(value)]
270
+ return [repr_value(value)]
329
271
  case _:
330
272
  raise TypeError(f"Unsupported {node=}")
331
273
 
@@ -358,17 +300,17 @@ def _codegen_graph_inputs(
358
300
 
359
301
  known_value_type = node.metadata.get("known_value_type", None)
360
302
  line = (
361
- f"{name}: {_repr_type(known_value_type)}"
303
+ f"{name}: {repr_type(known_value_type)}"
362
304
  if known_value_type is not None
363
305
  else f"{name}"
364
306
  )
365
307
 
366
308
  if (default := node.kwargs.get("default_value")) is not None:
367
- line += f" = {_repr_value(default)}"
309
+ line += f" = {repr_value(default)}"
368
310
 
369
311
  args_lines.append(line + ",")
370
312
 
371
- end_statement = "):" if typename is None else f") -> {typename}: "
313
+ end_statement = "):" if typename is None else f") -> {typename}:"
372
314
 
373
315
  return [f"def {func_name}("] + indent_lines(args_lines) + [end_statement]
374
316
 
@@ -384,7 +326,7 @@ def _codegen_namedtuple_def(outputs: pytree.PyTree):
384
326
  if vt is None:
385
327
  type_lines.append(f"{name}: Any")
386
328
  else:
387
- type_lines.append(f"{name}: {_repr_type(vt)}")
329
+ type_lines.append(f"{name}: {repr_type(vt)}")
388
330
 
389
331
  return [f"class {tupletype.__name__}(NamedTuple):"] + indent_lines(type_lines)
390
332
 
@@ -400,11 +342,11 @@ def _codegen_for_outputs(
400
342
  if len(graph.outputs) == 1:
401
343
  single_output = next(graph.outputs.values())
402
344
  vt = single_output.metadata.get("known_value_type", None)
403
- 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
404
346
  return type_name, [], [f"return {_repr_inp(single_output, scope_expressions)}"]
405
347
 
406
348
  graph_output_type = graph.outputs.toplevel_type()
407
- type_name = _repr_type(graph_output_type)
349
+ type_name = repr_type(graph_output_type)
408
350
 
409
351
  is_pf_type = hasattr(pf, graph_output_type.__name__)
410
352
  if is_pf_type:
@@ -423,7 +365,7 @@ def _codegen_for_outputs(
423
365
 
424
366
  reprs_tree = graph.outputs.map(lambda node: _repr_inp(node, scope_expressions))
425
367
  return_lines = [
426
- 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)}"
427
369
  ]
428
370
 
429
371
  return type_name, type_def, return_lines
@@ -579,6 +521,7 @@ def _code_paragraphing_predicate(
579
521
  def _codegen_for_assignment(
580
522
  assign_varname: str,
581
523
  node_code: list[str] | str,
524
+ node: cg.Node,
582
525
  add_line_comments: bool,
583
526
  ) -> list[str]:
584
527
  assert isinstance(assign_varname, str)
@@ -589,7 +532,7 @@ def _codegen_for_assignment(
589
532
  else:
590
533
  node_code = [f"{assign_varname} = {node_code}"]
591
534
  if add_line_comments:
592
- node_code[0] += f" # {node}" # noqa: F821
535
+ node_code[-1] += f" # {str(node).replace(chr(10), ' ')}"
593
536
 
594
537
  return node_code
595
538
 
@@ -669,7 +612,7 @@ def _codegen_for_graph(
669
612
  continue
670
613
 
671
614
  varname = expressions[id(node)]
672
- node_code = _codegen_for_assignment(varname, node_code, add_line_comments)
615
+ node_code = _codegen_for_assignment(varname, node_code, node, add_line_comments)
673
616
  code_lines.extend(node_code)
674
617
 
675
618
  if last_varname.split("_")[0] != varname.split("_")[0]:
@@ -778,43 +721,46 @@ def graphs_to_python_functions(
778
721
  np_linewidth = np.get_printoptions()["linewidth"]
779
722
  np.set_printoptions(linewidth=100000)
780
723
 
781
- targets = _topo_sort_subgraphs(graph)
724
+ try:
725
+ targets = _topo_sort_subgraphs(graph)
782
726
 
783
- def _clean_graph_name(name: str) -> str:
784
- for suffix in identifiers.NONDESCRIPTIVE_NODE_NAME_PARTS:
785
- if name.endswith("_" + suffix):
786
- name = name[: -(len(suffix) + 1)]
787
- 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
788
732
 
789
- for subgraph in cg.traverse_nested_graphs(graph):
790
- subgraph.name = _clean_graph_name(subgraph.name)
733
+ for subgraph in cg.traverse_nested_graphs(graph):
734
+ subgraph.name = _clean_graph_name(subgraph.name)
791
735
 
792
- subgraph_names = {
793
- id(subgraph): subgraph.name for subgraph in cg.traverse_nested_graphs(graph)
794
- }
795
- subgraph_names = identifiers.dedup_names_with_suffix(subgraph_names, separator="_")
796
-
797
- scope_expressions = subgraph_names.copy()
798
- for k, v in func_resolution.items():
799
- if isinstance(v, OperatorType):
800
- scope_expressions[id(k)] = OPERATOR_TEMPLATES[v]
801
- else:
802
- scope_expressions[id(k)] = v
803
-
804
- lines_for_modules = []
805
- for subgraph in targets:
806
- func_name = subgraph_names[id(subgraph)]
807
- result = _codegen_for_graph(
808
- subgraph,
809
- scope_expressions=scope_expressions.copy(),
810
- as_maincall=(subgraph is graph and toplevel_as_maincall),
811
- add_version_comment=add_version_comment,
812
- add_line_comments=add_line_comments,
813
- 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="_"
814
741
  )
815
- lines_for_modules.append((subgraph_names[id(subgraph)], result))
816
742
 
817
- 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)
818
764
 
819
765
  return OrderedDict(lines_for_modules)
820
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
 
@@ -0,0 +1,121 @@
1
+ import dataclasses
2
+ import enum
3
+ import logging
4
+ import math
5
+ from pathlib import Path
6
+ from typing import Any, Union, get_args, get_origin
7
+
8
+ import numpy as np
9
+
10
+ import procfunc as pf
11
+ from procfunc import compute_graph as cg
12
+ from procfunc.nodes import types as nt
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def repr_type(x: Any) -> str:
18
+ # TODO: make the user pass in special resolutions for types, or else we will just do verbose types
19
+
20
+ if isinstance(x, str):
21
+ return x
22
+
23
+ if x.__name__ == "NoneType":
24
+ return "None"
25
+
26
+ origin = get_origin(x)
27
+ args = get_args(x)
28
+
29
+ if x.__name__ == "ProcNode":
30
+ if len(args) == 1:
31
+ return f"pf.ProcNode[{repr_type(args[0])}]"
32
+ elif len(args) == 0:
33
+ return "pf.ProcNode"
34
+ else:
35
+ raise ValueError(f"Unsupported ProcNode type: {x} {args=}")
36
+
37
+ if hasattr(pf, x.__name__):
38
+ if len(args):
39
+ raise ValueError(f"procfunc type had unhandled annotations: {x} {args=}")
40
+ return f"pf.{x.__name__}"
41
+
42
+ if x.__module__ == "builtins":
43
+ return x.__name__
44
+
45
+ origin = get_origin(x)
46
+ args = get_args(x)
47
+
48
+ if origin is Union:
49
+ args_0 = get_args(args[0])
50
+ if get_origin(args[0]) is nt.ProcNode and args_0[0] is args[1]:
51
+ return f"t.SocketOrVal[{repr_type(args_0[0])}]"
52
+ else:
53
+ return " | ".join([repr_type(a) for a in args])
54
+
55
+ if getattr(x, "__module__", None) == "procfunc.nodes.types":
56
+ return f"t.{x.__name__}"
57
+
58
+ return x.__name__
59
+
60
+
61
+ def repr_float(value: float) -> str:
62
+ if not math.isfinite(value):
63
+ return f'float("{value}")'
64
+ # float32 socket values: shortest exact round-trip (round(x, 8) would destroy small magnitudes)
65
+ return str(np.float32(value))
66
+
67
+
68
+ def repr_value(value: Any) -> str:
69
+ if hasattr(value, "__wrapped__"):
70
+ value = value.__wrapped__
71
+
72
+ if isinstance(value, cg.Proxy):
73
+ logger.warning(
74
+ f"Proxy object {value} should never appear as a raw value in codegen - "
75
+ f"its underlying node {value.node} was not resolved to a variable"
76
+ )
77
+ if isinstance(value, nt.ProcNode):
78
+ logger.warning(
79
+ f"Procnode object {value} should never be treated as a raw value in codegen"
80
+ )
81
+
82
+ if isinstance(value, np.random.Generator):
83
+ return "np.random.default_rng()"
84
+ elif isinstance(value, type):
85
+ return repr_type(value)
86
+ elif isinstance(value, np.ndarray):
87
+ # tolist + per-element repr is exact, unlike repr(value) which truncates
88
+ # to numpy printoptions precision
89
+ body = (
90
+ repr_value(value.tolist())
91
+ if value.dtype == np.float32
92
+ else repr(value.tolist())
93
+ )
94
+ return f"np.array({body}, dtype=np.{value.dtype.name})"
95
+ elif isinstance(value, np.dtype):
96
+ return f"np.dtype('{value}')"
97
+ elif isinstance(value, pf.Matrix):
98
+ # matrix constants travel as numpy arrays; stray Matrix values delegate
99
+ return repr_value(np.array(value, dtype=np.float32))
100
+ elif isinstance(value, (pf.Color, pf.Vector, pf.Euler, pf.Quaternion)):
101
+ comps = ", ".join(repr_float(c) for c in value)
102
+ return f"pf.{value.__class__.__name__}(({comps}))"
103
+ elif isinstance(value, enum.Enum):
104
+ return f"{type(value).__name__}.{value.name}"
105
+ elif isinstance(value, Path):
106
+ return f"Path({str(value)!r})"
107
+ elif dataclasses.is_dataclass(value) and not isinstance(value, type):
108
+ args_str = ", ".join(
109
+ f"{f.name}={repr_value(getattr(value, f.name))}"
110
+ for f in dataclasses.fields(value)
111
+ )
112
+ return f"{type(value).__name__}({args_str})"
113
+ elif isinstance(value, list):
114
+ return f"[{', '.join([repr_value(x) for x in value])}]"
115
+ elif isinstance(value, tuple):
116
+ inner = ", ".join(repr_value(x) for x in value)
117
+ return f"({inner},)" if len(value) == 1 else f"({inner})"
118
+ elif isinstance(value, float) and not isinstance(value, bool):
119
+ return repr_float(value)
120
+ else:
121
+ return repr(value)
@@ -26,3 +26,29 @@ from .util import (
26
26
  traverse_nested_graphs,
27
27
  usages_per_node,
28
28
  )
29
+
30
+ __all__ = [
31
+ "ComputeGraph",
32
+ "ConstantNode",
33
+ "FunctionCallNode",
34
+ "GetAttributeNode",
35
+ "InputPlaceholderNode",
36
+ "MethodCallNode",
37
+ "MutatedArgumentNode",
38
+ "Node",
39
+ "ProceduralNode",
40
+ "SubgraphCallNode",
41
+ "normalize_args_to_kwargs",
42
+ "OperatorType",
43
+ "AttributeProxy",
44
+ "Proxy",
45
+ "LiteralConstant",
46
+ "graph_nodes_equal",
47
+ "transform_compute_graph",
48
+ "transform_nodetree",
49
+ "traverse_breadth_first",
50
+ "traverse_depth_first",
51
+ "traverse_depth_first_node",
52
+ "traverse_nested_graphs",
53
+ "usages_per_node",
54
+ ]
@@ -0,0 +1,32 @@
1
+ import logging
2
+ from dataclasses import dataclass
3
+ from typing import Any
4
+
5
+ from procfunc.util.pytree import PyTree
6
+
7
+ from .node import Node
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ @dataclass
13
+ class ComputeGraph:
14
+ inputs: PyTree[Any, Node]
15
+ outputs: PyTree[Any, Node]
16
+ name: str
17
+ metadata: dict[str, Any]
18
+
19
+ def __repr__(self):
20
+ return f"{self.__class__.__name__}({self.name!r})"
21
+
22
+ def __call__(
23
+ self,
24
+ *args,
25
+ **kwargs,
26
+ ):
27
+ """
28
+ Execute the compute graph. If this graph came from a tracer,
29
+ this should be exactly equivelant to executing the original python function.
30
+ """
31
+
32
+ raise NotImplementedError("Not implemented")