1""" 2Find modules used by a script, using bytecode analysis. 3 4Based on the stdlib modulefinder by Thomas Heller and Just van Rossum, 5but uses a graph data structure and 2.3 features 6 7XXX: Verify all calls to import_hook (and variants) to ensure that 8imports are done in the right way. 9""" 10from __future__ import absolute_import, print_function 11 12import pkg_resources 13 14import dis 15import imp 16import marshal 17import os 18import sys 19import struct 20import zipimport 21import re 22from collections import deque, namedtuple 23import ast 24 25from altgraph.ObjectGraph import ObjectGraph 26from altgraph import GraphError 27 28from itertools import count 29 30from modulegraph import util 31from modulegraph import zipio 32 33if sys.version_info[0] == 2: 34 from StringIO import StringIO as BytesIO 35 from StringIO import StringIO 36 from urllib import pathname2url 37 def _Bchr(value): 38 return chr(value) 39 40else: 41 from urllib.request import pathname2url 42 from io import BytesIO, StringIO 43 44 def _Bchr(value): 45 return value 46 47 48# File open mode for reading (univeral newlines) 49if sys.version_info[0] == 2: 50 _READ_MODE = "rU" 51else: 52 _READ_MODE = "r" 53 54 55 56 57# Modulegraph does a good job at simulating Python's, but it can not 58# handle packagepath modifications packages make at runtime. Therefore there 59# is a mechanism whereby you can register extra paths in this map for a 60# package, and it will be honored. 61# 62# Note this is a mapping is lists of paths. 63_packagePathMap = {} 64 65# Prefix used in magic .pth files used by setuptools to create namespace 66# packages without an __init__.py file. 67# 68# The value is a list of such prefixes as the prefix varies with versions of 69# setuptools. 70_SETUPTOOLS_NAMESPACEPKG_PTHs=( 71 "import sys,types,os; p = os.path.join(sys._getframe(1).f_locals['sitedir'], *('", 72 "import sys,new,os; p = os.path.join(sys._getframe(1).f_locals['sitedir'], *('", 73 "import sys, types, os;p = os.path.join(sys._getframe(1).f_locals['sitedir'], *('", 74) 75 76 77def _namespace_package_path(fqname, pathnames, path=None): 78 """ 79 Return the __path__ for the python package in *fqname*. 80 81 This function uses setuptools metadata to extract information 82 about namespace packages from installed eggs. 83 """ 84 working_set = pkg_resources.WorkingSet(path) 85 86 path = list(pathnames) 87 88 for dist in working_set: 89 if dist.has_metadata('namespace_packages.txt'): 90 namespaces = dist.get_metadata( 91 'namespace_packages.txt').splitlines() 92 if fqname in namespaces: 93 nspath = os.path.join(dist.location, *fqname.split('.')) 94 if nspath not in path: 95 path.append(nspath) 96 97 return path 98 99_strs = re.compile(r'''^\s*["']([A-Za-z0-9_]+)["'],?\s*''') # "<- emacs happy 100 101def _eval_str_tuple(value): 102 """ 103 Input is the repr of a tuple of strings, output 104 is that tuple. 105 106 This only works with a tuple where the members are 107 python identifiers. 108 """ 109 if not (value.startswith('(') and value.endswith(')')): 110 raise ValueError(value) 111 112 orig_value = value 113 value = value[1:-1] 114 115 result = [] 116 while value: 117 m = _strs.match(value) 118 if m is None: 119 raise ValueError(orig_value) 120 121 result.append(m.group(1)) 122 value = value[len(m.group(0)):] 123 124 return tuple(result) 125 126def _path_from_importerror(exc, default): 127 # This is a hack, but sadly enough the necessary information 128 # isn't available otherwise. 129 m = re.match('^No module named (\S+)$', str(exc)) 130 if m is not None: 131 return m.group(1) 132 133 return default 134 135def os_listdir(path): 136 """ 137 Deprecated name 138 """ 139 warnings.warn("Use zipio.listdir instead of os_listdir", 140 DeprecationWarning) 141 return zipio.listdir(path) 142 143 144def _code_to_file(co): 145 """ Convert code object to a .pyc pseudo-file """ 146 return BytesIO( 147 imp.get_magic() + b'\0\0\0\0' + marshal.dumps(co)) 148 149 150def find_module(name, path=None): 151 """ 152 A version of imp.find_module that works with zipped packages. 153 """ 154 if path is None: 155 path = sys.path 156 157 # Support for the PEP302 importer for normal imports: 158 # - Python 2.5 has pkgutil.ImpImporter 159 # - In setuptools 0.7 and later there's _pkgutil.ImpImporter 160 # - In earlier setuptools versions you pkg_resources.ImpWrapper 161 # 162 # XXX: This is a bit of a hack, should check if we can just rely on 163 # PEP302's get_code() method with all recent versions of pkgutil and/or 164 # setuptools (setuptools 0.6.latest, setuptools trunk and python2.[45]) 165 # 166 # For python 3.3 this code should be replaced by code using importlib, 167 # for python 3.2 and 2.7 this should be cleaned up a lot. 168 try: 169 from pkgutil import ImpImporter 170 except ImportError: 171 try: 172 from _pkgutil import ImpImporter 173 except ImportError: 174 ImpImporter = pkg_resources.ImpWrapper 175 176 namespace_path =[] 177 fp = None 178 for entry in path: 179 importer = pkg_resources.get_importer(entry) 180 if importer is None: 181 continue 182 183 if sys.version_info[:2] >= (3,3) and hasattr(importer, 'find_loader'): 184 loader, portions = importer.find_loader(name) 185 186 else: 187 loader = importer.find_module(name) 188 portions = [] 189 190 namespace_path.extend(portions) 191 192 if loader is None: continue 193 194 if isinstance(importer, ImpImporter): 195 filename = loader.filename 196 if filename.endswith('.pyc') or filename.endswith('.pyo'): 197 fp = open(filename, 'rb') 198 description = ('.pyc', 'rb', imp.PY_COMPILED) 199 return (fp, filename, description) 200 201 elif filename.endswith('.py'): 202 if sys.version_info[0] == 2: 203 fp = open(filename, _READ_MODE) 204 else: 205 with open(filename, 'rb') as fp: 206 encoding = util.guess_encoding(fp) 207 208 fp = open(filename, _READ_MODE, encoding=encoding) 209 description = ('.py', _READ_MODE, imp.PY_SOURCE) 210 return (fp, filename, description) 211 212 else: 213 for _sfx, _mode, _type in imp.get_suffixes(): 214 if _type == imp.C_EXTENSION and filename.endswith(_sfx): 215 description = (_sfx, 'rb', imp.C_EXTENSION) 216 break 217 else: 218 description = ('', '', imp.PKG_DIRECTORY) 219 220 return (None, filename, description) 221 222 if hasattr(loader, 'path'): 223 if loader.path.endswith('.pyc') or loader.path.endswith('.pyo'): 224 fp = open(loader.path, 'rb') 225 description = ('.pyc', 'rb', imp.PY_COMPILED) 226 return (fp, loader.path, description) 227 228 229 if hasattr(loader, 'get_source'): 230 source = loader.get_source(name) 231 fp = StringIO(source) 232 co = None 233 234 else: 235 source = None 236 237 if source is None: 238 if hasattr(loader, 'get_code'): 239 co = loader.get_code(name) 240 fp = _code_to_file(co) 241 242 else: 243 fp = None 244 co = None 245 246 pathname = os.path.join(entry, *name.split('.')) 247 248 if isinstance(loader, zipimport.zipimporter): 249 # Check if this happens to be a wrapper module introduced by 250 # setuptools, if it is we return the actual extension. 251 zn = '/'.join(name.split('.')) 252 for _sfx, _mode, _type in imp.get_suffixes(): 253 if _type == imp.C_EXTENSION: 254 p = loader.prefix + zn + _sfx 255 if loader._files is None: 256 loader_files = zipimport._zip_directory_cache[loader.archive] 257 else: 258 loader_files = loader._files 259 260 if p in loader_files: 261 description = (_sfx, 'rb', imp.C_EXTENSION) 262 return (None, pathname + _sfx, description) 263 264 if hasattr(loader, 'is_package') and loader.is_package(name): 265 return (None, pathname, ('', '', imp.PKG_DIRECTORY)) 266 267 if co is None: 268 if hasattr(loader, 'path'): 269 filename = loader.path 270 elif hasattr(loader, 'get_filename'): 271 filename = loader.get_filename(name) 272 if source is not None: 273 if filename.endswith(".pyc") or filename.endswith(".pyo"): 274 filename = filename[:-1] 275 else: 276 filename = None 277 278 if filename is not None and (filename.endswith('.py') or filename.endswith('.pyw')): 279 return (fp, filename, ('.py', 'rU', imp.PY_SOURCE)) 280 else: 281 if fp is not None: 282 fp.close() 283 return (None, filename, (os.path.splitext(filename)[-1], 'rb', imp.C_EXTENSION)) 284 285 else: 286 if hasattr(loader, 'path'): 287 return (fp, loader.path, ('.pyc', 'rb', imp.PY_COMPILED)) 288 else: 289 return (fp, pathname + '.pyc', ('.pyc', 'rb', imp.PY_COMPILED)) 290 291 if namespace_path: 292 if fp is not None: 293 fp.close() 294 return (None, namespace_path[0], ('', namespace_path, imp.PKG_DIRECTORY)) 295 296 raise ImportError(name) 297 298def moduleInfoForPath(path): 299 for (ext, readmode, typ) in imp.get_suffixes(): 300 if path.endswith(ext): 301 return os.path.basename(path)[:-len(ext)], readmode, typ 302 return None 303 304# A Public interface 305import warnings 306def AddPackagePath(packagename, path): 307 warnings.warn("Use addPackagePath instead of AddPackagePath", 308 DeprecationWarning) 309 310 addPackagePath(packagename, path) 311 312def addPackagePath(packagename, path): 313 paths = _packagePathMap.get(packagename, []) 314 paths.append(path) 315 _packagePathMap[packagename] = paths 316 317_replacePackageMap = {} 318 319# This ReplacePackage mechanism allows modulefinder to work around the 320# way the _xmlplus package injects itself under the name "xml" into 321# sys.modules at runtime by calling ReplacePackage("_xmlplus", "xml") 322# before running ModuleGraph. 323def ReplacePackage(oldname, newname): 324 warnings.warn("use replacePackage instead of ReplacePackage", 325 DeprecationWarning) 326 replacePackage(oldname, newname) 327 328def replacePackage(oldname, newname): 329 _replacePackageMap[oldname] = newname 330 331 332class DependencyInfo (namedtuple("DependencyInfo", ["conditional", "function", "tryexcept", "fromlist"])): 333 __slots__ = () 334 335 def _merged(self, other): 336 if (not self.conditional and not self.function and not self.tryexcept) \ 337 or (not other.conditional and not other.function and not other.tryexcept): 338 return DependencyInfo(conditional=False, function=False, tryexcept=False, fromlist=self.fromlist and other.fromlist) 339 340 else: 341 return DependencyInfo( 342 conditional=self.conditional or other.conditional, 343 function=self.function or other.function, 344 tryexcept=self.tryexcept or other.tryexcept, 345 fromlist=self.fromlist and other.fromlist) 346 347 348class Node(object): 349 def __init__(self, identifier): 350 self.debug = 0 351 self.graphident = identifier 352 self.identifier = identifier 353 self._namespace = {} 354 self.filename = None 355 self.packagepath = None 356 self.code = None 357 # The set of global names that are assigned to in the module. 358 # This includes those names imported through starimports of 359 # Python modules. 360 self.globalnames = set() 361 # The set of starimports this module did that could not be 362 # resolved, ie. a starimport from a non-Python module. 363 self.starimports = set() 364 365 def __contains__(self, name): 366 return name in self._namespace 367 368 def __getitem__(self, name): 369 return self._namespace[name] 370 371 def __setitem__(self, name, value): 372 self._namespace[name] = value 373 374 def get(self, *args): 375 return self._namespace.get(*args) 376 377 def __cmp__(self, other): 378 try: 379 otherIdent = getattr(other, 'graphident') 380 except AttributeError: 381 return NotImplemented 382 383 return cmp(self.graphident, otherIdent) 384 385 def __eq__(self, other): 386 try: 387 otherIdent = getattr(other, 'graphident') 388 except AttributeError: 389 return False 390 391 return self.graphident == otherIdent 392 393 def __ne__(self, other): 394 try: 395 otherIdent = getattr(other, 'graphident') 396 except AttributeError: 397 return True 398 399 return self.graphident != otherIdent 400 401 def __lt__(self, other): 402 try: 403 otherIdent = getattr(other, 'graphident') 404 except AttributeError: 405 return NotImplemented 406 407 return self.graphident < otherIdent 408 409 def __le__(self, other): 410 try: 411 otherIdent = getattr(other, 'graphident') 412 except AttributeError: 413 return NotImplemented 414 415 return self.graphident <= otherIdent 416 417 def __gt__(self, other): 418 try: 419 otherIdent = getattr(other, 'graphident') 420 except AttributeError: 421 return NotImplemented 422 423 return self.graphident > otherIdent 424 425 def __ge__(self, other): 426 try: 427 otherIdent = getattr(other, 'graphident') 428 except AttributeError: 429 return NotImplemented 430 431 return self.graphident >= otherIdent 432 433 434 def __hash__(self): 435 return hash(self.graphident) 436 437 def infoTuple(self): 438 return (self.identifier,) 439 440 def __repr__(self): 441 return '%s%r' % (type(self).__name__, self.infoTuple()) 442 443class Alias(str): 444 pass 445 446class AliasNode(Node): 447 def __init__(self, name, node): 448 super(AliasNode, self).__init__(name) 449 for k in 'identifier', 'packagepath', '_namespace', 'globalnames', 'starimports': 450 setattr(self, k, getattr(node, k, None)) 451 452 def infoTuple(self): 453 return (self.graphident, self.identifier) 454 455class BadModule(Node): 456 pass 457 458class ExcludedModule(BadModule): 459 pass 460 461class MissingModule(BadModule): 462 pass 463 464class Script(Node): 465 def __init__(self, filename): 466 super(Script, self).__init__(filename) 467 self.filename = filename 468 469 def infoTuple(self): 470 return (self.filename,) 471 472class BaseModule(Node): 473 def __init__(self, name, filename=None, path=None): 474 super(BaseModule, self).__init__(name) 475 self.filename = filename 476 self.packagepath = path 477 478 def infoTuple(self): 479 return tuple(filter(None, (self.identifier, self.filename, self.packagepath))) 480 481class BuiltinModule(BaseModule): 482 pass 483 484class SourceModule(BaseModule): 485 pass 486 487class InvalidSourceModule(SourceModule): 488 pass 489 490class CompiledModule(BaseModule): 491 pass 492 493class InvalidCompiledModule(BaseModule): 494 pass 495 496class Package(BaseModule): 497 pass 498 499class NamespacePackage(Package): 500 pass 501 502class Extension(BaseModule): 503 pass 504 505class FlatPackage(BaseModule): # nocoverage 506 def __init__(self, *args, **kwds): 507 warnings.warn("This class will be removed in a future version of modulegraph", 508 DeprecationWarning) 509 super(FlatPackage, *args, **kwds) 510 511class ArchiveModule(BaseModule): # nocoverage 512 def __init__(self, *args, **kwds): 513 warnings.warn("This class will be removed in a future version of modulegraph", 514 DeprecationWarning) 515 super(FlatPackage, *args, **kwds) 516 517# HTML templates for ModuleGraph generator 518header = """\ 519<html> 520 <head> 521 <title>%(TITLE)s</title> 522 <style> 523 .node { margin:1em 0; } 524 </style> 525 </head> 526 <body> 527 <h1>%(TITLE)s</h1>""" 528entry = """ 529<div class="node"> 530 <a name="%(NAME)s" /> 531 %(CONTENT)s 532</div>""" 533contpl = """<tt>%(NAME)s</tt> %(TYPE)s""" 534contpl_linked = """\ 535<a target="code" href="%(URL)s" type="text/plain"><tt>%(NAME)s</tt></a>""" 536imports = """\ 537 <div class="import"> 538%(HEAD)s: 539 %(LINKS)s 540 </div> 541""" 542footer = """ 543 </body> 544</html>""" 545 546def _ast_names(names): 547 result = [] 548 for nm in names: 549 if isinstance(nm, ast.alias): 550 result.append(nm.name) 551 else: 552 result.append(nm) 553 return result 554 555 556if sys.version_info[0] == 2: 557 DEFAULT_IMPORT_LEVEL= -1 558else: 559 DEFAULT_IMPORT_LEVEL= 0 560 561class _Visitor (ast.NodeVisitor): 562 def __init__(self, graph, module): 563 self._graph = graph 564 self._module = module 565 self._level = DEFAULT_IMPORT_LEVEL 566 self._in_if = [False] 567 self._in_def = [False] 568 self._in_tryexcept = [False] 569 570 @property 571 def in_if(self): 572 return self._in_if[-1] 573 574 @property 575 def in_def(self): 576 return self._in_def[-1] 577 578 @property 579 def in_tryexcept(self): 580 return self._in_tryexcept[-1] 581 582 def _process_import(self, name, fromlist, level): 583 584 if sys.version_info[0] == 2: 585 if name == '__future__' and 'absolute_import' in (fromlist or ()): 586 self._level = 0 587 588 have_star = False 589 if fromlist is not None: 590 fromlist = set(fromlist) 591 if '*' in fromlist: 592 fromlist.remove('*') 593 have_star = True 594 595 imported_module = self._graph._safe_import_hook(name, 596 self._module, fromlist, level, attr=DependencyInfo( 597 conditional=self.in_if, 598 tryexcept=self.in_tryexcept, 599 function=self.in_def, 600 fromlist=False, 601 ))[0] 602 if have_star: 603 self._module.globalnames.update(imported_module.globalnames) 604 self._module.starimports.update(imported_module.starimports) 605 if imported_module.code is None: 606 self._module.starimports.add(name) 607 608 609 def visit_Import(self, node): 610 for nm in _ast_names(node.names): 611 self._process_import(nm, None, self._level) 612 613 def visit_ImportFrom(self, node): 614 level = node.level if node.level != 0 else self._level 615 self._process_import(node.module or '', _ast_names(node.names), level) 616 617 def visit_If(self, node): 618 self._in_if.append(True) 619 self.generic_visit(node) 620 self._in_if.pop() 621 622 def visit_FunctionDef(self, node): 623 self._in_def.append(True) 624 self.generic_visit(node) 625 self._in_def.pop() 626 627 def visit_Try(self, node): 628 self._in_tryexcept.append(True) 629 self.generic_visit(node) 630 self._in_tryexcept.pop() 631 632 def visit_ExceptHandler(self, node): 633 self._in_tryexcept.append(True) 634 self.generic_visit(node) 635 self._in_tryexcept.pop() 636 637 def visit_TryExcept(self, node): 638 self._in_tryexcept.append(True) 639 self.generic_visit(node) 640 self._in_tryexcept.pop() 641 642 def visit_ExceptHandler(self, node): 643 self._in_tryexcept.append(True) 644 self.generic_visit(node) 645 self._in_tryexcept.pop() 646 647 def visit_Expression(self, node): 648 # Expression node's cannot contain import statements or 649 # other nodes that are relevant for us. 650 pass 651 652 # Expression isn't actually used as such in AST trees, 653 # therefore define visitors for all kinds of expression nodes. 654 visit_BoolOp = visit_Expression 655 visit_BinOp = visit_Expression 656 visit_UnaryOp = visit_Expression 657 visit_Lambda = visit_Expression 658 visit_IfExp = visit_Expression 659 visit_Dict = visit_Expression 660 visit_Set = visit_Expression 661 visit_ListComp = visit_Expression 662 visit_SetComp = visit_Expression 663 visit_ListComp = visit_Expression 664 visit_GeneratorExp = visit_Expression 665 visit_Compare = visit_Expression 666 visit_Yield = visit_Expression 667 visit_YieldFrom = visit_Expression 668 visit_Await = visit_Expression 669 visit_Call = visit_Expression 670 671 672 673class ModuleGraph(ObjectGraph): 674 def __init__(self, path=None, excludes=(), replace_paths=(), implies=(), graph=None, debug=0): 675 super(ModuleGraph, self).__init__(graph=graph, debug=debug) 676 if path is None: 677 path = sys.path 678 self.path = path 679 self.lazynodes = {} 680 # excludes is stronger than implies 681 self.lazynodes.update(dict(implies)) 682 for m in excludes: 683 self.lazynodes[m] = None 684 self.replace_paths = replace_paths 685 686 self.nspackages = self._calc_setuptools_nspackages() 687 688 def _calc_setuptools_nspackages(self): 689 # Setuptools has some magic handling for namespace 690 # packages when using 'install --single-version-externally-managed' 691 # (used by system packagers and also by pip) 692 # 693 # When this option is used namespace packages are writting to 694 # disk *without* an __init__.py file, which means the regular 695 # import machinery will not find them. 696 # 697 # We therefore explicitly look for the hack used by 698 # setuptools to get this kind of namespace packages to work. 699 700 pkgmap = {} 701 702 try: 703 from pkgutil import ImpImporter 704 except ImportError: 705 try: 706 from _pkgutil import ImpImporter 707 except ImportError: 708 ImpImporter = pkg_resources.ImpWrapper 709 710 if sys.version_info[:2] >= (3,3): 711 import importlib.machinery 712 ImpImporter = importlib.machinery.FileFinder 713 714 for entry in self.path: 715 importer = pkg_resources.get_importer(entry) 716 717 if isinstance(importer, ImpImporter): 718 try: 719 ldir = os.listdir(entry) 720 except os.error: 721 continue 722 723 for fn in ldir: 724 if fn.endswith('-nspkg.pth'): 725 fp = open(os.path.join(entry, fn), 'rU') 726 try: 727 for ln in fp: 728 for pfx in _SETUPTOOLS_NAMESPACEPKG_PTHs: 729 if ln.startswith(pfx): 730 try: 731 start = len(pfx)-2 732 stop = ln.index(')', start)+1 733 except ValueError: 734 continue 735 736 pkg = _eval_str_tuple(ln[start:stop]) 737 identifier = ".".join(pkg) 738 subdir = os.path.join(entry, *pkg) 739 if os.path.exists(os.path.join(subdir, '__init__.py')): 740 # There is a real __init__.py, ignore the setuptools hack 741 continue 742 743 if identifier in pkgmap: 744 pkgmap[identifier].append(subdir) 745 else: 746 pkgmap[identifier] = [subdir] 747 break 748 finally: 749 fp.close() 750 751 return pkgmap 752 753 def implyNodeReference(self, node, other, edge_data=None): 754 """ 755 Imply that one node depends on another. 756 other may be a module name or another node. 757 758 For use by extension modules and tricky import code 759 """ 760 if isinstance(other, Node): 761 self._updateReference(node, other, edge_data) 762 763 else: 764 if isinstance(other, tuple): 765 raise ValueError(other) 766 767 others = self._safe_import_hook(other, node, None) 768 for other in others: 769 self._updateReference(node, other, edge_data) 770 771 772 def getReferences(self, fromnode): 773 """ 774 Yield all nodes that 'fromnode' dependes on (that is, 775 all modules that 'fromnode' imports. 776 """ 777 node = self.findNode(fromnode) 778 out_edges, _ = self.get_edges(node) 779 return out_edges 780 781 def getReferers(self, tonode, collapse_missing_modules=True): 782 node = self.findNode(tonode) 783 _, in_edges = self.get_edges(node) 784 785 if collapse_missing_modules: 786 for n in in_edges: 787 if isinstance(n, MissingModule): 788 for n in self.getReferers(n, False): 789 yield n 790 791 else: 792 yield n 793 794 else: 795 for n in in_edges: 796 yield n 797 798 def hasEdge(self, fromnode, tonode): 799 """ Return True iff there is an edge from 'fromnode' to 'tonode' """ 800 fromnode = self.findNode(fromnode) 801 tonode = self.findNode(tonode) 802 803 return self.graph.edge_by_node(fromnode, tonode) is not None 804 805 806 def foldReferences(self, packagenode): 807 """ 808 Create edges to/from 'packagenode' based on the 809 edges to/from modules in package. The module nodes 810 are then hidden. 811 """ 812 pkg = self.findNode(packagenode) 813 814 for n in self.nodes(): 815 if not n.identifier.startswith(pkg.identifier + '.'): 816 continue 817 818 iter_out, iter_inc = n.get_edges() 819 for other in iter_out: 820 if other.identifier.startswith(pkg.identifier + '.'): 821 continue 822 823 if not self.hasEdge(pkg, other): 824 # Ignore circular dependencies 825 self._updateReference(pkg, other, 'pkg-internal-import') 826 827 for other in iter_in: 828 if other.identifier.startswith(pkg.identifier + '.'): 829 # Ignore circular dependencies 830 continue 831 832 if not self.hasEdge(other, pkg): 833 self._updateReference(other, pkg, 'pkg-import') 834 835 self.graph.hide_node(n) 836 837 # TODO: unfoldReferences(pkg) that restore the submodule nodes and 838 # removes 'pkg-import' and 'pkg-internal-import' edges. Care should 839 # be taken to ensure that references are correct if multiple packages 840 # are folded and then one of them in unfolded 841 842 843 def _updateReference(self, fromnode, tonode, edge_data): 844 try: 845 ed = self.edgeData(fromnode, tonode) 846 except (KeyError, GraphError): # XXX: Why 'GraphError' 847 return self.createReference(fromnode, tonode, edge_data) 848 849 if not (isinstance(ed, DependencyInfo) and isinstance(edge_data, DependencyInfo)): 850 self.updateEdgeData(fromnode, tonode, edge_data) 851 else: 852 self.updateEdgeData(fromnode, tonode, ed._merged(edge_data)) 853 854 855 def createReference(self, fromnode, tonode, edge_data='direct'): 856 """ 857 Create a reference from fromnode to tonode 858 """ 859 return super(ModuleGraph, self).createReference(fromnode, tonode, edge_data=edge_data) 860 861 def findNode(self, name): 862 """ 863 Find a node by identifier. If a node by that identifier exists, 864 it will be returned. 865 866 If a lazy node exists by that identifier with no dependencies (excluded), 867 it will be instantiated and returned. 868 869 If a lazy node exists by that identifier with dependencies, it and its 870 dependencies will be instantiated and scanned for additional dependencies. 871 """ 872 data = super(ModuleGraph, self).findNode(name) 873 if data is not None: 874 return data 875 if name in self.lazynodes: 876 deps = self.lazynodes.pop(name) 877 if deps is None: 878 # excluded module 879 m = self.createNode(ExcludedModule, name) 880 elif isinstance(deps, Alias): 881 other = self._safe_import_hook(deps, None, None).pop() 882 m = self.createNode(AliasNode, name, other) 883 self.implyNodeReference(m, other) 884 885 else: 886 m = self._safe_import_hook(name, None, None).pop() 887 for dep in deps: 888 self.implyNodeReference(m, dep) 889 return m 890 891 if name in self.nspackages: 892 # name is a --single-version-externally-managed 893 # namespace package (setuptools/distribute) 894 pathnames = self.nspackages.pop(name) 895 m = self.createNode(NamespacePackage, name) 896 897 # FIXME: The filename must be set to a string to ensure that py2app 898 # works, it is not clear yet why that is. Setting to None would be 899 # cleaner. 900 m.filename = '-' 901 m.packagepath = _namespace_package_path(name, pathnames, self.path) 902 903 # As per comment at top of file, simulate runtime packagepath additions. 904 m.packagepath = m.packagepath + _packagePathMap.get(name, []) 905 return m 906 907 return None 908 909 def run_script(self, pathname, caller=None): 910 """ 911 Create a node by path (not module name). It is expected to be a Python 912 source file, and will be scanned for dependencies. 913 """ 914 self.msg(2, "run_script", pathname) 915 pathname = os.path.realpath(pathname) 916 m = self.findNode(pathname) 917 if m is not None: 918 return m 919 920 if sys.version_info[0] != 2: 921 with open(pathname, 'rb') as fp: 922 encoding = util.guess_encoding(fp) 923 924 with open(pathname, _READ_MODE, encoding=encoding) as fp: 925 contents = fp.read() + '\n' 926 927 else: 928 with open(pathname, _READ_MODE) as fp: 929 contents = fp.read() + '\n' 930 931 co = compile(contents, pathname, 'exec', ast.PyCF_ONLY_AST, True) 932 m = self.createNode(Script, pathname) 933 self._updateReference(caller, m, None) 934 self._scan_code(co, m) 935 m.code = compile(co, pathname, 'exec', 0, True) 936 if self.replace_paths: 937 m.code = self._replace_paths_in_code(m.code) 938 return m 939 940 def import_hook(self, name, caller=None, fromlist=None, level=DEFAULT_IMPORT_LEVEL, attr=None): 941 """ 942 Import a module 943 944 Return the set of modules that are imported 945 """ 946 self.msg(3, "import_hook", name, caller, fromlist, level) 947 parent = self._determine_parent(caller) 948 q, tail = self._find_head_package(parent, name, level) 949 m = self._load_tail(q, tail) 950 modules = [m] 951 if fromlist and m.packagepath: 952 for s in self._ensure_fromlist(m, fromlist): 953 if s not in modules: 954 modules.append(s) 955 for m in modules: 956 self._updateReference(caller, m, edge_data=attr) 957 return modules 958 959 def _determine_parent(self, caller): 960 """ 961 Determine the package containing a node 962 """ 963 self.msgin(4, "determine_parent", caller) 964 parent = None 965 if caller: 966 pname = caller.identifier 967 968 if isinstance(caller, Package): 969 parent = caller 970 971 elif '.' in pname: 972 pname = pname[:pname.rfind('.')] 973 parent = self.findNode(pname) 974 975 elif caller.packagepath: 976 # XXX: I have no idea why this line 977 # is necessary. 978 parent = self.findNode(pname) 979 980 981 self.msgout(4, "determine_parent ->", parent) 982 return parent 983 984 def _find_head_package(self, parent, name, level=DEFAULT_IMPORT_LEVEL): 985 """ 986 Given a calling parent package and an import name determine the containing 987 package for the name 988 """ 989 self.msgin(4, "find_head_package", parent, name, level) 990 if '.' in name: 991 head, tail = name.split('.', 1) 992 else: 993 head, tail = name, '' 994 995 if level == -1: 996 if parent: 997 qname = parent.identifier + '.' + head 998 else: 999 qname = head 1000 1001 elif level == 0: 1002 qname = head 1003 1004 # Absolute import, ignore the parent 1005 parent = None 1006 1007 else: 1008 if parent is None: 1009 self.msg(2, "Relative import outside of package") 1010 raise ImportError("Relative import outside of package (name=%r, parent=%r, level=%r)"%(name, parent, level)) 1011 1012 for i in range(level-1): 1013 if '.' not in parent.identifier: 1014 self.msg(2, "Relative import outside of package") 1015 raise ImportError("Relative import outside of package (name=%r, parent=%r, level=%r)"%(name, parent, level)) 1016 1017 p_fqdn = parent.identifier.rsplit('.', 1)[0] 1018 new_parent = self.findNode(p_fqdn) 1019 if new_parent is None: 1020 self.msg(2, "Relative import outside of package") 1021 raise ImportError("Relative import outside of package (name=%r, parent=%r, level=%r)"%(name, parent, level)) 1022 1023 assert new_parent is not parent, (new_parent, parent) 1024 parent = new_parent 1025 1026 if head: 1027 qname = parent.identifier + '.' + head 1028 else: 1029 qname = parent.identifier 1030 1031 1032 q = self._import_module(head, qname, parent) 1033 if q: 1034 self.msgout(4, "find_head_package ->", (q, tail)) 1035 return q, tail 1036 if parent: 1037 qname = head 1038 parent = None 1039 q = self._import_module(head, qname, parent) 1040 if q: 1041 self.msgout(4, "find_head_package ->", (q, tail)) 1042 return q, tail 1043 self.msgout(4, "raise ImportError: No module named", qname) 1044 raise ImportError("No module named " + qname) 1045 1046 def _load_tail(self, mod, tail): 1047 self.msgin(4, "load_tail", mod, tail) 1048 result = mod 1049 while tail: 1050 i = tail.find('.') 1051 if i < 0: i = len(tail) 1052 head, tail = tail[:i], tail[i+1:] 1053 mname = "%s.%s" % (result.identifier, head) 1054 result = self._import_module(head, mname, result) 1055 if result is None: 1056 result = self.createNode(MissingModule, mname) 1057 #self.msgout(4, "raise ImportError: No module named", mname) 1058 #raise ImportError("No module named " + mname) 1059 self.msgout(4, "load_tail ->", result) 1060 return result 1061 1062 def _ensure_fromlist(self, m, fromlist): 1063 fromlist = set(fromlist) 1064 self.msg(4, "ensure_fromlist", m, fromlist) 1065 if '*' in fromlist: 1066 fromlist.update(self._find_all_submodules(m)) 1067 fromlist.remove('*') 1068 for sub in fromlist: 1069 submod = m.get(sub) 1070 if submod is None: 1071 if sub in m.globalnames: 1072 # Name is a global in the module 1073 continue 1074 # XXX: ^^^ need something simular for names imported 1075 # by 'm'. 1076 1077 fullname = m.identifier + '.' + sub 1078 submod = self._import_module(sub, fullname, m) 1079 if submod is None: 1080 raise ImportError("No module named " + fullname) 1081 yield submod 1082 1083 def _find_all_submodules(self, m): 1084 if not m.packagepath: 1085 return 1086 # 'suffixes' used to be a list hardcoded to [".py", ".pyc", ".pyo"]. 1087 # But we must also collect Python extension modules - although 1088 # we cannot separate normal dlls from Python extensions. 1089 suffixes = [triple[0] for triple in imp.get_suffixes()] 1090 for path in m.packagepath: 1091 try: 1092 names = zipio.listdir(path) 1093 except (os.error, IOError): 1094 self.msg(2, "can't list directory", path) 1095 continue 1096 for info in (moduleInfoForPath(p) for p in names): 1097 if info is None: continue 1098 if info[0] != '__init__': 1099 yield info[0] 1100 1101 def _import_module(self, partname, fqname, parent): 1102 # XXX: Review me for use with absolute imports. 1103 self.msgin(3, "import_module", partname, fqname, parent) 1104 m = self.findNode(fqname) 1105 if m is not None: 1106 self.msgout(3, "import_module ->", m) 1107 if parent: 1108 self._updateReference(m, parent, edge_data=DependencyInfo( 1109 conditional=False, fromlist=False, function=False, tryexcept=False 1110 )) 1111 return m 1112 1113 if parent and parent.packagepath is None: 1114 self.msgout(3, "import_module -> None") 1115 return None 1116 1117 try: 1118 searchpath = None 1119 if parent is not None and parent.packagepath: 1120 searchpath = parent.packagepath 1121 1122 fp, pathname, stuff = self._find_module(partname, 1123 searchpath, parent) 1124 1125 except ImportError: 1126 self.msgout(3, "import_module ->", None) 1127 return None 1128 1129 try: 1130 m = self._load_module(fqname, fp, pathname, stuff) 1131 1132 finally: 1133 if fp is not None: 1134 fp.close() 1135 1136 if parent: 1137 self.msgout(4, "create reference", m, "->", parent) 1138 self._updateReference(m, parent, edge_data=DependencyInfo( 1139 conditional=False, fromlist=False, function=False, tryexcept=False 1140 )) 1141 parent[partname] = m 1142 1143 self.msgout(3, "import_module ->", m) 1144 return m 1145 1146 def _load_module(self, fqname, fp, pathname, info): 1147 suffix, mode, typ = info 1148 self.msgin(2, "load_module", fqname, fp and "fp", pathname) 1149 1150 if typ == imp.PKG_DIRECTORY: 1151 if isinstance(mode, (list, tuple)): 1152 packagepath = mode 1153 else: 1154 packagepath = [] 1155 1156 m = self._load_package(fqname, pathname, packagepath) 1157 self.msgout(2, "load_module ->", m) 1158 return m 1159 1160 if typ == imp.PY_SOURCE: 1161 contents = fp.read() 1162 if isinstance(contents, bytes): 1163 contents += b'\n' 1164 else: 1165 contents += '\n' 1166 1167 try: 1168 co = compile(contents, pathname, 'exec', ast.PyCF_ONLY_AST, True) 1169 #co = compile(contents, pathname, 'exec', 0, True) 1170 except SyntaxError: 1171 co = None 1172 cls = InvalidSourceModule 1173 1174 else: 1175 cls = SourceModule 1176 1177 elif typ == imp.PY_COMPILED: 1178 if fp.read(4) != imp.get_magic(): 1179 self.msgout(2, "raise ImportError: Bad magic number", pathname) 1180 co = None 1181 cls = InvalidCompiledModule 1182 1183 else: 1184 fp.read(4) 1185 try: 1186 co = marshal.loads(fp.read()) 1187 cls = CompiledModule 1188 except Exception: 1189 co = None 1190 cls = InvalidCompiledModule 1191 1192 elif typ == imp.C_BUILTIN: 1193 cls = BuiltinModule 1194 co = None 1195 1196 else: 1197 cls = Extension 1198 co = None 1199 1200 m = self.createNode(cls, fqname) 1201 m.filename = pathname 1202 if co is not None: 1203 self._scan_code(co, m) 1204 1205 if isinstance(co, ast.AST): 1206 co = compile(co, pathname, 'exec', 0, True) 1207 if self.replace_paths: 1208 co = self._replace_paths_in_code(co) 1209 m.code = co 1210 1211 1212 self.msgout(2, "load_module ->", m) 1213 return m 1214 1215 def _safe_import_hook(self, name, caller, fromlist, level=DEFAULT_IMPORT_LEVEL, attr=None): 1216 # wrapper for self.import_hook() that won't raise ImportError 1217 try: 1218 mods = self.import_hook(name, caller, level=level, attr=attr) 1219 except ImportError as msg: 1220 self.msg(2, "ImportError:", str(msg)) 1221 m = self.createNode(MissingModule, _path_from_importerror(msg, name)) 1222 self._updateReference(caller, m, edge_data=attr) 1223 1224 else: 1225 assert len(mods) == 1 1226 m = list(mods)[0] 1227 1228 subs = [m] 1229 if isinstance(attr, DependencyInfo): 1230 attr = attr._replace(fromlist=True) 1231 for sub in (fromlist or ()): 1232 # If this name is in the module namespace already, 1233 # then add the entry to the list of substitutions 1234 if sub in m: 1235 sm = m[sub] 1236 if sm is not None: 1237 if sm not in subs: 1238 self._updateReference(caller, sm, edge_data=attr) 1239 subs.append(sm) 1240 continue 1241 1242 elif sub in m.globalnames: 1243 # Global variable in the module, ignore 1244 continue 1245 1246 1247 # See if we can load it 1248 # fullname = name + '.' + sub 1249 fullname = m.identifier + '.' + sub 1250 #else: 1251 # print("XXX", repr(name), repr(sub), repr(caller), repr(m)) 1252 sm = self.findNode(fullname) 1253 if sm is None: 1254 try: 1255 sm = self.import_hook(name, caller, fromlist=[sub], level=level, attr=attr) 1256 except ImportError as msg: 1257 self.msg(2, "ImportError:", str(msg)) 1258 #sm = self.createNode(MissingModule, _path_from_importerror(msg, fullname)) 1259 sm = self.createNode(MissingModule, fullname) 1260 else: 1261 sm = self.findNode(fullname) 1262 if sm is None: 1263 sm = self.createNode(MissingModule, fullname) 1264 1265 m[sub] = sm 1266 if sm is not None: 1267 self._updateReference(m, sm, edge_data=attr) 1268 self._updateReference(caller, sm, edge_data=attr) 1269 if sm not in subs: 1270 subs.append(sm) 1271 return subs 1272 1273 def _scan_code(self, co, m): 1274 if isinstance(co, ast.AST): 1275 #return self._scan_bytecode(compile(co, '-', 'exec', 0, True), m) 1276 self._scan_ast(co, m) 1277 self._scan_bytecode_stores( 1278 compile(co, '-', 'exec', 0, True), m) 1279 1280 else: 1281 self._scan_bytecode(co, m) 1282 1283 def _scan_ast(self, co, m): 1284 visitor = _Visitor(self, m) 1285 visitor.visit(co) 1286 1287 def _scan_bytecode_stores(self, co, m, 1288 STORE_NAME=_Bchr(dis.opname.index('STORE_NAME')), 1289 STORE_GLOBAL=_Bchr(dis.opname.index('STORE_GLOBAL')), 1290 HAVE_ARGUMENT=_Bchr(dis.HAVE_ARGUMENT), 1291 unpack=struct.unpack): 1292 1293 extended_import = bool(sys.version_info[:2] >= (2,5)) 1294 1295 code = co.co_code 1296 constants = co.co_consts 1297 n = len(code) 1298 i = 0 1299 1300 while i < n: 1301 c = code[i] 1302 i += 1 1303 if c >= HAVE_ARGUMENT: 1304 i = i+2 1305 1306 if c == STORE_NAME or c == STORE_GLOBAL: 1307 # keep track of all global names that are assigned to 1308 oparg = unpack('<H', code[i - 2:i])[0] 1309 name = co.co_names[oparg] 1310 m.globalnames.add(name) 1311 1312 cotype = type(co) 1313 for c in constants: 1314 if isinstance(c, cotype): 1315 self._scan_bytecode_stores(c, m) 1316 1317 def _scan_bytecode(self, co, m, 1318 HAVE_ARGUMENT=_Bchr(dis.HAVE_ARGUMENT), 1319 LOAD_CONST=_Bchr(dis.opname.index('LOAD_CONST')), 1320 IMPORT_NAME=_Bchr(dis.opname.index('IMPORT_NAME')), 1321 IMPORT_FROM=_Bchr(dis.opname.index('IMPORT_FROM')), 1322 STORE_NAME=_Bchr(dis.opname.index('STORE_NAME')), 1323 STORE_GLOBAL=_Bchr(dis.opname.index('STORE_GLOBAL')), 1324 unpack=struct.unpack): 1325 1326 # Python >=2.5: LOAD_CONST flags, LOAD_CONST names, IMPORT_NAME name 1327 # Python < 2.5: LOAD_CONST names, IMPORT_NAME name 1328 extended_import = bool(sys.version_info[:2] >= (2,5)) 1329 1330 code = co.co_code 1331 constants = co.co_consts 1332 n = len(code) 1333 i = 0 1334 1335 level = None 1336 fromlist = None 1337 1338 while i < n: 1339 c = code[i] 1340 i += 1 1341 if c >= HAVE_ARGUMENT: 1342 i = i+2 1343 1344 if c == IMPORT_NAME: 1345 if extended_import: 1346 assert code[i-9] == LOAD_CONST 1347 assert code[i-6] == LOAD_CONST 1348 arg1, arg2 = unpack('<xHxH', code[i-9:i-3]) 1349 level = co.co_consts[arg1] 1350 fromlist = co.co_consts[arg2] 1351 else: 1352 assert code[-6] == LOAD_CONST 1353 arg1, = unpack('<xH', code[i-6:i-3]) 1354 level = -1 1355 fromlist = co.co_consts[arg1] 1356 1357 assert fromlist is None or type(fromlist) is tuple 1358 oparg, = unpack('<H', code[i - 2:i]) 1359 name = co.co_names[oparg] 1360 have_star = False 1361 if fromlist is not None: 1362 fromlist = set(fromlist) 1363 if '*' in fromlist: 1364 fromlist.remove('*') 1365 have_star = True 1366 1367 #self.msgin(2, "Before import hook", repr(name), repr(m), repr(fromlist), repr(level)) 1368 1369 imported_module = self._safe_import_hook(name, m, fromlist, level)[0] 1370 1371 if have_star: 1372 m.globalnames.update(imported_module.globalnames) 1373 m.starimports.update(imported_module.starimports) 1374 if imported_module.code is None: 1375 m.starimports.add(name) 1376 1377 elif c == STORE_NAME or c == STORE_GLOBAL: 1378 # keep track of all global names that are assigned to 1379 oparg = unpack('<H', code[i - 2:i])[0] 1380 name = co.co_names[oparg] 1381 m.globalnames.add(name) 1382 1383 cotype = type(co) 1384 for c in constants: 1385 if isinstance(c, cotype): 1386 self._scan_bytecode(c, m) 1387 1388 def _load_package(self, fqname, pathname, pkgpath): 1389 """ 1390 Called only when an imp.PACKAGE_DIRECTORY is found 1391 """ 1392 self.msgin(2, "load_package", fqname, pathname, pkgpath) 1393 newname = _replacePackageMap.get(fqname) 1394 if newname: 1395 fqname = newname 1396 1397 ns_pkgpath = _namespace_package_path(fqname, pkgpath or [], self.path) 1398 if ns_pkgpath or pkgpath: 1399 # this is a namespace package 1400 m = self.createNode(NamespacePackage, fqname) 1401 m.filename = '-' 1402 m.packagepath = ns_pkgpath 1403 else: 1404 m = self.createNode(Package, fqname) 1405 m.filename = pathname 1406 m.packagepath = [pathname] + ns_pkgpath 1407 1408 # As per comment at top of file, simulate runtime packagepath additions. 1409 m.packagepath = m.packagepath + _packagePathMap.get(fqname, []) 1410 1411 1412 1413 try: 1414 self.msg(2, "find __init__ for %s"%(m.packagepath,)) 1415 fp, buf, stuff = self._find_module("__init__", m.packagepath, parent=m) 1416 except ImportError: 1417 pass 1418 1419 else: 1420 try: 1421 self.msg(2, "load __init__ for %s"%(m.packagepath,)) 1422 self._load_module(fqname, fp, buf, stuff) 1423 finally: 1424 if fp is not None: 1425 fp.close() 1426 self.msgout(2, "load_package ->", m) 1427 return m 1428 1429 def _find_module(self, name, path, parent=None): 1430 if parent is not None: 1431 # assert path is not None 1432 fullname = parent.identifier + '.' + name 1433 else: 1434 fullname = name 1435 1436 node = self.findNode(fullname) 1437 if node is not None: 1438 self.msgout(3, "find_module -> already included?", node) 1439 raise ImportError(name) 1440 1441 if path is None: 1442 if name in sys.builtin_module_names: 1443 return (None, None, ("", "", imp.C_BUILTIN)) 1444 1445 path = self.path 1446 1447 fp, buf, stuff = find_module(name, path) 1448 try: 1449 if buf: 1450 buf = os.path.realpath(buf) 1451 1452 return (fp, buf, stuff) 1453 except: 1454 fp.close() 1455 raise 1456 1457 def create_xref(self, out=None): 1458 global header, footer, entry, contpl, contpl_linked, imports 1459 if out is None: 1460 out = sys.stdout 1461 scripts = [] 1462 mods = [] 1463 for mod in self.flatten(): 1464 name = os.path.basename(mod.identifier) 1465 if isinstance(mod, Script): 1466 scripts.append((name, mod)) 1467 else: 1468 mods.append((name, mod)) 1469 scripts.sort() 1470 mods.sort() 1471 scriptnames = [name for name, m in scripts] 1472 scripts.extend(mods) 1473 mods = scripts 1474 1475 title = "modulegraph cross reference for " + ', '.join(scriptnames) 1476 print(header % {"TITLE": title}, file=out) 1477 1478 def sorted_namelist(mods): 1479 lst = [os.path.basename(mod.identifier) for mod in mods if mod] 1480 lst.sort() 1481 return lst 1482 for name, m in mods: 1483 content = "" 1484 if isinstance(m, BuiltinModule): 1485 content = contpl % {"NAME": name, 1486 "TYPE": "<i>(builtin module)</i>"} 1487 elif isinstance(m, Extension): 1488 content = contpl % {"NAME": name,\ 1489 "TYPE": "<tt>%s</tt>" % m.filename} 1490 else: 1491 url = pathname2url(m.filename or "") 1492 content = contpl_linked % {"NAME": name, "URL": url} 1493 oute, ince = map(sorted_namelist, self.get_edges(m)) 1494 if oute: 1495 links = "" 1496 for n in oute: 1497 links += """ <a href="#%s">%s</a>\n""" % (n, n) 1498 content += imports % {"HEAD": "imports", "LINKS": links} 1499 if ince: 1500 links = "" 1501 for n in ince: 1502 links += """ <a href="#%s">%s</a>\n""" % (n, n) 1503 content += imports % {"HEAD": "imported by", "LINKS": links} 1504 print(entry % {"NAME": name,"CONTENT": content}, file=out) 1505 print(footer, file=out) 1506 1507 1508 def itergraphreport(self, name='G', flatpackages=()): 1509 # XXX: Can this be implemented using Dot()? 1510 nodes = map(self.graph.describe_node, self.graph.iterdfs(self)) 1511 describe_edge = self.graph.describe_edge 1512 edges = deque() 1513 packagenodes = set() 1514 packageidents = {} 1515 nodetoident = {} 1516 inpackages = {} 1517 mainedges = set() 1518 1519 # XXX - implement 1520 flatpackages = dict(flatpackages) 1521 1522 def nodevisitor(node, data, outgoing, incoming): 1523 if not isinstance(data, Node): 1524 return {'label': str(node)} 1525 #if isinstance(d, (ExcludedModule, MissingModule, BadModule)): 1526 # return None 1527 s = '<f0> ' + type(data).__name__ 1528 for i,v in enumerate(data.infoTuple()[:1], 1): 1529 s += '| <f%d> %s' % (i,v) 1530 return {'label':s, 'shape':'record'} 1531 1532 1533 def edgevisitor(edge, data, head, tail): 1534 # XXX: This method nonsense, the edge 1535 # data is never initialized. 1536 if data == 'orphan': 1537 return {'style':'dashed'} 1538 elif data == 'pkgref': 1539 return {'style':'dotted'} 1540 return {} 1541 1542 yield 'digraph %s {\n' % (name,) 1543 attr = dict(rankdir='LR', concentrate='true') 1544 cpatt = '%s="%s"' 1545 for item in attr.items(): 1546 yield '\t%s;\n' % (cpatt % item,) 1547 1548 # find all packages (subgraphs) 1549 for (node, data, outgoing, incoming) in nodes: 1550 nodetoident[node] = getattr(data, 'identifier', None) 1551 if isinstance(data, Package): 1552 packageidents[data.identifier] = node 1553 inpackages[node] = set([node]) 1554 packagenodes.add(node) 1555 1556 1557 # create sets for subgraph, write out descriptions 1558 for (node, data, outgoing, incoming) in nodes: 1559 # update edges 1560 for edge in (describe_edge(e) for e in outgoing): 1561 edges.append(edge) 1562 1563 # describe node 1564 yield '\t"%s" [%s];\n' % ( 1565 node, 1566 ','.join([ 1567 (cpatt % item) for item in 1568 nodevisitor(node, data, outgoing, incoming).items() 1569 ]), 1570 ) 1571 1572 inside = inpackages.get(node) 1573 if inside is None: 1574 inside = inpackages[node] = set() 1575 ident = nodetoident[node] 1576 if ident is None: 1577 continue 1578 pkgnode = packageidents.get(ident[:ident.rfind('.')]) 1579 if pkgnode is not None: 1580 inside.add(pkgnode) 1581 1582 1583 graph = [] 1584 subgraphs = {} 1585 for key in packagenodes: 1586 subgraphs[key] = [] 1587 1588 while edges: 1589 edge, data, head, tail = edges.popleft() 1590 if ((head, tail)) in mainedges: 1591 continue 1592 mainedges.add((head, tail)) 1593 tailpkgs = inpackages[tail] 1594 common = inpackages[head] & tailpkgs 1595 if not common and tailpkgs: 1596 usepkgs = sorted(tailpkgs) 1597 if len(usepkgs) != 1 or usepkgs[0] != tail: 1598 edges.append((edge, data, head, usepkgs[0])) 1599 edges.append((edge, 'pkgref', usepkgs[-1], tail)) 1600 continue 1601 if common: 1602 common = common.pop() 1603 if tail == common: 1604 edges.append((edge, data, tail, head)) 1605 elif head == common: 1606 subgraphs[common].append((edge, 'pkgref', head, tail)) 1607 else: 1608 edges.append((edge, data, common, head)) 1609 edges.append((edge, data, common, tail)) 1610 1611 else: 1612 graph.append((edge, data, head, tail)) 1613 1614 def do_graph(edges, tabs): 1615 edgestr = tabs + '"%s" -> "%s" [%s];\n' 1616 # describe edge 1617 for (edge, data, head, tail) in edges: 1618 attribs = edgevisitor(edge, data, head, tail) 1619 yield edgestr % ( 1620 head, 1621 tail, 1622 ','.join([(cpatt % item) for item in attribs.items()]), 1623 ) 1624 1625 for g, edges in subgraphs.items(): 1626 yield '\tsubgraph "cluster_%s" {\n' % (g,) 1627 yield '\t\tlabel="%s";\n' % (nodetoident[g],) 1628 for s in do_graph(edges, '\t\t'): 1629 yield s 1630 yield '\t}\n' 1631 1632 for s in do_graph(graph, '\t'): 1633 yield s 1634 1635 yield '}\n' 1636 1637 1638 def graphreport(self, fileobj=None, flatpackages=()): 1639 if fileobj is None: 1640 fileobj = sys.stdout 1641 fileobj.writelines(self.itergraphreport(flatpackages=flatpackages)) 1642 1643 def report(self): 1644 """Print a report to stdout, listing the found modules with their 1645 paths, as well as modules that are missing, or seem to be missing. 1646 """ 1647 print() 1648 print("%-15s %-25s %s" % ("Class", "Name", "File")) 1649 print("%-15s %-25s %s" % ("-----", "----", "----")) 1650 # Print modules found 1651 sorted = [(os.path.basename(mod.identifier), mod) for mod in self.flatten()] 1652 sorted.sort() 1653 for (name, m) in sorted: 1654 print("%-15s %-25s %s" % (type(m).__name__, name, m.filename or "")) 1655 1656 def _replace_paths_in_code(self, co): 1657 new_filename = original_filename = os.path.normpath(co.co_filename) 1658 for f, r in self.replace_paths: 1659 f = os.path.join(f, '') 1660 r = os.path.join(r, '') 1661 if original_filename.startswith(f): 1662 new_filename = r + original_filename[len(f):] 1663 break 1664 1665 else: 1666 return co 1667 1668 consts = list(co.co_consts) 1669 for i in range(len(consts)): 1670 if isinstance(consts[i], type(co)): 1671 consts[i] = self._replace_paths_in_code(consts[i]) 1672 1673 code_func = type(co) 1674 1675 if hasattr(co, 'co_kwonlyargcount'): 1676 return code_func(co.co_argcount, co.co_kwonlyargcount, co.co_nlocals, co.co_stacksize, 1677 co.co_flags, co.co_code, tuple(consts), co.co_names, 1678 co.co_varnames, new_filename, co.co_name, 1679 co.co_firstlineno, co.co_lnotab, 1680 co.co_freevars, co.co_cellvars) 1681 else: 1682 return code_func(co.co_argcount, co.co_nlocals, co.co_stacksize, 1683 co.co_flags, co.co_code, tuple(consts), co.co_names, 1684 co.co_varnames, new_filename, co.co_name, 1685 co.co_firstlineno, co.co_lnotab, 1686 co.co_freevars, co.co_cellvars) 1687