Skip to content

Commit 661f80b

Browse files
committed
Closes #861
1 parent 813fc34 commit 661f80b

File tree

14 files changed

+257
-27
lines changed

14 files changed

+257
-27
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ We used to have incremental versioning before `0.1.0`.
3636
- Forbids to use consecutive `yield` expressions
3737
- Enforces to use `.items()` in loops
3838
- Enforces using `.get()` over `key in dict` checks
39+
- Forbids to use and declare `float` keys in arrays and dictionaries
3940

4041
### Bugfixes
4142

@@ -52,6 +53,7 @@ We used to have incremental versioning before `0.1.0`.
5253
- Fixes that `//` was not recognised as a math operation
5354
- Fixes false positive `BlockAndLocalOverlapViolation` on annotations without value assign
5455
- Fixes bug when `x and not x` was not detected as the similar conditions by `WPS408`
56+
- Fixed that `1.0` and `0.1` were treated as magic numbers
5557

5658
### Misc
5759

tests/fixtures/noqa.py

+1
Original file line numberDiff line numberDiff line change
@@ -617,3 +617,4 @@ def consecutive_yields():
617617

618618
if 'key' in some_dict:
619619
print(some_dict['key']) # noqa: WPS529
620+
print(other_dict[1.0]) # noqa: WPS449

tests/test_checker/test_noqa.py

+1
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@
182182
'WPS446': 1,
183183
'WPS447': 1,
184184
'WPS448': 1,
185+
'WPS449': 1,
185186

186187
'WPS500': 1,
187188
'WPS501': 1,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import pytest
4+
5+
from wemake_python_styleguide.violations.best_practices import (
6+
FloatKeyViolation,
7+
)
8+
from wemake_python_styleguide.visitors.ast.builtins import (
9+
WrongCollectionVisitor,
10+
)
11+
12+
dict_template1 = '{{ {0}: 1 }}'
13+
dict_template2 = '{{ {0}: {0} }}'
14+
dict_template3 = '{{ {0}: 1, **kwargs }}'
15+
dict_template4 = '{{ {0}: 1, other: value }}'
16+
17+
18+
@pytest.mark.parametrize('code', [
19+
dict_template1,
20+
dict_template2,
21+
dict_template3,
22+
dict_template4,
23+
])
24+
@pytest.mark.parametrize('element', [
25+
'1.0',
26+
'-0.3',
27+
'+0.0',
28+
])
29+
def test_dict_with_float_key(
30+
assert_errors,
31+
parse_ast_tree,
32+
code,
33+
element,
34+
default_options,
35+
):
36+
"""Testing that float keys are not allowed."""
37+
tree = parse_ast_tree(code.format(element, element))
38+
39+
visitor = WrongCollectionVisitor(default_options, tree=tree)
40+
visitor.run()
41+
42+
assert_errors(visitor, [FloatKeyViolation])
43+
44+
45+
@pytest.mark.parametrize('code', [
46+
dict_template1,
47+
dict_template2,
48+
dict_template3,
49+
dict_template4,
50+
])
51+
@pytest.mark.parametrize('element', [
52+
'1',
53+
'"-0.3"',
54+
'0 + 0.1',
55+
'0 - 1.0',
56+
'1 / 3',
57+
'1 // 3',
58+
'call()',
59+
'name',
60+
'attr.some',
61+
'done[key]',
62+
])
63+
def test_dict_with_regular(
64+
assert_errors,
65+
parse_ast_tree,
66+
code,
67+
element,
68+
default_options,
69+
):
70+
"""Testing that regular keys are allowed."""
71+
tree = parse_ast_tree(code.format(element, element))
72+
73+
visitor = WrongCollectionVisitor(default_options, tree=tree)
74+
visitor.run()
75+
76+
assert_errors(visitor, [])

tests/test_visitors/test_ast/test_builtins/test_collection_hashes/test_hash_elements.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def test_collection_with_impure(
6464
@pytest.mark.parametrize('element', [
6565
'1',
6666
'-1',
67-
'0.5',
67+
'1 - b',
6868
'variable_name',
6969
'True',
7070
'None',
@@ -96,7 +96,6 @@ def test_set_with_pure_unique(
9696
@pytest.mark.parametrize('element', [
9797
'1',
9898
'-1',
99-
'--0.5',
10099
'variable_name',
101100
'True',
102101
'None',

tests/test_visitors/test_ast/test_builtins/test_numbers/test_magic_numbers.py

+2
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ def method(self):
8787
-3.5,
8888
0,
8989
0.0,
90+
0.1,
9091
0.5,
92+
-1.0,
9193
8.3,
9294
10,
9395
765,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import pytest
4+
5+
from wemake_python_styleguide.violations.best_practices import (
6+
FloatKeyViolation,
7+
)
8+
from wemake_python_styleguide.visitors.ast.subscripts import CorrectKeyVisitor
9+
10+
usage_template = 'some_dict[{0}]'
11+
12+
13+
@pytest.mark.parametrize('expression', [
14+
'1.0',
15+
'-0.0',
16+
'+3.5',
17+
])
18+
def test_float_key_usage(
19+
assert_errors,
20+
parse_ast_tree,
21+
expression,
22+
default_options,
23+
):
24+
"""Testing that redundant subscripts are forbidden."""
25+
tree = parse_ast_tree(usage_template.format(expression))
26+
27+
visitor = CorrectKeyVisitor(default_options, tree=tree)
28+
visitor.run()
29+
30+
assert_errors(visitor, [FloatKeyViolation])
31+
32+
33+
@pytest.mark.parametrize('expression', [
34+
'5',
35+
'name',
36+
'call()',
37+
'name.attr',
38+
'name[sub]',
39+
'...',
40+
'"str"',
41+
'b""',
42+
'3j',
43+
'5 + 0.1',
44+
'3 / 2',
45+
])
46+
def test_correct_subscripts(
47+
assert_errors,
48+
parse_ast_tree,
49+
expression,
50+
default_options,
51+
):
52+
"""Testing that non-redundant subscripts are allowed."""
53+
tree = parse_ast_tree(usage_template.format(expression))
54+
55+
visitor = CorrectKeyVisitor(default_options, tree=tree)
56+
visitor.run()
57+
58+
assert_errors(visitor, [])

tests/test_visitors/test_ast/test_subscripts/test_redundant_subscripts.py

+21-21
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,17 @@
77
)
88
from wemake_python_styleguide.visitors.ast.subscripts import SubscriptVisitor
99

10-
usage_template = 'constant{0}'
10+
usage_template = 'constant[{0}]'
1111

1212

1313
@pytest.mark.parametrize('expression', [
14-
'[0:7]',
15-
'[0:7:1]',
16-
'[None:7]',
17-
'[3:None]',
18-
'[3:None:2]',
19-
'[3:7:None]',
20-
'[3:7:1]',
14+
'0:7',
15+
'0:7:1',
16+
'None:7',
17+
'3:None',
18+
'3:None:2',
19+
'3:7:None',
20+
'3:7:1',
2121
])
2222
def test_redundant_subscript(
2323
assert_errors,
@@ -35,19 +35,19 @@ def test_redundant_subscript(
3535

3636

3737
@pytest.mark.parametrize('expression', [
38-
'[5]',
39-
'[3:7]',
40-
'[3:7:2]',
41-
'[3:]',
42-
'[:7]',
43-
'[3::2]',
44-
'[3:7:]',
45-
'[:7:2]',
46-
'[3::]',
47-
'[:7:]',
48-
'[::2]',
49-
'[:]',
50-
'[::]',
38+
'5',
39+
'3:7',
40+
'3:7:2',
41+
'3:',
42+
':7',
43+
'3::2',
44+
'3:7:',
45+
':7:2',
46+
'3::',
47+
':7:',
48+
'::2',
49+
':',
50+
'::',
5151
])
5252
def test_correct_subscripts(
5353
assert_errors,

wemake_python_styleguide/constants.py

+2
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,9 @@
291291
#: Common numbers that are allowed to be used without being called "magic".
292292
MAGIC_NUMBERS_WHITELIST: Final = frozenset((
293293
0, # both int and float
294+
0.1,
294295
0.5,
296+
1.0,
295297
100,
296298
1000,
297299
1024, # bytes

wemake_python_styleguide/logic/safe_eval.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def _convert_num(node: AST):
2929
return node.value
3030
elif isinstance(node, Num):
3131
return node.n
32-
elif isinstance(node, Name):
32+
elif isinstance(node, Name): # That's what is modified from the original
3333
# We return string names as is, see how we return strings:
3434
return node.id
3535
raise ValueError('malformed node or string: {0!r}'.format(node))

wemake_python_styleguide/presets/types/tree.py

+1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103

104104
subscripts.SubscriptVisitor,
105105
subscripts.ImplicitDictGetVisitor,
106+
subscripts.CorrectKeyVisitor,
106107

107108
# Complexity:
108109
*complexity.PRESET,

wemake_python_styleguide/violations/best_practices.py

+37-1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
ApproximateConstantViolation
6969
StringConstantRedefinedViolation
7070
IncorrectExceptOrderViolation
71+
FloatKeyViolation
7172
7273
Best practices
7374
--------------
@@ -121,6 +122,7 @@
121122
.. autoclass:: ApproximateConstantViolation
122123
.. autoclass:: StringConstantRedefinedViolation
123124
.. autoclass:: IncorrectExceptOrderViolation
125+
.. autoclass:: FloatKeyViolation
124126
125127
"""
126128

@@ -1867,9 +1869,11 @@ class StringConstantRedefinedViolation(ASTViolation):
18671869
@final
18681870
class IncorrectExceptOrderViolation(ASTViolation):
18691871
"""
1870-
Forbids the use incorrect order of ``except``.
1872+
Forbids the use of incorrect order of ``except``.
18711873
18721874
Note, we only check for built-in exceptions.
1875+
Because we cannot statically identify
1876+
the inheritance order of custom ones.
18731877
18741878
Reasoning:
18751879
Using incorrect order of exceptions is error-prone, since
@@ -1905,3 +1909,35 @@ class IncorrectExceptOrderViolation(ASTViolation):
19051909

19061910
error_template = 'Found incorrect exception order'
19071911
code = 448
1912+
1913+
1914+
@final
1915+
class FloatKeyViolation(ASTViolation):
1916+
"""
1917+
Forbids to define and use ``float`` keys.
1918+
1919+
Reasoning:
1920+
``float`` is a very ugly data type.
1921+
It has a lot of "precision" errors.
1922+
When we use ``float`` as keys we can hit this wall.
1923+
We also cannot use ``float`` keys with lists by design.
1924+
1925+
Solution:
1926+
Use other data types: integers, decimals, or use fuzzy logic.
1927+
1928+
Example::
1929+
1930+
# Correct:
1931+
some = {1: 'a'}
1932+
some[1]
1933+
1934+
# Wrong:
1935+
some = {1.0: 'a'}
1936+
some[1.0]
1937+
1938+
.. versionadded:: 0.13.0
1939+
1940+
"""
1941+
1942+
error_template = 'Found float used as a key'
1943+
code = 449

wemake_python_styleguide/visitors/ast/builtins.py

+16
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from wemake_python_styleguide.violations import consistency
3131
from wemake_python_styleguide.violations.best_practices import (
3232
ApproximateConstantViolation,
33+
FloatKeyViolation,
3334
MagicNumberViolation,
3435
MultipleAssignmentsViolation,
3536
NonUniqueItemsInHashViolation,
@@ -290,10 +291,12 @@ def visit_Dict(self, node: ast.Dict) -> None:
290291
Raises:
291292
NonUniqueItemsInHashViolation
292293
UnhashableTypeInHashViolation
294+
FloatKeyViolation
293295
294296
"""
295297
self._check_set_elements(node, node.keys)
296298
self._check_unhashable_elements(node.keys)
299+
self._check_float_keys(node.keys)
297300
self.generic_visit(node)
298301

299302
def _check_unhashable_elements(
@@ -337,6 +340,19 @@ def _check_set_elements(
337340
)
338341
self._report_set_elements(node, elements, element_values)
339342

343+
def _check_float_keys(self, keys: Sequence[Optional[ast.AST]]) -> None:
344+
for dict_key in keys:
345+
if dict_key is None:
346+
continue
347+
348+
real_key = unwrap_unary_node(dict_key)
349+
is_float_key = (
350+
isinstance(real_key, ast.Num) and
351+
isinstance(real_key.n, float)
352+
)
353+
if is_float_key:
354+
self.add_violation(FloatKeyViolation(dict_key))
355+
340356
def _report_set_elements(
341357
self,
342358
node: Union[ast.Set, ast.Dict],

0 commit comments

Comments
 (0)