From 5d4b5489472e94d1e388825d2b77c0c1f57763d9 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 20 Aug 2023 09:04:07 -0300 Subject: [PATCH 1/5] Allow adding class names to method names for Halstead metric. --- radon/cli/__init__.py | 4 ++++ radon/cli/harvest.py | 3 ++- radon/metrics.py | 8 ++++---- radon/tests/test_cli.py | 26 ++++++++++++++++++++++++++ radon/visitors.py | 22 +++++++++++++++++++--- 5 files changed, 55 insertions(+), 8 deletions(-) diff --git a/radon/cli/__init__.py b/radon/cli/__init__.py index 3af5138..fbf8273 100644 --- a/radon/cli/__init__.py +++ b/radon/cli/__init__.py @@ -284,6 +284,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 +306,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 +315,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..af9a2db 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 = config.class_names 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..9583c88 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 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) From 9d31c7fa319ce6efd7035e3ae27bbe6d5d9aa875 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 20 Aug 2023 09:12:00 -0300 Subject: [PATCH 2/5] Add class_names to docs. --- docs/commandline.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 ++++++++ From f01655e984a72477d4bfadaa528f19a235156457 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 20 Aug 2023 11:02:23 -0300 Subject: [PATCH 3/5] Pass class_names=False to MIHarvester from mi(). --- radon/cli/__init__.py | 1 + radon/tests/test_cli.py | 1 + 2 files changed, 2 insertions(+) diff --git a/radon/cli/__init__.py b/radon/cli/__init__.py index fbf8273..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) diff --git a/radon/tests/test_cli.py b/radon/tests/test_cli.py index 9583c88..24cee34 100644 --- a/radon/tests/test_cli.py +++ b/radon/tests/test_cli.py @@ -184,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( From 7c062df0478bf8d19e5945b73918ca8c925e01af Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 20 Aug 2023 11:43:36 -0300 Subject: [PATCH 4/5] Guard against config not having a class_names attribute. --- radon/cli/harvest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/radon/cli/harvest.py b/radon/cli/harvest.py index af9a2db..3781daf 100644 --- a/radon/cli/harvest.py +++ b/radon/cli/harvest.py @@ -384,7 +384,9 @@ class HCHarvester(Harvester): def __init__(self, paths, config): super().__init__(paths, config) self.by_function = config.by_function - self.class_names = config.class_names + self.class_names = False + if hasattr(config, "class_names"): + self.class_names = config.class_names def gobble(self, fobj): """Analyze the content of the file object.""" From b116302d472619b79547cc31346475a3c61a5acb Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Mon, 9 Oct 2023 08:34:58 -0300 Subject: [PATCH 5/5] Use getattr to simplify guarding against config not having a .class_names attribute. --- radon/cli/harvest.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/radon/cli/harvest.py b/radon/cli/harvest.py index 3781daf..215df0d 100644 --- a/radon/cli/harvest.py +++ b/radon/cli/harvest.py @@ -384,9 +384,7 @@ class HCHarvester(Harvester): def __init__(self, paths, config): super().__init__(paths, config) self.by_function = config.by_function - self.class_names = False - if hasattr(config, "class_names"): - self.class_names = config.class_names + self.class_names = getattr(config, "class_names", False) def gobble(self, fobj): """Analyze the content of the file object."""