From dd89c811b8312c09329e54068afbab65acc54267 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 14 Apr 2011 13:46:12 +0300 Subject: [PATCH 1/6] Move util.py to compatibility.py --- pokedex/{util.py => compatibility.py} | 5 ++++- pokedex/lookup.py | 2 +- pokedex/struct/__init__.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) rename pokedex/{util.py => compatibility.py} (98%) diff --git a/pokedex/util.py b/pokedex/compatibility.py similarity index 98% rename from pokedex/util.py rename to pokedex/compatibility.py index d9aec70..67eff35 100644 --- a/pokedex/util.py +++ b/pokedex/compatibility.py @@ -1,4 +1,7 @@ -"""Functions missing from Python 2.5""" +"""Things missing from older versions of Python + +Currently these are functions missing from Python 2.5. +""" try: from itertools import permutations diff --git a/pokedex/lookup.py b/pokedex/lookup.py index 8488f21..768a674 100644 --- a/pokedex/lookup.py +++ b/pokedex/lookup.py @@ -14,7 +14,7 @@ from whoosh.qparser import QueryParser import whoosh.scoring import whoosh.spelling -from pokedex.util import namedtuple +from pokedex.compatibility import namedtuple from pokedex.db import connect import pokedex.db.tables as tables diff --git a/pokedex/struct/__init__.py b/pokedex/struct/__init__.py index 5b52539..b6a3e73 100644 --- a/pokedex/struct/__init__.py +++ b/pokedex/struct/__init__.py @@ -12,7 +12,7 @@ import struct from pokedex.db import tables from pokedex.formulae import calculated_hp, calculated_stat -from pokedex.util import namedtuple, permutations +from pokedex.compatibility import namedtuple, permutations from pokedex.struct._pokemon_struct import pokemon_struct def pokemon_prng(seed): From 4cd68fb5e32c25055fcd6fce3df2433e913299df Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 14 Apr 2011 14:25:42 +0300 Subject: [PATCH 2/6] Typo: multilang docstring was wrong --- pokedex/db/multilang.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokedex/db/multilang.py b/pokedex/db/multilang.py index 44e532c..f66ee20 100644 --- a/pokedex/db/multilang.py +++ b/pokedex/db/multilang.py @@ -48,7 +48,7 @@ def create_translation_table(_table_name, foreign_class, relation_name, are rows in the created tables. - `(relation_name)_local`, a relation to the row in the new table that matches the current default language. - - `(relation_name)_class`, the class created by this function. + - `(relation_name)_table`, the class created by this function. Note that these are distinct relations. Even though the former necessarily includes the latter, SQLAlchemy doesn't treat them as linked; loading one From 1f2df80a0058077511cf4455f9b976a27b1ea46d Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 14 Apr 2011 17:32:52 +0300 Subject: [PATCH 3/6] Make Pokemon.form an actual relation --- pokedex/db/tables.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pokedex/db/tables.py b/pokedex/db/tables.py index 928c7ee..9553349 100644 --- a/pokedex/db/tables.py +++ b/pokedex/db/tables.py @@ -1099,12 +1099,6 @@ class Pokemon(TableBase): ### Stuff to handle alternate Pokémon forms - @property - def form(self): - u"""Returns the Pokémon's form, using its default form as fallback.""" - - return self.unique_form or self.default_form - @property def is_base_form(self): u"""Returns True iff the Pokémon is the base form for its species, @@ -2021,6 +2015,14 @@ Pokemon.types = relation(Type, innerjoin=True, order_by=PokemonType.slot.asc(), backref=backref('pokemon', order_by=Pokemon.order)) +Pokemon.form = relation(PokemonForm, + primaryjoin=or_( + PokemonForm.unique_pokemon_id==Pokemon.id, + and_(PokemonForm.unique_pokemon_id==None, + PokemonForm.form_base_pokemon_id==Pokemon.id, + PokemonForm.is_default==True) + ), + uselist=False) PokemonDexNumber.pokedex = relation(Pokedex, innerjoin=True, lazy='joined') From 2f014411e141f7e0048e9f348d9cb1694e275008 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 14 Apr 2011 17:42:35 +0300 Subject: [PATCH 4/6] Add pokedex.util.get: helpers to get stuff out of the DB easily --- pokedex/tests/test_util.py | 75 ++++++++++++++++ pokedex/util/get.py | 175 +++++++++++++++++++++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 pokedex/tests/test_util.py create mode 100644 pokedex/util/get.py diff --git a/pokedex/tests/test_util.py b/pokedex/tests/test_util.py new file mode 100644 index 0000000..35a3fb1 --- /dev/null +++ b/pokedex/tests/test_util.py @@ -0,0 +1,75 @@ +# encoding: utf8 +from nose.tools import * +import unittest + +from pokedex.db import connect, tables +from pokedex.util import get + +session = connect() + +def test_get_item_identifier(): + item = get.get(session, tables.Item, identifier='master-ball') + assert item.name == 'Master Ball' + +def test_get_item_name(): + item = get.get(session, tables.Item, name='Awakening') + assert item.name == 'Awakening' + +def test_get_english_by_identifier(): + language = get.get(session, tables.Language, 'en') + assert language.name == 'English' + +def test_get_pokemon_baseform_identifier(): + for identifier in 'burmy shaymin unown cresselia'.split(): + poke = get.get(session, tables.Pokemon, identifier=identifier) + assert poke.identifier == identifier + assert poke.is_base_form + +def test_get_pokemon_baseform_name(): + for name in 'Burmy Shaymin Unown Cresselia'.split(): + poke = get.get(session, tables.Pokemon, name=name) + assert poke.name == name + assert poke.is_base_form + +def test_get_pokemon_baseform_name_explicit_language(): + french = get.get(session, tables.Language, 'fr') + for name in 'Cheniti Shaymin Zarbi Cresselia'.split(): + poke = get.get(session, tables.Pokemon, name=name, language=french) + assert poke.name_map[french] == name, poke.name_map[french] + assert poke.is_base_form + +def test_get_pokemon_other_form_identifier(): + for ii in 'wormadam/trash shaymin/sky shaymin/land'.split(): + pokemon_identifier, form_identifier = ii.split('/') + poke = get.get(session, tables.Pokemon, identifier=pokemon_identifier, form_identifier=form_identifier) + assert poke.identifier == pokemon_identifier + if poke.form.unique_pokemon_id: + assert poke.form.identifier == form_identifier + +def test_pokemon(): + pokemon = get.pokemon(session) + assert pokemon[0].identifier == 'bulbasaur' + assert pokemon[-1].identifier == 'genesect' + +def test_pokemon_by_name(): + pokemon = get.pokemon(session, order=tables.Pokemon.name) + assert pokemon[0].identifier == 'abomasnow' + assert pokemon[-1].identifier == 'zweilous' + +def test_types_french_order(): + french = get.get(session, tables.Language, 'fr') + types = get.types(session, order=None) + types = list(get.order_by_name(types, tables.Type, language=french)) + assert types[0].name_map[french] == 'Acier', types[0].name_map[french] + assert types[-1].name_map[french] == 'Vol', types[-1].name_map[french] + +def test_moves(): + moves = get.moves(session) + assert moves[0].identifier == 'absorb' + assert moves[-1].identifier == 'zen-headbutt' + +def test_items(): + items = get.items(session) + assert items[0].identifier == 'ability-urge' + assert items[-1].identifier == 'zoom-lens' + diff --git a/pokedex/util/get.py b/pokedex/util/get.py new file mode 100644 index 0000000..ab5e6b8 --- /dev/null +++ b/pokedex/util/get.py @@ -0,0 +1,175 @@ +"""Provides simple functions for common queries + +These include identifier- and name-based lookup, filtering out base forms +of pokemon, ordering by name, and getting canonical "pokedex" lists (i.e. +ordered and without cruft like alternate pokemon forms or Shadow moves) +""" + +from sqlalchemy.orm import aliased + +from pokedex.db import tables + +### Getters + +def get(session, table, identifier=None, name=None, id=None, + form_identifier=None, form_name=None, language=None, is_pokemon=None): + """Get one object from the database. + + session: The session to use (from pokedex.db.connect()) + table: The table to select from (such as pokedex.db.tables.Move) + + identifier: Identifier of the object + name: The name of the object + id: The ID number of the object + form_identifier: For pokemon, identifier of the form + form_name: For pokemon, name of the form + + language: A Language to use for name and form_name + is_pokemon: If true, specifies that the table should be treated as a + pokemon table (handling forms specially). If None and table is the + (unaliased) Pokemon, it is set to True. Otherwise, the pokemon forms + aren't handled. + + All conditions must match, so it's not a good idea to specify more than one + of identifier/name/id at once. + + If zero or more than one objects matching the criteria are found, the + appropriate SQLAlchemy exception is raised. + Exception: for pokemon, selects the form base unless form_* is given. + """ + + if is_pokemon is None: + is_pokemon = (table is tables.Pokemon) + + query = session.query(table) + + if identifier is not None: + query = query.filter_by(identifier=identifier) + + if name is not None: + query = filter_name(query, table, name, language) + + if id is not None: + query = query.filter_by(id=id) + + if form_identifier is not None or form_name is not None: + if is_pokemon: + query = query.join(table.unique_form) + if form_identifier is not None: + query = query.filter(tables.PokemonForm.identifier == + form_identifier) + if form_name is not None: + query = filter_name(query, table, form_name, language) + else: + raise ValueError( + "form_identifier and form_name only make sense for pokemon") + elif is_pokemon: + query = filter_base_forms(query) + + return query.one() + +### Helpers + +def filter_name(query, table, name, language): + """Filter a query by name, return the resulting query + + query: The query to filter + table: The table of named objects + name: The name to look for. May be a tuple of alternatives. + language: The language for "name", or None for the session default + """ + if language is None: + query = query.filter(table.name == name) + else: + names_table = table.names_table + query = query.join(names_table) + query = query.filter(names_table.foreign_id == table.id) + query = query.filter(names_table.local_language_id == language.id) + if isinstance(name, tuple): + query = query.filter(names_table.name in name) + else: + query = query.filter(names_table.name == name) + return query + +def filter_base_forms(query): + """Filter only base forms of pokemon, and return the resulting query + """ + query = query.filter(tables.Pokemon.forms.any()) + return query + +def order_by_name(query, table, language=None, *extra_languages): + """Order a query by name. + + query: The query to order + table: Table of the named objects + language: The language to order names by. If None, use the + connection default. + extra_languages: Extra languages to order by, should the translations for + `language` be incomplete (or ambiguous). + + Uses the identifier as a fallback ordering. + """ + if language is None: + query = query.outerjoin(table.names_local) + query = query.order_by(table.names_table.name) + else: + extra_languages = (language, ) + extra_languages + for language in extra_languages: + names_table = aliased(table.names_table) + query = query.outerjoin(names_table) + query = query.filter(names_table.foreign_id == table.id) + query = query.filter(names_table.local_language_id == language.id) + query = query.order_by(names_table.name) + query = query.order_by(table.identifier) + return query + +_name = object() +def get_all(session, table, order=_name): + """Shortcut to get an ordered query from table. + + session: The session to use + table: The table to select from + order: A clause to order by, or None for no ordering. + The default is to order by name; this can also be specified explicitly + with the table's name property (e.g. tables.Pokemon.name). Be aware + that the query's order_by will not order by name this way. + """ + query = session.query(table) + if order is table.name or order is _name: + query = order_by_name(query, table) + elif order is not None: + query = query.order_by(order) + return query + +### Shortcuts + +def pokemon(session, order=tables.Pokemon.id): + """Return a query for all base form pokemon, ordered by id by default + + See get_all for the session and order arguments (but note the default for + pokemon is to order by id). + """ + query = get_all(session, tables.Pokemon, order=order) + query = query.filter(tables.Pokemon.forms.any()) + return query + +def moves(session, order=_name): + """Return a query for moves in the mainline games (i.e. no Shadow moves) + + See get_all for the session and order arguments. + """ + return get_all(session, tables.Move, order=order).filter(tables.Move.id < 10000) + +def types(session, order=_name): + """Return a query for sane types (i.e. not ???, Shadow) + + See get_all for the session and order arguments. + """ + return get_all(session, tables.Type, order=order).filter(tables.Type.id < 10000) + +def items(session, order=_name): + """Return a query for items + + See get_all for the session and order arguments. + """ + return get_all(session, tables.Item, order=order) From f271812cf79193b82a2e122304ccd5f906d208e1 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 14 Apr 2011 19:24:30 +0300 Subject: [PATCH 5/6] Break "simple" query functions out of pokedex.util.get --- pokedex/tests/test_util.py | 32 +++++++++--------- pokedex/util/get.py | 58 ++------------------------------- pokedex/util/simple.py | 66 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 71 deletions(-) create mode 100644 pokedex/util/simple.py diff --git a/pokedex/tests/test_util.py b/pokedex/tests/test_util.py index 35a3fb1..0fc3b7a 100644 --- a/pokedex/tests/test_util.py +++ b/pokedex/tests/test_util.py @@ -3,7 +3,7 @@ from nose.tools import * import unittest from pokedex.db import connect, tables -from pokedex.util import get +from pokedex.util import get, simple session = connect() @@ -46,30 +46,30 @@ def test_get_pokemon_other_form_identifier(): if poke.form.unique_pokemon_id: assert poke.form.identifier == form_identifier -def test_pokemon(): - pokemon = get.pokemon(session) - assert pokemon[0].identifier == 'bulbasaur' - assert pokemon[-1].identifier == 'genesect' - -def test_pokemon_by_name(): - pokemon = get.pokemon(session, order=tables.Pokemon.name) - assert pokemon[0].identifier == 'abomasnow' - assert pokemon[-1].identifier == 'zweilous' - def test_types_french_order(): french = get.get(session, tables.Language, 'fr') - types = get.types(session, order=None) + types = session.query(tables.Type).filter(tables.Type.id < 10000) types = list(get.order_by_name(types, tables.Type, language=french)) assert types[0].name_map[french] == 'Acier', types[0].name_map[french] assert types[-1].name_map[french] == 'Vol', types[-1].name_map[french] -def test_moves(): - moves = get.moves(session) +def test_simple_pokemon(): + pokemon = simple.pokemon(session) + assert pokemon[0].identifier == 'bulbasaur' + assert pokemon[-1].identifier == 'genesect' + +def test_simple_types(): + types = simple.types(session) + assert types[0].identifier == 'bug' + assert types[-1].identifier == 'water' + +def test_simple_moves(): + moves = simple.moves(session) assert moves[0].identifier == 'absorb' assert moves[-1].identifier == 'zen-headbutt' -def test_items(): - items = get.items(session) +def test_simple_items(): + items = simple.items(session) assert items[0].identifier == 'ability-urge' assert items[-1].identifier == 'zoom-lens' diff --git a/pokedex/util/get.py b/pokedex/util/get.py index ab5e6b8..9131654 100644 --- a/pokedex/util/get.py +++ b/pokedex/util/get.py @@ -1,15 +1,14 @@ -"""Provides simple functions for common queries +"""Helpers for common ways to work with pokedex queries These include identifier- and name-based lookup, filtering out base forms -of pokemon, ordering by name, and getting canonical "pokedex" lists (i.e. -ordered and without cruft like alternate pokemon forms or Shadow moves) +of pokemon, and filtering/ordering by name. """ from sqlalchemy.orm import aliased from pokedex.db import tables -### Getters +### Getter def get(session, table, identifier=None, name=None, id=None, form_identifier=None, form_name=None, language=None, is_pokemon=None): @@ -122,54 +121,3 @@ def order_by_name(query, table, language=None, *extra_languages): query = query.order_by(names_table.name) query = query.order_by(table.identifier) return query - -_name = object() -def get_all(session, table, order=_name): - """Shortcut to get an ordered query from table. - - session: The session to use - table: The table to select from - order: A clause to order by, or None for no ordering. - The default is to order by name; this can also be specified explicitly - with the table's name property (e.g. tables.Pokemon.name). Be aware - that the query's order_by will not order by name this way. - """ - query = session.query(table) - if order is table.name or order is _name: - query = order_by_name(query, table) - elif order is not None: - query = query.order_by(order) - return query - -### Shortcuts - -def pokemon(session, order=tables.Pokemon.id): - """Return a query for all base form pokemon, ordered by id by default - - See get_all for the session and order arguments (but note the default for - pokemon is to order by id). - """ - query = get_all(session, tables.Pokemon, order=order) - query = query.filter(tables.Pokemon.forms.any()) - return query - -def moves(session, order=_name): - """Return a query for moves in the mainline games (i.e. no Shadow moves) - - See get_all for the session and order arguments. - """ - return get_all(session, tables.Move, order=order).filter(tables.Move.id < 10000) - -def types(session, order=_name): - """Return a query for sane types (i.e. not ???, Shadow) - - See get_all for the session and order arguments. - """ - return get_all(session, tables.Type, order=order).filter(tables.Type.id < 10000) - -def items(session, order=_name): - """Return a query for items - - See get_all for the session and order arguments. - """ - return get_all(session, tables.Item, order=order) diff --git a/pokedex/util/simple.py b/pokedex/util/simple.py new file mode 100644 index 0000000..46806f7 --- /dev/null +++ b/pokedex/util/simple.py @@ -0,0 +1,66 @@ +"""Simple lists of things for simple scripts + +If you want to get a pokemon list, and you don't want it to include three +Wormadams and a whole bunch of Rotoms because of how the database is +structured, this module is for you. + +The returned queries basically contain what a pokedex would show you. +You should make no other assumptions about them. + +If you need to make assumptions, feel free to use these functions as examples +of what to watch out for. +""" + +from pokedex.db import tables +from pokedex.util.get import filter_base_forms, order_by_name + +def pokemon(session): + """Get a "sane" list of pokemon + + WARNING: The result of this function is not very well defined. + If you want something specific, build that specific query yourself. + + Currently, all base forms are returned, in evolution-preserving order + """ + query = session.query(tables.Pokemon) + query = query.order_by(tables.Pokemon.order) + query = filter_base_forms(query) + return query + +def moves(session): + """Get a "sane" list of moves + + WARNING: The result of this function is not very well defined. + If you want something specific, build that specific query yourself. + + Currently, moves from mainline games are returned, sored by name + """ + query = session.query(tables.Move) + query = order_by_name(query, tables.Move) + query = query.filter(tables.Move.id < 10000) + return query + +def types(session): + """Get a "sane" list of types + + WARNING: The result of this function is not very well defined. + If you want something specific, build that specific query yourself. + + Currently, generation V types are returned, sored by name + """ + query = session.query(tables.Type) + query = order_by_name(query, tables.Type) + query = query.filter(tables.Type.id < 10000) + return query + +def items(session): + """Get a "sane" list of items + + WARNING: The result of this function is not very well defined. + If you want something specific, build that specific query yourself. + + Currently, items are sored by name + """ + query = session.query(tables.Item) + query = order_by_name(query, tables.Item) + return query From 69140a88f5c08d9c61faf1f10bf5b2dd2ef93298 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 14 Apr 2011 20:08:05 +0300 Subject: [PATCH 6/6] Move the rest of pokedex.util.get to pokedex.db.util --- pokedex/{util/get.py => db/util.py} | 0 pokedex/tests/test_util.py | 24 ++++++++++++------------ pokedex/util/simple.py | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) rename pokedex/{util/get.py => db/util.py} (100%) diff --git a/pokedex/util/get.py b/pokedex/db/util.py similarity index 100% rename from pokedex/util/get.py rename to pokedex/db/util.py diff --git a/pokedex/tests/test_util.py b/pokedex/tests/test_util.py index 0fc3b7a..a528d85 100644 --- a/pokedex/tests/test_util.py +++ b/pokedex/tests/test_util.py @@ -2,54 +2,54 @@ from nose.tools import * import unittest -from pokedex.db import connect, tables -from pokedex.util import get, simple +from pokedex.db import connect, tables, util +from pokedex.util import simple session = connect() def test_get_item_identifier(): - item = get.get(session, tables.Item, identifier='master-ball') + item = util.get(session, tables.Item, identifier='master-ball') assert item.name == 'Master Ball' def test_get_item_name(): - item = get.get(session, tables.Item, name='Awakening') + item = util.get(session, tables.Item, name='Awakening') assert item.name == 'Awakening' def test_get_english_by_identifier(): - language = get.get(session, tables.Language, 'en') + language = util.get(session, tables.Language, 'en') assert language.name == 'English' def test_get_pokemon_baseform_identifier(): for identifier in 'burmy shaymin unown cresselia'.split(): - poke = get.get(session, tables.Pokemon, identifier=identifier) + poke = util.get(session, tables.Pokemon, identifier=identifier) assert poke.identifier == identifier assert poke.is_base_form def test_get_pokemon_baseform_name(): for name in 'Burmy Shaymin Unown Cresselia'.split(): - poke = get.get(session, tables.Pokemon, name=name) + poke = util.get(session, tables.Pokemon, name=name) assert poke.name == name assert poke.is_base_form def test_get_pokemon_baseform_name_explicit_language(): - french = get.get(session, tables.Language, 'fr') + french = util.get(session, tables.Language, 'fr') for name in 'Cheniti Shaymin Zarbi Cresselia'.split(): - poke = get.get(session, tables.Pokemon, name=name, language=french) + poke = util.get(session, tables.Pokemon, name=name, language=french) assert poke.name_map[french] == name, poke.name_map[french] assert poke.is_base_form def test_get_pokemon_other_form_identifier(): for ii in 'wormadam/trash shaymin/sky shaymin/land'.split(): pokemon_identifier, form_identifier = ii.split('/') - poke = get.get(session, tables.Pokemon, identifier=pokemon_identifier, form_identifier=form_identifier) + poke = util.get(session, tables.Pokemon, identifier=pokemon_identifier, form_identifier=form_identifier) assert poke.identifier == pokemon_identifier if poke.form.unique_pokemon_id: assert poke.form.identifier == form_identifier def test_types_french_order(): - french = get.get(session, tables.Language, 'fr') + french = util.get(session, tables.Language, 'fr') types = session.query(tables.Type).filter(tables.Type.id < 10000) - types = list(get.order_by_name(types, tables.Type, language=french)) + types = list(util.order_by_name(types, tables.Type, language=french)) assert types[0].name_map[french] == 'Acier', types[0].name_map[french] assert types[-1].name_map[french] == 'Vol', types[-1].name_map[french] diff --git a/pokedex/util/simple.py b/pokedex/util/simple.py index 46806f7..d801f2b 100644 --- a/pokedex/util/simple.py +++ b/pokedex/util/simple.py @@ -12,7 +12,7 @@ of what to watch out for. """ from pokedex.db import tables -from pokedex.util.get import filter_base_forms, order_by_name +from pokedex.db.util import filter_base_forms, order_by_name def pokemon(session): """Get a "sane" list of pokemon