Skip to content

Commit 9dfafb1

Browse files
committed
Support generating tablenames in more cases.
Extends naming rules to apply to base classes of models.
1 parent 2143048 commit 9dfafb1

File tree

4 files changed

+144
-20
lines changed

4 files changed

+144
-20
lines changed

AUTHORS

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,5 @@ Patches and Suggestions
4545
- Matthew Geltz
4646
- Daniel Lepage
4747
- Ignacy Sokołowski
48-
- Steven Harms
48+
- Steven Harms
49+
- David Lord @davidism

CHANGES

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ Changelog
44
Here you can see the full list of changes between each Flask-SQLAlchemy
55
release.
66

7+
Version 2.1
8+
-----------
9+
10+
In development
11+
12+
- Table names are automatically generated in more cases, including
13+
subclassing mixins and abstract models.
14+
715
Version 2.0
816
-----------
917

flask_sqlalchemy/__init__.py

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -504,24 +504,51 @@ def get_engine(self):
504504
return rv
505505

506506

507-
def _defines_primary_key(d):
508-
"""Figures out if the given dictionary defines a primary key column."""
509-
return any(v.primary_key for k, v in iteritems(d)
510-
if isinstance(v, sqlalchemy.Column))
507+
def _should_set_tablename(bases, d):
508+
"""Check what values are set by a class and its bases to determine if a
509+
tablename should be automatically generated.
510+
511+
The class and its bases are checked in order of precedence: the class
512+
itself then each base in the order they were given at class definition.
513+
514+
Abstract classes do not generate a tablename, although they may have set
515+
or inherited a tablename elsewhere.
516+
517+
If a class defines a tablename or table, a new one will not be generated.
518+
Otherwise, if the class defines a primary key, a new name will be generated.
519+
520+
This supports:
521+
522+
* Joined table inheritance without explicitly naming sub-models.
523+
* Single table inheritance.
524+
* Inheriting from mixins or abstract models.
525+
526+
:param bases: base classes of new class
527+
:param d: new class dict
528+
:return: True if tablename should be set
529+
"""
530+
531+
if '__tablename__' in d or '__table__' in d or '__abstract__' in d:
532+
return False
533+
534+
if any(v.primary_key for v in itervalues(d) if isinstance(v, sqlalchemy.Column)):
535+
return True
536+
537+
for base in bases:
538+
if hasattr(base, '__tablename__') or hasattr(base, '__table__'):
539+
return False
540+
541+
for name in dir(base):
542+
attr = getattr(base, name)
543+
544+
if isinstance(attr, sqlalchemy.Column) and attr.primary_key:
545+
return True
511546

512547

513548
class _BoundDeclarativeMeta(DeclarativeMeta):
514549

515550
def __new__(cls, name, bases, d):
516-
tablename = d.get('__tablename__')
517-
518-
# generate a table name automatically if it's missing and the
519-
# class dictionary declares a primary key. We cannot always
520-
# attach a primary key to support model inheritance that does
521-
# not use joins. We also don't want a table name if a whole
522-
# table is defined
523-
if not tablename and d.get('__table__') is None and \
524-
_defines_primary_key(d):
551+
if _should_set_tablename(bases, d):
525552
def _join(match):
526553
word = match.group()
527554
if len(word) > 1:

test_sqlalchemy.py

Lines changed: 94 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from __future__ import with_statement
22

3-
import os
43
import atexit
54
import unittest
65
from datetime import datetime
76
import flask
87
from flask.ext import sqlalchemy
8+
from sqlalchemy.ext.declarative import declared_attr
99
from sqlalchemy.orm import sessionmaker
1010

1111

@@ -157,20 +157,108 @@ def committed(sender, changes):
157157
self.assertEqual(recorded[0][1], 'delete')
158158

159159

160-
class HelperTestCase(unittest.TestCase):
161-
162-
def test_default_table_name(self):
160+
class TablenameTestCase(unittest.TestCase):
161+
def test_name(self):
163162
app = flask.Flask(__name__)
164-
app.config['SQLALCHEMY_ENGINE'] = 'sqlite://'
163+
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
165164
db = sqlalchemy.SQLAlchemy(app)
166165

167166
class FOOBar(db.Model):
168167
id = db.Column(db.Integer, primary_key=True)
168+
169169
class BazBar(db.Model):
170170
id = db.Column(db.Integer, primary_key=True)
171171

172+
class Ham(db.Model):
173+
__tablename__ = 'spam'
174+
id = db.Column(db.Integer, primary_key=True)
175+
172176
self.assertEqual(FOOBar.__tablename__, 'foo_bar')
173177
self.assertEqual(BazBar.__tablename__, 'baz_bar')
178+
self.assertEqual(Ham.__tablename__, 'spam')
179+
180+
def test_single_name(self):
181+
"""Single table inheritance should not set a new name."""
182+
183+
app = flask.Flask(__name__)
184+
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
185+
db = sqlalchemy.SQLAlchemy(app)
186+
187+
class Duck(db.Model):
188+
id = db.Column(db.Integer, primary_key=True)
189+
190+
class Mallard(Duck):
191+
pass
192+
193+
self.assertEqual(Mallard.__tablename__, 'duck')
194+
195+
def test_joined_name(self):
196+
"""Model has a separate primary key; it should set a new name."""
197+
198+
app = flask.Flask(__name__)
199+
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
200+
db = sqlalchemy.SQLAlchemy(app)
201+
202+
class Duck(db.Model):
203+
id = db.Column(db.Integer, primary_key=True)
204+
205+
class Donald(Duck):
206+
id = db.Column(db.Integer, db.ForeignKey(Duck.id), primary_key=True)
207+
208+
self.assertEqual(Donald.__tablename__, 'donald')
209+
210+
def test_mixin_name(self):
211+
"""Primary key provided by mixin should still allow model to set tablename."""
212+
213+
app = flask.Flask(__name__)
214+
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
215+
db = sqlalchemy.SQLAlchemy(app)
216+
217+
class Base(object):
218+
id = db.Column(db.Integer, primary_key=True)
219+
220+
class Duck(Base, db.Model):
221+
pass
222+
223+
self.assertFalse(hasattr(Base, '__tablename__'))
224+
self.assertEqual(Duck.__tablename__, 'duck')
225+
226+
def test_abstract_name(self):
227+
"""Abstract model should not set a name. Subclass should set a name."""
228+
229+
app = flask.Flask(__name__)
230+
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
231+
db = sqlalchemy.SQLAlchemy(app)
232+
233+
class Base(db.Model):
234+
__abstract__ = True
235+
id = db.Column(db.Integer, primary_key=True)
236+
237+
class Duck(Base):
238+
pass
239+
240+
self.assertFalse(hasattr(Base, '__tablename__'))
241+
self.assertEqual(Duck.__tablename__, 'duck')
242+
243+
def test_complex_inheritance(self):
244+
"""Joined table inheritance, but the new primary key is provided by a mixin, not directly on the class."""
245+
246+
app = flask.Flask(__name__)
247+
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
248+
db = sqlalchemy.SQLAlchemy(app)
249+
250+
class Duck(db.Model):
251+
id = db.Column(db.Integer, primary_key=True)
252+
253+
class IdMixin(object):
254+
@declared_attr
255+
def id(cls):
256+
return db.Column(db.Integer, db.ForeignKey(Duck.id), primary_key=True)
257+
258+
class RubberDuck(IdMixin, Duck):
259+
pass
260+
261+
self.assertEqual(RubberDuck.__tablename__, 'rubber_duck')
174262

175263

176264
class PaginationTestCase(unittest.TestCase):
@@ -493,7 +581,7 @@ def suite():
493581
suite = unittest.TestSuite()
494582
suite.addTest(unittest.makeSuite(BasicAppTestCase))
495583
suite.addTest(unittest.makeSuite(TestQueryProperty))
496-
suite.addTest(unittest.makeSuite(HelperTestCase))
584+
suite.addTest(unittest.makeSuite(TablenameTestCase))
497585
suite.addTest(unittest.makeSuite(PaginationTestCase))
498586
suite.addTest(unittest.makeSuite(BindsTestCase))
499587
suite.addTest(unittest.makeSuite(DefaultQueryClassTestCase))

0 commit comments

Comments
 (0)