Coder Social home page Coder Social logo

mschwager / cohesion Goto Github PK

View Code? Open in Web Editor NEW
229.0 10.0 4.0 94 KB

A tool for measuring Python class cohesion.

License: GNU General Public License v3.0

Python 100.00%
python cohesion measure code lint linter-plugin quality module flake8 flake8-plugin

cohesion's Introduction

Cohesion

Python Versions PyPI Version

Cohesion is a tool for measuring Python class cohesion.

In computer programming, cohesion refers to the degree to which the elements of a module belong together. Thus, cohesion measures the strength of relationship between pieces of functionality within a given module. For example, in highly cohesive systems functionality is strongly related.

When cohesion is high, it means that the methods and variables of the class are co-dependent and hang together as a logical whole.

  • Clean Code pg. 140

Some of the advantages of high cohesion, also by Wikipedia:

  • Reduced module complexity (they are simpler, having fewer operations).
  • Increased system maintainability, because logical changes in the domain affect fewer modules, and because changes in one module require fewer changes in other modules.
  • Increased module reusability, because application developers will find the component they need more easily among the cohesive set of operations provided by the module.

Installing

$ python -m pip install cohesion
$ cohesion -h

OR

$ git clone https://github.com/mschwager/cohesion.git
$ cd cohesion
$ PYTHONPATH=lib/ python -m cohesion -h

Using

Cohesion measures class and instance variable usage across the methods of that class.

$ cat example.py
class ExampleClass1(object):
    class_variable1 = 5
    class_variable2 = 6

    def func1(self):
        self.instance_variable = 6

        def inner_func(b):
            return b + 5

        local_variable = self.class_variable1

        return local_variable

    def func2(self):
        print(self.class_variable2)

    @staticmethod
    def func3(variable):
        return variable + 7

class ExampleClass2(object):
    def func1(self):
        self.instance_variable1 = 7
$ cohesion --files example.py --verbose
File: example.py
  Class: ExampleClass1 (1:0)
    Function: func1 2/3 66.67%
      Variable: class_variable1 True
      Variable: class_variable2 False
      Variable: instance_variable True
    Function: func2 1/3 33.33%
      Variable: class_variable1 False
      Variable: class_variable2 True
      Variable: instance_variable False
    Function: func3 0/3 0.00%
      Variable: class_variable1 False
      Variable: class_variable2 False
      Variable: instance_variable False
    Total: 33.33%
  Class: ExampleClass2 (23:0)
    Function: func1 1/1 100.00%
      Variable: instance_variable1 True
    Total: 100.00%

The --below and --above flags can be specified to only show classes with a cohesion value below or above the specified percentage, respectively.

Flake8 Support

Cohesion supports being run by flake8. First, ensure your installation has registered cohesion:

$ flake8 --version
3.2.1 (pyflakes: 1.0.0, cohesion: 0.8.0, pycodestyle: 2.2.0, mccabe: 0.5.3) CPython 2.7.12 on Linux

And now use flake8 to lint your file:

$ flake8 example.py
example.py:1:1: H601 class has low cohesion

Python Versions

If you would like to simultaneously run Cohesion on Python 2 and Python 3 code then you will have to install it for both versions. I.e.

$ python2 -m pip install cohesion
$ python3 -m pip install cohesion

Then use the corresponding version to run the module:

$ python2 -m cohesion --files python2_file.py
$ python3 -m cohesion --files python3_file.py

Developing

First, install development packages:

$ python -m pip install -r requirements-dev.txt

Testing

$ pytest

Linting

$ flake8

Coverage

$ pytest --cov

cohesion's People

Contributors

jajajasalu2 avatar kyleking avatar mschwager 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

cohesion's Issues

flake8 does not raise any errors

For some reason flake8 does not produce any warnings. Even if it should.

Reproduction:

  1. create example.py from the readme
  2. run flake8 --select=H --cohesion-below=95 example.py

This should raise one error, since the same file produces this output:

» cohesion --below 95 -f example.py
File: example.py
  Class: ExampleClass1 (1:0)
    Function: func1 2/3 66.67%
    Function: func2 1/3 33.33%
    Function: func3 staticmethod
    Total: 33.33%

But, nothing happens.
Versions I am using:

{
  "dependencies": [
    {
      "dependency": "setuptools",
      "version": "39.0.1"
    }
  ],
  "platform": {
    "python_implementation": "CPython",
    "python_version": "3.6.5",
    "system": "Darwin"
  },
  "plugins": [
    {
      "is_local": false,
      "plugin": "cohesion",
      "version": "0.9.1"
    },
    {
      "is_local": false,
      "plugin": "mccabe",
      "version": "0.6.1"
    },
    {
      "is_local": false,
      "plugin": "pycodestyle",
      "version": "2.3.1"
    },
    {
      "is_local": false,
      "plugin": "pyflakes",
      "version": "1.6.0"
    }
  ],
  "version": "3.5.0"
}

Meaning of result returned by Cohesion

I ran the lib cohesion over my code and return me the result.. what the lib analyze ?

 Function: __eq__ 1/3 33.33%
    Function: __init__ 1/3 33.33%
    Function: __repr__ 2/3 66.67%
    Function: change_bit 2/3 66.67%
    Function: is_valid 1/3 33.33%
    Total: 46.67%
  Class: Hamming
    Function: __init__ 1/13 7.69%
    Function: __repr__ 2/13 15.38%
    Function: bits_verified_by 3/13 23.08%
    Function: calculate_parity 2/13 15.38%
    Function: check 4/13 30.77%
    Function: divisors 1/13 7.69%
    Function: encode 8/13 61.54%
    Function: fix 3/13 23.08%
    Function: is_power 2/13 15.38%
    Function: wrong_position 4/13 30.77%
    Total: 23.08%

Error on downloading package

I ran the command:

pip install cohesion

and show me result :

Downloading/unpacking cohesion
  Could not find any downloads that satisfy the requirement cohesion
No distributions at all found for cohesion
Storing complete log in /home/cliente/.pip/pip.log

Seeing the pip.log file:

Downloading/unpacking cohesion

  Getting page http://pypi.python.org/simple/cohesion
  URLs to search for versions for cohesion:
  * http://pypi.python.org/simple/cohesion/
  Getting page http://pypi.python.org/simple/cohesion/
  Analyzing links from page https://pypi.python.org/simple/cohesion/
    Skipping link https://pypi.python.org/packages/9d/56/536846a677d672f6176eccbc66d4da05101960ef755e865d36df3a3fbc25/cohesion-0.5.0-py2.py3-none-any.whl#md5=f735e4fa689b77313edb46c6434e7df5 (from https://pypi.python.org/simple/cohesion/); unknown archive format: .whl
    Skipping link https://pypi.python.org/packages/e1/db/2b29b55958681a994a52a06767465426cf1952ea9218a73f9c00926694c3/cohesion-0.5.1-py2.py3-none-any.whl#md5=0b5fc31e76753afb20c7e320318295a5 (from https://pypi.python.org/simple/cohesion/); unknown archive format: .whl
  Could not find any downloads that satisfy the requirement cohesion

No distributions at all found for cohesion

Exception information:
Traceback (most recent call last):
  File "/home/cliente/Dev/Apps/redes/tests/local/lib/python2.7/site-packages/pip-1.1-py2.7.egg/pip/basecommand.py", line 104, in main
    status = self.run(options, args)
  File "/home/cliente/Dev/Apps/redes/tests/local/lib/python2.7/site-packages/pip-1.1-py2.7.egg/pip/commands/install.py", line 245, in run
    requirement_set.prepare_files(finder, force_root_egg_info=self.bundle, bundle=self.bundle)
  File "/home/cliente/Dev/Apps/redes/tests/local/lib/python2.7/site-packages/pip-1.1-py2.7.egg/pip/req.py", line 978, in prepare_files
    url = finder.find_requirement(req_to_install, upgrade=self.upgrade)
  File "/home/cliente/Dev/Apps/redes/tests/local/lib/python2.7/site-packages/pip-1.1-py2.7.egg/pip/index.py", line 157, in find_requirement
    raise DistributionNotFound('No distributions at all found for %s' % req)
DistributionNotFound: No distributions at all found for cohesion

`--below` argument works incorrectly

After updating to 0.9.1 I have faced a new issue:

  1. create an example.py file from the readme
  2. run cohesion -f example.py, output will be:
File: example.py
  Class: ExampleClass1 (1:0)
    Function: func1 2/3 66.67%
    Function: func2 1/3 33.33%
    Function: func3 staticmethod
    Total: 33.33%
  Class: ExampleClass2 (22:0)
    Function: func1 1/1 100.00%
    Total: 100.00%
  1. then run cohesion --below 100 -f example.py, output will not change
File: example.py
  Class: ExampleClass1 (1:0)
    Function: func1 2/3 66.67%
    Function: func2 1/3 33.33%
    Function: func3 staticmethod
    Total: 33.33%
  Class: ExampleClass2 (22:0)
    Function: func1 1/1 100.00%
    Total: 100.00%

However, 100 is not below 100. It is equal.

Just for the reference, running cohesion --below 99 -f example.py:

File: example.py
  Class: ExampleClass1 (1:0)
    Function: func1 2/3 66.67%
    Function: func2 1/3 33.33%
    Function: func3 staticmethod
    Total: 33.33%

It works well.

Add mechanism for filtering cohesion results

E.g. if we only want to see classes or modules that meet some criteria, like high cohesion percentage than X%.

We'll probably want a way to perform this via library calls and a way to specify it from the command line.

Cohesion does not produce the same results with debug on and off

For example:

$ cat example.py 
class ExampleClass1(object):
    class_variable1 = 5
    class_variable2 = 6

    def func1(self):
        self.instance_variable = 6

        def inner_func(b):
            return b + 5

        local_variable = self.class_variable1

        return local_variable

    def func2(self):
        print(self.class_variable2)

    @staticmethod
    def func3(variable):
        return variable + 7

class ExampleClass2(object):
    def func1(self):
        self.instance_variable1 = 7

Without the --debug flag:

$ PYTHONPATH=lib/ python -m cohesion -f example.py 
File: example.py
  Class: ExampleClass2 (22:0)
    Function: func1 1/1 100.00%
    Total: 100.00%
  Class: ExampleClass1 (1:0)
    Function: func3 staticmethod
    Function: func2 1/3 33.33%
    Function: func1 2/3 66.67%
    Total: 33.33%

With the --debug flag:

$ PYTHONPATH=lib/ python -m cohesion --debug -f example.py 
{
    "ExampleClass2": {
        "cohesion": null,
        "variables": [
            "instance_variable1"
        ],
        "lineno": 22,
        "col_offset": 0,
        "functions": {
            "func1": {
                "classmethod": false,
                "variables": [
                    "instance_variable1"
                ],
                "bounded": true,
                "staticmethod": false
            }
        }
    },
    "ExampleClass1": {
        "cohesion": null,
        "variables": [
            "class_variable2",
            "class_variable1",
            "instance_variable"
        ],
        "lineno": 1,
        "col_offset": 0,
        "functions": {
            "func3": {
                "classmethod": false,
                "variables": [],
                "bounded": false,
                "staticmethod": true
            },
            "func2": {
                "classmethod": false,
                "variables": [
                    "class_variable2"
                ],
                "bounded": true,
                "staticmethod": false
            },
            "func1": {
                "classmethod": false,
                "variables": [
                    "class_variable1",
                    "instance_variable"
                ],
                "bounded": true,
                "staticmethod": false
            }
        }
    }
}

Notice the cohesion value is null with debugging turned on. It looks like this value is calculated differently depending on if debugging is turned on. We should remedy this.

Unable to specify `cohesion-below` option via `setup.cfg`

Hi, thanks for this plugin. I am working on integrating it into our linter: wemake-services/wemake-python-styleguide#134

But, currently there's an issue with configuration.

_______________ FLAKE8-check(ignoring N802 D100 D104 D106 D401) ________________
multiprocessing.pool.RemoteTraceback: 
"""
Traceback (most recent call last):
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/multiprocessing/pool.py", line 119, in worker
    result = (True, func(*args, **kwds))
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/flake8/checker.py", line 648, in _run_checks
    return checker.run_checks()
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/flake8/checker.py", line 579, in run_checks
    self.run_ast_checks()
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/flake8/checker.py", line 493, in run_ast_checks
    for (line_number, offset, text, check) in runner:
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/cohesion/flake8_extension.py", line 44, in run
    file_module.filter_below(self.cohesion_below)
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/cohesion/module.py", line 79, in filter_below
    self._filter(predicate)
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/cohesion/module.py", line 43, in _filter
    for class_name, class_structure in self.structure.items()
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/cohesion/module.py", line 44, in <dictcomp>
    if predicate(class_name)
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/cohesion/module.py", line 77, in predicate
    return operator.le(class_percentage, percentage)
TypeError: '<=' not supported between instances of 'float' and 'str'
"""

The above exception was the direct cause of the following exception:
.venv/lib/python3.6/site-packages/pluggy/hooks.py:258: in __call__
    return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
.venv/lib/python3.6/site-packages/pluggy/manager.py:67: in _hookexec
    return self._inner_hookexec(hook, methods, kwargs)
.venv/lib/python3.6/site-packages/pluggy/manager.py:61: in <lambda>
    firstresult=hook.spec_opts.get('firstresult'),
.venv/lib/python3.6/site-packages/_pytest/runner.py:111: in pytest_runtest_call
    item.runtest()
.venv/lib/python3.6/site-packages/pytest_flake8.py:117: in runtest
    self.statistics)
.venv/lib/python3.6/site-packages/py/_io/capture.py:150: in call
    res = func(*args, **kwargs)
.venv/lib/python3.6/site-packages/pytest_flake8.py:193: in check_file
    app.run_checks([str(path)])
.venv/lib/python3.6/site-packages/flake8/main/application.py:310: in run_checks
    self.file_checker_manager.run()
.venv/lib/python3.6/site-packages/flake8/checker.py:319: in run
    self.run_parallel()
.venv/lib/python3.6/site-packages/flake8/checker.py:288: in run_parallel
    for ret in pool_map:
/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/multiprocessing/pool.py:735: in next
    raise value
E   TypeError: '<=' not supported between instances of 'float' and 'str'

Here's how to reproduce it.

  1. install flake8 and cohesion
  2. create setup.cfg with the following contents:
[flake8]
cohesion-below = 100.0
  1. Run flake8 on any file with a class inside, for example the one from the readme.

Here's related flake8 information:

{
  "dependencies": [
    {
      "dependency": "setuptools",
      "version": "39.0.1"
    }
  ],
  "platform": {
    "python_implementation": "CPython",
    "python_version": "3.6.5",
    "system": "Darwin"
  },
  "plugins": [
    {
      "is_local": false,
      "plugin": "cohesion",
      "version": "0.8.0"
    },
    {
      "is_local": false,
      "plugin": "pycodestyle",
      "version": "2.3.1"
    },
    {
      "is_local": false,
      "plugin": "pyflakes",
      "version": "1.6.0"
    }
  ],
  "version": "3.5.0"
}

Function and class parsing think member methods are member variables

$ cat example.py 
class ExampleClass1(object):
    def func1(self):
        print("HAI")

    def func4(self):
        self.func1()
$ cohesion -f example.py -x
{
    "ExampleClass1": {
        "variables": [
            "func1"
        ],
        "functions": {
            "func1": {
                "classmethod": false,
                "variables": [],
                "bounded": true,
                "staticmethod": false
            },
            "func4": {
                "classmethod": false,
                "variables": [
                    "func1"
                ],
                "bounded": true,
                "staticmethod": false
            }
        }
    }
}

ValueError: 'float' is not callable

This might be from a change in Python 3.11 or flake 6, but I think this line should be: 'type': float,:

If not, I get the below error:

Traceback (most recent call last):
  File "./.venv/bin/flake8", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "./.venv/lib/python3.11/site-packages/flake8/main/cli.py", line 23, in main
    app.run(argv)
  File "./.venv/lib/python3.11/site-packages/flake8/main/application.py", line 198, in run
    self._run(argv)
  File "./.venv/lib/python3.11/site-packages/flake8/main/application.py", line 186, in _run
    self.initialize(argv)
  File "./.venv/lib/python3.11/site-packages/flake8/main/application.py", line 165, in initialize
    self.plugins, self.options = parse_args(argv)
                                 ^^^^^^^^^^^^^^^^
  File "./.venv/lib/python3.11/site-packages/flake8/options/parse_args.py", line 51, in parse_args
    option_manager.register_plugins(plugins)
  File "./.venv/lib/python3.11/site-packages/flake8/options/manager.py", line 259, in register_plugins
    add_options(self)
  File "./.venv/lib/python3.11/site-packages/cohesion/flake8_extension.py", line 36, in add_options
    parser.add_option(flag, **kwargs)
  File "./.venv/lib/python3.11/site-packages/flake8/options/manager.py", line 282, in add_option
    self._current_group.add_argument(*option_args, **option_kwargs)
  File "/opt/homebrew/Cellar/[email protected]/3.11.1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/argparse.py", line 1448, in add_argument
    raise ValueError('%r is not callable' % (type_func,))
ValueError: 'float' is not callable

When self._current_group.add_argument(*option_args, **option_kwargs) are:

['--cohesion-below']
{'action': 'store', 'default': 50.0, 'type': 'float', 'help': 'only show cohesion results with this percentage or lower'}

Environment:

Python 3.11.2
flake8==6.0.0
cohesion==1.0.0

Skip empty classes

The cohesion calculation should also be for classes that contain either no methods or only static, class or abstract methods, because the cohesion will be 0 %. This is annoying when dealing with abstract classes that will implement methods on subclasses.

Shouldn't we expand calls to class methods when evaluating cohesion?

Suppose you have this module:

class Data(object):
    def __init__(self, data):
        if not isinstance(data, list):
            raise ValueError('Data: Input arg must be list')
        self.data = data

    def get_value_at_start(self):
        return self.get_value_at_index(0)

    def get_value_at_index(self, index):
        return self.data[index]

data = Data([1, 3, 5, 7])
print 'Data 0: {}'.format(data.get_value_at_start())
print 'Data 2: {}'.format(data.get_value_at_index(2))

cohesion gives 0.00% for Function: get_value_at_start. IMO this is incorrect. By the definition you have in your documentation (extracted from wikipedia): "[...] cohesion refers to the degree to which the elements of a module belong together". Getting a cohesion of 0% would mean that the method does not belong to the class. However, it does; it's only that we're avoiding code repetition. If the function did 'return self.data[0]' we'd get cohesion score=100%, but this would go against good practices, because if later we change something in how we access data points, we need to change it in two places.
What do you think?

Skip abstract methods

Cohesion calculation should be skipped for abstract methods because the cohesion will return 0 %.

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.