Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow adding class names to method names for Halstead metric (fix #175) #247

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion docs/commandline.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
++++++++

Expand Down
5 changes: 5 additions & 0 deletions radon/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ def mi(
sort=sort,
include_ipynb=include_ipynb,
ipynb_cells=ipynb_cells,
class_names=False,
)

harvester = MIHarvester(paths, config)
Expand All @@ -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.
Expand All @@ -305,13 +307,16 @@ def hal(
:param -O, --output-file <str>: 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,
ignore=ignore,
by_function=functions,
include_ipynb=include_ipynb,
ipynb_cells=ipynb_cells,
class_names=class_names,
)

harvester = HCHarvester(paths, config)
Expand Down
3 changes: 2 additions & 1 deletion radon/cli/harvest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
8 changes: 4 additions & 4 deletions radon/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
Expand Down
27 changes: 27 additions & 0 deletions radon/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
22 changes: 19 additions & 3 deletions radon/visitors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down Expand Up @@ -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)
Expand All @@ -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)