Skip to content

Commit 1bbd722

Browse files
committed
Add disable_autonaming
1 parent 8fdc47a commit 1bbd722

File tree

4 files changed

+61
-61
lines changed

4 files changed

+61
-61
lines changed

docs/customizing.rst

Lines changed: 11 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -160,71 +160,25 @@ To customize only ``session.query``, pass the ``query_cls`` key to the
160160
db = SQLAlchemy(session_options={"query_cls": GetOrQuery})
161161
162162
163-
Model Metaclass
164-
---------------
165-
166-
.. warning::
167-
Metaclasses are an advanced topic, and you probably don't need to customize them to
168-
achieve what you want. It is mainly documented here to show how to disable table
169-
name generation.
170-
171-
The model metaclass is responsible for setting up the SQLAlchemy internals when defining
172-
model subclasses. Flask-SQLAlchemy adds some extra behaviors through mixins; its default
173-
metaclass, :class:`~.DefaultMeta`, inherits them all.
174-
175-
- :class:`.BindMetaMixin`: ``__bind_key__`` sets the bind to use for the model.
176-
- :class:`.NameMetaMixin`: If the model does not specify a ``__tablename__`` but does
177-
specify a primary key, a name is automatically generated.
178-
179-
You can add your own behaviors by defining your own metaclass and creating the
180-
declarative base yourself. Be sure to still inherit from the mixins you want (or just
181-
inherit from the default metaclass).
182-
183-
Passing a declarative base class instead of a simple model base class to ``model_class``
184-
will cause Flask-SQLAlchemy to use this base instead of constructing one with the
185-
default metaclass.
186-
187-
.. code-block:: python
188-
189-
from sqlalchemy.orm import declarative_base
190-
from flask_sqlalchemy import SQLAlchemy
191-
from flask_sqlalchemy.model import DefaultMeta, Model
192-
193-
class CustomMeta(DefaultMeta):
194-
def __init__(cls, name, bases, d):
195-
# custom class setup could go here
196-
197-
# be sure to call super
198-
super(CustomMeta, cls).__init__(name, bases, d)
199-
200-
# custom class-only methods could go here
201-
202-
CustomModel = declarative_base(cls=Model, metaclass=CustomMeta, name="Model")
203-
db = SQLAlchemy(model_class=CustomModel)
204-
205-
You can also pass whatever other arguments you want to
206-
:func:`~sqlalchemy.orm.declarative_base` to customize the base class.
207-
208-
209163
Disabling Table Name Generation
210-
```````````````````````````````
164+
-------------------------------
211165

212166
Some projects prefer to set each model's ``__tablename__`` manually rather than relying
213167
on Flask-SQLAlchemy's detection and generation. The simple way to achieve that is to
214168
set each ``__tablename__`` and not modify the base class. However, the table name
215-
generation can be disabled by defining a custom metaclass with only the
216-
``BindMetaMixin`` and not the ``NameMetaMixin``.
169+
generation can be disabled by setting `disable_autonaming=True` in the `SQLAlchemy` constructor.
170+
171+
Example code using the SQLAlchemy 1.x (legacy) API:
217172

218173
.. code-block:: python
219174
220-
from sqlalchemy.orm import DeclarativeMeta, declarative_base
221-
from flask_sqlalchemy.model import BindMetaMixin, Model
175+
db = SQLAlchemy(app, disable_autonaming=True)
222176
223-
class NoNameMeta(BindMetaMixin, DeclarativeMeta):
224-
pass
177+
Example code using the SQLAlchemy 2.x declarative base:
225178

226-
CustomModel = declarative_base(cls=Model, metaclass=NoNameMeta, name="Model")
227-
db = SQLAlchemy(model_class=CustomModel)
179+
.. code-block:: python
180+
181+
class Base(sa_orm.DeclarativeBase):
182+
pass
228183
229-
This creates a base that still supports the ``__bind_key__`` feature but does not
230-
generate table names.
184+
db = SQLAlchemy(app, model_class=Base, disable_autonaming=True)

src/flask_sqlalchemy/extension.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from .model import _QueryProperty
1818
from .model import BindMixin
1919
from .model import DefaultMeta
20+
from .model import DefaultMetaNoName
2021
from .model import Model
2122
from .model import NameMixin
2223
from .pagination import Pagination
@@ -86,6 +87,10 @@ class SQLAlchemy:
8687
:param add_models_to_shell: Add the ``db`` instance and all model classes to
8788
``flask shell``.
8889
90+
.. versionchanged:: 3.1.0
91+
Added the ``disable_autonaming`` parameter and changed ``model_class`` parameter
92+
to accept a SQLAlchemy 2.0-style declarative base subclass.
93+
8994
.. versionchanged:: 3.0
9095
An active Flask application context is always required to access ``session`` and
9196
``engine``.
@@ -147,6 +152,7 @@ def __init__(
147152
model_class: t.Type[_FSA_MC] = Model, # type: ignore[assignment]
148153
engine_options: dict[str, t.Any] | None = None,
149154
add_models_to_shell: bool = True,
155+
disable_autonaming: bool = False,
150156
):
151157
if session_options is None:
152158
session_options = {}
@@ -207,7 +213,9 @@ def __init__(
207213
This is a subclass of SQLAlchemy's ``Table`` rather than a function.
208214
"""
209215

210-
self.Model = self._make_declarative_base(model_class)
216+
self.Model = self._make_declarative_base(
217+
model_class, disable_autonaming=disable_autonaming
218+
)
211219
"""A SQLAlchemy declarative model class. Subclass this to define database
212220
models.
213221
@@ -467,6 +475,7 @@ def __new__(
467475
def _make_declarative_base(
468476
self,
469477
model_class: t.Type[_FSA_MC],
478+
disable_autonaming: bool = False,
470479
) -> t.Type[_FSAModel]:
471480
"""Create a SQLAlchemy declarative model class. The result is available as
472481
:attr:`Model`.
@@ -482,8 +491,11 @@ def _make_declarative_base(
482491
:param model_class: A model base class, or an already created declarative model
483492
class.
484493
485-
.. versionchanged:: 3.0.4
494+
:param disable_autonaming: Turns off automatic tablename generation in models.
495+
496+
.. versionchanged:: 3.1.0
486497
Added support for passing SQLAlchemy 2.x base class as model class.
498+
Added optional ``disable_autonaming`` parameter.
487499
488500
.. versionchanged:: 3.0
489501
Renamed with a leading underscore, this method is internal.
@@ -505,16 +517,20 @@ def _make_declarative_base(
505517
)
506518
elif len(declarative_bases) == 1:
507519
body = {"__fsa__": self}
520+
mixin_classes = [BindMixin, NameMixin, Model]
521+
if disable_autonaming:
522+
mixin_classes.remove(NameMixin)
508523
model = types.new_class(
509524
"FlaskSQLAlchemyBase",
510-
(BindMixin, NameMixin, Model, *model_class.__bases__),
525+
(*mixin_classes, *model_class.__bases__),
511526
{"metaclass": type(declarative_bases[0])},
512527
lambda ns: ns.update(body),
513528
)
514529
elif not isinstance(model_class, sa_orm.DeclarativeMeta):
515530
metadata = self._make_metadata(None)
531+
metaclass = DefaultMetaNoName if disable_autonaming else DefaultMeta
516532
model = sa_orm.declarative_base(
517-
metadata=metadata, cls=model_class, name="Model", metaclass=DefaultMeta
533+
metadata=metadata, cls=model_class, name="Model", metaclass=metaclass
518534
)
519535
else:
520536
model = model_class

src/flask_sqlalchemy/model.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,3 +314,9 @@ class DefaultMeta(BindMetaMixin, NameMetaMixin, sa_orm.DeclarativeMeta):
314314
"""SQLAlchemy declarative metaclass that provides ``__bind_key__`` and
315315
``__tablename__`` support.
316316
"""
317+
318+
319+
class DefaultMetaNoName(BindMetaMixin, sa_orm.DeclarativeMeta):
320+
"""SQLAlchemy declarative metaclass that provides ``__bind_key__`` and
321+
``__tablename__`` support.
322+
"""

tests/test_model.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import pytest
66
import sqlalchemy as sa
7+
import sqlalchemy.exc as sa_exc
78
import sqlalchemy.orm as sa_orm
89
from flask import Flask
910

@@ -68,3 +69,26 @@ class Base(sa_orm.DeclarativeBase, sa_orm.DeclarativeBaseNoMeta): # type: ignor
6869

6970
with pytest.raises(ValueError):
7071
SQLAlchemy(app, model_class=Base)
72+
73+
74+
@pytest.mark.usefixtures("app_ctx")
75+
def test_disable_autonaming_true_sql1(app: Flask) -> None:
76+
db = SQLAlchemy(app, disable_autonaming=True)
77+
78+
with pytest.raises(sa_exc.InvalidRequestError):
79+
80+
class User(db.Model):
81+
id = sa.Column(sa.Integer, primary_key=True)
82+
83+
84+
@pytest.mark.usefixtures("app_ctx")
85+
def test_disable_autonaming_true_sql2(app: Flask) -> None:
86+
class Base(sa_orm.DeclarativeBase):
87+
pass
88+
89+
db = SQLAlchemy(app, model_class=Base, disable_autonaming=True)
90+
91+
with pytest.raises(sa_exc.InvalidRequestError):
92+
93+
class User(db.Model):
94+
id: sa_orm.Mapped[int] = sa_orm.mapped_column(sa.Integer, primary_key=True)

0 commit comments

Comments
 (0)