veekun_pokedex/pokedex/doc/tabledoc.py

320 lines
11 KiB
Python
Raw Normal View History

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.util.compat import Directive, make_admonition
from sphinx.locale import _
from sphinx.domains.python import PyClasslike
from sphinx.util.docfields import Field, GroupedField, TypedField
from sphinx.ext.autodoc import ClassLevelDocumenter
from sqlalchemy import types
from sqlalchemy.orm.attributes import InstrumentedAttribute
from sqlalchemy.orm import configure_mappers
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
class dextabledoc(nodes.Admonition, nodes.Element):
pass
def visit_todo_node(self, node):
self.visit_admonition(node)
def depart_todo_node(self, node):
self.depart_admonition(node)
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]
if relation_name:
result.append(u'(%s' % c.name)
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''
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:
if len(common_col_headers) > 1:
common_col_headers[-1] = 'and ' + common_col_headers[-1]
if len(common_col_headers) > 2:
separator = u', '
else:
separator = u' '
yield u'Has'
yield separator.join(common_col_headers) + '.'
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)
yield (column_header(c, name,
relation=relation, relation_name=relation_name) + ':')
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__,
translation_class.__table__.name) + ':'
yield u''
yield u' ' + unicode(c.info['description'])
yield u''
@with_header(u'Relationships')
def generate_relationships(cls, remaining_attrs):
2012-02-12 21:43:59 +00:00
order = cls.relationship_info.get('_order', [])
def sort_key((key, value)):
try:
return 0, order.index(key)
except ValueError:
return 1, key
infos = sorted(cls.relationship_info.items(), key=sort_key)
for rel_name, info in infos:
2012-02-12 20:30:30 +00:00
if rel_name in remaining_attrs:
info = cls.relationship_info.get(rel_name)
yield u'%s.\ **%s**' % (cls.__name__, rel_name)
class_name = u':class:`~pokedex.db.tables.%s`' % info['argument'].__name__
if info.get('uselist', True):
class_name = u'[%s]' % class_name
yield u'(→ %s)' % class_name
if 'description' in info:
yield u''
yield u' ' + unicode(info['description'])
'''
if info.get('secondary') is not None:
yield u''
yield ' Association table: ``%s``' % info['secondary']
if 'primaryjoin' in info:
yield u'')
yield ' Join condition: ``%s``' % info['primaryjoin']
if 'secondaryjoin' in info:
yield ' , ``%s``' % info['secondaryjoin']
'''
if 'order_by' in info:
yield u''
try:
order = iter(info['order_by'])
except TypeError:
order = [info['order_by']]
yield u' '
yield ' Ordered by: ' + u', '.join(
u'``%s``' % o for o in order)
yield u''
remaining_attrs.remove(rel_name)
@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',
'add_relationships', 'relationship_info', 'summary_column'])
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))
generated_content.extend(generate_columns(cls, remaining_attrs))
generated_content.extend(generate_strings(cls, remaining_attrs))
generated_content.extend(generate_relationships(cls, remaining_attrs))
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 ...?