From 518c535a969d71dc43503e7f484a7f3c4ad82597 Mon Sep 17 00:00:00 2001 From: cocodrips Date: Wed, 13 Feb 2019 19:00:29 +0900 Subject: [PATCH 1/4] Implement ignore path option. --- doccov/main.py | 85 +++++++++++++++++++++++++++++++++------------- tests/test_main.py | 22 ++++++++++++ 2 files changed, 84 insertions(+), 23 deletions(-) diff --git a/doccov/main.py b/doccov/main.py index c7b7cbb..ff55d58 100644 --- a/doccov/main.py +++ b/doccov/main.py @@ -2,11 +2,12 @@ import enum import importlib import inspect +import logging import os +import pathlib import pkgutil import pydoc import sys -import logging logger = logging.getLogger(__name__) @@ -98,6 +99,10 @@ def __add__(self, other): return Coverage(counters=merge) + def __repr__(self): + c = self.counters[Type.FUNCTION.name] + return f'' + def add(self, object, type_): """ :param object(object): @@ -140,7 +145,7 @@ def count_module(object): for key, value in inspect.getmembers(object, inspect.isclass): # if __all__ exists, believe it. Otherwise use old heuristic. if (all is not None or - (inspect.getmodule(value) or object) is object): + (inspect.getmodule(value) or object) is object): if pydoc.visiblename(key, all, object): classes.append((key, value)) cdict[key] = cdict[value] = '#' + key @@ -157,7 +162,7 @@ def count_module(object): for key, value in inspect.getmembers(object, inspect.isroutine): # if __all__ exists, believe it. Otherwise use old heuristic. if (all is not None or - inspect.isbuiltin(value) or inspect.getmodule(value) is object): + inspect.isbuiltin(value) or inspect.getmodule(value) is object): if pydoc.visiblename(key, all, object): funcs.append((key, value)) fdict[key] = '#-' + key @@ -175,7 +180,45 @@ def count_module(object): return coverage -def walk(root_path): +def _get_coverage(package_name, importer, modname, ispkg, ignores): + """ + + Args: + importer: + modname: + ispkg: + ignores [pathlib.Path]: + + Returns: + + """ + + importer_path = pathlib.Path(importer.path) + for ignore in ignores: + if importer_path.samefile(ignore): + return + if str(pathlib.Path(importer.path).resolve()).startswith(str(ignore)): + return + if ispkg and (importer_path / modname.split('.')[-1]).samefile(ignore): + return + + try: + if ispkg: + spec = pkgutil._get_spec(importer, modname) + object = importlib._bootstrap._load(spec) + else: + import_path = f"{package_name}.{modname}" + object = importlib.import_module(import_path) + counter = count_module(object) + return counter + except ImportError as e: + logger.error(f"Failed to import {modname}: {e}") + return + except Exception as e: + return + + +def walk(root_path, ignore_paths=None): """ Count coverage of root_path tree. @@ -199,29 +242,22 @@ def walk(root_path): coverages = [] summary = Coverage() + + ignores = [] + if ignore_paths: + ignores = [pathlib.Path(ignore_path).resolve() for ignore_path in ignore_paths] + for importer, modname, ispkg in packages: - try: - if ispkg: - spec = pkgutil._get_spec(importer, modname) - object = importlib._bootstrap._load(spec) - else: - import_path = f"{package_name}.{modname}" - object = importlib.import_module(import_path) - counter = count_module(object) - - coverages.append(counter) - summary += counter - except ImportError as e: - logger.error(f"Failed to import {modname}: {e}") - continue - except Exception as e: - continue + coverage = _get_coverage(package_name, importer, modname, ispkg, ignores) + if coverage: + coverages.append(coverage) + summary += coverage summary.name = 'coverage' return coverages, summary -def summary(root_path, output, output_type, is_all): +def summary(root_path, output, output_type, is_all, ignore_paths=None): """ Args: root_path: Project path @@ -233,7 +269,7 @@ def summary(root_path, output, output_type, is_all): Returns: """ - coverages, summary = walk(root_path) + coverages, summary = walk(root_path, ignore_paths) if is_all: for coverage in coverages: @@ -247,6 +283,9 @@ def entry_point(): parser.add_argument("project_path", type=str) parser.add_argument("--output", dest='output', default='str', type=str, help="[str,csv]") + parser.add_argument("--ignore", dest='ignores', type=str, nargs='*', + help="Coverage of packages under path is not aggregated.") + parser.add_argument("--all", dest='all', action='store_true', default=False, help="Print all module coverage") parser.add_argument("-m", "--module", dest='module', action='store_true', @@ -269,7 +308,7 @@ def entry_point(): if args.function or not output_type: output_type.append(Type.FUNCTION) - summary(args.project_path, args.output, output_type, args.all) + summary(args.project_path, args.output, output_type, args.all, args.ignores) if __name__ == '__main__': diff --git a/tests/test_main.py b/tests/test_main.py index 8e80942..bdaba29 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -52,3 +52,25 @@ def test_output_all_csv(): def test_output_str(): summary('tests/sample_project', 'str', [Type.FUNCTION, Type.MODULE, Type.CLASS], False) + + +def test_ignore_path(): + _, coverage = walk('tests/sample_project', ['tests/sample_project/package_A']) + # report(coverage, 'str', [Type.FUNCTION, Type.CLASS, Type.MODULE], False) + assert coverage.counters[Type.FUNCTION.name].all == 4 + assert coverage.counters[Type.FUNCTION.name].true == 2 + assert coverage.counters[Type.MODULE.name].all == 5 + assert coverage.counters[Type.MODULE.name].true == 1 + assert coverage.counters[Type.CLASS.name].all == 2 + assert coverage.counters[Type.CLASS.name].true == 2 + + +def test_ignore_path_tree(): + _, coverage = walk('tests/sample_project', ['tests/sample_project/package_B']) + # report(coverage, 'str', [Type.FUNCTION, Type.CLASS, Type.MODULE], False) + assert coverage.counters[Type.FUNCTION.name].all == 2 + assert coverage.counters[Type.FUNCTION.name].true == 2 + assert coverage.counters[Type.MODULE.name].all == 3 + assert coverage.counters[Type.MODULE.name].true == 3 + assert coverage.counters[Type.CLASS.name].all == 1 + assert coverage.counters[Type.CLASS.name].true == 1 From e536f299359c1bd245e815c47788f20cb103c3bd Mon Sep 17 00:00:00 2001 From: cocodrips Date: Thu, 14 Feb 2019 11:18:40 +0900 Subject: [PATCH 2/4] Count coverage function in class. --- doccov/main.py | 90 ++++++++----- tests/sample_project/module_fulldoc.py | 13 +- .../package_A/module_fulldoc.py | 4 +- .../package_B/module_shortdoc.py | 17 --- tests/test_main.py | 118 ++++++++++++++---- 5 files changed, 160 insertions(+), 82 deletions(-) delete mode 100644 tests/sample_project/package_B/module_shortdoc.py diff --git a/doccov/main.py b/doccov/main.py index ff55d58..a8c6dda 100644 --- a/doccov/main.py +++ b/doccov/main.py @@ -97,7 +97,7 @@ def __add__(self, other): for t in Type.__members__: merge[t] = self.counters[t] + other.counters[t] - return Coverage(counters=merge) + return Coverage(name=self.name, counters=merge) def __repr__(self): c = self.counters[Type.FUNCTION.name] @@ -128,6 +128,46 @@ def has_doc(object): return int(object.__doc__.strip() != "") + +def visiblename(name, all=None, obj=None): + + if name in {'__author__', '__builtins__', '__cached__', '__credits__', + '__date__', '__doc__', '__file__', '__spec__', + '__loader__', '__module__', '__name__', '__package__', + '__path__', '__qualname__', '__slots__', '__version__'}: + return False + if name.startswith('__') and name.endswith('__'): + return True + if name.startswith('_') and hasattr(obj, '_fields'): + return True + if all is not None: + return name in all + else: + return not name.startswith('_') + + +def count_class(object): + + name = object.__name__ # ignore the passed-in name + try: + all = object.__all__ + except AttributeError: + all = None + + coverage = Coverage(name=name) + + if not inspect.isclass(object): + return coverage + + coverage.add(object, Type.CLASS) + for func_name, obj in inspect.getmembers(object, inspect.isfunction): + if inspect.isbuiltin(obj): + continue + if visiblename(func_name, all, object): + coverage.add(obj, Type.FUNCTION) + + return coverage + def count_module(object): """ Reference: pydoc.HTMLDoc.docmodule @@ -141,41 +181,22 @@ def count_module(object): except AttributeError: all = None - classes, cdict = [], {} - for key, value in inspect.getmembers(object, inspect.isclass): - # if __all__ exists, believe it. Otherwise use old heuristic. - if (all is not None or - (inspect.getmodule(value) or object) is object): - if pydoc.visiblename(key, all, object): - classes.append((key, value)) - cdict[key] = cdict[value] = '#' + key - for key, value in classes: - for base in value.__bases__: - key, modname = base.__name__, base.__module__ - module = sys.modules.get(modname) - if modname != name and module and hasattr(module, key): - if getattr(module, key) is base: - if not key in cdict: - cdict[key] = cdict[base] = modname + '.html#' + key - - funcs, fdict = [], {} - for key, value in inspect.getmembers(object, inspect.isroutine): - # if __all__ exists, believe it. Otherwise use old heuristic. - if (all is not None or - inspect.isbuiltin(value) or inspect.getmodule(value) is object): - if pydoc.visiblename(key, all, object): - funcs.append((key, value)) - fdict[key] = '#-' + key - if inspect.isfunction(value): fdict[value] = fdict[key] - coverage = Coverage(name=name) - coverage.add(object, Type.MODULE) - for _, obj in classes: - coverage.add(obj, Type.CLASS) + if inspect.ismodule(object): + coverage.add(object, Type.MODULE) + + for class_name, obj in inspect.getmembers(object, inspect.isclass): + # if __all__ exists, believe it. Otherwise use old heuristic. + if (all is not None or + (inspect.getmodule(obj) or object) is object): + if visiblename(class_name, all, object): + coverage += count_class(obj) - for _, obj in funcs: - coverage.add(obj, Type.FUNCTION) + for func_name, obj in inspect.getmembers(object, inspect.isfunction): + if visiblename(func_name, all, object): + if inspect.isfunction(obj): + coverage.add(obj, Type.FUNCTION) return coverage @@ -215,6 +236,7 @@ def _get_coverage(package_name, importer, modname, ispkg, ignores): logger.error(f"Failed to import {modname}: {e}") return except Exception as e: + logger.error(f"Failed to parse: {modname}: {e}") return @@ -272,7 +294,7 @@ def summary(root_path, output, output_type, is_all, ignore_paths=None): coverages, summary = walk(root_path, ignore_paths) if is_all: - for coverage in coverages: + for coverage in sorted(coverages, key=lambda x: x.name): report(coverage, output, output_type, is_all) report(summary, output, output_type, is_all) diff --git a/tests/sample_project/module_fulldoc.py b/tests/sample_project/module_fulldoc.py index a313d69..059cf4d 100644 --- a/tests/sample_project/module_fulldoc.py +++ b/tests/sample_project/module_fulldoc.py @@ -2,7 +2,7 @@ from . import package_A import logging # unused -class FullDoc(): +class NonPackageFullDoc(): """ Short class summary. @@ -13,8 +13,17 @@ def __init__(self): """init""" pass + def class_doc_func(self): + """ + Class func + """ + pass + + def class_no_doc_func(self): + pass + -def function_a(arg1, arg2): +def non_package_function(arg1, arg2): """ Summary. diff --git a/tests/sample_project/package_A/module_fulldoc.py b/tests/sample_project/package_A/module_fulldoc.py index 940efd2..3ffa7a0 100644 --- a/tests/sample_project/package_A/module_fulldoc.py +++ b/tests/sample_project/package_A/module_fulldoc.py @@ -1,6 +1,6 @@ """Module description""" -def full_doc(arg1, arg2): +def package_a_full_doc_func(arg1, arg2): """ Summary. @@ -13,5 +13,5 @@ def full_doc(arg1, arg2): """ return True -def _pricate_func(arg1, arg2): +def _package_a_private_func(arg1, arg2): return True diff --git a/tests/sample_project/package_B/module_shortdoc.py b/tests/sample_project/package_B/module_shortdoc.py deleted file mode 100644 index 25ca202..0000000 --- a/tests/sample_project/package_B/module_shortdoc.py +++ /dev/null @@ -1,17 +0,0 @@ -from sample_project import package_A - - -class ShortDoc(): - """ - Short class summary. - """ - def __init__(self): - """ - pass - """ - pass - -def oneline(): - """Summary.""" - return True - diff --git a/tests/test_main.py b/tests/test_main.py index bdaba29..aa45644 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,5 +1,78 @@ from doccov.main import * +sys.path.insert(0, '.') + + +def doc_func(): + """has doc""" + pass + + +def nodoc_func(): + """ """ + pass + + +class SampleClass(): + """ + Short class summary. + + ..... + """ + + def __init__(self): + """init""" + pass + + def doc_func(self): + """ + Class func + """ + pass + + def no_doc_func(self): + pass + + +def test_count_class(): + coverage = count_class(SampleClass) + + assert coverage.counters[Type.FUNCTION.name].true == 2 + assert coverage.counters[Type.FUNCTION.name].all == 3 + assert coverage.counters[Type.CLASS.name].true == 1 + assert coverage.counters[Type.CLASS.name].all == 1 + + +def test_count_module(): + from .sample_project import module_fulldoc + coverage = count_module(module_fulldoc) + assert coverage.counters[Type.FUNCTION.name].true == 3 + assert coverage.counters[Type.FUNCTION.name].all == 4 + assert coverage.counters[Type.CLASS.name].true == 1 + assert coverage.counters[Type.CLASS.name].all == 1 + + +def test_count_module_package(): + from .sample_project import package_A + coverage = count_module(package_A) + assert coverage.counters[Type.FUNCTION.name].true == 0 + assert coverage.counters[Type.FUNCTION.name].all == 0 + assert coverage.counters[Type.CLASS.name].true == 0 + assert coverage.counters[Type.CLASS.name].all == 0 + assert coverage.counters[Type.MODULE.name].true == 1 + assert coverage.counters[Type.MODULE.name].all == 1 + + +def test_count_module_package(): + from .sample_project.package_A import module_fulldoc + coverage = count_module(module_fulldoc) + assert coverage.counters[Type.FUNCTION.name].true == 1 + assert coverage.counters[Type.FUNCTION.name].all == 1 + assert coverage.counters[Type.CLASS.name].true == 0 + assert coverage.counters[Type.CLASS.name].all == 0 + assert coverage.counters[Type.MODULE.name].true == 1 + assert coverage.counters[Type.MODULE.name].all == 1 + def test_counter(): a = Counter(1, 1) @@ -25,24 +98,16 @@ def test_coverage(): assert counter.true == 2 -def doc_func(): - """has doc""" - pass - - -def nodoc_func(): - """ """ - pass - - def test_sample_project(): _, coverage = walk('tests/sample_project') - assert coverage.counters[Type.FUNCTION.name].all == 5 - assert coverage.counters[Type.FUNCTION.name].true == 3 - assert coverage.counters[Type.MODULE.name].all == 7 + report(coverage, 'str', [Type.FUNCTION, Type.CLASS, Type.MODULE], False) + + assert coverage.counters[Type.FUNCTION.name].true == 4 + assert coverage.counters[Type.FUNCTION.name].all == 7 + assert coverage.counters[Type.CLASS.name].true == 1 + assert coverage.counters[Type.CLASS.name].all == 1 assert coverage.counters[Type.MODULE.name].true == 3 - assert coverage.counters[Type.CLASS.name].all == 2 - assert coverage.counters[Type.CLASS.name].true == 2 + assert coverage.counters[Type.MODULE.name].all == 6 def test_output_all_csv(): @@ -56,21 +121,20 @@ def test_output_str(): def test_ignore_path(): _, coverage = walk('tests/sample_project', ['tests/sample_project/package_A']) - # report(coverage, 'str', [Type.FUNCTION, Type.CLASS, Type.MODULE], False) - assert coverage.counters[Type.FUNCTION.name].all == 4 - assert coverage.counters[Type.FUNCTION.name].true == 2 - assert coverage.counters[Type.MODULE.name].all == 5 + assert coverage.counters[Type.FUNCTION.name].true == 3 + assert coverage.counters[Type.FUNCTION.name].all == 6 + assert coverage.counters[Type.CLASS.name].true == 1 + assert coverage.counters[Type.CLASS.name].all == 1 assert coverage.counters[Type.MODULE.name].true == 1 - assert coverage.counters[Type.CLASS.name].all == 2 - assert coverage.counters[Type.CLASS.name].true == 2 + assert coverage.counters[Type.MODULE.name].all == 4 def test_ignore_path_tree(): _, coverage = walk('tests/sample_project', ['tests/sample_project/package_B']) - # report(coverage, 'str', [Type.FUNCTION, Type.CLASS, Type.MODULE], False) - assert coverage.counters[Type.FUNCTION.name].all == 2 - assert coverage.counters[Type.FUNCTION.name].true == 2 - assert coverage.counters[Type.MODULE.name].all == 3 - assert coverage.counters[Type.MODULE.name].true == 3 - assert coverage.counters[Type.CLASS.name].all == 1 + report(coverage, 'str', [Type.FUNCTION, Type.CLASS, Type.MODULE], False) + assert coverage.counters[Type.FUNCTION.name].true == 4 + assert coverage.counters[Type.FUNCTION.name].all == 5 assert coverage.counters[Type.CLASS.name].true == 1 + assert coverage.counters[Type.CLASS.name].all == 1 + assert coverage.counters[Type.MODULE.name].true == 3 + assert coverage.counters[Type.MODULE.name].all == 3 From 0f654f8530d07a92c5de6e676aabb58acc050213 Mon Sep 17 00:00:00 2001 From: cocodrips Date: Thu, 14 Feb 2019 11:19:55 +0900 Subject: [PATCH 3/4] Close #5, Close From 40ad52c78d144fb103ae6fa9eff6efce9a6a87ce Mon Sep 17 00:00:00 2001 From: cocodrips Date: Thu, 14 Feb 2019 11:20:43 +0900 Subject: [PATCH 4/4] Close #6