Skip to content

Commit

Permalink
Merge pull request #5 from dumbturtle/issue/#2
Browse files Browse the repository at this point in the history
Issue/#2
  • Loading branch information
Melevir authored May 18, 2020
2 parents 567b922 + 557296f commit 7c4ad05
Show file tree
Hide file tree
Showing 11 changed files with 124 additions and 89 deletions.
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
language: python
python:
- "3.6"
- "3.7"
- "3.8"
install:
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The validator checks for:

* CFQ001 - function length (default max length is 100)
* CFQ002 - function arguments number (default max arguments amount is 6)
* CFQ003 - function is not pure.

## Installation

Expand Down Expand Up @@ -72,6 +73,11 @@ that exceeds max allowed length 20
|:----------:|:--------------------------------------------------------------------------------------------------:|
| CFQ001 | Function "some_function" has length %function_length% that exceeds max allowed length %max_length% |
| CFQ002 | Function "some_function" has %args_amount% arguments that exceeds max allowed %max_args_amount% |
| CFQ003 | Function "some_function" is not pure. |

## Code prerequisites

1. Python 3.7+;

## Contributing

Expand Down
101 changes: 15 additions & 86 deletions flake8_functions/checker.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import ast
from typing import Generator, Tuple, List, Dict, Any, Union
import functools
from typing import Generator, Tuple, Union, List

from flake8_functions import __version__ as version

from flake8_functions.function_purity import check_purity_of_functions
from flake8_functions.function_lenght import get_length_errors
from flake8_functions.function_arguments_amount import get_arguments_amount_error

AnyFuncdef = Union[ast.FunctionDef, ast.AsyncFunctionDef]

Expand All @@ -21,81 +24,6 @@ def __init__(self, tree, filename: str):
self.filename = filename
self.tree = tree

@staticmethod
def _get_length_errors(
func_def_info: Dict[str, Any],
max_function_length: int,
) -> List[Tuple[int, int, str]]:
errors = []
if func_def_info['length'] > max_function_length:
errors.append((
func_def_info['lineno'],
func_def_info['col_offset'],
'CFQ001 Function "{0}" has length {1} that exceeds max allowed length {2}'.format(
func_def_info['name'],
func_def_info['length'],
max_function_length,
),
))
return errors

@staticmethod
def _get_arguments_amount_for(func_def: AnyFuncdef) -> int:
arguments_amount = 0
args = func_def.args
arguments_amount += len(args.args) + len(args.kwonlyargs)
if args.vararg:
arguments_amount += 1
if args.kwarg:
arguments_amount += 1
return arguments_amount

@staticmethod
def _get_function_start_row(func_def: AnyFuncdef) -> int:
first_meaningful_expression_index = 0
if (
isinstance(func_def.body[0], ast.Expr)
and isinstance(func_def.body[0].value, ast.Str)
and len(func_def.body) > 1
): # First expression is docstring - we ignore it
first_meaningful_expression_index = 1
return func_def.body[first_meaningful_expression_index].lineno

@staticmethod
def _get_function_last_row(func_def: AnyFuncdef) -> int:
function_last_line = 0
for statement in ast.walk(func_def):
if hasattr(statement, 'lineno'):
function_last_line = max(statement.lineno, function_last_line)

return function_last_line

@classmethod
def _get_function_length(
cls,
func_def: Union[ast.FunctionDef, ast.AsyncFunctionDef],
) -> Dict[str, Any]:
function_start_row = cls._get_function_start_row(func_def)
function_last_row = cls._get_function_last_row(func_def)
func_def_info = {
'name': func_def.name,
'lineno': func_def.lineno,
'col_offset': func_def.col_offset,
'length': function_last_row - function_start_row + 1,
}
return func_def_info

@classmethod
def _get_arguments_amount_error(cls, func_def: AnyFuncdef, max_parameters_amount: int) -> Tuple[int, int, str]:
arguments_amount = cls._get_arguments_amount_for(func_def)
if arguments_amount > max_parameters_amount:
return (
func_def.lineno,
func_def.col_offset,
f'CFQ002 Function "{func_def.name}" has {arguments_amount} arguments'
f' that exceeds max allowed {cls.max_parameters_amount}',
)

@classmethod
def add_options(cls, parser) -> None:
parser.add_option(
Expand All @@ -117,17 +45,18 @@ def parse_options(cls, options) -> None:
cls.max_parameters_amount = int(options.max_parameters_amount)

def run(self) -> Generator[Tuple[int, int, str, type], None, None]:
validators: List = [
functools.partial(get_arguments_amount_error, max_parameters_amount=self.max_parameters_amount),
functools.partial(get_length_errors, max_function_length=self.max_function_length),
check_purity_of_functions,
]
functions = [
n for n in ast.walk(self.tree)
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
]
for func_def in functions:
func_def_info = self._get_function_length(func_def)
for lineno, col_offset, error_msg in self._get_length_errors(
func_def_info, self.max_function_length,
):
yield lineno, col_offset, error_msg, type(self)
error_info = self._get_arguments_amount_error(func_def, self.max_parameters_amount)
if error_info:
full_error_info = *error_info, type(self)
yield full_error_info
for validator_callable in validators:
validator_errors: Tuple[int, int, str] = validator_callable(func_def)
if validator_errors:
full_error_info = *validator_errors, type(self)
yield full_error_info
26 changes: 26 additions & 0 deletions flake8_functions/function_arguments_amount.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import ast
from typing import Tuple, Union

AnyFuncdef = Union[ast.FunctionDef, ast.AsyncFunctionDef]


def get_arguments_amount_for(func_def: AnyFuncdef) -> int:
arguments_amount = 0
args = func_def.args
arguments_amount += len(args.args) + len(args.kwonlyargs)
if args.vararg:
arguments_amount += 1
if args.kwarg:
arguments_amount += 1
return arguments_amount


def get_arguments_amount_error(func_def: AnyFuncdef, max_parameters_amount: int) -> Tuple[int, int, str]:
arguments_amount = get_arguments_amount_for(func_def)
if arguments_amount > max_parameters_amount:
return (
func_def.lineno,
func_def.col_offset,
f'CFQ002 Function "{func_def.name}" has {arguments_amount} arguments'
f' that exceeds max allowed {max_parameters_amount}',
)
38 changes: 38 additions & 0 deletions flake8_functions/function_lenght.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@

import ast
from typing import Tuple, Union

AnyFuncdef = Union[ast.FunctionDef, ast.AsyncFunctionDef]


def get_function_start_row(func_def: AnyFuncdef) -> int:
first_meaningful_expression_index = 0
if (
isinstance(func_def.body[0], ast.Expr)
and isinstance(func_def.body[0].value, ast.Str)
and len(func_def.body) > 1
): # First expression is docstring - we ignore it
first_meaningful_expression_index = 1
return func_def.body[first_meaningful_expression_index].lineno


def get_function_last_row(func_def: AnyFuncdef) -> int:
function_last_line = 0
for statement in ast.walk(func_def):
if hasattr(statement, 'lineno'):
function_last_line = max(statement.lineno, function_last_line)

return function_last_line


def get_length_errors(func_def: AnyFuncdef, max_function_length: int) -> Tuple[int, int, str]:
function_start_row = get_function_start_row(func_def)
function_last_row = get_function_last_row(func_def)
function_lenght = function_last_row - function_start_row + 1
if function_lenght > max_function_length:
return (
func_def.lineno,
func_def.col_offset,
f'CFQ001 Function {func_def.name} has length {function_lenght}'
f' that exceeds max allowed length {max_function_length}',
)
17 changes: 17 additions & 0 deletions flake8_functions/function_purity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import ast

from typing import Union, Tuple

from mr_proper.public_api import is_function_pure


AnyFuncdef = Union[ast.FunctionDef, ast.AsyncFunctionDef]


def check_purity_of_functions(func_def: AnyFuncdef) -> Tuple[int, int, str]:
if 'pure' in func_def.name.split('_') and not is_function_pure(func_def):
return (
func_def.lineno,
func_def.col_offset,
f'CFQ003 Function "{func_def.name}" is not pure.',
)
2 changes: 1 addition & 1 deletion requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
pytest==5.4.1
pytest-cov==2.8.1
pydocstyle==5.0.2
flake8==3.7.9
Expand All @@ -22,6 +21,7 @@ flake8-string-format==0.3.0
flake8-tidy-imports==4.1.0
flake8-typing-imports==1.9.0
flake8-variables-names==0.0.3
mr-proper==0.0.5
mypy==0.770
mypy-extensions==0.4.3
safety==1.9.0
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def get_long_description() -> str:
version=get_version(),
author='Valery Pavlov',
author_email='lerikpav@gmail.com',
install_requires=['setuptools', 'pytest'],
install_requires=['setuptools', 'pytest', 'mr-proper'],
entry_points={
'flake8.extension': [
'CFQ = flake8_functions.checker:FunctionChecker',
Expand Down
2 changes: 2 additions & 0 deletions tests/test_files/file_not_pure_function.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def not_pure_function(users_qs: QuerySet) -> None:
print(f'Current amount of users is {users_qs.count()}')
2 changes: 2 additions & 0 deletions tests/test_files/file_pure_function.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def pure_function(n: int) -> int:
return n + 1
16 changes: 16 additions & 0 deletions tests/test_max_function_length.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,19 @@ def test_dosctrings_are_not_part_of_function_length(max_function_length, expecte
)

assert len(errors) == expected_errors_count


def test_function_purity():
errors = run_validator_for_test_file(
filename='file_pure_function.py',
)

assert len(errors) == 0


def test_not_function_purity():
errors = run_validator_for_test_file(
filename='file_not_pure_function.py',
)

assert len(errors) == 1

0 comments on commit 7c4ad05

Please sign in to comment.