Skip to content

Commit a0b97e3

Browse files
committed
more testing
1 parent 8b5b67f commit a0b97e3

File tree

13 files changed

+266
-263
lines changed

13 files changed

+266
-263
lines changed

idom/cli/settings.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,26 @@
88
IDOM_CLI_DEBUG = False
99

1010

11+
_to_str = {
12+
bool: lambda x: str(int(x)),
13+
str: lambda x: x,
14+
list: lambda x: ",".join(x),
15+
}
16+
17+
_from_str = {
18+
bool: lambda x: bool(int(x)),
19+
str: lambda x: x,
20+
list: lambda x: x.split(","),
21+
}
22+
23+
1124
def _load_from_environ():
1225
gs = globals()
1326
for k, v in globals().items():
1427
if k.startswith("IDOM_"):
15-
if isinstance(v, bool):
16-
gs[k] = bool(int(os.environ.setdefault(k, str(int(v)))))
17-
else:
18-
gs[k] = os.environ.setdefault(k, v)
28+
v_type = type(v)
29+
default = _to_str[v_type](v)
30+
gs[k] = _from_str[v_type](os.environ.setdefault(k, default))
1931
NAMES.append(k)
2032

2133

idom/client/build_config.py

Lines changed: 42 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import json
44
import ast
55
from copy import deepcopy
6-
from functools import wraps
7-
from contextlib import contextmanager
86
from hashlib import sha256
97
from pathlib import Path
108
from importlib.machinery import SourceFileLoader
@@ -14,114 +12,71 @@
1412
Dict,
1513
Optional,
1614
Any,
17-
Iterator,
1815
Iterable,
1916
TypeVar,
2017
Tuple,
21-
Callable,
22-
TypedDict,
18+
NamedTuple,
2319
)
2420

2521
from jsonschema import validate as validate_schema
2622

2723
import idom
2824

2925

30-
_Self = TypeVar("_Self")
31-
_Method = TypeVar("_Method", bound=Callable[..., Any])
32-
33-
BuildConfigItem = Dict[str, Any]
34-
35-
36-
def _requires_open_transaction(method: _Method) -> _Method:
37-
@wraps(method)
38-
def wrapper(self: BuildConfig, *args: Any, **kwargs: Any) -> Any:
39-
if not self._transaction_open:
40-
raise RuntimeError(
41-
f"BuildConfig method {method.__name__!r} must be used in a transaction."
42-
)
43-
return method(self, *args, **kwargs)
44-
45-
return wrapper
46-
47-
48-
def _modified_by_transaction(method: _Method) -> _Method:
49-
@wraps(method)
50-
def wrapper(self: BuildConfig, *args: Any, **kwargs: Any) -> Any:
51-
if self._transaction_open:
52-
raise RuntimeError(
53-
f"BuildConfig method {method.__name__!r} cannot be used in a transaction."
54-
)
55-
return method(self, *args, **kwargs)
56-
57-
return wrapper
26+
ConfigItem = Dict[str, Any]
5827

5928

6029
class BuildConfig:
6130

62-
__slots__ = "config", "_path", "_transaction_open", "_derived_properties"
31+
__slots__ = "data", "_path", "_item_info"
6332
_filename = "idom-build-config.json"
64-
_default_config = {"version": idom.__version__, "by_source": {}}
33+
_default_config = {"version": idom.__version__, "items": {}}
6534

6635
def __init__(self, path: Path) -> None:
6736
self._path = path / self._filename
68-
self.config = self._load()
69-
self._derived_properties = derive_config_properties(self.config)
70-
self._transaction_open = False
71-
72-
@contextmanager
73-
def transaction(self: _Self) -> Iterator[_Self]:
74-
"""Open a transaction to modify the config file state"""
75-
self._transaction_open = True
76-
old_config = deepcopy(self.config)
77-
try:
78-
yield self
79-
except Exception:
80-
self.config = old_config
81-
raise
82-
else:
83-
self._save()
84-
self._derived_properties = derive_config_properties(self.config)
85-
finally:
86-
self._transaction_open = False
37+
self.data = self._load()
38+
self._item_info: Dict[str, ConfigItemInfo] = {
39+
name: derive_config_item_info(item)
40+
for name, item in self.data["items"].items()
41+
}
8742

88-
@_requires_open_transaction
89-
def update_config_items(self, config_items: Iterable[BuildConfigItem]) -> None:
43+
def update(self, config_items: Iterable[ConfigItem]) -> None:
9044
for conf in map(validate_config_item, config_items):
91-
self.config["by_source"][conf["source_name"]] = conf
45+
src_name = conf["source_name"]
46+
self.data["items"][src_name] = conf
47+
self._item_info[src_name] = derive_config_item_info(conf)
48+
49+
def has_config_item(self, source_name: str) -> bool:
50+
return source_name in self.data["items"]
9251

93-
@_modified_by_transaction
9452
def get_js_dependency_alias(
9553
self,
9654
source_name: str,
9755
dependency_name: str,
9856
) -> Optional[str]:
99-
aliases_by_src = self._derived_properties["js_dependency_aliases_by_source"]
10057
try:
101-
return aliases_by_src[source_name][dependency_name]
58+
return self._item_info[source_name].js_dependency_aliases[dependency_name]
10259
except KeyError:
10360
return None
10461

105-
@_modified_by_transaction
10662
def all_aliased_js_dependencies(self) -> List[str]:
10763
return [
10864
dep
109-
for aliased_deps in self._derived_properties[
110-
"aliased_js_dependencies_by_source"
111-
].values()
112-
for dep in aliased_deps
65+
for info in self._item_info.values()
66+
for dep in info.aliased_js_dependencies
11367
]
11468

115-
@_modified_by_transaction
11669
def all_js_dependency_aliases(self) -> List[str]:
11770
return [
118-
als
119-
for aliases in self._derived_properties[
120-
"js_dependency_aliases_by_source"
121-
].values()
122-
for als in aliases.values()
71+
alias
72+
for info in self._item_info.values()
73+
for alias in info.js_dependency_aliases.values()
12374
]
12475

76+
def save(self) -> None:
77+
with self._path.open("w") as f:
78+
json.dump(validate_config(self.data), f)
79+
12580
def _load(self) -> Dict[str, Any]:
12681
if not self._path.exists():
12782
return deepcopy(self._default_config)
@@ -131,17 +86,13 @@ def _load(self) -> Dict[str, Any]:
13186
json.loads(f.read() or "null") or self._default_config
13287
)
13388

134-
def _save(self) -> None:
135-
with self._path.open("w") as f:
136-
json.dump(validate_config(self.config), f)
137-
13889
def __repr__(self) -> str:
139-
return f"{type(self).__name__}({self.config})"
90+
return f"{type(self).__name__}({self.data})"
14091

14192

14293
def find_python_packages_build_config_items(
14394
paths: Optional[List[str]] = None,
144-
) -> Tuple[List[BuildConfigItem], List[Exception]]:
95+
) -> Tuple[List[ConfigItem], List[Exception]]:
14596
"""Find javascript dependencies declared by Python modules
14697
14798
Parameters:
@@ -153,7 +104,7 @@ def find_python_packages_build_config_items(
153104
Mapping of module names to their corresponding list of discovered dependencies.
154105
"""
155106
failures: List[Tuple[str, Exception]] = []
156-
build_configs: List[BuildConfigItem] = []
107+
build_configs: List[ConfigItem] = []
157108
for module_info in iter_modules(paths):
158109
module_name = module_info.name
159110
module_loader = module_info.module_finder.find_module(module_name)
@@ -175,14 +126,14 @@ def find_python_packages_build_config_items(
175126

176127
def find_build_config_item_in_python_file(
177128
module_name: str, path: Path
178-
) -> Optional[BuildConfigItem]:
129+
) -> Optional[ConfigItem]:
179130
with path.open() as f:
180131
return find_build_config_item_in_python_source(module_name, f.read())
181132

182133

183134
def find_build_config_item_in_python_source(
184135
module_name: str, module_src: str
185-
) -> Optional[BuildConfigItem]:
136+
) -> Optional[ConfigItem]:
186137
for node in ast.parse(module_src).body:
187138
if isinstance(node, ast.Assign) and (
188139
len(node.targets) == 1
@@ -212,28 +163,13 @@ def split_package_name_and_version(pkg: str) -> Tuple[str, str]:
212163
return pkg, ""
213164

214165

215-
class _DerivedConfigProperties(TypedDict):
216-
js_dependency_aliases_by_source: Dict[str, Dict[str, str]]
217-
aliased_js_dependencies_by_source: Dict[str, List[str]]
218-
166+
class ConfigItemInfo(NamedTuple):
167+
js_dependency_aliases: Dict[str, str]
168+
aliased_js_dependencies: List[str]
219169

220-
def derive_config_properties(config: Dict[str, Any]) -> _DerivedConfigProperties:
221-
js_dependency_aliases_by_source = {}
222-
aliased_js_dependencies_by_source = {}
223-
for src, cfg in config["by_source"].items():
224-
cfg_hash = _hash_config_item(cfg)
225-
aliases, aliased_js_deps = _config_item_js_dependencies(cfg, cfg_hash)
226-
js_dependency_aliases_by_source[src] = aliases
227-
aliased_js_dependencies_by_source[src] = aliased_js_deps
228-
return {
229-
"js_dependency_aliases_by_source": js_dependency_aliases_by_source,
230-
"aliased_js_dependencies_by_source": aliased_js_dependencies_by_source,
231-
}
232170

233-
234-
def _config_item_js_dependencies(
235-
config_item: Dict[str, Any], config_hash: str
236-
) -> Tuple[Dict[str, str], List[str]]:
171+
def derive_config_item_info(config_item: Dict[str, Any]) -> ConfigItemInfo:
172+
config_hash = _hash_config_item(config_item)
237173
alias_suffix = f"{config_item['source_name']}-{config_hash}"
238174
aliases: Dict[str, str] = {}
239175
aliased_js_deps: List[str] = []
@@ -242,7 +178,10 @@ def _config_item_js_dependencies(
242178
dep_alias = f"{dep_name}-{alias_suffix}"
243179
aliases[dep_name] = dep_alias
244180
aliased_js_deps.append(f"{dep_alias}@npm:{dep}")
245-
return aliases, aliased_js_deps
181+
return ConfigItemInfo(
182+
js_dependency_aliases=aliases,
183+
aliased_js_dependencies=aliased_js_deps,
184+
)
246185

247186

248187
def _hash_config_item(config_item: Dict[str, Any]) -> str:
@@ -259,7 +198,7 @@ def _hash_config_item(config_item: Dict[str, Any]) -> str:
259198
"type": "object",
260199
"properties": {
261200
"version": {"type": "string"},
262-
"by_source": {
201+
"items": {
263202
"type": "object",
264203
"patternProperties": {".*": {"$ref": "#/definitions/ConfigItem"}},
265204
},
@@ -278,6 +217,7 @@ def _hash_config_item(config_item: Dict[str, Any]) -> str:
278217
},
279218
},
280219
"requiredProperties": ["source_name"],
220+
"additionalProperties": False,
281221
}
282222
},
283223
}

idom/client/manage.py

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from .build_config import (
88
BuildConfig,
9-
BuildConfigItem,
9+
ConfigItem,
1010
find_python_packages_build_config_items,
1111
)
1212
from .utils import open_modifiable_json, find_js_module_exports_in_source
@@ -51,27 +51,13 @@ def web_module_exists(source_name: str, package_name: str) -> bool:
5151
return True
5252

5353

54-
def find_client_build_path(rel_path: str) -> Optional[Path]:
55-
if rel_path.startswith("/"):
56-
raise ValueError(f"{rel_path!r} is not a relative path")
57-
builtin_path = BUILD_DIR.joinpath(*rel_path.split("/"))
58-
if builtin_path.exists():
59-
return builtin_path
60-
else:
61-
return None
62-
63-
64-
def build(config_items: Iterable[BuildConfigItem] = ()) -> None:
54+
def build(config_items: Iterable[ConfigItem] = ()) -> None:
6555
config = build_config()
66-
67-
with config.transaction():
68-
config.update_config_items(config_items)
69-
70-
with console.spinner("Discovering dependencies"):
71-
configs, errors = find_python_packages_build_config_items()
72-
for e in errors:
73-
console.echo(f"{e} because {e.__cause__}", color="red")
74-
config.update_config_items(configs)
56+
with console.spinner("Discovering dependencies"):
57+
py_pkg_configs, errors = find_python_packages_build_config_items()
58+
for e in errors: # pragma: no cover
59+
console.echo(f"{e} because {e.__cause__}", color="red")
60+
config.update(py_pkg_configs + config_items)
7561

7662
with TemporaryDirectory() as tempdir:
7763
tempdir_path = Path(tempdir)
@@ -105,10 +91,13 @@ def build(config_items: Iterable[BuildConfigItem] = ()) -> None:
10591

10692
shutil.copytree(temp_build_dir, BUILD_DIR, symlinks=True)
10793

94+
config.save()
95+
10896

10997
def restore() -> None:
11098
with console.spinner("Restoring"):
111-
shutil.rmtree(BUILD_DIR)
99+
if BUILD_DIR.exists():
100+
shutil.rmtree(BUILD_DIR)
112101
_run_subprocess(["npm", "install"], APP_DIR)
113102
_run_subprocess(["npm", "run", "build"], APP_DIR)
114103

@@ -121,12 +110,12 @@ def _web_module_alias_and_file_path(
121110
raise WebModuleError(
122111
f"Package {package_name!r} is not declared as a dependency of {source_name!r}"
123112
)
124-
module_file = find_client_build_path(f"web_modules/{alias}.js")
125-
if module_file is None:
113+
builtin_path = BUILD_DIR.joinpath("web_modules", *f"{alias}.js".split("/"))
114+
if not builtin_path.exists():
126115
raise WebModuleError(
127116
f"Dependency {package_name!r} of {source_name!r} was not installed"
128117
)
129-
return alias, module_file
118+
return alias, builtin_path
130119

131120

132121
def _npm_install(packages: Sequence[str], cwd: Path) -> None:

0 commit comments

Comments
 (0)