Skip to content

Commit d12330d

Browse files
Avasamabravalheri
andauthored
Initial pyright config (#4192)
* Bump importlib_metadata in type tests * New pyright specific workflow * Add missing spaces in comparison * Fix requirements * Typo and fix cygwin * Update .github/workflows/pyright.yml * get_ext_filename doesn't need to be modified for this PR --------- Co-authored-by: Anderson Bravalheri <[email protected]> Co-authored-by: Anderson Bravalheri <[email protected]>
1 parent 59ec6f9 commit d12330d

18 files changed

+183
-35
lines changed

.github/workflows/pyright.yml

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Split workflow file to not interfere with skeleton
2+
name: pyright
3+
4+
on:
5+
merge_group:
6+
push:
7+
branches-ignore:
8+
# temporary GH branches relating to merge queues (jaraco/skeleton#93)
9+
- gh-readonly-queue/**
10+
tags:
11+
# required if branches-ignore is supplied (jaraco/skeleton#103)
12+
- '**'
13+
pull_request:
14+
workflow_dispatch:
15+
16+
concurrency:
17+
group: >-
18+
${{ github.workflow }}-
19+
${{ github.ref_type }}-
20+
${{ github.event.pull_request.number || github.sha }}
21+
cancel-in-progress: true
22+
23+
env:
24+
# pin pyright version so a new version doesn't suddenly cause the CI to fail,
25+
# until types-setuptools is removed from typeshed.
26+
# For help with static-typing issues, or pyright update, ping @Avasam
27+
PYRIGHT_VERSION: "1.1.377"
28+
29+
# Environment variable to support color support (jaraco/skeleton#66)
30+
FORCE_COLOR: 1
31+
32+
# Suppress noisy pip warnings
33+
PIP_DISABLE_PIP_VERSION_CHECK: 'true'
34+
PIP_NO_PYTHON_VERSION_WARNING: 'true'
35+
PIP_NO_WARN_SCRIPT_LOCATION: 'true'
36+
37+
jobs:
38+
pyright:
39+
strategy:
40+
# https://blog.jaraco.com/efficient-use-of-ci-resources/
41+
matrix:
42+
python:
43+
- "3.8"
44+
- "3.12"
45+
platform:
46+
- ubuntu-latest
47+
runs-on: ${{ matrix.platform }}
48+
timeout-minutes: 10
49+
steps:
50+
- uses: actions/checkout@v4
51+
- name: Setup Python
52+
uses: actions/setup-python@v5
53+
with:
54+
python-version: ${{ matrix.python }}
55+
allow-prereleases: true
56+
- name: Install typed dependencies
57+
run: python -m pip install -e .[core,type]
58+
- name: Inform how to run locally
59+
run: |
60+
echo 'To run this test locally with npm pre-installed, run:'
61+
echo '> npx -y pyright@${{ env.PYRIGHT_VERSION }} --threads'
62+
echo 'You can also instead install "Pyright for Python" which will install npm for you:'
63+
if [ '$PYRIGHT_VERSION' == 'latest' ]; then
64+
echo '> pip install -U'
65+
else
66+
echo '> pip install pyright==${{ env.PYRIGHT_VERSION }}'
67+
fi
68+
echo 'pyright --threads'
69+
shell: bash
70+
- name: Run pyright
71+
uses: jakebailey/pyright-action@v2
72+
with:
73+
version: ${{ env.PYRIGHT_VERSION }}
74+
extra-args: --threads

pkg_resources/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,24 +561,30 @@ def get_entry_info(dist: _EPDistType, group: str, name: str) -> EntryPoint | Non
561561
class IMetadataProvider(Protocol):
562562
def has_metadata(self, name: str) -> bool:
563563
"""Does the package's distribution contain the named metadata?"""
564+
...
564565

565566
def get_metadata(self, name: str) -> str:
566567
"""The named metadata resource as a string"""
568+
...
567569

568570
def get_metadata_lines(self, name: str) -> Iterator[str]:
569571
"""Yield named metadata resource as list of non-blank non-comment lines
570572
571573
Leading and trailing whitespace is stripped from each line, and lines
572574
with ``#`` as the first non-blank character are omitted."""
575+
...
573576

574577
def metadata_isdir(self, name: str) -> bool:
575578
"""Is the named metadata a directory? (like ``os.path.isdir()``)"""
579+
...
576580

577581
def metadata_listdir(self, name: str) -> list[str]:
578582
"""List of metadata names in the directory (like ``os.listdir()``)"""
583+
...
579584

580585
def run_script(self, script_name: str, namespace: dict[str, Any]) -> None:
581586
"""Execute the named script in the supplied namespace dictionary"""
587+
...
582588

583589

584590
class IResourceProvider(IMetadataProvider, Protocol):
@@ -590,29 +596,35 @@ def get_resource_filename(
590596
"""Return a true filesystem path for `resource_name`
591597
592598
`manager` must be a ``ResourceManager``"""
599+
...
593600

594601
def get_resource_stream(
595602
self, manager: ResourceManager, resource_name: str
596603
) -> _ResourceStream:
597604
"""Return a readable file-like object for `resource_name`
598605
599606
`manager` must be a ``ResourceManager``"""
607+
...
600608

601609
def get_resource_string(
602610
self, manager: ResourceManager, resource_name: str
603611
) -> bytes:
604612
"""Return the contents of `resource_name` as :obj:`bytes`
605613
606614
`manager` must be a ``ResourceManager``"""
615+
...
607616

608617
def has_resource(self, resource_name: str) -> bool:
609618
"""Does the package contain the named resource?"""
619+
...
610620

611621
def resource_isdir(self, resource_name: str) -> bool:
612622
"""Is the named resource a directory? (like ``os.path.isdir()``)"""
623+
...
613624

614625
def resource_listdir(self, resource_name: str) -> list[str]:
615626
"""List of resource names in the directory (like ``os.listdir()``)"""
627+
...
616628

617629

618630
class WorkingSet:

pkg_resources/tests/test_pkg_resources.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def teardown_class(cls):
7070
finalizer()
7171

7272
def test_resource_listdir(self):
73-
import mod
73+
import mod # pyright: ignore[reportMissingImports] # Temporary package for test
7474

7575
zp = pkg_resources.ZipProvider(mod)
7676

@@ -84,7 +84,7 @@ def test_resource_listdir(self):
8484
assert zp.resource_listdir('nonexistent') == []
8585
assert zp.resource_listdir('nonexistent/') == []
8686

87-
import mod2
87+
import mod2 # pyright: ignore[reportMissingImports] # Temporary package for test
8888

8989
zp2 = pkg_resources.ZipProvider(mod2)
9090

@@ -100,7 +100,7 @@ def test_resource_filename_rewrites_on_change(self):
100100
same size and modification time, it should not be overwritten on a
101101
subsequent call to get_resource_filename.
102102
"""
103-
import mod
103+
import mod # pyright: ignore[reportMissingImports] # Temporary package for test
104104

105105
manager = pkg_resources.ResourceManager()
106106
zp = pkg_resources.ZipProvider(mod)

pkg_resources/tests/test_resources.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -817,11 +817,11 @@ def test_two_levels_deep(self, symlinked_tmpdir):
817817
(pkg1 / '__init__.py').write_text(self.ns_str, encoding='utf-8')
818818
(pkg2 / '__init__.py').write_text(self.ns_str, encoding='utf-8')
819819
with pytest.warns(DeprecationWarning, match="pkg_resources.declare_namespace"):
820-
import pkg1
820+
import pkg1 # pyright: ignore[reportMissingImports] # Temporary package for test
821821
assert "pkg1" in pkg_resources._namespace_packages
822822
# attempt to import pkg2 from site-pkgs2
823823
with pytest.warns(DeprecationWarning, match="pkg_resources.declare_namespace"):
824-
import pkg1.pkg2
824+
import pkg1.pkg2 # pyright: ignore[reportMissingImports] # Temporary package for test
825825
# check the _namespace_packages dict
826826
assert "pkg1.pkg2" in pkg_resources._namespace_packages
827827
assert pkg_resources._namespace_packages["pkg1"] == ["pkg1.pkg2"]
@@ -862,8 +862,8 @@ def test_path_order(self, symlinked_tmpdir):
862862
(subpkg / '__init__.py').write_text(vers_str % number, encoding='utf-8')
863863

864864
with pytest.warns(DeprecationWarning, match="pkg_resources.declare_namespace"):
865-
import nspkg
866-
import nspkg.subpkg
865+
import nspkg # pyright: ignore[reportMissingImports] # Temporary package for test
866+
import nspkg.subpkg # pyright: ignore[reportMissingImports] # Temporary package for test
867867
expected = [str(site.realpath() / 'nspkg') for site in site_dirs]
868868
assert nspkg.__path__ == expected
869869
assert nspkg.subpkg.__version__ == 1

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ type = [
131131
# until types-setuptools is removed from typeshed.
132132
# For help with static-typing issues, or mypy update, ping @Avasam
133133
"mypy==1.11.*",
134+
# Typing fixes in version newer than we require at runtime
135+
"importlib_metadata>=7.0.2; python_version < '3.10'",
136+
# Imported unconditionally in tools/finalize.py
137+
'jaraco.develop >= 7.21; sys_platform != "cygwin"',
134138
]
135139

136140

pyrightconfig.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"$schema": "https://raw.githubusercontent.com/microsoft/pyright/main/packages/vscode-pyright/schemas/pyrightconfig.schema.json",
3+
"exclude": [
4+
"build",
5+
".tox",
6+
".eggs",
7+
"**/_vendor", // Vendored
8+
"setuptools/_distutils", // Vendored
9+
"setuptools/config/_validate_pyproject/**", // Auto-generated
10+
],
11+
// Our testing setup doesn't allow passing CLI arguments, so local devs have to set this manually.
12+
// "pythonVersion": "3.8",
13+
// For now we don't mind if mypy's `type: ignore` comments accidentally suppresses pyright issues
14+
"enableTypeIgnoreComments": true,
15+
"typeCheckingMode": "basic",
16+
// Too many issues caused by dynamic patching, still worth fixing when we can
17+
"reportAttributeAccessIssue": "warning",
18+
// Fails on Python 3.12 due to missing distutils and on cygwin CI tests
19+
"reportAssignmentType": "warning",
20+
"reportMissingImports": "warning",
21+
"reportOptionalCall": "warning",
22+
// FIXME: A handful of reportOperatorIssue spread throughout the codebase
23+
"reportOperatorIssue": "warning",
24+
// Deferred initialization (initialize_options/finalize_options) causes many "potentially None" issues
25+
// TODO: Fix with type-guards or by changing how it's initialized
26+
"reportArgumentType": "warning", // A lot of these are caused by jaraco.path.build's spec argument not being a Mapping https://github.com/jaraco/jaraco.path/pull/3
27+
"reportCallIssue": "warning",
28+
"reportGeneralTypeIssues": "warning",
29+
"reportOptionalIterable": "warning",
30+
"reportOptionalMemberAccess": "warning",
31+
"reportOptionalOperand": "warning",
32+
}

setuptools/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import re
1313
import sys
1414
from abc import abstractmethod
15+
from collections.abc import Mapping
1516
from typing import TYPE_CHECKING, TypeVar, overload
1617

1718
sys.path.extend(((vendor_path := os.path.join(os.path.dirname(os.path.dirname(__file__)), 'setuptools', '_vendor')) not in sys.path) * [vendor_path]) # fmt: skip
@@ -59,7 +60,7 @@ class MinimalDistribution(distutils.core.Distribution):
5960
fetch_build_eggs interface.
6061
"""
6162

62-
def __init__(self, attrs):
63+
def __init__(self, attrs: Mapping[str, object]):
6364
_incl = 'dependency_links', 'setup_requires'
6465
filtered = {k: attrs[k] for k in set(_incl) & set(attrs)}
6566
super().__init__(filtered)

setuptools/_reqs.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,13 @@ def parse_strings(strs: _StrOrIter) -> Iterator[str]:
2828
return text.join_continuation(map(text.drop_comment, text.yield_lines(strs)))
2929

3030

31+
# These overloads are only needed because of a mypy false-positive, pyright gets it right
32+
# https://github.com/python/mypy/issues/3737
3133
@overload
3234
def parse(strs: _StrOrIter) -> Iterator[Requirement]: ...
33-
34-
3535
@overload
3636
def parse(strs: _StrOrIter, parser: Callable[[str], _T]) -> Iterator[_T]: ...
37-
38-
39-
def parse(strs, parser=parse_req):
37+
def parse(strs: _StrOrIter, parser: Callable[[str], _T] = parse_req) -> Iterator[_T]: # type: ignore[assignment]
4038
"""
4139
Replacement for ``pkg_resources.parse_requirements`` that uses ``packaging``.
4240
"""

setuptools/command/build.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,15 @@ def finalize_options(self):
8787

8888
def initialize_options(self):
8989
"""(Required by the original :class:`setuptools.Command` interface)"""
90+
...
9091

9192
def finalize_options(self):
9293
"""(Required by the original :class:`setuptools.Command` interface)"""
94+
...
9395

9496
def run(self):
9597
"""(Required by the original :class:`setuptools.Command` interface)"""
98+
...
9699

97100
def get_source_files(self) -> list[str]:
98101
"""
@@ -104,6 +107,7 @@ def get_source_files(self) -> list[str]:
104107
with all the files necessary to build the distribution.
105108
All files should be strings relative to the project root directory.
106109
"""
110+
...
107111

108112
def get_outputs(self) -> list[str]:
109113
"""
@@ -117,6 +121,7 @@ def get_outputs(self) -> list[str]:
117121
in ``get_output_mapping()`` plus files that are generated during the build
118122
and don't correspond to any source file already present in the project.
119123
"""
124+
...
120125

121126
def get_output_mapping(self) -> dict[str, str]:
122127
"""
@@ -127,3 +132,4 @@ def get_output_mapping(self) -> dict[str, str]:
127132
Destination files should be strings in the form of
128133
``"{build_lib}/destination/file/path"``.
129134
"""
135+
...

setuptools/command/build_clib.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
from distutils.errors import DistutilsSetupError
66

77
try:
8-
from distutils._modified import newer_pairwise_group
8+
from distutils._modified import ( # pyright: ignore[reportMissingImports]
9+
newer_pairwise_group,
10+
)
911
except ImportError:
1012
# fallback for SETUPTOOLS_USE_DISTUTILS=stdlib
1113
from .._distutils._modified import newer_pairwise_group

setuptools/command/easy_install.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1792,7 +1792,7 @@ def auto_chmod(func, arg, exc):
17921792
return func(arg)
17931793
et, ev, _ = sys.exc_info()
17941794
# TODO: This code doesn't make sense. What is it trying to do?
1795-
raise (ev[0], ev[1] + (" %s %s" % (func, arg)))
1795+
raise (ev[0], ev[1] + (" %s %s" % (func, arg))) # pyright: ignore[reportOptionalSubscript, reportIndexIssue]
17961796

17971797

17981798
def update_dist_caches(dist_path, fix_zipimporter_caches):
@@ -2018,7 +2018,9 @@ def is_python_script(script_text, filename):
20182018

20192019

20202020
try:
2021-
from os import chmod as _chmod
2021+
from os import (
2022+
chmod as _chmod, # pyright: ignore[reportAssignmentType] # Loosing type-safety w/ pyright, but that's ok
2023+
)
20222024
except ImportError:
20232025
# Jython compatibility
20242026
def _chmod(*args: object, **kwargs: object) -> None: # type: ignore[misc] # Mypy re-uses the imported definition anyway

setuptools/command/editable_wheel.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
from .install_scripts import install_scripts as install_scripts_cls
4040

4141
if TYPE_CHECKING:
42+
from typing_extensions import Self
43+
4244
from .._vendor.wheel.wheelfile import WheelFile
4345

4446
_P = TypeVar("_P", bound=StrPath)
@@ -379,7 +381,7 @@ def _select_strategy(
379381
class EditableStrategy(Protocol):
380382
def __call__(self, wheel: WheelFile, files: list[str], mapping: dict[str, str]): ...
381383

382-
def __enter__(self): ...
384+
def __enter__(self) -> Self: ...
383385

384386
def __exit__(self, _exc_type, _exc_value, _traceback): ...
385387

setuptools/config/pyprojecttoml.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,10 @@ def _obtain(self, dist: Distribution, field: str, package_dir: Mapping[str, str]
303303
def _obtain_version(self, dist: Distribution, package_dir: Mapping[str, str]):
304304
# Since plugins can set version, let's silently skip if it cannot be obtained
305305
if "version" in self.dynamic and "version" in self.dynamic_cfg:
306-
return _expand.version(self._obtain(dist, "version", package_dir))
306+
return _expand.version(
307+
# We already do an early check for the presence of "version"
308+
self._obtain(dist, "version", package_dir) # pyright: ignore[reportArgumentType]
309+
)
307310
return None
308311

309312
def _obtain_readme(self, dist: Distribution) -> dict[str, str] | None:
@@ -313,9 +316,10 @@ def _obtain_readme(self, dist: Distribution) -> dict[str, str] | None:
313316
dynamic_cfg = self.dynamic_cfg
314317
if "readme" in dynamic_cfg:
315318
return {
319+
# We already do an early check for the presence of "readme"
316320
"text": self._obtain(dist, "readme", {}),
317321
"content-type": dynamic_cfg["readme"].get("content-type", "text/x-rst"),
318-
}
322+
} # pyright: ignore[reportReturnType]
319323

320324
self._ensure_previously_set(dist, "readme")
321325
return None

0 commit comments

Comments
 (0)