From dd98f2136fcc563d5d7671f3d2312046ef32a8ac Mon Sep 17 00:00:00 2001 From: Tim Hatch Date: Sun, 12 Jun 2022 05:21:39 -0700 Subject: [PATCH 01/12] Initial version --- bumpreqs/__init__.py | 0 bumpreqs/cli.py | 77 ++++++++++++++++++++++++++++++++++++++++++++ requirements-dev.txt | 2 +- requirements.txt | 4 +++ setup.cfg | 5 +++ setup.py | 1 + 6 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 bumpreqs/__init__.py create mode 100644 bumpreqs/cli.py create mode 100644 requirements.txt diff --git a/bumpreqs/__init__.py b/bumpreqs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bumpreqs/cli.py b/bumpreqs/cli.py new file mode 100644 index 0000000..9f696cd --- /dev/null +++ b/bumpreqs/cli.py @@ -0,0 +1,77 @@ +import sys +from pathlib import Path + +from typing import List + +import click + +import requests + +from moreorless.click import echo_color_unified_diff +from packaging.requirements import Requirement +from packaging.specifiers import SpecifierSet +from packaging.utils import canonicalize_name +from packaging.version import parse as parse_version, Version + + +def fix(text: str) -> str: + new_lines = [] + for line in text.splitlines(True): + (value, _, comment) = line.strip().partition("#") + right_whitespace = value[len(value.rstrip()) :] + value = value.rstrip() + + req = Requirement(value) + if req.url: + # Skip git, etc + new_lines.append(line) + continue + + if "==" not in line and any(x in line for x in "<>=~"): + # Skip non-concrete deps in the hackiest way possible + new_lines.append(line) + continue + + obj = requests.get(f"https://pypi.org/pypi/{req.name}/json").json() + releases = [parse_version(v) for v in obj["releases"].keys()] + + # TODO this ought to use the install_requires from the project if easily + # accessible, which would also give a hint on whether pre are allowed. + # Right now this will downgrade any pre + latest_version = max([v for v in releases if not v.is_prerelease]) + + new_specifier = SpecifierSet(f"=={latest_version}") + if req.specifier != new_specifier: + # print(f"Bump {req.name} from {req.specifier} to {new_specifier}") + pass + + req.specifier = new_specifier + + new_line = str(req) + if comment: + new_line += right_whitespace + "#" + comment + new_lines.append(new_line + "\n") # Not sorry + + return "".join(new_lines) + + +@click.command() +@click.option("--diff", is_flag=True, default=True) +@click.option("--write", is_flag=True) +@click.argument("filenames", nargs=-1) +def main(diff: bool, write: bool, filenames: List[str]) -> None: + if not filenames: + click.echo("Provide filenames") + return + + for f in filenames: + old_text = Path(f).read_text() + new_text = fix(old_text) + if diff: + echo_color_unified_diff(old_text, new_text, f) + if write: + Path(f).write_text(new_text) + + +if __name__ == "__main__": + main() diff --git a/requirements-dev.txt b/requirements-dev.txt index 48fae80..522bcfa 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,7 @@ black==22.3.0 coverage==6.4.1 flake8==4.0.1 -mypy==0.960 +mypy==0.961 tox==3.25.0 twine==4.0.1 ufmt==1.3.3 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..75e3f5c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +click==8.1.3 +requests==2.27.1 +packaging==21.3 +moreorless==0.4.0 diff --git a/setup.cfg b/setup.cfg index 9cc2437..38048d9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,6 +14,11 @@ setup_requires = setuptools_scm setuptools >= 38.3.0 python_requires = >=3.6 +install_requires = + requests + packaging + click + moreorless [check] metadata = true diff --git a/setup.py b/setup.py index 460aabe..d5d43d7 100644 --- a/setup.py +++ b/setup.py @@ -1,2 +1,3 @@ from setuptools import setup + setup(use_scm_version=True) From 2814e60da5b7c7849e76267e54d4b2d395ff914b Mon Sep 17 00:00:00 2001 From: Tim Hatch Date: Sat, 6 Aug 2022 12:00:15 -0700 Subject: [PATCH 02/12] Move impl to core.py, add tests --- Makefile | 2 +- bumpreqs/__main__.py | 4 ++ bumpreqs/cli.py | 59 +++----------------- bumpreqs/core.py | 89 +++++++++++++++++++++++++++++ bumpreqs/tests/__init__.py | 6 ++ bumpreqs/tests/__main__.py | 6 ++ bumpreqs/tests/core.py | 111 +++++++++++++++++++++++++++++++++++++ setup.cfg | 6 +- 8 files changed, 230 insertions(+), 53 deletions(-) create mode 100644 bumpreqs/__main__.py create mode 100644 bumpreqs/core.py create mode 100644 bumpreqs/tests/__init__.py create mode 100644 bumpreqs/tests/__main__.py create mode 100644 bumpreqs/tests/core.py diff --git a/Makefile b/Makefile index 11e7aa6..a6fe7f1 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ format: lint: python -m ufmt check $(SOURCES) python -m flake8 $(SOURCES) - mypy --strict bumpreqs + mypy --strict --install-types --non-interactive -p bumpreqs .PHONY: release release: diff --git a/bumpreqs/__main__.py b/bumpreqs/__main__.py new file mode 100644 index 0000000..4aa96ef --- /dev/null +++ b/bumpreqs/__main__.py @@ -0,0 +1,4 @@ +from .cli import main + +if __name__ == "__main__": + main(prog_name="bumpreqs") diff --git a/bumpreqs/cli.py b/bumpreqs/cli.py index 9f696cd..dca67d0 100644 --- a/bumpreqs/cli.py +++ b/bumpreqs/cli.py @@ -1,70 +1,27 @@ -import sys from pathlib import Path -from typing import List +from typing import List, Optional import click - -import requests - from moreorless.click import echo_color_unified_diff -from packaging.requirements import Requirement -from packaging.specifiers import SpecifierSet -from packaging.utils import canonicalize_name -from packaging.version import parse as parse_version, Version - - -def fix(text: str) -> str: - new_lines = [] - for line in text.splitlines(True): - (value, _, comment) = line.strip().partition("#") - right_whitespace = value[len(value.rstrip()) :] - value = value.rstrip() - - req = Requirement(value) - if req.url: - # Skip git, etc - new_lines.append(line) - continue - - if "==" not in line and any(x in line for x in "<>=~"): - # Skip non-concrete deps in the hackiest way possible - new_lines.append(line) - continue - obj = requests.get(f"https://pypi.org/pypi/{req.name}/json").json() - releases = [parse_version(v) for v in obj["releases"].keys()] - - # TODO this ought to use the install_requires from the project if easily - # accessible, which would also give a hint on whether pre are allowed. - # Right now this will downgrade any pre - latest_version = max([v for v in releases if not v.is_prerelease]) - - new_specifier = SpecifierSet(f"=={latest_version}") - if req.specifier != new_specifier: - # print(f"Bump {req.name} from {req.specifier} to {new_specifier}") - pass - - req.specifier = new_specifier - - new_line = str(req) - if comment: - new_line += right_whitespace + "#" + comment - new_lines.append(new_line + "\n") # Not sorry - - return "".join(new_lines) +from .core import fix @click.command() -@click.option("--diff", is_flag=True, default=True) +@click.option("--diff", is_flag=True, default=None) @click.option("--write", is_flag=True) @click.argument("filenames", nargs=-1) -def main(diff: bool, write: bool, filenames: List[str]) -> None: +def main(diff: Optional[bool], write: bool, filenames: List[str]) -> None: if not filenames: click.echo("Provide filenames") return + if diff is None and not write: + diff = True + for f in filenames: + print(f) old_text = Path(f).read_text() new_text = fix(old_text) if diff: diff --git a/bumpreqs/core.py b/bumpreqs/core.py new file mode 100644 index 0000000..e8cbe8e --- /dev/null +++ b/bumpreqs/core.py @@ -0,0 +1,89 @@ +import logging + +from typing import List, Optional, Union + +import requests + +from packaging.requirements import Requirement +from packaging.specifiers import SpecifierSet +from packaging.version import LegacyVersion, parse as parse_version, Version + +LOG = logging.getLogger(__name__) + + +def fix(text: str, force: Optional[bool] = False) -> str: + new_lines = [] + for line in text.splitlines(True): + # This is an overly simplistic parser for requirements files, see + # pip/req/req_file.py for the real one. + (value, _, comment) = line.strip().partition("#") + right_whitespace = value[len(value.rstrip()) :] + + # See COMMENT_RE in pip/req/req_file.py + if value and comment and not right_whitespace: + value = line.strip() + comment = "" + else: + value = value.rstrip() + + if not value: + new_lines.append(line) + continue + + if value.startswith("-") or "://" in value: + # Skip git, etc + LOG.warning("Not bumping option/url line for %r", value) + new_lines.append(line) + continue + + req = Requirement(value) + assert not req.url + + # Only operate on `project` and `project==ver` for now. + # Skip non-concrete specifiers in the hackiest way possible. + if "==" not in line and any(x in line for x in "<>=~") and not force: + new_lines.append(line) + continue + + try: + releases = _fetch_versions(req.name) + except Exception as e: + LOG.warning("Failed to fetch versions for %r: %s", req.name, repr(e)) + new_lines.append(line) + continue + + # TODO this ought to use the install_requires from the project if easily + # accessible, which would also give a hint on whether pre are allowed. + # For now we just get the pre- intent from the existing pin + if req.specifier.prereleases: + candidates = releases + else: + candidates = [v for v in releases if not v.is_prerelease] + + if not candidates: + LOG.warning("No candidate versions for %r", value) + new_lines.append(line) + continue + + latest_version = max(candidates) + + new_specifier = SpecifierSet(f"=={latest_version}") + if req.specifier != new_specifier: + # print(f"Bump {req.name} from {req.specifier} to {new_specifier}") + pass + + req.specifier = new_specifier + + new_line = str(req) + if comment: + new_line += right_whitespace + "#" + comment + new_lines.append(new_line + "\n") # Not sorry + + return "".join(new_lines) + + +def _fetch_versions(project_name: str) -> List[Union[LegacyVersion, Version]]: + resp = requests.get(f"https://pypi.org/pypi/{project_name}/json") + resp.raise_for_status() + obj = resp.json() + return [parse_version(v) for v in obj["releases"].keys()] diff --git a/bumpreqs/tests/__init__.py b/bumpreqs/tests/__init__.py new file mode 100644 index 0000000..801de82 --- /dev/null +++ b/bumpreqs/tests/__init__.py @@ -0,0 +1,6 @@ +from .core import FetchVersionsTest, FixTest + +__all__ = [ + "FixTest", + "FetchVersionsTest", +] diff --git a/bumpreqs/tests/__main__.py b/bumpreqs/tests/__main__.py new file mode 100644 index 0000000..7385cd0 --- /dev/null +++ b/bumpreqs/tests/__main__.py @@ -0,0 +1,6 @@ +import unittest + +from bumpreqs.tests import * # noqa: F401,F403 + +if __name__ == "__main__": + unittest.main() diff --git a/bumpreqs/tests/core.py b/bumpreqs/tests/core.py new file mode 100644 index 0000000..41a5eb8 --- /dev/null +++ b/bumpreqs/tests/core.py @@ -0,0 +1,111 @@ +import unittest +from typing import Any, Dict +from unittest.mock import patch + +from packaging.version import Version + +from ..core import _fetch_versions, fix + +VERSIONS = ["1.0", "1.2", "1.2.3"] +FAKE_PROJECT_FOO_METADATA = { + "releases": dict.fromkeys(VERSIONS), +} + +PROJECTS = { + "foo": [Version(x) for x in VERSIONS], + "foup": [Version(x) for x in [*VERSIONS, "1.2.4a0", "1.2.4a1"]], + "empty": [], +} + + +class FakeResponse: + def __init__(self, status: int, metadata: Dict[Any, Any]) -> None: + self._status = status + self._json = metadata + + def raise_for_status(self) -> None: + if self._status != 200: + raise Exception(f"Status {self._status}") + + def json(self) -> Dict[Any, Any]: + return self._json + + +class FixTest(unittest.TestCase): + @patch("bumpreqs.core._fetch_versions", side_effect=PROJECTS.get) + def test_basic(self, fetch_versions_mock: Any) -> None: + # leave comments alone + self.assertEqual(" # comment", fix(" # comment")) + self.assertEqual(" # comment\n", fix(" # comment\n")) + + # fixes missing newline + self.assertEqual("foo==1.2.3\n", fix("foo==0.9")) + self.assertEqual("foo==1.2.3\n", fix("foo==0.9\n")) + self.assertEqual("foo==1.2.3\n", fix("foo\n")) + + # leaves alone >= unless force + self.assertEqual("foo>=0.9\n", fix("foo>=0.9\n")) + self.assertEqual("foo==1.2.3\n", fix("foo>=0.9\n", force=True)) + + # preserves comment and exact whitespace + self.assertEqual( + "foo==1.2.3 # com ment\n", fix("foo==1.2.2 # com ment\n") + ) + + # preserves conditionals + self.assertEqual( + 'foo==1.2.3; python_version >= "3.6"\n', + fix('foo==1.2.2 ; python_version >= "3.6"\n'), + ) + + @patch("bumpreqs.core._fetch_versions", side_effect=PROJECTS.get) + @patch("bumpreqs.core.LOG.warning") + def test_git_version(self, warning_mock: Any, fetch_versions_mock: Any) -> None: + url = "-e git+git://...#egg_info" + self.assertEqual(url, fix(url)) + warning_mock.assert_called_with( + "Not bumping option/url line for %r", "-e git+git://...#egg_info" + ) + + @patch("bumpreqs.core._fetch_versions", side_effect=PROJECTS.get) + def test_pre_handling(self, fetch_versions_mock: Any) -> None: + self.assertEqual("foup==1.2.4a1\n", fix("foup==1.2.4a0")) + self.assertEqual("foup==1.2.4a1\n", fix("foup==1.2.4a1")) + # current version doesn't need to exist + self.assertEqual("foup==1.2.4a1\n", fix("foup==1.2.4a9")) + # same but without pre- intent + self.assertEqual("foup==1.2.3\n", fix("foup==1.2.5")) + # force with pre- intent + self.assertEqual("foup==1.2.4a1\n", fix("foup>=1.0a1", force=True)) + + @patch("bumpreqs.core._fetch_versions", side_effect=PROJECTS.__getitem__) + @patch("bumpreqs.core.LOG.warning") + def test_fetch_messages(self, warning_mock: Any, fetch_versions_mock: Any) -> None: + self.assertEqual("nope==1.0", fix("nope==1.0")) + warning_mock.assert_called_with( + "Failed to fetch versions for %r: %s", "nope", "KeyError('nope')" + ) + + @patch("bumpreqs.core._fetch_versions", side_effect=PROJECTS.__getitem__) + @patch("bumpreqs.core.LOG.warning") + def test_no_releases(self, warning_mock: Any, fetch_versions_mock: Any) -> None: + self.assertEqual("empty==1.0", fix("empty==1.0")) + warning_mock.assert_called_with("No candidate versions for %r", "empty==1.0") + + @patch("bumpreqs.core._fetch_versions", side_effect=PROJECTS.__getitem__) + @patch("bumpreqs.core.LOG.warning") + def test_url(self, warning_mock: Any, fetch_versions_mock: Any) -> None: + self.assertEqual( + "empty @ https://example.com/", fix("empty @ https://example.com/") + ) + warning_mock.assert_called_with( + "Not bumping option/url line for %r", "empty @ https://example.com/" + ) + + +class FetchVersionsTest(unittest.TestCase): + @patch("bumpreqs.core.requests.get") + def test_success(self, get_mock: Any) -> None: + get_mock.return_value = FakeResponse(200, FAKE_PROJECT_FOO_METADATA) + self.assertEqual([Version(v) for v in VERSIONS], _fetch_versions("foo")) + get_mock.assert_called_with("https://pypi.org/pypi/foo/json") diff --git a/setup.cfg b/setup.cfg index 38048d9..ccba666 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,6 +20,10 @@ install_requires = click moreorless +[options.entry_points] +console_scripts = + bumpreqs = bumpreqs.cli:main + [check] metadata = true strict = true @@ -30,7 +34,7 @@ include = bumpreqs/* omit = bumpreqs/tests/* [coverage:report] -fail_under = 70 +fail_under = 99 precision = 1 show_missing = True skip_covered = True From 9d09c81ce9eef106def9dd2464d00413830f3880 Mon Sep 17 00:00:00 2001 From: Tim Hatch Date: Sat, 6 Aug 2022 12:08:01 -0700 Subject: [PATCH 03/12] Run bumpreqs on itself --- requirements-dev.txt | 12 ++++++------ requirements.txt | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 522bcfa..745e207 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,10 +1,10 @@ -black==22.3.0 -coverage==6.4.1 -flake8==4.0.1 -mypy==0.961 -tox==3.25.0 +black==22.6.0 +coverage==6.4.2 +flake8==5.0.4 +mypy==0.971 +tox==3.25.1 twine==4.0.1 ufmt==1.3.3 -usort==1.0.2 +usort==1.0.4 volatile==2.1.0 wheel==0.37.1 diff --git a/requirements.txt b/requirements.txt index 75e3f5c..39d8b6d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ click==8.1.3 -requests==2.27.1 +requests==2.28.1 packaging==21.3 moreorless==0.4.0 From a938d159f1b4aea8a6a3b556b3d75036e1f91beb Mon Sep 17 00:00:00 2001 From: Tim Hatch Date: Sat, 6 Aug 2022 12:30:01 -0700 Subject: [PATCH 04/12] Drop 3.6 support The latest versions of setuptools, setuptools-scm all don't work with it, so testing becomes more difficult. (This project likely still works fine, but I don't want the hassle of testing.) --- .github/workflows/build.yml | 3 +-- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1f57718..c22a287 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] os: [macOS-latest, ubuntu-latest, windows-latest] steps: @@ -34,4 +34,3 @@ jobs: run: make test - name: Lint run: make lint - if: ${{ matrix.python-version != '3.9' }} diff --git a/setup.cfg b/setup.cfg index ccba666..59183de 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,7 @@ packages = bumpreqs setup_requires = setuptools_scm setuptools >= 38.3.0 -python_requires = >=3.6 +python_requires = >=3.7 install_requires = requests packaging From 9a5ab68cabf604d2f2e7027ad741ad98c1bcf06b Mon Sep 17 00:00:00 2001 From: Tim Hatch Date: Sat, 6 Aug 2022 12:32:37 -0700 Subject: [PATCH 05/12] Use 3.11 beta --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c22a287..6d3ebb4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11.0-beta.5"] os: [macOS-latest, ubuntu-latest, windows-latest] steps: From 66911a52f240b8d22b07a3fad3621bd7951b983a Mon Sep 17 00:00:00 2001 From: Tim Hatch Date: Sat, 6 Aug 2022 12:47:57 -0700 Subject: [PATCH 06/12] Add basic readme --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 042649e..437d058 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,16 @@ # bumpreqs +Usage + +```shell-session +$ pip install bumpreqs +$ python -m bumpreqs --write requirements*.txt +``` + +It will update the requirements to all use the latest versions. It doesn't care +about `requires_python` or really any other constraints. Similar in that +respect to dependabot or pyup, although it won't update to a pre unless it's +already on a pre. # License From 078baab1477963eab139d8160d9fefb0aa948d6d Mon Sep 17 00:00:00 2001 From: Tim Hatch Date: Mon, 5 Sep 2022 11:10:16 -0700 Subject: [PATCH 07/12] Somewhat obey python_version environment marker There is much work left to do, but for simple cases this does a lot better now. Fixes #1 --- README.md | 19 +++- bumpreqs/core.py | 33 +++++- bumpreqs/marker_extract.py | 57 ++++++++++ bumpreqs/tests/__init__.py | 4 + bumpreqs/tests/core.py | 70 ++++++++++-- bumpreqs/tests/marker_extract.py | 55 +++++++++ bumpreqs/tests/vrange.py | 72 ++++++++++++ bumpreqs/vrange.py | 184 +++++++++++++++++++++++++++++++ setup.cfg | 2 +- 9 files changed, 480 insertions(+), 16 deletions(-) create mode 100644 bumpreqs/marker_extract.py create mode 100644 bumpreqs/tests/marker_extract.py create mode 100644 bumpreqs/tests/vrange.py create mode 100644 bumpreqs/vrange.py diff --git a/README.md b/README.md index 437d058..6520c79 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,21 @@ $ pip install bumpreqs $ python -m bumpreqs --write requirements*.txt ``` -It will update the requirements to all use the latest versions. It doesn't care -about `requires_python` or really any other constraints. Similar in that -respect to dependabot or pyup, although it won't update to a pre unless it's -already on a pre. +It will update the requirements to all use the latest versions. Most +environment markers are ignored (although preserved on modified lines). + + +## `python_version` and `full_python_version` + +There is some rudimentary support that works for simple comparisons using the +`and` operator (as in `requires_python` using `,`). If there is _any_ overlap +between the constraint and release's `requires_python`, then it can be bumped. + +The reason to check for _any_ overlap rather than _full_ overlap is because many +project specify a constraint like `python_version < "3.7"` without a lower +bound, and when checking against a release that has `Requires-Python: >=3.6` the +check would fail because `3.5` (or `2.1` or any other silly example) is not +included. See currently open issue for future work. # License diff --git a/bumpreqs/core.py b/bumpreqs/core.py index e8cbe8e..235df52 100644 --- a/bumpreqs/core.py +++ b/bumpreqs/core.py @@ -8,6 +8,10 @@ from packaging.specifiers import SpecifierSet from packaging.version import LegacyVersion, parse as parse_version, Version +from .marker_extract import extract_python + +from .vrange import TooComplicated, VersionIntervals + LOG = logging.getLogger(__name__) @@ -39,6 +43,13 @@ def fix(text: str, force: Optional[bool] = False) -> str: req = Requirement(value) assert not req.url + try: + only_on_python = extract_python(req.marker) + except TooComplicated: + LOG.warning("Python version comparison too complex for %r", value) + new_lines.append(line) + continue + # Only operate on `project` and `project==ver` for now. # Skip non-concrete specifiers in the hackiest way possible. if "==" not in line and any(x in line for x in "<>=~") and not force: @@ -46,7 +57,7 @@ def fix(text: str, force: Optional[bool] = False) -> str: continue try: - releases = _fetch_versions(req.name) + releases = _fetch_versions(req.name, only_on_python) except Exception as e: LOG.warning("Failed to fetch versions for %r: %s", req.name, repr(e)) new_lines.append(line) @@ -82,8 +93,24 @@ def fix(text: str, force: Optional[bool] = False) -> str: return "".join(new_lines) -def _fetch_versions(project_name: str) -> List[Union[LegacyVersion, Version]]: +def _fetch_versions( + project_name: str, + only_for_python: Optional[VersionIntervals] = None, +) -> List[Union[LegacyVersion, Version]]: resp = requests.get(f"https://pypi.org/pypi/{project_name}/json") resp.raise_for_status() obj = resp.json() - return [parse_version(v) for v in obj["releases"].keys()] + + versions: List[Union[LegacyVersion, Version]] = [] + for k, v in obj["releases"].items(): + # Skip older releases that have no archives + if not v: + continue + requires_python = v[0].get("requires_python") + if requires_python and only_for_python: + if only_for_python.intersect(VersionIntervals.from_str(requires_python)): + versions.append(parse_version(k)) + else: + versions.append(parse_version(k)) + + return versions diff --git a/bumpreqs/marker_extract.py b/bumpreqs/marker_extract.py new file mode 100644 index 0000000..7ca35ef --- /dev/null +++ b/bumpreqs/marker_extract.py @@ -0,0 +1,57 @@ +from typing import Any, Iterator, Optional, Tuple + +from packaging.markers import Marker, Variable + +from .vrange import TooComplicated, VersionIntervals + +# This does not include all operators, notably the string comparisons "in", "not +# in", and "~=" have no way to flip. +CONVERSE_MAP = { + "===": "===", + "==": "==", + "!=": "!=", + "<": ">=", + "<=": ">", + ">": "<=", + ">=": "<", +} + +# This is largely patterend after packaging/markers.py:_evaluate_markers +# The tuple is e.g. ("python_version", ">=", "3.0") and the version will always +# be the last item. +def _extract_python_internal(markers: Any) -> Iterator[Tuple[str, str, str]]: + for marker in markers: + if isinstance(marker, list): + yield from _extract_python_internal(marker) + elif isinstance(marker, tuple): + lhs, op, rhs = marker + + # packaging appears to have an additional restriction not mentioned + # in the grammar, that you can't compare two Variables or two + # literals. Duplicate that here. + if isinstance(lhs, Variable): + if lhs.value in ("python_version", "python_full_version"): + yield (lhs.value, op.value, rhs.value) + else: + if rhs.value in ("python_version", "python_full_version"): + if op.value in CONVERSE_MAP: + yield (rhs.value, CONVERSE_MAP[op.value], lhs.value) + else: + raise TooComplicated + # TODO: else 'and' 'or' + + +def extract_python(markers: Optional[Marker]) -> Optional[VersionIntervals]: + allow_all = VersionIntervals() + vi = VersionIntervals() + + if markers is None: + return None + + for (var, op, value) in _extract_python_internal(markers._markers): + vi = vi.intersect(VersionIntervals.from_str(f"{op}{value}")) + + if vi == allow_all: + return None + + return vi diff --git a/bumpreqs/tests/__init__.py b/bumpreqs/tests/__init__.py index 801de82..191fe8f 100644 --- a/bumpreqs/tests/__init__.py +++ b/bumpreqs/tests/__init__.py @@ -1,6 +1,10 @@ from .core import FetchVersionsTest, FixTest +from .marker_extract import MarkerExtractTest +from .vrange import VersionIntervalsTest __all__ = [ "FixTest", "FetchVersionsTest", + "VersionIntervalsTest", + "MarkerExtractTest", ] diff --git a/bumpreqs/tests/core.py b/bumpreqs/tests/core.py index 41a5eb8..c478c95 100644 --- a/bumpreqs/tests/core.py +++ b/bumpreqs/tests/core.py @@ -5,15 +5,26 @@ from packaging.version import Version from ..core import _fetch_versions, fix +from ..vrange import VersionIntervals -VERSIONS = ["1.0", "1.2", "1.2.3"] +VERSIONS = [("1.0", None), ("1.2", None), ("1.2.3", ">=3.8")] +PRE_VERSIONS = [("1.2.4a0", ">=3.8"), ("1.2.4a1", ">=3.8")] FAKE_PROJECT_FOO_METADATA = { - "releases": dict.fromkeys(VERSIONS), + "releases": { + str(k): [ + { + "filename": f"foo-{k}.tgz", + "packagetype": "sdist", + "requires_python": rp, + } + ] + for k, rp in VERSIONS + }, } PROJECTS = { - "foo": [Version(x) for x in VERSIONS], - "foup": [Version(x) for x in [*VERSIONS, "1.2.4a0", "1.2.4a1"]], + "foo": [Version(x) for x, y in VERSIONS], + "foup": [Version(x) for x, y in [*VERSIONS, *PRE_VERSIONS]], "empty": [], } @@ -78,7 +89,7 @@ def test_pre_handling(self, fetch_versions_mock: Any) -> None: # force with pre- intent self.assertEqual("foup==1.2.4a1\n", fix("foup>=1.0a1", force=True)) - @patch("bumpreqs.core._fetch_versions", side_effect=PROJECTS.__getitem__) + @patch("bumpreqs.core._fetch_versions", side_effect=lambda x, v=None: PROJECTS[x]) @patch("bumpreqs.core.LOG.warning") def test_fetch_messages(self, warning_mock: Any, fetch_versions_mock: Any) -> None: self.assertEqual("nope==1.0", fix("nope==1.0")) @@ -86,13 +97,13 @@ def test_fetch_messages(self, warning_mock: Any, fetch_versions_mock: Any) -> No "Failed to fetch versions for %r: %s", "nope", "KeyError('nope')" ) - @patch("bumpreqs.core._fetch_versions", side_effect=PROJECTS.__getitem__) + @patch("bumpreqs.core._fetch_versions", side_effect=lambda x, v=None: PROJECTS[x]) @patch("bumpreqs.core.LOG.warning") def test_no_releases(self, warning_mock: Any, fetch_versions_mock: Any) -> None: self.assertEqual("empty==1.0", fix("empty==1.0")) warning_mock.assert_called_with("No candidate versions for %r", "empty==1.0") - @patch("bumpreqs.core._fetch_versions", side_effect=PROJECTS.__getitem__) + @patch("bumpreqs.core._fetch_versions", side_effect=lambda x, v=None: PROJECTS[x]) @patch("bumpreqs.core.LOG.warning") def test_url(self, warning_mock: Any, fetch_versions_mock: Any) -> None: self.assertEqual( @@ -102,10 +113,53 @@ def test_url(self, warning_mock: Any, fetch_versions_mock: Any) -> None: "Not bumping option/url line for %r", "empty @ https://example.com/" ) + @patch("bumpreqs.core._fetch_versions", side_effect=lambda x, v=None: PROJECTS[x]) + @patch("bumpreqs.core.LOG.warning") + def test_old_python(self, warning_mock: Any, fetch_versions_mock: Any) -> None: + # The mock doesn't know about py version + self.assertEqual( + 'foo==1.2.3; python_version < "3.6"\n', + fix("foo==1.0; python_version<'3.6'"), + ) + fetch_versions_mock.assert_called_with("foo", VersionIntervals.from_str("<3.6")) + + @patch("bumpreqs.core.LOG.warning") + def test_too_complicated(self, warning_mock: Any) -> None: + self.assertEqual( + "foo==1.0; python_version<='3.6'", + fix("foo==1.0; python_version<='3.6'"), + ) + warning_mock.assert_called_with( + "Python version comparison too complex for %r", + "foo==1.0; python_version<='3.6'", + ) + class FetchVersionsTest(unittest.TestCase): @patch("bumpreqs.core.requests.get") def test_success(self, get_mock: Any) -> None: get_mock.return_value = FakeResponse(200, FAKE_PROJECT_FOO_METADATA) - self.assertEqual([Version(v) for v in VERSIONS], _fetch_versions("foo")) + self.assertEqual([Version(x) for x, y in VERSIONS], _fetch_versions("foo")) + get_mock.assert_called_with("https://pypi.org/pypi/foo/json") + + @patch("bumpreqs.core.requests.get") + def test_recent_version(self, get_mock: Any) -> None: + get_mock.return_value = FakeResponse(200, FAKE_PROJECT_FOO_METADATA) + self.assertEqual( + [Version(x) for x, y in VERSIONS], + _fetch_versions("foo", VersionIntervals.from_str("<3.9")), + ) + self.assertEqual( + [Version(x) for x, y in VERSIONS], + _fetch_versions("foo", VersionIntervals.from_str(">=3.6")), + ) + get_mock.assert_called_with("https://pypi.org/pypi/foo/json") + + @patch("bumpreqs.core.requests.get") + def test_older_version(self, get_mock: Any) -> None: + get_mock.return_value = FakeResponse(200, FAKE_PROJECT_FOO_METADATA) + self.assertEqual( + [Version("1.0"), Version("1.2")], + _fetch_versions("foo", VersionIntervals.from_str("<3.8")), + ) get_mock.assert_called_with("https://pypi.org/pypi/foo/json") diff --git a/bumpreqs/tests/marker_extract.py b/bumpreqs/tests/marker_extract.py new file mode 100644 index 0000000..96191b2 --- /dev/null +++ b/bumpreqs/tests/marker_extract.py @@ -0,0 +1,55 @@ +import unittest + +from packaging.markers import Marker + +from ..marker_extract import _extract_python_internal, extract_python +from ..vrange import TooComplicated + + +class MarkerExtractTest(unittest.TestCase): + def test_simple_behavior(self) -> None: + m = Marker( + "(python_version >= '3.3' and python_version < '4') and sys_platform=='linux'" + ) + self.assertEqual( + [ + ("python_version", ">=", "3.3"), + ("python_version", "<", "4"), + ], + list(_extract_python_internal(m._markers)), + ) + + def test_or_behavior_unfortunate(self) -> None: + # TODO: This isn't right, but it's what we do currently. Only about 2% + # of projects that have version-dependent deps use 'or' and we can't see + # this in requires_python on the target project -- it would be on the + # project you're currently running against. + m = Marker("python_version >= '3.3' or python_version < '4'") + self.assertEqual( + [ + ("python_version", ">=", "3.3"), + ("python_version", "<", "4"), + ], + list(_extract_python_internal(m._markers)), + ) + + def test_rhs_variable_flip(self) -> None: + m = Marker("'3.3' >= python_version") + self.assertEqual( + [ + ("python_version", "<", "3.3"), + ], + list(_extract_python_internal(m._markers)), + ) + + def test_rhs_variable_flip_toocomplicated(self) -> None: + m = Marker("'3.3' in python_version") + with self.assertRaises(TooComplicated): + list(_extract_python_internal(m._markers)) + + def test_extract_python(self) -> None: + m = Marker("sys_platform == 'linux'") + self.assertEqual(None, extract_python(m)) + + m = Marker("python_version >= '3.3' and python_version < '4'") + self.assertEqual(">=3.3,<4", str(extract_python(m))) diff --git a/bumpreqs/tests/vrange.py b/bumpreqs/tests/vrange.py new file mode 100644 index 0000000..7e68a75 --- /dev/null +++ b/bumpreqs/tests/vrange.py @@ -0,0 +1,72 @@ +import unittest + +from packaging.specifiers import Specifier, SpecifierSet + +from ..vrange import TooComplicated, VersionIntervals + + +class VersionIntervalsTest(unittest.TestCase): + def test_init(self) -> None: + self.assertEqual("", str(VersionIntervals())) + + self.assertEqual(">=2", str(VersionIntervals.from_specifier(Specifier(">=2")))) + self.assertEqual("<9", str(VersionIntervals.from_specifier(Specifier("<9")))) + + self.assertEqual( + ">=2,<9", str(VersionIntervals.from_specifier_set(SpecifierSet(">=2,<9"))) + ) + + self.assertEqual(">=2.0,<3", str(VersionIntervals.from_str("==2.*"))) + self.assertEqual(">=2.1.0,<2.2", str(VersionIntervals.from_str("==2.1.*"))) + self.assertEqual(">=2.0.0,<2.0.1", str(VersionIntervals.from_str("==2.0.0"))) + + # TODO somewhere might need to care about this precision... + self.assertEqual(">=2.0.0,<2.0.1", str(VersionIntervals.from_str("==2"))) + self.assertEqual(">=2.0.0,<2.0.1", str(VersionIntervals.from_str("==2.0"))) + + self.assertEqual( + ">=2.6,<3.0.0,>=3.3,<4", + str(VersionIntervals.from_str(">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4")), + ) + + def test_complex(self) -> None: + with self.assertRaises(TooComplicated): + VersionIntervals.from_str("<=9") + + def test_intersect_interval(self) -> None: + a = VersionIntervals.from_str(">=3.7") + self.assertEqual( + ">=3.7,<4", + str(a.intersect(VersionIntervals.from_str("<4"))), + ) + + def test_intersect_no_change(self) -> None: + a = VersionIntervals.from_str(">=3.7") + b = VersionIntervals.from_str(">=3.1") + c = a.intersect(b) + self.assertEqual(">=3.7", str(c)) + self.assertTrue(c) + + def test_intersect_impossible(self) -> None: + a = VersionIntervals.from_str(">=3.7") + b = VersionIntervals.from_str("<3.7") + c = a.intersect(b) + self.assertEqual("NONE", str(c)) + self.assertFalse(c) + + def test_simplify(self) -> None: + a = VersionIntervals.from_str("<3.7") + b = VersionIntervals.from_str(">=3.7,<4") + c = a.union(b) + self.assertEqual("<4", str(c)) + self.assertTrue(c) + + def test_eq(self) -> None: + a = VersionIntervals.from_str("<3.7") + b = VersionIntervals.from_str(">=3.7,<4") + self.assertFalse(a == b) + self.assertTrue(a == a) + + def test_not_equal_no_star(self) -> None: + with self.assertRaises(TooComplicated): + VersionIntervals.from_str("!=1.2.3") diff --git a/bumpreqs/vrange.py b/bumpreqs/vrange.py new file mode 100644 index 0000000..57adc48 --- /dev/null +++ b/bumpreqs/vrange.py @@ -0,0 +1,184 @@ +from __future__ import annotations + +from typing import List, Optional, Tuple + +from packaging.specifiers import Specifier, SpecifierSet +from packaging.version import Version + +IN = 1 +OUT = -1 + +SELF = "SELF" +OTHER = "OTHER" + +MIN = Version("0") +MAX = Version("999!1") + +Event = Tuple[Version, int] +EventList = List[Event] + + +class TooComplicated(Exception): + pass + + +class VersionIntervals: + """ + Envision version constraints as intervals on a line. + """ + + def __init__(self, events: Optional[EventList] = None) -> None: + if events is not None: + self._events = events[:] + else: + self._events = [ + (MIN, IN), + (MAX, OUT), + ] + + @classmethod + def from_specifier(cls, specifier: Specifier) -> VersionIntervals: + t = cls() + t.add_specifier(specifier) + return t + + @classmethod + def from_specifier_set(cls, specifier_set: SpecifierSet) -> VersionIntervals: + t = cls() + for spec in specifier_set._specs: + if not isinstance(spec, Specifier): # pragma: no cover + raise ValueError(f"Unknown specifier type {type(spec)}") + t.add_specifier(spec) + return t + + @classmethod + def from_str(cls, s: str) -> VersionIntervals: + t = cls.from_specifier_set(SpecifierSet(s)) + return t + + def add_specifier(self, specifier: Specifier) -> VersionIntervals: + tmp: EventList + if specifier.operator == "<": + tmp = [ + (MIN, IN), + (Version(specifier.version), OUT), + ] + elif specifier.operator == ">=": + tmp = [ + (Version(specifier.version), IN), + (MAX, OUT), + ] + elif specifier.operator == "==": + parts = specifier.version.split(".") + # TODO: This is a little inexact + try: + idx = parts.index("*") + next_parts = parts[: idx - 1] + [str(int(parts[idx - 1]) + 1)] + tmp = [ + (Version(specifier.version.replace("*", "0")), IN), + (Version(".".join(next_parts)), OUT), + ] + except ValueError: + while len(parts) < 3: + parts.append("0") + + next_parts = parts[:-1] + [str(int(parts[-1]) + 1)] + tmp = [ + (Version(".".join(parts)), IN), + (Version(".".join(next_parts)), OUT), + ] + elif specifier.operator == "!=": + parts = specifier.version.split(".") + try: + idx = parts.index("*") + next_parts = parts[: idx - 1] + [str(int(parts[idx - 1]) + 1)] + tmp = [ + (MIN, IN), + (Version(specifier.version.replace("*", "0")), OUT), + (Version(".".join(next_parts)), IN), + (MAX, OUT), + ] + except ValueError: + # TODO + raise TooComplicated + + else: + raise TooComplicated(specifier.operator) + + self._events = self._intersect(self._events + tmp) + + return self + + def intersect(self, other: VersionIntervals) -> VersionIntervals: + t = VersionIntervals( + self._intersect(self._events + other._events), + ) + return t + + @classmethod + def _intersect(self, other: EventList) -> EventList: + + new_events: EventList = [] + state = 0 + # Need "OUT" handled before "IN" + for (v, ev) in sorted(other, key=lambda i: (i[0], i[1])): + old_state = state + state += ev + if state == 2: + new_events.append((v, ev)) + elif old_state == 2: + new_events.append((v, ev)) + + assert state == 0 + return new_events + + def union(self, other: VersionIntervals) -> VersionIntervals: + t = VersionIntervals( + self._union(self._events + other._events), + ) + return t + + @classmethod + def _union(self, other: EventList) -> EventList: + + new_events: EventList = [] + state = 0 + # Need "OUT" handled before "IN" + for (v, ev) in sorted(other, key=lambda i: (i[0], i[1])): + old_state = state + state += ev + if state >= 1: + if new_events and new_events[-1] == (v, -ev): + del new_events[-1] + else: + new_events.append((v, ev)) + elif old_state >= 1: + new_events.append((v, ev)) + + assert state == 0 + return new_events + + def __str__(self) -> str: + buf: List[str] = [] + + if self._events == []: + return "NONE" + + for (v, ev) in self._events: + if (ev == IN and v == MIN) or (ev == OUT and v == MAX): + continue + + if ev == IN: + buf.append(f">={v}") + elif ev == OUT: + buf.append(f"<{v}") + else: # pragma: no cover + raise ValueError((v, ev)) + + return ",".join(buf) + + def __eq__(self, other: object) -> bool: + return isinstance(other, VersionIntervals) and self._events == other._events + + def __bool__(self) -> bool: + return bool(self._events) diff --git a/setup.cfg b/setup.cfg index 59183de..1108219 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,7 +34,7 @@ include = bumpreqs/* omit = bumpreqs/tests/* [coverage:report] -fail_under = 99 +fail_under = 98 precision = 1 show_missing = True skip_covered = True From fffbbd3d659863f71ef5ae75949d86b5f56d7b96 Mon Sep 17 00:00:00 2001 From: Tim Hatch Date: Mon, 5 Sep 2022 11:14:44 -0700 Subject: [PATCH 08/12] Bump our own formatter versions --- requirements-dev.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 745e207..f2a349e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,10 +1,10 @@ -black==22.6.0 -coverage==6.4.2 +black==22.8.0 +coverage==6.4.4 flake8==5.0.4 mypy==0.971 tox==3.25.1 twine==4.0.1 -ufmt==1.3.3 +ufmt==2.0.0 usort==1.0.4 volatile==2.1.0 wheel==0.37.1 From 95360399b6b58e2071778cf09a8a5b6a4b3ce1e9 Mon Sep 17 00:00:00 2001 From: Tim Hatch Date: Wed, 6 Dec 2023 16:47:05 -0800 Subject: [PATCH 09/12] Update dependency versions, fix LegacyVersion --- .github/workflows/build.yml | 6 +++--- bumpreqs/core.py | 8 +++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6d3ebb4..a8630ef 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,14 +15,14 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11.0-beta.5"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] os: [macOS-latest, ubuntu-latest, windows-latest] steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v4 - name: Set Up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install diff --git a/bumpreqs/core.py b/bumpreqs/core.py index 235df52..d48b5de 100644 --- a/bumpreqs/core.py +++ b/bumpreqs/core.py @@ -6,7 +6,7 @@ from packaging.requirements import Requirement from packaging.specifiers import SpecifierSet -from packaging.version import LegacyVersion, parse as parse_version, Version +from packaging.version import parse as parse_version, Version from .marker_extract import extract_python @@ -96,12 +96,12 @@ def fix(text: str, force: Optional[bool] = False) -> str: def _fetch_versions( project_name: str, only_for_python: Optional[VersionIntervals] = None, -) -> List[Union[LegacyVersion, Version]]: +) -> List[Version]: resp = requests.get(f"https://pypi.org/pypi/{project_name}/json") resp.raise_for_status() obj = resp.json() - versions: List[Union[LegacyVersion, Version]] = [] + versions: List[Version] = [] for k, v in obj["releases"].items(): # Skip older releases that have no archives if not v: @@ -109,8 +109,10 @@ def _fetch_versions( requires_python = v[0].get("requires_python") if requires_python and only_for_python: if only_for_python.intersect(VersionIntervals.from_str(requires_python)): + # TODO try/except versions.append(parse_version(k)) else: + # TODO try/except versions.append(parse_version(k)) return versions From 98f331a72ef4e59f530bfe5c608806f1e3b6ae86 Mon Sep 17 00:00:00 2001 From: Tim Hatch Date: Wed, 6 Dec 2023 16:47:11 -0800 Subject: [PATCH 10/12] Update requirements.txt, reformat --- bumpreqs/core.py | 2 +- bumpreqs/marker_extract.py | 3 ++- bumpreqs/vrange.py | 8 +++----- requirements-dev.txt | 18 +++++++++--------- requirements.txt | 6 +++--- 5 files changed, 18 insertions(+), 19 deletions(-) diff --git a/bumpreqs/core.py b/bumpreqs/core.py index d48b5de..3cd41c6 100644 --- a/bumpreqs/core.py +++ b/bumpreqs/core.py @@ -1,6 +1,6 @@ import logging -from typing import List, Optional, Union +from typing import List, Optional import requests diff --git a/bumpreqs/marker_extract.py b/bumpreqs/marker_extract.py index 7ca35ef..c650704 100644 --- a/bumpreqs/marker_extract.py +++ b/bumpreqs/marker_extract.py @@ -16,6 +16,7 @@ ">=": "<", } + # This is largely patterend after packaging/markers.py:_evaluate_markers # The tuple is e.g. ("python_version", ">=", "3.0") and the version will always # be the last item. @@ -48,7 +49,7 @@ def extract_python(markers: Optional[Marker]) -> Optional[VersionIntervals]: if markers is None: return None - for (var, op, value) in _extract_python_internal(markers._markers): + for var, op, value in _extract_python_internal(markers._markers): vi = vi.intersect(VersionIntervals.from_str(f"{op}{value}")) if vi == allow_all: diff --git a/bumpreqs/vrange.py b/bumpreqs/vrange.py index 57adc48..e08cb6d 100644 --- a/bumpreqs/vrange.py +++ b/bumpreqs/vrange.py @@ -117,11 +117,10 @@ def intersect(self, other: VersionIntervals) -> VersionIntervals: @classmethod def _intersect(self, other: EventList) -> EventList: - new_events: EventList = [] state = 0 # Need "OUT" handled before "IN" - for (v, ev) in sorted(other, key=lambda i: (i[0], i[1])): + for v, ev in sorted(other, key=lambda i: (i[0], i[1])): old_state = state state += ev if state == 2: @@ -140,11 +139,10 @@ def union(self, other: VersionIntervals) -> VersionIntervals: @classmethod def _union(self, other: EventList) -> EventList: - new_events: EventList = [] state = 0 # Need "OUT" handled before "IN" - for (v, ev) in sorted(other, key=lambda i: (i[0], i[1])): + for v, ev in sorted(other, key=lambda i: (i[0], i[1])): old_state = state state += ev if state >= 1: @@ -164,7 +162,7 @@ def __str__(self) -> str: if self._events == []: return "NONE" - for (v, ev) in self._events: + for v, ev in self._events: if (ev == IN and v == MIN) or (ev == OUT and v == MAX): continue diff --git a/requirements-dev.txt b/requirements-dev.txt index f2a349e..3a712a6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,10 +1,10 @@ -black==22.8.0 -coverage==6.4.4 -flake8==5.0.4 -mypy==0.971 -tox==3.25.1 -twine==4.0.1 -ufmt==2.0.0 -usort==1.0.4 +black==23.11.0 +coverage==7.3.2 +flake8==6.1.0 +mypy==1.7.1 +tox==4.11.4 +twine==4.0.2 +ufmt==2.3.0 +usort==1.0.7 volatile==2.1.0 -wheel==0.37.1 +wheel==0.42.0 diff --git a/requirements.txt b/requirements.txt index 39d8b6d..db4f592 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -click==8.1.3 -requests==2.28.1 -packaging==21.3 +click==8.1.7 +requests==2.31.0 +packaging==23.2 moreorless==0.4.0 From 31a8b0190237b3e7f292dd34a14df8b82321598e Mon Sep 17 00:00:00 2001 From: Tim Hatch Date: Wed, 6 Dec 2023 16:47:13 -0800 Subject: [PATCH 11/12] Ill-advised change to make new mypy happy with new packaging --- bumpreqs/marker_extract.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bumpreqs/marker_extract.py b/bumpreqs/marker_extract.py index c650704..d0092e9 100644 --- a/bumpreqs/marker_extract.py +++ b/bumpreqs/marker_extract.py @@ -1,6 +1,8 @@ from typing import Any, Iterator, Optional, Tuple -from packaging.markers import Marker, Variable +from packaging._parser import Variable + +from packaging.markers import Marker from .vrange import TooComplicated, VersionIntervals From dd391b4775a85678b5ac83d284fc1cf8106c12cd Mon Sep 17 00:00:00 2001 From: Tim Hatch Date: Wed, 6 Dec 2023 16:47:15 -0800 Subject: [PATCH 12/12] Change test versions --- setup.cfg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index 1108219..02a34a6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,7 @@ packages = bumpreqs setup_requires = setuptools_scm setuptools >= 38.3.0 -python_requires = >=3.7 +python_requires = >=3.8 install_requires = requests packaging @@ -50,15 +50,15 @@ use_parentheses = True ignore_missing_imports = True [tox:tox] -envlist = py36, py37, py38 +envlist = py38, py39, py310, py311, py312 [testenv] deps = -rrequirements-dev.txt -whitelist_externals = make +allowlist_externals = make commands = make test setenv = - py{36,37,38}: COVERAGE_FILE={envdir}/.coverage + py{38,39,310,311,312}: COVERAGE_FILE={envdir}/.coverage [flake8] ignore = E203, E231, E266, E302, E501, W503