#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# **************************************************************************** #
# This file is part of the pdpy project: https://github.com/pdpy-org
# Copyright (C) 2021 Fede Camara Halac
# **************************************************************************** #
"""
Base
====
"""
from json import dumps as json_dumps
from ..encoding.xmltagconvert import XmlTagConvert
from ..encoding.xmlbuilder import XmlBuilder
from ..utilities.utils import log
from ..utilities.exceptions import ArgumentException, MalformedName
from ..utilities.default import Default
from ..utilities.namespace import Namespace
__all__ = [ 'Base' ]
[docs]class Base(XmlBuilder, XmlTagConvert):
""" The base class for all pdpy objects
Parameters
----------
patchname : :class:`str` or ``None``
Name of the Pd patch file (default: ``None``)
pdtype : :class:`str` or ``None``
Type of the Pd object: ``X``, ``N``, or ``A`` (default: ``X``)
cls : :class:`str` or ``None``
Class of the Pd object, eg.: ``msg``, ``text``, ... (default: ``obj``)
json : ``dict`` or ``None``
A json dictionary of key/value pairs to populate the object.
xml : ``xml.etree.ElementTree.Element`` or ``None``
An Xml Element with the appropriate element structure.
default : :class:`pdpy_lib.utilities.default.Default` or ``None``
A class containing a template for default values.
.. document private functions
.. automethod:: __pd__
.. automethod:: __xml_load__
.. automethod:: __json__
"""
def __init__(self,
patchname=None,
pdtype=None,
cls=None,
json=None,
xml=None,
default=None):
""" Initialize the object """
self.patchname = self.__sane_name__(patchname) # the name of the patch
self.__type__ = pdtype if pdtype is not None else 'X' # pd's type
self.__cls__ = cls if cls is not None else 'obj' # pd's class
self.__d__ = Default() if default is None else default # object defaults
self.__n__ = Namespace() # pdpy module namespace
self.__end__ = ';\r\n' # The pd line end character sequence
self.__semi__ = ' \\;' # The pd end symbol for data structures
self.__arr_idx__ = 0 # the array index
XmlTagConvert.__init__(self) # for xml tag conversion (output)
if json: self.__populate__(self, json) # fill the object with the json data
if xml:
XmlBuilder.__init__(self) # initialize the xml base builder class
tree = XmlBuilder.__xmlparse__(self, xml) # fill the object with xml
self.__xml_load__(tree)
def __parent__(self, parent=None, scope=None):
"""
Sets the parent of this object if `parent` is present,
otherwise returns the parent of this object.
"""
if scope is None:
scope = self
if parent is not None:
setattr(scope, '__p__', parent)
# print(f"adding parent {parent.__class__.__name__} to child {self.__class__.__name__}")
return self
elif hasattr(self, '__p__'):
return self.__p__
else:
raise ValueError("No parent set in " + self.__class__.__name__)
def __addparents__(self, parent, children=('nodes','edges','comments')):
""" Sets the parents of all children """
# log(1,'addparents: parent <--',parent.__pdpy__)#,repr(dir(child)))
for c in children:
for child in getattr(parent, c, []):
# log(1,f"addparents: child {c} --> ",child.__pdpy__)#,repr(dir(child)))
# self.__parent__(parent=parent, scope=child)
setattr(child, '__p__', parent)
if hasattr(child, c):
# log(1, 'addparents: child has children')
self.__addparents__(child)
def __getroot__(self, child):
""" Recursively, return the parent root of this object """
if hasattr(child, '__p__'):
# log(1, child.__class__.__name__, "parented")
return self.__getroot__(child.__p__)
else:
# log(1, child.__class__.__name__, "has no parent")
return child
def __getstruct__(self):
return getattr(self.__getroot__(self), 'structs', None)
def __setdata__(self, scope, data, attrib='data'):
""" Sets the data of the object """
# log(1, "scope:",scope.__class__.__name__, "data:", data)
if not hasattr(scope, attrib):
setattr(scope, attrib, [])
attribute = getattr(scope, attrib)
attribute.append(data)
return attribute[-1]
def __setattr__(self, name, value):
""" Hijack setattr to return ourselves as a dictionary """
if value is not None:
self.__dict__[name] = value
def __set_default__(self, kwargs, parameters):
""" Sets the defaut values or uses the provided keyword argument """
# print(kwargs, parameters)
# label = l or iemgui['symbol'],
# xoff = default['xoff'],
# yoff = default['yoff'],
# fface = iemgui['fontface'],
# fsize = default['fsize'],
# lbcolor = default['lbcolor']
for param in parameters:
# print(param)
k, d = param[:2]
# print(k)
# print(d)
callback = param[2] if len(param) == 3 else lambda x:x
if k in kwargs:
v = callback(kwargs.pop(k))
# print('found', v)
else:
# v = d if isinstance(d, dict) else getattr(d, k)
# print('notfound', k, type(d))
if isinstance(d, dict):
# print("d is a dictionary")
if k in d:
# print(k,"is in", d)
v = callback(d[k])
# print("after callback", v)
else:
# print(k,"not in", d)
v = callback(d)
# print("after callback", v)
elif isinstance(d, Default):
# print("passed Default")
v = getattr(d, k)
v = callback(v)
# print("after callback", v)
else:
v = d
# print('after callback', v)
# print("setting", k, "with value", v)
setattr(self, k, v)
[docs] def __json__(self, indent=4):
""" Return a JSON representation of the instance's scope as a string """
# inner function to filter out variables that are
# prefixed with two underscores ('_')
# with the exception of '__pdpy__'
def __filter__(o):
return {
k : v
for k,v in o.__dict__.items()
if not k.startswith("__") or k=="__pdpy__"
}
return json_dumps(
self,
default = __filter__,
sort_keys = False,
indent = indent
)
def __dumps__(self):
log(0, self.__class__.__name__, '-'*79,'\n', self.__json__(1))
def __num__(self, n):
""" Returns a number (or list of number) object from a Pd file string """
pdnm = None
if isinstance(n, str):
if "#" in n: pdnm = n # skip css-style colors preceded by '#'
elif ("e" in n or "E" in n) and ("-" in n or "+" in n):
pdnm = "{:e}".format(int(float(n)))
elif "." in n: pdnm = float(n)
else:
pdnm = int(n)
elif isinstance(n, list):
# print("__num__(): input was a list of str numbers", n)
pdnm = list(map(lambda x:self.__num__(x),n))
elif 0.0 == n:
return 0
else:
pdnm = n
return pdnm
def __pdbool__(self, n):
""" Returns a boolean object from a Pd file string """
b = False
if n == "True" or n == "true":
b = True
elif n == "False" or n == "false":
b = False
else:
b = bool(int(float(n)))
# log(1, "__pdbool__():", n, '->', b)
return b
def __isnum__(self, n):
try:
n = self.__num__(n)
return True
except:
return False
def __get_obj_size__(self, parent):
# print(self.__json__())
font_size = parent.font
if hasattr(self, 'size') or hasattr(self, 'area'):
size = self.area if hasattr(self, 'area') else self.size
# print(self.getname(), "Size", size.__pd__())
if hasattr(size, 'width') and hasattr(size, 'height'):
return size.width, size.height
else:
if hasattr(size, 'height'):
if self.className == 'vradio':
return size.height, size.height * self.number
else:
return size.height, size.height
elif hasattr(size, 'width'):
return size.width, size.width
text_h = font_size * 2
text_w = 0
if hasattr(self, 'className'):
text_w += len(self.className)
text_w += 1
if hasattr(self, 'args'):
text_w += len(' '.join(map(lambda x:str(x),self.args)))
text_w += 1
if hasattr(self, 'targets'):
for target in self.targets:
if hasattr(target, 'messages'):
text_w += len(' '.join(target.messages))
text_w += 1
text_w += 1
if hasattr(self, 'text'):
text_w += len(' '.join(self.text))
text_w += 1
mod_80 = (text_w-1) // 80
if mod_80 >= 1: text_h *= mod_80
return (text_w-1)*font_size//2, text_h
def __populate__(self, child, json):
""" Populates the derived/child class instance with a dictionary """
# TODO: protect against overblowing child scope
if not hasattr(json, 'items'):
log(1, child.__class__.__name__, "json is not a dict")
if not hasattr(json, '__dict__'):
raise ArgumentException(child.__class__.__name__ + ": json is not a class. It is of type: " + type(json) +"\n"+ json)
json = json.__dict__
# map(lambda k,v: setattr(child, k, v), json.items())
for k,v in json.items():
try:
v = self.__num__(v)
except:
try:
v = self.__pdbool__(v)
except:
pass
setattr(child, k, v)
if hasattr(child, 'className') and self.__cls__ is None:
self.__cls__ = child.className
def __unescape__(self, argv):
""" Unescapes the arguments """
args = []
for a in argv:
# unescape the arguments
if isinstance(a, list):
a = ' '.join(map(lambda x:str(x).replace('\\','',1), a))
else:
a = str(a).replace('\\','',1)
# and convert them to numbers
if self.__isnum__(a):
a = self.__num__(a)
args.append(a)
return args
def __escape__(self, arg):
""" Escapes the arguments """
# print("escape:", repr(arg))
arg = str(arg).replace('\\', '\\\\',1)
arg = arg.replace(' ', '\\ ',1)
arg = arg.replace('$', '\\$',1)
return arg
def __closeline__(self, pdtype, pdcls, pdargs):
""" Closes a Pd file line
"""
s = str("#") + str(pdtype) + " " + str(pdcls)
if pdargs is not None:
if isinstance(pdargs, list):
s += ' ' + ' '.join(pdargs)
else:
s += str(" ") + str(pdargs)
s += self.__end__
s = s.replace(' ', ' ')
return s
[docs] def __pd__(self, args=None):
""" Returns a the pd line for this object
Called by all derived classes to return a Pd line for this object.
If ``args`` is present the pd line will end with ``;\\r\\n``, and:
* If ``args`` is a list of strings, each element is appended to the pd line::
#N canvas >>> 0 22 340 520 12 >>> ; <<<
* If ``args`` is a string, it is appended to the pd line::
#X obj >>> 10 30 print >>> ; <<<
* If ``args`` is ``None``, the pd line is returned without arguments::
#X connect >>> <<<
Parameters
----------
args : ``list`` of :class:`str` or :class:`str` or ``None``
The arguments to the pd line.
Return
------
:class:`str`
the pd line for this object built with ``__type__`` and ``__cls__``
"""
s = self.__closeline__(self.__type__, self.__cls__, args)
# log(1, "Base.__pd__()", repr(s))
# split line at 80 chars:
# insert \r\n on the last space char
# s = '\n'.join(s[i:i+79] for i in range(0, len(s), 79))
return s
[docs] def __xml_load__(self, xml_tree):
""" Parse an XML tree into a PdPy object
Called if loading a PdPy object from an XML file.
.. note::
This method assumes it belongs to a PdPy class, because:
:class:`pdpy_lib.patching.pdpy.PdPy` bases :class:`pdpy_lib.core.base.Base`
"""
# get the xml root element
xml_root = xml_tree.getroot()
# get the encoding
self.encoding = xml_root.get('encoding', 'utf-8')
# root element to which we add stuff
root_dict = {'__p__' : self}
# find the structs tag
structs = xml_root.find('structs')
if structs is not None:
for n in structs.findall('struct'):
self.addStruct(xml=n) # belongs to PdPy class
# find the dependencies tag
for n in xml_root.findall('dependencies'):
self.addDependencies(xml=n) # belongs to PdPy class
# go through every element in 'root' and add it to the root_dict
for n in xml_root.find('root'):
# print('tag', n.tag)
if n.tag == 'pdpy' or n.tag == 'root':
if 'pdpy' in n.attrib:
root_dict.update({'__pdpy__': n.attrib['pdpy']})
elif n.tag == 'nodes':
root_dict.update({
'nodes': [XmlBuilder.__elem_to_obj__(self, x) for x in n]
})
elif n.tag == 'comments':
root_dict.update({
'comments': [XmlBuilder.__elem_to_obj__(self, x) for x in n]
})
elif n.tag == 'edges':
root_dict.update({
'edges': [XmlBuilder.__elem_to_obj__(self, x) for x in n]
})
else:
# an element belonging to canvas' attributes
o = XmlBuilder.__elem_to_obj__(self, n)
if hasattr(o, 'items'):
for k,v in o.items():
root_dict.update({k:v})
else:
root_dict.update({n.tag:o})
# add the root_dict to the PdPy object
self.addRoot(json=root_dict) # belongs to PdPy class
# spawn the __parent__ json tree
self.__jsontree__() # belongs to PdPy class
def __sane_name__(self, name):
if name is not None:
for c in name:
if c in (";", "$", "&", "|", ",", "`", "%", "*"):
raise MalformedName("Special chars in name were detected.")
return name
[docs] def getid(self):
""" Returns the id attribute or 0 if missing """
return self.id if hasattr(self, 'id') else 0
[docs] def getname(self):
""" Returns the className or PdPy class of the object if missing """
return getattr(self, 'className', self.__class__.__name__)
[docs] def addpos(self, x, y):
""" Adds or updates the position :class:`pdpy_lib.primitives.point.Point` """
# print("Adding position for:", self.getname(), x, y)
if not hasattr(self, 'position'):
from ..primitives.point import Point
x = int(x)
y = int(y)
setattr(self, 'position', Point(x=x, y=y))
else:
self.position.set_x(int(x))
self.position.set_y(int(y))