Skip to content

Commit

Permalink
allow rats.projects to work on non-poetry components (#404)
Browse files Browse the repository at this point in the history
some small api breaking changes but i think this should make things
usable across any PEP 621 compliant project. i had to remove public apis
that relied on poetry configs that do not exist in PEP 621 because it
would result in a bunch of branching logic to handle every build
backend.

this should fix #341, #342, and #343 by updating ComponentTools methods
and deleting any `ci` command I couldn't easily update to be general.

i added some test projects that hopefully let us keep `poetry`, `uv`,
and `pdm` support working from now on.
  • Loading branch information
ms-lolo authored Dec 12, 2024
1 parent 1e58db5 commit 53bca28
Show file tree
Hide file tree
Showing 30 changed files with 313 additions and 70 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
PACKAGE_VERSION: "${{ steps.runner-context.outputs.package-version }}"
run: |
cd rats-devtools
rats-devtools.pipx ci update-version "$PACKAGE_VERSION"
poetry version "$PACKAGE_VERSION"
rats-devtools.pipx ci install
rats-devtools.pipx docs mkdocs-build
- name: "upload-gh-pages"
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/build-wheels.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ jobs:
PACKAGE_VERSION: "${{ steps.runner-context.outputs.package-version }}"
run: |
cd ${{ matrix.component }}
rats-devtools.pipx ci update-version "$PACKAGE_VERSION"
rats-devtools.pipx ci install build-wheel
poetry version "$PACKAGE_VERSION"
rats-devtools.pipx ci install
poetry build -f wheel
- name: "upload-artifacts"
uses: actions/upload-artifact@v4
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
PACKAGE_VERSION: "${{ steps.runner-context.outputs.package-version }}"
run: |
cd ${{ matrix.component }}
rats-devtools.pipx ci update-version "$PACKAGE_VERSION"
poetry version "$PACKAGE_VERSION"
rats-devtools.pipx ci install check test
- name: upload-coverage
uses: codecov/[email protected]
Expand Down
2 changes: 1 addition & 1 deletion rats-apps/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[tool.poetry]
name = "rats-apps"
description = "research analysis tools for building applications"
version = "0.3.0"
version = "0.4.0"
readme = "README.md"
authors = []
packages = [
Expand Down
2 changes: 1 addition & 1 deletion rats-devtools/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[tool.poetry]
name = "rats-devtools"
description = "Rats Development Tools"
version = "0.3.0"
version = "0.4.0"
readme = "README.md"
authors = []
packages = [
Expand Down
34 changes: 1 addition & 33 deletions rats-devtools/src/python/rats/ci/_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
import logging
from typing import NamedTuple

import click

from rats import apps, cli, projects

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -62,37 +60,7 @@ def test(self) -> None:
self._selected_component().run(*cmd)
print(f"ran {len(self._command_groups().test)} test commands")

@cli.command()
@click.argument("version")
def update_version(self, version: str) -> None:
"""Update the version of the package found in pyproject.toml."""
self._selected_component().poetry("version", version)

@cli.command()
def build_wheel(self) -> None:
"""Build a wheel for the package."""
self._selected_component().poetry("build", "-f", "wheel")

@cli.command()
def build_image(self) -> None:
"""Update the version of the package found in pyproject.toml."""
"""Build a container image of the component."""
self._project_tools().build_component_image(self._selected_component().find_path(".").name)

@cli.command()
@click.argument("repository_name")
def publish_wheel(self, repository_name: str) -> None:
"""
Publish the wheel to the specified repository.
This command assumes the caller has the required permissions and the specified repository
has been configured with poetry.
"""
self._selected_component().poetry(
"publish",
"--repository",
repository_name,
"--no-interaction",
# temporarily skip existing during testing
# not yet sure how to handle failures here
"--skip-existing",
)
44 changes: 27 additions & 17 deletions rats-devtools/src/python/rats/projects/_component_tools.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import logging
import subprocess
import sys
from collections.abc import Iterator
from collections.abc import Mapping
from os import symlink
from pathlib import Path
from shutil import copy, copytree, rmtree
from typing import NamedTuple
from typing import Any, NamedTuple

import toml

Expand All @@ -30,16 +30,10 @@ def __init__(self, path: Path) -> None:
self._path = path

def component_name(self) -> str:
# for now only supporting poetry components :(
return toml.loads((self.find_path("pyproject.toml")).read_text())["tool"]["poetry"]["name"]

def root_package_dirs(self) -> Iterator[str]:
# currently assuming packages have been specified in poetry pyproject.toml settings
pkgs = toml.loads(
(self.find_path("pyproject.toml")).read_text(),
)["tool"]["poetry"]["packages"]
for pkg in pkgs:
yield f"{pkg['from']}/{pkg['include']}"
if self.is_poetry_detected():
return self._load_pyproject()["tool"]["poetry"]["name"]
else:
return self._load_pyproject()["project"]["name"]

def symlink(self, src: Path, dst: Path) -> None:
"""
Expand Down Expand Up @@ -110,10 +104,6 @@ def _validate_project_path(self, path: Path) -> None:
if not path.is_relative_to(project):
raise ValueError(f"component path must be relative to project: {path}")

def install(self) -> None:
"""Install the dependencies of the component."""
self.poetry("install")

def pytest(self) -> None:
self.run("pytest")

Expand All @@ -124,9 +114,15 @@ def pyright(self) -> None:
self.run("pyright")

def run(self, *args: str) -> None:
self.poetry("run", *args)
if self.is_poetry_detected():
self.poetry("run", *args)
else:
self.exe(*args)

def poetry(self, *args: str) -> None:
if not self.is_poetry_detected():
raise RuntimeError(f"cannot run poetry commands in component: {self.component_name()}")

# when running a poetry command, we want to ignore any env we might be in.
# i'm not sure yet how reliable this is.
self.exe("env", "-u", "POETRY_ACTIVE", "-u", "VIRTUAL_ENV", "poetry", *args)
Expand All @@ -139,6 +135,20 @@ def exe(self, *cmd: str) -> None:
logger.error(f"failure detected: {' '.join(cmd)}")
sys.exit(e.returncode)

def is_poetry_detected(self) -> bool:
"""
Returns true if we think this component might be managed by poetry.
Since PEP 621 is gaining adoption, including by poetry, we should be able to remove most of
the complexity in trying to parse details out of pyproject.toml. This method is here until
we can fully delete any non PEP 621 code since we initially started as poetry-specific.
"""
data = self._load_pyproject()
return "tool" in data and "poetry" in data["tool"]

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


class UnsetComponentTools(ComponentTools):
def copy_tree(self, src: Path, dst: Path) -> None:
Expand Down
18 changes: 12 additions & 6 deletions rats-devtools/src/python/rats/projects/_project_tools.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import logging
import os
import subprocess
from collections.abc import Iterable
from functools import cache
from hashlib import sha256
from pathlib import Path
Expand Down Expand Up @@ -147,7 +146,7 @@ def devtools_component(self) -> ComponentId:
raise ComponentNotFoundError("was not able to find a devtools component in the project")

@cache # noqa: B019
def discover_components(self) -> Iterable[ComponentId]:
def discover_components(self) -> tuple[ComponentId, ...]:
valid_components = []
if self._is_single_component_project():
p = self.repo_root() / "pyproject.toml"
Expand Down Expand Up @@ -177,9 +176,13 @@ def discover_components(self) -> Iterable[ComponentId]:
logger.debug(f"detected unmanaged component: {p.name}")
continue

# how do we stop depending on poetry here?
poetry_name = component_info.get("tool", {}).get("poetry", {}).get("name")
# fall back to assuming PEP 621 compliance
name = poetry_name or component_info["project"]["name"]

# poetry code paths can be dropped once 2.x is released
# looks like we wait: https://github.com/python-poetry/poetry/pull/9135
valid_components.append(ComponentId(component_info["tool"]["poetry"]["name"]))
valid_components.append(ComponentId(name))

return tuple(valid_components)

Expand All @@ -195,15 +198,18 @@ def get_component(self, name: str) -> ComponentTools:

def repo_root(self) -> Path:
p = Path(self._config().path).resolve()
if not (p / ".git").exists():
# 99% of the time we just want the root of the repo
# but in tests we use sub-projects to create fake scenarios
# better test tooling can probably help us remove this later
if not (p / ".git").exists() and not (p / ".rats-root").exists():
raise ProjectNotFoundError(
f"repo root not found: {p}. devtools must be used on a project in a git repo."
)

return p

def _extract_tool_info(self, pyproject: Path) -> dict[str, bool]:
config = toml.loads(pyproject.read_text())["tool"].get("rats-devtools", {})
config = toml.loads(pyproject.read_text()).get("tool", {}).get("rats-devtools", {})

return {
"enabled": config.get("enabled", False),
Expand Down
2 changes: 0 additions & 2 deletions rats-devtools/test/python/rats_test/devtools/test_example.py

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from rats import projects


class TestComponentTools:
def test_basics(self) -> None:
tools = projects.ProjectTools(
lambda: projects.ProjectConfig(
name="example-project",
path="test/resources/projects",
image_registry="none",
image_push_on_build=False,
)
)

examples = ["pdm", "poetry", "uv"]
for x in examples:
component = tools.get_component(f"example-{x}")
assert component.component_name() == f"example-{x}"
15 changes: 15 additions & 0 deletions rats-devtools/test/python/rats_test/projects/test_project_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from rats import projects


class TestProjectTools:
def test_basics(self) -> None:
tools = projects.ProjectTools(
lambda: projects.ProjectConfig(
name="example-project",
path="test/resources/projects",
image_registry="none",
image_push_on_build=False,
)
)

assert len(tools.discover_components()) == 3
Loading

0 comments on commit 53bca28

Please sign in to comment.