2012-02-12 20:30:30 +00:00
|
|
|
|
# Encoding: UTF-8
|
|
|
|
|
|
|
|
|
|
u"""Automatic documentation generation for pokédex tables
|
|
|
|
|
|
|
|
|
|
This adds a "dex-table" directive to Sphinx, which works like "autoclass",
|
|
|
|
|
but documents Pokédex mapped classes.
|
|
|
|
|
"""
|
|
|
|
|
# XXX: This assumes all the tables are in pokedex.db.tables
|
|
|
|
|
|
|
|
|
|
import functools
|
|
|
|
|
import textwrap
|
|
|
|
|
|
|
|
|
|
from docutils import nodes
|
|
|
|
|
from docutils.statemachine import ViewList
|
|
|
|
|
from sphinx.domains.python import PyClasslike
|
2013-06-13 20:20:42 +00:00
|
|
|
|
from sphinx.util.docfields import TypedField
|
2012-02-12 20:30:30 +00:00
|
|
|
|
|
|
|
|
|
from sqlalchemy import types
|
|
|
|
|
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
2012-06-05 21:47:42 +00:00
|
|
|
|
from sqlalchemy.orm.properties import RelationshipProperty
|
2013-06-13 20:20:42 +00:00
|
|
|
|
from sqlalchemy.orm import configure_mappers
|
2012-02-12 20:30:30 +00:00
|
|
|
|
from sqlalchemy.ext.associationproxy import AssociationProxy
|
|
|
|
|
from pokedex.db.markdown import MoveEffectPropertyMap, MoveEffectProperty
|
|
|
|
|
|
|
|
|
|
from pokedex.db import tables, markdown
|
|
|
|
|
|
|
|
|
|
# Make sure all the backrefs are in place
|
|
|
|
|
configure_mappers()
|
|
|
|
|
|
|
|
|
|
column_to_cls = {}
|
|
|
|
|
for cls in tables.mapped_classes:
|
|
|
|
|
for column in cls.__table__.c:
|
|
|
|
|
column_to_cls[column] = cls
|
|
|
|
|
|
2013-06-13 22:49:25 +00:00
|
|
|
|
def isrelationship(prop):
|
|
|
|
|
return isinstance(prop, InstrumentedAttribute) and isinstance(prop.property, RelationshipProperty)
|
2012-02-12 20:30:30 +00:00
|
|
|
|
|
2013-06-13 22:49:25 +00:00
|
|
|
|
def human_join(seq):
|
|
|
|
|
if len(seq) == 0:
|
|
|
|
|
return u'none'
|
|
|
|
|
elif len(seq) <= 2:
|
|
|
|
|
return u' and '.join(seq)
|
|
|
|
|
else:
|
|
|
|
|
return u', '.join(seq[:-1]) + u', and ' + seq[-1]
|
2012-02-12 20:30:30 +00:00
|
|
|
|
|
|
|
|
|
def column_type_str(column):
|
|
|
|
|
"""Extract the type name from a SQLA column
|
|
|
|
|
"""
|
|
|
|
|
type_ = column.type
|
|
|
|
|
# We're checking the specific type here: no issubclass
|
|
|
|
|
if type(type_) in (types.Integer, types.SmallInteger):
|
|
|
|
|
return 'int'
|
|
|
|
|
if type(type_) == types.Boolean:
|
|
|
|
|
return 'bool'
|
|
|
|
|
if type(type_) == types.Unicode:
|
|
|
|
|
return u'unicode – %s' % column.info['format']
|
|
|
|
|
if type(type_) == types.Enum:
|
|
|
|
|
return 'enum: [%s]' % ', '.join(type_.enums)
|
|
|
|
|
if type(type_) == markdown.MarkdownColumn:
|
|
|
|
|
return 'markdown'
|
|
|
|
|
raise ValueError(repr(type_))
|
|
|
|
|
|
|
|
|
|
common_columns = 'id identifier name'.split()
|
|
|
|
|
|
|
|
|
|
def column_header(c, class_name=None, transl_name=None, show_type=True,
|
|
|
|
|
relation=None, relation_name=None):
|
|
|
|
|
"""Return the column header for the given column"""
|
|
|
|
|
result = []
|
|
|
|
|
if relation_name:
|
|
|
|
|
name = relation_name
|
|
|
|
|
else:
|
|
|
|
|
name = c.name
|
|
|
|
|
if class_name:
|
|
|
|
|
result.append(u'%s.\ **%s**' % (class_name, name))
|
|
|
|
|
else:
|
|
|
|
|
result.append(u'**%s**' % c.name)
|
|
|
|
|
if c.foreign_keys:
|
|
|
|
|
for fk in c.foreign_keys:
|
|
|
|
|
if fk.column in column_to_cls:
|
|
|
|
|
foreign_cls = column_to_cls[fk.column]
|
2012-02-12 22:19:40 +00:00
|
|
|
|
if relation_name and relation_name + '_id' == c.name:
|
2012-02-12 20:30:30 +00:00
|
|
|
|
result.append(u'(%s →' % c.name)
|
2012-02-12 22:19:40 +00:00
|
|
|
|
elif relation_name:
|
|
|
|
|
result.append(u'(**%s** →' % c.name)
|
2012-02-12 20:30:30 +00:00
|
|
|
|
else:
|
|
|
|
|
result.append(u'(→')
|
|
|
|
|
result.append(u':class:`~pokedex.db.tables.%s`.%s)' % (
|
|
|
|
|
foreign_cls.__name__,
|
|
|
|
|
fk.column.name
|
|
|
|
|
))
|
|
|
|
|
break
|
|
|
|
|
elif show_type:
|
|
|
|
|
result.append(u'(*%s*)' % column_type_str(c))
|
|
|
|
|
if transl_name:
|
|
|
|
|
result.append(u'via *%s*' % transl_name)
|
|
|
|
|
return ' '.join(result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def with_header(header=None):
|
|
|
|
|
"""Decorator that adds a section header if there's a any output
|
|
|
|
|
|
|
|
|
|
The decorated function should yield output lines; if there are any the
|
|
|
|
|
header gets added.
|
|
|
|
|
"""
|
|
|
|
|
def wrap(func):
|
|
|
|
|
@functools.wraps(func)
|
|
|
|
|
def wrapped(cls, remaining_attrs):
|
|
|
|
|
result = list(func(cls, remaining_attrs))
|
|
|
|
|
if result:
|
|
|
|
|
# Sphinx/ReST doesn't allow "-----" just anywhere :(
|
|
|
|
|
yield u''
|
|
|
|
|
yield u'.. raw:: html'
|
|
|
|
|
yield u''
|
|
|
|
|
yield u' <hr>'
|
|
|
|
|
yield u''
|
|
|
|
|
if header:
|
|
|
|
|
yield header + u':'
|
|
|
|
|
yield u''
|
|
|
|
|
for row in result:
|
|
|
|
|
yield row
|
|
|
|
|
return wrapped
|
|
|
|
|
return wrap
|
|
|
|
|
|
|
|
|
|
### Section generation functions
|
|
|
|
|
|
|
|
|
|
def generate_table_header(cls, remaining_attrs):
|
|
|
|
|
first_line, sep, next_lines = unicode(cls.__doc__).partition(u'\n')
|
|
|
|
|
yield first_line
|
|
|
|
|
for line in textwrap.dedent(next_lines).split('\n'):
|
|
|
|
|
yield line
|
|
|
|
|
yield ''
|
|
|
|
|
|
|
|
|
|
yield u'Table name: *%s*' % cls.__tablename__
|
|
|
|
|
try:
|
|
|
|
|
yield u'(single: *%s*)' % cls.__singlename__
|
|
|
|
|
except AttributeError:
|
|
|
|
|
pass
|
|
|
|
|
yield u''
|
|
|
|
|
|
2012-06-05 22:15:40 +00:00
|
|
|
|
yield u'Primary key: %s.' % u', '.join(
|
|
|
|
|
u'**%s**' % col.key for col in cls.__table__.primary_key.columns)
|
|
|
|
|
yield u''
|
|
|
|
|
|
2012-02-12 20:30:30 +00:00
|
|
|
|
def generate_common(cls, remaining_attrs):
|
|
|
|
|
common_col_headers = []
|
|
|
|
|
for c in cls.__table__.c:
|
|
|
|
|
if c.name in common_columns:
|
|
|
|
|
common_col_headers.append(column_header(c, show_type=False))
|
|
|
|
|
remaining_attrs.remove(c.name)
|
|
|
|
|
for translation_class in cls.translation_classes:
|
|
|
|
|
for c in translation_class.__table__.c:
|
|
|
|
|
if c.name in common_columns:
|
|
|
|
|
common_col_headers.append(column_header(c, None,
|
|
|
|
|
translation_class.__table__.name, show_type=False))
|
|
|
|
|
remaining_attrs.remove(c.name)
|
|
|
|
|
|
|
|
|
|
if common_col_headers:
|
|
|
|
|
yield u'Has'
|
2013-06-13 22:49:25 +00:00
|
|
|
|
yield human_join(common_col_headers) + '.'
|
|
|
|
|
yield u''
|
|
|
|
|
|
|
|
|
|
def generate_eagerloads(cls, remaining_attrs):
|
|
|
|
|
eagerloads = []
|
|
|
|
|
for attr_name in remaining_attrs:
|
|
|
|
|
prop = getattr(cls, attr_name)
|
|
|
|
|
if isrelationship(prop) and prop.property.lazy == 'joined':
|
|
|
|
|
eagerloads.append('**' + attr_name + '**')
|
|
|
|
|
|
|
|
|
|
if eagerloads:
|
|
|
|
|
eagerloads.sort()
|
|
|
|
|
yield u'Eagerloads:'
|
|
|
|
|
yield human_join(eagerloads) + '.'
|
2012-02-12 20:30:30 +00:00
|
|
|
|
yield u''
|
|
|
|
|
|
|
|
|
|
@with_header(u'Columns')
|
|
|
|
|
def generate_columns(cls, remaining_attrs):
|
|
|
|
|
name = cls.__name__
|
|
|
|
|
for c in [c for c in cls.__table__.c if c.name not in common_columns]:
|
|
|
|
|
remaining_attrs.remove(c.name)
|
|
|
|
|
relation_name = c.name[:-3]
|
|
|
|
|
if c.name.endswith('_id') and relation_name in remaining_attrs:
|
|
|
|
|
relation = getattr(cls, relation_name)
|
2012-02-12 22:19:40 +00:00
|
|
|
|
yield column_header(c, name,
|
|
|
|
|
relation=relation, relation_name=relation_name)
|
2012-02-12 20:30:30 +00:00
|
|
|
|
remaining_attrs.remove(relation_name)
|
|
|
|
|
else:
|
|
|
|
|
yield column_header(c, name) + ':'
|
|
|
|
|
yield u''
|
|
|
|
|
yield u' ' + unicode(c.info['description'])
|
|
|
|
|
yield u''
|
|
|
|
|
|
|
|
|
|
@with_header(u'Internationalized strings')
|
|
|
|
|
def generate_strings(cls, remaining_attrs):
|
|
|
|
|
for translation_class in cls.translation_classes:
|
|
|
|
|
for c in translation_class.__table__.c:
|
|
|
|
|
if 'format' in c.info:
|
|
|
|
|
remaining_attrs.discard(c.name)
|
|
|
|
|
remaining_attrs.discard(c.name + '_map')
|
|
|
|
|
if c.name in common_columns:
|
|
|
|
|
continue
|
|
|
|
|
yield column_header(c, cls.__name__,
|
2012-02-12 22:19:40 +00:00
|
|
|
|
translation_class.__table__.name)
|
2012-02-12 20:30:30 +00:00
|
|
|
|
yield u''
|
|
|
|
|
yield u' ' + unicode(c.info['description'])
|
|
|
|
|
yield u''
|
|
|
|
|
|
|
|
|
|
@with_header(u'Relationships')
|
|
|
|
|
def generate_relationships(cls, remaining_attrs):
|
2012-06-06 01:10:37 +00:00
|
|
|
|
for attr_name in sorted(remaining_attrs):
|
2012-06-05 21:47:42 +00:00
|
|
|
|
prop = getattr(cls, attr_name)
|
2012-06-06 01:10:37 +00:00
|
|
|
|
if not isrelationship(prop):
|
|
|
|
|
continue
|
|
|
|
|
rel = prop.property
|
2012-06-05 22:09:54 +00:00
|
|
|
|
yield u'%s.\ **%s**' % (cls.__name__, attr_name)
|
|
|
|
|
class_name = u':class:`~pokedex.db.tables.%s`' % rel.mapper.class_.__name__
|
|
|
|
|
if rel.uselist:
|
|
|
|
|
class_name = u'[%s]' % class_name
|
|
|
|
|
yield u'(→ %s)' % class_name
|
|
|
|
|
if rel.doc:
|
|
|
|
|
yield u''
|
|
|
|
|
yield u' ' + unicode(rel.doc)
|
|
|
|
|
if rel.secondary is not None:
|
|
|
|
|
yield u''
|
|
|
|
|
yield ' Association table: ``%s``' % rel.secondary
|
|
|
|
|
#if rel.primaryjoin is not None:
|
|
|
|
|
# yield u''
|
|
|
|
|
# yield ' Join condition: ``%s``' % rel.primaryjoin
|
|
|
|
|
# if rel.secondaryjoin is not None:
|
|
|
|
|
# yield ' , ``%s``' % rel.secondaryjoin
|
|
|
|
|
if rel.order_by:
|
|
|
|
|
yield u''
|
|
|
|
|
yield u' '
|
|
|
|
|
yield ' Ordered by: ' + u', '.join(
|
|
|
|
|
u'``%s``' % o for o in rel.order_by)
|
|
|
|
|
yield u''
|
|
|
|
|
remaining_attrs.remove(attr_name)
|
|
|
|
|
|
|
|
|
|
@with_header(u'Association Proxies')
|
|
|
|
|
def generate_associationproxies(cls, remaining_attrs):
|
|
|
|
|
for attr_name in sorted(remaining_attrs):
|
|
|
|
|
prop = getattr(cls, attr_name)
|
|
|
|
|
if isinstance(prop, AssociationProxy):
|
2012-06-05 21:47:42 +00:00
|
|
|
|
yield u'%s.\ **%s**:' % (cls.__name__, attr_name)
|
2012-06-05 22:09:54 +00:00
|
|
|
|
yield '``{prop.remote_attr.key}`` of ``self.{prop.local_attr.key}``'.format(
|
2012-06-05 21:47:42 +00:00
|
|
|
|
prop=prop)
|
2012-06-05 22:09:54 +00:00
|
|
|
|
yield u''
|
|
|
|
|
remaining_attrs.remove(attr_name)
|
|
|
|
|
|
2012-02-12 20:30:30 +00:00
|
|
|
|
|
|
|
|
|
@with_header(u'Undocumented')
|
|
|
|
|
def generate_undocumented(cls, remaining_attrs):
|
|
|
|
|
for c in sorted([c for c in remaining_attrs if isinstance(getattr(cls, c),
|
|
|
|
|
(InstrumentedAttribute, AssociationProxy,
|
|
|
|
|
MoveEffectPropertyMap, MoveEffectProperty))]):
|
|
|
|
|
yield u''
|
|
|
|
|
yield u'%s.\ **%s**' % (cls.__name__, c)
|
|
|
|
|
remaining_attrs.remove(c)
|
|
|
|
|
|
|
|
|
|
@with_header(None)
|
|
|
|
|
def generate_other(cls, remaining_attrs):
|
|
|
|
|
for c in sorted(remaining_attrs):
|
|
|
|
|
yield u''
|
|
|
|
|
member = getattr(cls, c)
|
|
|
|
|
if callable(member):
|
|
|
|
|
yield '.. automethod:: %s.%s' % (cls.__name__, c)
|
|
|
|
|
else:
|
|
|
|
|
yield '.. autoattribute:: %s.%s' % (cls.__name__, c)
|
|
|
|
|
yield u''
|
|
|
|
|
remaining_attrs.clear()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DexTable(PyClasslike):
|
|
|
|
|
"""The actual Sphinx documentation generation whatchamacallit
|
|
|
|
|
"""
|
|
|
|
|
doc_field_types = [
|
|
|
|
|
TypedField('field', label='Fields',
|
|
|
|
|
typerolename='obj', typenames=('fieldname', 'type')),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def get_signature_prefix(self, sig):
|
|
|
|
|
return ''
|
|
|
|
|
#return u'mapped class '
|
|
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
|
section = nodes.section()
|
|
|
|
|
super_result = super(DexTable, self).run()
|
|
|
|
|
title_text = self.names[0][0]
|
|
|
|
|
section += nodes.title(text=title_text)
|
|
|
|
|
section += super_result
|
|
|
|
|
section['ids'] = ['dex-table-%s' % title_text.lower()]
|
|
|
|
|
return [section]
|
|
|
|
|
|
|
|
|
|
def before_content(self):
|
|
|
|
|
name = self.names[0][0]
|
|
|
|
|
for cls in tables.mapped_classes:
|
|
|
|
|
if name == cls.__name__:
|
|
|
|
|
break
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError('Table %s not found' % name)
|
|
|
|
|
table = cls.__table__
|
|
|
|
|
|
|
|
|
|
remaining_attrs = set(x for x in dir(cls) if not x.startswith('_'))
|
|
|
|
|
remaining_attrs.difference_update(['metadata', 'translation_classes',
|
2012-06-05 22:09:54 +00:00
|
|
|
|
'add_relationships', 'summary_column'])
|
2012-02-12 20:30:30 +00:00
|
|
|
|
for transl_class in cls.translation_classes:
|
|
|
|
|
remaining_attrs.difference_update([
|
|
|
|
|
transl_class.relation_name,
|
|
|
|
|
transl_class.relation_name + '_table',
|
|
|
|
|
transl_class.relation_name + '_local',
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
generated_content = [] # Just a list of lines!
|
|
|
|
|
|
|
|
|
|
generated_content.extend(generate_table_header(cls, remaining_attrs))
|
|
|
|
|
generated_content.extend(generate_common(cls, remaining_attrs))
|
2013-06-13 22:49:25 +00:00
|
|
|
|
generated_content.extend(generate_eagerloads(cls, remaining_attrs))
|
2012-02-12 20:30:30 +00:00
|
|
|
|
generated_content.extend(generate_columns(cls, remaining_attrs))
|
|
|
|
|
generated_content.extend(generate_strings(cls, remaining_attrs))
|
|
|
|
|
generated_content.extend(generate_relationships(cls, remaining_attrs))
|
2012-06-05 22:09:54 +00:00
|
|
|
|
generated_content.extend(generate_associationproxies(cls, remaining_attrs))
|
2012-02-12 20:30:30 +00:00
|
|
|
|
generated_content.extend(generate_undocumented(cls, remaining_attrs))
|
|
|
|
|
generated_content.extend(generate_other(cls, remaining_attrs))
|
|
|
|
|
|
|
|
|
|
generated_content.append(u'')
|
|
|
|
|
self.content = ViewList(generated_content + list(self.content))
|
|
|
|
|
return super(DexTable, self).before_content()
|
|
|
|
|
|
|
|
|
|
def get_index_text(self, modname, name_cls):
|
|
|
|
|
return '%s (mapped class)' % name_cls[0]
|
|
|
|
|
|
|
|
|
|
def setup(app):
|
|
|
|
|
app.add_directive('dex-table', DexTable)
|
|
|
|
|
|
|
|
|
|
# XXX: Specify that this depends on pokedex.db.tables ...?
|