experimaestro 1.11.1__py3-none-any.whl → 2.0.0b4__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 experimaestro might be problematic. Click here for more details.

Files changed (133) hide show
  1. experimaestro/__init__.py +10 -11
  2. experimaestro/annotations.py +167 -206
  3. experimaestro/cli/__init__.py +140 -16
  4. experimaestro/cli/filter.py +42 -74
  5. experimaestro/cli/jobs.py +157 -106
  6. experimaestro/cli/progress.py +269 -0
  7. experimaestro/cli/refactor.py +249 -0
  8. experimaestro/click.py +0 -1
  9. experimaestro/commandline.py +19 -3
  10. experimaestro/connectors/__init__.py +22 -3
  11. experimaestro/connectors/local.py +12 -0
  12. experimaestro/core/arguments.py +192 -37
  13. experimaestro/core/identifier.py +127 -12
  14. experimaestro/core/objects/__init__.py +6 -0
  15. experimaestro/core/objects/config.py +702 -285
  16. experimaestro/core/objects/config_walk.py +24 -6
  17. experimaestro/core/serialization.py +91 -34
  18. experimaestro/core/serializers.py +1 -8
  19. experimaestro/core/subparameters.py +164 -0
  20. experimaestro/core/types.py +198 -83
  21. experimaestro/exceptions.py +26 -0
  22. experimaestro/experiments/cli.py +107 -25
  23. experimaestro/generators.py +50 -9
  24. experimaestro/huggingface.py +3 -1
  25. experimaestro/launcherfinder/parser.py +29 -0
  26. experimaestro/launcherfinder/registry.py +3 -3
  27. experimaestro/launchers/__init__.py +26 -1
  28. experimaestro/launchers/direct.py +12 -0
  29. experimaestro/launchers/slurm/base.py +154 -2
  30. experimaestro/mkdocs/base.py +6 -8
  31. experimaestro/mkdocs/metaloader.py +0 -1
  32. experimaestro/mypy.py +452 -7
  33. experimaestro/notifications.py +75 -16
  34. experimaestro/progress.py +404 -0
  35. experimaestro/rpyc.py +0 -1
  36. experimaestro/run.py +19 -6
  37. experimaestro/scheduler/__init__.py +18 -1
  38. experimaestro/scheduler/base.py +504 -959
  39. experimaestro/scheduler/dependencies.py +43 -28
  40. experimaestro/scheduler/dynamic_outputs.py +259 -130
  41. experimaestro/scheduler/experiment.py +582 -0
  42. experimaestro/scheduler/interfaces.py +474 -0
  43. experimaestro/scheduler/jobs.py +485 -0
  44. experimaestro/scheduler/services.py +186 -12
  45. experimaestro/scheduler/signal_handler.py +32 -0
  46. experimaestro/scheduler/state.py +1 -1
  47. experimaestro/scheduler/state_db.py +388 -0
  48. experimaestro/scheduler/state_provider.py +2345 -0
  49. experimaestro/scheduler/state_sync.py +834 -0
  50. experimaestro/scheduler/workspace.py +52 -10
  51. experimaestro/scriptbuilder.py +7 -0
  52. experimaestro/server/__init__.py +153 -32
  53. experimaestro/server/data/index.css +0 -125
  54. experimaestro/server/data/index.css.map +1 -1
  55. experimaestro/server/data/index.js +194 -58
  56. experimaestro/server/data/index.js.map +1 -1
  57. experimaestro/settings.py +47 -6
  58. experimaestro/sphinx/__init__.py +3 -3
  59. experimaestro/taskglobals.py +20 -0
  60. experimaestro/tests/conftest.py +80 -0
  61. experimaestro/tests/core/test_generics.py +2 -2
  62. experimaestro/tests/identifier_stability.json +45 -0
  63. experimaestro/tests/launchers/bin/sacct +6 -2
  64. experimaestro/tests/launchers/bin/sbatch +4 -2
  65. experimaestro/tests/launchers/common.py +2 -2
  66. experimaestro/tests/launchers/test_slurm.py +80 -0
  67. experimaestro/tests/restart.py +1 -1
  68. experimaestro/tests/tasks/all.py +7 -0
  69. experimaestro/tests/tasks/test_dynamic.py +231 -0
  70. experimaestro/tests/test_checkers.py +2 -2
  71. experimaestro/tests/test_cli_jobs.py +615 -0
  72. experimaestro/tests/test_dependencies.py +11 -17
  73. experimaestro/tests/test_deprecated.py +630 -0
  74. experimaestro/tests/test_environment.py +200 -0
  75. experimaestro/tests/test_experiment.py +3 -3
  76. experimaestro/tests/test_file_progress.py +425 -0
  77. experimaestro/tests/test_file_progress_integration.py +477 -0
  78. experimaestro/tests/test_forward.py +3 -3
  79. experimaestro/tests/test_generators.py +93 -0
  80. experimaestro/tests/test_identifier.py +520 -169
  81. experimaestro/tests/test_identifier_stability.py +458 -0
  82. experimaestro/tests/test_instance.py +16 -21
  83. experimaestro/tests/test_multitoken.py +442 -0
  84. experimaestro/tests/test_mypy.py +433 -0
  85. experimaestro/tests/test_objects.py +314 -30
  86. experimaestro/tests/test_outputs.py +8 -8
  87. experimaestro/tests/test_param.py +22 -26
  88. experimaestro/tests/test_partial_paths.py +231 -0
  89. experimaestro/tests/test_progress.py +2 -50
  90. experimaestro/tests/test_resumable_task.py +480 -0
  91. experimaestro/tests/test_serializers.py +141 -60
  92. experimaestro/tests/test_state_db.py +434 -0
  93. experimaestro/tests/test_subparameters.py +160 -0
  94. experimaestro/tests/test_tags.py +151 -15
  95. experimaestro/tests/test_tasks.py +137 -160
  96. experimaestro/tests/test_token_locking.py +252 -0
  97. experimaestro/tests/test_tokens.py +25 -19
  98. experimaestro/tests/test_types.py +133 -11
  99. experimaestro/tests/test_validation.py +19 -19
  100. experimaestro/tests/test_workspace_triggers.py +158 -0
  101. experimaestro/tests/token_reschedule.py +5 -3
  102. experimaestro/tests/utils.py +2 -2
  103. experimaestro/tokens.py +154 -57
  104. experimaestro/tools/diff.py +8 -1
  105. experimaestro/tui/__init__.py +8 -0
  106. experimaestro/tui/app.py +2303 -0
  107. experimaestro/tui/app.tcss +353 -0
  108. experimaestro/tui/log_viewer.py +228 -0
  109. experimaestro/typingutils.py +11 -2
  110. experimaestro/utils/__init__.py +23 -0
  111. experimaestro/utils/environment.py +148 -0
  112. experimaestro/utils/git.py +129 -0
  113. experimaestro/utils/resources.py +1 -1
  114. experimaestro/version.py +34 -0
  115. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info}/METADATA +70 -39
  116. experimaestro-2.0.0b4.dist-info/RECORD +181 -0
  117. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info}/WHEEL +1 -1
  118. experimaestro-2.0.0b4.dist-info/entry_points.txt +16 -0
  119. experimaestro/compat.py +0 -6
  120. experimaestro/core/objects.pyi +0 -225
  121. experimaestro/server/data/0c35d18bf06992036b69.woff2 +0 -0
  122. experimaestro/server/data/219aa9140e099e6c72ed.woff2 +0 -0
  123. experimaestro/server/data/3a4004a46a653d4b2166.woff +0 -0
  124. experimaestro/server/data/3baa5b8f3469222b822d.woff +0 -0
  125. experimaestro/server/data/4d73cb90e394b34b7670.woff +0 -0
  126. experimaestro/server/data/4ef4218c522f1eb6b5b1.woff2 +0 -0
  127. experimaestro/server/data/5d681e2edae8c60630db.woff +0 -0
  128. experimaestro/server/data/6f420cf17cc0d7676fad.woff2 +0 -0
  129. experimaestro/server/data/c380809fd3677d7d6903.woff2 +0 -0
  130. experimaestro/server/data/f882956fd323fd322f31.woff +0 -0
  131. experimaestro-1.11.1.dist-info/RECORD +0 -158
  132. experimaestro-1.11.1.dist-info/entry_points.txt +0 -17
  133. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info/licenses}/LICENSE +0 -0
experimaestro/__init__.py CHANGED
@@ -4,15 +4,7 @@ from pathlib import Path
4
4
 
5
5
  # Annotations
6
6
  from .annotations import (
7
- config,
8
- task,
9
- param,
10
- ConstantParam,
11
- constant,
12
- option,
13
- pathoption,
14
7
  cache,
15
- Identifier,
16
8
  Array,
17
9
  TagDict,
18
10
  tag,
@@ -21,12 +13,11 @@ from .annotations import (
21
13
  STDOUT,
22
14
  STDERR,
23
15
  deprecate,
24
- # deprecated
25
- argument,
26
16
  initializer,
27
17
  # Method
28
18
  config_only,
29
19
  )
20
+ from .core.types import Identifier
30
21
  from .core.serialization import (
31
22
  load,
32
23
  save,
@@ -47,15 +38,22 @@ from .core.arguments import (
47
38
  field,
48
39
  # Annotations helpers
49
40
  help,
50
- default,
51
41
  )
52
42
  from .generators import pathgenerator, PathGenerator
43
+ from .core.subparameters import (
44
+ subparameters,
45
+ param_group,
46
+ ParameterGroup,
47
+ Subparameters,
48
+ )
53
49
  from .core.objects import (
54
50
  Config,
51
+ InstanceConfig,
55
52
  copyconfig,
56
53
  setmeta,
57
54
  DependentMarker,
58
55
  Task,
56
+ ResumableTask,
59
57
  LightweightTask,
60
58
  ObjectStore,
61
59
  )
@@ -64,6 +62,7 @@ from .core.serializers import SerializationLWTask, PathSerializationLWTask
64
62
  from .core.types import Any, SubmitHook
65
63
  from .launchers import Launcher
66
64
  from .scheduler import Scheduler, experiment, FailedExperiment
65
+ from .exceptions import GracefulTimeout
67
66
  from .scheduler.workspace import Workspace, RunMode
68
67
  from .scheduler.state import get_experiment
69
68
  from .notifications import progress, tqdm
@@ -1,18 +1,14 @@
1
1
  # Import Python modules
2
2
 
3
3
  import inspect
4
- from pathlib import Path
5
- from typing import Callable, Type as TypingType, Optional, TypeVar, Union
4
+ from typing import Callable, Type as TypingType, TypeVar, Union
6
5
  from sortedcontainers import SortedDict
7
6
  import experimaestro.core.objects as objects
8
7
  import experimaestro.core.types as types
9
- from experimaestro.generators import PathGenerator
10
8
 
11
- from .core.arguments import Argument as CoreArgument, field
12
- from .core.objects import Config
13
- from .core.types import Any, Identifier, TypeProxy, Type, ObjectType
9
+ from .core.objects import Config, Task
10
+ from .core.types import Any, TypeProxy, Type
14
11
  from .utils import logger
15
- from .checkers import Checker
16
12
 
17
13
  # --- Annotations to define tasks and types
18
14
 
@@ -24,56 +20,6 @@ def configmethod(method):
24
20
  return method
25
21
 
26
22
 
27
- class config:
28
- """Annotations for experimaestro types"""
29
-
30
- def __init__(self, identifier=None, description=None):
31
- """[summary]
32
-
33
- Keyword Arguments:
34
- identifier {Identifier, str} -- Unique identifier of the type,
35
- generate by default (None)
36
-
37
- description {str} -- (deprecated, use
38
- comments) Description of the config/task, use comments with
39
- (default) None
40
-
41
- register {bool} -- False if the type should not be
42
- registered (debug only)
43
-
44
- The identifier, if not specified, will be set to `X.CLASSNAME`(by order
45
- of priority), where X is:
46
- - the parent identifier
47
- - the module qualified name
48
- """
49
- super().__init__()
50
- self.identifier = identifier
51
- if isinstance(self.identifier, str):
52
- self.identifier = Identifier(self.identifier)
53
-
54
- self.description = description
55
-
56
- def __call__(self, tp: T) -> T:
57
- """Annotate the class
58
-
59
- Depending on whether we are running or configuring,
60
- the behavior is different:
61
-
62
- - when configuring, we return a proxy class
63
- - when running, we return the same class
64
-
65
- Arguments:
66
- tp {type} -- The type
67
-
68
- Keyword Arguments:
69
- basetype {type} -- The base type of the class (Task or Config)
70
- """
71
- assert inspect.isclass(tp), f"{tp} is not a class"
72
-
73
- # Adds to xpminfo for on demand creation of information
74
- return ObjectType(tp, identifier=self.identifier).basetype
75
-
76
-
77
23
  class Array(TypeProxy):
78
24
  """Array of object"""
79
25
 
@@ -94,121 +40,12 @@ class Choice(TypeProxy):
94
40
  return types.StringType
95
41
 
96
42
 
97
- class task(config):
98
- """Register a task"""
99
-
100
- def __init__(self, identifier=None, pythonpath=None, description=None):
101
- super().__init__(identifier, description)
102
- self.pythonpath = pythonpath
103
-
104
- def __call__(self, tp) -> type:
105
- # Register the type
106
- tp = super().__call__(tp)
107
-
108
- def factory(xpmtype):
109
- return xpmtype.getpythontaskcommand(self.pythonpath)
110
-
111
- tp.__getxpmtype__().taskcommandfactory = factory
112
- return tp
113
-
114
-
115
- # --- argument related annotations
116
-
117
-
118
- class param:
119
- """Defines an argument for an experimaestro type"""
120
-
121
- def __init__(
122
- self,
123
- name,
124
- type=None,
125
- default=None,
126
- required: bool = None,
127
- ignored: Optional[bool] = None,
128
- help: Optional[str] = None,
129
- checker: Optional[Checker] = None,
130
- constant: bool = False,
131
- ):
132
- # Determine if required
133
- self.name = name
134
- self.type = Type.fromType(type) if type else None
135
- self.help = help
136
- self.ignored = ignored
137
- self.required = required
138
- self.checker = checker
139
- self.constant = constant
140
-
141
- self.generator = None
142
- self.default = None
143
-
144
- # Set default or generator
145
- if isinstance(default, field):
146
- if default.default is not None:
147
- self.default = default
148
- elif default.default_factory is not None:
149
- self.generator = default.default_factory
150
- else:
151
- self.default = default
152
-
153
- def __call__(self, tp):
154
- # Don't annotate in task mode
155
- tp.__getxpmtype__().addAnnotation(self)
156
- return tp
157
-
158
- def process(self, xpmtype):
159
- # Get type from default if needed
160
- if self.type is None:
161
- if self.default is not None:
162
- self.type = Type.fromType(type(self.default))
163
-
164
- # Type = any if no type
165
- if self.type is None:
166
- self.type = types.Any
167
-
168
- argument = CoreArgument(
169
- self.name,
170
- self.type,
171
- help=self.help,
172
- required=self.required,
173
- ignored=self.ignored,
174
- generator=self.generator,
175
- default=self.default,
176
- checker=self.checker,
177
- constant=self.constant,
178
- )
179
- xpmtype.addArgument(argument)
180
-
181
-
182
- # Just a rebind (back-compatibility)
183
- argument = param
184
-
185
-
186
- class option(param):
187
- """An argument which is ignored
188
-
189
- See argument
190
- """
191
-
192
- def __init__(self, *args, **kwargs):
193
- kwargs["ignored"] = True
194
- super().__init__(*args, **kwargs)
195
-
196
-
197
- class pathoption(param):
198
- """Defines a an argument that will be a relative path (automatically
199
- set by experimaestro)"""
200
-
201
- def __init__(self, name: str, path=None, help=""):
202
- """
203
- :param name: The name of argument (in python)
204
- :param path: The relative path or a function
205
- """
206
- super().__init__(name, type=Path, help=help)
43
+ def config_only(method):
44
+ """Marks a configuration-only method"""
45
+ assert inspect.ismethod(method)
207
46
 
208
- if path is None:
209
- path = name
210
47
 
211
- self.generator = PathGenerator(path)
48
+ # --- Path generators for task stdout/stderr
212
49
 
213
50
 
214
51
  def STDERR(jobcontext, config):
@@ -219,28 +56,31 @@ def STDOUT(jobcontext, config):
219
56
  return "%s.out" % jobcontext.name
220
57
 
221
58
 
222
- class constant(param):
223
- """
224
- A constant argument (useful for versionning tasks)
225
- """
226
-
227
- def __init__(self, name: str, value, type=None, help=""):
228
- super().__init__(name, default=value, constant=True, type=type, help=help)
229
-
59
+ # --- Cache
230
60
 
231
- ConstantParam = constant
232
61
 
62
+ def cache(name: str):
63
+ """Decorator for caching method results to disk.
233
64
 
234
- def config_only(method):
235
- """Marks a configuration-only method"""
236
- assert inspect.ismethod(method)
65
+ The cache is stored in the workspace's config directory, keyed by the
66
+ configuration's identifier.
237
67
 
68
+ Example::
238
69
 
239
- # --- Cache
70
+ class MyConfig(Config):
71
+ data_path: Param[Path]
240
72
 
73
+ @cache("processed.pkl")
74
+ def process(self, cache_path: Path):
75
+ if cache_path.exists():
76
+ return pickle.load(cache_path.open("rb"))
77
+ result = expensive_computation(self.data_path)
78
+ pickle.dump(result, cache_path.open("wb"))
79
+ return result
241
80
 
242
- def cache(name: str):
243
- """Use a cache path for a given config"""
81
+ :param name: Filename for the cache file
82
+ :return: A decorator that wraps the method with caching logic
83
+ """
244
84
 
245
85
  def annotate(method):
246
86
  return objects.cache(method, name)
@@ -252,7 +92,22 @@ def cache(name: str):
252
92
 
253
93
 
254
94
  def tag(value):
255
- """Tag a value"""
95
+ """Tag a parameter value for tracking in experiments.
96
+
97
+ Tagged values appear in experiment logs and can be used for filtering
98
+ and organizing results. Tags are included in the task's ``__tags__``
99
+ dictionary.
100
+
101
+ Example::
102
+
103
+ task = MyTask.C(
104
+ learning_rate=tag(0.001), # Will appear in task tags
105
+ batch_size=32,
106
+ ).submit()
107
+
108
+ :param value: The value to tag (str, int, float, or bool)
109
+ :return: A tagged value wrapper that preserves the original value
110
+ """
256
111
  return objects.TaggedValue(value)
257
112
 
258
113
 
@@ -267,7 +122,19 @@ class TagDict(SortedDict):
267
122
 
268
123
 
269
124
  def tags(value) -> TagDict:
270
- """Return the tags associated with a value"""
125
+ """Return the tags associated with a configuration.
126
+
127
+ Returns a dictionary of all tagged parameter values from this configuration
128
+ and its nested configurations.
129
+
130
+ Example::
131
+
132
+ config = MyTask.C(learning_rate=tag(0.001), epochs=tag(100))
133
+ task_tags = tags(config) # {"learning_rate": 0.001, "epochs": 100}
134
+
135
+ :param value: A configuration object
136
+ :return: A TagDict with tag names as keys and tagged values as values
137
+ """
271
138
  return TagDict(value.__xpm__.tags())
272
139
 
273
140
 
@@ -277,8 +144,20 @@ def _normalizepathcomponent(v: Any):
277
144
  return v
278
145
 
279
146
 
280
- def tagspath(value: Config):
281
- """Return a unique path made of tags and their values"""
147
+ def tagspath(value: Config) -> str:
148
+ """Generate a unique path string from a configuration's tags.
149
+
150
+ Useful for creating tag-based directory structures. Tags are sorted
151
+ alphabetically and joined with underscores.
152
+
153
+ Example::
154
+
155
+ config = MyTask.C(learning_rate=tag(0.001), epochs=tag(100))
156
+ path = tagspath(config) # "epochs=100_learning_rate=0.001"
157
+
158
+ :param value: A configuration object
159
+ :return: A string with sorted tags in ``key=value`` format, joined by ``_``
160
+ """
282
161
  return "_".join(
283
162
  f"""{_normalizepathcomponent(key)}={_normalizepathcomponent(value)}"""
284
163
  for key, value in tags(value).items()
@@ -288,33 +167,101 @@ def tagspath(value: Config):
288
167
  # --- Deprecated
289
168
 
290
169
 
291
- def deprecate(config: Union[TypingType[Config], Callable]):
292
- """Deprecate a configuration / task or
293
- an attribute (via a method)
170
+ def deprecate(
171
+ config_or_target: Union[TypingType[Config], Callable, None] = None,
172
+ *,
173
+ replace: bool = False,
174
+ ):
175
+ """Deprecate a configuration/task class or a parameter.
176
+
177
+ Deprecated configurations maintain backwards compatibility while allowing
178
+ migration to new structures. The identifier is computed from the converted
179
+ configuration, ensuring consistency.
180
+
181
+ **Usage patterns:**
294
182
 
295
- Usage:
183
+ 1. Simple deprecation (class inherits from new class)::
296
184
 
297
185
  @deprecate
298
186
  class OldConfig(NewConfig):
299
187
  pass
300
188
 
301
- # Or only a parameter
302
- class MyConfig():
189
+ 2. Deprecation with conversion::
190
+
191
+ @deprecate(NewConfig)
192
+ class OldConfig(Config):
193
+ value: Param[int]
194
+
195
+ def __convert__(self):
196
+ return NewConfig.C(values=[self.value])
197
+
198
+ 3. Immediate replacement::
199
+
200
+ @deprecate(NewConfig, replace=True)
201
+ class OldConfig(Config):
202
+ value: Param[int]
203
+
204
+ def __convert__(self):
205
+ return NewConfig.C(values=[self.value])
206
+
207
+ 4. Deprecate a parameter::
208
+
209
+ class MyConfig(Config):
210
+ new_param: Param[list[int]]
211
+
303
212
  @deprecate
304
- def oldattribute(self, value):
305
- # Do something with the value
306
- pass
307
- """
308
- if inspect.isclass(config):
309
- config.__getxpmtype__().deprecate()
310
- return config
213
+ def old_param(self, value: int):
214
+ self.new_param = [value]
311
215
 
312
- if inspect.isfunction(config):
216
+ :param config_or_target: Target class for conversion, or the deprecated
217
+ class/method when used as a simple decorator
218
+ :param replace: If True, creating the deprecated class immediately returns
219
+ the converted instance
220
+ """
221
+ # Case 1: @deprecate on a function (deprecated attribute)
222
+ if inspect.isfunction(config_or_target):
313
223
  from experimaestro.core.types import DeprecatedAttribute
314
224
 
315
- return DeprecatedAttribute(config)
225
+ return DeprecatedAttribute(config_or_target)
226
+
227
+ # Case 2: @deprecate (no parens) on a class - legacy pattern
228
+ # The class inherits from its target (NewConfig), not directly from Config
229
+ if config_or_target is not None and inspect.isclass(config_or_target):
230
+ # Check if this looks like a deprecated class (legacy pattern)
231
+ # Legacy pattern: @deprecate class OldConfig(NewConfig) where NewConfig is a Config subclass
232
+ # The deprecated class inherits from exactly one Config subclass (the target)
233
+ # We exclude Config and Task as base classes since those indicate the new pattern
234
+ base_classes_for_new_pattern = (Config, Task)
235
+ if (
236
+ not replace
237
+ and len(config_or_target.__bases__) == 1
238
+ and config_or_target.__bases__[0] not in base_classes_for_new_pattern
239
+ and issubclass(config_or_target.__bases__[0], Config)
240
+ ):
241
+ # This is the legacy pattern: @deprecate on a class
242
+ deprecated_class = config_or_target
243
+ deprecated_class.__getxpmtype__().deprecate()
244
+ return deprecated_class
245
+
246
+ # Otherwise, this is the new pattern: @deprecate(TargetConfig)
247
+ target = config_or_target
316
248
 
317
- raise NotImplementedError("Cannot deprecate %s", config)
249
+ def decorator(deprecated_class: TypingType[Config]):
250
+ deprecated_class.__getxpmtype__().deprecate(target=target, replace=replace)
251
+ return deprecated_class
252
+
253
+ return decorator
254
+
255
+ # Case 3: @deprecate() with parentheses but no arguments (legacy, uses parent class)
256
+ if config_or_target is None:
257
+
258
+ def decorator(deprecated_class: TypingType[Config]):
259
+ deprecated_class.__getxpmtype__().deprecate()
260
+ return deprecated_class
261
+
262
+ return decorator
263
+
264
+ raise NotImplementedError("Cannot deprecate %s" % config_or_target)
318
265
 
319
266
 
320
267
  def deprecateClass(klass):
@@ -337,7 +284,21 @@ def deprecateClass(klass):
337
284
 
338
285
 
339
286
  def initializer(method):
340
- """Defines a method as an initializer that can only be called once"""
287
+ """Decorator for methods that should only execute once.
288
+
289
+ After the first call, subsequent calls return the cached result.
290
+ This is useful for lazy initialization of expensive resources.
291
+
292
+ Example::
293
+
294
+ class MyConfig(Config):
295
+ @initializer
296
+ def model(self):
297
+ return load_expensive_model()
298
+
299
+ :param method: The method to wrap
300
+ :return: A wrapper that caches the result after first execution
301
+ """
341
302
 
342
303
  def wrapper(self, *args, **kwargs):
343
304
  value = method(self, *args, **kwargs)