Skip to content

Pyright typecheck typeshed #9298

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ jobs:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
fail-fast: false
env:
PYRIGHT_VERSION: 1.1.278 # Must match pyright_test.py.
PYRIGHT_VERSION: 1.1.278 # Must match pyright_test.py and typecheck_typeshed_code.yml.
steps:
- uses: actions/checkout@v3
- uses: jakebailey/pyright-action@v1
Expand Down
21 changes: 21 additions & 0 deletions .github/workflows/typecheck_typeshed_code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,24 @@ jobs:
cache-dependency-path: requirements-tests.txt
- run: pip install -r requirements-tests.txt
- run: python ./tests/typecheck_typeshed.py --platform=${{ matrix.platform }}
pyright:
name: Run pyright against the scripts and tests directories
runs-on: ubuntu-latest
strategy:
matrix:
python-platform: ["Linux", "Windows"]
fail-fast: false
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.9"
cache: pip
cache-dependency-path: requirements-tests.txt
- run: pip install -r requirements-tests.txt
- uses: jakebailey/pyright-action@v1
with:
version: 1.1.278 # Must match pyright_test.py and typecheck_typeshed_code.yml.
python-platform: ${{ matrix.python-platform }}
python-version: "3.9"
project: ./pyrightconfig.typeshed.json
6 changes: 6 additions & 0 deletions pyrightconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@
"reportInconsistentConstructor": "error",
"reportTypeCommentUsage": "error",
"reportUnnecessaryComparison": "error",
// Conflicts with mypy "# type: ignore". Resolved next version
// https://github.com/microsoft/pyright/issues/4243
"reportUnnecessaryTypeIgnoreComment": "none",
// Stubs are allowed to use private variables
"reportPrivateUsage": "none",
// Stubs don't need the actual modules to be installed
"reportMissingModuleSource": "none",
// Incompatible overrides and property type mismatches are out of typeshed's control
// as they are inherited from the implementation.
Expand Down
8 changes: 7 additions & 1 deletion pyrightconfig.stricter.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,14 @@
"stubs/vobject",
],
"typeCheckingMode": "strict",
"reportPrivateUsage": "none",
// TODO: Complete incomplete stubs
"reportIncompleteStub": "none",
// Conflicts with mypy "# type: ignore". Resolved next version
// https://github.com/microsoft/pyright/issues/4243
"reportUnnecessaryTypeIgnoreComment": "none",
// Stubs are allowed to use private variables
"reportPrivateUsage": "none",
// Stubs don't need the actual modules to be installed
"reportMissingModuleSource": "none",
// Incompatible overrides and property type mismatches are out of typeshed's control
// as they are inherited from the implementation.
Expand Down
9 changes: 7 additions & 2 deletions pyrightconfig.testcases.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@
"test_cases",
],
"typeCheckingMode": "strict",
// Extra strict settings
"reportMissingModuleSource": "error",
"reportShadowedImports": "error",
"reportCallInDefaultInitializer": "error",
"reportImplicitStringConcatenation": "error",
"reportMissingSuperCall": "error",
"reportPropertyTypeMismatch": "error",
"reportUninitializedInstanceVariable": "error",
"reportUnnecessaryTypeIgnoreComment": "error",
"reportMissingModuleSource": "none",
"reportPrivateUsage": "none",
// isinstance checks are still needed when validating inputs outside of typeshed's control
"reportUnnecessaryIsInstance": "none",
// The name of the self/cls parameter is out of typeshed's control.
Expand Down
21 changes: 21 additions & 0 deletions pyrightconfig.typeshed.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://raw.githubusercontent.com/microsoft/pyright/main/packages/vscode-pyright/schemas/pyrightconfig.schema.json",
"typeshedPath": ".",
"include": [
"scripts",
"tests",
],
"typeCheckingMode": "strict",
"useLibraryCodeForTypes": true, // Runtime libraries used by typeshed are not all py.typed
// Extra strict settings
"reportMissingModuleSource": "error",
"reportShadowedImports": "error",
"reportCallInDefaultInitializer": "error",
"reportImplicitStringConcatenation": "error",
"reportMissingSuperCall": "error",
"reportPropertyTypeMismatch": "error",
"reportUninitializedInstanceVariable": "error",
// Conflicts with mypy "# type: ignore". Resolved next version
// https://github.com/microsoft/pyright/issues/4243
"reportUnnecessaryTypeIgnoreComment": "warning",
}
3 changes: 2 additions & 1 deletion scripts/runtests.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
import re
import subprocess
import sys
from collections.abc import Iterable
from pathlib import Path

try:
from termcolor import colored
except ImportError:

def colored(text: str, color: str = "") -> str: # type: ignore[misc]
def colored(text: str, color: str | None = None, on_color: str | None = None, attrs: Iterable[str] | None = None) -> str:
return text


Expand Down
31 changes: 21 additions & 10 deletions scripts/stubsabot.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
import tomlkit
from termcolor import colored

# Checks made outside of scope lead to many possibly unbound variables
# pyright: reportUnboundVariable=false

ActionLevelSelf = TypeVar("ActionLevelSelf", bound="ActionLevel")


Expand Down Expand Up @@ -237,7 +240,7 @@ async def get_github_repo_info(session: aiohttp.ClientSession, pypi_info: PypiIn
github_tags_info_url = f"https://api.github.com/repos/{url_path}/tags"
async with session.get(github_tags_info_url, headers=get_github_api_headers()) as response:
if response.status == 200:
tags = await response.json()
tags: list[dict[str, Any]] = await response.json()
assert isinstance(tags, list)
return GithubInfo(repo_path=url_path, tags=tags)
return None
Expand All @@ -262,7 +265,7 @@ async def get_diff_info(
if github_info is None:
return None

versions_to_tags = {}
versions_to_tags: dict[packaging.version.Version, str] = {}
for tag in github_info.tags:
tag_name = tag["name"]
# Some packages in typeshed (e.g. emoji) have tag names
Expand Down Expand Up @@ -374,7 +377,7 @@ def describe_typeshed_files_modified(self) -> str:
return analysis

def __str__(self) -> str:
data_points = []
data_points: list[str] = []
if self.runtime_definitely_has_consistent_directory_structure_with_typeshed:
data_points += [
self.describe_public_files_added(),
Expand All @@ -394,7 +397,7 @@ async def analyze_diff(
url = f"https://api.github.com/repos/{github_repo_path}/compare/{old_tag}...{new_tag}"
async with session.get(url, headers=get_github_api_headers()) as response:
response.raise_for_status()
json_resp = await response.json()
json_resp: dict[str, list[FileInfo]] = await response.json()
assert isinstance(json_resp, dict)
# https://docs.github.com/en/rest/commits/commits#compare-two-commits
py_files: list[FileInfo] = [file for file in json_resp["files"] if Path(file["filename"]).suffix == ".py"]
Expand Down Expand Up @@ -578,7 +581,11 @@ def get_update_pr_body(update: Update, metadata: dict[str, Any]) -> str:
if update.diff_analysis is not None:
body += f"\n\n{update.diff_analysis}"

stubtest_will_run = not metadata.get("tool", {}).get("stubtest", {}).get("skip", False)
stubtest_will_run = (
not metadata.get("tool", {}).get("stubtest", {})
# Loss of type due to infered dict[Unknown, Unknown]
.get("skip", False) # pyright: ignore[reportUnknownMemberType]
)
if stubtest_will_run:
body += textwrap.dedent(
"""
Expand Down Expand Up @@ -608,10 +615,12 @@ async def suggest_typeshed_update(update: Update, session: aiohttp.ClientSession
branch_name = f"{BRANCH_PREFIX}/{normalize(update.distribution)}"
subprocess.check_call(["git", "checkout", "-B", branch_name, "origin/main"])
with open(update.stub_path / "METADATA.toml", "rb") as f:
meta = tomlkit.load(f)
# tomlkit.load has partially unknown IO type
meta = tomlkit.load(f) # pyright: ignore[reportUnknownMemberType]
meta["version"] = update.new_version_spec
with open(update.stub_path / "METADATA.toml", "w", encoding="UTF-8") as f:
tomlkit.dump(meta, f)
# tomlkit.dump has partially unknown Mapping type
tomlkit.dump(meta, f) # pyright: ignore[reportUnknownMemberType]
body = get_update_pr_body(update, meta)
subprocess.check_call(["git", "commit", "--all", "-m", f"{title}\n\n{body}"])
if action_level <= ActionLevel.local:
Expand All @@ -634,12 +643,14 @@ async def suggest_typeshed_obsolete(obsolete: Obsolete, session: aiohttp.ClientS
branch_name = f"{BRANCH_PREFIX}/{normalize(obsolete.distribution)}"
subprocess.check_call(["git", "checkout", "-B", branch_name, "origin/main"])
with open(obsolete.stub_path / "METADATA.toml", "rb") as f:
meta = tomlkit.load(f)
# tomlkit.load has partially unknown IO type
meta = tomlkit.load(f) # pyright: ignore[reportUnknownMemberType]
obs_string = tomlkit.string(obsolete.obsolete_since_version)
obs_string.comment(f"Released on {obsolete.obsolete_since_date.date().isoformat()}")
meta["obsolete_since"] = obs_string
with open(obsolete.stub_path / "METADATA.toml", "w", encoding="UTF-8") as f:
tomlkit.dump(meta, f)
# tomlkit.dump has partially unknown Mapping type
tomlkit.dump(meta, f) # pyright: ignore[reportUnknownMemberType]
body = "\n".join(f"{k}: {v}" for k, v in obsolete.links.items())
subprocess.check_call(["git", "commit", "--all", "-m", f"{title}\n\n{body}"])
if action_level <= ActionLevel.local:
Expand Down Expand Up @@ -724,7 +735,7 @@ async def main() -> None:
if isinstance(update, Update):
await suggest_typeshed_update(update, session, action_level=args.action_level)
continue
if isinstance(update, Obsolete):
if isinstance(update, Obsolete): # pyright: ignore[reportUnnecessaryIsInstance]
await suggest_typeshed_obsolete(update, session, action_level=args.action_level)
continue
except RemoteConflict as e:
Expand Down
2 changes: 1 addition & 1 deletion test_cases/stdlib/asyncio/check_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@


class Waiter:
def __init__(self) -> None:
def __init__(self) -> None: # pyright: ignore[reportMissingSuperCall] # Is this a false-positive?
self.tasks: list[asyncio.Task[object]] = []

def add(self, t: asyncio.Task[object]) -> None:
Expand Down
2 changes: 1 addition & 1 deletion test_cases/stdlib/builtins/check_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@


class KeysAndGetItem(Generic[_KT, _VT]):
data: dict[_KT, _VT]
data: dict[_KT, _VT] = {}

def keys(self) -> Iterable[_KT]:
return self.data.keys()
Expand Down
15 changes: 9 additions & 6 deletions tests/check_consistent.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import re
import sys
from pathlib import Path
from typing import Any

import tomli
import yaml
Expand Down Expand Up @@ -125,7 +126,7 @@ def check_no_symlinks() -> None:


def check_versions() -> None:
versions = set()
versions: set[str] = set()
with open("stdlib/VERSIONS", encoding="UTF-8") as f:
data = f.read().splitlines()
for line in data:
Expand All @@ -147,7 +148,7 @@ def check_versions() -> None:


def _find_stdlib_modules() -> set[str]:
modules = set()
modules: set[str] = set()
for path, _, files in os.walk("stdlib"):
for filename in files:
base_module = ".".join(os.path.normpath(path).split(os.sep)[1:])
Expand Down Expand Up @@ -186,10 +187,11 @@ def check_metadata() -> None:

assert set(data.get("tool", [])).issubset(tool_keys.keys()), f"Unrecognised tool for {distribution}"
for tool, tk in tool_keys.items():
for key in data.get("tool", {}).get(tool, {}):
keys: dict[str, Any] = data.get("tool", {}).get(tool, {})
for key in keys:
assert key in tk, f"Unrecognised {tool} key {key} for {distribution}"

tool_stubtest = data.get("tool", {}).get("stubtest", {})
tool_stubtest: dict[str, list[str]] = data.get("tool", {}).get("stubtest", {})
specified_stubtest_platforms = set(tool_stubtest.get("platforms", []))
assert (
specified_stubtest_platforms <= supported_stubtest_platforms
Expand All @@ -213,8 +215,9 @@ def get_txt_requirements() -> dict[str, SpecifierSet]:
def get_precommit_requirements() -> dict[str, SpecifierSet]:
with open(".pre-commit-config.yaml", encoding="UTF-8") as precommit_file:
precommit = precommit_file.read()
yam = yaml.load(precommit, Loader=yaml.Loader)
precommit_requirements = {}
# yaml.load has Unknown Loader parameter
yam = yaml.load(precommit, Loader=yaml.Loader) # pyright: ignore[reportUnknownMemberType]
precommit_requirements: dict[str, SpecifierSet] = {}
for repo in yam["repos"]:
hook = repo["hooks"][0]
package_name, package_rev = hook["id"], repo["rev"]
Expand Down
6 changes: 3 additions & 3 deletions tests/check_new_syntax.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@


def check_new_syntax(tree: ast.AST, path: Path, stub: str) -> list[str]:
errors = []
errors: list[str] = []
sourcelines = stub.splitlines()

class AnnotationUnionFinder(ast.NodeVisitor):
Expand Down Expand Up @@ -84,7 +84,7 @@ def visit_If(self, node: ast.If) -> None:
new_syntax = "if " + ast.unparse(node.test).replace("<", ">=", 1)
errors.append(
f"{path}:{node.lineno}: When using if/else with sys.version_info, "
f"put the code for new Python versions first, e.g. `{new_syntax}`"
+ f"put the code for new Python versions first, e.g. `{new_syntax}`"
)
self.generic_visit(node)

Expand All @@ -94,7 +94,7 @@ def visit_If(self, node: ast.If) -> None:


def main() -> None:
errors = []
errors: list[str] = []
for path in chain(Path("stdlib").rglob("*.pyi"), Path("stubs").rglob("*.pyi")):
with open(path, encoding="UTF-8") as f:
stub = f.read()
Expand Down
9 changes: 8 additions & 1 deletion tests/get_packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,12 @@
if platform in METADATA_MAPPING:
for distribution in distributions:
with open(f"stubs/{distribution}/METADATA.toml", "rb") as file:
for package in tomli.load(file).get("tool", {}).get("stubtest", {}).get(METADATA_MAPPING[platform], []):
packages: list[str] = (
tomli.load(file)
.get("tool", {})
.get("stubtest", {})
# Loss of type due to infered dict[Unknown, Unknown]
.get(METADATA_MAPPING[platform], []) # pyright: ignore[reportUnknownMemberType]
)
for package in packages:
print(package)
16 changes: 8 additions & 8 deletions tests/mypy_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@


class CommandLineArgs(argparse.Namespace):
verbose: int
filter: list[Path]
exclude: list[Path] | None
python_version: list[VersionString] | None
platform: list[Platform] | None
verbose: int = 0
filter: list[Path] = []
exclude: list[Path] | None = None
python_version: list[VersionString] | None = None
platform: list[Platform] | None = None


def valid_path(cmd_arg: str) -> Path:
Expand Down Expand Up @@ -134,7 +134,7 @@ def match(path: Path, args: TestConfig) -> bool:


def parse_versions(fname: StrPath) -> dict[str, tuple[VersionTuple, VersionTuple]]:
result = {}
result: dict[str, tuple[tuple[int, int], tuple[int, int]]] = {}
with open(fname, encoding="UTF-8") as f:
for line in f:
line = strip_comments(line)
Expand Down Expand Up @@ -185,7 +185,7 @@ def add_configuration(configurations: list[MypyDistConf], distribution: str) ->
with Path("stubs", distribution, "METADATA.toml").open("rb") as f:
data = tomli.load(f)

mypy_tests_conf = data.get("mypy-tests")
mypy_tests_conf: dict[str, dict[str, Any]] | None = data.get("mypy-tests")
if not mypy_tests_conf:
return

Expand All @@ -197,7 +197,7 @@ def add_configuration(configurations: list[MypyDistConf], distribution: str) ->
assert module_name is not None, f"{section_name} should have a module_name key"
assert isinstance(module_name, str), f"{section_name} should be a key-value pair"

values = mypy_section.get("values")
values: dict[str, dict[str, Any]] | None = mypy_section.get("values")
assert values is not None, f"{section_name} should have a values section"
assert isinstance(values, dict), "values should be a section"

Expand Down
2 changes: 1 addition & 1 deletion tests/pyright_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import sys
from pathlib import Path

_PYRIGHT_VERSION = "1.1.278" # Must match .github/workflows/tests.yml.
_PYRIGHT_VERSION = "1.1.278" # Must match .github/workflows/tests.yml and typecheck_typeshed_code.yml.
_WELL_KNOWN_FILE = Path("tests", "pyright_test.py")


Expand Down
Loading