Skip to content

Commit ed09f8d

Browse files
authored
Allow redefinition of underscore functions (#10811)
This PR causes mypy to not show an error if a function named _ is redefined, as a single underscore is often used as a name for a throwaway function. This handles the case where _ is used as an alias for gettext by differentiating between functions that are aliased as _ but defined with another name, and functions that are defined as _. Overwriting a function named _ is allowed, but overwriting a function named something else but aliased as _ is not allowed, which should prevent people from accidentally overwriting gettext after importing it. This also turns calling a function named _ directly into an error, as currently we keep track of the type of the first definition if there are multiple functions defined as _, instead of the type of the last definition, and functions are usually not meant to be called directly if they're named _.
1 parent d0bd1c8 commit ed09f8d

File tree

6 files changed

+100
-8
lines changed

6 files changed

+100
-8
lines changed

mypy/checkexpr.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,13 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) ->
333333
isinstance(callee_type, CallableType)
334334
and callee_type.implicit):
335335
self.msg.untyped_function_call(callee_type, e)
336+
337+
if (isinstance(callee_type, CallableType)
338+
and not callee_type.is_type_obj()
339+
and callee_type.name == "_"):
340+
self.msg.underscore_function_call(e)
341+
return AnyType(TypeOfAny.from_error)
342+
336343
# Figure out the full name of the callee for plugin lookup.
337344
object_type = None
338345
member = None

mypy/messages.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,9 @@ def does_not_return_value(self, callee_type: Optional[Type], context: Context) -
705705
else:
706706
self.fail('Function does not return a value', context, code=codes.FUNC_RETURNS_VALUE)
707707

708+
def underscore_function_call(self, context: Context) -> None:
709+
self.fail('Calling function named "_" is not allowed', context)
710+
708711
def deleted_as_rvalue(self, typ: DeletedType, context: Context) -> None:
709712
"""Report an error about using an deleted type as an rvalue."""
710713
if typ.source is None:

mypy/semanal.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,8 @@ def f(): ... # Error: 'f' redefined
666666
"""
667667
if isinstance(new, Decorator):
668668
new = new.func
669+
if isinstance(previous, (FuncDef, Decorator)) and new.name == previous.name == "_":
670+
return True
669671
if isinstance(previous, (FuncDef, Var, Decorator)) and new.is_conditional:
670672
new.original_def = previous
671673
return True
@@ -810,7 +812,7 @@ def handle_missing_overload_decorators(self,
810812
else:
811813
self.fail("The implementation for an overloaded function "
812814
"must come last", defn.items[idx])
813-
else:
815+
elif defn.name != "_":
814816
for idx in non_overload_indexes[1:]:
815817
self.name_already_defined(defn.name, defn.items[idx], defn.items[0])
816818
if defn.impl:

test-data/unit/check-inference.test

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2751,12 +2751,6 @@ def foo() -> None:
27512751
pass
27522752
_().method() # E: "_" has no attribute "method"
27532753

2754-
[case testUnusedTargetNotDef]
2755-
def foo() -> None:
2756-
def _() -> int:
2757-
pass
2758-
_() + '' # E: Unsupported operand types for + ("int" and "str")
2759-
27602754
[case testUnusedTargetForLoop]
27612755
def f() -> None:
27622756
a = [(0, '', 0)]

test-data/unit/check-redefine.test

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,3 +483,89 @@ try:
483483
except Exception as typing:
484484
pass
485485
[builtins fixtures/exception.pyi]
486+
487+
[case testRedefiningUnderscoreFunctionIsntAnError]
488+
def _(arg):
489+
pass
490+
491+
def _(arg):
492+
pass
493+
494+
[case testTypeErrorsInUnderscoreFunctionsReported]
495+
def _(arg: str):
496+
x = arg + 1 # E: Unsupported left operand type for + ("str")
497+
498+
def _(arg: int) -> int:
499+
return 'a' # E: Incompatible return value type (got "str", expected "int")
500+
501+
[case testCallingUnderscoreFunctionIsNotAllowed]
502+
def _(arg: str) -> None:
503+
pass
504+
505+
def _(arg: int) -> int:
506+
return arg
507+
508+
_('a') # E: Calling function named "_" is not allowed
509+
510+
y = _(5) # E: Calling function named "_" is not allowed
511+
512+
[case testFunctionStillTypeCheckedWhenAliasedAsUnderscoreDuringImport]
513+
from a import f as _
514+
515+
_(1) # E: Argument 1 to "f" has incompatible type "int"; expected "str"
516+
reveal_type(_('a')) # N: Revealed type is "builtins.str"
517+
518+
[file a.py]
519+
def f(arg: str) -> str:
520+
return arg
521+
522+
[case testCallToFunctionStillTypeCheckedWhenAssignedToUnderscoreVariable]
523+
from a import g
524+
_ = g
525+
526+
_('a') # E: Argument 1 has incompatible type "str"; expected "int"
527+
reveal_type(_(1)) # N: Revealed type is "builtins.int"
528+
529+
[file a.py]
530+
def g(arg: int) -> int:
531+
return arg
532+
533+
[case testRedefiningUnderscoreFunctionWithDecoratorWithUnderscoreFunctionsNextToEachOther]
534+
def dec(f):
535+
return f
536+
537+
@dec
538+
def _(arg):
539+
pass
540+
541+
@dec
542+
def _(arg):
543+
pass
544+
545+
[case testRedefiningUnderscoreFunctionWithDecoratorInDifferentPlaces]
546+
def dec(f):
547+
return f
548+
549+
def dec2(f):
550+
return f
551+
552+
@dec
553+
def _(arg):
554+
pass
555+
556+
def f(arg):
557+
pass
558+
559+
@dec2
560+
def _(arg):
561+
pass
562+
563+
[case testOverwritingImportedFunctionThatWasAliasedAsUnderscore]
564+
from a import f as _
565+
566+
def _(arg: str) -> str: # E: Name "_" already defined (possibly by an import)
567+
return arg
568+
569+
[file a.py]
570+
def f(s: str) -> str:
571+
return s

test-data/unit/check-singledispatch.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ fun(1) # E: Argument 1 to "fun" has incompatible type "int"; expected "A"
1616
# probably won't be required after singledispatch is special cased
1717
[builtins fixtures/args.pyi]
1818

19-
[case testMultipleUnderscoreFunctionsIsntError-xfail]
19+
[case testMultipleUnderscoreFunctionsIsntError]
2020
from functools import singledispatch
2121

2222
@singledispatch

0 commit comments

Comments
 (0)