Code Monkey home page Code Monkey logo

mr_proper's Introduction

mr. Proper

Build Status PyPI version Maintainability Test Coverage

Static Python code analyzer, that tries to check if functions in code are pure or not and why.

Have fun with mr Clean

DISCLAIMER: this library is very experimental and has a lot of edge cases. Functions that mr. Proper marks as pure can be not pure, but they are usually cleaner than other functions.

Installation

pip install mr_proper

What mr. Proper check

  1. that function has no blacklisted calls (like print) and blacklisted attributes access (like smth.count);
  2. that function not uses global objects (only local vars and function arguments);
  3. that function has al least one return;
  4. that function not mutates it's arguments;
  5. that function has no local imports;
  6. that function has no arguments of forbidden types (like ORM objects);
  7. that function not uses self, class or super;
  8. that function has calls of only pure functions.

This list is not enough to say that function is pure and some points are quite controversial, but it's a nice start.

Example

Console usage:

    # test.py
    def add_one(n: int) -> int:
        return n + 1

    def print_amount_of_users(users_qs: QuerySet) -> None:
        print(f'Current amount of users is {users_qs.count()}')
$ mr_propper test.py
add_one is pure!
print_amount_of_users is not pure because of:
    it uses forbidden argument types (QuerySet)
    it calls not pure functions (print)
    it has no return

Usage inside Python code sample:

>>> import ast
>>> from mr_propper.utils import is_function_pure
>>> funcdef = ast.parse('''
    def add_one(n: int) -> int:
        return n + 1
''').body[0]
>>> is_function_pure(funcdef)
True
>>> is_function_pure(funcdef, with_errors=True)
(True, [])

Parameters

CLI interface:

  • filepath: path to .py file to check (directories are not supported for now);
  • --recursive: require inner calls to be pure for function pureness.

Code prerequisites

  1. Python 3.7+;
  2. Functions are fully type-annotated;
  3. No dynamic calls (like getattr(sender, 'send_' + message_type)(message)).

Contributing

We would love you to contribute to our project. It's simple:

  1. Create an issue with bug you found or proposal you have. Wait for approve from maintainer.
  2. Create a pull request. Make sure all checks are green.
  3. Fix review comments if any.
  4. Be awesome.

Here are useful tips:

mr_proper's People

Contributors

arondit avatar dodgyturtle avatar edwardbetts avatar korneevm avatar kyleking avatar lgroux avatar mcproger avatar melevir avatar sutorei avatar vquadx avatar

Stargazers

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

Watchers

 avatar  avatar  avatar

mr_proper's Issues

Add config option for `uses_only_args_and_local_vars/allow_external_class_usage`

Is your feature request related to a problem? Please describe.

I get these errors on my "pure" functions:

_combine is not pure because of:
	it uses external name (entries)
	it uses external name (Language)
	it uses external name (phrase)
	it uses external name (defn)
	it uses external name (lang)
	it uses external name (Definition)

IMO, these are false positives. It looks like setting allow_external_class_usage to True would resolve some of these. (?) The option was coded, but not accessible from the CLI.

Some, though, seem to be an inability to understand Python 3.10 match and type hints:

def _combine(*entries: Iterable[Entry]) -> Iterable[Entry]:
    """Combine single-entry terms with the same phrase."""

    all_entries = cast(Iterable[Entry], concat(entries))

    term_dict: dict[tuple[str, Language], list[Definition]] = defaultdict(list)

    for entry in sorted(all_entries):
        match(entry.definitions):
            case [defn]:
                key = (entry.phrase, entry.language)
                term_dict[key].append(defn)
            case _:
                raise ValueError(f"Entry expected to have exactly one definition: {entry}")

    for (phrase, lang), defns in term_dict.items():
        yield Entry(phrase, defns, lang)

Add directory processing, not per file

Now mr_proper accepts filename argument and checks only one file. We should specify directory_or_filename parameter.

If directory is specified, all files in the dir should be checked.

Manually check mr_proper on 200+ python repos

To be sure, that mr_proper handles most of widely used code cases in not breaks, we should write
a script, that:

  • Downloads 200+ python repos from Github.
  • Runs mr_proper on all of them.
  • Checks that mr_proper not breaks with exception.

Config of the maximum number of function arguments

How about limiting the number of arguments to a function? When a function seems to need more than two or three arguments, it is likely that some of those arguments ought to be wrapped into a function of their own. This will help to avoid situations with large function signatures and will certainly increase readability, as well as separation of logic.

Missing `mypy_extensions` Upstream Dependency

Describe the bug

The upstream dependency mypy-extensions was removed in the commit - 613ff72

Usage of this dependency in code -

from mypy_extensions import TypedDict

To Reproduce

Steps to reproduce the behavior:

  1. Install flake8-functions==0.0.5
  2. Run it on any *.py file

Stack Trace

/usr/local/lib/python3.8/site-packages/flake8/plugins/manager.py:157: in load_plugin
    self._load()
/usr/local/lib/python3.8/site-packages/flake8/plugins/manager.py:134: in _load
    self._plugin = self.entry_point.load()
/usr/local/lib/python3.8/importlib/metadata.py:77: in load
    module = import_module(match.group('module'))
/usr/local/lib/python3.8/importlib/__init__.py:127: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
<frozen importlib._bootstrap>:1014: in _gcd_import
    ???
<frozen importlib._bootstrap>:991: in _find_and_load
    ???
<frozen importlib._bootstrap>:975: in _find_and_load_unlocked
    ???
<frozen importlib._bootstrap>:671: in _load_unlocked
    ???
<frozen importlib._bootstrap_external>:783: in exec_module
    ???
<frozen importlib._bootstrap>:219: in _call_with_frames_removed
    ???
/usr/local/lib/python3.8/site-packages/flake8_functions/checker.py:6: in <module>
    from flake8_functions.function_purity import check_purity_of_functions
/usr/local/lib/python3.8/site-packages/flake8_functions/function_purity.py:5: in <module>
    from mr_proper.public_api import is_function_pure
/usr/local/lib/python3.8/site-packages/mr_proper/public_api.py:13: in <module>
    from mr_proper.utils.ast_pure import get_not_pure_internal_calls
/usr/local/lib/python3.8/site-packages/mr_proper/utils/ast_pure.py:6: in <module>
    from mypy_extensions import TypedDict
E   ModuleNotFoundError: No module named 'mypy_extensions'

During handling of the above exception, another exception occurred:
/usr/local/lib/python3.8/site-packages/pluggy/hooks.py:286: in __call__
    return self._hookexec(self, self.get_hookimpls(), kwargs)
/usr/local/lib/python3.8/site-packages/pluggy/manager.py:93: in _hookexec
    return self._inner_hookexec(hook, methods, kwargs)
/usr/local/lib/python3.8/site-packages/pluggy/manager.py:84: in <lambda>
    self._inner_hookexec = lambda hook, methods, kwargs: hook.multicall(
/usr/local/lib/python3.8/site-packages/_pytest/runner.py:171: in pytest_runtest_call
    raise e
/usr/local/lib/python3.8/site-packages/_pytest/runner.py:163: in pytest_runtest_call
    item.runtest()
/usr/local/lib/python3.8/site-packages/pytest_flake8.py:120: in runtest
    found_errors, out, err = call(
/usr/local/lib/python3.8/site-packages/py/_io/capture.py:150: in call
    res = func(*args, **kwargs)
/usr/local/lib/python3.8/site-packages/pytest_flake8.py:203: in check_file
    app.find_plugins(config_finder)
/usr/local/lib/python3.8/site-packages/flake8/main/application.py:159: in find_plugins
    self.check_plugins.load_plugins()
/usr/local/lib/python3.8/site-packages/flake8/plugins/manager.py:415: in load_plugins
    plugins = list(self.manager.map(load_plugin))
/usr/local/lib/python3.8/site-packages/flake8/plugins/manager.py:302: in map
    yield func(self.plugins[name], *args, **kwargs)
/usr/local/lib/python3.8/site-packages/flake8/plugins/manager.py:413: in load_plugin
    return plugin.load_plugin()
/usr/local/lib/python3.8/site-packages/flake8/plugins/manager.py:164: in load_plugin
    raise failed_to_load
E   flake8.exceptions.FailedToLoadPlugin: Flake8 failed to load plugin "CFQ" due to No module named 'mypy_extensions'.

Allow `_` usage

Now if function uses _ as var name, it considered not pure (it uses external name (_))

functions using context managers wrongly detected as not pure

Describe the bug
Variables assigned from contextmanager are not detected as local variables.

To Reproduce
run

from contextlib import contextmanager
from mr_proper.main import check_file


@contextmanager
def foo():
    yield "foobar"


def main():
    with foo() as f:
        return f


if __name__ == "__main__":
    check_file(__file__, recursive=True)

This gives the ouput

foo is pure!
main is not pure because of:
        it uses external name (f)

Expected behavior
main should be detected a pure

Incorrect Package Description

Hi, I ran into issue #13 and while tracing the error, I was temporarily confused by the package description in the lock file until I read the full README. I think you might want to change the description to Static Python code analyzer that tries to check if functions in code are pure or not and why.

description='Package to find typos in russian text.',

(The description is also shown on PyPi: https://pypi.org/project/mr-proper/#history)

Add --exit-zero kwarg

If mr_proper finds not pure function, it should exit with non-zero code. If --exit-zero is specified, it should always exit with zero code.

Repo is missing a LICENSE file

and so does the pypi release tarball.
It would be really nice to know the exact terms and conditions this library is licensed under.

But I assume that the MIT set in pypi meta data is correct for now

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.