From 4e08c3a409f69b41a0b811e418430bb2f4b04062 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Wed, 31 Jul 2024 21:09:16 -0400 Subject: [PATCH 01/10] Switch from flake8 to ruff for linting --- noxfile.py | 2 +- pyproject.toml | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/noxfile.py b/noxfile.py index d2e8398..c788ea6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -18,7 +18,7 @@ def lint(session): session.run("black", "--check", ".") session.run("isort", ".") - session.run("flake8", ".") + session.run("ruff", "check", ".") session.run("mypy", "src", "tests") diff --git a/pyproject.toml b/pyproject.toml index fd392b0..9bd3cf6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,8 +26,7 @@ Homepage = "https://github.com/sarugaku/resolvelib" [project.optional-dependencies] lint = [ "black==23.12.1", - "flake8", - "Flake8-pyproject", + "ruff", "isort", "mypy", "types-requests", @@ -92,10 +91,12 @@ directory = 'trivial' name = 'Trivial Changes' showcontent = false -[tool.flake8] -max-line-length = 88 +[tool.ruff] +line-length = 88 + +[tool.ruff.lint] select = ["C","E","F","W","B"] -ignore = ["E203", "W503", "F401"] +ignore = ["E203", "F401"] exclude = [ ".git", ".venv", From e0514aefa2745db9ba4db0e89b8730220661a091 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Wed, 31 Jul 2024 21:10:18 -0400 Subject: [PATCH 02/10] Manual fix B904: https://docs.astral.sh/ruff/rules/raise-without-from-inside-except/ --- src/resolvelib/resolvers.py | 6 +++--- tests/functional/python/py2index.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/resolvelib/resolvers.py b/src/resolvelib/resolvers.py index 4651f56..87f2033 100644 --- a/src/resolvelib/resolvers.py +++ b/src/resolvelib/resolvers.py @@ -109,8 +109,8 @@ def __init__( def state(self) -> State[RT, CT, KT]: try: return self._states[-1] - except IndexError: - raise AttributeError("state") + except IndexError as e: + raise AttributeError("state") from e def _push_new_state(self) -> None: """Push a new state into history. @@ -421,7 +421,7 @@ def resolve( try: self._add_to_criteria(self.state.criteria, r, parent=None) except RequirementsConflicted as e: - raise ResolutionImpossible(e.criterion.information) + raise ResolutionImpossible(e.criterion.information) from e # The root state is saved as a sentinel so the first ever pin can have # something to backtrack to if it fails. The root state is basically diff --git a/tests/functional/python/py2index.py b/tests/functional/python/py2index.py index 4770a66..2bb4340 100644 --- a/tests/functional/python/py2index.py +++ b/tests/functional/python/py2index.py @@ -121,8 +121,8 @@ def get_output_path(path: pathlib.Path, overwrite: bool) -> pathlib.Path: def _parse_tag(s: str) -> FrozenSet[packaging.tags.Tag]: try: return packaging.tags.parse_tag(s) - except ValueError: - raise ValueError(f"invalid tag {s!r}") + except ValueError as e: + raise ValueError(f"invalid tag {s!r}") from e @dataclasses.dataclass() From 949e6120fbc7b714fde71c2118a96d085a69acb2 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Wed, 31 Jul 2024 21:11:41 -0400 Subject: [PATCH 03/10] Manually fix B023: https://docs.astral.sh/ruff/rules/function-uses-loop-variable/ --- src/resolvelib/resolvers.py | 70 +++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/src/resolvelib/resolvers.py b/src/resolvelib/resolvers.py index 87f2033..579ba8f 100644 --- a/src/resolvelib/resolvers.py +++ b/src/resolvelib/resolvers.py @@ -269,6 +269,41 @@ def _attempt_to_pin_criterion(self, name: KT) -> list[Criterion[RT, CT]]: # end, signal for backtracking. return causes + def _patch_criteria( + self, incompatibilities_from_broken: list[tuple[KT, list[CT]]] + ) -> bool: + # Create a new state from the last known-to-work one, and apply + # the previously gathered incompatibility information. + for k, incompatibilities in incompatibilities_from_broken: + if not incompatibilities: + continue + try: + criterion = self.state.criteria[k] + except KeyError: + continue + matches = self._p.find_matches( + identifier=k, + requirements=IteratorMapping( + self.state.criteria, + operator.methodcaller("iter_requirement"), + ), + incompatibilities=IteratorMapping( + self.state.criteria, + operator.attrgetter("incompatibilities"), + {k: incompatibilities}, + ), + ) + candidates: IterableView[CT] = build_iter_view(matches) + if not candidates: + return False + incompatibilities.extend(criterion.incompatibilities) + self.state.criteria[k] = Criterion( + candidates=candidates, + information=list(criterion.information), + incompatibilities=incompatibilities, + ) + return True + def _backjump(self, causes: list[RequirementInformation[RT, CT]]) -> bool: """Perform backjumping. @@ -347,41 +382,8 @@ def _backjump(self, causes: list[RequirementInformation[RT, CT]]) -> bool: # Also mark the newly known incompatibility. incompatibilities_from_broken.append((name, [candidate])) - # Create a new state from the last known-to-work one, and apply - # the previously gathered incompatibility information. - def _patch_criteria() -> bool: - for k, incompatibilities in incompatibilities_from_broken: - if not incompatibilities: - continue - try: - criterion = self.state.criteria[k] - except KeyError: - continue - matches = self._p.find_matches( - identifier=k, - requirements=IteratorMapping( - self.state.criteria, - operator.methodcaller("iter_requirement"), - ), - incompatibilities=IteratorMapping( - self.state.criteria, - operator.attrgetter("incompatibilities"), - {k: incompatibilities}, - ), - ) - candidates: IterableView[CT] = build_iter_view(matches) - if not candidates: - return False - incompatibilities.extend(criterion.incompatibilities) - self.state.criteria[k] = Criterion( - candidates=candidates, - information=list(criterion.information), - incompatibilities=incompatibilities, - ) - return True - self._push_new_state() - success = _patch_criteria() + success = self._patch_criteria(incompatibilities_from_broken) # It works! Let's work on this new state. if success: From d38dae1401328c8e940d085aefe38be3a9f3211c Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Wed, 31 Jul 2024 21:14:10 -0400 Subject: [PATCH 04/10] Add RUF rule selection and autofix --- examples/reporter_demo.py | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/reporter_demo.py b/examples/reporter_demo.py index d98c299..622e41c 100644 --- a/examples/reporter_demo.py +++ b/examples/reporter_demo.py @@ -26,14 +26,14 @@ """ -class Requirement(namedtuple("Requirement", "name specifier")): # noqa +class Requirement(namedtuple("Requirement", "name specifier")): def __repr__(self): return "".format( name=self.name, specifier=self.specifier ) -class Candidate(namedtuple("Candidate", "name version")): # noqa +class Candidate(namedtuple("Candidate", "name version")): def __repr__(self): return "<{name}=={version}>".format( name=self.name, version=self.version diff --git a/pyproject.toml b/pyproject.toml index 9bd3cf6..59c28c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,7 +95,7 @@ showcontent = false line-length = 88 [tool.ruff.lint] -select = ["C","E","F","W","B"] +select = ["C","E","F","W","B","RUF"] ignore = ["E203", "F401"] exclude = [ ".git", From 1fab1c29d410d5361af6fd8534f8b3c4e539b742 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Wed, 31 Jul 2024 21:20:21 -0400 Subject: [PATCH 05/10] Add Pylint Warnings except PLW2901: https://docs.astral.sh/ruff/rules/redefined-loop-name/ --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 59c28c0..1550ef3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,8 +95,8 @@ showcontent = false line-length = 88 [tool.ruff.lint] -select = ["C","E","F","W","B","RUF"] -ignore = ["E203", "F401"] +select = ["C","E","F","W","B","RUF","PLE","PLW"] +ignore = ["E203", "F401", "PLW2901"] exclude = [ ".git", ".venv", From 5f30f5226c8a6eeb4bcfc4c785d7fe087de6ec38 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Wed, 31 Jul 2024 21:26:03 -0400 Subject: [PATCH 06/10] Run `ruff check . --select UP --fix --unsafe-fixes` and keep correct looking upgrades --- examples/extras_provider.py | 4 +-- examples/pypi_wheel_provider.py | 4 +-- examples/reporter_demo.py | 8 ++--- tests/conftest.py | 1 - .../cocoapods/test_resolvers_cocoapods.py | 3 +- tests/functional/python/py2index.py | 34 +++++++++---------- .../python/test_resolvers_python.py | 4 +-- 7 files changed, 26 insertions(+), 32 deletions(-) diff --git a/examples/extras_provider.py b/examples/extras_provider.py index b4c4eb6..e193790 100644 --- a/examples/extras_provider.py +++ b/examples/extras_provider.py @@ -39,7 +39,7 @@ def get_base_requirement(self, candidate): raise NotImplementedError def identify(self, requirement_or_candidate): - base = super(ExtrasProvider, self).identify(requirement_or_candidate) + base = super().identify(requirement_or_candidate) extras = self.get_extras_for(requirement_or_candidate) if extras: return (base, extras) @@ -47,7 +47,7 @@ def identify(self, requirement_or_candidate): return base def get_dependencies(self, candidate): - deps = super(ExtrasProvider, self).get_dependencies(candidate) + deps = super().get_dependencies(candidate) if candidate.extras: req = self.get_base_requirement(candidate) deps.append(req) diff --git a/examples/pypi_wheel_provider.py b/examples/pypi_wheel_provider.py index aed9e7e..888aa69 100644 --- a/examples/pypi_wheel_provider.py +++ b/examples/pypi_wheel_provider.py @@ -68,7 +68,7 @@ def dependencies(self): def get_project_from_pypi(project, extras): """Return candidates created from the project name and extras.""" - url = "https://pypi.org/simple/{}".format(project) + url = f"https://pypi.org/simple/{project}" data = requests.get(url).content doc = html5lib.parse(data, namespaceHTMLElements=False) for i in doc.findall(".//a"): @@ -120,7 +120,7 @@ def get_extras_for(self, requirement_or_candidate): return tuple(sorted(requirement_or_candidate.extras)) def get_base_requirement(self, candidate): - return Requirement("{}=={}".format(candidate.name, candidate.version)) + return Requirement(f"{candidate.name}=={candidate.version}") def get_preference(self, identifier, resolutions, candidates, information): return sum(1 for _ in candidates[identifier]) diff --git a/examples/reporter_demo.py b/examples/reporter_demo.py index 622e41c..a279563 100644 --- a/examples/reporter_demo.py +++ b/examples/reporter_demo.py @@ -28,16 +28,12 @@ class Requirement(namedtuple("Requirement", "name specifier")): def __repr__(self): - return "".format( - name=self.name, specifier=self.specifier - ) + return f"" class Candidate(namedtuple("Candidate", "name version")): def __repr__(self): - return "<{name}=={version}>".format( - name=self.name, version=self.version - ) + return f"<{self.name}=={self.version}>" def splitstrip(s, parts): diff --git a/tests/conftest.py b/tests/conftest.py index f2e23dd..2279094 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,3 @@ -from __future__ import print_function import pytest diff --git a/tests/functional/cocoapods/test_resolvers_cocoapods.py b/tests/functional/cocoapods/test_resolvers_cocoapods.py index c73e4b6..a7eee16 100644 --- a/tests/functional/cocoapods/test_resolvers_cocoapods.py +++ b/tests/functional/cocoapods/test_resolvers_cocoapods.py @@ -146,8 +146,7 @@ def _clean_identifier(s): def _iter_resolved(dependencies): for entry in dependencies: yield (entry["name"], Version(entry["version"])) - for sub in _iter_resolved(entry["dependencies"]): - yield sub + yield from _iter_resolved(entry["dependencies"]) class CocoaPodsInputProvider(AbstractProvider): diff --git a/tests/functional/python/py2index.py b/tests/functional/python/py2index.py index 2bb4340..c482ae0 100644 --- a/tests/functional/python/py2index.py +++ b/tests/functional/python/py2index.py @@ -64,7 +64,7 @@ def _parse_python_version(s: str) -> PythonVersion: return (int(major),) -def _parse_output_path(s: str) -> Optional[pathlib.Path]: +def _parse_output_path(s: str) -> pathlib.Path | None: if s == "-": return None if os.sep in s or (os.altsep and os.altsep in s): @@ -72,7 +72,7 @@ def _parse_output_path(s: str) -> Optional[pathlib.Path]: return pathlib.Path(__file__).with_name("inputs").joinpath("index", s) -def parse_args(args: Optional[List[str]]) -> argparse.Namespace: +def parse_args(args: list[str] | None) -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument( "package_names", @@ -118,7 +118,7 @@ def get_output_path(path: pathlib.Path, overwrite: bool) -> pathlib.Path: return path -def _parse_tag(s: str) -> FrozenSet[packaging.tags.Tag]: +def _parse_tag(s: str) -> frozenset[packaging.tags.Tag]: try: return packaging.tags.parse_tag(s) except ValueError as e: @@ -128,14 +128,14 @@ def _parse_tag(s: str) -> FrozenSet[packaging.tags.Tag]: @dataclasses.dataclass() class WheelMatcher: required_python: packaging.version.Version - tags: Dict[packaging.tags.Tag, int] + tags: dict[packaging.tags.Tag, int] @classmethod def compatible_with( cls, python_version: PythonVersion, - impl: Optional[str], - plats: Optional[List[str]], + impl: str | None, + plats: list[str] | None, ) -> WheelMatcher: required_python = packaging.version.Version( ".".join(str(v) for v in python_version) @@ -148,7 +148,7 @@ def compatible_with( tags = {t: i for i, t in enumerate(tag_it)} return cls(required_python, tags) - def rank(self, tag: str, requires_python: Optional[str]) -> Optional[int]: + def rank(self, tag: str, requires_python: str | None) -> int | None: if requires_python: spec = packaging.specifiers.SpecifierSet(requires_python) if self.required_python not in spec: @@ -197,7 +197,7 @@ def tell(self): return self._offset -def _parse_wheel_name(rest: str) -> Tuple[str, str, str]: +def _parse_wheel_name(rest: str) -> tuple[str, str, str]: name, rest = rest.split("-", 1) version, x, y, z = rest.rsplit("-", 3) return name, version, f"{x}-{y}-{z}" @@ -205,7 +205,7 @@ def _parse_wheel_name(rest: str) -> Tuple[str, str, str]: class PackageEntry(NamedTuple): version: str - dependencies: List[str] + dependencies: list[str] DistListMapping = Dict[str, List[Tuple[int, str]]] @@ -213,11 +213,11 @@ class PackageEntry(NamedTuple): @dataclasses.dataclass() class Finder: - index_urls: List[str] + index_urls: list[str] matcher: WheelMatcher session: requests.Session - def collect_best_metadta_urls(self, name: str) -> Dict[str, str]: + def collect_best_metadta_urls(self, name: str) -> dict[str, str]: all_dists: DistListMapping = collections.defaultdict(list) for index_url in self.index_urls: res = requests.get(f"{index_url}/{name}") @@ -259,12 +259,12 @@ def iter_package_entries(self, name: str) -> Iterator[PackageEntry]: http_file = HttpFile(url, self.session) parser = email.parser.BytesParser() data = parser.parsebytes(http_file.read(), headersonly=True) - dependencies: List[str] = data.get_all("Requires-Dist", []) + dependencies: list[str] = data.get_all("Requires-Dist", []) yield PackageEntry(version, dependencies) def process_package_entry( self, name: str, entry: PackageEntry - ) -> Optional[Set[str]]: + ) -> set[str] | None: more = set() for dep in entry.dependencies: try: @@ -283,10 +283,10 @@ def process_package_entry( def find(self, package_names: Iterable[str]) -> dict: data = {} while package_names: - more: Set[str] = set() + more: set[str] = set() logger.info("Discovering %s", ", ".join(package_names)) for name in package_names: - entries: Dict[str, dict] = {} + entries: dict[str, dict] = {} for e in self.iter_package_entries(name): result = self.process_package_entry(name, e) if result is None: @@ -298,10 +298,10 @@ def find(self, package_names: Iterable[str]) -> dict: return data -def main(args: Optional[List[str]]) -> int: +def main(args: list[str] | None) -> int: options = parse_args(args) if not options.output: - output_path: Optional[pathlib.Path] = None + output_path: pathlib.Path | None = None else: output_path = get_output_path(options.output, options.overwrite) matcher = WheelMatcher.compatible_with( diff --git a/tests/functional/python/test_resolvers_python.py b/tests/functional/python/test_resolvers_python.py index 19d9f86..0847354 100644 --- a/tests/functional/python/test_resolvers_python.py +++ b/tests/functional/python/test_resolvers_python.py @@ -72,7 +72,7 @@ def identify(self, requirement_or_candidate): name = packaging.utils.canonicalize_name(requirement_or_candidate.name) if requirement_or_candidate.extras: extras_str = ",".join(sorted(requirement_or_candidate.extras)) - return "{}[{}]".format(name, extras_str) + return f"{name}[{extras_str}]" return name def get_preference( @@ -112,7 +112,7 @@ def is_satisfied_by(self, requirement, candidate): def _iter_dependencies(self, candidate): name = packaging.utils.canonicalize_name(candidate.name) if candidate.extras: - r = "{}=={}".format(name, candidate.version) + r = f"{name}=={candidate.version}" yield packaging.requirements.Requirement(r) for r in self.index[name][str(candidate.version)]["dependencies"]: requirement = packaging.requirements.Requirement(r) From 7142972d5e321a78cef4ba166a8e23e2b1fe6a89 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Wed, 31 Jul 2024 21:27:27 -0400 Subject: [PATCH 07/10] Remove no longer used compatability collections_abc --- src/resolvelib/compat/__init__.py | 0 src/resolvelib/compat/collections_abc.py | 6 ------ src/resolvelib/compat/collections_abc.pyi | 1 - 3 files changed, 7 deletions(-) delete mode 100644 src/resolvelib/compat/__init__.py delete mode 100644 src/resolvelib/compat/collections_abc.py delete mode 100644 src/resolvelib/compat/collections_abc.pyi diff --git a/src/resolvelib/compat/__init__.py b/src/resolvelib/compat/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/resolvelib/compat/collections_abc.py b/src/resolvelib/compat/collections_abc.py deleted file mode 100644 index 1becc50..0000000 --- a/src/resolvelib/compat/collections_abc.py +++ /dev/null @@ -1,6 +0,0 @@ -__all__ = ["Mapping", "Sequence"] - -try: - from collections.abc import Mapping, Sequence -except ImportError: - from collections import Mapping, Sequence diff --git a/src/resolvelib/compat/collections_abc.pyi b/src/resolvelib/compat/collections_abc.pyi deleted file mode 100644 index 2a088b1..0000000 --- a/src/resolvelib/compat/collections_abc.pyi +++ /dev/null @@ -1 +0,0 @@ -from collections.abc import Mapping, Sequence From 413e49e177b212e850b9980990c273749dbb4113 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Wed, 31 Jul 2024 21:28:03 -0400 Subject: [PATCH 08/10] Remove F401 from ignore: https://docs.astral.sh/ruff/rules/unused-import/ --- pyproject.toml | 2 +- tests/functional/python/py2index.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1550ef3..9286dc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,7 +96,7 @@ line-length = 88 [tool.ruff.lint] select = ["C","E","F","W","B","RUF","PLE","PLW"] -ignore = ["E203", "F401", "PLW2901"] +ignore = ["E203", "PLW2901"] exclude = [ ".git", ".venv", diff --git a/tests/functional/python/py2index.py b/tests/functional/python/py2index.py index c482ae0..fc6a0bc 100644 --- a/tests/functional/python/py2index.py +++ b/tests/functional/python/py2index.py @@ -31,13 +31,10 @@ import urllib.parse from typing import ( Dict, - FrozenSet, Iterable, Iterator, List, NamedTuple, - Optional, - Set, Tuple, Union, ) From 962e2aaa3f538010167f11ee6e8f0a1b6992ebbb Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Wed, 31 Jul 2024 21:28:56 -0400 Subject: [PATCH 09/10] Remove E203 from ignore: E203 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9286dc8..51c6a78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,7 +96,7 @@ line-length = 88 [tool.ruff.lint] select = ["C","E","F","W","B","RUF","PLE","PLW"] -ignore = ["E203", "PLW2901"] +ignore = ["PLW2901"] exclude = [ ".git", ".venv", From f2891e1d465d9f2cb2844d568a74c80eac3d0309 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Wed, 31 Jul 2024 21:40:39 -0400 Subject: [PATCH 10/10] Fix linting --- tests/conftest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2279094..fca2799 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,3 @@ - import pytest from resolvelib import BaseReporter