Skip to content

Commit

Permalink
add test and init method detection
Browse files Browse the repository at this point in the history
  • Loading branch information
Илья Лебедев committed Feb 15, 2019
1 parent 6c1624b commit dac5a8a
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 58 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
check:
flake8 .
mypy .
python -m pytest --cov=flake8_class_attributes_order --cov-report=xml
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ class PhoneForm(forms.Form):
## Example

```python
DEBUG = True


class User:
def fetch_info_from_crm(self):
pass
Expand All @@ -49,7 +52,7 @@ class UserNode:
class Meta:
model = User

if settings.DEBUG: # not great idea at all
if DEBUG: # not great idea at all
def is_synced_with_crm(self):
pass

Expand Down
115 changes: 59 additions & 56 deletions flake8_class_attributes_order/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class ClassAttributesOrderChecker:
'outer_field': 25,
'if': 26,
'expression': 27,
'init_method': 28,
'str_method': 31,
'save_method': 32,
'delete_method': 33,
Expand All @@ -34,6 +35,7 @@ class ClassAttributesOrderChecker:
'outer_field': 25,
'if': 26,
'expression': 27,
'init_method': 28,
'str_method': 31,
'save_method': 32,
'delete_method': 33,
Expand All @@ -52,16 +54,55 @@ def __init__(self, tree, filename: str):
self.tree = tree
self.use_strict_mode = True

def run(self) -> Generator[Tuple[int, int, str, type], None, None]:
weight_info = self.STRICT_NODE_TYPE_WEIGHTS if self.use_strict_mode else self.NON_STRICT_NODE_TYPE_WEIGHTS
classes = [n for n in ast.walk(self.tree) if isinstance(n, ast.ClassDef)]
errors: List[Tuple[int, int, str]] = []
for class_def in classes:
model_parts_info = self._get_model_parts_info(class_def, weight_info)
errors += self._get_ordering_errors(model_parts_info)
@staticmethod
def _get_funcdef_type(child_node) -> str:
methods_names_to_types_map = {
'__str__': 'str_method',
'__init__': 'init_method',
'save': 'save_method',
'delete': 'delete_method',
}
decorator_names_to_types_map = {
'property': 'property_method',
'staticmethod': 'static_method',
'classmethod': 'class_method',
}
for decorator_info in child_node.decorator_list:
if (
isinstance(decorator_info, ast.Name)
and decorator_info.id in decorator_names_to_types_map
):
return decorator_names_to_types_map[decorator_info.id]
if child_node.name in methods_names_to_types_map:
return methods_names_to_types_map[child_node.name]
if child_node.name.startswith('_'):
return 'private_method'
return 'method'

for lineno, col_offset, error_msg in errors:
yield lineno, col_offset, error_msg, type(self)
@staticmethod
def _is_caps_lock_str(var_name: str) -> bool:
return var_name.upper() == var_name

@staticmethod
def _get_node_name(node, node_type: str):
name_getters_by_type = [
('docstring', lambda n: 'docstring'),
('meta_class', lambda n: 'Meta'),
('constant', lambda n: n.target.id if isinstance(n, ast.AnnAssign) else n.targets[0].id), # type: ignore
(
'field',
lambda n: n.target.id if isinstance(n, ast.AnnAssign) else ( # type: ignore
n.targets[0].id if isinstance(n.targets[0], ast.Name) else n.targets[0].attr
),
),
('method', lambda n: n.name),
('nested_class', lambda n: n.name),
('expression', lambda n: '<class_level_expression>'),
('if', lambda n: 'if ...'),
]
for type_postfix, name_getter in name_getters_by_type:
if node_type.endswith(type_postfix):
return name_getter(node)

@classmethod
def add_options(cls, parser) -> None:
Expand Down Expand Up @@ -157,51 +198,13 @@ def _get_ordering_errors(cls, model_parts_info) -> List[Tuple[int, int, str]]:
))
return errors

@staticmethod
def _get_funcdef_type(child_node) -> str:
methods_names_to_types_map = {
'__str__': 'str_method',
'save': 'save_method',
'delete': 'delete_method',
}
decorator_names_to_types_map = {
'property': 'property_method',
'staticmethod': 'static_method',
'classmethod': 'class_method',
}
for decorator_info in child_node.decorator_list:
if (
isinstance(decorator_info, ast.Name)
and decorator_info.id in decorator_names_to_types_map
):
return decorator_names_to_types_map[decorator_info.id]
if child_node.name in methods_names_to_types_map:
return methods_names_to_types_map[child_node.name]
if child_node.name.startswith('_'):
return 'private_method'
return 'method'

@staticmethod
def _is_caps_lock_str(var_name: str) -> bool:
return var_name.upper() == var_name
def run(self) -> Generator[Tuple[int, int, str, type], None, None]:
weight_info = self.STRICT_NODE_TYPE_WEIGHTS if self.use_strict_mode else self.NON_STRICT_NODE_TYPE_WEIGHTS
classes = [n for n in ast.walk(self.tree) if isinstance(n, ast.ClassDef)]
errors: List[Tuple[int, int, str]] = []
for class_def in classes:
model_parts_info = self._get_model_parts_info(class_def, weight_info)
errors += self._get_ordering_errors(model_parts_info)

@staticmethod
def _get_node_name(node, node_type: str):
name_getters_by_type = [
('docstring', lambda n: 'docstring'),
('meta_class', lambda n: 'Meta'),
('constant', lambda n: n.target.id if isinstance(n, ast.AnnAssign) else n.targets[0].id), # type: ignore
(
'field',
lambda n: n.target.id if isinstance(n, ast.AnnAssign) else ( # type: ignore
n.targets[0].id if isinstance(n.targets[0], ast.Name) else n.targets[0].attr
),
),
('method', lambda n: n.name),
('nested_class', lambda n: n.name),
('expression', lambda n: '<class_level_expression>'),
('if', lambda n: 'if ...'),
]
for type_postfix, name_getter in name_getters_by_type:
if node_type.endswith(type_postfix):
return name_getter(node)
for lineno, col_offset, error_msg in errors:
yield lineno, col_offset, error_msg, type(self)
1 change: 0 additions & 1 deletion requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ flake8-commas==2.0.0
flake8-comprehensions==1.4.1
flake8-debugger==3.1.0
flake8-docstrings==1.3.0
flake8-module-name==0.1.5
flake8-polyfill==1.0.2
flake8-print==3.1.0
flake8-quotes==1.0.0
Expand Down
20 changes: 20 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import ast
import os

from flake8_class_attributes_order.checker import ClassAttributesOrderChecker


def run_validator_for_test_file(filename, max_annotations_complexity=None):
test_file_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'test_files',
filename,
)
with open(test_file_path, 'r') as file_handler:
raw_content = file_handler.read()
tree = ast.parse(raw_content)
checker = ClassAttributesOrderChecker(tree=tree, filename=filename)
if max_annotations_complexity:
checker.max_annotations_complexity = max_annotations_complexity

return list(checker.run())
6 changes: 6 additions & 0 deletions tests/test_class_attributes_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from conftest import run_validator_for_test_file


def test_always_ok_for_empty_file():
errors = run_validator_for_test_file('errored.py')
assert len(errors) == 2
17 changes: 17 additions & 0 deletions tests/test_files/errored.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
DEBUG = True


class User:
def fetch_info_from_crm(self):
pass

LOGIN_FIELD = 'email' # wtf? this should on top of class definition!


class UserNode:
class Meta:
model = User

if DEBUG: # not great idea at all
def is_synced_with_crm(self):
pass

0 comments on commit dac5a8a

Please sign in to comment.