diff --git a/leapp/snactor/commands/discover.py b/leapp/snactor/commands/discover.py index 5c7180bb4..00429582e 100644 --- a/leapp/snactor/commands/discover.py +++ b/leapp/snactor/commands/discover.py @@ -10,6 +10,7 @@ from leapp.utils.repository import requires_repository, find_repository_basedir, get_repository_name from leapp.utils.clicmd import command, command_opt from leapp.workflows import get_workflows +from leapp.snactor.utils import safe_discover def _is_local(repository, cls, base_dir, all_repos=False): @@ -85,9 +86,22 @@ def _get_model_details(model): description=_LONG_DESCRIPTION) @command_opt('json', is_flag=True, help='Output in json format instead of human readable form') @command_opt('all', is_flag=True, help='Include items from linked repositories') +@command_opt('safe', is_flag=True, help='Analyze actor files statically to work around runtime errors') @requires_repository def cli(args): base_dir = find_repository_basedir('.') + + if args.safe and args.json: + sys.stderr.write('The options --safe and --json are currently mutually exclusive\n') + sys.exit(1) + + if args.safe: + sys.stdout.write( + 'Repository:\n Name: {repository}\n Path: {base_dir}\n\n'.format(repository=get_repository_name(base_dir), + base_dir=base_dir)) + safe_discover(base_dir) + sys.exit(0) + repository = find_and_scan_repositories(base_dir, include_locals=True) try: repository.load() diff --git a/leapp/snactor/utils.py b/leapp/snactor/utils.py new file mode 100644 index 000000000..83887efb1 --- /dev/null +++ b/leapp/snactor/utils.py @@ -0,0 +1,128 @@ +import ast +import functools +import itertools +import os +import six + + +def print_section(data, section, pivot): + "Print a section of obtained data" + type_data = data[section] + print('{}'.format(section.capitalize())) + for td in type_data: + fp = format_file_path(pivot, td['file']) + first_part = ' - {}({})'.format(td['name'], ', '.join(td['bases'])) + pad = '.' * (60 - len(first_part)) + print('{} {} {}'.format(first_part, pad, fp)) + print('') + + +def format_file_path(pivot, path): + "Format path as relative to a pivot" + if not pivot or pivot == '.': + pivot = os.getcwd() + return os.path.relpath(path, pivot) + + +def get_candidate_files(start='.'): + "Find all .py files in a directory tree" + for root, _, files in os.walk(start): + for f in files: + if not f.endswith('py'): + continue + yield os.path.join(root, f) + + +def ast_parse_file(file): + "Parse a python file and return tuple (ast, filename)" + with open(file, mode='r') as fp: + try: + return ast.parse(fp.read(), file), file + except Exception: + return None, file + + +def get_base_classes(bases, via): + "Get base classes of a type, only direct names are supported currently" + bases_set = set() + errors = [] + for base in bases: + if isinstance(base, ast.Name): + bases_set.add(base.id) + else: + errors.append('Unknown base: {} via {}'.format(base.__class__.__name__, via)) + return bases_set, errors + + +def inspect(tree_file, collected_types={}, type_infos={}): + "Inspect and collect data from AST tree" + tree, file = tree_file + if not tree: + return ['Unable to parse: {}'.format(file)] + errors = [] + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef): + base_classes, err = get_base_classes(node.bases, file) + errors += err + + if base_classes & collected_types['models']: + collected_types['models'].add(node.name) + type_infos['models'].append({ + 'name': node.name, + 'bases': list(base_classes), + 'file': file + }) + if base_classes & collected_types['actors']: + collected_types['actors'].add(node.name) + type_infos['actors'].append({ + 'name': node.name, + 'bases': list(base_classes), + 'file': file + }) + if base_classes & collected_types['tags']: + collected_types['tags'].add(node.name) + type_infos['tags'].append({ + 'name': node.name, + 'bases': list(base_classes), + 'file': file + }) + return errors + + +def safe_discover(pivot): + # Here we collect all the types that inherit from Model/Actor/Tag types + # so that we can use this dict as a lookup for deeper search to support + # use cases like: + # + # class FirstOrderModel(Model): + # pass + # + # class SecondOrderModel(FirstOrderModel): + # pass + # + collected_types = { + 'models': set(['Model']), + 'actors': set(['Actor']), + 'tags': set(['Tag']) + } + type_infos = { + 'models': [], + 'actors': [], + 'tags': [] + } + + inspector = functools.partial( + inspect, + collected_types=collected_types, + type_infos=type_infos + ) + + errors = filter(None, map(inspector, map(ast_parse_file, get_candidate_files(pivot)))) + flat_errors = list(itertools.chain(*errors)) + + print_section(type_infos, 'actors', pivot) + print_section(type_infos, 'models', pivot) + print_section(type_infos, 'tags', pivot) + if flat_errors: + print('Errors:') + print('\n'.join(flat_errors))