Skip to content

Commit 93548fa

Browse files
kurmanfacebook-github-bot
authored andcommitted
Move factory/builder methods from datastruct specific "specs.api" to "specs.builders". (#499)
Summary: Pull Request resolved: #499 Extract functional module out out `specs.api` to new `specs.builders` module. - Diff/PR should be 100% refactoring without any functional impact. - `parse_app_handle` could not be moved due to external usage via `specs.api` reference and will be handled separately #368 Reviewed By: d4l3k Differential Revision: D36617402 fbshipit-source-id: 563aa15aa354244d09a8eb32da7f5275e7c9ade0
1 parent 5a88b4d commit 93548fa

File tree

11 files changed

+741
-719
lines changed

11 files changed

+741
-719
lines changed

torchx/cli/cmd_cancel.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from torchx.cli.cmd_base import SubCommand
1212
from torchx.runner import get_runner
13-
from torchx.specs import api
13+
from torchx.specs.api import parse_app_handle
1414

1515
logger: logging.Logger = logging.getLogger(__name__)
1616

@@ -25,6 +25,6 @@ def add_arguments(self, subparser: argparse.ArgumentParser) -> None:
2525

2626
def run(self, args: argparse.Namespace) -> None:
2727
app_handle = args.app_handle
28-
_, session_name, _ = api.parse_app_handle(app_handle)
28+
_, session_name, _ = parse_app_handle(app_handle)
2929
runner = get_runner(name=session_name)
3030
runner.cancel(app_handle)

torchx/cli/cmd_describe.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from torchx.cli.cmd_base import SubCommand
1515
from torchx.runner import get_runner
16-
from torchx.specs import api
16+
from torchx.specs.api import parse_app_handle
1717

1818
logger: logging.Logger = logging.getLogger(__name__)
1919

@@ -28,7 +28,7 @@ def add_arguments(self, subparser: argparse.ArgumentParser) -> None:
2828

2929
def run(self, args: argparse.Namespace) -> None:
3030
app_handle = args.app_handle
31-
scheduler, session_name, app_id = api.parse_app_handle(app_handle)
31+
scheduler, session_name, app_id = parse_app_handle(app_handle)
3232
runner = get_runner(name=session_name)
3333
app = runner.describe(app_handle)
3434

torchx/cli/cmd_log.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
from torchx.cli.colors import ENDC, GREEN
2121
from torchx.runner import get_runner, Runner
2222
from torchx.schedulers.api import Stream
23-
from torchx.specs.api import is_started, make_app_handle
23+
from torchx.specs.api import is_started
24+
from torchx.specs.builders import make_app_handle
2425

2526
logger: logging.Logger = logging.getLogger(__name__)
2627

torchx/cli/cmd_status.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from torchx.cli.cmd_base import SubCommand
1818
from torchx.runner import get_runner
1919
from torchx.specs import api
20-
from torchx.specs.api import NONE
20+
from torchx.specs.api import NONE, parse_app_handle
2121

2222
logger: logging.Logger = logging.getLogger(__name__)
2323

@@ -159,7 +159,7 @@ def add_arguments(self, subparser: argparse.ArgumentParser) -> None:
159159

160160
def run(self, args: argparse.Namespace) -> None:
161161
app_handle = args.app_handle
162-
scheduler, session_name, app_id = api.parse_app_handle(app_handle)
162+
scheduler, session_name, app_id = parse_app_handle(app_handle)
163163
runner = get_runner(name=session_name)
164164
app_status = runner.status(app_handle)
165165
filter_roles = parse_list_arg(args.roles)

torchx/components/component_test_base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import unittest
1919
from types import ModuleType
2020

21-
from torchx.specs.api import _create_args_parser
21+
from torchx.specs.builders import _create_args_parser
2222
from torchx.specs.finder import get_component
2323

2424

torchx/runner/api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
AppHandle,
2424
AppStatus,
2525
CfgVal,
26-
from_function,
2726
make_app_handle,
27+
materialize_appdef,
2828
parse_app_handle,
2929
runopts,
3030
UnknownAppException,
@@ -161,7 +161,7 @@ def dryrun_component(
161161
component, but just returns what "would" have run.
162162
"""
163163
component_def = get_component(component)
164-
app = from_function(
164+
app = materialize_appdef(
165165
component_def.fn,
166166
component_args,
167167
self._component_defaults.get(component, None),

torchx/specs/__init__.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,15 @@
2626
BindMount,
2727
CfgVal,
2828
DeviceMount,
29-
from_function,
3029
get_type_name,
3130
InvalidRunConfigException,
3231
is_terminal,
3332
macros,
34-
make_app_handle,
3533
MalformedAppHandleException,
3634
MISSING,
3735
NONE,
3836
NULL_RESOURCE,
3937
parse_app_handle,
40-
parse_mounts,
4138
ReplicaState,
4239
ReplicaStatus,
4340
Resource,
@@ -50,7 +47,7 @@
5047
UnknownSchedulerException,
5148
VolumeMount,
5249
)
53-
50+
from .builders import make_app_handle, materialize_appdef, parse_mounts # noqa
5451

5552
GiB: int = 1024
5653

torchx/specs/api.py

Lines changed: 1 addition & 227 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55
# This source code is licensed under the BSD-style license found in the
66
# LICENSE file in the root directory of this source tree.
77

8-
import argparse
98
import copy
10-
import inspect
119
import json
1210
from dataclasses import asdict, dataclass, field
1311
from enum import Enum
@@ -28,15 +26,7 @@
2826
)
2927

3028
import yaml
31-
from torchx.specs.file_linter import get_fn_docstring, TorchXArgumentHelpFormatter
32-
from torchx.util.types import (
33-
decode_from_string,
34-
decode_optional,
35-
get_argparse_param_type,
36-
is_bool,
37-
is_primitive,
38-
to_dict,
39-
)
29+
from torchx.util.types import to_dict
4030

4131

4232
# ========================================
@@ -832,10 +822,6 @@ def __init__(self, app_handle: "AppHandle") -> None:
832822
)
833823

834824

835-
def make_app_handle(scheduler_backend: str, session_name: str, app_id: str) -> str:
836-
return f"{scheduler_backend}://{session_name}/{app_id}"
837-
838-
839825
def parse_app_handle(app_handle: AppHandle) -> Tuple[str, str, str]:
840826
"""
841827
parses the app handle into ```(scheduler_backend, session_name, and app_id)```
@@ -851,215 +837,3 @@ def parse_app_handle(app_handle: AppHandle) -> Tuple[str, str, str]:
851837
raise MalformedAppHandleException(app_handle)
852838
gd = match.groupdict()
853839
return gd["scheduler_backend"], gd["session_name"], gd["app_id"]
854-
855-
856-
def _create_args_parser(
857-
cmpnt_fn: Callable[..., AppDef], cmpnt_defaults: Optional[Dict[str, str]] = None
858-
) -> argparse.ArgumentParser:
859-
parameters = inspect.signature(cmpnt_fn).parameters
860-
function_desc, args_desc = get_fn_docstring(cmpnt_fn)
861-
script_parser = argparse.ArgumentParser(
862-
prog=f"torchx run <run args...> {cmpnt_fn.__name__} ",
863-
description=function_desc,
864-
formatter_class=TorchXArgumentHelpFormatter,
865-
# enables components to have "h" as a parameter
866-
# otherwise argparse by default adds -h/--help as the help argument
867-
# we still add --help but reserve "-"h" to be used as a component argument
868-
add_help=False,
869-
)
870-
# add help manually since we disabled auto help to allow "h" in component arg
871-
script_parser.add_argument(
872-
"--help",
873-
action="help",
874-
default=argparse.SUPPRESS,
875-
help="show this help message and exit",
876-
)
877-
878-
class _reminder_action(argparse.Action):
879-
def __call__(
880-
self,
881-
parser: argparse.ArgumentParser,
882-
namespace: argparse.Namespace,
883-
values: Any,
884-
option_string: Optional[str] = None,
885-
) -> None:
886-
setattr(
887-
namespace,
888-
self.dest,
889-
(self.default or "").split() if len(values) == 0 else values,
890-
)
891-
892-
for param_name, parameter in parameters.items():
893-
param_desc = args_desc[parameter.name]
894-
args: Dict[str, Any] = {
895-
"help": param_desc,
896-
"type": get_argparse_param_type(parameter),
897-
}
898-
# set defaults specified in the component function declaration
899-
if parameter.default != inspect.Parameter.empty:
900-
if is_bool(type(parameter.default)):
901-
args["default"] = str(parameter.default)
902-
else:
903-
args["default"] = parameter.default
904-
905-
# set defaults supplied directly to this method (overwrites the declared defaults)
906-
# the defaults are given as str (as option values passed from CLI) since
907-
# these are typically read from .torchxconfig
908-
if cmpnt_defaults and param_name in cmpnt_defaults:
909-
args["default"] = cmpnt_defaults[param_name]
910-
911-
if parameter.kind == inspect._ParameterKind.VAR_POSITIONAL:
912-
args["nargs"] = argparse.REMAINDER
913-
args["action"] = _reminder_action
914-
script_parser.add_argument(param_name, **args)
915-
else:
916-
arg_names = [f"--{param_name}"]
917-
if len(param_name) == 1:
918-
arg_names = [f"-{param_name}"] + arg_names
919-
if "default" not in args:
920-
args["required"] = True
921-
script_parser.add_argument(*arg_names, **args)
922-
return script_parser
923-
924-
925-
# FIXME this function invokes the component fn to materialize the AppDef
926-
# rename to something more appropriate like - materialize_appdef or eval_component_fn
927-
# seee: https://github.com/pytorch/torchx/issues/368
928-
def from_function(
929-
cmpnt_fn: Callable[..., AppDef],
930-
cmpnt_args: List[str],
931-
cmpnt_defaults: Optional[Dict[str, str]] = None,
932-
) -> AppDef:
933-
"""
934-
Creates an application by running user defined ``app_fn``.
935-
936-
``app_fn`` has the following restrictions:
937-
* Name must be ``app_fn``
938-
* All arguments should be annotated
939-
* Supported argument types:
940-
- primitive: int, str, float
941-
- Dict[primitive, primitive]
942-
- List[primitive]
943-
- Optional[Dict[primitive, primitive]]
944-
- Optional[List[primitive]]
945-
* ``app_fn`` can define a vararg (*arg) at the end
946-
* There should be a docstring for the function that defines
947-
All arguments in a google-style format
948-
* There can be default values for the function arguments.
949-
* The return object must be ``AppDef``
950-
951-
Args:
952-
cmpnt_fn: Component function
953-
cmpnt_args: Function args
954-
cmpnt_defaults: Additional default values for parameters of ``app_fn``
955-
(overrides the defaults set on the fn declaration)
956-
Returns:
957-
An application spec
958-
"""
959-
960-
script_parser = _create_args_parser(cmpnt_fn, cmpnt_defaults)
961-
parsed_args = script_parser.parse_args(cmpnt_args)
962-
963-
function_args = []
964-
var_arg = []
965-
kwargs = {}
966-
967-
parameters = inspect.signature(cmpnt_fn).parameters
968-
for param_name, parameter in parameters.items():
969-
arg_value = getattr(parsed_args, param_name)
970-
parameter_type = parameter.annotation
971-
parameter_type = decode_optional(parameter_type)
972-
if is_bool(parameter_type):
973-
arg_value = arg_value.lower() == "true"
974-
elif not is_primitive(parameter_type):
975-
arg_value = decode_from_string(arg_value, parameter_type)
976-
if parameter.kind == inspect.Parameter.VAR_POSITIONAL:
977-
var_arg = arg_value
978-
elif parameter.kind == inspect.Parameter.KEYWORD_ONLY:
979-
kwargs[param_name] = arg_value
980-
elif parameter.kind == inspect.Parameter.VAR_KEYWORD:
981-
raise TypeError("**kwargs are not supported for component definitions")
982-
else:
983-
function_args.append(arg_value)
984-
if len(var_arg) > 0 and var_arg[0] == "--":
985-
var_arg = var_arg[1:]
986-
987-
return cmpnt_fn(*function_args, *var_arg, **kwargs)
988-
989-
990-
_MOUNT_OPT_MAP: Mapping[str, str] = {
991-
"type": "type",
992-
"destination": "dst",
993-
"dst": "dst",
994-
"target": "dst",
995-
"read_only": "readonly",
996-
"readonly": "readonly",
997-
"source": "src",
998-
"src": "src",
999-
"perm": "perm",
1000-
}
1001-
1002-
1003-
def parse_mounts(opts: List[str]) -> List[Union[BindMount, VolumeMount, DeviceMount]]:
1004-
"""
1005-
parse_mounts parses a list of options into typed mounts following a similar
1006-
format to Dockers bind mount.
1007-
1008-
Multiple mounts can be specified in the same list. ``type`` must be
1009-
specified first in each.
1010-
1011-
Ex:
1012-
type=bind,src=/host,dst=/container,readonly,[type=bind,src=...,dst=...]
1013-
1014-
Supported types:
1015-
BindMount: type=bind,src=<host path>,dst=<container path>[,readonly]
1016-
VolumeMount: type=volume,src=<name/id>,dst=<container path>[,readonly]
1017-
DeviceMount: type=device,src=/dev/<dev>[,dst=<container path>][,perm=rwm]
1018-
"""
1019-
mount_opts = []
1020-
cur = {}
1021-
for opt in opts:
1022-
key, _, val = opt.partition("=")
1023-
if key not in _MOUNT_OPT_MAP:
1024-
raise KeyError(
1025-
f"unknown mount option {key}, must be one of {list(_MOUNT_OPT_MAP.keys())}"
1026-
)
1027-
key = _MOUNT_OPT_MAP[key]
1028-
if key == "type":
1029-
cur = {}
1030-
mount_opts.append(cur)
1031-
elif len(mount_opts) == 0:
1032-
raise KeyError("type must be specified first")
1033-
cur[key] = val
1034-
1035-
mounts = []
1036-
for opts in mount_opts:
1037-
typ = opts.get("type")
1038-
if typ == MountType.BIND:
1039-
mounts.append(
1040-
BindMount(
1041-
src_path=opts["src"],
1042-
dst_path=opts["dst"],
1043-
read_only="readonly" in opts,
1044-
)
1045-
)
1046-
elif typ == MountType.VOLUME:
1047-
mounts.append(
1048-
VolumeMount(
1049-
src=opts["src"], dst_path=opts["dst"], read_only="readonly" in opts
1050-
)
1051-
)
1052-
elif typ == MountType.DEVICE:
1053-
src = opts["src"]
1054-
dst = opts.get("dst", src)
1055-
perm = opts.get("perm", "rwm")
1056-
for c in perm:
1057-
if c not in "rwm":
1058-
raise ValueError(
1059-
f"{c} is not a valid permission flags must one of r,w,m"
1060-
)
1061-
mounts.append(DeviceMount(src_path=src, dst_path=dst, permissions=perm))
1062-
else:
1063-
valid = list(str(item.value) for item in MountType)
1064-
raise ValueError(f"invalid mount type {repr(typ)}, must be one of {valid}")
1065-
return mounts

0 commit comments

Comments
 (0)