diff --git a/docs/commandline.rst b/docs/commandline.rst index f6beb48..c719994 100644 --- a/docs/commandline.rst +++ b/docs/commandline.rst @@ -437,7 +437,8 @@ The :command:`hal` command This command analyzes Python source files and computes their Halstead complexity metrics. Files can be analyzed as wholes, or in terms of their -top-level functions with the :option:`-f` flag. +top-level functions with the :option:`-f` flag. Method names can be +prefixed with their class names if the :option:`-c` flag is used. Excluding files or directories is supported through glob patterns with the :option:`-e` flag. Every positional argument is interpreted as a path. The @@ -489,6 +490,13 @@ Options Value can be set in a configuration file using the ``ipynb_cells`` property. +.. option:: -c, --class_names + + When showing metrics on the function level, prefix method names with their + class names. + + Value can be set in a configuration file using the ``class_names`` property. + Examples ++++++++ diff --git a/radon/cli/__init__.py b/radon/cli/__init__.py index 3af5138..2157660 100644 --- a/radon/cli/__init__.py +++ b/radon/cli/__init__.py @@ -266,6 +266,7 @@ def mi( sort=sort, include_ipynb=include_ipynb, ipynb_cells=ipynb_cells, + class_names=False, ) harvester = MIHarvester(paths, config) @@ -284,6 +285,7 @@ def hal( output_file=_cfg.get_value('output_file', str, None), include_ipynb=_cfg.get_value('include_ipynb', bool, False), ipynb_cells=_cfg.get_value('ipynb_cells', bool, False), + class_names=_cfg.get_value('class_names', bool, False), ): """ Analyze the given Python modules and compute their Halstead metrics. @@ -305,6 +307,8 @@ def hal( :param -O, --output-file : The output file (default to stdout). :param --include-ipynb: Include IPython Notebook files :param --ipynb-cells: Include reports for individual IPYNB cells + :param -c, --class-names: Include class names before method names as + class.method. """ config = Config( exclude=exclude, @@ -312,6 +316,7 @@ def hal( by_function=functions, include_ipynb=include_ipynb, ipynb_cells=ipynb_cells, + class_names=class_names, ) harvester = HCHarvester(paths, config) diff --git a/radon/cli/harvest.py b/radon/cli/harvest.py index ac14fe5..215df0d 100644 --- a/radon/cli/harvest.py +++ b/radon/cli/harvest.py @@ -384,11 +384,12 @@ class HCHarvester(Harvester): def __init__(self, paths, config): super().__init__(paths, config) self.by_function = config.by_function + self.class_names = getattr(config, "class_names", False) def gobble(self, fobj): """Analyze the content of the file object.""" code = fobj.read() - return h_visit(code) + return h_visit(code, self.class_names) def as_json(self): """Format the results as JSON.""" diff --git a/radon/metrics.py b/radon/metrics.py index 7c6ec63..b0993eb 100644 --- a/radon/metrics.py +++ b/radon/metrics.py @@ -22,14 +22,14 @@ Halstead = collections.namedtuple("Halstead", "total functions") -def h_visit(code): +def h_visit(code, class_names=False): '''Compile the code into an AST tree and then pass it to :func:`~radon.metrics.h_visit_ast`. ''' - return h_visit_ast(ast.parse(code)) + return h_visit_ast(ast.parse(code), class_names) -def h_visit_ast(ast_node): +def h_visit_ast(ast_node, class_names=False): ''' Visit the AST node using the :class:`~radon.visitors.HalsteadVisitor` visitor. The results are `HalsteadReport` namedtuples with the following @@ -56,7 +56,7 @@ def h_visit_ast(ast_node): Nested functions are not tracked. ''' - visitor = HalsteadVisitor.from_ast(ast_node) + visitor = HalsteadVisitor.from_ast(ast_node, class_names=class_names) total = halstead_visitor_report(visitor) functions = [ (v.context, halstead_visitor_report(v)) diff --git a/radon/tests/test_cli.py b/radon/tests/test_cli.py index 8b19372..24cee34 100644 --- a/radon/tests/test_cli.py +++ b/radon/tests/test_cli.py @@ -119,6 +119,32 @@ def test_cc(mocker, log_mock): ) +def test_hal(mocker, log_mock): + harv_mock = mocker.patch('radon.cli.HCHarvester') + harv_mock.return_value = mocker.sentinel.harvester + + cli.hal(['-'], class_names=True) + + harv_mock.assert_called_once_with( + ['-'], + cli.Config( + exclude=None, + ignore=None, + by_function=False, + include_ipynb=False, + ipynb_cells=False, + class_names=True, + ), + ) + log_mock.assert_called_once_with( + mocker.sentinel.harvester, + json=False, + stream=sys.stdout, + xml=False, + md=False + ) + + def test_raw(mocker, log_mock): harv_mock = mocker.patch('radon.cli.RawHarvester') harv_mock.return_value = mocker.sentinel.harvester @@ -158,6 +184,7 @@ def test_mi(mocker, log_mock): sort=False, include_ipynb=False, ipynb_cells=False, + class_names=False, ), ) log_mock.assert_called_once_with( diff --git a/radon/visitors.py b/radon/visitors.py index e774648..e740e35 100644 --- a/radon/visitors.py +++ b/radon/visitors.py @@ -348,13 +348,15 @@ class HalsteadVisitor(CodeVisitor): "Constant": "value", } - def __init__(self, context=None): + def __init__(self, context=None, classname=None, class_names=False): '''*context* is a string used to keep track the analysis' context.''' self.operators_seen = set() self.operands_seen = set() self.operators = 0 self.operands = 0 self.context = context + self.classname = classname + self.class_names = class_names # A new visitor is spawned for every scanned function. self.function_visitors = [] @@ -436,10 +438,13 @@ def visit_FunctionDef(self, node): analyze the function's body. We also track information on the function itself. ''' - func_visitor = HalsteadVisitor(context=node.name) + name = node.name + if self.classname and self.class_names: + name = '{0}.{1}'.format(self.classname, node.name) + func_visitor = HalsteadVisitor(context=name, class_names=self.class_names) for child in node.body: - visitor = HalsteadVisitor.from_ast(child, context=node.name) + visitor = HalsteadVisitor.from_ast(child, context=name) self.operators += visitor.operators self.operands += visitor.operands self.operators_seen.update(visitor.operators_seen) @@ -458,3 +463,14 @@ def visit_AsyncFunctionDef(self, node): such. ''' self.visit_FunctionDef(node) + + def visit_ClassDef(self, node): + name = node.name if self.class_names else None + for child in node.body: + visitor = HalsteadVisitor(classname=name, class_names=self.class_names) + visitor.visit(child) + self.function_visitors.extend(visitor.function_visitors) + self.operators += visitor.operators + self.operands += visitor.operands + self.operators_seen.update(visitor.operators_seen) + self.operands_seen.update(visitor.operands_seen)