mirror of
https://github.com/veekun/pokedex.git
synced 2024-08-20 18:16:34 +00:00
Improve schema ergonomics a bit, especially around slicing
The previous approach was moving towards having each attribute on a locus do multiple different things, depending on context, and I think that was headed towards being a mess. This idea is to have actual locus objects be dumb containers, and have various wrappers that call methods on the attributes to do interesting work.
This commit is contained in:
parent
aa92ccb7ad
commit
7f7cca6c58
1 changed files with 74 additions and 44 deletions
|
@ -105,12 +105,17 @@ class LocusMeta(type):
|
||||||
sup.__init_subclass__(**kwargs)
|
sup.__init_subclass__(**kwargs)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def __init__(cls, *args, **kwargs):
|
||||||
|
super().__init__(*args)
|
||||||
|
|
||||||
|
|
||||||
class Locus(metaclass=LocusMeta):
|
class Locus(metaclass=LocusMeta):
|
||||||
_attributes = {}
|
_attributes = {}
|
||||||
|
|
||||||
def __init_subclass__(cls, **kwargs):
|
def __init_subclass__(cls, *, sliced_by=(), **kwargs):
|
||||||
# super().__init_subclass__(**kwargs)
|
# super().__init_subclass__(**kwargs)
|
||||||
|
# TODO how... do i... make an attribute on the class that's not inherited by instances
|
||||||
|
cls.sliced_by = sliced_by
|
||||||
cls._attributes = cls._attributes.copy()
|
cls._attributes = cls._attributes.copy()
|
||||||
for key, value in cls.__dict__.items():
|
for key, value in cls.__dict__.items():
|
||||||
if isinstance(value, _Attribute):
|
if isinstance(value, _Attribute):
|
||||||
|
@ -132,7 +137,7 @@ class Locus(metaclass=LocusMeta):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class VersionedLocus(Locus):
|
class VersionedLocus(Locus, sliced_by=['game']):
|
||||||
def __init_subclass__(cls, **kwargs):
|
def __init_subclass__(cls, **kwargs):
|
||||||
super(VersionedLocus, cls).__init_subclass__(**kwargs)
|
super(VersionedLocus, cls).__init_subclass__(**kwargs)
|
||||||
|
|
||||||
|
@ -145,6 +150,10 @@ class VersionedLocus(Locus):
|
||||||
|
|
||||||
cls._slices = {}
|
cls._slices = {}
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Loci definitions
|
||||||
|
|
||||||
# TODO seems to me that each of these, regardless of whether they have any
|
# TODO seems to me that each of these, regardless of whether they have any
|
||||||
# additional data attached or not, are restricted to a fixed extra-game-ular
|
# additional data attached or not, are restricted to a fixed extra-game-ular
|
||||||
# list of identifiers
|
# list of identifiers
|
||||||
|
@ -158,9 +167,6 @@ Pokedex = _ForwardDeclaration()
|
||||||
|
|
||||||
|
|
||||||
class Pokémon(VersionedLocus):
|
class Pokémon(VersionedLocus):
|
||||||
# TODO version, language. but those are kind of meta-fields; do they need
|
|
||||||
# treating specially?
|
|
||||||
|
|
||||||
name = _Localized(str)
|
name = _Localized(str)
|
||||||
|
|
||||||
types = _List(Type, min=1, max=2)
|
types = _List(Type, min=1, max=2)
|
||||||
|
@ -199,6 +205,41 @@ class Pokémon(VersionedLocus):
|
||||||
Pokemon = Pokémon
|
Pokemon = Pokémon
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# The repository class, primary interface to the data
|
||||||
|
|
||||||
|
class LocusReader:
|
||||||
|
def __init__(self, identifier, locus, **kwargs):
|
||||||
|
self.identifier = identifier
|
||||||
|
self.locus = locus
|
||||||
|
# TODO what is kwargs here? in this case we really want a slice, right...?
|
||||||
|
|
||||||
|
def __getattr__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __dir__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class QuantumProperty:
|
||||||
|
def __init__(self, qlocus, attr):
|
||||||
|
self.qlocus = qlocus
|
||||||
|
self.attr = attr
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return repr({key: getattr(locus, self.attr) for (key, locus) in self.qlocus.locus_map.items()})
|
||||||
|
|
||||||
|
class QuantumLocusReader:
|
||||||
|
def __init__(self, identifier, locus_cls, locus_map):
|
||||||
|
self.identifier = identifier
|
||||||
|
self.locus_cls = locus_cls
|
||||||
|
self.locus_map = locus_map
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return QuantumProperty(self, attr)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<{}*: {}>".format(self.locus_cls.__name__, self.identifier)
|
||||||
|
|
||||||
class Repository:
|
class Repository:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# type -> identifier -> object
|
# type -> identifier -> object
|
||||||
|
@ -206,28 +247,27 @@ class Repository:
|
||||||
# type -> property -> value -> list of objects
|
# type -> property -> value -> list of objects
|
||||||
self.index = defaultdict(lambda: defaultdict(lambda: defaultdict(set)))
|
self.index = defaultdict(lambda: defaultdict(lambda: defaultdict(set)))
|
||||||
|
|
||||||
def add(self, obj):
|
def add(self, identifier, locus, **kwargs):
|
||||||
|
# TODO kwargs are used for slicing, e.g. a pokemon has a game, but this needs some rigid definition
|
||||||
# TODO this should be declared by the type itself, obviously
|
# TODO this should be declared by the type itself, obviously
|
||||||
cls = type(obj)
|
cls = type(locus)
|
||||||
# TODO both branches here should check for duplicates
|
_basket = self.objects[cls].setdefault(identifier, {})
|
||||||
if isinstance(obj, Slice):
|
# TODO so in the case of slicing (which is most loci), we don't
|
||||||
cls = cls.base_class
|
# actually want to store a single object, but a sliced-up collection of
|
||||||
if obj.identifier not in self.objects[cls]:
|
# them (indicated by kwargs). but then it's kind of up in the air what
|
||||||
glom = cls()
|
# we'll actually get /back/ when we go to fetch that object, and that
|
||||||
glom.identifier = obj.identifier
|
# is unsatisfying to me. i could make this a list of "baskets", which
|
||||||
self.objects[cls][obj.identifier] = glom
|
# may hold one object or a number of slices, but i'm not sure how i
|
||||||
else:
|
# feel about that; might have to just see how other loci work out.
|
||||||
glom = self.objects[cls][obj.identifier]
|
# TODO either way, this is very hardcoded and needs to not be
|
||||||
# TODO this... feels special-cased, but i guess, it is?
|
_basket[kwargs['game']] = locus
|
||||||
glom._slices[obj.game] = obj
|
|
||||||
else:
|
|
||||||
self.objects[cls][obj.identifier] = obj
|
|
||||||
# TODO this is more complex now that names are multi-language
|
# TODO this is more complex now that names are multi-language
|
||||||
#self.index[cls][cls.name][obj.name].add(obj)
|
#self.index[cls][cls.name][locus.name].add(locus)
|
||||||
|
|
||||||
def fetch(self, cls, identifier):
|
def fetch(self, cls, identifier):
|
||||||
# TODO wrap in a... multi-thing
|
# TODO wrap in a... multi-thing
|
||||||
return self.objects[cls][identifier]
|
#return self.objects[cls][identifier]
|
||||||
|
return QuantumLocusReader(identifier, cls, self.objects[cls][identifier])
|
||||||
|
|
||||||
|
|
||||||
# TODO clean this garbage up -- better way of iterating the type, actually work for something other than pokemon...
|
# TODO clean this garbage up -- better way of iterating the type, actually work for something other than pokemon...
|
||||||
|
@ -247,7 +287,7 @@ def _dump_locus(locus):
|
||||||
|
|
||||||
@POKEDEX_TYPES.loader('pokemon', version=None)
|
@POKEDEX_TYPES.loader('pokemon', version=None)
|
||||||
def _load_locus(data, version):
|
def _load_locus(data, version):
|
||||||
cls = Pokemon.Sliced
|
cls = Pokémon
|
||||||
# TODO wrap with a writer thing?
|
# TODO wrap with a writer thing?
|
||||||
obj = cls()
|
obj = cls()
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
|
@ -258,33 +298,23 @@ def _load_locus(data, version):
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
def _temp_main():
|
def load_repository():
|
||||||
repository = Repository()
|
repository = Repository()
|
||||||
|
|
||||||
# just testing for now
|
# just testing for now
|
||||||
cam = camel.Camel([POKEDEX_TYPES])
|
cam = camel.Camel([POKEDEX_TYPES])
|
||||||
PATH = 'pokedex/data/ww-red/pokemon.yaml'
|
for game in ('jp-red', 'jp-green', 'jp-blue', 'ww-red', 'ww-blue', 'yellow'):
|
||||||
with open(PATH) as f:
|
path = "pokedex/data/{}/pokemon.yaml".format(game)
|
||||||
|
with open(path) as f:
|
||||||
all_pokemon = cam.load(f.read())
|
all_pokemon = cam.load(f.read())
|
||||||
for identifier, pokemon in all_pokemon.items():
|
for identifier, pokemon in all_pokemon.items():
|
||||||
# TODO i don't reeeally like this, but configuring a camel to do it
|
repository.add(identifier, pokemon, game=game)
|
||||||
# is a little unwieldy
|
|
||||||
pokemon.game = 'ww-red'
|
|
||||||
# TODO this in particular seems extremely clumsy, but identifiers ARE fundamentally keys...
|
|
||||||
pokemon.identifier = identifier
|
|
||||||
|
|
||||||
repository.add(pokemon)
|
return repository, all_pokemon
|
||||||
PATH = 'pokedex/data/ww-blue/pokemon.yaml'
|
|
||||||
with open(PATH) as f:
|
|
||||||
all_pokemon = cam.load(f.read())
|
|
||||||
for identifier, pokemon in all_pokemon.items():
|
|
||||||
# TODO i don't reeeally like this, but configuring a camel to do it
|
|
||||||
# is a little unwieldy
|
|
||||||
pokemon.game = 'ww-blue'
|
|
||||||
# TODO this in particular seems extremely clumsy, but identifiers ARE fundamentally keys...
|
|
||||||
pokemon.identifier = identifier
|
|
||||||
|
|
||||||
repository.add(pokemon)
|
|
||||||
|
def _temp_main():
|
||||||
|
repository, all_pokemon = load_repository()
|
||||||
|
|
||||||
# TODO NEXT TODO
|
# TODO NEXT TODO
|
||||||
# - how does the composite object work, exactly? eevee.name.single? eevee.name.latest? no, name needs a language...
|
# - how does the composite object work, exactly? eevee.name.single? eevee.name.latest? no, name needs a language...
|
||||||
|
|
Loading…
Reference in a new issue