X-Git-Url: http://git.veekun.com/zzz-pokedex.git/blobdiff_plain/31634e7159d8c385b5382ed47037972e88478358..f2accada71ca3bc0977f3a735c9dbd5e8cb2a41c:/pokedex/db/tables.py diff --git a/pokedex/db/tables.py b/pokedex/db/tables.py index 8ea9586..b77864c 100644 --- a/pokedex/db/tables.py +++ b/pokedex/db/tables.py @@ -18,10 +18,16 @@ Columns have a info dictionary with these keys: """ # XXX: Check if "gametext" is set correctly everywhere +import operator + from sqlalchemy import Column, ForeignKey, MetaData, PrimaryKeyConstraint, Table -from sqlalchemy.ext.declarative import declarative_base, declared_attr +from sqlalchemy.ext.declarative import ( + declarative_base, declared_attr, DeclarativeMeta, + ) from sqlalchemy.ext.associationproxy import association_proxy -from sqlalchemy.orm import backref, eagerload_all, relation, class_mapper +from sqlalchemy.orm import ( + backref, eagerload_all, relation, class_mapper, synonym, mapper, + ) from sqlalchemy.orm.session import Session from sqlalchemy.orm.collections import attribute_mapped_collection from sqlalchemy.sql import and_ @@ -30,8 +36,17 @@ from inspect import isclass from pokedex.db import markdown +# A list of all table classes will live in table_classes +table_classes = [] + +class TableMetaclass(DeclarativeMeta): + def __init__(cls, name, bases, attrs): + super(TableMetaclass, cls).__init__(name, bases, attrs) + if hasattr(cls, '__tablename__'): + table_classes.append(cls) + metadata = MetaData() -TableBase = declarative_base(metadata=metadata) +TableBase = declarative_base(metadata=metadata, metaclass=TableMetaclass) ### Helper classes class Named(object): @@ -63,10 +78,13 @@ class LanguageSpecific(object): class LanguageSpecificColumn(object): """A column that will not appear in the table it's defined in, but in a related one""" + _ordering = [1] def __init__(self, *args, **kwargs): self.args = args self.plural = kwargs.pop('plural') self.kwargs = kwargs + self.order = self._ordering[0] + self._ordering[0] += 1 def makeSAColumn(self): return Column(*self.args, **self.kwargs) @@ -1521,11 +1539,20 @@ Move.super_contest_combo_prev = association_proxy('super_contest_combo_second', Move.target = relation(MoveTarget, backref='moves') Move.type = relation(Type, back_populates='moves') +Move.effect = markdown.MoveEffectProperty('effect') +Move.effects = markdown.MoveEffectsProperty('effect') +Move.short_effect = markdown.MoveEffectProperty('short_effect') +Move.short_effects = markdown.MoveEffectsProperty('short_effect') MoveChangelog.changed_in = relation(VersionGroup, backref='move_changelog') MoveChangelog.move_effect = relation(MoveEffect, backref='move_changelog') MoveChangelog.type = relation(Type, backref='move_changelog') +MoveChangelog.effect = markdown.MoveEffectProperty('effect') +MoveChangelog.effects = markdown.MoveEffectsProperty('effect') +MoveChangelog.short_effect = markdown.MoveEffectProperty('short_effect') +MoveChangelog.short_effects = markdown.MoveEffectsProperty('short_effect') + MoveEffect.category_map = relation(MoveEffectCategoryMap) MoveEffect.categories = association_proxy('category_map', 'category') MoveEffect.changelog = relation(MoveEffectChangelog, @@ -1723,16 +1750,9 @@ VersionGroup.version_group_regions = relation(VersionGroupRegion, backref='versi VersionGroup.regions = association_proxy('version_group_regions', 'region') VersionGroup.pokedex = relation(Pokedex, back_populates='version_groups') -### Convenience function -def all_tables(): - u"""Yields all tables in the pokédex""" - for table in set(t for t in globals().values() if isclass(t)): - if issubclass(table, TableBase) and table is not TableBase: - yield table - ### Add name tables -for table in all_tables(): +for table in list(table_classes): if issubclass(table, OfficiallyNamed): cls = TextColumn info=dict(description="The name", format='plaintext', official=True) @@ -1752,52 +1772,53 @@ def makeTextTable(object_table, name_plural, name_singular, columns, lazy): if safe_name == 'language': safe_name = 'lang' - fields = { - safe_name + '_id': Column(Integer, - ForeignKey(object_table.id), primary_key=True, nullable=False, - info=dict(description="ID of the object this table represents") - ), - '__tablename__': table.__singlename__ + '_' + name_plural, - '__singlename__': table.__singlename__ + '_' + name_singular, - 'is_%s_table' % name_singular: True, - } + tablename = object_table.__singlename__ + '_' + name_plural + singlename = object_table.__singlename__ + '_' + name_singular - fields.update((name, col) for name, plural, col in columns) + class Strings(object): + __tablename__ = tablename + __singlename__ = singlename - name = table.__name__ + name_singular.capitalize() + for name, plural, column in columns: + column.name = name - # There are some dynamic things that can only be set at class - # creation time because of declarative metaclass magic. - # So create class dynamically. - Strings = type(name, (TableBase, LanguageSpecific), fields) + table = Table(tablename, metadata, + Column(safe_name + '_id', Integer, ForeignKey(object_table.id), + primary_key=True, nullable=False), + Column('language_id', Integer, ForeignKey(Language.id), + primary_key=True, nullable=False), + *(column for name, plural, column in columns) + ) - # Alias the described thing to 'object', to make meta stuff easier - Strings.object_id = getattr(Strings, safe_name + '_id') + mapper(Strings, table, + properties={ + "object_id": synonym(safe_name + '_id'), + "language": relation( + Language, + primaryjoin=table.c.language_id == Language.id, + ), + }, + ) # The relation to the object - setattr(table, name_plural, relation( + setattr(object_table, name_plural, relation( Strings, - primaryjoin=(table.id == Strings.object_id), + primaryjoin=(object_table.id == Strings.object_id), backref=safe_name, collection_class=attribute_mapped_collection('language'), lazy=lazy, )) - Strings.object = getattr(Strings, safe_name) - Strings.object_table = table - setattr(table, name_singular + '_table', Strings) - - Strings.language = relation( - Language, - primaryjoin=Strings.language_id == Language.id, - ) + # Link the tables themselves, so we can get to them + Strings.object_table = object_table + setattr(object_table, name_singular + '_table', Strings) for colname, pluralname, column in columns: # Provide a relation with all the names, and an English accessor # for backwards compatibility def scope(colname, pluralname, column): - def get_string(self): + def get_strings(self): return dict( (l, getattr(t, colname)) for l, t in getattr(self, name_plural).items() @@ -1805,38 +1826,48 @@ def makeTextTable(object_table, name_plural, name_singular, columns, lazy): def get_english_string(self): try: - return get_string(self)['en'] + return get_strings(self)['en'] except KeyError: raise AttributeError(colname) - setattr(table, pluralname, property(get_string)) - setattr(table, colname, property(get_english_string)) + setattr(object_table, pluralname, property(get_strings)) + setattr(object_table, colname, property(get_english_string)) scope(colname, pluralname, column) if colname == 'name': - table.name_table = Strings + object_table.name_table = Strings return Strings -for table in all_tables(): - text_columns = [] - prose_columns = [] +for table in list(table_classes): + # Find all the language-specific columns, keeping them in the order they + # were defined + all_columns = [] for colname in dir(table): column = getattr(table, colname) + if isinstance(column, LanguageSpecificColumn): + all_columns.append((colname, column)) + all_columns.sort(key=lambda pair: pair[1].order) + + # Break them into text and prose columns + text_columns = [] + prose_columns = [] + for colname, column in all_columns: + spec = colname, column.plural, column.makeSAColumn() if isinstance(column, TextColumn): - text_columns.append((colname, column.plural, column.makeSAColumn())) + text_columns.append(spec) elif isinstance(column, ProseColumn): - prose_columns.append((colname, column.plural, column.makeSAColumn())) + prose_columns.append(spec) + + if (text_columns or prose_columns) and issubclass(table, LanguageSpecific): + raise AssertionError("Language-specific table %s shouldn't have explicit language-specific columns" % table) + if text_columns: string_table = makeTextTable(table, 'texts', 'text', text_columns, lazy=False) - globals()[string_table.__name__] = string_table if prose_columns: string_table = makeTextTable(table, 'prose', 'prose', prose_columns, lazy=True) - globals()[string_table.__name__] = string_table - if (text_columns or prose_columns) and issubclass(table, LanguageSpecific): - raise AssertionError("Language-specific table %s shouldn't have explicit language-specific columns" % table) ### Add language relations -for table in all_tables(): +for table in list(table_classes): if issubclass(table, LanguageSpecific): table.language = relation(Language, primaryjoin=table.language_id == Language.id)