diff --git a/co3/co3.py b/co3/co3.py index 37655fd..9283a1b 100644 --- a/co3/co3.py +++ b/co3/co3.py @@ -316,7 +316,8 @@ class CO3(metaclass=FormatRegistryMeta): if args is None: args = [] 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)] if key not in self.key_registry: @@ -345,7 +346,8 @@ class CO3(metaclass=FormatRegistryMeta): result = method(self, *args, **kwargs) - self._collate_cache[(key, group)] = result + if pure_compose: + self._collate_cache[(key, group)] = result return result diff --git a/co3/databases/sql.py b/co3/databases/sql.py index 1222589..05bab40 100644 --- a/co3/databases/sql.py +++ b/co3/databases/sql.py @@ -9,7 +9,7 @@ from co3.engines import SQLEngine 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; ``TabluarAccessor[Self, C]`` is of type ``type[RelationalAccessor[Self, C]]`` diff --git a/co3/mapper.py b/co3/mapper.py index 08795cb..11ce59b 100644 --- a/co3/mapper.py +++ b/co3/mapper.py @@ -35,6 +35,7 @@ from inspect import signature from typing import Callable, Any from collections import defaultdict +from co3 import util from co3.co3 import CO3 from co3.schema import Schema from co3.collector import Collector @@ -247,20 +248,23 @@ class Mapper[C: Component]: for group_name, group_method in group_dict.items(): 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 # pass to CO3.collate to call the method key_collation_data = {} 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( - f'Equivalence class "{collation_groups}" yielded no data, skipping' + f'Method equivalence class "{collation_groups}" yielded ' + + 'non-dict-like result, skipping' ) continue + key_method_collation_data = util.types.dictlike_to_dict(collation_result) + for collation_group in collation_groups: # gather connective data for collation components # -> 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.coll_compose_map = coll_compose_map - self.type_compose_cache = {} + self._compose_cache = {} def attach(self, *args, **kwargs): - self.type_compose_cache = {} + self._compose_cache = {} super().attach(*args, **kwargs) def attach_many(self, *args, **kwargs): - self.type_compose_cache = {} + self._compose_cache = {} super().attach_many(*args, **kwargs) def compose( self, - co3_ref: CO3 | type[CO3], - groups: list[str] | None = None, - *compose_args, - **compose_kwargs, + co3_ref : CO3 | type[CO3], + groups : list[str] | None = None, + compose_args : list | None = None, + compose_kwargs : dict | None = None, ): ''' 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): type_ref = co3_ref.__class__ - if type_ref in self.type_compose_cache: - return self.type_compose_cache[type_ref] + if groups is None: groups = [] + 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 last_attr_comp = None @@ -477,39 +487,39 @@ class ComposableMapper[C: ComposableComponent](Mapper[C]): # compose horizontally with components from provided action groups coll_list = [] - if groups is not None: - for group in groups: - coll_comp = self.get_coll_comp(_cls, group=group) + for group in groups: + coll_comp = self.get_coll_comp(_cls, group=group) - if coll_comp is None: - continue + if coll_comp is None: + continue - # valid collation comps added to coll_list, to be passed to the - # coll_map in the next iteration - coll_list.append(coll_comp) + # valid collation comps added to coll_list, to be passed to the + # coll_map in the next iteration + coll_list.append(coll_comp) - # note how the join condition is specified using the non-composite - # `attr_comp` and new `coll_comp`; the composite doesn't typically - # have the same attribute access and needs a ref to a specific comp - if len(signature(self.coll_compose_map).parameters) > 2: - compose_condition = self.coll_compose_map( - attr_comp, - coll_comp, - last_coll_comps - ) - else: - compose_condition = self.coll_compose_map(attr_comp, coll_comp) - - comp_agg = comp_agg.compose( + # note how the join condition is specified using the non-composite + # `attr_comp` and new `coll_comp`; the composite doesn't typically + # have the same attribute access and needs a ref to a specific comp + if len(signature(self.coll_compose_map).parameters) > 2: + compose_condition = self.coll_compose_map( + attr_comp, coll_comp, - compose_condition, - *compose_args, - **compose_kwargs, + last_coll_comps ) + 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_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 diff --git a/co3/util/__init__.py b/co3/util/__init__.py index 9ba947c..da83426 100644 --- a/co3/util/__init__.py +++ b/co3/util/__init__.py @@ -1,2 +1,3 @@ from co3.util import db from co3.util import regex +from co3.util import types diff --git a/co3/util/types.py b/co3/util/types.py index 6e96983..7fe2b4a 100644 --- a/co3/util/types.py +++ b/co3/util/types.py @@ -1,6 +1,45 @@ from typing import TypeVar +from collections import namedtuple +from dataclasses import is_dataclass, asdict import sqlalchemy as sa - +# custom types 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 {}