torchx-nightly 2025.7.9__py3-none-any.whl → 2025.11.12__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.
- torchx/{schedulers/ray/__init__.py → _version.py} +3 -1
- torchx/cli/cmd_list.py +1 -2
- torchx/cli/cmd_run.py +202 -28
- torchx/cli/cmd_tracker.py +1 -1
- torchx/components/__init__.py +1 -8
- torchx/components/dist.py +9 -3
- torchx/components/integration_tests/component_provider.py +2 -2
- torchx/components/utils.py +1 -1
- torchx/distributed/__init__.py +1 -1
- torchx/runner/api.py +92 -81
- torchx/runner/config.py +11 -9
- torchx/runner/events/__init__.py +20 -10
- torchx/runner/events/api.py +1 -1
- torchx/schedulers/__init__.py +7 -10
- torchx/schedulers/api.py +20 -15
- torchx/schedulers/aws_batch_scheduler.py +45 -2
- torchx/schedulers/docker_scheduler.py +3 -0
- torchx/schedulers/kubernetes_scheduler.py +200 -17
- torchx/schedulers/local_scheduler.py +1 -0
- torchx/schedulers/slurm_scheduler.py +160 -26
- torchx/specs/__init__.py +23 -6
- torchx/specs/api.py +279 -33
- torchx/specs/builders.py +109 -28
- torchx/specs/file_linter.py +117 -53
- torchx/specs/finder.py +25 -37
- torchx/specs/named_resources_aws.py +13 -2
- torchx/tracker/__init__.py +2 -2
- torchx/tracker/api.py +1 -1
- torchx/util/entrypoints.py +1 -6
- torchx/util/strings.py +1 -1
- torchx/util/types.py +12 -1
- torchx/version.py +2 -2
- torchx/workspace/api.py +102 -5
- {torchx_nightly-2025.7.9.dist-info → torchx_nightly-2025.11.12.dist-info}/METADATA +34 -48
- {torchx_nightly-2025.7.9.dist-info → torchx_nightly-2025.11.12.dist-info}/RECORD +39 -51
- {torchx_nightly-2025.7.9.dist-info → torchx_nightly-2025.11.12.dist-info}/WHEEL +1 -1
- torchx/examples/pipelines/__init__.py +0 -0
- torchx/examples/pipelines/kfp/__init__.py +0 -0
- torchx/examples/pipelines/kfp/advanced_pipeline.py +0 -289
- torchx/examples/pipelines/kfp/dist_pipeline.py +0 -71
- torchx/examples/pipelines/kfp/intro_pipeline.py +0 -83
- torchx/pipelines/kfp/__init__.py +0 -30
- torchx/pipelines/kfp/adapter.py +0 -274
- torchx/pipelines/kfp/version.py +0 -19
- torchx/schedulers/gcp_batch_scheduler.py +0 -497
- torchx/schedulers/ray/ray_common.py +0 -22
- torchx/schedulers/ray/ray_driver.py +0 -307
- torchx/schedulers/ray_scheduler.py +0 -454
- {torchx_nightly-2025.7.9.dist-info → torchx_nightly-2025.11.12.dist-info}/entry_points.txt +0 -0
- {torchx_nightly-2025.7.9.dist-info → torchx_nightly-2025.11.12.dist-info/licenses}/LICENSE +0 -0
- {torchx_nightly-2025.7.9.dist-info → torchx_nightly-2025.11.12.dist-info}/top_level.txt +0 -0
torchx/specs/__init__.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
1
|
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
2
|
# All rights reserved.
|
|
4
3
|
#
|
|
@@ -13,6 +12,8 @@ used by components to define the apps which can then be launched via a TorchX
|
|
|
13
12
|
scheduler or pipeline adapter.
|
|
14
13
|
"""
|
|
15
14
|
import difflib
|
|
15
|
+
|
|
16
|
+
import os
|
|
16
17
|
from typing import Callable, Dict, Mapping, Optional
|
|
17
18
|
|
|
18
19
|
from torchx.specs.api import (
|
|
@@ -42,9 +43,11 @@ from torchx.specs.api import (
|
|
|
42
43
|
RoleStatus,
|
|
43
44
|
runopt,
|
|
44
45
|
runopts,
|
|
46
|
+
TORCHX_HOME,
|
|
45
47
|
UnknownAppException,
|
|
46
48
|
UnknownSchedulerException,
|
|
47
49
|
VolumeMount,
|
|
50
|
+
Workspace,
|
|
48
51
|
)
|
|
49
52
|
from torchx.specs.builders import make_app_handle, materialize_appdef, parse_mounts
|
|
50
53
|
|
|
@@ -52,14 +55,22 @@ from torchx.util.entrypoints import load_group
|
|
|
52
55
|
|
|
53
56
|
from torchx.util.modules import import_attr
|
|
54
57
|
|
|
55
|
-
|
|
58
|
+
GiB: int = 1024
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
ResourceFactory = Callable[[], Resource]
|
|
62
|
+
|
|
63
|
+
AWS_NAMED_RESOURCES: Mapping[str, ResourceFactory] = import_attr(
|
|
56
64
|
"torchx.specs.named_resources_aws", "NAMED_RESOURCES", default={}
|
|
57
65
|
)
|
|
58
|
-
GENERIC_NAMED_RESOURCES: Mapping[str,
|
|
66
|
+
GENERIC_NAMED_RESOURCES: Mapping[str, ResourceFactory] = import_attr(
|
|
59
67
|
"torchx.specs.named_resources_generic", "NAMED_RESOURCES", default={}
|
|
60
68
|
)
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
CUSTOM_NAMED_RESOURCES: Mapping[str, ResourceFactory] = import_attr(
|
|
70
|
+
os.environ.get("TORCHX_CUSTOM_NAMED_RESOURCES", "torchx.specs.fb.named_resources"),
|
|
71
|
+
"NAMED_RESOURCES",
|
|
72
|
+
default={},
|
|
73
|
+
)
|
|
63
74
|
|
|
64
75
|
|
|
65
76
|
def _load_named_resources() -> Dict[str, Callable[[], Resource]]:
|
|
@@ -69,6 +80,7 @@ def _load_named_resources() -> Dict[str, Callable[[], Resource]]:
|
|
|
69
80
|
for name, resource in {
|
|
70
81
|
**GENERIC_NAMED_RESOURCES,
|
|
71
82
|
**AWS_NAMED_RESOURCES,
|
|
83
|
+
**CUSTOM_NAMED_RESOURCES,
|
|
72
84
|
**resource_methods,
|
|
73
85
|
}.items():
|
|
74
86
|
materialized_resources[name] = resource
|
|
@@ -122,7 +134,7 @@ def resource(
|
|
|
122
134
|
|
|
123
135
|
If ``h`` is specified then it is used to look up the
|
|
124
136
|
resource specs from the list of registered named resources.
|
|
125
|
-
See `registering named resource <https://pytorch.org/torchx/latest/advanced.html#registering-named-resources>`_.
|
|
137
|
+
See `registering named resource <https://meta-pytorch.org/torchx/latest/advanced.html#registering-named-resources>`_.
|
|
126
138
|
|
|
127
139
|
Otherwise a ``Resource`` object is created from the raw resource specs.
|
|
128
140
|
|
|
@@ -225,5 +237,10 @@ __all__ = [
|
|
|
225
237
|
"make_app_handle",
|
|
226
238
|
"materialize_appdef",
|
|
227
239
|
"parse_mounts",
|
|
240
|
+
"torchx_run_args_from_argparse",
|
|
241
|
+
"torchx_run_args_from_json",
|
|
242
|
+
"TorchXRunArgs",
|
|
228
243
|
"ALL",
|
|
244
|
+
"TORCHX_HOME",
|
|
245
|
+
"Workspace",
|
|
229
246
|
]
|
torchx/specs/api.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
1
|
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
2
|
# All rights reserved.
|
|
4
3
|
#
|
|
@@ -12,11 +11,15 @@ import copy
|
|
|
12
11
|
import inspect
|
|
13
12
|
import json
|
|
14
13
|
import logging as logger
|
|
14
|
+
import os
|
|
15
|
+
import pathlib
|
|
15
16
|
import re
|
|
17
|
+
import shutil
|
|
16
18
|
import typing
|
|
19
|
+
import warnings
|
|
17
20
|
from dataclasses import asdict, dataclass, field
|
|
18
21
|
from datetime import datetime
|
|
19
|
-
from enum import Enum
|
|
22
|
+
from enum import Enum, IntEnum
|
|
20
23
|
from json import JSONDecodeError
|
|
21
24
|
from string import Template
|
|
22
25
|
from typing import (
|
|
@@ -67,6 +70,32 @@ YELLOW_BOLD = "\033[1;33m"
|
|
|
67
70
|
RESET = "\033[0m"
|
|
68
71
|
|
|
69
72
|
|
|
73
|
+
def TORCHX_HOME(*subdir_paths: str) -> pathlib.Path:
|
|
74
|
+
"""
|
|
75
|
+
Path to the "dot-directory" for torchx.
|
|
76
|
+
Defaults to `~/.torchx` and is overridable via the `TORCHX_HOME` environment variable.
|
|
77
|
+
|
|
78
|
+
Usage:
|
|
79
|
+
|
|
80
|
+
.. doc-test::
|
|
81
|
+
|
|
82
|
+
from pathlib import Path
|
|
83
|
+
from torchx.specs import TORCHX_HOME
|
|
84
|
+
|
|
85
|
+
assert TORCHX_HOME() == Path.home() / ".torchx"
|
|
86
|
+
assert TORCHX_HOME("conda-pack-out") == Path.home() / ".torchx" / "conda-pack-out"
|
|
87
|
+
```
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
default_dir = str(pathlib.Path.home() / ".torchx")
|
|
91
|
+
torchx_home = pathlib.Path(os.getenv("TORCHX_HOME", default_dir))
|
|
92
|
+
|
|
93
|
+
torchx_home = torchx_home / os.path.sep.join(subdir_paths)
|
|
94
|
+
torchx_home.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
|
|
96
|
+
return torchx_home
|
|
97
|
+
|
|
98
|
+
|
|
70
99
|
# ========================================
|
|
71
100
|
# ==== Distributed AppDef API =======
|
|
72
101
|
# ========================================
|
|
@@ -83,6 +112,8 @@ class Resource:
|
|
|
83
112
|
memMB: MB of ram
|
|
84
113
|
capabilities: additional hardware specs (interpreted by scheduler)
|
|
85
114
|
devices: a list of named devices with their quantities
|
|
115
|
+
tags: metadata tags for the resource (not interpreted by schedulers)
|
|
116
|
+
used to add non-functional information about resources (e.g. whether it is an alias of another resource)
|
|
86
117
|
|
|
87
118
|
Note: you should prefer to use named_resources instead of specifying the raw
|
|
88
119
|
resource requirement directly.
|
|
@@ -93,6 +124,7 @@ class Resource:
|
|
|
93
124
|
memMB: int
|
|
94
125
|
capabilities: Dict[str, Any] = field(default_factory=dict)
|
|
95
126
|
devices: Dict[str, int] = field(default_factory=dict)
|
|
127
|
+
tags: Dict[str, object] = field(default_factory=dict)
|
|
96
128
|
|
|
97
129
|
@staticmethod
|
|
98
130
|
def copy(original: "Resource", **capabilities: Any) -> "Resource":
|
|
@@ -101,6 +133,7 @@ class Resource:
|
|
|
101
133
|
are present in the original resource and as parameter, the one from parameter
|
|
102
134
|
will be used.
|
|
103
135
|
"""
|
|
136
|
+
|
|
104
137
|
res_capabilities = dict(original.capabilities)
|
|
105
138
|
res_capabilities.update(capabilities)
|
|
106
139
|
return Resource(
|
|
@@ -319,6 +352,121 @@ class DeviceMount:
|
|
|
319
352
|
permissions: str = "rwm"
|
|
320
353
|
|
|
321
354
|
|
|
355
|
+
@dataclass
|
|
356
|
+
class Workspace:
|
|
357
|
+
"""
|
|
358
|
+
Specifies a local "workspace" (a set of directories). Workspaces are ad-hoc built
|
|
359
|
+
into an (usually ephemeral) image. This effectively mirrors the local code changes
|
|
360
|
+
at job submission time.
|
|
361
|
+
|
|
362
|
+
For example:
|
|
363
|
+
|
|
364
|
+
1. ``projects={"~/github/torch": "torch"}`` copies ``~/github/torch/**`` into ``$REMOTE_WORKSPACE_ROOT/torch/**``
|
|
365
|
+
2. ``projects={"~/github/torch": ""}`` copies ``~/github/torch/**`` into ``$REMOTE_WORKSPACE_ROOT/**``
|
|
366
|
+
|
|
367
|
+
The exact location of ``$REMOTE_WORKSPACE_ROOT`` is implementation dependent and varies between
|
|
368
|
+
different implementations of :py:class:`~torchx.workspace.api.WorkspaceMixin`.
|
|
369
|
+
Check the scheduler documentation for details on which workspace it supports.
|
|
370
|
+
|
|
371
|
+
Note: ``projects`` maps the location of the local project to a sub-directory in the remote workspace root directory.
|
|
372
|
+
Typically the local project location is a directory path (e.g. ``/home/foo/github/torch``).
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
Attributes:
|
|
376
|
+
projects: mapping of local project to the sub-dir in the remote workspace dir.
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
projects: dict[str, str]
|
|
380
|
+
|
|
381
|
+
def __bool__(self) -> bool:
|
|
382
|
+
"""False if no projects mapping. Lets us use workspace object in an if-statement"""
|
|
383
|
+
return bool(self.projects)
|
|
384
|
+
|
|
385
|
+
def __eq__(self, other: object) -> bool:
|
|
386
|
+
if not isinstance(other, Workspace):
|
|
387
|
+
return False
|
|
388
|
+
return self.projects == other.projects
|
|
389
|
+
|
|
390
|
+
def __hash__(self) -> int:
|
|
391
|
+
# makes it possible to use Workspace as the key in the workspace build cache
|
|
392
|
+
# see WorkspaceMixin.caching_build_workspace_and_update_role
|
|
393
|
+
return hash(frozenset(self.projects.items()))
|
|
394
|
+
|
|
395
|
+
def is_unmapped_single_project(self) -> bool:
|
|
396
|
+
"""
|
|
397
|
+
Returns ``True`` if this workspace only has 1 project
|
|
398
|
+
and its target mapping is an empty string.
|
|
399
|
+
"""
|
|
400
|
+
return len(self.projects) == 1 and not next(iter(self.projects.values()))
|
|
401
|
+
|
|
402
|
+
def merge_into(self, outdir: str | pathlib.Path) -> None:
|
|
403
|
+
"""
|
|
404
|
+
Copies each project dir of this workspace into the specified ``outdir``.
|
|
405
|
+
Each project dir is copied into ``{outdir}/{target}`` where ``target`` is
|
|
406
|
+
the target mapping of the project dir.
|
|
407
|
+
|
|
408
|
+
For example:
|
|
409
|
+
|
|
410
|
+
.. code-block:: python
|
|
411
|
+
from os.path import expanduser
|
|
412
|
+
|
|
413
|
+
workspace = Workspace(
|
|
414
|
+
projects={
|
|
415
|
+
expanduser("~/workspace/torch"): "torch",
|
|
416
|
+
expanduser("~/workspace/my_project": "")
|
|
417
|
+
}
|
|
418
|
+
)
|
|
419
|
+
workspace.merge_into(expanduser("~/tmp"))
|
|
420
|
+
|
|
421
|
+
Copies:
|
|
422
|
+
|
|
423
|
+
* ``~/workspace/torch/**`` into ``~/tmp/torch/**``
|
|
424
|
+
* ``~/workspace/my_project/**`` into ``~/tmp/**``
|
|
425
|
+
|
|
426
|
+
"""
|
|
427
|
+
|
|
428
|
+
for src, dst in self.projects.items():
|
|
429
|
+
dst_path = pathlib.Path(outdir) / dst
|
|
430
|
+
if pathlib.Path(src).is_file():
|
|
431
|
+
shutil.copy2(src, dst_path)
|
|
432
|
+
else: # src is dir
|
|
433
|
+
shutil.copytree(src, dst_path, dirs_exist_ok=True)
|
|
434
|
+
|
|
435
|
+
@staticmethod
|
|
436
|
+
def from_str(workspace: str | None) -> "Workspace":
|
|
437
|
+
import yaml
|
|
438
|
+
|
|
439
|
+
if not workspace:
|
|
440
|
+
return Workspace({})
|
|
441
|
+
|
|
442
|
+
projects = yaml.safe_load(workspace)
|
|
443
|
+
if isinstance(projects, str): # single project workspace
|
|
444
|
+
projects = {projects: ""}
|
|
445
|
+
else: # multi-project workspace
|
|
446
|
+
# Replace None mappings with "" (empty string)
|
|
447
|
+
projects = {k: ("" if v is None else v) for k, v in projects.items()}
|
|
448
|
+
|
|
449
|
+
return Workspace(projects)
|
|
450
|
+
|
|
451
|
+
def __str__(self) -> str:
|
|
452
|
+
"""
|
|
453
|
+
Returns a string representation of the Workspace by concatenating
|
|
454
|
+
the project mappings using ';' as a delimiter and ':' between key and value.
|
|
455
|
+
If the single-project workspace with no target mapping, then simply
|
|
456
|
+
returns the src (local project dir)
|
|
457
|
+
|
|
458
|
+
NOTE: meant to be used for logging purposes not serde.
|
|
459
|
+
Therefore not symmetric with :py:func:`Workspace.from_str`.
|
|
460
|
+
|
|
461
|
+
"""
|
|
462
|
+
if self.is_unmapped_single_project():
|
|
463
|
+
return next(iter(self.projects))
|
|
464
|
+
else:
|
|
465
|
+
return ";".join(
|
|
466
|
+
k if not v else f"{k}:{v}" for k, v in self.projects.items()
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
|
|
322
470
|
@dataclass
|
|
323
471
|
class Role:
|
|
324
472
|
"""
|
|
@@ -371,12 +519,15 @@ class Role:
|
|
|
371
519
|
metadata: Free form information that is associated with the role, for example
|
|
372
520
|
scheduler specific data. The key should follow the pattern: ``$scheduler.$key``
|
|
373
521
|
mounts: a list of mounts on the machine
|
|
522
|
+
workspace: local project directories to be mirrored on the remote job.
|
|
523
|
+
NOTE: The workspace argument provided to the :py:class:`~torchx.runner.api.Runner` APIs
|
|
524
|
+
only takes effect on ``appdef.role[0]`` and overrides this attribute.
|
|
525
|
+
|
|
374
526
|
"""
|
|
375
527
|
|
|
376
528
|
name: str
|
|
377
529
|
image: str
|
|
378
530
|
min_replicas: Optional[int] = None
|
|
379
|
-
base_image: Optional[str] = None # DEPRECATED DO NOT SET, WILL BE REMOVED SOON
|
|
380
531
|
entrypoint: str = MISSING
|
|
381
532
|
args: List[str] = field(default_factory=list)
|
|
382
533
|
env: Dict[str, str] = field(default_factory=dict)
|
|
@@ -386,9 +537,10 @@ class Role:
|
|
|
386
537
|
resource: Resource = field(default_factory=_null_resource)
|
|
387
538
|
port_map: Dict[str, int] = field(default_factory=dict)
|
|
388
539
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
389
|
-
mounts: List[
|
|
390
|
-
|
|
391
|
-
|
|
540
|
+
mounts: List[BindMount | VolumeMount | DeviceMount] = field(default_factory=list)
|
|
541
|
+
workspace: Workspace | None = None
|
|
542
|
+
|
|
543
|
+
# DEPRECATED DO NOT SET, WILL BE REMOVED SOON
|
|
392
544
|
overrides: Dict[str, Any] = field(default_factory=dict)
|
|
393
545
|
|
|
394
546
|
# pyre-ignore
|
|
@@ -788,6 +940,62 @@ class runopt:
|
|
|
788
940
|
opt_type: Type[CfgVal]
|
|
789
941
|
is_required: bool
|
|
790
942
|
help: str
|
|
943
|
+
aliases: list[str] | None = None
|
|
944
|
+
deprecated_aliases: list[str] | None = None
|
|
945
|
+
|
|
946
|
+
@property
|
|
947
|
+
def is_type_list_of_str(self) -> bool:
|
|
948
|
+
"""
|
|
949
|
+
Checks if the option type is a list of strings.
|
|
950
|
+
|
|
951
|
+
Returns:
|
|
952
|
+
bool: True if the option type is either List[str] or list[str], False otherwise.
|
|
953
|
+
"""
|
|
954
|
+
return self.opt_type in (List[str], list[str])
|
|
955
|
+
|
|
956
|
+
@property
|
|
957
|
+
def is_type_dict_of_str(self) -> bool:
|
|
958
|
+
"""
|
|
959
|
+
Checks if the option type is a dict of string keys to string values.
|
|
960
|
+
|
|
961
|
+
Returns:
|
|
962
|
+
bool: True if the option type is either Dict[str, str] or dict[str, str], False otherwise.
|
|
963
|
+
"""
|
|
964
|
+
return self.opt_type in (Dict[str, str], dict[str, str])
|
|
965
|
+
|
|
966
|
+
def cast_to_type(self, value: str) -> CfgVal:
|
|
967
|
+
"""Casts the given `value` (in its string representation) to the type of this run option.
|
|
968
|
+
Below are the cast rules for each option type and value literal:
|
|
969
|
+
|
|
970
|
+
1. opt_type=str, value="foo" -> "foo"
|
|
971
|
+
1. opt_type=bool, value="True"/"False" -> True/False
|
|
972
|
+
1. opt_type=int, value="1" -> 1
|
|
973
|
+
1. opt_type=float, value="1.1" -> 1.1
|
|
974
|
+
1. opt_type=list[str]/List[str], value="a,b,c" or value="a;b;c" -> ["a", "b", "c"]
|
|
975
|
+
1. opt_type=dict[str,str]/Dict[str,str],
|
|
976
|
+
value="key1:val1,key2:val2" or value="key1:val1;key2:val2" -> {"key1": "val1", "key2": "val2"}
|
|
977
|
+
|
|
978
|
+
NOTE: dict parsing uses ":" as the kv separator (rather than the standard "=") because "=" is used
|
|
979
|
+
at the top-level cfg to parse runopts (notice the plural) from the CLI. Originally torchx only supported
|
|
980
|
+
primitives and list[str] as CfgVal but dict[str,str] was added in https://github.com/meta-pytorch/torchx/pull/855
|
|
981
|
+
"""
|
|
982
|
+
|
|
983
|
+
if self.opt_type is None:
|
|
984
|
+
raise ValueError("runopt's opt_type cannot be `None`")
|
|
985
|
+
elif self.opt_type == bool:
|
|
986
|
+
return value.lower() == "true"
|
|
987
|
+
elif self.opt_type in (List[str], list[str]):
|
|
988
|
+
# lists may be ; or , delimited
|
|
989
|
+
# also deal with trailing "," by removing empty strings
|
|
990
|
+
return [v for v in value.replace(";", ",").split(",") if v]
|
|
991
|
+
elif self.opt_type in (Dict[str, str], dict[str, str]):
|
|
992
|
+
return {
|
|
993
|
+
s.split(":", 1)[0]: s.split(":", 1)[1]
|
|
994
|
+
for s in value.replace(";", ",").split(",")
|
|
995
|
+
}
|
|
996
|
+
else:
|
|
997
|
+
assert self.opt_type in (str, int, float)
|
|
998
|
+
return self.opt_type(value)
|
|
791
999
|
|
|
792
1000
|
|
|
793
1001
|
class runopts:
|
|
@@ -825,6 +1033,7 @@ class runopts:
|
|
|
825
1033
|
|
|
826
1034
|
def __init__(self) -> None:
|
|
827
1035
|
self._opts: Dict[str, runopt] = {}
|
|
1036
|
+
self._alias_to_key: dict[str, str] = {}
|
|
828
1037
|
|
|
829
1038
|
def __iter__(self) -> Iterator[Tuple[str, runopt]]:
|
|
830
1039
|
return self._opts.items().__iter__()
|
|
@@ -852,9 +1061,16 @@ class runopts:
|
|
|
852
1061
|
|
|
853
1062
|
def get(self, name: str) -> Optional[runopt]:
|
|
854
1063
|
"""
|
|
855
|
-
Returns option if any was registered, or None otherwise
|
|
1064
|
+
Returns option if any was registered, or None otherwise.
|
|
1065
|
+
First searches for the option by ``name``, then falls-back to matching ``name`` with any
|
|
1066
|
+
registered aliases.
|
|
1067
|
+
|
|
856
1068
|
"""
|
|
857
|
-
|
|
1069
|
+
if name in self._opts:
|
|
1070
|
+
return self._opts[name]
|
|
1071
|
+
if name in self._alias_to_key:
|
|
1072
|
+
return self._opts[self._alias_to_key[name]]
|
|
1073
|
+
return None
|
|
858
1074
|
|
|
859
1075
|
def resolve(self, cfg: Mapping[str, CfgVal]) -> Dict[str, CfgVal]:
|
|
860
1076
|
"""
|
|
@@ -869,6 +1085,36 @@ class runopts:
|
|
|
869
1085
|
|
|
870
1086
|
for cfg_key, runopt in self._opts.items():
|
|
871
1087
|
val = resolved_cfg.get(cfg_key)
|
|
1088
|
+
resolved_name = None
|
|
1089
|
+
aliases = runopt.aliases or []
|
|
1090
|
+
deprecated_aliases = runopt.deprecated_aliases or []
|
|
1091
|
+
if val is None:
|
|
1092
|
+
for alias in aliases:
|
|
1093
|
+
val = resolved_cfg.get(alias)
|
|
1094
|
+
if alias in cfg or val is not None:
|
|
1095
|
+
resolved_name = alias
|
|
1096
|
+
break
|
|
1097
|
+
for alias in deprecated_aliases:
|
|
1098
|
+
val = resolved_cfg.get(alias)
|
|
1099
|
+
if val is not None:
|
|
1100
|
+
resolved_name = alias
|
|
1101
|
+
use_instead = self._alias_to_key.get(alias)
|
|
1102
|
+
warnings.warn(
|
|
1103
|
+
f"Run option `{alias}` is deprecated, use `{use_instead}` instead",
|
|
1104
|
+
UserWarning,
|
|
1105
|
+
stacklevel=2,
|
|
1106
|
+
)
|
|
1107
|
+
break
|
|
1108
|
+
else:
|
|
1109
|
+
resolved_name = cfg_key
|
|
1110
|
+
for alias in aliases:
|
|
1111
|
+
duplicate_val = resolved_cfg.get(alias)
|
|
1112
|
+
if alias in cfg or duplicate_val is not None:
|
|
1113
|
+
raise InvalidRunConfigException(
|
|
1114
|
+
f"Duplicate opt name. runopt: `{resolved_name}``, is an alias of runopt: `{alias}`",
|
|
1115
|
+
resolved_name,
|
|
1116
|
+
cfg,
|
|
1117
|
+
)
|
|
872
1118
|
|
|
873
1119
|
# check required opt
|
|
874
1120
|
if runopt.is_required and val is None:
|
|
@@ -888,7 +1134,7 @@ class runopts:
|
|
|
888
1134
|
)
|
|
889
1135
|
|
|
890
1136
|
# not required and not set, set to default
|
|
891
|
-
if val is None:
|
|
1137
|
+
if val is None and resolved_name is None:
|
|
892
1138
|
resolved_cfg[cfg_key] = runopt.default
|
|
893
1139
|
return resolved_cfg
|
|
894
1140
|
|
|
@@ -948,27 +1194,11 @@ class runopts:
|
|
|
948
1194
|
|
|
949
1195
|
"""
|
|
950
1196
|
|
|
951
|
-
def _cast_to_type(value: str, opt_type: Type[CfgVal]) -> CfgVal:
|
|
952
|
-
if opt_type == bool:
|
|
953
|
-
return value.lower() == "true"
|
|
954
|
-
elif opt_type == List[str]:
|
|
955
|
-
# lists may be ; or , delimited
|
|
956
|
-
# also deal with trailing "," by removing empty strings
|
|
957
|
-
return [v for v in value.replace(";", ",").split(",") if v]
|
|
958
|
-
elif opt_type == Dict[str, str]:
|
|
959
|
-
return {
|
|
960
|
-
s.split(":", 1)[0]: s.split(":", 1)[1]
|
|
961
|
-
for s in value.replace(";", ",").split(",")
|
|
962
|
-
}
|
|
963
|
-
else:
|
|
964
|
-
# pyre-ignore[19, 6] type won't be dict here as we handled it above
|
|
965
|
-
return opt_type(value)
|
|
966
|
-
|
|
967
1197
|
cfg: Dict[str, CfgVal] = {}
|
|
968
1198
|
for key, val in to_dict(cfg_str).items():
|
|
969
|
-
|
|
970
|
-
if
|
|
971
|
-
cfg[key] =
|
|
1199
|
+
opt = self.get(key)
|
|
1200
|
+
if opt:
|
|
1201
|
+
cfg[key] = opt.cast_to_type(val)
|
|
972
1202
|
else:
|
|
973
1203
|
logger.warning(
|
|
974
1204
|
f"{YELLOW_BOLD}Unknown run option passed to scheduler: {key}={val}{RESET}"
|
|
@@ -982,16 +1212,16 @@ class runopts:
|
|
|
982
1212
|
cfg: Dict[str, CfgVal] = {}
|
|
983
1213
|
cfg_dict = json.loads(json_repr)
|
|
984
1214
|
for key, val in cfg_dict.items():
|
|
985
|
-
|
|
986
|
-
if
|
|
1215
|
+
opt = self.get(key)
|
|
1216
|
+
if opt:
|
|
987
1217
|
# Optional runopt cfg values default their value to None,
|
|
988
1218
|
# but use `_type` to specify their type when provided.
|
|
989
1219
|
# Make sure not to treat None's as lists/dictionaries
|
|
990
1220
|
if val is None:
|
|
991
1221
|
cfg[key] = val
|
|
992
|
-
elif
|
|
1222
|
+
elif opt.is_type_list_of_str:
|
|
993
1223
|
cfg[key] = [str(v) for v in val]
|
|
994
|
-
elif
|
|
1224
|
+
elif opt.is_type_dict_of_str:
|
|
995
1225
|
cfg[key] = {str(k): str(v) for k, v in val.items()}
|
|
996
1226
|
else:
|
|
997
1227
|
cfg[key] = val
|
|
@@ -1004,12 +1234,16 @@ class runopts:
|
|
|
1004
1234
|
help: str,
|
|
1005
1235
|
default: CfgVal = None,
|
|
1006
1236
|
required: bool = False,
|
|
1237
|
+
aliases: Optional[list[str]] = None,
|
|
1238
|
+
deprecated_aliases: Optional[list[str]] = None,
|
|
1007
1239
|
) -> None:
|
|
1008
1240
|
"""
|
|
1009
1241
|
Adds the ``config`` option with the given help string and ``default``
|
|
1010
1242
|
value (if any). If the ``default`` is not specified then this option
|
|
1011
1243
|
is a required option.
|
|
1012
1244
|
"""
|
|
1245
|
+
aliases = aliases or []
|
|
1246
|
+
deprecated_aliases = deprecated_aliases or []
|
|
1013
1247
|
if required and default is not None:
|
|
1014
1248
|
raise ValueError(
|
|
1015
1249
|
f"Required option: {cfg_key} must not specify default value. Given: {default}"
|
|
@@ -1021,7 +1255,19 @@ class runopts:
|
|
|
1021
1255
|
f" Given: {default} ({type(default).__name__})"
|
|
1022
1256
|
)
|
|
1023
1257
|
|
|
1024
|
-
|
|
1258
|
+
opt = runopt(
|
|
1259
|
+
default,
|
|
1260
|
+
type_,
|
|
1261
|
+
required,
|
|
1262
|
+
help,
|
|
1263
|
+
list(set(aliases)),
|
|
1264
|
+
list(set(deprecated_aliases)),
|
|
1265
|
+
)
|
|
1266
|
+
for alias in aliases:
|
|
1267
|
+
self._alias_to_key[alias] = cfg_key
|
|
1268
|
+
for deprecated_alias in deprecated_aliases:
|
|
1269
|
+
self._alias_to_key[deprecated_alias] = cfg_key
|
|
1270
|
+
self._opts[cfg_key] = opt
|
|
1025
1271
|
|
|
1026
1272
|
def update(self, other: "runopts") -> None:
|
|
1027
1273
|
self._opts.update(other._opts)
|