procfunc 0.30.0__py3-none-any.whl
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.
- procfunc/__init__.py +87 -0
- procfunc/color.py +57 -0
- procfunc/compute_graph/__init__.py +28 -0
- procfunc/compute_graph/compute_graph.py +115 -0
- procfunc/compute_graph/node.py +200 -0
- procfunc/compute_graph/operators_info.py +92 -0
- procfunc/compute_graph/proxy.py +173 -0
- procfunc/compute_graph/util.py +282 -0
- procfunc/context.py +115 -0
- procfunc/control.py +174 -0
- procfunc/nodes/__init__.py +66 -0
- procfunc/nodes/bindings_util.py +196 -0
- procfunc/nodes/bpy_node_info.py +280 -0
- procfunc/nodes/compositor.py +2242 -0
- procfunc/nodes/execute/construct_nodes.py +571 -0
- procfunc/nodes/execute/construct_special_cases.py +246 -0
- procfunc/nodes/execute/execute.py +548 -0
- procfunc/nodes/execute/infer_runtime_data_type.py +195 -0
- procfunc/nodes/execute/util.py +247 -0
- procfunc/nodes/func.py +1417 -0
- procfunc/nodes/geo.py +4240 -0
- procfunc/nodes/manifest.json +8769 -0
- procfunc/nodes/math.py +644 -0
- procfunc/nodes/node_function.py +160 -0
- procfunc/nodes/shader.py +2359 -0
- procfunc/nodes/types.py +347 -0
- procfunc/ops/__init__.py +35 -0
- procfunc/ops/_util.py +275 -0
- procfunc/ops/addons.py +59 -0
- procfunc/ops/attr.py +426 -0
- procfunc/ops/collection.py +90 -0
- procfunc/ops/curve.py +18 -0
- procfunc/ops/file.py +126 -0
- procfunc/ops/manifest.json +39149 -0
- procfunc/ops/mesh.py +1510 -0
- procfunc/ops/modifier.py +603 -0
- procfunc/ops/object.py +258 -0
- procfunc/ops/primitives/__init__.py +31 -0
- procfunc/ops/primitives/camera.py +45 -0
- procfunc/ops/primitives/curve.py +71 -0
- procfunc/ops/primitives/light.py +114 -0
- procfunc/ops/primitives/mesh.py +358 -0
- procfunc/ops/uv.py +271 -0
- procfunc/random.py +247 -0
- procfunc/tracer/__init__.py +43 -0
- procfunc/tracer/decorator.py +121 -0
- procfunc/tracer/patch.py +494 -0
- procfunc/tracer/proxy.py +127 -0
- procfunc/tracer/trace.py +222 -0
- procfunc/transforms/__init__.py +49 -0
- procfunc/transforms/cleanup.py +214 -0
- procfunc/transforms/convert.py +20 -0
- procfunc/transforms/distribution.py +191 -0
- procfunc/transforms/extract_materials.py +116 -0
- procfunc/transforms/infer_distribution.py +326 -0
- procfunc/transforms/parameters.py +15 -0
- procfunc/transforms/util.py +35 -0
- procfunc/transpiler/__init__.py +24 -0
- procfunc/transpiler/bpy_to_computegraph.py +1348 -0
- procfunc/transpiler/codegen.py +919 -0
- procfunc/transpiler/identifiers.py +595 -0
- procfunc/transpiler/main.py +299 -0
- procfunc/types.py +380 -0
- procfunc/util/__init__.py +0 -0
- procfunc/util/bpy_info.py +145 -0
- procfunc/util/camera.py +0 -0
- procfunc/util/keyframe.py +70 -0
- procfunc/util/log.py +96 -0
- procfunc/util/manifest.py +121 -0
- procfunc/util/pytree.py +343 -0
- procfunc/util/teardown.py +37 -0
- procfunc-0.30.0.dist-info/METADATA +120 -0
- procfunc-0.30.0.dist-info/RECORD +76 -0
- procfunc-0.30.0.dist-info/WHEEL +5 -0
- procfunc-0.30.0.dist-info/licenses/LICENSE.md +11 -0
- procfunc-0.30.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import uuid
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import bpy
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from procfunc import types as t
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Attribute data type dimensions
|
|
15
|
+
DATATYPE_DIMS = {
|
|
16
|
+
"FLOAT": 1,
|
|
17
|
+
"INT": 1,
|
|
18
|
+
"INT8": 1,
|
|
19
|
+
"FLOAT_VECTOR": 3,
|
|
20
|
+
"FLOAT2": 2,
|
|
21
|
+
"FLOAT_COLOR": 4,
|
|
22
|
+
"BYTE_COLOR": 4,
|
|
23
|
+
"RGBA": 4,
|
|
24
|
+
"BOOLEAN": 1,
|
|
25
|
+
"INT32_2D": 2,
|
|
26
|
+
"QUATERNION": 4,
|
|
27
|
+
"FLOAT4X4": 16,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Attribute data type field names for foreach_get/foreach_set
|
|
31
|
+
DATATYPE_FIELDS = {
|
|
32
|
+
"FLOAT": "value",
|
|
33
|
+
"INT": "value",
|
|
34
|
+
"INT8": "value",
|
|
35
|
+
"FLOAT_VECTOR": "vector",
|
|
36
|
+
"FLOAT2": "vector",
|
|
37
|
+
"FLOAT_COLOR": "color",
|
|
38
|
+
"BYTE_COLOR": "color",
|
|
39
|
+
"RGBA": "color",
|
|
40
|
+
"BOOLEAN": "value",
|
|
41
|
+
"INT32_2D": "value",
|
|
42
|
+
"QUATERNION": "value",
|
|
43
|
+
"FLOAT4X4": "value",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Attribute data type to Python type mapping
|
|
47
|
+
DATATYPE_TO_PYTYPE = {
|
|
48
|
+
"INT": int,
|
|
49
|
+
"FLOAT": np.float32,
|
|
50
|
+
"FLOAT_VECTOR": np.float64,
|
|
51
|
+
"FLOAT_COLOR": np.float32,
|
|
52
|
+
"RGBA": np.float32,
|
|
53
|
+
"BOOLEAN": bool,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Python type to attribute data type mapping
|
|
57
|
+
PYTYPE_DATATYPE_TABLE = [
|
|
58
|
+
(int, "INT"),
|
|
59
|
+
(np.dtype(np.int32), "INT"),
|
|
60
|
+
(float, "FLOAT"),
|
|
61
|
+
(np.dtype(np.float32), "FLOAT"),
|
|
62
|
+
(np.dtype(np.float64), "FLOAT"),
|
|
63
|
+
(bool, "BOOLEAN"),
|
|
64
|
+
(np.dtype(np.bool_), "BOOLEAN"),
|
|
65
|
+
(t.Color, "RGBA"),
|
|
66
|
+
(t.Vector, "FLOAT_VECTOR"),
|
|
67
|
+
(t.Euler, "FLOAT_VECTOR"),
|
|
68
|
+
(t.Quaternion, "FLOAT_VECTOR"),
|
|
69
|
+
(t.Matrix, "FLOAT_VECTOR"),
|
|
70
|
+
]
|
|
71
|
+
PYTYPE_TO_DATATYPE = {k: v for k, v in PYTYPE_DATATYPE_TABLE}
|
|
72
|
+
DATATYPE_TO_PYTYPE = {v: k for k, v in PYTYPE_DATATYPE_TABLE}
|
|
73
|
+
DATATYPE_TO_PYTYPE["FLOAT2"] = np.float32
|
|
74
|
+
DATATYPE_TO_PYTYPE["INT8"] = np.int8
|
|
75
|
+
DATATYPE_TO_PYTYPE["INT32_2D"] = np.int32
|
|
76
|
+
DATATYPE_TO_PYTYPE["BYTE_COLOR"] = np.uint8
|
|
77
|
+
DATATYPE_TO_PYTYPE["QUATERNION"] = np.float32
|
|
78
|
+
DATATYPE_TO_PYTYPE["FLOAT4X4"] = np.float32
|
|
79
|
+
|
|
80
|
+
UNSUPPORTED_DATATYPES = {"STRING"}
|
|
81
|
+
UNSUPPORTED_DOMAINS = {"LAYER"}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class NodeGroupType(Enum):
|
|
85
|
+
GEOMETRY = "GeometryNodeGroup"
|
|
86
|
+
SHADER = "ShaderNodeGroup"
|
|
87
|
+
COMPOSITOR = "CompositorNodeGroup"
|
|
88
|
+
TEXTURE = "TextureNodeGroup"
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def from_str(cls, s: str) -> "NodeGroupType | None":
|
|
92
|
+
try:
|
|
93
|
+
return cls(s)
|
|
94
|
+
except ValueError:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class NodeTreeType(Enum):
|
|
99
|
+
GEOMETRY = "GeometryNodeTree"
|
|
100
|
+
SHADER = "ShaderNodeTree"
|
|
101
|
+
COMPOSITOR = "CompositorNodeTree"
|
|
102
|
+
TEXTURE = "TextureNodeTree"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
NODETREE_TO_NODEGROUP = {
|
|
106
|
+
NodeTreeType.GEOMETRY: NodeGroupType.GEOMETRY,
|
|
107
|
+
NodeTreeType.SHADER: NodeGroupType.SHADER,
|
|
108
|
+
NodeTreeType.COMPOSITOR: NodeGroupType.COMPOSITOR,
|
|
109
|
+
NodeTreeType.TEXTURE: NodeGroupType.TEXTURE,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
NODETREE_TYPE_TO_MAIN_OUTPUT = {
|
|
113
|
+
NodeTreeType.GEOMETRY: "GeometryNodeOutput",
|
|
114
|
+
NodeTreeType.SHADER: "ShaderNodeOutputMaterial",
|
|
115
|
+
NodeTreeType.COMPOSITOR: "CompositorNodeOutput",
|
|
116
|
+
NodeTreeType.TEXTURE: "TextureNodeOutput",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
NODEGROUP_TYPE_TO_INPUT_NODE = {
|
|
120
|
+
NodeGroupType.GEOMETRY: "GeometryNodeInput",
|
|
121
|
+
NodeGroupType.SHADER: "ShaderNodeInput",
|
|
122
|
+
NodeGroupType.COMPOSITOR: "CompositorNodeInput",
|
|
123
|
+
NodeGroupType.TEXTURE: "TextureNodeInput",
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def bpy_nocollide_data_name(
|
|
128
|
+
x: Any,
|
|
129
|
+
bpy_data: bpy.types.bpy_prop_collection,
|
|
130
|
+
retries: int = 30,
|
|
131
|
+
) -> str:
|
|
132
|
+
name = uuid.uuid4().hex[:12]
|
|
133
|
+
if name not in bpy_data:
|
|
134
|
+
return name
|
|
135
|
+
|
|
136
|
+
for i in range(retries):
|
|
137
|
+
newname = f"{name}_{len(bpy_data) + i}"
|
|
138
|
+
if newname not in bpy_data:
|
|
139
|
+
return newname
|
|
140
|
+
else:
|
|
141
|
+
logger.warning(
|
|
142
|
+
f"Could not find a unique name for {x} in {bpy_data} after {retries=}"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return newname
|
procfunc/util/camera.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, Generic, TypeVar
|
|
3
|
+
|
|
4
|
+
INTERPOLATION_TYPES = {
|
|
5
|
+
"LINEAR",
|
|
6
|
+
"BEZIER",
|
|
7
|
+
# TODO more
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Keyframes(Generic[T]):
|
|
16
|
+
"""
|
|
17
|
+
Represents keyframe animation data for a parameter.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
keyframes: List of (frame, value) tuples where value can be any type
|
|
21
|
+
interpolation: Blender interpolation mode ('LINEAR', 'BEZIER', 'CONSTANT', etc.)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
keyframes: list[tuple[int, T]]
|
|
25
|
+
interpolation: str = "LINEAR"
|
|
26
|
+
|
|
27
|
+
def __post_init__(self):
|
|
28
|
+
if not self.keyframes:
|
|
29
|
+
raise ValueError("Keyframes list cannot be empty")
|
|
30
|
+
if self.interpolation not in INTERPOLATION_TYPES:
|
|
31
|
+
raise ValueError(
|
|
32
|
+
f"Invalid interpolation mode: {self.interpolation}, "
|
|
33
|
+
f"current supported are {INTERPOLATION_TYPES}, adding more may be trivial"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def apply_keyframes(
|
|
38
|
+
target: Any,
|
|
39
|
+
data_path_name: str,
|
|
40
|
+
keyframes: Keyframes,
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
Apply keyframe animation to a node socket.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
target_socket: The Blender node socket to animate
|
|
47
|
+
keyframes: Keyframes object containing frame/value pairs and interpolation mode
|
|
48
|
+
"""
|
|
49
|
+
if not hasattr(target, data_path_name):
|
|
50
|
+
raise ValueError(
|
|
51
|
+
f"Cannot apply keyframes to {target} - no {data_path_name} attribute"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
setattr(target, data_path_name, keyframes.keyframes[0][1])
|
|
55
|
+
|
|
56
|
+
# Apply all keyframes
|
|
57
|
+
for frame, value in keyframes.keyframes:
|
|
58
|
+
setattr(target, data_path_name, value)
|
|
59
|
+
target.keyframe_insert(data_path=data_path_name, frame=frame)
|
|
60
|
+
|
|
61
|
+
# Set interpolation mode for all keyframes
|
|
62
|
+
if hasattr(target, "id_data") and hasattr(target.id_data, "animation_data"):
|
|
63
|
+
anim_data = target.id_data.animation_data
|
|
64
|
+
if anim_data and anim_data.action:
|
|
65
|
+
for fcurve in anim_data.action.fcurves:
|
|
66
|
+
# Check if this fcurve corresponds to our socket
|
|
67
|
+
socket_path = target.path_from_id()
|
|
68
|
+
if socket_path and socket_path in fcurve.data_path:
|
|
69
|
+
for keyframe in fcurve.keyframe_points:
|
|
70
|
+
keyframe.interpolation = keyframes.interpolation
|
procfunc/util/log.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Suppress:
|
|
11
|
+
def __enter__(self, logfile=os.devnull):
|
|
12
|
+
open(logfile, "w").close()
|
|
13
|
+
self.old = os.dup(1)
|
|
14
|
+
sys.stdout.flush()
|
|
15
|
+
os.close(1)
|
|
16
|
+
os.open(logfile, os.O_WRONLY)
|
|
17
|
+
self.level = logging.root.manager.disable
|
|
18
|
+
logging.disable(logging.CRITICAL)
|
|
19
|
+
|
|
20
|
+
def __exit__(self, type, value, traceback):
|
|
21
|
+
os.close(1)
|
|
22
|
+
os.dup(self.old)
|
|
23
|
+
os.close(self.old)
|
|
24
|
+
logging.disable(self.level)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LogLevel:
|
|
28
|
+
def __init__(self, logger, level):
|
|
29
|
+
self.logger = logger
|
|
30
|
+
self.level = level
|
|
31
|
+
self.orig_level = None
|
|
32
|
+
|
|
33
|
+
def __enter__(self):
|
|
34
|
+
self.orig_level = self.logger.level
|
|
35
|
+
self.logger.setLevel(self.level)
|
|
36
|
+
|
|
37
|
+
def __exit__(self, *_):
|
|
38
|
+
self.logger.setLevel(self.orig_level)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def clamp_with_log(val, logger, name, level=logging.WARNING, min=None, max=None):
|
|
42
|
+
if min is not None and val < min:
|
|
43
|
+
logger.log(level, f"{name} had {val} but will be clamped to {min=}")
|
|
44
|
+
val = min
|
|
45
|
+
if max is not None and val > max:
|
|
46
|
+
logger.log(level, f"{name} had {val} but will be clamped to {max=}")
|
|
47
|
+
val = max
|
|
48
|
+
return val
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def raise_error_or_warn(
|
|
52
|
+
msg: str,
|
|
53
|
+
mode: Literal["throw", "warn", "ignore"],
|
|
54
|
+
warning_logger: logging.Logger | None,
|
|
55
|
+
error_class: type = ValueError,
|
|
56
|
+
):
|
|
57
|
+
if warning_logger is None:
|
|
58
|
+
warning_logger = logger
|
|
59
|
+
|
|
60
|
+
match mode:
|
|
61
|
+
case "throw":
|
|
62
|
+
raise error_class(msg)
|
|
63
|
+
case "warn":
|
|
64
|
+
warning_logger.warning(msg)
|
|
65
|
+
case "ignore":
|
|
66
|
+
pass
|
|
67
|
+
case _:
|
|
68
|
+
raise ValueError(f"Unknown mode: {mode}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@contextmanager
|
|
72
|
+
def add_exception_context_msg(
|
|
73
|
+
prefix: str = "",
|
|
74
|
+
postfix: str = "",
|
|
75
|
+
unparseable_exception_throw: Literal["throw", "warn", "ignore"] = "throw",
|
|
76
|
+
):
|
|
77
|
+
try:
|
|
78
|
+
yield
|
|
79
|
+
except Exception as e:
|
|
80
|
+
is_arg_msg = (
|
|
81
|
+
isinstance(e.args, tuple) and len(e.args) > 0 and isinstance(e.args[0], str)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
orig_msg = e.args[0] if is_arg_msg else f"{type(e).__name__}: {str(e)}"
|
|
85
|
+
|
|
86
|
+
msg = orig_msg
|
|
87
|
+
if prefix:
|
|
88
|
+
msg = f"{prefix} {msg}"
|
|
89
|
+
if postfix and not msg.endswith(postfix):
|
|
90
|
+
msg = f"{msg} {postfix}"
|
|
91
|
+
|
|
92
|
+
if is_arg_msg:
|
|
93
|
+
e.args = (msg, *e.args[1:])
|
|
94
|
+
raise e from e
|
|
95
|
+
else:
|
|
96
|
+
raise ValueError(msg) from e
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Utilities for reading and filtering manifest CSV files."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def module_path():
|
|
14
|
+
return Path(__file__).parent.parent
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def filter_manifest(
|
|
18
|
+
manifest: pd.DataFrame,
|
|
19
|
+
filter: dict[str, str] | None = None,
|
|
20
|
+
exclude: dict[str, list[str]] | None = None,
|
|
21
|
+
require_nonempty: list[str] | None = None,
|
|
22
|
+
min_entries: int | None = None,
|
|
23
|
+
) -> pd.DataFrame:
|
|
24
|
+
if filter is None:
|
|
25
|
+
filter = {}
|
|
26
|
+
if exclude is None:
|
|
27
|
+
exclude = {}
|
|
28
|
+
if require_nonempty is None:
|
|
29
|
+
require_nonempty = []
|
|
30
|
+
|
|
31
|
+
for k, v in filter.items():
|
|
32
|
+
assert k in manifest.columns, (
|
|
33
|
+
f"Filter {k}={v} did not match any columns in {manifest.columns}"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
before_count = len(manifest)
|
|
37
|
+
manifest = manifest.dropna(subset=[k])
|
|
38
|
+
manifest = manifest[manifest[k] == v]
|
|
39
|
+
after_count = len(manifest)
|
|
40
|
+
logger.debug(
|
|
41
|
+
f"Filter {k}={v}: {before_count} -> {after_count} (dropped {before_count - after_count})"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
for column, patterns in exclude.items():
|
|
45
|
+
if not isinstance(patterns, list):
|
|
46
|
+
patterns = [patterns]
|
|
47
|
+
|
|
48
|
+
before_count = len(manifest)
|
|
49
|
+
if manifest[column].dtype == "object":
|
|
50
|
+
mask = pd.Series([False] * len(manifest), index=manifest.index)
|
|
51
|
+
for pattern in patterns:
|
|
52
|
+
if isinstance(pattern, str):
|
|
53
|
+
mask |= manifest[column].str.contains(pattern, na=False)
|
|
54
|
+
else:
|
|
55
|
+
mask |= manifest[column] == pattern
|
|
56
|
+
else:
|
|
57
|
+
mask = manifest[column].isin(patterns)
|
|
58
|
+
manifest = manifest[~mask]
|
|
59
|
+
after_count = len(manifest)
|
|
60
|
+
logger.debug(
|
|
61
|
+
f"Exclude {column} patterns {patterns}: {before_count} -> {after_count} (dropped {before_count - after_count})"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if require_nonempty:
|
|
65
|
+
before_count = len(manifest)
|
|
66
|
+
manifest = manifest.dropna(subset=require_nonempty)
|
|
67
|
+
after_count = len(manifest)
|
|
68
|
+
logger.debug(
|
|
69
|
+
f"Require nonempty {require_nonempty}: {before_count} -> {after_count} (dropped {before_count - after_count})"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if min_entries is not None and len(manifest) < min_entries:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"Expected at least {min_entries} entries, got {len(manifest)} "
|
|
75
|
+
f"with {filter=} and {exclude=} and {require_nonempty=}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return manifest
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def import_item(name: str) -> Any:
|
|
82
|
+
"""
|
|
83
|
+
Find and import a function or class by its dotted module path.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
name: Dotted module path like "mymodule.generators.objects.funcname"
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
The imported function or class
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
ModuleNotFoundError: If the module or attribute cannot be found
|
|
93
|
+
"""
|
|
94
|
+
*path_parts, item_name = name.split(".")
|
|
95
|
+
try:
|
|
96
|
+
return importlib.import_module("." + item_name, ".".join(path_parts))
|
|
97
|
+
except Exception as e:
|
|
98
|
+
if not isinstance(e, ModuleNotFoundError):
|
|
99
|
+
raise e
|
|
100
|
+
|
|
101
|
+
mod = importlib.import_module(".".join(path_parts))
|
|
102
|
+
try:
|
|
103
|
+
return getattr(mod, item_name)
|
|
104
|
+
except AttributeError as e:
|
|
105
|
+
raise AttributeError(
|
|
106
|
+
f"Attribute {item_name} not found in module {mod.__name__}, {dir(mod)=}"
|
|
107
|
+
) from e
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def import_item_iterative(name: str, from_mod: Any | None = None) -> Callable:
|
|
111
|
+
first, *rest = name.split(".")
|
|
112
|
+
|
|
113
|
+
if len(rest) == 0:
|
|
114
|
+
assert from_mod is not None, f"No module specified for {name}"
|
|
115
|
+
return getattr(from_mod, name)
|
|
116
|
+
elif from_mod is None:
|
|
117
|
+
mod = importlib.import_module(first)
|
|
118
|
+
return import_item_iterative(".".join(rest), mod)
|
|
119
|
+
else:
|
|
120
|
+
mod = getattr(from_mod, first)
|
|
121
|
+
return import_item_iterative(".".join(rest), mod)
|