146d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)# Copyright 2012 The Chromium Authors. All rights reserved.
22a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)# Use of this source code is governed by a BSD-style license that can be
32a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)# found in the LICENSE file.
43551c9c881056c480085172ff9840cab31610854Torne (Richard Coles)
52a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import fnmatch
62a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import inspect
72a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import os
82a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import re
92a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
105d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)from telemetry import decorators
113551c9c881056c480085172ff9840cab31610854Torne (Richard Coles)from telemetry.core import camel_case
123551c9c881056c480085172ff9840cab31610854Torne (Richard Coles)
132a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
145d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)@decorators.Cache
157d4cd473f85ac64c3747c96c277f9e506a0d2246Torne (Richard Coles)def DiscoverModules(start_dir, top_level_dir, pattern='*'):
162a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  """Discover all modules in |start_dir| which match |pattern|.
172a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
182a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  Args:
192a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    start_dir: The directory to recursively search.
202a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    top_level_dir: The top level of the package, for importing.
212a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    pattern: Unix shell-style pattern for filtering the filenames to import.
222a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
232a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  Returns:
242a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    list of modules.
252a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  """
262a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  modules = []
272a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  for dir_path, _, filenames in os.walk(start_dir):
282a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    for filename in filenames:
292a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      # Filter out unwanted filenames.
302a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      if filename.startswith('.') or filename.startswith('_'):
312a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        continue
322a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      if os.path.splitext(filename)[1] != '.py':
332a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        continue
342a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      if not fnmatch.fnmatch(filename, pattern):
352a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        continue
362a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
372a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      # Find the module.
382a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      module_rel_path = os.path.relpath(os.path.join(dir_path, filename),
392a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                                        top_level_dir)
402a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      module_name = re.sub(r'[/\\]', '.', os.path.splitext(module_rel_path)[0])
412a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
422a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      # Import the module.
437d4cd473f85ac64c3747c96c277f9e506a0d2246Torne (Richard Coles)      module = __import__(module_name, fromlist=[True])
442a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
452a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      modules.append(module)
462a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  return modules
472a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
482a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
497d4cd473f85ac64c3747c96c277f9e506a0d2246Torne (Richard Coles)# TODO(dtu): Normalize all discoverable classes to have corresponding module
507d4cd473f85ac64c3747c96c277f9e506a0d2246Torne (Richard Coles)# and class names, then always index by class name.
515d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)@decorators.Cache
522a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)def DiscoverClasses(start_dir, top_level_dir, base_class, pattern='*',
537d4cd473f85ac64c3747c96c277f9e506a0d2246Torne (Richard Coles)                    index_by_class_name=False):
542a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  """Discover all classes in |start_dir| which subclass |base_class|.
552a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
5658537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)  Base classes that contain subclasses are ignored by default.
5758537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)
582a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  Args:
592a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    start_dir: The directory to recursively search.
602a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    top_level_dir: The top level of the package, for importing.
612a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    base_class: The base class to search for.
622a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    pattern: Unix shell-style pattern for filtering the filenames to import.
637d4cd473f85ac64c3747c96c277f9e506a0d2246Torne (Richard Coles)    index_by_class_name: If True, use class name converted to
647d4cd473f85ac64c3747c96c277f9e506a0d2246Torne (Richard Coles)        lowercase_with_underscores instead of module name in return dict keys.
652a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
662a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  Returns:
677d4cd473f85ac64c3747c96c277f9e506a0d2246Torne (Richard Coles)    dict of {module_name: class} or {underscored_class_name: class}
682a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  """
697d4cd473f85ac64c3747c96c277f9e506a0d2246Torne (Richard Coles)  modules = DiscoverModules(start_dir, top_level_dir, pattern)
702a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  classes = {}
712a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  for module in modules:
720529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    new_classes = DiscoverClassesInModule(
730529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch        module, base_class, index_by_class_name)
740529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    classes = dict(classes.items() + new_classes.items())
750529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch  return classes
761e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)
770529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch@decorators.Cache
780529e5d033099cbfc42635f6f6183833b09dff6eBen Murdochdef DiscoverClassesInModule(module, base_class, index_by_class_name=False):
790529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch  """Discover all classes in |module| which subclass |base_class|.
800529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch
810529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch  Base classes that contain subclasses are ignored by default.
820529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch
830529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch  Args:
840529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    module: The module to search.
850529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    base_class: The base class to search for.
860529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    index_by_class_name: If True, use class name converted to
870529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch        lowercase_with_underscores instead of module name in return dict keys.
880529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch
890529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch  Returns:
900529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    dict of {module_name: class} or {underscored_class_name: class}
910529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch  """
920529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch  classes = {}
930529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch  for _, obj in inspect.getmembers(module):
940529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    # Ensure object is a class.
950529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    if not inspect.isclass(obj):
960529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch      continue
970529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    # Include only subclasses of base_class.
980529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    if not issubclass(obj, base_class):
990529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch      continue
1000529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    # Exclude the base_class itself.
1010529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    if obj is base_class:
1020529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch      continue
1030529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    # Exclude protected or private classes.
1040529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    if obj.__name__.startswith('_'):
1050529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch      continue
1060529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    # Include only the module in which the class is defined.
1070529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    # If a class is imported by another module, exclude those duplicates.
1080529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    if obj.__module__ != module.__name__:
1090529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch      continue
1100529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch
1110529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    if index_by_class_name:
1120529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch      key_name = camel_case.ToUnderscore(obj.__name__)
1130529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    else:
1140529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch      key_name = module.__name__.split('.')[-1]
1150529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    classes[key_name] = obj
1167d4cd473f85ac64c3747c96c277f9e506a0d2246Torne (Richard Coles)
1172a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  return classes
118a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
1190529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch
120effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch_counter = [0]
121effb81e5f8246d0db0270817048dc992db66e9fbBen Murdochdef _GetUniqueModuleName():
122effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  _counter[0] += 1
123effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  return "module_" + str(_counter[0])
124