Skip to content

Commit f611897

Browse files
authored
Improve handling of long numbers, strings and bytes (#351)
Currently, we allow this: ```py from typing_extensions import Literal foo: Literal[1000000000000000000000000000000000000000] ``` But we disallow this: ```python from typing_extensions import Final foo: Final = 1000000000000000000000000000000000000000 ``` This seems pretty inconsistent; the rationale for disallowing the latter applies equally well to the former. Fixing this inconsistency also allows us to clean up the code somewhat.
1 parent 69df360 commit f611897

5 files changed

Lines changed: 91 additions & 62 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
# Change Log
22

3+
## Unreleased
4+
5+
New error codes:
6+
* Y053: Disallow string or bytes literals with length >50 characters.
7+
Previously this rule only applied to parameter default values;
8+
it now applies everywhere.
9+
* Y054: Disallow numeric literals with a string representation >10 characters long.
10+
Previously this rule only applied to parameter default values;
11+
it now applies everywhere.
12+
13+
Other changes:
14+
* Y052 is now emitted more consistently.
15+
* Some things that used to result in Y011, Y014 or Y015 being emitted
16+
now result in Y053 or Y054 being emitted.
17+
318
## 23.3.0
419

520
* Y011/Y014/Y015: Allow `math` constants `math.inf`, `math.nan`, `math.e`,

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ currently emitted:
8585
| Y050 | Prefer `typing_extensions.Never` over `typing.NoReturn` for argument annotations. This is a purely stylistic choice in the name of readability.
8686
| Y051 | Y051 detects redundant unions between `Literal` types and builtin supertypes. For example, `Literal[5]` is redundant in the union `int \| Literal[5]`, and `Literal[True]` is redundant in the union `Literal[True] \| bool`.
8787
| Y052 | Y052 disallows assignments to constant values where the assignment does not have a type annotation. For example, `x = 0` in the global namespace is ambiguous in a stub, as there are four different types that could be inferred for the variable `x`: `int`, `Final[int]`, `Literal[0]`, or `Final[Literal[0]]`. Enum members are excluded from this check, as are various special assignments such as `__all__` and `__match_args__`.
88+
| Y053 | Only string and bytes literals <=50 characters long are permitted.
89+
| Y054 | Only numeric literals with a string representation <=10 characters long are permitted.
8890

8991
Note that several error codes recommend using types from `typing_extensions` or
9092
`_typeshed`. Strictly speaking, these packages are not part of the standard

pyi.py

Lines changed: 61 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -726,51 +726,31 @@ def _is_valid_default_value_with_annotation(node: ast.expr) -> bool:
726726
the validity of default values for ast.AnnAssign nodes.
727727
(E.g. `foo: int = 5` is OK, but `foo: TypeVar = TypeVar("foo")` is not.)
728728
"""
729-
# `...`, bools, None
730-
if isinstance(node, (ast.Ellipsis, ast.NameConstant)):
729+
# `...`, bools, None, str, bytes,
730+
# positive ints, positive floats, positive complex numbers with no real part
731+
if isinstance(node, (ast.Ellipsis, ast.NameConstant, ast.Str, ast.Bytes, ast.Num)):
731732
return True
732733

733-
# strings, bytes
734-
if isinstance(node, (ast.Str, ast.Bytes)):
735-
return len(str(node.s)) <= 50
736-
737-
def _is_valid_Num(node: ast.expr) -> TypeGuard[ast.Num]:
738-
# The maximum character limit is arbitrary, but here's what it's based on:
739-
# Hex representation of 32-bit integers tend to be 10 chars.
740-
# So is the decimal representation of the maximum positive signed 32-bit integer.
741-
# 0xFFFFFFFF --> 4294967295
742-
return isinstance(node, ast.Num) and len(str(node.n)) <= 10
743-
744-
def _is_valid_math_constant(
745-
node: ast.expr, allow_nan: bool = True
746-
) -> TypeGuard[ast.Attribute]:
747-
# math.inf, math.nan, math.e, math.pi, math.tau
748-
return (
749-
isinstance(node, ast.Attribute)
750-
and isinstance(node.value, ast.Name)
751-
and f"{node.value.id}.{node.attr}" in _ALLOWED_MATH_ATTRIBUTES_IN_DEFAULTS
752-
and (allow_nan or f"{node.value.id}.{node.attr}" != "math.nan")
753-
)
734+
# Negative ints, negative floats, negative complex numbers with no real part,
735+
# some constants from the math module
736+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
737+
if isinstance(node.operand, ast.Num):
738+
return True
739+
if isinstance(node.operand, ast.Attribute) and isinstance(
740+
node.operand.value, ast.Name
741+
):
742+
fullname = f"{node.operand.value.id}.{node.operand.attr}"
743+
return (
744+
fullname in _ALLOWED_MATH_ATTRIBUTES_IN_DEFAULTS
745+
and fullname != "math.nan"
746+
)
747+
return False
754748

755-
# Positive ints, positive floats, positive complex numbers with no real part, math constants
756-
if _is_valid_Num(node) or _is_valid_math_constant(node):
757-
return True
758-
# Negative ints, negative floats, negative complex numbers with no real part, math constants
759-
if (
760-
isinstance(node, ast.UnaryOp)
761-
and isinstance(node.op, ast.USub)
762-
and (
763-
_is_valid_Num(node.operand)
764-
# Don't allow -math.nan
765-
or _is_valid_math_constant(node.operand, allow_nan=False)
766-
)
767-
):
768-
return True
769749
# Complex numbers with a real part and an imaginary part...
770750
if (
771751
isinstance(node, ast.BinOp)
772752
and isinstance(node.op, (ast.Add, ast.Sub))
773-
and _is_valid_Num(node.right)
753+
and isinstance(node.right, ast.Num)
774754
and type(node.right.n) is complex
775755
):
776756
left = node.left
@@ -781,17 +761,19 @@ def _is_valid_math_constant(
781761
if (
782762
isinstance(left, ast.UnaryOp)
783763
and isinstance(left.op, ast.USub)
784-
and _is_valid_Num(left.operand)
764+
and isinstance(left.operand, ast.Num)
785765
and type(left.operand.n) is not complex
786766
):
787767
return True
768+
return False
769+
788770
# Special cases
789-
if (
790-
isinstance(node, ast.Attribute)
791-
and isinstance(node.value, ast.Name)
792-
and f"{node.value.id}.{node.attr}" in _ALLOWED_ATTRIBUTES_IN_DEFAULTS
793-
):
794-
return True
771+
if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name):
772+
fullname = f"{node.value.id}.{node.attr}"
773+
return (fullname in _ALLOWED_ATTRIBUTES_IN_DEFAULTS) or (
774+
fullname in _ALLOWED_MATH_ATTRIBUTES_IN_DEFAULTS
775+
)
776+
795777
return False
796778

797779

@@ -1130,20 +1112,42 @@ def visit_Call(self, node: ast.Call) -> None:
11301112
for kw in node.keywords:
11311113
self.visit(kw)
11321114

1115+
def _check_for_Y053(self, node: ast.Constant | ast.Str | ast.Bytes) -> None:
1116+
if len(node.s) > 50:
1117+
self.error(node, Y053)
1118+
1119+
def _check_for_Y054(self, node: ast.Constant | ast.Num) -> None:
1120+
# The maximum character limit is arbitrary, but here's what it's based on:
1121+
# Hex representation of 32-bit integers tend to be 10 chars.
1122+
# So is the decimal representation of the maximum positive signed 32-bit integer.
1123+
# 0xFFFFFFFF --> 4294967295
1124+
if len(str(node.n)) > 10:
1125+
self.error(node, Y054)
1126+
11331127
# 3.8+
11341128
def visit_Constant(self, node: ast.Constant) -> None:
1135-
if (
1136-
isinstance(node.value, str)
1137-
and node.value
1138-
and not self.string_literals_allowed.active
1139-
):
1129+
if isinstance(node.value, str) and not self.string_literals_allowed.active:
11401130
self.error(node, Y020)
1131+
elif isinstance(node.value, (str, bytes)):
1132+
self._check_for_Y053(node)
1133+
elif isinstance(node.value, (int, float, complex)):
1134+
self._check_for_Y054(node)
11411135

1142-
# 3.7 and lower
1136+
# 3.7
11431137
def visit_Str(self, node: ast.Str) -> None:
1144-
if node.s and not self.string_literals_allowed.active:
1138+
if self.string_literals_allowed.active:
1139+
self._check_for_Y053(node)
1140+
else:
11451141
self.error(node, Y020)
11461142

1143+
# 3.7
1144+
def visit_Bytes(self, node: ast.Bytes) -> None:
1145+
self._check_for_Y053(node)
1146+
1147+
# 3.7
1148+
def visit_Num(self, node: ast.Num) -> None:
1149+
self._check_for_Y054(node)
1150+
11471151
def visit_Expr(self, node: ast.Expr) -> None:
11481152
if isinstance(node.value, ast.Str):
11491153
self.error(node, Y021)
@@ -2047,3 +2051,8 @@ def parse_options(
20472051
)
20482052
Y051 = 'Y051 "{literal_subtype}" is redundant in a union with "{builtin_supertype}"'
20492053
Y052 = 'Y052 Need type annotation for "{variable}"'
2054+
Y053 = "Y053 String and bytes literals >50 characters long are not permitted"
2055+
Y054 = (
2056+
"Y054 Numeric literals with a string representation "
2057+
">10 characters long are not permitted"
2058+
)

tests/attribute_annotations.pyi

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,10 @@ field22: Final = {"foo": 5} # Y015 Only simple default values are allowed for a
5454
field23 = "foo" + "bar" # Y015 Only simple default values are allowed for assignments
5555
field24 = b"foo" + b"bar" # Y015 Only simple default values are allowed for assignments
5656
field25 = 5 * 5 # Y015 Only simple default values are allowed for assignments
57-
field26: int = 0xFFFFFFFFF # Y015 Only simple default values are allowed for assignments
58-
field27: int = 12345678901 # Y015 Only simple default values are allowed for assignments
59-
field28: int = -0xFFFFFFFFF # Y015 Only simple default values are allowed for assignments
60-
field29: int = -12345678901 # Y015 Only simple default values are allowed for assignments
57+
field26: int = 0xFFFFFFFFF # Y054 Numeric literals with a string representation >10 characters long are not permitted
58+
field27: int = 12345678901 # Y054 Numeric literals with a string representation >10 characters long are not permitted
59+
field28: int = -0xFFFFFFFFF # Y054 Numeric literals with a string representation >10 characters long are not permitted
60+
field29: int = -12345678901 # Y054 Numeric literals with a string representation >10 characters long are not permitted
6161

6262
class Foo:
6363
field1: int
@@ -98,10 +98,10 @@ class Foo:
9898
field24 = "foo" + "bar" # Y015 Only simple default values are allowed for assignments
9999
field25 = b"foo" + b"bar" # Y015 Only simple default values are allowed for assignments
100100
field26 = 5 * 5 # Y015 Only simple default values are allowed for assignments
101-
field27 = 0xFFFFFFFFF # Y015 Only simple default values are allowed for assignments
102-
field28 = 12345678901 # Y015 Only simple default values are allowed for assignments
103-
field29 = -0xFFFFFFFFF # Y015 Only simple default values are allowed for assignments
104-
field30 = -12345678901 # Y015 Only simple default values are allowed for assignments
101+
field27 = 0xFFFFFFFFF # Y052 Need type annotation for "field27" # Y054 Numeric literals with a string representation >10 characters long are not permitted
102+
field28 = 12345678901 # Y052 Need type annotation for "field28" # Y054 Numeric literals with a string representation >10 characters long are not permitted
103+
field29 = -0xFFFFFFFFF # Y052 Need type annotation for "field29" # Y054 Numeric literals with a string representation >10 characters long are not permitted
104+
field30 = -12345678901 # Y052 Need type annotation for "field30" # Y054 Numeric literals with a string representation >10 characters long are not permitted
105105

106106
Field95: TypeAlias = None
107107
Field96: TypeAlias = int | None

tests/defaults.pyi

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,8 @@ def f34(x: str = sys.version) -> None: ...
6262
def f35(x: tuple[int, ...] = sys.version_info) -> None: ...
6363
def f36(x: str = sys.winver) -> None: ...
6464

65-
def f37(x: str = "a_very_long_stringgggggggggggggggggggggggggggggggggggggggggggggg") -> None: ... # Y011 Only simple default values allowed for typed arguments
66-
def f38(x: bytes = b"a_very_long_byte_stringggggggggggggggggggggggggggggggggggggg") -> None: ... # Y011 Only simple default values allowed for typed arguments
65+
def f37(x: str = "a_very_long_stringgggggggggggggggggggggggggggggggggggggggggggggg") -> None: ... # Y053 String and bytes literals >50 characters long are not permitted
66+
def f38(x: bytes = b"a_very_long_byte_stringggggggggggggggggggggggggggggggggggggg") -> None: ... # Y053 String and bytes literals >50 characters long are not permitted
67+
68+
foo: str = "a_very_long_stringgggggggggggggggggggggggggggggggggggggggggggggg" # Y053 String and bytes literals >50 characters long are not permitted
69+
bar: bytes = b"a_very_long_byte_stringggggggggggggggggggggggggggggggggggggg" # Y053 String and bytes literals >50 characters long are not permitted

0 commit comments

Comments
 (0)