add caching to compose and collate methods, add wider collation result support

This commit is contained in:
Sam G. 2024-05-09 00:38:10 -07:00
parent 545c20e26f
commit 557df15d81
5 changed files with 94 additions and 42 deletions

View File

@ -316,7 +316,8 @@ class CO3(metaclass=FormatRegistryMeta):
if args is None: args = [] if args is None: args = []
if kwargs is None: kwargs = {} if kwargs is None: kwargs = {}
if (key, group) in self._collate_cache: pure_compose = not (args or kwargs)
if (key, group) in self._collate_cache and pure_compose:
return self._collate_cache[(key, group)] return self._collate_cache[(key, group)]
if key not in self.key_registry: if key not in self.key_registry:
@ -345,7 +346,8 @@ class CO3(metaclass=FormatRegistryMeta):
result = method(self, *args, **kwargs) result = method(self, *args, **kwargs)
self._collate_cache[(key, group)] = result if pure_compose:
self._collate_cache[(key, group)] = result
return result return result

View File

@ -9,7 +9,7 @@ from co3.engines import SQLEngine
from co3.components import Relation, SQLTable from co3.components import Relation, SQLTable
class RelationalDatabase[C: RelationR](Database): class RelationalDatabase[C: Relation](Database[C]):
''' '''
accessor/manager assignments satisfy supertype's type settings; accessor/manager assignments satisfy supertype's type settings;
``TabluarAccessor[Self, C]`` is of type ``type[RelationalAccessor[Self, C]]`` ``TabluarAccessor[Self, C]`` is of type ``type[RelationalAccessor[Self, C]]``

View File

@ -35,6 +35,7 @@ from inspect import signature
from typing import Callable, Any from typing import Callable, Any
from collections import defaultdict from collections import defaultdict
from co3 import util
from co3.co3 import CO3 from co3.co3 import CO3
from co3.schema import Schema from co3.schema import Schema
from co3.collector import Collector from co3.collector import Collector
@ -247,20 +248,23 @@ class Mapper[C: Component]:
for group_name, group_method in group_dict.items(): for group_name, group_method in group_dict.items():
method_groups[group_method].append(group_name) method_groups[group_method].append(group_name)
logger.debug(f'Equivalence classes: "{list(method_groups.values())}"') logger.debug(f'Method equivalence classes: "{list(method_groups.values())}"')
# collate for method equivalence classes; only need on representative group to # collate for method equivalence classes; only need on representative group to
# pass to CO3.collate to call the method # pass to CO3.collate to call the method
key_collation_data = {} key_collation_data = {}
for collation_method, collation_groups in method_groups.items(): for collation_method, collation_groups in method_groups.items():
key_method_collation_data = obj.collate(key, group=collation_groups[0]) collation_result = obj.collate(key, group=collation_groups[0])
if key_method_collation_data is None: if not util.types.is_dictlike(collation_result):
logger.debug( logger.debug(
f'Equivalence class "{collation_groups}" yielded no data, skipping' f'Method equivalence class "{collation_groups}" yielded '
+ 'non-dict-like result, skipping'
) )
continue continue
key_method_collation_data = util.types.dictlike_to_dict(collation_result)
for collation_group in collation_groups: for collation_group in collation_groups:
# gather connective data for collation components # gather connective data for collation components
# -> we do this here as it's obj dependent # -> we do this here as it's obj dependent
@ -406,24 +410,24 @@ class ComposableMapper[C: ComposableComponent](Mapper[C]):
self.attr_compose_map = attr_compose_map self.attr_compose_map = attr_compose_map
self.coll_compose_map = coll_compose_map self.coll_compose_map = coll_compose_map
self.type_compose_cache = {} self._compose_cache = {}
def attach(self, *args, **kwargs): def attach(self, *args, **kwargs):
self.type_compose_cache = {} self._compose_cache = {}
super().attach(*args, **kwargs) super().attach(*args, **kwargs)
def attach_many(self, *args, **kwargs): def attach_many(self, *args, **kwargs):
self.type_compose_cache = {} self._compose_cache = {}
super().attach_many(*args, **kwargs) super().attach_many(*args, **kwargs)
def compose( def compose(
self, self,
co3_ref: CO3 | type[CO3], co3_ref : CO3 | type[CO3],
groups: list[str] | None = None, groups : list[str] | None = None,
*compose_args, compose_args : list | None = None,
**compose_kwargs, compose_kwargs : dict | None = None,
): ):
''' '''
Compose tables up the type hierarchy, and across through action groups to Compose tables up the type hierarchy, and across through action groups to
@ -444,8 +448,14 @@ class ComposableMapper[C: ComposableComponent](Mapper[C]):
if isinstance(co3_ref, CO3): if isinstance(co3_ref, CO3):
type_ref = co3_ref.__class__ type_ref = co3_ref.__class__
if type_ref in self.type_compose_cache: if groups is None: groups = []
return self.type_compose_cache[type_ref] if compose_args is None: compose_args = []
if compose_kwargs is None: compose_kwargs = {}
idx_tup = (type_ref, tuple(groups))
pure_compose = not (compose_args or compose_kwargs)
if idx_tup in self._compose_cache and pure_compose:
return self._compose_cache[idx_tup]
comp_agg = None comp_agg = None
last_attr_comp = None last_attr_comp = None
@ -477,39 +487,39 @@ class ComposableMapper[C: ComposableComponent](Mapper[C]):
# compose horizontally with components from provided action groups # compose horizontally with components from provided action groups
coll_list = [] coll_list = []
if groups is not None: for group in groups:
for group in groups: coll_comp = self.get_coll_comp(_cls, group=group)
coll_comp = self.get_coll_comp(_cls, group=group)
if coll_comp is None: if coll_comp is None:
continue continue
# valid collation comps added to coll_list, to be passed to the # valid collation comps added to coll_list, to be passed to the
# coll_map in the next iteration # coll_map in the next iteration
coll_list.append(coll_comp) coll_list.append(coll_comp)
# note how the join condition is specified using the non-composite # note how the join condition is specified using the non-composite
# `attr_comp` and new `coll_comp`; the composite doesn't typically # `attr_comp` and new `coll_comp`; the composite doesn't typically
# have the same attribute access and needs a ref to a specific comp # have the same attribute access and needs a ref to a specific comp
if len(signature(self.coll_compose_map).parameters) > 2: if len(signature(self.coll_compose_map).parameters) > 2:
compose_condition = self.coll_compose_map( compose_condition = self.coll_compose_map(
attr_comp, attr_comp,
coll_comp,
last_coll_comps
)
else:
compose_condition = self.coll_compose_map(attr_comp, coll_comp)
comp_agg = comp_agg.compose(
coll_comp, coll_comp,
compose_condition, last_coll_comps
*compose_args,
**compose_kwargs,
) )
else:
compose_condition = self.coll_compose_map(attr_comp, coll_comp)
comp_agg = comp_agg.compose(
coll_comp,
compose_condition,
*compose_args,
**compose_kwargs,
)
last_attr_comp = attr_comp last_attr_comp = attr_comp
last_coll_comps = coll_list last_coll_comps = coll_list
self.type_compose_cache[type_ref] = comp_agg if not pure_compose:
self._compose_cache[idx_tup] = comp_agg
return comp_agg return comp_agg

View File

@ -1,2 +1,3 @@
from co3.util import db from co3.util import db
from co3.util import regex from co3.util import regex
from co3.util import types

View File

@ -1,6 +1,45 @@
from typing import TypeVar from typing import TypeVar
from collections import namedtuple
from dataclasses import is_dataclass, asdict
import sqlalchemy as sa import sqlalchemy as sa
# custom types
SQLTableLike = TypeVar('SQLTableLike', bound=sa.Table | sa.Subquery | sa.Join) SQLTableLike = TypeVar('SQLTableLike', bound=sa.Table | sa.Subquery | sa.Join)
# type checking/conversion methods
def is_dataclass_instance(obj) -> bool:
return is_dataclass(obj) and not isinstance(obj, type)
def is_namedtuple_instance(obj) -> bool:
return (
isinstance(obj, tuple) and
hasattr(obj, '_asdict') and
hasattr(obj, '_fields')
)
def is_dictlike(obj) -> bool:
if isinstance(obj, dict):
return True
elif is_dataclass_instance(obj):
return True
elif is_namedtuple_instance(obj):
return True
return False
def dictlike_to_dict(obj) -> dict:
'''
Attempt to convert provided object to dict. Will return dict no matter what, including
an empty dict if not dict-like. Consider using ``is_dictlike`` to determine if this
method should be called.
'''
if isinstance(obj, dict):
return obj
elif is_dataclass_instance(obj):
return asdict(obj)
elif is_namedtuple_instance(obj):
return obj._asdict()
return {}