Repository: wemake-services/wemake-python-styleguide

Issue:
WPS440 Property false positive or missing solution
# Bug report

## What's wrong

The following code will raise WPS440 even though it is the pythonic way (as far as I understand the documentation):

```
    @property
    def last_timestamp(self) -> int:
        """Return last timestamp a frame was pre processed."""
        return self._last_timestamp

    @last_timestamp.setter
    def last_timestamp(self, last_timestamp: int):
        """Set the last timestamp a frame was pre processed."""
        self._last_timestamp = last_timestamp
```

## How is that should be

It should either not raise WPS440 or there should be a solution on how to deal with properties or more specifically property setters/getters.

## System information

### flake8 information

Contents of `flake8 --bug-report`:

```
{
  "dependencies": [
    {
      "dependency": "entrypoints",
      "version": "0.3"
    }
  ],
  "platform": {
    "python_implementation": "CPython",
    "python_version": "3.7.3",
    "system": "Windows"
  },
  "plugins": [
    {
      "is_local": false,
      "plugin": "flake8-annotations-complexity",
      "version": "0.0.2"
    },
    {
      "is_local": false,
      "plugin": "flake8-bandit",
      "version": "2.1.1"
    },
    {
      "is_local": false,
      "plugin": "flake8-broken-line",
      "version": "0.1.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-bugbear",
      "version": "19.3.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-comprehensions",
      "version": "2.1.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-debugger",
      "version": "3.1.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-docstrings",
      "version": "1.5.0, pydocstyle: 3.0.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-eradicate",
      "version": "0.2.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-executable",
      "version": "2.0.3"
    },
    {
      "is_local": false,
      "plugin": "flake8-print",
      "version": "3.1.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-string-format",
      "version": "0.2.3"
    },
    {
      "is_local": false,
      "plugin": "flake8_builtins",
      "version": "1.4.1"
    },
    {
      "is_local": false,
      "plugin": "flake8_coding",
      "version": "1.3.2"
    },
    {
      "is_local": false,
      "plugin": "flake8_commas",
      "version": "2.0.0"
    },
    {
      "is_local": false,
      "plugin": "flake8_isort",
      "version": "2.3"
    },
    {
      "is_local": false,
      "plugin": "flake8_pep3101",
      "version": "1.2.1"
    },
    {
      "is_local": false,
      "plugin": "flake8_quotes",
      "version": "2.0.1"
    },
    {
      "is_local": false,
      "plugin": "logging-format",
      "version": "0.6.0"
    },
    {
      "is_local": false,
      "plugin": "mccabe",
      "version": "0.6.1"
    },
    {
      "is_local": false,
      "plugin": "naming",
      "version": "0.8.2"
    },
    {
      "is_local": false,
      "plugin": "pycodestyle",
      "version": "2.5.0"
    },
    {
      "is_local": false,
      "plugin": "pyflakes",
      "version": "2.1.1"
    },
    {
      "is_local": false,
      "plugin": "radon",
      "version": "2.4.0"
    },
    {
      "is_local": false,
      "plugin": "rst-docstrings",
      "version": "0.0.11"
    },
    {
      "is_local": false,
      "plugin": "wemake-python-styleguide",
      "version": "0.12.5"
    }
  ],
  "version": "3.7.7"
}
```

### pip information

Contents of `pip freeze`:

```
absl-py==0.7.1
aspy.yaml==1.3.0
astor==0.8.0
attrs==18.2.0
bandit==1.6.1
cfgv==2.0.0
cloudpickle==1.2.1
colorama==0.3.9
coverage==4.5.3
docutils==0.15.2
entrypoints==0.3
eradicate==1.0
flake8==3.7.7
flake8-annotations-complexity==0.0.2
flake8-bandit==2.1.1
flake8-broken-line==0.1.0
flake8-bugbear==19.3.0
flake8-builtins==1.4.1
flake8-coding==1.3.2
flake8-commas==2.0.0
flake8-comprehensions==2.1.0
flake8-debugger==3.1.0
flake8-docstrings==1.5.0
flake8-eradicate==0.2.0
flake8-executable==2.0.3
flake8-isort==2.7.0
flake8-logging-format==0.6.0
flake8-pep3101==1.2.1
flake8-polyfill==1.0.2
flake8-print==3.1.0
flake8-quotes==2.0.1
flake8-rst-docstrings==0.0.11
flake8-string-format==0.2.3
future==0.17.1
gast==0.2.2
gitdb2==2.0.5
GitPython==2.1.11
google-pasta==0.1.7
grpcio==1.21.1
gym==0.13.0
h5py==2.9.0
identify==1.4.5
importlib-metadata==0.18
isort==4.3.20
Keras==2.2.4
Keras-Applications==1.0.8
Keras-Preprocessing==1.1.0
keras-rl==0.4.2
lxml==4.3.4
mando==0.6.4
Markdown==3.1.1
mccabe==0.6.1
nodeenv==1.3.3
numpy==1.16.4
opcua==0.98.7
opencv-python==4.1.0.25
pbr==5.3.1
pep8-naming==0.8.2
pre-commit==1.17.0
protobuf==3.8.0
pycodestyle==2.5.0
pydocstyle==3.0.0
pyflakes==2.1.1
pyglet==1.3.2
Pygments==2.4.2
python-dateutil==2.8.0
python-interface==1.5.1
pytz==2019.1
PyYAML==5.1.1
pyzmq==18.0.1
radon==2.4.0
restructuredtext-lint==1.3.0
scipy==1.3.0
six==1.12.0
smmap2==2.0.5
snowballstemmer==1.9.0
stevedore==1.30.1
tensorboard==1.14.0
tensorflow==1.14.0
tensorflow-estimator==1.14.0
termcolor==1.1.0
testfixtures==6.10.0
toml==0.10.0
typing-extensions==3.7.4
virtualenv==16.6.1
wemake-python-styleguide==0.12.5
Werkzeug==0.15.4
wrapt==1.11.2
zipp==0.5.1
```


Discussion:
Yes, that's correct. We should respect properties. We already do respect `@overload` decorator in this rule, adding new one should be simple enough.

@MLNW do you want to contribute? To fix this, we need to add new check here: https://github.com/wemake-services/wemake-python-styleguide/blob/master/wemake_python_styleguide/visitors/ast/blocks.py#L91

And a new test here: https://github.com/wemake-services/wemake-python-styleguide/blob/master/tests/test_visitors/test_ast/test_blocks/test_overlap/test_function_block.py#L200
If I find the time I may have a look but the probability is slim in the near future. For now I will ignore the warning on the affected parts.
Ok, it will be fixed in `0.13.0`

Resolving patch:
diff --git a/wemake_python_styleguide/logic/scopes.py b/wemake_python_styleguide/logic/scopes.py
--- a/wemake_python_styleguide/logic/scopes.py
+++ b/wemake_python_styleguide/logic/scopes.py
@@ -18,6 +18,9 @@
 #: That's what we expect from `@overload` decorator:
 _overload_exceptions = frozenset(('overload', 'typing.overload'))
 
+#: That's what we expect from `@property` decorator:
+_property_exceptions = frozenset(('property', '.setter'))
+
 
 class _BaseScope(object):
     """Base class for scope operations."""
@@ -145,6 +148,15 @@ def is_function_overload(node: ast.AST) -> bool:
     return False
 
 
+def is_property_setter(node: ast.AST, _=None):
+    """Check that function decorated with `property.setter`."""
+    if isinstance(node, FunctionNodes):
+        for decorator in node.decorator_list:
+            if node_to_string(decorator) in _property_exceptions:
+                return True
+    return False
+
+
 def is_same_value_reuse(node: ast.AST, names: Set[str]) -> bool:
     """Checks if the given names are reused by the given node."""
     if isinstance(node, AssignNodes) and node.value:
diff --git a/wemake_python_styleguide/visitors/ast/blocks.py b/wemake_python_styleguide/visitors/ast/blocks.py
--- a/wemake_python_styleguide/visitors/ast/blocks.py
+++ b/wemake_python_styleguide/visitors/ast/blocks.py
@@ -25,6 +25,7 @@
     OuterScope,
     extract_names,
     is_function_overload,
+    is_property_setter,
     is_same_value_reuse,
 )
 from wemake_python_styleguide.logic.walk import is_contained_by
@@ -90,10 +91,12 @@ class BlockVariableVisitor(base.BaseNodeVisitor):
 
     _naming_predicates: Tuple[_NamePredicate, ...] = (
         is_function_overload,
+        is_property_setter,
     )
 
     _scope_predicates: Tuple[_ScopePredicate, ...] = (
         is_same_value_reuse,
+        is_property_setter,
     )
 
     # Blocks:


Test patch:
diff --git a/tests/test_visitors/test_ast/test_blocks/test_overlap/test_function_block.py b/tests/test_visitors/test_ast/test_blocks/test_overlap/test_function_block.py
--- a/tests/test_visitors/test_ast/test_blocks/test_overlap/test_function_block.py
+++ b/tests/test_visitors/test_ast/test_blocks/test_overlap/test_function_block.py
@@ -240,3 +240,30 @@ def test_no_function_overload(
     visitor.run()
 
     assert_errors(visitor, [BlockAndLocalOverlapViolation])
+
+
+method_setter_template = """
+class Test(object):
+    @property
+    def {0}():
+        ...
+    @{0}.setter
+    def {0}():
+        {0} = ...
+"""
+
+
+def test_property_setter(
+    assert_errors,
+    parse_ast_tree,
+    default_options,
+    mode,
+):
+    """Ensures that property setter do not overlap."""
+    code = method_setter_template.format('func')
+    tree = parse_ast_tree(mode(code))
+
+    visitor = BlockVariableVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])