Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/python-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ jobs:
python-version-file: ".python-version"

- name: Run Tests
run: make uv-test
run: make uv-test-isolated
28 changes: 26 additions & 2 deletions Makefile
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes to the Makefile seem out of scope for "test stitch-models", and in general, I think it's a bit over-complicated for what we want.

I think my general tendency here would be (in a separate Makefile-cleaning PR) to change to something like test-python that would depend on test-stitch-models and test-api and all our other python testing commands (but with better target names), which would run their own exact installs

(but I'm still missing the advantages of that over just syncing the whole project)

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ DOCKER_COMPOSE := docker compose -f docker-compose.yml
DOCKER_COMPOSE_DEV := $(DOCKER_COMPOSE) -f docker-compose.local.yml
PYTEST := $(UV) run pytest
RUFF := $(UV) run ruff
TEST_PKG := ./scripts/test-package.py

# Aggregate check: can be run in parallel with -j
check: lint test format-check lock-check
Expand All @@ -21,8 +22,31 @@ lock-check: uv-lock-check
uv-lint: uv-dev
$(RUFF) check

# All workspace packages with tests
TEST_PACKAGES := stitch-api stitch-models

define newline


endef

# --- local: full sync, no --exact (fast, no venv mutation) ---
ifdef pkg
uv-test: uv-dev
$(TEST_PKG) $(pkg)
else
uv-test: uv-dev
$(PYTEST) deployments/api
$(foreach p,$(TEST_PACKAGES),$(TEST_PKG) $(p)$(newline))
endif

# --- isolated (CI): per-package --exact deps only ---
ifdef pkg
uv-test-isolated:
$(TEST_PKG) --exact $(pkg)
else
uv-test-isolated:
$(foreach p,$(TEST_PACKAGES),$(TEST_PKG) --exact $(p)$(newline))
endif

uv-format: uv-dev
$(RUFF) format
Expand Down Expand Up @@ -127,7 +151,7 @@ prod-docker:
.PHONY: all build clean \
build-python \
check lint test format format-check \
uv-lint uv-test uv-format uv-format-check \
uv-lint uv-test uv-test-isolated uv-format uv-format-check \
uv-sync uv-sync-dev uv-sync-all \
uv-dev \
clean-build clean-cache \
Expand Down
23 changes: 12 additions & 11 deletions packages/stitch-models/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
from __future__ import annotations

from typing import Literal, Mapping
from uuid import UUID
from uuid import UUID, uuid4

from pydantic import Field
import pytest

from stitch.models import (
Expand All @@ -24,7 +25,7 @@ class FooSource(Source[int, Literal["foo"]]):
value: float


class BarSource(Source[str, Literal["bar"]]):
class BarSource(Source[UUID, Literal["bar"]]):
source: Literal["bar"] = "bar"
label: str

Expand All @@ -39,16 +40,16 @@ class UuidSource(Source[UUID, Literal["uuid_src"]]):


class FooPayload(SourcePayload):
foos: Mapping[int, FooSource] = {}
foos: Mapping[int, FooSource] = Field(default_factory=dict)


class MultiPayload(SourcePayload):
foos: Mapping[int, FooSource] = {}
bars: Mapping[str, BarSource] = {}
foos: Mapping[int, FooSource] = Field(default_factory=dict)
bars: Mapping[str, BarSource] = Field(default_factory=dict)


class UuidPayload(SourcePayload):
uuids: Mapping[UUID, UuidSource] = {}
uuids: Mapping[UUID, UuidSource] = Field(default_factory=dict)


# ---------------------------------------------------------------------------
Expand All @@ -60,15 +61,15 @@ class EmptyPayload(SourcePayload):
pass


class FooResource(Resource[int, int, str, FooPayload]):
class FooResource(Resource[int, FooPayload]):
pass


class MultiResource(Resource[int, int, str, MultiPayload]):
class MultiResource(Resource[int, MultiPayload]):
pass


class ExtendedResource(Resource[int, int, str, EmptyPayload]):
class ExtendedResource(Resource[int, EmptyPayload]):
extra: str


Expand All @@ -85,7 +86,7 @@ def __init__(self, id: int, source: str, value: float):


class BarSourceORM:
def __init__(self, id: str, source: str, label: str):
def __init__(self, id: UUID, source: str, label: str):
self.id = id
self.source = source
self.label = label
Expand All @@ -103,7 +104,7 @@ def foo_source():

@pytest.fixture
def bar_source():
return BarSource(id="abc", label="test")
return BarSource(id=uuid4(), label="test")


@pytest.fixture
Expand Down
4 changes: 2 additions & 2 deletions packages/stitch-models/tests/test_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

import json
from uuid import UUID
from uuid import UUID, uuid4

import pytest
from pydantic import ValidationError
Expand Down Expand Up @@ -112,7 +112,7 @@ def test_rejects_missing_source_data(self):
assert "source_data" in missing_locs

def test_rejects_wrong_source_in_payload(self):
bar = BarSource(id="abc", label="test")
bar = BarSource(id=uuid4(), label="test")
with pytest.raises(ValidationError) as exc_info:
FooPayload(foos={1: bar}) # type: ignore[arg-type]
errors = exc_info.value.errors()
Expand Down
22 changes: 12 additions & 10 deletions packages/stitch-models/tests/test_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

import json
from uuid import UUID
from uuid import UUID, uuid4

import pytest
from pydantic import ValidationError
Expand Down Expand Up @@ -42,7 +42,7 @@ def test_source_default_from_subclass(self):
foo = FooSource(id=1, value=1.0)
assert foo.source == "foo"

bar = BarSource(id="abc", label="test")
bar = BarSource(id=uuid4(), label="test")
assert bar.source == "bar"

def test_id_type_specialization(self):
Expand All @@ -51,10 +51,11 @@ def test_id_type_specialization(self):
assert foo.id == 42
assert isinstance(foo.id, int)

# str id
bar = BarSource(id="abc", label="test")
assert bar.id == "abc"
assert isinstance(bar.id, str)
# UUID id
uid = uuid4()
bar = BarSource(id=uid, label="test")
assert bar.id == uid
assert isinstance(bar.id, UUID)

# UUID id
uid = UUID("550e8400-e29b-41d4-a716-446655440000")
Expand All @@ -77,9 +78,10 @@ def test_from_attributes_orm_object(self):
assert foo.source == "foo"
assert foo.value == 3.14

bar_orm = BarSourceORM(id="abc", source="bar", label="test")
uid = uuid4()
bar_orm = BarSourceORM(id=uid, source="bar", label="test")
bar = BarSource.model_validate(bar_orm, from_attributes=True)
assert bar.id == "abc"
assert bar.id == uid
assert bar.label == "test"


Expand All @@ -106,11 +108,11 @@ def test_int_id_rejects_non_numeric_string(self):
FooSource.model_validate_json(payload)
assert_has_error(exc_info.value.errors(), type="int_parsing", loc=("id",))

def test_str_id_rejects_int_via_json(self):
def test_uuid_id_rejects_int_via_json(self):
payload = json.dumps({"id": 123, "source": "bar", "label": "test"})
with pytest.raises(ValidationError) as exc_info:
BarSource.model_validate_json(payload)
assert_has_error(exc_info.value.errors(), type="string_type", loc=("id",))
assert_has_error(exc_info.value.errors(), type="uuid_type", loc=("id",))

def test_uuid_id_rejects_malformed_string(self):
payload = json.dumps({"id": "not-a-uuid", "source": "uuid_src"})
Expand Down
45 changes: 45 additions & 0 deletions scripts/test-package.py
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment on Makefile, but I think we want to keep this process simpler (i.e. not have this script)

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12"
# ///

import argparse
import subprocess
import sys
import tomllib
from pathlib import Path


def find_package_dir(name: str) -> str:
root = tomllib.loads(Path("pyproject.toml").read_text())
for member in root["tool"]["uv"]["workspace"]["members"]:
p = Path(member) / "pyproject.toml"
if p.exists() and tomllib.loads(p.read_text())["project"]["name"] == name:
return member
print(f"error: package {name!r} not found in workspace", file=sys.stderr)
sys.exit(1)


def main() -> None:
parser = argparse.ArgumentParser(
description="Run pytest for a uv workspace package.",
)
parser.add_argument("package", help="workspace package name")
parser.add_argument(
"--exact",
action="store_true",
help="install only the package's declared deps (CI isolation)",
)
known, pytest_args = parser.parse_known_args()

pkg_dir = find_package_dir(known.package)

cmd = ["uv", "run", "--package", known.package]
if known.exact:
cmd += ["--exact", "--group", "dev"]
cmd += ["pytest", pkg_dir, *pytest_args]

sys.exit(subprocess.call(cmd))


main()