diff --git a/lektor_groupby/backref.py b/lektor_groupby/backref.py index a0a17ca..7010103 100644 --- a/lektor_groupby/backref.py +++ b/lektor_groupby/backref.py @@ -1,6 +1,7 @@ from lektor.context import get_ctx from typing import TYPE_CHECKING, Union, Iterable, Iterator, Optional import weakref +from .util import split_strip if TYPE_CHECKING: from lektor.builder import Builder from lektor.db import Record @@ -48,7 +49,7 @@ class VGroups: fields: Union[str, Iterable[str], None] = None, flows: Union[str, Iterable[str], None] = None, recursive: bool = False, - order_by: Optional[str] = None, + order_by: Union[str, Iterable[str], None] = None, ) -> Iterator['GroupBySource']: ''' Extract all referencing groupby virtual objects from a page. ''' ctx = get_ctx() @@ -85,7 +86,13 @@ class VGroups: done_list.add(vobj()) if order_by: - order = order_by.split(',') + if isinstance(order_by, str): + order = split_strip(order_by, ',') + elif isinstance(order_by, (list, tuple)): + order = order_by + else: + raise TypeError('order_by must be either str or list type.') + # using get_sort_key() of GroupBySource yield from sorted(done_list, key=lambda x: x.get_sort_key(order)) else: yield from done_list diff --git a/lektor_groupby/config.py b/lektor_groupby/config.py index 828c8a7..37caa12 100644 --- a/lektor_groupby/config.py +++ b/lektor_groupby/config.py @@ -1,7 +1,8 @@ from inifile import IniFile from lektor.utils import slugify +from .util import split_strip -from typing import Set, Dict, Optional, Union, Any +from typing import Set, Dict, Optional, Union, Any, List AnyConfig = Union['Config', IniFile, Dict] @@ -31,6 +32,7 @@ class Config: self.dependencies = set() # type: Set[str] self.fields = {} # type: Dict[str, Any] self.key_map = {} # type: Dict[str, str] + self.order_by = None # type: Optional[List[str]] def slugify(self, k: str) -> str: ''' key_map replace and slugify. ''' @@ -48,6 +50,10 @@ class Config: ''' This mapping replaces group keys before slugify. ''' self.key_map = key_map or {} + def set_order_by(self, order_by: Optional[str]) -> None: + ''' If specified, children will be sorted according to keys. ''' + self.order_by = split_strip(order_by, ',') or None + def __repr__(self) -> str: txt = ' None: def most_used_key(keys: List[str]) -> str: + ''' Find string with most occurrences. ''' if len(keys) < 3: return keys[0] # TODO: first vs last occurrence best_count = 0 @@ -26,3 +27,13 @@ def most_used_key(keys: List[str]) -> str: best_count = num best_key = k return best_key + + +def split_strip(data: str, delimiter: str = ',') -> List[str]: + ''' Split by delimiter and strip each str separately. Omit if empty. ''' + ret = [] + for x in (data or '').split(delimiter): + x = x.strip() + if x: + ret.append(x) + return ret diff --git a/lektor_groupby/vobj.py b/lektor_groupby/vobj.py index 5144d33..7cd43bc 100644 --- a/lektor_groupby/vobj.py +++ b/lektor_groupby/vobj.py @@ -4,7 +4,7 @@ from lektor.db import _CmpHelper from lektor.environment import Expression from lektor.sourceobj import VirtualSourceObject # subclass from lektor.utils import build_url -from typing import TYPE_CHECKING, Dict, List, Any, Optional, Iterator, Iterable +from typing import TYPE_CHECKING, List, Any, Optional, Iterator, Iterable from .util import report_config_error, most_used_key if TYPE_CHECKING: from lektor.builder import Artifact @@ -29,13 +29,11 @@ class GroupBySource(VirtualSourceObject): super().__init__(record) self.key = slug self._group_map = [] # type: List[str] - self._children = {} # type: Dict[Record, List[Any]] + self._children = [] # type: List[Record] - def append_child(self, child: 'Record', extra: Any, group: str) -> None: + def append_child(self, child: 'Record', group: str) -> None: if child not in self._children: - self._children[child] = [extra] - else: - self._children[child].append(extra) + self._children.append(child) # _group_map is later used to find most used group self._group_map.append(group) @@ -59,6 +57,10 @@ class GroupBySource(VirtualSourceObject): # extra fields for attr, expr in config.fields.items(): setattr(self, attr, self._eval(expr, field='fields.' + attr)) + # sort children + if config.order_by: + # using get_sort_key() of Record + self._children.sort(key=lambda x: x.get_sort_key(config.order_by)) return self def _eval(self, value: Any, *, field: str) -> Any: @@ -94,6 +96,25 @@ class GroupBySource(VirtualSourceObject): for record in self._children: yield from record.iter_source_filenames() + # def get_checksum(self, path_cache: 'PathCache') -> Optional[str]: + # deps = [self.pad.env.jinja_env.get_or_select_template( + # self.config.template).filename] + # deps.extend(self.iter_source_filenames()) + # sums = '|'.join(path_cache.get_file_info(x).filename_and_checksum + # for x in deps if x) + str(len(self._children)) + # return hashlib.sha1(sums.encode('utf-8')).hexdigest() if sums else None + + # @property + # def pagination(self): + # print('pagination') + # return None + + # def __for_page__(self, page_num): + # """Get source object for a (possibly) different page number. + # """ + # print('for page', page_num) + # return self + def get_sort_key(self, fields: Iterable[str]) -> List: def cmp_val(field: str) -> Any: reverse = field.startswith('-') @@ -101,14 +122,14 @@ class GroupBySource(VirtualSourceObject): field = field[1:] return _CmpHelper(getattr(self, field, None), reverse) - return [cmp_val(field) for field in fields] + return [cmp_val(field) for field in fields or []] # ----------------------- # Properties & Helper # ----------------------- @property - def children(self) -> Dict['Record', List[Any]]: + def children(self) -> List['Record']: ''' Returns dict with page record key and (optional) extra value. ''' return self._children @@ -119,14 +140,6 @@ class GroupBySource(VirtualSourceObject): return iter(self._children).__next__() return None - @property - def first_extra(self) -> Optional[Any]: - ''' Returns first additional / extra info object of first page. ''' - if not self._children: - return None - val = iter(self._children.values()).__next__() - return val[0] if val else None - def __getitem__(self, key: str) -> Any: # Used for virtual path resolver if key in ('_path', '_alt'): diff --git a/lektor_groupby/watcher.py b/lektor_groupby/watcher.py index dcb13e7..8a9eb89 100644 --- a/lektor_groupby/watcher.py +++ b/lektor_groupby/watcher.py @@ -66,28 +66,22 @@ class Watcher: for key, field in self._model_reader.read(record): _gen = self.callback(GroupByCallbackArgs(record, key, field)) try: - obj = next(_gen) + group = next(_gen) while True: - if not isinstance(obj, (str, tuple)): - raise TypeError(f'Unsupported groupby yield: {obj}') - slug = self._persist(record, key, obj) + if not isinstance(group, str): + raise TypeError(f'Unsupported groupby yield: {group}') + slug = self._persist(record, key, group) # return slugified group key and continue iteration if isinstance(_gen, Generator) and not _gen.gi_yieldfrom: - obj = _gen.send(slug) + group = _gen.send(slug) else: - obj = next(_gen) + group = next(_gen) except StopIteration: del _gen - def _persist( - self, record: 'Record', key: 'FieldKeyPath', obj: Union[str, tuple] - ) -> str: + def _persist(self, record: 'Record', key: 'FieldKeyPath', group: str) \ + -> str: ''' Update internal state. Return slugified string. ''' - if isinstance(obj, str): - group, extra = obj, key.fieldKey - else: - group, extra = obj - alt = record.alt slug = self.config.slugify(group) if slug not in self._state[alt]: @@ -96,7 +90,7 @@ class Watcher: else: src = self._state[alt][slug] - src.append_child(record, extra, group) + src.append_child(record, group) # reverse reference VGroups.of(record).add(key, src) return slug