Skip to content
Merged
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
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ repos:
- id: check-github-workflows
args: ["--verbose"]
- repo: https://github.com/codespell-project/codespell
rev: v2.4.2
rev: v2.4.3
hooks:
- id: codespell
additional_dependencies: ["tomli>=2.4"]
- repo: https://github.com/tox-dev/pyproject-fmt
rev: "v2.25.2"
rev: "v2.25.3"
hooks:
- id: pyproject-fmt
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.15.21"
rev: "v0.15.22"
hooks:
- id: ruff-format
- id: ruff
Expand All @@ -39,7 +39,7 @@ repos:
- mdformat-gfm>=1
- mdformat-ruff>=0.1.3
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.26.1
rev: v1.27.0
hooks:
- id: zizmor
- repo: meta
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
name = "python-discovery"
version = ".".join(__version__.split(".")[:2])
release = __version__
copyright = f"2026-{datetime.now(tz=timezone.utc).year}, {company}" # noqa: A001
copyright = f"2026-{datetime.now(tz=timezone.utc).year}, {company}" # ruff:ignore[builtin-variable-shadowing]

extensions = [
"sphinx.ext.autodoc",
Expand Down
10 changes: 5 additions & 5 deletions src/python_discovery/_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def remove(self) -> None:

@contextmanager
def locked(self) -> Generator[None]:
from filelock import FileLock # noqa: PLC0415
from filelock import FileLock # ruff:ignore[import-outside-top-level]

lock_path = self._folder / f"{self._key}.lock"
lock_path.parent.mkdir(parents=True, exist_ok=True)
Expand Down Expand Up @@ -148,10 +148,10 @@ def py_info_clear(self) -> None:
class NoOpContentStore(ContentStore):
"""Content store that does nothing -- implements ContentStore protocol."""

def exists(self) -> bool: # noqa: PLR6301
def exists(self) -> bool: # ruff:ignore[no-self-use]
return False

def read(self) -> dict | None: # noqa: PLR6301
def read(self) -> dict | None: # ruff:ignore[no-self-use]
return None

def write(self, content: dict) -> None:
Expand All @@ -161,14 +161,14 @@ def remove(self) -> None:
pass

@contextmanager
def locked(self) -> Generator[None]: # noqa: PLR6301
def locked(self) -> Generator[None]: # ruff:ignore[no-self-use]
yield


class NoOpCache(PyInfoCache):
"""Cache that does nothing -- implements PyInfoCache protocol."""

def py_info(self, path: Path) -> NoOpContentStore: # noqa: ARG002, PLR6301
def py_info(self, path: Path) -> NoOpContentStore: # ruff:ignore[unused-method-argument, no-self-use]
return NoOpContentStore()

def py_info_clear(self) -> None:
Expand Down
8 changes: 4 additions & 4 deletions src/python_discovery/_cached_py_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
import os
import pkgutil
import secrets
import subprocess # noqa: S404
import subprocess # ruff:ignore[suspicious-subprocess-import]
import sys
import tempfile
from collections import OrderedDict
from contextlib import contextmanager
from pathlib import Path
from shlex import quote
from subprocess import Popen, TimeoutExpired # noqa: S404
from subprocess import Popen, TimeoutExpired # ruff:ignore[suspicious-subprocess-import]
from typing import TYPE_CHECKING, Final

from ._cache import NoOpCache
Expand All @@ -32,7 +32,7 @@
_LOGGER: Final[logging.Logger] = logging.getLogger(__name__)


def from_exe( # noqa: PLR0913
def from_exe( # ruff:ignore[too-many-arguments]
cls: type[PythonInfo],
cache: PyInfoCache | None,
exe: str,
Expand Down Expand Up @@ -197,7 +197,7 @@ def _run_subprocess(
env["PYTHONUTF8"] = "1"
_LOGGER.debug("get interpreter info via cmd: %s", LogCmd(cmd))
try:
process = Popen( # noqa: S603
process = Popen( # ruff:ignore[subprocess-without-shell-equals-true]
cmd,
universal_newlines=True,
stdin=subprocess.PIPE,
Expand Down
4 changes: 2 additions & 2 deletions src/python_discovery/_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def iter_interpreters(
yield from _iter_for_spec(spec_str, first_with, cache, env_map, predicate, seen)


def _iter_for_spec( # noqa: PLR0913, PLR0917
def _iter_for_spec( # ruff:ignore[too-many-arguments, too-many-positional-arguments]
spec_str: str | None,
try_first_with: tuple[str, ...],
cache: PyInfoCache | None,
Expand Down Expand Up @@ -231,7 +231,7 @@ def _propose_current_and_windows(
yield current_python, True

if IS_WIN: # pragma: win32 cover
from ._windows import propose_interpreters as win_propose # noqa: PLC0415
from ._windows import propose_interpreters as win_propose # ruff:ignore[import-outside-top-level]

for interpreter in win_propose(spec, cache, env):
if _is_new_exe(str(interpreter.executable), tested_exes):
Expand Down
31 changes: 17 additions & 14 deletions src/python_discovery/_py_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def _get_path_extensions() -> list[str]:
)


class PythonInfo: # noqa: PLR0904
class PythonInfo: # ruff:ignore[too-many-public-methods]
"""Contains information for a Python interpreter."""

def __init__(self) -> None:
Expand Down Expand Up @@ -161,7 +161,7 @@ def _get_tcl_tk_libs() -> tuple[
"""Detect the tcl and tk libraries using tkinter."""
tcl_lib, tk_lib = None, None
try:
import tkinter as tk # noqa: PLC0415
import tkinter as tk # ruff:ignore[import-outside-top-level]
except ImportError:
pass
else:
Expand All @@ -177,7 +177,7 @@ def _get_tcl_tk_libs() -> tuple[
@staticmethod
def _query_tk_library(tcl: tk.Tk) -> str | None: # pragma: no cover
"""Try to get the TK library path directly from Tcl."""
import tkinter as tk # noqa: PLC0415
import tkinter as tk # ruff:ignore[import-outside-top-level]

try:
if (tk_lib := tcl.eval("set tk_library")) and os.path.isdir(tk_lib):
Expand Down Expand Up @@ -303,16 +303,19 @@ def _distutils_install() -> dict[str, str]:
with warnings.catch_warnings(): # disable warning for PEP-632
warnings.simplefilter("ignore")
try:
from distutils import dist # noqa: PLC0415 # ty: ignore[unresolved-import]
from distutils.command.install import SCHEME_KEYS # noqa: PLC0415 # ty: ignore[unresolved-import]
# ruff:ignore[import-outside-top-level]
from distutils import dist # ty: ignore[unresolved-import]

# ruff:ignore[import-outside-top-level]
from distutils.command.install import SCHEME_KEYS # ty: ignore[unresolved-import]
except ImportError: # pragma: no cover # if removed or not installed ignore
return {}

distribution = dist.Distribution({
"script_args": "--no-user-cfg",
}) # conf files not parsed so they do not hijack paths
if hasattr(sys, "_framework"): # pragma: no cover # macOS framework builds only
sys._framework = None # noqa: SLF001 # disable macOS static paths for framework
sys._framework = None # ruff:ignore[private-member-access] # disable macOS static paths for framework

with warnings.catch_warnings(): # disable warning for PEP-632
warnings.simplefilter("ignore")
Expand Down Expand Up @@ -459,12 +462,12 @@ def clear_cache(cls, cache: PyInfoCache) -> None:

:param cache: the cache store to clear.
"""
from ._cached_py_info import clear # noqa: PLC0415
from ._cached_py_info import clear # ruff:ignore[import-outside-top-level]

clear(cache)
cls._cache_exe_discovery.clear()

def satisfies(self, spec: PythonSpec, *, impl_must_match: bool) -> bool: # noqa: PLR0911
def satisfies(self, spec: PythonSpec, *, impl_must_match: bool) -> bool: # ruff:ignore[too-many-return-statements]
"""
Check if a given specification can be satisfied by this python interpreter instance.

Expand Down Expand Up @@ -510,7 +513,7 @@ def _satisfies_version_specifier(self, spec: PythonSpec) -> bool:
return True
version_info = self.version_info
for specifier in spec.version_specifier:
assert specifier.version is not None # noqa: S101
assert specifier.version is not None # ruff:ignore[assert]
numeric_version = specifier.version_str
for prefix in ("rc", "b", "a"):
if prefix in numeric_version:
Expand All @@ -520,7 +523,7 @@ def _satisfies_version_specifier(self, spec: PythonSpec) -> bool:
release = ".".join(str(c) for c in [version_info.major, version_info.minor, version_info.micro][:precision])
if (
version_info.releaselevel != "final"
and (precision == 3 or specifier.version.pre_type is not None) # noqa: PLR2004
and (precision == 3 or specifier.version.pre_type is not None) # ruff:ignore[magic-value-comparison]
and (suffix := {"alpha": "a", "beta": "b", "candidate": "rc"}.get(version_info.releaselevel))
):
release = f"{release}{suffix}{version_info.serial}"
Expand Down Expand Up @@ -573,7 +576,7 @@ def to_dict(self) -> dict[str, object]:
return data

@classmethod
def from_exe( # noqa: PLR0913
def from_exe( # ruff:ignore[too-many-arguments]
cls,
exe: str,
cache: PyInfoCache | None = None,
Expand All @@ -593,7 +596,7 @@ def from_exe( # noqa: PLR0913
:param resolve_to_host: resolve through virtualenv layers to the system interpreter.
:param env: environment mapping; defaults to :data:`os.environ`.
"""
from ._cached_py_info import from_exe # noqa: PLC0415
from ._cached_py_info import from_exe # ruff:ignore[import-outside-top-level]

env = os.environ if env is None else env
proposed = from_exe(cls, cache, exe, env=env, raise_on_error=raise_on_error, ignore_cache=ignore_cache)
Expand Down Expand Up @@ -703,7 +706,7 @@ def discover_exe(
msg = "failed to detect {} in {}".format("|".join(possible_names), os.pathsep.join(possible_folders))
raise RuntimeError(msg)

def _check_exe( # noqa: PLR0913
def _check_exe( # ruff:ignore[too-many-arguments]
self,
cache: PyInfoCache | None,
folder: str,
Expand Down Expand Up @@ -797,7 +800,7 @@ def _possible_base(self) -> Generator[str, None, None]:
for base in possible_base:
lower = base.lower()
yield lower
from ._compat import fs_is_case_sensitive # noqa: PLC0415
from ._compat import fs_is_case_sensitive # ruff:ignore[import-outside-top-level]

if fs_is_case_sensitive(): # pragma: no branch
if base != lower:
Expand Down
6 changes: 3 additions & 3 deletions src/python_discovery/_py_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def _parse_version_parts(version: str) -> tuple[int | None, int | None, int | No
raise ValueError(msg)
if len(versions) == _MAX_VERSION_PARTS:
return versions[0], versions[1], versions[2]
if len(versions) == 2: # noqa: PLR2004
if len(versions) == 2: # ruff:ignore[magic-value-comparison]
return versions[0], versions[1], None
version_data = versions[0]
major = int(str(version_data)[0])
Expand Down Expand Up @@ -127,7 +127,7 @@ class PythonSpec:
constraints, or ``None``.
"""

def __init__( # noqa: PLR0913, PLR0917
def __init__( # ruff:ignore[too-many-arguments, too-many-positional-arguments]
self,
str_spec: str,
implementation: str | None,
Expand Down Expand Up @@ -233,7 +233,7 @@ def _get_required_precision(item: SimpleSpecifier) -> int | None:
return len(item.version.release)
return None

def satisfies(self, spec: PythonSpec) -> bool: # noqa: PLR0911
def satisfies(self, spec: PythonSpec) -> bool: # ruff:ignore[too-many-return-statements]
"""
Check if this spec is compatible with the given *spec* (e.g. PEP-514 on Windows).

Expand Down
4 changes: 2 additions & 2 deletions src/python_discovery/_specifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def __eq__(self, other: object) -> bool:
def __hash__(self) -> int:
return hash((self.release, self.pre_type, self.pre_num))

def __lt__(self, other: object) -> bool: # noqa: PLR0911
def __lt__(self, other: object) -> bool: # ruff:ignore[too-many-return-statements]
if not isinstance(other, SimpleVersion):
return NotImplemented
if self.release != other.release:
Expand Down Expand Up @@ -225,7 +225,7 @@ def _check_compatible_release(self, candidate: SimpleVersion) -> bool:
return False
if candidate < self.version:
return False
if len(self.version.release) >= 2: # noqa: PLR2004 # pragma: no branch # SimpleVersion always has 3-part release
if len(self.version.release) >= 2: # ruff:ignore[magic-value-comparison] # pragma: no branch # SimpleVersion always has 3-part release
upper_parts = list(self.version.release[:-1])
upper_parts[-1] += 1
upper = SimpleVersion.from_string(".".join(str(p) for p in upper_parts))
Expand Down
22 changes: 11 additions & 11 deletions src/python_discovery/_windows/_pep514.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
)


def enum_keys(key: Any) -> Generator[str, None, None]: # noqa: ANN401
def enum_keys(key: Any) -> Generator[str, None, None]: # ruff:ignore[any-type]
at = 0
while True:
try:
Expand All @@ -57,7 +57,7 @@ def enum_keys(key: Any) -> Generator[str, None, None]: # noqa: ANN401
at += 1


def get_value(key: Any, value_name: str | None) -> Any: # noqa: ANN401
def get_value(key: Any, value_name: str | None) -> Any: # ruff:ignore[any-type]
try:
return winreg.QueryValueEx(key, value_name)[0] # ty: ignore[unresolved-attribute]
except OSError:
Expand Down Expand Up @@ -93,7 +93,7 @@ def process_set(
def process_company(
hive_name: str,
company: str,
root_key: Any, # noqa: ANN401
root_key: Any, # ruff:ignore[any-type]
default_arch: int,
) -> Generator[_RegistrySpec, None, None]:
with winreg.OpenKeyEx(root_key, company) as company_key: # ty: ignore[unresolved-attribute]
Expand All @@ -103,7 +103,7 @@ def process_company(
yield spec


def process_tag(hive_name: str, company: str, company_key: Any, tag: str, default_arch: int) -> _RegistrySpec | None: # noqa: ANN401
def process_tag(hive_name: str, company: str, company_key: Any, tag: str, default_arch: int) -> _RegistrySpec | None: # ruff:ignore[any-type]
with winreg.OpenKeyEx(company_key, tag) as tag_key: # ty: ignore[unresolved-attribute]
version = load_version_data(hive_name, company, tag, tag_key)
if version is not None: # if failed to get version bail
Expand All @@ -120,7 +120,7 @@ def process_tag(hive_name: str, company: str, company_key: Any, tag: str, defaul
return None


def load_exe(hive_name: str, company: str, company_key: Any, tag: str) -> tuple[str, str | None] | None: # noqa: ANN401
def load_exe(hive_name: str, company: str, company_key: Any, tag: str) -> tuple[str, str | None] | None: # ruff:ignore[any-type]
key_path = f"{hive_name}/{company}/{tag}"
try:
with winreg.OpenKeyEx(company_key, rf"{tag}\InstallPath") as ip_key, ip_key: # ty: ignore[unresolved-attribute]
Expand All @@ -132,7 +132,7 @@ def load_exe(hive_name: str, company: str, company_key: Any, tag: str) -> tuple[
return None


def _resolve_exe(ip_key: Any, key_path: str) -> str | None: # noqa: ANN401
def _resolve_exe(ip_key: Any, key_path: str) -> str | None: # ruff:ignore[any-type]
if (exe := get_value(ip_key, "ExecutablePath")) is not None:
return exe
if (ip := get_value(ip_key, None)) is None:
Expand All @@ -141,7 +141,7 @@ def _resolve_exe(ip_key: Any, key_path: str) -> str | None: # noqa: ANN401
return os.path.join(ip, "python.exe")


def load_arch_data(hive_name: str, company: str, tag: str, tag_key: Any, default_arch: int) -> int | None: # noqa: ANN401
def load_arch_data(hive_name: str, company: str, tag: str, tag_key: Any, default_arch: int) -> int | None: # ruff:ignore[any-type]
arch_str = get_value(tag_key, "SysArchitecture")
if arch_str is not None:
key_path = f"{hive_name}/{company}/{tag}/SysArchitecture"
Expand All @@ -152,7 +152,7 @@ def load_arch_data(hive_name: str, company: str, tag: str, tag_key: Any, default
return default_arch


def parse_arch(arch_str: Any) -> int: # noqa: ANN401
def parse_arch(arch_str: Any) -> int: # ruff:ignore[any-type]
if isinstance(arch_str, str):
if match := _ARCH_RE.match(arch_str):
return int(next(iter(match.groups())))
Expand All @@ -166,7 +166,7 @@ def load_version_data(
hive_name: str,
company: str,
tag: str,
tag_key: Any, # noqa: ANN401
tag_key: Any, # ruff:ignore[any-type]
) -> tuple[int | None, int | None, int | None] | None:
for candidate, key_path in [
(get_value(tag_key, "SysVersion"), f"{hive_name}/{company}/{tag}/SysVersion"),
Expand All @@ -180,7 +180,7 @@ def load_version_data(
return None


def parse_version(version_str: Any) -> tuple[int | None, int | None, int | None]: # noqa: ANN401
def parse_version(version_str: Any) -> tuple[int | None, int | None, int | None]: # ruff:ignore[any-type]
if isinstance(version_str, str):
if match := _VERSION_RE.match(version_str):
g1, g2, g3 = match.groups()
Expand All @@ -195,7 +195,7 @@ def parse_version(version_str: Any) -> tuple[int | None, int | None, int | None]
raise ValueError(error)


def load_threaded(hive_name: str, company: str, tag: str, tag_key: Any) -> bool: # noqa: ANN401
def load_threaded(hive_name: str, company: str, tag: str, tag_key: Any) -> bool: # ruff:ignore[any-type]
display_name = get_value(tag_key, "DisplayName")
if display_name is not None:
if isinstance(display_name, str):
Expand Down
Loading