diff --git a/CHANGELOG.md b/CHANGELOG.md index fa69be12a..173853854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - Enable stub mode within `TYPE_CHECKING` branches (#702) - Infer from overloads - add default value in impl (#697) - Warn for missing returns with explicit `Any` return types (#715) +- Added `--helpful-string-allow-none` to allow `None` with `helpful-string` (#717) +- Enabled `helpful-string` by default (#717) ### Fixes - positional arguments on overloads break super (#697) - positional arguments on overloads duplicate unions (#697) diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py index db885d3a2..76390901a 100644 --- a/mypy/checkstrformat.py +++ b/mypy/checkstrformat.py @@ -51,6 +51,7 @@ TypedDictType, TypeOfAny, TypeStrVisitor, + TypeVarLikeType, TypeVarTupleType, TypeVarType, UnionType, @@ -442,6 +443,13 @@ def check_specs_in_format_call( def helpful_check(self, actual_type: ProperType, context: Context) -> bool: if isinstance(actual_type, (TupleType, TypedDictType, LiteralType)): return True + if isinstance(actual_type, TypeVarType): + if actual_type.values: + for value in actual_type.values: + self.helpful_check(get_proper_type(value), context) + return False + while isinstance(actual_type, TypeVarLikeType): + actual_type = actual_type.upper_bound bad_builtin = False if isinstance(actual_type, Instance): if "dataclass" in actual_type.type.metadata: @@ -464,13 +472,11 @@ def helpful_check(self, actual_type: ProperType, context: Context) -> bool: if base.module_name == "builtins": return True type_string = actual_type.accept(TypeStrVisitor(options=self.chk.options)) - if ( - custom_special_method(actual_type, "__format__") - or custom_special_method(actual_type, "__str__") - or custom_special_method(actual_type, "__repr__") - ): + if custom_special_method(actual_type, ("__format__", "__str__", "__repr__")): return True - if bad_builtin or isinstance(actual_type, NoneType): + if bad_builtin or ( + not self.msg.options.helpful_string_allow_none and isinstance(actual_type, NoneType) + ): self.msg.fail( f'The string for "{type_string}" isn\'t helpful in a user-facing or semantic string', context, diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index aa75fd363..37a5788e5 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -130,7 +130,7 @@ def __hash__(self) -> int: "str-format", "Check that string formatting/interpolation is type-safe", "General" ) HELPFUL_STRING: Final = ErrorCode( - "helpful-string", "Check that string conversions are useful", "General", default_enabled=False + "helpful-string", "Check that string conversions are useful", "General" ) STR_BYTES_PY3: Final = ErrorCode( "str-bytes-safe", "Warn about implicit coercions related to bytes and string types", "General" diff --git a/mypy/main.py b/mypy/main.py index 1f51d04aa..df2cd518b 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -619,6 +619,12 @@ def add_invertible_flag( "You probably want to set this on a module override", group=based_group, ) + add_invertible_flag( + "--helpful-string-allow-none", + default=False, + help="Allow Nones to appear in f-strings", + group=based_group, + ) add_invertible_flag( "--ide", default=False, help="Best default for IDE integration.", group=based_group ) @@ -1351,6 +1357,7 @@ def add_invertible_flag( "callable-functiontype", "possible-function", "bad-cast", + "helpful-string", } if mypy.options._based else set() diff --git a/mypy/options.py b/mypy/options.py index e78b6cc25..027b03c61 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -43,6 +43,7 @@ class BuildType: "extra_checks", "follow_imports_for_stubs", "follow_imports", + "helpful_string_allow_none", "ignore_errors", "ignore_missing_imports", "ignore_missing_py_typed", @@ -175,6 +176,7 @@ def __init__(self) -> None: self.bare_literals = True self.ignore_missing_py_typed = False self.ide = False + self.helpful_string_allow_none = False # disallow_any options self.disallow_any_generics = flip_if_not_based(True) diff --git a/mypy/typeops.py b/mypy/typeops.py index 461e3987c..e48173757 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -1078,11 +1078,13 @@ def visit_type_var_tuple(self, t: TypeVarTupleType) -> list[TypeVarLikeType]: return [t] if self.include_all else [] -def custom_special_method(typ: Type, name: str, check_all: bool = False) -> bool: +def custom_special_method(typ: Type, name: str | Iterable[str], check_all: bool = False) -> bool: """Does this type have a custom special method such as __format__() or __eq__()? If check_all is True ensure all items of a union have a custom method, not just some. """ + if not isinstance(name, str): + return any(custom_special_method(typ, n, check_all) for n in name) typ = get_proper_type(typ) if isinstance(typ, Instance): method = typ.type.get(name) @@ -1104,6 +1106,8 @@ def custom_special_method(typ: Type, name: str, check_all: bool = False) -> bool if isinstance(typ, AnyType): # Avoid false positives in uncertain cases. return True + if isinstance(typ, TypeVarLikeType): + return custom_special_method(typ.upper_bound, name, check_all) # TODO: support other types (see ExpressionChecker.has_member())? return False diff --git a/test-data/unit/check-based-format.test b/test-data/unit/check-based-format.test index 4d8a2568f..f5eb0d30a 100644 --- a/test-data/unit/check-based-format.test +++ b/test-data/unit/check-based-format.test @@ -129,3 +129,8 @@ class D: data: D f"{data}" [builtins fixtures/primitives.pyi] + + +[case testHelpfulStringAllowNone] +# flags: --helpful-string-allow-none +f"{None}"