Files
lektor-groupby-plugin/lektor_groupby/config.py
2022-11-22 19:41:07 +01:00

201 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, Set, Dict, Optional, Union, Any, List, Generator
)
from .util import split_strip
if TYPE_CHECKING:
from lektor.sourceobj import SourceObject
AnyConfig = Union['Config', IniFile, Dict]
class ConfigError(Exception):
''' Used to print a Lektor console error. '''
def __init__(
self, key: str, field: str, expr: str, error: Union[Exception, str]
):
self.key = key
self.field = field
self.expr = expr
self.error = error
def __str__(self) -> str:
return 'Invalid config for [{}.{}] = "{}" Error: {}'.format(
self.key, self.field, self.expr, repr(self.error))
class Config:
'''
Holds information for GroupByWatcher and GroupBySource.
This object is accessible in your template file ({{this.config}}).
Available attributes:
key, root, slug, template, enabled, dependencies, fields, key_map
'''
def __init__(
self,
key: str, *,
root: Optional[str] = None, # default: "/"
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_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_obj_fn = key_obj_fn
# editable after init
self.enabled = True
self.dependencies = set() # type: Set[str]
self.fields = {} # type: Dict[str, Any]
self.key_map = {} # type: Dict[str, str]
self.pagination = {} # type: Dict[str, Any]
self.order_by = None # type: Optional[List[str]]
def slugify(self, k: str) -> str:
''' key_map replace and slugify. '''
rv = self.key_map.get(k, k)
return _slugify(rv) or rv # the `or` allows for example "_"
def set_fields(self, fields: Optional[Dict[str, Any]]) -> None:
'''
The fields dict is a mapping of attrib = Expression values.
Each dict key will be added to the GroupBySource virtual object.
Each dict value is passed through jinja context first.
'''
self.fields = fields or {}
def set_key_map(self, key_map: Optional[Dict[str, str]]) -> None:
''' This mapping replaces group keys before slugify. '''
self.key_map = key_map or {}
def set_pagination(
self,
enabled: Optional[bool] = None,
per_page: Optional[int] = None,
url_suffix: Optional[str] = None,
items: Optional[str] = None,
) -> None:
''' Used for pagination. '''
self.pagination = dict(
enabled=enabled,
per_page=per_page,
url_suffix=url_suffix,
items=items,
)
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 '', ',') or None
def __repr__(self) -> str:
txt = '<GroupByConfig'
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:
txt += ' order_by="{}"'.format(' ,'.join(self.order_by))
return txt + '>'
@staticmethod
def from_dict(key: str, cfg: Dict[str, str]) -> 'Config':
''' Set config fields manually. Allowed: key, root, slug, template. '''
return Config(
key=key,
root=cfg.get('root'),
slug=cfg.get('slug'),
template=cfg.get('template'),
replace_none_key=cfg.get('replace_none_key'),
key_obj_fn=cfg.get('key_obj_fn'),
)
@staticmethod
def from_ini(key: str, ini: IniFile) -> 'Config':
''' Read and parse ini file. Also adds dependency tracking. '''
cfg = ini.section_as_dict(key) # type: Dict[str, str]
conf = Config.from_dict(key, cfg)
conf.enabled = ini.get_bool(key + '.enabled', True)
conf.dependencies.add(ini.filename)
conf.set_fields(ini.section_as_dict(key + '.fields'))
conf.set_key_map(ini.section_as_dict(key + '.key_map'))
conf.set_pagination(
enabled=ini.get_bool(key + '.pagination.enabled', None),
per_page=ini.get_int(key + '.pagination.per_page', None),
url_suffix=ini.get(key + '.pagination.url_suffix'),
items=ini.get(key + '.pagination.items'),
)
conf.set_order_by(ini.get(key + '.children.order_by', None))
return conf
@staticmethod
def from_any(key: str, config: AnyConfig) -> 'Config':
assert isinstance(config, (Config, IniFile, Dict))
if isinstance(config, Config):
return config
elif isinstance(config, IniFile):
return Config.from_ini(key, config)
elif isinstance(config, Dict):
return Config.from_dict(key, config)
# -----------------------------------
# Field Expressions
# -----------------------------------
def _make_expression(self, expr: Any, *, on: 'SourceObject', field: str) \
-> Union[Expression, Any]:
''' Create Expression and report any config error. '''
if not isinstance(expr, str):
return expr
try:
return Expression(on.pad.env, expr)
except Exception as e:
raise ConfigError(self.key, field, expr, e)
def eval_field(self, attr: str, *, on: 'SourceObject') \
-> Union[Expression, Any]:
''' Create an expression for a custom defined user field. '''
# do not `gather_dependencies` because fields are evaluated on the fly
# dependency tracking happens whenever a field is accessed
return self._make_expression(
self.fields[attr], on=on, field='fields.' + attr)
def eval_slug(self, key: str, *, on: 'SourceObject') -> Optional[str]:
''' Either perform a "{key}" substitution or evaluate expression. '''
cfg_slug = self.slug
if not cfg_slug:
return None
if '{key}' in cfg_slug:
if key:
return cfg_slug.replace('{key}', key)
else:
raise ConfigError(self.key, 'slug', cfg_slug,
'Cannot replace {key} with None')
return None
else:
# TODO: do we need `gather_dependencies` here too?
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_obj_fn(self, *, on: 'SourceObject', context: Dict) -> Any:
'''
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_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)
if isinstance(res, Generator):
res = list(res) # unpack for 1-to-n replacement
return res