Skip to content

Commit fa54665

Browse files
authored
deprecate poetry specific component tools methods, and add support for uv (#555)
temporarily added manual support for uv but want to look for better options to support any arbitrary packaging tool of the past or future.
1 parent 41c74c3 commit fa54665

23 files changed

Lines changed: 339 additions & 300 deletions

File tree

.github/actions/setup-environment/action.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ runs:
6060
shell: bash
6161
run: |
6262
pipx install poetry
63+
pipx install uv
6364
poetry self add poetry-monorepo-dependency-plugin
6465
- id: "install-devtools"
6566
if: ${{ inputs.install-devtools }}

rats-apps/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
22
name = "rats-apps"
33
description = "research analysis tools for building applications"
4-
version = "0.11.2"
4+
version = "0.12.0"
55
readme = "README.md"
66
requires-python = ">=3.10,<4.0"
77
authors = [

rats-devtools/poetry.lock

Lines changed: 78 additions & 136 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rats-devtools/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
22
name = "rats-devtools"
33
description = "Rats Development Tools"
4-
version = "0.11.2"
4+
version = "0.12.0"
55
readme = "README.md"
66
requires-python = ">=3.10,<4.0"
77
authors = [

rats-devtools/src/rats/projects/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@
2020
"""
2121

2222
from ._component_tools import ComponentId, ComponentTools, UnsetComponentTools
23-
from ._plugin import PluginConfigs, PluginContainer, PluginServices, find_repo_root
23+
from ._plugin import (
24+
PluginConfigs,
25+
PluginContainer,
26+
PluginServices,
27+
find_nearest_component,
28+
find_repo_root,
29+
)
2430
from ._project_tools import (
2531
ComponentNotFoundError,
2632
ProjectConfig,
@@ -39,5 +45,6 @@
3945
"ProjectNotFoundError",
4046
"ProjectTools",
4147
"UnsetComponentTools",
48+
"find_nearest_component",
4249
"find_repo_root",
4350
]

rats-devtools/src/rats/projects/_component_tools.py

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
import subprocess
33
import sys
4+
import warnings
45
from collections.abc import Mapping
56
from os import symlink
67
from pathlib import Path
@@ -110,20 +111,27 @@ def ruff(self, *args: str) -> None:
110111
def pyright(self) -> None:
111112
self.run("pyright")
112113

113-
def run(self, *args: str) -> None:
114-
if self.is_poetry_detected():
115-
self.poetry("run", *args)
116-
else:
117-
self.exe(*args)
118-
119114
def poetry(self, *args: str) -> None:
115+
warnings.warn(
116+
"The ComponentTools.poetry() method is deprecated. Use ComponentTools.run() instead.",
117+
DeprecationWarning,
118+
stacklevel=2,
119+
)
120120
if not self.is_poetry_detected():
121121
raise RuntimeError(f"cannot run poetry commands in component: {self.component_name()}")
122122

123-
# when running a poetry command, we want to ignore any env we might be in.
124-
# i'm not sure yet how reliable this is.
125123
self.exe("env", "-u", "POETRY_ACTIVE", "-u", "VIRTUAL_ENV", "poetry", *args)
126124

125+
def run(self, *args: str) -> None:
126+
"""Tries to run a command within the component's venv."""
127+
# generally try to unset any package manager venv specific details
128+
if self.is_poetry_detected():
129+
self.exe("env", "-u", "POETRY_ACTIVE", "-u", "VIRTUAL_ENV", "poetry", "run", *args)
130+
elif self._is_uv_detected():
131+
self.exe("env", "-u", "UV_ACTIVE", "-u", "VIRTUAL_ENV", "uv", "run", *args)
132+
else:
133+
self.exe(*args)
134+
127135
def exe(self, *cmd: str) -> None:
128136
"""Run a command from the root of a component."""
129137
logger.debug(f"executing in {self._path}/: {' '.join(cmd)}")
@@ -142,7 +150,27 @@ def is_poetry_detected(self) -> bool:
142150
we can fully delete any non PEP 621 code since we initially started as poetry-specific.
143151
"""
144152
data = self._load_pyproject()
145-
return "tool" in data and "poetry" in data["tool"]
153+
if "tool" in data and "poetry" in data["tool"]:
154+
# we found some poetry values in the toml file
155+
return True
156+
157+
# make double sure by checking if we see a lockfile for poetry
158+
return self.find_path("poetry.lock").is_file()
159+
160+
def _is_uv_detected(self) -> bool:
161+
"""
162+
Returns true if we think this component might be managed by uv.
163+
164+
Keeping this private because the hope is to remove all coupling to specific packaging
165+
tools,
166+
"""
167+
data = self._load_pyproject()
168+
if "tool" in data and "uv" in data["tool"]:
169+
# we found some uv values in the toml file
170+
return True
171+
172+
# make double sure by checking if we see a lockfile for poetry
173+
return self.find_path("uv.lock").is_file()
146174

147175
def _load_pyproject(self) -> Mapping[str, Any]:
148176
return toml.loads((self.find_path("pyproject.toml")).read_text())

rats-devtools/src/rats/projects/_plugin.py

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
from rats import apps
66

77
from ._component_tools import ComponentTools
8-
from ._project_tools import ProjectConfig, ProjectNotFoundError, ProjectTools
8+
from ._project_tools import (
9+
ComponentNotFoundError,
10+
ProjectConfig,
11+
ProjectNotFoundError,
12+
ProjectTools,
13+
)
914

1015
logger = logging.getLogger(__name__)
1116

@@ -44,7 +49,7 @@ def _active_component_tools(self) -> ComponentTools:
4449
# or we're not in a component and we should use the devtools component by default
4550
# i'm not sure this is the best and most expected behavior
4651
# but it lets us run things like the docs tools from the root of a big repo
47-
return ptools.get_component(cwd.name)
52+
return ptools.get_component(ptools.project_name())
4853

4954
name = relative.split("/")[0] # the component sits at the root of the project, for now
5055
return ptools.get_component(name)
@@ -58,10 +63,11 @@ def _project_tools(self) -> ProjectTools:
5863

5964
@apps.fallback_service(PluginServices.CONFIGS.PROJECT)
6065
def _project_config(self) -> ProjectConfig:
61-
repo_root = str(find_repo_root())
66+
repo_root = find_repo_root()
6267
return ProjectConfig(
63-
name=os.environ.get("DEVTOOLS_PROJECT_NAME", "default-project"),
64-
path=repo_root,
68+
# default to assuming the repo root folder name is the project name
69+
name=os.environ.get("DEVTOOLS_PROJECT_NAME", repo_root.name),
70+
path=repo_root.as_posix(),
6571
image_registry=os.environ.get("DEVTOOLS_IMAGE_REGISTRY", "default.local"),
6672
image_push_on_build=bool(os.environ.get("DEVTOOLS_IMAGE_PUSH_ON_BUILD", True)),
6773
# default tag gets calculated from the project hash
@@ -70,19 +76,25 @@ def _project_config(self) -> ProjectConfig:
7076
)
7177

7278

73-
def find_repo_root() -> Path:
79+
def find_repo_root(cwd: Path | None = None) -> Path:
7480
"""
7581
Try to find the path to the root of the repo.
7682
7783
This method traverses up the directory tree, starting from the working directory, and looks for
7884
the first directory that contains a `.git` directory. This behavior can be overwritten by
7985
defining a `DEVTOOLS_PROJECT_ROOT` environment variable.
86+
87+
Args:
88+
cwd: optionally provide a starting search directory
8089
"""
8190
env = os.environ.get("DEVTOOLS_PROJECT_ROOT")
8291
if env:
8392
return Path(env)
8493

85-
guess = Path().resolve()
94+
if cwd is None:
95+
cwd = Path.cwd()
96+
97+
guess = cwd.resolve()
8698
while str(guess) != "/":
8799
if (guess / ".git").exists():
88100
return guess
@@ -91,3 +103,25 @@ def find_repo_root() -> Path:
91103
raise ProjectNotFoundError(
92104
"repo root not found. devtools must be used on a project in a git repo."
93105
)
106+
107+
108+
def find_nearest_component(cwd: Path | None = None) -> Path:
109+
"""
110+
Try to find the path to the root of the nearest component.
111+
112+
This method traverses up the directory tree, starting from the working directory, and looks for
113+
the first directory that contains a `pyproject.toml` file.
114+
115+
Args:
116+
cwd: optionally provide a starting search directory
117+
"""
118+
if cwd is None:
119+
cwd = Path.cwd()
120+
121+
guess = cwd.resolve()
122+
while str(guess) != "/":
123+
if (guess / "pyproject.toml").exists() and (guess / "pyproject.toml").is_file():
124+
return guess
125+
guess = guess.parent
126+
127+
raise ComponentNotFoundError(f"component root not found from cwd: {cwd.as_posix()}.")

rats-devtools/test/rats_test/projects/test_component_tools.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import subprocess
12
from pathlib import Path
23

34
from rats import projects
@@ -39,3 +40,41 @@ def test_nested(self) -> None:
3940
"test/rats_test_resources/projects/example-1/nested/example-nested-uv/README.md",
4041
).resolve()
4142
assert str(readme_path) == str(expected)
43+
44+
def test_executing_uv(self) -> None:
45+
tools = projects.ProjectTools(
46+
lambda: projects.ProjectConfig(
47+
name="example-project",
48+
path="test/rats_test_resources/projects/example-1",
49+
image_registry="none",
50+
image_push_on_build=False,
51+
)
52+
)
53+
54+
examples = [
55+
"uv",
56+
"nested-uv",
57+
]
58+
for x in examples:
59+
component = tools.get_component(f"example-{x}")
60+
61+
cmd = [
62+
"env",
63+
"-u",
64+
"UV_ACTIVE",
65+
"-u",
66+
"VIRTUAL_ENV",
67+
"uv",
68+
"run",
69+
"python",
70+
"-m",
71+
component.component_name().replace("-", "_"),
72+
]
73+
74+
result = subprocess.run(
75+
cmd,
76+
capture_output=True,
77+
text=True,
78+
cwd=component.find_path("."),
79+
)
80+
assert result.stdout.strip() == f"hello, world! example-1/{component.component_name()}"

rats-devtools/test/rats_test_resources/projects/example-1/example-poetry/README.md

Whitespace-only changes.

rats-devtools/test/rats_test_resources/projects/example-1/example-poetry/poetry.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)