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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
.idea
.coverage
.claude
.devcontainer
*.pyc
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Read that file for the full list. The most important ones for day-to-day work a
| `make test` | Runs the full pipeline: unit tests, feature tests, style, type checks, security. Run this before marking work complete. |
| `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 type_checks` | Runs ty type checks 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 Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@ setup_prod_env: setup_build_env
setup_pulumi_env: setup_build_env
$(EXECUTE_BUILD_STEPS_COMMAND) setup_pulumi_env

# Suppress initial docker build output unless LOG_LEVEL is TRACE
DOCKER_BUILD_QUIET = $(if $(filter TRACE,$(LOG_LEVEL)), , > /dev/null 2>&1)

.PHONY: setup_build_env
setup_build_env:
ifeq ($(CI_CD_FEATURE_TEST_MODE_FLAG), )
Expand All @@ -202,7 +205,7 @@ ifeq ($(CI_CD_FEATURE_TEST_MODE_FLAG), )
-f $(DOCKERFILE) \
--target build \
--build-arg BUILDKIT_INLINE_CACHE=1 \
-t $(DOCKER_BUILD_IMAGE) $(MAKEFILE_DIR)
-t $(DOCKER_BUILD_IMAGE) $(MAKEFILE_DIR)$(DOCKER_BUILD_QUIET)
else
echo "Skipping building build docker image in CI/CD mode."
endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,15 +152,18 @@ def run(self) -> None:
get_feature_test_scratch_folder(project_root=self.docker_project_root),
]
)
run_process(
args=["rm", "-rf", self.docker_project_root.joinpath(".mypy_cache")]
)
run_process(args=["rm", "-rf", self.docker_project_root.joinpath(".ty_cache")])
run_process(
args=["rm", "-rf", self.docker_project_root.joinpath(".pytest_cache")]
)
run_process(
args=["rm", "-rf", self.docker_project_root.joinpath(".ruff_cache")]
)
run_process(
args=["rm", "-rf", str(self.docker_project_root.joinpath(".coverage"))]
)
for path in self.docker_project_root.glob(".coverage.*"):
run_process(args=["rm", "-rf", str(path)])


class GitInfo(BaseModel):
Expand Down
27 changes: 13 additions & 14 deletions build_support/src/build_support/ci_cd_tasks/validation_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from copy import deepcopy
from dataclasses import dataclass
from pathlib import Path
from typing import override
from typing import cast, override

from junitparser import JUnitXml
from tomlkit import TOMLDocument, document, dumps, table
Expand All @@ -27,7 +27,7 @@
get_base_docker_command_for_image,
get_docker_command_for_image,
get_docker_image_name,
get_mypy_path_env,
get_ty_extra_search_path_args,
)
from build_support.ci_cd_vars.file_and_dir_path_vars import (
get_all_non_test_folders,
Expand Down Expand Up @@ -184,17 +184,16 @@ def run(self) -> None:
docker_project_root=self.docker_project_root,
target_image=DockerTarget.DEV,
),
"-e",
get_mypy_path_env(
docker_project_root=self.docker_project_root,
target_image=DockerTarget.DEV,
),
get_docker_image_name(
project_root=self.docker_project_root,
target_image=DockerTarget.DEV,
),
"mypy",
"--explicit-package-bases",
"ty",
"check",
get_ty_extra_search_path_args(
docker_project_root=self.docker_project_root,
target_image=DockerTarget.DEV,
),
self.subproject.get_root_dir(),
]
)
Expand Down Expand Up @@ -414,7 +413,8 @@ def get_coverage_settings(self) -> TOMLDocument:
TOMLDocument.
"""
pyproject_data = get_pyproject_toml_data(project_root=self.docker_project_root)
return pyproject_data["tool"]["coverage"] # type: ignore[index, return-value]
tool = cast(TOMLDocument, pyproject_data["tool"])
return cast(TOMLDocument, tool["coverage"])

def build_omit_list(self, unit_test_info: UnitTestInfo) -> list[str]:
"""Builds the list of omit patterns for a coverage config file.
Expand Down Expand Up @@ -475,11 +475,10 @@ def write_coverage_config_file(
coverage_config = deepcopy(coverage_settings)
if "run" not in coverage_config:
coverage_config["run"] = table()
existing_omit = coverage_config["run"].get( # type: ignore[union-attr]
"omit", []
)
run_table = cast(TOMLDocument, coverage_config["run"])
existing_omit = run_table.get("omit", [])
merged_omit = list(dict.fromkeys(list(existing_omit) + new_omit_list))
coverage_config["run"]["omit"] = merged_omit # type: ignore[index]
run_table["omit"] = merged_omit

# Write TOML file
config_doc = document()
Expand Down
40 changes: 22 additions & 18 deletions build_support/src/build_support/ci_cd_vars/docker_vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from enum import StrEnum
from os import environ
from pathlib import Path
from typing import Any
from typing import Any, cast

from build_support.ci_cd_vars.file_and_dir_path_vars import (
get_all_python_folders,
Expand Down Expand Up @@ -118,23 +118,23 @@ def get_python_path_env(docker_project_root: Path, target_image: DockerTarget) -
)


def get_mypy_path_for_target_image(
def get_ty_extra_search_paths_for_target_image(
docker_project_root: Path, target_image: DockerTarget
) -> str:
"""Gets the mypy path to use with this project.
) -> list[Path]:
"""Gets ty extra search paths for this project.

Args:
docker_project_root (Path): Path to this project's root when running in docker
containers.
target_image (DockerTarget): An enum specifying which type of docker image we
are requesting the mypy path for.
are requesting extra search paths for.

Returns:
str: The MYPYPATH for the specified docker image.
list[Path]: Extra search paths for ty for the specified docker image.
"""
match target_image:
case DockerTarget.BUILD:
python_folders = [
search_paths = [
get_python_subproject(
subproject_context=SubprojectContext.BUILD_SUPPORT,
project_root=docker_project_root,
Expand All @@ -143,16 +143,16 @@ def get_mypy_path_for_target_image(
case DockerTarget.DEV:
src_folders = get_all_src_folders(project_root=docker_project_root)
test_folders = get_all_test_folders(project_root=docker_project_root)
python_folders = src_folders + test_folders
search_paths = src_folders + test_folders
case DockerTarget.PROD:
python_folders = [
search_paths = [
get_python_subproject(
subproject_context=SubprojectContext.PYPI,
project_root=docker_project_root,
).get_src_dir()
]
case DockerTarget.INFRA:
python_folders = [
search_paths = [
get_python_subproject(
subproject_context=SubprojectContext.INFRA,
project_root=docker_project_root,
Expand All @@ -161,25 +161,29 @@ def get_mypy_path_for_target_image(
case _: # pragma: no cov - can't hit if all enums are implemented
msg = f"{target_image!r} is not a valid enum of DockerType."
raise ValueError(msg)
return ":".join(concatenate_args(args=[python_folders]))
return search_paths


def get_mypy_path_env(docker_project_root: Path, target_image: DockerTarget) -> str:
"""Gets the mypy path ENV to use with this project.
def get_ty_extra_search_path_args(
docker_project_root: Path, target_image: DockerTarget
) -> list[str]:
"""Gets CLI args for ty extra search paths.

Args:
docker_project_root (Path): Path to this project's root when running in docker
containers.
target_image (DockerTarget): An enum specifying which type of docker image we
are requesting the mypy path for.
are requesting extra search path args for.

Returns:
str: The MYPYPATH for the specified docker image in the form on an ENV variable
that can be used on the command line.
list[str]: Flattened ``--extra-search-path`` arguments for ty.
"""
return "MYPYPATH=" + get_mypy_path_for_target_image(
extra_search_args: list[str] = []
for search_path in get_ty_extra_search_paths_for_target_image(
docker_project_root=docker_project_root, target_image=target_image
)
):
extra_search_args.extend(["--extra-search-path", str(search_path)])
return concatenate_args(args=cast(list[Any | list[Any]], extra_search_args))


def get_base_docker_command_for_image(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def get_test_utils_dirs(project_root: Path) -> list[Path]:

The test_utils folder contains utility Python source files that are used by
test files but are not test files themselves. These directories need to be in
MYPYPATH so mypy can resolve imports from test files.
ty extra search paths so imports from test files can be resolved.

Args:
project_root (Path): Path to this project's root.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import re
import tomllib
from pathlib import Path
from typing import cast

from tomlkit import TOMLDocument, parse

Expand Down Expand Up @@ -46,7 +47,7 @@ def get_project_version(project_root: Path) -> str:
"""
pyproject_data = get_pyproject_toml_data(project_root=project_root)
version_str: str
version_str = str(pyproject_data["project"]["version"]) # type: ignore[index]
version_str = str(cast(TOMLDocument, pyproject_data["project"])["version"])
if not ALLOWED_VERSION_REGEX.match(version_str):
msg = (
"Project version in pyproject.toml must match the regex "
Expand Down Expand Up @@ -90,7 +91,7 @@ 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 str(pyproject_data["project"]["name"]) # type: ignore[index]
return str(cast(TOMLDocument, pyproject_data["project"])["name"])


########################################
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,18 +86,18 @@ def get_feature_test_scratch_folder(project_root: Path) -> Path:
return maybe_build_dir(dir_to_build=project_root.joinpath("test_scratch_folder"))


def get_feature_test_log_name(project_root: Path, test_name: str) -> Path:
def get_feature_test_log_file(project_root: Path, log_name: str) -> Path:
"""Gets the path to a log file for a feature test.

Args:
project_root (Path): Path to this project's root.
test_name (str): Name of the test (will be sanitized for filename).
log_name (str): Name of the test (will be sanitized for filename).

Returns:
Path: Path to the log file for the test in test_scratch_folder/test_logs/.
"""
# Sanitize test name for use as filename
safe_test_name = test_name.replace("[", "_").replace("]", "_").replace("::", "_")
safe_test_name = log_name.replace("[", "_").replace("]", "_").replace("::", "_")
# Remove trailing underscores that may result from sanitization
safe_test_name = safe_test_name.rstrip("_")
log_dir = maybe_build_dir(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Module exists to conceptually organize all changes to the pyproject.toml file."""

from pathlib import Path
from typing import cast

from tomlkit import TOMLDocument, dumps

Expand All @@ -24,15 +25,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)
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.as_pyproject_author()
]
hatch = pyproject_data["tool"]["hatch"] # type: ignore[index]
hatch["build"]["targets"]["wheel"]["packages"] = [ # type: ignore[index]
f"pypi_package/src/{new_project_settings.name}"
]
project = cast(TOMLDocument, pyproject_data["project"])
project["name"] = new_project_settings.name
project["version"] = "0.0.0"
project["license"] = new_project_settings.license
project["authors"] = [new_project_settings.organization.as_pyproject_author()]
tool = cast(TOMLDocument, pyproject_data["tool"])
hatch = cast(TOMLDocument, tool["hatch"])
hatch_build = cast(TOMLDocument, hatch["build"])
targets = cast(TOMLDocument, hatch_build["targets"])
wheel = cast(TOMLDocument, targets["wheel"])
wheel["packages"] = [f"pypi_package/src/{new_project_settings.name}"]
path_to_pyproject_toml.write_text(dumps(pyproject_data))
26 changes: 25 additions & 1 deletion build_support/test/feature_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
get_python_subproject,
)
from git import Head, Repo
from test_utils.command_runner import FeatureTestCommandContext


def remove_dir_and_all_contents(path: Path) -> None:
Expand Down Expand Up @@ -114,6 +115,27 @@ def make_command_prefix(make_command_prefix_without_tag_suffix: list[str]) -> li
return [*make_command_prefix_without_tag_suffix, f"TAG_SUFFIX={tag_suffix}"]


@pytest.fixture
def default_command_context(
mock_project_root: Path,
make_command_prefix: list[str],
real_project_root_dir: Path,
request: SubRequest,
) -> FeatureTestCommandContext:
"""Default feature test context for run_command_and_save_logs.

Tests call run_command_and_save_logs(context=..., command_args=...). Copy and
override fields when needed (e.g. expect_failure, log_name, args_prefix).
"""
return FeatureTestCommandContext(
args_prefix=make_command_prefix,
mock_project_root=mock_project_root,
real_project_root_dir=real_project_root_dir,
test_name=request.node.name,
log_name=request.node.name,
)


@pytest.fixture(scope="session")
def mock_lightweight_project_copy_dir(real_build_dir: Path) -> Path:
"""Return the directory path for the lightweight project cache.
Expand Down Expand Up @@ -347,7 +369,9 @@ def mock_lightweight_project_with_unit_tests_and_feature_tests(
Repo: The updated mock project repository.
"""
for subproject in [
get_python_subproject(SubprojectContext.PYPI, mock_project_root)
get_python_subproject(
subproject_context=SubprojectContext.PYPI, project_root=mock_project_root
)
]:
subproject_pkg_dir = subproject.get_python_package_dir()
subproject_pkg_init_file = subproject_pkg_dir.joinpath("__init__.py")
Expand Down
Loading