refactor: rename group -> key_obj

This commit is contained in:
relikd
2022-11-22 19:41:07 +01:00
parent 390d44a02c
commit 521ac39a83
4 changed files with 68 additions and 60 deletions

View File

@@ -2,8 +2,9 @@ from inifile import IniFile
from lektor.environment import Expression from lektor.environment import Expression
from lektor.context import Context from lektor.context import Context
from lektor.utils import slugify as _slugify from lektor.utils import slugify as _slugify
from typing import TYPE_CHECKING from typing import (
from typing import Set, Dict, Optional, Union, Any, List, Generator TYPE_CHECKING, Set, Dict, Optional, Union, Any, List, Generator
)
from .util import split_strip from .util import split_strip
if TYPE_CHECKING: if TYPE_CHECKING:
from lektor.sourceobj import SourceObject from lektor.sourceobj import SourceObject
@@ -44,14 +45,14 @@ class Config:
slug: Optional[str] = None, # default: "{attr}/{group}/index.html" slug: Optional[str] = None, # default: "{attr}/{group}/index.html"
template: Optional[str] = None, # default: "groupby-{attr}.html" template: Optional[str] = None, # default: "groupby-{attr}.html"
replace_none_key: Optional[str] = None, # default: None replace_none_key: Optional[str] = None, # default: None
key_map_fn: Optional[str] = None, # default: None key_obj_fn: Optional[str] = None, # default: None
) -> None: ) -> None:
self.key = key self.key = key
self.root = (root or '/').rstrip('/') or '/' self.root = (root or '/').rstrip('/') or '/'
self.slug = slug or (key + '/{key}/') # key = GroupBySource.key self.slug = slug or (key + '/{key}/') # key = GroupBySource.key
self.template = template or f'groupby-{self.key}.html' self.template = template or f'groupby-{self.key}.html'
self.replace_none_key = replace_none_key self.replace_none_key = replace_none_key
self.key_map_fn = key_map_fn self.key_obj_fn = key_obj_fn
# editable after init # editable after init
self.enabled = True self.enabled = True
self.dependencies = set() # type: Set[str] self.dependencies = set() # type: Set[str]
@@ -98,7 +99,7 @@ class Config:
def __repr__(self) -> str: def __repr__(self) -> str:
txt = '<GroupByConfig' txt = '<GroupByConfig'
for x in ['enabled', 'key', 'root', 'slug', 'template', 'key_map_fn']: for x in ['enabled', 'key', 'root', 'slug', 'template', 'key_obj_fn']:
txt += ' {}="{}"'.format(x, getattr(self, x)) txt += ' {}="{}"'.format(x, getattr(self, x))
txt += f' fields="{", ".join(self.fields)}"' txt += f' fields="{", ".join(self.fields)}"'
if self.order_by: if self.order_by:
@@ -114,7 +115,7 @@ class Config:
slug=cfg.get('slug'), slug=cfg.get('slug'),
template=cfg.get('template'), template=cfg.get('template'),
replace_none_key=cfg.get('replace_none_key'), replace_none_key=cfg.get('replace_none_key'),
key_map_fn=cfg.get('key_map_fn'), key_obj_fn=cfg.get('key_obj_fn'),
) )
@staticmethod @staticmethod
@@ -184,13 +185,13 @@ class Config:
expr = self._make_expression(cfg_slug, on=on, field='slug') expr = self._make_expression(cfg_slug, on=on, field='slug')
return expr.evaluate(on.pad, this=on, alt=on.alt) or None return expr.evaluate(on.pad, this=on, alt=on.alt) or None
def eval_key_map_fn(self, *, on: 'SourceObject', context: Dict) -> Any: def eval_key_obj_fn(self, *, on: 'SourceObject', context: Dict) -> Any:
''' '''
If `key_map_fn` is set, evaluate field expression. If `key_obj_fn` is set, evaluate field expression.
Note: The function does not check whether `key_map_fn` is set. Note: The function does not check whether `key_obj_fn` is set.
Return: A Generator result is automatically unpacked into a list. Return: A Generator result is automatically unpacked into a list.
''' '''
exp = self._make_expression(self.key_map_fn, on=on, field='key_map_fn') exp = self._make_expression(self.key_obj_fn, on=on, field='key_obj_fn')
with Context(pad=on.pad) as ctx: with Context(pad=on.pad) as ctx:
with ctx.gather_dependencies(self.dependencies.add): with ctx.gather_dependencies(self.dependencies.add):
res = exp.evaluate(on.pad, this=on, alt=on.alt, values=context) res = exp.evaluate(on.pad, this=on, alt=on.alt, values=context)

View File

@@ -1,5 +1,7 @@
from lektor.db import Page # isinstance from lektor.db import Page # isinstance
from typing import TYPE_CHECKING, Dict, List, NamedTuple, Optional, Iterable from typing import (
TYPE_CHECKING, NamedTuple, Dict, List, Any, Optional, Iterable
)
from .util import build_url from .util import build_url
from .vobj import VPATH, GroupBySource from .vobj import VPATH, GroupBySource
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -9,16 +11,16 @@ if TYPE_CHECKING:
class ResolverEntry(NamedTuple): class ResolverEntry(NamedTuple):
slug: str key: str
group: str key_obj: Any
config: 'Config' config: 'Config'
page: Optional[int] page: Optional[int]
def equals( def equals(
self, path: str, attribute: str, group: str, page: Optional[int] self, path: str, conf_key: str, vobj_key: str, page: Optional[int]
) -> bool: ) -> bool:
return self.slug == group \ return self.key == vobj_key \
and self.config.key == attribute \ and self.config.key == conf_key \
and self.config.root == path \ and self.config.root == path \
and self.page == page and self.page == page
@@ -53,9 +55,9 @@ class Resolver:
def add(self, vobj: GroupBySource) -> None: def add(self, vobj: GroupBySource) -> None:
''' Track new virtual object (only if slug is set). ''' ''' Track new virtual object (only if slug is set). '''
if vobj.slug: if vobj.slug:
# page_num = 1 overwrites page_num = None -> same url_path() # `page_num = 1` overwrites `page_num = None` -> same url_path()
self._data[vobj.url_path] = ResolverEntry( self._data[vobj.url_path] = ResolverEntry(
vobj.key, vobj.group, vobj.config, vobj.page_num) vobj.key, vobj.key_obj, vobj.config, vobj.page_num)
# ------------ # ------------
# Resolver # Resolver
@@ -68,15 +70,16 @@ class Resolver:
rv = self._data.get(build_url([node.url_path] + pieces)) rv = self._data.get(build_url([node.url_path] + pieces))
if rv: if rv:
return GroupBySource( return GroupBySource(
node, rv.slug, rv.page).finalize(rv.config, rv.group) node, rv.key, rv.page).finalize(rv.config, rv.key_obj)
return None return None
def resolve_virtual_path(self, node: 'SourceObject', pieces: List[str]) \ def resolve_virtual_path(self, node: 'SourceObject', pieces: List[str]) \
-> Optional[GroupBySource]: -> Optional[GroupBySource]:
''' Admin UI only: Prevent server error and null-redirect. ''' ''' Admin UI only: Prevent server error and null-redirect. '''
# format: /path/to/page@groupby/{config-key}/{vobj-key}/{page-num}
if isinstance(node, Page) and len(pieces) >= 2: if isinstance(node, Page) and len(pieces) >= 2:
path = node['_path'] # type: str path = node['_path'] # type: str
attr, grp, *optional_page = pieces conf_key, vobj_key, *optional_page = pieces
page = None page = None
if optional_page: if optional_page:
try: try:
@@ -84,7 +87,7 @@ class Resolver:
except ValueError: except ValueError:
pass pass
for rv in self._data.values(): for rv in self._data.values():
if rv.equals(path, attr, grp, page): if rv.equals(path, conf_key, vobj_key, page):
return GroupBySource( return GroupBySource(
node, rv.slug, rv.page).finalize(rv.config, rv.group) node, rv.key, rv.page).finalize(rv.config, rv.key_obj)
return None return None

View File

@@ -3,8 +3,9 @@ from lektor.context import get_ctx
from lektor.db import _CmpHelper from lektor.db import _CmpHelper
from lektor.environment import Expression from lektor.environment import Expression
from lektor.sourceobj import VirtualSourceObject # subclass from lektor.sourceobj import VirtualSourceObject # subclass
from typing import TYPE_CHECKING from typing import (
from typing import List, Any, Dict, Optional, Generator, Iterator, Iterable TYPE_CHECKING, List, Any, Dict, Optional, Generator, Iterator, Iterable
)
from .pagination import PaginationConfig from .pagination import PaginationConfig
from .query import FixedRecordsQuery from .query import FixedRecordsQuery
from .util import most_used_key, insert_before_ext, build_url, cached_property from .util import most_used_key, insert_before_ext, build_url, cached_property
@@ -25,31 +26,32 @@ class GroupBySource(VirtualSourceObject):
''' '''
Holds information for a single group/cluster. Holds information for a single group/cluster.
This object is accessible in your template file. This object is accessible in your template file.
Attributes: record, key, group, slug, children, config Attributes: record, key, key_obj, slug, children, config
''' '''
def __init__( def __init__(
self, self,
record: 'Record', record: 'Record',
slug: str, key: str,
page_num: Optional[int] = None page_num: Optional[int] = None
) -> None: ) -> None:
super().__init__(record) super().__init__(record)
self.key = slug
self.page_num = page_num
self._expr_fields = {} # type: Dict[str, Expression]
self.__children = [] # type: List[str] self.__children = [] # type: List[str]
self.__group_map = [] # type: List[Any] self.__key_obj_map = [] # type: List[Any]
self._expr_fields = {} # type: Dict[str, Expression]
self.key = key
self.page_num = page_num
def append_child(self, child: 'Record', group: Any) -> None: def append_child(self, child: 'Record', key_obj: Any) -> None:
if child not in self.__children: if child not in self.__children:
self.__children.append(child.path) self.__children.append(child.path)
# TODO: rename group to value # __key_obj_map is later used to find most used key_obj
# __group_map is later used to find most used group self.__key_obj_map.append(key_obj)
self.__group_map.append(group)
def _update_attr(self, key: str, value: Any) -> None: def _update_attr(self, key: str, value: Any) -> None:
''' Set or remove Jinja evaluated Expression field. ''' ''' Set or remove Jinja evaluated Expression field. '''
# TODO: instead we could evaluate the fields only once.
# But then we need to record_dependency() every successive access
if isinstance(value, Expression): if isinstance(value, Expression):
self._expr_fields[key] = value self._expr_fields[key] = value
try: try:
@@ -65,22 +67,22 @@ class GroupBySource(VirtualSourceObject):
# Evaluate Extra Fields # Evaluate Extra Fields
# ------------------------- # -------------------------
def finalize(self, config: 'Config', group: Optional[Any] = None) \ def finalize(self, config: 'Config', key_obj: Optional[Any] = None) \
-> 'GroupBySource': -> 'GroupBySource':
self.config = config self.config = config
# make a sorted children query # make a sorted children query
self._query = FixedRecordsQuery(self.pad, self.__children, self.alt) self._query = FixedRecordsQuery(self.pad, self.__children, self.alt)
self._query._order_by = config.order_by self._query._order_by = config.order_by
del self.__children del self.__children
# set group name # set indexed original value (can be: str, int, float, bool, obj)
self.group = group or most_used_key(self.__group_map) self.key_obj = key_obj or most_used_key(self.__key_obj_map)
del self.__group_map del self.__key_obj_map
# evaluate slug Expression # evaluate slug Expression
self.slug = config.eval_slug(self.key, on=self) self.slug = config.eval_slug(self.key, on=self)
if self.slug and self.slug.endswith('/index.html'): if self.slug and self.slug.endswith('/index.html'):
self.slug = self.slug[:-10] self.slug = self.slug[:-10]
if group: # exit early if initialized through resolver if key_obj: # exit early if initialized through resolver
return self return self
# extra fields # extra fields
for attr in config.fields: for attr in config.fields:
@@ -202,15 +204,15 @@ class GroupBySource(VirtualSourceObject):
raise AttributeError raise AttributeError
def __lt__(self, other: 'GroupBySource') -> bool: def __lt__(self, other: 'GroupBySource') -> bool:
# Used for |sort filter ("group" is the provided original string) # Used for |sort filter (`key_obj` is the indexed original value)
if isinstance(self.group, (bool, int, float)) and \ if isinstance(self.key_obj, (bool, int, float)) and \
isinstance(other.group, (bool, int, float)): isinstance(other.key_obj, (bool, int, float)):
return self.group < other.group return self.key_obj < other.key_obj
if self.group is None: if self.key_obj is None:
return False return False # this will sort None at the end
if other.group is None: if other.key_obj is None:
return True return True
return str(self.group).lower() < str(other.group).lower() return str(self.key_obj).lower() < str(other.key_obj).lower()
def __eq__(self, other: object) -> bool: def __eq__(self, other: object) -> bool:
# Used for |unique filter # Used for |unique filter

View File

@@ -1,5 +1,7 @@
from typing import TYPE_CHECKING, Dict, List, Any, Union, NamedTuple from typing import (
from typing import Optional, Callable, Iterator, Generator TYPE_CHECKING, Dict, List, Any, Union, NamedTuple,
Optional, Callable, Iterator, Generator
)
from .backref import VGroups from .backref import VGroups
from .model import ModelReader from .model import ModelReader
from .vobj import GroupBySource from .vobj import GroupBySource
@@ -24,7 +26,7 @@ GroupingCallback = Callable[[GroupByCallbackArgs], Union[
class Watcher: class Watcher:
''' '''
Callback is called with (Record, FieldKeyPath, field-value). Callback is called with (Record, FieldKeyPath, field-value).
Callback may yield one or more (group, extra-info) tuples. Callback may yield 0-n objects.
''' '''
def __init__(self, config: 'Config') -> None: def __init__(self, config: 'Config') -> None:
@@ -37,7 +39,7 @@ class Watcher:
Decorator to subscribe to attrib-elements. Decorator to subscribe to attrib-elements.
If flatten = False, dont explode FlowType. If flatten = False, dont explode FlowType.
(record, field-key, field) -> (group, extra-info) (record, field-key, field) -> value
''' '''
def _decorator(fn: GroupingCallback) -> None: def _decorator(fn: GroupingCallback) -> None:
self.flatten = flatten self.flatten = flatten
@@ -68,25 +70,25 @@ class Watcher:
args = GroupByCallbackArgs(record, key, field) args = GroupByCallbackArgs(record, key, field)
_gen = self.callback(args) _gen = self.callback(args)
try: try:
group = next(_gen) key_obj = next(_gen)
while True: while True:
if self.config.key_map_fn: if self.config.key_obj_fn:
slug = self._persist_multiple(args, group) slug = self._persist_multiple(args, key_obj)
else: else:
slug = self._persist(args, group) slug = self._persist(args, key_obj)
# return slugified group key and continue iteration # return slugified key and continue iteration
if isinstance(_gen, Generator) and not _gen.gi_yieldfrom: if isinstance(_gen, Generator) and not _gen.gi_yieldfrom:
group = _gen.send(slug) key_obj = _gen.send(slug)
else: else:
group = next(_gen) key_obj = next(_gen)
except StopIteration: except StopIteration:
del _gen del _gen
def _persist_multiple(self, args: 'GroupByCallbackArgs', obj: Any) \ def _persist_multiple(self, args: 'GroupByCallbackArgs', obj: Any) \
-> Optional[str]: -> Optional[str]:
# if custom key mapping function defined, use that first # if custom key mapping function defined, use that first
res = self.config.eval_key_map_fn(on=args.record, res = self.config.eval_key_obj_fn(on=args.record,
context={'X': obj, 'SRC': args}) context={'X': obj, 'ARGS': args})
if isinstance(res, (list, tuple)): if isinstance(res, (list, tuple)):
for k in res: for k in res:
self._persist(args, k) # 1-to-n replacement self._persist(args, k) # 1-to-n replacement