Last active
August 24, 2019 12:01
-
-
Save me2beats/eb8c54fe3ac2388c5255e721804763bb to your computer and use it in GitHub Desktop.
MyBuilder test
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from kivy.app import App | |
from kivy.lang.builder import Builder | |
from kivy.context import get_current_context | |
from copy import copy | |
from my_builder import MyBuilder | |
KV = """ | |
BoxLayout | |
Button *2 | |
text: 'default button' | |
MyButton *2 | |
text: 'my button' | |
<-MyButton@Button> | |
my_normal: 0.4,0.3,0.5,1 | |
my_down: 0.3,0.2,0.4,1 | |
background_normal: '' | |
background_down: '' | |
background_color: self.my_normal if self.state == 'normal'\ | |
else self.my_down | |
canvas: | |
Color: | |
rgba: self.background_color | |
Rectangle: | |
size: self.size | |
pos: self.pos | |
Color: | |
rgba: 1, 1, 1, 1 | |
Rectangle: | |
texture: self.texture | |
size: self.texture_size | |
pos: int(self.center_x - self.texture_size[0] / 2.), int(self.center_y - self.texture_size[1] / 2.) | |
""" | |
class ProjectApp(App): | |
def __init__(self): | |
super().__init__() | |
context = copy(get_current_context()) | |
context['Builder'] = MyBuilder(self, Builder) | |
context.push() | |
def build(self): | |
self.root = Builder.load_string(KV) | |
#====always at the end (?)========# | |
context = get_current_context() | |
context.pop() | |
return super().build() | |
#================================== | |
#Idea? | |
# maybe subclass ProjectApp to make it more good looking | |
ProjectApp().run() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import sys | |
import re | |
from kivy.factory import Factory | |
from copy import copy | |
from kivy.lang.builder import BuilderException | |
#from kivy.lang.builder import * | |
from kivy.lang.parser import ParserRuleProperty, global_idmap, Parser, _handlers | |
from types import CodeType | |
from kivy.lang import BuilderBase | |
from kivy._event import Observable, EventDispatcher | |
from kivy.logger import Logger | |
from my_parser import MyParser | |
trace = Logger.trace | |
def call_fn(args, instance, v): | |
element, key, value, rule, idmap = args | |
if __debug__: | |
trace('Lang: call_fn %s, key=%s, value=%r, %r' % ( | |
element, key, value, rule.value)) | |
rule.count += 1 | |
e_value = eval(value, idmap) | |
if __debug__: | |
trace('Lang: call_fn => value=%r' % (e_value, )) | |
setattr(element, key, e_value) | |
def create_handler(iself, element, key, value, rule, idmap, delayed=False): | |
idmap = copy(idmap) | |
idmap.update(global_idmap) | |
idmap['self'] = iself.proxy_ref | |
bound_list = _handlers[iself.uid][key] | |
handler_append = bound_list.append | |
# we need a hash for when delayed, so we don't execute duplicate canvas | |
# callbacks from the same handler during a sync op | |
if delayed: | |
fn = delayed_call_fn | |
args = [element, key, value, rule, idmap, None] # see _delayed_start | |
else: | |
fn = call_fn | |
args = (element, key, value, rule, idmap) | |
# bind every key.value | |
if rule.watched_keys is not None: | |
for keys in rule.watched_keys: | |
base = idmap.get(keys[0]) | |
if base is None: | |
continue | |
f = base = getattr(base, 'proxy_ref', base) | |
bound = [] | |
was_bound = False | |
append = bound.append | |
# bind all attrs, except last to update_intermediates | |
k = 1 | |
for val in keys[1:-1]: | |
# if we need to dynamically rebind, bindm otherwise | |
# just add the attr to the list | |
if isinstance(f, (EventDispatcher, Observable)): | |
prop = f.property(val, True) | |
if prop is not None and getattr(prop, 'rebind', False): | |
# fbind should not dispatch, otherwise | |
# update_intermediates might be called in the middle | |
# here messing things up | |
uid = f.fbind( | |
val, update_intermediates, base, keys, bound, k, | |
fn, args) | |
append([f.proxy_ref, val, update_intermediates, uid]) | |
was_bound = True | |
else: | |
append([f.proxy_ref, val, None, None]) | |
elif not isinstance(f, type): | |
append([getattr(f, 'proxy_ref', f), val, None, None]) | |
else: | |
append([f, val, None, None]) | |
f = getattr(f, val, None) | |
if f is None: | |
break | |
k += 1 | |
# for the last attr we bind directly to the setting | |
# function, because that attr sets the value of the rule. | |
if isinstance(f, (EventDispatcher, Observable)): | |
uid = f.fbind(keys[-1], fn, args) # f is not None | |
if uid: | |
append([f.proxy_ref, keys[-1], fn, uid]) | |
was_bound = True | |
if was_bound: | |
handler_append(bound) | |
try: | |
return eval(value, idmap), bound_list | |
except Exception as e: | |
tb = sys.exc_info()[2] | |
raise BuilderException(rule.ctx, rule.line, | |
'{}: {}'.format(e.__class__.__name__, e), | |
cause=tb) | |
class MyBuilder(BuilderBase): | |
def __init__(self, app, builder): | |
super().__init__() | |
self.app = app | |
self.files = builder.files | |
self.dynamic_classes = builder.dynamic_classes | |
self.templates = builder.templates | |
self.rules = builder.rules | |
self.rulectx = builder.rulectx | |
def load_string(self, string, **kwargs): | |
'''Insert a string into the Language Builder and return the root widget | |
(if defined) of the kv string. | |
:Parameters: | |
`rulesonly`: bool, defaults to False | |
If True, the Builder will raise an exception if you have a root | |
widget inside the definition. | |
`filename`: str, defaults to None | |
If specified, the filename used to index the kv rules. | |
The filename parameter can be used to unload kv strings in the same way | |
as you unload kv files. This can be achieved using pseudo file names | |
e.g.:: | |
Build.load_string(""" | |
<MyRule>: | |
Label: | |
text="Hello" | |
""", filename="myrule.kv") | |
can be unloaded via:: | |
Build.unload_file("myrule.kv") | |
''' | |
kwargs.setdefault('rulesonly', False) | |
self._current_filename = fn = kwargs.get('filename', None) | |
# put a warning if a file is loaded multiple times | |
if fn in self.files: | |
Logger.warning( | |
'Lang: The file {} is loaded multiples times, ' | |
'you might have unwanted behaviors.'.format(fn)) | |
try: | |
# parse the string | |
#parser = Parser(content=string, filename=fn) #<-- default | |
parser = MyParser(content=string, filename=fn) | |
# merge rules with our rules | |
self.rules.extend(parser.rules) | |
self._clear_matchcache() | |
# add the template found by the parser into ours | |
for name, cls, template in parser.templates: | |
self.templates[name] = (cls, template, fn) | |
Factory.register(name, | |
cls=partial(self.template, name), | |
is_template=True, warn=True) | |
# register all the dynamic classes | |
for name, baseclasses in parser.dynamic_classes.items(): | |
Factory.register(name, baseclasses=baseclasses, filename=fn, | |
warn=True) | |
# create root object is exist | |
if kwargs['rulesonly'] and parser.root: | |
filename = kwargs.get('rulesonly', '<string>') | |
raise Exception('The file <%s> contain also non-rules ' | |
'directives' % filename) | |
# save the loaded files only if there is a root without | |
# template/dynamic classes | |
if fn and (parser.templates or | |
parser.dynamic_classes or parser.rules): | |
self.files.append(fn) | |
if parser.root: | |
widget = Factory.get(parser.root.name)(__no_builder=True) | |
rule_children = [] | |
widget.apply_class_lang_rules( | |
root=widget, rule_children=rule_children) | |
self._apply_rule( | |
widget, parser.root, parser.root, | |
rule_children=rule_children) | |
for child in rule_children: | |
child.dispatch('on_kv_post', widget) | |
widget.dispatch('on_kv_post', widget) | |
return widget | |
finally: | |
self._current_filename = None | |
def _apply_rule(self, widget, rule, rootrule, template_ctx=None, | |
ignored_consts=set(), rule_children=None): | |
# widget: the current instantiated widget | |
# rule: the current rule | |
# rootrule: the current root rule (for children of a rule) | |
# will collect reference to all the id in children | |
assert(rule not in self.rulectx) | |
self.rulectx[rule] = rctx = { | |
'ids': {'root': widget.proxy_ref}, | |
'set': [], 'hdl': []} | |
# extract the context of the rootrule (not rule!) | |
assert(rootrule in self.rulectx) | |
rctx = self.rulectx[rootrule] | |
# if a template context is passed, put it as "ctx" | |
if template_ctx is not None: | |
rctx['ids']['ctx'] = QueryDict(template_ctx) | |
# if we got an id, put it in the root rule for a later global usage | |
if rule.id: | |
# use only the first word as `id` discard the rest. | |
rule.id = rule.id.split('#', 1)[0].strip() | |
rctx['ids'][rule.id] = widget.proxy_ref | |
# set id name as a attribute for root widget so one can in python | |
# code simply access root_widget.id_name | |
_ids = dict(rctx['ids']) | |
_root = _ids.pop('root') | |
_new_ids = _root.ids | |
for _key in _ids.keys(): | |
if _ids[_key] == _root: | |
# skip on self | |
continue | |
_new_ids[_key] = _ids[_key] | |
_root.ids = _new_ids | |
# first, ensure that the widget have all the properties used in | |
# the rule if not, they will be created as ObjectProperty. | |
rule.create_missing(widget) | |
# build the widget canvas | |
if rule.canvas_before: | |
with widget.canvas.before: | |
self._build_canvas(widget.canvas.before, widget, | |
rule.canvas_before, rootrule) | |
if rule.canvas_root: | |
with widget.canvas: | |
self._build_canvas(widget.canvas, widget, | |
rule.canvas_root, rootrule) | |
if rule.canvas_after: | |
with widget.canvas.after: | |
self._build_canvas(widget.canvas.after, widget, | |
rule.canvas_after, rootrule) | |
# create children tree | |
Factory_get = Factory.get | |
Factory_is_template = Factory.is_template | |
for crule in rule.children: | |
cname = crule.name | |
#HERE WE GO=============================================== | |
if ' *' in cname: | |
multiplied = int(re.search('(\d+)', cname)[0]) | |
cname = cname.split(' *')[0] | |
else: | |
multiplied = 1 | |
#========================================================= | |
if cname in ('canvas', 'canvas.before', 'canvas.after'): | |
raise ParserException( | |
crule.ctx, crule.line, | |
'Canvas instructions added in kv must ' | |
'be declared before child widgets.') | |
# depending if the child rule is a template or not, we are not | |
# having the same approach | |
cls = Factory_get(cname) | |
if Factory_is_template(cname): | |
# we got a template, so extract all the properties and | |
# handlers, and push them in a "ctx" dictionary. | |
ctx = {} | |
idmap = copy(global_idmap) | |
idmap.update({'root': rctx['ids']['root']}) | |
if 'ctx' in rctx['ids']: | |
idmap.update({'ctx': rctx['ids']['ctx']}) | |
try: | |
for prule in crule.properties.values(): | |
value = prule.co_value | |
if type(value) is CodeType: | |
value = eval(value, idmap) | |
ctx[prule.name] = value | |
for prule in crule.handlers: | |
value = eval(prule.value, idmap) | |
ctx[prule.name] = value | |
except Exception as e: | |
tb = sys.exc_info()[2] | |
raise BuilderException( | |
prule.ctx, prule.line, | |
'{}: {}'.format(e.__class__.__name__, e), cause=tb) | |
# create the template with an explicit ctx | |
child = cls(**ctx) | |
widget.add_widget(child) | |
# reference it on our root rule context | |
if crule.id: | |
rctx['ids'][crule.id] = child | |
else: | |
# we got a "normal" rule, construct it manually | |
# we can't construct it without __no_builder=True, because the | |
# previous implementation was doing the add_widget() before | |
# apply(), and so, we could use "self.parent". | |
''' | |
child = cls(__no_builder=True) | |
widget.add_widget(child) | |
child.apply_class_lang_rules( | |
root=rctx['ids']['root'], rule_children=rule_children) | |
self._apply_rule( | |
child, crule, rootrule, rule_children=rule_children) | |
if rule_children is not None: | |
rule_children.append(child) | |
''' | |
#''' my code is here | |
for i in range(multiplied): | |
child = cls(__no_builder=True) | |
widget.add_widget(child) | |
child.apply_class_lang_rules( | |
root=rctx['ids']['root'], rule_children=rule_children) | |
self._apply_rule( | |
child, crule, rootrule, rule_children=rule_children) | |
if rule_children is not None: | |
rule_children.append(child) | |
#''' | |
# append the properties and handlers to our final resolution task | |
if rule.properties: | |
rctx['set'].append((widget.proxy_ref, | |
list(rule.properties.values()))) | |
for key, crule in rule.properties.items(): | |
# clear previously applied rules if asked | |
if crule.ignore_prev: | |
Builder.unbind_property(widget, key) | |
if rule.handlers: | |
rctx['hdl'].append((widget.proxy_ref, rule.handlers)) | |
# if we are applying another rule that the root one, then it's done for | |
# us! | |
if rootrule is not rule: | |
del self.rulectx[rule] | |
return | |
# normally, we can apply a list of properties with a proper context | |
try: | |
rule = None | |
for widget_set, rules in reversed(rctx['set']): | |
for rule in rules: | |
assert(isinstance(rule, ParserRuleProperty)) | |
key = rule.name | |
value = rule.co_value | |
if type(value) is CodeType: | |
value, bound = create_handler( | |
widget_set, widget_set, key, value, rule, | |
rctx['ids']) | |
# if there's a rule | |
if (widget_set != widget or bound or | |
key not in ignored_consts): | |
setattr(widget_set, key, value) | |
else: | |
if (widget_set != widget or | |
key not in ignored_consts): | |
setattr(widget_set, key, value) | |
except Exception as e: | |
if rule is not None: | |
tb = sys.exc_info()[2] | |
raise BuilderException(rule.ctx, rule.line, | |
'{}: {}'.format(e.__class__.__name__, | |
e), cause=tb) | |
raise e | |
# build handlers | |
try: | |
crule = None | |
for widget_set, rules in rctx['hdl']: | |
for crule in rules: | |
assert(isinstance(crule, ParserRuleProperty)) | |
assert(crule.name.startswith('on_')) | |
key = crule.name | |
if not widget_set.is_event_type(key): | |
key = key[3:] | |
idmap = copy(global_idmap) | |
idmap.update(rctx['ids']) | |
idmap['self'] = widget_set.proxy_ref | |
if not widget_set.fbind(key, custom_callback, crule, | |
idmap): | |
raise AttributeError(key) | |
# hack for on_parent | |
if crule.name == 'on_parent': | |
Factory.Widget.parent.dispatch(widget_set.__self__) | |
except Exception as e: | |
if crule is not None: | |
tb = sys.exc_info()[2] | |
raise BuilderException( | |
crule.ctx, crule.line, | |
'{}: {}'.format(e.__class__.__name__, e), cause=tb) | |
raise e | |
# rule finished, forget it | |
del self.rulectx[rootrule] | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from kivy.lang.parser import Parser, ParserRule, ParserRuleProperty | |
class MyParser(Parser): | |
def parse_level(self, level, lines, spaces=0): | |
'''Parse the current level (level * spaces) indentation. | |
''' | |
indent = spaces * level if spaces > 0 else 0 | |
objects = [] | |
current_object = None | |
current_property = None | |
current_propobject = None | |
i = 0 | |
while i < len(lines): | |
line = lines[i] | |
ln, content = line | |
# Get the number of space | |
tmp = content.lstrip(' \t') | |
# Replace any tab with 4 spaces | |
tmp = content[:len(content) - len(tmp)] | |
tmp = tmp.replace('\t', ' ') | |
# first indent designates the indentation | |
if spaces == 0: | |
spaces = len(tmp) | |
count = len(tmp) | |
if spaces > 0 and count % spaces != 0: | |
raise ParserException(self, ln, | |
'Invalid indentation, ' | |
'must be a multiple of ' | |
'%s spaces' % spaces) | |
content = content.strip() | |
rlevel = count // spaces if spaces > 0 else 0 | |
# Level finished | |
if count < indent: | |
return objects, lines[i - 1:] | |
# Current level, create an object | |
elif count == indent: | |
x = content.split(':', 1) | |
if not len(x[0]): | |
raise ParserException(self, ln, 'Identifier missing') | |
if (len(x) == 2 and len(x[1]) and | |
not x[1].lstrip().startswith('#')): | |
raise ParserException(self, ln, | |
'Invalid data after declaration') | |
name = x[0].rstrip() | |
# if it's not a root rule, then we got some restriction | |
# aka, a valid name, without point or everything else | |
if count != 0: | |
if False in [ord(z) in Parser.PROP_RANGE for z in name]: | |
#raise ParserException(self, ln, 'Invalid class name') # <-- kivy defa | |
#=================================== | |
if ' *' not in name: | |
raise ParserException(self, ln, 'Invalid class name') | |
#=================================== | |
current_object = ParserRule(self, ln, name, rlevel) | |
current_property = None | |
objects.append(current_object) | |
# Next level, is it a property or an object ? | |
elif count == indent + spaces: | |
x = content.split(':', 1) | |
if not len(x[0]): | |
raise ParserException(self, ln, 'Identifier missing') | |
# It's a class, add to the current object as a children | |
current_property = None | |
name = x[0].rstrip() | |
ignore_prev = name[0] == '-' | |
if ignore_prev: | |
name = name[1:] | |
if ord(name[0]) in Parser.CLASS_RANGE: | |
if ignore_prev: | |
raise ParserException( | |
self, ln, 'clear previous, `-`, not allowed here') | |
_objects, _lines = self.parse_level( | |
level + 1, lines[i:], spaces) | |
current_object.children = _objects | |
lines = _lines | |
i = 0 | |
# It's a property | |
else: | |
if name not in Parser.PROP_ALLOWED: | |
if not all(ord(z) in Parser.PROP_RANGE for z in name): | |
raise ParserException(self, ln, | |
'Invalid property name') | |
if len(x) == 1: | |
raise ParserException(self, ln, 'Syntax error') | |
value = x[1].strip() | |
if name == 'id': | |
if len(value) <= 0: | |
raise ParserException(self, ln, 'Empty id') | |
if value in ('self', 'root'): | |
raise ParserException( | |
self, ln, | |
'Invalid id, cannot be "self" or "root"') | |
current_object.id = value | |
elif len(value): | |
rule = ParserRuleProperty( | |
self, ln, name, value, ignore_prev) | |
if name[:3] == 'on_': | |
current_object.handlers.append(rule) | |
else: | |
ignore_prev = False | |
current_object.properties[name] = rule | |
else: | |
current_property = name | |
current_propobject = None | |
if ignore_prev: # it wasn't consumed | |
raise ParserException( | |
self, ln, 'clear previous, `-`, not allowed here') | |
# Two more levels? | |
elif count == indent + 2 * spaces: | |
if current_property in ( | |
'canvas', 'canvas.after', 'canvas.before'): | |
_objects, _lines = self.parse_level( | |
level + 2, lines[i:], spaces) | |
rl = ParserRule(self, ln, current_property, rlevel) | |
rl.children = _objects | |
if current_property == 'canvas': | |
current_object.canvas_root = rl | |
elif current_property == 'canvas.before': | |
current_object.canvas_before = rl | |
else: | |
current_object.canvas_after = rl | |
current_property = None | |
lines = _lines | |
i = 0 | |
else: | |
if current_propobject is None: | |
current_propobject = ParserRuleProperty( | |
self, ln, current_property, content) | |
if current_property[:3] == 'on_': | |
current_object.handlers.append(current_propobject) | |
else: | |
current_object.properties[current_property] = \ | |
current_propobject | |
else: | |
current_propobject.value += '\n' + content | |
# Too much indentation, invalid | |
else: | |
raise ParserException(self, ln, | |
'Invalid indentation (too many levels)') | |
# Check the next line | |
i += 1 | |
return objects, [] | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
you cannot just subclass kivy Builder, because this is a global kivy instance (something like a singleton ?).
but you can use something like that
https://github.com/kivy/kivy/wiki/Kv-language-preprocessing.
the code has been tested very little,
this is just an example that demonstrates a way to solve this problem (when you want to change the behavior of the kivy Builder and / or Parser)
demo gif
https://imgur.com/a/Hk7PQg2