From 96499fae30be5e6e9aadb73b03da07154efedfc4 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <encukou@gmail.com>
Date: Sat, 11 Feb 2012 21:05:52 +0100
Subject: [PATCH 01/11] Allow ordering and filtering on translated texts, e.g.
 Move.name

---
 pokedex/db/multilang.py | 27 ++++++++++++++++++++++++---
 1 file changed, 24 insertions(+), 3 deletions(-)

diff --git a/pokedex/db/multilang.py b/pokedex/db/multilang.py
index a778869..dffe6a4 100644
--- a/pokedex/db/multilang.py
+++ b/pokedex/db/multilang.py
@@ -1,16 +1,37 @@
 from functools import partial
 
-from sqlalchemy.ext.associationproxy import association_proxy
+from sqlalchemy.ext.associationproxy import association_proxy, AssociationProxy
 from sqlalchemy.orm import Query, aliased, mapper, relationship, synonym
 from sqlalchemy.orm.collections import attribute_mapped_collection
 from sqlalchemy.orm.scoping import ScopedSession
 from sqlalchemy.orm.session import Session, object_session
 from sqlalchemy.schema import Column, ForeignKey, Table
-from sqlalchemy.sql.expression import and_, bindparam, select
+from sqlalchemy.sql.expression import and_, bindparam, select, exists
+from sqlalchemy.sql.operators import ColumnOperators
 from sqlalchemy.types import Integer
 
 from pokedex.db import markdown
 
+class LocalAssociationProxy(AssociationProxy, ColumnOperators):
+    """An association proxy for names in the default language
+
+    Over the regular association_proxy, this provides sorting and filtering
+    capabilities, implemented via SQL subqueries.
+    """
+    def __clause_element__(self):
+        q = select([self.remote_attr])
+        q = q.where(self.target_class.foreign_id == self.owning_class.id)
+        q = q.where(self.target_class.local_language_id == bindparam('_default_language_id'))
+        return q
+
+    def operate(self, op, *other, **kwargs):
+        q = select([self.remote_attr])
+        q = q.where(self.target_class.foreign_id == self.owning_class.id)
+        q = q.where(self.target_class.local_language_id == bindparam('_default_language_id'))
+        q = q.where(op(self.remote_attr, *other))
+        return exists(q)
+
+
 def _getset_factory_factory(column_name, string_getter):
     """Hello!  I am a factory for creating getset_factory functions for SQLA.
     I exist to avoid the closure-in-a-loop problem.
@@ -165,7 +186,7 @@ def create_translation_table(_table_name, foreign_class, relation_name,
 
         # Class.(column) -- accessor for the default language's value
         setattr(foreign_class, name,
-            association_proxy(local_relation_name, name,
+            LocalAssociationProxy(local_relation_name, name,
                     getset_factory=getset_factory))
 
         # Class.(column)_map -- accessor for the language dict

From 0cb1f8a1b83dd7c8a48c191941c11e29861a6c19 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <encukou@gmail.com>
Date: Sun, 12 Feb 2012 16:19:09 +0100
Subject: [PATCH 02/11] Make the table schema a bit more introspectable

This solves two problems: first, the relationships are now defined in
the class they apply to, rather than in a separate section of the module,
and second, their metadata -- both creation arguments and extra info such
as `description` (or, later, possibly, info for API properties) -- is
stored.
---
 pokedex/db/multilang.py |   1 +
 pokedex/db/tables.py    | 142 ++++++++++++++++++++++++----------------
 2 files changed, 88 insertions(+), 55 deletions(-)

diff --git a/pokedex/db/multilang.py b/pokedex/db/multilang.py
index dffe6a4..7609f7f 100644
--- a/pokedex/db/multilang.py
+++ b/pokedex/db/multilang.py
@@ -118,6 +118,7 @@ def create_translation_table(_table_name, foreign_class, relation_name,
 
     Translations = type(_table_name, (object,), {
         '_language_identifier': association_proxy('local_language', 'identifier'),
+        'relation_name': relation_name,
     })
 
     # Create the table object
diff --git a/pokedex/db/tables.py b/pokedex/db/tables.py
index 7726794..c36b8a6 100644
--- a/pokedex/db/tables.py
+++ b/pokedex/db/tables.py
@@ -30,7 +30,8 @@ classes in that module can be used to change the default language.
 import collections
 from functools import partial
 
-from sqlalchemy import Column, ForeignKey, MetaData, PrimaryKeyConstraint, Table, UniqueConstraint
+from sqlalchemy import (Column, ForeignKey, MetaData, PrimaryKeyConstraint,
+    Table, UniqueConstraint)
 from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
 from sqlalchemy.ext.associationproxy import association_proxy
 from sqlalchemy.orm import backref, relationship
@@ -76,6 +77,7 @@ class TableMetaclass(DeclarativeMeta):
         if hasattr(cls, '__tablename__'):
             mapped_classes.append(cls)
             cls.translation_classes = []
+        cls.relationship_info = {}
 
 metadata = MetaData()
 TableBase = declarative_base(metadata=metadata, cls=TableSuperclass, metaclass=TableMetaclass)
@@ -1400,6 +1402,69 @@ class PokemonSpecies(TableBase):
     forms_switchable = Column(Boolean, nullable=False,
         info=dict(description=u"True iff a particular individual of this species can switch beween its different forms."))
 
+    @staticmethod
+    def _add_relationships(add_relationship, **kwargs):
+        add_relationship('parent_species', PokemonSpecies,
+            primaryjoin=PokemonSpecies.evolves_from_species_id==PokemonSpecies.id,
+            remote_side=[PokemonSpecies.id],
+            backref='child_species',
+            info=dict(
+                description=u"The species from which this one evolves"))
+        add_relationship('evolutions', PokemonEvolution,
+            primaryjoin=PokemonSpecies.id==PokemonEvolution.evolved_species_id,
+            backref=backref('evolved_species',
+                innerjoin=True,
+                lazy='joined'))
+        add_relationship('flavor_text', PokemonSpeciesFlavorText,
+            order_by=PokemonSpeciesFlavorText.version_id.asc(),
+            backref='species')
+        add_relationship('growth_rate', GrowthRate,
+            innerjoin=True,
+            backref='evolution_chains')
+        add_relationship('habitat', PokemonHabitat,
+            backref='species')
+        add_relationship('color', PokemonColor,
+            innerjoin=True,
+            backref='species')
+        add_relationship('egg_groups', EggGroup,
+            secondary=PokemonEggGroup.__table__,
+            innerjoin=True,
+            order_by=PokemonEggGroup.egg_group_id.asc(),
+            backref=backref('species', order_by=Pokemon.order.asc()))
+        add_relationship('forms', PokemonForm,
+            secondary=Pokemon.__table__,
+            primaryjoin=PokemonSpecies.id==Pokemon.species_id,
+            secondaryjoin=Pokemon.id==PokemonForm.pokemon_id,
+            order_by=(PokemonForm.order.asc(),
+                PokemonForm.form_identifier.asc()))
+        add_relationship('default_form', PokemonForm,
+            secondary=Pokemon.__table__,
+            primaryjoin=and_(PokemonSpecies.id==Pokemon.species_id,
+                    Pokemon.is_default==True),
+            secondaryjoin=and_(Pokemon.id==PokemonForm.pokemon_id,
+                    PokemonForm.is_default==True),
+            uselist=False,
+            info=dict(
+                description=u"A representative form of this species"))
+        add_relationship('default_pokemon', Pokemon,
+            primaryjoin=and_(
+                PokemonSpecies.id==Pokemon.species_id,
+                Pokemon.is_default==True),
+            uselist=False, lazy='joined')
+        add_relationship('evolution_chain', EvolutionChain,
+            innerjoin=True,
+            backref=backref('species', order_by=PokemonSpecies.id.asc()))
+        add_relationship('dex_numbers', PokemonDexNumber,
+            innerjoin=True,
+            order_by=PokemonDexNumber.pokedex_id.asc(),
+            backref='species')
+        add_relationship('generation', Generation,
+            innerjoin=True,
+            backref='species')
+        add_relationship('shape', PokemonShape,
+            innerjoin=True,
+            backref='species')
+
 create_translation_table('pokemon_species_names', PokemonSpecies, 'names',
     relation_lazy='joined',
     name = Column(Unicode(20), nullable=True, index=True,
@@ -1607,6 +1672,27 @@ class VersionGroupRegion(TableBase):
 
 ### Relationships down here, to avoid dependency ordering problems
 
+def add_relationships():
+    for cls in mapped_classes:
+        try:
+            add_relationships = cls._add_relationships
+        except AttributeError:
+            pass
+        else:
+            def add_relationship(name, argument, secondary=None, **kwargs):
+                cls.relationship_info.setdefault('_order', [])
+                cls.relationship_info['_order'].append(name)
+                cls.relationship_info[name] = kwargs.pop('info', {})
+                cls.relationship_info[name].update(kwargs)
+                cls.relationship_info[name].update(dict(
+                        type='relationship',
+                        argument=argument,
+                        secondary=secondary))
+                setattr(cls, name, relationship(argument, secondary=secondary,
+                        **kwargs))
+            add_relationships(add_relationship=add_relationship)
+add_relationships()
+
 Ability.changelog = relationship(AbilityChangelog,
     order_by=AbilityChangelog.changed_in_version_group_id.desc(),
     backref=backref('ability', innerjoin=True, lazy='joined'))
@@ -2005,60 +2091,6 @@ PokemonMove.method = relationship(PokemonMoveMethod,
 PokemonStat.stat = relationship(Stat,
     innerjoin=True, lazy='joined')
 
-PokemonSpecies.parent_species = relationship(PokemonSpecies,
-    primaryjoin=PokemonSpecies.evolves_from_species_id==PokemonSpecies.id,
-    remote_side=[PokemonSpecies.id],
-    backref='child_species')
-PokemonSpecies.evolutions = relationship(PokemonEvolution,
-    primaryjoin=PokemonSpecies.id==PokemonEvolution.evolved_species_id,
-    backref=backref('evolved_species', innerjoin=True, lazy='joined'))
-PokemonSpecies.flavor_text = relationship(PokemonSpeciesFlavorText,
-    order_by=PokemonSpeciesFlavorText.version_id.asc(),
-    backref='species')
-PokemonSpecies.growth_rate = relationship(GrowthRate,
-    innerjoin=True,
-    backref='evolution_chains')
-PokemonSpecies.habitat = relationship(PokemonHabitat,
-    backref='species')
-PokemonSpecies.color = relationship(PokemonColor,
-    innerjoin=True,
-    backref='species')
-PokemonSpecies.egg_groups = relationship(EggGroup,
-    secondary=PokemonEggGroup.__table__,
-    innerjoin=True,
-    order_by=PokemonEggGroup.egg_group_id.asc(),
-    backref=backref('species', order_by=Pokemon.order.asc()))
-PokemonSpecies.forms = relationship(PokemonForm,
-    secondary=Pokemon.__table__,
-    primaryjoin=PokemonSpecies.id==Pokemon.species_id,
-    secondaryjoin=Pokemon.id==PokemonForm.pokemon_id,
-    order_by=(PokemonForm.order.asc(), PokemonForm.form_identifier.asc()))
-PokemonSpecies.default_form = relationship(PokemonForm,
-    secondary=Pokemon.__table__,
-    primaryjoin=and_(PokemonSpecies.id==Pokemon.species_id,
-            Pokemon.is_default==True),
-    secondaryjoin=and_(Pokemon.id==PokemonForm.pokemon_id,
-            PokemonForm.is_default==True),
-    uselist=False)
-PokemonSpecies.default_pokemon = relationship(Pokemon,
-    primaryjoin=and_(
-        PokemonSpecies.id==Pokemon.species_id,
-        Pokemon.is_default==True),
-    uselist=False, lazy='joined')
-PokemonSpecies.evolution_chain = relationship(EvolutionChain,
-    innerjoin=True,
-    backref=backref('species', order_by=PokemonSpecies.id.asc()))
-PokemonSpecies.dex_numbers = relationship(PokemonDexNumber,
-    innerjoin=True,
-    order_by=PokemonDexNumber.pokedex_id.asc(),
-    backref='species')
-PokemonSpecies.generation = relationship(Generation,
-    innerjoin=True,
-    backref='species')
-PokemonSpecies.shape = relationship(PokemonShape,
-    innerjoin=True,
-    backref='species')
-
 PokemonSpeciesFlavorText.version = relationship(Version, innerjoin=True, lazy='joined')
 PokemonSpeciesFlavorText.language = relationship(Language, innerjoin=True, lazy='joined')
 

From 34481e9a110fc4142b2d3e79f918f55b254a975e Mon Sep 17 00:00:00 2001
From: Petr Viktorin <encukou@gmail.com>
Date: Sun, 12 Feb 2012 21:30:30 +0100
Subject: [PATCH 03/11] Add Sphinx documentation

---
 doc/Makefile            | 146 +++++++++++++++++++
 doc/conf.py             | 230 +++++++++++++++++++++++++++++
 doc/index.rst           |  25 ++++
 doc/installing.rst      | 156 ++++++++++++++++++++
 doc/main-tables.rst     | 175 ++++++++++++++++++++++
 doc/schema.rst          |   6 +
 doc/usage.rst           | 193 +++++++++++++++++++++++++
 pokedex/doc/__init__.py |   0
 pokedex/doc/tabledoc.py | 313 ++++++++++++++++++++++++++++++++++++++++
 9 files changed, 1244 insertions(+)
 create mode 100644 doc/Makefile
 create mode 100644 doc/conf.py
 create mode 100644 doc/index.rst
 create mode 100644 doc/installing.rst
 create mode 100644 doc/main-tables.rst
 create mode 100644 doc/schema.rst
 create mode 100644 doc/usage.rst
 create mode 100644 pokedex/doc/__init__.py
 create mode 100644 pokedex/doc/tabledoc.py

diff --git a/doc/Makefile b/doc/Makefile
new file mode 100644
index 0000000..39dd586
--- /dev/null
+++ b/doc/Makefile
@@ -0,0 +1,146 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+BUILDDIR      = _build
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
+
+all: doctest
+	$(MAKE) html coverage
+
+commit:
+	mkdir -p _gh-pages
+	(cd _gh-pages && git init || true)
+	rm -rf _build/html/.git
+	cp -r _gh-pages/.git _build/html
+	(cd _build/html && git add . && git commit -m "sphinx build $$(date --rfc-3339=seconds)" || true)
+	(cd _gh-pages && git pull ../_build/html)
+
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  html       to make standalone HTML files"
+	@echo "  dirhtml    to make HTML files named index.html in directories"
+	@echo "  singlehtml to make a single large HTML file"
+	@echo "  pickle     to make pickle files"
+	@echo "  json       to make JSON files"
+	@echo "  htmlhelp   to make HTML files and a HTML help project"
+	@echo "  qthelp     to make HTML files and a qthelp project"
+	@echo "  devhelp    to make HTML files and a Devhelp project"
+	@echo "  epub       to make an epub"
+	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
+	@echo "  text       to make text files"
+	@echo "  man        to make manual pages"
+	@echo "  changes    to make an overview of all changed/added/deprecated items"
+	@echo "  linkcheck  to check all external links for integrity"
+	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+	-rm -rf $(BUILDDIR)/*
+
+html:
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+	touch $(BUILDDIR)/html/.nojekyll
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+	@echo
+	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files."
+
+json:
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+	@echo
+	@echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+	@echo
+	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
+	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pokedex.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pokedex.qhc"
+
+devhelp:
+	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+	@echo
+	@echo "Build finished."
+	@echo "To view the help file:"
+	@echo "# mkdir -p $$HOME/.local/share/devhelp/pokedex"
+	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pokedex"
+	@echo "# devhelp"
+
+epub:
+	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+	@echo
+	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+	@echo "Run \`make' in that directory to run these through (pdf)latex" \
+	      "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo "Running LaTeX files through pdflatex..."
+	make -C $(BUILDDIR)/latex all-pdf
+	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+	@echo
+	@echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+	@echo
+	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+changes:
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+	@echo
+	@echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+	@echo "Testing of doctests in the sources finished, look at the " \
+	      "results in $(BUILDDIR)/doctest/output.txt."
+
+coverage:
+	$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
+	cat $(BUILDDIR)/coverage/*.txt
diff --git a/doc/conf.py b/doc/conf.py
new file mode 100644
index 0000000..5fcb53a
--- /dev/null
+++ b/doc/conf.py
@@ -0,0 +1,230 @@
+# -*- coding: utf-8 -*-
+#
+# pokedex documentation build configuration file, created by
+# sphinx-quickstart on Tue Apr 12 17:43:05 2011.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os
+
+reload(sys)
+sys.setdefaultencoding("UTF-8")
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration -----------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = [
+        'sphinx.ext.autodoc',
+        'sphinx.ext.doctest',
+        'sphinx.ext.todo',
+        'sphinx.ext.pngmath',
+        'sphinx.ext.intersphinx',
+        #'sphinx.ext.viewcode',
+        'sphinx.ext.coverage',
+        'pokedex.doc.tabledoc',
+    ]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+source_encoding = 'utf-8'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'pokedex'
+copyright = u'2011, Alex Munroe (Eevee)'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '0.1'
+# The full version, including alpha/beta/rc tags.
+release = '0.1'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+intersphinx_mapping = {'sqlalchemy': ('http://www.sqlalchemy.org/docs', None)}
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+html_show_copyright = False
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'pokedexdoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+  ('index', 'pokedex.tex', u'Pokedex Documentation',
+   u'veekun', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    ('index', 'pokedex', u'Pokedex Documentation',
+     [u'veekun'], 1)
+]
diff --git a/doc/index.rst b/doc/index.rst
new file mode 100644
index 0000000..a525f6f
--- /dev/null
+++ b/doc/index.rst
@@ -0,0 +1,25 @@
+.. pokedex documentation master file, created by
+   sphinx-quickstart on Tue Apr 12 17:43:05 2011.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+The pokedex documentation
+=========================
+
+Jump right in!
+
+Contents:
+
+.. toctree::
+   :maxdepth: 2
+
+   installing
+   usage
+   schema
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`search`
+
diff --git a/doc/installing.rst b/doc/installing.rst
new file mode 100644
index 0000000..44b94a9
--- /dev/null
+++ b/doc/installing.rst
@@ -0,0 +1,156 @@
+Installing the pokedex library
+==============================
+
+Quick startup with Ubuntu/Debian-like systems
+---------------------------------------------
+
+Run the following from an empty directory::
+
+    $ sudo apt-get install git python python-pip python-sqlalchemy
+    $ git clone git://github.com/veekun/pokedex.git
+    $ pip install -E env -e pokedex
+    $ source env/bin/activate
+    (env)$ pokedex setup -v
+    (env)$ pokedex lookup eevee
+
+If it all goes smoothly, you can now use ``env/bin/pokedex``, the command-line
+tool, and ``env/bin/python``, a Python interpreter configured to use the
+pokedex library.
+
+That is all you need. Feel free to skip the rest of this chapter if you're not
+interested in the details.
+
+Prerequisites
+-------------
+
+Linux
+^^^^^
+
+Ubuntu/Debian users should run the following::
+
+    $ sudo apt-get install git python python-pip
+
+With other Linuxes, install the packages for git, python (2.6 or 2.7,
+*not* 3.x), and python-pip.
+
+If you succeeded, skip the Detailed instructions.
+
+Detailed instructions
+^^^^^^^^^^^^^^^^^^^^^
+
+You should know what a command line is and how to work with it.
+The here we assume you're using Linux [#]_, if that's not the case, make
+sure you have enough computer knowledge to translate the instructions to your
+operating system.
+
+Pokedex is distributed via Git_. So, get Git.
+
+You will also need Python_ 2; the language pokedex is written in. Be sure to get
+version **2.6** or **2.7**. Pokedex does not work with Python 3.x yet, and it
+most likely won't work with 2.5 or earlier.
+
+Next, get pip_, a tool to install Python packages. Experts can use another
+tool, of course.
+
+Make sure git and pip are on your path.
+
+Optionally you can install SQLAlchemy_, `Python markdown`_, Whoosh_,
+or construct_. If you don't, pip will atuomatically download and install a copy
+for you, but some are pretty big so you might want to install it system-wide.
+(Unfortunately, many distros have outdated versions of these libraries, so pip
+will install pokedex's own copy anyway.)
+
+Getting and installing pokedex
+------------------------------
+
+Run the following from an empty directory::
+
+    $ git clone git://git.veekun.com/pokedex.git
+    $ pip install -E env -e pokedex
+
+This will give you two directories: pokedex (containing the source code and
+data), and env (a virtualenv_).
+
+In env/bin, there are three interesting files:
+
+* pokedex: The pokedex program
+* python: A copy of Python that knows about pokedex and its prerequisites.
+* activate: Typing ``source env/bin/activate`` in a shell will put
+  pokedex and our bin/python on the $PATH, and generally set things up to work
+  with them. Your prompt will change to let you know of this. You can end such
+  a session by typing ``deactivate``.
+
+This documentation will assume that you've activated the virtualenv, so
+``pokedex`` means ``env/bin/pokedex``.
+
+Advanced
+^^^^^^^^
+
+You can of course install into an existing virtualenv, by either using its pip
+and leaving out the ``-E env``, or running the setup script directly::
+
+    (anotherenv)$ cd pokedex
+    (anotherenv)pokedex$ python setup.py develop
+
+It is also possible to install pokedex system-wide. There are problems with
+that. Don't do it. The only time you need ``sudo`` is for getting the
+prerequisites.
+
+Loading the database
+--------------------
+
+Before you can do anything useful with pokedex, you need to load the database::
+
+    $ pokedex setup -v
+
+This will load the data into a default SQLite database and create a default
+Whoosh index.
+
+Advanced
+^^^^^^^^
+
+If you want to use another database, make sure you have the corresponding
+`SQLAlchemy engine`_ for it and either use the ``-e`` switch, (e.g.
+``-e postgresql://@/pokedex``), or set the ``POKEDEX_DB_ENGINE`` environment
+variable.
+
+To use another lookup index directory, specify it with ``-i`` or the
+``POKEDEX_INDEX_DIR`` variable.
+
+Make sure you always use the same options whenever you use pokedex.
+
+If you're confused about what pokedex thinks its settings are, check
+``pokedex status``.
+
+See ``pokedex help`` for even more options.
+
+All done
+--------
+
+To verify that all went smoothly, check that the pokedex tool finds your
+favorite pokémon::
+
+    $ pokedex lookup eevee
+
+Yes, that was a bit anti-climatic. The command-line tool doesn't do much,
+currently.
+
+
+
+
+
+
+.. _Git: http://git-scm.com/
+.. _Python: http://www.python.org/
+.. _pip: http://pypi.python.org/pypi/pip
+.. _SQLAlchemy: www.sqlalchemy.org/
+.. _`Python markdown`: http://www.freewisdom.org/projects/python-markdown/
+.. _Whoosh: http://whoosh.ca/
+.. _construct: pypi.python.org/pypi/construct
+.. _virtualenv: http://www.virtualenv.org/en/latest/
+.. _`SQLAlchemy engine`: http://www.sqlalchemy.org/docs/core/engines.html
+
+.. rubric:: Footnotes
+.. [#] If you write instructions for another OS, well be happy to include them
+    here. The reason your OS is not listed here is because the author doesn't
+    use it, so naturally he can't write instructions for it.
diff --git a/doc/main-tables.rst b/doc/main-tables.rst
new file mode 100644
index 0000000..7cef777
--- /dev/null
+++ b/doc/main-tables.rst
@@ -0,0 +1,175 @@
+The pokédex tables
+==================
+
+.. module:: pokedex.db.tables
+
+The :mod:`pokedex.db.tables` module defines all of the tables in the Pokédex.
+They are all defined with SQLAlchemy's
+:mod:`~sqlalchemy.ext.declarative` extension.
+
+To introspect the tables programmatically, you can use the following:
+
+.. data:: mapped_classes
+
+    A list of all the classes you see below.
+
+.. data:: metadata
+
+    The SQLAlchemy :class:`~sqlalchemy.schema.MetaData` containing all the
+    tables.
+
+Each of the classes has a ``translation_classes`` attribute: a potentially
+empty list of translation classes. See :mod:`pokedex.db.multilang` for how
+these work.
+
+Many tables have these columns:
+
+- **id**: An integer primary key. Sometimes it's semantically meaningful, most
+  often it isn't.
+- **identifier**: A string identifier of the class, and the preferred way to
+  access individual items.
+- **name**: A name (uses the multilang functionality)
+
+Pokémon
+-------
+
+.. dex-table:: PokemonSpecies
+.. dex-table:: Pokemon
+.. dex-table:: PokemonForm
+.. dex-table:: EvolutionChain
+.. dex-table:: PokemonEvolution
+
+Moves
+-----
+
+.. dex-table:: Move
+.. dex-table:: MoveEffect
+.. dex-table:: MoveMeta
+
+Items
+-----
+
+.. dex-table:: Item
+.. dex-table:: Berry
+
+Types
+-----
+
+.. dex-table:: Type
+
+Abilities
+---------
+
+.. dex-table:: Ability
+
+Language
+--------
+
+.. dex-table:: Language
+
+Version stuff
+-------------
+
+.. dex-table:: Generation
+.. dex-table:: VersionGroup
+.. dex-table:: Version
+.. dex-table:: Pokedex
+.. dex-table:: Region
+
+Encounters
+----------
+
+.. dex-table:: Location
+.. dex-table:: LocationArea
+.. dex-table:: LocationAreaEncounterRate
+.. dex-table:: Encounter
+.. dex-table:: EncounterCondition
+.. dex-table:: EncounterConditionValue
+.. dex-table:: EncounterMethod
+.. dex-table:: EncounterSlot
+
+
+Contests
+--------
+
+.. dex-table:: ContestCombo
+.. dex-table:: ContestEffect
+.. dex-table:: SuperContestCombo
+.. dex-table:: SuperContestEffect
+
+Enum tables
+-----------
+
+.. dex-table:: BerryFirmness
+.. dex-table:: ContestType
+.. dex-table:: EggGroup
+.. dex-table:: EvolutionTrigger
+.. dex-table:: GrowthRate
+.. dex-table:: ItemCategory
+.. dex-table:: ItemFlingEffect
+.. dex-table:: ItemPocket
+.. dex-table:: MoveBattleStyle
+.. dex-table:: MoveDamageClass
+.. dex-table:: MoveMetaAilment
+.. dex-table:: MoveMetaCategory
+.. dex-table:: MoveTarget
+.. dex-table:: Nature
+.. dex-table:: PokemonColor
+.. dex-table:: PokemonMoveMethod
+.. dex-table:: PokemonShape
+.. dex-table:: Stat
+
+Changelogs
+----------
+
+.. dex-table:: AbilityChangelog
+.. dex-table:: MoveEffectChangelog
+.. dex-table:: MoveChangelog
+
+Flavor text
+-----------
+
+.. dex-table:: ItemFlavorText
+.. dex-table:: AbilityFlavorText
+.. dex-table:: MoveFlavorText
+.. dex-table:: PokemonSpeciesFlavorText
+
+Association tables
+------------------
+
+.. dex-table:: BerryFlavor
+.. dex-table:: EncounterConditionValueMap
+.. dex-table:: ItemFlag
+.. dex-table:: ItemFlagMap
+.. dex-table:: Machine
+.. dex-table:: MoveFlag
+.. dex-table:: MoveFlagMap
+.. dex-table:: MoveMetaStatChange
+.. dex-table:: NatureBattleStylePreference
+.. dex-table:: NaturePokeathlonStat
+.. dex-table:: PokeathlonStat
+.. dex-table:: PokemonAbility
+.. dex-table:: PokemonEggGroup
+.. dex-table:: PokemonFormPokeathlonStat
+.. dex-table:: PokemonHabitat
+.. dex-table:: PokemonMove
+.. dex-table:: PokemonStat
+.. dex-table:: PokemonItem
+.. dex-table:: PokemonType
+.. dex-table:: TypeEfficacy
+.. dex-table:: VersionGroupRegion
+
+Index maps
+----------
+
+.. dex-table:: ItemGameIndex
+.. dex-table:: LocationGameIndex
+.. dex-table:: PokemonDexNumber
+.. dex-table:: PokemonGameIndex
+
+Mics tables
+-----------
+
+.. dex-table:: Experience
+.. dex-table:: StatHint
+
diff --git a/doc/schema.rst b/doc/schema.rst
new file mode 100644
index 0000000..48db3d7
--- /dev/null
+++ b/doc/schema.rst
@@ -0,0 +1,6 @@
+The database schema
+===================
+
+.. toctree::
+
+    main-tables
diff --git a/doc/usage.rst b/doc/usage.rst
new file mode 100644
index 0000000..b4a6ef5
--- /dev/null
+++ b/doc/usage.rst
@@ -0,0 +1,193 @@
+Using pokedex
+=============
+
+The pokédex is, first and foremost, a Python library. To get the most of it,
+you'll need to learn `Python`_ and `SQLAlchemy`_.
+
+Here is a small example of using pokedex:
+
+.. testcode::
+
+    from pokedex.db import connect, tables, util
+    session = connect()
+    pokemon = util.get(session, tables.PokemonSpecies, 'bulbasaur')
+    print u'{0.name}, the {0.genus} Pokemon'.format(pokemon)
+
+Running this will give you some Bulbasaur info:
+
+.. testoutput::
+
+    Bulbasaur, the Seed Pokemon
+
+Connecting
+----------
+
+To get information out of the Pokédex, you will need to create a
+:class:`Session <pokedex.db.multilang.MultilangSession>`. To do that, use
+:func:`pokedex.db.connect`. For simple uses, you don't need to give it any
+arguments: it the database that ``pokedex load`` fills up by default. If you
+need to select another database, give its URI as the first argument.
+
+The object :func:`~pokedex.db.connect` gives you is actually a
+:class:`SQLAlchemy session <sqlalchemy.orm.session.Session>`, giving you the
+full power of SQLAlchemy for working with the data. We'll cover some basics
+here, but if you intend to do some serious work, do read SQLAlchemy's docs.
+
+Pokédex tables
+--------------
+
+Data in the pokédex is organized in tables, defined in
+:mod:`pokedex.db.tables`.
+There is quite a few or them. To get you started, here are a few common ones:
+
+* :class:`~pokedex.db.tables.PokemonSpecies`
+* :class:`~pokedex.db.tables.Move`
+* :class:`~pokedex.db.tables.Item`
+* :class:`~pokedex.db.tables.Type`
+
+Getting things
+--------------
+
+If you know what you want from the pokédex, you can use the
+:func:`pokedex.db.util.get` function. It looks up a thing in a table, based on
+its identifier, name, or ID, and returns it.
+
+.. testcode::
+
+    def print_pokemon(pokemon):
+        print u'{0.name}, the {0.genus} Pokemon'.format(pokemon)
+
+    print_pokemon(util.get(session, tables.PokemonSpecies, identifier='eevee'))
+    print_pokemon(util.get(session, tables.PokemonSpecies, name=u'Ho-Oh'))
+    print_pokemon(util.get(session, tables.PokemonSpecies, id=50))
+
+    def print_item(item):
+        print u'{0.name}: ${0.cost}'.format(item)
+
+    print_item(util.get(session, tables.Item, identifier='great-ball'))
+    print_item(util.get(session, tables.Item, name='Potion'))
+    print_item(util.get(session, tables.Item, id=30))
+
+.. testoutput::
+
+    Eevee, the Evolution Pokemon
+    Ho-Oh, the Rainbow Pokemon
+    Diglett, the Mole Pokemon
+    Great Ball: $600
+    Potion: $300
+    Fresh Water: $200
+
+Querying
+--------
+
+So, how do you get data from the session? You use the session's
+:meth:`~sqlalchemy.orm.session.Session.query` method, and give it a pokédex
+Table as an argument. This will give you a :class:`SQLAlchemy query
+<sqlalchemy.orm.query.Query>`.
+
+Ordering
+^^^^^^^^
+
+As always with SQL, you should not rely on query results being in some
+particular order – unless you have ordered the query first. This means that
+you'll want to sort just about every query you will make.
+
+For example, you can get a list of all pokémon species, sorted by their
+:attr:`~pokedex.db.tables.PokemonSpecies.id`, like so:
+
+.. testcode::
+
+    for pokemon in session.query(tables.PokemonSpecies).order_by(tables.PokemonSpecies.id):
+        print pokemon.name
+
+.. testoutput::
+
+    Bulbasaur
+    Ivysaur
+    Venusaur
+    Charmander
+    Charmeleon
+    ...
+    Keldeo
+    Meloetta
+    Genesect
+
+Or to order by :attr:`~pokedex.db.tables.PokemonSpecies.name`:
+
+.. testcode::
+
+    for pokemon in session.query(tables.PokemonSpecies).order_by(tables.PokemonSpecies.name):
+        print pokemon.name
+
+.. testoutput::
+
+        Abomasnow
+        ...
+        Zweilous
+
+
+Filtering
+^^^^^^^^^
+
+Another major operation on queries is filtering, using the query's
+:meth:`~sqlalchemy.orm.query.Query.filter` or
+:meth:`~sqlalchemy.orm.query.Query.filter_by` methods:
+
+.. testcode::
+
+    for move in session.query(tables.Move).filter(tables.Move.power > 200):
+        print move.name
+
+.. testoutput::
+
+    Explosion
+
+Joining
+^^^^^^^
+
+The final operation we'll cover here is joining other tables to the query,
+using the query's :meth:`~sqlalchemy.orm.query.Query.join`.
+You will usually want to join on a relationship, such as in the following
+example:
+
+.. testcode::
+
+    query = session.query(tables.Move)
+    query = query.join(tables.Move.type)
+    query = query.filter(tables.Type.identifier == 'grass')
+    query = query.filter(tables.Move.power >= 100)
+    query = query.order_by(tables.Move.power)
+    query = query.order_by(tables.Move.name)
+
+    print 'The most powerful Grass-type moves:'
+    for move in query:
+        print u'{0.name} ({0.power})'.format(move)
+
+.. testoutput::
+
+    The most powerful Grass-type moves:
+    Petal Dance (120)
+    Power Whip (120)
+    Seed Flare (120)
+    SolarBeam (120)
+    Wood Hammer (120)
+    Leaf Storm (140)
+    Frenzy Plant (150)
+
+That concludes our brief tutorial.
+If you need to do more, consult the `SQLAlchemy documentation`_.
+
+API documentation
+-----------------
+
+.. autofunction:: pokedex.db.connect
+
+    See :class:`sqlalchemy.orm.session.Session` for more documentation on the
+    returned object.
+
+.. autofunction:: pokedex.db.util.get
+
+
+.. _Python: http://www.python.org
+.. _SQLAlchemy: http://www.sqlalchemy.org
+.. _`SQLAlchemy documentation`: http://www.sqlalchemy.org/docs/orm/tutorial.html
diff --git a/pokedex/doc/__init__.py b/pokedex/doc/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pokedex/doc/tabledoc.py b/pokedex/doc/tabledoc.py
new file mode 100644
index 0000000..74287ea
--- /dev/null
+++ b/pokedex/doc/tabledoc.py
@@ -0,0 +1,313 @@
+# 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):
+    for rel_name in [c for c in cls.relationship_info.get('_order', [])
+            if c in remaining_attrs]:
+        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 ...?

From 949ff883ea83d71ad382e547135686768be5501a Mon Sep 17 00:00:00 2001
From: Petr Viktorin <encukou@gmail.com>
Date: Sun, 12 Feb 2012 22:43:59 +0100
Subject: [PATCH 04/11] Autodoc for backrefs

---
 pokedex/db/tables.py    | 23 +++++++++++++++++++----
 pokedex/doc/tabledoc.py | 10 ++++++++--
 2 files changed, 27 insertions(+), 6 deletions(-)

diff --git a/pokedex/db/tables.py b/pokedex/db/tables.py
index c36b8a6..57ca1bd 100644
--- a/pokedex/db/tables.py
+++ b/pokedex/db/tables.py
@@ -1407,9 +1407,9 @@ class PokemonSpecies(TableBase):
         add_relationship('parent_species', PokemonSpecies,
             primaryjoin=PokemonSpecies.evolves_from_species_id==PokemonSpecies.id,
             remote_side=[PokemonSpecies.id],
-            backref='child_species',
-            info=dict(
-                description=u"The species from which this one evolves"))
+            backref=backref('child_species',
+                info=dict(description=u"The species to which this one evolves")),
+            info=dict(description=u"The species from which this one evolves"))
         add_relationship('evolutions', PokemonEvolution,
             primaryjoin=PokemonSpecies.id==PokemonEvolution.evolved_species_id,
             backref=backref('evolved_species',
@@ -1682,12 +1682,27 @@ def add_relationships():
             def add_relationship(name, argument, secondary=None, **kwargs):
                 cls.relationship_info.setdefault('_order', [])
                 cls.relationship_info['_order'].append(name)
-                cls.relationship_info[name] = kwargs.pop('info', {})
+                cls.relationship_info[name] = info = kwargs.pop('info', {})
                 cls.relationship_info[name].update(kwargs)
                 cls.relationship_info[name].update(dict(
                         type='relationship',
                         argument=argument,
                         secondary=secondary))
+                the_backref = kwargs.get('backref')
+                if isinstance(the_backref, basestring):
+                    the_backref = backref(the_backref)
+                if the_backref:
+                    # XXX: This line exploits a SQLA implementation detail,
+                    # after all relationships are converted we should make our
+                    # own backref wrapper with known semantics.
+                    backref_name, backref_kwargs = the_backref
+
+                    backref_info = dict(
+                            type='backref',
+                            argument=cls,
+                            secondary=secondary)
+                    backref_info.update(backref_kwargs.pop('info', {}))
+                    argument.relationship_info[backref_name] = backref_info
                 setattr(cls, name, relationship(argument, secondary=secondary,
                         **kwargs))
             add_relationships(add_relationship=add_relationship)
diff --git a/pokedex/doc/tabledoc.py b/pokedex/doc/tabledoc.py
index 74287ea..ee92040 100644
--- a/pokedex/doc/tabledoc.py
+++ b/pokedex/doc/tabledoc.py
@@ -194,8 +194,14 @@ def generate_strings(cls, remaining_attrs):
 
 @with_header(u'Relationships')
 def generate_relationships(cls, remaining_attrs):
-    for rel_name in [c for c in cls.relationship_info.get('_order', [])
-            if c in remaining_attrs]:
+    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:
         if rel_name in remaining_attrs:
             info = cls.relationship_info.get(rel_name)
             yield u'%s.\ **%s**' % (cls.__name__, rel_name)

From ef3fb2f5364c4edb0fbd0ab5973138891d3c170e Mon Sep 17 00:00:00 2001
From: Petr Viktorin <encukou@gmail.com>
Date: Sun, 12 Feb 2012 21:50:13 +0100
Subject: [PATCH 05/11] Move relationships to _add_relationships calls: Pokemon

---
 pokedex/db/tables.py | 242 +++++++++++++++++++++----------------------
 1 file changed, 120 insertions(+), 122 deletions(-)

diff --git a/pokedex/db/tables.py b/pokedex/db/tables.py
index 57ca1bd..a955722 100644
--- a/pokedex/db/tables.py
+++ b/pokedex/db/tables.py
@@ -325,6 +325,25 @@ class Encounter(TableBase):
     max_level = Column(Integer, nullable=False, autoincrement=False,
         info=dict(description=u"The maxmum level of the encountered Pokémon"))
 
+    @staticmethod
+    def _add_relationships(add_relationship, add_association_proxy, **kwargs):
+        add_relationship('condition_value_map', EncounterConditionValueMap,
+            backref='encounter')
+        add_association_proxy('condition_values',
+                'condition_value_map', 'condition_value')
+        add_relationship('location_area', LocationArea,
+            innerjoin=True, lazy='joined',
+            backref='encounters')
+        add_relationship('pokemon', Pokemon,
+            innerjoin=True, lazy='joined',
+            backref='encounters')
+        add_relationship('version', Version,
+            innerjoin=True, lazy='joined',
+            backref='encounters')
+        add_relationship('slot', EncounterSlot,
+            innerjoin=True, lazy='joined',
+            backref='encounters')
+
 class EncounterCondition(TableBase):
     u"""A conditions in the game world that affects Pokémon encounters, such as time of day.
     """
@@ -1082,6 +1101,62 @@ class Pokemon(TableBase):
     is_default = Column(Boolean, nullable=False, index=True,
         info=dict(description=u'Set for exactly one pokemon used as the default for each species.'))
 
+    @staticmethod
+    def _add_relationships(add_relationship, **kwargs):
+        add_relationship('all_abilities', Ability,
+            secondary=PokemonAbility.__table__,
+            order_by=PokemonAbility.slot.asc(),
+            innerjoin=True,
+            backref=backref('all_pokemon', order_by=Pokemon.order.asc()),
+            info=dict(
+                description=u"All abilities the Pokémon can have, including the Hidden Ability"))
+        add_relationship('abilities', Ability,
+            secondary=PokemonAbility.__table__,
+            primaryjoin=and_(
+                Pokemon.id == PokemonAbility.pokemon_id,
+                PokemonAbility.is_dream == False),
+            innerjoin=True,
+            order_by=PokemonAbility.slot.asc(),
+            backref=backref('pokemon', order_by=Pokemon.order.asc()),
+            info=dict(
+                description=u"Abilities the Pokémon can have in the wild"))
+        add_relationship('dream_ability', Ability,
+            secondary=PokemonAbility.__table__,
+            primaryjoin=and_(
+                Pokemon.id == PokemonAbility.pokemon_id,
+                PokemonAbility.is_dream == True),
+            uselist=False,
+            backref=backref('dream_pokemon', order_by=Pokemon.order),
+            info=dict(
+                description=u"The Pokémon's Hidden Ability"))
+        add_relationship('forms', PokemonForm,
+            primaryjoin=Pokemon.id==PokemonForm.pokemon_id,
+            order_by=(PokemonForm.order.asc(),
+                PokemonForm.form_identifier.asc()))
+        add_relationship('default_form', PokemonForm,
+            primaryjoin=and_(
+                Pokemon.id==PokemonForm.pokemon_id,
+                PokemonForm.is_default==True),
+            uselist=False,
+            lazy='joined',
+            info=dict(description=u"A representative form of this pokémon"))
+        add_relationship('items', PokemonItem,
+            backref='pokemon',
+            info=dict(
+                description=u"Info about items this pokémon holds in the wild"))
+        add_relationship('stats', PokemonStat,
+            innerjoin=True,
+            order_by=PokemonStat.stat_id.asc(),
+            backref='pokemon')
+        add_relationship('species', PokemonSpecies,
+            innerjoin=True,
+            backref='pokemon')
+        add_relationship('types', Type,
+            secondary=PokemonType.__table__,
+            innerjoin=True, lazy='joined',
+            order_by=PokemonType.slot.asc(),
+            backref=backref('pokemon', order_by=Pokemon.order))
+
     @property
     def name(self):
         u"""Returns a name for this Pokémon, specifiying the form iff it
@@ -1216,6 +1291,27 @@ class PokemonEvolution(TableBase):
     trade_species_id = Column(Integer, ForeignKey('pokemon_species.id'), nullable=True,
         info=dict(description=u"The ID of the species for which this one must be traded."))
 
+    @staticmethod
+    def _add_relationships(add_relationship, **kwargs):
+        add_relationship('trigger', EvolutionTrigger,
+            innerjoin=True, lazy='joined',
+            backref='evolutions')
+        add_relationship('trigger_item', Item,
+            primaryjoin=PokemonEvolution.trigger_item_id==Item.id,
+            backref='triggered_evolutions')
+        add_relationship('held_item', Item,
+            primaryjoin=PokemonEvolution.held_item_id==Item.id,
+            backref='required_for_evolutions')
+        add_relationship('location', Location,
+            backref='triggered_evolutions')
+        add_relationship('known_move', Move,
+            backref='triggered_evolutions')
+        add_relationship('party_species', PokemonSpecies,
+            primaryjoin=PokemonEvolution.party_species_id==PokemonSpecies.id,
+            backref='triggered_evolutions')
+        add_relationship('trade_species', PokemonSpecies,
+            primaryjoin=PokemonEvolution.trade_species_id==PokemonSpecies.id)
+
 class PokemonForm(TableBase):
     u"""An individual form of a Pokémon.  This includes *every* variant (except
     color differences) of every Pokémon, regardless of how the games treat
@@ -1327,6 +1423,26 @@ class PokemonMove(TableBase):
         {},
     )
 
+    @staticmethod
+    def _add_relationships(add_relationship, **kwargs):
+        add_relationship('pokemon', Pokemon,
+            innerjoin=True, lazy='joined',
+            backref='pokemon_moves')
+        add_relationship('version_group', VersionGroup,
+            innerjoin=True, lazy='joined')
+        add_relationship('machine', Machine,
+            primaryjoin=and_(
+                Machine.version_group_id==PokemonMove.version_group_id,
+                Machine.move_id==PokemonMove.move_id),
+            foreign_keys=[Machine.version_group_id, Machine.move_id],
+            uselist=False,
+            backref='pokemon_moves')
+        add_relationship('move', Move,
+            innerjoin=True, lazy='joined',
+            backref='pokemon_moves')
+        add_relationship('method', PokemonMoveMethod,
+            innerjoin=True, lazy='joined')
+
 class PokemonMoveMethod(TableBase):
     u"""A method a move can be learned by, such as "Level up" or "Tutor".
     """
@@ -1412,9 +1528,7 @@ class PokemonSpecies(TableBase):
             info=dict(description=u"The species from which this one evolves"))
         add_relationship('evolutions', PokemonEvolution,
             primaryjoin=PokemonSpecies.id==PokemonEvolution.evolved_species_id,
-            backref=backref('evolved_species',
-                innerjoin=True,
-                lazy='joined'))
+            backref=backref('evolved_species', innerjoin=True, lazy='joined'))
         add_relationship('flavor_text', PokemonSpeciesFlavorText,
             order_by=PokemonSpeciesFlavorText.version_id.asc(),
             backref='species')
@@ -1444,8 +1558,7 @@ class PokemonSpecies(TableBase):
             secondaryjoin=and_(Pokemon.id==PokemonForm.pokemon_id,
                     PokemonForm.is_default==True),
             uselist=False,
-            info=dict(
-                description=u"A representative form of this species"))
+            info=dict(description=u"A representative form of this species"))
         add_relationship('default_pokemon', Pokemon,
             primaryjoin=and_(
                 PokemonSpecies.id==Pokemon.species_id,
@@ -1683,8 +1796,8 @@ def add_relationships():
                 cls.relationship_info.setdefault('_order', [])
                 cls.relationship_info['_order'].append(name)
                 cls.relationship_info[name] = info = kwargs.pop('info', {})
-                cls.relationship_info[name].update(kwargs)
-                cls.relationship_info[name].update(dict(
+                info.update(kwargs)
+                info.update(dict(
                         type='relationship',
                         argument=argument,
                         secondary=secondary))
@@ -1750,22 +1863,6 @@ ContestCombo.second = relationship(Move,
     backref='contest_combo_second')
 
 
-Encounter.condition_value_map = relationship(EncounterConditionValueMap,
-    backref='encounter')
-Encounter.condition_values = association_proxy('condition_value_map', 'condition_value')
-Encounter.location_area = relationship(LocationArea,
-    innerjoin=True, lazy='joined',
-    backref='encounters')
-Encounter.pokemon = relationship(Pokemon,
-    innerjoin=True, lazy='joined',
-    backref='encounters')
-Encounter.version = relationship(Version,
-    innerjoin=True, lazy='joined',
-    backref='encounters')
-Encounter.slot = relationship(EncounterSlot,
-    innerjoin=True, lazy='joined',
-    backref='encounters')
-
 EncounterConditionValue.condition = relationship(EncounterCondition,
     innerjoin=True, lazy='joined',
     backref='values')
@@ -1990,91 +2087,9 @@ Pokedex.version_groups = relationship(VersionGroup,
     backref='pokedex')
 
 
-Pokemon.all_abilities = relationship(Ability,
-    secondary=PokemonAbility.__table__,
-    order_by=PokemonAbility.slot.asc(),
-    innerjoin=True,
-    backref=backref('all_pokemon',
-        order_by=Pokemon.order.asc(),
-    ),
-)
-Pokemon.abilities = relationship(Ability,
-    secondary=PokemonAbility.__table__,
-    primaryjoin=and_(
-        Pokemon.id == PokemonAbility.pokemon_id,
-        PokemonAbility.is_dream == False,
-    ),
-    innerjoin=True,
-    order_by=PokemonAbility.slot.asc(),
-    backref=backref('pokemon',
-        order_by=Pokemon.order.asc(),
-    ),
-)
-Pokemon.dream_ability = relationship(Ability,
-    secondary=PokemonAbility.__table__,
-    primaryjoin=and_(
-        Pokemon.id == PokemonAbility.pokemon_id,
-        PokemonAbility.is_dream == True,
-    ),
-    uselist=False,
-    backref=backref('dream_pokemon',
-        order_by=Pokemon.order,
-    ),
-)
-Pokemon.forms = relationship(PokemonForm,
-    primaryjoin=Pokemon.id==PokemonForm.pokemon_id,
-    order_by=(PokemonForm.order.asc(), PokemonForm.form_identifier.asc()))
-Pokemon.default_form = relationship(PokemonForm,
-    primaryjoin=and_(
-        Pokemon.id==PokemonForm.pokemon_id,
-        PokemonForm.is_default==True),
-    uselist=False, lazy='joined')
-Pokemon.items = relationship(PokemonItem,
-    backref='pokemon')
-Pokemon.stats = relationship(PokemonStat,
-    innerjoin=True,
-    order_by=PokemonStat.stat_id.asc(),
-    backref='pokemon')
-Pokemon.species = relationship(PokemonSpecies,
-    innerjoin=True,
-    backref='pokemon')
-Pokemon.types = relationship(Type,
-    secondary=PokemonType.__table__,
-    innerjoin=True, lazy='joined',
-    order_by=PokemonType.slot.asc(),
-    backref=backref('pokemon', order_by=Pokemon.order))
-
 PokemonDexNumber.pokedex = relationship(Pokedex,
     innerjoin=True, lazy='joined')
 
-PokemonEvolution.trigger = relationship(EvolutionTrigger,
-    innerjoin=True, lazy='joined',
-    backref='evolutions')
-PokemonEvolution.trigger_item = relationship(Item,
-    primaryjoin=PokemonEvolution.trigger_item_id==Item.id,
-    backref='triggered_evolutions')
-PokemonEvolution.held_item = relationship(Item,
-    primaryjoin=PokemonEvolution.held_item_id==Item.id,
-    backref='required_for_evolutions')
-PokemonEvolution.location = relationship(Location,
-    backref='triggered_evolutions')
-PokemonEvolution.known_move = relationship(Move,
-    backref='triggered_evolutions')
-PokemonEvolution.party_species = relationship(PokemonSpecies,
-    primaryjoin=PokemonEvolution.party_species_id==PokemonSpecies.id,
-    backref='triggered_evolutions')
-PokemonEvolution.trade_species = relationship(PokemonSpecies,
-    primaryjoin=PokemonEvolution.trade_species_id==PokemonSpecies.id)
-
-PokemonForm.pokemon = relationship(Pokemon,
-    primaryjoin=PokemonForm.pokemon_id==Pokemon.id,
-    innerjoin=True, lazy='joined')
-PokemonForm.species = association_proxy('pokemon', 'species')
-PokemonForm.version_group = relationship(VersionGroup,
-    innerjoin=True)
-PokemonForm.pokeathlon_stats = relationship(PokemonFormPokeathlonStat,
-    order_by=PokemonFormPokeathlonStat.pokeathlon_stat_id,
-    backref='pokemon_form')
 
 PokemonFormPokeathlonStat.pokeathlon_stat = relationship(PokeathlonStat,
     innerjoin=True, lazy='joined')
@@ -2085,23 +2100,6 @@ PokemonItem.item = relationship(Item,
 PokemonItem.version = relationship(Version,
     innerjoin=True, lazy='joined')
 
-PokemonMove.pokemon = relationship(Pokemon,
-    innerjoin=True, lazy='joined',
-    backref='pokemon_moves')
-PokemonMove.version_group = relationship(VersionGroup,
-    innerjoin=True, lazy='joined')
-PokemonMove.machine = relationship(Machine,
-    primaryjoin=and_(
-        Machine.version_group_id==PokemonMove.version_group_id,
-        Machine.move_id==PokemonMove.move_id),
-    foreign_keys=[Machine.version_group_id, Machine.move_id],
-    uselist=False,
-    backref='pokemon_moves')
-PokemonMove.move = relationship(Move,
-    innerjoin=True, lazy='joined',
-    backref='pokemon_moves')
-PokemonMove.method = relationship(PokemonMoveMethod,
-    innerjoin=True, lazy='joined')
 
 PokemonStat.stat = relationship(Stat,
     innerjoin=True, lazy='joined')

From 391fd1c1ac04a75bb8e8695bc841580ee9b3d4ad Mon Sep 17 00:00:00 2001
From: Petr Viktorin <encukou@gmail.com>
Date: Sun, 12 Feb 2012 23:19:40 +0100
Subject: [PATCH 06/11] Support association proxies

---
 pokedex/db/tables.py    | 27 ++++++++++++++-
 pokedex/doc/tabledoc.py | 74 ++++++++++++++++++++++++-----------------
 2 files changed, 69 insertions(+), 32 deletions(-)

diff --git a/pokedex/db/tables.py b/pokedex/db/tables.py
index a955722..58329f6 100644
--- a/pokedex/db/tables.py
+++ b/pokedex/db/tables.py
@@ -1337,8 +1337,21 @@ class PokemonForm(TableBase):
 
     @property
     def name(self):
+        """Name of this form: the form_name, if set; otherwise the species name"""
         return self.pokemon_name or self.species.name
 
+    @staticmethod
+    def _add_relationships(add_relationship, add_association_proxy, **kwargs):
+        add_relationship('pokemon', Pokemon,
+            primaryjoin=PokemonForm.pokemon_id==Pokemon.id,
+            innerjoin=True, lazy='joined')
+        add_association_proxy('species', 'pokemon', 'species')
+        add_relationship('version_group', VersionGroup,
+            innerjoin=True)
+        add_relationship('pokeathlon_stats', PokemonFormPokeathlonStat,
+            order_by=PokemonFormPokeathlonStat.pokeathlon_stat_id,
+            backref='pokemon_form')
+
 create_translation_table('pokemon_form_names', PokemonForm, 'names',
     relation_lazy='joined',
     form_name = Column(Unicode(32), nullable=True, index=True,
@@ -1818,7 +1831,19 @@ def add_relationships():
                     argument.relationship_info[backref_name] = backref_info
                 setattr(cls, name, relationship(argument, secondary=secondary,
                         **kwargs))
-            add_relationships(add_relationship=add_relationship)
+            def add_association_proxy(name, target_collection, attr, **kwargs):
+                cls.relationship_info.setdefault('_order', [])
+                cls.relationship_info['_order'].append(name)
+                cls.relationship_info[name] = info = kwargs.pop('info', {})
+                info.update(kwargs)
+                info.update(dict(
+                        type='association_proxy',
+                        target_collection=target_collection,
+                        attr=attr))
+                setattr(cls, name, association_proxy(name, target_collection,
+                        **kwargs))
+            add_relationships(add_relationship=add_relationship,
+                    add_association_proxy=add_association_proxy)
 add_relationships()
 
 Ability.changelog = relationship(AbilityChangelog,
diff --git a/pokedex/doc/tabledoc.py b/pokedex/doc/tabledoc.py
index ee92040..a121152 100644
--- a/pokedex/doc/tabledoc.py
+++ b/pokedex/doc/tabledoc.py
@@ -79,8 +79,10 @@ def column_header(c, class_name=None, transl_name=None, show_type=True,
         for fk in c.foreign_keys:
             if fk.column in column_to_cls:
                 foreign_cls = column_to_cls[fk.column]
-                if relation_name:
+                if relation_name and relation_name + '_id' == c.name:
                     result.append(u'(%s →' % c.name)
+                elif relation_name:
+                    result.append(u'(**%s** →' % c.name)
                 else:
                     result.append(u'(→')
                 result.append(u':class:`~pokedex.db.tables.%s`.%s)' % (
@@ -168,8 +170,8 @@ def generate_columns(cls, remaining_attrs):
         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) + ':')
+            yield column_header(c, name,
+                    relation=relation, relation_name=relation_name)
             remaining_attrs.remove(relation_name)
         else:
             yield column_header(c, name) + ':'
@@ -187,7 +189,7 @@ def generate_strings(cls, remaining_attrs):
                 if c.name in common_columns:
                     continue
                 yield column_header(c, cls.__name__,
-                        translation_class.__table__.name) + ':'
+                        translation_class.__table__.name)
                 yield u''
                 yield u'  ' + unicode(c.info['description'])
                 yield u''
@@ -204,33 +206,43 @@ def generate_relationships(cls, remaining_attrs):
     for rel_name, info in infos:
         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)
+            if info['type'] in ('relationship', 'backref'):
+                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)
+            elif info['type'] == 'association_proxy':
+                yield u'%s.\ **%s**:' % (cls.__name__, rel_name)
+                yield '``{info[attr]}`` of ``self.{info[target_collection]}``'.format(
+                        info=info)
+                if 'description' in info:
+                    yield u''
+                    yield u'  ' + unicode(info['description'])
+            else:
+                continue
             yield u''
             remaining_attrs.remove(rel_name)
 

From 04b941755a19aa4a4b8929202c4733e6296fa7ee Mon Sep 17 00:00:00 2001
From: Andrew Ekstedt <andrew.ekstedt@gmail.com>
Date: Tue, 5 Jun 2012 14:47:42 -0700
Subject: [PATCH 07/11] Introspect relationships directly.

Possibly more fragile, but this way we don't need _set_relationships.

SQLAlchemy version bump for AssociationProxy.remote_attr.
---
 pokedex/db/tables.py    |  5 ++-
 pokedex/doc/tabledoc.py | 90 ++++++++++++++++++-----------------------
 setup.py                |  2 +-
 3 files changed, 44 insertions(+), 53 deletions(-)

diff --git a/pokedex/db/tables.py b/pokedex/db/tables.py
index 58329f6..edb83e3 100644
--- a/pokedex/db/tables.py
+++ b/pokedex/db/tables.py
@@ -1829,7 +1829,8 @@ def add_relationships():
                             secondary=secondary)
                     backref_info.update(backref_kwargs.pop('info', {}))
                     argument.relationship_info[backref_name] = backref_info
-                setattr(cls, name, relationship(argument, secondary=secondary,
+                doc = info.get('description', None)
+                setattr(cls, name, relationship(argument, secondary=secondary, doc=doc,
                         **kwargs))
             def add_association_proxy(name, target_collection, attr, **kwargs):
                 cls.relationship_info.setdefault('_order', [])
@@ -1840,7 +1841,7 @@ def add_relationships():
                         type='association_proxy',
                         target_collection=target_collection,
                         attr=attr))
-                setattr(cls, name, association_proxy(name, target_collection,
+                setattr(cls, name, association_proxy(target_collection, attr,
                         **kwargs))
             add_relationships(add_relationship=add_relationship,
                     add_association_proxy=add_association_proxy)
diff --git a/pokedex/doc/tabledoc.py b/pokedex/doc/tabledoc.py
index a121152..2abc0d0 100644
--- a/pokedex/doc/tabledoc.py
+++ b/pokedex/doc/tabledoc.py
@@ -20,7 +20,8 @@ from sphinx.ext.autodoc import ClassLevelDocumenter
 
 from sqlalchemy import types
 from sqlalchemy.orm.attributes import InstrumentedAttribute
-from sqlalchemy.orm import configure_mappers
+from sqlalchemy.orm.properties import RelationshipProperty
+from sqlalchemy.orm import Mapper, configure_mappers
 from sqlalchemy.ext.associationproxy import AssociationProxy
 from pokedex.db.markdown import MoveEffectPropertyMap, MoveEffectProperty
 
@@ -196,55 +197,44 @@ def generate_strings(cls, remaining_attrs):
 
 @with_header(u'Relationships')
 def generate_relationships(cls, remaining_attrs):
-    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:
-        if rel_name in remaining_attrs:
-            info = cls.relationship_info.get(rel_name)
-            if info['type'] in ('relationship', 'backref'):
-                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)
-            elif info['type'] == 'association_proxy':
-                yield u'%s.\ **%s**:' % (cls.__name__, rel_name)
-                yield '``{info[attr]}`` of ``self.{info[target_collection]}``'.format(
-                        info=info)
-                if 'description' in info:
-                    yield u''
-                    yield u'  ' + unicode(info['description'])
-            else:
-                continue
-            yield u''
-            remaining_attrs.remove(rel_name)
+    for attr_name in sorted(remaining_attrs):
+        prop = getattr(cls, attr_name)
+        def isrelationship(prop):
+            return isinstance(prop, InstrumentedAttribute) and isinstance(prop.property, RelationshipProperty)
+        if isrelationship(prop):
+            rel = prop.property
+            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 prop.__doc__:
+                yield u''
+                yield u'  ' + unicode(prop.__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)
+        elif isinstance(prop, AssociationProxy):
+            yield u'%s.\ **%s**:' % (cls.__name__, attr_name)
+            yield '``{prop.remote_attr.key}`` of ``self.{prop.target_collection}``'.format(
+                    prop=prop)
+            '''if 'description' in info:
+                yield u''
+                yield u'  ' + unicode(info['description'])'''
+        else:
+            continue
+        yield u''
+        remaining_attrs.remove(attr_name)
 
 @with_header(u'Undocumented')
 def generate_undocumented(cls, remaining_attrs):
diff --git a/setup.py b/setup.py
index da17d38..5c71530 100644
--- a/setup.py
+++ b/setup.py
@@ -8,7 +8,7 @@ setup(
         'pokedex': ['data/csv/*.csv']
     },
     install_requires=[
-        'SQLAlchemy>=0.7',
+        'SQLAlchemy>=0.7.3',
         'whoosh>=2.2.2',
         'markdown',
         'construct',

From 739c6fdd7cfac98fe8533e77c0eff5f28d89a789 Mon Sep 17 00:00:00 2001
From: Andrew Ekstedt <andrew.ekstedt@gmail.com>
Date: Tue, 5 Jun 2012 15:09:54 -0700
Subject: [PATCH 08/11] Sort relationships by creation order.

Also:

- Split association proxies into their own section.

- Remove relationship_info.
---
 pokedex/db/tables.py    | 41 +++++-----------------
 pokedex/doc/tabledoc.py | 76 ++++++++++++++++++++++++-----------------
 2 files changed, 53 insertions(+), 64 deletions(-)

diff --git a/pokedex/db/tables.py b/pokedex/db/tables.py
index edb83e3..b1005f4 100644
--- a/pokedex/db/tables.py
+++ b/pokedex/db/tables.py
@@ -77,7 +77,6 @@ class TableMetaclass(DeclarativeMeta):
         if hasattr(cls, '__tablename__'):
             mapped_classes.append(cls)
             cls.translation_classes = []
-        cls.relationship_info = {}
 
 metadata = MetaData()
 TableBase = declarative_base(metadata=metadata, cls=TableSuperclass, metaclass=TableMetaclass)
@@ -1806,41 +1805,19 @@ def add_relationships():
             pass
         else:
             def add_relationship(name, argument, secondary=None, **kwargs):
-                cls.relationship_info.setdefault('_order', [])
-                cls.relationship_info['_order'].append(name)
-                cls.relationship_info[name] = info = kwargs.pop('info', {})
-                info.update(kwargs)
-                info.update(dict(
-                        type='relationship',
-                        argument=argument,
-                        secondary=secondary))
-                the_backref = kwargs.get('backref')
-                if isinstance(the_backref, basestring):
-                    the_backref = backref(the_backref)
-                if the_backref:
-                    # XXX: This line exploits a SQLA implementation detail,
-                    # after all relationships are converted we should make our
-                    # own backref wrapper with known semantics.
-                    backref_name, backref_kwargs = the_backref
-
-                    backref_info = dict(
-                            type='backref',
-                            argument=cls,
-                            secondary=secondary)
-                    backref_info.update(backref_kwargs.pop('info', {}))
-                    argument.relationship_info[backref_name] = backref_info
+                info = kwargs.pop('info', {})
                 doc = info.get('description', None)
+                backref_ = kwargs.get('backref')
+                if type(backref_) == tuple:
+                    backref_name, backref_kwargs = backref_
+                    backref_info = backref_kwargs.pop('info', {})
+                    backref_kwargs['doc'] = backref_info.get('doc', None)
+                    backref_ = backref(backref_name, **backref_kwargs)
+                    kwargs['backref'] = backref_
                 setattr(cls, name, relationship(argument, secondary=secondary, doc=doc,
                         **kwargs))
             def add_association_proxy(name, target_collection, attr, **kwargs):
-                cls.relationship_info.setdefault('_order', [])
-                cls.relationship_info['_order'].append(name)
-                cls.relationship_info[name] = info = kwargs.pop('info', {})
-                info.update(kwargs)
-                info.update(dict(
-                        type='association_proxy',
-                        target_collection=target_collection,
-                        attr=attr))
+                info = kwargs.pop('info', {})
                 setattr(cls, name, association_proxy(target_collection, attr,
                         **kwargs))
             add_relationships(add_relationship=add_relationship,
diff --git a/pokedex/doc/tabledoc.py b/pokedex/doc/tabledoc.py
index 2abc0d0..ebd0f03 100644
--- a/pokedex/doc/tabledoc.py
+++ b/pokedex/doc/tabledoc.py
@@ -197,44 +197,55 @@ def generate_strings(cls, remaining_attrs):
 
 @with_header(u'Relationships')
 def generate_relationships(cls, remaining_attrs):
+    def isrelationship(prop):
+        return isinstance(prop, InstrumentedAttribute) and isinstance(prop.property, RelationshipProperty)
+
+    relationships = []
+    for attr_name in remaining_attrs:
+        prop = getattr(cls, attr_name)
+        if isrelationship(prop):
+            relationships.append((attr_name, prop.property))
+    relationships.sort(key=lambda x: x[1]._creation_order)
+
+    for attr_name, rel in relationships:
+        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)
-        def isrelationship(prop):
-            return isinstance(prop, InstrumentedAttribute) and isinstance(prop.property, RelationshipProperty)
-        if isrelationship(prop):
-            rel = prop.property
-            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 prop.__doc__:
-                yield u''
-                yield u'  ' + unicode(prop.__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)
-        elif isinstance(prop, AssociationProxy):
+        if isinstance(prop, AssociationProxy):
             yield u'%s.\ **%s**:' % (cls.__name__, attr_name)
-            yield '``{prop.remote_attr.key}`` of ``self.{prop.target_collection}``'.format(
+            yield '``{prop.remote_attr.key}`` of ``self.{prop.local_attr.key}``'.format(
                     prop=prop)
             '''if 'description' in info:
                 yield u''
                 yield u'  ' + unicode(info['description'])'''
-        else:
-            continue
-        yield u''
-        remaining_attrs.remove(attr_name)
+            yield u''
+            remaining_attrs.remove(attr_name)
+
 
 @with_header(u'Undocumented')
 def generate_undocumented(cls, remaining_attrs):
@@ -290,7 +301,7 @@ class DexTable(PyClasslike):
 
         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'])
+                'add_relationships', 'summary_column'])
         for transl_class in cls.translation_classes:
             remaining_attrs.difference_update([
                     transl_class.relation_name,
@@ -305,6 +316,7 @@ class DexTable(PyClasslike):
         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_associationproxies(cls, remaining_attrs))
         generated_content.extend(generate_undocumented(cls, remaining_attrs))
         generated_content.extend(generate_other(cls, remaining_attrs))
 

From e07e64dfa5c6bea32da4c4ce645795cb1ff85209 Mon Sep 17 00:00:00 2001
From: Andrew Ekstedt <andrew.ekstedt@gmail.com>
Date: Tue, 5 Jun 2012 15:15:40 -0700
Subject: [PATCH 09/11] Display primary keys in table docs.

---
 pokedex/doc/tabledoc.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/pokedex/doc/tabledoc.py b/pokedex/doc/tabledoc.py
index ebd0f03..a24c82d 100644
--- a/pokedex/doc/tabledoc.py
+++ b/pokedex/doc/tabledoc.py
@@ -139,6 +139,10 @@ def generate_table_header(cls, remaining_attrs):
         pass
     yield u''
 
+    yield u'Primary key: %s.' % u', '.join(
+        u'**%s**' % col.key for col in cls.__table__.primary_key.columns)
+    yield u''
+
 def generate_common(cls, remaining_attrs):
     common_col_headers = []
     for c in cls.__table__.c:

From 527b3ce05651a47964c0e10da8e03c69afe17c4c Mon Sep 17 00:00:00 2001
From: Andrew Ekstedt <andrew.ekstedt@gmail.com>
Date: Tue, 5 Jun 2012 18:07:07 -0700
Subject: [PATCH 10/11] Remove _add_relationships.

Back out all the _add_relationships functions. This leaves tables.py
almost unchanged from before their addition, except for some added
documentation.
---
 pokedex/db/tables.py | 385 +++++++++++++++++++------------------------
 1 file changed, 168 insertions(+), 217 deletions(-)

diff --git a/pokedex/db/tables.py b/pokedex/db/tables.py
index b1005f4..482231f 100644
--- a/pokedex/db/tables.py
+++ b/pokedex/db/tables.py
@@ -30,8 +30,7 @@ classes in that module can be used to change the default language.
 import collections
 from functools import partial
 
-from sqlalchemy import (Column, ForeignKey, MetaData, PrimaryKeyConstraint,
-    Table, UniqueConstraint)
+from sqlalchemy import Column, ForeignKey, MetaData, PrimaryKeyConstraint, Table, UniqueConstraint
 from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
 from sqlalchemy.ext.associationproxy import association_proxy
 from sqlalchemy.orm import backref, relationship
@@ -324,25 +323,6 @@ class Encounter(TableBase):
     max_level = Column(Integer, nullable=False, autoincrement=False,
         info=dict(description=u"The maxmum level of the encountered Pokémon"))
 
-    @staticmethod
-    def _add_relationships(add_relationship, add_association_proxy, **kwargs):
-        add_relationship('condition_value_map', EncounterConditionValueMap,
-            backref='encounter')
-        add_association_proxy('condition_values',
-                'condition_value_map', 'condition_value')
-        add_relationship('location_area', LocationArea,
-            innerjoin=True, lazy='joined',
-            backref='encounters')
-        add_relationship('pokemon', Pokemon,
-            innerjoin=True, lazy='joined',
-            backref='encounters')
-        add_relationship('version', Version,
-            innerjoin=True, lazy='joined',
-            backref='encounters')
-        add_relationship('slot', EncounterSlot,
-            innerjoin=True, lazy='joined',
-            backref='encounters')
-
 class EncounterCondition(TableBase):
     u"""A conditions in the game world that affects Pokémon encounters, such as time of day.
     """
@@ -1100,62 +1080,6 @@ class Pokemon(TableBase):
     is_default = Column(Boolean, nullable=False, index=True,
         info=dict(description=u'Set for exactly one pokemon used as the default for each species.'))
 
-    @staticmethod
-    def _add_relationships(add_relationship, **kwargs):
-        add_relationship('all_abilities', Ability,
-            secondary=PokemonAbility.__table__,
-            order_by=PokemonAbility.slot.asc(),
-            innerjoin=True,
-            backref=backref('all_pokemon', order_by=Pokemon.order.asc()),
-            info=dict(
-                description=u"All abilities the Pokémon can have, including the Hidden Ability"))
-        add_relationship('abilities', Ability,
-            secondary=PokemonAbility.__table__,
-            primaryjoin=and_(
-                Pokemon.id == PokemonAbility.pokemon_id,
-                PokemonAbility.is_dream == False),
-            innerjoin=True,
-            order_by=PokemonAbility.slot.asc(),
-            backref=backref('pokemon', order_by=Pokemon.order.asc()),
-            info=dict(
-                description=u"Abilities the Pokémon can have in the wild"))
-        add_relationship('dream_ability', Ability,
-            secondary=PokemonAbility.__table__,
-            primaryjoin=and_(
-                Pokemon.id == PokemonAbility.pokemon_id,
-                PokemonAbility.is_dream == True),
-            uselist=False,
-            backref=backref('dream_pokemon', order_by=Pokemon.order),
-            info=dict(
-                description=u"The Pokémon's Hidden Ability"))
-        add_relationship('forms', PokemonForm,
-            primaryjoin=Pokemon.id==PokemonForm.pokemon_id,
-            order_by=(PokemonForm.order.asc(),
-                PokemonForm.form_identifier.asc()))
-        add_relationship('default_form', PokemonForm,
-            primaryjoin=and_(
-                Pokemon.id==PokemonForm.pokemon_id,
-                PokemonForm.is_default==True),
-            uselist=False,
-            lazy='joined',
-            info=dict(description=u"A representative form of this pokémon"))
-        add_relationship('items', PokemonItem,
-            backref='pokemon',
-            info=dict(
-                description=u"Info about items this pokémon holds in the wild"))
-        add_relationship('stats', PokemonStat,
-            innerjoin=True,
-            order_by=PokemonStat.stat_id.asc(),
-            backref='pokemon')
-        add_relationship('species', PokemonSpecies,
-            innerjoin=True,
-            backref='pokemon')
-        add_relationship('types', Type,
-            secondary=PokemonType.__table__,
-            innerjoin=True, lazy='joined',
-            order_by=PokemonType.slot.asc(),
-            backref=backref('pokemon', order_by=Pokemon.order))
-
     @property
     def name(self):
         u"""Returns a name for this Pokémon, specifiying the form iff it
@@ -1290,27 +1214,6 @@ class PokemonEvolution(TableBase):
     trade_species_id = Column(Integer, ForeignKey('pokemon_species.id'), nullable=True,
         info=dict(description=u"The ID of the species for which this one must be traded."))
 
-    @staticmethod
-    def _add_relationships(add_relationship, **kwargs):
-        add_relationship('trigger', EvolutionTrigger,
-            innerjoin=True, lazy='joined',
-            backref='evolutions')
-        add_relationship('trigger_item', Item,
-            primaryjoin=PokemonEvolution.trigger_item_id==Item.id,
-            backref='triggered_evolutions')
-        add_relationship('held_item', Item,
-            primaryjoin=PokemonEvolution.held_item_id==Item.id,
-            backref='required_for_evolutions')
-        add_relationship('location', Location,
-            backref='triggered_evolutions')
-        add_relationship('known_move', Move,
-            backref='triggered_evolutions')
-        add_relationship('party_species', PokemonSpecies,
-            primaryjoin=PokemonEvolution.party_species_id==PokemonSpecies.id,
-            backref='triggered_evolutions')
-        add_relationship('trade_species', PokemonSpecies,
-            primaryjoin=PokemonEvolution.trade_species_id==PokemonSpecies.id)
-
 class PokemonForm(TableBase):
     u"""An individual form of a Pokémon.  This includes *every* variant (except
     color differences) of every Pokémon, regardless of how the games treat
@@ -1339,18 +1242,6 @@ class PokemonForm(TableBase):
         """Name of this form: the form_name, if set; otherwise the species name"""
         return self.pokemon_name or self.species.name
 
-    @staticmethod
-    def _add_relationships(add_relationship, add_association_proxy, **kwargs):
-        add_relationship('pokemon', Pokemon,
-            primaryjoin=PokemonForm.pokemon_id==Pokemon.id,
-            innerjoin=True, lazy='joined')
-        add_association_proxy('species', 'pokemon', 'species')
-        add_relationship('version_group', VersionGroup,
-            innerjoin=True)
-        add_relationship('pokeathlon_stats', PokemonFormPokeathlonStat,
-            order_by=PokemonFormPokeathlonStat.pokeathlon_stat_id,
-            backref='pokemon_form')
-
 create_translation_table('pokemon_form_names', PokemonForm, 'names',
     relation_lazy='joined',
     form_name = Column(Unicode(32), nullable=True, index=True,
@@ -1435,26 +1326,6 @@ class PokemonMove(TableBase):
         {},
     )
 
-    @staticmethod
-    def _add_relationships(add_relationship, **kwargs):
-        add_relationship('pokemon', Pokemon,
-            innerjoin=True, lazy='joined',
-            backref='pokemon_moves')
-        add_relationship('version_group', VersionGroup,
-            innerjoin=True, lazy='joined')
-        add_relationship('machine', Machine,
-            primaryjoin=and_(
-                Machine.version_group_id==PokemonMove.version_group_id,
-                Machine.move_id==PokemonMove.move_id),
-            foreign_keys=[Machine.version_group_id, Machine.move_id],
-            uselist=False,
-            backref='pokemon_moves')
-        add_relationship('move', Move,
-            innerjoin=True, lazy='joined',
-            backref='pokemon_moves')
-        add_relationship('method', PokemonMoveMethod,
-            innerjoin=True, lazy='joined')
-
 class PokemonMoveMethod(TableBase):
     u"""A method a move can be learned by, such as "Level up" or "Tutor".
     """
@@ -1530,66 +1401,6 @@ class PokemonSpecies(TableBase):
     forms_switchable = Column(Boolean, nullable=False,
         info=dict(description=u"True iff a particular individual of this species can switch beween its different forms."))
 
-    @staticmethod
-    def _add_relationships(add_relationship, **kwargs):
-        add_relationship('parent_species', PokemonSpecies,
-            primaryjoin=PokemonSpecies.evolves_from_species_id==PokemonSpecies.id,
-            remote_side=[PokemonSpecies.id],
-            backref=backref('child_species',
-                info=dict(description=u"The species to which this one evolves")),
-            info=dict(description=u"The species from which this one evolves"))
-        add_relationship('evolutions', PokemonEvolution,
-            primaryjoin=PokemonSpecies.id==PokemonEvolution.evolved_species_id,
-            backref=backref('evolved_species', innerjoin=True, lazy='joined'))
-        add_relationship('flavor_text', PokemonSpeciesFlavorText,
-            order_by=PokemonSpeciesFlavorText.version_id.asc(),
-            backref='species')
-        add_relationship('growth_rate', GrowthRate,
-            innerjoin=True,
-            backref='evolution_chains')
-        add_relationship('habitat', PokemonHabitat,
-            backref='species')
-        add_relationship('color', PokemonColor,
-            innerjoin=True,
-            backref='species')
-        add_relationship('egg_groups', EggGroup,
-            secondary=PokemonEggGroup.__table__,
-            innerjoin=True,
-            order_by=PokemonEggGroup.egg_group_id.asc(),
-            backref=backref('species', order_by=Pokemon.order.asc()))
-        add_relationship('forms', PokemonForm,
-            secondary=Pokemon.__table__,
-            primaryjoin=PokemonSpecies.id==Pokemon.species_id,
-            secondaryjoin=Pokemon.id==PokemonForm.pokemon_id,
-            order_by=(PokemonForm.order.asc(),
-                PokemonForm.form_identifier.asc()))
-        add_relationship('default_form', PokemonForm,
-            secondary=Pokemon.__table__,
-            primaryjoin=and_(PokemonSpecies.id==Pokemon.species_id,
-                    Pokemon.is_default==True),
-            secondaryjoin=and_(Pokemon.id==PokemonForm.pokemon_id,
-                    PokemonForm.is_default==True),
-            uselist=False,
-            info=dict(description=u"A representative form of this species"))
-        add_relationship('default_pokemon', Pokemon,
-            primaryjoin=and_(
-                PokemonSpecies.id==Pokemon.species_id,
-                Pokemon.is_default==True),
-            uselist=False, lazy='joined')
-        add_relationship('evolution_chain', EvolutionChain,
-            innerjoin=True,
-            backref=backref('species', order_by=PokemonSpecies.id.asc()))
-        add_relationship('dex_numbers', PokemonDexNumber,
-            innerjoin=True,
-            order_by=PokemonDexNumber.pokedex_id.asc(),
-            backref='species')
-        add_relationship('generation', Generation,
-            innerjoin=True,
-            backref='species')
-        add_relationship('shape', PokemonShape,
-            innerjoin=True,
-            backref='species')
-
 create_translation_table('pokemon_species_names', PokemonSpecies, 'names',
     relation_lazy='joined',
     name = Column(Unicode(20), nullable=True, index=True,
@@ -1797,33 +1608,6 @@ class VersionGroupRegion(TableBase):
 
 ### Relationships down here, to avoid dependency ordering problems
 
-def add_relationships():
-    for cls in mapped_classes:
-        try:
-            add_relationships = cls._add_relationships
-        except AttributeError:
-            pass
-        else:
-            def add_relationship(name, argument, secondary=None, **kwargs):
-                info = kwargs.pop('info', {})
-                doc = info.get('description', None)
-                backref_ = kwargs.get('backref')
-                if type(backref_) == tuple:
-                    backref_name, backref_kwargs = backref_
-                    backref_info = backref_kwargs.pop('info', {})
-                    backref_kwargs['doc'] = backref_info.get('doc', None)
-                    backref_ = backref(backref_name, **backref_kwargs)
-                    kwargs['backref'] = backref_
-                setattr(cls, name, relationship(argument, secondary=secondary, doc=doc,
-                        **kwargs))
-            def add_association_proxy(name, target_collection, attr, **kwargs):
-                info = kwargs.pop('info', {})
-                setattr(cls, name, association_proxy(target_collection, attr,
-                        **kwargs))
-            add_relationships(add_relationship=add_relationship,
-                    add_association_proxy=add_association_proxy)
-add_relationships()
-
 Ability.changelog = relationship(AbilityChangelog,
     order_by=AbilityChangelog.changed_in_version_group_id.desc(),
     backref=backref('ability', innerjoin=True, lazy='joined'))
@@ -1865,6 +1649,21 @@ ContestCombo.second = relationship(Move,
     innerjoin=True, lazy='joined',
     backref='contest_combo_second')
 
+Encounter.condition_value_map = relationship(EncounterConditionValueMap,
+    backref='encounter')
+Encounter.condition_values = association_proxy('condition_value_map', 'condition_value')
+Encounter.location_area = relationship(LocationArea,
+    innerjoin=True, lazy='joined',
+    backref='encounters')
+Encounter.pokemon = relationship(Pokemon,
+    innerjoin=True, lazy='joined',
+    backref='encounters')
+Encounter.version = relationship(Version,
+    innerjoin=True, lazy='joined',
+    backref='encounters')
+Encounter.slot = relationship(EncounterSlot,
+    innerjoin=True, lazy='joined',
+    backref='encounters')
 
 EncounterConditionValue.condition = relationship(EncounterCondition,
     innerjoin=True, lazy='joined',
@@ -2090,9 +1889,87 @@ Pokedex.version_groups = relationship(VersionGroup,
     backref='pokedex')
 
 
+Pokemon.all_abilities = relationship(Ability,
+    secondary=PokemonAbility.__table__,
+    order_by=PokemonAbility.slot.asc(),
+    innerjoin=True,
+    backref=backref('all_pokemon', order_by=Pokemon.order.asc()),
+    doc=u"All abilities the Pokémon can have, including the Hidden Ability")
+Pokemon.abilities = relationship(Ability,
+    secondary=PokemonAbility.__table__,
+    primaryjoin=and_(
+        Pokemon.id == PokemonAbility.pokemon_id,
+        PokemonAbility.is_dream == False,
+    ),
+    innerjoin=True,
+    order_by=PokemonAbility.slot.asc(),
+    backref=backref('pokemon', order_by=Pokemon.order.asc()),
+    doc=u"Abilities the Pokémon can have in the wild")
+Pokemon.dream_ability = relationship(Ability,
+    secondary=PokemonAbility.__table__,
+    primaryjoin=and_(
+        Pokemon.id == PokemonAbility.pokemon_id,
+        PokemonAbility.is_dream == True,
+    ),
+    uselist=False,
+    backref=backref('dream_pokemon', order_by=Pokemon.order),
+    doc=u"The Pokémon's Hidden Ability")
+Pokemon.forms = relationship(PokemonForm,
+    primaryjoin=Pokemon.id==PokemonForm.pokemon_id,
+    order_by=(PokemonForm.order.asc(), PokemonForm.form_identifier.asc()))
+Pokemon.default_form = relationship(PokemonForm,
+    primaryjoin=and_(
+        Pokemon.id==PokemonForm.pokemon_id,
+        PokemonForm.is_default==True),
+    uselist=False, lazy='joined',
+    doc=u"A representative form of this pokémon")
+Pokemon.items = relationship(PokemonItem,
+    backref='pokemon',
+    doc=u"Info about items this pokémon holds in the wild")
+Pokemon.stats = relationship(PokemonStat,
+    innerjoin=True,
+    order_by=PokemonStat.stat_id.asc(),
+    backref='pokemon')
+Pokemon.species = relationship(PokemonSpecies,
+    innerjoin=True,
+    backref='pokemon')
+Pokemon.types = relationship(Type,
+    secondary=PokemonType.__table__,
+    innerjoin=True, lazy='joined',
+    order_by=PokemonType.slot.asc(),
+    backref=backref('pokemon', order_by=Pokemon.order))
+
 PokemonDexNumber.pokedex = relationship(Pokedex,
     innerjoin=True, lazy='joined')
 
+PokemonEvolution.trigger = relationship(EvolutionTrigger,
+    innerjoin=True, lazy='joined',
+    backref='evolutions')
+PokemonEvolution.trigger_item = relationship(Item,
+    primaryjoin=PokemonEvolution.trigger_item_id==Item.id,
+    backref='triggered_evolutions')
+PokemonEvolution.held_item = relationship(Item,
+    primaryjoin=PokemonEvolution.held_item_id==Item.id,
+    backref='required_for_evolutions')
+PokemonEvolution.location = relationship(Location,
+    backref='triggered_evolutions')
+PokemonEvolution.known_move = relationship(Move,
+    backref='triggered_evolutions')
+PokemonEvolution.party_species = relationship(PokemonSpecies,
+    primaryjoin=PokemonEvolution.party_species_id==PokemonSpecies.id,
+    backref='triggered_evolutions')
+PokemonEvolution.trade_species = relationship(PokemonSpecies,
+    primaryjoin=PokemonEvolution.trade_species_id==PokemonSpecies.id)
+
+PokemonForm.pokemon = relationship(Pokemon,
+    primaryjoin=PokemonForm.pokemon_id==Pokemon.id,
+    innerjoin=True, lazy='joined')
+PokemonForm.species = association_proxy('pokemon', 'species')
+PokemonForm.version_group = relationship(VersionGroup,
+    innerjoin=True)
+PokemonForm.pokeathlon_stats = relationship(PokemonFormPokeathlonStat,
+    order_by=PokemonFormPokeathlonStat.pokeathlon_stat_id,
+    backref='pokemon_form')
 
 PokemonFormPokeathlonStat.pokeathlon_stat = relationship(PokeathlonStat,
     innerjoin=True, lazy='joined')
@@ -2103,10 +1980,84 @@ PokemonItem.item = relationship(Item,
 PokemonItem.version = relationship(Version,
     innerjoin=True, lazy='joined')
 
+PokemonMove.pokemon = relationship(Pokemon,
+    innerjoin=True, lazy='joined',
+    backref='pokemon_moves')
+PokemonMove.version_group = relationship(VersionGroup,
+    innerjoin=True, lazy='joined')
+PokemonMove.machine = relationship(Machine,
+    primaryjoin=and_(
+        Machine.version_group_id==PokemonMove.version_group_id,
+        Machine.move_id==PokemonMove.move_id),
+    foreign_keys=[Machine.version_group_id, Machine.move_id],
+    uselist=False,
+    backref='pokemon_moves')
+PokemonMove.move = relationship(Move,
+    innerjoin=True, lazy='joined',
+    backref='pokemon_moves')
+PokemonMove.method = relationship(PokemonMoveMethod,
+    innerjoin=True, lazy='joined')
 
 PokemonStat.stat = relationship(Stat,
     innerjoin=True, lazy='joined')
 
+PokemonSpecies.parent_species = relationship(PokemonSpecies,
+    primaryjoin=PokemonSpecies.evolves_from_species_id==PokemonSpecies.id,
+    remote_side=[PokemonSpecies.id],
+    backref=backref('child_species',
+        doc=u"The species to which this one evolves"),
+    doc=u"The species from which this one evolves")
+PokemonSpecies.evolutions = relationship(PokemonEvolution,
+    primaryjoin=PokemonSpecies.id==PokemonEvolution.evolved_species_id,
+    backref=backref('evolved_species', innerjoin=True, lazy='joined'))
+PokemonSpecies.flavor_text = relationship(PokemonSpeciesFlavorText,
+    order_by=PokemonSpeciesFlavorText.version_id.asc(),
+    backref='species')
+PokemonSpecies.growth_rate = relationship(GrowthRate,
+    innerjoin=True,
+    backref='evolution_chains')
+PokemonSpecies.habitat = relationship(PokemonHabitat,
+    backref='species')
+PokemonSpecies.color = relationship(PokemonColor,
+    innerjoin=True,
+    backref='species')
+PokemonSpecies.egg_groups = relationship(EggGroup,
+    secondary=PokemonEggGroup.__table__,
+    innerjoin=True,
+    order_by=PokemonEggGroup.egg_group_id.asc(),
+    backref=backref('species', order_by=Pokemon.order.asc()))
+PokemonSpecies.forms = relationship(PokemonForm,
+    secondary=Pokemon.__table__,
+    primaryjoin=PokemonSpecies.id==Pokemon.species_id,
+    secondaryjoin=Pokemon.id==PokemonForm.pokemon_id,
+    order_by=(PokemonForm.order.asc(), PokemonForm.form_identifier.asc()))
+PokemonSpecies.default_form = relationship(PokemonForm,
+    secondary=Pokemon.__table__,
+    primaryjoin=and_(PokemonSpecies.id==Pokemon.species_id,
+            Pokemon.is_default==True),
+    secondaryjoin=and_(Pokemon.id==PokemonForm.pokemon_id,
+            PokemonForm.is_default==True),
+    uselist=False,
+    doc=u"A representative form of this species")
+PokemonSpecies.default_pokemon = relationship(Pokemon,
+    primaryjoin=and_(
+        PokemonSpecies.id==Pokemon.species_id,
+        Pokemon.is_default==True),
+    uselist=False, lazy='joined')
+PokemonSpecies.evolution_chain = relationship(EvolutionChain,
+    innerjoin=True,
+    backref=backref('species', order_by=PokemonSpecies.id.asc()))
+PokemonSpecies.dex_numbers = relationship(PokemonDexNumber,
+    innerjoin=True,
+    order_by=PokemonDexNumber.pokedex_id.asc(),
+    backref='species')
+PokemonSpecies.generation = relationship(Generation,
+    innerjoin=True,
+    backref='species')
+PokemonSpecies.shape = relationship(PokemonShape,
+    innerjoin=True,
+    backref='species')
+
 PokemonSpeciesFlavorText.version = relationship(Version, innerjoin=True, lazy='joined')
 PokemonSpeciesFlavorText.language = relationship(Language, innerjoin=True, lazy='joined')
 

From f4c51c845dbbeb3a43f3391f767a220b12be6e79 Mon Sep 17 00:00:00 2001
From: Andrew Ekstedt <andrew.ekstedt@gmail.com>
Date: Tue, 5 Jun 2012 18:10:37 -0700
Subject: [PATCH 11/11] Sort relationship docs alphabetically.

Creation order wasn't that big of a win and didn't play nicely with
backrefs.
---
 pokedex/doc/tabledoc.py | 11 ++++-------
 1 file changed, 4 insertions(+), 7 deletions(-)

diff --git a/pokedex/doc/tabledoc.py b/pokedex/doc/tabledoc.py
index a24c82d..172a454 100644
--- a/pokedex/doc/tabledoc.py
+++ b/pokedex/doc/tabledoc.py
@@ -204,14 +204,11 @@ def generate_relationships(cls, remaining_attrs):
     def isrelationship(prop):
         return isinstance(prop, InstrumentedAttribute) and isinstance(prop.property, RelationshipProperty)
 
-    relationships = []
-    for attr_name in remaining_attrs:
+    for attr_name in sorted(remaining_attrs):
         prop = getattr(cls, attr_name)
-        if isrelationship(prop):
-            relationships.append((attr_name, prop.property))
-    relationships.sort(key=lambda x: x[1]._creation_order)
-
-    for attr_name, rel in relationships:
+        if not isrelationship(prop):
+            continue
+        rel = prop.property
         yield u'%s.\ **%s**' % (cls.__name__, attr_name)
         class_name = u':class:`~pokedex.db.tables.%s`' % rel.mapper.class_.__name__
         if rel.uselist: