Skip to content

Commit 2d0f6d8

Browse files
authored
Bundle path handling in ts_utils.paths (#12805)
1 parent 36fb63e commit 2d0f6d8

File tree

10 files changed

+81
-80
lines changed

10 files changed

+81
-80
lines changed

lib/ts_utils/metadata.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
from __future__ import annotations
77

8-
import os
98
import re
109
import urllib.parse
1110
from collections.abc import Mapping
@@ -18,6 +17,7 @@
1817
from packaging.requirements import Requirement
1918
from packaging.specifiers import Specifier
2019

20+
from .paths import PYPROJECT_PATH, STUBS_PATH, distribution_path
2121
from .utils import cache
2222

2323
__all__ = [
@@ -43,20 +43,15 @@ def _is_list_of_strings(obj: object) -> TypeGuard[list[str]]:
4343

4444
@cache
4545
def _get_oldest_supported_python() -> str:
46-
with open("pyproject.toml", "rb") as config:
46+
with PYPROJECT_PATH.open("rb") as config:
4747
val = tomli.load(config)["tool"]["typeshed"]["oldest_supported_python"]
4848
assert type(val) is str
4949
return val
5050

5151

52-
def stubs_path(distribution: str) -> Path:
53-
"""Return the path to the directory of a third-party distribution."""
54-
return Path("stubs", distribution)
55-
56-
5752
def metadata_path(distribution: str) -> Path:
5853
"""Return the path to the METADATA.toml file of a third-party distribution."""
59-
return stubs_path(distribution) / "METADATA.toml"
54+
return distribution_path(distribution) / "METADATA.toml"
6055

6156

6257
@final
@@ -317,7 +312,7 @@ class PackageDependencies(NamedTuple):
317312

318313
@cache
319314
def get_pypi_name_to_typeshed_name_mapping() -> Mapping[str, str]:
320-
return {read_metadata(typeshed_name).stub_distribution: typeshed_name for typeshed_name in os.listdir("stubs")}
315+
return {read_metadata(dir.name).stub_distribution: dir.name for dir in STUBS_PATH.iterdir()}
321316

322317

323318
@cache

lib/ts_utils/paths.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from pathlib import Path
2+
from typing import Final
3+
4+
# TODO: Use base path relative to this file. Currently, ts_utils gets
5+
# installed into the user's virtual env, so we can't determine the path
6+
# to typeshed. Installing ts_utils editable would solve that, see
7+
# https://github.com/python/typeshed/pull/12806.
8+
TS_BASE_PATH: Final = Path("")
9+
STDLIB_PATH: Final = TS_BASE_PATH / "stdlib"
10+
STUBS_PATH: Final = TS_BASE_PATH / "stubs"
11+
12+
PYPROJECT_PATH: Final = TS_BASE_PATH / "pyproject.toml"
13+
REQUIREMENTS_PATH: Final = TS_BASE_PATH / "requirements-tests.txt"
14+
15+
TESTS_DIR: Final = "@tests"
16+
TEST_CASES_DIR: Final = "test_cases"
17+
18+
19+
def distribution_path(distribution_name: str) -> Path:
20+
"""Return the path to the directory of a third-party distribution."""
21+
return STUBS_PATH / distribution_name
22+
23+
24+
def tests_path(distribution_name: str) -> Path:
25+
if distribution_name == "stdlib":
26+
return STDLIB_PATH / TESTS_DIR
27+
else:
28+
return STUBS_PATH / distribution_name / TESTS_DIR
29+
30+
31+
def test_cases_path(distribution_name: str) -> Path:
32+
return tests_path(distribution_name) / TEST_CASES_DIR
33+
34+
35+
def allowlists_path(distribution_name: str) -> Path:
36+
if distribution_name == "stdlib":
37+
return tests_path("stdlib") / "stubtest_allowlists"
38+
else:
39+
return tests_path(distribution_name)

lib/ts_utils/utils.py

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,9 @@ def colored(text: str, color: str | None = None, **kwargs: Any) -> str: # type:
2121
return text
2222

2323

24-
PYTHON_VERSION: Final = f"{sys.version_info.major}.{sys.version_info.minor}"
24+
from .paths import REQUIREMENTS_PATH, STDLIB_PATH, STUBS_PATH, TEST_CASES_DIR, allowlists_path, test_cases_path
2525

26-
STDLIB_PATH = Path("stdlib")
27-
STUBS_PATH = Path("stubs")
26+
PYTHON_VERSION: Final = f"{sys.version_info.major}.{sys.version_info.minor}"
2827

2928

3029
# A backport of functools.cache for Python <3.9
@@ -90,16 +89,14 @@ def venv_python(venv_dir: Path) -> Path:
9089
# ====================================================================
9190

9291

93-
REQS_FILE: Final = "requirements-tests.txt"
94-
95-
9692
@cache
9793
def parse_requirements() -> Mapping[str, Requirement]:
9894
"""Return a dictionary of requirements from the requirements file."""
9995

100-
with open(REQS_FILE, encoding="UTF-8") as requirements_file:
96+
with REQUIREMENTS_PATH.open(encoding="UTF-8") as requirements_file:
10197
stripped_lines = map(strip_comments, requirements_file)
102-
requirements = map(Requirement, filter(None, stripped_lines))
98+
stripped_more = [li for li in stripped_lines if not li.startswith("-")]
99+
requirements = map(Requirement, filter(None, stripped_more))
103100
return {requirement.name: requirement for requirement in requirements}
104101

105102

@@ -155,10 +152,6 @@ def _parse_version(v_str: str) -> tuple[int, int]:
155152
# ====================================================================
156153

157154

158-
TESTS_DIR: Final = "@tests"
159-
TEST_CASES_DIR: Final = "test_cases"
160-
161-
162155
class DistributionTests(NamedTuple):
163156
name: str
164157
test_cases_path: Path
@@ -179,17 +172,6 @@ def distribution_info(distribution_name: str) -> DistributionTests:
179172
raise RuntimeError(f"No test cases found for {distribution_name!r}!")
180173

181174

182-
def tests_path(distribution_name: str) -> Path:
183-
if distribution_name == "stdlib":
184-
return STDLIB_PATH / TESTS_DIR
185-
else:
186-
return STUBS_PATH / distribution_name / TESTS_DIR
187-
188-
189-
def test_cases_path(distribution_name: str) -> Path:
190-
return tests_path(distribution_name) / TEST_CASES_DIR
191-
192-
193175
def get_all_testcase_directories() -> list[DistributionTests]:
194176
testcase_directories: list[DistributionTests] = []
195177
for distribution_path in STUBS_PATH.iterdir():
@@ -201,13 +183,6 @@ def get_all_testcase_directories() -> list[DistributionTests]:
201183
return [distribution_info("stdlib"), *sorted(testcase_directories)]
202184

203185

204-
def allowlists_path(distribution_name: str) -> Path:
205-
if distribution_name == "stdlib":
206-
return tests_path("stdlib") / "stubtest_allowlists"
207-
else:
208-
return tests_path(distribution_name)
209-
210-
211186
def allowlists(distribution_name: str) -> list[str]:
212187
prefix = "" if distribution_name == "stdlib" else "stubtest_allowlist_"
213188
version_id = f"py{sys.version_info.major}{sys.version_info.minor}"

scripts/stubsabot.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
from packaging.specifiers import Specifier
3030
from termcolor import colored
3131

32-
from ts_utils.metadata import StubMetadata, metadata_path, read_metadata, stubs_path
32+
from ts_utils.metadata import StubMetadata, metadata_path, read_metadata
33+
from ts_utils.paths import STUBS_PATH, distribution_path
3334

3435
TYPESHED_OWNER = "python"
3536
TYPESHED_API_URL = f"https://api.github.com/repos/{TYPESHED_OWNER}/typeshed"
@@ -463,7 +464,7 @@ async def analyze_diff(
463464
assert isinstance(json_resp, dict)
464465
# https://docs.github.com/en/rest/commits/commits#compare-two-commits
465466
py_files: list[FileInfo] = [file for file in json_resp["files"] if Path(file["filename"]).suffix == ".py"]
466-
stub_path = stubs_path(distribution)
467+
stub_path = distribution_path(distribution)
467468
files_in_typeshed = set(stub_path.rglob("*.pyi"))
468469
py_files_stubbed_in_typeshed = [file for file in py_files if (stub_path / f"{file['filename']}i") in files_in_typeshed]
469470
return DiffAnalysis(py_files=py_files, py_files_stubbed_in_typeshed=py_files_stubbed_in_typeshed)
@@ -759,7 +760,7 @@ async def main() -> None:
759760
if args.distributions:
760761
dists_to_update = args.distributions
761762
else:
762-
dists_to_update = [path.name for path in Path("stubs").iterdir()]
763+
dists_to_update = [path.name for path in STUBS_PATH.iterdir()]
763764

764765
if args.action_level > ActionLevel.nothing:
765766
subprocess.run(["git", "update-index", "--refresh"], capture_output=True)

tests/check_typeshed_structure.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,13 @@
1313
from pathlib import Path
1414

1515
from ts_utils.metadata import read_metadata
16+
from ts_utils.paths import REQUIREMENTS_PATH, STDLIB_PATH, STUBS_PATH, TEST_CASES_DIR, TESTS_DIR, tests_path
1617
from ts_utils.utils import (
17-
REQS_FILE,
18-
STDLIB_PATH,
19-
TEST_CASES_DIR,
20-
TESTS_DIR,
2118
get_all_testcase_directories,
2219
get_gitignore_spec,
2320
parse_requirements,
2421
parse_stdlib_versions_file,
2522
spec_matches_path,
26-
tests_path,
2723
)
2824

2925
extension_descriptions = {".pyi": "stub", ".py": ".py"}
@@ -66,7 +62,7 @@ def check_stdlib() -> None:
6662
def check_stubs() -> None:
6763
"""Check that the stubs directory contains only the correct files."""
6864
gitignore_spec = get_gitignore_spec()
69-
for dist in Path("stubs").iterdir():
65+
for dist in STUBS_PATH.iterdir():
7066
if spec_matches_path(gitignore_spec, dist):
7167
continue
7268
assert dist.is_dir(), f"Only directories allowed in stubs, got {dist}"
@@ -97,8 +93,8 @@ def check_distutils() -> None:
9793
def all_relative_paths_in_directory(path: Path) -> set[Path]:
9894
return {pyi.relative_to(path) for pyi in path.rglob("*.pyi")}
9995

100-
all_setuptools_files = all_relative_paths_in_directory(Path("stubs", "setuptools", "setuptools", "_distutils"))
101-
all_distutils_files = all_relative_paths_in_directory(Path("stubs", "setuptools", "distutils"))
96+
all_setuptools_files = all_relative_paths_in_directory(STUBS_PATH / "setuptools" / "setuptools" / "_distutils")
97+
all_distutils_files = all_relative_paths_in_directory(STUBS_PATH / "setuptools" / "distutils")
10298
assert all_setuptools_files and all_distutils_files, "Looks like this test might be out of date!"
10399
extra_files = all_setuptools_files - all_distutils_files
104100
joined = "\n".join(f" * {f}" for f in extra_files)
@@ -164,10 +160,10 @@ def check_requirement_pins() -> None:
164160
"""Check that type checkers and linters are pinned to an exact version."""
165161
requirements = parse_requirements()
166162
for package in linters:
167-
assert package in requirements, f"type checker/linter '{package}' not found in {REQS_FILE}"
163+
assert package in requirements, f"type checker/linter '{package}' not found in {REQUIREMENTS_PATH.name}"
168164
spec = requirements[package].specifier
169-
assert len(spec) == 1, f"type checker/linter '{package}' has complex specifier in {REQS_FILE}"
170-
msg = f"type checker/linter '{package}' is not pinned to an exact version in {REQS_FILE}"
165+
assert len(spec) == 1, f"type checker/linter '{package}' has complex specifier in {REQUIREMENTS_PATH.name}"
166+
msg = f"type checker/linter '{package}' is not pinned to an exact version in {REQUIREMENTS_PATH.name}"
171167
assert str(spec).startswith("=="), msg
172168

173169

tests/mypy_test.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,10 @@
2222
import tomli
2323
from packaging.requirements import Requirement
2424

25-
from ts_utils.metadata import PackageDependencies, get_recursive_requirements, read_metadata
25+
from ts_utils.metadata import PackageDependencies, get_recursive_requirements, metadata_path, read_metadata
26+
from ts_utils.paths import STDLIB_PATH, STUBS_PATH, TESTS_DIR, TS_BASE_PATH, distribution_path
2627
from ts_utils.utils import (
2728
PYTHON_VERSION,
28-
STDLIB_PATH,
29-
TESTS_DIR,
3029
colored,
3130
get_gitignore_spec,
3231
get_mypy_req,
@@ -47,7 +46,7 @@
4746

4847
SUPPORTED_VERSIONS = ["3.13", "3.12", "3.11", "3.10", "3.9", "3.8"]
4948
SUPPORTED_PLATFORMS = ("linux", "win32", "darwin")
50-
DIRECTORIES_TO_TEST = [Path("stdlib"), Path("stubs")]
49+
DIRECTORIES_TO_TEST = [STDLIB_PATH, STUBS_PATH]
5150

5251
VersionString: TypeAlias = Annotated[str, "Must be one of the entries in SUPPORTED_VERSIONS"]
5352
Platform: TypeAlias = Annotated[str, "Must be one of the entries in SUPPORTED_PLATFORMS"]
@@ -170,7 +169,7 @@ class MypyDistConf(NamedTuple):
170169

171170

172171
def add_configuration(configurations: list[MypyDistConf], distribution: str) -> None:
173-
with Path("stubs", distribution, "METADATA.toml").open("rb") as f:
172+
with metadata_path(distribution).open("rb") as f:
174173
data = tomli.load(f)
175174

176175
# TODO: This could be added to ts_utils.metadata, but is currently unused
@@ -229,7 +228,7 @@ def run_mypy(
229228
"--platform",
230229
args.platform,
231230
"--custom-typeshed-dir",
232-
str(Path(__file__).parent.parent),
231+
str(TS_BASE_PATH),
233232
"--strict",
234233
# Stub completion is checked by pyright (--allow-*-defs)
235234
"--allow-untyped-defs",
@@ -283,7 +282,7 @@ def add_third_party_files(
283282
return
284283
seen_dists.add(distribution)
285284
seen_dists.update(r.name for r in typeshed_reqs)
286-
root = Path("stubs", distribution)
285+
root = distribution_path(distribution)
287286
for name in os.listdir(root):
288287
if name.startswith("."):
289288
continue
@@ -319,7 +318,7 @@ def test_third_party_distribution(
319318
print_error("no files found")
320319
sys.exit(1)
321320

322-
mypypath = os.pathsep.join(str(Path("stubs", dist)) for dist in seen_dists)
321+
mypypath = os.pathsep.join(str(distribution_path(dist)) for dist in seen_dists)
323322
if args.verbose:
324323
print(colored(f"\nMYPYPATH={mypypath}", "blue"))
325324
result = run_mypy(
@@ -516,16 +515,12 @@ def test_third_party_stubs(args: TestConfig, tempdir: Path) -> TestSummary:
516515
distributions_to_check: dict[str, PackageDependencies] = {}
517516

518517
for distribution in sorted(os.listdir("stubs")):
519-
distribution_path = Path("stubs", distribution)
518+
dist_path = distribution_path(distribution)
520519

521-
if spec_matches_path(gitignore_spec, distribution_path):
520+
if spec_matches_path(gitignore_spec, dist_path):
522521
continue
523522

524-
if (
525-
distribution_path in args.filter
526-
or Path("stubs") in args.filter
527-
or any(distribution_path in path.parents for path in args.filter)
528-
):
523+
if dist_path in args.filter or STUBS_PATH in args.filter or any(dist_path in path.parents for path in args.filter):
529524
metadata = read_metadata(distribution)
530525
if not metadata.requires_python.contains(PYTHON_VERSION):
531526
msg = (

tests/regr_test.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
from typing_extensions import TypeAlias
2323

2424
from ts_utils.metadata import get_recursive_requirements, read_metadata
25+
from ts_utils.paths import STDLIB_PATH, TEST_CASES_DIR, TS_BASE_PATH, distribution_path
2526
from ts_utils.utils import (
2627
PYTHON_VERSION,
27-
TEST_CASES_DIR,
2828
DistributionTests,
2929
colored,
3030
distribution_info,
@@ -134,14 +134,14 @@ def setup_testcase_dir(package: DistributionTests, tempdir: Path, verbosity: Ver
134134
# that has only the required stubs copied over.
135135
new_typeshed = tempdir / TYPESHED
136136
new_typeshed.mkdir()
137-
shutil.copytree(Path("stdlib"), new_typeshed / "stdlib")
137+
shutil.copytree(STDLIB_PATH, new_typeshed / "stdlib")
138138
requirements = get_recursive_requirements(package.name)
139139
# mypy refuses to consider a directory a "valid typeshed directory"
140140
# unless there's a stubs/mypy-extensions path inside it,
141141
# so add that to the list of stubs to copy over to the new directory
142142
typeshed_requirements = [r.name for r in requirements.typeshed_pkgs]
143143
for requirement in {package.name, *typeshed_requirements, "mypy-extensions"}:
144-
shutil.copytree(Path("stubs", requirement), new_typeshed / "stubs" / requirement)
144+
shutil.copytree(distribution_path(requirement), new_typeshed / "stubs" / requirement)
145145

146146
if requirements.external_pkgs:
147147
venv_location = str(tempdir / VENV_DIR)
@@ -190,7 +190,7 @@ def run_testcases(
190190

191191
if package.is_stdlib:
192192
python_exe = sys.executable
193-
custom_typeshed = Path(__file__).parent.parent
193+
custom_typeshed = TS_BASE_PATH
194194
flags.append("--no-site-packages")
195195
else:
196196
custom_typeshed = tempdir / TYPESHED

tests/runtests.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
from importlib.util import find_spec
1111
from pathlib import Path
1212

13-
from ts_utils.utils import TEST_CASES_DIR, colored, test_cases_path
13+
from ts_utils.paths import TEST_CASES_DIR, test_cases_path
14+
from ts_utils.utils import colored
1415

1516
_STRICTER_CONFIG_FILE = "pyrightconfig.stricter.json"
1617
_TESTCASES_CONFIG_FILE = "pyrightconfig.testcases.json"

tests/stubtest_stdlib.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
import sys
1414
from pathlib import Path
1515

16-
from ts_utils.utils import allowlist_stubtest_arguments, allowlists_path
16+
from ts_utils.paths import TS_BASE_PATH, allowlists_path
17+
from ts_utils.utils import allowlist_stubtest_arguments
1718

1819

1920
def run_stubtest(typeshed_dir: Path) -> int:
@@ -57,4 +58,4 @@ def run_stubtest(typeshed_dir: Path) -> int:
5758

5859

5960
if __name__ == "__main__":
60-
sys.exit(run_stubtest(typeshed_dir=Path(".")))
61+
sys.exit(run_stubtest(typeshed_dir=TS_BASE_PATH))

0 commit comments

Comments
 (0)