From 863e6ce6e954e4802d9508ee74ebcf31f2a8dd5e Mon Sep 17 00:00:00 2001 From: d3vyce <44915747+d3vyce@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:12:08 +0200 Subject: [PATCH] feat: add get_field_by_attr fixtures helper (#245) --- src/fastapi_toolsets/fixtures/__init__.py | 8 ++++- src/fastapi_toolsets/fixtures/utils.py | 25 ++++++++++++++++ tests/test_fixtures.py | 36 +++++++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/fastapi_toolsets/fixtures/__init__.py b/src/fastapi_toolsets/fixtures/__init__.py index 89804cb..66a2e7d 100644 --- a/src/fastapi_toolsets/fixtures/__init__.py +++ b/src/fastapi_toolsets/fixtures/__init__.py @@ -2,12 +2,18 @@ from .enum import LoadStrategy from .registry import Context, FixtureRegistry -from .utils import get_obj_by_attr, load_fixtures, load_fixtures_by_context +from .utils import ( + get_field_by_attr, + get_obj_by_attr, + load_fixtures, + load_fixtures_by_context, +) __all__ = [ "Context", "FixtureRegistry", "LoadStrategy", + "get_field_by_attr", "get_obj_by_attr", "load_fixtures", "load_fixtures_by_context", diff --git a/src/fastapi_toolsets/fixtures/utils.py b/src/fastapi_toolsets/fixtures/utils.py index ac26d3d..7443422 100644 --- a/src/fastapi_toolsets/fixtures/utils.py +++ b/src/fastapi_toolsets/fixtures/utils.py @@ -250,6 +250,31 @@ def get_obj_by_attr( ) from None +def get_field_by_attr( + fixtures: Callable[[], Sequence[ModelType]], + attr_name: str, + value: Any, + *, + field: str = "id", +) -> Any: + """Get a single field value from a fixture object matched by an attribute. + + Args: + fixtures: A fixture function registered via ``@registry.register`` + that returns a sequence of SQLAlchemy model instances. + attr_name: Name of the attribute to match against. + value: Value to match. + field: Attribute name to return from the matched object (default: ``"id"``). + + Returns: + The value of ``field`` on the first matching model instance. + + Raises: + StopIteration: If no matching object is found in the fixture group. + """ + return getattr(get_obj_by_attr(fixtures, attr_name, value), field) + + async def load_fixtures( session: AsyncSession, registry: FixtureRegistry, diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 81f2c2d..009d0be 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -10,6 +10,7 @@ from fastapi_toolsets.fixtures import ( Context, FixtureRegistry, LoadStrategy, + get_field_by_attr, get_obj_by_attr, load_fixtures, load_fixtures_by_context, @@ -951,6 +952,41 @@ class TestGetObjByAttr: get_obj_by_attr(self.roles, "id", "not-a-uuid") +class TestGetFieldByAttr: + """Tests for get_field_by_attr helper function.""" + + def setup_method(self): + self.registry = FixtureRegistry() + self.role_id_1 = uuid.uuid4() + self.role_id_2 = uuid.uuid4() + role_id_1 = self.role_id_1 + role_id_2 = self.role_id_2 + + @self.registry.register + def roles() -> list[Role]: + return [ + Role(id=role_id_1, name="admin"), + Role(id=role_id_2, name="user"), + ] + + self.roles = roles + + def test_returns_id_by_default(self): + """Returns the id field when no field is specified.""" + result = get_field_by_attr(self.roles, "name", "admin") + assert result == self.role_id_1 + + def test_returns_specified_field(self): + """Returns the requested field instead of id.""" + result = get_field_by_attr(self.roles, "id", self.role_id_2, field="name") + assert result == "user" + + def test_no_match_raises_stop_iteration(self): + """Propagates StopIteration from get_obj_by_attr when no match found.""" + with pytest.raises(StopIteration, match="No object with name=missing"): + get_field_by_attr(self.roles, "name", "missing") + + class TestGetPrimaryKey: """Unit tests for the _get_primary_key helper (composite PK paths)."""