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
14 changes: 3 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,18 +102,10 @@ jobs:
uv run --no-sync python -m tests._docs_checks docs/pages docs/examples src/ferro

- name: Run the docs tests
# Every docs test module is named test_doc*.py, which
# tests/test_docs_site.py pins, so a new one runs here unlisted.
run: |
uv run --no-sync pytest --no-cov -p no:cacheprovider \
tests/test_docs_site.py \
tests/test_docs_examples.py \
tests/test_docstring_examples.py \
tests/test_docs_migrations.py \
tests/test_docs_sql.py \
tests/test_docs_testing_fixtures.py \
tests/test_docs_checks.py \
tests/test_docs_lint.py \
tests/test_docs_field_type_table.py \
tests/test_documentation_features.py
uv run --no-sync pytest --no-cov -p no:cacheprovider tests/test_doc*.py

test-rust:
name: Rust tests
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ Version bumps are determined by commit types:

Tests that cross the Python and Rust bridge, or exercise Pydantic models, live in `tests/`. A test connects with the `db_url` fixture, opens a session and asserts on what comes back:

```python
```python {test=compile reason="a test for this repository's suite, which supplies its db_url fixture"}
import pytest

from ferro import Field, Model, connect, engines
Expand Down
22 changes: 21 additions & 1 deletion docs/examples/_preludes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
python docs/examples/_preludes/__init__.py <prelude> <path> <first line>

``path`` and ``first line`` are where the block's source sits, so a traceback
points at the docs page or docstring, not at this runner.
points at the docs page or docstring, not at this runner. With
``FERRO_DOCS_DATABASE_URL`` set (a ``backend=postgres`` fence), every
``connect()`` the prelude or the block makes goes to that database instead of
the URL it names.
"""

from __future__ import annotations
Expand All @@ -20,6 +23,7 @@
import asyncio
import importlib.util
import inspect
import os
import sys
from pathlib import Path
from types import ModuleType
Expand Down Expand Up @@ -80,8 +84,24 @@ async def run_block(prelude: str, source: str, path: str, first_line: int) -> No
await result


def point_connect_at(url: str) -> None:
"""Send every ``ferro.connect`` call to ``url``, whatever URL it names."""
import ferro

connect = ferro.connect

async def connect_to(_url: str, *args: object, **kwargs: object) -> None:
await connect(url, *args, **kwargs)

ferro.connect = connect_to


def main(argv: list[str]) -> None:
prelude, path, first_line = argv
url = os.environ.get("FERRO_DOCS_DATABASE_URL")
if url:
# Before the prelude imports connect, so its name is this one too.
point_connect_at(url)
asyncio.run(run_block(prelude, sys.stdin.read(), path, int(first_line)))


Expand Down
27 changes: 0 additions & 27 deletions docs/examples/_preludes/aggregations.py

This file was deleted.

45 changes: 12 additions & 33 deletions docs/examples/aggregations.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@
(docs/pages/guide/aggregations.md).

Seeds a small ledger of four transactions, then runs every aggregate query
the guide shows and asserts the exact records that come back. The
``aggregations`` prelude imports ``Account``, ``Transaction`` and ``seed`` from
here, so the guide's inline blocks run against the same rows.
the guide shows and asserts the exact records that come back.
"""

import asyncio
from collections import defaultdict
from decimal import Decimal
from typing import Annotated

from ferro import BackRef, Field, ForeignKey, Model, Relation, connect, engines
from ferro import BackRef, Field, ForeignKey, Model, Relation


# --8<-- [start:schema]
Expand Down Expand Up @@ -50,11 +48,12 @@ async def seed() -> None:


async def main() -> None:
await connect("sqlite::memory:", auto_migrate=True)

# --8<-- [start:global]
from ferro import connect, engines

await connect("sqlite::memory:", auto_migrate=True)
async with engines.session():
await seed()
await seed() # adds the ledger's four transactions

# An aggregate-only projection collapses to exactly one record,
# read idiomatically with first().
Expand Down Expand Up @@ -253,12 +252,7 @@ async def main() -> None:
try:
Transaction.select(lambda transaction: {"x": transaction.memo.sum()})
except TypeError as exc:
assert str(exc) == (
"t.memo.sum() is not supported: Transaction.memo is string-typed, "
"and sum() takes a numeric column (int, float, or Decimal). No "
"portable cross-backend meaning exists for this aggregate over a "
"string column."
)
print(f"{type(exc).__name__}: {exc}")
else:
raise AssertionError("expected TypeError")
# --8<-- [end:error-family]
Expand All @@ -268,11 +262,7 @@ async def main() -> None:
try:
Transaction.where(lambda transaction: transaction.amount.sum() > 100)
except TypeError as exc:
assert str(exc).startswith(
"t.amount.sum() > ... is not a where() predicate: WHERE filters "
"rows before aggregation, so an aggregate cannot appear in it. "
"Post-aggregation filtering is having()"
)
print(f"{type(exc).__name__}: {exc}")
else:
raise AssertionError("expected TypeError")
# --8<-- [end:error-where]
Expand All @@ -286,10 +276,7 @@ async def main() -> None:
}
)
except TypeError as exc:
assert str(exc).startswith(
"t.amount is a column reference and cannot be iterated; "
"did you mean t.amount.sum()?"
)
print(f"{type(exc).__name__}: {exc}")
else:
raise AssertionError("expected TypeError")
# --8<-- [end:error-builtin-sum]
Expand All @@ -299,8 +286,7 @@ async def main() -> None:
try:
Transaction.select(lambda transaction: transaction.amount.sum())
except TypeError as exc:
assert str(exc).startswith("aggregate fields are user-named")
assert "give t.amount.sum() an output name" in str(exc)
print(f"{type(exc).__name__}: {exc}")
else:
raise AssertionError("expected TypeError")
# --8<-- [end:error-unnamed]
Expand All @@ -315,10 +301,7 @@ async def main() -> None:
}
).order_by("memo")
except ValueError as exc:
assert str(exc).startswith(
"order_by('memo') on an aggregate projection must name a group key "
"or an aggregate"
)
print(f"{type(exc).__name__}: {exc}")
else:
raise AssertionError("expected ValueError")
# --8<-- [end:error-order-key]
Expand All @@ -333,11 +316,7 @@ async def main() -> None:
}
).count()
except ValueError as exc:
assert str(exc) == (
"count() on an aggregate projection is ambiguous: count the matching "
"rows with an unprojected query (Model.where(...).count()), or count "
"the groups with len(await q.all())."
)
print(f"{type(exc).__name__}: {exc}")
else:
raise AssertionError("expected ValueError")
# --8<-- [end:error-count]
Expand Down
Loading
Loading