Skip to content

Instantly share code, notes, and snippets.

@me2beats
Last active August 24, 2019 12:01
Show Gist options
  • Save me2beats/eb8c54fe3ac2388c5255e721804763bb to your computer and use it in GitHub Desktop.
Save me2beats/eb8c54fe3ac2388c5255e721804763bb to your computer and use it in GitHub Desktop.
MyBuilder test
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()
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]
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, []
@me2beats
Copy link
Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment