diff --git a/lib/pyulib/src/ulib/ext/xpath/README.rst b/lib/pyulib/src/ulib/ext/xpath/README.rst index 6b260ef..0425286 100644 --- a/lib/pyulib/src/ulib/ext/xpath/README.rst +++ b/lib/pyulib/src/ulib/ext/xpath/README.rst @@ -217,7 +217,7 @@ Expression Context Objects The context contains the following attributes and methods: .. attribute:: default_namespace - + The default namespace URI. .. attribute:: namespaces diff --git a/lib/pyulib/src/ulib/ext/xpath/__init__.py b/lib/pyulib/src/ulib/ext/xpath/__init__.py index 6cfabb2..0741f64 100644 --- a/lib/pyulib/src/ulib/ext/xpath/__init__.py +++ b/lib/pyulib/src/ulib/ext/xpath/__init__.py @@ -1,10 +1,149 @@ -import exceptions +from xpath.exceptions import * +import xpath.exceptions +import xpath.expr +import xpath.parser +import xpath.yappsrt -from _xpath import api, XPathContext, XPath -from exceptions import * +__all__ = ['find', 'findnode', 'findvalue', 'XPathContext', 'XPath'] +__all__.extend((x for x in dir(xpath.exceptions) if not x.startswith('_'))) -__all__ = ['find', 'findnode', 'findvalue', 'findvalues', 'XPathContext', 'XPath'] -__all__.extend((x for x in dir(exceptions) if not x.startswith('_'))) +def api(f): + """Decorator for functions and methods that are part of the external + module API and that can throw XPathError exceptions. + + The call stack for these exceptions can be very large, and not very + interesting to the user. This decorator rethrows XPathErrors to + trim the stack. + + """ + def api_function(*args, **kwargs): + try: + return f(*args, **kwargs) + except XPathError, e: + raise e + api_function.__name__ = f.__name__ + api_function.__doc__ = f.__doc__ + return api_function + +class XPathContext(object): + def __init__(self, document=None, **kwargs): + self.default_namespace = None + self.namespaces = {} + self.variables = {} + + if document is not None: + if document.nodeType != document.DOCUMENT_NODE: + document = document.ownerDocument + if document.documentElement is not None: + attrs = document.documentElement.attributes + for attr in (attrs.item(i) for i in xrange(attrs.length)): + if attr.name == 'xmlns': + self.default_namespace = attr.value + elif attr.name.startswith('xmlns:'): + self.namespaces[attr.name[6:]] = attr.value + + self.update(**kwargs) + + def clone(self): + dup = XPathContext() + dup.default_namespace = self.default_namespace + dup.namespaces.update(self.namespaces) + dup.variables.update(self.variables) + return dup + + def update(self, default_namespace=None, namespaces=None, + variables=None, **kwargs): + if default_namespace is not None: + self.default_namespace = default_namespace + if namespaces is not None: + self.namespaces = namespaces + if variables is not None: + self.variables = variables + self.variables.update(kwargs) + + @api + def find(self, expr, node, **kwargs): + return xpath.find(expr, node, context=self, **kwargs) + + @api + def findnode(self, expr, node, **kwargs): + return xpath.findnode(expr, node, context=self, **kwargs) + + @api + def findvalue(self, expr, node, **kwargs): + return xpath.findvalue(expr, node, context=self, **kwargs) + + @api + def findvalues(self, expr, node, **kwargs): + return xpath.findvalues(expr, node, context=self, **kwargs) + +class XPath(): + _max_cache = 100 + _cache = {} + + def __init__(self, expr): + """Init docs. + """ + try: + parser = xpath.parser.XPath(xpath.parser.XPathScanner(str(expr))) + self.expr = parser.XPath() + except xpath.yappsrt.SyntaxError, e: + raise XPathParseError(str(expr), e.pos, e.msg) + + @classmethod + def get(cls, s): + if isinstance(s, cls): + return s + try: + return cls._cache[s] + except KeyError: + if len(cls._cache) > cls._max_cache: + cls._cache.clear() + expr = cls(s) + cls._cache[s] = expr + return expr + + @api + def find(self, node, context=None, **kwargs): + if context is None: + context = XPathContext(node, **kwargs) + elif kwargs: + context = context.clone() + context.update(**kwargs) + return self.expr.evaluate(node, 1, 1, context) + + @api + def findnode(self, node, context=None, **kwargs): + result = self.find(node, context, **kwargs) + if not xpath.expr.nodesetp(result): + raise XPathTypeError("expression is not a node-set") + if len(result) == 0: + return None + return result[0] + + @api + def findvalue(self, node, context=None, **kwargs): + result = self.find(node, context, **kwargs) + if xpath.expr.nodesetp(result): + if len(result) == 0: + return None + result = xpath.expr.string(result) + return result + + @api + def findvalues(self, node, context=None, **kwargs): + result = self.find(node, context, **kwargs) + if not xpath.expr.nodesetp(result): + raise XPathTypeError("expression is not a node-set") + return [xpath.expr.string_value(x) for x in result] + + def __repr__(self): + return '%s.%s(%s)' % (self.__class__.__module__, + self.__class__.__name__, + repr(str(self.expr))) + + def __str__(self): + return str(self.expr) @api def find(expr, node, **kwargs): diff --git a/lib/pyulib/src/ulib/ext/xpath/exceptions.py b/lib/pyulib/src/ulib/ext/xpath/exceptions.py index 1597670..7ce017a 100644 --- a/lib/pyulib/src/ulib/ext/xpath/exceptions.py +++ b/lib/pyulib/src/ulib/ext/xpath/exceptions.py @@ -15,7 +15,7 @@ class XPathParseError(XPathError): XPathError.__init__(self) self.expr = expr self.pos = pos - self.message = message + self.err = message def __str__(self): return ("Syntax error:\n" + diff --git a/lib/pyulib/src/ulib/ext/xpath/expr.py b/lib/pyulib/src/ulib/ext/xpath/expr.py index b681a0d..4199813 100644 --- a/lib/pyulib/src/ulib/ext/xpath/expr.py +++ b/lib/pyulib/src/ulib/ext/xpath/expr.py @@ -6,7 +6,8 @@ import re import xml.dom import weakref -from exceptions import * +from xpath.exceptions import * +import xpath # @@ -21,6 +22,8 @@ def string_value(node): for n in axes['descendant'](node): if n.nodeType == n.TEXT_NODE: s += n.data + elif n.nodeType == n.CDATA_SECTION_NODE: + s += n.nodeValue return s elif node.nodeType == node.ATTRIBUTE_NODE: @@ -31,9 +34,12 @@ def string_value(node): node.nodeType == node.TEXT_NODE): return node.data + elif node.nodeType == node.CDATA_SECTION_NODE: + return node.nodeValue + def document_order(node): """Compute a document order value for the node. - + cmp(document_order(a), document_order(b)) will return -1, 0, or 1 if a is before, identical to, or after b in the document respectively. @@ -100,10 +106,10 @@ def string(v): return u'Infinity' elif v == float('-inf'): return u'-Infinity' - elif int(v) == v and v <= 0xffffffff: - v = int(v) elif str(v) == 'nan': return u'NaN' + elif int(v) == v and v <= 0xffffffff: + v = int(v) return unicode(v) elif booleanp(v): return u'true' if v else u'false' @@ -747,7 +753,7 @@ class PathExpr(Expr): class PredicateList(Expr): """A list of predicates. - + Predicates are handled as an expression wrapping the expression filtered by the predicates. @@ -883,7 +889,8 @@ class CommentTest(object): class TextTest(object): def match(self, node, axis, context): - return node.nodeType == node.TEXT_NODE + return (node.nodeType == node.TEXT_NODE or + node.nodeType == node.CDATA_SECTION_NODE) def __str__(self): return 'text()' diff --git a/lib/pyulib/src/ulib/ext/xpath/parser.g b/lib/pyulib/src/ulib/ext/xpath/parser.g deleted file mode 100644 index df75bb1..0000000 --- a/lib/pyulib/src/ulib/ext/xpath/parser.g +++ /dev/null @@ -1,252 +0,0 @@ -import expr as X -from yappsrt import * - -%% - -parser XPath: - option: 'no-support-module' - - ignore: r'\s+' - token END: r'$' - - token FORWARD_AXIS_NAME: - r'child|descendant-or-self|attribute|self|descendant|following-sibling|following|namespace' - token REVERSE_AXIS_NAME: - r'parent|preceding-sibling|preceding|ancestor-or-self|ancestor' - - # Dire hack here, since yapps2 has only one token of lookahead: NCNAME - # does not match when followed by a open paren. - token NCNAME: r'[a-zA-Z_][a-zA-Z0-9_\-\.\w]*(?!\()' - token FUNCNAME: r'[a-zA-Z_][a-zA-Z0-9_\-\.\w]*' - - token DQUOTE: r'\"(?:[^\"])*\"' - token SQUOTE: r"\'(?:[^\'])*\'" - token NUMBER: r'((\.[0-9]+)|([0-9]+(\.[0-9]*)?))([eE][\+\-]?[0-9]+)?' - token EQ_COMP: r'\!?\=' - token REL_COMP: r'[\<\>]\=?' - token ADD_COMP: r'[\+\-]' - token MUL_COMP: r'\*|div|mod' - - rule XPath: - Expr END {{ return Expr }} - - rule Expr: - OrExpr {{ return OrExpr }} - - rule OrExpr: - AndExpr {{ Expr = AndExpr }} - ( - r'or' AndExpr - {{ Expr = X.OrExpr('or', Expr, AndExpr) }} - )* {{ return Expr }} - - rule AndExpr: - EqualityExpr {{ Expr = EqualityExpr }} - ( - r'and' EqualityExpr - {{ Expr = X.AndExpr('and', Expr, EqualityExpr) }} - )* {{ return Expr }} - - rule EqualityExpr: - RelationalExpr {{ Expr = RelationalExpr }} - ( - EQ_COMP - RelationalExpr - {{ Expr = X.EqualityExpr(EQ_COMP, Expr, RelationalExpr) }} - )* {{ return Expr }} - - rule RelationalExpr: - AdditiveExpr {{ Expr = AdditiveExpr }} - ( - REL_COMP - AdditiveExpr - {{ Expr = X.EqualityExpr(REL_COMP, Expr, AdditiveExpr) }} - )* {{ return Expr }} - - rule AdditiveExpr: - MultiplicativeExpr {{ Expr = MultiplicativeExpr }} - ( - ADD_COMP - MultiplicativeExpr - {{ Expr = X.ArithmeticalExpr(ADD_COMP, Expr, MultiplicativeExpr) }} - )* {{ return Expr }} - - rule MultiplicativeExpr: - UnionExpr {{ Expr = UnionExpr }} - ( - MUL_COMP - UnionExpr - {{ Expr = X.ArithmeticalExpr(MUL_COMP, Expr, UnionExpr) }} - )* {{ return Expr }} - - rule UnionExpr: - UnaryExpr {{ Expr = UnaryExpr }} - ( - '\|' UnaryExpr - {{ Expr = X.UnionExpr('|', Expr, UnaryExpr) }} - )* {{ return Expr }} - - rule UnaryExpr: - r'\-' ValueExpr {{ return X.NegationExpr(ValueExpr) }} - | ValueExpr {{ return ValueExpr }} - - rule ValueExpr: - PathExpr {{ return PathExpr }} - - rule PathExpr: - r'\/' {{ path = None }} - [ - RelativePathExpr {{ path = RelativePathExpr }} - ] {{ return X.AbsolutePathExpr(path) }} - | r'\/\/' RelativePathExpr - {{ step = X.AxisStep('descendant-or-self') }} - {{ RelativePathExpr.steps.insert(0, step) }} - {{ return X.AbsolutePathExpr(RelativePathExpr) }} - | RelativePathExpr {{ return RelativePathExpr }} - - rule RelativePathExpr: - StepExpr {{ steps = [StepExpr] }} - ( - ( - r'\/' - | r'\/\/' - {{ steps.append(X.AxisStep('descendant-or-self')) }} - ) - StepExpr {{ steps.append(StepExpr) }} - )* - {{ return X.PathExpr(steps) }} - - rule StepExpr: - AxisStep {{ return AxisStep }} - | FilterExpr {{ return FilterExpr }} - - rule AxisStep: - ( - ForwardStep {{ step = ForwardStep }} - | ReverseStep {{ step = ReverseStep }} - ) {{ expr = X.AxisStep(*step) }} - [ - PredicateList - {{ expr = X.PredicateList(expr, PredicateList, step[0]) }} - ] - {{ return expr }} - - rule ForwardStep: - ForwardAxis NodeTest {{ return [ForwardAxis, NodeTest] }} - | AbbrevForwardStep {{ return AbbrevForwardStep }} - - rule ForwardAxis: - FORWARD_AXIS_NAME r'::' {{ return FORWARD_AXIS_NAME }} - - rule AbbrevForwardStep: - {{ axis = 'child' }} - [ - r'@' {{ axis = 'attribute' }} - ] - NodeTest {{ return [axis, NodeTest] }} - - rule ReverseStep: - ReverseAxis NodeTest {{ return [ReverseAxis, NodeTest] }} - | AbbrevReverseStep {{ return AbbrevReverseStep }} - - rule ReverseAxis: - REVERSE_AXIS_NAME r'::' {{ return REVERSE_AXIS_NAME }} - - rule AbbrevReverseStep: - r'\.\.' {{ return ['parent', None] }} - - rule NodeTest: - KindTest {{ return KindTest }} - | NameTest {{ return NameTest }} - - rule NameTest: - # We also support the XPath 2.0 :*. - {{ prefix = None }} - WildcardOrNCName {{ localpart = WildcardOrNCName }} - [ - r':' WildcardOrNCName {{ prefix = localpart }} - {{ localpart = WildcardOrNCName }} - ] - {{ return X.NameTest(prefix, localpart) }} - - rule WildcardOrNCName: - r'\*' {{ return '*' }} - | NCNAME {{ return NCNAME }} - - rule FilterExpr: - PrimaryExpr - [ - PredicateList - {{ PrimaryExpr = X.PredicateList(PrimaryExpr,PredicateList) }} - ] {{ return PrimaryExpr }} - - rule PredicateList: - Predicate {{ predicates = [Predicate] }} - ( - Predicate {{ predicates.append(Predicate) }} - )* {{ return predicates }} - - rule Predicate: - r'\[' Expr r'\]' {{ return Expr }} - - rule PrimaryExpr: - Literal {{ return X.LiteralExpr(Literal) }} - | VariableReference {{ return VariableReference }} - | r'\(' Expr r'\)' {{ return Expr }} - | ContextItemExpr {{ return ContextItemExpr }} - | FunctionCall {{ return FunctionCall }} - - rule VariableReference: - r'\$' QName - {{ return X.VariableReference(*QName) }} - - rule ContextItemExpr: - r'\.' {{ return X.AxisStep('self') }} - - rule FunctionCall: - FUNCNAME r'\(' {{ args = [] }} - [ - Expr {{ args.append(Expr) }} - ( - r'\,' Expr {{ args.append(Expr) }} - )* - ] r'\)' {{ return X.Function(FUNCNAME, args) }} - - rule KindTest: - PITest {{ return PITest }} - | CommentTest {{ return CommentTest }} - | TextTest {{ return TextTest }} - | AnyKindTest {{ return AnyKindTest }} - - rule PITest: - r'processing-instruction' {{ name = None }} - r'\(' [ - NCNAME {{ name = NCNAME }} - | StringLiteral {{ name = StringLiteral }} - ] r'\)' {{ return X.PITest(name) }} - - rule CommentTest: - r'comment' r'\(' r'\)' {{ return X.CommentTest() }} - - rule TextTest: - r'text' r'\(' r'\)' {{ return X.TextTest() }} - - rule AnyKindTest: - r'node' r'\(' r'\)' {{ return X.AnyKindTest() }} - - rule Literal: - NumericLiteral {{ return NumericLiteral }} - | StringLiteral {{ return StringLiteral }} - - rule NumericLiteral: - NUMBER {{ return float(NUMBER) }} - - rule StringLiteral: - DQUOTE {{ return DQUOTE[1:-1] }} - | SQUOTE {{ return SQUOTE[1:-1] }} - - rule QName: - NCNAME {{ name = NCNAME }} - [ - r'\:' NCNAME {{ return (name, NCNAME) }} - ] {{ return (None, name) }} diff --git a/lib/pyulib/src/ulib/ext/xpath/parser.py b/lib/pyulib/src/ulib/ext/xpath/parser.py index bb673f9..504de78 100644 --- a/lib/pyulib/src/ulib/ext/xpath/parser.py +++ b/lib/pyulib/src/ulib/ext/xpath/parser.py @@ -1,5 +1,5 @@ -import expr as X -from yappsrt import * +import xpath.expr as X +from xpath.yappsrt import * from string import * @@ -8,10 +8,10 @@ import re class XPathScanner(Scanner): patterns = [ ("r'\\:'", re.compile('\\:')), - ("r'node'", re.compile('node')), - ("r'text'", re.compile('text')), - ("r'comment'", re.compile('comment')), - ("r'processing-instruction'", re.compile('processing-instruction')), + ("r'node\\s*\\('", re.compile('node\\s*\\(')), + ("r'text\\s*\\('", re.compile('text\\s*\\(')), + ("r'comment\\s*\\('", re.compile('comment\\s*\\(')), + ("r'processing-instruction\\s*\\('", re.compile('processing-instruction\\s*\\(')), ("r'\\,'", re.compile('\\,')), ("r'\\.'", re.compile('\\.')), ("r'\\$'", re.compile('\\$')), @@ -121,7 +121,7 @@ class XPath(Parser): return Expr def UnaryExpr(self): - _token_ = self._peek("r'\\-'", "r'\\/'", "r'\\/\\/'", "r'\\('", 'FORWARD_AXIS_NAME', "r'@'", 'REVERSE_AXIS_NAME', "r'\\.\\.'", "r'\\$'", "r'\\.'", 'FUNCNAME', 'NUMBER', 'DQUOTE', 'SQUOTE', "r'processing-instruction'", "r'comment'", "r'text'", "r'node'", "r'\\*'", 'NCNAME') + _token_ = self._peek("r'\\-'", "r'\\/'", "r'\\/\\/'", "r'\\('", 'FORWARD_AXIS_NAME', "r'@'", 'REVERSE_AXIS_NAME', "r'\\.\\.'", "r'\\$'", "r'\\.'", 'FUNCNAME', 'NUMBER', 'DQUOTE', 'SQUOTE', "r'processing-instruction\\s*\\('", "r'comment\\s*\\('", "r'text\\s*\\('", "r'node\\s*\\('", "r'\\*'", 'NCNAME') if _token_ == "r'\\-'": self._scan("r'\\-'") ValueExpr = self.ValueExpr() @@ -135,11 +135,11 @@ class XPath(Parser): return PathExpr def PathExpr(self): - _token_ = self._peek("r'\\/'", "r'\\/\\/'", "r'\\('", 'FORWARD_AXIS_NAME', "r'@'", 'REVERSE_AXIS_NAME', "r'\\.\\.'", "r'\\$'", "r'\\.'", 'FUNCNAME', 'NUMBER', 'DQUOTE', 'SQUOTE', "r'processing-instruction'", "r'comment'", "r'text'", "r'node'", "r'\\*'", 'NCNAME') + _token_ = self._peek("r'\\/'", "r'\\/\\/'", "r'\\('", 'FORWARD_AXIS_NAME', "r'@'", 'REVERSE_AXIS_NAME', "r'\\.\\.'", "r'\\$'", "r'\\.'", 'FUNCNAME', 'NUMBER', 'DQUOTE', 'SQUOTE', "r'processing-instruction\\s*\\('", "r'comment\\s*\\('", "r'text\\s*\\('", "r'node\\s*\\('", "r'\\*'", 'NCNAME') if _token_ == "r'\\/'": self._scan("r'\\/'") path = None - if self._peek("r'\\('", 'FORWARD_AXIS_NAME', "r'@'", 'REVERSE_AXIS_NAME', "r'\\.\\.'", "r'\\$'", "r'\\.'", 'FUNCNAME', 'NUMBER', 'DQUOTE', 'SQUOTE', "r'processing-instruction'", "r'comment'", "r'text'", "r'node'", "r'\\*'", 'NCNAME', "'\\|'", 'MUL_COMP', 'ADD_COMP', 'REL_COMP', 'EQ_COMP', "r'and'", "r'or'", 'END', "r'\\]'", "r'\\)'", "r'\\,'") not in ["'\\|'", 'MUL_COMP', 'ADD_COMP', 'REL_COMP', 'EQ_COMP', "r'and'", "r'or'", 'END', "r'\\]'", "r'\\)'", "r'\\,'"]: + if self._peek("r'\\('", 'FORWARD_AXIS_NAME', "r'@'", 'REVERSE_AXIS_NAME', "r'\\.\\.'", "r'\\$'", "r'\\.'", 'FUNCNAME', 'NUMBER', 'DQUOTE', 'SQUOTE', "r'processing-instruction\\s*\\('", "r'comment\\s*\\('", "r'text\\s*\\('", "r'node\\s*\\('", "r'\\*'", 'NCNAME', "'\\|'", 'MUL_COMP', 'ADD_COMP', 'REL_COMP', 'EQ_COMP', "r'and'", "r'or'", 'END', "r'\\]'", "r'\\)'", "r'\\,'") not in ["'\\|'", 'MUL_COMP', 'ADD_COMP', 'REL_COMP', 'EQ_COMP', "r'and'", "r'or'", 'END', "r'\\]'", "r'\\)'", "r'\\,'"]: RelativePathExpr = self.RelativePathExpr() path = RelativePathExpr return X.AbsolutePathExpr(path) @@ -168,7 +168,7 @@ class XPath(Parser): return X.PathExpr(steps) def StepExpr(self): - _token_ = self._peek("r'\\('", 'FORWARD_AXIS_NAME', "r'@'", 'REVERSE_AXIS_NAME', "r'\\.\\.'", "r'\\$'", "r'\\.'", 'FUNCNAME', 'NUMBER', 'DQUOTE', 'SQUOTE', "r'processing-instruction'", "r'comment'", "r'text'", "r'node'", "r'\\*'", 'NCNAME') + _token_ = self._peek("r'\\('", 'FORWARD_AXIS_NAME', "r'@'", 'REVERSE_AXIS_NAME', "r'\\.\\.'", "r'\\$'", "r'\\.'", 'FUNCNAME', 'NUMBER', 'DQUOTE', 'SQUOTE', "r'processing-instruction\\s*\\('", "r'comment\\s*\\('", "r'text\\s*\\('", "r'node\\s*\\('", "r'\\*'", 'NCNAME') if _token_ not in ["r'\\('", "r'\\$'", "r'\\.'", 'FUNCNAME', 'NUMBER', 'DQUOTE', 'SQUOTE']: AxisStep = self.AxisStep() return AxisStep @@ -177,7 +177,7 @@ class XPath(Parser): return FilterExpr def AxisStep(self): - _token_ = self._peek('FORWARD_AXIS_NAME', "r'@'", 'REVERSE_AXIS_NAME', "r'\\.\\.'", "r'processing-instruction'", "r'comment'", "r'text'", "r'node'", "r'\\*'", 'NCNAME') + _token_ = self._peek('FORWARD_AXIS_NAME', "r'@'", 'REVERSE_AXIS_NAME', "r'\\.\\.'", "r'processing-instruction\\s*\\('", "r'comment\\s*\\('", "r'text\\s*\\('", "r'node\\s*\\('", "r'\\*'", 'NCNAME') if _token_ not in ['REVERSE_AXIS_NAME', "r'\\.\\.'"]: ForwardStep = self.ForwardStep() step = ForwardStep @@ -191,7 +191,7 @@ class XPath(Parser): return expr def ForwardStep(self): - _token_ = self._peek('FORWARD_AXIS_NAME', "r'@'", "r'processing-instruction'", "r'comment'", "r'text'", "r'node'", "r'\\*'", 'NCNAME') + _token_ = self._peek('FORWARD_AXIS_NAME', "r'@'", "r'processing-instruction\\s*\\('", "r'comment\\s*\\('", "r'text\\s*\\('", "r'node\\s*\\('", "r'\\*'", 'NCNAME') if _token_ == 'FORWARD_AXIS_NAME': ForwardAxis = self.ForwardAxis() NodeTest = self.NodeTest() @@ -207,7 +207,7 @@ class XPath(Parser): def AbbrevForwardStep(self): axis = 'child' - if self._peek("r'@'", "r'processing-instruction'", "r'comment'", "r'text'", "r'node'", "r'\\*'", 'NCNAME') == "r'@'": + if self._peek("r'@'", "r'processing-instruction\\s*\\('", "r'comment\\s*\\('", "r'text\\s*\\('", "r'node\\s*\\('", "r'\\*'", 'NCNAME') == "r'@'": self._scan("r'@'") axis = 'attribute' NodeTest = self.NodeTest() @@ -233,7 +233,7 @@ class XPath(Parser): return ['parent', None] def NodeTest(self): - _token_ = self._peek("r'processing-instruction'", "r'comment'", "r'text'", "r'node'", "r'\\*'", 'NCNAME') + _token_ = self._peek("r'processing-instruction\\s*\\('", "r'comment\\s*\\('", "r'text\\s*\\('", "r'node\\s*\\('", "r'\\*'", 'NCNAME') if _token_ not in ["r'\\*'", 'NCNAME']: KindTest = self.KindTest() return KindTest @@ -315,7 +315,7 @@ class XPath(Parser): FUNCNAME = self._scan('FUNCNAME') self._scan("r'\\('") args = [] - if self._peek("r'\\,'", "r'\\)'", "r'\\-'", "r'\\/'", "r'\\/\\/'", "r'\\('", 'FORWARD_AXIS_NAME', "r'@'", 'REVERSE_AXIS_NAME', "r'\\.\\.'", "r'\\$'", "r'\\.'", 'FUNCNAME', 'NUMBER', 'DQUOTE', 'SQUOTE', "r'processing-instruction'", "r'comment'", "r'text'", "r'node'", "r'\\*'", 'NCNAME') not in ["r'\\,'", "r'\\)'"]: + if self._peek("r'\\,'", "r'\\)'", "r'\\-'", "r'\\/'", "r'\\/\\/'", "r'\\('", 'FORWARD_AXIS_NAME', "r'@'", 'REVERSE_AXIS_NAME', "r'\\.\\.'", "r'\\$'", "r'\\.'", 'FUNCNAME', 'NUMBER', 'DQUOTE', 'SQUOTE', "r'processing-instruction\\s*\\('", "r'comment\\s*\\('", "r'text\\s*\\('", "r'node\\s*\\('", "r'\\*'", 'NCNAME') not in ["r'\\,'", "r'\\)'"]: Expr = self.Expr() args.append(Expr) while self._peek("r'\\,'", "r'\\)'") == "r'\\,'": @@ -326,24 +326,23 @@ class XPath(Parser): return X.Function(FUNCNAME, args) def KindTest(self): - _token_ = self._peek("r'processing-instruction'", "r'comment'", "r'text'", "r'node'") - if _token_ == "r'processing-instruction'": + _token_ = self._peek("r'processing-instruction\\s*\\('", "r'comment\\s*\\('", "r'text\\s*\\('", "r'node\\s*\\('") + if _token_ == "r'processing-instruction\\s*\\('": PITest = self.PITest() return PITest - elif _token_ == "r'comment'": + elif _token_ == "r'comment\\s*\\('": CommentTest = self.CommentTest() return CommentTest - elif _token_ == "r'text'": + elif _token_ == "r'text\\s*\\('": TextTest = self.TextTest() return TextTest - else:# == "r'node'" + else:# == "r'node\\s*\\('" AnyKindTest = self.AnyKindTest() return AnyKindTest def PITest(self): - self._scan("r'processing-instruction'") + self._scan("r'processing-instruction\\s*\\('") name = None - self._scan("r'\\('") if self._peek('NCNAME', "r'\\)'", 'DQUOTE', 'SQUOTE') != "r'\\)'": _token_ = self._peek('NCNAME', 'DQUOTE', 'SQUOTE') if _token_ == 'NCNAME': @@ -356,20 +355,17 @@ class XPath(Parser): return X.PITest(name) def CommentTest(self): - self._scan("r'comment'") - self._scan("r'\\('") + self._scan("r'comment\\s*\\('") self._scan("r'\\)'") return X.CommentTest() def TextTest(self): - self._scan("r'text'") - self._scan("r'\\('") + self._scan("r'text\\s*\\('") self._scan("r'\\)'") return X.TextTest() def AnyKindTest(self): - self._scan("r'node'") - self._scan("r'\\('") + self._scan("r'node\\s*\\('") self._scan("r'\\)'") return X.AnyKindTest() diff --git a/lib/pyulib/src/ulib/ext/xpath/yappsrt.py b/lib/pyulib/src/ulib/ext/xpath/yappsrt.py index 6dff734..c8d8933 100644 --- a/lib/pyulib/src/ulib/ext/xpath/yappsrt.py +++ b/lib/pyulib/src/ulib/ext/xpath/yappsrt.py @@ -51,14 +51,14 @@ class Scanner: raise NotImplementedError("Unimplemented: restriction set changed") return self.tokens[i] raise NoMoreTokens() - + def __repr__(self): """Print the last 10 tokens that have been scanned in""" output = '' for t in self.tokens[-10:]: output = '%s\n (@%s) %s = %s' % (output,t[0],t[2],repr(t[3])) return output - + def scan(self, restrict): """Should scan another token and add it to the list, self.tokens, and add the restriction to self.restrictions""" @@ -77,7 +77,7 @@ class Scanner: # We got a match that's better than the previous one best_pat = p best_match = len(m.group(0)) - + # If we didn't find anything, raise an error if best_pat == '(error)' and best_match < 0: msg = "Bad Token" @@ -105,13 +105,13 @@ class Parser: def __init__(self, scanner): self._scanner = scanner self._pos = 0 - + def _peek(self, *types): """Returns the token type for lookahead; if there are any args then the list of args is the set of token types to allow""" tok = self._scanner.token(self._pos, types) return tok[2] - + def _scan(self, type): """Returns the matched text, and moves to the next token""" tok = self._scanner.token(self._pos, [type]) diff --git a/lib/uinst/conf b/lib/uinst/conf index b0a5061..510fc38 100644 --- a/lib/uinst/conf +++ b/lib/uinst/conf @@ -14,6 +14,10 @@ rm -f .nutools-devel # supprimer fichiers de développement rm -rf lib/pyulib/{build,devel,migrate,test} +# compiler les modules python de support +estep "Compilation des modules python" +python -m compileall lib/ulib/support/python + # liens pour les scripts python for i in plver plbck uencdetect urandomize umail uxpath wofixsql; do ln -s lib/pywrapper "$i" @@ -21,6 +25,7 @@ done ln -s lib/ulib/support/cgiupload.py ln -s lib/ulib/support/cgiparams.py ln -s lib/ulib/support/cgilsxml.py +ln -s lib/ulib/support/xpathtool.py # liens pour les scripts shell for i in cg cgs; do diff --git a/lib/ulib/ptools b/lib/ulib/ptools index 2205179..0206f92 100644 --- a/lib/ulib/ptools +++ b/lib/ulib/ptools @@ -3,8 +3,9 @@ ##@cooked nocomments ##@include vcs ##@include semver +##@include xmlsupport uprovide ptools -urequire vcs semver +urequire vcs semver xmlsupport function is_any_branch() { local branch="$1"; shift @@ -99,43 +100,93 @@ function list_feature_branches() { ################################################################################ # Outils de haut niveau -function __pom_get_version() { - # obtenir la version dans le pom $1(=pom.xml) - local pom="${1:-pom.xml}" - awk <"$pom" '/^[ \t]*/ { - sub(/^.*/, "") - sub(/<\/version>.*$/, "") - print - exit -}' +__pver_perror() { + local r="$1"; shift + [ $# -gt 0 ] || set "Une erreur s'est produite" + local m=$r + [ $m -gt $# ] && m=$# + [ $m -gt 0 ] && eerror "${!m}" + return $r } -function __pom_set_version() { - # modifier la version du le fichier $1(=pom.xml) à la valeur - # $2(=1.0.0-SNAPSHOT) + +__pver_unless() { + local r m + if [[ "$1" == -* ]]; then + [ "${@:1:2}" ] 2>/dev/null; r=$? + shift; shift + else + [ "${@:1:3}" ] 2>/dev/null; r=$? + shift; shift; shift + fi + __pver_perror $r "$@" +} + +function __pver_get_prel_version() { + # retourner la version correspondant à la branche courante si c'est une + # branche de release. retourner 1 si la branche courante n'est pas une + # branche de release, 2 si on n'est pas dans un dépôt git + local branch + branch="$(git_get_branch)" || return 2 + if [[ "$branch" == release-* ]]; then + echo "${branch#release-}" + return 0 + else + return 1 + fi +} +function __pver_pom_get_vpath() { + # pour le fichier $1(=pom.xml), retourner le chemin dans lequel se trouve la + # version du projet + local pom="${1:-pom.xml}" + if xpathtool -tf "$pom" /project/version 2>/dev/null; then + echo /project/version + elif xpathtool -tf "$pom" /project/parent/version 2>/dev/null; then + echo /project/parent/version + else + echo /project/version + fi +} + +function __pver_pom_get_version() { + # obtenir la version dans le pom $1(=pom.xml) à partir du chemin $2, qui est + # déterminé automatiquement s'il n'est pas spécifié. + local pom="${1:-pom.xml}" + local vpath="$2" + local version + if [ -n "$vpath" ]; then + xpathtool -f "$pom" -g "$vpath" + return + fi + version="$(xpathtool -f "$pom" -g /project/version 2>/dev/null)" + if [ -n "$version" ]; then + echo "$version" + return + fi + version="$(xpathtool -f "$pom" -g /project/parent/version 2>/dev/null)" + if [ -n "$version" ]; then + echo "$version" + return + fi +} + +function __pver_pom_set_version() { + # modifier la version dans le pom $1(=pom.xml) à la valeur + # $2(=1.0.0-SNAPSHOT) en utilisant le chemin xpath $3, qui est déterminé + # automatiquement s'il n'est pas spécifié. + # retourner 0 si la version a été mise à jour dans le chemin /project/version + # retourner 1 si la version a été mise à jour dans le chemin /project/parent/version + # retourner 2 si la version a été mise à jour dans un autre chemin + # retourner 3 si une erreur s'est produite local pom="${1:-pom.xml}" local version="${2:-1.0.0-SNAPSHOT}" - local tmpfile; ac_set_tmpfile tmpfile - awk <"$pom" >"$tmpfile" ' -BEGIN { - version = '"$(qawk "$version")"' - found = 0 -} -!found && $0 ~ /^[ \t]*/ { - prefix = "" - if (match($0, /^.*/)) { - prefix = substr($0, RSTART, RLENGTH) - } - suffix = "" - if (match($0, /<\/version>.*$/)) { - suffix = substr($0, RSTART, RLENGTH) - } - print prefix version suffix - found = 1 - next -} -{ print }' - cat "$tmpfile" >"$pom" - ac_clean "$tmpfile" + local vpath="$3" + [ -n "$vpath" ] || vpath="$(__pver_pom_get_vpath "$pom")" + xpathtool -f "$pom" -s "$vpath" "$version" || return 3 + case "$vpath" in + /project/version) return 0;; + /project/parent/version) return 1;; + *) return 2;; + esac } function pver() { @@ -145,92 +196,170 @@ function pver() { local -a args local action=auto local source=auto - local file= - local git= - local version= - local allow_empty= - local convert=auto - local operator= - local oversion= - local setversion= - local incversion= - local setprelease= - local setalpha= - local setbeta= - local setrc= - local setrelease= - local setmetadata= addmetadata= - local vcsmetadata= + local file git version operator oversion + local setversion incversion + local setpr setprelease setsnapshot setalpha setbeta setrc setrelease + local setmd resetmetadata setmetadata addmetadata vcsmetadata + local vpath setmapfile mapfile allow_empty convert=auto maven_update parse_opts "${PRETTYOPTS[@]}" \ --help '$exit_with pver_display_help' \ - -f:,--file: '$set@ file; source=file' \ - -e:,--maven:,--pom: '$set@ file; source=pom' \ - -F:,--file-string: '$set@ file; source=file-string' \ - -g:,--git-string: '$set@ git; source=git-string' \ - -s:,--string: '$set@ version; source=string' \ + -w:,--w:,--fw:,--auto-file: '$set@ file; source=auto-file' \ + --sw:,--auto-string: '$set@ file; source=auto-string' \ + --gw:,--auto-git-string: '$set@ git; source=auto-git-string' \ + -e:,--e:,--pom:,--maven: '$set@ file; source=pom' \ + -E:,--E:,--pom-string:,--maven-string: '$set@ file; source=pom-string' \ + -f:,--f:,--file: '$set@ file; source=file' \ + -F:,--F:,--file-string: '$set@ file; source=file-string' \ + -g:,--g:,--git-file-string:,--git-string: '$set@ git; source=git-file-string' \ + -G:,--G:,--git-pom-string: '$set@ git; source=git-pom-string' \ + --git-prel-string source=git-prel-string \ + -s:,--s:,--string: '$set@ version; source=string' \ --show action=show \ - --allow-empty allow_empty=1 \ + --show-source action=show-source \ --check action=check \ - --convert convert=1 \ - --no-convert convert= \ --eq: '$action=compare; operator=eq; set@ oversion' \ --ne: '$action=compare; operator=ne; set@ oversion' \ --lt: '$action=compare; operator=lt; set@ oversion' \ --le: '$action=compare; operator=le; set@ oversion' \ --gt: '$action=compare; operator=gt; set@ oversion' \ --ge: '$action=compare; operator=ge; set@ oversion' \ - -v:,--set-version: '$action=update; set@ setversion; incversion=' \ - --prel '$action=update; setversion=prel; incversion=' \ -u,--update '$action=update; [ -z "$incversion" ] && incversion=auto' \ --menu '$action=update; incversion=menu' \ + -v:,--set-version: '$action=update; set@ setversion; incversion=' \ + --prel '$action=update; setversion=prel; incversion=' \ -x,--major '$action=update; incversion=major' \ -z,--minor '$action=update; incversion=minor' \ -p,--patchlevel '$action=update; incversion=patchlevel' \ - -l:,--prelease:,--prerelease: '$action=update; set@ setprelease; [ -z "$setprelease" ] && { setalpha=; setbeta=; setrc=; setrelease=1; }' \ - -a,--alpha '$action=update; setalpha=1; setbeta=; setrc=; setrelease=' \ - -b,--beta '$action=update; setalpha=; setbeta=1; setrc=; setrelease=' \ - -r,--rc '$action=update; setalpha=; setbeta=; setrc=1; setrelease=' \ - -R,--release,--final '$action=update; setalpha=; setbeta=; setrc=; setrelease=1' \ - -m:,--metadata:,--set-metadata: '$action=update; set@ setmetadata' \ - --add-metadata: '$action=update; set@ addmetadata' \ - -M,--vcs-metadata '$action=update; vcsmetadata=1' \ + -k,--keep '$action=update; incversion=' \ + -l:,--prelease:,--prerelease: '$action=update; setpr=1; setsnapshot=; setalpha=; setbeta=; setrc=; setrelease=; set@ setprelease; [ -z "$setprelease" ] && setrelease=1' \ + -S,--snapshot '$action=update; setpr=1; setsnapshot=1; setalpha=; setbeta=; setrc=; setrelease=' \ + -a,--alpha '$action=update; setpr=1; setsnapshot=; setalpha=1; setbeta=; setrc=; setrelease=' \ + -b,--beta '$action=update; setpr=1; setsnapshot=; setalpha=; setbeta=1; setrc=; setrelease=' \ + -r,--rc '$action=update; setpr=1; setsnapshot=; setalpha=; setbeta=; setrc=1; setrelease=' \ + -R,--release,--final '$action=update; setpr=1; setsnapshot=; setalpha=; setbeta=; setrc=; setrelease=1' \ + -m:,--metadata:,--set-metadata: '$action=update; setmd=1; resetmetadata=1; set@ setmetadata' \ + --add-metadata: '$action=update; setmd=1; set@ addmetadata' \ + -M,--vcs-metadata '$action=update; setmd=1; vcsmetadata=1' \ + --vpath: vpath= \ + --map: '$setmapfile=1; set@ mapfile' \ + --allow-empty allow_empty=1 \ + --convert convert=1 \ + --no-convert convert= \ + -t,--maven-update maven_update=1 \ @ args -- "$@" && set -- "${args[@]}" || { eerror "$args"; return 1; } # Calculer la source + if [ "$source" == auto-file -o "$source" == auto-string ]; then + [ -n "$file" ] || file=. + if [ -d "$file" ]; then + if [ -f "$file/$DEFAULT_POM" ]; then + file="$file/$DEFAULT_POM" + elif [ -f "$file/$DEFAULT_FILE" ]; then + file="$file/$DEFAULT_FILE" + elif [ -f "$file/version.txt" ]; then + file="$file/version.txt" + else + file="$file/$DEFAULT_FILE" + fi + fi + if [ "$source" == auto-file ]; then + if [[ "$file" == *.xml ]]; then source=pom + else source=file + fi + elif [ "$source" == auto-string ]; then + if [[ "$file" == *.xml ]]; then source=pom-string + else source=file-string + fi + fi + edebug "Auto-sélection de $(ppath "$file")" + + elif [ "$source" == auto-git-string ]; then + git_check_gitvcs || return 2 + splitfsep2 "$git" : branch name + [ -n "$branch" ] || branch=master + if [ "$(git cat-file -t "$branch:$name" 2>/dev/null)" == tree ]; then + if git rev-parse --verify --quiet "$branch:${name:+$name/}$DEFAULT_POM" >/dev/null; then + name="${name:+$name/}$DEFAULT_POM" + elif git rev-parse --verify --quiet "$branch:${name:+$name/}$DEFAULT_FILE" >/dev/null; then + name="${name:+$name/}$DEFAULT_FILE" + elif git rev-parse --verify --quiet "$branch:${name:+$name/}version.txt" >/dev/null; then + name="${name:+$name/}version.txt" + fi + fi + if git rev-parse --verify --quiet "$branch:$name" >/dev/null; then + if [[ "$name" == *.xml ]]; then source=git-pom-string + else source=git-file-string + fi + git="$branch:$name" + edebug "Auto-sélection de $git" + else + eerror "$name: fichier introuvable sur la branche $branch" + return 1 + fi + fi + if [ "$source" == auto ]; then source=file - for i in "$DEFAULT_FILE" version.txt "$DEFAULT_POM"; do + for i in "$DEFAULT_POM" "$DEFAULT_FILE" version.txt; do if [ -f "$i" ]; then case "$i" in - "$DEFAULT_POM") - source=pom - file="$i" - break - ;; - *) - source=file - file="$i" - break - ;; + "$DEFAULT_POM") source=pom; file="$i"; break;; + *) source=file; file="$i"; break;; esac fi done - elif [ "$source" == file ]; then + elif [ "$source" == file -o "$source" == pom ]; then [ "$action" == auto ] && action=update fi [ "$source" == file -a -z "$file" ] && file="$DEFAULT_FILE" [ "$source" == pom -a -z "$file" ] && file="$DEFAULT_POM" [ "$action" == auto ] && action=show + if [[ "$source" == auto* ]]; then + eerror "bug: impossible de déterminer la source" + return 1 + fi + + if [ "$action" == show-source ]; then + echo "$source" + return 0 + fi + # Lire la version + local -a maprules + if [ "$source" == file -o "$source" == pom ]; then + local mapdir="$(dirname -- "$file")" + if [ -z "$setmapfile" ]; then + local tmpfile + tmpfile="$mapdir/.pver-map" + [ -f "$tmpfile" ] && mapfile="$tmpfile" + fi + if [ -n "$mapfile" ]; then + __pver_unless -f "$mapfile" "$mapfile: fichier introuvable" || return + + mapdir="$(dirname -- "$mapfile")" + array_from_lines maprules "$(<"$mapfile" filter_conf)" + local maprule filespec filevpath filename + local -a filenames + for maprule in "${maprules[@]}"; do + splitpair "$maprule" filespec filevpath + [ "$filevpath" != - ] || continue + array_lsfiles filenames "$mapdir" "$filespec" + [ ${#filenames[*]} -gt 0 ] || continue + file="${filenames[0]}" + if [[ "$file" == *.xml ]]; then source=pom + else source=file + fi + edebug "Sélection de $file comme fichier de référence" + break + done + fi + fi + [ "$source" == pom ] && maven_update=1 + if [ "$source" == file ]; then [ -f "$file" ] && version="$(<"$file")" - elif [ "$source" == pom ]; then - [ -f "$file" ] || { - eerror "$file: fichier introuvable" - return 1 - } - version="$(__pom_get_version "$file")" + elif [ "$source" == file-string ]; then if [ -z "$file" ]; then file="$DEFAULT_FILE" @@ -239,14 +368,51 @@ function pver() { fi [ -f "$file" ] && version="$(<"$file")" file= - elif [ "$source" == git-string ]; then + + elif [ "$source" == git-file-string ]; then + git_check_gitvcs || return 2 splitfsep2 "$git" : branch name [ -n "$branch" ] || branch=master [ -n "$name" ] || name="$DEFAULT_FILE" if git rev-parse --verify --quiet "$branch:$name" >/dev/null; then version="$(git cat-file blob "$branch:$name" 2>/dev/null)" fi + + elif [ "$source" == pom ]; then + __pver_unless -f "$file" "$file: fichier introuvable" || return + [ -n "$vpath" ] || vpath="$(__pver_pom_get_vpath "$file")" + version="$(__pver_pom_get_version "$file" "$vpath")" + + elif [ "$source" == pom-string ]; then + if [ -z "$file" ]; then file="$DEFAULT_POM" + elif [ -d "$file" ]; then file="$file/$DEFAULT_POM" + fi + __pver_unless -f "$file" "$file: fichier introuvable" || return + + [ -n "$vpath" ] || vpath="$(__pver_pom_get_vpath "$file")" + version="$(__pver_pom_get_version "$file" "$vpath")" + file= + + elif [ "$source" == git-pom-string ]; then + git_check_gitvcs || return 2 + splitfsep2 "$git" : branch name + [ -n "$branch" ] || branch=master + [ -n "$name" ] || name="$DEFAULT_POM" + if git rev-parse --verify --quiet "$branch:$name" >/dev/null; then + local tmpfile; ac_set_tmpfile tmpfile + git cat-file blob "$branch:$name" >"$tmpfile" 2>/dev/null + [ -n "$vpath" ] || vpath="$(__pver_pom_get_vpath "$tmpfile")" + version="$(__pver_pom_get_version "$tmpfile" "$vpath")" + ac_clean "$tmpfile" + else + eerror "$name: fichier introuvable sur la branche $branch" + return 1 + fi + + elif [ "$source" == git-prel-string ]; then + version="$(__pver_get_prel_version)" || return fi + [ -n "$version" -o -n "$allow_empty" ] || version=0.0.0 # Conversion éventuelle du numéro de version @@ -285,7 +451,7 @@ BEGIN { version = version ".0" } # afficher la version migrée au format semver - if (metadata != "") print version "+" metadata + if (metadata != "") print version "+r" metadata else print version ### maven, pom.xml @@ -339,7 +505,7 @@ BEGIN { fi if [ "$action" == check ]; then - [ -n "$valid" ] || { eerror "Numéro de version invalide: $version"; return 1; } + __pver_unless -n "$valid" "Numéro de version invalide: $version" || return elif [ "$action" == compare ]; then psemver_parse "$oversion" o @@ -423,94 +589,100 @@ BEGIN { elif [ "$action" == update ]; then [ -z "$version" -a -n "$allow_empty" ] && return 1 - [ -n "$valid" ] || { eerror "Numéro de version invalide: $version"; return 1; } + __pver_unless -n "$valid" "Numéro de version invalide: $version" || return if [ -n "$file" ]; then if [ -f "$file" ]; then - if isatty; then - estepi "La version actuelle est $version" - fi + isatty && estepi "La version actuelle est $version" elif [ "$source" == pom ]; then eerror "$file: fichier introuvable" return 1 - else - if isatty; then - ask_yesno "Le fichier $(ppath "$file") n'existe pas. Faut-il le créer?" O || return 1 - fi + elif isatty; then + ask_yesno "Le fichier $(ppath "$file") n'existe pas. Faut-il le créer?" O || return 1 fi fi # forcer le numéro de version if [ -n "$setversion" ]; then if [ "$setversion" == prel ]; then - local branch; branch="$(git_get_branch)" || return 2 - if [[ "$branch" == release-* ]]; then - setversion="${branch#release-}" - else - eerror "$branch: n'est pas une release branch" - return 1 - fi + setversion="$(__pver_get_prel_version)" + __pver_perror $? \ + "$(git_get_branch): n'est pas une release branch" \ + "Dépôt git introuvable" || return fi - psemver_setversion "$setversion" "" || { eerror "Numéro de version invalide: $setversion"; return 1; } + psemver_setversion "$setversion" "" || { + eerror "Numéro de version invalide: $setversion" + return 1 + } fi # Calculer metadata if [ -n "$vcsmetadata" ]; then + resetmetadata=1 setmetadata="$(git rev-parse --short HEAD)" || return 1 fi # incrémenter les numéros de version - if [ "$incversion" == auto ]; then - if [ -n "$setrelease" -o -n "$setprelease" -o -n "$setmetadata" -o -n "$addmetadata" ]; then - incversion= - else - incversion=menu + if [ -n "$maven_update" -a -n "$incversion" ]; then + # il y a des règles particulières pour maven + if [ -n "$setrelease" -a -z "$prelease" ]; then + # on est déjà en release, faire le traitement normal + maven_update= + elif [ -n "$setsnapshot" -a "$prelease" == SNAPSHOT ]; then + # on est déjà en snapshot, faire le traitement normal, mais garder le préfixe SNAPSHOT + maven_update= + if [ "$incversion" == auto ]; then + [ -n "$setmd" ] && incversion= || incversion=menu + fi + elif [ -z "$prelease" ]; then + # on est en release, passer en snapshot + setsnapshot=1 + setrelease= + if [ "$incversion" == auto ]; then + [ -n "$setmd" ] && incversion= || incversion=menu + fi + elif [ "$prelease" == SNAPSHOT ]; then + # on est en snapshot, passer en release + setsnapshot= + setrelease=1 + if [ "$incversion" == auto ]; then + [ -n "$setmd" ] && incversion= || incversion=menu + fi fi fi + if [ "$incversion" == auto ]; then + [ -n "$setpr" -o -n "$setmd" ] && incversion= || incversion=menu + fi + if [ -z "$incversion" -a -n "$maven_update" ]; then + # on doit incrémenter avec les règles maven, mais aucune des options + # -[xzp] n'est spécifiée + if [ -n "$setrelease" ]; then + incversion=patchlevel + elif [ -n "$setsnapshot" ]; then + if [ "$patchlevel" -eq 0 ]; then + incversion=minor + else + incversion=patchlevel + fi + fi + fi + + if [ -n "$setrelease" ]; then setprelease= + elif [ -n "$setsnapshot" ]; then setprelease=SNAPSHOT + fi if [ "$incversion" == menu ]; then - psemver_copy x; { - psemver_incmajor x - psemver_setprelease "$setprelease" x - if [ -n "$addmetadata" ]; then - [ -n "$setmetadata" ] && psemver_setmetadata "$setmetadata" x - psemver_addmetadata "$addmetadata" x - else - psemver_setmetadata "$setmetadata" x - fi - psemver_setvar versionx x - } - psemver_copy z; { - psemver_incminor z - psemver_setprelease "$setprelease" z - if [ -n "$addmetadata" ]; then - [ -n "$setmetadata" ] && psemver_setmetadata "$setmetadata" z - psemver_addmetadata "$addmetadata" z - else - psemver_setmetadata "$setmetadata" z - fi - psemver_setvar versionz z - } - psemver_copy p; { - psemver_incpatchlevel p - psemver_setprelease "$setprelease" p - if [ -n "$addmetadata" ]; then - [ -n "$setmetadata" ] && psemver_setmetadata "$setmetadata" p - psemver_addmetadata "$addmetadata" p - else - psemver_setmetadata "$setmetadata" p - fi - psemver_setvar versionp p - } - psemver_copy k; { - psemver_setprelease "$setprelease" k - if [ -n "$addmetadata" ]; then - [ -n "$setmetadata" ] && psemver_setmetadata "$setmetadata" k - psemver_addmetadata "$addmetadata" k - else - psemver_setmetadata "$setmetadata" k - fi - psemver_setvar versionk k - } + psemver_copy x + psemver_incsetprmd major x "$setprelease" 1 "$setmetadata" "$addmetadata" "$maven_update" + psemver_setvar versionx x + psemver_copy z + psemver_incsetprmd minor z "$setprelease" 1 "$setmetadata" "$addmetadata" "$maven_update" + psemver_setvar versionz z + psemver_copy p + psemver_incsetprmd patchlevel p "$setprelease" 1 "$setmetadata" "$addmetadata" "$maven_update" + psemver_setvar versionp p + psemver_copy k + psemver_incsetprmd "" k "$setprelease" "$resetmetadata" "$setmetadata" "$addmetadata" "$maven_update" + psemver_setvar versionk k nextvs=( "$versionx : maj incompatibles de l'API (-x)" "$versionz : maj compatibles de l'API (-z)" @@ -528,49 +700,74 @@ BEGIN { *) incversion=;; esac fi + + local r if [ -n "$incversion" ]; then - case "$incversion" in - major) psemver_incmajor;; - minor) psemver_incminor;; - patchlevel) psemver_incpatchlevel;; - esac - # Quand on incrémente, réinitialiser la valeur de prérelease et metadata - psemver_setprelease - [ -z "$addmetadata" ] && psemver_setmetadata - fi - - # spécifier prerelease - if [ -n "$setrelease" ]; then - psemver_setprelease "" - elif [ -n "$setprelease" ]; then - psemver_setprelease "$setprelease" || { eerror "Identifiant de pre-release invalide: $setprelease"; return 1; } - fi - if [ -n "$setalpha" ]; then - : #XXX - elif [ -n "$setbeta" ]; then - : #XXX - elif [ -n "$setrc" ]; then - : #XXX - fi - - # spécifier metadata - if [ -n "$setmetadata" ]; then - psemver_setmetadata "$setmetadata" || { eerror "Identifiant de build invalide: $setmetadata"; return 1; } - fi - if [ -n "$addmetadata" ]; then - psemver_addmetadata "$addmetadata" || { eerror "Identifiant de build invalide: $addmetadata"; return 1; } + psemver_incsetprmd "$incversion" "" "$setprelease" 1 "$setmetadata" "$addmetadata" "$maven_update" + r=$? + else + psemver_setprmd "" "$setprelease" "$resetmetadata" "$setmetadata" "$addmetadata" + r=$? fi + case $r in + 2) eerror "Identifiant de pre-release invalide: $setprelease"; return 1;; + 3) eerror "Identifiant de build invalide: $setmetadata"; return 1;; + 4) eerror "Identifiant de build invalide: $addmetadata"; return 1;; + esac # afficher le résultat final + local -a depfiles psemver_setvar version if [ -n "$file" ]; then - case "$source" in - file) echo "$version" >"$file";; - pom) __pom_set_version "$file" "$version";; - esac + if [ "$source" == file ]; then + echo "$version" >"$file" + elif [ "$source" == pom ]; then + __pver_pom_set_version "$file" "$version" + [ $? -eq 3 ] && return 1 + fi + if [ ${#maprules[*]} -gt 0 ]; then + # mettre à jour les fichiers mentionnés dans mapfile + local -a donefiles + for maprule in "${maprules[@]}"; do + splitpair "$maprule" filespec filevpath + array_lsfiles filenames "$mapdir" "$filespec" + for file in "${filenames[@]}"; do + file="$(abspath "$file")" + edebug "Examen de $file" + # si c'est un wildcard, ne traiter que si le fichier n'a + # pas encore été traité + case "$filespec" in *\**|*\?*) array_contains donefiles "$file" && break;; esac + if [ "$filevpath" != - ]; then + if [[ "$file" == *.xml ]]; then + edebug "Maj version de $file au chemin XPATH ${filevpath:-par défaut}" + __pver_pom_set_version "$file" "$version" "$filevpath" + case $? in + 0|1) :;; + 2) array_addu depfiles "$file";; + *) return 1;; + esac + else + edebug "Maj version de $file" + echo "$version" >"$file" + fi + else + edebug "Fichier $file ignoré" + fi + array_addu donefiles "$file" + done + done + fi fi if isatty; then estepn "La nouvelle version est $version" + if [ ${#depfiles[*]} -gt 0 ]; then + local msg="Les fichiers suivants ont été modifiés, et leur version doit être mise à jour:" + for file in "${depfiles[@]}"; do + msg="$msg + $(qvals pver -uzw "$file" -R --menu)" + done + eimportant "$msg" + fi else echo "$version" fi diff --git a/lib/ulib/semver b/lib/ulib/semver index 94c63e0..eaa69aa 100644 --- a/lib/ulib/semver +++ b/lib/ulib/semver @@ -10,6 +10,7 @@ function __semver_check_prelease() { [ -z "${1//[a-zA-Z0-9.-]/}" ]; } function __semver_check_metadata() { [ -z "${1//[a-zA-Z0-9.-]/}" ]; } function semver_parse() { + # args: version majorV minorV patchlevelV preleaseV metadataV validV local __p_ver="$1" local __p_ma="${2:-major}" __p_mi="${3:-minor}" __p_pl="${4:-patchlevel}" local __p_pr="${5:-prelease}" __p_md="${6:-metadata}" __p_va="${7:-valid}" @@ -84,34 +85,79 @@ function semver_parse() { } function semver_incmajor() { - setv "$1" $((${!1} + 1)) - setv "$2" 0 - setv "$3" 0 - array_new "$4" + # args: majorV minorV patchlevelV preleaseV metadataV maven_update setprelease + if [ -z "$6" ]; then + setv "$1" $((${!1} + 1)) + setv "$2" 0 + setv "$3" 0 + array_new "$4" + elif [ -z "$7" ]; then + # maven snapshot --> release + if [ ${!2} -ne 0 -o ${!3} -ne 0 ]; then + setv "$1" $((${!1} + 1)) + setv "$2" 0 + setv "$3" 0 + fi + array_new "$4" + else + # maven release --> snapshot + setv "$2" $((${!2} + 1)) + setv "$3" 0 + array_split "$4" "$setprelease" . + fi } function semver_incminor() { - setv "$2" $((${!2} + 1)) - setv "$3" 0 - array_new "$4" + # args: majorV minorV patchlevelV preleaseV metadataV maven_update setprelease + if [ -z "$6" ]; then + setv "$2" $((${!2} + 1)) + setv "$3" 0 + array_new "$4" + elif [ -z "$7" ]; then + # maven snapshot --> release + if [ ${!3} -ne 0 ]; then + setv "$2" $((${!2} + 1)) + setv "$3" 0 + fi + array_new "$4" + else + # maven release --> snapshot + setv "$2" $((${!2} + 1)) + setv "$3" 0 + array_split "$4" "$setprelease" . + fi } function semver_incpatchlevel() { - setv "$3" $((${!3} + 1)) - array_new "$4" + # args: majorV minorV patchlevelV preleaseV metadataV maven_update setprelease + if [ -z "$6" ]; then + setv "$3" $((${!3} + 1)) + array_new "$4" + elif [ -z "$7" ]; then + # maven snapshot --> release + : + else + # maven release --> snapshot + setv "$3" $((${!3} + 1)) + array_split "$4" "$setprelease" . + fi } function semver_setversion() { + # args: version majorV minorV patchlevelV preleaseV metadataV local __sv_ma __sv_mi __sv_svl __sv_svr __sv_md __sv_va semver_parse "$1" __sv_ma __sv_mi __sv_pl __sv_pr __sv_md __sv_va [ -n "$__sv_va" ] || return 1 setv "$2" "$__sv_ma" setv "$3" "$__sv_mi" setv "$4" "$__sv_pl" + array_copy "$5" __sv_pr + array_copy "$6" __sv_md return 0 } function semver_setprelease() { + # args: setprelease majorV minorV patchlevelV preleaseV metadataV if [ -n "$1" ]; then __semver_check_prelease "$1" || return 1 array_split "$5" "$1" . @@ -122,6 +168,7 @@ function semver_setprelease() { } function semver_compare_prelease() { + # args: prelease1 prelease2 local -a __cp_pr1 __cp_pr2 __cp_len i __cp_v1 __cp_v2 array_copy __cp_pr1 "$1" array_copy __cp_pr2 "$2" @@ -179,6 +226,7 @@ function semver_compare_prelease() { } function semver_setmetadata() { + # args: setmetadata majorV minorV patchlevelV preleaseV metadataV if [ -n "$1" ]; then __semver_check_metadata "$1" || return 1 array_split "$6" "$1" . @@ -189,6 +237,7 @@ function semver_setmetadata() { } function semver_addmetadata() { + # args: addmetadata majorV minorV patchlevelV preleaseV metadataV if [ -n "$1" ]; then __semver_check_metadata "$1" || return 1 local -a __sam_metadata @@ -199,11 +248,13 @@ function semver_addmetadata() { } function semver_compare_metadata() { + # args: metadata1 metadata2 # même algo que pour prelease semver_compare_prelease "$@" } function semver_copy() { + # args: majorDV minorDV patchlevelDV preleaseDV metadataDV majorSV minorSV patchlevelSV preleaseSV metadataSV setv "$1" "${!6}" setv "$2" "${!7}" setv "$3" "${!8}" @@ -212,6 +263,7 @@ function semver_copy() { } function semver_build() { + # args: majorV minorV patchlevelV preleaseV metadataV echo_ "${!1}.${!2}.${!3}" array_isempty "$4" || recho_ "-$(array_join "$4" .)" array_isempty "$5" || recho_ "+$(array_join "$5" .)" @@ -219,6 +271,7 @@ function semver_build() { } function semver_setvar() { + # args: versionV majorV minorV patchlevelV preleaseV metadataV setv "$1" "$(semver_build "$2" "$3" "$4" "$5" "$6")" } @@ -228,9 +281,9 @@ function semver_setvar() { # uniquement un préfixe pour les noms de variable function psemver_parse() { semver_parse "$1" "${2}major" "${2}minor" "${2}patchlevel" "${2}prelease" "${2}metadata" "${2}valid"; } -function psemver_incmajor() { semver_incmajor "${1}major" "${1}minor" "${1}patchlevel" "${1}prelease" "${1}metadata"; } -function psemver_incminor() { semver_incminor "${1}major" "${1}minor" "${1}patchlevel" "${1}prelease" "${1}metadata"; } -function psemver_incpatchlevel() { semver_incpatchlevel "${1}major" "${1}minor" "${1}patchlevel" "${1}prelease" "${1}metadata"; } +function psemver_incmajor() { semver_incmajor "${1}major" "${1}minor" "${1}patchlevel" "${1}prelease" "${2}metadata" "$2" "$3"; } +function psemver_incminor() { semver_incminor "${1}major" "${1}minor" "${1}patchlevel" "${1}prelease" "${2}metadata" "$2" "$3"; } +function psemver_incpatchlevel() { semver_incpatchlevel "${1}major" "${1}minor" "${1}patchlevel" "${1}prelease" "${2}metadata" "$2" "$3"; } function psemver_setversion() { semver_setversion "$1" "${2}major" "${2}minor" "${2}patchlevel" "${2}prelease" "${2}metadata"; } function psemver_setprelease() { semver_setprelease "$1" "${2}major" "${2}minor" "${2}patchlevel" "${2}prelease" "${2}metadata"; } function psemver_compare_prelease() { semver_compare_prelease "${1}prelease" "${2}prelease"; } @@ -240,3 +293,32 @@ function psemver_compare_metadata() { semver_compare_metadata "${1}metadata" "${ function psemver_copy() { semver_copy "${1}major" "${1}minor" "${1}patchlevel" "${1}prelease" "${1}metadata" "${2}major" "${2}minor" "${2}patchlevel" "${2}prelease" "${2}metadata"; } function psemver_build() { semver_build "${1}major" "${1}minor" "${1}patchlevel" "${1}prelease" "${1}metadata"; } function psemver_setvar() { semver_setvar "$1" "${2}major" "${2}minor" "${2}patchlevel" "${2}prelease" "${2}metadata"; } + +function psemver_setprmd() { + local setprelease="$2" resetmetadata="$3" setmetadata="$4" addmetadata="$5" + + psemver_setprelease "$setprelease" "$1" || return 2 + # XXX pas encore implémenté + local setalpha setbeta setrc + if [ -n "$setalpha" ]; then : + elif [ -n "$setbeta" ]; then : + elif [ -n "$setrc" ]; then : + fi + + [ -n "$resetmetadata" ] && psemver_setmetadata "" "$1" + if [ -n "$setmetadata" ]; then + psemver_setmetadata "$setmetadata" "$1" || return 3 + fi + if [ -n "$addmetadata" ]; then + psemver_addmetadata "$addmetadata" "$1" || return 4 + fi + + return 0 +} + +function psemver_incsetprmd() { + local setprelease="$3" resetmetadata="$4" setmetadata="$5" addmetadata="$6" maven_update="$7" + [ -n "$1" ] && "psemver_inc$1" "$2" "$maven_update" "$setprelease" + shift + psemver_setprmd "$@" +} diff --git a/lib/ulib/support/python/__init__.py b/lib/ulib/support/python/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/pyulib/src/ulib/ext/xpath/_xpath.py b/lib/ulib/support/python/xpath/__init__.py similarity index 75% rename from lib/pyulib/src/ulib/ext/xpath/_xpath.py rename to lib/ulib/support/python/xpath/__init__.py index d1c48bc..0741f64 100644 --- a/lib/pyulib/src/ulib/ext/xpath/_xpath.py +++ b/lib/ulib/support/python/xpath/__init__.py @@ -1,8 +1,11 @@ -import expr as E -import parser as P -import yappsrt as Y +from xpath.exceptions import * +import xpath.exceptions +import xpath.expr +import xpath.parser +import xpath.yappsrt -from exceptions import * +__all__ = ['find', 'findnode', 'findvalue', 'XPathContext', 'XPath'] +__all__.extend((x for x in dir(xpath.exceptions) if not x.startswith('_'))) def api(f): """Decorator for functions and methods that are part of the external @@ -60,19 +63,19 @@ class XPathContext(object): @api def find(self, expr, node, **kwargs): - return XPath.get(expr).find(node, context=self, **kwargs) + return xpath.find(expr, node, context=self, **kwargs) @api def findnode(self, expr, node, **kwargs): - return XPath.get(expr).findnode(node, context=self, **kwargs) + return xpath.findnode(expr, node, context=self, **kwargs) @api def findvalue(self, expr, node, **kwargs): - return XPath.get(expr).findvalue(node, context=self, **kwargs) + return xpath.findvalue(expr, node, context=self, **kwargs) @api def findvalues(self, expr, node, **kwargs): - return XPath.get(expr).findvalues(node, context=self, **kwargs) + return xpath.findvalues(expr, node, context=self, **kwargs) class XPath(): _max_cache = 100 @@ -82,9 +85,9 @@ class XPath(): """Init docs. """ try: - parser = P.XPath(P.XPathScanner(str(expr))) + parser = xpath.parser.XPath(xpath.parser.XPathScanner(str(expr))) self.expr = parser.XPath() - except Y.SyntaxError, e: + except xpath.yappsrt.SyntaxError, e: raise XPathParseError(str(expr), e.pos, e.msg) @classmethod @@ -112,7 +115,7 @@ class XPath(): @api def findnode(self, node, context=None, **kwargs): result = self.find(node, context, **kwargs) - if not E.nodesetp(result): + if not xpath.expr.nodesetp(result): raise XPathTypeError("expression is not a node-set") if len(result) == 0: return None @@ -121,18 +124,18 @@ class XPath(): @api def findvalue(self, node, context=None, **kwargs): result = self.find(node, context, **kwargs) - if E.nodesetp(result): + if xpath.expr.nodesetp(result): if len(result) == 0: return None - result = E.string(result) + result = xpath.expr.string(result) return result @api def findvalues(self, node, context=None, **kwargs): result = self.find(node, context, **kwargs) - if not E.nodesetp(result): + if not xpath.expr.nodesetp(result): raise XPathTypeError("expression is not a node-set") - return [E.string_value(x) for x in result] + return [xpath.expr.string_value(x) for x in result] def __repr__(self): return '%s.%s(%s)' % (self.__class__.__module__, @@ -141,3 +144,19 @@ class XPath(): def __str__(self): return str(self.expr) + +@api +def find(expr, node, **kwargs): + return XPath.get(expr).find(node, **kwargs) + +@api +def findnode(expr, node, **kwargs): + return XPath.get(expr).findnode(node, **kwargs) + +@api +def findvalue(expr, node, **kwargs): + return XPath.get(expr).findvalue(node, **kwargs) + +@api +def findvalues(expr, node, **kwargs): + return XPath.get(expr).findvalues(node, **kwargs) diff --git a/lib/ulib/support/python/xpath/exceptions.py b/lib/ulib/support/python/xpath/exceptions.py new file mode 100644 index 0000000..7ce017a --- /dev/null +++ b/lib/ulib/support/python/xpath/exceptions.py @@ -0,0 +1,49 @@ + +class XPathError(Exception): + """Base exception class used for all XPath exceptions.""" + +class XPathNotImplementedError(XPathError): + """Raised when an XPath expression contains a feature of XPath which + has not been implemented. + + """ + +class XPathParseError(XPathError): + """Raised when an XPath expression could not be parsed.""" + + def __init__(self, expr, pos, message): + XPathError.__init__(self) + self.expr = expr + self.pos = pos + self.err = message + + def __str__(self): + return ("Syntax error:\n" + + self.expr.replace("\n", " ") + "\n" + + ("-" * self.pos) + "^") + +class XPathTypeError(XPathError): + """Raised when an XPath expression is found to contain a type error. + For example, the expression "string()/node()" contains a type error + because the "string()" function does not return a node-set. + + """ + +class XPathUnknownFunctionError(XPathError): + """Raised when an XPath expression contains a function that has no + binding in the expression context. + + """ + +class XPathUnknownPrefixError(XPathError): + """Raised when an XPath expression contains a QName with a namespace + prefix that has no corresponding namespace declaration in the expression + context. + + """ + +class XPathUnknownVariableError(XPathError): + """Raised when an XPath expression contains a variable that has no + binding in the expression context. + + """ diff --git a/lib/ulib/support/python/xpath/expr.py b/lib/ulib/support/python/xpath/expr.py new file mode 100644 index 0000000..4199813 --- /dev/null +++ b/lib/ulib/support/python/xpath/expr.py @@ -0,0 +1,903 @@ +from __future__ import division +from itertools import * +import math +import operator +import re +import xml.dom +import weakref + +from xpath.exceptions import * +import xpath + + +# +# Data model functions. +# + +def string_value(node): + """Compute the string-value of a node.""" + if (node.nodeType == node.DOCUMENT_NODE or + node.nodeType == node.ELEMENT_NODE): + s = u'' + for n in axes['descendant'](node): + if n.nodeType == n.TEXT_NODE: + s += n.data + elif n.nodeType == n.CDATA_SECTION_NODE: + s += n.nodeValue + return s + + elif node.nodeType == node.ATTRIBUTE_NODE: + return node.value + + elif (node.nodeType == node.PROCESSING_INSTRUCTION_NODE or + node.nodeType == node.COMMENT_NODE or + node.nodeType == node.TEXT_NODE): + return node.data + + elif node.nodeType == node.CDATA_SECTION_NODE: + return node.nodeValue + +def document_order(node): + """Compute a document order value for the node. + + cmp(document_order(a), document_order(b)) will return -1, 0, or 1 if + a is before, identical to, or after b in the document respectively. + + We represent document order as a list of sibling indexes. That is, + the third child of the document node has an order of [2]. The first + child of that node has an order of [2,0]. + + Attributes have a sibling index of -1 (coming before all children of + their node) and are further ordered by name--e.g., [2,0,-1,'href']. + + """ + + # Attributes: parent-order + [-1, attribute-name] + if node.nodeType == node.ATTRIBUTE_NODE: + order = document_order(node.ownerElement) + order.extend((-1, node.name)) + return order + + # The document root (hopefully): [] + if node.parentNode is None: + return [] + + # Determine which child this is of its parent. + sibpos = 0 + sib = node + while sib.previousSibling is not None: + sibpos += 1 + sib = sib.previousSibling + + # Order: parent-order + [sibling-position] + order = document_order(node.parentNode) + order.append(sibpos) + return order + +# +# Type functions, operating on the various XPath types. +# +# Internally, we use the following representations: +# nodeset - list of DOM tree nodes in document order +# string - str or unicode +# boolean - bool +# number - int or float +# + +def nodeset(v): + """Convert a value to a nodeset.""" + if not nodesetp(v): + raise XPathTypeError, "value is not a node-set" + return v + +def nodesetp(v): + """Return true iff 'v' is a node-set.""" + if isinstance(v, list): + return True + +def string(v): + """Convert a value to a string.""" + if nodesetp(v): + if not v: + return u'' + return string_value(v[0]) + elif numberp(v): + if v == float('inf'): + return u'Infinity' + elif v == float('-inf'): + return u'-Infinity' + elif str(v) == 'nan': + return u'NaN' + elif int(v) == v and v <= 0xffffffff: + v = int(v) + return unicode(v) + elif booleanp(v): + return u'true' if v else u'false' + return v + +def stringp(v): + """Return true iff 'v' is a string.""" + return isinstance(v, basestring) + +def boolean(v): + """Convert a value to a boolean.""" + if nodesetp(v): + return len(v) > 0 + elif numberp(v): + if v == 0 or v != v: + return False + return True + elif stringp(v): + return v != '' + return v + +def booleanp(v): + """Return true iff 'v' is a boolean.""" + return isinstance(v, bool) + +def number(v): + """Convert a value to a number.""" + if nodesetp(v): + v = string(v) + try: + return float(v) + except ValueError: + return float('NaN') + +def numberp(v): + """Return true iff 'v' is a number.""" + return (not(isinstance(v, bool)) and + (isinstance(v, int) or isinstance(v, float))) + +class Expr(object): + """Abstract base class for XPath expressions.""" + + def evaluate(self, node, pos, size, context): + """Evaluate the expression. + + The context node, context position, and context size are passed as + arguments. + + Returns an XPath value: a nodeset, string, boolean, or number. + + """ + +class BinaryOperatorExpr(Expr): + """Base class for all binary operators.""" + + def __init__(self, op, left, right): + self.op = op + self.left = left + self.right = right + + def evaluate(self, node, pos, size, context): + # Subclasses either override evaluate() or implement operate(). + return self.operate(self.left.evaluate(node, pos, size, context), + self.right.evaluate(node, pos, size, context)) + + def __str__(self): + return '(%s %s %s)' % (self.left, self.op, self.right) + +class AndExpr(BinaryOperatorExpr): + """ and """ + + def evaluate(self, node, pos, size, context): + # Note that XPath boolean operations short-circuit. + return (boolean(self.left.evaluate(node, pos, size, context) and + boolean(self.right.evaluate(node, pos, size, context)))) + +class OrExpr(BinaryOperatorExpr): + """ or """ + + def evaluate(self, node, pos, size, context): + # Note that XPath boolean operations short-circuit. + return (boolean(self.left.evaluate(node, pos, size, context) or + boolean(self.right.evaluate(node, pos, size, context)))) + +class EqualityExpr(BinaryOperatorExpr): + """ = , != , etc.""" + + operators = { + '=' : operator.eq, + '!=' : operator.ne, + '<=' : operator.le, + '<' : operator.lt, + '>=' : operator.ge, + '>' : operator.gt, + } + + def operate(self, a, b): + if nodesetp(a): + for node in a: + if self.operate(string_value(node), b): + return True + return False + + if nodesetp(b): + for node in b: + if self.operate(a, string_value(node)): + return True + return False + + if self.op in ('=', '!='): + if booleanp(a) or booleanp(b): + convert = boolean + elif numberp(a) or numberp(b): + convert = number + else: + convert = string + else: + convert = number + + a, b = convert(a), convert(b) + return self.operators[self.op](a, b) + +def divop(x, y): + try: + return x / y + except ZeroDivisionError: + if x == 0 and y == 0: + return float('nan') + if x < 0: + return float('-inf') + return float('inf') + +class ArithmeticalExpr(BinaryOperatorExpr): + """ + , - , etc.""" + + # Note that we must use math.fmod for the correct modulo semantics. + operators = { + '+' : operator.add, + '-' : operator.sub, + '*' : operator.mul, + 'div' : divop, + 'mod' : math.fmod + } + + def operate(self, a, b): + return self.operators[self.op](number(a), number(b)) + +class UnionExpr(BinaryOperatorExpr): + """ | """ + + def operate(self, a, b): + if not nodesetp(a) or not nodesetp(b): + raise XPathTypeError("union operand is not a node-set") + + # Need to sort the result to preserve document order. + return sorted(set(chain(a, b)), key=document_order) + +class NegationExpr(Expr): + """- """ + + def __init__(self, expr): + self.expr = expr + + def evaluate(self, node, pos, size, context): + return -number(self.expr.evaluate(node, pos, size, context)) + + def __str__(self): + return '(-%s)' % self.expr + +class LiteralExpr(Expr): + """Literals--either numbers or strings.""" + + def __init__(self, literal): + self.literal = literal + + def evaluate(self, node, pos, size, context): + return self.literal + + def __str__(self): + if stringp(self.literal): + if "'" in self.literal: + return '"%s"' % self.literal + else: + return "'%s'" % self.literal + return string(self.literal) + +class VariableReference(Expr): + """Variable references.""" + + def __init__(self, prefix, name): + self.prefix = prefix + self.name = name + + def evaluate(self, node, pos, size, context): + try: + if self.prefix is not None: + try: + namespaceURI = context.namespaces[self.prefix] + except KeyError: + raise XPathUnknownPrefixError(self.prefix) + return context.variables[(namespaceURI, self.name)] + else: + return context.variables[self.name] + except KeyError: + raise XPathUnknownVariableError(str(self)) + + def __str__(self): + if self.prefix is None: + return '$%s' % self.name + else: + return '$%s:%s' % (self.prefix, self.name) + +class Function(Expr): + """Functions.""" + + def __init__(self, name, args): + self.name = name + self.args = args + self.evaluate = getattr(self, 'f_%s' % name.replace('-', '_'), None) + if self.evaluate is None: + raise XPathUnknownFunctionError, 'unknown function "%s()"' % name + + if len(self.args) < self.evaluate.minargs: + raise XPathTypeError, 'too few arguments for "%s()"' % name + if (self.evaluate.maxargs is not None and + len(self.args) > self.evaluate.maxargs): + raise XPathTypeError, 'too many arguments for "%s()"' % name + + # + # XPath functions are implemented by methods of the Function class. + # + # A method implementing an XPath function is decorated with the function + # decorator, and receives the evaluated function arguments as positional + # parameters. + # + + def function(minargs, maxargs, implicit=False, first=False, convert=None): + """Function decorator. + + minargs -- Minimum number of arguments taken by the function. + maxargs -- Maximum number of arguments taken by the function. + implicit -- True for functions which operate on a nodeset consisting + of the current context node when passed no argument. + (e.g., string() and number().) + convert -- When non-None, a function used to filter function arguments. + """ + def decorator(f): + def new_f(self, node, pos, size, context): + if implicit and len(self.args) == 0: + args = [[node]] + else: + args = [x.evaluate(node, pos, size, context) + for x in self.args] + if first: + args[0] = nodeset(args[0]) + if len(args[0]) > 0: + args[0] = args[0][0] + else: + args[0] = None + if convert is not None: + args = [convert(x) for x in args] + return f(self, node, pos, size, context, *args) + + new_f.minargs = minargs + new_f.maxargs = maxargs + new_f.__name__ = f.__name__ + new_f.__doc__ = f.__doc__ + return new_f + return decorator + + # Node Set Functions + + @function(0, 0) + def f_last(self, node, pos, size, context): + return size + + @function(0, 0) + def f_position(self, node, pos, size, context): + return pos + + @function(1, 1, convert=nodeset) + def f_count(self, node, pos, size, context, nodes): + return len(nodes) + + @function(1, 1) + def f_id(self, node, pos, size, context, arg): + if nodesetp(arg): + ids = (string_value(x) for x in arg) + else: + ids = [string(arg)] + if node.nodeType != node.DOCUMENT_NODE: + node = node.ownerDocument + return list(filter(None, (node.getElementById(id) for id in ids))) + + @function(0, 1, implicit=True, first=True) + def f_local_name(self, node, pos, size, context, argnode): + if argnode is None: + return '' + if (argnode.nodeType == argnode.ELEMENT_NODE or + argnode.nodeType == argnode.ATTRIBUTE_NODE): + return argnode.localName + elif argnode.nodeType == argnode.PROCESSING_INSTRUCTION_NODE: + return argnode.target + return '' + + @function(0, 1, implicit=True, first=True) + def f_namespace_uri(self, node, pos, size, context, argnode): + if argnode is None: + return '' + return argnode.namespaceURI + + @function(0, 1, implicit=True, first=True) + def f_name(self, node, pos, size, context, argnode): + if argnode is None: + return '' + if argnode.nodeType == argnode.ELEMENT_NODE: + return argnode.tagName + elif argnode.nodeType == argnode.ATTRIBUTE_NODE: + return argnode.name + elif argnode.nodeType == argnode.PROCESSING_INSTRUCTION_NODE: + return argnode.target + return '' + + # String Functions + + @function(0, 1, implicit=True, convert=string) + def f_string(self, node, pos, size, context, arg): + return arg + + @function(2, None, convert=string) + def f_concat(self, node, pos, size, context, *args): + return ''.join((x for x in args)) + + @function(2, 2, convert=string) + def f_starts_with(self, node, pos, size, context, a, b): + return a.startswith(b) + + @function(2, 2, convert=string) + def f_contains(self, node, pos, size, context, a, b): + return b in a + + @function(2, 2, convert=string) + def f_substring_before(self, node, pos, size, context, a, b): + try: + return a[0:a.index(b)] + except ValueError: + return '' + + @function(2, 2, convert=string) + def f_substring_after(self, node, pos, size, context, a, b): + try: + return a[a.index(b)+len(b):] + except ValueError: + return '' + + @function(2, 3) + def f_substring(self, node, pos, size, context, s, start, count=None): + s = string(s) + start = round(number(start)) + if start != start: + # Catch NaN + return '' + + if count is None: + end = len(s) + 1 + else: + end = start + round(number(count)) + if end != end: + # Catch NaN + return '' + if end > len(s): + end = len(s)+1 + + if start < 1: + start = 1 + if start > len(s): + return '' + if end <= start: + return '' + return s[int(start)-1:int(end)-1] + + @function(0, 1, implicit=True, convert=string) + def f_string_length(self, node, pos, size, context, s): + return len(s) + + @function(0, 1, implicit=True, convert=string) + def f_normalize_space(self, node, pos, size, context, s): + return re.sub(r'\s+', ' ', s.strip()) + + @function(3, 3, convert=lambda x: unicode(string(x))) + def f_translate(self, node, pos, size, context, s, source, target): + # str.translate() and unicode.translate() are completely different. + # The translate() arguments are coerced to unicode. + table = {} + for schar, tchar in izip(source, target): + schar = ord(schar) + if schar not in table: + table[schar] = tchar + if len(source) > len(target): + for schar in source[len(target):]: + schar = ord(schar) + if schar not in table: + table[schar] = None + return s.translate(table) + + # Boolean functions + + @function(1, 1, convert=boolean) + def f_boolean(self, node, pos, size, context, b): + return b + + @function(1, 1, convert=boolean) + def f_not(self, node, pos, size, context, b): + return not b + + @function(0, 0) + def f_true(self, node, pos, size, context): + return True + + @function(0, 0) + def f_false(self, node, pos, size, context): + return False + + @function(1, 1, convert=string) + def f_lang(self, node, pos, size, context, s): + s = s.lower() + for n in axes['ancestor-or-self'](node): + if n.nodeType == n.ELEMENT_NODE and n.hasAttribute('xml:lang'): + lang = n.getAttribute('xml:lang').lower() + if s == lang or lang.startswith(s + u'-'): + return True + break + return False + + # Number functions + + @function(0, 1, implicit=True, convert=number) + def f_number(self, node, pos, size, context, n): + return n + + @function(1, 1, convert=nodeset) + def f_sum(self, node, pos, size, context, nodes): + return sum((number(string_value(x)) for x in nodes)) + + @function(1, 1, convert=number) + def f_floor(self, node, pos, size, context, n): + return math.floor(n) + + @function(1, 1, convert=number) + def f_ceiling(self, node, pos, size, context, n): + return math.ceil(n) + + @function(1, 1, convert=number) + def f_round(self, node, pos, size, context, n): + # XXX round(-0.0) should be -0.0, not 0.0. + # XXX round(-1.5) should be -1.0, not -2.0. + return round(n) + + def __str__(self): + return '%s(%s)' % (self.name, ', '.join((str(x) for x in self.args))) + +# +# XPath axes. +# + +# Dictionary of all axis functions. +axes = {} + +def axisfn(reverse=False, principal_node_type=xml.dom.Node.ELEMENT_NODE): + """Axis function decorator. + + An axis function will take a node as an argument and return a sequence + over the nodes along an XPath axis. Axis functions have two extra + attributes indicating the axis direction and principal node type. + """ + def decorate(f): + f.__name__ = f.__name__.replace('_', '-') + f.reverse = reverse + f.principal_node_type = principal_node_type + return f + return decorate + +def make_axes(): + """Define functions to walk each of the possible XPath axes.""" + + @axisfn() + def child(node): + return node.childNodes + + @axisfn() + def descendant(node): + for child in node.childNodes: + for node in descendant_or_self(child): + yield node + + @axisfn() + def parent(node): + if node.parentNode is not None: + yield node.parentNode + + @axisfn(reverse=True) + def ancestor(node): + while node.parentNode is not None: + node = node.parentNode + yield node + + @axisfn() + def following_sibling(node): + while node.nextSibling is not None: + node = node.nextSibling + yield node + + @axisfn(reverse=True) + def preceding_sibling(node): + while node.previousSibling is not None: + node = node.previousSibling + yield node + + @axisfn() + def following(node): + while node is not None: + while node.nextSibling is not None: + node = node.nextSibling + for n in descendant_or_self(node): + yield n + node = node.parentNode + + @axisfn(reverse=True) + def preceding(node): + while node is not None: + while node.previousSibling is not None: + node = node.previousSibling + # Could be more efficient here. + for n in reversed(list(descendant_or_self(node))): + yield n + node = node.parentNode + + @axisfn(principal_node_type=xml.dom.Node.ATTRIBUTE_NODE) + def attribute(node): + if node.attributes is not None: + return (node.attributes.item(i) + for i in xrange(node.attributes.length)) + return () + + @axisfn() + def namespace(node): + raise XPathNotImplementedError("namespace axis is not implemented") + + @axisfn() + def self(node): + yield node + + @axisfn() + def descendant_or_self(node): + yield node + for child in node.childNodes: + for node in descendant_or_self(child): + yield node + + @axisfn(reverse=True) + def ancestor_or_self(node): + return chain([node], ancestor(node)) + + # Place each axis function defined here into the 'axes' dict. + for axis in locals().values(): + axes[axis.__name__] = axis + +make_axes() + +def merge_into_nodeset(target, source): + """Place all the nodes from the source node-set into the target + node-set, preserving document order. Both node-sets must be in + document order to begin with. + + """ + if len(target) == 0: + target.extend(source) + return + + source = [n for n in source if n not in target] + if len(source) == 0: + return + + # If the last node in the target set comes before the first node in the + # source set, then we can just concatenate the sets. Otherwise, we + # will need to sort. (We could also check to see if the last node in + # the source set comes before the first node in the target set, but this + # situation is very unlikely in practice.) + if document_order(target[-1]) < document_order(source[0]): + target.extend(source) + else: + target.extend(source) + target.sort(key=document_order) + +class AbsolutePathExpr(Expr): + """Absolute location paths.""" + + def __init__(self, path): + self.path = path + + def evaluate(self, node, pos, size, context): + if node.nodeType != node.DOCUMENT_NODE: + node = node.ownerDocument + if self.path is None: + return [node] + return self.path.evaluate(node, 1, 1, context) + + def __str__(self): + return '/%s' % (self.path or '') + +class PathExpr(Expr): + """Location path expressions.""" + + def __init__(self, steps): + self.steps = steps + + def evaluate(self, node, pos, size, context): + # The first step in the path is evaluated in the current context. + # If this is the only step in the path, the return value is + # unimportant. If there are other steps, however, it must be a + # node-set. + result = self.steps[0].evaluate(node, pos, size, context) + if len(self.steps) > 1 and not nodesetp(result): + raise XPathTypeError("path step is not a node-set") + + # Subsequent steps are evaluated for each node in the node-set + # resulting from the previous step. + for step in self.steps[1:]: + aggregate = [] + for i in xrange(len(result)): + nodes = step.evaluate(result[i], i+1, len(result), context) + if not nodesetp(nodes): + raise XPathTypeError("path step is not a node-set") + merge_into_nodeset(aggregate, nodes) + result = aggregate + + return result + + def __str__(self): + return '/'.join((str(s) for s in self.steps)) + +class PredicateList(Expr): + """A list of predicates. + + Predicates are handled as an expression wrapping the expression + filtered by the predicates. + + """ + def __init__(self, expr, predicates, axis='child'): + self.predicates = predicates + self.expr = expr + self.axis = axes[axis] + + def evaluate(self, node, pos, size, context): + result = self.expr.evaluate(node, pos, size, context) + if not nodesetp(result): + raise XPathTypeError("predicate input is not a node-set") + + if self.axis.reverse: + result.reverse() + + for pred in self.predicates: + match = [] + for i, node in izip(count(1), result): + r = pred.evaluate(node, i, len(result), context) + + # If a predicate evaluates to a number, select the node + # with that position. Otherwise, select nodes for which + # the boolean value of the predicate is true. + if numberp(r): + if r == i: + match.append(node) + elif boolean(r): + match.append(node) + result = match + + if self.axis.reverse: + result.reverse() + + return result + + def __str__(self): + s = str(self.expr) + if '/' in s: + s = '(%s)' % s + return s + ''.join(('[%s]' % x for x in self.predicates)) + +class AxisStep(Expr): + """One step in a location path expression.""" + + def __init__(self, axis, test=None, predicates=None): + if test is None: + test = AnyKindTest() + self.axis = axes[axis] + self.test = test + + def evaluate(self, node, pos, size, context): + match = [] + for n in self.axis(node): + if self.test.match(n, self.axis, context): + match.append(n) + + if self.axis.reverse: + match.reverse() + + return match + + def __str__(self): + return '%s::%s' % (self.axis.__name__, self.test) + +# +# Node tests. +# + +class Test(object): + """Abstract base class for node tests.""" + + def match(self, node, axis, context): + """Return True if 'node' matches the test along 'axis'.""" + +class NameTest(object): + def __init__(self, prefix, localpart): + self.prefix = prefix + self.localName = localpart + if self.prefix == None and self.localName == '*': + self.prefix = '*' + + def match(self, node, axis, context): + if node.nodeType != axis.principal_node_type: + return False + + if self.prefix != '*': + namespaceURI = None + if self.prefix is not None: + try: + namespaceURI = context.namespaces[self.prefix] + except KeyError: + raise XPathUnknownPrefixError(self.prefix) + elif axis.principal_node_type == node.ELEMENT_NODE: + namespaceURI = context.default_namespace + if namespaceURI != node.namespaceURI: + return False + if self.localName != '*': + if self.localName != node.localName: + return False + return True + + def __str__(self): + if self.prefix is not None: + return '%s:%s' % (self.prefix, self.localName) + else: + return self.localName + +class PITest(object): + def __init__(self, name=None): + self.name = name + + def match(self, node, axis, context): + return (node.nodeType == node.PROCESSING_INSTRUCTION_NODE and + (self.name is None or node.target == self.name)) + + def __str__(self): + if self.name is None: + name = '' + elif "'" in self.name: + name = '"%s"' % self.name + else: + name = "'%s'" % self.name + return 'processing-instruction(%s)' % name + +class CommentTest(object): + def match(self, node, axis, context): + return node.nodeType == node.COMMENT_NODE + + def __str__(self): + return 'comment()' + +class TextTest(object): + def match(self, node, axis, context): + return (node.nodeType == node.TEXT_NODE or + node.nodeType == node.CDATA_SECTION_NODE) + + def __str__(self): + return 'text()' + +class AnyKindTest(object): + def match(self, node, axis, context): + return True + + def __str__(self): + return 'node()' diff --git a/lib/ulib/support/python/xpath/parser.py b/lib/ulib/support/python/xpath/parser.py new file mode 100644 index 0000000..504de78 --- /dev/null +++ b/lib/ulib/support/python/xpath/parser.py @@ -0,0 +1,416 @@ +import xpath.expr as X +from xpath.yappsrt import * + + +from string import * +import re + +class XPathScanner(Scanner): + patterns = [ + ("r'\\:'", re.compile('\\:')), + ("r'node\\s*\\('", re.compile('node\\s*\\(')), + ("r'text\\s*\\('", re.compile('text\\s*\\(')), + ("r'comment\\s*\\('", re.compile('comment\\s*\\(')), + ("r'processing-instruction\\s*\\('", re.compile('processing-instruction\\s*\\(')), + ("r'\\,'", re.compile('\\,')), + ("r'\\.'", re.compile('\\.')), + ("r'\\$'", re.compile('\\$')), + ("r'\\)'", re.compile('\\)')), + ("r'\\('", re.compile('\\(')), + ("r'\\]'", re.compile('\\]')), + ("r'\\['", re.compile('\\[')), + ("r'\\*'", re.compile('\\*')), + ("r':'", re.compile(':')), + ("r'\\.\\.'", re.compile('\\.\\.')), + ("r'@'", re.compile('@')), + ("r'::'", re.compile('::')), + ("r'\\/\\/'", re.compile('\\/\\/')), + ("r'\\/'", re.compile('\\/')), + ("r'\\-'", re.compile('\\-')), + ("'\\|'", re.compile('\\|')), + ("r'and'", re.compile('and')), + ("r'or'", re.compile('or')), + ('\\s+', re.compile('\\s+')), + ('END', re.compile('$')), + ('FORWARD_AXIS_NAME', re.compile('child|descendant-or-self|attribute|self|descendant|following-sibling|following|namespace')), + ('REVERSE_AXIS_NAME', re.compile('parent|preceding-sibling|preceding|ancestor-or-self|ancestor')), + ('NCNAME', re.compile('[a-zA-Z_][a-zA-Z0-9_\\-\\.\\w]*(?!\\()')), + ('FUNCNAME', re.compile('[a-zA-Z_][a-zA-Z0-9_\\-\\.\\w]*')), + ('DQUOTE', re.compile('\\"(?:[^\\"])*\\"')), + ('SQUOTE', re.compile("\\'(?:[^\\'])*\\'")), + ('NUMBER', re.compile('((\\.[0-9]+)|([0-9]+(\\.[0-9]*)?))([eE][\\+\\-]?[0-9]+)?')), + ('EQ_COMP', re.compile('\\!?\\=')), + ('REL_COMP', re.compile('[\\<\\>]\\=?')), + ('ADD_COMP', re.compile('[\\+\\-]')), + ('MUL_COMP', re.compile('\\*|div|mod')), + ] + def __init__(self, str): + Scanner.__init__(self,None,['\\s+'],str) + +class XPath(Parser): + def XPath(self): + Expr = self.Expr() + END = self._scan('END') + return Expr + + def Expr(self): + OrExpr = self.OrExpr() + return OrExpr + + def OrExpr(self): + AndExpr = self.AndExpr() + Expr = AndExpr + while self._peek("r'or'", 'END', "r'\\]'", "r'\\)'", "r'\\,'") == "r'or'": + self._scan("r'or'") + AndExpr = self.AndExpr() + Expr = X.OrExpr('or', Expr, AndExpr) + return Expr + + def AndExpr(self): + EqualityExpr = self.EqualityExpr() + Expr = EqualityExpr + while self._peek("r'and'", "r'or'", 'END', "r'\\]'", "r'\\)'", "r'\\,'") == "r'and'": + self._scan("r'and'") + EqualityExpr = self.EqualityExpr() + Expr = X.AndExpr('and', Expr, EqualityExpr) + return Expr + + def EqualityExpr(self): + RelationalExpr = self.RelationalExpr() + Expr = RelationalExpr + while self._peek('EQ_COMP', "r'and'", "r'or'", 'END', "r'\\]'", "r'\\)'", "r'\\,'") == 'EQ_COMP': + EQ_COMP = self._scan('EQ_COMP') + RelationalExpr = self.RelationalExpr() + Expr = X.EqualityExpr(EQ_COMP, Expr, RelationalExpr) + return Expr + + def RelationalExpr(self): + AdditiveExpr = self.AdditiveExpr() + Expr = AdditiveExpr + while self._peek('REL_COMP', 'EQ_COMP', "r'and'", "r'or'", 'END', "r'\\]'", "r'\\)'", "r'\\,'") == 'REL_COMP': + REL_COMP = self._scan('REL_COMP') + AdditiveExpr = self.AdditiveExpr() + Expr = X.EqualityExpr(REL_COMP, Expr, AdditiveExpr) + return Expr + + def AdditiveExpr(self): + MultiplicativeExpr = self.MultiplicativeExpr() + Expr = MultiplicativeExpr + while self._peek('ADD_COMP', 'REL_COMP', 'EQ_COMP', "r'and'", "r'or'", 'END', "r'\\]'", "r'\\)'", "r'\\,'") == 'ADD_COMP': + ADD_COMP = self._scan('ADD_COMP') + MultiplicativeExpr = self.MultiplicativeExpr() + Expr = X.ArithmeticalExpr(ADD_COMP, Expr, MultiplicativeExpr) + return Expr + + def MultiplicativeExpr(self): + UnionExpr = self.UnionExpr() + Expr = UnionExpr + while self._peek('MUL_COMP', 'ADD_COMP', 'REL_COMP', 'EQ_COMP', "r'and'", "r'or'", 'END', "r'\\]'", "r'\\)'", "r'\\,'") == 'MUL_COMP': + MUL_COMP = self._scan('MUL_COMP') + UnionExpr = self.UnionExpr() + Expr = X.ArithmeticalExpr(MUL_COMP, Expr, UnionExpr) + return Expr + + def UnionExpr(self): + UnaryExpr = self.UnaryExpr() + Expr = UnaryExpr + while self._peek("'\\|'", 'MUL_COMP', 'ADD_COMP', 'REL_COMP', 'EQ_COMP', "r'and'", "r'or'", 'END', "r'\\]'", "r'\\)'", "r'\\,'") == "'\\|'": + self._scan("'\\|'") + UnaryExpr = self.UnaryExpr() + Expr = X.UnionExpr('|', Expr, UnaryExpr) + return Expr + + def UnaryExpr(self): + _token_ = self._peek("r'\\-'", "r'\\/'", "r'\\/\\/'", "r'\\('", 'FORWARD_AXIS_NAME', "r'@'", 'REVERSE_AXIS_NAME', "r'\\.\\.'", "r'\\$'", "r'\\.'", 'FUNCNAME', 'NUMBER', 'DQUOTE', 'SQUOTE', "r'processing-instruction\\s*\\('", "r'comment\\s*\\('", "r'text\\s*\\('", "r'node\\s*\\('", "r'\\*'", 'NCNAME') + if _token_ == "r'\\-'": + self._scan("r'\\-'") + ValueExpr = self.ValueExpr() + return X.NegationExpr(ValueExpr) + else: + ValueExpr = self.ValueExpr() + return ValueExpr + + def ValueExpr(self): + PathExpr = self.PathExpr() + return PathExpr + + def PathExpr(self): + _token_ = self._peek("r'\\/'", "r'\\/\\/'", "r'\\('", 'FORWARD_AXIS_NAME', "r'@'", 'REVERSE_AXIS_NAME', "r'\\.\\.'", "r'\\$'", "r'\\.'", 'FUNCNAME', 'NUMBER', 'DQUOTE', 'SQUOTE', "r'processing-instruction\\s*\\('", "r'comment\\s*\\('", "r'text\\s*\\('", "r'node\\s*\\('", "r'\\*'", 'NCNAME') + if _token_ == "r'\\/'": + self._scan("r'\\/'") + path = None + if self._peek("r'\\('", 'FORWARD_AXIS_NAME', "r'@'", 'REVERSE_AXIS_NAME', "r'\\.\\.'", "r'\\$'", "r'\\.'", 'FUNCNAME', 'NUMBER', 'DQUOTE', 'SQUOTE', "r'processing-instruction\\s*\\('", "r'comment\\s*\\('", "r'text\\s*\\('", "r'node\\s*\\('", "r'\\*'", 'NCNAME', "'\\|'", 'MUL_COMP', 'ADD_COMP', 'REL_COMP', 'EQ_COMP', "r'and'", "r'or'", 'END', "r'\\]'", "r'\\)'", "r'\\,'") not in ["'\\|'", 'MUL_COMP', 'ADD_COMP', 'REL_COMP', 'EQ_COMP', "r'and'", "r'or'", 'END', "r'\\]'", "r'\\)'", "r'\\,'"]: + RelativePathExpr = self.RelativePathExpr() + path = RelativePathExpr + return X.AbsolutePathExpr(path) + elif _token_ == "r'\\/\\/'": + self._scan("r'\\/\\/'") + RelativePathExpr = self.RelativePathExpr() + step = X.AxisStep('descendant-or-self') + RelativePathExpr.steps.insert(0, step) + return X.AbsolutePathExpr(RelativePathExpr) + else: + RelativePathExpr = self.RelativePathExpr() + return RelativePathExpr + + def RelativePathExpr(self): + StepExpr = self.StepExpr() + steps = [StepExpr] + while self._peek("r'\\/'", "r'\\/\\/'", "'\\|'", 'MUL_COMP', 'ADD_COMP', 'REL_COMP', 'EQ_COMP', "r'and'", "r'or'", 'END', "r'\\]'", "r'\\)'", "r'\\,'") in ["r'\\/'", "r'\\/\\/'"]: + _token_ = self._peek("r'\\/'", "r'\\/\\/'") + if _token_ == "r'\\/'": + self._scan("r'\\/'") + else:# == "r'\\/\\/'" + self._scan("r'\\/\\/'") + steps.append(X.AxisStep('descendant-or-self')) + StepExpr = self.StepExpr() + steps.append(StepExpr) + return X.PathExpr(steps) + + def StepExpr(self): + _token_ = self._peek("r'\\('", 'FORWARD_AXIS_NAME', "r'@'", 'REVERSE_AXIS_NAME', "r'\\.\\.'", "r'\\$'", "r'\\.'", 'FUNCNAME', 'NUMBER', 'DQUOTE', 'SQUOTE', "r'processing-instruction\\s*\\('", "r'comment\\s*\\('", "r'text\\s*\\('", "r'node\\s*\\('", "r'\\*'", 'NCNAME') + if _token_ not in ["r'\\('", "r'\\$'", "r'\\.'", 'FUNCNAME', 'NUMBER', 'DQUOTE', 'SQUOTE']: + AxisStep = self.AxisStep() + return AxisStep + else: + FilterExpr = self.FilterExpr() + return FilterExpr + + def AxisStep(self): + _token_ = self._peek('FORWARD_AXIS_NAME', "r'@'", 'REVERSE_AXIS_NAME', "r'\\.\\.'", "r'processing-instruction\\s*\\('", "r'comment\\s*\\('", "r'text\\s*\\('", "r'node\\s*\\('", "r'\\*'", 'NCNAME') + if _token_ not in ['REVERSE_AXIS_NAME', "r'\\.\\.'"]: + ForwardStep = self.ForwardStep() + step = ForwardStep + else:# in ['REVERSE_AXIS_NAME', "r'\\.\\.'"] + ReverseStep = self.ReverseStep() + step = ReverseStep + expr = X.AxisStep(*step) + if self._peek("r'\\['", "r'\\/'", "r'\\/\\/'", "'\\|'", 'MUL_COMP', 'ADD_COMP', 'REL_COMP', 'EQ_COMP', "r'and'", "r'or'", 'END', "r'\\]'", "r'\\)'", "r'\\,'") == "r'\\['": + PredicateList = self.PredicateList() + expr = X.PredicateList(expr, PredicateList, step[0]) + return expr + + def ForwardStep(self): + _token_ = self._peek('FORWARD_AXIS_NAME', "r'@'", "r'processing-instruction\\s*\\('", "r'comment\\s*\\('", "r'text\\s*\\('", "r'node\\s*\\('", "r'\\*'", 'NCNAME') + if _token_ == 'FORWARD_AXIS_NAME': + ForwardAxis = self.ForwardAxis() + NodeTest = self.NodeTest() + return [ForwardAxis, NodeTest] + else: + AbbrevForwardStep = self.AbbrevForwardStep() + return AbbrevForwardStep + + def ForwardAxis(self): + FORWARD_AXIS_NAME = self._scan('FORWARD_AXIS_NAME') + self._scan("r'::'") + return FORWARD_AXIS_NAME + + def AbbrevForwardStep(self): + axis = 'child' + if self._peek("r'@'", "r'processing-instruction\\s*\\('", "r'comment\\s*\\('", "r'text\\s*\\('", "r'node\\s*\\('", "r'\\*'", 'NCNAME') == "r'@'": + self._scan("r'@'") + axis = 'attribute' + NodeTest = self.NodeTest() + return [axis, NodeTest] + + def ReverseStep(self): + _token_ = self._peek('REVERSE_AXIS_NAME', "r'\\.\\.'") + if _token_ == 'REVERSE_AXIS_NAME': + ReverseAxis = self.ReverseAxis() + NodeTest = self.NodeTest() + return [ReverseAxis, NodeTest] + else:# == "r'\\.\\.'" + AbbrevReverseStep = self.AbbrevReverseStep() + return AbbrevReverseStep + + def ReverseAxis(self): + REVERSE_AXIS_NAME = self._scan('REVERSE_AXIS_NAME') + self._scan("r'::'") + return REVERSE_AXIS_NAME + + def AbbrevReverseStep(self): + self._scan("r'\\.\\.'") + return ['parent', None] + + def NodeTest(self): + _token_ = self._peek("r'processing-instruction\\s*\\('", "r'comment\\s*\\('", "r'text\\s*\\('", "r'node\\s*\\('", "r'\\*'", 'NCNAME') + if _token_ not in ["r'\\*'", 'NCNAME']: + KindTest = self.KindTest() + return KindTest + else:# in ["r'\\*'", 'NCNAME'] + NameTest = self.NameTest() + return NameTest + + def NameTest(self): + prefix = None + WildcardOrNCName = self.WildcardOrNCName() + localpart = WildcardOrNCName + if self._peek("r':'", "r'\\['", "r'\\/'", "r'\\/\\/'", "'\\|'", 'MUL_COMP', 'ADD_COMP', 'REL_COMP', 'EQ_COMP', "r'and'", "r'or'", 'END', "r'\\]'", "r'\\)'", "r'\\,'") == "r':'": + self._scan("r':'") + WildcardOrNCName = self.WildcardOrNCName() + prefix = localpart + localpart = WildcardOrNCName + return X.NameTest(prefix, localpart) + + def WildcardOrNCName(self): + _token_ = self._peek("r'\\*'", 'NCNAME') + if _token_ == "r'\\*'": + self._scan("r'\\*'") + return '*' + else:# == 'NCNAME' + NCNAME = self._scan('NCNAME') + return NCNAME + + def FilterExpr(self): + PrimaryExpr = self.PrimaryExpr() + if self._peek("r'\\['", "r'\\/'", "r'\\/\\/'", "'\\|'", 'MUL_COMP', 'ADD_COMP', 'REL_COMP', 'EQ_COMP', "r'and'", "r'or'", 'END', "r'\\]'", "r'\\)'", "r'\\,'") == "r'\\['": + PredicateList = self.PredicateList() + PrimaryExpr = X.PredicateList(PrimaryExpr,PredicateList) + return PrimaryExpr + + def PredicateList(self): + Predicate = self.Predicate() + predicates = [Predicate] + while self._peek("r'\\['", "r'\\/'", "r'\\/\\/'", "'\\|'", 'MUL_COMP', 'ADD_COMP', 'REL_COMP', 'EQ_COMP', "r'and'", "r'or'", 'END', "r'\\]'", "r'\\)'", "r'\\,'") == "r'\\['": + Predicate = self.Predicate() + predicates.append(Predicate) + return predicates + + def Predicate(self): + self._scan("r'\\['") + Expr = self.Expr() + self._scan("r'\\]'") + return Expr + + def PrimaryExpr(self): + _token_ = self._peek("r'\\('", "r'\\$'", "r'\\.'", 'FUNCNAME', 'NUMBER', 'DQUOTE', 'SQUOTE') + if _token_ not in ["r'\\('", "r'\\$'", "r'\\.'", 'FUNCNAME']: + Literal = self.Literal() + return X.LiteralExpr(Literal) + elif _token_ == "r'\\$'": + VariableReference = self.VariableReference() + return VariableReference + elif _token_ == "r'\\('": + self._scan("r'\\('") + Expr = self.Expr() + self._scan("r'\\)'") + return Expr + elif _token_ == "r'\\.'": + ContextItemExpr = self.ContextItemExpr() + return ContextItemExpr + else:# == 'FUNCNAME' + FunctionCall = self.FunctionCall() + return FunctionCall + + def VariableReference(self): + self._scan("r'\\$'") + QName = self.QName() + return X.VariableReference(*QName) + + def ContextItemExpr(self): + self._scan("r'\\.'") + return X.AxisStep('self') + + def FunctionCall(self): + FUNCNAME = self._scan('FUNCNAME') + self._scan("r'\\('") + args = [] + if self._peek("r'\\,'", "r'\\)'", "r'\\-'", "r'\\/'", "r'\\/\\/'", "r'\\('", 'FORWARD_AXIS_NAME', "r'@'", 'REVERSE_AXIS_NAME', "r'\\.\\.'", "r'\\$'", "r'\\.'", 'FUNCNAME', 'NUMBER', 'DQUOTE', 'SQUOTE', "r'processing-instruction\\s*\\('", "r'comment\\s*\\('", "r'text\\s*\\('", "r'node\\s*\\('", "r'\\*'", 'NCNAME') not in ["r'\\,'", "r'\\)'"]: + Expr = self.Expr() + args.append(Expr) + while self._peek("r'\\,'", "r'\\)'") == "r'\\,'": + self._scan("r'\\,'") + Expr = self.Expr() + args.append(Expr) + self._scan("r'\\)'") + return X.Function(FUNCNAME, args) + + def KindTest(self): + _token_ = self._peek("r'processing-instruction\\s*\\('", "r'comment\\s*\\('", "r'text\\s*\\('", "r'node\\s*\\('") + if _token_ == "r'processing-instruction\\s*\\('": + PITest = self.PITest() + return PITest + elif _token_ == "r'comment\\s*\\('": + CommentTest = self.CommentTest() + return CommentTest + elif _token_ == "r'text\\s*\\('": + TextTest = self.TextTest() + return TextTest + else:# == "r'node\\s*\\('" + AnyKindTest = self.AnyKindTest() + return AnyKindTest + + def PITest(self): + self._scan("r'processing-instruction\\s*\\('") + name = None + if self._peek('NCNAME', "r'\\)'", 'DQUOTE', 'SQUOTE') != "r'\\)'": + _token_ = self._peek('NCNAME', 'DQUOTE', 'SQUOTE') + if _token_ == 'NCNAME': + NCNAME = self._scan('NCNAME') + name = NCNAME + else:# in ['DQUOTE', 'SQUOTE'] + StringLiteral = self.StringLiteral() + name = StringLiteral + self._scan("r'\\)'") + return X.PITest(name) + + def CommentTest(self): + self._scan("r'comment\\s*\\('") + self._scan("r'\\)'") + return X.CommentTest() + + def TextTest(self): + self._scan("r'text\\s*\\('") + self._scan("r'\\)'") + return X.TextTest() + + def AnyKindTest(self): + self._scan("r'node\\s*\\('") + self._scan("r'\\)'") + return X.AnyKindTest() + + def Literal(self): + _token_ = self._peek('NUMBER', 'DQUOTE', 'SQUOTE') + if _token_ == 'NUMBER': + NumericLiteral = self.NumericLiteral() + return NumericLiteral + else:# in ['DQUOTE', 'SQUOTE'] + StringLiteral = self.StringLiteral() + return StringLiteral + + def NumericLiteral(self): + NUMBER = self._scan('NUMBER') + return float(NUMBER) + + def StringLiteral(self): + _token_ = self._peek('DQUOTE', 'SQUOTE') + if _token_ == 'DQUOTE': + DQUOTE = self._scan('DQUOTE') + return DQUOTE[1:-1] + else:# == 'SQUOTE' + SQUOTE = self._scan('SQUOTE') + return SQUOTE[1:-1] + + def QName(self): + NCNAME = self._scan('NCNAME') + name = NCNAME + if self._peek("r'\\:'", "r'\\['", "r'\\/'", "r'\\/\\/'", "'\\|'", 'MUL_COMP', 'ADD_COMP', 'REL_COMP', 'EQ_COMP', "r'and'", "r'or'", 'END', "r'\\]'", "r'\\)'", "r'\\,'") == "r'\\:'": + self._scan("r'\\:'") + NCNAME = self._scan('NCNAME') + return (name, NCNAME) + return (None, name) + + +def parse(rule, text): + P = XPath(XPathScanner(text)) + return wrap_error_reporter(P, rule) + +if __name__ == '__main__': + from sys import argv, stdin + if len(argv) >= 2: + if len(argv) >= 3: + f = open(argv[2],'r') + else: + f = stdin + print parse(argv[1], f.read()) + else: print 'Args: []' diff --git a/lib/ulib/support/python/xpath/yappsrt.py b/lib/ulib/support/python/xpath/yappsrt.py new file mode 100644 index 0000000..c8d8933 --- /dev/null +++ b/lib/ulib/support/python/xpath/yappsrt.py @@ -0,0 +1,174 @@ +# Yapps 2.0 Runtime +# +# This module is needed to run generated parsers. + +from string import join, count, find, rfind +import re + +class SyntaxError(Exception): + """When we run into an unexpected token, this is the exception to use""" + def __init__(self, pos=-1, msg="Bad Token"): + Exception.__init__(self) + self.pos = pos + self.msg = msg + def __repr__(self): + if self.pos < 0: return "#" + else: return "SyntaxError[@ char %s: %s]" % (repr(self.pos), self.msg) + +class NoMoreTokens(Exception): + """Another exception object, for when we run out of tokens""" + pass + +class Scanner: + def __init__(self, patterns, ignore, input): + """Patterns is [(terminal,regex)...] + Ignore is [terminal,...]; + Input is a string""" + self.tokens = [] + self.restrictions = [] + self.input = input + self.pos = 0 + self.ignore = ignore + # The stored patterns are a pair (compiled regex,source + # regex). If the patterns variable passed in to the + # constructor is None, we assume that the class already has a + # proper .patterns list constructed + if patterns is not None: + self.patterns = [] + for k, r in patterns: + self.patterns.append( (k, re.compile(r)) ) + + def token(self, i, restrict=0): + """Get the i'th token, and if i is one past the end, then scan + for another token; restrict is a list of tokens that + are allowed, or 0 for any token.""" + if i == len(self.tokens): self.scan(restrict) + if i < len(self.tokens): + # Make sure the restriction is more restricted + if restrict and self.restrictions[i]: + for r in restrict: + if r not in self.restrictions[i]: + raise NotImplementedError("Unimplemented: restriction set changed") + return self.tokens[i] + raise NoMoreTokens() + + def __repr__(self): + """Print the last 10 tokens that have been scanned in""" + output = '' + for t in self.tokens[-10:]: + output = '%s\n (@%s) %s = %s' % (output,t[0],t[2],repr(t[3])) + return output + + def scan(self, restrict): + """Should scan another token and add it to the list, self.tokens, + and add the restriction to self.restrictions""" + # Keep looking for a token, ignoring any in self.ignore + while 1: + # Search the patterns for the longest match, with earlier + # tokens in the list having preference + best_match = -1 + best_pat = '(error)' + for p, regexp in self.patterns: + # First check to see if we're ignoring this token + if restrict and p not in restrict and p not in self.ignore: + continue + m = regexp.match(self.input, self.pos) + if m and len(m.group(0)) > best_match: + # We got a match that's better than the previous one + best_pat = p + best_match = len(m.group(0)) + + # If we didn't find anything, raise an error + if best_pat == '(error)' and best_match < 0: + msg = "Bad Token" + if restrict: + msg = "Trying to find one of "+join(restrict,", ") + raise SyntaxError(self.pos, msg) + + # If we found something that isn't to be ignored, return it + if best_pat not in self.ignore: + # Create a token with this data + token = (self.pos, self.pos+best_match, best_pat, + self.input[self.pos:self.pos+best_match]) + self.pos = self.pos + best_match + # Only add this token if it's not in the list + # (to prevent looping) + if not self.tokens or token != self.tokens[-1]: + self.tokens.append(token) + self.restrictions.append(restrict) + return + else: + # This token should be ignored .. + self.pos = self.pos + best_match + +class Parser: + def __init__(self, scanner): + self._scanner = scanner + self._pos = 0 + + def _peek(self, *types): + """Returns the token type for lookahead; if there are any args + then the list of args is the set of token types to allow""" + tok = self._scanner.token(self._pos, types) + return tok[2] + + def _scan(self, type): + """Returns the matched text, and moves to the next token""" + tok = self._scanner.token(self._pos, [type]) + if tok[2] != type: + raise SyntaxError(tok[0], 'Trying to find '+type) + self._pos = 1+self._pos + return tok[3] + + + +def print_error(input, err, scanner): + """This is a really dumb long function to print error messages nicely.""" + p = err.pos + # Figure out the line number + line = count(input[:p], '\n') + print err.msg+" on line "+repr(line+1)+":" + # Now try printing part of the line + text = input[max(p-80, 0):p+80] + p = p - max(p-80, 0) + + # Strip to the left + i = rfind(text[:p], '\n') + j = rfind(text[:p], '\r') + if i < 0 or (0 <= j < i): i = j + if 0 <= i < p: + p = p - i - 1 + text = text[i+1:] + + # Strip to the right + i = find(text,'\n', p) + j = find(text,'\r', p) + if i < 0 or (0 <= j < i): i = j + if i >= 0: + text = text[:i] + + # Now shorten the text + while len(text) > 70 and p > 60: + # Cut off 10 chars + text = "..." + text[10:] + p = p - 7 + + # Now print the string, along with an indicator + print '> ',text + print '> ',' '*p + '^' + print 'List of nearby tokens:', scanner + +def wrap_error_reporter(parser, rule): + return_value = None + try: + return_value = getattr(parser, rule)() + except SyntaxError, s: + input = parser._scanner.input + try: + print_error(input, s, parser._scanner) + except ImportError: + print 'Syntax Error',s.msg,'on line',1+count(input[:s.pos], '\n') + except NoMoreTokens: + print 'Could not complete parsing; stopped around here:' + print parser._scanner + return return_value diff --git a/lib/ulib/support/xpathtool.py b/lib/ulib/support/xpathtool.py new file mode 100755 index 0000000..ce2039e --- /dev/null +++ b/lib/ulib/support/xpathtool.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python +# -*- coding: utf-8 mode: python -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +u"""Ce script permet d'obtenir ou de modifier un élément identifié par une expression XPATH +""" + +import os, sys, re, types, tempfile, codecs, shutil +from os import path +sys.path.insert(0, path.join(path.dirname(__file__), 'python')) +from xml.dom import Node, minidom +import xpath + +def get_text(node, as_xml=False): + if isinstance(node, Node): + if node.nodeType == Node.ELEMENT_NODE: + if as_xml: return node.toxml("utf-8") + else: return xpath.expr.string_value(node).encode("utf-8") + elif node.nodeType == Node.ATTRIBUTE_NODE: + return node.value.encode("utf-8") + elif node.nodeType == Node.TEXT_NODE or node.nodeType == Node.CDATA_SECTION_NODE: + return node.data.encode("utf-8") + elif type(node) is types.UnicodeType: + return node.encode("utf-8") + else: + return str(node) + +def set_text(node, value, doc): + if not isinstance(node, Node): + raise ValueError("L'expression ne désigne pas un noeud XML") + if node.nodeType == Node.ELEMENT_NODE: + firstChild = node.firstChild + if value is not None: + textNode = doc.createTextNode(value) + if firstChild is None: + node.appendChild(textNode) + elif firstChild.nodeType == Node.TEXT_NODE or firstChild.nodeType == Node.CDATA_SECTION_NODE: + node.replaceChild(textNode, firstChild) + else: + node.insertBefore(textNode, firstChild) + elif firstChild is not None: + if firstChild.nodeType == Node.TEXT_NODE or firstChild.nodeType == Node.CDATA_SECTION_NODE: + node.removeChild(firstChild) + elif node.nodeType == Node.ATTRIBUTE_NODE: + if value is not None: node.value = value + else: pass # impossible d'accéder au parent d'un attribut + elif node.nodeType == Node.TEXT_NODE or node.nodeType == Node.CDATA_SECTION_NODE: + if value is not None: node.data = value + else: node.parentNode.removeChild(node) + else: + raise ValueError("Type de noeud non supporté: %s" % node.nodeType) + +RE_PARENT0 = re.compile(r'(^|/)parent\[') +RE_PARENT1 = re.compile(r'(^|/)parent(?=/|$)') +def py_dom_xpath_compat(expr): + expr = RE_PARENT0.sub(r"\1*[local-name()='parent' and ", expr) + expr = RE_PARENT1.sub(r"\1*[local-name()='parent']", expr) + return expr + +def run_xpathtool(): + from optparse import OptionParser + OP = OptionParser(usage=u"\n\t%prog -g XPATH [INPUT [OUTPUT]]\n\t%prog -s XPATH VALUE [INPUT [OUTPUT]]", description=__doc__) + OP.add_option('-f', '--input', dest='inf', + help=u"Spécifier le fichier en entrée") + OP.add_option('-o', '--output', dest='outf', + help=u"Spécifier le fichier en sortie") + OP.add_option('-g', '--get', dest='mode', action='store_const', const='get', + help=u"Forcer l'affichage de la valeur. " + + u"Par défaut, ce mode est sélectionné s'il n'y a aucun argument après XPATH") + OP.add_option('-t', '--exist', dest='mode', action='store_const', const='exist', + help=u"Tester l'existence du chemin spécifié.") + OP.add_option('-s', '--set', dest='mode', action='store_const', const='set', + help=u"Forcer la modification de la valeur. " + + u"Par défaut, ce mode est sélectionné s'il y a un argument VALUE après XPATH") + OP.add_option('-x', '--as-xml', dest='as_xml', action='store_true', + help=u"Retourner le résultat de l'expression en XML") + OP.add_option('--no-compat', dest='compat', action='store_false', default=True, + help=u"Ne pas transfomer certaines expression en un équivalent compatible avec py-dom-xpath. " + + u"Par exemple, par défaut \"/parent\" est transformé en \"/*[local-name='parent']\". " + + u"Cette option désactive ce comportement.") + o, args = OP.parse_args() + inf = o.inf + outf = o.outf + mode = o.mode + as_xml = o.as_xml + compat = o.compat + + count = len(args) + if count == 0: raise ValueError("Vous devez spécifier l'expression XPATH") + expr = args[0] + if compat: expr = py_dom_xpath_compat(expr) + value = None + + args = args[1:] + count = len(args) + if mode is None: + if count == 0: # xpathtool.py XPATH + mode = 'get' + elif inf is None: + if count == 1: # xpathtool.py XPATH INPUT + mode = 'get' + inf = args[0] + elif count >= 2: # xpathtool.py XPATH VALUE INPUT [OUTPUT] + mode = 'set' + value = args[0] + inf = args[1] + if count > 2: outf = args[2] + elif inf is not None: # xpathtool.py XPATH VALUE -f INPUT + mode = 'set' + value = args[0] + elif mode == 'get': + if inf is None: # xpathtool.py -g XPATH [INPUT] + if count > 0: inf = args[0] + elif mode == 'set': + if count > 0: value = args[0] + if inf is None: # xpathtool.py -s XPATH VALUE [INPUT [OUTPUT]] + if count > 1: inf = args[1] + if count > 2: outf = args[2] + + if inf == '-': inf = None + if outf == '-': outf = sys.stdout + if inf is None: + inf = sys.stdin + if outf is None: outf = sys.stdout + elif outf is None: + outf = inf + + doc = minidom.parse(inf) + if mode == 'get' or mode == 'exist': + #print "search %r from %r" % (expr, inf) #DEBUG + nodes = xpath.find(expr, doc) + if mode == 'get': + for node in nodes: + print get_text(node, as_xml) + if nodes: r = 0 + else: r = 1 + sys.exit(r) + elif mode == 'set': + if value is not None and not type(value) is types.UnicodeType: + value = unicode(value, "utf-8") + #print "search %r in %r, replace with %r then write in %r" % (expr, inf, value, outf) #DEBUG + for node in xpath.find(expr, doc): + set_text(node, value, doc) + #print "writing to %r" % outf #DEBUG + if type(outf) in types.StringTypes: + fd, tmpf = tempfile.mkstemp() + try: + os.close(fd) + out = codecs.open(tmpf, "w", "utf-8") + doc.writexml(out, encoding="utf-8") + out.close() + shutil.copyfile(tmpf, outf) + finally: + os.remove(tmpf) + else: + doc.writexml(outf, encoding="utf-8") + +if __name__ == '__main__': + run_xpathtool() diff --git a/lib/ulib/vcs b/lib/ulib/vcs index e9923dd..7fe12be 100644 --- a/lib/ulib/vcs +++ b/lib/ulib/vcs @@ -610,7 +610,7 @@ __VCS_GIT_ADVANCED=( git_is_merged ) function git_check_gitvcs() { - [ "$(_vcs_get_type "$(_vcs_get_dir)")" == git ] + git rev-parse --show-toplevel >&/dev/null } function git_ensure_gitvcs() { git_check_gitvcs || die "Ce n'est pas un dépôt git" diff --git a/lib/ulib/xmlsupport b/lib/ulib/xmlsupport new file mode 100644 index 0000000..1235be7 --- /dev/null +++ b/lib/ulib/xmlsupport @@ -0,0 +1,8 @@ +##@cooked comments # -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +## Fonction de support pour gérer des fichiers XML +##@cooked nocomments +##@require ulib +uprovide xmlsupport +urequire ulib + +function xpathtool() { "$ULIBDIR/support/xpathtool.py" "$@"; } diff --git a/prel b/prel index ac9a7ff..c5f616a 100755 --- a/prel +++ b/prel @@ -94,7 +94,7 @@ OPTIONS -m, --merge Si la branche actuelle est une branche de release, ou s'il existe une branche de release, la merger dans master, puis dans develop, puis la - supprimer. Puis basculer sur la branche master. + supprimer. A l'issu de cette opération, rester sur la branche develop. S'il n'existe pas de branche de release, proposer de fusionner les modifications de la branche develop dans la branche master, sans préparer de branche de release au préalable. @@ -114,13 +114,18 @@ OPTIONS release par rapport à develop. -d, --diff Afficher les modifications actuellement effectuée dans la branche de - release par rapport à develop, sous forme de diff." + release par rapport à develop, sous forme de diff. + +OPTIONS AVANCEES + --uc, --upgrade-changes + Convertir un fichier CHANGES.txt en CHANGES.md" } function show_summary() { git log --oneline --graph "$@" | grep -vF '|\' | grep -vF '|/' | sed 's/\* //; s/^ /+ /' | - grep -v "Intégration de la branche release-" + grep -v "Intégration de la branche release-" | + grep -v "Branche develop en version .*-SNAPSHOT" } function format_md() { @@ -143,6 +148,21 @@ $1 == "|" { ' } +function __get_newver_from_release() { + local relver filever + is_release_branch "$release" && relver="${release#release-}" + setx filever=pver --sw "" --allow-empty + if [ -n "$relver" -a "$filever" != "$relver" ]; then + newver="$filever" + ewarn "La version de la branche de release est différente de la version dans le fichier" + enote "La version effectivement sélectionnée est $newver" + elif [ -n "$filever" ]; then + newver="$filever" + elif [ -n "$relver" ]; then + newver="$relver" + fi +} + projdir= origin=origin action=auto @@ -208,6 +228,9 @@ fi git_ensure_gitvcs +setx vertype=pver --sw "" --show-source +[[ "$vertype" == pom* ]] && maven_update=1 || maven_update= + push_branches=() push_tags=() push_deferred= @@ -216,14 +239,20 @@ push_deferred= setx branch=git_get_branch +update_opt= if [ "$action" == update ]; then - setx oldver=pver -g "" + if [ -n "$maven_update" ]; then + setx oldver=pver --gw develop: + enote "La version de la branche develop est $oldver" + else + setx oldver=pver --gw master: + fi newver= if [ "$incversion" == auto ]; then if [ ${#pver_opts[*]} -gt 0 ]; then # des options ont été spécifiées, les honorer - setx newver=pver -s "$oldver" "${pver_opts[@]}" + setx newver=pver -s "$oldver" ${maven_update:+--maven-update -R} "${pver_opts[@]}" release="release-$newver" else # sinon, prendre une décision en fonction des branches de release @@ -246,6 +275,7 @@ if [ "$action" == update ]; then fi fi fi + pver_opts=(${maven_update:+--maven-update -R} "${pver_opts[@]}") case "$incversion" in menu) setx majorv=pver -s "$oldver" -ux "${pver_opts[@]}" @@ -257,18 +287,28 @@ if [ "$action" == update ]; then -t "Basculer vers une nouvelle branche de release" \ -m "Veuillez choisir la branche à créer" [ "$release" != master ] && newver="${release#release-}" + if [ "$release" == "release-$majorv" ]; then + update_opt=-x + elif [ "$release" == "release-$minorv" ]; then + update_opt=-z + elif [ "$release" == "release-$patchlevelv" ]; then + update_opt=-p + fi ;; major) setx newver=pver -s "$oldver" -ux "${pver_opts[@]}" release="release-$newver" + update_opt=-x ;; minor) setx newver=pver -s "$oldver" -uz "${pver_opts[@]}" release="release-$newver" + update_opt=-z ;; patchlevel) setx newver=pver -s "$oldver" -up "${pver_opts[@]}" release="release-$newver" + update_opt=-p ;; esac @@ -331,7 +371,7 @@ Vous allez créer la nouvelle branche de release ${COULEUR_VERTE}$release${COULE if [ -z "$newver" ]; then # le cas échéant, tenter de calculer la version en fonction de la release - is_release_branch "$release" && newver="${release#release-}" + __get_newver_from_release fi if [ "$r" -eq 0 -a -n "$write" ]; then @@ -445,7 +485,7 @@ if [ "$action" == merge ]; then if [ -z "$newver" ]; then # le cas échéant, tenter de calculer la version en fonction de la release - is_release_branch "$release" && newver="${release#release-}" + __get_newver_from_release fi if [ -n "$newver" ]; then @@ -469,6 +509,13 @@ ou celle-ci pour pour pousser TOUS les tags: estepn "Intégration ${COULEUR_VERTE}$release${COULEUR_NORMALE} --> ${COULEUR_BLEUE}develop${COULEUR_NORMALE}" git checkout develop git merge "$release" -m "Intégration de la branche $release" --no-ff || die + + if [ -n "$maven_update" ]; then + [ -n "$update_opt" ] || update_opt=-z + pver -u $update_opt -S + git add -A + git commit -m "Branche develop en version $(pver --show)" || die + fi fi # mettre à jour la branche sur laquelle on se trouve diff --git a/pver b/pver index cb10543..33fc3db 100755 --- a/pver +++ b/pver @@ -1,7 +1,7 @@ #!/bin/bash # -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 source "$(dirname "$0")/lib/ulib/ulib" || exit 1 -urequire DEFAULTS ptools +urequire DEFAULTS ptools xmlsupport function pver_display_help() { uecho "$scriptname: gérer des numéros de version selon les règles du versionage sémantique v2.0.0 (http://semver.org/) @@ -10,38 +10,55 @@ USAGE $scriptname [options] OPTIONS + -w, --auto-file DIR + Gérer le numéro de version du répertoire spécifié. Si un fichier pom.xml + existe dans ce répertoire, alors c'est l'option '-e DIR/pom.xml' qui est + activé. Si un fichier VERSION.txt existe dans ce répertoire, alors c'est + l'option '-f DIR/VERSION.txt' qui est activée. Sinon, un nouveau fichier + VERSION.txt est créé dans ce répertoire. C'est l'option par défaut. + --sw, --auto-string DIR + Prendre pour valeur de départ la version du répertoire spécifié. Si un + fichier pom.xml existe dans ce répertoire, alors c'est l'option -E qui + est activé. Si un fichier VERSION.txt existe dans ce répertoire, alors + c'est l'option -F qui est activée. + --gw, --auto-git-string [BRANCH:]DIR + Prendre pour valeur de départ la version du répertoire spécifié dans la + branche BRANCH (qui vaut par défaut master) du dépôt git. Si un fichier + pom.xml existe dans ce répertoire, alors c'est l'option -g qui est + activé. Si un fichier VERSION.txt existe dans ce répertoire, alors c'est + l'option -G qui est activée. + -e, --pom POMFILE + Gérer le numéro de version se trouvant dans le fichier pom.xml spécifié. + Le fichier DOIT exister. Implique --maven-update + -E, --pom-string POMFILE + Prendre pour valeur de départ la version contenue dans le fichier + pom.xml spécifié. Le fichier DOIT exister. Implique --maven-update -f, --file VERSIONFILE Gérer le numéro de version se trouvant dans le fichier spécifié. Le - fichier est créé si nécessaire. C'est l'option par défaut si un fichier - nommé VERSION.txt se trouve dans le répertoire courant. - -e, --maven POMFILE - Gérer le numéro de version se trouvant dans le fichier pom.xml spécifié. - Le fichier DOIT exister. C'est l'option par défaut si un fichier nommé - pom.xml se trouve dans le répertoire courant. + fichier est créé si nécessaire. -F, --file-string VERSIONFILE - Prendre pour valeur de départ le contenu du fichier VERSIONFILE (qui - vaut par défaut VERSION.txt) - -g, --git-string [branch:]VERSIONFILE + Prendre pour valeur de départ le contenu du fichier VERSIONFILE. + -g, --git-file-string [BRANCH:]VERSIONFILE Prendre pour valeur de départ le contenu du fichier VERSIONFILE (qui vaut par défaut VERSION.txt) dans la branche BRANCH (qui vaut par défaut - master) du dépôt git situé dans le répertoire courant. + master) du dépôt git. Retourner 2 si on n'est pas situé dans un dépôt + git. + -G, --git-pom-string [BRANCH:]POMFILE + Prendre pour valeur de départ la version du fichier POMFILE (qui vaut + par défaut pom.xml) dans la branche BRANCH (qui vaut par défaut master) + du dépôt git. Retourner 2 si on n'est pas situé dans un dépôt git. + --git-prel-string + Prendre pour valeur de départ le numéro de version correspondant à la + branche de release courante. Retourner 1 si la branche courante n'est + pas une branche de release, 2 si on n'est pas situé dans un dépôt git. -s, --string VERSION Prendre pour valeur de départ le numéro de version spécifié --show Afficher le numéro de version. C'est l'action par défaut - --allow-empty - Supporter que la version puisse ne pas être spécifiée ni trouvée. Dans - ce cas, ne pas assumer que la version effective est 0.0.0 - Avec --show et --update, ne rien afficher si la version est vide. --check Vérifier que le numéro de version est conforme aux règles du versionage sémantique - --convert - --no-convert - Activer (resp. désactiver) la conversion automatique. Par défaut, si la - version est au format classique 'x.z[.p]-rDD/MM/YYYY', elle est - convertie automatiquement au format sémantique x.z.p+rYYYYMMDD --eq VERSION --ne VERSION --lt VERSION @@ -54,6 +71,10 @@ OPTIONS --le, --gt, et --ge ignorent l'identifiant de build (comme le demande la règle du versionage sémantique). Les opérateurs --same et --diff comparent aussi les identifiants de build. + -u, --update + Mettre à jour le numéro de version. + +Les options suivantes impliquent --update: -v, --set-version VERSION Spécifier un nouveau numéro de version qui écrase la valeur actuelle. Cette option ne devrait pas être utilisée en temps normal parce que cela @@ -62,18 +83,20 @@ OPTIONS Spécifier un nouveau numéro de version qui écrase la valeur actuelle. Le numéro de version est obtenu à partir du nom de la branche git courante, qui doit être de la forme release-VERSION - -u, --update - Mettre à jour le numéro de version. - --menu Afficher un menu permettant de choisir le composant de la version à - incrémenter + incrémenter. -x, --major - Augmenter le numéro de version majeure + Augmenter le numéro de version majeure. -z, --minor Augmenter le numéro de version mineure. C'est la valeur par défaut. -p, --patchlevel - Augmenter le numéro de patch + Augmenter le numéro de patch. + -k, --keep + Ne pas augmenter le numéro de version. Cette option est surtout utile + pour *convertir* un numéro de version existant et mettre à jour le + fichier correspondant. Elle est assumée si aucune option -[xzp] n'est + spécifiée et qu'une des options -[labrSRmM] est utilisée. -l, --prelease ID Spécifier un identifiant de pré-release, à ajouter au numéro de version. @@ -85,17 +108,95 @@ OPTIONS de l'identifiant, e.g. alpha deviant alpha.1, beta-1.2 devient beta-1.3, rc1 devient rc2 XXX ces fonctions ne sont pas encore implémentées + -S, --snapshot + Ajouter l'identifiant SNAPSHOT, utilisé par Maven -R, --final, --release - Supprimer l'identifiant de prérelease + Supprimer l'identifiant de prérelease, utilisé par Maven -m, --metadata ID Spécifier un identifiant de build, à ajouter au numéro de version. -M, --vcs-metadata - Spécifier l'identifiant à partir de la révision actuelle dans le + Calculer l'identifiant de build à partir de la révision actuelle dans le gestionnaire de version. Note: pour le moment, seul git est supporté. --add-metadata ID Ajouter l'identifiant spécifié à la valeur actuelle, au lieu de la - remplacer. Séparer l'identifiant de la valeur précédente avec un '.'" + remplacer. Séparer l'identifiant de la valeur précédente avec un '.' + +OPTIONS AVANCEES + --show-source + Afficher le type de source qui sera traité, i.e. pom, file, pom-string, + file-string, git-pom-string, git-file-string + --vpath VPATH + Pour les options -e et -E, spécifier le chemin XPATH du tag qui contient + le numéro de version. + --map MAPFILE + Cette option permet de spécifier un fichier de règles qui indique les + fichiers pom.xml et VERSION.txt qui doivent être mis à jour dans un + projet multi-modules pour lequel les versions doivent être mises à jour + en même temps. + Par défaut, si un fichier nommé .pver-map existe dans le répertoire de + {POM,VERSION}FILE, cette option est automatiquement activée. Ainsi, on + n'aura besoin d'utiliser cette option que si l'on désire charger un + fichier alternatif ou ignorer le fichier par défaut. + Si une valeur vide est fournie, seul le fichier {POM,VERSION}FILE est + traité. Sinon, {POM,VERSION}FILE est utilisé uniquement pour chercher le + fichier .pver-map et seuls les fichiers mentionnés dans MAPFILE sont + traités. + Le fichier MAPFILE est constitué d'un ensemble de lignes de la forme + FILESPEC:VPATH + FILESPEC est requis et prend la forme d'une spécification de chemin + relatif au répertoire de MAPFILE et identifiant un ensemble de fichiers + de version. Si FILESPEC contient des wildcards, alors les fichiers + identifiés par ce chemin sont ignorés s'ils ont déjà été traités par une + règle précédente. Si FILESPEC ne contient pas de wildcards, alors le + fichier est systématiquement traité. + VPATH désigne le chemin XPATH vers le numéro de version qu'il faut + mettre à jour dans les fichiers pom.xml. Si VPATH vaut -, alors le + fichier pom.xml correspondant n'est pas modifié (il est ignoré). Si + VPATH est vide alors le chemin par défaut est utilisé pour ce + fichier. Pour les fichiers VERSION.txt, VPATH doit être vide + Le fichier de version correspondant au premier fichier de la première + ligne de MAPFILE contient la version de référence, qui est dupliquée + dans tous les autres fichiers. + --allow-empty + Supporter que la version puisse ne pas être spécifiée ni trouvée. Sans + cette option, on assume que la version effective est 0.0.0 si elle n'est + pas spécifiée ni trouvée. + Avec --show et --update, ne rien afficher si la version est vide. + --convert + --no-convert + Activer (resp. désactiver) la conversion automatique. Par défaut, si la + version est au format classique 'x.z[.p]-rDD/MM/YYYY', elle est + convertie automatiquement au format sémantique x.z.p+rYYYYMMDD + -t, --maven-update + Mettre à jour le numéro de version selons les règles de Maven. Cette + option est automatiquement activée si -e est sélectionné. Elle n'est + prise en compte qu'avec l'option -u + Si les options -R et -S ne sont pas spécifiée, alors une release est + transformée en snapshot et une snapshot en release. Avec -R, une + snapshot est transformée en release, et une release est traitée / + incrémentée normalement. Avec -S une release est transformée en + snapshot, et un snapshot est traité / incrémentée normalement. + Si l'une des options -x, -z, -p est utilisée, alors la partie spécifiée + est incrémentée selon les règles suivantes. Dans les exemples suivants, + A est un nombre quelconque, B = A + 1, et x, y et z sont des nombres + quelconques. + Avec l'option -x: + A.0.0-SNAPSHOT --> A.0.0 + A.x.y-SNAPSHOT --> B.0.0 + x.A.y --> x.B.0-SNAPSHOT + Avec l'option -z: + x.A.0-SNAPSHOT --> x.A.0 + x.A.y-SNAPSHOT --> x.B.0 + x.A.y --> x.B.0-SNAPSHOT + Avec l'option -p: + x.y.z-SNAPSHOT --> x.y.z + x.y.A --> x.y.B-SNAPSHOT + Si aucune des options -x, -z, -p n'est utilisée, alors les parties sont + incrémentées selon les règles suivantes: + x.y.z-SNAPSHOT --> x.y.z (comme -p) + x.A.0 --> x.B.0-SNAPSHOT (comme -z) + x.y.A --> x.y.B-SNAPSHOT (comme -p)" } pver "$@"