Package pyxb :: Package utils :: Module utility
[hide private]
[frames] | no frames]

Source Code for Module pyxb.utils.utility

   1  # -*- coding: utf-8 -*- 
   2  # Copyright 2009-2013, Peter A. Bigot 
   3  # 
   4  # Licensed under the Apache License, Version 2.0 (the "License"); you may 
   5  # not use this file except in compliance with the License. You may obtain a 
   6  # copy of the License at: 
   7  # 
   8  #            http://www.apache.org/licenses/LICENSE-2.0 
   9  # 
  10  # Unless required by applicable law or agreed to in writing, software 
  11  # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 
  12  # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 
  13  # License for the specific language governing permissions and limitations 
  14  # under the License. 
  15   
  16  """Utility functions and classes.""" 
  17   
  18  import re 
  19  import os 
  20  import errno 
  21  import pyxb 
  22  import urlparse 
  23  import time 
  24  import datetime 
  25  import logging 
  26   
  27  _log = logging.getLogger(__name__) 
28 29 -def BackfillComparisons (cls):
30 """Class decorator that fills in missing ordering methods. 31 32 Concept derived from Python 2.7.5 functools.total_ordering, 33 but this version requires that __eq__ and __lt__ be provided, 34 and unconditionally overrides __ne__, __gt__, __le__, and __ge__ 35 with the derived versions. 36 37 This is still necessary in Python 3 because in Python 3 the 38 comparison x >= y is done by the __ge__ inherited from object, 39 which does not handle the case where x and y are not the same type 40 even if the underlying y < x would convert x to be compatible. """ 41 42 def applyconvert (cls, derived): 43 for (opn, opx) in derived: 44 opx.__name__ = opn 45 opx.__doc__ = getattr(int, opn).__doc__ 46 setattr(cls, opn, opx)
47 48 applyconvert(cls, ( 49 ('__gt__', lambda self, other: not (self.__lt__(other) or self.__eq__(other))), 50 ('__le__', lambda self, other: self.__lt__(other) or self.__eq__(other)), 51 ('__ge__', lambda self, other: not self.__lt__(other)) 52 )) 53 applyconvert(cls, ( 54 ('__ne__', lambda self, other: not self.__eq__(other)), 55 )) 56 return cls 57
58 -def IteratedCompareMixed (lhs, rhs):
59 """Tuple comparison that permits C{None} as lower than any value, 60 and defines other cross-type comparison. 61 62 @return: -1 if lhs < rhs, 0 if lhs == rhs, 1 if lhs > rhs.""" 63 li = iter(lhs) 64 ri = iter(rhs) 65 while True: 66 try: 67 (lv, rv) = (li.next(), ri.next()) 68 if lv is None: 69 if rv is None: 70 continue 71 return -1 72 if rv is None: 73 return 1 74 if lv == rv: 75 continue 76 if lv < rv: 77 return -1 78 return 1 79 except StopIteration: 80 nl = len(lhs) 81 nr = len(rhs) 82 if nl < nr: 83 return -1 84 if nl == nr: 85 return 0 86 return 1
87
88 -def QuotedEscaped (s):
89 """Convert a string into a literal value that can be used in Python source. 90 91 This just calls C{repr}. No point in getting all complex when the language 92 already gives us what we need. 93 94 @rtype: C{str} 95 """ 96 return repr(s)
97
98 -def _DefaultXMLIdentifierToPython (identifier):
99 """Default implementation for _XMLIdentifierToPython 100 101 For historical reasons, this converts the identifier from a str to 102 unicode in the system default encoding. This should have no 103 practical effect. 104 105 @param identifier : some XML identifier 106 107 @return: C{unicode(identifier)} 108 """ 109 110 return unicode(identifier)
111
112 -def _SetXMLIdentifierToPython (xml_identifier_to_python):
113 """Configure a callable L{MakeIdentifier} uses to pre-process an XM Lidentifier. 114 115 In Python3, identifiers can be full Unicode tokens, but in Python2, 116 all identifiers must be ASCII characters. L{MakeIdentifier} enforces 117 this by removing all characters that are not valid within an 118 identifier. 119 120 In some cases, an application generating bindings may be able to 121 transliterate Unicode code points that are not valid Python identifier 122 characters into something else. This callable can be assigned to 123 perform that translation before the invalid characters are 124 stripped. 125 126 It is not the responsibility of this callable to do anything other 127 than replace whatever characters it wishes to. All 128 transformations performed by L{MakeIdentifier} will still be 129 applied, to ensure the output is in fact a legal identifier. 130 131 @param xml_identifier_to_python : A callable that takes a string 132 and returns a Unicode, possibly with non-identifier characters 133 replaced by other characters. Pass C{None} to reset to the 134 default implementation, which is L{_DefaultXMLIdentifierToPython}. 135 136 @rtype: C{unicode} 137 """ 138 global _XMLIdentifierToPython 139 if xml_identifier_to_python is None: 140 xml_identifier_to_python = _DefaultXMLIdentifierToPython 141 _XMLIdentifierToPython = xml_identifier_to_python
142 143 _XMLIdentifierToPython = _DefaultXMLIdentifierToPython 144 145 _UnderscoreSubstitute_re = re.compile(r'[- .]') 146 _NonIdentifier_re = re.compile(r'[^a-zA-Z0-9_]') 147 _PrefixUnderscore_re = re.compile(r'^_+') 148 _PrefixDigit_re = re.compile(r'^\d+') 149 _CamelCase_re = re.compile(r'_\w')
150 151 -def MakeIdentifier (s, camel_case=False):
152 """Convert a string into something suitable to be a Python identifier. 153 154 The string is processed by L{_XMLIdentifierToPython}. Following 155 this, dashes, spaces, and periods are replaced by underscores, and 156 characters not permitted in Python identifiers are stripped. 157 Furthermore, any leading underscores are removed. If the result 158 begins with a digit, the character 'n' is prepended. If the 159 result is the empty string, the string 'emptyString' is 160 substituted. 161 162 No check is made for L{conflicts with keywords <DeconflictKeyword>}. 163 164 @keyword camel_case : If C{True}, any underscore in the result 165 string that is immediately followed by an alphanumeric is replaced 166 by the capitalized version of that alphanumeric. Thus, 167 'one_or_two' becomes 'oneOrTwo'. If C{False} (default), has no 168 effect. 169 170 @rtype: C{str} 171 """ 172 s = _XMLIdentifierToPython(s) 173 s = _PrefixUnderscore_re.sub('', _NonIdentifier_re.sub('', _UnderscoreSubstitute_re.sub('_', s))) 174 if camel_case: 175 s = _CamelCase_re.sub(lambda _m: _m.group(0)[1].upper(), s) 176 if _PrefixDigit_re.match(s): 177 s = 'n' + s 178 if 0 == len(s): 179 s = 'emptyString' 180 return s
181 182 _PythonKeywords = frozenset( ( 183 "and", "as", "assert", "break", "class", "continue", "def", "del", 184 "elif", "else", "except", "exec", "finally", "for", "from", "global", 185 "if", "import", "in", "is", "lambda", "not", "or", "pass", "print", 186 "raise", "return", "try", "while", "with", "yield" 187 ) ) 188 """Python keywords. Note that types like int and float are not 189 keywords. 190 191 @see: U{http://docs.python.org/reference/lexical_analysis.html#keywords}.""" 192 193 _PythonBuiltInConstants = frozenset( ( 194 "False", "True", "None", "NotImplemented", "Ellipsis", "__debug__", 195 # "set" is neither a keyword nor a constant, but if some fool 196 # like {http://www.w3.org/2001/SMIL20/}set gets defined there's 197 # no way to access the builtin constructor. 198 "set" 199 ) ) 200 """Other symbols that aren't keywords but that can't be used. 201 202 @see: U{http://docs.python.org/library/constants.html}.""" 203 204 _Keywords = frozenset(_PythonKeywords.union(_PythonBuiltInConstants)) 205 """The keywords reserved for Python, derived from L{_PythonKeywords} 206 and L{_PythonBuiltInConstants}."""
207 208 -def DeconflictKeyword (s, aux_keywords=frozenset()):
209 """If the provided string C{s} matches a Python language keyword, 210 append an underscore to distinguish them. 211 212 See also L{MakeUnique}. 213 214 @param s: string to be deconflicted 215 216 @keyword aux_keywords: optional iterable of additional strings 217 that should be treated as keywords. 218 219 @rtype: C{str} 220 221 """ 222 if (s in _Keywords) or (s in aux_keywords): 223 return '%s_' % (s,) 224 return s
225
226 -def MakeUnique (s, in_use):
227 """Return an identifier based on C{s} that is not in the given set. 228 229 The returned identifier is made unique by appending an underscore 230 and, if necessary, a serial number. 231 232 The order is : C{x}, C{x_}, C{x_2}, C{x_3}, ... 233 234 @param in_use: The set of identifiers already in use in the 235 relevant scope. C{in_use} is updated to contain the returned 236 identifier. 237 238 @rtype: C{str} 239 """ 240 if s in in_use: 241 ctr = 2 242 s = s.rstrip('_') 243 candidate = '%s_' % (s,) 244 while candidate in in_use: 245 candidate = '%s_%d' % (s, ctr) 246 ctr += 1 247 s = candidate 248 in_use.add(s) 249 return s
250
251 -def PrepareIdentifier (s, in_use, aux_keywords=frozenset(), private=False, protected=False):
252 """Combine everything required to create a unique identifier. 253 254 Leading and trailing underscores are stripped from all 255 identifiers. 256 257 @param in_use: the set of already used identifiers. Upon return 258 from this function, it is updated to include the returned 259 identifier. 260 261 @keyword aux_keywords: an optional set of additional symbols that 262 are illegal in the given context; use this to prevent conflicts 263 with known method names. 264 265 @keyword private: if C{False} (default), all leading underscores 266 are stripped, guaranteeing the identifier will not be private. If 267 C{True}, the returned identifier has two leading underscores, 268 making it a private variable within a Python class. 269 270 @keyword protected: as for C{private}, but uses only one 271 underscore. 272 273 @rtype: C{str} 274 275 @note: Only module-level identifiers should be treated as 276 protected. The class-level L{_DeconflictSymbols_mixin} 277 infrastructure does not include protected symbols. All class and 278 instance members beginning with a single underscore are reserved 279 for the PyXB infrastructure.""" 280 s = DeconflictKeyword(MakeIdentifier(s).strip('_'), aux_keywords) 281 if private: 282 s = '__' + s 283 elif protected: 284 s = '_' + s 285 return MakeUnique(s, in_use)
286
287 # @todo: descend from pyxb.cscRoot, if we import pyxb 288 -class _DeconflictSymbols_mixin (object):
289 """Mix-in used to deconflict public symbols in classes that may be 290 inherited by generated binding classes. 291 292 Some classes, like the L{pyxb.binding.basis.element} or 293 L{pyxb.binding.basis.simpleTypeDefinition} classes in 294 L{pyxb.binding.basis}, have public symbols associated with 295 functions and variables. It is possible that an XML schema might 296 include tags and attribute names that match these symbols. To 297 avoid conflict, the reserved symbols marked in this class are 298 added to the pre-defined identifier set. 299 300 Subclasses should create a class-level variable 301 C{_ReservedSymbols} that contains a set of strings denoting the 302 symbols reserved in this class, combined with those from any 303 superclasses that also have reserved symbols. Code like the 304 following is suggested:: 305 306 # For base classes (direct mix-in): 307 _ReservedSymbols = set([ 'one', 'two' ]) 308 # For subclasses: 309 _ReservedSymbols = SuperClass._ReservedSymbols.union(set([ 'three' ])) 310 311 Only public symbols (those with no underscores) are currently 312 supported. (Private symbols can't be deconflicted that easily, 313 and no protected symbols that derive from the XML are created by 314 the binding generator.) 315 """ 316 317 _ReservedSymbols = set() 318 """There are no reserved symbols in the base class."""
319 320 # Regular expression detecting tabs, carriage returns, and line feeds 321 __TabCRLF_re = re.compile("[\t\n\r]") 322 # Regular expressoin detecting sequences of two or more spaces 323 __MultiSpace_re = re.compile(" +")
324 325 -def NormalizeWhitespace (text, preserve=False, replace=False, collapse=False):
326 """Normalize the given string. 327 328 Exactly one of the C{preserve}, C{replace}, and C{collapse} keyword 329 parameters must be assigned the value C{True} by the caller. 330 331 - C{preserve}: the text is returned unchanged. 332 333 - C{replace}: all tabs, newlines, and carriage returns are 334 replaced with ASCII spaces. 335 336 - C{collapse}: the C{replace} normalization is done, then 337 sequences of two or more spaces are replaced by a single space. 338 339 See the U{whiteSpace facet<http://www.w3.org/TR/xmlschema-2/#rf-whiteSpace>}. 340 341 @rtype: C{str} 342 """ 343 if preserve: 344 return text 345 text = __TabCRLF_re.sub(' ', text) 346 if replace: 347 return text 348 if collapse: 349 return __MultiSpace_re.sub(' ', text).strip() 350 # pyxb not imported here; could be. 351 raise Exception('NormalizeWhitespace: No normalization specified')
352
353 -class Graph:
354 """Represent a directed graph with arbitrary objects as nodes. 355 356 This is used in the L{code 357 generator<pyxb.binding.generate.Generator>} to determine order 358 dependencies among components within a namespace, and schema that 359 comprise various namespaces. An edge from C{source} to C{target} 360 indicates that some aspect of C{source} requires that some aspect 361 of C{target} already be available. 362 """ 363
364 - def __init__ (self, root=None):
365 self.__roots = None 366 if root is not None: 367 self.__roots = set([root]) 368 self.__edges = set() 369 self.__edgeMap = { } 370 self.__reverseMap = { } 371 self.__nodes = set()
372 373 __scc = None 374 __sccMap = None 375 __dfsOrder = None 376
377 - def addEdge (self, source, target):
378 """Add a directed edge from the C{source} to the C{target}. 379 380 The nodes are added to the graph if necessary. 381 """ 382 self.__edges.add( (source, target) ) 383 self.__edgeMap.setdefault(source, set()).add(target) 384 if source != target: 385 self.__reverseMap.setdefault(target, set()).add(source) 386 self.__nodes.add(source) 387 self.__nodes.add(target)
388
389 - def addNode (self, node):
390 """Add the given node to the graph.""" 391 self.__nodes.add(node)
392 393 __roots = None
394 - def roots (self, reset=False):
395 """Return the set of nodes calculated to be roots (i.e., those that have no incoming edges). 396 397 This caches the roots calculated in a previous invocation 398 unless the C{reset} keyword is given the value C{True}. 399 400 @note: Upon reset, any notes that had been manually added 401 using L{addNode} will no longer be in the set. 402 403 @keyword reset: If C{True}, any cached value is discarded and 404 recomputed. No effect if C{False} (defalut). 405 406 @rtype: C{set} 407 """ 408 if reset or (self.__roots is None): 409 self.__roots = set() 410 for n in self.__nodes: 411 if not (n in self.__reverseMap): 412 self.__roots.add(n) 413 return self.__roots
414 - def addRoot (self, root):
415 """Add the provided node as a root node, even if it has incoming edges. 416 417 The node need not be present in the graph (if necessary, it is added). 418 419 Note that roots added in this way do not survive a reset using 420 L{roots}. 421 422 @return: C{self} 423 """ 424 if self.__roots is None: 425 self.__roots = set() 426 self.__nodes.add(root) 427 self.__roots.add(root) 428 return self
429
430 - def edgeMap (self):
431 """Return the edges in the graph. 432 433 The edge data structure is a map from the source node to the 434 set of nodes that can be reached in a single step from the 435 source. 436 """ 437 return self.__edgeMap
438 __edgeMap = None 439
440 - def edges (self):
441 """Return the edges in the graph. 442 443 The edge data structure is a set of node pairs represented as C{( source, target )}. 444 """ 445 return self.__edges
446
447 - def nodes (self):
448 """Return the set of nodes in the graph. 449 450 The node collection data structure is a set containing node 451 objects, whatever they may be.""" 452 return self.__nodes
453
454 - def tarjan (self, reset=False):
455 """Execute Tarjan's algorithm on the graph. 456 457 U{Tarjan's 458 algorithm<http://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm>} 459 computes the U{strongly-connected 460 components<http://en.wikipedia.org/wiki/Strongly_connected_component>} 461 of the graph: i.e., the sets of nodes that form a minimal 462 closed set under edge transition. In essence, the loops. We 463 use this to detect groups of components that have a dependency 464 cycle. 465 466 @keyword reset: If C{True}, any cached component set is erased 467 and recomputed. If C{True}, an existing previous result is 468 left unchanged.""" 469 470 if (self.__scc is not None) and (not reset): 471 return 472 self.__sccMap = { } 473 self.__stack = [] 474 self.__sccOrder = [] 475 self.__scc = [] 476 self.__index = 0 477 self.__tarjanIndex = { } 478 self.__tarjanLowLink = { } 479 for v in self.__nodes: 480 self.__tarjanIndex[v] = None 481 roots = self.roots() 482 if (0 == len(roots)) and (0 < len(self.__nodes)): 483 raise Exception('TARJAN: No roots found in graph with %d nodes' % (len(self.__nodes),)) 484 for r in roots: 485 self._tarjan(r) 486 self.__didTarjan = True
487
488 - def _tarjan (self, v):
489 """Do the work of Tarjan's algorithm for a given root node.""" 490 if self.__tarjanIndex.get(v) is not None: 491 # "Root" was already reached. 492 return 493 self.__tarjanIndex[v] = self.__tarjanLowLink[v] = self.__index 494 self.__index += 1 495 self.__stack.append(v) 496 source = v 497 for target in self.__edgeMap.get(source, []): 498 if self.__tarjanIndex[target] is None: 499 self._tarjan(target) 500 self.__tarjanLowLink[v] = min(self.__tarjanLowLink[v], self.__tarjanLowLink[target]) 501 elif target in self.__stack: 502 self.__tarjanLowLink[v] = min(self.__tarjanLowLink[v], self.__tarjanLowLink[target]) 503 else: 504 pass 505 506 if self.__tarjanLowLink[v] == self.__tarjanIndex[v]: 507 scc = [] 508 while True: 509 scc.append(self.__stack.pop()) 510 if v == scc[-1]: 511 break 512 self.__sccOrder.append(scc) 513 if 1 < len(scc): 514 self.__scc.append(scc) 515 [ self.__sccMap.setdefault(_v, scc) for _v in scc ]
516
517 - def scc (self, reset=False):
518 """Return the strongly-connected components of the graph. 519 520 The data structure is a set, each element of which is itself a 521 set containing one or more nodes from the graph. 522 523 @see: L{tarjan}. 524 """ 525 if reset or (self.__scc is None): 526 self.tarjan(reset) 527 return self.__scc
528 __scc = None 529
530 - def sccMap (self, reset=False):
531 """Return a map from nodes to the strongly-connected component 532 to which the node belongs. 533 534 @keyword reset: If C{True}, the L{tarjan} method will be 535 re-invoked, propagating the C{reset} value. If C{False} 536 (default), a cached value will be returned if available. 537 538 @see: L{tarjan}. 539 """ 540 if reset or (self.__sccMap is None): 541 self.tarjan(reset) 542 return self.__sccMap
543 __sccMap = None 544
545 - def sccOrder (self, reset=False):
546 """Return the strongly-connected components in order. 547 548 The data structure is a list, in dependency order, of strongly 549 connected components (which can be single nodes). Appearance 550 of a node in a set earlier in the list indicates that it has 551 no dependencies on any node that appears in a subsequent set. 552 This order is preferred over L{dfsOrder} for code generation, 553 since it detects loops. 554 555 @see: L{tarjan}. 556 """ 557 if reset or (self.__sccOrder is None): 558 self.tarjan(reset) 559 return self.__sccOrder
560 __sccOrder = None 561
562 - def sccForNode (self, node, **kw):
563 """Return the strongly-connected component to which the given 564 node belongs. 565 566 Any keywords suppliend when invoking this method are passed to 567 the L{sccMap} method. 568 569 @return: The SCC set, or C{None} if the node is not present in 570 the results of Tarjan's algorithm.""" 571 572 return self.sccMap(**kw).get(node)
573
574 - def cyclomaticComplexity (self):
575 """Return the cyclomatic complexity of the graph.""" 576 self.tarjan() 577 return len(self.__edges) - len(self.__nodes) + 2 * len(self.__scc)
578
579 - def __dfsWalk (self, source):
580 assert not (source in self.__dfsWalked) 581 self.__dfsWalked.add(source) 582 for target in self.__edgeMap.get(source, []): 583 if not (target in self.__dfsWalked): 584 self.__dfsWalk(target) 585 self.__dfsOrder.append(source)
586
587 - def _generateDOT (self, title='UNKNOWN', labeller=None):
588 node_map = { } 589 idx = 1 590 for n in self.__nodes: 591 node_map[n] = idx 592 idx += 1 593 text = [] 594 text.append('digraph "%s" {' % (title,)) 595 for n in self.__nodes: 596 if labeller is not None: 597 nn = labeller(n) 598 else: 599 nn = str(n) 600 text.append('%s [shape=box,label="%s"];' % (node_map[n], nn)) 601 for s in self.__nodes: 602 for d in self.__edgeMap.get(s, []): 603 if s != d: 604 text.append('%s -> %s;' % (node_map[s], node_map[d])) 605 text.append("};") 606 return "\n".join(text)
607
608 - def dfsOrder (self, reset=False):
609 """Return the nodes of the graph in U{depth-first-search 610 order<http://en.wikipedia.org/wiki/Depth-first_search>}. 611 612 The data structure is a list. Calculated lists are retained 613 and returned on future invocations, subject to the C{reset} 614 keyword. 615 616 @keyword reset: If C{True}, discard cached results and recompute the order.""" 617 if reset or (self.__dfsOrder is None): 618 self.__dfsWalked = set() 619 self.__dfsOrder = [] 620 for root in self.roots(reset=reset): 621 self.__dfsWalk(root) 622 self.__dfsWalked = None 623 if len(self.__dfsOrder) != len(self.__nodes): 624 raise Exception('DFS walk did not cover all nodes (walk %d versus nodes %d)' % (len(self.__dfsOrder), len(self.__nodes))) 625 return self.__dfsOrder
626
627 - def rootSetOrder (self):
628 """Return the nodes of the graph as a sequence of root sets. 629 630 The first root set is the set of nodes that are roots: i.e., 631 have no incoming edges. The second root set is the set of 632 nodes that have incoming nodes in the first root set. This 633 continues until all nodes have been reached. The sets impose 634 a partial order on the nodes, without being as constraining as 635 L{sccOrder}. 636 637 @return: a list of the root sets.""" 638 order = [] 639 nodes = set(self.__nodes) 640 edge_map = {} 641 for (d, srcs) in self.__edgeMap.iteritems(): 642 edge_map[d] = srcs.copy() 643 while nodes: 644 freeset = set() 645 for n in nodes: 646 if not (n in edge_map): 647 freeset.add(n) 648 if 0 == len(freeset): 649 _log.error('dependency cycle in named components') 650 return None 651 order.append(freeset) 652 nodes.difference_update(freeset) 653 new_edge_map = {} 654 for (d, srcs) in edge_map.iteritems(): 655 srcs.difference_update(freeset) 656 if 0 != len(srcs): 657 new_edge_map[d] = srcs 658 edge_map = new_edge_map 659 return order
660 661 LocationPrefixRewriteMap_ = { }
662 663 -def SetLocationPrefixRewriteMap (prefix_map):
664 """Set the map that is used to by L{NormalizeLocation} to rewrite URI prefixes.""" 665 666 LocationPrefixRewriteMap_.clear() 667 LocationPrefixRewriteMap_.update(prefix_map)
668
669 -def NormalizeLocation (uri, parent_uri=None, prefix_map=None):
670 """Normalize a URI against an optional parent_uri in the way that is 671 done for C{schemaLocation} attribute values. 672 673 If no URI schema is present, this will normalize a file system 674 path. 675 676 Optionally, the resulting absolute URI can subsequently be 677 rewritten to replace specified prefix strings with alternative 678 strings, e.g. to convert a remote URI to a local repository. This 679 rewriting is done after the conversion to an absolute URI, but 680 before normalizing file system URIs. 681 682 @param uri : The URI to normalize. If C{None}, function returns 683 C{None} 684 685 @param parent_uri : The base URI against which normalization is 686 done, if C{uri} is a relative URI. 687 688 @param prefix_map : A map used to rewrite URI prefixes. If 689 C{None}, the value defaults to that stored by 690 L{SetLocationPrefixRewriteMap}. 691 692 """ 693 if uri is None: 694 return uri 695 if parent_uri is None: 696 abs_uri = uri 697 else: 698 #if (0 > parent_uri.find(':')) and (not parent_uri.endswith(os.sep)): 699 # parent_uri = parent_uri + os.sep 700 abs_uri = urlparse.urljoin(parent_uri, uri) 701 if prefix_map is None: 702 prefix_map = LocationPrefixRewriteMap_ 703 for (pfx, sub) in prefix_map.iteritems(): 704 if abs_uri.startswith(pfx): 705 abs_uri = sub + abs_uri[len(pfx):] 706 if 0 > abs_uri.find(':'): 707 abs_uri = os.path.realpath(abs_uri) 708 return abs_uri
709
710 711 -def DataFromURI (uri, archive_directory=None):
712 """Retrieve the contents of the uri as raw data. 713 714 If the uri does not include a scheme (e.g., C{http:}), it is 715 assumed to be a file path on the local system.""" 716 import urllib 717 import urllib2 718 stream = None 719 exc = None 720 # Only something that has a colon is a non-file URI. Some things 721 # that have a colon are a file URI (sans schema). Prefer urllib2, 722 # but allow urllib (which apparently works better on Windows). 723 if 0 <= uri.find(':'): 724 try: 725 stream = urllib2.urlopen(uri) 726 except Exception as e: 727 exc = e 728 if stream is None: 729 try: 730 stream = urllib.urlopen(uri) 731 exc = None 732 except: 733 # Prefer urllib exception 734 pass 735 if stream is None: 736 # No go as URI; give file a chance 737 try: 738 stream = open(uri, 'rb') 739 exc = None 740 except Exception as e: 741 if exc is None: 742 exc = e 743 if exc is not None: 744 _log.error('open %s', uri, exc_info=exc) 745 raise exc 746 try: 747 # Protect this in case whatever stream is doesn't have an fp 748 # attribute. 749 if isinstance(stream, file) or isinstance(stream.fp, file): 750 archive_directory = None 751 except: 752 pass 753 xmld = stream.read() 754 if archive_directory: 755 base_name = os.path.basename(os.path.normpath(urlparse.urlparse(uri)[2])) 756 counter = 1 757 dest_file = os.path.join(archive_directory, base_name) 758 while os.path.isfile(dest_file): 759 dest_file = os.path.join(archive_directory, '%s.%d' % (base_name, counter)) 760 counter += 1 761 try: 762 OpenOrCreate(dest_file).write(xmld) 763 except OSError as e: 764 _log.warning('Unable to save %s in %s: %s', uri, dest_file, e) 765 return xmld
766
767 -def OpenOrCreate (file_name, tag=None, preserve_contents=False):
768 """Return a file object used to write binary data into the given file. 769 770 Use the C{tag} keyword to preserve the contents of existing files 771 that are not supposed to be overwritten. 772 773 To get a writable file but leaving any existing contents in place, 774 set the C{preserve_contents} keyword to C{True}. Normally, existing file 775 contents are erased. 776 777 The returned file pointer is positioned at the end of the file. 778 779 @keyword tag: If not C{None} and the file already exists, absence 780 of the given value in the first 4096 bytes of the file (decoded as 781 UTF-8) causes an C{IOError} to be raised with C{errno} set to 782 C{EEXIST}. I.e., only files with this value in the first 4KB will 783 be returned for writing. 784 785 @keyword preserve_contents: This value controls whether existing 786 contents of the file will be erased (C{False}, default) or left in 787 place (C{True}). 788 """ 789 (path, leaf) = os.path.split(file_name) 790 if path: 791 try: 792 os.makedirs(path) 793 except Exception as e: 794 if not (isinstance(e, (OSError, IOError)) and (errno.EEXIST == e.errno)): 795 raise 796 fp = open(file_name, 'ab+') 797 if (tag is not None) and (0 < os.fstat(fp.fileno()).st_size): 798 fp.seek(0) # os.SEEK_SET 799 blockd = fp.read(4096) 800 blockt = blockd.decode('utf-8') 801 if 0 > blockt.find(tag): 802 raise OSError(errno.EEXIST, os.strerror(errno.EEXIST)) 803 if not preserve_contents: 804 fp.seek(0) # os.SEEK_SET 805 fp.truncate() 806 else: 807 fp.seek(2) # os.SEEK_END 808 return fp
809 810 # hashlib didn't show up until 2.5, and sha is deprecated in 2.6. 811 __Hasher = None 812 try: 813 import hashlib 814 __Hasher = hashlib.sha1 815 except ImportError: 816 import sha 817 __Hasher = sha.new
818 819 -def HashForText (text):
820 """Calculate a cryptographic hash of the given string. 821 822 For example, this is used to verify that a given module file 823 contains bindings from a previous generation run for the same 824 namespace. See L{OpenOrCreate}. If the text is in Unicode, the 825 hash is calculated on the UTF-8 encoding of the text. 826 827 @return: A C{str}, generally a sequence of hexadecimal "digit"s. 828 """ 829 if isinstance(text, unicode): 830 text = text.encode('utf-8') 831 return __Hasher(text).hexdigest()
832 833 # uuid didn't show up until 2.5 834 __HaveUUID = False 835 try: 836 import uuid 837 __HaveUUID = True 838 except ImportError: 839 import random
840 -def _NewUUIDString ():
841 """Obtain a UUID using the best available method. On a version of 842 python that does not incorporate the C{uuid} class, this creates a 843 string combining the current date and time (to the second) with a 844 random number. 845 846 @rtype: C{str} 847 """ 848 if __HaveUUID: 849 return uuid.uuid1().urn 850 return '%s:%08.8x' % (time.strftime('%Y%m%d%H%M%S'), random.randint(0, 0xFFFFFFFFL))
851
852 -class UniqueIdentifier (object):
853 """Records a unique identifier, generally associated with a 854 binding generation action. 855 856 The identifier is a string, but gets wrapped in an instance of 857 this class to optimize comparisons and reduce memory footprint. 858 859 Invoking the constructor for this class on the same string 860 multiple times will return the same Python object. 861 862 An instance of this class compares equal to, and hashes equivalent 863 to, the uid string. When C{str}'d, the result is the uid; when 864 C{repr}'d, the result is a constructor call to 865 C{pyxb.utils.utility.UniqueIdentifier}. 866 """ 867 868 # A map from UID string to the instance that represents it 869 __ExistingUIDs = {} 870
871 - def uid (self):
872 """The string unique identifier""" 873 return self.__uid
874 __uid = None 875 876 # Support pickling, which is done using only the UID.
877 - def __getnewargs__ (self):
878 return (self.__uid,)
879
880 - def __getstate__ (self):
881 return self.__uid
882
883 - def __setstate__ (self, state):
884 assert self.__uid == state
885 886 # Singleton-like
887 - def __new__ (cls, *args):
888 if 0 == len(args): 889 uid = _NewUUIDString() 890 else: 891 uid = args[0] 892 if isinstance(uid, UniqueIdentifier): 893 uid = uid.uid() 894 if not isinstance(uid, basestring): 895 raise TypeError('UniqueIdentifier uid must be a string') 896 rv = cls.__ExistingUIDs.get(uid) 897 if rv is None: 898 rv = super(UniqueIdentifier, cls).__new__(cls) 899 rv.__uid = uid 900 cls.__ExistingUIDs[uid] = rv 901 return rv
902
903 - def associateObject (self, obj):
904 """Associate the given object witth this identifier. 905 906 This is a one-way association: the object is not provided with 907 a return path to this identifier instance.""" 908 self.__associatedObjects.add(obj)
909 - def associatedObjects (self):
910 """The set of objects that have been associated with this 911 identifier instance.""" 912 return self.__associatedObjects
913 __associatedObjects = None 914
915 - def __init__ (self, uid=None):
916 """Create a new UniqueIdentifier instance. 917 918 @param uid: The unique identifier string. If present, it is 919 the callers responsibility to ensure the value is universally 920 unique. If C{None}, one will be provided. 921 @type uid: C{str} or C{unicode} 922 """ 923 assert (uid is None) or (self.uid() == uid), 'UniqueIdentifier: ctor %s, actual %s' % (uid, self.uid()) 924 self.__associatedObjects = set()
925
926 - def __eq__ (self, other):
927 if other is None: 928 return False 929 elif isinstance(other, UniqueIdentifier): 930 other_uid = other.uid() 931 elif isinstance(other, basestring): 932 other_uid = other 933 else: 934 raise TypeError('UniqueIdentifier: Cannot compare with type %s' % (type(other),)) 935 return self.uid() == other_uid
936
937 - def __hash__ (self):
938 return hash(self.uid())
939
940 - def __str__ (self):
941 return self.uid()
942
943 - def __repr__ (self):
944 return 'pyxb.utils.utility.UniqueIdentifier(%s)' % (repr(self.uid()),)
945
946 @BackfillComparisons 947 -class UTCOffsetTimeZone (datetime.tzinfo):
948 """A C{datetime.tzinfo} subclass that helps deal with UTC 949 conversions in an ISO8601 world. 950 951 This class only supports fixed offsets from UTC. 952 """ 953 954 # Regular expression that matches valid ISO8601 time zone suffixes 955 __Lexical_re = re.compile('^([-+])(\d\d):(\d\d)$') 956 957 # The offset in minutes east of UTC. 958 __utcOffset_min = 0 959 960 # Same as __utcOffset_min, but as a datetime.timedelta 961 __utcOffset_td = None 962 963 # A zero-length duration 964 __ZeroDuration = datetime.timedelta(0) 965 966 # Range limits 967 __MaxOffset_td = datetime.timedelta(hours=14) 968
969 - def __init__ (self, spec=None):
970 """Create a time zone instance with a fixed offset from UTC. 971 972 @param spec: Specifies the offset. Can be an integer counting 973 minutes east of UTC, the value C{None} (equal to 0 minutes 974 east), or a string that conform to the ISO8601 time zone 975 sequence (B{Z}, or B{[+-]HH:MM}). 976 """ 977 978 if spec is not None: 979 if isinstance(spec, basestring): 980 if 'Z' == spec: 981 self.__utcOffset_min = 0 982 else: 983 match = self.__Lexical_re.match(spec) 984 if match is None: 985 raise ValueError('Bad time zone: %s' % (spec,)) 986 self.__utcOffset_min = int(match.group(2)) * 60 + int(match.group(3)) 987 if '-' == match.group(1): 988 self.__utcOffset_min = - self.__utcOffset_min 989 elif isinstance(spec, int): 990 self.__utcOffset_min = spec 991 elif isinstance(spec, datetime.timedelta): 992 self.__utcOffset_min = spec.seconds // 60 993 else: 994 raise TypeError('%s: unexpected type %s' % (type(self), type(spec))) 995 self.__utcOffset_td = datetime.timedelta(minutes=self.__utcOffset_min) 996 if self.__utcOffset_td < -self.__MaxOffset_td or self.__utcOffset_td > self.__MaxOffset_td: 997 raise ValueError('XSD timezone offset %s larger than %s' % (self.__utcOffset_td, self.__MaxOffset_td)) 998 if 0 == self.__utcOffset_min: 999 self.__tzName = 'Z' 1000 elif 0 > self.__utcOffset_min: 1001 self.__tzName = '-%02d:%02d' % divmod(-self.__utcOffset_min, 60) 1002 else: 1003 self.__tzName = '+%02d:%02d' % divmod(self.__utcOffset_min, 60)
1004
1005 - def utcoffset (self, dt):
1006 """Returns the constant offset for this zone.""" 1007 return self.__utcOffset_td
1008
1009 - def tzname (self, dt):
1010 """Return the name of the timezone in the format expected by XML Schema.""" 1011 return self.__tzName
1012
1013 - def dst (self, dt):
1014 """Returns a constant zero duration.""" 1015 return self.__ZeroDuration
1016
1017 - def __otherForComparison (self, other):
1018 if isinstance(other, UTCOffsetTimeZone): 1019 return other.__utcOffset_min 1020 return other.utcoffset(datetime.datetime.now())
1021
1022 - def __hash__ (self):
1023 return hash(self.__utcOffset_min)
1024
1025 - def __eq__ (self, other):
1026 return self.__utcOffset_min == self.__otherForComparison(other)
1027
1028 - def __lt__ (self, other):
1029 return self.__utcOffset_min < self.__otherForComparison(other)
1030
1031 -class LocalTimeZone (datetime.tzinfo):
1032 """A C{datetime.tzinfo} subclass for the local time zone. 1033 1034 Mostly pinched from the C{datetime.tzinfo} documentation in Python 2.5.1. 1035 """ 1036 1037 __STDOffset = datetime.timedelta(seconds=-time.timezone) 1038 __DSTOffset = __STDOffset 1039 if time.daylight: 1040 __DSTOffset = datetime.timedelta(seconds=-time.altzone) 1041 __ZeroDelta = datetime.timedelta(0) 1042 __DSTDelta = __DSTOffset - __STDOffset 1043
1044 - def utcoffset (self, dt):
1045 if self.__isDST(dt): 1046 return self.__DSTOffset 1047 return self.__STDOffset
1048
1049 - def dst (self, dt):
1050 if self.__isDST(dt): 1051 return self.__DSTDelta 1052 return self.__ZeroDelta
1053
1054 - def tzname (self, dt):
1055 return time.tzname[self.__isDST(dt)]
1056
1057 - def __isDST (self, dt):
1058 tt = (dt.year, dt.month, dt.day, 1059 dt.hour, dt.minute, dt.second, 1060 0, 0, -1) 1061 tt = time.localtime(time.mktime(tt)) 1062 return tt.tm_isdst > 0
1063
1064 -class PrivateTransient_mixin (pyxb.cscRoot):
1065 """Emulate the B{transient} keyword from Java for private member 1066 variables. 1067 1068 This class defines a C{__getstate__} method which returns a copy 1069 of C{self.__dict__} with certain members removed. Specifically, 1070 if a string "s" appears in a class member variable named 1071 C{__PrivateTransient} defined in the "Class" class, then the 1072 corresponding private variable "_Class__s" will be removed from 1073 the state dictionary. This is used to eliminate unnecessary 1074 fields from instances placed in L{namespace 1075 archives<pyxb.namespace.archive.NamespaceArchive>} without having 1076 to implement a C{__getstate__} method in every class in the 1077 instance hierarchy. 1078 1079 For an example, see 1080 L{pyxb.xmlschema.structures._SchemaComponent_mixin} 1081 1082 If you use this, it is your responsibility to define the 1083 C{__PrivateTransient} class variable and add to it the required 1084 variable names. 1085 1086 Classes that inherit from this are free to define their own 1087 C{__getstate__} method, which may or may not invoke the superclass 1088 one. If you do this, be sure that the class defining 1089 C{__getstate__} lists L{PrivateTransient_mixin} as one of its 1090 direct superclasses, lest the latter end up earlier in the mro and 1091 consequently bypass the local override. 1092 """ 1093 1094 # Suffix used when creating the class member variable in which the 1095 # transient members are cached. 1096 __Attribute = '__PrivateTransient' 1097
1098 - def __getstate__ (self):
1099 state = self.__dict__.copy() 1100 # Note that the aggregate set is stored in a class variable 1101 # with a slightly different name than the class-level set. 1102 attr = '_%s%s_' % (self.__class__.__name__, self.__Attribute) 1103 skipped = getattr(self.__class__, attr, None) 1104 if skipped is None: 1105 skipped = set() 1106 for cl in self.__class__.mro(): 1107 for (k, v) in cl.__dict__.iteritems(): 1108 if k.endswith(self.__Attribute): 1109 cl2 = k[:-len(self.__Attribute)] 1110 skipped.update([ '%s__%s' % (cl2, _n) for _n in v ]) 1111 setattr(self.__class__, attr, skipped) 1112 for k in skipped: 1113 if state.get(k) is not None: 1114 del state[k] 1115 # Uncomment the following to test whether undesirable types 1116 # are being pickled, generally by accidently leaving a 1117 # reference to one in an instance private member. 1118 #for (k, v) in state.iteritems(): 1119 # import pyxb.namespace 1120 # import xml.dom 1121 # import pyxb.xmlschema.structures 1122 # if isinstance(v, (pyxb.namespace.resolution.NamespaceContext, xml.dom.Node, pyxb.xmlschema.structures.Schema)): 1123 # raise pyxb.LogicError('Unexpected instance of %s key %s in %s' % (type(v), k, self)) 1124 1125 return state
1126
1127 -def GetMatchingFiles (path, pattern=None, default_path_wildcard=None, default_path=None, prefix_pattern=None, prefix_substituend=None):
1128 """Provide a list of absolute paths to files present in any of a 1129 set of directories and meeting certain criteria. 1130 1131 This is used, for example, to locate namespace archive files 1132 within the archive path specified by the user. One could use:: 1133 1134 files = GetMatchingFiles('&bundles//:+', 1135 pattern=re.compile('.*\.wxs$'), 1136 default_path_wildcard='+', 1137 default_path='/usr/local/pyxb/nsarchives', 1138 prefix_pattern='&', 1139 prefix_substituend='/opt/pyxb') 1140 1141 to obtain all files that can be recursively found within 1142 C{/opt/pyxb/bundles}, or non-recursively within 1143 C{/usr/local/pyxb/nsarchives}. 1144 1145 @param path: A list of directories in which the search should be 1146 performed. The entries are separated by os.pathsep, which is a 1147 colon on POSIX platforms and a semi-colon on Windows. If a path 1148 entry ends with C{//} regardless of platform, the suffix C{//} is 1149 stripped and any directory beneath the path is scanned as well, 1150 recursively. 1151 1152 @keyword pattern: Optional regular expression object used to 1153 determine whether a given directory entry should be returned. If 1154 left as C{None}, all directory entries will be returned. 1155 1156 @keyword default_path_wildcard: An optional string which, if 1157 present as a single directory in the path, is replaced by the 1158 value of C{default-path}. 1159 1160 @keyword default_path: A system-defined directory which can be 1161 restored to the path by placing the C{default_path_wildcard} in 1162 the C{path}. 1163 1164 @keyword prefix_pattern: An optional string which, if present at 1165 the start of a path element, is replaced by the value of 1166 C{prefix_substituend}. 1167 1168 @keyword prefix_substituend: A system-defined string (path prefix) 1169 which can be combined with the user-provided path information to 1170 identify a file or subdirectory within an installation-specific 1171 area. 1172 """ 1173 matching_files = [] 1174 path_set = path.split(os.pathsep) 1175 while 0 < len(path_set): 1176 path = path_set.pop(0) 1177 if default_path_wildcard == path: 1178 if default_path is not None: 1179 path_set[0:0] = default_path.split(os.pathsep) 1180 default_path = None 1181 continue 1182 recursive = False 1183 if (prefix_pattern is not None) and path.startswith(prefix_pattern): 1184 path = os.path.join(prefix_substituend, path[len(prefix_pattern):]) 1185 if path.endswith('//'): 1186 recursive = True 1187 path = path[:-2] 1188 if os.path.isfile(path): 1189 if (pattern is None) or (pattern.search(path) is not None): 1190 matching_files.append(path) 1191 else: 1192 for (root, dirs, files) in os.walk(path): 1193 for f in files: 1194 if (pattern is None) or (pattern.search(f) is not None): 1195 matching_files.append(os.path.join(root, f)) 1196 if not recursive: 1197 break 1198 return matching_files
1199
1200 @BackfillComparisons 1201 -class Location (object):
1202 __locationBase = None 1203 __lineNumber = None 1204 __columnNumber = None 1205
1206 - def __init__ (self, location_base=None, line_number=None, column_number=None):
1207 if isinstance(location_base, str): 1208 location_base = intern(location_base) 1209 self.__locationBase = location_base 1210 self.__lineNumber = line_number 1211 self.__columnNumber = column_number
1212
1213 - def newLocation (self, locator=None, line_number=None, column_number=None):
1214 if locator is not None: 1215 try: 1216 line_number = locator.getLineNumber() 1217 column_number = locator.getColumnNumber() 1218 except: 1219 pass 1220 return Location(self.__locationBase, line_number, column_number)
1221 1222 locationBase = property(lambda _s: _s.__locationBase) 1223 lineNumber = property(lambda _s: _s.__lineNumber) 1224 columnNumber = property(lambda _s: _s.__columnNumber) 1225
1226 - def __cmpSingleUnlessNone (self, v1, v2):
1227 if v1 is None: 1228 if v2 is None: 1229 return None 1230 return 1 1231 if v2 is None: 1232 return -1 1233 if v1 < v2: 1234 return -1 1235 if v1 == v2: 1236 return 0 1237 return 1
1238
1239 - def __cmpTupleUnlessNone (self, v1, v2):
1240 rv = self.__cmpSingleUnlessNone(v1.__locationBase, v2.__locationBase) 1241 if rv is None: 1242 rv = self.__cmpSingleUnlessNone(v1.__lineNumber, v2.__lineNumber) 1243 if rv is None: 1244 rv = self.__cmpSingleUnlessNone(v1.__columnNumber, v2.__columnNumber) 1245 return rv
1246
1247 - def __hash__ (self):
1248 return hash((self.__locationBase, self.__lineNumber, self.__columnNumber))
1249
1250 - def __eq__ (self, other):
1251 """Comparison by locationBase, then lineNumber, then columnNumber.""" 1252 if other is None: 1253 return False 1254 rv = self.__cmpTupleUnlessNone(self, other) 1255 if rv is None: 1256 return True 1257 return 0 == rv
1258
1259 - def __lt__ (self, other):
1260 if other is None: 1261 return False 1262 rv = self.__cmpTupleUnlessNone(self, other) 1263 if rv is None: 1264 return False 1265 return -1 == rv
1266
1267 - def __str__ (self):
1268 if self.locationBase is None: 1269 lb = '<unknown>' 1270 else: 1271 # No, this should not be os.sep. The location is 1272 # expected to be a URI. 1273 lb = self.locationBase.rsplit('/', 1)[-1] 1274 return '%s[%s:%s]' % (lb, self.lineNumber, self.columnNumber)
1275
1276 - def __repr__ (self):
1277 t = type(self) 1278 ctor = '%s.%s' % (t.__module__, t.__name__) 1279 return '%s(%r, %r, %r)' % (ctor, self.__locationBase, self.__lineNumber, self.__columnNumber)
1280
1281 -class Locatable_mixin (pyxb.cscRoot):
1282 __location = None 1283
1284 - def __init__ (self, *args, **kw):
1285 self.__location = kw.pop('location', None) 1286 super(Locatable_mixin, self).__init__(*args, **kw)
1287
1288 - def _setLocation (self, location):
1289 self.__location = location
1290
1291 - def _location (self):
1292 return self.__location
1293