Skip to content

Commit 5bf8c75

Browse files
committed
fix: refactor code structure for improved readability and maintainability
1 parent 8885e29 commit 5bf8c75

File tree

4 files changed

+346
-409
lines changed

4 files changed

+346
-409
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ repos:
2222
- id: unasyncd
2323
additional_dependencies: ["ruff"]
2424
- repo: https://github.com/charliermarsh/ruff-pre-commit
25-
rev: "v0.14.2"
25+
rev: "v0.14.3"
2626
hooks:
2727
# Run the linter.
2828
- id: ruff

advanced_alchemy/base.py

Lines changed: 22 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
DeclarativeBase,
1414
Mapper,
1515
declared_attr,
16-
has_inherited_table,
1716
)
1817
from sqlalchemy.orm import (
1918
registry as SQLAlchemyRegistry, # noqa: N812
@@ -220,75 +219,16 @@ def __tablename__(cls) -> Optional[str]:
220219
Returns:
221220
Optional[str]: Snake-case table name derived from class name, or None for STI child classes.
222221
"""
223-
# Check if class explicitly defined __tablename__ (captured in __init_subclass__)
224-
# This must come FIRST to respect user's explicit choice
225-
if hasattr(cls, "_advanced_alchemy_explicit_tablename"):
222+
# Check if THIS class explicitly defined __tablename__
223+
if "_advanced_alchemy_explicit_tablename" in cls.__dict__:
226224
return cls._advanced_alchemy_explicit_tablename # type: ignore[attr-defined]
227225

228-
# No explicit tablename - proceed with STI detection and auto-generation
229-
is_concrete_table_inheritance = getattr(cls, "__mapper_args__", {}).get("concrete", False)
230-
231-
if has_inherited_table(cls) and not is_concrete_table_inheritance:
232-
# Check if any parent has polymorphic_on to confirm this is truly STI
233-
for parent in cls.__mro__[1:]:
234-
parent_mapper_args = getattr(parent, "__mapper_args__", {})
235-
if "polymorphic_on" in parent_mapper_args:
236-
# This is STI - return None to use parent's table
237-
return None
238-
239-
# Not STI - auto-generate tablename from class name
240-
return table_name_regexp.sub(r"_\1", cls.__name__).lower()
241-
242-
@classmethod
243-
def __table_cls__(cls, name: str, metadata: MetaData, *args: Any, **kwargs: Any) -> Optional[Any]:
244-
"""Control table creation to support inheritance patterns.
245-
246-
This hook is called by SQLAlchemy when constructing a Table. It allows
247-
us to inspect the columns and determine if this is truly a new table
248-
(JTI/CTI) or if it should use the parent's table (STI).
249-
250-
For STI, when a child class inherits __tablename__ from its parent,
251-
we prevent table creation by returning None.
252-
"""
253-
from sqlalchemy import Column, PrimaryKeyConstraint, Table
254-
255-
# Check if this class has its own primary key
256-
# JTI/CTI classes define their own PK, STI children do not
257-
has_own_pk = False
258-
for arg in args:
259-
if isinstance(arg, Column) and arg.primary_key:
260-
has_own_pk = True
261-
break
262-
if isinstance(arg, PrimaryKeyConstraint):
263-
has_own_pk = True
264-
break
265-
266-
if has_own_pk:
267-
# Has its own PK - this is JTI or CTI, create the table
268-
return Table(name, metadata, *args, **kwargs)
269-
270-
# No own PK - check if this is STI (inheriting from a mapped parent)
271-
is_concrete = getattr(cls, "__mapper_args__", {}).get("concrete", False)
272-
273-
is_sti_child = False
274-
if has_inherited_table(cls) and not is_concrete:
275-
# Check if any parent has polymorphic_on to confirm this is truly STI
276-
for parent in cls.__mro__[1:]:
277-
parent_mapper_args = getattr(parent, "__mapper_args__", {})
278-
if "polymorphic_on" in parent_mapper_args:
279-
is_sti_child = True
280-
break
281-
282-
if is_sti_child:
283-
# This is STI - don't create a table, use parent's
284-
# Delete the inherited tablename so SQLAlchemy uses parent table
285-
if hasattr(cls, "__tablename__"):
286-
with contextlib.suppress(AttributeError, TypeError):
287-
del cls.__tablename__
226+
# Check if __init_subclass__ set __tablename__ to None (STI child)
227+
if "__tablename__" in cls.__dict__ and cls.__dict__["__tablename__"] is None:
288228
return None
289229

290-
# Default: create the table
291-
return Table(name, metadata, *args, **kwargs)
230+
# Auto-generate tablename from class name
231+
return table_name_regexp.sub(r"_\1", cls.__name__).lower()
292232

293233

294234
def create_registry(
@@ -415,45 +355,34 @@ class AdvancedDeclarativeBase(DeclarativeBase):
415355

416356
def __init_subclass__(cls, **kwargs: Any) -> None:
417357
# Capture explicit __tablename__ BEFORE SQLAlchemy processes it
418-
# Store as a class attribute (not in __dict__) so it persists through SQLAlchemy's processing
419358
if "__tablename__" in cls.__dict__:
420359
tablename_value = cls.__dict__["__tablename__"]
421360
if isinstance(tablename_value, str):
422-
# Store it as a special attribute that persists
423361
cls._advanced_alchemy_explicit_tablename = tablename_value # type: ignore[attr-defined]
424362

425-
# Handle STI: if child doesn't explicitly define __tablename__, set it to None
426-
# so it uses parent's table (must be done before super().__init_subclass__)
363+
# Detect STI child and set __tablename__ = None BEFORE super().__init_subclass__
427364
if "__tablename__" not in cls.__dict__ and not cls.__dict__.get("__abstract__", False):
428-
# Child didn't explicitly set __tablename__
429-
# Check if it would inherit one from a CONCRETE mapped parent (not an abstract base)
430-
has_mapped_parent = False
431-
for base in cls.__mro__[1:]:
432-
# Skip abstract classes
433-
if base.__dict__.get("__abstract__", False):
434-
continue
435-
# Check if this base has an EXPLICIT tablename set in its __dict__
436-
# (not a @declared_attr, which would be a function/property)
437-
if "__tablename__" in base.__dict__ and isinstance(base.__dict__["__tablename__"], str):
438-
# This base has an explicit tablename - likely a concrete mapped class
439-
# Check this isn't JTI or CTI by looking for concrete=True
440-
mapper_args = getattr(cls, "__mapper_args__", {})
441-
is_concrete = mapper_args.get("concrete", False)
442-
443-
if not is_concrete:
444-
# Likely STI - set tablename to None
445-
# (JTI must set explicit __tablename__, so they won't hit this path)
446-
has_mapped_parent = True
447-
break
448-
449-
if has_mapped_parent:
450-
cls.__tablename__ = None # type: ignore[misc]
365+
# Child didn't explicitly set __tablename__ - check for STI pattern
366+
is_concrete = getattr(cls, "__mapper_args__", {}).get("concrete", False)
367+
if not is_concrete:
368+
for parent in cls.__mro__[1:]:
369+
# Skip parents that explicitly set __abstract__ = True in THEIR __dict__
370+
if parent.__dict__.get("__abstract__", False):
371+
continue
372+
if "_advanced_alchemy_explicit_tablename" in parent.__dict__:
373+
parent_mapper_args = getattr(parent, "__mapper_args__", {})
374+
if "polymorphic_on" in parent_mapper_args:
375+
# STI child - set __tablename__ to None BEFORE SQLAlchemy processes
376+
cls.__tablename__ = None # type: ignore[misc]
377+
break
451378

452379
bind_key = getattr(cls, "__bind_key__", None)
453380
if bind_key is not None:
454381
cls.metadata = cls.__metadata_registry__.get(bind_key)
455382
elif None not in cls.__metadata_registry__ and getattr(cls, "metadata", None) is not None:
456383
cls.__metadata_registry__[None] = cls.metadata
384+
385+
# Call super() AFTER setting __tablename__
457386
super().__init_subclass__(**kwargs)
458387

459388

tests/conftest.py

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,27 +32,13 @@ def _clear_sqlalchemy_mappers() -> Generator[None, None, None]:
3232
This prevents table name conflicts when tests define models with the same
3333
table names. The global orm_registry persists across tests, so we need to
3434
clear it between test runs.
35-
36-
Also clears the model caches to prevent using stale models that reference
37-
the disposed registry.
3835
"""
3936
from advanced_alchemy.base import orm_registry
4037

4138
yield
42-
# Don't dispose the registry - just clear the metadata
43-
# Disposing causes issues when subsequent tests try to create models
39+
orm_registry.dispose()
4440
orm_registry.metadata.clear()
4541

46-
# Clear model caches so next test gets fresh models with fresh metadata
47-
try:
48-
from tests.integration.repository_fixtures import _bigint_model_cache, _uuid_model_cache
49-
50-
_uuid_model_cache.clear()
51-
_bigint_model_cache.clear()
52-
except ImportError:
53-
# Not in integration test context
54-
pass
55-
5642

5743
@pytest.fixture(autouse=True, scope="session")
5844
def configure_logging() -> None:

0 commit comments

Comments
 (0)