pipelime-python 2.2.0__py3-none-any.whl → 3.0.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.
Files changed (54) hide show
  1. pipelime/__init__.py +1 -1
  2. pipelime/choixe/ast/nodes.py +41 -3
  3. pipelime/choixe/utils/common.py +35 -1
  4. pipelime/choixe/visitors/decoder.py +5 -2
  5. pipelime/choixe/visitors/inspector.py +8 -5
  6. pipelime/choixe/visitors/processor.py +19 -8
  7. pipelime/choixe/xconfig.py +3 -3
  8. pipelime/cli/main.py +16 -8
  9. pipelime/cli/pretty_print.py +72 -77
  10. pipelime/cli/tui/tui.py +3 -2
  11. pipelime/cli/tui/utils.py +27 -24
  12. pipelime/cli/utils.py +64 -37
  13. pipelime/commands/general.py +56 -53
  14. pipelime/commands/interfaces.py +92 -106
  15. pipelime/commands/piper.py +13 -19
  16. pipelime/commands/resume.py +9 -7
  17. pipelime/commands/shell.py +1 -3
  18. pipelime/commands/split_ops.py +15 -28
  19. pipelime/commands/tempman.py +2 -2
  20. pipelime/commands/toy_dataset.py +1 -3
  21. pipelime/piper/__init__.py +1 -0
  22. pipelime/piper/checkpoint.py +5 -3
  23. pipelime/piper/model.py +105 -80
  24. pipelime/piper/parsers/choixe_parser.py +1 -1
  25. pipelime/piper/progress/listener/receiver/zmq.py +1 -1
  26. pipelime/piper/progress/model.py +3 -3
  27. pipelime/piper/progress/tracker/zmq.py +1 -1
  28. pipelime/sequences/grabber.py +3 -2
  29. pipelime/sequences/pipes/base.py +2 -3
  30. pipelime/sequences/pipes/mapping.py +4 -3
  31. pipelime/sequences/pipes/operations.py +7 -14
  32. pipelime/sequences/pipes/validation.py +3 -5
  33. pipelime/sequences/pipes/writers.py +9 -7
  34. pipelime/sequences/sample.py +6 -2
  35. pipelime/sequences/samples_sequence.py +16 -17
  36. pipelime/sequences/sources/from_callable.py +1 -1
  37. pipelime/sequences/sources/raw.py +1 -1
  38. pipelime/sequences/sources/readers.py +22 -13
  39. pipelime/sequences/sources/toy_dataset.py +3 -2
  40. pipelime/sequences/utils.py +3 -8
  41. pipelime/stages/augmentations.py +30 -36
  42. pipelime/stages/base.py +33 -42
  43. pipelime/stages/entities.py +167 -99
  44. pipelime/stages/item_info.py +3 -2
  45. pipelime/stages/item_replacement.py +7 -8
  46. pipelime/stages/item_sources.py +1 -1
  47. pipelime/stages/key_transformations.py +3 -2
  48. pipelime/utils/pydantic_compat.py +1064 -0
  49. pipelime/utils/pydantic_types.py +186 -220
  50. {pipelime_python-2.2.0.dist-info → pipelime_python-3.0.0.dist-info}/METADATA +9 -4
  51. {pipelime_python-2.2.0.dist-info → pipelime_python-3.0.0.dist-info}/RECORD +54 -53
  52. {pipelime_python-2.2.0.dist-info → pipelime_python-3.0.0.dist-info}/WHEEL +1 -1
  53. {pipelime_python-2.2.0.dist-info → pipelime_python-3.0.0.dist-info}/entry_points.txt +0 -0
  54. {pipelime_python-2.2.0.dist-info → pipelime_python-3.0.0.dist-info}/licenses/LICENSE +0 -0
pipelime/cli/tui/utils.py CHANGED
@@ -1,12 +1,13 @@
1
+ import inspect
1
2
  from enum import Enum
2
3
  from typing import List, Mapping, Tuple, Type, cast
3
4
 
4
- from pydantic.v1 import BaseModel
5
- from pydantic.v1.fields import ModelField
5
+ from pydantic import BaseModel
6
6
 
7
7
  from pipelime.cli.utils import PipelimeSymbolsHelper
8
8
  from pipelime.piper import PipelimeCommand
9
9
  from pipelime.stages import SampleStage, StageInput
10
+ from pipelime.utils.pydantic_compat import FieldView, iter_fields, strip_annotated
10
11
 
11
12
 
12
13
  class TuiField(BaseModel):
@@ -31,9 +32,9 @@ def is_tui_needed(cmd_cls: Type[PipelimeCommand], cmd_args: Mapping) -> bool:
31
32
  Returns:
32
33
  True if the TUI is needed, False otherwise.
33
34
  """
34
- for field in cmd_cls.__fields__.values():
35
+ for field in iter_fields(cmd_cls):
35
36
  name = field.name
36
- alias = field.alias
37
+ alias = field.effective_alias
37
38
  required = field.required
38
39
 
39
40
  if (name not in cmd_args) and (alias not in cmd_args) and required:
@@ -41,7 +42,7 @@ def is_tui_needed(cmd_cls: Type[PipelimeCommand], cmd_args: Mapping) -> bool:
41
42
  return True
42
43
 
43
44
  # if present, check if it's a StageInput
44
- if field.type_ == StageInput:
45
+ if field.inner_type is StageInput:
45
46
  if name in cmd_args:
46
47
  stage_input_args = cmd_args.get(name)
47
48
  else:
@@ -81,9 +82,9 @@ def are_stageinput_args_present(
81
82
  Returns:
82
83
  True if the StageInput required args are present, False otherwise.
83
84
  """
84
- for field in stage_cls.__fields__.values():
85
+ for field in iter_fields(stage_cls):
85
86
  name = field.name
86
- alias = field.alias
87
+ alias = field.effective_alias
87
88
  required = field.required
88
89
 
89
90
  if (name not in stage_args) and (alias not in stage_args) and required:
@@ -93,7 +94,7 @@ def are_stageinput_args_present(
93
94
  return True
94
95
 
95
96
 
96
- def init_tui_field(field: ModelField, args: Mapping) -> TuiField:
97
+ def init_tui_field(field: FieldView, args: Mapping) -> TuiField:
97
98
  """Initialize a TuiField.
98
99
 
99
100
  Args:
@@ -108,10 +109,10 @@ def init_tui_field(field: ModelField, args: Mapping) -> TuiField:
108
109
 
109
110
  if field.name in args:
110
111
  default = str(args[field.name])
111
- elif field.alias in args:
112
- default = str(args[field.alias])
112
+ elif field.effective_alias in args:
113
+ default = str(args[field.effective_alias])
113
114
  else:
114
- field_default = field.get_default()
115
+ field_default = None if field.required else field.default
115
116
 
116
117
  if field_default is not None:
117
118
  if isinstance(field_default, BaseModel):
@@ -124,7 +125,7 @@ def init_tui_field(field: ModelField, args: Mapping) -> TuiField:
124
125
  tui_field = TuiField(
125
126
  simple=True,
126
127
  name=field.name,
127
- description=str(field.field_info.description),
128
+ description=str(field.description),
128
129
  hint=hint,
129
130
  type_=get_field_type(field),
130
131
  value=default,
@@ -132,7 +133,7 @@ def init_tui_field(field: ModelField, args: Mapping) -> TuiField:
132
133
  return tui_field
133
134
 
134
135
 
135
- def init_stageinput_tui_field(field: ModelField, cmd_args: Mapping) -> TuiField:
136
+ def init_stageinput_tui_field(field: FieldView, cmd_args: Mapping) -> TuiField:
136
137
  """Initialize a TuiField for a StageInput.
137
138
 
138
139
  Args:
@@ -142,11 +143,11 @@ def init_stageinput_tui_field(field: ModelField, cmd_args: Mapping) -> TuiField:
142
143
  Returns:
143
144
  The initialized TuiField.
144
145
  """
145
- if (field.name not in cmd_args) and (field.alias not in cmd_args):
146
+ if (field.name not in cmd_args) and (field.effective_alias not in cmd_args):
146
147
  tui_field = TuiField(
147
148
  simple=True,
148
149
  name=field.name,
149
- description=str(field.field_info.description),
150
+ description=str(field.description),
150
151
  type_=get_field_type(field),
151
152
  )
152
153
  return tui_field
@@ -154,7 +155,7 @@ def init_stageinput_tui_field(field: ModelField, cmd_args: Mapping) -> TuiField:
154
155
  if field.name in cmd_args:
155
156
  stage_input_args = cmd_args.get(field.name)
156
157
  else:
157
- stage_input_args = cmd_args.get(field.alias)
158
+ stage_input_args = cmd_args.get(field.effective_alias)
158
159
 
159
160
  stage_name = ""
160
161
  stage_args = {}
@@ -170,8 +171,8 @@ def init_stageinput_tui_field(field: ModelField, cmd_args: Mapping) -> TuiField:
170
171
  stage_cls = stage_info[-1]
171
172
 
172
173
  tui_fields = []
173
- for field in stage_cls.__fields__.values():
174
- tui_fields.append(init_tui_field(field, stage_args))
174
+ for f in iter_fields(stage_cls):
175
+ tui_fields.append(init_tui_field(f, stage_args))
175
176
 
176
177
  tui_field = TuiField(
177
178
  simple=False,
@@ -182,7 +183,7 @@ def init_stageinput_tui_field(field: ModelField, cmd_args: Mapping) -> TuiField:
182
183
  return tui_field
183
184
 
184
185
 
185
- def get_field_type(field: ModelField) -> str:
186
+ def get_field_type(field: FieldView) -> str:
186
187
  """Get the type of a field.
187
188
 
188
189
  Args:
@@ -191,12 +192,14 @@ def get_field_type(field: ModelField) -> str:
191
192
  Returns:
192
193
  The type of the field.
193
194
  """
194
- type_ = field.annotation
195
-
196
- if "typing." in str(type_):
197
- type_ = str(type_).replace("typing.", "")
195
+ # constrained types (`Annotated[int, Gt(gt=0)]`) are shown as their base type
196
+ type_ = strip_annotated(field.annotation)
197
+ if inspect.isclass(type_) and not getattr(type_, "__args__", None):
198
+ type_ = type_.__name__
198
199
  else:
199
- type_ = field.annotation.__name__
200
+ # `typing` aliases print as `typing.List[int]`, modern hints as written
201
+ # (`list[int]`, `int | None`); never rely on `__name__` (UnionType has none)
202
+ type_ = str(type_).replace("typing.", "")
200
203
 
201
204
  # replace common pipelime types
202
205
  common_pipelime_types = {
pipelime/cli/utils.py CHANGED
@@ -9,7 +9,7 @@ from types import ModuleType
9
9
 
10
10
  import yaml
11
11
  from loguru import logger
12
- from pydantic.v1 import BaseModel, ValidationError
12
+ from pydantic import BaseModel, ValidationError
13
13
  from rich import get_console
14
14
  from rich.prompt import Prompt
15
15
  from yaml.error import YAMLError
@@ -108,13 +108,15 @@ class PipelimeSymbolsHelper:
108
108
 
109
109
  @classmethod
110
110
  def _symbol_name(cls, symbol):
111
- from pydantic.v1 import BaseModel
111
+ import inspect
112
112
 
113
- return (
114
- symbol.__config__.title
115
- if issubclass(symbol, BaseModel) and symbol.__config__.title
116
- else symbol.__name__
117
- )
113
+ from pydantic import BaseModel
114
+
115
+ from pipelime.utils.pydantic_compat import model_title
116
+
117
+ if inspect.isclass(symbol) and issubclass(symbol, BaseModel):
118
+ return model_title(symbol)
119
+ return symbol.__name__
118
120
 
119
121
  @classmethod
120
122
  def _warn_double_def(cls, type_, name, first, second):
@@ -148,22 +150,28 @@ class PipelimeSymbolsHelper:
148
150
  )
149
151
 
150
152
  # set module path when loading from file
153
+ # NB: only for the classes defined there, a class the file imports
154
+ # (eg, `from pipelime.stages import StageEntity`) keeps its own path
151
155
  if module_name.endswith(".py"):
152
156
  for _, sym_cls in module_symbols:
153
- sym_cls._classpath = f"{module_name}:{sym_cls.__name__}"
157
+ if sym_cls.__module__ == module_.__name__:
158
+ sym_cls._classpath = f"{module_name}:{sym_cls.__name__}"
154
159
 
155
160
  # check for double symbols in the same module
161
+ # NB: the same class re-exported under another name is not a duplicate
156
162
  for idx, (sym_name, sym_cls) in enumerate(module_symbols):
157
163
  for other_sym_name, other_sym_cls in module_symbols[idx + 1 :]: # noqa
158
- if sym_name == other_sym_name:
164
+ if sym_name == other_sym_name and sym_cls is not other_sym_cls:
159
165
  cls._warn_double_def(
160
166
  symbol_type, sym_name, sym_cls, other_sym_cls
161
167
  )
162
168
 
163
169
  # check for double commands across modules
170
+ # NB: a module importing a symbol from another one (eg, a user file
171
+ # with `from pipelime.stages import StageEntity`) is not a duplicate
164
172
  module_symbols = dict(module_symbols)
165
173
  for sym_name, sym_cls in module_symbols.items():
166
- if sym_name in all_syms:
174
+ if sym_name in all_syms and all_syms[sym_name] is not sym_cls:
167
175
  cls._warn_double_def(
168
176
  symbol_type, sym_name, sym_cls, all_syms[sym_name]
169
177
  )
@@ -631,7 +639,7 @@ def pl_print(
631
639
  """
632
640
  import inspect
633
641
 
634
- from pydantic.v1 import BaseModel
642
+ from pydantic import BaseModel
635
643
  from rich import print as rprint
636
644
 
637
645
  from pipelime.cli.pretty_print import print_model_info
@@ -720,9 +728,11 @@ def get_pipelime_command_cls(
720
728
  return cmd_cls[1]
721
729
 
722
730
 
723
- def get_pipelime_command(
731
+ def resolve_pipelime_command(
724
732
  cmd: "T_DAG_NODE", checkpoint: t.Optional["CheckpointNamespace"] = None
725
- ) -> "PipelimeCommand":
733
+ ):
734
+ """Splits a DAG node definition into the command class (None when `cmd` is
735
+ already an instance) and a zero-arg builder returning the command."""
726
736
  from pipelime.piper.model import LazyCommand, PipelimeCommand
727
737
 
728
738
  cmd_cls, cmd_args = None, {}
@@ -734,19 +744,28 @@ def get_pipelime_command(
734
744
  elif isinstance(cmd, LazyCommand):
735
745
  cmd_cls = cmd
736
746
 
737
- if cmd_cls is not None:
738
- cmd = (
739
- cmd_cls(**cmd_args)
740
- if checkpoint is None
741
- else cmd_cls.init_from_checkpoint(checkpoint, **cmd_args)
742
- )
747
+ def _build() -> "PipelimeCommand":
748
+ obj = cmd
749
+ if cmd_cls is not None:
750
+ obj = (
751
+ cmd_cls(**cmd_args)
752
+ if checkpoint is None
753
+ else cmd_cls.init_from_checkpoint(checkpoint, **cmd_args)
754
+ )
755
+ if not isinstance(obj, PipelimeCommand):
756
+ raise ValueError(f"{obj} is not a pipelime command.")
757
+ if checkpoint is not None:
758
+ obj._checkpoint = checkpoint
759
+ return obj
760
+
761
+ real_cls = cmd_cls.command_class if isinstance(cmd_cls, LazyCommand) else cmd_cls
762
+ return real_cls, _build
743
763
 
744
- if not isinstance(cmd, PipelimeCommand):
745
- raise ValueError(f"{cmd} is not a pipelime command.")
746
764
 
747
- if checkpoint is not None:
748
- cmd._checkpoint = checkpoint
749
- return cmd
765
+ def get_pipelime_command(
766
+ cmd: "T_DAG_NODE", checkpoint: t.Optional["CheckpointNamespace"] = None
767
+ ) -> "PipelimeCommand":
768
+ return resolve_pipelime_command(cmd, checkpoint)[1]()
750
769
 
751
770
 
752
771
  def time_to_str(nanosec: int) -> str:
@@ -761,21 +780,29 @@ def time_to_str(nanosec: int) -> str:
761
780
  return str(timedelta(microseconds=nanosec / 1000))
762
781
 
763
782
 
764
- def show_field_alias_valerr(e: ValidationError):
765
- def _replace_alias(val):
766
- if isinstance(val, str):
767
- for field in e.model.__fields__.values(): # type: ignore
768
- if (
769
- field.model_config.allow_population_by_field_name
770
- and field.has_alias
771
- and field.alias == val
772
- ):
773
- return f"{field.name} / {field.alias}"
774
- return val
783
+ def format_validation_error(e: ValidationError, model_cls=None) -> str:
784
+ """The error text with `name / alias` locations, as pipelime 2.x printed them.
785
+ `model_cls` is the class that raised (v2 errors do not carry it)."""
786
+ from pipelime.utils.pydantic_compat import iter_fields
787
+
788
+ alias_to_name = {}
789
+ if model_cls is not None and model_cls.model_config.get("populate_by_name"):
790
+ alias_to_name = {
791
+ f.alias: f"{f.name} / {f.alias}"
792
+ for f in iter_fields(model_cls)
793
+ if f.has_alias
794
+ }
775
795
 
796
+ lines = [f"{e.error_count()} validation error(s) for {e.title}"]
776
797
  for err in e.errors():
777
- if "loc" in err:
778
- err["loc"] = tuple(_replace_alias(pos) for pos in err["loc"])
798
+ loc = " -> ".join(str(alias_to_name.get(p, p)) for p in err.get("loc", ()))
799
+ lines.append(f"{loc or '(root)'}\n {err['msg']} [type={err['type']}]")
800
+ return "\n".join(lines)
801
+
802
+
803
+ def show_field_alias_valerr(e: ValidationError, model_cls=None) -> str:
804
+ """The pipelime 2.x name of `format_validation_error`."""
805
+ return format_validation_error(e, model_cls)
779
806
 
780
807
 
781
808
  def parse_user_input(s: str) -> t.Any: