Skip to content

Commit aebdf12

Browse files
committed
rework config using jsonschema
1 parent c145172 commit aebdf12

File tree

8 files changed

+285
-286
lines changed

8 files changed

+285
-286
lines changed

idom/__init__.py

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

6+
try:
7+
__version__ = _get_distribution(__name__).version
8+
except _DistributionNotFound: # pragma: no cover
9+
# package is not installed
10+
__version__ = "0.0.0"
11+
612
from . import cli
713
from .utils import Ref, html_to_vdom
814

@@ -22,12 +28,6 @@
2228

2329
from . import widgets
2430

25-
try:
26-
__version__ = _get_distribution(__name__).version
27-
except _DistributionNotFound: # pragma: no cover
28-
# package is not installed
29-
__version__ = "0.0.0"
30-
3131
# try to automatically setup the dialect's import hook
3232
try:
3333
import pyalect

idom/cli/commands.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def build(
2323
"--entrypoint",
2424
"-e",
2525
help="A python file containing a build config",
26-
)
26+
),
2727
) -> None:
2828
"""Configure and build the client"""
2929
if entrypoint is None:

idom/client/build_config.py

Lines changed: 79 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,22 @@
1515
Optional,
1616
Any,
1717
Iterator,
18+
Iterable,
1819
TypeVar,
1920
Tuple,
2021
Callable,
2122
TypedDict,
2223
)
2324

24-
from fastjsonschema import compile as compile_schema
25+
from jsonschema import validate as validate_schema
2526

2627
import idom
27-
from .utils import split_package_name_and_version
2828

2929

3030
_Self = TypeVar("_Self")
3131
_Method = TypeVar("_Method", bound=Callable[..., Any])
3232

33-
_ConfigItem = Dict[str, Any]
33+
BuildConfigItem = Dict[str, Any]
3434

3535

3636
def _requires_open_transaction(method: _Method) -> _Method:
@@ -43,16 +43,26 @@ def wrapper(self: BuildConfig, *args: Any, **kwargs: Any) -> Any:
4343
return wrapper
4444

4545

46+
def _modified_by_transaction(method: _Method) -> _Method:
47+
@wraps(method)
48+
def wrapper(self: BuildConfig, *args: Any, **kwargs: Any) -> Any:
49+
if self._transaction_open:
50+
raise RuntimeError("Wait for transaction to end before using this method.")
51+
return method(self, *args, **kwargs)
52+
53+
return wrapper
54+
55+
4656
class BuildConfig:
4757

48-
__slots__ = "config", "_path", "_transaction_open"
58+
__slots__ = "config", "_path", "_transaction_open", "_derived_properties"
4959
_filename = "idom-build-config.json"
5060
_default_config = {"version": idom.__version__, "by_source": {}}
5161

5262
def __init__(self, path: Path) -> None:
5363
self._path = path / self._filename
5464
self.config = self._load()
55-
self._derived_properties = _derive_config_properties(self.config)
65+
self._derived_properties = derive_config_properties(self.config)
5666
self._transaction_open = False
5767

5868
@contextmanager
@@ -67,13 +77,21 @@ def transaction(self: _Self) -> Iterator[_Self]:
6777
raise
6878
else:
6979
self._save()
80+
self._derived_properties = derive_config_properties(self.config)
7081
finally:
7182
self._transaction_open = False
7283

84+
@_requires_open_transaction
85+
def update_config_items(self, config_items: Iterable[BuildConfigItem]) -> None:
86+
for conf in map(validate_config_item, config_items):
87+
self.config["by_source"][conf["source_name"]] = conf
88+
89+
@_modified_by_transaction
7390
def get_js_dependency_alias(self, source_name: str, dependency_name: str) -> str:
7491
aliases_by_src = self._derived_properties["js_dependency_aliases_by_source"]
7592
return aliases_by_src[source_name][dependency_name]
7693

94+
@_modified_by_transaction
7795
def all_aliased_js_dependencies(self) -> List[str]:
7896
return [
7997
dep
@@ -83,27 +101,43 @@ def all_aliased_js_dependencies(self) -> List[str]:
83101
for dep in aliased_deps
84102
]
85103

104+
@_modified_by_transaction
105+
def all_js_dependency_aliases(self) -> List[str]:
106+
return [
107+
als
108+
for aliases in self._derived_properties[
109+
"js_dependency_aliases_by_source"
110+
].values()
111+
for als in aliases
112+
]
113+
86114
def _load(self) -> Dict[str, Any]:
87-
with self._path.open() as f:
88-
return validate_config(
89-
json.loads(f.read() or "null") or self._default_config
90-
)
115+
if not self._path.exists():
116+
return deepcopy(self._default_config)
117+
else:
118+
with self._path.open() as f:
119+
return validate_config(
120+
json.loads(f.read() or "null") or self._default_config
121+
)
91122

92123
def _save(self) -> None:
93124
with self._path.open("w") as f:
94125
json.dump(validate_config(self.config), f)
95126

127+
def __repr__(self) -> str:
128+
return f"{type(self).__name__}({self.config})"
129+
96130

97131
def find_build_config_item_in_python_file(
98132
module_name: str, path: Path
99-
) -> Optional[_ConfigItem]:
133+
) -> Optional[BuildConfigItem]:
100134
with path.open() as f:
101135
return find_build_config_item_in_python_source(module_name, f.read())
102136

103137

104138
def find_python_packages_build_config_items(
105139
paths: Optional[List[str]] = None,
106-
) -> Tuple[List[_ConfigItem], List[Exception]]:
140+
) -> Tuple[List[BuildConfigItem], List[Exception]]:
107141
"""Find javascript dependencies declared by Python modules
108142
109143
Parameters:
@@ -115,7 +149,7 @@ def find_python_packages_build_config_items(
115149
Mapping of module names to their corresponding list of discovered dependencies.
116150
"""
117151
failures: List[Tuple[str, Exception]] = []
118-
build_configs: List[_ConfigItem] = []
152+
build_configs: List[BuildConfigItem] = []
119153
for module_info in iter_modules(paths):
120154
module_name = module_info.name
121155
module_loader = module_info.module_finder.find_module(module_name)
@@ -137,7 +171,7 @@ def find_python_packages_build_config_items(
137171

138172
def find_build_config_item_in_python_source(
139173
module_name: str, module_src: str
140-
) -> Optional[_ConfigItem]:
174+
) -> Optional[BuildConfigItem]:
141175
for node in ast.parse(module_src).body:
142176
if isinstance(node, ast.Assign) and (
143177
len(node.targets) == 1
@@ -153,12 +187,26 @@ def find_build_config_item_in_python_source(
153187
return None
154188

155189

190+
def split_package_name_and_version(pkg: str) -> Tuple[str, str]:
191+
at_count = pkg.count("@")
192+
if pkg.startswith("@"):
193+
if at_count == 1:
194+
return pkg, ""
195+
else:
196+
name, version = pkg[1:].split("@", 1)
197+
return ("@" + name), version
198+
elif at_count:
199+
return tuple(pkg.split("@", 1))
200+
else:
201+
return pkg, ""
202+
203+
156204
class _DerivedConfigProperties(TypedDict):
157205
js_dependency_aliases_by_source: Dict[str, Dict[str, str]]
158206
aliased_js_dependencies_by_source: Dict[str, List[str]]
159207

160208

161-
def _derive_config_properties(config: Dict[str, Any]) -> _DerivedConfigProperties:
209+
def derive_config_properties(config: Dict[str, Any]) -> _DerivedConfigProperties:
162210
js_dependency_aliases_by_source = {}
163211
aliased_js_dependencies_by_source = {}
164212
for src, cfg in config["by_source"].items():
@@ -178,7 +226,7 @@ def _config_item_js_dependencies(
178226
alias_suffix = f"{config_item['source_name']}-{config_hash}"
179227
aliases: Dict[str, str] = {}
180228
aliased_js_deps: List[str] = []
181-
for dep in config_item["js_dependencies"]:
229+
for dep in config_item.get("js_dependencies", []):
182230
dep_name = split_package_name_and_version(dep)[0]
183231
dep_alias = f"{dep_name}-{alias_suffix}"
184232
aliases[dep_name] = dep_alias
@@ -209,16 +257,29 @@ def _hash_config_item(config_item: Dict[str, Any]) -> str:
209257
"ConfigItem": {
210258
"type": "object",
211259
"properties": {
212-
"source_name": {"type": "string"},
260+
"source_name": {
261+
"type": "string",
262+
"pattern": r"^[\w\d\-]+$",
263+
},
213264
"js_dependencies": {
214265
"type": "array",
215266
"items": {"type": "string"},
216267
},
217268
},
269+
"requiredProperties": ["source_name"],
218270
}
219271
},
220272
}
221273

222274

223-
validate_config = compile_schema(_CONFIG_SCHEMA)
224-
validate_config_item = compile_schema(_CONFIG_SCHEMA["definitions"]["ConfigItem"])
275+
_V = TypeVar("_V")
276+
277+
278+
def validate_config(value: _V) -> _V:
279+
validate_schema(value, _CONFIG_SCHEMA)
280+
return value
281+
282+
283+
def validate_config_item(value: _V) -> _V:
284+
validate_schema(value, _CONFIG_SCHEMA["definitions"]["ConfigItem"])
285+
return value

idom/client/manage.py

Lines changed: 44 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
from typing import Optional, Iterable, Sequence, List
66

77
from .build_config import (
8-
BuildConfigFile,
8+
BuildConfig,
99
BuildConfigItem,
1010
find_python_packages_build_config_items,
1111
)
12-
from .utils import open_modifiable_json, split_package_name_and_version
12+
from .utils import open_modifiable_json
1313

1414
from idom.cli import console
1515

@@ -18,8 +18,8 @@
1818
BUILD_DIR = APP_DIR / "build"
1919

2020

21-
def build_config_file() -> BuildConfigFile:
22-
return BuildConfigFile(BUILD_DIR)
21+
def build_config() -> BuildConfig:
22+
return BuildConfig(BUILD_DIR)
2323

2424

2525
def find_path(url_path: str) -> Optional[Path]:
@@ -33,7 +33,7 @@ def find_path(url_path: str) -> Optional[Path]:
3333

3434

3535
def web_module_url(source_name: str, package_name: str) -> Optional[str]:
36-
config = build_config_file().configs.get(source_name)
36+
config = build_config().configs.get(source_name)
3737
if config is None:
3838
return None
3939
if package_name not in config.js_dependency_aliases:
@@ -46,57 +46,52 @@ def web_module_url(source_name: str, package_name: str) -> Optional[str]:
4646

4747

4848
def build(
49-
configs: Optional[Iterable[BuildConfigItem]] = None,
49+
config_items: Optional[Iterable[BuildConfigItem]] = None,
5050
output_dir: Path = BUILD_DIR,
5151
) -> None:
52-
with build_config_file().transaction() as config_file:
53-
if configs is not None:
54-
config_file.add(configs, ignore_existing=True)
52+
config = build_config()
53+
54+
with config.transaction():
55+
if config_items is not None:
56+
config.update_config_items(config_items)
5557

5658
with console.spinner("Discovering dependencies"):
5759
configs, errors = find_python_packages_build_config_items()
5860
for e in errors:
5961
console.echo(f"{e} because {e.__cause__}", color="red")
60-
config_file.add(configs, ignore_existing=True)
61-
62-
with TemporaryDirectory() as tempdir:
63-
tempdir_path = Path(tempdir)
64-
temp_app_dir = tempdir_path / "app"
65-
temp_build_dir = temp_app_dir / "build"
66-
package_json_path = temp_app_dir / "package.json"
67-
68-
# copy over the whole APP_DIR directory into the temp one
69-
shutil.copytree(APP_DIR, temp_app_dir, symlinks=True)
70-
71-
packages_to_install = [
72-
dep
73-
for conf in config_file.configs.values()
74-
for dep in conf.js_dependencies
75-
]
76-
77-
with open_modifiable_json(package_json_path) as package_json:
78-
snowpack_config = package_json.setdefault("snowpack", {})
79-
snowpack_config.setdefault("install", []).extend(
80-
[
81-
split_package_name_and_version(dep)[0]
82-
for dep in packages_to_install
83-
]
84-
)
85-
86-
with console.spinner(
87-
f"Installing {len(packages_to_install)} dependencies"
88-
if packages_to_install
89-
else "Installing dependencies"
90-
):
91-
_npm_install(packages_to_install, temp_app_dir)
92-
93-
with console.spinner("Building client"):
94-
_npm_run_build(temp_app_dir)
95-
96-
if output_dir.exists():
97-
shutil.rmtree(output_dir)
98-
99-
shutil.copytree(temp_build_dir, output_dir, symlinks=True)
62+
config.update_config_items(configs)
63+
64+
with TemporaryDirectory() as tempdir:
65+
tempdir_path = Path(tempdir)
66+
temp_app_dir = tempdir_path / "app"
67+
temp_build_dir = temp_app_dir / "build"
68+
package_json_path = temp_app_dir / "package.json"
69+
70+
# copy over the whole APP_DIR directory into the temp one
71+
shutil.copytree(APP_DIR, temp_app_dir, symlinks=True)
72+
73+
packages_to_install = config.all_aliased_js_dependencies()
74+
75+
with open_modifiable_json(package_json_path) as package_json:
76+
snowpack_config = package_json.setdefault("snowpack", {})
77+
snowpack_config.setdefault("install", []).extend(
78+
config.all_js_dependency_aliases()
79+
)
80+
81+
with console.spinner(
82+
f"Installing {len(packages_to_install)} dependencies"
83+
if packages_to_install
84+
else "Installing dependencies"
85+
):
86+
_npm_install(packages_to_install, temp_app_dir)
87+
88+
with console.spinner("Building client"):
89+
_npm_run_build(temp_app_dir)
90+
91+
if output_dir.exists():
92+
shutil.rmtree(output_dir)
93+
94+
shutil.copytree(temp_build_dir, output_dir, symlinks=True)
10095

10196

10297
def restore() -> None:

0 commit comments

Comments
 (0)