'''
kv Compilation
===============
'''
import textwrap
import inspect
import re
import ast
import copy
import inspect
from inspect import ismethod as _inspect_ismethod, \
isfunction as _inspect_isfunction
from kivy.lang.compiler.src_gen import KvCompiler
from kivy.lang.compiler.ast_parse import ParseKvFunctionTransformer, \
ParseKvBindTransformer, generate_source, KvCompilerParserException, \
ASTStrPlaceholder, verify_readonly_nodes
from kivy.lang.compiler.runtime import load_kvc_from_file, save_kvc_to_file
from kivy.lang.compiler.ast_parse import KvException
__all__ = ('kv', 'kv_apply_manual', 'patch_inspect', 'unpatch_inspect')
# XXX: https://bugs.python.org/issue31772
[docs]def kv(kv_syntax='minimal', proxy=False, rebind=True, bind_on_enter=False,
captures_are_readonly=True):
'''
Decorator factory function that returns a decorator that compiles a kv
containing function. Typical usage::
class MyWidget(Widget):
@kv() # notice that it MUST be called
def apply_kv(self):
with KvContext():
self.x @= self.y + 256
:param kv_syntax: The binding syntax to support. Default: `"minimal"`
With `"minimal"`, it's similar to traditional kv, and binds rules e.g.
`self.x @= self.widget.y` and `self.dict[self.name]`. When None, it
binds to a expanded set of syntax, e.g.
`(self.widget + self.widget2).width`.
The `"minimal"` syntax is recommended for most situations and `None`
should only be used in exceptional circumstance.
:param proxy: glob pattern(s) describing the widgets that should not hold
a direct reference to other widgets for garbage collection purposes.
Defaults to `False`
It is either `False` - when all widgets should hold direct references,
or `True` - when no widgets should hold direct references, or
a glob string describing the widget(s) that should not hold a
reference, e.g. `"*widget"` will match `self.widget` and
`self.my_widget`. Or it can be a list of glob strings and any that
match will not hold a direct reference.
This is to be used when binding widgets that have a very long life, and
it's not desirable that the widget prevent other widgets from being
garbage collected. This is mostly encountered when binding to the
global App, which never dies.
This is used for widgets that are being bound to, e.g. in the rule
`self.x @= self.app.my_x`, `proxy` may be set to `"*app"`, to prevent
the `self` widget from being kept alive by `app`. But is not needed
e.g. when binding to widgets that are not independently kept alive.
.. warning::
If all binding widgets are using proxies, no one will keep the kv
rules alive, and the rules will not be executed once garbage
collection runs. You can save a reference to the KvContext, and that
will keep that context alive as long as the context is held.
:param rebind: glob pattern(s) describing the intermediate widgets that
should be rebound when they change. Defaults to `True`.
It is either `True` - when all widgets should rebind,
or `False` - when no widgets should rebind, or
a glob string describing the widget(s) that should rebind, e.g.
`"*widget"` will match `self.widget` and
`self.my_widget` and both `widget` and `my_widget` will rebind. Or it
can be a list of glob strings and any that match will rebind.
This is used in rule e.g. `self.x @= self.widget.x`, if `self.widget`
is rebound, then when `self.widget` changes, the rulee rebind to `x`
belonging to the new widget stored in `self.widget`.
:param bind_on_enter: Where kv binding should occur for the context.
Defaults to False.
For a rule such as::
with KvContext():
...
binding can occur when the context is entered or exited. When
`bind_on_enter` is `True`, it occurs upon entrance, when `False` it
occurs upon exit. The default is `False`. Binding upon entrance is not
recommended because it's unintuitive and doesn't follow the typical
python programmatic flow.
:param captures_are_readonly: Whether any variables that participate
in a rule may be changed between rule execution and binding. Defaults
to `True`.
This parameter should not be changed to `False` except for debugging
purposes or if you truly understand the internals of binding.
:return: The compiled function associated with the original function,
or the original function if there was nothing to compile.
Once a function is decorated, calling kv again on it will not recompile
the function, unless the source or the compile options changed. This means
calling `kv()(f)` will not re-compile `f` (and it shouldn't need to).
'''
compile_flags = (
kv_syntax, proxy, rebind, bind_on_enter, captures_are_readonly)
def kv_decorate(func):
if func.__closure__ or '<locals>' in func.__qualname__:
raise KvException(
'The kv decorator cannot be used on a function that is a '
'closure or a local. It must be defined as a global function,'
'such as a class method or global function in a module')
# no lambda
mod, f = load_kvc_from_file(func, compile_flags=compile_flags)
if f is not None:
if f == 'use_original':
return func
f._kv_src_func_globals = func.__globals__
return f
compiler = KvCompiler()
inspect.getfile(func)
src = textwrap.dedent(inspect.getsource(func))
transformer = ParseKvFunctionTransformer(kv_syntax=kv_syntax)
graph = ast.parse(src)
# remove the kv decorator
func_def = graph.body[0]
assert isinstance(func_def, ast.FunctionDef)
if len(func_def.decorator_list) > 1:
raise KvCompilerParserException(
'Kv decorated functions can have only one decorator - a '
'single kv decorator')
del func_def.decorator_list[:]
copied_graph = None
if captures_are_readonly:
copied_graph = copy.deepcopy(graph)
ast_nodes = transformer.visit(graph)
if not transformer.context_infos:
save_kvc_to_file(
func,
'# There was no kv context or rules, the original function '
'will be returned instead.\n{} = "use_original"\n'.
format(func.__name__))
return func
func_def = graph.body[0]
assert isinstance(func_def, ast.FunctionDef)
func_body = func_def.body
assert isinstance(func_body, list)
for ctx_info in transformer.context_infos:
ctx = ctx_info['ctx']
ctx.parse_rules()
# these must happen *before* anything else and after all rules
ctx.set_nodes_proxy(proxy)
ctx.set_nodes_rebind(rebind)
ctx_name, funcs, rule_creation, rule_finalization, reinit = \
compiler.generate_bindings(ctx, None, create_rules=True)
ctx_info['assign_target_node'].id = ctx_name
before_ctx_node = ctx_info['before_ctx']
after_ctx_node = ctx_info['after_ctx']
if bind_on_enter:
before_ctx_node.src_lines = \
[''] + rule_creation + funcs + rule_finalization
else:
before_ctx_node.src_lines = [''] + rule_creation
after_ctx_node.src_lines = funcs + rule_finalization
after_ctx_node.src_lines += reinit
creation, deletion = compiler.gen_temp_vars_creation_deletion()
if captures_are_readonly:
# needs to happen after all the rules are parsed
verify_readonly_nodes(copied_graph, transformer, bind_on_enter)
update_node = ASTStrPlaceholder()
imports = [
'from kivy.lang.compiler.kv_context import KvContext as __KvContext, '
'KvRule as __KvRule']
if compiler.used_canvas_rule:
imports.append(
'from kivy.lang.compiler.runtime import add_graphics_callback '
'as __kv_add_graphics_callback')
if compiler.used_clock_rule:
imports.append('from kivy.clock import Clock as __kv_Clock')
if compiler.used_weak_ref:
imports.append(
'from weakref import ref as __kv_ref')
globals_update = [
'__kv_mod_func = {}'.format(func.__name__),
'globals().clear()',
'globals().update(__kv_mod_func._kv_src_func_globals)',
'globals()["{}"] = __kv_mod_func'.format(func.__name__),
'']
update_node.src_lines = imports + globals_update + creation
func_body.insert(0, update_node)
src_code = generate_source(ast_nodes) + '\n\n'
src_code = src_code + '\n'.join(
' {}'.format(line) for line in deletion)
src_code = re.sub('^ +$', '', src_code, flags=re.M) # del empty space
src_code = re.sub('\n\n\n+', '\n\n', src_code) # reduce newlines
save_kvc_to_file(func, src_code, compile_flags=compile_flags)
mod, f = load_kvc_from_file(func, compile_flags=compile_flags)
f._kv_src_func_globals = func.__globals__
return f
return kv_decorate
[docs]def kv_apply_manual(
ctx, callback, local_vars, global_vars, kv_syntax='minimal',
proxy=False, rebind=True):
'''
Similar to :func:`kv`, except that is is called manually to compile
bindings. Similalrly to :func:`kv`, the bindings are compiled once, and are
only recompiled if any of the compile options or source code file
containing the callback is modified.
This is meant to be used for debugging purposes, and may change in the
future.
Typical usage::
class ManualkvWidget(Widget):
def apply_kv(self):
ctx = KvParserContext()
def manage_val(*largs):
self.x = self.y + 512
manage_val()
# will bind to `self.y`.
rule = KvParserRule('self.y + 512')
rule.callback = manage_val
rule.callback_name = manage_val.__name__
ctx.add_rule(rule)
kv_apply_manual(ctx, self.apply_kv, locals(), globals())
:param ctx: a `KvParserContext` used for parsing the rules.
:param callback: The callback to call when any of the bindings change.
:param local_vars: `locals()` of the function where the bindings occur.
:param global_vars: `globals()` of the function where the bindings occur.
:param kv_syntax: See :func:`kv`.
:param proxy: See :func:`kv`.
:param rebind: See :func:`kv`.
:return: None - it executes the compiled rule in the provided `locals()`,
`globals()` environment.
'''
ctx_name = '__kv_ctx'
compiler = KvCompiler()
mod, f = load_kvc_from_file(callback, '__kv_manual_wrapper', 'manual')
if f is None:
transformer = ParseKvBindTransformer(kv_syntax)
ctx.set_kv_binding_ast_transformer(transformer)
ctx.parse_rules()
# these must happen *before* anything else and after all rules
ctx.set_nodes_proxy(proxy)
ctx.set_nodes_rebind(rebind)
_, funcs, rule_creation, rule_finalization, reinit = \
compiler.generate_bindings(ctx, ctx_name, create_rules=False)
creation, deletion = compiler.gen_temp_vars_creation_deletion()
globals_update = [
'__kv_mod_func = __kv_manual_wrapper',
'globals().clear()',
'globals().update(__kv_src_func_globals)',
'globals()["__kv_manual_wrapper"] = __kv_mod_func',
'']
lines = globals_update + creation + rule_creation + funcs + \
rule_finalization + reinit + deletion
lines = (' {}'.format(line) if line else '' for line in lines)
src = 'def __kv_manual_wrapper(__kv_src_func_globals, {}):\n{}'.\
format(ctx_name, '\n'.join(lines))
save_kvc_to_file(callback, src, 'manual')
mod, f = load_kvc_from_file(callback, '__kv_manual_wrapper', 'manual')
global_vars = global_vars.copy()
global_vars.update(local_vars)
f(global_vars, ctx)
def patch_inspect():
def ismethod_cython(object):
if _inspect_ismethod(object):
return True
if object.__class__.__name__ == 'cython_function_or_method' and \
hasattr(object, '__func__'):
return True
return False
inspect.ismethod = ismethod_cython
def isfunction_cython(object):
if _inspect_isfunction(object):
return True
if object.__class__.__name__ == 'cython_function_or_method' and not \
hasattr(object, '__func__'):
return True
return False
inspect.isfunction = isfunction_cython
def unpatch_inspect():
inspect.ismethod = _inspect_ismethod
inspect.isfunction = _inspect_isfunction