Skip to content

Enable Ruff flake8-use-pathlib (PTH) #13795

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

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions lib/ts_utils/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

PYPROJECT_PATH: Final = TS_BASE_PATH / "pyproject.toml"
REQUIREMENTS_PATH: Final = TS_BASE_PATH / "requirements-tests.txt"
GITIGNORE_PATH: Final = TS_BASE_PATH / ".gitignore"

TESTS_DIR: Final = "@tests"
TEST_CASES_DIR: Final = "test_cases"
Expand Down
5 changes: 2 additions & 3 deletions lib/ts_utils/requirements.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

import itertools
import os
import sys
from collections.abc import Iterable

Expand All @@ -13,14 +12,14 @@

def get_external_stub_requirements(distributions: Iterable[str] = ()) -> set[Requirement]:
if not distributions:
distributions = os.listdir(STUBS_PATH)
distributions = [distribution.name for distribution in STUBS_PATH.iterdir()]

return set(itertools.chain.from_iterable([read_dependencies(distribution).external_pkgs for distribution in distributions]))


def get_stubtest_system_requirements(distributions: Iterable[str] = (), platform: str = sys.platform) -> list[str]:
if not distributions:
distributions = os.listdir(STUBS_PATH)
distributions = [distribution.name for distribution in STUBS_PATH.iterdir()]

requirements: list[str] = []
for distribution in distributions:
Expand Down
4 changes: 2 additions & 2 deletions lib/ts_utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def colored(text: str, color: str | None = None, **kwargs: Any) -> str: # type:
return text


from .paths import REQUIREMENTS_PATH, STDLIB_PATH, STUBS_PATH, TEST_CASES_DIR, allowlists_path, test_cases_path
from .paths import GITIGNORE_PATH, REQUIREMENTS_PATH, STDLIB_PATH, STUBS_PATH, TEST_CASES_DIR, allowlists_path, test_cases_path

PYTHON_VERSION: Final = f"{sys.version_info.major}.{sys.version_info.minor}"

Expand Down Expand Up @@ -203,7 +203,7 @@ def allowlists(distribution_name: str) -> list[str]:

@functools.cache
def get_gitignore_spec() -> pathspec.PathSpec:
with open(".gitignore", encoding="UTF-8") as f:
with GITIGNORE_PATH.open(encoding="UTF-8") as f:
return pathspec.GitIgnoreSpec.from_lines(f)


Expand Down
4 changes: 1 addition & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ select = [
"PGH", # pygrep-hooks
"PIE", # flake8-pie
"PL", # Pylint
"PTH", # flake8-use-pathlib
"RSE", # flake8-raise
"RUF", # Ruff-specific and unused-noqa
"SLOT", # flake8-slots
Expand All @@ -85,9 +86,6 @@ select = [
"FURB187", # Use of assignment of `reversed` on list `{name}`
# Used for lint.flake8-import-conventions.aliases
"ICN001", # `{name}` should be imported as `{asname}`
# Autofixable flake8-use-pathlib only
"PTH201", # Do not pass the current directory explicitly to `Path`
"PTH210", # Invalid suffix passed to `.with_suffix()`
# PYI: only enable rules that have autofixes and that we always want to fix (even manually),
# avoids duplicate # noqa with flake8-pyi
"PYI009", # Empty body should contain `...`, not pass
Expand Down
44 changes: 22 additions & 22 deletions scripts/create_baseline_stubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,20 @@

import argparse
import asyncio
import glob
import os.path
import re
import subprocess
import sys
import urllib.parse
from http import HTTPStatus
from importlib.metadata import distribution
from pathlib import Path

import aiohttp
import termcolor

PYRIGHT_CONFIG = "pyrightconfig.stricter.json"
from ts_utils.paths import STDLIB_PATH, STUBS_PATH

PYRIGHT_CONFIG = Path("pyrightconfig.stricter.json")


def search_pip_freeze_output(project: str, output: str) -> tuple[str, str] | None:
Expand Down Expand Up @@ -52,22 +53,22 @@ def get_installed_package_info(project: str) -> tuple[str, str] | None:
return search_pip_freeze_output(project, r.stdout)


def run_stubgen(package: str, output: str) -> None:
def run_stubgen(package: str, output: Path) -> None:
print(f"Running stubgen: stubgen -o {output} -p {package}")
subprocess.run(["stubgen", "-o", output, "-p", package, "--export-less"], check=True)


def run_stubdefaulter(stub_dir: str) -> None:
def run_stubdefaulter(stub_dir: Path) -> None:
print(f"Running stubdefaulter: stubdefaulter --packages {stub_dir}")
subprocess.run(["stubdefaulter", "--packages", stub_dir], check=False)


def run_black(stub_dir: str) -> None:
def run_black(stub_dir: Path) -> None:
print(f"Running Black: black {stub_dir}")
subprocess.run(["pre-commit", "run", "black", "--files", *glob.iglob(f"{stub_dir}/**/*.pyi")], check=False)
subprocess.run(["pre-commit", "run", "black", "--files", *stub_dir.rglob("*.pyi")], check=False)


def run_ruff(stub_dir: str) -> None:
def run_ruff(stub_dir: Path) -> None:
print(f"Running Ruff: ruff check {stub_dir} --fix-only")
subprocess.run([sys.executable, "-m", "ruff", "check", stub_dir, "--fix-only"], check=False)

Expand Down Expand Up @@ -115,14 +116,14 @@ async def get_upstream_repo_url(project: str) -> str | None:
return None


def create_metadata(project: str, stub_dir: str, version: str) -> None:
def create_metadata(project: str, stub_dir: Path, version: str) -> None:
"""Create a METADATA.toml file."""
match = re.match(r"[0-9]+.[0-9]+", version)
if match is None:
sys.exit(f"Error: Cannot parse version number: {version}")
filename = os.path.join(stub_dir, "METADATA.toml")
filename = stub_dir / "METADATA.toml"
version = match.group(0)
if os.path.exists(filename):
if filename.exists():
return
metadata = f'version = "{version}.*"\n'
upstream_repo_url = asyncio.run(get_upstream_repo_url(project))
Expand All @@ -135,13 +136,12 @@ def create_metadata(project: str, stub_dir: str, version: str) -> None:
else:
metadata += f'upstream_repository = "{upstream_repo_url}"\n'
print(f"Writing {filename}")
with open(filename, "w", encoding="UTF-8") as file:
file.write(metadata)
filename.write_text(metadata, encoding="UTF-8")


def add_pyright_exclusion(stub_dir: str) -> None:
def add_pyright_exclusion(stub_dir: Path) -> None:
"""Exclude stub_dir from strict pyright checks."""
with open(PYRIGHT_CONFIG, encoding="UTF-8") as f:
with PYRIGHT_CONFIG.open(encoding="UTF-8") as f:
lines = f.readlines()
i = 0
while i < len(lines) and not lines[i].strip().startswith('"exclude": ['):
Expand All @@ -167,7 +167,7 @@ def add_pyright_exclusion(stub_dir: str) -> None:
third_party_excludes[-1] = last_line + "\n"

# Must use forward slash in the .json file
line_to_add = f' "{stub_dir}",\n'.replace("\\", "/")
line_to_add = f' "{stub_dir.as_posix()}",\n'

if line_to_add in third_party_excludes:
print(f"{PYRIGHT_CONFIG} already up-to-date")
Expand All @@ -177,7 +177,7 @@ def add_pyright_exclusion(stub_dir: str) -> None:
third_party_excludes.sort(key=str.lower)

print(f"Updating {PYRIGHT_CONFIG}")
with open(PYRIGHT_CONFIG, "w", encoding="UTF-8") as f:
with PYRIGHT_CONFIG.open("w", encoding="UTF-8") as f:
f.writelines(before_third_party_excludes)
f.writelines(third_party_excludes)
f.writelines(after_third_party_excludes)
Expand All @@ -194,7 +194,7 @@ def main() -> None:
parser.add_argument("--package", help="generate stubs for this Python package (default is autodetected)")
args = parser.parse_args()
project = args.project
package = args.package
package: str = args.package

if not re.match(r"[a-zA-Z0-9-_.]+$", project):
sys.exit(f"Invalid character in project name: {project!r}")
Expand All @@ -214,7 +214,7 @@ def main() -> None:
print(f'Using detected package "{package}" for project "{project}"', file=sys.stderr)
print("Suggestion: Try again with --package argument if that's not what you wanted", file=sys.stderr)

if not os.path.isdir("stubs") or not os.path.isdir("stdlib"):
if not STUBS_PATH.is_dir() or not STDLIB_PATH.is_dir():
sys.exit("Error: Current working directory must be the root of typeshed repository")

# Get normalized project name and version of installed package.
Expand All @@ -226,9 +226,9 @@ def main() -> None:
sys.exit(1)
project, version = info

stub_dir = os.path.join("stubs", project)
package_dir = os.path.join(stub_dir, package)
if os.path.exists(package_dir):
stub_dir = STUBS_PATH / project
package_dir = stub_dir / package
if package_dir.exists():
sys.exit(f"Error: {package_dir} already exists (delete it first)")

run_stubgen(package, stub_dir)
Expand Down
7 changes: 4 additions & 3 deletions scripts/sync_protobuf/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import sys
from collections.abc import Iterable
from http.client import HTTPResponse
from pathlib import Path
from typing import TYPE_CHECKING
from urllib.request import urlopen
from zipfile import ZipFile
Expand All @@ -18,11 +19,11 @@
MYPY_PROTOBUF_VERSION = mypy_protobuf__version__


def download_file(url: str, destination: StrPath) -> None:
def download_file(url: str, destination: Path) -> None:
print(f"Downloading '{url}' to '{destination}'")
resp: HTTPResponse
with urlopen(url) as resp, open(destination, "wb") as file:
file.write(resp.read())
with urlopen(url) as resp:
destination.write_bytes(resp.read())


def extract_archive(archive_path: StrPath, destination: StrPath) -> None:
Expand Down
4 changes: 2 additions & 2 deletions scripts/sync_protobuf/google_protobuf.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

def extract_python_version(file_path: Path) -> str:
"""Extract the Python version from https://github.com/protocolbuffers/protobuf/blob/main/version.json ."""
with open(file_path) as file:
with file_path.open() as file:
data: dict[str, Any] = json.load(file)
# The root key will be the protobuf source code version
version = next(iter(data.values()))["languages"]["python"]
Expand All @@ -47,7 +47,7 @@ def extract_proto_file_paths(temp_dir: Path) -> list[str]:
as described in py_proto_library calls in
https://github.com/protocolbuffers/protobuf/blob/main/python/dist/BUILD.bazel .
"""
with open(temp_dir / EXTRACTED_PACKAGE_DIR / "python" / "dist" / "BUILD.bazel") as file:
with (temp_dir / EXTRACTED_PACKAGE_DIR / "python" / "dist" / "BUILD.bazel").open() as file:
matched_lines = filter(None, (re.search(PROTO_FILE_PATTERN, line) for line in file))
proto_files = [
EXTRACTED_PACKAGE_DIR + "/src/google/protobuf/" + match.group(1).replace("compiler_", "compiler/") + ".proto"
Expand Down
9 changes: 3 additions & 6 deletions scripts/sync_protobuf/tensorflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

from __future__ import annotations

import os
import re
import shutil
import subprocess
Expand Down Expand Up @@ -72,21 +71,19 @@ def post_creation() -> None:

for path in STUBS_FOLDER.rglob("*_pb2.pyi"):
print(f"Fixing imports in '{path}'")
with open(path, encoding="utf-8") as file:
filedata = file.read()
filedata = path.read_text(encoding="utf-8")

# Replace the target string
filedata = re.sub(TSL_IMPORT_PATTERN, "\\1tensorflow.tsl.", filedata)
filedata = re.sub(XLA_IMPORT_PATTERN, "\\1tensorflow.compiler.xla.", filedata)

# Write the file out again
with open(path, "w", encoding="utf-8") as file:
file.write(filedata)
path.write_text(filedata, encoding="utf-8")

print()
for to_remove in PROTOS_TO_REMOVE:
file_path = STUBS_FOLDER / "tensorflow" / to_remove
os.remove(file_path)
file_path.unlink()
print(f"Removed '{file_path}'")


Expand Down
13 changes: 6 additions & 7 deletions tests/check_typeshed_structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,10 @@ def check_test_cases() -> None:

def check_no_symlinks() -> None:
"""Check that there are no symlinks in the typeshed repository."""
files = [os.path.join(root, file) for root, _, files in os.walk(".") for file in files]
files = [Path(root, file) for root, _, files in os.walk(".") for file in files]
no_symlink = "You cannot use symlinks in typeshed, please copy {} to its link."
for file in files:
_, ext = os.path.splitext(file)
if ext == ".pyi" and os.path.islink(file):
if file.suffix == ".pyi" and file.is_symlink():
raise ValueError(no_symlink.format(file))


Expand All @@ -141,20 +140,20 @@ def _find_stdlib_modules() -> set[str]:
modules = set[str]()
for path, _, files in os.walk(STDLIB_PATH):
for filename in files:
base_module = ".".join(os.path.normpath(path).split(os.sep)[1:])
base_module = ".".join(Path(path).parts[1:])
if filename == "__init__.pyi":
modules.add(base_module)
elif filename.endswith(".pyi"):
mod, _ = os.path.splitext(filename)
mod = filename[:-4]
modules.add(f"{base_module}.{mod}" if base_module else mod)
return modules


def check_metadata() -> None:
"""Check that all METADATA.toml files are valid."""
for distribution in os.listdir("stubs"):
for distribution in STUBS_PATH.iterdir():
# This function does various sanity checks for METADATA.toml files
read_metadata(distribution)
read_metadata(distribution.name)


def check_requirement_pins() -> None:
Expand Down
19 changes: 9 additions & 10 deletions tests/mypy_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def _named_temporary_file() -> Generator[tempfile._TemporaryFileWrapper[str]]:
yield temp
finally:
temp.close()
os.remove(temp.name)
Path(temp.name).unlink()


SUPPORTED_VERSIONS = ["3.13", "3.12", "3.11", "3.10", "3.9"]
Expand Down Expand Up @@ -170,6 +170,8 @@ def match(path: Path, args: TestConfig) -> bool:

def add_files(files: list[Path], module: Path, args: TestConfig) -> None:
"""Add all files in package or module represented by 'name' located in 'root'."""
if module.name.startswith("."):
return
if module.is_file() and module.suffix == ".pyi":
if match(module, args):
files.append(module)
Expand Down Expand Up @@ -307,10 +309,8 @@ def add_third_party_files(
seen_dists.add(distribution)
seen_dists.update(r.name for r in typeshed_reqs)
root = distribution_path(distribution)
for name in os.listdir(root):
if name.startswith("."):
continue
add_files(files, (root / name), args)
for path in root.iterdir():
add_files(files, path, args)
add_configuration(configurations, distribution)


Expand Down Expand Up @@ -359,7 +359,7 @@ def test_third_party_distribution(
def test_stdlib(args: TestConfig) -> TestResult:
files: list[Path] = []
for file in STDLIB_PATH.iterdir():
if file.name in ("VERSIONS", TESTS_DIR) or file.name.startswith("."):
if file.name in ("VERSIONS", TESTS_DIR):
continue
add_files(files, file, args)

Expand Down Expand Up @@ -536,7 +536,7 @@ def test_third_party_stubs(args: TestConfig, tempdir: Path) -> TestSummary:
gitignore_spec = get_gitignore_spec()
distributions_to_check: dict[str, PackageDependencies] = {}

for distribution in sorted(os.listdir("stubs")):
for distribution in sorted([distribution.name for distribution in STUBS_PATH.iterdir()]):
dist_path = distribution_path(distribution)

if spec_matches_path(gitignore_spec, dist_path):
Expand Down Expand Up @@ -589,15 +589,14 @@ def test_third_party_stubs(args: TestConfig, tempdir: Path) -> TestSummary:

def test_typeshed(args: TestConfig, tempdir: Path) -> TestSummary:
print(f"*** Testing Python {args.version} on {args.platform}")
stdlib_dir, stubs_dir = Path("stdlib"), Path("stubs")
summary = TestSummary()

if stdlib_dir in args.filter or any(stdlib_dir in path.parents for path in args.filter):
if STDLIB_PATH in args.filter or any(STDLIB_PATH in path.parents for path in args.filter):
mypy_result, files_checked = test_stdlib(args)
summary.register_result(mypy_result, files_checked)
print()

if stubs_dir in args.filter or any(stubs_dir in path.parents for path in args.filter):
if STUBS_PATH in args.filter or any(STUBS_PATH in path.parents for path in args.filter):
tp_results = test_third_party_stubs(args, tempdir)
summary.merge(tp_results)
print()
Expand Down
Loading
Loading