Skip to content

Commit 6ef1f53

Browse files
committed
fix(docs): escape type names in changelog to fix rst reference error
The trailing underscore in `SyncServiceT_…` was being interpreted as an RST reference target, causing the doc build to fail with "Unknown target name: syncservicet".
1 parent acc57d0 commit 6ef1f53

File tree

5 files changed

+55
-95
lines changed

5 files changed

+55
-95
lines changed

advanced_alchemy/base.py

Lines changed: 29 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import contextlib
44
import datetime
55
import re
6-
from collections.abc import Iterator
6+
from collections.abc import Iterator, Mapping
77
from typing import TYPE_CHECKING, Any, Optional, Protocol, Union, cast, runtime_checkable
88
from uuid import UUID
99

@@ -216,64 +216,42 @@ def __init_subclass__(cls, **kwargs: Any) -> None:
216216
they share the parent's table. This hook enforces that rule.
217217
218218
The detection logic identifies STI children by checking:
219-
1. Class has ``polymorphic_identity`` in ``__mapper_args__`` (explicit STI child marker)
219+
1. Class doesn't explicitly define ``__tablename__`` in its own ``__dict__``
220220
2. AND doesn't have ``concrete=True`` (which would make it CTI)
221-
3. AND doesn't have ``polymorphic_on`` itself (which would make it a base)
222-
4. AND doesn't explicitly define ``__tablename__`` in its own ``__dict__``
221+
3. AND doesn't define ``polymorphic_on`` in its own ``__mapper_args__`` (which would make it a base)
222+
4. AND inherits from a parent that defines ``polymorphic_on`` in ``__mapper_args__`` (STI hierarchy)
223223
224-
For children without ``polymorphic_identity`` but with a parent that has
225-
``polymorphic_on``, SQLAlchemy treats them as abstract intermediate classes
226-
and will issue a warning. We don't modify ``__tablename__`` for these cases.
224+
For intermediate classes without ``polymorphic_identity`` but with a parent that has
225+
``polymorphic_on``, SQLAlchemy can emit a warning. When an intermediate class should
226+
not be instantiated, set ``polymorphic_abstract=True`` in ``__mapper_args__`` or mark it
227+
with ``__abstract__ = True``.
227228
228229
This allows both usage patterns:
229230
1. Auto-generated names (don't set ``__tablename__`` on parent)
230231
2. Explicit names (set ``__tablename__`` on parent, STI still works)
231232
"""
232-
# IMPORTANT: Modify the class BEFORE calling super().__init_subclass__()
233-
# because super() triggers SQLAlchemy's declarative processing
234-
mapper_args = getattr(cls, "__mapper_args__", {})
235-
236-
# Skip if this class explicitly defines its own __tablename__
237233
if "__tablename__" in cls.__dict__:
238234
super().__init_subclass__(**kwargs)
239235
return
240236

241-
# Skip if this is CTI (concrete table inheritance)
242-
if mapper_args.get("concrete", False):
237+
cls_dict = cast("Mapping[str, Any]", cls.__dict__)
238+
own_mapper_args = cls_dict.get("__mapper_args__")
239+
own_mapper_args_dict = cast("dict[str, Any]", own_mapper_args) if isinstance(own_mapper_args, dict) else {}
240+
241+
if own_mapper_args_dict.get("concrete", False):
242+
super().__init_subclass__(**kwargs)
243+
return
244+
245+
if "polymorphic_on" in own_mapper_args_dict:
243246
super().__init_subclass__(**kwargs)
244247
return
245248

246-
# Check if this class might be an STI child
247-
# An STI child either has polymorphic_identity in its own __mapper_args__,
248-
# or inherits from a parent with polymorphic_on
249-
is_potential_sti_child = False
250-
251-
# Check if THIS class (not inherited) defines polymorphic_on
252-
# If it does, it's a base class, not a child
253-
if "__mapper_args__" in cls.__dict__:
254-
own_mapper_args = cls.__dict__["__mapper_args__"]
255-
if "polymorphic_on" in own_mapper_args:
256-
# This is a base class, not a child - skip
257-
super().__init_subclass__(**kwargs)
258-
return
259-
260-
# Check if any parent has polymorphic_on (indicates we're in an STI hierarchy)
261249
for parent in cls.__mro__[1:]:
262-
if not hasattr(parent, "__mapper_args__"):
263-
continue
264-
parent_mapper_args = getattr(parent, "__mapper_args__", {})
265-
if "polymorphic_on" in parent_mapper_args:
266-
# We're inheriting from a polymorphic base, so we're an STI child
267-
is_potential_sti_child = True
250+
parent_mapper_args = getattr(parent, "__mapper_args__", None)
251+
if isinstance(parent_mapper_args, dict) and "polymorphic_on" in parent_mapper_args:
252+
cls.__tablename__ = None # type: ignore[misc]
268253
break
269254

270-
if is_potential_sti_child and "__tablename__" not in cls.__dict__:
271-
# For STI children that inherited an explicit __tablename__ from a parent,
272-
# we need to explicitly set it to None so SQLAlchemy knows to use the parent's table.
273-
# This overrides the inherited string value.
274-
cls.__tablename__ = None # type: ignore[misc]
275-
276-
# Now call super() which triggers SQLAlchemy's declarative system
277255
super().__init_subclass__(**kwargs)
278256

279257
if TYPE_CHECKING:
@@ -359,40 +337,20 @@ class Manager(Employee):
359337
__tablename__ = "manager" # Independent table
360338
__mapper_args__ = {"concrete": True}
361339
"""
362-
# Check if class explicitly defines __tablename__ in its own __dict__
363-
if "__tablename__" in cls.__dict__:
364-
value = cls.__dict__["__tablename__"]
365-
# If explicitly set to None (e.g., by __init_subclass__ for STI), return None
366-
if value is None:
367-
return None
368-
return value
340+
cls_dict = cast("Mapping[str, Any]", cls.__dict__)
341+
if "__tablename__" in cls_dict:
342+
return cast("Optional[str]", cls_dict["__tablename__"])
369343

370-
# Check if this is an STI child class that needs auto-detection
371-
# This handles cases where the parent didn't explicitly set __tablename__
372344
mapper_args = getattr(cls, "__mapper_args__", {})
345+
mapper_args_dict = cast("dict[str, Any]", mapper_args) if isinstance(mapper_args, dict) else {}
346+
if mapper_args_dict.get("concrete", False) or "polymorphic_on" in mapper_args_dict:
347+
return table_name_regexp.sub(r"_\1", cls.__name__).lower()
373348

374-
# Skip STI detection if this class defines polymorphic_on (it's a base, not a child)
375-
if "polymorphic_on" not in mapper_args:
376-
is_sti_child = False
377-
378-
# Check explicit STI marker
379-
if "polymorphic_identity" in mapper_args:
380-
is_sti_child = True
381-
else:
382-
# Check if any parent has polymorphic_on (indicates STI hierarchy)
383-
for parent in cls.__mro__[1:]:
384-
if not hasattr(parent, "__mapper_args__"):
385-
continue
386-
parent_mapper_args = getattr(parent, "__mapper_args__", {})
387-
if "polymorphic_on" in parent_mapper_args:
388-
is_sti_child = True
389-
break
390-
391-
if is_sti_child:
392-
# This is an STI child - return None to use parent's table
349+
for parent in cls.__mro__[1:]:
350+
parent_mapper_args = getattr(parent, "__mapper_args__", None)
351+
if isinstance(parent_mapper_args, dict) and "polymorphic_on" in parent_mapper_args:
393352
return None
394353

395-
# Generate table name from class name using snake_case conversion
396354
return table_name_regexp.sub(r"_\1", cls.__name__).lower()
397355

398356

docs/changelog.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
Cause:
2222
We added the `sa.PasslibHasher = PasslibHasher` and `sa.PwdlibHasher = PwdlibHasher` types in `script.py.mako`. As a result, when a user installs only Advanced Alchemy and creates a migration, these files are imported. Since they reference types from `passlib` and `pwdlib`, which are not installed by default, the import fails and triggers this error.
2323

24-
.. change:: add missing type parameter to AsyncServiceT_co and SyncServiceT_…
24+
.. change:: add missing type parameter to ``AsyncServiceT_co`` and ``SyncServiceT_co``
2525
:type: bugfix
2626
:pr: 612
2727

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ filterwarnings = [
220220
"ignore::DeprecationWarning:google.gcloud",
221221
"ignore::DeprecationWarning:google.iam",
222222
"ignore::DeprecationWarning:google",
223+
"ignore:You are using a Python version \\(.*\\) which Google will stop supporting.*:FutureWarning:google.api_core._python_version_support",
223224
"ignore::DeprecationWarning:websockets.connection",
224225
"ignore::DeprecationWarning:websockets.legacy",
225226
"ignore:Accessing argon2.__version__ is deprecated:DeprecationWarning:passlib.handlers.argon2",

tests/integration/test_inheritance.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,15 @@
77
"""
88

99
import datetime
10-
from typing import TYPE_CHECKING, Any, Optional
10+
from typing import Optional
1111

1212
import pytest
1313
from sqlalchemy import ForeignKey, MetaData, select
14-
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column
14+
from sqlalchemy.engine import Engine
15+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
1516

1617
from advanced_alchemy import base
1718

18-
if TYPE_CHECKING:
19-
pass
20-
21-
2219
# ============================================================================
2320
# Single Table Inheritance (STI) Tests
2421
# ============================================================================
@@ -117,7 +114,7 @@ class SeniorManager(Manager):
117114

118115
@pytest.mark.integration
119116
@pytest.mark.sqlite
120-
def test_sti_crud_operations(session: Session, sqlite_engine: Any) -> None:
117+
def test_sti_crud_operations(sqlite_engine: Engine) -> None:
121118
"""STI: CRUD operations work correctly with polymorphic models."""
122119
from sqlalchemy.orm import Session as SessionType
123120

@@ -242,7 +239,7 @@ class Engineer(Employee):
242239

243240
@pytest.mark.integration
244241
@pytest.mark.sqlite
245-
def test_jti_crud_operations(session: Session, sqlite_engine: Any) -> None:
242+
def test_jti_crud_operations(sqlite_engine: Engine) -> None:
246243
"""JTI: CRUD operations with joined tables."""
247244
from sqlalchemy.orm import Session as SessionType
248245

@@ -356,7 +353,7 @@ class Engineer(Employee):
356353

357354
@pytest.mark.integration
358355
@pytest.mark.sqlite
359-
def test_cti_crud_operations(session: Session, sqlite_engine: Any) -> None:
356+
def test_cti_crud_operations(sqlite_engine: Engine) -> None:
360357
"""CTI: CRUD operations with concrete tables."""
361358
from sqlalchemy.orm import Session as SessionType
362359

@@ -489,6 +486,10 @@ class StandaloneModel(base.CommonTableAttributes, LocalBase):
489486

490487

491488
@pytest.mark.integration
489+
@pytest.mark.filterwarnings(
490+
"ignore:Mapper\\[Manager\\(employee_no_poly_id\\)\\] does not indicate a 'polymorphic_identity'.*:"
491+
"sqlalchemy.exc.SAWarning"
492+
)
492493
def test_sti_without_polymorphic_identity_on_child() -> None:
493494
"""STI child without explicit polymorphic_identity still uses parent table."""
494495
test_metadata = MetaData()

tests/unit/test_base.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,11 @@ def test_identity_primary_key_generates_identity_ddl() -> None:
5858
class TestMixin(IdentityPrimaryKey):
5959
pass
6060

61-
class TestModel(TestMixin, BigIntBase):
61+
class IdentityPrimaryKeyModel(TestMixin, BigIntBase):
6262
__tablename__ = "test_identity"
6363

6464
# Get the CREATE TABLE statement
65-
create_stmt = CreateTable(cast(Table, TestModel.__table__))
65+
create_stmt = CreateTable(cast(Table, IdentityPrimaryKeyModel.__table__))
6666

6767
# Test with PostgreSQL dialect
6868
pg_ddl = str(create_stmt.compile(dialect=postgresql.dialect())) # type: ignore[no-untyped-call,unused-ignore]
@@ -78,11 +78,11 @@ def test_identity_audit_base_generates_identity_ddl() -> None:
7878
"""Test that IdentityAuditBase generates proper IDENTITY DDL for PostgreSQL."""
7979
from advanced_alchemy.base import IdentityAuditBase
8080

81-
class TestModel(IdentityAuditBase):
81+
class IdentityAuditBaseModel(IdentityAuditBase):
8282
__tablename__ = "test_identity_audit"
8383

8484
# Get the CREATE TABLE statement
85-
create_stmt = CreateTable(cast(Table, TestModel.__table__))
85+
create_stmt = CreateTable(cast(Table, IdentityAuditBaseModel.__table__))
8686

8787
# Test with PostgreSQL dialect
8888
pg_ddl = str(create_stmt.compile(dialect=postgresql.dialect())) # type: ignore[no-untyped-call,unused-ignore]
@@ -101,11 +101,11 @@ def test_bigint_primary_key_still_uses_sequence() -> None:
101101
class TestMixin(BigIntPrimaryKey):
102102
pass
103103

104-
class TestModel(TestMixin, BigIntBase):
104+
class BigIntPrimaryKeyModel(TestMixin, BigIntBase):
105105
__tablename__ = "test_bigint"
106106

107107
# Get the CREATE TABLE statement
108-
create_stmt = CreateTable(cast(Table, TestModel.__table__))
108+
create_stmt = CreateTable(cast(Table, BigIntPrimaryKeyModel.__table__))
109109

110110
# Test with PostgreSQL dialect
111111
pg_ddl = str(create_stmt.compile(dialect=postgresql.dialect())) # type: ignore[no-untyped-call,unused-ignore]
@@ -114,18 +114,18 @@ class TestModel(TestMixin, BigIntBase):
114114
assert "GENERATED" not in pg_ddl
115115
assert "IDENTITY" not in pg_ddl.upper()
116116
# The sequence is defined on the column but rendered separately
117-
assert TestModel.__table__.c.id.default is not None
118-
assert TestModel.__table__.c.id.default.name == "test_bigint_id_seq"
117+
assert BigIntPrimaryKeyModel.__table__.c.id.default is not None
118+
assert BigIntPrimaryKeyModel.__table__.c.id.default.name == "test_bigint_id_seq"
119119

120120

121121
def test_identity_ddl_for_oracle() -> None:
122122
"""Test Identity DDL generation for Oracle."""
123123
from advanced_alchemy.base import IdentityAuditBase
124124

125-
class TestModel(IdentityAuditBase):
125+
class OracleIdentityAuditBaseModel(IdentityAuditBase):
126126
__tablename__ = "test_oracle"
127127

128-
create_stmt = CreateTable(cast(Table, TestModel.__table__))
128+
create_stmt = CreateTable(cast(Table, OracleIdentityAuditBaseModel.__table__))
129129
oracle_ddl = str(create_stmt.compile(dialect=oracle.dialect())) # type: ignore[no-untyped-call,unused-ignore]
130130

131131
# Oracle should generate IDENTITY
@@ -136,10 +136,10 @@ def test_identity_ddl_for_mssql() -> None:
136136
"""Test Identity DDL generation for SQL Server."""
137137
from advanced_alchemy.base import IdentityAuditBase
138138

139-
class TestModel(IdentityAuditBase):
139+
class MSSQLIdentityAuditBaseModel(IdentityAuditBase):
140140
__tablename__ = "test_mssql"
141141

142-
create_stmt = CreateTable(cast(Table, TestModel.__table__))
142+
create_stmt = CreateTable(cast(Table, MSSQLIdentityAuditBaseModel.__table__))
143143
mssql_ddl = str(create_stmt.compile(dialect=mssql.dialect())) # type: ignore[no-untyped-call,unused-ignore]
144144

145145
# SQL Server should generate IDENTITY
@@ -150,12 +150,12 @@ def test_identity_works_with_sqlite() -> None:
150150
"""Test that Identity columns work with SQLite (fallback to autoincrement)."""
151151
from advanced_alchemy.base import IdentityAuditBase
152152

153-
class TestModel(IdentityAuditBase):
153+
class SQLiteIdentityAuditBaseModel(IdentityAuditBase):
154154
__tablename__ = "test_sqlite"
155155

156156
# Create an in-memory SQLite engine
157157
engine = create_engine("sqlite:///:memory:")
158-
cast(Table, TestModel.__table__).create(engine)
158+
cast(Table, SQLiteIdentityAuditBaseModel.__table__).create(engine)
159159

160160
# Should not raise any errors
161161
assert True # If we get here, it worked

0 commit comments

Comments
 (0)