From 2b41db480e0c8552665f37c9aa4b24157e5aefc7 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 20 Feb 2022 13:38:25 +0000 Subject: [PATCH 1/8] Rebase --- mypy/stubtest.py | 50 ++++++++++++++++++++++++++++++++++++--- mypy/test/teststubtest.py | 7 +++++- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index e15a5503ab11..ff599361de8a 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -15,7 +15,7 @@ import warnings from functools import singledispatch from pathlib import Path -from typing import Any, Dict, Generic, Iterator, List, Optional, Tuple, TypeVar, Union, cast +from typing import Any, Dict, Generic, Iterator, List, Optional, Set, Tuple, TypeVar, Union, cast from typing_extensions import Type @@ -194,6 +194,42 @@ def verify( yield Error(object_path, "is an unknown mypy node", stub, runtime) +def _verify_exported_names( + object_path: List[str], stub: nodes.MypyFile, runtime_all_as_set: Set[str] +) -> Iterator[Error]: + public_names_in_stub = { + m + for m, o in stub.names.items() + if o.module_public and m not in IGNORED_MODULE_DUNDERS + } + if not runtime_all_as_set.symmetric_difference(public_names_in_stub): + return + sorted_runtime_names = sorted( + runtime_all_as_set, + key=lambda name: ((name not in public_names_in_stub), name) + ) + sorted_names_in_stub = sorted( + public_names_in_stub, + key=lambda name: ((name not in runtime_all_as_set), name) + ) + yield Error( + object_path, + ( + "module: names exported from the stub " + "do not correspond to the names exported at runtime. " + "(Note: This may be due to a missing or inaccurate " + "`__all__` in the stub.)" + ), + # pass in MISSING instead of the stub and runtime objects, + # as the line numbers aren't very relevant here, + # and it makes for a prettier error message. + MISSING, + MISSING, + stub_desc=f"Names exported are: {sorted_names_in_stub}", + runtime_desc=f"Names exported are: {sorted_runtime_names}" + ) + + @verify.register(nodes.MypyFile) def verify_mypyfile( stub: nodes.MypyFile, runtime: MaybeMissing[types.ModuleType], object_path: List[str] @@ -205,6 +241,14 @@ def verify_mypyfile( yield Error(object_path, "is not a module", stub, runtime) return + runtime_all_as_set: Optional[Set[str]] + + if hasattr(runtime, "__all__"): + runtime_all_as_set = set(runtime.__all__) + yield from _verify_exported_names(object_path, stub, runtime_all_as_set) + else: + runtime_all_as_set = None + # Check things in the stub to_check = set( m @@ -223,8 +267,8 @@ def _belongs_to_runtime(r: types.ModuleType, attr: str) -> bool: return not isinstance(obj, types.ModuleType) runtime_public_contents = ( - runtime.__all__ - if hasattr(runtime, "__all__") + ["__all__", *runtime_all_as_set] + if runtime_all_as_set is not None else [ m for m in dir(runtime) diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 9cdb12afdf07..a46507d3b034 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -708,7 +708,12 @@ def h(x: str): ... runtime="", error="h", ) - yield Case(stub="", runtime="__all__ = []", error=None) # dummy case + # __all__ present at runtime, but not in stub -> error + yield Case(stub="", runtime="__all__ = []", error="__all__") + # If runtime has __all__ but stub does not, + # we should raise an error with the module name itself + # if there are any names defined in the stub that are not in the runtime __all__ + yield Case(stub="_Z = int", runtime="", error="") yield Case(stub="", runtime="__all__ += ['y']\ny = 5", error="y") yield Case(stub="", runtime="__all__ += ['g']\ndef g(): pass", error="g") # Here we should only check that runtime has B, since the stub explicitly re-exports it From 859df5ace52e043a402b3fc04a698898b5fda9ef Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 20 Feb 2022 16:22:28 +0000 Subject: [PATCH 2/8] Reduce duplicate errors when `__all__` is missing from the stub --- mypy/stubtest.py | 14 +++++++++----- mypy/test/teststubtest.py | 16 +++++++++++----- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index ff599361de8a..e47356683702 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -217,7 +217,7 @@ def _verify_exported_names( ( "module: names exported from the stub " "do not correspond to the names exported at runtime. " - "(Note: This may be due to a missing or inaccurate " + "(Note: This is probably due to an inaccurate " "`__all__` in the stub.)" ), # pass in MISSING instead of the stub and runtime objects, @@ -245,7 +245,11 @@ def verify_mypyfile( if hasattr(runtime, "__all__"): runtime_all_as_set = set(runtime.__all__) - yield from _verify_exported_names(object_path, stub, runtime_all_as_set) + if "__all__" in stub.names: + # Only verify the contents of the stub's __all__ + # if the stub actually defines __all__ + # Otherwise we end up with duplicate errors when __all__ is missing + yield from _verify_exported_names(object_path, stub, runtime_all_as_set) else: runtime_all_as_set = None @@ -267,16 +271,16 @@ def _belongs_to_runtime(r: types.ModuleType, attr: str) -> bool: return not isinstance(obj, types.ModuleType) runtime_public_contents = ( - ["__all__", *runtime_all_as_set] + runtime_all_as_set | {"__all__"} if runtime_all_as_set is not None - else [ + else { m for m in dir(runtime) if not is_probably_private(m) # Ensure that the object's module is `runtime`, since in the absence of __all__ we # don't have a good way to detect re-exports at runtime. and _belongs_to_runtime(runtime, m) - ] + } ) # Check all things declared in module's __all__, falling back to our best guess to_check.update(runtime_public_contents) diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index a46507d3b034..d8ba2cf78b53 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -692,6 +692,10 @@ def f(): return 3 error=None, ) + @collect_cases + def test_missing_all(self) -> Iterator[Case]: + yield Case(stub="", runtime="__all__ = []", error="__all__") + @collect_cases def test_missing(self) -> Iterator[Case]: yield Case(stub="x = 5", runtime="", error="x") @@ -708,12 +712,14 @@ def h(x: str): ... runtime="", error="h", ) - # __all__ present at runtime, but not in stub -> error - yield Case(stub="", runtime="__all__ = []", error="__all__") # If runtime has __all__ but stub does not, - # we should raise an error with the module name itself - # if there are any names defined in the stub that are not in the runtime __all__ - yield Case(stub="_Z = int", runtime="", error="") + # we'll already raise an error for a missing __all__, + # so we shouldn't raise another error for inconsistent exported names + yield Case(stub="_Z = int", runtime="__all__ = []", error=None) + # But we *should* raise an error with the module name itself, + # if the stub *does* define __all__, + # but the stub's __all__ is inconsistent with the runtime's __all__ + yield Case(stub="__all__ = ['foo']", runtime="", error="") yield Case(stub="", runtime="__all__ += ['y']\ny = 5", error="y") yield Case(stub="", runtime="__all__ += ['g']\ndef g(): pass", error="g") # Here we should only check that runtime has B, since the stub explicitly re-exports it From ccd59a1e32b774da587cb9cbe3ce150d6da85adc Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 23 Feb 2022 19:39:43 +0000 Subject: [PATCH 3/8] Improve error messages --- mypy/stubtest.py | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index e47356683702..30f41375b525 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -197,36 +197,34 @@ def verify( def _verify_exported_names( object_path: List[str], stub: nodes.MypyFile, runtime_all_as_set: Set[str] ) -> Iterator[Error]: - public_names_in_stub = { - m - for m, o in stub.names.items() - if o.module_public and m not in IGNORED_MODULE_DUNDERS - } - if not runtime_all_as_set.symmetric_difference(public_names_in_stub): + public_names_in_stub = {m for m, o in stub.names.items() if o.module_public} + names_in_stub_not_runtime = sorted(public_names_in_stub - runtime_all_as_set) + names_in_runtime_not_stub = sorted(runtime_all_as_set - public_names_in_stub) + if not (names_in_runtime_not_stub or names_in_stub_not_runtime): return - sorted_runtime_names = sorted( - runtime_all_as_set, - key=lambda name: ((name not in public_names_in_stub), name) - ) - sorted_names_in_stub = sorted( - public_names_in_stub, - key=lambda name: ((name not in runtime_all_as_set), name) - ) yield Error( object_path, ( "module: names exported from the stub " - "do not correspond to the names exported at runtime. " - "(Note: This is probably due to an inaccurate " - "`__all__` in the stub.)" + "do not correspond to the names exported at runtime.\n" + "(Note: This is probably either due to an inaccurate " + "`__all__` in the stub, " + "or due to a name being declared in `__all__` " + "but not actually defined in the stub.)" ), # pass in MISSING instead of the stub and runtime objects, # as the line numbers aren't very relevant here, # and it makes for a prettier error message. - MISSING, - MISSING, - stub_desc=f"Names exported are: {sorted_names_in_stub}", - runtime_desc=f"Names exported are: {sorted_runtime_names}" + stub_object=MISSING, + runtime_object=MISSING, + stub_desc=( + f"Names exported in the stub but not at runtime: " + f"{names_in_stub_not_runtime}" + ), + runtime_desc=( + f"Names exported at runtime but not in the stub: " + f"{names_in_runtime_not_stub}" + ) ) From 3015767effa304350023e84567b105cf68f025bc Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Wed, 27 Jul 2022 15:19:52 +0100 Subject: [PATCH 4/8] Blacken --- mypy/stubtest.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 581021904ff8..b1f523477a0a 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -266,13 +266,11 @@ def _verify_exported_names( stub_object=MISSING, runtime_object=MISSING, stub_desc=( - f"Names exported in the stub but not at runtime: " - f"{names_in_stub_not_runtime}" + f"Names exported in the stub but not at runtime: " f"{names_in_stub_not_runtime}" ), runtime_desc=( - f"Names exported at runtime but not in the stub: " - f"{names_in_runtime_not_stub}" - ) + f"Names exported at runtime but not in the stub: " f"{names_in_runtime_not_stub}" + ), ) From 3f991e32b34e6ca58330897641dfd71140b11ee9 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 29 Jul 2022 11:05:47 +0100 Subject: [PATCH 5/8] Fix and clarify tests --- mypy/test/teststubtest.py | 40 +++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index f8b870744ad9..d2934c231e24 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -742,8 +742,35 @@ def f(): return 3 ) @collect_cases - def test_missing_all(self) -> Iterator[Case]: - yield Case(stub="", runtime="__all__ = []", error="__all__") + def test_all_at_runtime_not_stub(self) -> Iterator[Case]: + yield Case( + stub="Z: int", + runtime=""" + __all__ = [] + Z = 5""", + error="__all__" + ) + + @collect_cases + def test_all_in_stub_not_at_runtime(self) -> Iterator[Case]: + yield Case(stub="__all__ = ()", runtime="", error="__all__") + + @collect_cases + def test_all_in_stub_different_to_all_at_runtime(self) -> Iterator[Case]: + # We *should* raise an error with the module name itself, + # if the stub *does* define __all__, + # but the stub's __all__ is inconsistent with the runtime's __all__ + yield Case( + stub=""" + __all__ = ['foo'] + foo: str + """, + runtime=""" + __all__ = [] + foo = 'foo' + """, + error="" + ) @collect_cases def test_missing(self) -> Iterator[Case]: @@ -761,14 +788,7 @@ def h(x: str): ... runtime="", error="h", ) - # If runtime has __all__ but stub does not, - # we'll already raise an error for a missing __all__, - # so we shouldn't raise another error for inconsistent exported names - yield Case(stub="_Z = int", runtime="__all__ = []", error=None) - # But we *should* raise an error with the module name itself, - # if the stub *does* define __all__, - # but the stub's __all__ is inconsistent with the runtime's __all__ - yield Case(stub="__all__ = ['foo']", runtime="", error="") + yield Case(stub="", runtime="__all__ = []", error="__all__") # dummy case yield Case(stub="", runtime="__all__ += ['y']\ny = 5", error="y") yield Case(stub="", runtime="__all__ += ['g']\ndef g(): pass", error="g") # Here we should only check that runtime has B, since the stub explicitly re-exports it From 7d10045a8a2052eedd72ccb21a39f938e7da7776 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 29 Jul 2022 11:07:02 +0100 Subject: [PATCH 6/8] Blacken --- mypy/test/teststubtest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index d2934c231e24..ad587d057cad 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -748,7 +748,7 @@ def test_all_at_runtime_not_stub(self) -> Iterator[Case]: runtime=""" __all__ = [] Z = 5""", - error="__all__" + error="__all__", ) @collect_cases @@ -769,7 +769,7 @@ def test_all_in_stub_different_to_all_at_runtime(self) -> Iterator[Case]: __all__ = [] foo = 'foo' """, - error="" + error="", ) @collect_cases From 02b4f14eed85bf81d1dc8ce9af8452ea8476fb93 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sun, 31 Jul 2022 10:00:00 +0100 Subject: [PATCH 7/8] Only test the contents of `__all__`, not the presence --- mypy/stubtest.py | 2 +- mypy/test/teststubtest.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index b63b7451e300..102040bd3ba3 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -316,7 +316,7 @@ def _belongs_to_runtime(r: types.ModuleType, attr: str) -> bool: return not isinstance(obj, types.ModuleType) runtime_public_contents = ( - runtime_all_as_set | {"__all__"} + runtime_all_as_set if runtime_all_as_set is not None else { m diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index ad482d26b78f..f1ff9b467d27 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -934,7 +934,7 @@ def test_all_at_runtime_not_stub(self) -> Iterator[Case]: runtime=""" __all__ = [] Z = 5""", - error="__all__", + error=None, ) @collect_cases @@ -974,7 +974,7 @@ def h(x: str): ... runtime="", error="h", ) - yield Case(stub="", runtime="__all__ = []", error="__all__") # dummy case + yield Case(stub="", runtime="__all__ = []", error=None) # dummy case yield Case(stub="", runtime="__all__ += ['y']\ny = 5", error="y") yield Case(stub="", runtime="__all__ += ['g']\ndef g(): pass", error="g") # Here we should only check that runtime has B, since the stub explicitly re-exports it From f949b4965ffbc6bef10bf1f25ed196a14fbc3520 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sun, 31 Jul 2022 17:21:07 +0100 Subject: [PATCH 8/8] Improve comments --- mypy/stubtest.py | 1 - mypy/test/teststubtest.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 102040bd3ba3..6d6b2cd76fba 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -293,7 +293,6 @@ def verify_mypyfile( if "__all__" in stub.names: # Only verify the contents of the stub's __all__ # if the stub actually defines __all__ - # Otherwise we end up with duplicate errors when __all__ is missing yield from _verify_exported_names(object_path, stub, runtime_all_as_set) else: runtime_all_as_set = None diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index f1ff9b467d27..36579f17c579 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -943,7 +943,7 @@ def test_all_in_stub_not_at_runtime(self) -> Iterator[Case]: @collect_cases def test_all_in_stub_different_to_all_at_runtime(self) -> Iterator[Case]: - # We *should* raise an error with the module name itself, + # We *should* emit an error with the module name itself, # if the stub *does* define __all__, # but the stub's __all__ is inconsistent with the runtime's __all__ yield Case(