code-loader 1.0.188.dev1__tar.gz → 1.0.189__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 (37) hide show
  1. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/PKG-INFO +1 -1
  2. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/inner_leap_binder/leapbinder.py +38 -6
  3. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/inner_leap_binder/leapbinder_decorators.py +65 -1
  4. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/leaploader.py +20 -17
  5. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/pyproject.toml +1 -1
  6. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/LICENSE +0 -0
  7. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/README.md +0 -0
  8. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/__init__.py +0 -0
  9. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/contract/__init__.py +0 -0
  10. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/contract/datasetclasses.py +0 -0
  11. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/contract/enums.py +0 -0
  12. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/contract/exceptions.py +0 -0
  13. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/contract/mapping.py +0 -0
  14. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/contract/responsedataclasses.py +0 -0
  15. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/contract/sim_config.py +0 -0
  16. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/contract/visualizer_classes.py +0 -0
  17. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/default_losses.py +0 -0
  18. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/default_metrics.py +0 -0
  19. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/experiment_api/__init__.py +0 -0
  20. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/experiment_api/api.py +0 -0
  21. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/experiment_api/cli_config_utils.py +0 -0
  22. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/experiment_api/client.py +0 -0
  23. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/experiment_api/epoch.py +0 -0
  24. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/experiment_api/experiment.py +0 -0
  25. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/experiment_api/experiment_context.py +0 -0
  26. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/experiment_api/types.py +0 -0
  27. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/experiment_api/utils.py +0 -0
  28. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/experiment_api/workingspace_config_utils.py +0 -0
  29. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/inner_leap_binder/__init__.py +0 -0
  30. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/leaploaderbase.py +0 -0
  31. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/mixpanel_tracker.py +0 -0
  32. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/plot_functions/__init__.py +0 -0
  33. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/plot_functions/plot_functions.py +0 -0
  34. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/plot_functions/visualize.py +0 -0
  35. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/utils.py +0 -0
  36. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/visualizers/__init__.py +0 -0
  37. {code_loader-1.0.188.dev1 → code_loader-1.0.189}/code_loader/visualizers/default_visualizers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: code-loader
3
- Version: 1.0.188.dev1
3
+ Version: 1.0.189
4
4
  Summary:
5
5
  Home-page: https://github.com/tensorleap/code-loader
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
 
2
2
  import inspect
3
- from typing import Callable, List, Optional, Dict, Any, Type, Union
3
+ from typing import Callable, List, Optional, Dict, Any, Type, Union, get_args
4
4
 
5
5
  import numpy as np
6
6
  import numpy.typing as npt
@@ -31,6 +31,27 @@ from code_loader.visualizers.default_visualizers import DefaultVisualizer, \
31
31
  mapping_runtime_mode_env_var_mame = '__MAPPING_RUNTIME_MODE__'
32
32
 
33
33
 
34
+ def _stringized_annotation_type_name(annotation: Any) -> Optional[str]:
35
+ """Bare type name if ``annotation`` is a stringized annotation (from ``from __future__
36
+ import annotations`` or a quoted hint), else ``None``. code_loader inspects raw
37
+ annotations expecting real classes, so stringization silently breaks type detection."""
38
+ if isinstance(annotation, str):
39
+ return annotation.split("[")[0].strip().rsplit(".", 1)[-1]
40
+ return None
41
+
42
+
43
+ def _reject_stringized_sample_preprocess_response(function: Callable[..., Any], arg_name: str, annotation: Any) -> None:
44
+ """Fail fast on a stringized SamplePreprocessResponse arg (otherwise it silently
45
+ mis-wires as a regular input, surfacing only on the platform)."""
46
+ if _stringized_annotation_type_name(annotation) == SamplePreprocessResponse.__name__:
47
+ raise Exception(
48
+ f"Argument '{arg_name}' of function '{function.__name__}' is annotated with a string "
49
+ f"('{annotation}') instead of the SamplePreprocessResponse type. This usually means the "
50
+ f"file uses 'from __future__ import annotations' (or a quoted hint), which stringizes "
51
+ f"annotations and breaks Tensorleap type detection. Remove that import (or the quotes) so "
52
+ f"SamplePreprocessResponse is referenced as a real type.")
53
+
54
+
34
55
 
35
56
 
36
57
  class LeapBinder:
@@ -123,6 +144,7 @@ class LeapBinder:
123
144
  regular_arg_names = inspect.getfullargspec(function)[0]
124
145
  preprocess_response_arg_name = None
125
146
  for arg_name, arg_type in inspect.getfullargspec(function).annotations.items():
147
+ _reject_stringized_sample_preprocess_response(function, arg_name, arg_type)
126
148
  if arg_type == SamplePreprocessResponse:
127
149
  if preprocess_response_arg_name is not None:
128
150
  raise Exception("only one argument can be of type SamplePreprocessResponse")
@@ -144,8 +166,8 @@ class LeapBinder:
144
166
 
145
167
  if visualizer_type.value not in map_leap_data_type_to_visualizer_class:
146
168
  raise Exception(
147
- f'The visualizer_type is invalid. current visualizer_type: {visualizer_type}, ' # type: ignore[attr-defined]
148
- f'should be one of : {", ".join([arg.__name__ for arg in LeapData.__args__])}')
169
+ f'The visualizer_type is invalid. current visualizer_type: {visualizer_type}, '
170
+ f'should be one of : {", ".join([arg.__name__ for arg in get_args(LeapData)])}')
149
171
 
150
172
  func_annotations = function.__annotations__
151
173
  if "return" not in func_annotations:
@@ -155,10 +177,17 @@ class LeapBinder:
155
177
  f"https://docs.python.org/3/library/typing.html")
156
178
  else:
157
179
  return_type = func_annotations["return"]
158
- if return_type not in LeapData.__args__: # type: ignore[attr-defined]
180
+ if isinstance(return_type, str):
181
+ raise Exception(
182
+ f"The return type hint of function '{function.__name__}' is a string "
183
+ f"('{return_type}') instead of a type. This usually means the file uses "
184
+ f"'from __future__ import annotations' (or a quoted return hint), which stringizes "
185
+ f"annotations and prevents Tensorleap from detecting the visualizer type. Remove "
186
+ f"that import (or the quotes) so the return type is a real class.")
187
+ if return_type not in get_args(LeapData):
159
188
  raise Exception(
160
- f'The return type of function {function.__name__} is invalid. current return type: {return_type}, ' # type: ignore[attr-defined]
161
- f'should be one of : {", ".join([arg.__name__ for arg in LeapData.__args__])}')
189
+ f'The return type of function {function.__name__} is invalid. current return type: {return_type}, '
190
+ f'should be one of : {", ".join([arg.__name__ for arg in get_args(LeapData)])}')
162
191
 
163
192
  expected_return_type = map_leap_data_type_to_visualizer_class[visualizer_type.value]
164
193
  if not issubclass(return_type, expected_return_type):
@@ -296,6 +325,7 @@ class LeapBinder:
296
325
  regular_arg_names = inspect.getfullargspec(function)[0]
297
326
  preprocess_response_arg_name = None
298
327
  for arg_name, arg_type in inspect.getfullargspec(function).annotations.items():
328
+ _reject_stringized_sample_preprocess_response(function, arg_name, arg_type)
299
329
  if arg_type == SamplePreprocessResponse:
300
330
  if preprocess_response_arg_name is not None:
301
331
  raise Exception("only one argument can be of type SamplePreprocessResponse")
@@ -339,6 +369,7 @@ class LeapBinder:
339
369
  regular_arg_names = inspect.getfullargspec(function)[0]
340
370
  preprocess_response_arg_name = None
341
371
  for arg_name, arg_type in inspect.getfullargspec(function).annotations.items():
372
+ _reject_stringized_sample_preprocess_response(function, arg_name, arg_type)
342
373
  if arg_type == SamplePreprocessResponse:
343
374
  if preprocess_response_arg_name is not None:
344
375
  raise Exception("only one argument can be of type SamplePreprocessResponse")
@@ -380,6 +411,7 @@ class LeapBinder:
380
411
  regular_arg_names = inspect.getfullargspec(function)[0]
381
412
  preprocess_response_arg_name = None
382
413
  for arg_name, arg_type in inspect.getfullargspec(function).annotations.items():
414
+ _reject_stringized_sample_preprocess_response(function, arg_name, arg_type)
383
415
  if arg_type == SamplePreprocessResponse:
384
416
  if preprocess_response_arg_name is not None:
385
417
  raise Exception("only one argument can be of type SamplePreprocessResponse")
@@ -223,6 +223,59 @@ def _add_mapping_connections(connects_to, arg_names, node_mapping_type, name):
223
223
  _add_mapping_connection(user_unique_name, connection_destinations, arg_names, name, node_mapping_type)
224
224
 
225
225
 
226
+ def _require_sample_preprocess_response_supplied(user_function: Callable, args: tuple, kwargs: dict) -> None:
227
+ """A SamplePreprocessResponse argument is auto-injected by the platform / check_dataset
228
+ but NOT inside integration_test, where the author calls the function directly. Fail fast
229
+ with an actionable message instead of a raw 'missing argument' TypeError."""
230
+ spr_arg_name = None
231
+ for arg_name, arg_type in inspect.getfullargspec(user_function).annotations.items():
232
+ if arg_type == SamplePreprocessResponse:
233
+ spr_arg_name = arg_name
234
+ break
235
+ if spr_arg_name is None:
236
+ return
237
+ signature = inspect.signature(user_function)
238
+ param = signature.parameters.get(spr_arg_name)
239
+ # Only enforce a REQUIRED SamplePreprocessResponse arg. If it has a default it is
240
+ # optional and the function may legitimately be called with or without it.
241
+ if param is None or param.default is not inspect.Parameter.empty:
242
+ return
243
+ try:
244
+ supplied = signature.bind_partial(*args, **kwargs).arguments
245
+ except TypeError:
246
+ return
247
+ if spr_arg_name not in supplied:
248
+ raise Exception(
249
+ f"'{user_function.__name__}' takes a SamplePreprocessResponse argument "
250
+ f"'{spr_arg_name}', but it was not provided. Inside integration_test pass it "
251
+ f"explicitly, e.g. {user_function.__name__}(..., "
252
+ f"SamplePreprocessResponse(sample_id, preprocess)).")
253
+
254
+
255
+ def _warn_loss_inputs_not_wired_to_model() -> None:
256
+ """Warn when a custom loss consumes an input encoder the model is not given.
257
+
258
+ Such an input stays a plain NodeMappingType.Input (model-fed inputs get re-tagged to
259
+ Input0/Input1/...) and crashes the platform model build. Losses only — the same pattern
260
+ in a metric or visualizer builds fine."""
261
+ loss_types = {NodeMappingType.Loss, NodeMappingType.CustomLoss}
262
+ for connection in leap_binder.mapping_connections:
263
+ if connection.node is None or connection.node.type not in loss_types:
264
+ continue
265
+ for arg_name, source in (connection.node_inputs or {}).items():
266
+ if source is not None and source.type == NodeMappingType.Input:
267
+ input_name = source.name or arg_name
268
+ store_general_warning(
269
+ key=("loss_input_not_wired_to_model", connection.node.name, input_name),
270
+ message=(
271
+ f"Custom loss '{connection.node.name}' consumes input '{input_name}', "
272
+ f"which is not fed to the model. This is not supported: the platform builds "
273
+ f"losses into the model graph, so an input the model does not take has no "
274
+ f"source there and the model build fails. Instead, carry this data in the "
275
+ f"ground truth and derive it inside the loss (not as a separate input "
276
+ f"encoder)."))
277
+
278
+
226
279
  def tensorleap_integration_test():
227
280
  def decorating_function(integration_test_function: Callable):
228
281
  leap_binder.integration_test_func = integration_test_function
@@ -279,6 +332,8 @@ def tensorleap_integration_test():
279
332
  finally:
280
333
  _called_from_inside_tl_integration_test_decorator = False
281
334
 
335
+ _warn_loss_inputs_not_wired_to_model()
336
+
282
337
  leap_binder.check()
283
338
 
284
339
  return inner
@@ -822,6 +877,7 @@ def tensorleap_custom_metric(name: str,
822
877
  _called_from_inside_tl_decorator += 1
823
878
 
824
879
  try:
880
+ _require_sample_preprocess_response_supplied(user_function, args, kwargs)
825
881
  result = user_function(*args, **kwargs)
826
882
  finally:
827
883
  _called_from_inside_tl_decorator -= 1
@@ -1031,6 +1087,7 @@ def tensorleap_custom_instances_metric(name: str,
1031
1087
  _called_from_inside_tl_decorator += 1
1032
1088
 
1033
1089
  try:
1090
+ _require_sample_preprocess_response_supplied(user_function, args, kwargs)
1034
1091
  result = user_function(*args, **kwargs)
1035
1092
  finally:
1036
1093
  _called_from_inside_tl_decorator -= 1
@@ -1383,6 +1440,7 @@ def tensorleap_custom_visualizer(name: str, visualizer_type: LeapDataType,
1383
1440
  _called_from_inside_tl_decorator += 1
1384
1441
 
1385
1442
  try:
1443
+ _require_sample_preprocess_response_supplied(user_function, args, kwargs)
1386
1444
  result = user_function(*args, **kwargs)
1387
1445
  finally:
1388
1446
  _called_from_inside_tl_decorator -= 1
@@ -1929,8 +1987,13 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
1929
1987
 
1930
1988
  )
1931
1989
 
1990
+ # channel_dim is the channel axis in the BATCHED tensor (batch = axis 0):
1991
+ # 1 for channel-first (NCHW), -1 for channel-last (NHWC); 0 is invalid.
1932
1992
  if channel_dim <= 0 and channel_dim != -1:
1933
- raise Exception(f"Channel dim for input {name} is expected to be either -1 or positive")
1993
+ raise Exception(
1994
+ f"Invalid channel_dim ({channel_dim}) for input '{name}'. channel_dim is the "
1995
+ f"channel axis in the batched tensor (batch = axis 0), so 0 is invalid. Use 1 "
1996
+ f"for channel-first (NCHW) and -1 for channel-last (NHWC).")
1934
1997
 
1935
1998
  def _validate_input_args(sample_id: Union[int, str], preprocess_response: PreprocessResponse):
1936
1999
  assert type(sample_id) == preprocess_response.sample_id_type, \
@@ -2149,6 +2212,7 @@ def tensorleap_custom_loss(name: str, connects_to=None):
2149
2212
  _called_from_inside_tl_decorator += 1
2150
2213
 
2151
2214
  try:
2215
+ _require_sample_preprocess_response_supplied(user_function, args, kwargs)
2152
2216
  result = user_function(*args, **kwargs)
2153
2217
  finally:
2154
2218
  _called_from_inside_tl_decorator -= 1
@@ -64,9 +64,6 @@ class LeapLoader(LeapLoaderBase):
64
64
  self.evaluate_module()
65
65
  if global_leap_binder.integration_test_func is not None:
66
66
  global_leap_binder.integration_test_func(None, PreprocessResponse(state=DataStateType.training, length=0))
67
- except DatasetScriptException:
68
- global_leap_binder.setup_container = DatasetIntegrationSetup()
69
- raise
70
67
  except TypeError as e:
71
68
  import traceback
72
69
  global_leap_binder.setup_container = DatasetIntegrationSetup()
@@ -88,17 +85,10 @@ class LeapLoader(LeapLoaderBase):
88
85
  return
89
86
 
90
87
  parent_path = str(Path(full_path).parent)
91
- if parent_path == full_path:
92
- return
93
88
  append_path_recursively(parent_path)
94
89
  sys.path.append(parent_path)
95
90
 
96
91
  file_path = Path(self.code_path, self.code_entry_name)
97
- if not file_path.is_file():
98
- raise DatasetScriptException(
99
- f"Entry file not found: '{self.code_entry_name}' does not exist under '{self.code_path}'. "
100
- f"Check the 'entryFile' setting in your leap.yaml."
101
- )
102
92
  append_path_recursively(str(file_path))
103
93
 
104
94
  importlib.invalidate_caches()
@@ -113,13 +103,6 @@ class LeapLoader(LeapLoaderBase):
113
103
 
114
104
  spec.loader.exec_module(file)
115
105
 
116
- if global_leap_binder.setup_container.preprocess is None:
117
- raise DatasetScriptException(
118
- f"No Tensorleap integration found in '{self.code_entry_name}': the script never called "
119
- f"leap_binder.set_preprocess(...). Check the 'entryFile' setting in your leap.yaml — "
120
- f"it may be pointing to the wrong file."
121
- )
122
-
123
106
  @lru_cache()
124
107
  def metric_by_name(self) -> Dict[str, MetricHandlerData]:
125
108
  self.exec_script()
@@ -337,6 +320,9 @@ class LeapLoader(LeapLoaderBase):
337
320
  try:
338
321
  self.exec_script()
339
322
 
323
+ integration_test_test_payload = self._check_integration_test_exists()
324
+ test_payloads.append(integration_test_test_payload)
325
+
340
326
  preprocess_test_payload = self._check_preprocess()
341
327
  test_payloads.append(preprocess_test_payload)
342
328
  handlers_test_payloads = self._check_handlers()
@@ -373,6 +359,23 @@ class LeapLoader(LeapLoaderBase):
373
359
  engine_file_contract=EngineFileContract(global_leap_binder.mapping_connections,
374
360
  global_leap_binder.leap_analysis_configuration))
375
361
 
362
+ def _check_integration_test_exists(self) -> DatasetTestResultPayload:
363
+ test_result = DatasetTestResultPayload('integration_test')
364
+ # Only enforced on the Tensorleap platform (i.e. during a push / inspection,
365
+ # where IS_TENSORLEAP_PLATFORM=true). Running code-loader locally or in unit
366
+ # tests must not require the decorator, so pre-decorator integrations and
367
+ # local check runs keep working unchanged.
368
+ is_on_platform = os.environ.get('IS_TENSORLEAP_PLATFORM') == 'true'
369
+ if is_on_platform and global_leap_binder.integration_test_func is None:
370
+ test_result.is_passed = False
371
+ test_result.display[TestingSectionEnum.Errors.name] = (
372
+ "No integration test was found in your integration file. A valid Tensorleap "
373
+ "integration file must define an integration test function decorated with "
374
+ "@tensorleap_integration_test. Without it the integration cannot be validated "
375
+ "and the push is aborted. See the mnist leap_integration.py for a valid example."
376
+ )
377
+ return test_result
378
+
376
379
  def _check_preprocess(self) -> DatasetTestResultPayload:
377
380
  test_result = DatasetTestResultPayload('preprocess')
378
381
  try:
@@ -1,7 +1,7 @@
1
1
  [tool.poetry]
2
2
  name = "code-loader"
3
3
 
4
- version = "1.0.188.dev1"
4
+ version = "1.0.189"
5
5
  description = ""
6
6
  authors = ["dorhar <doron.harnoy@tensorleap.ai>"]
7
7
  license = "MIT"