# -*- coding: utf-8 -*-
from __future__ import print_function
import math
import re
from .exception import PathError
from .node import GuerillaNode
from .plug import GuerillaPlug
from .util import iteritems
from .util import open_
# use to print missing implementation of python to lua value conversion
_print_missing_implementation = False
_print_unknown_command = False
_print_expression_node_connection = False
# guerilla class names inheriting from guerilla.Plug
plug_class_names = {'BakePlug',
'DynAttrPlug',
'ExpressionInput',
'ExpressionOutput',
'HostPlug',
'MeshPlug',
'Plug',
'UserPlug',
'HSetPlug',
'HVisiblePlug',
'HMattePlug',
'SceneGraphNodePropsPlug',
'SceneGraphNodeRenderPropsPlug',
'AttributePlug',
'AttributeShaderPlug'}
# types always having double quoted string ('"loop"')
parse_type_double_quoted_str = {
'types.texture',
'types.projectors',
'LUIPSTypeString',
'types.metal',
'types.materials',
'types.animationmode',
'types.colorspaces',
}
# types being simple floats (no parameters)
parse_type_float = {
'LUIPSTypeFloat',
'LUIPSTypeAngle0Pi2',
'LUIPSTypeFloat01Open',
'types.radians0pi4',
}
# types being floats with parameters (limits, etc.)
parse_type_float_with_param = {
'types.float',
'types.angle',
'types.radians',
'LUIPSTypeNumber',
}
# types being tuple
parse_type_tuple = {
'types.color',
'types.vector',
'types.vector2',
'LUIPSTypeColor',
'LUIPSTypeVector',
'LUIPSTypePoint'
}
# types being simple strings (often double quoted, but sometimes not)
parse_type_string = {
'types.string',
'types.set',
'types.typeboxmode',
}
# floating value regex "4.64"
_FLOAT_PARSE = re.compile('^[0-9.-]+$')
# float table {127.5,-80, ...}
_FLOAT_TABLE_PARSE = re.compile('^{[0-9.,-]+}$')
[docs]class GuerillaParser(object):
"""Guerilla .gproject file parser.
:ivar objs: Guerilla "object" per id (parsed in ``oid[<id>]``).
:vartype objs: dict[int, GuerillaNode|GuerillaPlug]
:ivar diagnose: Diagnose mode.
:vartype diagnose: bool
"""
_PY_TO_LUA_BOOL = {True: 'true',
False: 'false'}
_LUA_TO_PY_BOOL = {v: k for (k, v) in iteritems(_PY_TO_LUA_BOOL)}
# main line regex
_LINE_PARSE = re.compile(
r'\s*(oid\[(?P<oid>\d+)\]=)?'
r'(?P<cmd>\w+)'
r'\((?P<args>.*)\)\n')
# per command argument regex
_CMD_CREATE_ARG_PARSE = re.compile(
r'"(?P<type>[a-zA-Z0-9]+)",'
r'"(?P<parent>(\\"|[^"])*)",'
r'(("(?P<name>(\\"|[^"])*)")|(?P<name_number>-?\d+))'
r'(?P<rest>.*)')
_CREATE_REF_REST_PARSE = re.compile(
r'^,"(?P<path>(\\"|[^"])+)",(true|false),(true|false),'
r'(?P<param>({.*}|nil)),(true|false)$')
_CREATE_PLUG_REST_PARSE = re.compile(
r'^,(?P<flag>\d+),'
r'(?P<type>[a-zA-Z0-9.]+)( (?P<param>{.*}))?,'
r'(?P<value>.*)$')
_CMD_SET_ARG_PARSE = re.compile(
r'"\$(?P<id>\d+)(?P<path>(\\"|[^"])+)?\.(?P<plug>[\w\s]+)",'
r'(?P<value>.+)', re.UNICODE)
_CMD_CONNECT_ARG_PARSE = re.compile(
r'"\$(?P<in_id>\d+)((?P<in_path>(\\"|[^"])+)?\.(?P<in_plug>\w+))?",'
r'"\$(?P<out_id>\d+)((?P<out_path>(\\"|[^"])+)?\.(?P<out_plug>\w+))?"')
_CMD_DEPEND_ARG_PARSE = re.compile(
r'"\$(?P<in_id>\d+)((?P<in_path>(\\"|[^"])+)?\.(?P<in_plug>\w+))?",'
r'"\$(?P<out_id>\d+)((?P<out_path>(\\"|[^"])+)?\.(?P<out_plug>\w+))?"')
_PARENT_PARSE = re.compile(r'\$(?P<id>\d+)(?P<path>(\\"|[^"])+)?')
[docs] def __init__(self, content, diagnose=False):
"""Init the parser.
:param content: Raw Guerilla file content to parse.
:type content: str
:param diagnose: Will print some diagnostic information if True.
:type diagnose: bool
"""
super(GuerillaParser, self).__init__()
# original content of the gproject, never modified
self.__org_content = content # :type: str
# modified content of the gproject (modified by set_plug_value())
self.__mod_content = None # :type: str
self.__doc_format_rev = None
self.objs = {}
self._implicit_nodes = [] # :type: list[GuerillaNode]
self.diagnose = diagnose
# __create_and_get_implicit_node() do a huge amount of calls to
# GuerillaNode.path property, we have to cache its result for
# performance purpose.
# implicit nodes are "$44|foo|bar"
# key is (source node, path) where source node is the GuerillaNode
# representing $44 and path is "|foo|bar".
self.__implicit_node_cache = {}
self.__parse_nodes()
def __eq__(self, other):
"""Compare the content of this instance with the content of an other
parser.
:param other: Other parser instance to compare content from.
:type other: GuerillaParser
:return: True if both parser instance have same modified content.
:rtype: bool
"""
if self is other:
return True
return self.__mod_content == other.__mod_content
[docs] @classmethod
def from_file(cls, path, *args, **kwords):
"""Construct parser reading given file `path` content.
This is the main method to use if you want to use the parser.
:param path: Path of the Guerilla file to parse.
:type path: str
:return: Parser filled with content of given `path`.
:rtype: GuerillaParser
"""
with open_(path) as f:
content = f.read()
return cls(content, *args, **kwords)
@property
def has_changed(self):
"""Return if current parsed file has changed.
A parsed file can be changed using :meth:`set_plug_value()` method.
:return: True if both parser instance have same modified content.
:rtype: bool
"""
# no modified content mean we didn't tried to modified it
if self.__mod_content is None:
return False
else:
return self.__org_content != self.__mod_content
@property
def modified_content(self):
"""Modified parsed Guerilla file content.
A parsed file can be changed using :meth:`set_plug_value()` method.
:return: Modified parsed Guerilla file content.
:rtype: str
"""
if self.__mod_content is None:
return self.__org_content
else:
return self.__mod_content
@property
def original_content(self):
"""Original (unmodified) parsed Guerilla file content.
:return: Original (unmodified) parsed Guerilla file content.
:rtype: str
"""
return self.__org_content
[docs] def write(self, path):
"""Write modified content to given file `path`.
:param path: File path to write modified content in.
:type path: str
"""
with open(path, 'w') as f:
f.write(self.modified_content)
@property
def root(self):
"""Root node (top node of the parsed file).
On standard .gproject files, root node is the `Document`.
:return: Root node.
:rtype: GuerillaNode
"""
return self.objs[1]
@property
def doc_format_rev(self):
"""Document format revision.
:return: Document format revision.
:rtype: int
:raises AttributeError: If no document format revision is present in
file.
"""
if self.__doc_format_rev is None:
raise AttributeError("Missing doc format revision")
return self.__doc_format_rev
@classmethod
def __recursive_node(cls, node):
"""Macro to recursively iterate over children of given `node`
:param node: Node to iterate into children.
:type node: GuerillaNode
:rtype: collections.iterator[GuerillaNode]
"""
for child in node.children:
yield child
for sub_child in cls.__recursive_node(child):
yield sub_child
@property
def nodes(self):
"""Recursively iterate over nodes of the gproject file (except root
node).
:return: Generator of nodes of the parsed Guerilla file.
:rtype: collections.iterator[GuerillaNode]
"""
for node in self.__recursive_node(self.root):
yield node
@property
def plugs(self):
"""Iterate over plugs of the gproject file.
:return: Generator of plugs of the parsed Guerilla file.
:rtype: collections.iterator[GuerillaPlug]
"""
for node in self.nodes:
for plug in node.plugs:
yield plug
@staticmethod
def __clean_path(path):
"""Clean node path.
"|foo|sphereShape\\\\$" -> "|foo|sphereShape$"
"|bar|clous\\\\[1\\\\]" -> "|foo|clous[1]"
"""
return re.sub(r'\\\\(.)', r'\g<1>', path)
def __parse_nodes(self):
"""Parse commands in Guerilla file.
"""
self.objs = {}
for match in self._LINE_PARSE.finditer(self.original_content):
cmd = match.group('cmd')
args = match.group('args')
if cmd in 'docformatrevision':
self.__doc_format_rev = int(args)
elif cmd in ('create', 'createnotref'):
###############################################################
# create
###############################################################
oid = int(match.group('oid'))
match_arg = self._CMD_CREATE_ARG_PARSE.match(args)
type_ = match_arg.group('type')
parent = match_arg.group('parent')
name = match_arg.group('name')
if name is None:
name = match_arg.group('name_number')
if name is not None: # we have something !
name = int(name) # let's convert it to int
if name is None:
name = ""
# unescaped node names
if isinstance(name, str):
name = re.sub(r'\\(.)', r'\g<1>', name)
if parent in (r'\"\"', ''): # GADocument or root
parent = None
else:
parent_match_grp = self._PARENT_PARSE.match(parent)
parent_id = int(parent_match_grp.group('id'))
parent_path = parent_match_grp.group('path')
parent = self.objs[parent_id]
if parent_path:
parent_path = self.__clean_path(parent_path)
parent = self.__create_and_get_implicit_node(
parent, parent_path)
if type_ in plug_class_names:
assert not isinstance(name, int), (type(name), name)
###########################################################
# Plugs
###########################################################
rest = match_arg.group('rest')
match_rest = self._CREATE_PLUG_REST_PARSE.match(rest)
flag = int(match_rest.group('flag'))
plug_type = match_rest.group('type')
param = match_rest.group('param')
value = match_rest.group('value')
# convert value to python type
if plug_type in parse_type_string:
value = str(value)
elif plug_type in parse_type_float_with_param:
# value = float(value)
try:
value = float(value)
except ValueError:
pass # leave the value as float
if param is not None:
param = self.__lua_dict_to_python(param)
elif plug_type == 'types.bool':
value = bool(value)
elif plug_type == 'types.int':
value = int(value)
if param is not None:
param = self.__lua_dict_to_python(param)
elif plug_type in parse_type_tuple:
# "{1,0.5,0.5}" to (1,0.5,0.5)
value = eval(value.replace('{', '(').replace('}', ')'))
elif plug_type == 'types.enum':
value = str(value)
# '{{"Enabled","enable"},{"Disabled","disable"}}'
# TODO
param = {}
elif plug_type == 'types.hset':
# "Diffuse,-Reflection,-Refraction,Shadows"
value = set((s.replace(' ', '')
for s in value.split(',')))
elif plug_type == 'LUIPSTypeInt':
if value == 'nil':
# Tested in Guerilla 2.1, a 'nil' int value return
# None. So we reproduce this behavior here
value = None
else:
value = int(value)
elif plug_type in parse_type_float:
value = float(value)
elif plug_type in parse_type_double_quoted_str:
value = value[1:-1]
elif plug_type == 'types.combo':
# {"color","coordinates","density","fallof","fuel","pressure","temperature","velocity"},"density"
# TODO
param = {}
elif plug_type == 'types.multistring':
value = value[1:-1].split('\\010')
param = self.__lua_dict_to_python(param)
else:
assert False, args
# convert param to python dict
if any((param == '{}',
param is None)):
param = {}
assert isinstance(param, dict), param
plug = GuerillaPlug(name, type_, parent, value, flag)
assert oid not in self.objs, oid
self.objs[oid] = plug
else:
###########################################################
# Nodes
###########################################################
node = GuerillaNode(oid, name, type_, parent)
assert oid not in self.objs, oid
self.objs[oid] = node
if type_ == 'ArchReference':
#######################################################
# ArchReference
#######################################################
rest = match_arg.group('rest')
match_rest = self._CREATE_REF_REST_PARSE.match(rest)
path = match_rest.group('path')
param = match_rest.group('param')
GuerillaPlug('ReferenceFileName', 'Plug', node, path)
if self.diagnose:
if node.id == 1:
node_path = ""
else:
node_path = node.path
print(("Create '{node_path}' "
"'{node.type}'").format(**locals()))
elif cmd == 'set':
###############################################################
# set
###############################################################
match_arg = self._CMD_SET_ARG_PARSE.match(args)
oid = int(match_arg.group('id'))
path = match_arg.group('path')
plug_name = match_arg.group('plug')
org_value = match_arg.group('value')
value = self._lua_to_py_value(org_value)
node = self.objs[oid]
if path:
path = self.__clean_path(path)
node = self.__create_and_get_implicit_node(node, path)
GuerillaPlug(plug_name, 'Plug', node, value,
org_value=org_value)
if self.diagnose:
if node.id == 1:
node_path = ""
else:
node_path = node.path
print(('Set: {node_path}.{plug_name} -> '
'{value}').format(**locals()))
elif cmd == 'connect':
###############################################################
# connect
###############################################################
match_arg = self._CMD_CONNECT_ARG_PARSE.match(args)
in_oid = int(match_arg.group('in_id'))
in_path = match_arg.group('in_path')
in_plug_name = match_arg.group('in_plug')
out_oid = int(match_arg.group('out_id'))
out_path = match_arg.group('out_path')
out_plug_name = match_arg.group('out_plug')
in_node = self.objs[in_oid]
if out_oid is 0 and 0 not in self.objs:
# 0 is a special value referencing root document, we have a
# glayer trying to connect to document root attribute, we
# don't support this.
print(("Trying to connect to document reference "
"'{args}'").format(**locals()))
continue
out_node = self.objs[out_oid]
if in_path:
in_path = self.__clean_path(in_path)
in_node = self.__create_and_get_implicit_node(in_node,
in_path)
if out_path:
out_path = self.__clean_path(out_path)
out_node = self.__create_and_get_implicit_node(out_node,
out_path)
if not out_path and not out_plug_name:
# output is in the form "$64", an expression node
if _print_expression_node_connection:
print(out_node.type, out_node.path, '->',
in_node.type, in_node.path, in_plug_name)
# TODO: support when output is $64-like
continue
if not in_path and not in_plug_name:
# same here for inputs
if _print_expression_node_connection:
print(in_node.type, in_node.path, '->',
in_node.type, in_node.path, in_plug_name)
# TODO: support when input is $64-like
continue
# document is referencing a plug by its id, this mean a plug
# node has been created in the gproject so we "offset" the
# hierarchy to be consistent with the rest.
if in_plug_name is None:
in_plug_name = in_node.name
in_node = in_node.parent
if out_plug_name is None:
out_plug_name = out_node.name
out_node = out_node.parent
assert in_plug_name is not None
assert out_plug_name is not None
try:
in_plug = in_node.plug_dict[in_plug_name]
except KeyError:
in_plug = GuerillaPlug(in_plug_name, 'Plug', in_node)
try:
out_plug = out_node.plug_dict[out_plug_name]
except KeyError:
out_plug = GuerillaPlug(out_plug_name, 'Plug', out_node)
assert in_plug.input is None, in_plug_name
# p1.out -> p2.in
out_plug.outputs.append(in_plug)
in_plug.input = out_plug
if self.diagnose:
if out_node.id == 1:
out_node_path = ""
else:
out_node_path = out_node.path
if in_node.id == 1:
in_node_path = ""
else:
in_node_path = in_node.path
print(('Connect: {out_node_path}.{out_plug_name} -> '
'{in_node_path}.{in_plug_name}').format(**locals()))
elif cmd == 'depend':
###############################################################
# depend
###############################################################
match_arg = self._CMD_DEPEND_ARG_PARSE.match(args)
in_oid = int(match_arg.group('in_id'))
in_path = match_arg.group('in_path')
in_plug_name = match_arg.group('in_plug')
out_oid = int(match_arg.group('out_id'))
out_path = match_arg.group('out_path')
out_plug_name = match_arg.group('out_plug')
# Some .grendergraph/.glayer files attempts to connect to
# Guerilla root node. This node doesn't exists in the context
# of parsing:
# depend("$17.Out","$0|Preferences.ShutterClose")
if out_oid is 0 and 0 not in self.objs:
print(("Trying to depends on document reference "
"'{args}'").format(**locals()))
continue
in_node = self.objs[in_oid]
out_node = self.objs[out_oid]
if in_path:
in_path = self.__clean_path(in_path)
in_node = self.__create_and_get_implicit_node(in_node,
in_path)
if out_path:
out_path = self.__clean_path(out_path)
out_node = self.__create_and_get_implicit_node(out_node,
out_path)
if self.diagnose:
if out_node.id == 1:
out_node_path = ""
else:
out_node_path = out_node.path
if in_node.id == 1:
in_node_path = ""
else:
in_node_path = in_node.path
print(('Depend: {out_node_path}.{out_plug_name} -> '
'{in_node_path}.{in_plug_name}').format(**locals()))
# TODO: For now, dependencies are not supported
elif _print_unknown_command:
print("Unknown command '{cmd}'".format(**locals()))
def __create_and_get_implicit_node(self, start_node, path):
"""Macro to recursively create implicit nodes from given `path`
starting from given `start_node`.
if a direct path is present (instead of a direct node id)
this mean we have an "implicit" node (aka: not created by
the gproject itself).
$36|Frustum -> $36 is `start_node` and "|Fustum" is `path`, will create
a node from "UNKNOWN" type named "Frustrum" with node with id 36 as
parent.
:param start_node:
:type start_node: GuerillaNode
:param path:
:type path: str
:return:
:rtype: GuerillaNode
"""
# get in the cache for full path first
try:
return self.__implicit_node_cache[(start_node, path)]
except KeyError:
pass
# no implicit node found? create it along its path!
cur_parent = start_node
# use to store path along iteration
cur_path = ""
# for |foo|bar|toto, get-or-create 'foo', then 'bar', then 'toto'
for name in path.split('|')[1:]:
# aggregate current path ('|foo', then '|foo|bar', then
# '|foo|bar|toto')
cur_path = '|'.join((cur_path, name))
# get-or-create implicit node
try:
implicit_node = self.__implicit_node_cache[(start_node,
cur_path)]
except KeyError:
implicit_node = GuerillaNode(-1, name, 'UNKNOWN', cur_parent)
self._implicit_nodes.append(implicit_node)
# store it in the cache
self.__implicit_node_cache[(start_node, cur_path)] = \
implicit_node
# prepare next iteration
cur_parent = implicit_node
# we now have our implicit node
return cur_parent
@staticmethod
def __lua_dict_to_python(lua_dict_str):
"""Convert given lua table representation to python dict.
"{foo=1,bar=2}" -> {'foo': 1, 'bar': 2}
{min=0,max=16} to {'min': 0, max': 16}
{slidermax=4,min=0} to {'slidermax': 4, 'min': 0}
:param lua_dict_str: Lua table representation to convert in python.
:type lua_dict_str: str
:return: Lua table representation converted to python dict.
:rtype: dict
"""
# reformat
lua_dict_str = re.sub(r'([a-zA-Z0-9_-]+)=([a-zA-Z0-9_-]+)',
r"'\g<1>':\g<2>",
lua_dict_str)
# and eval as python expression
return eval(lua_dict_str.replace('=', ':'))
@staticmethod
def _lua_to_py_value(raw_str):
"""Convert given guerilla lua `raw_str` value expression to python.
:param raw_str: Raw string representing lua value to convert to python.
:type raw_str: str
:return: Value converted from lua to python.
:rtype: bool|float|list[float]|str
"""
if raw_str == 'true':
return True
elif raw_str == 'false':
return False
elif raw_str[0] == raw_str[-1] == '"': # surrounded by '"'?
# lua string
return str(raw_str[1:-1].replace(r'\010', '\n')
.replace(r'\009', '\t')
.replace(r'\"', '"')
.replace('\\\\', '\\'))
elif _FLOAT_PARSE.match(raw_str):
# we don't support int as "1" can be a float but lua hide
# fractional part
return float(raw_str)
elif _FLOAT_TABLE_PARSE.match(raw_str):
# float table: {127.5,-80, ...}
# eg. NodePos, PreClamp, PostClamp, Value, etc.
return [float(v) for v in raw_str[1:-1].split(',')]
# TODO: "matrix.create" and "transform.create" are not supported yet
# because we loose the matrix.create and transform.create
# information when setting plug values
'''elif raw_str.startswith('matrix.create{'):
content = raw_str[len('matrix.create{'):-1]
return [float(v) for v in content.split(',')]
elif raw_str.startswith('transform.create{'):
content = raw_str[len('transform.create{'):-1]
return [float(v) for v in content.split(',')]'''
elif raw_str in ('transform.Id', 'matrix.Id'):
# those are Guerilla shortcut to identity matrix
return raw_str
if _print_missing_implementation:
print(("Missing lua to python conversion "
"'{raw_str}'").format(**locals()))
return raw_str
@staticmethod
def __is_float_intable(value):
"""Return if given float `value` is possible to convert to int without
lost.
4.2 returns False, 4.0 return True
:param value:
:return: True if given `value` is convertible in `int` without.
precision loss.
"""
return (value - math.floor(value)) == 0.0
@classmethod
def _py_to_lua_value(cls, value):
"""Convert given python `value` to guerilla lua string representation.
:param value: Python value to convert in lua string representation.
:type value: bool|int|float|string|list[float]|dict
:return: Value converted from python to lua representation.
:rtype: str
"""
if type(value) is bool:
return cls._PY_TO_LUA_BOOL[value]
elif type(value) is int:
return str(value)
elif type(value) is float:
# transform 1000.0 to "1000"
if cls.__is_float_intable(value):
return str(int(value))
else:
return str(value)
elif type(value) is str:
if value in ('transform.Id', 'matrix.Id'):
return value
else:
value = value.replace('\\', '\\\\')\
.replace('"', '\\"')\
.replace('\n', '\\010')\
.replace('\t', '\\009')
return "\"{value}\"".format(**locals())
elif type(value) is list:
res = ['{']
for v in value:
if cls.__is_float_intable(v):
v = int(v)
res += [str(v), ',']
res.pop() # remove latest ","
res.append('}')
return "".join(res)
else:
print(("Missing python to lua conversion "
"'{value}'").format(**locals()))
return value
[docs] def path_to_node(self, path):
"""Find and return node at given `path`.
:Example:
>>> p.path_to_node('|foo|bar|bee')
GuerillaNode('bee', 10, 'primitive')
>>> p.path_to_node('$65|bar|bee')
GuerillaNode('bee', 10, 'primitive')
:param path: Path to get node from.
:type path: str
:return: Node found from given `path`.
:rtype: GuerillaNode
:raises PathError: If root node can't be found.
:raises PathError: If path contain unreachable nodes.
"""
assert path is not None
assert isinstance(path, str), (type(path), path)
# find first node of the path
if path.startswith('|'): # "|foo|bar|bee" like
cur_node = self.root # absolute path
elif path.startswith('$'): # "$65|bar|bee"
oid = int(path[1:path.find('|')]) # id "$65|" -> 65
cur_node = self.objs[oid]
else:
raise PathError("Can't find root '{path}'".format(**locals()))
# find node for each name in path:
# "|foo|bar|bee" -> look for "foo" in document children, then "bar" in
# "foo" children, etc.
for path_node_name in re.split(r'(?<!\\)\|', path)[1:]:
for node in cur_node.children:
if node._name_for_path == path_node_name:
cur_node = node
break # we've found it! let's move to the next node
else: # no break?
raise PathError("Can't find node '{path}'".format(**locals()))
return cur_node
[docs] def path_to_plug(self, path):
"""Find and return plug at given `path`.
:Example:
>>> p.path_to_plug('|foo|bar.DiffuseColor')
GuerillaPlug('DiffuseColor', 'Plug', '|foo|bar|bee')
:param path: Path to get plug from.
:type path: str
:return: Plug found from given `path`.
:rtype: GuerillaPlug
:raises PathError: If path doen't point to a plugs.
"""
# ('|foo|bar', 'DiffuseColor')
try:
node_path, plug_name = path.rsplit('.', 1)
except ValueError:
raise PathError("No plug in path '{path}'".format(
**locals()))
if node_path:
node = self.path_to_node(node_path)
else:
node = self.root # plug is connected to root document
try:
return node.get_plug(plug_name)
except KeyError:
raise PathError("Can't find plug '{}' in node '{}'".format(
plug_name, node.path))
[docs] @staticmethod
def node_to_id_path(node):
"""Return the shortest `id path` for given `node`.
Node id paths are paths relative to the first parent node with an id.
This methode is mostly for internal use to find plug value in
``set_plug_value()`` but is exposed to the user for his own
convinience; debugging, file compare, etc.
:Example:
>>> p.node_to_id_path(my_node)
'$60'
>>> p.node_to_id_path(my_other_node)
'$37|Frustum'
In the second line above, ``my_other_node`` is an implicit node. See
:doc:`file format information <../file_format_info>` page for more
information.
:param node: Implicit node to get `id path` from.
:type node: GuerillaNode
:return:
:rtype: str
"""
path = []
cur_node = node
# move to parent until we find a node with a valid id
while cur_node.id == -1:
path.append(cur_node.name)
cur_node = cur_node.parent
path.append("${cur_node.id}".format(**locals()))
return '|'.join(reversed(path))
@staticmethod
def __escape_str_for_regex(value):
"""Escape given string `value` to make it parsed in regex.
:param value: String to escape for regex.
:type value: str
:return: Escaped string.
:rtype: str
"""
return value.replace('\\', '\\\\')\
.replace('^', '\\^')\
.replace('$', '\\$')\
.replace('{', '\\{')\
.replace('}', '\\}')\
.replace('[', '\\[')\
.replace(']', '\\]')\
.replace('(', '\\(')\
.replace(')', '\\)')\
.replace('.', '\\.')\
.replace('*', '\\*')\
.replace('+', '\\+')\
.replace('?', '\\?')\
.replace('|', '\\|')\
.replace('<', '\\<')\
.replace('>', '\\>')\
.replace('-', '\\-')\
.replace('&', '\\&')
[docs] def set_plug_value(self, plug_values):
"""While exposed, this method is not stable yet and could potentially
change in the future.
:param plug_values:
:type plug_values: list[(GuerillaPlug, str)]
"""
# this list will be filled with "set(attr, value)" regex so we can
# create a "set(attr1, value1)|set(attr2, value2)|set(attr3, value3)"
# string that will be used to apply regex and set values only once
regex_list = []
# plug path (eg. "$60.Color"): value to set (eg. "{1,0,0}")
# this variable is used in the replace_func() function to find the
# value to set based on plug path
plug_value_list = {}
for plug, value in plug_values:
old_lua_value = self._py_to_lua_value(plug.value)
new_lua_value = self._py_to_lua_value(value)
path = self.node_to_id_path(plug.parent)
plug_path = "{path}.{plug.name}".format(**locals())
plug_value_list[plug_path] = new_lua_value
# regex is separated in three groups:
# set(" | $3.AxisColor | ",{0,0,0,1})
# we need the second
# the job of the replace function (outside the loop) is to
# recreated the latest group
regex_str = [r'(\s*set\(")(',
plug_path.replace('$', '\\$').replace('.', '\\.'),
')(",',
self.__escape_str_for_regex(old_lua_value),
'\\)\n)']
regex_list.append(''.join(regex_str))
# and of course, don't forget to set the value on the plug object
plug.value = value
# the "set(attr1, value1)|set(attr2, value2)|set(attr3, value3)" string
set_plug_regex = re.compile('|'.join(regex_list))
# the replace function point is to recreated the parsed line
def replace_func(match_grp):
set_prefix = match_grp.group(1) # 'set("'
plug_path = match_grp.group(2) # '$60.Color'
new_lua_value = plug_value_list[plug_path] # "{1,1,1}"
return '{0}{1}",{2})\n'.format(set_prefix,
plug_path,
new_lua_value)
if not self.__mod_content:
self.__mod_content = self.__org_content
self.__mod_content = set_plug_regex.sub(replace_func,
self.__mod_content)