Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 36 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Read that file for the full list. The most important ones for day-to-day work a
| `make format` | Formats all Python files with Ruff. Run this before committing. |
| `make lint` | Lints with safe fixes. |
| `make type_checks` | Runs mypy across all packages. |
| `make uv_lock` | Updates `uv.lock` in the dev container after changing dependencies. |
| `make test_pypi` | Runs unit and feature tests for `pypi_package` only. Faster than `make test`. |
| `make test_build_support` | Runs unit and feature tests for `build_support` only. |

Expand All @@ -63,6 +64,8 @@ handle the Docker plumbing automatically.

**`build` (`template_python_project:build`)**

(Image name comes from `[project] name` in pyproject.toml.)

The CI/CD orchestration container. This is what every `make` command uses internally.
It installs only the `build` dependency group and puts only `build_support/src` on
`PYTHONPATH`. You never need to target this image directly; the Makefile does it for
Expand Down Expand Up @@ -103,7 +106,7 @@ docker run --rm \
--workdir=/usr/dev \
-v "$(pwd):/usr/dev" \
-e PYTHONPATH=/usr/dev/pypi_package/src:/usr/dev/build_support/src:/usr/dev/infra/src:/usr/dev/pypi_package/test:/usr/dev/build_support/test \
template-python-project:dev \
template_python_project:dev \
<command>
```

Expand All @@ -120,14 +123,42 @@ docker run --rm \
--workdir=/usr/dev \
-v "$(pwd):/usr/dev" \
-e PYTHONPATH=/usr/dev/pypi_package/src \
template-python-project:prod \
template_python_project:prod \
python pypi_package/src/template_python_project/main.py --input in.json --output out.json
```

### Dependency Changes
### Lock file / dependency updates

A convenient way to update the lock file after changing dependencies in ``pyproject.toml``:

```bash
make uv_lock
```

That target builds the dev image if needed and runs ``uv lock`` in it, then you commit the
updated ``uv.lock``.

If you need to run the lock step without the Makefile (e.g. in a bad state), use the dev
image directly:

```bash
docker run --rm -v "$(pwd):/usr/dev" -w /usr/dev template_python_project:dev uv lock
```

**If in a bad state**

If ``pyproject.toml``, ``uv.lock``, and the Dockerfile are out of sync:

#. Stash or discard your local changes to ``pyproject.toml`` and ``uv.lock``, then reset
them to the last known-good commit.
#. Run ``make uv_lock`` (or ``make open_dev_docker_shell`` and run ``uv lock`` in the
shell).
#. Commit the updated ``uv.lock``. The next Docker build will use it.

### Dependency changes

Never run `poetry install` or `poetry lock` outside a container.
`docker run ... <command>` is sufficient.
Never run ``uv sync`` or ``uv lock`` outside a container. Use ``make uv_lock`` or the
dev image command above for lock file updates.

---

Expand Down
32 changes: 15 additions & 17 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
FROM python:3.13.5 AS base
# If python version changed here change in pyproject.toml
# [tool.poetry.dependencies]
# If python version changed here change in pyproject.toml [project] requires-python

# make sure to update build.system.requries poetry version in pyproject.toml
ENV POETRY_VERSION="2.1.3"
ENV POETRY_VIRTUALENVS_CREATE=false
ENV POETRY_NO_INTERACTION=1
ENV POETRY_HOME="/opt/poetry"
COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /uvx /bin/

ENV PATH="$POETRY_HOME/bin:$PATH"
ENV UV_SYSTEM_PYTHON=1

RUN pip install --upgrade pip==25.1.1
RUN pip install poetry==$POETRY_VERSION
RUN pip install --upgrade pip==26.0.1

COPY poetry.lock poetry.lock
COPY uv.lock uv.lock
COPY README.md README.md
COPY pyproject.toml pyproject.toml

Expand Down Expand Up @@ -66,26 +60,30 @@ RUN chown $CURRENT_USER_ID:$CURRENT_GROUP_ID $DOCKER_REMOTE_PROJECT_ROOT

FROM git_enabled AS build

RUN poetry install --no-root --with build
RUN uv sync --frozen --no-install-project --extra build
ENV PATH="/.venv/bin:$PATH"

FROM docker_enabled AS dev

RUN poetry install --no-root --with dev --with build --with pulumi
RUN uv sync --frozen --no-install-project --extra build --extra dev --extra pulumi
ENV PATH="/.venv/bin:$PATH"

FROM base AS prod

RUN poetry install --no-root
RUN uv sync --frozen --no-install-project
ENV PATH="/.venv/bin:$PATH"

COPY pypi_package/src /usr/dev/pypi_package/src
ENV PYTHONPATH=/usr/dev/pypi_package/src
WORKDIR /usr/dev

FROM base AS pulumi

RUN poetry install --no-root --with pulumi
RUN uv sync --frozen --no-install-project --extra pulumi
ENV PATH="/.venv/bin:$PATH"

# make sure to update tool.poetry.group.pulumi.dependencies.pulumi in pyproject.toml
ARG PULUMI_VERSION=3.185.0
# If changed here change [project.optional-dependencies] pulumi in pyproject.toml
ARG PULUMI_VERSION=3.223.0

ENV PULUMI_VERSION=$PULUMI_VERSION
ENV PULUMI_HOME="/root/.pulumi/"
Expand Down
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,14 @@ else
endif
$(DUMP_RUN_INFO_COMMAND)

.PHONY: uv_lock
uv_lock: setup_dev_env
docker run --rm \
-v $(NON_DOCKER_ROOT):$(DOCKER_REMOTE_PROJECT_ROOT) \
-w $(DOCKER_REMOTE_PROJECT_ROOT) \
$(DOCKER_DEV_IMAGE) \
uv lock

.PHONY: docker_prune_all
docker_prune_all:
docker ps -q | xargs -r docker stop
Expand Down
2 changes: 1 addition & 1 deletion build_support/src/build_support/ci_cd_tasks/build_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def run(self) -> None:
docker_project_root=self.docker_project_root,
target_image=DockerTarget.PROD,
),
"poetry",
"uv",
"build",
"--output",
get_dist_dir(project_root=self.docker_project_root),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
get_modified_subprojects,
get_ticket_id,
git_fetch,
poetry_lock_file_was_modified,
uv_lock_file_was_modified,
)
from build_support.ci_cd_vars.project_setting_vars import get_pulumi_version
from build_support.ci_cd_vars.project_structure import (
Expand Down Expand Up @@ -171,7 +171,7 @@ class GitInfo(BaseModel):
ticket_id: str | None = None
modified_subprojects: list[SubprojectContext] = Field(default_factory=list)
dockerfile_modified: bool
poetry_lock_file_modified: bool
uv_lock_file_modified: bool

@staticmethod
def get_primary_branch_name() -> str:
Expand Down Expand Up @@ -240,7 +240,7 @@ def run(self) -> None:
dockerfile_modified=dockerfile_was_modified(
modified_files=modified_files, project_root=self.docker_project_root
),
poetry_lock_file_modified=poetry_lock_file_was_modified(
uv_lock_file_modified=uv_lock_file_was_modified(
modified_files=modified_files, project_root=self.docker_project_root
),
).to_yaml()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ def get_subprojects_to_test(project_root: Path) -> list[SubprojectContext]:
git_info = GitInfo.from_yaml(
get_git_info_yaml(project_root=project_root).read_text()
)
if git_info.dockerfile_modified or git_info.poetry_lock_file_modified:
if git_info.dockerfile_modified or git_info.uv_lock_file_modified:
return get_sorted_subproject_contexts()
return git_info.modified_subprojects

Expand Down
15 changes: 6 additions & 9 deletions build_support/src/build_support/ci_cd_vars/git_status_vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@
from git.cmd import execute_kwargs
from git.diff import Diff

from build_support.ci_cd_vars.project_structure import (
get_dockerfile,
get_poetry_lock_file,
)
from build_support.ci_cd_vars.project_structure import get_dockerfile, get_uv_lock_file
from build_support.ci_cd_vars.subproject_structure import (
SubprojectContext,
get_python_subproject,
Expand Down Expand Up @@ -360,12 +357,12 @@ def dockerfile_was_modified(modified_files: Iterable[Path], project_root: Path)
return get_dockerfile(project_root=project_root) in modified_files


def poetry_lock_file_was_modified(
def uv_lock_file_was_modified(
modified_files: Iterable[Path], project_root: Path
) -> bool:
"""Checks if the poetry lock file was modified.
"""Checks if the uv lock file was modified.

If the poetry lock file has been modified it implies that our environment is
If the uv lock file has been modified it implies that our environment is
different, and we should cast a wide net when testing.

Args:
Expand All @@ -374,6 +371,6 @@ def poetry_lock_file_was_modified(
project_root (Path): The root of the project.

Returns:
bool: Has the poetry lock file been modified since the last commit on main.
bool: Has the uv lock file been modified since the last commit on main.
"""
return get_poetry_lock_file(project_root=project_root) in modified_files
return get_uv_lock_file(project_root=project_root) in modified_files
18 changes: 8 additions & 10 deletions build_support/src/build_support/ci_cd_vars/project_setting_vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
from tomlkit import TOMLDocument, parse

from build_support.ci_cd_vars.project_structure import (
get_poetry_lock_file,
get_pyproject_toml,
get_uv_lock_file,
)

########################################
Expand Down Expand Up @@ -46,9 +46,7 @@ def get_project_version(project_root: Path) -> str:
"""
pyproject_data = get_pyproject_toml_data(project_root=project_root)
version_str: str
version_str = pyproject_data["tool"]["poetry"][ # type: ignore[index, assignment]
"version"
]
version_str = str(pyproject_data["project"]["version"]) # type: ignore[index]
if not ALLOWED_VERSION_REGEX.match(version_str):
msg = (
"Project version in pyproject.toml must match the regex "
Expand Down Expand Up @@ -92,30 +90,30 @@ def get_project_name(project_root: Path) -> str:
str: The name of the project.
"""
pyproject_data = get_pyproject_toml_data(project_root=project_root)
return pyproject_data["tool"]["poetry"]["name"] # type: ignore[index, return-value]
return str(pyproject_data["project"]["name"]) # type: ignore[index]


########################################
# Settings pulled from poetry.lock
# Settings pulled from uv.lock
########################################


def get_pulumi_version(project_root: Path) -> str:
"""Get the infra version in the poetry lock file.
"""Get the pulumi version in the uv lock file.

Args:
project_root (Path): Path to this project's root.

Returns:
str: The infra version in the poetry lock file.
str: The pulumi version in the uv lock file.
"""
lock_file = get_poetry_lock_file(project_root=project_root)
lock_file = get_uv_lock_file(project_root=project_root)
lock_data = tomllib.loads(lock_file.read_text())
for package in lock_data["package"]:
if package["name"] == "pulumi":
return str(package["version"])
msg = (
"poetry.lock does not have a pulumi package installed, "
"uv.lock does not have a pulumi package installed, "
"or is no longer a toml format."
)
raise ValueError(msg)
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,16 @@ def get_license_file(project_root: Path) -> Path:
return project_root.joinpath("LICENSE")


def get_poetry_lock_file(project_root: Path) -> Path:
"""Get a path to the poetry lock file in a project.
def get_uv_lock_file(project_root: Path) -> Path:
"""Get a path to the uv lock file in a project.

Args:
project_root (Path): Path to this project's root.

Returns:
Path: Path to the poetry lock file in this project.
Path: Path to the uv lock file in this project.
"""
return project_root.joinpath("poetry.lock")
return project_root.joinpath("uv.lock")


def get_build_dir(project_root: Path) -> Path:
Expand Down
1 change: 0 additions & 1 deletion build_support/src/build_support/file_caching.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

Attributes:
| CONFTEST_NAME: The file name of conftest files.

"""

from datetime import UTC, datetime
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ def update_pyproject_toml(
"""
path_to_pyproject_toml = get_pyproject_toml(project_root=project_root)
pyproject_data: TOMLDocument = get_pyproject_toml_data(project_root=project_root)
poetry = pyproject_data["tool"]["poetry"] # type: ignore[index]
poetry["name"] = new_project_settings.name # type: ignore[index]
poetry["version"] = "0.0.0" # type: ignore[index]
poetry["license"] = new_project_settings.license # type: ignore[index]
poetry["authors"] = [ # type: ignore[index]
project = pyproject_data["project"]
project["name"] = new_project_settings.name # type: ignore[index]
project["version"] = "0.0.0" # type: ignore[index]
project["license"] = new_project_settings.license # type: ignore[index]
project["authors"] = [ # type: ignore[index]
new_project_settings.organization.formatted_name_and_email()
]
poetry["packages"][0]["include"] = ( # type: ignore[index]
new_project_settings.name
)
hatch = pyproject_data["tool"]["hatch"] # type: ignore[index]
hatch["build"]["targets"]["wheel"]["packages"] = [ # type: ignore[index]
f"pypi_package/src/{new_project_settings.name}"
]
path_to_pyproject_toml.write_text(dumps(pyproject_data))
3 changes: 1 addition & 2 deletions build_support/test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@

import pytest
from _pytest.fixtures import SubRequest
from git import Repo

from build_support.ci_cd_tasks.env_setup_tasks import GitInfo
from build_support.ci_cd_vars.build_paths import get_git_info_yaml
from build_support.ci_cd_vars.git_status_vars import PRIMARY_BRANCH_NAME
Expand All @@ -19,6 +17,7 @@
get_python_subproject,
get_sorted_subproject_contexts,
)
from git import Repo


@pytest.fixture(scope="session")
Expand Down
3 changes: 1 addition & 2 deletions build_support/test/feature_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
import pytest
import yaml
from _pytest.fixtures import SubRequest
from git import Head, Repo

from build_support.ci_cd_vars.build_paths import get_local_info_yaml
from build_support.ci_cd_vars.docker_vars import get_docker_tag_suffix
from build_support.ci_cd_vars.git_status_vars import (
Expand All @@ -31,6 +29,7 @@
get_all_python_subprojects_dict,
get_python_subproject,
)
from git import Head, Repo


def remove_dir_and_all_contents(path: Path) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@

import pytest
from _pytest.fixtures import SubRequest
from git import Repo
from test_utils.command_runner import run_command_and_save_logs

from build_support.ci_cd_tasks.env_setup_tasks import GitInfo
from build_support.ci_cd_vars.project_setting_vars import get_project_name
from git import Repo
from test_utils.command_runner import run_command_and_save_logs


@pytest.mark.usefixtures("mock_new_branch", "dummy_feature_test")
Expand Down
Loading