ATLAS Offline Software
apydep.py
Go to the documentation of this file.
1 #!/usr/bin/env python3
2 # Copyright (C) 2002-2024 CERN for the benefit of the ATLAS collaboration
3 #
4 # Created: Oct 2020, Frank Winklmeier
5 #
6 """
7 Extract Python dependencies between packages and create DOT graph.
8 Both `import` and `include` dependencies are considered.
9 """
10 
11 import ast
12 import sys
13 import os
14 import argparse
15 import pygraphviz
16 from collections import defaultdict
17 
18 class DependencyFinder(ast.NodeVisitor):
19  """Walk an AST collecting import/include statements."""
20 
21  def __init__(self):
22  self.imports = set()
23  self.includes = set()
24 
25  def visit_Import(self, node):
26  """import XYZ"""
27  self.imports.update(alias.name.split('.',1)[0] for alias in node.names)
28 
29  def visit_ImportFrom(self, node):
30  """from XYZ import ABC"""
31  if node.level==0: # ignore relative imports
32  self.imports.add(node.module.split('.',1)[0])
33 
34  def visit_Call(self, node):
35  """"include(XYZ/ABC.py)"""
36  if isinstance(node.func, ast.Name) and node.func.id=='include' and node.args:
37  if isinstance(node.args[0], ast.Str):
38  self.includes.add(node.args[0].s.split('/',1)[0])
39 
40 
41 def get_dependencies(filename, print_error=False):
42  """Get all the imports/includes in a file."""
43 
44  try:
45  tree = ast.parse(open(filename,'rb').read(), filename=filename)
46  except Exception as e:
47  if print_error:
48  print(e, file=sys.stderr)
49  return DependencyFinder()
50 
51  finder = DependencyFinder()
52  try:
53  finder.visit(tree)
54  except Exception as e:
55  if print_error:
56  print(e, f'({os.path.basename(filename)})', file=sys.stderr)
57 
58  return finder
59 
60 
61 def walk_tree(path='./', print_error=False, filterFnc=None):
62  """Walk the source tree and extract python dependencies, filtered by FilterFnc"""
63 
64  pkg = 'UNKNOWN'
65  deps = defaultdict(lambda : defaultdict(set))
66  for root, dirs, files in os.walk(path):
67  if 'CMakeLists.txt' in files:
68  pkg = os.path.basename(root)
69 
70  if (filterFnc and not filterFnc(pkg)):
71  continue
72 
73  for f in filter(lambda p : os.path.splitext(p)[1]=='.py', files):
74  d = get_dependencies(os.path.join(root,f), print_error)
75  deps[pkg]['import'].update(d.imports)
76  deps[pkg]['include'].update(d.includes)
77 
78  return deps
79 
80 
81 def make_graph(deps, filterFnc=None):
82  """Save the dependencies as dot graph, nodes filtered by filterFnc"""
83 
84  graph = pygraphviz.AGraph(name='AthPyGraph', directed=True)
85  for a in deps:
86  for t in ['import','include']:
87  graph.add_edges_from(((a,b) for b in deps[a][t]
88  if a!=b and (filterFnc is None or (filterFnc(a) and filterFnc(b)))),
89  label = t)
90  return graph
91 
92 
93 def main():
94  parser = argparse.ArgumentParser(description=__doc__)
95 
96  parser.add_argument('path', metavar='DIRECTORY', nargs='?', default='./',
97  help='root of source tree [%(default)s]')
98 
99  parser.add_argument('-o', '--output', metavar='FILE', type=str,
100  help='output file for DOT graph')
101 
102  parser.add_argument('-p', '--packages', metavar='FILE', type=str,
103  help='path to packages.txt file [from release]')
104 
105  parser.add_argument('-a', '--all', action='store_true',
106  help='include non-athena dependencies')
107 
108  parser.add_argument('-v', '--verbose', action='store_true',
109  help='print parse errors')
110 
111  args = parser.parse_args()
112 
113  packages = None
114  if not args.all:
115  package_file = args.packages or os.path.join(os.environ['AtlasArea'],'InstallArea',
116  os.environ['BINARY_TAG'],'packages.txt')
117 
118  try:
119  with open(package_file) as f:
120  packages = set(line.rstrip().split('/')[-1] for line in f if not line.startswith('#'))
121  except FileNotFoundError:
122  parser.error(f"Cannot read '{package_file}'. Specify via '-p/--packages' or run with '-a/--all'")
123 
124  # By default only show athena packages:
125  filterFnc = None if args.all else lambda p : p in packages
126 
127  # Walk source tree and create DOT graph:
128  g = make_graph(walk_tree(args.path, args.verbose, filterFnc), filterFnc)
129 
130  if args.output:
131  g.write(args.output)
132  else:
133  print(g)
134 
135 if __name__ == "__main__":
136  sys.exit(main())
read
IovVectorMap_t read(const Folder &theFolder, const SelectionCriterion &choice, const unsigned int limit=10)
Definition: openCoraCool.cxx:569
apydep.main
def main()
Definition: apydep.py:93
apydep.DependencyFinder.imports
imports
Definition: apydep.py:22
apydep.walk_tree
def walk_tree(path='./', print_error=False, filterFnc=None)
Definition: apydep.py:61
covarianceTool.filter
filter
Definition: covarianceTool.py:514
apydep.DependencyFinder.visit_ImportFrom
def visit_ImportFrom(self, node)
Definition: apydep.py:29
apydep.DependencyFinder.includes
includes
Definition: apydep.py:23
add
bool add(const std::string &hname, TKey *tobj)
Definition: fastadd.cxx:55
apydep.DependencyFinder.visit_Call
def visit_Call(self, node)
Definition: apydep.py:34
CxxUtils::set
constexpr std::enable_if_t< is_bitmask_v< E >, E & > set(E &lhs, E rhs)
Convenience function to set bits in a class enum bitmask.
Definition: bitmask.h:232
print
void print(char *figname, TCanvas *c1)
Definition: TRTCalib_StrawStatusPlots.cxx:25
apydep.DependencyFinder
Definition: apydep.py:18
apydep.make_graph
def make_graph(deps, filterFnc=None)
Definition: apydep.py:81
apydep.get_dependencies
def get_dependencies(filename, print_error=False)
Definition: apydep.py:41
apydep.DependencyFinder.__init__
def __init__(self)
Definition: apydep.py:21
Trk::open
@ open
Definition: BinningType.h:40
apydep.DependencyFinder.visit_Import
def visit_Import(self, node)
Definition: apydep.py:25
WriteBchToCool.update
update
Definition: WriteBchToCool.py:67
Trk::split
@ split
Definition: LayerMaterialProperties.h:38