Skip to content

Commit 1c0deac

Browse files
committed
final test coverage
1 parent 02cd54b commit 1c0deac

File tree

9 files changed

+206
-95
lines changed

9 files changed

+206
-95
lines changed

docs/main.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
"@material-ui/core",
1717
"victory",
1818
"semantic-ui-react",
19-
"jquery",
2019
]
2120
}
2221

idom/cli/commands.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
import json
23
from pathlib import Path
34
from typing import Optional
@@ -9,6 +10,7 @@
910
from idom.client.build_config import find_build_config_item_in_python_file
1011

1112
from . import settings
13+
from .console import echo
1214

1315

1416
main = typer.Typer()
@@ -24,39 +26,50 @@ def build(
2426
"-e",
2527
help="A python file containing a build config",
2628
),
29+
restore: bool = typer.Option(
30+
None,
31+
"--restore",
32+
"-r",
33+
help="Restore the client build",
34+
),
2735
) -> None:
2836
"""Configure and build the client"""
37+
if restore and entrypoint:
38+
echo(
39+
"--restore and --entrypoint are mutually exclusive options",
40+
message_color="red",
41+
)
42+
raise typer.Exit(1)
43+
44+
if restore:
45+
manage_client.restore()
46+
return None
47+
2948
if entrypoint is None:
3049
manage_client.build()
3150
return None
3251

3352
config = find_build_config_item_in_python_file("__main__", Path.cwd() / entrypoint)
3453
if config is None:
35-
typer.echo(f"No build config found in {entrypoint!r}")
54+
echo(f"No build config found in {entrypoint!r}")
3655
manage_client.build()
3756
else:
3857
manage_client.build([config])
3958

4059

41-
@main.command()
42-
def restore() -> None:
43-
"""Reset the client to its original state"""
44-
manage_client.restore()
45-
46-
4760
@show.command()
4861
def build_config() -> None:
4962
"""Show the state of IDOM's build config"""
50-
typer.echo(json.dumps(manage_client.build_config().data, indent=2))
63+
echo(json.dumps(manage_client.build_config().data, indent=2))
5164
return None
5265

5366

5467
@show.command()
5568
def version() -> None:
56-
typer.echo(idom.__version__)
69+
echo(idom.__version__)
5770

5871

5972
@show.command()
6073
def environment() -> None:
6174
for n in settings.NAMES:
62-
typer.echo(f"{n}={getattr(settings, n)}")
75+
echo(f"{n}={os.environ[n]}")

idom/cli/console.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,21 @@
77
from .settings import IDOM_CLI_SHOW_SPINNER, IDOM_CLI_SHOW_OUTPUT, IDOM_CLI_DEBUG
88

99

10-
def echo(message: str, message_color: Optional[str] = None, **kwargs: Any) -> None:
10+
def echo(
11+
message: str,
12+
message_color: Optional[str] = None,
13+
debug: bool = False,
14+
**kwargs: Any
15+
) -> None:
1116
if message_color is not None:
1217
message = typer.style(message, fg=getattr(typer.colors, message_color.upper()))
13-
if IDOM_CLI_SHOW_OUTPUT:
18+
if (
19+
IDOM_CLI_SHOW_OUTPUT
20+
# filter debug messages
21+
and (not debug or IDOM_CLI_DEBUG)
22+
):
1423
typer.echo(message, **kwargs)
24+
return None
1525

1626

1727
@contextmanager
@@ -24,7 +34,7 @@ def spinner(message: str) -> Iterator[None]:
2434
disable=not (IDOM_CLI_SHOW_OUTPUT and IDOM_CLI_SHOW_SPINNER)
2535
):
2636
yield None
27-
except Exception as error:
37+
except Exception as error: # pragma: no cover
2838
echo(typer.style("✖️", fg=typer.colors.RED))
2939
echo(str(error), message_color="red")
3040
if IDOM_CLI_DEBUG:

idom/client/build_config.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,14 @@ def __init__(self, path: Path) -> None:
4040
for name, item in self.data["items"].items()
4141
}
4242

43-
def update(self, config_items: Iterable[Dict[str, Any]]) -> None:
43+
def clear_items(self) -> None:
44+
self.data["items"].clear()
45+
self._item_info.clear()
46+
47+
def update_items(self, config_items: Iterable[Dict[str, Any]]) -> None:
48+
# a useful debug assertion - the error which would result otherwise is confusing
49+
assert not isinstance(config_items, dict), "expected list, not a dict"
50+
4451
for conf in map(
4552
# BUG: https://github.com/python/mypy/issues/6697
4653
validate_config_item, # type: ignore
@@ -168,8 +175,6 @@ def find_build_config_item_in_python_file(
168175
js_pkg = module_path.parent.joinpath(
169176
*config_item["js_package"].split("/")
170177
)
171-
if not (js_pkg / "package.json").exists():
172-
raise ValueError(f"{str(js_pkg)!r} does not contain 'package.json'")
173178
config_item["js_package"] = str(js_pkg.resolve().absolute())
174179
config_item.setdefault("source_name", module_name)
175180
return config_item
@@ -213,14 +218,21 @@ def derive_config_item_info(config_item: Dict[str, Any]) -> ConfigItemInfo:
213218
aliases[dep_name] = dep_alias
214219
aliased_js_deps.append(f"{dep_alias}@npm:{dep}")
215220

216-
try:
221+
if "js_package" in config_item:
217222
js_pkg_path = Path(config_item["js_package"])
218-
with (js_pkg_path / "package.json").open() as f:
219-
js_pkg_name = str(json.load(f)["name"])
220-
except Exception:
221-
js_pkg_info = None
223+
js_pkg_json = js_pkg_path / "package.json"
224+
try:
225+
with js_pkg_json.open() as f:
226+
js_pkg_name = str(json.load(f)["name"])
227+
except FileNotFoundError as error:
228+
raise ValueError(
229+
f"Path to package {str(js_pkg_json)!r} specified by "
230+
f"{config_item['source_name']!r} does not exist"
231+
) from error
232+
else:
233+
js_pkg_info = JsPackageDef(path=js_pkg_path, name=js_pkg_name)
222234
else:
223-
js_pkg_info = JsPackageDef(path=js_pkg_path, name=js_pkg_name)
235+
js_pkg_info = None
224236

225237
return ConfigItemInfo(
226238
js_dependency_aliases=aliases,

idom/client/manage.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,18 @@ def build(config_items: Iterable[ConfigItem] = ()) -> None:
5757
py_pkg_configs, errors = find_python_packages_build_config_items()
5858
for e in errors: # pragma: no cover
5959
console.echo(f"{e} because {e.__cause__}", message_color="red")
60-
config.update(py_pkg_configs + list(config_items))
60+
config.update_items(py_pkg_configs + list(config_items))
61+
_build_and_save_config()
6162

63+
64+
def restore() -> None:
65+
config = build_config()
66+
config.clear_items()
67+
_build_and_save_config()
68+
69+
70+
def _build_and_save_config() -> None:
71+
config = build_config()
6272
with TemporaryDirectory() as tempdir:
6373
tempdir_path = Path(tempdir)
6474
temp_app_dir = tempdir_path / "app"
@@ -72,9 +82,15 @@ def build(config_items: Iterable[ConfigItem] = ()) -> None:
7282

7383
with open_modifiable_json(package_json_path) as package_json:
7484
snowpack_config = package_json.setdefault("snowpack", {})
85+
7586
snowpack_install = snowpack_config.setdefault("install", [])
7687
snowpack_install.extend(config.all_js_dependency_names())
7788

89+
snowpack_build = snowpack_config.setdefault("buildOptions", {})
90+
snowpack_build["clean"] = True
91+
92+
console.echo(f"Current config: {config.data}", debug=True)
93+
7894
with console.spinner(
7995
f"Installing {len(packages_to_install)} dependencies"
8096
if packages_to_install
@@ -89,20 +105,9 @@ def build(config_items: Iterable[ConfigItem] = ()) -> None:
89105
shutil.rmtree(BUILD_DIR)
90106

91107
shutil.copytree(temp_build_dir, BUILD_DIR, symlinks=True)
92-
93-
print(BUILD_DIR.exists())
94-
95108
config.save()
96109

97110

98-
def restore() -> None:
99-
with console.spinner("Restoring"):
100-
if BUILD_DIR.exists():
101-
shutil.rmtree(BUILD_DIR)
102-
_run_subprocess(["npm", "install"], APP_DIR)
103-
_run_subprocess(["npm", "run", "build"], APP_DIR)
104-
105-
106111
def _get_web_module_name_and_file_path(
107112
source_name: str, package_name: str
108113
) -> Tuple[str, Path]:
@@ -111,12 +116,12 @@ def _get_web_module_name_and_file_path(
111116
raise WebModuleError(
112117
f"Package {package_name!r} is not declared as a dependency of {source_name!r}"
113118
)
114-
builtin_path = BUILD_DIR.joinpath("web_modules", *f"{pkg_name}.js".split("/"))
115-
if not builtin_path.exists():
119+
build_path = BUILD_DIR.joinpath("web_modules", *f"{pkg_name}.js".split("/"))
120+
if not build_path.exists():
116121
raise WebModuleError(
117122
f"Dependency {package_name!r} of {source_name!r} was not installed"
118123
)
119-
return pkg_name, builtin_path
124+
return pkg_name, build_path
120125

121126

122127
def _npm_install(packages: Sequence[str], cwd: Path) -> None:
@@ -132,6 +137,6 @@ def _run_subprocess(args: List[str], cwd: Path) -> None:
132137
subprocess.run(
133138
args, cwd=cwd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
134139
)
135-
except subprocess.CalledProcessError as error:
140+
except subprocess.CalledProcessError as error: # pragma: no cover
136141
raise subprocess.SubprocessError(error.stderr.decode()) from error
137142
return None

tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,11 +307,11 @@ def add_dependency(*packages):
307307
@pytest.fixture
308308
def temp_build_config():
309309
config = build_config()
310-
original_data = deepcopy(config.data)
310+
original_cfgs = [deepcopy(cfg) for cfg in config.data["items"].values()]
311311
try:
312312
yield config
313313
finally:
314-
config.data = original_data
314+
config.update_items(original_cfgs)
315315
config.save()
316316

317317

tests/test_cli/test_commands.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import os
2+
import json
3+
4+
from typer.testing import CliRunner
5+
6+
import idom
7+
from idom.cli import main, settings
8+
from idom.client.manage import web_module_exists, build_config
9+
10+
11+
cli_runner = CliRunner()
12+
13+
14+
def test_build_no_args():
15+
result = cli_runner.invoke(main, ["build"])
16+
assert result.exit_code == 0
17+
18+
19+
def test_build_entrypoint(tmp_path):
20+
entrypoint_1 = tmp_path / "entrypoint_1.py"
21+
with entrypoint_1.open("w+") as f:
22+
f.write("idom_build_config = {'js_dependencies': ['jquery']}")
23+
24+
result = cli_runner.invoke(main, ["build", "--entrypoint", str(entrypoint_1)])
25+
assert result.exit_code == 0
26+
assert web_module_exists("__main__", "jquery")
27+
28+
entrypoint_2 = tmp_path / "entrypoint_2.py"
29+
entrypoint_2.touch()
30+
31+
result = cli_runner.invoke(main, ["build", "--entrypoint", str(entrypoint_2)])
32+
assert result.exit_code == 0
33+
assert f"No build config found in {str(entrypoint_2)!r}" in result.stdout
34+
35+
36+
def test_build_restore():
37+
result = cli_runner.invoke(main, ["build", "--restore"])
38+
assert result.exit_code == 0
39+
40+
41+
def test_build_entrypoint_and_restore_are_mutually_exclusive():
42+
result = cli_runner.invoke(
43+
main, ["build", "--restore", "--entrypoint", "something.py"]
44+
)
45+
assert result.exit_code == 1
46+
assert "mutually exclusive" in result.stdout
47+
48+
49+
def test_show_version():
50+
result = cli_runner.invoke(main, ["show", "version"])
51+
assert result.exit_code == 0
52+
assert idom.__version__ in result.stdout
53+
54+
55+
def test_show_build_config():
56+
result = cli_runner.invoke(main, ["show", "build-config"])
57+
assert result.exit_code == 0
58+
assert json.loads(result.stdout) == build_config().data
59+
60+
61+
def test_show_environment():
62+
result = cli_runner.invoke(main, ["show", "environment"])
63+
assert result.exit_code == 0
64+
shown = {}
65+
for line in result.stdout.split("\n"):
66+
if line.strip():
67+
name, value = line.split("=")
68+
shown[name] = value
69+
assert shown == {name: os.environ[name] for name in settings.NAMES}

0 commit comments

Comments
 (0)