Skip to content

Commit 6f7ead8

Browse files
committed
Add support for installing project with extras
ghstack-source-id: 0b6103f Pull Request resolved: #85
1 parent d7a1ed3 commit 6f7ead8

File tree

5 files changed

+82
-9
lines changed

5 files changed

+82
-9
lines changed

thx/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ def load_config(path: Optional[Path] = None) -> Config:
148148
requirements: List[str] = ensure_listish(
149149
data.pop("requirements", None), "tool.thx.requirements"
150150
)
151+
extras: List[str] = ensure_listish(data.pop("extras", None), "tool.thx.extras")
151152
watch_paths: Set[Path] = {
152153
Path(p)
153154
for p in ensure_listish(
@@ -167,6 +168,7 @@ def load_config(path: Optional[Path] = None) -> Config:
167168
values=values,
168169
versions=versions,
169170
requirements=requirements,
171+
extras=extras,
170172
watch_paths=watch_paths,
171173
)
172174
)

thx/context.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import shutil
88
import subprocess
99
import time
10+
from itertools import chain
1011
from pathlib import Path
1112
from typing import AsyncIterator, Dict, List, Optional, Sequence, Tuple
1213

@@ -160,10 +161,12 @@ def needs_update(context: Context, config: Config) -> bool:
160161
if timestamp.exists():
161162
base = timestamp.stat().st_mtime_ns
162163
newest = 0
163-
reqs = project_requirements(config)
164-
for req in reqs:
165-
if req.exists():
166-
mod_time = req.stat().st_mtime_ns
164+
for path in chain(
165+
[config.root / "pyproject.toml"],
166+
project_requirements(config),
167+
):
168+
if path.exists():
169+
mod_time = path.stat().st_mtime_ns
167170
newest = max(newest, mod_time)
168171
return newest > base
169172

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

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

231234
# install local project
232235
yield VenvCreate(context, message="installing project")
233-
await check_command([pip, "install", "-U", config.root])
236+
if config.extras:
237+
proj = f"{config.root}[{','.join(config.extras)}]"
238+
else:
239+
proj = str(config.root)
240+
await check_command([pip, "install", "-U", proj])
234241

235242
# timestamp marker
236243
content = f"{time.time_ns()}\n"

thx/tests/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ def test_complex_config(self) -> None:
162162
[tool.thx]
163163
default = ["test", "lint"]
164164
module = "foobar"
165+
requirements = "requirements/dev.txt"
166+
extras = "docs"
165167
watch_paths = ["foobar", "pyproject.toml"]
166168
167169
[tool.thx.values]
@@ -204,6 +206,8 @@ def test_complex_config(self) -> None:
204206
),
205207
},
206208
values={"module": "foobar", "something": "else"},
209+
requirements=["requirements/dev.txt"],
210+
extras=["docs"],
207211
watch_paths={Path("foobar"), Path("pyproject.toml")},
208212
)
209213
result = load_config(td)

thx/tests/context.py

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Copyright 2022 Amethyst Reese
22
# Licensed under the MIT License
33

4+
import asyncio
45
import platform
56
import subprocess
67
from pathlib import Path
@@ -116,8 +117,8 @@ def test_find_runtime_no_venv_binary_found(
116117
tdp = Path(td).resolve()
117118
config = Config(root=tdp)
118119

119-
which_mock.side_effect = (
120-
lambda b: f"/fake/bin/{b}" if "." not in b else None
120+
which_mock.side_effect = lambda b: (
121+
f"/fake/bin/{b}" if "." not in b else None
121122
)
122123

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

344+
pyproj = tdp / "pyproject.toml"
345+
pyproj.write_text("\n")
343346
reqs = tdp / "requirements.txt"
344347
reqs.write_text("\n")
345348

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

361+
with self.subTest("touch pyproject.toml"):
362+
await asyncio.sleep(0.01)
363+
pyproj.write_text("\n\n")
364+
self.assertTrue(context.needs_update(ctx, config))
365+
366+
@patch("thx.context.check_command")
367+
@patch("thx.context.which")
368+
@async_test
369+
async def test_prepare_virtualenv_extras(
370+
self, which_mock: Mock, run_mock: Mock
371+
) -> None:
372+
self.maxDiff = None
373+
374+
async def fake_check_command(cmd: Sequence[StrPath]) -> CommandResult:
375+
return CommandResult(0, "", "")
376+
377+
run_mock.side_effect = fake_check_command
378+
which_mock.side_effect = lambda b, ctx: f"{ctx.venv / 'bin'}/{b}"
379+
380+
with TemporaryDirectory() as td:
381+
tdp = Path(td).resolve()
382+
venv = tdp / ".thx" / "venv" / "3.9"
383+
venv.mkdir(parents=True)
384+
385+
config = Config(root=tdp, extras=["more"])
386+
ctx = Context(Version("3.9"), venv / "bin" / "python", venv)
387+
pip = which_mock("pip", ctx)
388+
reqs = context.project_requirements(config)
389+
self.assertEqual([], reqs)
390+
391+
events = [event async for event in context.prepare_virtualenv(ctx, config)]
392+
expected = [
393+
VenvCreate(ctx, "creating virtualenv"),
394+
VenvCreate(ctx, "upgrading pip"),
395+
VenvCreate(ctx, "installing project"),
396+
VenvReady(ctx),
397+
]
398+
self.assertEqual(expected, events)
399+
400+
run_mock.assert_has_calls(
401+
[
402+
call(
403+
[
404+
ctx.python_path,
405+
"-m",
406+
"pip",
407+
"install",
408+
"-U",
409+
"pip",
410+
"setuptools",
411+
]
412+
),
413+
call([pip, "install", "-U", str(config.root) + "[more]"]),
414+
],
415+
)
416+
358417
@patch("thx.context.check_command")
359418
@patch("thx.context.which")
360419
@async_test
@@ -397,7 +456,7 @@ async def fake_check_command(cmd: Sequence[StrPath]) -> CommandResult:
397456
]
398457
),
399458
call([pip, "install", "-U", "-r", reqs]),
400-
call([pip, "install", "-U", config.root]),
459+
call([pip, "install", "-U", str(config.root)]),
401460
]
402461
)
403462

thx/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ class Config:
7474
values: Mapping[str, str] = field(default_factory=dict)
7575
versions: Sequence[Version] = field(default_factory=list)
7676
requirements: Sequence[str] = field(default_factory=list)
77+
extras: Sequence[str] = field(default_factory=list)
7778
watch_paths: Set[Path] = field(default_factory=set)
7879

7980
def __post_init__(self) -> None:

0 commit comments

Comments
 (0)