From dd6baa39e00f60e9a375674535a7fcbade37669c Mon Sep 17 00:00:00 2001 From: Pranav Rajpal <78008260+pranavrajpal@users.noreply.github.com> Date: Mon, 12 Jul 2021 13:47:42 -0700 Subject: [PATCH 1/7] Add tests for underscore functions Add tests to make sure that functions named '_' are allowed to be redefined multiple times. --- test-data/unit/check-redefine.test | 76 ++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/test-data/unit/check-redefine.test b/test-data/unit/check-redefine.test index 4ac56edb24ef7..7d8cbb0106cf3 100644 --- a/test-data/unit/check-redefine.test +++ b/test-data/unit/check-redefine.test @@ -483,3 +483,79 @@ 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 From 3eef16233332bcba3d64793ca475f4ff5b223c02 Mon Sep 17 00:00:00 2001 From: Pranav Rajpal <78008260+pranavrajpal@users.noreply.github.com> Date: Mon, 12 Jul 2021 14:13:04 -0700 Subject: [PATCH 2/7] Add test with imported function being overwritten Add a test that makes sure that we reject overwriting an imported function that is aliased as _ with an underscore function, as that is probably an error (especially when using _ as an alias for gettext). --- test-data/unit/check-redefine.test | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test-data/unit/check-redefine.test b/test-data/unit/check-redefine.test index 7d8cbb0106cf3..d2229dba470f3 100644 --- a/test-data/unit/check-redefine.test +++ b/test-data/unit/check-redefine.test @@ -559,3 +559,13 @@ def f(arg): @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 From eb974e0c976c3e42dbefcb904de15811c00736e4 Mon Sep 17 00:00:00 2001 From: Pranav Rajpal <78008260+pranavrajpal@users.noreply.github.com> Date: Mon, 12 Jul 2021 14:18:44 -0700 Subject: [PATCH 3/7] Allow redefinition of underscore functions Allow functions called '_' to be redefined, instead of showing an error as we would if a function with any other name was redefined. --- mypy/checkexpr.py | 5 +++++ mypy/fastparse.py | 2 +- mypy/messages.py | 3 +++ mypy/semanal.py | 2 ++ 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 2d78adee3e3d5..bc4e6795e291c 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -333,6 +333,11 @@ 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 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 diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 1f0c88a27215c..74940b6d318df 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -455,7 +455,7 @@ def fix_function_overloads(self, stmts: List[Statement]) -> List[Statement]: elif len(current_overload) > 1: ret.append(OverloadedFuncDef(current_overload)) - if isinstance(stmt, Decorator): + if isinstance(stmt, Decorator) and stmt.name != "_": current_overload = [stmt] current_overload_name = stmt.name else: diff --git a/mypy/messages.py b/mypy/messages.py index 39241925aa25b..018b0c4631bde 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -701,6 +701,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: diff --git a/mypy/semanal.py b/mypy/semanal.py index 21af9322452ae..7b467f049a8cb 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -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 From 65df17cdc4e09667d057700bcaab75bbacd0a40c Mon Sep 17 00:00:00 2001 From: Pranav Rajpal <78008260+pranavrajpal@users.noreply.github.com> Date: Mon, 12 Jul 2021 15:57:09 -0700 Subject: [PATCH 4/7] Change singledispatch test to non-xfail Change a singledispatch test that tested whether multiple underscore functions being used wasn't an error from xfail to regular test, as the previous commits made that not be an error any more. --- test-data/unit/check-singledispatch.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-singledispatch.test b/test-data/unit/check-singledispatch.test index 47714b8310d0b..2757173f3ae7a 100644 --- a/test-data/unit/check-singledispatch.test +++ b/test-data/unit/check-singledispatch.test @@ -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 From c29ccd4fdc6d53b6dbff40eba1a94b49564ccea3 Mon Sep 17 00:00:00 2001 From: Pranav Rajpal <78008260+pranavrajpal@users.noreply.github.com> Date: Mon, 12 Jul 2021 21:34:05 -0700 Subject: [PATCH 5/7] Avoid error for creating underscore object Don't show an error about calling a function named '_' directly when '_' refers to a class named '_' and we're actually creating an object. Having a class named '_' is probably rare and using it directly is probably even rarer, but the other parts of the PR only deal with functions, not classes, so we probably should keep that consistent. --- mypy/checkexpr.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index bc4e6795e291c..bd697d5c3dbbf 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -334,7 +334,9 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) -> and callee_type.implicit): self.msg.untyped_function_call(callee_type, e) - if isinstance(callee_type, CallableType) and callee_type.name == "_": + 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) From 36f8bcc0475536f25cc41df8fd8350a06753de51 Mon Sep 17 00:00:00 2001 From: Pranav Rajpal <78008260+pranavrajpal@users.noreply.github.com> Date: Mon, 12 Jul 2021 21:39:46 -0700 Subject: [PATCH 6/7] Remove inference test with underscore function Remove a test that has a function named '_' that it calls directly, as naming functions '_' is now an error (which makes it impossible to test what that test was originally testing). --- test-data/unit/check-inference.test | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 17b36051446e0..c945d9c68aeeb 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -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)] From 385554f557345b8b117c868e5e7fa574c793e687 Mon Sep 17 00:00:00 2001 From: Pranav Rajpal <78008260+pranavrajpal@users.noreply.github.com> Date: Wed, 14 Jul 2021 11:58:12 -0700 Subject: [PATCH 7/7] Handle adjacent decorated underscore functions in semanal Instead of turning adjacent decorated underscore functions into a series of FuncDefs instead of one OverloadedFuncDef in the parser, handle OverloadedFuncDefs named '_' in semanal. That prevents us from needing to make that change in fastparse2, and move the handling of the underscore function case closer to where the error would be emitted. --- mypy/fastparse.py | 2 +- mypy/semanal.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 74940b6d318df..1f0c88a27215c 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -455,7 +455,7 @@ def fix_function_overloads(self, stmts: List[Statement]) -> List[Statement]: elif len(current_overload) > 1: ret.append(OverloadedFuncDef(current_overload)) - if isinstance(stmt, Decorator) and stmt.name != "_": + if isinstance(stmt, Decorator): current_overload = [stmt] current_overload_name = stmt.name else: diff --git a/mypy/semanal.py b/mypy/semanal.py index 7b467f049a8cb..124ac4fcc3273 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -812,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: