You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

155 lines
4.9KB

  1. import re
  2. import sqlalchemy as sa
  3. from sqlalchemy import inspect
  4. from sqlalchemy.ext.declarative import DeclarativeMeta, declared_attr
  5. from sqlalchemy.schema import _get_table_key
  6. from ._compat import to_str
  7. def should_set_tablename(cls):
  8. """Determine whether ``__tablename__`` should be automatically generated
  9. for a model.
  10. * If no class in the MRO sets a name, one should be generated.
  11. * If a declared attr is found, it should be used instead.
  12. * If a name is found, it should be used if the class is a mixin, otherwise
  13. one should be generated.
  14. * Abstract models should not have one generated.
  15. Later, :meth:`._BoundDeclarativeMeta.__table_cls__` will determine if the
  16. model looks like single or joined-table inheritance. If no primary key is
  17. found, the name will be unset.
  18. """
  19. if (
  20. cls.__dict__.get('__abstract__', False)
  21. or not any(isinstance(b, DeclarativeMeta) for b in cls.__mro__[1:])
  22. ):
  23. return False
  24. for base in cls.__mro__:
  25. if '__tablename__' not in base.__dict__:
  26. continue
  27. if isinstance(base.__dict__['__tablename__'], declared_attr):
  28. return False
  29. return not (
  30. base is cls
  31. or base.__dict__.get('__abstract__', False)
  32. or not isinstance(base, DeclarativeMeta)
  33. )
  34. return True
  35. camelcase_re = re.compile(r'([A-Z]+)(?=[a-z0-9])')
  36. def camel_to_snake_case(name):
  37. def _join(match):
  38. word = match.group()
  39. if len(word) > 1:
  40. return ('_%s_%s' % (word[:-1], word[-1])).lower()
  41. return '_' + word.lower()
  42. return camelcase_re.sub(_join, name).lstrip('_')
  43. class NameMetaMixin(type):
  44. def __init__(cls, name, bases, d):
  45. if should_set_tablename(cls):
  46. cls.__tablename__ = camel_to_snake_case(cls.__name__)
  47. super(NameMetaMixin, cls).__init__(name, bases, d)
  48. # __table_cls__ has run at this point
  49. # if no table was created, use the parent table
  50. if (
  51. '__tablename__' not in cls.__dict__
  52. and '__table__' in cls.__dict__
  53. and cls.__dict__['__table__'] is None
  54. ):
  55. del cls.__table__
  56. def __table_cls__(cls, *args, **kwargs):
  57. """This is called by SQLAlchemy during mapper setup. It determines the
  58. final table object that the model will use.
  59. If no primary key is found, that indicates single-table inheritance,
  60. so no table will be created and ``__tablename__`` will be unset.
  61. """
  62. # check if a table with this name already exists
  63. # allows reflected tables to be applied to model by name
  64. key = _get_table_key(args[0], kwargs.get('schema'))
  65. if key in cls.metadata.tables:
  66. return sa.Table(*args, **kwargs)
  67. # if a primary key or constraint is found, create a table for
  68. # joined-table inheritance
  69. for arg in args:
  70. if (
  71. (isinstance(arg, sa.Column) and arg.primary_key)
  72. or isinstance(arg, sa.PrimaryKeyConstraint)
  73. ):
  74. return sa.Table(*args, **kwargs)
  75. # if no base classes define a table, return one
  76. # ensures the correct error shows up when missing a primary key
  77. for base in cls.__mro__[1:-1]:
  78. if '__table__' in base.__dict__:
  79. break
  80. else:
  81. return sa.Table(*args, **kwargs)
  82. # single-table inheritance, use the parent tablename
  83. if '__tablename__' in cls.__dict__:
  84. del cls.__tablename__
  85. class BindMetaMixin(type):
  86. def __init__(cls, name, bases, d):
  87. bind_key = (
  88. d.pop('__bind_key__', None)
  89. or getattr(cls, '__bind_key__', None)
  90. )
  91. super(BindMetaMixin, cls).__init__(name, bases, d)
  92. if bind_key is not None and getattr(cls, '__table__', None) is not None:
  93. cls.__table__.info['bind_key'] = bind_key
  94. class DefaultMeta(NameMetaMixin, BindMetaMixin, DeclarativeMeta):
  95. pass
  96. class Model(object):
  97. """Base class for SQLAlchemy declarative base model.
  98. To define models, subclass :attr:`db.Model <SQLAlchemy.Model>`, not this
  99. class. To customize ``db.Model``, subclass this and pass it as
  100. ``model_class`` to :class:`SQLAlchemy`.
  101. """
  102. #: Query class used by :attr:`query`. Defaults to
  103. # :class:`SQLAlchemy.Query`, which defaults to :class:`BaseQuery`.
  104. query_class = None
  105. #: Convenience property to query the database for instances of this model
  106. # using the current session. Equivalent to ``db.session.query(Model)``
  107. # unless :attr:`query_class` has been changed.
  108. query = None
  109. def __repr__(self):
  110. identity = inspect(self).identity
  111. if identity is None:
  112. pk = "(transient {0})".format(id(self))
  113. else:
  114. pk = ', '.join(to_str(value) for value in identity)
  115. return '<{0} {1}>'.format(type(self).__name__, pk)