| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330 |
- from __future__ import annotations
- import re
- import typing as t
- import sqlalchemy as sa
- import sqlalchemy.orm as sa_orm
- from .query import Query
- if t.TYPE_CHECKING:
- from .extension import SQLAlchemy
- class _QueryProperty:
- """A class property that creates a query object for a model.
- :meta private:
- """
- def __get__(self, obj: Model | None, cls: type[Model]) -> Query:
- return cls.query_class(
- cls, session=cls.__fsa__.session() # type: ignore[arg-type]
- )
- class Model:
- """The base class of the :attr:`.SQLAlchemy.Model` declarative model class.
- To define models, subclass :attr:`db.Model <.SQLAlchemy.Model>`, not this. To
- customize ``db.Model``, subclass this and pass it as ``model_class`` to
- :class:`.SQLAlchemy`. To customize ``db.Model`` at the metaclass level, pass an
- already created declarative model class as ``model_class``.
- """
- __fsa__: t.ClassVar[SQLAlchemy]
- """Internal reference to the extension object.
- :meta private:
- """
- query_class: t.ClassVar[type[Query]] = Query
- """Query class used by :attr:`query`. Defaults to :attr:`.SQLAlchemy.Query`, which
- defaults to :class:`.Query`.
- """
- query: t.ClassVar[Query] = _QueryProperty() # type: ignore[assignment]
- """A SQLAlchemy query for a model. Equivalent to ``db.session.query(Model)``. Can be
- customized per-model by overriding :attr:`query_class`.
- .. warning::
- The query interface is considered legacy in SQLAlchemy. Prefer using
- ``session.execute(select())`` instead.
- """
- def __repr__(self) -> str:
- state = sa.inspect(self)
- assert state is not None
- if state.transient:
- pk = f"(transient {id(self)})"
- elif state.pending:
- pk = f"(pending {id(self)})"
- else:
- pk = ", ".join(map(str, state.identity))
- return f"<{type(self).__name__} {pk}>"
- class BindMetaMixin(type):
- """Metaclass mixin that sets a model's ``metadata`` based on its ``__bind_key__``.
- If the model sets ``metadata`` or ``__table__`` directly, ``__bind_key__`` is
- ignored. If the ``metadata`` is the same as the parent model, it will not be set
- directly on the child model.
- """
- __fsa__: SQLAlchemy
- metadata: sa.MetaData
- def __init__(
- cls, name: str, bases: tuple[type, ...], d: dict[str, t.Any], **kwargs: t.Any
- ) -> None:
- if not ("metadata" in cls.__dict__ or "__table__" in cls.__dict__):
- bind_key = getattr(cls, "__bind_key__", None)
- parent_metadata = getattr(cls, "metadata", None)
- metadata = cls.__fsa__._make_metadata(bind_key)
- if metadata is not parent_metadata:
- cls.metadata = metadata
- super().__init__(name, bases, d, **kwargs)
- class BindMixin:
- """DeclarativeBase mixin to set a model's ``metadata`` based on ``__bind_key__``.
- If no ``__bind_key__`` is specified, the model will use the default metadata
- provided by ``DeclarativeBase`` or ``DeclarativeBaseNoMeta``.
- If the model doesn't set ``metadata`` or ``__table__`` directly
- and does set ``__bind_key__``, the model will use the metadata
- for the specified bind key.
- If the ``metadata`` is the same as the parent model, it will not be set
- directly on the child model.
- .. versionchanged:: 3.1.0
- """
- __fsa__: SQLAlchemy
- metadata: sa.MetaData
- @classmethod
- def __init_subclass__(cls: t.Type[BindMixin], **kwargs: t.Dict[str, t.Any]) -> None:
- if not ("metadata" in cls.__dict__ or "__table__" in cls.__dict__) and hasattr(
- cls, "__bind_key__"
- ):
- bind_key = getattr(cls, "__bind_key__", None)
- parent_metadata = getattr(cls, "metadata", None)
- metadata = cls.__fsa__._make_metadata(bind_key)
- if metadata is not parent_metadata:
- cls.metadata = metadata
- super().__init_subclass__(**kwargs)
- class NameMetaMixin(type):
- """Metaclass mixin that sets a model's ``__tablename__`` by converting the
- ``CamelCase`` class name to ``snake_case``. A name is set for non-abstract models
- that do not otherwise define ``__tablename__``. If a model does not define a primary
- key, it will not generate a name or ``__table__``, for single-table inheritance.
- """
- metadata: sa.MetaData
- __tablename__: str
- __table__: sa.Table
- def __init__(
- cls, name: str, bases: tuple[type, ...], d: dict[str, t.Any], **kwargs: t.Any
- ) -> None:
- if should_set_tablename(cls):
- cls.__tablename__ = camel_to_snake_case(cls.__name__)
- super().__init__(name, bases, d, **kwargs)
- # __table_cls__ has run. If no table was created, use the parent table.
- if (
- "__tablename__" not in cls.__dict__
- and "__table__" in cls.__dict__
- and cls.__dict__["__table__"] is None
- ):
- del cls.__table__
- def __table_cls__(cls, *args: t.Any, **kwargs: t.Any) -> sa.Table | None:
- """This is called by SQLAlchemy during mapper setup. It determines the final
- table object that the model will use.
- If no primary key is found, that indicates single-table inheritance, so no table
- will be created and ``__tablename__`` will be unset.
- """
- schema = kwargs.get("schema")
- if schema is None:
- key = args[0]
- else:
- key = f"{schema}.{args[0]}"
- # Check if a table with this name already exists. Allows reflected tables to be
- # applied to models by name.
- if key in cls.metadata.tables:
- return sa.Table(*args, **kwargs)
- # If a primary key is found, create a table for joined-table inheritance.
- for arg in args:
- if (isinstance(arg, sa.Column) and arg.primary_key) or isinstance(
- arg, sa.PrimaryKeyConstraint
- ):
- return sa.Table(*args, **kwargs)
- # If no base classes define a table, return one that's missing a primary key
- # so SQLAlchemy shows the correct error.
- for base in cls.__mro__[1:-1]:
- if "__table__" in base.__dict__:
- break
- else:
- return sa.Table(*args, **kwargs)
- # Single-table inheritance, use the parent table name. __init__ will unset
- # __table__ based on this.
- if "__tablename__" in cls.__dict__:
- del cls.__tablename__
- return None
- class NameMixin:
- """DeclarativeBase mixin that sets a model's ``__tablename__`` by converting the
- ``CamelCase`` class name to ``snake_case``. A name is set for non-abstract models
- that do not otherwise define ``__tablename__``. If a model does not define a primary
- key, it will not generate a name or ``__table__``, for single-table inheritance.
- .. versionchanged:: 3.1.0
- """
- metadata: sa.MetaData
- __tablename__: str
- __table__: sa.Table
- @classmethod
- def __init_subclass__(cls: t.Type[NameMixin], **kwargs: t.Dict[str, t.Any]) -> None:
- if should_set_tablename(cls):
- cls.__tablename__ = camel_to_snake_case(cls.__name__)
- super().__init_subclass__(**kwargs)
- # __table_cls__ has run. If no table was created, use the parent table.
- if (
- "__tablename__" not in cls.__dict__
- and "__table__" in cls.__dict__
- and cls.__dict__["__table__"] is None
- ):
- del cls.__table__
- @classmethod
- def __table_cls__(cls, *args: t.Any, **kwargs: t.Any) -> sa.Table | None:
- """This is called by SQLAlchemy during mapper setup. It determines the final
- table object that the model will use.
- If no primary key is found, that indicates single-table inheritance, so no table
- will be created and ``__tablename__`` will be unset.
- """
- schema = kwargs.get("schema")
- if schema is None:
- key = args[0]
- else:
- key = f"{schema}.{args[0]}"
- # Check if a table with this name already exists. Allows reflected tables to be
- # applied to models by name.
- if key in cls.metadata.tables:
- return sa.Table(*args, **kwargs)
- # If a primary key is found, create a table for joined-table inheritance.
- for arg in args:
- if (isinstance(arg, sa.Column) and arg.primary_key) or isinstance(
- arg, sa.PrimaryKeyConstraint
- ):
- return sa.Table(*args, **kwargs)
- # If no base classes define a table, return one that's missing a primary key
- # so SQLAlchemy shows the correct error.
- for base in cls.__mro__[1:-1]:
- if "__table__" in base.__dict__:
- break
- else:
- return sa.Table(*args, **kwargs)
- # Single-table inheritance, use the parent table name. __init__ will unset
- # __table__ based on this.
- if "__tablename__" in cls.__dict__:
- del cls.__tablename__
- return None
- def should_set_tablename(cls: type) -> bool:
- """Determine whether ``__tablename__`` should be generated for a model.
- - If no class in the MRO sets a name, one should be generated.
- - If a declared attr is found, it should be used instead.
- - If a name is found, it should be used if the class is a mixin, otherwise one
- should be generated.
- - Abstract models should not have one generated.
- Later, ``__table_cls__`` will determine if the model looks like single or
- joined-table inheritance. If no primary key is found, the name will be unset.
- """
- if (
- cls.__dict__.get("__abstract__", False)
- or (
- not issubclass(cls, (sa_orm.DeclarativeBase, sa_orm.DeclarativeBaseNoMeta))
- and not any(isinstance(b, sa_orm.DeclarativeMeta) for b in cls.__mro__[1:])
- )
- or any(
- (b is sa_orm.DeclarativeBase or b is sa_orm.DeclarativeBaseNoMeta)
- for b in cls.__bases__
- )
- ):
- return False
- for base in cls.__mro__:
- if "__tablename__" not in base.__dict__:
- continue
- if isinstance(base.__dict__["__tablename__"], sa_orm.declared_attr):
- return False
- return not (
- base is cls
- or base.__dict__.get("__abstract__", False)
- or not (
- # SQLAlchemy 1.x
- isinstance(base, sa_orm.DeclarativeMeta)
- # 2.x: DeclarativeBas uses this as metaclass
- or isinstance(base, sa_orm.decl_api.DeclarativeAttributeIntercept)
- # 2.x: DeclarativeBaseNoMeta doesn't use a metaclass
- or issubclass(base, sa_orm.DeclarativeBaseNoMeta)
- )
- )
- return True
- def camel_to_snake_case(name: str) -> str:
- """Convert a ``CamelCase`` name to ``snake_case``."""
- name = re.sub(r"((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))", r"_\1", name)
- return name.lower().lstrip("_")
- class DefaultMeta(BindMetaMixin, NameMetaMixin, sa_orm.DeclarativeMeta):
- """SQLAlchemy declarative metaclass that provides ``__bind_key__`` and
- ``__tablename__`` support.
- """
- class DefaultMetaNoName(BindMetaMixin, sa_orm.DeclarativeMeta):
- """SQLAlchemy declarative metaclass that provides ``__bind_key__`` and
- ``__tablename__`` support.
- """
|