Skip to content

Commit

Permalink
Allow redefinition of underscore functions (#10811)
Browse files Browse the repository at this point in the history
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 _.
  • Loading branch information
pranavrajpal committed Jul 15, 2021
1 parent d0bd1c8 commit ed09f8d
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 8 deletions.
7 changes: 7 additions & 0 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,13 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) ->
isinstance(callee_type, CallableType)
and callee_type.implicit):
self.msg.untyped_function_call(callee_type, e)

if (isinstance(callee_type, CallableType)
and not callee_type.is_type_obj()
and callee_type.name == "_"):
self.msg.underscore_function_call(e)
return AnyType(TypeOfAny.from_error)

# Figure out the full name of the callee for plugin lookup.
object_type = None
member = None
Expand Down
3 changes: 3 additions & 0 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,9 @@ def does_not_return_value(self, callee_type: Optional[Type], context: Context) -
else:
self.fail('Function does not return a value', context, code=codes.FUNC_RETURNS_VALUE)

def underscore_function_call(self, context: Context) -> None:
self.fail('Calling function named "_" is not allowed', context)

def deleted_as_rvalue(self, typ: DeletedType, context: Context) -> None:
"""Report an error about using an deleted type as an rvalue."""
if typ.source is None:
Expand Down
4 changes: 3 additions & 1 deletion mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,8 @@ def f(): ... # Error: 'f' redefined
"""
if isinstance(new, Decorator):
new = new.func
if isinstance(previous, (FuncDef, Decorator)) and new.name == previous.name == "_":
return True
if isinstance(previous, (FuncDef, Var, Decorator)) and new.is_conditional:
new.original_def = previous
return True
Expand Down Expand Up @@ -810,7 +812,7 @@ def handle_missing_overload_decorators(self,
else:
self.fail("The implementation for an overloaded function "
"must come last", defn.items[idx])
else:
elif defn.name != "_":
for idx in non_overload_indexes[1:]:
self.name_already_defined(defn.name, defn.items[idx], defn.items[0])
if defn.impl:
Expand Down
6 changes: 0 additions & 6 deletions test-data/unit/check-inference.test
Original file line number Diff line number Diff line change
Expand Up @@ -2751,12 +2751,6 @@ def foo() -> None:
pass
_().method() # E: "_" has no attribute "method"

[case testUnusedTargetNotDef]
def foo() -> None:
def _() -> int:
pass
_() + '' # E: Unsupported operand types for + ("int" and "str")

[case testUnusedTargetForLoop]
def f() -> None:
a = [(0, '', 0)]
Expand Down
86 changes: 86 additions & 0 deletions test-data/unit/check-redefine.test
Original file line number Diff line number Diff line change
Expand Up @@ -483,3 +483,89 @@ try:
except Exception as typing:
pass
[builtins fixtures/exception.pyi]

[case testRedefiningUnderscoreFunctionIsntAnError]
def _(arg):
pass

def _(arg):
pass

[case testTypeErrorsInUnderscoreFunctionsReported]
def _(arg: str):
x = arg + 1 # E: Unsupported left operand type for + ("str")

def _(arg: int) -> int:
return 'a' # E: Incompatible return value type (got "str", expected "int")

[case testCallingUnderscoreFunctionIsNotAllowed]
def _(arg: str) -> None:
pass

def _(arg: int) -> int:
return arg

_('a') # E: Calling function named "_" is not allowed

y = _(5) # E: Calling function named "_" is not allowed

[case testFunctionStillTypeCheckedWhenAliasedAsUnderscoreDuringImport]
from a import f as _

_(1) # E: Argument 1 to "f" has incompatible type "int"; expected "str"
reveal_type(_('a')) # N: Revealed type is "builtins.str"

[file a.py]
def f(arg: str) -> str:
return arg

[case testCallToFunctionStillTypeCheckedWhenAssignedToUnderscoreVariable]
from a import g
_ = g

_('a') # E: Argument 1 has incompatible type "str"; expected "int"
reveal_type(_(1)) # N: Revealed type is "builtins.int"

[file a.py]
def g(arg: int) -> int:
return arg

[case testRedefiningUnderscoreFunctionWithDecoratorWithUnderscoreFunctionsNextToEachOther]
def dec(f):
return f

@dec
def _(arg):
pass

@dec
def _(arg):
pass

[case testRedefiningUnderscoreFunctionWithDecoratorInDifferentPlaces]
def dec(f):
return f

def dec2(f):
return f

@dec
def _(arg):
pass

def f(arg):
pass

@dec2
def _(arg):
pass

[case testOverwritingImportedFunctionThatWasAliasedAsUnderscore]
from a import f as _

def _(arg: str) -> str: # E: Name "_" already defined (possibly by an import)
return arg

[file a.py]
def f(s: str) -> str:
return s
2 changes: 1 addition & 1 deletion test-data/unit/check-singledispatch.test
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ fun(1) # E: Argument 1 to "fun" has incompatible type "int"; expected "A"
# probably won't be required after singledispatch is special cased
[builtins fixtures/args.pyi]

[case testMultipleUnderscoreFunctionsIsntError-xfail]
[case testMultipleUnderscoreFunctionsIsntError]
from functools import singledispatch

@singledispatch
Expand Down

0 comments on commit ed09f8d

Please sign in to comment.