Skip to content

Commit

Permalink
Add support for installing project with extras
Browse files Browse the repository at this point in the history
ghstack-source-id: b3b19a6bec81e1341a14f8b089d24ccedd95e321
Pull Request resolved: #85
  • Loading branch information
amyreese committed May 17, 2024
1 parent 237bdd5 commit 606a66e
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 9 deletions.
2 changes: 2 additions & 0 deletions thx/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ def load_config(path: Optional[Path] = None) -> Config:
requirements: List[str] = ensure_listish(
data.pop("requirements", None), "tool.thx.requirements"
)
extras: List[str] = ensure_listish(data.pop("extras", None), "tool.thx.extras")
watch_paths: Set[Path] = {
Path(p)
for p in ensure_listish(
Expand All @@ -167,6 +168,7 @@ def load_config(path: Optional[Path] = None) -> Config:
values=values,
versions=versions,
requirements=requirements,
extras=extras,
watch_paths=watch_paths,
)
)
Expand Down
19 changes: 13 additions & 6 deletions thx/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import shutil
import subprocess
import time
from itertools import chain
from pathlib import Path
from typing import AsyncIterator, Dict, List, Optional, Sequence, Tuple

Expand Down Expand Up @@ -160,10 +161,12 @@ def needs_update(context: Context, config: Config) -> bool:
if timestamp.exists():
base = timestamp.stat().st_mtime_ns
newest = 0
reqs = project_requirements(config)
for req in reqs:
if req.exists():
mod_time = req.stat().st_mtime_ns
for path in chain(
[config.root / "pyproject.toml"],
project_requirements(config),
):
if path.exists():
mod_time = path.stat().st_mtime_ns
newest = max(newest, mod_time)
return newest > base

Expand Down Expand Up @@ -219,9 +222,9 @@ async def prepare_virtualenv(context: Context, config: Config) -> AsyncIterator[
pip = which("pip", context)

# install requirements.txt
yield VenvCreate(context, message="installing requirements")
requirements = project_requirements(config)
if requirements:
yield VenvCreate(context, message="installing requirements")
LOG.debug("installing deps from %s", requirements)
cmd: List[StrPath] = [pip, "install", "-U"]
for requirement in requirements:
Expand All @@ -230,7 +233,11 @@ async def prepare_virtualenv(context: Context, config: Config) -> AsyncIterator[

# install local project
yield VenvCreate(context, message="installing project")
await check_command([pip, "install", "-U", config.root])
if config.extras:
proj = f"{config.root}[{','.join(config.extras)}]"
else:
proj = str(config.root)
await check_command([pip, "install", "-U", proj])

# timestamp marker
content = f"{time.time_ns()}\n"
Expand Down
4 changes: 4 additions & 0 deletions thx/tests/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ def test_complex_config(self) -> None:
[tool.thx]
default = ["test", "lint"]
module = "foobar"
requirements = "requirements/dev.txt"
extras = "docs"
watch_paths = ["foobar", "pyproject.toml"]
[tool.thx.values]
Expand Down Expand Up @@ -204,6 +206,8 @@ def test_complex_config(self) -> None:
),
},
values={"module": "foobar", "something": "else"},
requirements=["requirements/dev.txt"],
extras=["docs"],
watch_paths={Path("foobar"), Path("pyproject.toml")},
)
result = load_config(td)
Expand Down
65 changes: 62 additions & 3 deletions thx/tests/context.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright 2022 Amethyst Reese
# Licensed under the MIT License

import asyncio
import platform
import subprocess
from pathlib import Path
Expand Down Expand Up @@ -116,8 +117,8 @@ def test_find_runtime_no_venv_binary_found(
tdp = Path(td).resolve()
config = Config(root=tdp)

which_mock.side_effect = (
lambda b: f"/fake/bin/{b}" if "." not in b else None
which_mock.side_effect = lambda b: (
f"/fake/bin/{b}" if "." not in b else None
)

for version in TEST_VERSIONS:
Expand Down Expand Up @@ -340,6 +341,8 @@ async def test_needs_update(self) -> None:
with TemporaryDirectory() as td:
tdp = Path(td).resolve()

pyproj = tdp / "pyproject.toml"
pyproj.write_text("\n")
reqs = tdp / "requirements.txt"
reqs.write_text("\n")

Expand All @@ -355,6 +358,62 @@ async def test_needs_update(self) -> None:
(venv / context.TIMESTAMP).write_text("0\n")
self.assertFalse(context.needs_update(ctx, config))

with self.subTest("touch pyproject.toml"):
await asyncio.sleep(0.01)
pyproj.write_text("\n\n")
self.assertTrue(context.needs_update(ctx, config))

@patch("thx.context.check_command")
@patch("thx.context.which")
@async_test
async def test_prepare_virtualenv_extras(
self, which_mock: Mock, run_mock: Mock
) -> None:
self.maxDiff = None

async def fake_check_command(cmd: Sequence[StrPath]) -> CommandResult:
return CommandResult(0, "", "")

run_mock.side_effect = fake_check_command
which_mock.side_effect = lambda b, ctx: f"{ctx.venv / 'bin'}/{b}"

with TemporaryDirectory() as td:
tdp = Path(td).resolve()
venv = tdp / ".thx" / "venv" / "3.9"
venv.mkdir(parents=True)

config = Config(root=tdp, extras=["more"])
ctx = Context(Version("3.9"), venv / "bin" / "python", venv)
pip = which_mock("pip", ctx)
reqs = context.project_requirements(config)
self.assertEqual([], reqs)

events = [event async for event in context.prepare_virtualenv(ctx, config)]
expected = [
VenvCreate(ctx, "creating virtualenv"),
VenvCreate(ctx, "upgrading pip"),
VenvCreate(ctx, "installing project"),
VenvReady(ctx),
]
self.assertEqual(expected, events)

run_mock.assert_has_calls(
[
call(
[
ctx.python_path,
"-m",
"pip",
"install",
"-U",
"pip",
"setuptools",
]
),
call([pip, "install", "-U", str(config.root) + "[more]"]),
],
)

@patch("thx.context.check_command")
@patch("thx.context.which")
@async_test
Expand Down Expand Up @@ -397,7 +456,7 @@ async def fake_check_command(cmd: Sequence[StrPath]) -> CommandResult:
]
),
call([pip, "install", "-U", "-r", reqs]),
call([pip, "install", "-U", config.root]),
call([pip, "install", "-U", str(config.root)]),
]
)

Expand Down
1 change: 1 addition & 0 deletions thx/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class Config:
values: Mapping[str, str] = field(default_factory=dict)
versions: Sequence[Version] = field(default_factory=list)
requirements: Sequence[str] = field(default_factory=list)
extras: Sequence[str] = field(default_factory=list)
watch_paths: Set[Path] = field(default_factory=set)

def __post_init__(self) -> None:
Expand Down

0 comments on commit 606a66e

Please sign in to comment.