Skip to content

Commit a0bfd3f

Browse files
[consider-math-not-float] Add a check for float("inf"), float("nan") and float("typos") (#10621)
Co-authored-by: Morwenn <[email protected]>
1 parent 5f0298d commit a0bfd3f

File tree

12 files changed

+135
-6
lines changed

12 files changed

+135
-6
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
swag = float("inf") # [consider-math-not-float]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
This is an extension check because the typing advantage could be fixed.
2+
3+
Regarding performance, float("nan") and float("inf") are slower than their counterpart math.inf and math.nan by a factor of 4 after the initial import of math.
4+
5+
.. code-block:: python
6+
7+
import math
8+
import timeit
9+
10+
time_math_inf = timeit.timeit('math.nan', globals=globals(), number=10**8)
11+
print(f'math.nan: {time_math_inf:.2f} seconds')
12+
13+
import timeit
14+
time_inf_str = timeit.timeit('float("nan")', number=10**8)
15+
print(f'float("nan"): {time_inf_str:.2f} seconds')
16+
17+
Result::
18+
19+
math.nan: 1.24 seconds
20+
float("nan"): 5.15 seconds
21+
22+
But if we take the initial import into account it's worse.
23+
24+
.. code-block:: python
25+
26+
import timeit
27+
28+
time_math_inf = timeit.timeit('import math;math.nan', globals=globals(), number=10**8)
29+
print(f'math.nan: {time_math_inf:.2f} seconds')
30+
31+
import timeit
32+
time_inf_str = timeit.timeit('float("nan")', number=10**8)
33+
print(f'float("nan"): {time_inf_str:.2f} seconds')
34+
35+
Result::
36+
37+
math.nan: 9.08 seconds
38+
float("nan"): 5.33 seconds
39+
40+
So the decision depends on how and how often you need to use it and what matter to you.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import math
2+
3+
swag = math.inf
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[MAIN]
2+
load-plugins=pylint.extensions.code_style

doc/user_guide/checkers/extensions.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ Code Style checker Messages
8888
'typing.NamedTuple' uses the well-known 'class' keyword with type-hints for
8989
readability (it's also faster as it avoids an internal exec call). Disabled
9090
by default!
91+
:consider-math-not-float (R6106): *Consider %smath.%s instead of %s*
92+
Using math.inf or math.nan permits to benefit from typing and it is up to 4
93+
times faster than a float call (after the initial import of math). This check
94+
also catches typos in float calls as a side effect.
9195

9296

9397
.. _pylint.extensions.comparison_placement:

doc/user_guide/checkers/features.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -689,11 +689,11 @@ Match Statements checker Messages
689689
:invalid-match-args-definition (E1902): *`__match_args__` must be a tuple of strings.*
690690
Emitted if `__match_args__` isn't a tuple of strings required for match.
691691
:too-many-positional-sub-patterns (E1903): *%s expects %d positional sub-patterns (given %d)*
692-
Emitted when the number of allowed positional sub-patterns exceeds the
693-
number of allowed sub-patterns specified in `__match_args__`.
692+
Emitted when the number of allowed positional sub-patterns exceeds the number
693+
of allowed sub-patterns specified in `__match_args__`.
694694
:multiple-class-sub-patterns (E1904): *Multiple sub-patterns for attribute %s*
695-
Emitted when there is more than one sub-pattern for a specific attribute in
696-
a class pattern.
695+
Emitted when there is more than one sub-pattern for a specific attribute in a
696+
class pattern.
697697
:match-class-bind-self (R1905): *Use '%s() as %s' instead*
698698
Match class patterns are faster if the name binding happens for the whole
699699
pattern and any lookup for `__match_args__` can be avoided.

doc/user_guide/messages/messages_overview.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,7 @@ All messages in the refactor category:
491491
refactor/condition-evals-to-constant
492492
refactor/confusing-consecutive-elif
493493
refactor/consider-alternative-union-syntax
494+
refactor/consider-math-not-float
494495
refactor/consider-merging-isinstance
495496
refactor/consider-refactoring-into-while-condition
496497
refactor/consider-swap-variables
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Add a ``consider-math-not-float`` message. ``float("nan")`` and ``float("inf")`` are slower
2+
than their counterpart ``math.inf`` and ``math.nan`` by a factor of 4 (notwithstanding
3+
the initial import of math) and they are also not well typed when using mypy.
4+
This check also catches typos in float calls as a side effect.
5+
6+
The :ref:`pylint.extensions.code_style` need to be activated for this check to work.
7+
8+
Refs #10621

pylint/extensions/code_style.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from __future__ import annotations
66

7+
import difflib
78
from typing import TYPE_CHECKING, TypeGuard, cast
89

910
from astroid import nodes
@@ -74,6 +75,13 @@ class CodeStyleChecker(BaseChecker):
7475
"default_enabled": False,
7576
},
7677
),
78+
"R6106": (
79+
"Consider %smath.%s instead of %s",
80+
"consider-math-not-float",
81+
"Using math.inf or math.nan permits to benefit from typing and it is up "
82+
"to 4 times faster than a float call (after the initial import of math). "
83+
"This check also catches typos in float calls as a side effect.",
84+
),
7785
}
7886
options = (
7987
(
@@ -101,14 +109,42 @@ def open(self) -> None:
101109
or self.linter.config.max_line_length
102110
)
103111

104-
@only_required_for_messages("prefer-typing-namedtuple")
112+
@only_required_for_messages("prefer-typing-namedtuple", "consider-math-not-float")
105113
def visit_call(self, node: nodes.Call) -> None:
106114
if self._py36_plus:
107115
called = safe_infer(node.func)
108-
if called and called.qname() == "collections.namedtuple":
116+
if not called:
117+
return
118+
if called.qname() == "collections.namedtuple":
109119
self.add_message(
110120
"prefer-typing-namedtuple", node=node, confidence=INFERENCE
111121
)
122+
elif called.qname() == "builtins.float":
123+
if (
124+
node.args
125+
and isinstance(node.args[0], nodes.Const)
126+
and isinstance(node.args[0].value, str)
127+
and any(
128+
c.isalpha() and c.lower() != "e" for c in node.args[0].value
129+
)
130+
):
131+
value = node.args[0].value.lower()
132+
math_call: str
133+
if "nan" in value:
134+
math_call = "nan"
135+
elif "inf" in value:
136+
math_call = "inf"
137+
else:
138+
math_call = difflib.get_close_matches(
139+
value, ["inf", "nan"], n=1, cutoff=0
140+
)[0]
141+
minus = "-" if math_call == "inf" and value.startswith("-") else ""
142+
self.add_message(
143+
"consider-math-not-float",
144+
node=node,
145+
args=(minus, math_call, node.as_string()),
146+
confidence=INFERENCE,
147+
)
112148

113149
@only_required_for_messages("consider-using-namedtuple-or-dataclass")
114150
def visit_dict(self, node: nodes.Dict) -> None:
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""Functional test for consider-math-not-float."""
2+
3+
inf_float = float("inf") # [consider-math-not-float]
4+
neg_inf_float = float('-inf') # [consider-math-not-float]
5+
pos_inf_float = float("+inf") # [consider-math-not-float]
6+
infinity_float = float("infinity") # [consider-math-not-float]
7+
neg_infinity_float = float("-infinity") # [consider-math-not-float]
8+
large_exp_float = float("1e1000")
9+
neg_large_exp_float = float("-1e1000")
10+
very_large_exp_float = float("2.5E9999")
11+
invalid_inf_float = float("in") # [consider-math-not-float]
12+
invalid_float_call = float("in", base=10) # [consider-math-not-float]
13+
nan_float = float("nan") # [consider-math-not-float]
14+
neg_nan_float = float("-nan") # [consider-math-not-float]
15+
pos_nan_float = float("+nan") # [consider-math-not-float]
16+
upper_nan_float = float("NaN") # [consider-math-not-float]
17+
typo_nan_float = float("nani") # [consider-math-not-float]
18+
other_typo_nan_float = float("nna") # [consider-math-not-float]

0 commit comments

Comments
 (0)