Skip to content

Commit 4f97ab0

Browse files
committed
feat: minor improvements
1 parent 13a0041 commit 4f97ab0

4 files changed

Lines changed: 41 additions & 19 deletions

File tree

openhexa/sdk/pipelines/parameter/ast_constructible.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@ class AstConstructible:
88
"""Mixin that enables reconstruction of a class instance from an AST Call node.
99
1010
Any class whose ``__init__`` takes only scalar (``ast.Constant``) arguments
11-
can inherit from this mixin to gain automatic ``from_ast_call`` support in
12-
the pipeline AST parser — no changes to the parser needed when new parameters
13-
are added to the class.
11+
can inherit from this mixin and get ``from_ast_call`` for free. Adding or
12+
renaming ``__init__`` parameters does *not* require touching the parser.
13+
14+
To make the AST parser recognise a new subclass by name, add one entry to
15+
``_AST_CALLABLE_TYPES`` in ``runtime.py`` (and ensure the subclass module is
16+
imported there). Auto-registration via ``__init_subclass__`` would not remove
17+
that requirement — the registry entry only exists after the module is imported,
18+
so an explicit import would still be needed.
1419
"""
1520

1621
@classmethod
@@ -21,10 +26,19 @@ def from_ast_call(cls, node: ast.Call) -> "AstConstructible":
2126
``inspect.signature``, then merges keyword args, and calls ``cls``.
2227
"""
2328
param_names = list(inspect.signature(cls).parameters.keys())
24-
kwargs = {
25-
param_names[i]: arg.value
26-
for i, arg in enumerate(node.args)
27-
if isinstance(arg, ast.Constant) and i < len(param_names)
28-
}
29-
kwargs |= {kw.arg: kw.value.value for kw in node.keywords if isinstance(kw.value, ast.Constant)}
29+
kwargs = {}
30+
for i, arg in enumerate(node.args):
31+
if i >= len(param_names):
32+
break
33+
if not isinstance(arg, ast.Constant):
34+
raise ValueError(
35+
f"{cls.__name__}() positional argument {i + 1} must be a literal value, not a dynamic expression."
36+
)
37+
kwargs[param_names[i]] = arg.value
38+
for kw in node.keywords:
39+
if not isinstance(kw.value, ast.Constant):
40+
raise ValueError(
41+
f"{cls.__name__}() keyword argument '{kw.arg}' must be a literal value, not a dynamic expression."
42+
)
43+
kwargs[kw.arg] = kw.value.value
3044
return cls(**kwargs)

openhexa/sdk/pipelines/parameter/choices.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,11 @@ class ChoicesFromFile(AstConstructible):
2626
def __init__(self, path: str, column: str | None = None):
2727
self.path = path
2828
self.column = column
29-
self.format = self._detect_format(path)
3029
self.validate_spec()
30+
self.format = self._detect_format(path)
3131

3232
@staticmethod
3333
def _detect_format(path: str) -> str:
34-
if not path or not isinstance(path, str):
35-
raise InvalidParameterError("ChoicesFromFile path must be a non-empty string.")
3634
ext = path.rsplit(".", 1)[-1].lower() if "." in path else ""
3735
if ext not in _SUPPORTED_FORMATS:
3836
raise InvalidParameterError(
@@ -48,6 +46,19 @@ def validate_spec(self):
4846
if self.column is not None and not isinstance(self.column, str):
4947
raise InvalidParameterError("ChoicesFromFile column must be a string.")
5048

49+
def __repr__(self) -> str:
50+
if self.column is not None:
51+
return f"ChoicesFromFile({self.path!r}, column={self.column!r})"
52+
return f"ChoicesFromFile({self.path!r})"
53+
54+
def __eq__(self, other: object) -> bool:
55+
if not isinstance(other, ChoicesFromFile):
56+
return NotImplemented
57+
return self.path == other.path and self.column == other.column
58+
59+
def __hash__(self) -> int:
60+
return hash((self.path, self.column))
61+
5162
def to_dict(self) -> dict:
5263
"""Return a dictionary representation of the choices spec."""
5364
return {

openhexa/sdk/pipelines/parameter/decorator.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,7 @@ def __init__(
6969
raise InvalidParameterError(f"Parameters of type {self.type} don't accept choices.")
7070
if isinstance(choices, str):
7171
choices = ChoicesFromFile(choices)
72-
if isinstance(choices, ChoicesFromFile):
73-
# validate_spec() already ran in ChoicesFromFile.__init__; nothing more to check here
74-
pass
75-
else:
72+
elif not isinstance(choices, ChoicesFromFile):
7673
if len(choices) == 0:
7774
raise InvalidParameterError("Choices, if provided, cannot be empty.")
7875
try:

openhexa/sdk/pipelines/runtime.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@
2323
Parameter,
2424
validate_parameters,
2525
)
26+
from openhexa.sdk.utils import Settings
27+
28+
from .pipeline import Pipeline
2629

2730
# Maps AST function names to classes that support from_ast_call().
2831
# Add an entry here when introducing a new AstConstructible type.
2932
_AST_CALLABLE_TYPES: dict[str, type] = {
3033
"ChoicesFromFile": ChoicesFromFile,
3134
}
32-
from openhexa.sdk.utils import Settings
33-
34-
from .pipeline import Pipeline
3535

3636

3737
@dataclass

0 commit comments

Comments
 (0)