'''
Kv Compiler Contexts and Rules
================================
Describes the classes that captures a kv binding rule as well as the context
that stores the rules.
Typical usage::
class MyWidget(Widget):
kv_ctx = None
def __init__(self, **kwargs):
super(MyWidget, self).__init__(**kwargs)
self.apply_kv()
@kv()
def apply_rule(self):
with KvContext() as self.kv_ctx:
self.width @= self.height + 10
with KvRule(name='my_rule') as rule:
print('callback args is:', rule.largs)
self.x @= self.y + 25
Then::
>>> widget = MyWidget()
>>> widget.height = 43 # sets widget.width to 53
>>> rule = widget.kv_ctx.named_rules['my_rule']
>>> rule.unbind_rule() # unbinds the rule
>>> widget.kv_ctx.unbind_all_rules() # unbinds all the rules
'''
import ast
import fnmatch
import re
__all__ = ('KvRule', 'KvContext', 'KvParserRule', 'KvParserContext')
[docs]class KvRule(object):
'''
Describes a kv rule.
:param *binds: captures all positional arguments and add them to the bind
list. E.g.::
with KvRule():
self.x @= self.y
does the same things as::
with KvRule(self.y):
self.x = self.y
and you can add as many positional args there as you like, and the rule
will bind to all fo them. The args can be actual variable names, e.g.
`self.y`, or as a string, e.g. `"self.y"` - they will do the same
thing.
:param delay: the rule type: can be None (default), `"canvas"`, or a
number. Describes the rule type.
* If `None`, it's a normal kv rule
* If `"canvas"`, it's meant to be used with a canvas instruction and
the rule will be scheduled to be executed with other graphics *after*
the frame, rather than every time it is called.
* If a number, a Clock trigger event will be created with the given
delay and when the rule dispatches, the event will be triggered,
rather than be executed immediately.
.. warning:
If setting the kv decorator proxy parameter to True and a clock
rule is created, a reference must be held to the rule or context,
otherwise the rule will be freed by the garbage collection.
:param name: a optional name for the rule. Defaults to `None`
Class attributes:
:var largs: The largs provided when the rule is dispatched.
:var bind_store: (internal): Maintains a reference to the list that
stores all the bindings created for the rule.
:var bind_store_leaf_indices: (internal): The list of all the indices in
`bind_store` that store the leaf bindings created for the rule.
:var callback: (internal): The callback function that is called by the rule
whenever it is dispatched.
:var _callback: (internal): If it's a clock or canvas rule, contains the
underlying callback that actually executed the rule. In that case,
`callback` contains the clock trigger or function that schedules the
canvas update.
'''
__slots__ = ('bind_store', 'bind_store_leaf_indices', 'callback', 'delay',
'name', 'largs', '_callback')
def __init__(self, *binds, delay=None, name=None, triggered_only=False):
# we don't save any of the args here, because when a rule is created by
# the manual compiler, it is the KvParserRule. For the auto compiler,
# the user writes in the code KvRule, but the compiler intercepts it
# and creates a KvParserRule instead when parsing and saves the args
# there. Finally, the compiler emits the creation of a KvRule without
# any args/kwargs and instead emits manual code that sets the
# attributes of the KvRule based on the internal KvParserRule, rather
# than passing it to the constructor.
# Calss variables are not init here as a optimization
self.largs = ()
def __enter__(self):
raise TypeError(
"Something went wrong and the kv code was not compiled. Did "
"you forget to decorate the function with the kv compiler? "
"Make sure the kv rule class is KvRule because it cannot be "
"inherited from as it's parsed syntactically at compile time.")
def __exit__(self, exc_type, exc_val, exc_tb):
raise NotImplemented
[docs] def unbind_rule(self):
'''Unbinds the rule, so that it will not be triggered by any of the
properties that the rule is bound to.
If the rule is already scheduled, e.g. with a canvas instruction or
clock trigger, it may still execute (this may change in the future
to immediately cancel that as well), but it won't be scheduled again.
'''
# we are slowly trimming leaves until all are unbound
bind_store = self.bind_store
for leaf_index in self.bind_store_leaf_indices:
leaf = bind_store[leaf_index]
if leaf is None:
# we're in the middle of binding and this should not have
# been called
raise Exception(
'Cannot unbind a rule before it was finished binding')
leaf_graph_indices = leaf[5]
assert leaf[4] == 1
for bind_idx in leaf_graph_indices:
bind_item = bind_store[bind_idx]
if bind_item is None:
raise Exception(
'Cannot unbind a rule before it was finished '
'binding')
obj, attr, _, uid, count, _ = bind_item
if count == 1:
# last item bound, we're done with this one
obj.unbind_uid(attr, uid)
bind_store[bind_idx] = None
else:
bind_item[4] -= 1
assert bind_item[4] >= 1
self.bind_store = None
self.bind_store_leaf_indices = ()
# it's ok to release the (possibly last) ref to the callback because at
# worst it's scheduled in the clock, which has no problem dealing with
# abandoned refs, or its scheduled with the canvas instructions that
# holds a direct ref to it.
self.callback = None
[docs]class KvParserRule(KvRule):
'''Created by the parser when it encounters a :class:`KvRule`.
It is also used when manually compiling kv.
'''
__slots__ = ('callback_name', 'captures', 'src', 'with_var_name_ast',
'_callback_name', 'binds', 'triggered_only')
def __init__(self, *binds, delay=None, name=None, triggered_only=False):
super(KvParserRule, self).__init__()
self.with_var_name_ast = None
self._callback = None
self.binds = binds
self.delay = delay
self.name = name
self.bind_store = None
self.triggered_only = triggered_only
self.bind_store_leaf_indices = ()
self.callback = None
[docs]class KvContext(object):
'''
Manages kv rules created under the context.
:param reinit_after: Whether all the rules that are not
:attr:`KvRule.triggered_only` should be executed again after the
bindings are complete when the :class:`KvContext` exits. Defaults to False.
This is only useful when `bind_on_enter` in
:func:`~kivy.lang.compiler.compile.kv` is `False`, because then the
rules are executed before the bindings occur, and it may be desirable
for the rules to be executed again, once all the bindings occurs.
This is particularly useful for circular rules. It is `False` by
default for performance reasons.
Class attributes:
:var rules: List of :class:`KvRule`
Contains all the kv rules created under the context, ignoring rules
created under a second context within this context. E.g.::
with KvContext() as my_ctx:
self.x @= self.y
with KvContext() as my_ctx2:
self.y @= self.height + 10
with KvRule(name='my_rule'):
self.width @= self.height
then `my_ctx` will contain 2 rules, and my_ctx2 will contain 1 rule.
The rules are ordered and numbered in the order they occur in the
function.
:var named_rules: dictionary with all the rules that are named.
Similarly to `rules`, but contains the rules that have been given
names. In the example above, `my_ctx.named_rules` contains one rule
with name/key value `"my_rule"`.
:var bind_store: (internal): Maintains a reference to the list
that stores all the bindings created for all the rules in the context.
:var rebind_functions: (internal): Maintains a reference to all the
callbacks used in all the context's rules.
'''
__slots__ = (
'bind_store', 'rebind_functions', 'named_rules', 'rules')
def __init__(self, reinit_after=False):
self.rules = []
self.named_rules = {}
def __enter__(self):
raise TypeError(
"Something went wrong and the kv code was not compiled. Did "
"you forget to decorate the function with the kv compiler? "
"Make sure the kv context class is KvContext because it cannot be "
"inherited from as it's parsed syntactically at compile time.")
def __exit__(self, exc_type, exc_val, exc_tb):
raise NotImplemented
[docs] def add_rule(self, rule):
'''Adds the rule to the context.
It is called automatically by the compiler and should only be called
when manually compiling kv.
'''
self.rules.append(rule)
if rule.name:
self.named_rules[rule.name] = rule
[docs] def unbind_all_rules(self):
'''Calls :meth:`KvRule.unbind_rule` for all the rules in the context
to unbind all the rules.
'''
for rule in self.rules:
rule.unbind_rule()
[docs]class KvParserContext(KvContext):
'''Created by the parser when it encounters a :class:`KvContext`.
It is also used when manually compiling kv.
'''
__slots__ = ('transformer', 'kv_syntax', 'reinit_after')
def __init__(self, reinit_after=False):
super(KvParserContext, self).__init__()
self.transformer = None
self.reinit_after = reinit_after
def set_kv_binding_ast_transformer(self, transformer):
self.transformer = transformer
def parse_rules(self):
for rule in self.rules:
if not rule.binds:
raise ValueError(
'To create a rule, some binding parameters must be '
'specified, or added in the rule using the x @= y syntax')
if isinstance(rule.binds, str):
nodes = [ast.parse(rule.binds)]
elif isinstance(rule.binds, ast.AST):
nodes = [rule.binds]
else:
nodes = [ast.parse(bind) if isinstance(bind, str) else bind
for bind in rule.binds]
self.transformer.update_graph(nodes, rule)
def set_nodes_proxy(self, use_proxy, use_proxy_exclude=None):
nodes_rules = self.transformer.nodes_by_rule
if use_proxy is True:
if use_proxy_exclude:
if isinstance(use_proxy_exclude, str):
pat = re.compile(fnmatch.translate(use_proxy_exclude))
else:
pat = re.compile(
'|'.join(map(fnmatch.translate, use_proxy_exclude)))
match = re.match
for nodes_rule in nodes_rules:
for node in nodes_rule:
node.proxy = any(
dep.is_attribute for dep in node.depends_on_me) \
and match(pat, node.src) is None
else:
for nodes_rule in nodes_rules:
for node in nodes_rule:
node.proxy = any(
dep.is_attribute for dep in node.depends_on_me)
elif use_proxy is False:
for nodes_rule in nodes_rules:
for node in nodes_rule:
node.proxy = False
elif isinstance(use_proxy, str):
pat = re.compile(fnmatch.translate(use_proxy))
match = re.match
for nodes_rule in nodes_rules:
for node in nodes_rule:
node.proxy = any(
dep.is_attribute for dep in node.depends_on_me) \
and match(pat, node.src) is not None
else:
pat = re.compile('|'.join(map(fnmatch.translate, use_proxy)))
match = re.match
for nodes_rule in nodes_rules:
for node in nodes_rule:
node.proxy = any(
dep.is_attribute for dep in node.depends_on_me) \
and match(pat, node.src) is not None
def set_nodes_rebind(self, rebind, rebind_exclude=None):
nodes_rules = self.transformer.nodes_by_rule
if rebind is True:
if rebind_exclude:
if isinstance(rebind_exclude, str):
pat = re.compile(fnmatch.translate(rebind_exclude))
else:
pat = re.compile(
'|'.join(map(fnmatch.translate, rebind_exclude)))
match = re.match
for nodes_rule in nodes_rules:
for node in nodes_rule:
node.rebind = node.is_attribute and \
node.leaf_rule is None and \
match(pat, node.src) is None
else:
for nodes_rule in nodes_rules:
for node in nodes_rule:
node.rebind = node.is_attribute and \
node.leaf_rule is None
elif rebind is False:
for nodes_rule in nodes_rules:
for node in nodes_rule:
node.rebind = False
elif isinstance(rebind, str):
pat = re.compile(fnmatch.translate(rebind))
match = re.match
for nodes_rule in nodes_rules:
for node in nodes_rule:
node.rebind = node.is_attribute and \
node.leaf_rule is None and \
match(pat, node.src) is not None
else:
pat = re.compile('|'.join(map(fnmatch.translate, rebind)))
match = re.match
for nodes_rule in nodes_rules:
for node in nodes_rule:
node.rebind = node.is_attribute and \
node.leaf_rule is None and \
match(pat, node.src) is not None