diff --git a/doc/data/messages/c/consider-math-not-float/bad.py b/doc/data/messages/c/consider-math-not-float/bad.py new file mode 100644 index 0000000000..6cab7399ea --- /dev/null +++ b/doc/data/messages/c/consider-math-not-float/bad.py @@ -0,0 +1 @@ +swag = float("inf") # [consider-math-not-float] diff --git a/doc/data/messages/c/consider-math-not-float/details.rst b/doc/data/messages/c/consider-math-not-float/details.rst new file mode 100644 index 0000000000..2e5e09c977 --- /dev/null +++ b/doc/data/messages/c/consider-math-not-float/details.rst @@ -0,0 +1,40 @@ +This is an extension check because the typing advantage could be fixed. + +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. + +.. code-block:: python + + import math + import timeit + + time_math_inf = timeit.timeit('math.nan', globals=globals(), number=10**8) + print(f'math.nan: {time_math_inf:.2f} seconds') + + import timeit + time_inf_str = timeit.timeit('float("nan")', number=10**8) + print(f'float("nan"): {time_inf_str:.2f} seconds') + +Result:: + + math.nan: 1.24 seconds + float("nan"): 5.15 seconds + +But if we take the initial import into account it's worse. + +.. code-block:: python + + import timeit + + time_math_inf = timeit.timeit('import math;math.nan', globals=globals(), number=10**8) + print(f'math.nan: {time_math_inf:.2f} seconds') + + import timeit + time_inf_str = timeit.timeit('float("nan")', number=10**8) + print(f'float("nan"): {time_inf_str:.2f} seconds') + +Result:: + + math.nan: 9.08 seconds + float("nan"): 5.33 seconds + +So the decision depends on how and how often you need to use it and what matter to you. diff --git a/doc/data/messages/c/consider-math-not-float/good.py b/doc/data/messages/c/consider-math-not-float/good.py new file mode 100644 index 0000000000..711a0f3b90 --- /dev/null +++ b/doc/data/messages/c/consider-math-not-float/good.py @@ -0,0 +1,3 @@ +import math + +swag = math.inf diff --git a/doc/data/messages/c/consider-math-not-float/pylintrc b/doc/data/messages/c/consider-math-not-float/pylintrc new file mode 100644 index 0000000000..8663ab085d --- /dev/null +++ b/doc/data/messages/c/consider-math-not-float/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.code_style diff --git a/doc/user_guide/checkers/extensions.rst b/doc/user_guide/checkers/extensions.rst index 1ff270a6e4..c9dead7ca3 100644 --- a/doc/user_guide/checkers/extensions.rst +++ b/doc/user_guide/checkers/extensions.rst @@ -88,6 +88,10 @@ Code Style checker Messages 'typing.NamedTuple' uses the well-known 'class' keyword with type-hints for readability (it's also faster as it avoids an internal exec call). Disabled by default! +:consider-math-not-float (R6106): *Consider %smath.%s instead of %s* + Using math.inf or math.nan permits to benefit from typing and it is up to 4 + times faster than a float call (after the initial import of math). This check + also catches typos in float calls as a side effect. .. _pylint.extensions.comparison_placement: diff --git a/doc/user_guide/checkers/features.rst b/doc/user_guide/checkers/features.rst index ef2424522d..8a4a2a4286 100644 --- a/doc/user_guide/checkers/features.rst +++ b/doc/user_guide/checkers/features.rst @@ -689,11 +689,11 @@ Match Statements checker Messages :invalid-match-args-definition (E1902): *`__match_args__` must be a tuple of strings.* Emitted if `__match_args__` isn't a tuple of strings required for match. :too-many-positional-sub-patterns (E1903): *%s expects %d positional sub-patterns (given %d)* - Emitted when the number of allowed positional sub-patterns exceeds the - number of allowed sub-patterns specified in `__match_args__`. + Emitted when the number of allowed positional sub-patterns exceeds the number + of allowed sub-patterns specified in `__match_args__`. :multiple-class-sub-patterns (E1904): *Multiple sub-patterns for attribute %s* - Emitted when there is more than one sub-pattern for a specific attribute in - a class pattern. + Emitted when there is more than one sub-pattern for a specific attribute in a + class pattern. :match-class-bind-self (R1905): *Use '%s() as %s' instead* Match class patterns are faster if the name binding happens for the whole pattern and any lookup for `__match_args__` can be avoided. diff --git a/doc/user_guide/messages/messages_overview.rst b/doc/user_guide/messages/messages_overview.rst index 5b1378474e..87685835bb 100644 --- a/doc/user_guide/messages/messages_overview.rst +++ b/doc/user_guide/messages/messages_overview.rst @@ -491,6 +491,7 @@ All messages in the refactor category: refactor/condition-evals-to-constant refactor/confusing-consecutive-elif refactor/consider-alternative-union-syntax + refactor/consider-math-not-float refactor/consider-merging-isinstance refactor/consider-refactoring-into-while-condition refactor/consider-swap-variables diff --git a/doc/whatsnew/fragments/10621.new_check b/doc/whatsnew/fragments/10621.new_check new file mode 100644 index 0000000000..85ecc32d1b --- /dev/null +++ b/doc/whatsnew/fragments/10621.new_check @@ -0,0 +1,8 @@ +Add a ``consider-math-not-float`` message. ``float("nan")`` and ``float("inf")`` are slower +than their counterpart ``math.inf`` and ``math.nan`` by a factor of 4 (notwithstanding +the initial import of math) and they are also not well typed when using mypy. +This check also catches typos in float calls as a side effect. + +The :ref:`pylint.extensions.code_style` need to be activated for this check to work. + +Refs #10621 diff --git a/pylint/extensions/code_style.py b/pylint/extensions/code_style.py index db80a2a2f1..d8ea869cb9 100644 --- a/pylint/extensions/code_style.py +++ b/pylint/extensions/code_style.py @@ -4,6 +4,7 @@ from __future__ import annotations +import difflib from typing import TYPE_CHECKING, TypeGuard, cast from astroid import nodes @@ -74,6 +75,13 @@ class CodeStyleChecker(BaseChecker): "default_enabled": False, }, ), + "R6106": ( + "Consider %smath.%s instead of %s", + "consider-math-not-float", + "Using math.inf or math.nan permits to benefit from typing and it is up " + "to 4 times faster than a float call (after the initial import of math). " + "This check also catches typos in float calls as a side effect.", + ), } options = ( ( @@ -101,14 +109,42 @@ def open(self) -> None: or self.linter.config.max_line_length ) - @only_required_for_messages("prefer-typing-namedtuple") + @only_required_for_messages("prefer-typing-namedtuple", "consider-math-not-float") def visit_call(self, node: nodes.Call) -> None: if self._py36_plus: called = safe_infer(node.func) - if called and called.qname() == "collections.namedtuple": + if not called: + return + if called.qname() == "collections.namedtuple": self.add_message( "prefer-typing-namedtuple", node=node, confidence=INFERENCE ) + elif called.qname() == "builtins.float": + if ( + node.args + and isinstance(node.args[0], nodes.Const) + and isinstance(node.args[0].value, str) + and any( + c.isalpha() and c.lower() != "e" for c in node.args[0].value + ) + ): + value = node.args[0].value.lower() + math_call: str + if "nan" in value: + math_call = "nan" + elif "inf" in value: + math_call = "inf" + else: + math_call = difflib.get_close_matches( + value, ["inf", "nan"], n=1, cutoff=0 + )[0] + minus = "-" if math_call == "inf" and value.startswith("-") else "" + self.add_message( + "consider-math-not-float", + node=node, + args=(minus, math_call, node.as_string()), + confidence=INFERENCE, + ) @only_required_for_messages("consider-using-namedtuple-or-dataclass") def visit_dict(self, node: nodes.Dict) -> None: diff --git a/tests/functional/ext/code_style/cs_use_math_not_float.py b/tests/functional/ext/code_style/cs_use_math_not_float.py new file mode 100644 index 0000000000..e8ac8ab302 --- /dev/null +++ b/tests/functional/ext/code_style/cs_use_math_not_float.py @@ -0,0 +1,18 @@ +"""Functional test for consider-math-not-float.""" + +inf_float = float("inf") # [consider-math-not-float] +neg_inf_float = float('-inf') # [consider-math-not-float] +pos_inf_float = float("+inf") # [consider-math-not-float] +infinity_float = float("infinity") # [consider-math-not-float] +neg_infinity_float = float("-infinity") # [consider-math-not-float] +large_exp_float = float("1e1000") +neg_large_exp_float = float("-1e1000") +very_large_exp_float = float("2.5E9999") +invalid_inf_float = float("in") # [consider-math-not-float] +invalid_float_call = float("in", base=10) # [consider-math-not-float] +nan_float = float("nan") # [consider-math-not-float] +neg_nan_float = float("-nan") # [consider-math-not-float] +pos_nan_float = float("+nan") # [consider-math-not-float] +upper_nan_float = float("NaN") # [consider-math-not-float] +typo_nan_float = float("nani") # [consider-math-not-float] +other_typo_nan_float = float("nna") # [consider-math-not-float] diff --git a/tests/functional/ext/code_style/cs_use_math_not_float.rc b/tests/functional/ext/code_style/cs_use_math_not_float.rc new file mode 100644 index 0000000000..5081401e9f --- /dev/null +++ b/tests/functional/ext/code_style/cs_use_math_not_float.rc @@ -0,0 +1,3 @@ +[MAIN] +load-plugins=pylint.extensions.code_style +enable=consider-math-not-float diff --git a/tests/functional/ext/code_style/cs_use_math_not_float.txt b/tests/functional/ext/code_style/cs_use_math_not_float.txt new file mode 100644 index 0000000000..5d30d7e0b8 --- /dev/null +++ b/tests/functional/ext/code_style/cs_use_math_not_float.txt @@ -0,0 +1,13 @@ +consider-math-not-float:3:12:3:24::Consider math.inf instead of float('inf'):INFERENCE +consider-math-not-float:4:16:4:29::Consider -math.inf instead of float('-inf'):INFERENCE +consider-math-not-float:5:16:5:29::Consider math.inf instead of float('+inf'):INFERENCE +consider-math-not-float:6:17:6:34::Consider math.inf instead of float('infinity'):INFERENCE +consider-math-not-float:7:21:7:39::Consider -math.inf instead of float('-infinity'):INFERENCE +consider-math-not-float:11:20:11:31::Consider math.inf instead of float('in'):INFERENCE +consider-math-not-float:12:21:12:41::Consider math.inf instead of float('in', base=10):INFERENCE +consider-math-not-float:13:12:13:24::Consider math.nan instead of float('nan'):INFERENCE +consider-math-not-float:14:16:14:29::Consider math.nan instead of float('-nan'):INFERENCE +consider-math-not-float:15:16:15:29::Consider math.nan instead of float('+nan'):INFERENCE +consider-math-not-float:16:18:16:30::Consider math.nan instead of float('NaN'):INFERENCE +consider-math-not-float:17:17:17:30::Consider math.nan instead of float('nani'):INFERENCE +consider-math-not-float:18:23:18:35::Consider math.nan instead of float('nna'):INFERENCE