Skip to content

Instantly share code, notes, and snippets.

@AmatanHead
Created January 26, 2018 16:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AmatanHead/afc67c9da0ef2ae724e12a51986d67b5 to your computer and use it in GitHub Desktop.
Save AmatanHead/afc67c9da0ef2ae724e12a51986d67b5 to your computer and use it in GitHub Desktop.
# -*- coding: utf-8 -*-
from __future__ import print_function, absolute_import, division
import re
from statbox_abt_metrics.meta import Meta
from yandex_inflector_python import Inflector
from ....utils import typehints as _t
__all__ = [
'Context',
'Dim',
'Slice',
'Custom',
'If',
'IfAll',
'IfAny',
]
class Context(object):
#: Инфлектор для текущего языка.
inflector = None # type: Inflector
#: Название локали, для которой происходит генерация. Может быть
#: ``'ru_name'``, ``'en_name'``, ``'description'``.
locale = 'ru_name' # type: str
#: Список названий всех измерений куба меты. Названия перечислены в порядке
#: применения декартова произведения.
dimensions = () # type: _t.Tuple[str, ...]
#: Данные по каждому из измерений. Ключами являются названия измерений,
#: значениями -- объекты :py:class:`~.Meta`.
data = {} # type: _t.Dict[str, _t.Meta]
def get(self, dim):
"""
Получить значение измерения для текущей локали.
:param dim: название измерения.
:return: значение измерения или пустая строка.
:rtype: ``str``
"""
if dim in self.data:
return getattr(self.data[dim], self.locale, '')
else:
return u''
def has(self, dim):
"""
Проверить, есть ли значение измерения для текущей локали
и не пусто ли оно.
:param dim: название измерения.
:return: ``True``, если значение не пусто.
:rtype: ``bool``
"""
return bool(self.get(dim))
# Типы для улучшения статического анализа
TPredicate = _t.Callable[[Context], bool]
TMutator = _t.Callable[[Context, unicode, bool, bool], _t.Tuple[unicode, bool, bool]]
class TemplateNode(object):
def eval(self, context):
"""
Сгенерировать текст для данного контекста.
:param context: контекст, в котором выполняется этот узел.
:type context: :py:class:`~.Context`
:return: тьюпл ``(text, any, all)``, где
* ``text`` -- текстовый результат выполнения узла.
* ``all`` -- ``bool``, ``True`` если все подузлы дерева вернули
непустое значение.
* ``any`` -- ``bool``, ``True`` если хотя бы один из подузлов
``Dim``, ``Slice``, ``Custom`` дерева вернул непустое значение.
"""
raise NotImplementedError()
def inflect(self, gram):
# type: (str) -> Inflect
"""
Изменить падеж и число словосочетания в шаблоне.
:param gram: обозначение граммемы нужного падежа.
"""
return Inflect(self, gram)
def inflect_if(self, predicate, gram):
# type: (TPredicate, str) -> InflectIf
"""
Если выполняется условие, изменить падеж и число
словосочетания в шаблоне.
:param predicate: callable, принимающий на вход :py:class:`~.Context`
и возвращающий ``bool`` -- результат проверки.
:param gram: обозначение граммемы нужного падежа.
"""
return InflectIf(self, predicate, gram)
def mutate(self, mutator):
# type: (TMutator) -> Mutate
"""
Применить функцию к тексту, полученному в результате выполнения
поддерева.
:param mutator: callable, функция, изменяющая текст.
Принимает на вход контекст исполнения, изменяемый текст,
переменные ``all_ok``, ``any_ok`` (см.
:py:meth:`.TemplateNode.eval`) и возвращает тьюпл с измененным
текстом и новыми ``all_ok``, ``any_ok``.
"""
return Mutate(self, mutator)
def mutate_if(self, predicate, mutator):
# type: (TPredicate, TMutator) -> MutateIf
"""
Если выполняется условие, применить к тексту мутатор.
:param predicate: callable, принимающий на вход :py:class:`~.Context`
и возвращающий ``bool`` -- результат проверки.
:param mutator: см. :py:meth:`.TemplateNode.mutate`.
"""
return MutateIf(self, predicate, mutator)
def mutate_simple(self, mutator):
# type: (typing.Callable[[unicode], unicode]) -> Mutate
"""
Применить к тексту упрощенный мутатор, не изменяющий
``all_ok`` и ``any_ok``.
:param mutator: callable, принимающий на вход строку и возвращающий
другую строку.
"""
return self.mutate(lambda ctx, text, a, b: (mutator(text), a, b))
def lower(self):
"""
Перевести текст в поддереве в нижний регистр.
"""
return self.mutate_simple(unicode.lower)
def upper(self):
"""
Перевести текст в поддереве в верхний регистр.
"""
return self.mutate_simple(unicode.upper)
def title(self):
"""
Перевести первую букву каждого слова в поддереве в верхний регистр,
остальные буквы -- в нижний.
"""
return self.mutate_simple(unicode.title)
def clause(self):
"""
Перевести первую букву текста в поддереве в верхний регистр,
остальные не трогать.
"""
return self.mutate_simple(lambda s: s[0].upper() + s[1:] if s else s)
def __add__(self, rhs):
return _add(self, rhs)
def __iadd__(self, rhs):
return _add(self, rhs)
def __radd__(self, lhs):
return _add(lhs, self)
class Dim(TemplateNode):
"""
Извлекает из контекста значение измерения для текущей локали.
"""
def __init__(self, dim):
# type: (str) -> Dim
self.dim = dim
def eval(self, context):
data = context.get(self.dim)
return data, bool(data), bool(data)
class Slice(TemplateNode):
"""
Извлекает из контекста значения измерений, название которых начинается на
``base`` и конкатенирует их с использованием ``joiner``.
"""
def __init__(self, base, joiner=u', '):
# type: (str, unicode) -> Slice
self.base = base
self.joiner = joiner
def eval(self, context):
keys = [k for k in context.dimensions if k.startswith(self.base)]
slices = [context.get(k) for k in keys]
slices = filter(None, slices)
return self.joiner.join(slices), bool(slices), bool(slices)
class Custom(TemplateNode):
"""
Применяет к контексту функцию ``functor``.
:param functor: callable, принимающий на вход :py:class:`~.Context`
и возвращающий тьюпл ``(text, any, all)``.
См. :py:meth:`.TemplateNode.eval`.
"""
def __init__(self, functor):
# type: (typing.Callable[[Context], typing.Tuple[unicode, bool, bool]]) -> Custom
self.functor = functor
def eval(self, context):
return self.functor(context)
class Const(TemplateNode):
"""
Служебный узел с константным значением.
"""
def __init__(self, data):
# type: (typing.Any) -> Const
self.data = unicode(data)
def eval(self, context):
return self.data, True, False
def __repr__(self):
return self.data
class Sum(TemplateNode):
"""
Служебный узел, получающийся в результате сложения нескольких других узлов.
"""
def __init__(self, components):
# type: (typing.List[TemplateNode]) -> Sum
self.components = components
def eval(self, context):
text, all_ok, any_ok = '', True, False
for component in self.components:
result = component.eval(context)
text += result[0]
all_ok &= result[1]
any_ok |= result[2]
return text, all_ok, any_ok
class If(TemplateNode):
"""
Ветвление шаблона. Если предикат верен для данного контекста,
в шаблон подставляется ``on_then``, иначе -- ``on_else``.
:param predicate: callable, принимающий на вход :py:class:`~.Context`
и возвращающий ``bool`` -- результат проверки.
:param on_then: строка или другой узел шаблона, подставляется в случае
прохождения проверки.
:param on_else: строка или другой узел шаблона, подставляется в случае
провала проверки.
"""
def __init__(self, predicate, on_then, on_else=''):
# type: (TPredicate, typing.Any, typing.Any) -> If
self.predicate = predicate
if not isinstance(on_then, TemplateNode):
on_then = Const(on_then)
self.on_then = on_then
if not isinstance(on_else, TemplateNode):
on_else = Const(on_else)
self.on_else = on_else
def eval(self, context):
if self.predicate(context):
return self.on_then.eval(context)
else:
return self.on_else.eval(context)
class IfAll(TemplateNode):
"""
Специальный иф для скрытия пустых размерностей.
Выполняет поддерево ``on_then``. Если при выполнении ``on_then``
все узлы типов :py:class:`.Dim`, :py:class:`.Slice`, :py:class:`.Custom`
развернулись в непустое значение, возвращает результат выполнения
``on_then``, иначе запускает и возвращает поддерево ``on_else``.
"""
_check = staticmethod(lambda all_ok, any_ok: all_ok)
def __init__(self, on_then, on_else=''):
# type: (typing.Any, typing.Any) -> IfAll
if not isinstance(on_then, TemplateNode):
on_then = Const(on_then)
self.on_then = on_then
if not isinstance(on_else, TemplateNode):
on_else = Const(on_else)
self.on_else = on_else
def eval(self, context):
text, all_ok, any_ok = self.on_then.eval(context)
if self._check(all_ok, any_ok):
return text, all_ok, any_ok
else:
return self.on_else.eval(context)
class IfAny(IfAll):
"""
Выполняет поддерево ``on_then``. Если хотябы один узел :py:class:`.Dim`,
:py:class:`.Slice`, :py:class:`.Custom` этого поддерева развернулся
в непустое значение, возвращает ``on_then``, иначе возвращает ``on_else``.
См. :py:class:`~.IfAll`.
"""
_check = staticmethod(lambda all_ok, any_ok: any_ok)
class Inflect(TemplateNode):
"""
Служебный узел, получающийся в результате вызова
:py:meth:`.TemplateNode.inflect`.
"""
def __init__(self, child, gram):
# type: (typing.Any, str) -> Inflect
if not isinstance(child, TemplateNode):
child = Const(child)
self.child = child
self.gram = gram
def eval(self, context):
text, all_ok, any_ok = self.child.eval(context)
text = context.inflector.Inflect(text, self.gram)
return text, all_ok, any_ok
class InflectIf(Inflect):
"""
Служебный узел, получающийся в результате вызова
:py:meth:`.TemplateNode.inflect_if`.
"""
def __init__(self, child, predicate, gram):
# type: (typing.Any, TPredicate, str) -> InflectIf
super(InflectIf, self).__init__(child, gram)
self.predicate = predicate
def eval(self, context):
if self.predicate(context):
return super(InflectIf, self).eval(context)
else:
return self.child.eval(context)
class Mutate(TemplateNode):
"""
Служебный узел, получающийся в результате вызова
:py:meth:`.TemplateNode.mutate`.
"""
def __init__(self, child, mutator):
# type: (typing.Any, TMutator) -> Mutate
if not isinstance(child, TemplateNode):
child = Const(child)
self.child = child
self.mutator = mutator
def eval(self, context):
text, all_ok, any_ok = self.child.eval(context)
return self.mutator(context, text, all_ok, any_ok)
class MutateIf(Mutate):
"""
Служебный узел, получающийся в результате вызова
:py:meth:`.TemplateNode.mutate_if`.
"""
def __init__(self, child, predicate, mutator):
# type: (typing.Any, TPredicate, TMutator) -> MutateIf
super(MutateIf, self).__init__(child, mutator)
self.predicate = predicate
def eval(self, context):
if self.predicate(context):
return super(MutateIf, self).eval(context)
else:
return self.child.eval(context)
def _add(lhs, rhs):
# type: (typing.Any, typing.Any) -> TemplateNode
if not isinstance(lhs, TemplateNode):
lhs = Const(lhs)
if not isinstance(rhs, TemplateNode):
rhs = Const(rhs)
lhs_is_const = isinstance(lhs, Const)
lhs_is_sum = isinstance(lhs, Sum)
rhs_is_const = isinstance(rhs, Const)
rhs_is_sum = isinstance(rhs, Sum)
if lhs_is_sum and not lhs.components:
return rhs
if rhs_is_sum and not rhs.components:
return lhs
if lhs_is_const and rhs_is_const:
return Const(lhs.data + rhs.data)
elif lhs_is_sum:
if rhs_is_const and isinstance(lhs.components[-1], Const):
last = Const(lhs.components[-1].data + rhs.data)
return Sum(lhs.components[:-1] + [last])
else:
return Sum(lhs.components + [rhs])
elif rhs_is_sum:
if lhs_is_const and isinstance(rhs.components[0], Const):
first = Const(lhs.data + rhs.components[0].data)
return Sum([first] + rhs.components[1:])
else:
return Sum([lhs] + rhs.components)
elif lhs_is_sum and rhs_is_sum:
return Sum(lhs.components + rhs.components)
else:
return Sum([lhs, rhs])
def main():
from itertools import product
test_measures = [u'Среднее', u'Сумма']
uses = [u'Good Use', u'Deep Use', u'Good или Deep Use', u'Всего']
event_types = [u'Сессии{g=pl}', u'Хиты{g=pl}', u'Пользователи{g=pl}', u'Хитов{g=pl} на сессию', u'Всего']
counters = [u'Поисковый запрос']
oss = [u'Windows', u'iOS', u'Android']
test_measure_mutator = lambda _, t, a, b: (u'Доля', a, b) if t == u'Среднее' else (t, a, b)
untotal = lambda _, t, a, b: (u'', False, False) if t == u'Всего' else (t, a, b)
context = Context()
context.inflector = Inflector('ru')
context.dimensions = ('test_measure', 'event_type', 'counter', 'counter_os', 'use_type')
# ======== TEMPLATE DEFINITION ========
combiner = IfAll(
Dim('test_measure').mutate(test_measure_mutator).lower() +
u' ' +
Dim('event_type').mutate(untotal).inflect('gen').lower() +
u' для ' +
Dim('counter').inflect('gen').lower(),
Dim('counter').lower()
)
combiner += IfAll(u' с ' + Dim('use_type').mutate(untotal))
combiner += IfAll(u', ' + Slice('counter_'))
combiner = combiner.clause()
# ====== END TEMPLATE DEFINITION ======
for test_measure, use, event_type, counter, os in product(test_measures, uses, event_types, counters, oss):
context.data = dict(
test_measure=Meta('', test_measure),
use_type=Meta('', use),
event_type=Meta('', event_type),
counter=Meta('', counter),
counter_os=Meta('', os),
)
print(combiner.eval(context)[0])
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment