pipelime-python 2.3.0__tar.gz → 3.0.1__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 (121) hide show
  1. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/PKG-INFO +6 -2
  2. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/README.md +2 -0
  3. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/__init__.py +1 -1
  4. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/choixe/ast/nodes.py +41 -3
  5. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/choixe/utils/common.py +35 -1
  6. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/choixe/visitors/decoder.py +5 -2
  7. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/choixe/visitors/inspector.py +8 -5
  8. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/choixe/visitors/processor.py +19 -8
  9. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/choixe/xconfig.py +18 -3
  10. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/cli/main.py +16 -8
  11. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/cli/pretty_print.py +72 -77
  12. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/cli/tui/tui.py +3 -2
  13. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/cli/tui/utils.py +27 -24
  14. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/cli/utils.py +64 -37
  15. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/commands/general.py +56 -53
  16. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/commands/interfaces.py +92 -106
  17. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/commands/piper.py +13 -19
  18. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/commands/resume.py +9 -7
  19. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/commands/shell.py +1 -3
  20. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/commands/split_ops.py +15 -28
  21. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/commands/tempman.py +2 -2
  22. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/commands/toy_dataset.py +1 -3
  23. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/__init__.py +1 -0
  24. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/checkpoint.py +5 -3
  25. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/model.py +105 -80
  26. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/parsers/choixe_parser.py +1 -1
  27. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/progress/listener/receiver/zmq.py +1 -1
  28. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/progress/model.py +3 -3
  29. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/progress/tracker/zmq.py +1 -1
  30. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/sequences/grabber.py +3 -2
  31. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/sequences/pipes/base.py +2 -3
  32. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/sequences/pipes/mapping.py +4 -3
  33. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/sequences/pipes/operations.py +7 -14
  34. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/sequences/pipes/validation.py +3 -5
  35. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/sequences/pipes/writers.py +9 -7
  36. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/sequences/sample.py +6 -2
  37. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/sequences/samples_sequence.py +16 -17
  38. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/sequences/sources/from_callable.py +1 -1
  39. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/sequences/sources/raw.py +1 -1
  40. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/sequences/sources/readers.py +22 -13
  41. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/sequences/sources/toy_dataset.py +3 -2
  42. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/sequences/utils.py +3 -8
  43. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/stages/augmentations.py +30 -36
  44. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/stages/base.py +33 -42
  45. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/stages/entities.py +167 -99
  46. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/stages/item_info.py +3 -2
  47. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/stages/item_replacement.py +7 -8
  48. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/stages/item_sources.py +1 -1
  49. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/stages/key_transformations.py +3 -2
  50. pipelime_python-3.0.1/pipelime/utils/pydantic_compat.py +1064 -0
  51. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/utils/pydantic_types.py +186 -220
  52. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pyproject.toml +3 -2
  53. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/.gitignore +0 -0
  54. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/LICENSE +0 -0
  55. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/choixe/__init__.py +0 -0
  56. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/choixe/ast/__init__.py +0 -0
  57. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/choixe/ast/parser.py +0 -0
  58. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/choixe/utils/__init__.py +0 -0
  59. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/choixe/utils/imports.py +0 -0
  60. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/choixe/utils/io.py +0 -0
  61. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/choixe/utils/rand.py +0 -0
  62. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/choixe/visitors/__init__.py +0 -0
  63. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/choixe/visitors/processor_ui.py +0 -0
  64. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/choixe/visitors/unparser.py +0 -0
  65. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/choixe/visitors/walker.py +0 -0
  66. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/cli/__init__.py +0 -0
  67. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/cli/parser.py +0 -0
  68. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/cli/subcommands.py +0 -0
  69. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/cli/tui/__init__.py +0 -0
  70. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/cli/tui/tui.css +0 -0
  71. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/commands/__init__.py +0 -0
  72. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/items/__init__.py +0 -0
  73. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/items/base.py +0 -0
  74. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/items/binary_item.py +0 -0
  75. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/items/image_item.py +0 -0
  76. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/items/metadata_item.py +0 -0
  77. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/items/model3d_item.py +0 -0
  78. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/items/numpy_item.py +0 -0
  79. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/items/pickle_item.py +0 -0
  80. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/drawing/__init__.py +0 -0
  81. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/drawing/base.py +0 -0
  82. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/drawing/factory.py +0 -0
  83. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/drawing/graphviz.py +0 -0
  84. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/drawing/mermaid.py +0 -0
  85. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/executors/__init__.py +0 -0
  86. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/executors/base.py +0 -0
  87. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/executors/factory.py +0 -0
  88. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/executors/naive.py +0 -0
  89. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/graph.py +0 -0
  90. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/parsers/__init__.py +0 -0
  91. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/parsers/base.py +0 -0
  92. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/parsers/factory.py +0 -0
  93. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/progress/__init__.py +0 -0
  94. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/progress/estimator/__init__.py +0 -0
  95. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/progress/estimator/base.py +0 -0
  96. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/progress/estimator/factory.py +0 -0
  97. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/progress/estimator/naive.py +0 -0
  98. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/progress/listener/__init__.py +0 -0
  99. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/progress/listener/base.py +0 -0
  100. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/progress/listener/callbacks/__init__.py +0 -0
  101. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/progress/listener/callbacks/file.py +0 -0
  102. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/progress/listener/callbacks/loguru.py +0 -0
  103. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/progress/listener/callbacks/rich_table.py +0 -0
  104. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/progress/listener/callbacks/tqdm_bars.py +0 -0
  105. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/progress/listener/factory.py +0 -0
  106. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/progress/listener/receiver/__init__.py +0 -0
  107. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/progress/tracker/__init__.py +0 -0
  108. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/progress/tracker/base.py +0 -0
  109. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/progress/tracker/direct.py +0 -0
  110. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/progress/tracker/factory.py +0 -0
  111. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/piper/progress/tracker/loguru.py +0 -0
  112. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/sequences/__init__.py +0 -0
  113. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/sequences/direct_access.py +0 -0
  114. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/sequences/pipes/__init__.py +0 -0
  115. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/sequences/sources/__init__.py +0 -0
  116. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/sequences/torch.py +0 -0
  117. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/stages/__init__.py +0 -0
  118. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/utils/__init__.py +0 -0
  119. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/utils/context_managers.py +0 -0
  120. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/utils/deprecated.py +0 -0
  121. {pipelime_python-2.3.0 → pipelime_python-3.0.1}/pipelime/utils/inspection.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pipelime-python
3
- Version: 2.3.0
3
+ Version: 3.0.1
4
4
  Summary: Data workflows, cli and dataflow automation.
5
5
  Project-URL: Source, https://github.com/eyecan-ai/pipelime-python
6
6
  Project-URL: Issues, https://github.com/eyecan-ai/pipelime-python/issues
@@ -32,7 +32,8 @@ Requires-Dist: imageio[ffmpeg,pyav]>=2.17.0
32
32
  Requires-Dist: loguru
33
33
  Requires-Dist: networkx
34
34
  Requires-Dist: numpy
35
- Requires-Dist: pydantic<3,>=1.10.17
35
+ Requires-Dist: pydantic-extra-types
36
+ Requires-Dist: pydantic<3,>=2.10
36
37
  Requires-Dist: pydash
37
38
  Requires-Dist: python-box
38
39
  Requires-Dist: pyyaml
@@ -64,6 +65,7 @@ Provides-Extra: tests
64
65
  Requires-Dist: pytest; extra == 'tests'
65
66
  Requires-Dist: pytest-asyncio; extra == 'tests'
66
67
  Requires-Dist: pytest-cov; extra == 'tests'
68
+ Requires-Dist: pytest-xdist; extra == 'tests'
67
69
  Requires-Dist: tox; extra == 'tests'
68
70
  Description-Content-Type: text/markdown
69
71
 
@@ -101,6 +103,8 @@ Install `pipelime` using pip:
101
103
  pip install pipelime-python
102
104
  ```
103
105
 
106
+ pipelime ≥ 3.0 requires pydantic v2 — see [`docs/migration/pydantic_v2.md`](docs/migration/pydantic_v2.md) if you upgrade from 2.x.
107
+
104
108
  To be able to *draw* the dataflow graphs, you need the `draw` variant:
105
109
 
106
110
  ```
@@ -32,6 +32,8 @@ Install `pipelime` using pip:
32
32
  pip install pipelime-python
33
33
  ```
34
34
 
35
+ pipelime ≥ 3.0 requires pydantic v2 — see [`docs/migration/pydantic_v2.md`](docs/migration/pydantic_v2.md) if you upgrade from 2.x.
36
+
35
37
  To be able to *draw* the dataflow graphs, you need the `draw` variant:
36
38
 
37
39
  ```
@@ -2,4 +2,4 @@
2
2
 
3
3
  __author__ = "Eyecan.ai"
4
4
  __email__ = "info@eyecan.ai"
5
- __version__ = "2.3.0"
5
+ __version__ = "3.0.1"
@@ -1,11 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import functools
4
+ import types
5
+ import typing
3
6
  from abc import ABC, abstractmethod
4
- from dataclasses import field
7
+ from dataclasses import dataclass, field, fields
5
8
  from typing import Any, Dict, Optional, Sequence, Tuple, Union
6
9
 
7
- from pydantic.v1.dataclasses import dataclass
8
-
9
10
 
10
11
  class NodeVisitor: # pragma: no cover
11
12
  """Base class for all Choixe visitors. A visitor can be seen as a special recursive
@@ -87,10 +88,47 @@ class NodeVisitor: # pragma: no cover
87
88
  return self._ignore(node)
88
89
 
89
90
 
91
+ @functools.lru_cache(maxsize=None)
92
+ def _node_typed_fields(cls: type) -> Tuple[Tuple[str, Tuple[type, ...], bool], ...]:
93
+ """For each dataclass field of `cls` whose resolved annotation is a `Node`
94
+ subclass, `Optional[<Node subclass>]`, or a `Union` of `Node` subclasses,
95
+ returns `(field_name, allowed_node_types, allow_none)`. Container-typed
96
+ fields (e.g. `Dict[...]`, `Sequence[...]`) and `Any` are left out. Cached
97
+ per class (outside any instance, since `HashNode.__hash__` hashes
98
+ `self.__dict__.values()`)."""
99
+ hints = typing.get_type_hints(cls)
100
+ specs = []
101
+ for f in fields(cls):
102
+ hint = hints.get(f.name)
103
+ is_union = typing.get_origin(hint) is Union or isinstance(hint, types.UnionType)
104
+ candidates = typing.get_args(hint) if is_union else (hint,)
105
+ node_types = tuple(
106
+ c for c in candidates if isinstance(c, type) and issubclass(c, Node)
107
+ )
108
+ if node_types:
109
+ specs.append((f.name, node_types, type(None) in candidates))
110
+ return tuple(specs)
111
+
112
+
90
113
  @dataclass
91
114
  class Node(ABC):
92
115
  """A generic element of the Choixe AST, all nodes must implement this interface."""
93
116
 
117
+ def __post_init__(self) -> None:
118
+ for name, node_types, allow_none in _node_typed_fields(type(self)):
119
+ value = getattr(self, name, None)
120
+ if value is None and allow_none:
121
+ continue
122
+ if isinstance(value, node_types):
123
+ continue
124
+ expected = " or ".join(t.__name__ for t in node_types)
125
+ if allow_none:
126
+ expected += " or None"
127
+ raise TypeError(
128
+ f"{type(self).__name__}.{name} expected {expected}, "
129
+ f"got {type(value).__name__}"
130
+ )
131
+
94
132
  @abstractmethod
95
133
  def accept(self, visitor: NodeVisitor) -> Any:
96
134
  """Accepts an incoming visitor. This method should simply call the respective
@@ -1,5 +1,39 @@
1
+ import re
1
2
  import typing as t
2
3
 
4
+ # pydash's string path delimiter (the same in pydash 8.0 and 8.1): an unescaped `.`,
5
+ # or a `[<int>]` list index (captured, so that it is kept as a path part)
6
+ _PYDASH_PATH_DELIM = re.compile(r"(?<!\\)(?:\\\\)*\.|(\[-?\d+\])")
7
+
8
+
9
+ def pydash_path(key_path: t.Any) -> t.Any:
10
+ r"""A key path for pydash (``get``, ``set_``, ``has``, ``to_path``) addressing the
11
+ same value on every pydash version.
12
+
13
+ pydash 8.1 reads the empty key of a leading, trailing or doubled ``.`` in a string
14
+ path (``".a"`` is ``["", "a"]``, ``"."`` is ``["", ""]``), where earlier versions
15
+ dropped it (``["a"]``, ``[]``: the whole object). pipelime keeps the pre-8.1 meaning:
16
+ the empty keys of a dotted or indexed string path are dropped here, and the other
17
+ parts, escapes included, are joined back into a canonical path (``"a.b[0].c"``) that
18
+ every pydash version splits the same way. A path left with a single key is returned
19
+ unescaped (pydash takes a plain key as is), with no key as ``[]``. Any other path (a
20
+ plain key without ``.`` or ``[``, a key list, an int) is returned unchanged.
21
+ """
22
+ if not (isinstance(key_path, str) and ("." in key_path or "[" in key_path)):
23
+ return key_path
24
+ # `re.split` alternates the text between two delimiters (a key, "" if empty) and
25
+ # the captured group (a `[<int>]` list index, None for a `.`)
26
+ parts = _PYDASH_PATH_DELIM.split(key_path)
27
+ path = ""
28
+ for i, part in enumerate(parts):
29
+ if part:
30
+ path += part if i % 2 or not path else "." + part
31
+ if not path:
32
+ return []
33
+ if "." in path or "[" in path:
34
+ return path
35
+ return path.replace("\\\\", "\\") # pydash's unescape; no `\.` is left here
36
+
3
37
 
4
38
  class _RefItem:
5
39
  def __init__(self, parent, idx):
@@ -73,7 +107,7 @@ def deep_set_( # noqa: C901
73
107
  from pydash.utilities import to_path
74
108
 
75
109
  if key_path is not None: # pragma: no branch
76
- key_path_tokens = to_path(key_path)
110
+ key_path_tokens = to_path(pydash_path(key_path))
77
111
  parent_node = _RefItem(obj, None)
78
112
  for tk in key_path_tokens:
79
113
  if isinstance(tk, int):
@@ -3,7 +3,7 @@ from pathlib import Path
3
3
  from typing import Any
4
4
 
5
5
  import numpy as np
6
- from pydantic.v1 import BaseModel
6
+ from pydantic import BaseModel
7
7
 
8
8
  from pipelime.choixe.ast.nodes import LiteralNode, Node
9
9
  from pipelime.choixe.visitors.unparser import Unparser
@@ -20,7 +20,10 @@ class Decoder(Unparser):
20
20
  return data.item()
21
21
  elif isinstance(data, BaseModel):
22
22
  symbol = f"{data.__module__}.{data.__class__.__qualname__}"
23
- return {"$model": symbol, "$args": json.loads(data.json(by_alias=True))}
23
+ return {
24
+ "$model": symbol,
25
+ "$args": json.loads(data.model_dump_json(by_alias=True)),
26
+ }
24
27
  elif isinstance(data, Path):
25
28
  return str(data)
26
29
  else:
@@ -10,6 +10,7 @@ import pydash as py_
10
10
 
11
11
  import pipelime.choixe.ast.nodes as ast
12
12
  from pipelime.choixe.ast.parser import parse
13
+ from pipelime.choixe.utils.common import pydash_path
13
14
  from pipelime.choixe.utils.io import load
14
15
 
15
16
 
@@ -87,11 +88,11 @@ class Inspector(ast.NodeVisitor):
87
88
 
88
89
  id_ = node.identifier.data # type: ignore
89
90
  default = None if node.default is None else unparse(node.default)
90
- variables = py_.set_({}, id_, default)
91
+ variables = py_.set_({}, pydash_path(id_), default)
91
92
 
92
93
  help_strings = {}
93
94
  if node.help is not None:
94
- help_strings = py_.set_({}, id_, node.help.data)
95
+ help_strings = py_.set_({}, pydash_path(id_), node.help.data)
95
96
 
96
97
  environ = {}
97
98
  if node.env is not None and node.env.data: # type: ignore
@@ -150,7 +151,7 @@ class Inspector(ast.NodeVisitor):
150
151
  if node.identifier is not None: # pragma: no branch
151
152
  self._named_for_loops[node.identifier.data] = node.iterable.data
152
153
  iterable_insp = (
153
- Inspection(variables=py_.set_({}, node.iterable.data, None))
154
+ Inspection(variables=py_.set_({}, pydash_path(node.iterable.data), None))
154
155
  if isinstance(node.iterable.data, str)
155
156
  else Inspection(processed=True)
156
157
  )
@@ -160,7 +161,9 @@ class Inspector(ast.NodeVisitor):
160
161
  def visit_switch(self, node: ast.SwitchNode) -> Inspection:
161
162
  insp = node.value.accept(self)
162
163
  if insp.processed: # pragma: no branch
163
- insp = Inspection(variables=py_.set_({}, node.value.data, None)) # type: ignore
164
+ insp = Inspection(
165
+ variables=py_.set_({}, pydash_path(node.value.data), None) # type: ignore
166
+ )
164
167
  cases_insp = sum(
165
168
  [x.accept(self) + y.accept(self) for x, y in node.cases],
166
169
  start=Inspection(processed=True),
@@ -186,7 +189,7 @@ class Inspector(ast.NodeVisitor):
186
189
  if key:
187
190
  iterable_name = self._named_for_loops[loop_id]
188
191
  full_path = f"{iterable_name}.{key}"
189
- py_.set_(variables, full_path, None)
192
+ py_.set_(variables, pydash_path(full_path), None)
190
193
  insp = insp + sub_insp + Inspection(variables=variables)
191
194
  return insp
192
195
 
@@ -11,6 +11,7 @@ import pydash as py_
11
11
 
12
12
  import pipelime.choixe.ast.nodes as ast
13
13
  from pipelime.choixe.ast.parser import parse
14
+ from pipelime.choixe.utils.common import pydash_path
14
15
  from pipelime.choixe.utils.imports import import_symbol
15
16
  from pipelime.choixe.utils.io import PipelimeTmp, load
16
17
  from pipelime.choixe.utils.rand import rand
@@ -148,8 +149,8 @@ class Processor(ast.NodeVisitor):
148
149
  var_value = None
149
150
  found = False
150
151
 
151
- if py_.has(self._context, id_):
152
- var_value = py_.get(self._context, id_)
152
+ if py_.has(self._context, pydash_path(id_)):
153
+ var_value = py_.get(self._context, pydash_path(id_))
153
154
  found = True
154
155
  if not found and env:
155
156
  value = os.getenv(id_)
@@ -214,14 +215,24 @@ class Processor(ast.NodeVisitor):
214
215
  symbol_branches = node.symbol.accept(self)
215
216
  args_branches = node.args.accept(self)
216
217
  branches = self._branches(symbol_branches, args_branches)
217
- return [import_symbol(s, cwd=self._cwd).parse_obj(a) for s, a in branches]
218
+ models = []
219
+ for s, a in branches:
220
+ model_cls = import_symbol(s, cwd=self._cwd)
221
+ if not hasattr(model_cls, "model_validate"):
222
+ raise TypeError(
223
+ f"`$model` needs a pydantic v2 model, got {model_cls!r}: "
224
+ "pipelime 3 no longer supports pydantic.v1 models "
225
+ "(see docs/migration/pydantic_v2.md)"
226
+ )
227
+ models.append(model_cls.model_validate(a))
228
+ return models
218
229
 
219
230
  def visit_for(self, node: ast.ForNode) -> List[Any]:
220
231
  if isinstance(node.iterable.data, str):
221
- if not py_.has(self._context, node.iterable.data):
232
+ if not py_.has(self._context, pydash_path(node.iterable.data)):
222
233
  iterable = self._handle_missing_for(node.iterable.data)
223
234
  else:
224
- iterable = py_.get(self._context, node.iterable.data)
235
+ iterable = py_.get(self._context, pydash_path(node.iterable.data))
225
236
  else:
226
237
  iterable = node.iterable.data
227
238
 
@@ -278,10 +289,10 @@ class Processor(ast.NodeVisitor):
278
289
  for branch in all_branches:
279
290
  varname = branch[0]
280
291
 
281
- if not py_.has(self._context, varname):
292
+ if not py_.has(self._context, pydash_path(varname)):
282
293
  value = self._handle_missing_switch(varname)
283
294
  else:
284
- value = py_.get(self._context, varname)
295
+ value = py_.get(self._context, pydash_path(varname))
285
296
 
286
297
  # Match the value to the correct case
287
298
  for i in range(len(node.cases)):
@@ -321,7 +332,7 @@ class Processor(ast.NodeVisitor):
321
332
  items = []
322
333
  for branch in branches:
323
334
  loop_id, _, key = str(branch).partition(sep) # type: ignore
324
- item = py_.get(self._loop_data[loop_id].item, f"{sep}{key}")
335
+ item = py_.get(self._loop_data[loop_id].item, pydash_path(f"{sep}{key}"))
325
336
  items.append(item)
326
337
  return items
327
338
 
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from copy import deepcopy
3
4
  from pathlib import Path
4
5
  from typing import Any, Dict, List, Mapping, Optional, Tuple, Union
5
6
 
@@ -9,7 +10,7 @@ from schema import Schema
9
10
 
10
11
  from pipelime.choixe.ast.nodes import Node
11
12
  from pipelime.choixe.ast.parser import parse
12
- from pipelime.choixe.utils.common import deep_set_
13
+ from pipelime.choixe.utils.common import deep_set_, pydash_path
13
14
  from pipelime.choixe.utils.io import dump, load
14
15
  from pipelime.choixe.visitors import Inspection, decode, inspect, process, walk
15
16
 
@@ -38,6 +39,11 @@ class XConfig(Box):
38
39
  validation. Defaults to None.
39
40
  """
40
41
 
42
+ # Initialize the box config. Nested containers must be plain `Box` objects:
43
+ # `XConfig.__init__` does not accept box options, and recent python-box
44
+ # versions would otherwise default `box_class` to the subclass itself.
45
+ super().__init__(box_class=Box)
46
+
41
47
  # options
42
48
  self._cwd = cwd
43
49
  self._schema = None
@@ -93,6 +99,15 @@ class XConfig(Box):
93
99
  """
94
100
  return XConfig(data=self.decode(), cwd=self.get_cwd(), schema=self.get_schema())
95
101
 
102
+ def __deepcopy__(self, memo: Optional[Dict[int, Any]] = None) -> XConfig:
103
+ # `Box.__deepcopy__` would build the copy passing box options to
104
+ # `XConfig.__init__` (or return a plain `Box`, on older python-box versions)
105
+ return XConfig(
106
+ data=deepcopy(self.to_dict(), memo),
107
+ cwd=self.get_cwd(),
108
+ schema=self.get_schema(),
109
+ )
110
+
96
111
  def validate(self, replace: bool = True):
97
112
  """Validate internal schema, if any
98
113
 
@@ -139,7 +154,7 @@ class XConfig(Box):
139
154
  Returns:
140
155
  Any: The value at the specified path.
141
156
  """
142
- return py_.get(self, full_key, default=default)
157
+ return py_.get(self, pydash_path(full_key), default=default)
143
158
 
144
159
  def deep_set(
145
160
  self,
@@ -159,7 +174,7 @@ class XConfig(Box):
159
174
  Defaults to False.
160
175
  """
161
176
 
162
- if not only_valid_keys or py_.has(self, full_key):
177
+ if not only_valid_keys or py_.has(self, pydash_path(full_key)):
163
178
  deep_set_(self, key_path=full_key, value=value, append=append_values)
164
179
 
165
180
  def deep_update(
@@ -6,7 +6,6 @@ import typing as t
6
6
  from pathlib import Path
7
7
 
8
8
  import typer
9
- from pydantic.v1 import BaseModel
10
9
 
11
10
  from pipelime.choixe import XConfig
12
11
  from pipelime.choixe.visitors.processor_ui import ProcessorUi
@@ -18,12 +17,15 @@ from pipelime.cli.utils import (
18
17
  print_command_op_stage_info,
19
18
  print_commands_ops_stages_list,
20
19
  )
20
+ from pipelime.utils.pydantic_compat import PipelimeModel
21
21
 
22
22
  if t.TYPE_CHECKING:
23
23
  from pipelime.piper.checkpoint import Checkpoint
24
24
 
25
25
 
26
- class PlCliOptions(BaseModel):
26
+ class PlCliOptions(PipelimeModel):
27
+ # a `PipelimeModel` for the v1 Optional rule: the options saved in a checkpoint
28
+ # may lack an `Optional` key
27
29
  _namespace: t.ClassVar[str] = "__plmain"
28
30
 
29
31
  config: t.List[Path]
@@ -43,7 +45,7 @@ class PlCliOptions(BaseModel):
43
45
  pipelime_tmp: t.Optional[str]
44
46
 
45
47
  def purged_dict(self):
46
- return self._purge(self.dict())
48
+ return self._purge(self.model_dump())
47
49
 
48
50
  def _purge(self, data):
49
51
  if isinstance(data, Path):
@@ -918,14 +920,19 @@ def run_command(
918
920
 
919
921
  import time
920
922
 
921
- from pydantic.v1.error_wrappers import ValidationError
923
+ from pydantic import ValidationError
924
+ from rich.markup import escape
922
925
 
923
926
  from pipelime.choixe.utils.io import PipelimeTmp, dump
924
- from pipelime.cli.pretty_print import print_command_outputs, print_info
927
+ from pipelime.cli.pretty_print import (
928
+ print_command_outputs,
929
+ print_error,
930
+ print_info,
931
+ )
925
932
  from pipelime.cli.tui import TuiApp, is_tui_needed
926
933
  from pipelime.cli.utils import (
934
+ format_validation_error,
927
935
  get_pipelime_command_cls,
928
- show_field_alias_valerr,
929
936
  time_to_str,
930
937
  )
931
938
  from pipelime.commands import TempCommand
@@ -972,12 +979,13 @@ def run_command(
972
979
  # so let's show the tui again if it was needed in the first place
973
980
 
974
981
  except ValidationError as e:
975
- show_field_alias_valerr(e)
982
+ # plain text, not Rich markup: `[type=...]` and bracketed values must survive
983
+ print_error(escape(format_validation_error(e, cmd_cls)))
976
984
  raise e
977
985
 
978
986
  if verbose > 0:
979
987
  print_info(f"\nCreated command `{command}`:")
980
- print_info(cmd_obj.dict(), pretty=True)
988
+ print_info(cmd_obj.model_dump(), pretty=True)
981
989
 
982
990
  if dry_run or verbose > 0:
983
991
  print_info(f"\nRunning `{command}`...")