Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SCR-119: Add #1

Merged
merged 2 commits into from Mar 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/lint.yml
Expand Up @@ -5,9 +5,9 @@ name: Lint

on:
push:
branches: [ master ]
branches: [ main ]
pull_request:
branches: [ master ]
branches: [ main ]

jobs:
build:
Expand All @@ -26,7 +26,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements/lint.txt
pip install -r requirements/ci.txt
pip install .
- name: Test with mypy
run: |
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/unit-tests.yml
Expand Up @@ -5,9 +5,9 @@ name: Unit Tests

on:
push:
branches: [ master ]
branches: [ main ]
pull_request:
branches: [ master ]
branches: [ main ]

jobs:
build:
Expand Down
2 changes: 1 addition & 1 deletion .isort.cfg
Expand Up @@ -3,5 +3,5 @@ line_length=79
indent=' '
multi_line_output=3
length_sort=0
known_third_party =flake8_simplify,setuptools
known_third_party =flake8_simplify,pytest,setuptools
include_trailing_comma=True
36 changes: 36 additions & 0 deletions README.md
Expand Up @@ -6,3 +6,39 @@
# flake8-scream

A [flake8](https://flake8.pycqa.org/en/latest/index.html) plugin that helps you scream your code.


## Rules

* [`SCR119`](https://github.com/MartinThoma/flake8-simplify/issues/37) ![](https://shields.io/badge/-legacyfix-inactive): Use dataclasses for data containers ([example](#SCR119))


## Disabling Rules

You might have good reasons to
[ignore some flake8 rules](https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html).
To do that, use the standard Flake8 configuration. For example, within the `setup.cfg` file:

```python
[flake8]
ignore = SCR106, SCR113, SCR119, SCR9
```

## Examples

### SCR119

Dataclasses were introduced with [PEP 557](https://www.python.org/dev/peps/pep-0557/)
in Python 3.7. The main reason not to use dataclasses is to support legacy Python versions.

Dataclasses create a lot of the boilerplate code for you:

* `__init__`
* `__eq__`
* `__hash__`
* `__str__`
* `__repr__`

A lot of projects use them:

* [black](https://github.com/psf/black/blob/master/src/black/__init__.py#L1472)
5 changes: 5 additions & 0 deletions flake8_scream/__init__.py
Expand Up @@ -5,6 +5,7 @@

from flake8_simplify.utils import UnaryOp

from flake8_scream.rules.ast_classdef import get_scr119
from flake8_scream.rules.ast_unary_op import (
get_scr204,
get_scr205,
Expand Down Expand Up @@ -35,6 +36,10 @@ def visit_UnaryOp(self, node_v: ast.UnaryOp) -> None:
self.errors += get_scr207(node)
self.generic_visit(node)

def visit_ClassDef(self, node: ast.ClassDef) -> None:
self.errors += get_scr119(node)
self.generic_visit(node)


class Plugin:
name = __name__
Expand Down
84 changes: 84 additions & 0 deletions flake8_scream/rules/ast_classdef.py
@@ -0,0 +1,84 @@
import ast
from typing import List, Tuple


def get_scr119(node: ast.ClassDef) -> List[Tuple[int, int, str]]:
"""
Get a list of all classes that should be dataclasses"

ClassDef(
name='Person',
bases=[],
keywords=[],
body=[
AnnAssign(
target=Name(id='first_name', ctx=Store()),
annotation=Name(id='str', ctx=Load()),
value=None,
simple=1,
),
AnnAssign(
target=Name(id='last_name', ctx=Store()),
annotation=Name(id='str', ctx=Load()),
value=None,
simple=1,
),
AnnAssign(
target=Name(id='birthdate', ctx=Store()),
annotation=Name(id='date', ctx=Load()),
value=None,
simple=1,
),
],
decorator_list=[Name(id='dataclass', ctx=Load())],
)
"""
RULE = "SCR119 Use a dataclass for 'class {classname}'"
errors: List[Tuple[int, int, str]] = []

if not (len(node.decorator_list) == 0 and len(node.bases) == 0):
return errors

dataclass_functions = [
"__init__",
"__eq__",
"__hash__",
"__repr__",
"__str__",
]
has_only_dataclass_functions = True
has_any_functions = False
has_complex_statements = False
for body_el in node.body:
if isinstance(body_el, (ast.FunctionDef, ast.AsyncFunctionDef)):
has_any_functions = True
if body_el.name == "__init__":
# Ensure constructor only has pure assignments
# without any calculation.
for el in body_el.body:
if not isinstance(el, ast.Assign):
has_complex_statements = True
break
# It is an assignment, but we only allow
# `self.attribute = name`.
if any(
[
not isinstance(target, ast.Attribute)
for target in el.targets
]
) or not isinstance(el.value, ast.Name):
has_complex_statements = True
break
if body_el.name not in dataclass_functions:
has_only_dataclass_functions = False

if (
has_any_functions
and has_only_dataclass_functions
and not has_complex_statements
):
errors.append(
(node.lineno, node.col_offset, RULE.format(classname=node.name))
)

return errors
16 changes: 12 additions & 4 deletions flake8_scream/rules/ast_unary_op.py
Expand Up @@ -22,7 +22,9 @@ def get_scr204(node: UnaryOp) -> List[Tuple[int, int, str]]:
comparison = node.operand
left = to_source(comparison.left)
right = to_source(comparison.comparators[0])
errors.append((node.lineno, node.col_offset, SCR204.format(a=left, b=right)))
errors.append(
(node.lineno, node.col_offset, SCR204.format(a=left, b=right))
)
return errors


Expand All @@ -44,7 +46,9 @@ def get_scr205(node: UnaryOp) -> List[Tuple[int, int, str]]:
comparison = node.operand
left = to_source(comparison.left)
right = to_source(comparison.comparators[0])
errors.append((node.lineno, node.col_offset, SCR205.format(a=left, b=right)))
errors.append(
(node.lineno, node.col_offset, SCR205.format(a=left, b=right))
)
return errors


Expand All @@ -66,7 +70,9 @@ def get_scr206(node: UnaryOp) -> List[Tuple[int, int, str]]:
comparison = node.operand
left = to_source(comparison.left)
right = to_source(comparison.comparators[0])
errors.append((node.lineno, node.col_offset, SCR206.format(a=left, b=right)))
errors.append(
(node.lineno, node.col_offset, SCR206.format(a=left, b=right))
)
return errors


Expand All @@ -88,5 +94,7 @@ def get_scr207(node: UnaryOp) -> List[Tuple[int, int, str]]:
comparison = node.operand
left = to_source(comparison.left)
right = to_source(comparison.comparators[0])
errors.append((node.lineno, node.col_offset, SCR207.format(a=left, b=right)))
errors.append(
(node.lineno, node.col_offset, SCR207.format(a=left, b=right))
)
return errors
4 changes: 4 additions & 0 deletions pyproject.toml
@@ -1,3 +1,7 @@
[tool.black]
line-length = 79
target-version = ['py37']

[tool.pytest.ini_options]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
Expand Down
101 changes: 101 additions & 0 deletions tests/test_100_rules.py
@@ -0,0 +1,101 @@
import pytest

from tests import _results


def test_scr119():
results = _results(
"""
class FooBar:
def __init__(self, a, b):
self.a = a
self.b = b
"""
)
assert results == {"2:0 SCR119 Use a dataclass for 'class FooBar'"}


def test_scr119_ignored_dunder_methods():
"""
Dunder methods do not make a class not be a dataclass candidate.
Examples for dunder (double underscore) methods are:
* __str__
* __eq__
* __hash__
"""
results = _results(
"""
class FooBar:
def __init__(self, a, b):
self.a = a
self.b = b

def __str__(self):
return "FooBar"
"""
)
assert results == {"2:0 SCR119 Use a dataclass for 'class FooBar'"}


@pytest.mark.xfail(
reason="https://github.com/MartinThoma/flake8-simplify/issues/63"
)
def test_scr119_false_positive():
results = _results(
'''class OfType:
"""
>>> 3 == OfType(int, str, bool)
True
>>> 'txt' == OfType(int)
False
"""

def __init__(self, *types):
self.types = types

def __eq__(self, other):
return isinstance(other, self.types)'''
)
for el in results:
assert "SCR119" not in el


def test_scr119_async():
results = _results(
"""
class FooBar:
def __init__(self, a, b):
self.a = a
self.b = b

async def foo(self):
return "FooBar"
"""
)
assert results == set()


def test_scr119_constructor_processing():
results = _results(
"""
class FooBar:
def __init__(self, a):
self.a = a + 5
"""
)
assert results == set()


def test_scr119_pydantic():
results = _results(
"""
from pydantic import BaseModel

class FooBar(BaseModel):
foo : str

class Config:
extra = "allow"
"""
)
assert results == set()