Skip to content

Commit 67afae9

Browse files
authored
Add verify command (#3)
1 parent 4fea4bb commit 67afae9

File tree

10 files changed

+148
-23
lines changed

10 files changed

+148
-23
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
44

55
[tool.poetry]
66
name = "pyvarium"
7-
version = "0.1.1"
7+
version = "0.2.0"
88
description = "Tool for managing mixed Spack and pip packages"
99
readme = "README.md"
1010
authors = ["Robert Rosca <[email protected]>"]

src/pyvarium/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.1.1"
1+
__version__ = "0.2.0"

src/pyvarium/cli/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from rich.markdown import Markdown
1212
from rich.prompt import Confirm
1313

14-
from . import add, config, install, modulegen, new, sync
14+
from . import add, config, install, modulegen, new, sync, verify
1515

1616
app = typer.Typer()
1717

@@ -89,6 +89,7 @@ def main(
8989
app.add_typer(modulegen.app, name="modulegen")
9090
app.add_typer(new.app, name="new")
9191
app.add_typer(sync.app, name="sync")
92+
app.add_typer(verify.app, name="verify")
9293

9394

9495
if __name__ == "__main__":

src/pyvarium/cli/new.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def main(path: Path = typer.Argument(..., file_okay=False)):
2323
with Status("Spack setup") as status:
2424
se = spack.SpackEnvironment(path, status=status)
2525
se.new()
26-
se.add("python", "py-pip")
26+
se.add("python", "py-pip", "py-setuptools")
2727
se.concretize()
2828
se.install()
2929

src/pyvarium/cli/verify.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from pathlib import Path
2+
3+
import typer
4+
from rich.status import Status
5+
from loguru import logger
6+
7+
from pyvarium.installers import spack
8+
9+
app = typer.Typer(
10+
help="Check that python packages in view are still provided by spack."
11+
)
12+
13+
14+
@app.callback(invoke_without_command=True)
15+
def main(path: Path = typer.Option(".", file_okay=False), fix: bool = False):
16+
path = path.resolve()
17+
18+
with Status("Checking status of Spack packages in view") as status:
19+
se = spack.SpackEnvironment(path, status=status)
20+
warnings = se.verify()
21+
22+
if all(len(w) == 0 for w in warnings.values()):
23+
logger.info("All packages in view are correctly symlinked to spack")
24+
raise typer.Exit(0)
25+
26+
for path, warning in warnings.items():
27+
package_name = path.name
28+
if len(warning) > 0:
29+
logger.warning(
30+
f"[bold]{package_name}[/bold] has "
31+
f"[bold red]{len(warning)}[/bold red] files which are not linked correctly"
32+
)
33+
if fix:
34+
logger.info(f"Fixing {package_name}")
35+
for file, target in warning:
36+
if file.exists():
37+
file.unlink()
38+
file.symlink_to(target)
39+
40+
if fix:
41+
logger.info("Re-checking status of Spack packages in view")
42+
warnings = se.verify()
43+
if all(len(w) == 0 for w in warnings.values()):
44+
logger.info("All packages in view are correctly symlinked to spack")
45+
raise typer.Exit(0)
46+
else:
47+
broken_packages = [p.name for p in warnings if len(warnings[p]) > 0]
48+
logger.error(
49+
f"Some packages are still not linked correctly: {broken_packages}"
50+
)
51+
52+
raise typer.Exit(1)

src/pyvarium/config.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import shutil
22
from enum import Enum
33
from pathlib import Path
4-
from typing import Dict, Optional, Tuple
4+
from typing import Dict, Optional
55

66
import rtoml
77
from dynaconf import Dynaconf # type: ignore
@@ -16,7 +16,7 @@ class Scope(str, Enum):
1616
local = "local"
1717

1818

19-
def to_snake_case(d: dict) -> dict:
19+
def to_snake_case(d: Dict) -> Dict:
2020
"""Recursively converts the keys of a dictionary to snake_case style."""
2121
res = {}
2222
for k, v in d.items():
@@ -28,7 +28,7 @@ def to_snake_case(d: dict) -> dict:
2828
return res
2929

3030

31-
def to_str(d: dict) -> dict:
31+
def to_str(d: Dict) -> Dict:
3232
"""Recursively converts the values of a dictionary to str."""
3333
res = {}
3434
for k, v in d.items():
@@ -78,7 +78,7 @@ def load_dynaconf(cls) -> "Settings":
7878
)
7979

8080
# TODO: there is a planned feature for dynaconf to allow defining schemas with
81-
# pydantic directly, which would avoid this weird dynaconf -> dict -> dict with
81+
# pydantic directly, which would avoid this weird dynaconf -> Dict -> Dict with
8282
# different keys -> pydantic steps
8383
__dynaconf_dict__ = to_snake_case(cls.__dynaconf_settings__.to_dict()) # type: ignore
8484

src/pyvarium/installers/base.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,10 @@ def cmd(self, *args) -> subprocess.CompletedProcess:
6464
logger.debug(res)
6565

6666
if res.returncode != 0:
67-
raise RuntimeError(f"Process return code is not 0: {res=}")
67+
logger.error(res.stderr.decode())
68+
raise RuntimeError(
69+
f"Process return code is not 0: {res.args=}, {res.returncode=}"
70+
)
6871

6972
return res
7073

src/pyvarium/installers/pipenv.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class PipenvEnvironment(Environment):
2626
def __post_init__(self):
2727
self.program.cwd = self.path
2828
self.program.env["VIRTUAL_ENV"] = str((self.path / ".venv").absolute())
29+
self.program.env["PIPENV_VERBOSITY"] = "-1"
2930
self.program.env["PIPENV_PYTHON"] = str(
3031
(self.path / ".venv" / "bin" / "python").absolute()
3132
)
@@ -49,10 +50,10 @@ def new(
4950
return self.program.cmd(*commands)
5051

5152
def add(self, *packages):
52-
return self.program.cmd("install", *packages)
53+
return self.program.cmd("--site-packages", "install", *packages)
5354

5455
def install(self):
55-
return self.program.cmd("install")
56+
return self.program.cmd("--site-packages", "install")
5657

5758
def lock(self):
5859
return self.program.cmd("lock")

src/pyvarium/installers/spack.py

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import shutil
33
import subprocess
44
from pathlib import Path
5-
from typing import List, Literal, Union, overload
5+
from typing import List, Literal, Union, overload, Dict
66

77
import yaml
88
from loguru import logger
@@ -21,7 +21,7 @@ def recursive_dict_update(d, u):
2121
return d
2222

2323

24-
def cmd_json_to_dict(cmd: subprocess.CompletedProcess) -> dict:
24+
def cmd_json_to_dict(cmd: subprocess.CompletedProcess) -> Dict:
2525
return json.loads(cmd.stdout.decode())
2626

2727

@@ -87,18 +87,18 @@ def install(self):
8787

8888
return self.cmd("install", "--only-concrete", "--no-add")
8989

90-
# def spec(self, spec: str) -> dict:
90+
# def spec(self, spec: str) -> Dict:
9191
# res = self.cmd("spec", "-I", "--reuse", "--json", spec)
9292
# return cmd_json_to_dict(res)
9393

9494
def concretize(self):
9595
return self.cmd("concretize", "--reuse")
9696

97-
def find(self) -> dict:
97+
def find(self) -> Dict:
9898
res = self.cmd("find", "--json")
9999
return cmd_json_to_dict(res)
100100

101-
# def find_missing(self) -> dict:
101+
# def find_missing(self) -> Dict:
102102
# res = self.cmd(
103103
# "find", "--show-concretized", "--deps", "--only-missing", "--json"
104104
# )
@@ -130,18 +130,40 @@ def find_python_packages(
130130
packages_dict: List[dict] = json.loads(packages_json)
131131

132132
if only_names:
133-
return [
134-
f"{p['name']}=={p['version']}"
135-
for p in packages_dict
136-
if p["name"] != "pip"
137-
]
133+
return [f"{p['name']}=={p['version']}" for p in packages_dict]
138134
else:
139135
return packages_dict
140136

141-
def get_config(self) -> dict:
137+
def verify(self) -> Dict[Path, list]:
138+
view_path = self.path / ".venv"
139+
packages = list((view_path / ".spack").iterdir())
140+
141+
package_warnings = {}
142+
143+
for package in packages:
144+
self.program.update_status(f"{package.name}")
145+
manifest_file = package / "install_manifest.json"
146+
manifest = json.loads(manifest_file.read_text())
147+
package_path = manifest_file.resolve().parent.parent
148+
files = {
149+
Path(k.replace(str(package_path), str(view_path))): Path(k)
150+
for k, v in manifest.items()
151+
if v["type"] == "file" and ".spack" not in k and "bin" not in k
152+
}
153+
154+
warnings = []
155+
for link, target in files.items():
156+
if not link.resolve() == target.resolve():
157+
warnings.append((link, target))
158+
159+
package_warnings[package] = warnings
160+
161+
return package_warnings
162+
163+
def get_config(self) -> Dict:
142164
return yaml.safe_load((self.path / "spack.yaml").read_text())
143165

144-
def set_config(self, config: dict) -> None:
166+
def set_config(self, config: Dict) -> None:
145167
current_config = self.get_config()
146168
new_config = recursive_dict_update(current_config, config)
147169

tests/cli/test_verify.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import os
2+
from pathlib import Path
3+
4+
import pytest
5+
from typer.testing import CliRunner
6+
7+
from pyvarium.cli import app
8+
9+
runner = CliRunner()
10+
11+
12+
@pytest.fixture(autouse=True, scope="module")
13+
def tmp_cwd(tmp_path_factory):
14+
tmpdir = tmp_path_factory.mktemp("cli.verify")
15+
os.chdir(tmpdir)
16+
17+
res = runner.invoke(app, ["new", "test-env"])
18+
assert res.exit_code == 0
19+
20+
os.chdir(tmpdir / "test-env")
21+
22+
res = runner.invoke(app, ["add", "spack", "py-numpy"])
23+
assert res.exit_code == 0
24+
25+
yield tmpdir / "test-env"
26+
27+
28+
def test_verify_success(tmp_cwd: Path):
29+
res = runner.invoke(app, ["verify"])
30+
venv_site_packages = tmp_cwd / ".venv" / "lib" / "python3.8" / "site-packages"
31+
assert (venv_site_packages / "numpy" / "version.py").is_symlink()
32+
assert res.exit_code == 0
33+
assert "correctly symlinked to spack" in res.stdout
34+
35+
36+
@pytest.mark.xfail(
37+
reason="binary cache injects placeholders to path which breaks tests in image"
38+
)
39+
def test_verify_failure(tmp_cwd: Path):
40+
venv_site_packages = tmp_cwd / ".venv" / "lib" / "python3.8" / "site-packages"
41+
numpy_file = venv_site_packages / "numpy" / "version.py"
42+
numpy_file.unlink()
43+
assert not numpy_file.is_symlink()
44+
res = runner.invoke(app, ["verify"])
45+
print(res.stdout)
46+
assert res.exit_code != 0

0 commit comments

Comments
 (0)