From d3260adc22968cde19f246bdafec423bec64b55d Mon Sep 17 00:00:00 2001 From: jakkdl Date: Wed, 20 Dec 2023 16:18:42 +0100 Subject: [PATCH 1/8] bump pyright, introducing warnings for tons of missing docstrings --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 093beb4102..723108abff 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -106,7 +106,7 @@ pyopenssl==23.3.0 # via -r test-requirements.in pyproject-hooks==1.0.0 # via build -pyright==1.1.339 +pyright==1.1.342 # via -r test-requirements.in pytest==7.4.3 # via -r test-requirements.in From 5acf76f48a450d56e7f55bfd4467c5ace30bf7b6 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Thu, 21 Dec 2023 13:44:48 +0100 Subject: [PATCH 2/8] WIP pyright filter script. calling it from check.sh. Copy docstrings to SocketType from stdlib --- check.sh | 12 +- pyproject.toml | 2 + src/trio/_core/_run.py | 1 + src/trio/_socket.py | 19 +++ src/trio/_tests/check_type_completeness.py | 136 +++++++++++++++++++++ 5 files changed, 159 insertions(+), 11 deletions(-) create mode 100755 src/trio/_tests/check_type_completeness.py diff --git a/check.sh b/check.sh index 3e07056dcd..0bc6bc8b78 100755 --- a/check.sh +++ b/check.sh @@ -96,18 +96,8 @@ fi codespell || EXIT_STATUS=$? -PYRIGHT=0 echo "::group::Pyright interface tests" -pyright --verifytypes --ignoreexternal --pythonplatform=Linux --verifytypes=trio \ - || { echo "* Pyright --verifytypes (Linux) found errors." >> "$GITHUB_STEP_SUMMARY"; PYRIGHT=1; } -pyright --verifytypes --ignoreexternal --pythonplatform=Darwin --verifytypes=trio \ - || { echo "* Pyright --verifytypes (Mac) found errors." >> "$GITHUB_STEP_SUMMARY"; PYRIGHT=1; } -pyright --verifytypes --ignoreexternal --pythonplatform=Windows --verifytypes=trio \ - || { echo "* Pyright --verifytypes (Windows) found errors." >> "$GITHUB_STEP_SUMMARY"; PYRIGHT=1; } -if [ $PYRIGHT -ne 0 ]; then - echo "::error:: Pyright --verifytypes returned errors." - EXIT_STATUS=1 -fi +python src/trio/_tests/check_type_completeness.py || EXIT_STATUS=$? pyright src/trio/_tests/type_tests || EXIT_STATUS=$? pyright src/trio/_core/_tests/type_tests || EXIT_STATUS=$? diff --git a/pyproject.toml b/pyproject.toml index 48c97eccd2..641073d28c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -243,6 +243,8 @@ omit = [ "*/trio/_core/_generated_*", # Type tests aren't intended to be run, just passed to type checkers. "*/type_tests/*", + # Script used to check type completeness that isn't run in tests + "*/trio/_tests/check_type_completeness.py", ] # The test suite spawns subprocesses to test some stuff, so make sure # this doesn't corrupt the coverage files diff --git a/src/trio/_core/_run.py b/src/trio/_core/_run.py index 015065863e..d94d4c6329 100644 --- a/src/trio/_core/_run.py +++ b/src/trio/_core/_run.py @@ -162,6 +162,7 @@ def function_with_unique_name_xyzzy() -> NoReturn: @attr.s(frozen=True, slots=True) class SystemClock(Clock): + "aoeu" # Add a large random offset to our clock to ensure that if people # accidentally call time.perf_counter() directly or start comparing clocks # between different runs, then they'll notice the bug quickly: diff --git a/src/trio/_socket.py b/src/trio/_socket.py index 1e530b14a1..008085b899 100644 --- a/src/trio/_socket.py +++ b/src/trio/_socket.py @@ -616,6 +616,7 @@ def proto(self) -> int: @property def did_shutdown_SHUT_WR(self) -> bool: + """Return True if the socket has been shut down with the SHUT_WR flag""" raise NotImplementedError def __repr__(self) -> str: @@ -634,9 +635,11 @@ def shutdown(self, flag: int) -> None: raise NotImplementedError def is_readable(self) -> bool: + """Return True if the socket is readable. This is checked with `select.select` on Windows, otherwise `select.poll`.""" raise NotImplementedError async def wait_writable(self) -> None: + """Convenience method that calls trio.lowlevel.wait_writable for the object.""" raise NotImplementedError async def accept(self) -> tuple[SocketType, AddressFormat]: @@ -726,6 +729,22 @@ async def sendmsg( raise NotImplementedError +# copy docstrings from socket.SocketType +# this doesn't satisfy pyright for platform-specific objects though +for name, obj in SocketType.__dict__.items(): + # skip dunders and already defined docstrings + if name.startswith("__") or obj.__doc__: + continue + # try both socket.socket and socket.SocketType + for stdlib_type in _stdlib_socket.socket, _stdlib_socket.SocketType: + stdlib_obj = getattr(stdlib_type, name, None) + if stdlib_obj and stdlib_obj.__doc__: + break + else: + continue + obj.__doc__ = stdlib_obj.__doc__ + + class _SocketType(SocketType): def __init__(self, sock: _stdlib_socket.socket): if type(sock) is not _stdlib_socket.socket: diff --git a/src/trio/_tests/check_type_completeness.py b/src/trio/_tests/check_type_completeness.py new file mode 100755 index 0000000000..6faf341c5d --- /dev/null +++ b/src/trio/_tests/check_type_completeness.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +# this file is not run as part of the tests, instead it's run standalone from check.sh +import argparse +import json +import subprocess +import sys +from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Mapping + +# the result file is not marked in MANIFEST.in so it's not included in the package +failed = False + +printed_diagnostics: set[str] = set() + + +# TODO: consider checking manually without `--ignoreexternal`, and/or +# removing it from the below call later on. +def run_pyright(platform: str) -> subprocess.CompletedProcess[bytes]: + return subprocess.run( + [ + "pyright", + # Specify a platform and version to keep imported modules consistent. + f"--pythonplatform={platform}", + "--pythonversion=3.8", + "--verifytypes=trio", + "--outputjson", + "--ignoreexternal", + ], + capture_output=True, + ) + + +def check_zero(key: str, current_dict: Mapping[str, float]) -> None: + global failed + if current_dict[key] != 0: + failed = True + print(f"ERROR: {key} is {current_dict[key]}") + + +def check_type(args: argparse.Namespace, platform: str) -> int: + res = run_pyright(platform) + current_result = json.loads(res.stdout) + + if res.stderr: + print(res.stderr) + + if args.full_diagnostics_file: + with open(args.full_diagnostics_file, "w") as f: + json.dump(current_result, f, sort_keys=True, indent=4) + + missingFunctionDocStringCount = current_result["typeCompleteness"][ + "missingFunctionDocStringCount" + ] + # missingClassDocStringCount = current_result["typeCompleteness"][ + # "missingClassDocStringCount" + # ] + + for symbol in current_result["typeCompleteness"]["symbols"]: + diagnostics = symbol["diagnostics"] + if not diagnostics: + continue + for diagnostic in diagnostics: + if diagnostic["message"].startswith("No docstring found for"): + # check if it actually has a docstring at runtime + # this is rickety and incomplete - but works for the current + # missing docstrings + name_parts = symbol["name"].split(".") + if name_parts[1] == "_core": # noqa: SIM108 + split_i = 2 + else: + split_i = 1 + module = sys.modules[".".join(name_parts[:split_i])] + obj = module + try: + for obj_name in name_parts[split_i:]: + obj = getattr(obj, obj_name) + except AttributeError as exc: + # asynciowrapper does funky getattr stuff + if "AsyncIOWrapper" in str(exc): + missingFunctionDocStringCount -= 1 + continue + if obj.__doc__: + missingFunctionDocStringCount -= 1 + continue + + if diagnostic["message"] in printed_diagnostics: + continue + print(diagnostic["message"]) + printed_diagnostics.add(diagnostic["message"]) + + continue + + if missingFunctionDocStringCount > 0: + print( + f"ERROR: missingFunctionDocStringCount is {missingFunctionDocStringCount}" + ) + failed = True + + for key in "errorCount", "warningCount", "informationCount": + check_zero(key, current_result["summary"]) + + for key in ( + # "missingFunctionDocStringCount", + "missingClassDocStringCount", + "missingDefaultParamCount", + # "completenessScore", + ): + check_zero(key, current_result["typeCompleteness"]) + + for key in ("withUnknownType", "withAmbiguousType"): + check_zero(key, current_result["typeCompleteness"]["exportedSymbolCounts"]) + + return int(failed) + + +def main(args: argparse.Namespace) -> int: + res = 0 + for platform in "Linux", "Windows", "Darwin": + print("*" * 20, f"\nChecking {platform}...") + res += check_type(args, platform) + print("*" * 20) + return res + + +parser = argparse.ArgumentParser() +parser.add_argument("--overwrite-file", action="store_true", default=False) +parser.add_argument("--full-diagnostics-file", type=Path, default=None) +args = parser.parse_args() + +assert __name__ == "__main__", "This script should be run standalone" +sys.exit(main(args)) From d9c5adae843c3ab2680c8e35ac75ab6eac8587d9 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Tue, 20 Feb 2024 15:22:15 +0100 Subject: [PATCH 3/8] wip --- src/trio/_tests/check_type_completeness.py | 127 ++++++++++++--------- 1 file changed, 70 insertions(+), 57 deletions(-) diff --git a/src/trio/_tests/check_type_completeness.py b/src/trio/_tests/check_type_completeness.py index 6faf341c5d..273e7db897 100755 --- a/src/trio/_tests/check_type_completeness.py +++ b/src/trio/_tests/check_type_completeness.py @@ -7,13 +7,9 @@ import subprocess import sys from pathlib import Path -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from collections.abc import Mapping - -# the result file is not marked in MANIFEST.in so it's not included in the package -failed = False +import trio +import trio.testing printed_diagnostics: set[str] = set() @@ -35,14 +31,35 @@ def run_pyright(platform: str) -> subprocess.CompletedProcess[bytes]: ) -def check_zero(key: str, current_dict: Mapping[str, float]) -> None: - global failed - if current_dict[key] != 0: - failed = True - print(f"ERROR: {key} is {current_dict[key]}") +def has_docstring_at_runtime(name: str) -> bool: + assert trio.testing + # this is rickety, but works for all current symbols + name_parts = name.split(".") + split_i = 1 + if name_parts[1] == "tests": + return True + if name_parts[1] in ("_core", "testing"): # noqa: SIM108 + split_i = 2 + else: + split_i = 1 + module = sys.modules[".".join(name_parts[:split_i])] + obj = module + try: + for obj_name in name_parts[split_i:]: + obj = getattr(obj, obj_name) + except AttributeError as exc: + # asynciowrapper does funky getattr stuff + if "AsyncIOWrapper" in str(exc): + return True + # raise + print(exc) + return bool(obj.__doc__) def check_type(args: argparse.Namespace, platform: str) -> int: + # convince isort we use the trio import + assert trio + res = run_pyright(platform) current_result = json.loads(res.stdout) @@ -53,40 +70,51 @@ def check_type(args: argparse.Namespace, platform: str) -> int: with open(args.full_diagnostics_file, "w") as f: json.dump(current_result, f, sort_keys=True, indent=4) - missingFunctionDocStringCount = current_result["typeCompleteness"][ - "missingFunctionDocStringCount" - ] - # missingClassDocStringCount = current_result["typeCompleteness"][ - # "missingClassDocStringCount" - # ] + counts = {} + for where, key in ( + (("typeCompleteness",), "missingFunctionDocStringCount"), + (("typeCompleteness",), "missingClassDocStringCount"), + (("typeCompleteness",), "missingDefaultParamCount"), + (("summary",), "errorCount"), + (("summary",), "warningCount"), + (("summary",), "informationCount"), + (("typeCompleteness", "exportedSymbolCounts"), "withUnknownType"), + (("typeCompleteness", "exportedSymbolCounts"), "withAmbiguousType"), + ): + curr_dict = current_result + for subdict in where: + curr_dict = curr_dict[subdict] + + counts[key] = curr_dict[key] for symbol in current_result["typeCompleteness"]["symbols"]: + category = symbol["category"] diagnostics = symbol["diagnostics"] + name = symbol["name"] if not diagnostics: + if ( + category + not in ("variable", "symbol", "type alias", "constant", "module") + and not name.endswith("__") + and not has_docstring_at_runtime(symbol["name"]) + ): + print( + "not warned by pyright, but missing docstring:", + symbol["name"], + symbol["category"], + ) continue for diagnostic in diagnostics: - if diagnostic["message"].startswith("No docstring found for"): - # check if it actually has a docstring at runtime - # this is rickety and incomplete - but works for the current - # missing docstrings - name_parts = symbol["name"].split(".") - if name_parts[1] == "_core": # noqa: SIM108 - split_i = 2 + if diagnostic["message"].startswith( + "No docstring found for" + ) and has_docstring_at_runtime(symbol["name"]): + if category in ("method", "function"): + counts["missingFunctionDocStringCount"] -= 1 + elif category == "class": + counts["missingClassDocStringCount"] -= 1 else: - split_i = 1 - module = sys.modules[".".join(name_parts[:split_i])] - obj = module - try: - for obj_name in name_parts[split_i:]: - obj = getattr(obj, obj_name) - except AttributeError as exc: - # asynciowrapper does funky getattr stuff - if "AsyncIOWrapper" in str(exc): - missingFunctionDocStringCount -= 1 - continue - if obj.__doc__: - missingFunctionDocStringCount -= 1 - continue + raise AssertionError("This category shouldn't be possible here") + continue if diagnostic["message"] in printed_diagnostics: continue @@ -95,25 +123,10 @@ def check_type(args: argparse.Namespace, platform: str) -> int: continue - if missingFunctionDocStringCount > 0: - print( - f"ERROR: missingFunctionDocStringCount is {missingFunctionDocStringCount}" - ) - failed = True - - for key in "errorCount", "warningCount", "informationCount": - check_zero(key, current_result["summary"]) - - for key in ( - # "missingFunctionDocStringCount", - "missingClassDocStringCount", - "missingDefaultParamCount", - # "completenessScore", - ): - check_zero(key, current_result["typeCompleteness"]) - - for key in ("withUnknownType", "withAmbiguousType"): - check_zero(key, current_result["typeCompleteness"]["exportedSymbolCounts"]) + for name, val in counts.items(): + if val > 0: + print(f"ERROR: {name} is {val}") + failed = True return int(failed) From 825e7991f9c2a5513e1ce89d58e9cc870906ede4 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Wed, 21 Feb 2024 12:00:31 +0100 Subject: [PATCH 4/8] ugly implementation --- src/trio/_core/_run.py | 1 - src/trio/_socket.py | 3 +- src/trio/_tests/_check_type_completeness.json | 156 ++++++++++++++++++ src/trio/_tests/check_type_completeness.py | 156 ++++++++++++------ 4 files changed, 266 insertions(+), 50 deletions(-) create mode 100644 src/trio/_tests/_check_type_completeness.json diff --git a/src/trio/_core/_run.py b/src/trio/_core/_run.py index 83ccf41254..c5d6b65712 100644 --- a/src/trio/_core/_run.py +++ b/src/trio/_core/_run.py @@ -157,7 +157,6 @@ def function_with_unique_name_xyzzy() -> NoReturn: @attrs.frozen class SystemClock(Clock): - "aoeu" # Add a large random offset to our clock to ensure that if people # accidentally call time.perf_counter() directly or start comparing clocks # between different runs, then they'll notice the bug quickly: diff --git a/src/trio/_socket.py b/src/trio/_socket.py index 65ef3f77ec..14b0dc7cb5 100644 --- a/src/trio/_socket.py +++ b/src/trio/_socket.py @@ -727,8 +727,7 @@ async def sendmsg( raise NotImplementedError -# copy docstrings from socket.SocketType -# this doesn't satisfy pyright for platform-specific objects though +# copy docstrings from socket.SocketType / socket.socket for name, obj in SocketType.__dict__.items(): # skip dunders and already defined docstrings if name.startswith("__") or obj.__doc__: diff --git a/src/trio/_tests/_check_type_completeness.json b/src/trio/_tests/_check_type_completeness.json new file mode 100644 index 0000000000..2e2d7b6ef7 --- /dev/null +++ b/src/trio/_tests/_check_type_completeness.json @@ -0,0 +1,156 @@ +{ + "Darwin": [ + "No docstring found for class \"trio.MemoryReceiveChannel\"", + "No docstring found for class \"trio._channel.MemoryReceiveChannel\"", + "No docstring found for function \"trio._channel.MemoryReceiveChannel.statistics\"", + "No docstring found for class \"trio._channel.MemoryChannelStats\"", + "No docstring found for function \"trio._channel.MemoryReceiveChannel.aclose\"", + "No docstring found for class \"trio.MemorySendChannel\"", + "No docstring found for class \"trio._channel.MemorySendChannel\"", + "No docstring found for function \"trio._channel.MemorySendChannel.statistics\"", + "No docstring found for function \"trio._channel.MemorySendChannel.aclose\"", + "No docstring found for class \"trio._core._run.Task\"", + "No docstring found for class \"trio._socket.SocketType\"", + "No docstring found for function \"trio._highlevel_socket.SocketStream.send_all\"", + "No docstring found for function \"trio._highlevel_socket.SocketStream.wait_send_all_might_not_block\"", + "No docstring found for function \"trio._highlevel_socket.SocketStream.send_eof\"", + "No docstring found for function \"trio._highlevel_socket.SocketStream.receive_some\"", + "No docstring found for function \"trio._highlevel_socket.SocketStream.aclose\"", + "No docstring found for function \"trio._path.Path.absolute\"", + "No docstring found for class \"trio._path.AsyncAutoWrapperType\"", + "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_forwards\"", + "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_wraps\"", + "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_magic\"", + "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_iter\"", + "No docstring found for function \"trio._subprocess.HasFileno.fileno\"", + "No docstring found for class \"trio._sync.AsyncContextManagerMixin\"", + "No docstring found for function \"trio._sync._HasAcquireRelease.acquire\"", + "No docstring found for function \"trio._sync._HasAcquireRelease.release\"", + "No docstring found for class \"trio._sync._LockImpl\"", + "No docstring found for class \"trio._core._local._NoValue\"", + "No docstring found for class \"trio._core._local.RunVarToken\"", + "No docstring found for class \"trio.lowlevel.RunVarToken\"", + "No docstring found for class \"trio.lowlevel.Task\"", + "No docstring found for class \"trio._core._ki.KIProtectionSignature\"", + "No docstring found for function \"trio._unix_pipes.FdStream.send_all\"", + "No docstring found for function \"trio._unix_pipes.FdStream.wait_send_all_might_not_block\"", + "No docstring found for function \"trio._unix_pipes.FdStream.receive_some\"", + "No docstring found for function \"trio._unix_pipes.FdStream.close\"", + "No docstring found for function \"trio._unix_pipes.FdStream.aclose\"", + "No docstring found for function \"trio._unix_pipes.FdStream.fileno\"", + "No docstring found for class \"trio.socket.SocketType\"", + "No docstring found for class \"trio.socket.gaierror\"", + "No docstring found for class \"trio.socket.herror\"", + "No docstring found for function \"trio._core._mock_clock.MockClock.start_clock\"", + "No docstring found for function \"trio._core._mock_clock.MockClock.current_time\"", + "No docstring found for function \"trio._core._mock_clock.MockClock.deadline_to_sleep_time\"", + "trio.testing._raises_group.RaisesGroup: Type of metaclass unknown", + "trio.testing._raises_group.RaisesGroup: Type of base class \"contextlib.AbstractContextManager\" is partially unknown\n\u00a0\u00a0Type argument 1 for class \"AbstractContextManager\" has partially unknown type", + "trio.testing._raises_group.RaisesGroup: Type of base class unknown", + "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.exconly\"", + "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.errisinstance\"", + "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.getrepr\"", + "No docstring found for function \"trio.testing._raises_group.RaisesGroup.expected_type\"" + ], + "Linux": [ + "No docstring found for class \"trio.MemoryReceiveChannel\"", + "No docstring found for class \"trio._channel.MemoryReceiveChannel\"", + "No docstring found for function \"trio._channel.MemoryReceiveChannel.statistics\"", + "No docstring found for class \"trio._channel.MemoryChannelStats\"", + "No docstring found for function \"trio._channel.MemoryReceiveChannel.aclose\"", + "No docstring found for class \"trio.MemorySendChannel\"", + "No docstring found for class \"trio._channel.MemorySendChannel\"", + "No docstring found for function \"trio._channel.MemorySendChannel.statistics\"", + "No docstring found for function \"trio._channel.MemorySendChannel.aclose\"", + "No docstring found for class \"trio._core._run.Task\"", + "No docstring found for class \"trio._socket.SocketType\"", + "No docstring found for function \"trio._highlevel_socket.SocketStream.send_all\"", + "No docstring found for function \"trio._highlevel_socket.SocketStream.wait_send_all_might_not_block\"", + "No docstring found for function \"trio._highlevel_socket.SocketStream.send_eof\"", + "No docstring found for function \"trio._highlevel_socket.SocketStream.receive_some\"", + "No docstring found for function \"trio._highlevel_socket.SocketStream.aclose\"", + "No docstring found for function \"trio._path.Path.absolute\"", + "No docstring found for class \"trio._path.AsyncAutoWrapperType\"", + "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_forwards\"", + "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_wraps\"", + "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_magic\"", + "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_iter\"", + "No docstring found for function \"trio._subprocess.HasFileno.fileno\"", + "No docstring found for class \"trio._sync.AsyncContextManagerMixin\"", + "No docstring found for function \"trio._sync._HasAcquireRelease.acquire\"", + "No docstring found for function \"trio._sync._HasAcquireRelease.release\"", + "No docstring found for class \"trio._sync._LockImpl\"", + "No docstring found for class \"trio._core._io_epoll._EpollStatistics\"", + "No docstring found for class \"trio._core._local._NoValue\"", + "No docstring found for class \"trio._core._local.RunVarToken\"", + "No docstring found for class \"trio.lowlevel.RunVarToken\"", + "No docstring found for class \"trio.lowlevel.Task\"", + "No docstring found for class \"trio._core._ki.KIProtectionSignature\"", + "No docstring found for function \"trio._unix_pipes.FdStream.send_all\"", + "No docstring found for function \"trio._unix_pipes.FdStream.wait_send_all_might_not_block\"", + "No docstring found for function \"trio._unix_pipes.FdStream.receive_some\"", + "No docstring found for function \"trio._unix_pipes.FdStream.close\"", + "No docstring found for function \"trio._unix_pipes.FdStream.aclose\"", + "No docstring found for function \"trio._unix_pipes.FdStream.fileno\"", + "No docstring found for class \"trio.socket.SocketType\"", + "No docstring found for class \"trio.socket.gaierror\"", + "No docstring found for class \"trio.socket.herror\"", + "No docstring found for function \"trio._core._mock_clock.MockClock.start_clock\"", + "No docstring found for function \"trio._core._mock_clock.MockClock.current_time\"", + "No docstring found for function \"trio._core._mock_clock.MockClock.deadline_to_sleep_time\"", + "trio.testing._raises_group.RaisesGroup: Type of metaclass unknown", + "trio.testing._raises_group.RaisesGroup: Type of base class \"contextlib.AbstractContextManager\" is partially unknown\n\u00a0\u00a0Type argument 1 for class \"AbstractContextManager\" has partially unknown type", + "trio.testing._raises_group.RaisesGroup: Type of base class unknown", + "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.exconly\"", + "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.errisinstance\"", + "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.getrepr\"", + "No docstring found for function \"trio.testing._raises_group.RaisesGroup.expected_type\"" + ], + "Windows": [ + "No docstring found for class \"trio.MemoryReceiveChannel\"", + "No docstring found for class \"trio._channel.MemoryReceiveChannel\"", + "No docstring found for function \"trio._channel.MemoryReceiveChannel.statistics\"", + "No docstring found for class \"trio._channel.MemoryChannelStats\"", + "No docstring found for function \"trio._channel.MemoryReceiveChannel.aclose\"", + "No docstring found for class \"trio.MemorySendChannel\"", + "No docstring found for class \"trio._channel.MemorySendChannel\"", + "No docstring found for function \"trio._channel.MemorySendChannel.statistics\"", + "No docstring found for function \"trio._channel.MemorySendChannel.aclose\"", + "No docstring found for class \"trio._core._run.Task\"", + "No docstring found for class \"trio._socket.SocketType\"", + "No docstring found for function \"trio._highlevel_socket.SocketStream.send_all\"", + "No docstring found for function \"trio._highlevel_socket.SocketStream.wait_send_all_might_not_block\"", + "No docstring found for function \"trio._highlevel_socket.SocketStream.send_eof\"", + "No docstring found for function \"trio._highlevel_socket.SocketStream.receive_some\"", + "No docstring found for function \"trio._highlevel_socket.SocketStream.aclose\"", + "No docstring found for function \"trio._path.Path.absolute\"", + "No docstring found for class \"trio._path.AsyncAutoWrapperType\"", + "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_forwards\"", + "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_wraps\"", + "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_magic\"", + "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_iter\"", + "No docstring found for function \"trio._subprocess.HasFileno.fileno\"", + "No docstring found for class \"trio._sync.AsyncContextManagerMixin\"", + "No docstring found for function \"trio._sync._HasAcquireRelease.acquire\"", + "No docstring found for function \"trio._sync._HasAcquireRelease.release\"", + "No docstring found for class \"trio._sync._LockImpl\"", + "No docstring found for class \"trio._core._local._NoValue\"", + "No docstring found for class \"trio._core._local.RunVarToken\"", + "No docstring found for class \"trio.lowlevel.RunVarToken\"", + "No docstring found for class \"trio.lowlevel.Task\"", + "No docstring found for class \"trio._core._ki.KIProtectionSignature\"", + "No docstring found for class \"trio.socket.SocketType\"", + "No docstring found for class \"trio.socket.gaierror\"", + "No docstring found for class \"trio.socket.herror\"", + "No docstring found for function \"trio._core._mock_clock.MockClock.start_clock\"", + "No docstring found for function \"trio._core._mock_clock.MockClock.current_time\"", + "No docstring found for function \"trio._core._mock_clock.MockClock.deadline_to_sleep_time\"", + "trio.testing._raises_group.RaisesGroup: Type of metaclass unknown", + "trio.testing._raises_group.RaisesGroup: Type of base class \"contextlib.AbstractContextManager\" is partially unknown\n\u00a0\u00a0Type argument 1 for class \"AbstractContextManager\" has partially unknown type", + "trio.testing._raises_group.RaisesGroup: Type of base class unknown", + "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.exconly\"", + "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.errisinstance\"", + "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.getrepr\"", + "No docstring found for function \"trio.testing._raises_group.RaisesGroup.expected_type\"" + ] +} diff --git a/src/trio/_tests/check_type_completeness.py b/src/trio/_tests/check_type_completeness.py index 273e7db897..4a45fc63a4 100755 --- a/src/trio/_tests/check_type_completeness.py +++ b/src/trio/_tests/check_type_completeness.py @@ -32,8 +32,14 @@ def run_pyright(platform: str) -> subprocess.CompletedProcess[bytes]: def has_docstring_at_runtime(name: str) -> bool: + """Pyright gives us an object identifier of xx.yy.zz + This function tries to decompose that into its constituent parts, such that we + can resolve it, in order to check whether it has a `__doc__` at runtime and + verifytypes misses it because we're doing overly fancy stuff. + """ assert trio.testing - # this is rickety, but works for all current symbols + + # figure out what part of the name is the module, so we can "import" it name_parts = name.split(".") split_i = 1 if name_parts[1] == "tests": @@ -43,49 +49,69 @@ def has_docstring_at_runtime(name: str) -> bool: else: split_i = 1 module = sys.modules[".".join(name_parts[:split_i])] + + # traverse down the remaining identifiers with getattr obj = module try: for obj_name in name_parts[split_i:]: obj = getattr(obj, obj_name) except AttributeError as exc: # asynciowrapper does funky getattr stuff - if "AsyncIOWrapper" in str(exc): + if "AsyncIOWrapper" in str(exc) or name in ( + # Symbols not existing on all platforms, so we can't dynamically inspect them. + # Manually confirmed to have docstrings but pyright doesn't see them due to + # export shenanigans. + # darwin + "trio.lowlevel.current_kqueue", + "trio.lowlevel.monitor_kevent", + "trio.lowlevel.wait_kevent", + "trio._core._io_kqueue._KqueueStatistics", + # windows + "trio._socket.SocketType.share", + "trio._core._io_windows._WindowsStatistics", + "trio._core._windows_cffi.Handle", + "trio.lowlevel.current_iocp", + "trio.lowlevel.monitor_completion_key", + "trio.lowlevel.readinto_overlapped", + "trio.lowlevel.register_with_iocp", + "trio.lowlevel.wait_overlapped", + "trio.lowlevel.write_overlapped", + "trio.lowlevel.WaitForSingleObject", + "trio.socket.fromshare", + # these are erroring on all platforms + "trio._highlevel_generic.StapledStream.send_stream", + "trio._highlevel_generic.StapledStream.receive_stream", + "trio._ssl.SSLStream.transport_stream", + "trio._file_io._HasFileNo", + "trio._file_io._HasFileNo.fileno", + ): return True - # raise - print(exc) + + else: + print( + f"Pyright sees {name} at runtime, but unable to getattr({obj.__name__}, {obj_name})." + ) return bool(obj.__doc__) -def check_type(args: argparse.Namespace, platform: str) -> int: +def check_type( + platform: str, full_diagnostics_file: Path | None, expected_errors: list[object] +) -> list[object]: # convince isort we use the trio import assert trio + # run pyright, load output into json res = run_pyright(platform) current_result = json.loads(res.stdout) if res.stderr: print(res.stderr) - if args.full_diagnostics_file: - with open(args.full_diagnostics_file, "w") as f: + if full_diagnostics_file: + with open(full_diagnostics_file, "a") as f: json.dump(current_result, f, sort_keys=True, indent=4) - counts = {} - for where, key in ( - (("typeCompleteness",), "missingFunctionDocStringCount"), - (("typeCompleteness",), "missingClassDocStringCount"), - (("typeCompleteness",), "missingDefaultParamCount"), - (("summary",), "errorCount"), - (("summary",), "warningCount"), - (("summary",), "informationCount"), - (("typeCompleteness", "exportedSymbolCounts"), "withUnknownType"), - (("typeCompleteness", "exportedSymbolCounts"), "withAmbiguousType"), - ): - curr_dict = current_result - for subdict in where: - curr_dict = curr_dict[subdict] - - counts[key] = curr_dict[key] + errors = [] for symbol in current_result["typeCompleteness"]["symbols"]: category = symbol["category"] @@ -105,39 +131,75 @@ def check_type(args: argparse.Namespace, platform: str) -> int: ) continue for diagnostic in diagnostics: - if diagnostic["message"].startswith( - "No docstring found for" - ) and has_docstring_at_runtime(symbol["name"]): - if category in ("method", "function"): - counts["missingFunctionDocStringCount"] -= 1 - elif category == "class": - counts["missingClassDocStringCount"] -= 1 - else: - raise AssertionError("This category shouldn't be possible here") - continue - - if diagnostic["message"] in printed_diagnostics: - continue - print(diagnostic["message"]) - printed_diagnostics.add(diagnostic["message"]) + message = diagnostic["message"] + # ignore errors about missing docstrings if they're available at runtime + if message.startswith("No docstring found for"): + if has_docstring_at_runtime(symbol["name"]): + continue + else: + message = f"{name}: {message}" + # try: + # expected_errors.remove(diagnostic) + # # decrement count for object + # except ValueError: + if message not in expected_errors and message not in printed_diagnostics: + print(f"new error: {message}") + errors.append(message) + printed_diagnostics.add(message) continue - for name, val in counts.items(): - if val > 0: - print(f"ERROR: {name} is {val}") - failed = True - - return int(failed) + return errors def main(args: argparse.Namespace) -> int: - res = 0 + if args.full_diagnostics_file: + full_diagnostics_file = Path(args.full_diagnostics_file) + full_diagnostics_file.write_text("") + else: + full_diagnostics_file = None + + errors_by_platform_file = Path(__file__).parent / "_check_type_completeness.json" + if errors_by_platform_file.exists(): + with open(errors_by_platform_file) as f: + errors_by_platform = json.load(f) + else: + errors_by_platform = {"Linux": [], "Windows": [], "Darwin": []} + + changed = False for platform in "Linux", "Windows", "Darwin": print("*" * 20, f"\nChecking {platform}...") - res += check_type(args, platform) + errors = check_type( + platform, full_diagnostics_file, errors_by_platform[platform] + ) + + new_errors = [e for e in errors if e not in errors_by_platform[platform]] + missing_errors = [e for e in errors_by_platform[platform] if e not in errors] + + if new_errors: + print( + f"New errors introduced in `pyright --verifytypes`. Fix them, or ignore them by modifying {errors_by_platform_file}. The latter can be done by pre-commit CI bot." + ) + # print(new_errors) + changed = True + if missing_errors: + print( + f"Congratulations, you have resolved existing errors! Please remove them from {errors_by_platform_file}, either manually or with the pre-commit CI bot." + ) + # print(missing_errors) + changed = True + + errors_by_platform[platform] = errors print("*" * 20) - return res + + if changed and args.overwrite_file: + with open(errors_by_platform_file, "w") as f: + json.dump(errors_by_platform, f, indent=4, sort_keys=True) + # newline at end of file + f.write("\n") + + # True -> 1 -> non-zero exit value -> error + return changed parser = argparse.ArgumentParser() From ecde0eecacb8558909f7d8c3278dfa058c9c7d1f Mon Sep 17 00:00:00 2001 From: jakkdl Date: Wed, 21 Feb 2024 12:41:30 +0100 Subject: [PATCH 5/8] move errors on all platforms into a separate category. add comments and stuff. Remove logic for 'not warned by pyright but missing docstring' cause that mostly shouldn't be a thing anymore --- src/trio/_tests/_check_type_completeness.json | 89 +++---------------- src/trio/_tests/check_type_completeness.py | 69 +++++++------- 2 files changed, 51 insertions(+), 107 deletions(-) diff --git a/src/trio/_tests/_check_type_completeness.json b/src/trio/_tests/_check_type_completeness.json index 2e2d7b6ef7..8405b9bb1e 100644 --- a/src/trio/_tests/_check_type_completeness.json +++ b/src/trio/_tests/_check_type_completeness.json @@ -1,112 +1,47 @@ { "Darwin": [ - "No docstring found for class \"trio.MemoryReceiveChannel\"", - "No docstring found for class \"trio._channel.MemoryReceiveChannel\"", - "No docstring found for function \"trio._channel.MemoryReceiveChannel.statistics\"", - "No docstring found for class \"trio._channel.MemoryChannelStats\"", - "No docstring found for function \"trio._channel.MemoryReceiveChannel.aclose\"", - "No docstring found for class \"trio.MemorySendChannel\"", - "No docstring found for class \"trio._channel.MemorySendChannel\"", - "No docstring found for function \"trio._channel.MemorySendChannel.statistics\"", - "No docstring found for function \"trio._channel.MemorySendChannel.aclose\"", - "No docstring found for class \"trio._core._run.Task\"", - "No docstring found for class \"trio._socket.SocketType\"", - "No docstring found for function \"trio._highlevel_socket.SocketStream.send_all\"", - "No docstring found for function \"trio._highlevel_socket.SocketStream.wait_send_all_might_not_block\"", - "No docstring found for function \"trio._highlevel_socket.SocketStream.send_eof\"", - "No docstring found for function \"trio._highlevel_socket.SocketStream.receive_some\"", - "No docstring found for function \"trio._highlevel_socket.SocketStream.aclose\"", - "No docstring found for function \"trio._path.Path.absolute\"", - "No docstring found for class \"trio._path.AsyncAutoWrapperType\"", - "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_forwards\"", - "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_wraps\"", - "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_magic\"", - "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_iter\"", - "No docstring found for function \"trio._subprocess.HasFileno.fileno\"", - "No docstring found for class \"trio._sync.AsyncContextManagerMixin\"", - "No docstring found for function \"trio._sync._HasAcquireRelease.acquire\"", - "No docstring found for function \"trio._sync._HasAcquireRelease.release\"", - "No docstring found for class \"trio._sync._LockImpl\"", - "No docstring found for class \"trio._core._local._NoValue\"", - "No docstring found for class \"trio._core._local.RunVarToken\"", - "No docstring found for class \"trio.lowlevel.RunVarToken\"", - "No docstring found for class \"trio.lowlevel.Task\"", - "No docstring found for class \"trio._core._ki.KIProtectionSignature\"", "No docstring found for function \"trio._unix_pipes.FdStream.send_all\"", "No docstring found for function \"trio._unix_pipes.FdStream.wait_send_all_might_not_block\"", "No docstring found for function \"trio._unix_pipes.FdStream.receive_some\"", "No docstring found for function \"trio._unix_pipes.FdStream.close\"", "No docstring found for function \"trio._unix_pipes.FdStream.aclose\"", - "No docstring found for function \"trio._unix_pipes.FdStream.fileno\"", - "No docstring found for class \"trio.socket.SocketType\"", - "No docstring found for class \"trio.socket.gaierror\"", - "No docstring found for class \"trio.socket.herror\"", - "No docstring found for function \"trio._core._mock_clock.MockClock.start_clock\"", - "No docstring found for function \"trio._core._mock_clock.MockClock.current_time\"", - "No docstring found for function \"trio._core._mock_clock.MockClock.deadline_to_sleep_time\"", - "trio.testing._raises_group.RaisesGroup: Type of metaclass unknown", - "trio.testing._raises_group.RaisesGroup: Type of base class \"contextlib.AbstractContextManager\" is partially unknown\n\u00a0\u00a0Type argument 1 for class \"AbstractContextManager\" has partially unknown type", - "trio.testing._raises_group.RaisesGroup: Type of base class unknown", - "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.exconly\"", - "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.errisinstance\"", - "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.getrepr\"", - "No docstring found for function \"trio.testing._raises_group.RaisesGroup.expected_type\"" + "No docstring found for function \"trio._unix_pipes.FdStream.fileno\"" ], "Linux": [ + "No docstring found for class \"trio._core._io_epoll._EpollStatistics\"", + "No docstring found for function \"trio._unix_pipes.FdStream.send_all\"", + "No docstring found for function \"trio._unix_pipes.FdStream.wait_send_all_might_not_block\"", + "No docstring found for function \"trio._unix_pipes.FdStream.receive_some\"", + "No docstring found for function \"trio._unix_pipes.FdStream.close\"", + "No docstring found for function \"trio._unix_pipes.FdStream.aclose\"", + "No docstring found for function \"trio._unix_pipes.FdStream.fileno\"" + ], + "Windows": [], + "all": [ "No docstring found for class \"trio.MemoryReceiveChannel\"", - "No docstring found for class \"trio._channel.MemoryReceiveChannel\"", "No docstring found for function \"trio._channel.MemoryReceiveChannel.statistics\"", - "No docstring found for class \"trio._channel.MemoryChannelStats\"", "No docstring found for function \"trio._channel.MemoryReceiveChannel.aclose\"", - "No docstring found for class \"trio.MemorySendChannel\"", "No docstring found for class \"trio._channel.MemorySendChannel\"", - "No docstring found for function \"trio._channel.MemorySendChannel.statistics\"", "No docstring found for function \"trio._channel.MemorySendChannel.aclose\"", - "No docstring found for class \"trio._core._run.Task\"", "No docstring found for class \"trio._socket.SocketType\"", - "No docstring found for function \"trio._highlevel_socket.SocketStream.send_all\"", "No docstring found for function \"trio._highlevel_socket.SocketStream.wait_send_all_might_not_block\"", - "No docstring found for function \"trio._highlevel_socket.SocketStream.send_eof\"", "No docstring found for function \"trio._highlevel_socket.SocketStream.receive_some\"", - "No docstring found for function \"trio._highlevel_socket.SocketStream.aclose\"", "No docstring found for function \"trio._path.Path.absolute\"", - "No docstring found for class \"trio._path.AsyncAutoWrapperType\"", "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_forwards\"", - "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_wraps\"", "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_magic\"", - "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_iter\"", "No docstring found for function \"trio._subprocess.HasFileno.fileno\"", - "No docstring found for class \"trio._sync.AsyncContextManagerMixin\"", "No docstring found for function \"trio._sync._HasAcquireRelease.acquire\"", - "No docstring found for function \"trio._sync._HasAcquireRelease.release\"", "No docstring found for class \"trio._sync._LockImpl\"", - "No docstring found for class \"trio._core._io_epoll._EpollStatistics\"", "No docstring found for class \"trio._core._local._NoValue\"", - "No docstring found for class \"trio._core._local.RunVarToken\"", "No docstring found for class \"trio.lowlevel.RunVarToken\"", - "No docstring found for class \"trio.lowlevel.Task\"", "No docstring found for class \"trio._core._ki.KIProtectionSignature\"", - "No docstring found for function \"trio._unix_pipes.FdStream.send_all\"", - "No docstring found for function \"trio._unix_pipes.FdStream.wait_send_all_might_not_block\"", - "No docstring found for function \"trio._unix_pipes.FdStream.receive_some\"", - "No docstring found for function \"trio._unix_pipes.FdStream.close\"", - "No docstring found for function \"trio._unix_pipes.FdStream.aclose\"", - "No docstring found for function \"trio._unix_pipes.FdStream.fileno\"", "No docstring found for class \"trio.socket.SocketType\"", - "No docstring found for class \"trio.socket.gaierror\"", "No docstring found for class \"trio.socket.herror\"", - "No docstring found for function \"trio._core._mock_clock.MockClock.start_clock\"", "No docstring found for function \"trio._core._mock_clock.MockClock.current_time\"", - "No docstring found for function \"trio._core._mock_clock.MockClock.deadline_to_sleep_time\"", "trio.testing._raises_group.RaisesGroup: Type of metaclass unknown", - "trio.testing._raises_group.RaisesGroup: Type of base class \"contextlib.AbstractContextManager\" is partially unknown\n\u00a0\u00a0Type argument 1 for class \"AbstractContextManager\" has partially unknown type", "trio.testing._raises_group.RaisesGroup: Type of base class unknown", - "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.exconly\"", "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.errisinstance\"", - "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.getrepr\"", - "No docstring found for function \"trio.testing._raises_group.RaisesGroup.expected_type\"" - ], - "Windows": [ + "No docstring found for function \"trio.testing._raises_group.RaisesGroup.expected_type\"", "No docstring found for class \"trio.MemoryReceiveChannel\"", "No docstring found for class \"trio._channel.MemoryReceiveChannel\"", "No docstring found for function \"trio._channel.MemoryReceiveChannel.statistics\"", diff --git a/src/trio/_tests/check_type_completeness.py b/src/trio/_tests/check_type_completeness.py index 4a45fc63a4..179f04275f 100755 --- a/src/trio/_tests/check_type_completeness.py +++ b/src/trio/_tests/check_type_completeness.py @@ -1,4 +1,10 @@ #!/usr/bin/env python3 +"""This is a file that wraps calls to `pyright --verifytypes`, achieving two things: +1. give an error if docstrings are missing. + pyright will give a number of missing docstrings, and error messages, but not exit with a non-zero value. +2. filter out specific errors we don't care about. + this is largely due to 1, but also because Trio does some very complex stuff and --verifytypes has few to no ways of ignoring specific errors. +""" from __future__ import annotations # this file is not run as part of the tests, instead it's run standalone from check.sh @@ -11,6 +17,8 @@ import trio import trio.testing +# not needed if everything is working, but if somebody does something to generate +# tons of errors, we can be nice and stop them from getting 3*tons of output printed_diagnostics: set[str] = set() @@ -60,7 +68,7 @@ def has_docstring_at_runtime(name: str) -> bool: if "AsyncIOWrapper" in str(exc) or name in ( # Symbols not existing on all platforms, so we can't dynamically inspect them. # Manually confirmed to have docstrings but pyright doesn't see them due to - # export shenanigans. + # export shenanigans. TODO: actually manually confirm that. # darwin "trio.lowlevel.current_kqueue", "trio.lowlevel.monitor_kevent", @@ -78,7 +86,7 @@ def has_docstring_at_runtime(name: str) -> bool: "trio.lowlevel.write_overlapped", "trio.lowlevel.WaitForSingleObject", "trio.socket.fromshare", - # these are erroring on all platforms + # TODO: these are erroring on all platforms, why? "trio._highlevel_generic.StapledStream.send_stream", "trio._highlevel_generic.StapledStream.receive_stream", "trio._ssl.SSLStream.transport_stream", @@ -91,6 +99,7 @@ def has_docstring_at_runtime(name: str) -> bool: print( f"Pyright sees {name} at runtime, but unable to getattr({obj.__name__}, {obj_name})." ) + return False return bool(obj.__doc__) @@ -114,22 +123,8 @@ def check_type( errors = [] for symbol in current_result["typeCompleteness"]["symbols"]: - category = symbol["category"] diagnostics = symbol["diagnostics"] name = symbol["name"] - if not diagnostics: - if ( - category - not in ("variable", "symbol", "type alias", "constant", "module") - and not name.endswith("__") - and not has_docstring_at_runtime(symbol["name"]) - ): - print( - "not warned by pyright, but missing docstring:", - symbol["name"], - symbol["category"], - ) - continue for diagnostic in diagnostics: message = diagnostic["message"] # ignore errors about missing docstrings if they're available at runtime @@ -137,11 +132,9 @@ def check_type( if has_docstring_at_runtime(symbol["name"]): continue else: + # Missing docstring messages include the name of the object. + # Other errors don't, so we add it. message = f"{name}: {message}" - # try: - # expected_errors.remove(diagnostic) - # # decrement count for object - # except ValueError: if message not in expected_errors and message not in printed_diagnostics: print(f"new error: {message}") errors.append(message) @@ -164,34 +157,40 @@ def main(args: argparse.Namespace) -> int: with open(errors_by_platform_file) as f: errors_by_platform = json.load(f) else: - errors_by_platform = {"Linux": [], "Windows": [], "Darwin": []} + errors_by_platform = {"Linux": [], "Windows": [], "Darwin": [], "all": []} changed = False for platform in "Linux", "Windows", "Darwin": + platform_errors = errors_by_platform[platform] + errors_by_platform["all"] print("*" * 20, f"\nChecking {platform}...") - errors = check_type( - platform, full_diagnostics_file, errors_by_platform[platform] - ) + errors = check_type(platform, full_diagnostics_file, platform_errors) - new_errors = [e for e in errors if e not in errors_by_platform[platform]] - missing_errors = [e for e in errors_by_platform[platform] if e not in errors] + new_errors = [e for e in errors if e not in platform_errors] + missing_errors = [e for e in platform_errors if e not in errors] if new_errors: print( f"New errors introduced in `pyright --verifytypes`. Fix them, or ignore them by modifying {errors_by_platform_file}. The latter can be done by pre-commit CI bot." ) - # print(new_errors) changed = True if missing_errors: print( f"Congratulations, you have resolved existing errors! Please remove them from {errors_by_platform_file}, either manually or with the pre-commit CI bot." ) - # print(missing_errors) changed = True + print(missing_errors) errors_by_platform[platform] = errors print("*" * 20) + # cut down the size of the json file by a lot, and make it easier to parse for + # humans, by moving errors that appear on all platforms to a separate category + for e in errors_by_platform["Linux"].copy(): + if e in errors_by_platform["Darwin"] and e in errors_by_platform["Windows"]: + for platform in "Linux", "Windows", "Darwin": + errors_by_platform[platform].remove(e) + errors_by_platform["all"].append(e) + if changed and args.overwrite_file: with open(errors_by_platform_file, "w") as f: json.dump(errors_by_platform, f, indent=4, sort_keys=True) @@ -203,8 +202,18 @@ def main(args: argparse.Namespace) -> int: parser = argparse.ArgumentParser() -parser.add_argument("--overwrite-file", action="store_true", default=False) -parser.add_argument("--full-diagnostics-file", type=Path, default=None) +parser.add_argument( + "--overwrite-file", + action="store_true", + default=False, + help="Use this flag to overwrite the current stored results. Either in CI together with a diff check, or to avoid having to manually correct it.", +) +parser.add_argument( + "--full-diagnostics-file", + type=Path, + default=None, + help="Use this for debugging, it will dump the output of all three pyright runs by platform into this file.", +) args = parser.parse_args() assert __name__ == "__main__", "This script should be run standalone" From 930e1a0b2d72f991d9373aeddb47b4c3bba7b926 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Wed, 21 Feb 2024 12:57:35 +0100 Subject: [PATCH 6/8] running with typing dependencies installed is good, actually --- src/trio/_tests/_check_type_completeness.json | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/trio/_tests/_check_type_completeness.json b/src/trio/_tests/_check_type_completeness.json index 8405b9bb1e..638de610b6 100644 --- a/src/trio/_tests/_check_type_completeness.json +++ b/src/trio/_tests/_check_type_completeness.json @@ -86,6 +86,48 @@ "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.exconly\"", "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.errisinstance\"", "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.getrepr\"", + "No docstring found for function \"trio.testing._raises_group.RaisesGroup.expected_type\"", + "No docstring found for class \"trio.MemoryReceiveChannel\"", + "No docstring found for class \"trio._channel.MemoryReceiveChannel\"", + "No docstring found for function \"trio._channel.MemoryReceiveChannel.statistics\"", + "No docstring found for class \"trio._channel.MemoryChannelStats\"", + "No docstring found for function \"trio._channel.MemoryReceiveChannel.aclose\"", + "No docstring found for class \"trio.MemorySendChannel\"", + "No docstring found for class \"trio._channel.MemorySendChannel\"", + "No docstring found for function \"trio._channel.MemorySendChannel.statistics\"", + "No docstring found for function \"trio._channel.MemorySendChannel.aclose\"", + "No docstring found for class \"trio._core._run.Task\"", + "No docstring found for class \"trio._socket.SocketType\"", + "No docstring found for function \"trio._highlevel_socket.SocketStream.send_all\"", + "No docstring found for function \"trio._highlevel_socket.SocketStream.wait_send_all_might_not_block\"", + "No docstring found for function \"trio._highlevel_socket.SocketStream.send_eof\"", + "No docstring found for function \"trio._highlevel_socket.SocketStream.receive_some\"", + "No docstring found for function \"trio._highlevel_socket.SocketStream.aclose\"", + "No docstring found for function \"trio._path.Path.absolute\"", + "No docstring found for class \"trio._path.AsyncAutoWrapperType\"", + "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_forwards\"", + "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_wraps\"", + "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_magic\"", + "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_iter\"", + "No docstring found for function \"trio._subprocess.HasFileno.fileno\"", + "No docstring found for class \"trio._sync.AsyncContextManagerMixin\"", + "No docstring found for function \"trio._sync._HasAcquireRelease.acquire\"", + "No docstring found for function \"trio._sync._HasAcquireRelease.release\"", + "No docstring found for class \"trio._sync._LockImpl\"", + "No docstring found for class \"trio._core._local._NoValue\"", + "No docstring found for class \"trio._core._local.RunVarToken\"", + "No docstring found for class \"trio.lowlevel.RunVarToken\"", + "No docstring found for class \"trio.lowlevel.Task\"", + "No docstring found for class \"trio._core._ki.KIProtectionSignature\"", + "No docstring found for class \"trio.socket.SocketType\"", + "No docstring found for class \"trio.socket.gaierror\"", + "No docstring found for class \"trio.socket.herror\"", + "No docstring found for function \"trio._core._mock_clock.MockClock.start_clock\"", + "No docstring found for function \"trio._core._mock_clock.MockClock.current_time\"", + "No docstring found for function \"trio._core._mock_clock.MockClock.deadline_to_sleep_time\"", + "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.exconly\"", + "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.errisinstance\"", + "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.getrepr\"", "No docstring found for function \"trio.testing._raises_group.RaisesGroup.expected_type\"" ] } From 3b48476be18793037416baf09e07efeb1de8a14a Mon Sep 17 00:00:00 2001 From: jakkdl Date: Wed, 21 Feb 2024 15:13:11 +0100 Subject: [PATCH 7/8] remove old errors from 'all' --- src/trio/_tests/_check_type_completeness.json | 69 ------------------- src/trio/_tests/check_type_completeness.py | 1 + 2 files changed, 1 insertion(+), 69 deletions(-) diff --git a/src/trio/_tests/_check_type_completeness.json b/src/trio/_tests/_check_type_completeness.json index 638de610b6..21654d2030 100644 --- a/src/trio/_tests/_check_type_completeness.json +++ b/src/trio/_tests/_check_type_completeness.json @@ -18,75 +18,6 @@ ], "Windows": [], "all": [ - "No docstring found for class \"trio.MemoryReceiveChannel\"", - "No docstring found for function \"trio._channel.MemoryReceiveChannel.statistics\"", - "No docstring found for function \"trio._channel.MemoryReceiveChannel.aclose\"", - "No docstring found for class \"trio._channel.MemorySendChannel\"", - "No docstring found for function \"trio._channel.MemorySendChannel.aclose\"", - "No docstring found for class \"trio._socket.SocketType\"", - "No docstring found for function \"trio._highlevel_socket.SocketStream.wait_send_all_might_not_block\"", - "No docstring found for function \"trio._highlevel_socket.SocketStream.receive_some\"", - "No docstring found for function \"trio._path.Path.absolute\"", - "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_forwards\"", - "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_magic\"", - "No docstring found for function \"trio._subprocess.HasFileno.fileno\"", - "No docstring found for function \"trio._sync._HasAcquireRelease.acquire\"", - "No docstring found for class \"trio._sync._LockImpl\"", - "No docstring found for class \"trio._core._local._NoValue\"", - "No docstring found for class \"trio.lowlevel.RunVarToken\"", - "No docstring found for class \"trio._core._ki.KIProtectionSignature\"", - "No docstring found for class \"trio.socket.SocketType\"", - "No docstring found for class \"trio.socket.herror\"", - "No docstring found for function \"trio._core._mock_clock.MockClock.current_time\"", - "trio.testing._raises_group.RaisesGroup: Type of metaclass unknown", - "trio.testing._raises_group.RaisesGroup: Type of base class unknown", - "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.errisinstance\"", - "No docstring found for function \"trio.testing._raises_group.RaisesGroup.expected_type\"", - "No docstring found for class \"trio.MemoryReceiveChannel\"", - "No docstring found for class \"trio._channel.MemoryReceiveChannel\"", - "No docstring found for function \"trio._channel.MemoryReceiveChannel.statistics\"", - "No docstring found for class \"trio._channel.MemoryChannelStats\"", - "No docstring found for function \"trio._channel.MemoryReceiveChannel.aclose\"", - "No docstring found for class \"trio.MemorySendChannel\"", - "No docstring found for class \"trio._channel.MemorySendChannel\"", - "No docstring found for function \"trio._channel.MemorySendChannel.statistics\"", - "No docstring found for function \"trio._channel.MemorySendChannel.aclose\"", - "No docstring found for class \"trio._core._run.Task\"", - "No docstring found for class \"trio._socket.SocketType\"", - "No docstring found for function \"trio._highlevel_socket.SocketStream.send_all\"", - "No docstring found for function \"trio._highlevel_socket.SocketStream.wait_send_all_might_not_block\"", - "No docstring found for function \"trio._highlevel_socket.SocketStream.send_eof\"", - "No docstring found for function \"trio._highlevel_socket.SocketStream.receive_some\"", - "No docstring found for function \"trio._highlevel_socket.SocketStream.aclose\"", - "No docstring found for function \"trio._path.Path.absolute\"", - "No docstring found for class \"trio._path.AsyncAutoWrapperType\"", - "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_forwards\"", - "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_wraps\"", - "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_magic\"", - "No docstring found for function \"trio._path.AsyncAutoWrapperType.generate_iter\"", - "No docstring found for function \"trio._subprocess.HasFileno.fileno\"", - "No docstring found for class \"trio._sync.AsyncContextManagerMixin\"", - "No docstring found for function \"trio._sync._HasAcquireRelease.acquire\"", - "No docstring found for function \"trio._sync._HasAcquireRelease.release\"", - "No docstring found for class \"trio._sync._LockImpl\"", - "No docstring found for class \"trio._core._local._NoValue\"", - "No docstring found for class \"trio._core._local.RunVarToken\"", - "No docstring found for class \"trio.lowlevel.RunVarToken\"", - "No docstring found for class \"trio.lowlevel.Task\"", - "No docstring found for class \"trio._core._ki.KIProtectionSignature\"", - "No docstring found for class \"trio.socket.SocketType\"", - "No docstring found for class \"trio.socket.gaierror\"", - "No docstring found for class \"trio.socket.herror\"", - "No docstring found for function \"trio._core._mock_clock.MockClock.start_clock\"", - "No docstring found for function \"trio._core._mock_clock.MockClock.current_time\"", - "No docstring found for function \"trio._core._mock_clock.MockClock.deadline_to_sleep_time\"", - "trio.testing._raises_group.RaisesGroup: Type of metaclass unknown", - "trio.testing._raises_group.RaisesGroup: Type of base class \"contextlib.AbstractContextManager\" is partially unknown\n\u00a0\u00a0Type argument 1 for class \"AbstractContextManager\" has partially unknown type", - "trio.testing._raises_group.RaisesGroup: Type of base class unknown", - "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.exconly\"", - "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.errisinstance\"", - "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.getrepr\"", - "No docstring found for function \"trio.testing._raises_group.RaisesGroup.expected_type\"", "No docstring found for class \"trio.MemoryReceiveChannel\"", "No docstring found for class \"trio._channel.MemoryReceiveChannel\"", "No docstring found for function \"trio._channel.MemoryReceiveChannel.statistics\"", diff --git a/src/trio/_tests/check_type_completeness.py b/src/trio/_tests/check_type_completeness.py index 179f04275f..1e18dbbd59 100755 --- a/src/trio/_tests/check_type_completeness.py +++ b/src/trio/_tests/check_type_completeness.py @@ -185,6 +185,7 @@ def main(args: argparse.Namespace) -> int: # cut down the size of the json file by a lot, and make it easier to parse for # humans, by moving errors that appear on all platforms to a separate category + errors_by_platform["all"] = [] for e in errors_by_platform["Linux"].copy(): if e in errors_by_platform["Darwin"] and e in errors_by_platform["Windows"]: for platform in "Linux", "Windows", "Darwin": From dd976ba6b21fa394718ba4ee9e50ab14a8e86b85 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Mon, 4 Mar 2024 13:47:27 +0100 Subject: [PATCH 8/8] fixes after review from a5rocks --- src/trio/_tests/check_type_completeness.py | 35 +++++++++++++--------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/trio/_tests/check_type_completeness.py b/src/trio/_tests/check_type_completeness.py index 1e18dbbd59..3773925644 100755 --- a/src/trio/_tests/check_type_completeness.py +++ b/src/trio/_tests/check_type_completeness.py @@ -45,23 +45,20 @@ def has_docstring_at_runtime(name: str) -> bool: can resolve it, in order to check whether it has a `__doc__` at runtime and verifytypes misses it because we're doing overly fancy stuff. """ + # This assert is solely for stopping isort from removing our imports of trio & trio.testing + # It could also be done with isort:skip, but that'd also disable import sorting and the like. assert trio.testing # figure out what part of the name is the module, so we can "import" it name_parts = name.split(".") - split_i = 1 + assert name_parts[0] == "trio" if name_parts[1] == "tests": return True - if name_parts[1] in ("_core", "testing"): # noqa: SIM108 - split_i = 2 - else: - split_i = 1 - module = sys.modules[".".join(name_parts[:split_i])] # traverse down the remaining identifiers with getattr - obj = module + obj = trio try: - for obj_name in name_parts[split_i:]: + for obj_name in name_parts[1:]: obj = getattr(obj, obj_name) except AttributeError as exc: # asynciowrapper does funky getattr stuff @@ -69,6 +66,9 @@ def has_docstring_at_runtime(name: str) -> bool: # Symbols not existing on all platforms, so we can't dynamically inspect them. # Manually confirmed to have docstrings but pyright doesn't see them due to # export shenanigans. TODO: actually manually confirm that. + # In theory we could verify these at runtime, probably by running the script separately + # on separate platforms. It might also be a decent idea to work the other way around, + # a la test_static_tool_sees_class_members # darwin "trio.lowlevel.current_kqueue", "trio.lowlevel.monitor_kevent", @@ -86,6 +86,10 @@ def has_docstring_at_runtime(name: str) -> bool: "trio.lowlevel.write_overlapped", "trio.lowlevel.WaitForSingleObject", "trio.socket.fromshare", + # linux + # this test will fail on linux, but I don't develop on linux. So the next + # person to do so is very welcome to open a pull request and populate with + # objects # TODO: these are erroring on all platforms, why? "trio._highlevel_generic.StapledStream.send_stream", "trio._highlevel_generic.StapledStream.receive_stream", @@ -97,7 +101,8 @@ def has_docstring_at_runtime(name: str) -> bool: else: print( - f"Pyright sees {name} at runtime, but unable to getattr({obj.__name__}, {obj_name})." + f"Pyright sees {name} at runtime, but unable to getattr({obj.__name__}, {obj_name}).", + file=sys.stderr, ) return False return bool(obj.__doc__) @@ -114,7 +119,7 @@ def check_type( current_result = json.loads(res.stdout) if res.stderr: - print(res.stderr) + print(res.stderr, file=sys.stderr) if full_diagnostics_file: with open(full_diagnostics_file, "a") as f: @@ -136,7 +141,7 @@ def check_type( # Other errors don't, so we add it. message = f"{name}: {message}" if message not in expected_errors and message not in printed_diagnostics: - print(f"new error: {message}") + print(f"new error: {message}", file=sys.stderr) errors.append(message) printed_diagnostics.add(message) @@ -170,15 +175,17 @@ def main(args: argparse.Namespace) -> int: if new_errors: print( - f"New errors introduced in `pyright --verifytypes`. Fix them, or ignore them by modifying {errors_by_platform_file}. The latter can be done by pre-commit CI bot." + f"New errors introduced in `pyright --verifytypes`. Fix them, or ignore them by modifying {errors_by_platform_file}. The latter can be done by pre-commit CI bot.", + file=sys.stderr, ) changed = True if missing_errors: print( - f"Congratulations, you have resolved existing errors! Please remove them from {errors_by_platform_file}, either manually or with the pre-commit CI bot." + f"Congratulations, you have resolved existing errors! Please remove them from {errors_by_platform_file}, either manually or with the pre-commit CI bot.", + file=sys.stderr, ) changed = True - print(missing_errors) + print(missing_errors, file=sys.stderr) errors_by_platform[platform] = errors print("*" * 20)