pax_global_header00006660000000000000000000000064141733762670014532gustar00rootroot0000000000000052 comment=835a5400881b7460998be51d871fd36f836db3c9 mccabe-0.7.0/000077500000000000000000000000001417337626700127505ustar00rootroot00000000000000mccabe-0.7.0/.gitignore000066400000000000000000000000771417337626700147440ustar00rootroot00000000000000*.egg *.eggs *.egg-info *.py[cod] .hypothesis/ .tox dist build mccabe-0.7.0/.travis.yml000066400000000000000000000014351417337626700150640ustar00rootroot00000000000000language: python before_script: - pip install tox script: - tox jobs: fast_finish: true allow_failures: - env: TOXENV=docstrings - env: TOXENV=py27 - env: TOXENV=py34 - env: TOXENV=py35 - env: TOXENV=pypy include: - python: 2.7 env: TOXENV=py27 - python: 3.4 env: TOXENV=py34 - python: 3.5 env: TOXENV=py35 - python: 3.6 env: TOXENV=py36 - python: 3.7 env: TOXENV=py37 - python: 3.8 env: TOXENV=py38 - python: 3.9 env: TOXENV=py39 - python: pypy env: TOXENV=pypy - python: pypy3 env: TOXENV=pypy3 - env: TOXENV=docstrings - env: TOXENV=flake8 notifications: irc: channels: - "irc.freenode.org##python-code-quality" use_notice: true skip_join: true mccabe-0.7.0/LICENSE000066400000000000000000000023051417337626700137550ustar00rootroot00000000000000Copyright © Ned Batchelder Copyright © 2011-2013 Tarek Ziade Copyright © 2013 Florent Xicluna Licensed under the terms of the Expat License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. mccabe-0.7.0/MANIFEST.in000066400000000000000000000000721417337626700145050ustar00rootroot00000000000000include LICENSE include README.rst include test_mccabe.py mccabe-0.7.0/README.rst000066400000000000000000000076311417337626700144460ustar00rootroot00000000000000McCabe complexity checker ========================= Ned's script to check McCabe complexity. This module provides a plugin for ``flake8``, the Python code checker. Installation ------------ You can install, upgrade, or uninstall ``mccabe`` with these commands:: $ pip install mccabe $ pip install --upgrade mccabe $ pip uninstall mccabe Standalone script ----------------- The complexity checker can be used directly:: $ python -m mccabe --min 5 mccabe.py ("185:1: 'PathGraphingAstVisitor.visitIf'", 5) ("71:1: 'PathGraph.to_dot'", 5) ("245:1: 'McCabeChecker.run'", 5) ("283:1: 'main'", 7) ("203:1: 'PathGraphingAstVisitor.visitTryExcept'", 5) ("257:1: 'get_code_complexity'", 5) Plugin for Flake8 ----------------- When both ``flake8 2+`` and ``mccabe`` are installed, the plugin is available in ``flake8``:: $ flake8 --version 2.0 (pep8: 1.4.2, pyflakes: 0.6.1, mccabe: 0.2) By default the plugin is disabled. Use the ``--max-complexity`` switch to enable it. It will emit a warning if the McCabe complexity of a function is higher than the provided value:: $ flake8 --max-complexity 10 coolproject ... coolproject/mod.py:1204:1: C901 'CoolFactory.prepare' is too complex (14) This feature is quite useful for detecting over-complex code. According to McCabe, anything that goes beyond 10 is too complex. Flake8 has many features that mccabe does not provide. Flake8 allows users to ignore violations reported by plugins with ``# noqa``. Read more about this in `their documentation `__. To silence violations reported by ``mccabe``, place your ``# noqa: C901`` on the function definition line, where the error is reported for (possibly a decorator). Links ----- * Feedback and ideas: http://mail.python.org/mailman/listinfo/code-quality * Cyclomatic complexity: http://en.wikipedia.org/wiki/Cyclomatic_complexity * Ned Batchelder's script: http://nedbatchelder.com/blog/200803/python_code_complexity_microtool.html * McCabe complexity: http://en.wikipedia.org/wiki/Cyclomatic_complexity Changes ------- 0.7.0 - 2021-01-23 `````````````````` * Drop support for all versions of Python lower than 3.6 * Add support for Python 3.8, 3.9, and 3.10 * Fix option declaration for Flake8 0.6.1 - 2017-01-26 `````````````````` * Fix signature for ``PathGraphingAstVisitor.default`` to match the signature for ``ASTVisitor`` 0.6.0 - 2017-01-23 `````````````````` * Add support for Python 3.6 * Fix handling for missing statement types 0.5.3 - 2016-12-14 `````````````````` * Report actual column number of violation instead of the start of the line 0.5.2 - 2016-07-31 `````````````````` * When opening files ourselves, make sure we always name the file variable 0.5.1 - 2016-07-28 `````````````````` * Set default maximum complexity to -1 on the class itself 0.5.0 - 2016-05-30 `````````````````` * PyCon 2016 PDX release * Add support for Flake8 3.0 0.4.0 - 2016-01-27 `````````````````` * Stop testing on Python 3.2 * Add support for async/await keywords on Python 3.5 from PEP 0492 0.3.1 - 2015-06-14 `````````````````` * Include ``test_mccabe.py`` in releases. * Always coerce the ``max_complexity`` value from Flake8's entry-point to an integer. 0.3 - 2014-12-17 ```````````````` * Computation was wrong: the mccabe complexity starts at 1, not 2. * The ``max-complexity`` value is now inclusive. E.g.: if the value is 10 and the reported complexity is 10, then it passes. * Add tests. 0.2.1 - 2013-04-03 `````````````````` * Do not require ``setuptools`` in setup.py. It works around an issue with ``pip`` and Python 3. 0.2 - 2013-02-22 ```````````````` * Rename project to ``mccabe``. * Provide ``flake8.extension`` setuptools entry point. * Read ``max-complexity`` from the configuration file. * Rename argument ``min_complexity`` to ``threshold``. 0.1 - 2013-02-11 ```````````````` * First release mccabe-0.7.0/mccabe.py000066400000000000000000000246361417337626700145470ustar00rootroot00000000000000""" Meager code path measurement tool. Ned Batchelder http://nedbatchelder.com/blog/200803/python_code_complexity_microtool.html MIT License. """ from __future__ import with_statement import optparse import sys import tokenize from collections import defaultdict try: import ast from ast import iter_child_nodes except ImportError: # Python 2.5 from flake8.util import ast, iter_child_nodes __version__ = '0.7.0' class ASTVisitor(object): """Performs a depth-first walk of the AST.""" def __init__(self): self.node = None self._cache = {} def default(self, node, *args): for child in iter_child_nodes(node): self.dispatch(child, *args) def dispatch(self, node, *args): self.node = node klass = node.__class__ meth = self._cache.get(klass) if meth is None: className = klass.__name__ meth = getattr(self.visitor, 'visit' + className, self.default) self._cache[klass] = meth return meth(node, *args) def preorder(self, tree, visitor, *args): """Do preorder walk of tree using visitor""" self.visitor = visitor visitor.visit = self.dispatch self.dispatch(tree, *args) # XXX *args make sense? class PathNode(object): def __init__(self, name, look="circle"): self.name = name self.look = look def to_dot(self): print('node [shape=%s,label="%s"] %d;' % ( self.look, self.name, self.dot_id())) def dot_id(self): return id(self) class PathGraph(object): def __init__(self, name, entity, lineno, column=0): self.name = name self.entity = entity self.lineno = lineno self.column = column self.nodes = defaultdict(list) def connect(self, n1, n2): self.nodes[n1].append(n2) # Ensure that the destination node is always counted. self.nodes[n2] = [] def to_dot(self): print('subgraph {') for node in self.nodes: node.to_dot() for node, nexts in self.nodes.items(): for next in nexts: print('%s -- %s;' % (node.dot_id(), next.dot_id())) print('}') def complexity(self): """ Return the McCabe complexity for the graph. V-E+2 """ num_edges = sum([len(n) for n in self.nodes.values()]) num_nodes = len(self.nodes) return num_edges - num_nodes + 2 class PathGraphingAstVisitor(ASTVisitor): """ A visitor for a parsed Abstract Syntax Tree which finds executable statements. """ def __init__(self): super(PathGraphingAstVisitor, self).__init__() self.classname = "" self.graphs = {} self.reset() def reset(self): self.graph = None self.tail = None def dispatch_list(self, node_list): for node in node_list: self.dispatch(node) def visitFunctionDef(self, node): if self.classname: entity = '%s%s' % (self.classname, node.name) else: entity = node.name name = '%d:%d: %r' % (node.lineno, node.col_offset, entity) if self.graph is not None: # closure pathnode = self.appendPathNode(name) self.tail = pathnode self.dispatch_list(node.body) bottom = PathNode("", look='point') self.graph.connect(self.tail, bottom) self.graph.connect(pathnode, bottom) self.tail = bottom else: self.graph = PathGraph(name, entity, node.lineno, node.col_offset) pathnode = PathNode(name) self.tail = pathnode self.dispatch_list(node.body) self.graphs["%s%s" % (self.classname, node.name)] = self.graph self.reset() visitAsyncFunctionDef = visitFunctionDef def visitClassDef(self, node): old_classname = self.classname self.classname += node.name + "." self.dispatch_list(node.body) self.classname = old_classname def appendPathNode(self, name): if not self.tail: return pathnode = PathNode(name) self.graph.connect(self.tail, pathnode) self.tail = pathnode return pathnode def visitSimpleStatement(self, node): if node.lineno is None: lineno = 0 else: lineno = node.lineno name = "Stmt %d" % lineno self.appendPathNode(name) def default(self, node, *args): if isinstance(node, ast.stmt): self.visitSimpleStatement(node) else: super(PathGraphingAstVisitor, self).default(node, *args) def visitLoop(self, node): name = "Loop %d" % node.lineno self._subgraph(node, name) visitAsyncFor = visitFor = visitWhile = visitLoop def visitIf(self, node): name = "If %d" % node.lineno self._subgraph(node, name) def _subgraph(self, node, name, extra_blocks=()): """create the subgraphs representing any `if` and `for` statements""" if self.graph is None: # global loop self.graph = PathGraph(name, name, node.lineno, node.col_offset) pathnode = PathNode(name) self._subgraph_parse(node, pathnode, extra_blocks) self.graphs["%s%s" % (self.classname, name)] = self.graph self.reset() else: pathnode = self.appendPathNode(name) self._subgraph_parse(node, pathnode, extra_blocks) def _subgraph_parse(self, node, pathnode, extra_blocks): """parse the body and any `else` block of `if` and `for` statements""" loose_ends = [] self.tail = pathnode self.dispatch_list(node.body) loose_ends.append(self.tail) for extra in extra_blocks: self.tail = pathnode self.dispatch_list(extra.body) loose_ends.append(self.tail) if node.orelse: self.tail = pathnode self.dispatch_list(node.orelse) loose_ends.append(self.tail) else: loose_ends.append(pathnode) if pathnode: bottom = PathNode("", look='point') for le in loose_ends: self.graph.connect(le, bottom) self.tail = bottom def visitTryExcept(self, node): name = "TryExcept %d" % node.lineno self._subgraph(node, name, extra_blocks=node.handlers) visitTry = visitTryExcept def visitWith(self, node): name = "With %d" % node.lineno self.appendPathNode(name) self.dispatch_list(node.body) visitAsyncWith = visitWith class McCabeChecker(object): """McCabe cyclomatic complexity checker.""" name = 'mccabe' version = __version__ _code = 'C901' _error_tmpl = "C901 %r is too complex (%d)" max_complexity = -1 def __init__(self, tree, filename): self.tree = tree @classmethod def add_options(cls, parser): flag = '--max-complexity' kwargs = { 'default': -1, 'action': 'store', 'type': int, 'help': 'McCabe complexity threshold', 'parse_from_config': 'True', } config_opts = getattr(parser, 'config_options', None) if isinstance(config_opts, list): # Flake8 2.x kwargs.pop('parse_from_config') parser.add_option(flag, **kwargs) parser.config_options.append('max-complexity') else: parser.add_option(flag, **kwargs) @classmethod def parse_options(cls, options): cls.max_complexity = int(options.max_complexity) def run(self): if self.max_complexity < 0: return visitor = PathGraphingAstVisitor() visitor.preorder(self.tree, visitor) for graph in visitor.graphs.values(): if graph.complexity() > self.max_complexity: text = self._error_tmpl % (graph.entity, graph.complexity()) yield graph.lineno, graph.column, text, type(self) def get_code_complexity(code, threshold=7, filename='stdin'): try: tree = compile(code, filename, "exec", ast.PyCF_ONLY_AST) except SyntaxError: e = sys.exc_info()[1] sys.stderr.write("Unable to parse %s: %s\n" % (filename, e)) return 0 complx = [] McCabeChecker.max_complexity = threshold for lineno, offset, text, check in McCabeChecker(tree, filename).run(): complx.append('%s:%d:1: %s' % (filename, lineno, text)) if len(complx) == 0: return 0 print('\n'.join(complx)) return len(complx) def get_module_complexity(module_path, threshold=7): """Returns the complexity of a module""" code = _read(module_path) return get_code_complexity(code, threshold, filename=module_path) def _read(filename): if (2, 5) < sys.version_info < (3, 0): with open(filename, 'rU') as f: return f.read() elif (3, 0) <= sys.version_info < (4, 0): """Read the source code.""" try: with open(filename, 'rb') as f: (encoding, _) = tokenize.detect_encoding(f.readline) except (LookupError, SyntaxError, UnicodeError): # Fall back if file encoding is improperly declared with open(filename, encoding='latin-1') as f: return f.read() with open(filename, 'r', encoding=encoding) as f: return f.read() def main(argv=None): if argv is None: argv = sys.argv[1:] opar = optparse.OptionParser() opar.add_option("-d", "--dot", dest="dot", help="output a graphviz dot file", action="store_true") opar.add_option("-m", "--min", dest="threshold", help="minimum complexity for output", type="int", default=1) options, args = opar.parse_args(argv) code = _read(args[0]) tree = compile(code, args[0], "exec", ast.PyCF_ONLY_AST) visitor = PathGraphingAstVisitor() visitor.preorder(tree, visitor) if options.dot: print('graph {') for graph in visitor.graphs.values(): if (not options.threshold or graph.complexity() >= options.threshold): graph.to_dot() print('}') else: for graph in visitor.graphs.values(): if graph.complexity() >= options.threshold: print(graph.name, graph.complexity()) if __name__ == '__main__': main(sys.argv[1:]) mccabe-0.7.0/setup.cfg000066400000000000000000000000771417337626700145750ustar00rootroot00000000000000[bdist_wheel] universal = 1 [metadata] license_file = LICENSE mccabe-0.7.0/setup.py000066400000000000000000000032721417337626700144660ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import with_statement from setuptools import setup def get_version(fname="mccabe.py"): with open(fname) as f: for line in f: if line.startswith("__version__"): return eval(line.split("=")[-1]) def get_long_description(): descr = [] for fname in ("README.rst",): with open(fname) as f: descr.append(f.read()) return "\n\n".join(descr) setup( name="mccabe", version=get_version(), description="McCabe checker, plugin for flake8", long_description=get_long_description(), keywords="flake8 mccabe", author="Tarek Ziade", author_email="tarek@ziade.org", maintainer="Ian Stapleton Cordasco", maintainer_email="graffatcolmingov@gmail.com", url="https://github.com/pycqa/mccabe", license="Expat license", py_modules=["mccabe"], zip_safe=False, entry_points={ "flake8.extension": [ "C90 = mccabe:McCabeChecker", ], }, classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Quality Assurance", ], python_requires=">=3.6", ) mccabe-0.7.0/test_mccabe.py000066400000000000000000000174761417337626700156120ustar00rootroot00000000000000import unittest import sys try: from StringIO import StringIO except ImportError: from io import StringIO import pytest try: import hypothesmith from hypothesis import HealthCheck, given, settings, strategies as st except ImportError: hypothesmith = None import mccabe from mccabe import get_code_complexity # Snippets are put outside of testcases because of spacing issue that would # otherwise occur with triple quoted strings. trivial = 'def f(): pass' expr_as_statement = '''\ def f(): 0xF00D ''' sequential = """\ def f(n): k = n + 4 s = k + n return s """ sequential_unencapsulated = """\ k = 2 + 4 s = k + 3 """ if_elif_else_dead_path = """\ def f(n): if n > 3: return "bigger than three" elif n > 4: return "is never executed" else: return "smaller than or equal to three" """ for_loop = """\ def f(): for i in range(10): print(i) """ for_else = """\ def f(mylist): for i in mylist: print(i) else: print(None) """ recursive = """\ def f(n): if n > 4: return f(n - 1) else: return n """ nested_functions = """\ def a(): def b(): def c(): pass c() b() """ try_else = """\ try: print(1) except TypeA: print(2) except TypeB: print(3) else: print(4) """ async_keywords = """\ async def foobar(a, b, c): await whatever(a, b, c) if await b: pass async with c: pass async for x in a: pass """ annotated_assign = """\ def f(): x: Any = None """ def get_complexity_number(snippet, strio, max=0): """Get the complexity number from the printed string.""" # Report from the lowest complexity number. get_code_complexity(snippet, max) strio_val = strio.getvalue() if strio_val: return int(strio_val.split()[-1].strip("()")) else: return None class McCabeTestCase(unittest.TestCase): def setUp(self): # If not assigned to sys.stdout then getvalue() won't capture anything. self._orig_stdout = sys.stdout sys.stdout = self.strio = StringIO() def tearDown(self): # https://mail.python.org/pipermail/tutor/2012-January/088031.html self.strio.close() sys.stdout = self._orig_stdout def assert_complexity(self, snippet, max): complexity = get_complexity_number(snippet, self.strio) self.assertEqual(complexity, max) # should have the same complexity when inside a function as well. infunc = 'def f():\n ' + snippet.replace('\n', '\n ') complexity = get_complexity_number(infunc, self.strio) self.assertEqual(complexity, max) def test_print_message(self): get_code_complexity(sequential, 0) printed_message = self.strio.getvalue() self.assertEqual(printed_message, "stdin:1:1: C901 'f' is too complex (1)\n") def test_sequential_snippet(self): complexity = get_complexity_number(sequential, self.strio) self.assertEqual(complexity, 1) def test_sequential_unencapsulated_snippet(self): complexity = get_complexity_number(sequential_unencapsulated, self.strio) self.assertEqual(complexity, None) def test_if_elif_else_dead_path_snippet(self): complexity = get_complexity_number(if_elif_else_dead_path, self.strio) # Paths that will never be executed are counted! self.assertEqual(complexity, 3) def test_for_loop_snippet(self): complexity = get_complexity_number(for_loop, self.strio) # The for loop adds an execution path; sometimes it won't be run. self.assertEqual(complexity, 2) def test_for_else_snippet(self): complexity = get_complexity_number(for_else, self.strio) # The for loop doesn't add an execution path, but its `else` does self.assertEqual(complexity, 2) def test_recursive_snippet(self): complexity = get_complexity_number(recursive, self.strio) self.assertEqual(complexity, 2) def test_nested_functions_snippet(self): complexity = get_complexity_number(nested_functions, self.strio) self.assertEqual(complexity, 3) def test_trivial(self): """The most-trivial program should pass a max-complexity=1 test""" complexity = get_complexity_number(trivial, self.strio, max=1) self.assertEqual(complexity, None) printed_message = self.strio.getvalue() self.assertEqual(printed_message, "") def test_expr_as_statement(self): complexity = get_complexity_number(expr_as_statement, self.strio) self.assertEqual(complexity, 1) def test_try_else(self): self.assert_complexity(try_else, 4) @pytest.mark.skipif(sys.version_info < (3, 5), reason="Async keywords are only valid on Python 3.5+") def test_async_keywords(self): """Validate that we properly process async keyword usage.""" complexity = get_complexity_number(async_keywords, self.strio) self.assertEqual(complexity, 3) @pytest.mark.skipif( sys.version_info < (3, 6), reason="Annotated assignments are only valid on Python 3.6+", ) def test_annotated_assignment(self): complexity = get_complexity_number(annotated_assign, self.strio) self.assertEqual(complexity, 1) class RegressionTests(unittest.TestCase): def setUp(self): self.original_complexity = mccabe.McCabeChecker.max_complexity def tearDown(self): mccabe.McCabeChecker.max_complexity = self.original_complexity def test_max_complexity_is_always_an_int(self): """Ensure bug #32 does not regress.""" class _options(object): max_complexity = None options = _options() options.max_complexity = '16' self.assertEqual(0, mccabe.McCabeChecker.max_complexity) mccabe.McCabeChecker.parse_options(options) self.assertEqual(16, mccabe.McCabeChecker.max_complexity) def test_get_module_complexity(self): self.assertEqual(0, mccabe.get_module_complexity("mccabe.py")) # This test uses the Hypothesis and Hypothesmith libraries to generate random # syntatically-valid Python source code and applies McCabe on it. @settings( max_examples=1000, # roughly 1k tests/minute, or half that under coverage derandomize=False, # deterministic mode to avoid CI flakiness deadline=None, # ignore Hypothesis' health checks; we already know that suppress_health_check=HealthCheck.all(), # this is slow and filter-heavy. ) @given( # Note that while Hypothesmith might generate code unlike that written by # humans, it's a general test that should pass for any *valid* source code. # (so e.g. running it against code scraped of the internet might also help) src_contents=hypothesmith.from_grammar() | hypothesmith.from_node(), max_complexity=st.integers(min_value=1), ) @pytest.mark.skipif(not hypothesmith, reason="hypothesmith could not be imported") def test_idempotent_any_syntatically_valid_python( src_contents: str, max_complexity: int ) -> None: """Property-based tests for mccabe. This test case is based on a similar test for Black, the code formatter. Black's test was written by Zac Hatfield-Dodds, the author of Hypothesis and the Hypothesmith tool for source code generation. You can run this file with `python`, `pytest`, or (soon) a coverage-guided fuzzer Zac is working on. """ # Before starting, let's confirm that the input string is valid Python: compile(src_contents, "", "exec") # else bug is in hypothesmith # Then try to apply get_complexity_number to the code... get_code_complexity(src_contents, max_complexity) if __name__ == "__main__": test_idempotent_any_syntatically_valid_python() unittest.main() mccabe-0.7.0/tox.ini000066400000000000000000000010171417337626700142620ustar00rootroot00000000000000[tox] envlist = ,py36,py37,py38,py39,py310,pypy,pypy3,flake8 [testenv] deps = pytest hypothesis ; python_version >= "3.6" hypothesmith ; python_version >= "3.6" commands = pytest [flake8] max-line-length = 88 [testenv:flake8] deps = flake8 commands = flake8 [testenv:docstrings] deps = flake8 flake8-docstrings commands = flake8 [testenv:release] deps = twine >= 1.4.0 wheel commands = python setup.py sdist bdist_wheel twine upload {posargs:--skip-existing} dist/*