ob-metaflow 2.15.18.1__py2.py3-none-any.whl → 2.16.0.1__py2.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.

Potentially problematic release.


This version of ob-metaflow might be problematic. Click here for more details.

Files changed (93) hide show
  1. metaflow/__init__.py +7 -1
  2. metaflow/_vendor/imghdr/__init__.py +180 -0
  3. metaflow/cli.py +16 -1
  4. metaflow/cli_components/init_cmd.py +1 -0
  5. metaflow/cli_components/run_cmds.py +6 -2
  6. metaflow/client/core.py +22 -30
  7. metaflow/cmd/develop/stub_generator.py +19 -2
  8. metaflow/datastore/task_datastore.py +0 -1
  9. metaflow/debug.py +5 -0
  10. metaflow/decorators.py +230 -70
  11. metaflow/extension_support/__init__.py +15 -8
  12. metaflow/extension_support/_empty_file.py +2 -2
  13. metaflow/flowspec.py +80 -53
  14. metaflow/graph.py +24 -2
  15. metaflow/meta_files.py +13 -0
  16. metaflow/metadata_provider/metadata.py +7 -1
  17. metaflow/metaflow_config.py +5 -0
  18. metaflow/metaflow_environment.py +82 -25
  19. metaflow/metaflow_version.py +1 -1
  20. metaflow/package/__init__.py +664 -0
  21. metaflow/packaging_sys/__init__.py +870 -0
  22. metaflow/packaging_sys/backend.py +113 -0
  23. metaflow/packaging_sys/distribution_support.py +153 -0
  24. metaflow/packaging_sys/tar_backend.py +86 -0
  25. metaflow/packaging_sys/utils.py +91 -0
  26. metaflow/packaging_sys/v1.py +476 -0
  27. metaflow/plugins/__init__.py +3 -0
  28. metaflow/plugins/airflow/airflow.py +11 -1
  29. metaflow/plugins/airflow/airflow_cli.py +15 -4
  30. metaflow/plugins/argo/argo_workflows.py +346 -301
  31. metaflow/plugins/argo/argo_workflows_cli.py +16 -4
  32. metaflow/plugins/argo/exit_hooks.py +209 -0
  33. metaflow/plugins/aws/aws_utils.py +1 -1
  34. metaflow/plugins/aws/batch/batch.py +22 -3
  35. metaflow/plugins/aws/batch/batch_cli.py +3 -0
  36. metaflow/plugins/aws/batch/batch_decorator.py +13 -5
  37. metaflow/plugins/aws/step_functions/step_functions.py +10 -1
  38. metaflow/plugins/aws/step_functions/step_functions_cli.py +15 -4
  39. metaflow/plugins/cards/card_cli.py +20 -1
  40. metaflow/plugins/cards/card_creator.py +24 -1
  41. metaflow/plugins/cards/card_decorator.py +57 -6
  42. metaflow/plugins/cards/card_modules/convert_to_native_type.py +5 -2
  43. metaflow/plugins/cards/card_modules/test_cards.py +16 -0
  44. metaflow/plugins/cards/metadata.py +22 -0
  45. metaflow/plugins/exit_hook/__init__.py +0 -0
  46. metaflow/plugins/exit_hook/exit_hook_decorator.py +46 -0
  47. metaflow/plugins/exit_hook/exit_hook_script.py +52 -0
  48. metaflow/plugins/kubernetes/kubernetes.py +8 -1
  49. metaflow/plugins/kubernetes/kubernetes_cli.py +3 -0
  50. metaflow/plugins/kubernetes/kubernetes_decorator.py +13 -5
  51. metaflow/plugins/package_cli.py +25 -23
  52. metaflow/plugins/parallel_decorator.py +4 -2
  53. metaflow/plugins/pypi/bootstrap.py +8 -2
  54. metaflow/plugins/pypi/conda_decorator.py +39 -82
  55. metaflow/plugins/pypi/conda_environment.py +6 -2
  56. metaflow/plugins/pypi/pypi_decorator.py +4 -4
  57. metaflow/plugins/secrets/__init__.py +3 -0
  58. metaflow/plugins/secrets/secrets_decorator.py +9 -173
  59. metaflow/plugins/secrets/secrets_func.py +49 -0
  60. metaflow/plugins/secrets/secrets_spec.py +101 -0
  61. metaflow/plugins/secrets/utils.py +74 -0
  62. metaflow/plugins/test_unbounded_foreach_decorator.py +2 -2
  63. metaflow/plugins/timeout_decorator.py +0 -1
  64. metaflow/plugins/uv/bootstrap.py +11 -0
  65. metaflow/plugins/uv/uv_environment.py +4 -2
  66. metaflow/pylint_wrapper.py +5 -1
  67. metaflow/runner/click_api.py +5 -4
  68. metaflow/runner/metaflow_runner.py +16 -1
  69. metaflow/runner/subprocess_manager.py +14 -2
  70. metaflow/runtime.py +82 -11
  71. metaflow/task.py +91 -7
  72. metaflow/user_configs/config_options.py +13 -8
  73. metaflow/user_configs/config_parameters.py +0 -4
  74. metaflow/user_decorators/__init__.py +0 -0
  75. metaflow/user_decorators/common.py +144 -0
  76. metaflow/user_decorators/mutable_flow.py +499 -0
  77. metaflow/user_decorators/mutable_step.py +424 -0
  78. metaflow/user_decorators/user_flow_decorator.py +263 -0
  79. metaflow/user_decorators/user_step_decorator.py +712 -0
  80. metaflow/util.py +4 -1
  81. metaflow/version.py +1 -1
  82. {ob_metaflow-2.15.18.1.data → ob_metaflow-2.16.0.1.data}/data/share/metaflow/devtools/Tiltfile +27 -2
  83. {ob_metaflow-2.15.18.1.dist-info → ob_metaflow-2.16.0.1.dist-info}/METADATA +2 -2
  84. {ob_metaflow-2.15.18.1.dist-info → ob_metaflow-2.16.0.1.dist-info}/RECORD +90 -70
  85. metaflow/info_file.py +0 -25
  86. metaflow/package.py +0 -203
  87. metaflow/user_configs/config_decorators.py +0 -568
  88. {ob_metaflow-2.15.18.1.data → ob_metaflow-2.16.0.1.data}/data/share/metaflow/devtools/Makefile +0 -0
  89. {ob_metaflow-2.15.18.1.data → ob_metaflow-2.16.0.1.data}/data/share/metaflow/devtools/pick_services.sh +0 -0
  90. {ob_metaflow-2.15.18.1.dist-info → ob_metaflow-2.16.0.1.dist-info}/WHEEL +0 -0
  91. {ob_metaflow-2.15.18.1.dist-info → ob_metaflow-2.16.0.1.dist-info}/entry_points.txt +0 -0
  92. {ob_metaflow-2.15.18.1.dist-info → ob_metaflow-2.16.0.1.dist-info}/licenses/LICENSE +0 -0
  93. {ob_metaflow-2.15.18.1.dist-info → ob_metaflow-2.16.0.1.dist-info}/top_level.txt +0 -0
metaflow/__init__.py CHANGED
@@ -104,7 +104,13 @@ from .flowspec import FlowSpec
104
104
  from .parameters import Parameter, JSONTypeClass, JSONType
105
105
 
106
106
  from .user_configs.config_parameters import Config, ConfigValue, config_expr
107
- from .user_configs.config_decorators import CustomFlowDecorator, CustomStepDecorator
107
+ from .user_decorators.user_step_decorator import (
108
+ UserStepDecorator,
109
+ StepMutator,
110
+ user_step_decorator,
111
+ USER_SKIP_STEP,
112
+ )
113
+ from .user_decorators.user_flow_decorator import FlowMutator
108
114
 
109
115
  # data layer
110
116
  # For historical reasons, we make metaflow.plugins.datatools accessible as
@@ -0,0 +1,180 @@
1
+ """Recognize image file formats based on their first few bytes."""
2
+
3
+ from os import PathLike
4
+ import warnings
5
+
6
+ __all__ = ["what"]
7
+
8
+
9
+ warnings._deprecated(__name__, remove=(3, 13))
10
+
11
+
12
+ #-------------------------#
13
+ # Recognize image headers #
14
+ #-------------------------#
15
+
16
+ def what(file, h=None):
17
+ """Return the type of image contained in a file or byte stream."""
18
+ f = None
19
+ try:
20
+ if h is None:
21
+ if isinstance(file, (str, PathLike)):
22
+ f = open(file, 'rb')
23
+ h = f.read(32)
24
+ else:
25
+ location = file.tell()
26
+ h = file.read(32)
27
+ file.seek(location)
28
+ for tf in tests:
29
+ res = tf(h, f)
30
+ if res:
31
+ return res
32
+ finally:
33
+ if f: f.close()
34
+ return None
35
+
36
+
37
+ #---------------------------------#
38
+ # Subroutines per image file type #
39
+ #---------------------------------#
40
+
41
+ tests = []
42
+
43
+ def test_jpeg(h, f):
44
+ """Test for JPEG data with JFIF or Exif markers; and raw JPEG."""
45
+ if h[6:10] in (b'JFIF', b'Exif'):
46
+ return 'jpeg'
47
+ elif h[:4] == b'\xff\xd8\xff\xdb':
48
+ return 'jpeg'
49
+
50
+ tests.append(test_jpeg)
51
+
52
+ def test_png(h, f):
53
+ """Verify if the image is a PNG."""
54
+ if h.startswith(b'\211PNG\r\n\032\n'):
55
+ return 'png'
56
+
57
+ tests.append(test_png)
58
+
59
+ def test_gif(h, f):
60
+ """Verify if the image is a GIF ('87 or '89 variants)."""
61
+ if h[:6] in (b'GIF87a', b'GIF89a'):
62
+ return 'gif'
63
+
64
+ tests.append(test_gif)
65
+
66
+ def test_tiff(h, f):
67
+ """Verify if the image is a TIFF (can be in Motorola or Intel byte order)."""
68
+ if h[:2] in (b'MM', b'II'):
69
+ return 'tiff'
70
+
71
+ tests.append(test_tiff)
72
+
73
+ def test_rgb(h, f):
74
+ """test for the SGI image library."""
75
+ if h.startswith(b'\001\332'):
76
+ return 'rgb'
77
+
78
+ tests.append(test_rgb)
79
+
80
+ def test_pbm(h, f):
81
+ """Verify if the image is a PBM (portable bitmap)."""
82
+ if len(h) >= 3 and \
83
+ h[0] == ord(b'P') and h[1] in b'14' and h[2] in b' \t\n\r':
84
+ return 'pbm'
85
+
86
+ tests.append(test_pbm)
87
+
88
+ def test_pgm(h, f):
89
+ """Verify if the image is a PGM (portable graymap)."""
90
+ if len(h) >= 3 and \
91
+ h[0] == ord(b'P') and h[1] in b'25' and h[2] in b' \t\n\r':
92
+ return 'pgm'
93
+
94
+ tests.append(test_pgm)
95
+
96
+ def test_ppm(h, f):
97
+ """Verify if the image is a PPM (portable pixmap)."""
98
+ if len(h) >= 3 and \
99
+ h[0] == ord(b'P') and h[1] in b'36' and h[2] in b' \t\n\r':
100
+ return 'ppm'
101
+
102
+ tests.append(test_ppm)
103
+
104
+ def test_rast(h, f):
105
+ """test for the Sun raster file."""
106
+ if h.startswith(b'\x59\xA6\x6A\x95'):
107
+ return 'rast'
108
+
109
+ tests.append(test_rast)
110
+
111
+ def test_xbm(h, f):
112
+ """Verify if the image is a X bitmap (X10 or X11)."""
113
+ if h.startswith(b'#define '):
114
+ return 'xbm'
115
+
116
+ tests.append(test_xbm)
117
+
118
+ def test_bmp(h, f):
119
+ """Verify if the image is a BMP file."""
120
+ if h.startswith(b'BM'):
121
+ return 'bmp'
122
+
123
+ tests.append(test_bmp)
124
+
125
+ def test_webp(h, f):
126
+ """Verify if the image is a WebP."""
127
+ if h.startswith(b'RIFF') and h[8:12] == b'WEBP':
128
+ return 'webp'
129
+
130
+ tests.append(test_webp)
131
+
132
+ def test_exr(h, f):
133
+ """verify is the image ia a OpenEXR fileOpenEXR."""
134
+ if h.startswith(b'\x76\x2f\x31\x01'):
135
+ return 'exr'
136
+
137
+ tests.append(test_exr)
138
+
139
+ #--------------------#
140
+ # Small test program #
141
+ #--------------------#
142
+
143
+ def test():
144
+ import sys
145
+ recursive = 0
146
+ if sys.argv[1:] and sys.argv[1] == '-r':
147
+ del sys.argv[1:2]
148
+ recursive = 1
149
+ try:
150
+ if sys.argv[1:]:
151
+ testall(sys.argv[1:], recursive, 1)
152
+ else:
153
+ testall(['.'], recursive, 1)
154
+ except KeyboardInterrupt:
155
+ sys.stderr.write('\n[Interrupted]\n')
156
+ sys.exit(1)
157
+
158
+ def testall(list, recursive, toplevel):
159
+ import sys
160
+ import os
161
+ for filename in list:
162
+ if os.path.isdir(filename):
163
+ print(filename + '/:', end=' ')
164
+ if recursive or toplevel:
165
+ print('recursing down:')
166
+ import glob
167
+ names = glob.glob(os.path.join(glob.escape(filename), '*'))
168
+ testall(names, recursive, 0)
169
+ else:
170
+ print('*** directory (use -r) ***')
171
+ else:
172
+ print(filename + ':', end=' ')
173
+ sys.stdout.flush()
174
+ try:
175
+ print(what(filename))
176
+ except OSError:
177
+ print('*** not found ***')
178
+
179
+ if __name__ == '__main__':
180
+ test()
metaflow/cli.py CHANGED
@@ -28,6 +28,7 @@ from .metaflow_config import (
28
28
  from .metaflow_current import current
29
29
  from metaflow.system import _system_monitor, _system_logger
30
30
  from .metaflow_environment import MetaflowEnvironment
31
+ from .packaging_sys import MetaflowCodeContent
31
32
  from .plugins import (
32
33
  DATASTORES,
33
34
  ENVIRONMENTS,
@@ -152,8 +153,13 @@ def check(obj, warnings=False):
152
153
  def show(obj):
153
154
  echo_always("\n%s" % obj.graph.doc)
154
155
  for node_name in obj.graph.sorted_nodes:
156
+ echo_always("")
155
157
  node = obj.graph[node_name]
156
- echo_always("\nStep *%s*" % node.name, err=False)
158
+ for deco in node.decorators:
159
+ echo_always("@%s" % deco.name, err=False)
160
+ for deco in node.wrappers:
161
+ echo_always("@%s" % deco.decorator_name, err=False)
162
+ echo_always("Step *%s*" % node.name, err=False)
157
163
  echo_always(node.doc if node.doc else "?", indent=True, err=False)
158
164
  if node.type != "end":
159
165
  echo_always(
@@ -336,6 +342,11 @@ def start(
336
342
  echo(" executing *%s*" % ctx.obj.flow.name, fg="magenta", nl=False)
337
343
  echo(" for *%s*" % resolve_identity(), fg="magenta")
338
344
 
345
+ # Check if we need to setup the distribution finder (if running )
346
+ dist_info = MetaflowCodeContent.get_distribution_finder()
347
+ if dist_info:
348
+ sys.meta_path.append(dist_info)
349
+
339
350
  # Setup the context
340
351
  cli_args._set_top_kwargs(ctx.params)
341
352
  ctx.obj.echo = echo
@@ -436,6 +447,10 @@ def start(
436
447
  # be raised. For resume, since we ignore those options, we ignore the error.
437
448
  raise ctx.obj.delayed_config_exception
438
449
 
450
+ # Init all values in the config decorators and then process them
451
+ for decorator in ctx.obj.flow._flow_state.get(_FlowState.CONFIG_DECORATORS, []):
452
+ decorator.external_init()
453
+
439
454
  new_cls = ctx.obj.flow._process_config_decorators(config_options)
440
455
  if new_cls:
441
456
  ctx.obj.flow = new_cls(use_cli=False)
@@ -46,6 +46,7 @@ def init(obj, run_id=None, task_id=None, tags=None, **kwargs):
46
46
  obj.event_logger,
47
47
  obj.monitor,
48
48
  run_id=run_id,
49
+ skip_decorator_hooks=True,
49
50
  )
50
51
  obj.flow._set_constants(obj.graph, kwargs, obj.config_options)
51
52
  runtime.persist_constants(task_id=task_id)
@@ -8,7 +8,7 @@ from .. import decorators, namespace, parameters, tracing
8
8
  from ..exception import CommandException
9
9
  from ..graph import FlowGraph
10
10
  from ..metaflow_current import current
11
- from ..metaflow_config import DEFAULT_DECOSPECS
11
+ from ..metaflow_config import DEFAULT_DECOSPECS, FEAT_ALWAYS_UPLOAD_CODE_PACKAGE
12
12
  from ..package import MetaflowPackage
13
13
  from ..runtime import NativeRuntime
14
14
  from ..system import _system_logger
@@ -61,7 +61,11 @@ def before_run(obj, tags, decospecs):
61
61
  # We explicitly avoid doing this in `start` since it is invoked for every
62
62
  # step in the run.
63
63
  obj.package = MetaflowPackage(
64
- obj.flow, obj.environment, obj.echo, obj.package_suffixes
64
+ obj.flow,
65
+ obj.environment,
66
+ obj.echo,
67
+ suffixes=obj.package_suffixes,
68
+ flow_datastore=obj.flow_datastore if FEAT_ALWAYS_UPLOAD_CODE_PACKAGE else None,
65
69
  )
66
70
 
67
71
 
metaflow/client/core.py CHANGED
@@ -32,11 +32,12 @@ from metaflow.exception import (
32
32
  from metaflow.includefile import IncludedFile
33
33
  from metaflow.metaflow_config import DEFAULT_METADATA, MAX_ATTEMPTS
34
34
  from metaflow.metaflow_environment import MetaflowEnvironment
35
+ from metaflow.package import MetaflowPackage
36
+ from metaflow.packaging_sys import ContentType
35
37
  from metaflow.plugins import ENVIRONMENTS, METADATA_PROVIDERS
36
38
  from metaflow.unbounded_foreach import CONTROL_TASK_TAG
37
39
  from metaflow.util import cached_property, is_stringish, resolve_identity, to_unicode
38
40
 
39
- from ..info_file import INFO_FILE
40
41
  from .filecache import FileCache
41
42
 
42
43
  if TYPE_CHECKING:
@@ -816,20 +817,26 @@ class MetaflowCode(object):
816
817
  self._path = info["location"]
817
818
  self._ds_type = info["ds_type"]
818
819
  self._sha = info["sha"]
820
+ self._code_metadata = info.get(
821
+ "metadata",
822
+ '{"version": 0, "archive_format": "tgz", "mfcontent_version": 0}',
823
+ )
824
+
825
+ self._backend = MetaflowPackage.get_backend(self._code_metadata)
819
826
 
820
827
  if filecache is None:
821
828
  filecache = FileCache()
822
829
  _, blobdata = filecache.get_data(
823
830
  self._ds_type, self._flow_name, self._path, self._sha
824
831
  )
825
- code_obj = BytesIO(blobdata)
826
- self._tar = tarfile.open(fileobj=code_obj, mode="r:gz")
827
- # The JSON module in Python3 deals with Unicode. Tar gives bytes.
828
- info_str = (
829
- self._tar.extractfile(os.path.basename(INFO_FILE)).read().decode("utf-8")
830
- )
831
- self._info = json.loads(info_str)
832
- self._flowspec = self._tar.extractfile(self._info["script"]).read()
832
+ self._code_obj = BytesIO(blobdata)
833
+ self._info = MetaflowPackage.cls_get_info(self._code_metadata, self._code_obj)
834
+ if self._info:
835
+ self._flowspec = MetaflowPackage.cls_get_content(
836
+ self._code_metadata, self._code_obj, self._info["script"]
837
+ )
838
+ else:
839
+ raise MetaflowInternalError("Code package metadata is invalid.")
833
840
 
834
841
  @property
835
842
  def path(self) -> str:
@@ -877,7 +884,9 @@ class MetaflowCode(object):
877
884
  TarFile
878
885
  TarFile for everything in this code package
879
886
  """
880
- return self._tar
887
+ if self._backend.type == "tgz":
888
+ return self._backend.cls_open(self._code_obj)
889
+ raise RuntimeError("Archive is not a tarball")
881
890
 
882
891
  def extract(self) -> TemporaryDirectory:
883
892
  """
@@ -908,27 +917,10 @@ class MetaflowCode(object):
908
917
  The directory and its contents are automatically deleted when
909
918
  this object is garbage collected.
910
919
  """
911
- exclusions = [
912
- "metaflow/",
913
- "metaflow_extensions/",
914
- "INFO",
915
- "CONFIG_PARAMETERS",
916
- "conda.manifest",
917
- # This file is created when using the conda/pypi features available in
918
- # nflx-metaflow-extensions: https://github.com/Netflix/metaflow-nflx-extensions
919
- "condav2-1.cnd",
920
- ]
921
- members = [
922
- m
923
- for m in self.tarball.getmembers()
924
- if not any(
925
- (x.endswith("/") and m.name.startswith(x)) or (m.name == x)
926
- for x in exclusions
927
- )
928
- ]
929
-
930
920
  tmp = TemporaryDirectory()
931
- self.tarball.extractall(tmp.name, members)
921
+ MetaflowPackage.cls_extract_into(
922
+ self._code_metadata, self._code_obj, tmp.name, ContentType.USER_CONTENT
923
+ )
932
924
  return tmp
933
925
 
934
926
  @property
@@ -7,7 +7,6 @@ import pathlib
7
7
  import re
8
8
  import time
9
9
  import typing
10
-
11
10
  from datetime import datetime
12
11
  from io import StringIO
13
12
  from types import ModuleType
@@ -335,6 +334,8 @@ class StubGenerator:
335
334
 
336
335
  # Imports that are needed at the top of the file
337
336
  self._imports = set() # type: Set[str]
337
+
338
+ self._sub_module_imports = set() # type: Set[Tuple[str, str]]``
338
339
  # Typing imports (behind if TYPE_CHECKING) that are needed at the top of the file
339
340
  self._typing_imports = set() # type: Set[str]
340
341
  # Typevars that are defined
@@ -643,6 +644,21 @@ class StubGenerator:
643
644
  "deployer"
644
645
  ] = (self._current_module_name + "." + name)
645
646
 
647
+ # Handle TypedDict gracefully for Python 3.7 compatibility
648
+ # _TypedDictMeta is not available in Python 3.7
649
+ typed_dict_meta = getattr(typing, "_TypedDictMeta", None)
650
+ if typed_dict_meta is not None and isinstance(clazz, typed_dict_meta):
651
+ self._sub_module_imports.add(("typing", "TypedDict"))
652
+ total_flag = getattr(clazz, "__total__", False)
653
+ buff = StringIO()
654
+ # Emit the TypedDict base and total flag
655
+ buff.write(f"class {name}(TypedDict, total={total_flag}):\n")
656
+ # Write out each field from __annotations__
657
+ for field_name, field_type in clazz.__annotations__.items():
658
+ ann = self._get_element_name_with_module(field_type)
659
+ buff.write(f"{TAB}{field_name}: {ann}\n")
660
+ return buff.getvalue()
661
+
646
662
  buff = StringIO()
647
663
  # Class prototype
648
664
  buff.write("class " + name.split(".")[-1] + "(")
@@ -987,7 +1003,6 @@ class StubGenerator:
987
1003
  ]
988
1004
 
989
1005
  docs = split_docs(raw_doc, section_boundaries)
990
-
991
1006
  parameters, no_arg_version = parse_params_from_doc(docs["param_doc"])
992
1007
 
993
1008
  if docs["add_to_current_doc"]:
@@ -1515,6 +1530,8 @@ class StubGenerator:
1515
1530
  f.write("import " + module + "\n")
1516
1531
  if module == "typing":
1517
1532
  imported_typing = True
1533
+ for module, sub_module in self._sub_module_imports:
1534
+ f.write(f"from {module} import {sub_module}\n")
1518
1535
  if self._typing_imports:
1519
1536
  if not imported_typing:
1520
1537
  f.write("import typing\n")
@@ -99,7 +99,6 @@ class TaskDataStore(object):
99
99
  mode="r",
100
100
  allow_not_done=False,
101
101
  ):
102
-
103
102
  self._storage_impl = flow_datastore._storage_impl
104
103
  self.TYPE = self._storage_impl.TYPE
105
104
  self._ca_store = flow_datastore.ca_store
metaflow/debug.py CHANGED
@@ -42,6 +42,11 @@ class Debug(object):
42
42
  filename = inspect.stack()[1][1]
43
43
  print("debug[%s %s:%s]: %s" % (typ, filename, lineno, s), file=sys.stderr)
44
44
 
45
+ def __getattr__(self, name):
46
+ # Small piece of code to get pyright and other linters to recognize that there
47
+ # are dynamic attributes.
48
+ return getattr(self, name)
49
+
45
50
  def noop(self, args):
46
51
  pass
47
52