Giter VIP home page Giter VIP logo

mccabe's Introduction

McCabe 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).

Changes

0.7.0 - 2022-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's People

Contributors

asottile avatar blueyed avatar bukzor avatar cclauss avatar degustaf avatar florentx avatar galaxysnail avatar hroncok avatar jaraco avatar jdufresne avatar jwiggins avatar maxg87 avatar nchammas avatar nikolas avatar sambrightman avatar samuelcolvin avatar schlamar avatar sigmavirus24 avatar sobolevn avatar tirkarthi avatar tushar-deepsource avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

mccabe's Issues

'noqa: C901' does not work as documented

I have the following code:

# noqa: C901
def complex_function():
    if True:
        if True:
            pass
        else:
            pass
    else:
        if True:
            pass
        else:
            pass

I disabled mccabe for the function, so I should not be seeing any error. Instead, I get this:

$ flake8 --max-complexity 3 cyclo.py
cyclo.py:2:1: C901 'complex_function' is too complex (4)

How do I disable mccabe for a single function?

$ flake8 --version
3.7.6 (mccabe: 0.6.1, pycodestyle: 2.5.0, pyflakes: 2.1.0) CPython 3.7.2 on Darwin

A flag to treat inner functions as separate from outer function

Hey!

When I have a function with several helper functions, I want to group them in a common context for readability. If only the main function is called from outside and all those helper functions are just readability helpers (so this main function isn't huge), this gives me:

  • too many tightly related functions (in total) to not have a common context
  • too little code to create a module (especially when there are many modules already)
  • no side effects (state)

Then I create a function with inner functions:

  • first inner function: _run() that is the highest abstraction level inner function
  • then 2-4 simple inner functions
  • lastly in the body of the outer function: return _run()

Details why this is probably the best solution here: https://stackoverflow.com/questions/50253517/how-to-group-functions-without-side-effects

The problem is that currently mccabe sums up every inner function complexity with outer function complexity and outputs this as only one, huge result. Please consider:

def inner_uncalled_functions():
    def _run():
        return 0

    def _f1():
        return 0

    def _f2():
        return 0

    return 0


def inner_uncalled_branched_functions():
    def _run():
        return 0

    def _f1():
        if True:
            return 0
        else:
            return 1

    def _f2():
        if True:
            return 0
        else:
            return 1

    return 0


def inner_connected_functions():
    def _run():
        return _f1()

    def _f1():
        return _f2()

    def _f2():
        return 0

    return _run()
$ python -m mccabe mccabe_nested_test.py 
1:0: 'inner_uncalled_functions' 4
14:0: 'inner_uncalled_branched_functions' 6
33:0: 'inner_connected_functions' 4

(sorted for readability)

I'd love if there was a flag to treat inner functions as separate from outer function.

Python 3.7.0b4 test failures

$ tox -e py37
...
============================= test session starts ==============================
platform linux -- Python 3.7.0b4, pytest-3.6.0, py-1.5.3, pluggy-0.6.0
rootdir: .../mccabe, inifile:
collected 14 items                                                             

test_mccabe.py ..F...........                                            [100%]

=================================== FAILURES ===================================
____________________ McCabeTestCase.test_expr_as_statement _____________________

self = <test_mccabe.McCabeTestCase testMethod=test_expr_as_statement>

    def test_expr_as_statement(self):
        complexity = get_complexity_number(expr_as_statement, self.strio)
>       self.assertEqual(complexity, 1)
E       AssertionError: 2 != 1

test_mccabe.py:192: AssertionError
===================== 1 failed, 13 passed in 0.06 seconds ======================
ERROR: InvocationError: '.../mccabe/.tox/py37/bin/python setup.py test -q'
___________________________________ summary ____________________________________
ERROR:   py37: commands failed
  • pytest 3.6.0
  • six 1.11.0
  • py 1.5.3
  • pluggy 0.6.0
  • more_itertools 4.2.0
  • attrs 18.1.0
  • atomicwrites 1.1.5

The mccabe complexity output in module flake8

I have found that if a function name appears more than one time in a file, the output of McCabe complexity for this function name only appear once. I think it should output all of them.

RecursionError on long elif

Given

def func(x):
    if x == "a":
        return "a"
    elif x == "a":
        return "a"
    elif x == "a":
        return "a"
    elif x == "a":
        return "a"
    elif x == "a":  # repeat elif/return 160 times total
        return "a"

We get the error when running flake8 on it:

ERROR:1:1: X002 File "flake8/main/cli.py", line 16, in main   
ERROR:1:1: X002 File "flake8/main/application.py", line 412, in run
ERROR:1:1: X002 File "flake8/main/application.py", line 400, in _run
ERROR:1:1: X002 File "flake8/main/application.py", line 318, in run_checks
ERROR:1:1: X002 File "flake8/checker.py", line 340, in run    
ERROR:1:1: X002 File "flake8/checker.py", line 324, in run_serial
ERROR:1:1: X002 File "flake8/checker.py", line 612, in run_checks
ERROR:1:1: X002 File "flake8/checker.py", line 520, in run_ast_checks
ERROR:1:1: X002 File "mccabe.py", line 266, in run            
ERROR:1:1: X002 File "mccabe.py", line 47, in preorder        
ERROR:1:1: X002 File "mccabe.py", line 41, in dispatch        
ERROR:1:1: X002 File "mccabe.py", line 167, in default        
ERROR:1:1: X002 File "mccabe.py", line 31, in default         
ERROR:1:1: X002 File "mccabe.py", line 41, in dispatch        
ERROR:1:1: X002 File "mccabe.py", line 135, in visitFunctionDef
ERROR:1:1: X002 File "mccabe.py", line 111, in dispatch_list       
ERROR:1:1: X002 File "mccabe.py", line 41, in dispatch
ERROR:1:1: X002 File "mccabe.py", line 177, in visitIf
ERROR:1:1: X002 File "mccabe.py", line 190, in _subgraph
ERROR:1:1: X002 File "mccabe.py", line 204, in _subgraph_parse
ERROR:1:1: X002 File "mccabe.py", line 111, in dispatch_list
...
ERROR:1:1: X002 File "mccabe.py", line 177, in visitIf
ERROR:1:1: X002 File "mccabe.py", line 190, in _subgraph
ERROR:1:1: X002 File "mccabe.py", line 204, in _subgraph_parse
ERROR:1:1: X002 File "mccabe.py", line 111, in dispatch_list
ERROR:1:1: X002 File "mccabe.py", line 41, in dispatch
ERROR:1:1: X002 File "mccabe.py", line 177, in visitIf
ERROR:1:1: X002 File "mccabe.py", line 190, in _subgraph
ERROR:1:1: X002 File "mccabe.py", line 204, in _subgraph_parse
ERROR:1:1: X002 File "mccabe.py", line 111, in dispatch_list
ERROR:1:1: X002 File "mccabe.py", line 41, in dispatch
ERROR:1:1: X002 File "mccabe.py", line 177, in visitIf
ERROR:1:1: X002 File "mccabe.py", line 190, in _subgraph
ERROR:1:1: X002 File "mccabe.py", line 196, in _subgraph_parse
ERROR:1:1: X002 File "mccabe.py", line 111, in dispatch_list
ERROR:1:1: X002 File "mccabe.py", line 41, in dispatch
ERROR:1:1: X002 RecursionError: maximum recursion depth exceeded

"SyntaxError: invalid character in identifier" on files with the UTF-8-BOM

on a file that has the UTF-8-BOM (see this stackoverflow post), mccabe throws something like this:

vagrant@localhost:/vagrant$ python -m mccabe --min 5 evap/grades/templatetags/grades_templatetags.py
Traceback (most recent call last):
  File "/usr/lib/python3.4/runpy.py", line 170, in _run_module_as_main
    "__main__", mod_spec)
  File "/usr/lib/python3.4/runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "/usr/local/lib/python3.4/dist-packages/mccabe.py", line 310, in <module>
    main(sys.argv[1:])
  File "/usr/local/lib/python3.4/dist-packages/mccabe.py", line 292, in main
    tree = compile(code, args[0], "exec", ast.PyCF_ONLY_AST)
  File "evap/grades/templatetags/grades_templatetags.py", line 1
    from django.template import Library
        ^
SyntaxError: invalid character in identifier

before this error, i didn't even know this BOM thing existed and i needed quite a while to figure it out. while i generally don't think such archaic stuff needs to be supported, i think mccabe could handle this more gracefully, especially since it is allowed per the standard and all other tools don't seem to be bothered by those bytes.

example file that has the BOM: https://raw.githubusercontent.com/fsr-itse/EvaP/803ac9825aac1024137220bed631e4302d580d18/evap/grades/templatetags/grades_templatetags.py

GitHub Action

It would be great if we'd have a GitHub Action for mccabe.

global if after global loop crashes

Repro script:

while []: pass
if False: pass

Demo:

$ python -m mccabe demo.py
Traceback (most recent call last):
  File "/Users/buck/prefices/brew/Cellar/python/2.7.7_2/Frameworks/Python.framework/Versions/2.7/lib/python2.7/runpy.py", line 162, in _run_module_as_main
    "__main__", fname, loader, pkg_name)
  File "/Users/buck/prefices/brew/Cellar/python/2.7.7_2/Frameworks/Python.framework/Versions/2.7/lib/python2.7/runpy.py", line 72, in _run_code
    exec code in run_globals
  File "/Users/buck/trees/mine/mccabe/mccabe.py", line 306, in <module>
    main(sys.argv[1:])
  File "/Users/buck/trees/mine/mccabe/mccabe.py", line 291, in main
    visitor.preorder(tree, visitor)
  File "/Users/buck/trees/mine/mccabe/mccabe.py", line 45, in preorder
    self.dispatch(tree, *args)  # XXX *args make sense?
  File "/Users/buck/trees/mine/mccabe/mccabe.py", line 39, in dispatch
    return meth(node, *args)
  File "/Users/buck/trees/mine/mccabe/mccabe.py", line 29, in default
    self.dispatch(child, *args)
  File "/Users/buck/trees/mine/mccabe/mccabe.py", line 39, in dispatch
    return meth(node, *args)
  File "/Users/buck/trees/mine/mccabe/mccabe.py", line 181, in visitIf
    pathnode = self.appendPathNode(name)
  File "/Users/buck/trees/mine/mccabe/mccabe.py", line 146, in appendPathNode
    self.graph.connect(self.tail, pathnode)
AttributeError: 'NoneType' object has no attribute 'connect'

naming proposal ...

I am just about to package this peace of software for Macports and was inclined to name the port py-flake8-mccabe instead of just py-mccabe, account for the fact that this is a related plug-in.

But I think this naming issue is better addressed upstream.

Any thoughts?

Use start of identifier as start position

Would it be possible to have the ranges reported by this tool updated to start at the start of the offending identifier?

For example with the following code:

from example import (
    f401_unused as unused_module)


def c901_too_complex(x):
    if x > 1:
        if x > 2:
            if x > 3:
                if x > 4:
                    if x > 5:
                        if x > 6:
                            if x > 7:
                                if x > 8:
                                    if x > 9:
                                        if x > 10:
                                            if x > 11:
                                                pass


class Foo:
    def foo_c901_too_complex(x):
        if x > 1:
            if x > 2:
                if x > 3:
                    if x > 4:
                        if x > 5:
                            if x > 6:
                                if x > 7:
                                    if x > 8:
                                        if x > 9:
                                            if x > 10:
                                                if x > 11:
                                                    pass


def indent_unaligned():
    try:
        print('H501 %(self)s' % locals())
    except:  # <- H201
        pass

This module simply reports the start of the line as the problem location:

> python -m mccabe --min 10 customRange.py
5:1: 'c901_too_complex' 12
21:1: 'Foo.foo_c901_too_complex' 12

If the identifier start was reported instead, the positions would be 5:5 and 21:9 respectively. As it stands right now the column number is just plain wrong.

Module or package specific complexity limits

Is it possible to set a different complexity limit for a specific package or module? We have an application that has a particularly complicated subpackage (rendering engine related) which really needs a much higher limit, than the remainder of the code.

If not, please point me in the right direction so I can submit a PR that adds the functionality!

Release mccabe > 0.6.1 to remove pytest-runner requirement?

Current flake8 requires mccabe >= 0.6.0, < 0.7.0, which means that if you use flake8 then there's currently no way to avoid the deprecated requirement of pytest-runner that was removed over a year ago in 9274755. This seems unfortunate. Is there anything blocking a release, and/or can anyone do anything to help?

Not working with python 3.5 "async def ..."

It looks like mccabe is ignoring python 3.5 coroutines defined like

async def foobar(a, b, c):
    whatever(a, b, c)

I tried it via flake8 version:

2.5.1 (pep8: 1.7.0, pyflakes: 1.0.0, mccabe: 0.3.1) CPython 3.5.0+ on Linux

Config: Ignore local functions

Hello,

I'm not sue but it seems to me that mccab does take local functions into account when calculating the cc score.

Just a stupid example.

def foobar():
    def _my_local_function():
        return 'local'

    return _my_local_function()

IMHO in some cases there are better reasons to keep a function local then reducing the cc score.

Is there a way to configure mccab that way that it ignore local functions when calculating cc score?

I'm using mccab via flake8 in Emacs via python-lsp-server and eglot package.

Some way to ignore specific marked branches for the purposes of the complexity measure

I was recently fixing a bunch of flake8 violations in a project. Two such failures were B006 and B008, from PyCQA/flake8-bugbear. The way I like to fix these is to change the default value to None and put an if statement within the function that sets the previous default value if the argument value is None. You can probably see where this is going - each argument that I had to do this for resulted in the mccabe complexity going up by 1, which resulted in hitting our max-complexity setting on a couple functions. These changes did not modify the actual behavior of the functions, and in general these branches do not increase the complexity of the function in any meaningful way. I don't want to modify the structure of the code just to work around a spurious linting error. It is also possible to use the condition expression syntax (e.g. arg = [] if arg is None else arg), which this package does not count as a branch, but I find the if statement syntax more readable and prefer it.

Right now the only way to silence this error is to tell flake8 to ignore violations from this package (which flake8 designates as C901) on the offending functions. However, it will continue to be suppressed if the function does happen to grow too complex in the future. I don't want that either. I'm wondering whether it might be possible to add a feature to this package that enables ignoring specific marked branches for the purposes of the complexity measure. This would probably be done with a comment, similar to how flake8's control comments work.

You might argue that the addition of this feature will encourage people to just ignore branches that are inconvenient to them. But this is already the case - many developers will simply set the C901 violation to ignored the moment they see it instead of taking it as a sign that the function has grown too complex and should be simplified. They may even do it on the file or project level. And I've done the same thing - in the case of the work I've been doing recently, these errors are mostly just annoying and do not indicate actual problems, and my only choice was to add flake8 ignores. Adding a way to specify finer-grained ignores will allow those developers, such as myself, who care in principle about measuring complexity to continue making use of this tool.

Improve error message on invalid use: `python -m mccabe`

Right now it generates this error message:

» python -m mccabe
Traceback (most recent call last):
  File "/Users/sobolev/.pyenv/versions/3.10.0/lib/python3.10/runpy.py", line 196, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/Users/sobolev/.pyenv/versions/3.10.0/lib/python3.10/runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "/Users/sobolev/Desktop/mypy/.venv/lib/python3.10/site-packages/mccabe.py", line 346, in <module>
    main(sys.argv[1:])
  File "/Users/sobolev/Desktop/mypy/.venv/lib/python3.10/site-packages/mccabe.py", line 327, in main
    code = _read(args[0])
IndexError: list index out of range

Compare it with, for example, venv:

» python -m venv
usage: venv [-h] [--system-site-packages] [--symlinks | --copies] [--clear]
            [--upgrade] [--without-pip] [--prompt PROMPT] [--upgrade-deps]
            ENV_DIR [ENV_DIR ...]
venv: error: the following arguments are required: ENV_DIR

I think that this can be improved. Showing help by default seems like a reasonable thing:

» python -m mccabe
Usage: mccabe.py [options]

Options:
  -h, --help            show this help message and exit
  -d, --dot             output a graphviz dot file
  -m THRESHOLD, --min=THRESHOLD
                        minimum complexity for output

I will send a PR soon.

Move to PyCQA organization

The flint project is no longer maintained. GitHub will redirect anyone from flintwork/mccabe to PyCQA/mccabe. @florentx has an invitation pending to PyCQA and it will make sense to have flake8 and mccabe under the same organization. I can do this arbitrarily but I won't. I really want everyone's input. And if there isn't unanimous agreement, then I won't do it.

Add support for pytest 7+

Since pytest version 7.0.0 there were some deprecations to API that emit warnings like:

../.venv/lib/python3.11/site-packages/_pytest/nodes.py:146
  /Users/slava/Space/Emporus/emporus-core-runtime/.venv/lib/python3.11/site-packages/_pytest/nodes.py:146: PytestDeprecationWarning: <class 'pytest_mccabe.McCabeItem'> is not using a cooperative constructor and only takes {'fspath', 'parent', 'complexity'}.
  See https://docs.pytest.org/en/stable/deprecations.html#constructors-of-custom-pytest-node-subclasses-should-take-kwargs for more details.
    warnings.warn(

../.venv/lib/python3.11/site-packages/_pytest/nodes.py:686
  /Users/slava/Space/Emporus/emporus-core-runtime/.venv/lib/python3.11/site-packages/_pytest/nodes.py:686: PytestRemovedIn8Warning: The (fspath: py.path.local) argument to McCabeItem is deprecated. Please use the (path: pathlib.Path) argument instead.
  See https://docs.pytest.org/en/latest/deprecations.html#fspath-argument-for-node-constructors-replaced-with-pathlib-path
    super().__init__(

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================================================================================== 7 passed, 14 warnings in 0.03s ===========================================================================================

Pytest warns about:

The latest working pytest version is 7.1.4 after which pytest integration fails with an internal error on any version higher.

PyPi tarball misses License file

There are two minor issues with this:
(1) Having different tarballs (different content or different checksum) with the same name around is a bit problematic for packaging the software (here MacPorts);
(2) I think, having the License file around is required by a MIT like license;

ignoring functions via special comments

like # noqa for PyFlakes and # pragma: no cover for coverage it would be great if mccabe had a way of ignoring certain functions.

Usage:

  1. functions are necessarily greater than the complexity level I want to enforce elsewhere.
  2. Starting to use mccabe to improve existing code bases without having to either have mccabe always fail or have to set a max-complexity which is way to high to have a real impact.

(I kind of assumed there would be an existing issue about this but I couldn't see it, sorry if I'm being blind)

max-complexity overwritten with string or bool in pep8.py

Using flake8, mccabe and setuptools leads to setting wrong max_complexity option.

When you set setup.cfg to

[flake8]
max-complexity = 2

the command

./setup.py flake8 

will not produce correct output, while calling

flake8

does.

Setting it to max-complexity=1 works, but the type of the option is set to boolean. When set to 2 it's a string instead of the requested int thus leading to always False comparisons at the mccabe code.

The option is parsed correctly at pep8 but is overwritten at pep8.py in class StyleGuide:1629:

       if options_dict:
            options.__dict__.update(options_dict)

I don't know if the problem lies in the mccabe code, pep8 or flake8, so reporting it here.

Versions used:

Python 2.7.6

argparse (1.2.1)
autopep8 (0.9.1)
coverage (3.7.1)
flake8 (2.4.0)
matplotlib (1.3.1)
mccabe (0.3)
nose (1.3.1)
numpy (1.8.2)
pandas (0.13.1)
pep8 (1.5.7)
pexpect (3.1)
Pillow (2.3.0)
pip (1.5.4)
ply (3.4)
Pmw (1.3.2)
pyflakes (0.8.1)
Pygments (1.6)
pyinotify (0.9.4)
pyparsing (2.0.1)
python-dateutil (1.5)
pytz (2012c)
PyX (0.12.1)
requests (2.2.1)
ScientificPython (2.9.4)
scikit-learn (0.14.1)
scipy (0.13.3)
SciTools (0.9.0)
setuptools (2.2)
Shapely (1.3.0)
simplegeneric (0.8.1)
simplejson (3.3.1)
six (1.5.2)
SQLAlchemy (0.8.4)
ssh-import-id (3.21)
statsmodels (0.5.0)
sympy (0.7.4.1)
tables (3.1.1)
Twisted-Core (13.2.0)
UFL (1.5.0)
urllib3 (1.7.1)
virtualenv (1.11.4)
VTK (5.8.0)
Werkzeug (0.9.4)
wheel (0.24.0)
wsgiref (0.1.2)
xlrd (0.9.2)
xlwt (0.7.5)

Allow setting max complexity per function

I've been using flake8 for checking complexity of methods. I now set the max complexity to 6, which mostly results in more readable code, but I did find there are some exceptions. Now it's easy to exclude one method from the complexity check, but I would prefer to have a different max complexity just for this method instead. That way it won't go up further without anyone noticing.

I'm not sure what an implementation for this would look like, though. If I had an idea, I could give it a shot.

Perhaps something like # max-complexity: 7 could work. Right now # noqa: C901 is the only option.

install offline mccabe with error

I have downloaded the mccabe source code file mccabe-0.6.1.tar.gz and transfer it to my offline server, and got error when install it using /usr/local/bin/pip3 install mccabe-0.6.1.tar.gz --no-index --find-links /usr/lib64/comheadcloud/pypisoft/sourcecode. It was said in the log that the error was caused by "Could not find a version that satisfies the requirement setuptools>=34.4" . The installation process wanted to get setuptools by Internet, but I have installed setuptools with version 47.3.2, and I used --no-index -find-links options to instruct the installation process to find dependencies locally. Why the installation process still search http://pypi.org.

`

2021-05-26T05:33:10,010 Using pip 21.0.1 from /usr/local/lib/python3.6/site-packages/pip (python 3.6)
2021-05-26T05:33:10,013 Non-user install because site-packages writeable
2021-05-26T05:33:10,050 Created temporary directory: /tmp/pip-ephem-wheel-cache-6uru8aqx
2021-05-26T05:33:10,051 Created temporary directory: /tmp/pip-req-tracker-ptvg3wgt
2021-05-26T05:33:10,051 Initialized build tracking at /tmp/pip-req-tracker-ptvg3wgt
2021-05-26T05:33:10,051 Created build tracker: /tmp/pip-req-tracker-ptvg3wgt
2021-05-26T05:33:10,051 Entered build tracker: /tmp/pip-req-tracker-ptvg3wgt
2021-05-26T05:33:10,052 Created temporary directory: /tmp/pip-install-76xz8yzr
2021-05-26T05:33:10,084 Processing ./setuptools_scm-4.1.2.tar.gz
2021-05-26T05:33:10,085 Created temporary directory: /tmp/pip-req-build-1dkt5ol0
2021-05-26T05:33:10,129 Added file:///usr/lib64/comheadcloud/pypisoft/sourcecode/setuptools_scm-4.1.2.tar.gz to build tracker '/tmp/pip-req-tracker-ptvg3wgt'
2021-05-26T05:33:10,133 Created temporary directory: /tmp/pip-build-env-colihfbk
2021-05-26T05:33:10,134 Running command /usr/bin/python3 /usr/local/lib/python3.6/site-packages/pip install --ignore-installed --no-user --prefix /tmp/pip-build-env-colihfbk/overlay --no-warn-script-location -v --no-binary :none: --only-binary :none: -i https://pypi.o
rg/simple -- 'setuptools>=34.4' wheel
2021-05-26T05:33:10,666 Using pip 21.0.1 from /usr/local/lib/python3.6/site-packages/pip (python 3.6)
2021-05-26T05:33:10,667 Non-user install by explicit request
2021-05-26T05:33:10,695 Created temporary directory: /tmp/pip-ephem-wheel-cache-bpogfslk
2021-05-26T05:33:10,696 Created build tracker: /tmp/pip-req-tracker-ptvg3wgt
2021-05-26T05:33:10,696 Entered build tracker: /tmp/pip-req-tracker-ptvg3wgt
2021-05-26T05:33:10,696 Created temporary directory: /tmp/pip-install-etvadzrp
2021-05-26T05:33:10,711 1 location(s) to search for versions of setuptools:
2021-05-26T05:33:10,711 * https://pypi.org/simple/setuptools/
2021-05-26T05:33:10,711 Fetching project page and analyzing links: https://pypi.org/simple/setuptools/
2021-05-26T05:33:10,711 Getting page https://pypi.org/simple/setuptools/
2021-05-26T05:33:10,712 Found index url https://pypi.org/simple
2021-05-26T05:33:10,714 Looking up "https://pypi.org/simple/setuptools/" in the cache
2021-05-26T05:33:10,714 Request header has "max_age" as 0, cache bypassed
2021-05-26T05:33:10,715 Starting new HTTPS connection (1): pypi.org:443
2021-05-26T05:33:10,717 Incremented Retry for (url='/simple/setuptools/'): Retry(total=4, connect=None, read=None, redirect=None, status=None)
2021-05-26T05:33:10,717 WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.HTTPSConnection object at 0x7f9698c683c8>: Failed to establish a new connect
ion: [Errno -2] Name or service not known',)': /simple/setuptools/
2021-05-26T05:33:10,718 Starting new HTTPS connection (2): pypi.org:443
2021-05-26T05:33:10,718 Incremented Retry for (url='/simple/setuptools/'): Retry(total=3, connect=None, read=None, redirect=None, status=None)
2021-05-26T05:33:11,219 WARNING: Retrying (Retry(total=3, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.HTTPSConnection object at 0x7f9698c68630>: Failed to establish a new connect
ion: [Errno -2] Name or service not known',)': /simple/setuptools/
2021-05-26T05:33:11,219 Starting new HTTPS connection (3): pypi.org:443
2021-05-26T05:33:11,220 Incremented Retry for (url='/simple/setuptools/'): Retry(total=2, connect=None, read=None, redirect=None, status=None)
2021-05-26T05:33:12,221 WARNING: Retrying (Retry(total=2, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.HTTPSConnection object at 0x7f9698c68780>: Failed to establish a new connect
ion: [Errno -2] Name or service not known',)': /simple/setuptools/
2021-05-26T05:33:12,222 Starting new HTTPS connection (4): pypi.org:443
2021-05-26T05:33:12,223 Incremented Retry for (url='/simple/setuptools/'): Retry(total=1, connect=None, read=None, redirect=None, status=None)
2021-05-26T05:33:14,226 WARNING: Retrying (Retry(total=1, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.HTTPSConnection object at 0x7f9698c688d0>: Failed to establish a new connect
ion: [Errno -2] Name or service not known',)': /simple/setuptools/
2021-05-26T05:33:14,226 Starting new HTTPS connection (5): pypi.org:443
2021-05-26T05:33:14,227 Incremented Retry for (url='/simple/setuptools/'): Retry(total=0, connect=None, read=None, redirect=None, status=None)
2021-05-26T05:33:18,232 WARNING: Retrying (Retry(total=0, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.HTTPSConnection object at 0x7f9698c68a20>: Failed to establish a new connect
ion: [Errno -2] Name or service not known',)': /simple/setuptools/
2021-05-26T05:33:18,232 Starting new HTTPS connection (6): pypi.org:443
2021-05-26T05:33:18,234 Could not fetch URL https://pypi.org/simple/setuptools/: connection error: HTTPSConnectionPool(host='pypi.org', port=443): Max retries exceeded with url: /simple/setuptools/ (Caused by NewConnectionError('<pip._vendor.urllib3.connection.HTTPSCo
nnection object at 0x7f9698c68b70>: Failed to establish a new connection: [Errno -2] Name or service not known',)) - skipping
2021-05-26T05:33:18,251 Given no hashes to check 0 links for project 'setuptools': discarding no candidates
2021-05-26T05:33:18,251 ERROR: Could not find a version that satisfies the requirement setuptools>=34.4
2021-05-26T05:33:18,252 ERROR: No matching distribution found for setuptools>=34.4`

ignore guard clauses

hi all,

guard clauses are a neat way to reduce complexity (and indenting) of code, since the ‘main flow’ is the ‘happy path’, while the ‘unhappy paths’ lead to raising exceptions, returning early, or break/continue from a loop.

for example:

def f():
    if error_condition:
        raise ...
    
    a()
    if error_condition:
        raise ...

    b()
    if error_condition:
        raise ...

something similar for early returns:

def f():
    if check_something():
        return

    a()

    if error:
       return

    result = ...
    return result

and a loop example:

for item in ...:
    if ...:
        break  # or continue
    if ...:
        break  # or continue

    actual_work_goes_here()

right now, code like below is an easy way to make the complexity go up (and potentially break flake8 linting, for example):

def f():
    if False:
        return
    if False:
        return
    if False:
        return
    if False:
        return
    if False:
        return
    if False:
        return
    if False:
        return
    if False:
        return

would it be useful to make it possible to not count such constructs for complexity metrics?

the generic pattern would be something along those lines:

  • if ast node has a single child node
  • this should be a return / continue / break statement

i'm not sure this is even possible or desirable, so i'm just dumping this idea here since others may have useful thoughts to add.

Complexity in list comprehensions

Having asdf.py:

elements = [0, 1, 2, 3, 4]
ignore = [1, 2]


def loop():
    for x in elements:
        if x in ignore:
            continue
        print(x)


def comprehension():
    filtered = [x for x in elements if x not in ignore]
    for x in filtered:
        print(x)


loop()
print('----')
comprehension()

The McCabe complexity in loop() seems to be higher than in comprehension():

$ python -m mccabe asdf.py 
5:0: 'loop' 3
12:0: 'comprehension' 2

Is that really expected? Should not fors and ifs in list comprehensions count towards total complexity?

cut 0.3.0

There have been some major changes/improvements since 0.2.
Please cut a fresh release.

💖

0.7: test failure

When running the self tests on the pypi sdist file, I see:

# setup.py  test
running test
WARNING: Testing via this command is deprecated and will be removed in a future version. Users looking for a generic test entry point independent of test runner are encouraged to use tox.
running egg_info
writing manifest file 'mccabe.egg-info/SOURCES.txt'
running build_ext
test_mccabe (unittest.loader._FailedTest) ... ERROR

======================================================================
ERROR: test_mccabe (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: test_mccabe
Traceback (most recent call last):
  File "/usr/pkg/lib/python3.10/unittest/loader.py", line 436, in _find_test_path
    module = self._get_module_from_name(name)
  File "/usr/pkg/lib/python3.10/unittest/loader.py", line 377, in _get_module_from_name
    __import__(name)
  File "/scratch/devel/py-mccabe/work/mccabe-0.7.0/test_mccabe.py", line 244, in <module>
    @settings(
NameError: name 'settings' is not defined


----------------------------------------------------------------------
Ran 1 test in 0.000s

I see that it trys to import hypothesmith, but this is not installed on my system, and it has an except clause just ignoring the problem.

mccable should allow ranged noqa exceptions

Assuming you have some code that for historical reason on not may need an exemption from the desired max-complexity value, you should be able to specify an upper limit for the exception.

Without an upper limit, there is no way to prevent complexity from increasing because the linter will always be happy.

Practical example at https://github.com/ansible/ansible-lint/blob/master/.flake8#L54-L55 which adds exception for specific file.

If working correctly something like noqa: C901(12) should be allowed, one that states that C901 should be ignored unless is bigger than 12 (assume that your overall project has lower limit).

I am not aware of any workaround for achieving this.

[Proposal] Add a check for none/multiple positional arguments

The current behaviour of python -m mccabe (no positional argument provided) is an IndexError

File "/home/junior/.local/lib/python3.10/site-packages/mccabe.py", line 327, in main
    code = _read(args[0])
IndexError: list index out of range

Also, for python -m mccabe file1 file2 it just ignores file2.

Would it be possible to add a check for a missing positional argument, and another for multiple ones?

state project maintenance status

As I see that last commit was made more than 6 months ago, that there are open PRs more than two years old and that last release is was about 3 years ago, it makes me feel that the project is not maintained.

Is that true or is only deeply dormant?

The fact that it is not possible to increase complexity limit using # noqa seems like something that drives people towards dropping it. Currently there is nothing that can prevent a noqa function from increasing its complexity so, while trying to keep complexity limited you are sometimes forced to add exceptions, once added nothing will prevent them from growing, creating a vicious cycle.

def inside def

Hi!

Why def inside def increase complexity of outer function?
def inside def looks like method inside class. Maybe, complexity of inner function should be independent from outer function?

For example, complexity of test_nested_functions_snippet equals to 1 instead of 3.

Cyclomatic complexity of code with path that will never be executed

Is cyclomatic complexity supposed to count paths that will never be executed?

Observe the following code:

def f(n):
    if n > 4:
        return "bigger than four"
    elif n > 5:
        return "is never executed"
    else:
        return "smaller than or equal to four"

The program outputs a cyclomatic complexity of 3 even though the elif path will never be executed.

Regardless of if cyclomatic complexity as described in this article does (type I) or does not (type II) count code paths that will never be executed I think it is useful to make this distinction more clear than it is now.

Depending on what cyclomatic complexity is supposed to represent you could name their specific types. Some suggestions:

Type I cc : 'unminimized cc' or 'unreduced cc' (this is what the mccabe program currently evaluates for)
Type II cc : 'minimized cc' or 'reduced cc'

cut 0.2.2

I'm still seeing the off-by-one-twice issue with flake8 complexity today.

Can we get a release to make it right?

try-else clauses measured at 1 more than correct complexity

Currently the below code generates the following graph, giving a complexity of 5:

def f():
    try:
        print(1)
    except TypeA:
        print(2)
    except TypeB:
        print(3)
    else:
        print(4)
    finally:
        if x:
            print(5.1)
        else:
            print(5.2)

mccabe

I don't think this models the try-else clause correctly. The above graph seems to indicate that the else sometimes runs instead of the main clause, which isn't correct. I believe this is the correct model:

mccabe

This seems to correctly indicate that the else always runs directly after the main try clause, if it runs, and never in a line of execution that involves the exception handlers. This has 1 less complexity than the above graph. This yields one less complexity than the current interpretation.

How should try-except clauses be modeled?

From @sigmavirus24

Actually now that I think of a more complex example

def f():
    try:
        function_one(1)
        function_two(arg)
    except TypeA:
        print(2)
    except TypeB:
        print(3)
    else:
        print(4)
    finally:
        if x:
            print(5.1)
        else:
            print(5.2)

I see that actually you could branch to any of the except clauses from either of the statements in the try block. I still don't think that the except branches coming from the try block is correct so maybe something like

           -----------------> except clause 1
          /                 /               \
try -> function_one -> function_two ----> else
          \                 \               /
           -----------------> except clause 2

off by two

The minimum complexity on the mccabe scale is one.
The simplest function should pass flake8 when max-complexity is 1.

There's two reasons this doesn't work:

  • "max complexity" in fact specifies the minimum complexity which is not acceptable. Please note the difference in adjectives between the field's name and its description.
  • The simplest function has a measured complexity of two. Using a definition from Wikipedia ( M = π − s + 2 where π is the number of decision points in the program, and s is the number of exit points), I count zero decision points and one exit point, resulting in M==1.
$ cat trivial.py 
def trivial():
    return None

$ flake8 --max-complexity 1 trivial.py 
trivial.py:1:1: C901 'trivial' is too complex (2)

[Proposal] Allow disabling "try-except" clauses evaluation.

When using third party services libraries, or even on your own code, you may find a scenario were a function may return several different types of exceptions each of them requiring a simple but different action to be taken.

Exhaustive Exception treatment on those cases leads to a high complexity result which is right, as per the complexity calculation strategy, but an undesired result.

def foo(bar):
    try:
        do_whatever(bar)
    except TypeA:
        do_something_related_to_TypeA()
    except TypeB:
        do_something_related_to_TypeB()
    except TypeC:
        do_something_related_to_TypeC()
    except TypeD:
        do_something_related_to_TypeD()
    except TypeE:
        do_something_related_to_TypeE()
    except TypeF:
        do_something_related_to_TypeF()
    except TypeG:
        do_something_related_to_TypeG()
    except TypeH:
        do_something_related_to_TypeH()
    except TypeI:
        do_something_related_to_TypeI()
    except TypeJ:
        do_something_related_to_TypeJ()
python -Bm mccabe deffoobar.py                     
1:0: 'foo' 12

It will be interesting being able to ignore exceptions branching to properly compute the complexity of the actual function logic without the burden of the exception treatment one.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.