Skip to content

Commit cf912e2

Browse files
committed
reorganize and rename
1 parent 610539f commit cf912e2

File tree

13 files changed

+132
-117
lines changed

13 files changed

+132
-117
lines changed

idom/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
DistributionNotFound as _DistributionNotFound,
44
)
55

6+
from . import cli
67
from .utils import Ref, html_to_vdom
78

89
from .core.element import element, Element
@@ -64,4 +65,5 @@
6465
"VdomDict",
6566
"widgets",
6667
"client",
68+
"cli",
6769
]

idom/cli/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .commands import main
2+
from . import console
3+
4+
__all__ = ["main", "console"]

idom/cli.py renamed to idom/cli/commands.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import idom
66
from idom.client import manage as manage_client
7-
from idom.client.build_config import find_build_config_in_python_source
7+
from idom.client.build_config import find_build_config_item_in_python_source
88

99

1010
main = typer.Typer()
@@ -16,7 +16,9 @@
1616
def build(entrypoint: str) -> None:
1717
"""Configure and build the client"""
1818
if entrypoint.endswith(".py"):
19-
config = find_build_config_in_python_source("__main__", Path.cwd() / entrypoint)
19+
config = find_build_config_item_in_python_source(
20+
"__main__", Path.cwd() / entrypoint
21+
)
2022
if config is None:
2123
typer.echo(f"No build config found in {entrypoint!r}")
2224
manage_client.build()
@@ -36,7 +38,7 @@ def restore():
3638
@show.command()
3739
def config() -> None:
3840
"""Show the state of IDOM's build config"""
39-
typer.echo(manage_client.BUILD_STATE.show())
41+
typer.echo(manage_client.BUILD_CONFIG_FILE.show())
4042
return None
4143

4244

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
import click_spinner
77

88

9+
os.environ.setdefault("IDOM_CLI_SHOW_SPINNER", "1")
10+
os.environ.setdefault("IDOM_CLI_SHOW_OUTPUT", "1")
11+
12+
913
def echo(message: str, message_color: Optional[str] = None, **kwargs: Any) -> None:
1014
if message_color is not None:
1115
message = typer.style(message, fg=getattr(typer.colors, message_color.upper()))
@@ -30,8 +34,8 @@ def spinner(message: str) -> Iterator[None]:
3034

3135

3236
def _show_spinner() -> bool:
33-
return _show_output() and bool(int(os.environ.get("IDOM_SHOW_SPINNER", 1)))
37+
return _show_output() and bool(int(os.environ["IDOM_CLI_SHOW_SPINNER"]))
3438

3539

3640
def _show_output() -> bool:
37-
return bool(int(os.environ.get("IDOM_SHOW_CLIENT_MANAGEMENT_OUTPUT", 1)))
41+
return bool(int(os.environ["IDOM_CLI_SHOW_OUTPUT"]))

idom/client/build_config.py

Lines changed: 59 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,20 @@
55
from pathlib import Path
66
from importlib.machinery import SourceFileLoader
77
from pkgutil import iter_modules
8-
from typing import (
9-
List,
10-
Dict,
11-
Optional,
12-
Any,
13-
NamedTuple,
14-
Iterable,
15-
TypeVar,
16-
Callable,
17-
Iterator,
18-
)
19-
20-
from .sh import echo
21-
from .utils import split_package_name_and_version
8+
from typing import List, Dict, Optional, Any, NamedTuple, Iterable, Iterator, TypeVar
229

10+
from .utils import split_package_name_and_version
2311

24-
_F = TypeVar("_F", bound=Callable[..., Any])
12+
from idom.cli import console
2513

2614

27-
class BuildState:
15+
class BuildConfigFile:
2816

29-
__slots__ = "save_location", "configs"
17+
__slots__ = "configs", "_path"
18+
_filename = ".idom-build-configs.json"
3019

3120
def __init__(self, path: Path) -> None:
32-
self.save_location = path
21+
self._path = path / self._filename
3322
self.configs = self._load_configs()
3423

3524
@contextmanager
@@ -45,15 +34,15 @@ def transaction(self) -> Iterator[None]:
4534
self.save()
4635

4736
def add(self, build_configs: Iterable[Any], ignore_existing: bool = False) -> None:
48-
for config in map(to_build_config, build_configs):
37+
for config in map(BuildConfigItem.cast, build_configs):
4938
source_name = config.source_name
5039
if not ignore_existing and source_name in self.configs:
5140
raise ValueError(f"A build config for {source_name!r} already exists")
5241
self.configs[source_name] = config
5342
return None
5443

5544
def save(self) -> None:
56-
with (self.save_location / ".idom-build-state.json").open("w") as f:
45+
with self._path.open("w") as f:
5746
json.dump({name: conf._asdict() for name, conf in self.configs.items()}, f)
5847

5948
def show(self, indent: int = 2) -> str:
@@ -65,22 +54,39 @@ def show(self, indent: int = 2) -> str:
6554
def clear(self) -> None:
6655
self.configs = {}
6756

68-
def _load_configs(self) -> Dict[str, "BuildConfig"]:
69-
fname = self.save_location / ".idom-build-state.json"
70-
if not fname.exists():
57+
def _load_configs(self) -> Dict[str, "BuildConfigItem"]:
58+
if not self._path.exists():
7159
return {}
72-
with fname.open() as f:
60+
with self._path.open() as f:
7361
content = f.read()
7462
if not content:
7563
return {}
7664
else:
77-
return {n: BuildConfig(**c) for n, c in json.loads(content).items()}
65+
return {n: BuildConfigItem(**c) for n, c in json.loads(content).items()}
7866

7967
def __repr__(self) -> str:
8068
return f"{type(self).__name__}({self.show(indent=0)})"
8169

8270

83-
class BuildConfig(NamedTuple):
71+
_Class = TypeVar("_Class")
72+
_Self = TypeVar("_Self")
73+
74+
75+
class BuildConfigItem(NamedTuple):
76+
"""Describes build requirements for a Python package or application"""
77+
78+
@classmethod
79+
def cast(cls: _Class, value: Any, source_name: Optional[str] = None) -> _Class:
80+
if isinstance(value, cls):
81+
return value
82+
elif isinstance(value, tuple):
83+
return cls(*value)._validate()
84+
elif isinstance(value, dict):
85+
if source_name is not None:
86+
value = {"source_name": source_name, **value}
87+
return cls(**value)._validate()
88+
else:
89+
raise ValueError(f"Expected a dict or tuple, not {value!r}")
8490

8591
source_name: str
8692
js_dependencies: List[str]
@@ -100,42 +106,33 @@ def aliased_js_dependencies(self) -> List[str]:
100106
aliased_dependencies.append(f"{name}-{idf}@npm:{dep}")
101107
return aliased_dependencies
102108

103-
104-
def to_build_config(config: Any, source_name: Optional[str] = None) -> BuildConfig:
105-
if isinstance(config, BuildConfig):
106-
return config
107-
108-
if not isinstance(config, dict):
109-
raise ValueError(
110-
f"build config must be a dictionary, but found {config!r}",
111-
)
112-
113-
source_name = config.setdefault("source_name", source_name)
114-
if not isinstance(source_name, str):
115-
raise ValueError(f"'source_name' must be a string, not {source_name!r}")
116-
117-
js_dependencies = config.get("js_dependencies")
118-
if not isinstance(js_dependencies, list):
119-
raise ValueError(f"'js_dependencies' must be a list, not {js_dependencies!r}")
120-
for item in js_dependencies:
121-
if not isinstance(item, str):
109+
def _validate(self: _Self) -> _Self:
110+
if not isinstance(self.source_name, str):
122111
raise ValueError(
123-
f"items of 'js_dependencies' must be strings, not {item!r}"
112+
f"'source_name' must be a string, not {self.source_name!r}"
124113
)
125-
126-
return BuildConfig(**config)
114+
if not isinstance(self.js_dependencies, list):
115+
raise ValueError(
116+
f"'js_dependencies' must be a list, not {self.js_dependencies!r}"
117+
)
118+
for item in self.js_dependencies:
119+
if not isinstance(item, str):
120+
raise ValueError(
121+
f"items of 'js_dependencies' must be strings, not {item!r}"
122+
)
123+
return self
127124

128125

129-
def find_build_config_in_python_source(
126+
def find_build_config_item_in_python_source(
130127
module_name: str, path: Path
131-
) -> Optional[BuildConfig]:
128+
) -> Optional[BuildConfigItem]:
132129
with path.open() as f:
133-
return _parse_build_config_from_python_source(module_name, f.read())
130+
return _parse_build_config_item_from_python_source(module_name, f.read())
134131

135132

136-
def find_python_packages_build_configs(
133+
def find_python_packages_build_config_items(
137134
path: Optional[str] = None,
138-
) -> List[BuildConfig]:
135+
) -> List[BuildConfigItem]:
139136
"""Find javascript dependencies declared by Python modules
140137
141138
Parameters:
@@ -146,20 +143,22 @@ def find_python_packages_build_configs(
146143
Returns:
147144
Mapping of module names to their corresponding list of discovered dependencies.
148145
"""
149-
build_configs: List[BuildConfig] = []
146+
build_configs: List[BuildConfigItem] = []
150147
for module_info in iter_modules(path):
151148
module_loader = module_info.module_finder.find_module(module_info.name)
152149
if isinstance(module_loader, SourceFileLoader):
153150
module_src = module_loader.get_source(module_info.name)
154-
conf = _parse_build_config_from_python_source(module_info.name, module_src)
151+
conf = _parse_build_config_item_from_python_source(
152+
module_info.name, module_src
153+
)
155154
if conf is not None:
156155
build_configs.append(conf)
157156
return build_configs
158157

159158

160-
def _parse_build_config_from_python_source(
159+
def _parse_build_config_item_from_python_source(
161160
module_name: str, module_src: str
162-
) -> Optional[BuildConfig]:
161+
) -> Optional[BuildConfigItem]:
163162
for node in ast.parse(module_src).body:
164163
if isinstance(node, ast.Assign) and (
165164
len(node.targets) == 1
@@ -175,14 +174,14 @@ def _parse_build_config_from_python_source(
175174
return None
176175

177176
try:
178-
return to_build_config(raw_config, module_name)
177+
return BuildConfigItem.cast(raw_config, module_name)
179178
except ValueError as error:
180179
_echo_error(
181-
f"Failed to load build config for {module_name!r} - {error}"
180+
f"Failed to load build config for {module_name!r} because {error}"
182181
)
183182
return None
184183
return None
185184

186185

187186
def _echo_error(msg: str) -> None:
188-
echo(msg, color="red")
187+
console.echo(msg, color="red")

idom/client/manage.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,19 @@
44
from tempfile import TemporaryDirectory
55
from typing import Optional, Iterable, Sequence, List
66

7-
from .sh import spinner
87
from .build_config import (
9-
BuildState,
10-
BuildConfig,
11-
find_python_packages_build_configs,
8+
BuildConfigFile,
9+
BuildConfigItem,
10+
find_python_packages_build_config_items,
1211
)
1312
from .utils import open_modifiable_json, split_package_name_and_version
1413

14+
from idom.cli import console
15+
1516

1617
APP_DIR = Path(__file__).parent / "app"
1718
BUILD_DIR = APP_DIR / "build"
18-
BUILD_STATE = BuildState(BUILD_DIR)
19+
BUILD_CONFIG_FILE = BuildConfigFile(BUILD_DIR)
1920

2021

2122
def find_path(url_path: str) -> Optional[Path]:
@@ -29,7 +30,7 @@ def find_path(url_path: str) -> Optional[Path]:
2930

3031

3132
def web_module_url(source_name: str, package_name: str) -> str:
32-
config = BUILD_STATE.configs.get(source_name)
33+
config = BUILD_CONFIG_FILE.configs.get(source_name)
3334
if config is None:
3435
return None
3536
alias = config.get_js_dependency_alias(package_name)
@@ -40,15 +41,17 @@ def web_module_url(source_name: str, package_name: str) -> str:
4041

4142

4243
def build(
43-
configs: Optional[Iterable[BuildConfig]] = None,
44+
configs: Optional[Iterable[BuildConfigItem]] = None,
4445
output_dir: Path = BUILD_DIR,
4546
) -> None:
46-
with BUILD_STATE.transaction():
47+
with BUILD_CONFIG_FILE.transaction():
4748
if configs is not None:
48-
BUILD_STATE.add(configs, ignore_existing=True)
49+
BUILD_CONFIG_FILE.add(configs, ignore_existing=True)
4950

50-
with spinner("Discovering dependencies"):
51-
BUILD_STATE.add(find_python_packages_build_configs(), ignore_existing=True)
51+
with console.spinner("Discovering dependencies"):
52+
BUILD_CONFIG_FILE.add(
53+
find_python_packages_build_config_items(), ignore_existing=True
54+
)
5255

5356
with TemporaryDirectory() as tempdir:
5457
tempdir_path = Path(tempdir)
@@ -61,7 +64,7 @@ def build(
6164

6265
packages_to_install = [
6366
dep
64-
for conf in BUILD_STATE.configs.values()
67+
for conf in BUILD_CONFIG_FILE.configs.values()
6568
for dep in conf.aliased_js_dependencies()
6669
]
6770

@@ -74,10 +77,10 @@ def build(
7477
]
7578
)
7679

77-
with spinner(f"Installing {len(packages_to_install)} dependencies"):
80+
with console.spinner(f"Installing {len(packages_to_install)} dependencies"):
7881
_npm_install(packages_to_install, temp_app_dir)
7982

80-
with spinner("Building client"):
83+
with console.spinner("Building client"):
8184
_npm_run_build(temp_app_dir)
8285

8386
if output_dir.exists():
@@ -87,7 +90,7 @@ def build(
8790

8891

8992
def restore() -> None:
90-
with spinner("Restoring"):
93+
with console.spinner("Restoring"):
9194
shutil.rmtree(BUILD_DIR)
9295
_run_subprocess(["npm", "install"], APP_DIR)
9396
_run_subprocess(["npm", "run", "build"], APP_DIR)

idom/client/protocol.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from typing_extensions import Protocol
44

5-
from idom import Ref
5+
from idom.utils import Ref
66

77
from . import manage
88

idom/client/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
@contextmanager
88
def open_modifiable_json(path: Path) -> Iterator[Any]:
99
with path.open() as f:
10-
data = json.load(f)
10+
data = json.loads(f.read().strip() or "{}")
1111

1212
yield data
1313

@@ -24,6 +24,6 @@ def split_package_name_and_version(pkg: str) -> Tuple[str, str]:
2424
name, version = pkg[1:].split("@", 1)
2525
return ("@" + name), version
2626
elif at_count:
27-
return pkg.split("@", 1)
27+
return tuple(pkg.split("@", 1))
2828
else:
2929
return pkg, ""

0 commit comments

Comments
 (0)