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.context import Context
from lektor.utils import slugify as _slugify
from typing import TYPE_CHECKING
from typing import Set, Dict, Optional, Union, Any, List, Generator
from typing import (
TYPE_CHECKING, Set, Dict, Optional, Union, Any, List, Generator
)
from .util import split_strip
if TYPE_CHECKING:
from lektor.sourceobj import SourceObject
@@ -44,14 +45,14 @@ class Config:
slug: Optional[str] = None, # default: "{attr}/{group}/index.html"
template: Optional[str] = None, # default: "groupby-{attr}.html"
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:
self.key = key
self.root = (root or '/').rstrip('/') or '/'
self.slug = slug or (key + '/{key}/') # key = GroupBySource.key
self.template = template or f'groupby-{self.key}.html'
self.replace_none_key = replace_none_key
self.key_map_fn = key_map_fn
self.key_obj_fn = key_obj_fn
# editable after init
self.enabled = True
self.dependencies = set() # type: Set[str]
@@ -98,7 +99,7 @@ class Config:
def __repr__(self) -> str:
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 += f' fields="{", ".join(self.fields)}"'
if self.order_by:
@@ -114,7 +115,7 @@ class Config:
slug=cfg.get('slug'),
template=cfg.get('template'),
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
@@ -184,13 +185,13 @@ class Config:
expr = self._make_expression(cfg_slug, on=on, field='slug')
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.
Note: The function does not check whether `key_map_fn` is set.
If `key_obj_fn` is set, evaluate field expression.
Note: The function does not check whether `key_obj_fn` is set.
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 ctx.gather_dependencies(self.dependencies.add):
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 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 .vobj import VPATH, GroupBySource
if TYPE_CHECKING:
@@ -9,16 +11,16 @@ if TYPE_CHECKING:
class ResolverEntry(NamedTuple):
slug: str
group: str
key: str
key_obj: Any
config: 'Config'
page: Optional[int]
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:
return self.slug == group \
and self.config.key == attribute \
return self.key == vobj_key \
and self.config.key == conf_key \
and self.config.root == path \
and self.page == page
@@ -53,9 +55,9 @@ class Resolver:
def add(self, vobj: GroupBySource) -> None:
''' Track new virtual object (only if slug is set). '''
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(
vobj.key, vobj.group, vobj.config, vobj.page_num)
vobj.key, vobj.key_obj, vobj.config, vobj.page_num)
# ------------
# Resolver
@@ -68,15 +70,16 @@ class Resolver:
rv = self._data.get(build_url([node.url_path] + pieces))
if rv:
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
def resolve_virtual_path(self, node: 'SourceObject', pieces: List[str]) \
-> Optional[GroupBySource]:
''' 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:
path = node['_path'] # type: str
attr, grp, *optional_page = pieces
conf_key, vobj_key, *optional_page = pieces
page = None
if optional_page:
try:
@@ -84,7 +87,7 @@ class Resolver:
except ValueError:
pass
for rv in self._data.values():
if rv.equals(path, attr, grp, page):
if rv.equals(path, conf_key, vobj_key, page):
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

View File

@@ -3,8 +3,9 @@ from lektor.context import get_ctx
from lektor.db import _CmpHelper
from lektor.environment import Expression
from lektor.sourceobj import VirtualSourceObject # subclass
from typing import TYPE_CHECKING
from typing import List, Any, Dict, Optional, Generator, Iterator, Iterable
from typing import (
TYPE_CHECKING, List, Any, Dict, Optional, Generator, Iterator, Iterable
)
from .pagination import PaginationConfig
from .query import FixedRecordsQuery
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.
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__(
self,
record: 'Record',
slug: str,
key: str,
page_num: Optional[int] = None
) -> None:
super().__init__(record)
self.key = slug
self.page_num = page_num
self._expr_fields = {} # type: Dict[str, Expression]
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:
self.__children.append(child.path)
# TODO: rename group to value
# __group_map is later used to find most used group
self.__group_map.append(group)
# __key_obj_map is later used to find most used key_obj
self.__key_obj_map.append(key_obj)
def _update_attr(self, key: str, value: Any) -> None:
''' 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):
self._expr_fields[key] = value
try:
@@ -65,22 +67,22 @@ class GroupBySource(VirtualSourceObject):
# Evaluate Extra Fields
# -------------------------
def finalize(self, config: 'Config', group: Optional[Any] = None) \
def finalize(self, config: 'Config', key_obj: Optional[Any] = None) \
-> 'GroupBySource':
self.config = config
# make a sorted children query
self._query = FixedRecordsQuery(self.pad, self.__children, self.alt)
self._query._order_by = config.order_by
del self.__children
# set group name
self.group = group or most_used_key(self.__group_map)
del self.__group_map
# set indexed original value (can be: str, int, float, bool, obj)
self.key_obj = key_obj or most_used_key(self.__key_obj_map)
del self.__key_obj_map
# evaluate slug Expression
self.slug = config.eval_slug(self.key, on=self)
if self.slug and self.slug.endswith('/index.html'):
self.slug = self.slug[:-10]
if group: # exit early if initialized through resolver
if key_obj: # exit early if initialized through resolver
return self
# extra fields
for attr in config.fields:
@@ -202,15 +204,15 @@ class GroupBySource(VirtualSourceObject):
raise AttributeError
def __lt__(self, other: 'GroupBySource') -> bool:
# Used for |sort filter ("group" is the provided original string)
if isinstance(self.group, (bool, int, float)) and \
isinstance(other.group, (bool, int, float)):
return self.group < other.group
if self.group is None:
return False
if other.group is None:
# Used for |sort filter (`key_obj` is the indexed original value)
if isinstance(self.key_obj, (bool, int, float)) and \
isinstance(other.key_obj, (bool, int, float)):
return self.key_obj < other.key_obj
if self.key_obj is None:
return False # this will sort None at the end
if other.key_obj is None:
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:
# Used for |unique filter

View File

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