diff --git a/README.md b/README.md index 042649e..6520c79 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,27 @@ # bumpreqs +Usage + +```shell-session +$ pip install bumpreqs +$ python -m bumpreqs --write requirements*.txt +``` + +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/__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 new file mode 100644 index 0000000..dca67d0 --- /dev/null +++ b/bumpreqs/cli.py @@ -0,0 +1,34 @@ +from pathlib import Path + +from typing import List, Optional + +import click +from moreorless.click import echo_color_unified_diff + +from .core import fix + + +@click.command() +@click.option("--diff", is_flag=True, default=None) +@click.option("--write", is_flag=True) +@click.argument("filenames", nargs=-1) +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: + echo_color_unified_diff(old_text, new_text, f) + if write: + Path(f).write_text(new_text) + + +if __name__ == "__main__": + main() diff --git a/bumpreqs/core.py b/bumpreqs/core.py new file mode 100644 index 0000000..3cd41c6 --- /dev/null +++ b/bumpreqs/core.py @@ -0,0 +1,118 @@ +import logging + +from typing import List, Optional + +import requests + +from packaging.requirements import Requirement +from packaging.specifiers import SpecifierSet +from packaging.version import parse as parse_version, Version + +from .marker_extract import extract_python + +from .vrange import TooComplicated, VersionIntervals + +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 + + 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: + new_lines.append(line) + continue + + try: + 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) + 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, + only_for_python: Optional[VersionIntervals] = None, +) -> List[Version]: + resp = requests.get(f"https://pypi.org/pypi/{project_name}/json") + resp.raise_for_status() + obj = resp.json() + + versions: List[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)): + # TODO try/except + versions.append(parse_version(k)) + else: + # TODO try/except + 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..d0092e9 --- /dev/null +++ b/bumpreqs/marker_extract.py @@ -0,0 +1,60 @@ +from typing import Any, Iterator, Optional, Tuple + +from packaging._parser import Variable + +from packaging.markers import Marker + +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 67742dd..191fe8f 100644 --- a/bumpreqs/tests/__init__.py +++ b/bumpreqs/tests/__init__.py @@ -1,5 +1,10 @@ -# from .foo import FooTest +from .core import FetchVersionsTest, FixTest +from .marker_extract import MarkerExtractTest +from .vrange import VersionIntervalsTest __all__ = [ - # FooTest, + "FixTest", + "FetchVersionsTest", + "VersionIntervalsTest", + "MarkerExtractTest", ] diff --git a/bumpreqs/tests/core.py b/bumpreqs/tests/core.py new file mode 100644 index 0000000..c478c95 --- /dev/null +++ b/bumpreqs/tests/core.py @@ -0,0 +1,165 @@ +import unittest +from typing import Any, Dict +from unittest.mock import patch + +from packaging.version import Version + +from ..core import _fetch_versions, fix +from ..vrange import VersionIntervals + +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": { + str(k): [ + { + "filename": f"foo-{k}.tgz", + "packagetype": "sdist", + "requires_python": rp, + } + ] + for k, rp in VERSIONS + }, +} + +PROJECTS = { + "foo": [Version(x) for x, y in VERSIONS], + "foup": [Version(x) for x, y in [*VERSIONS, *PRE_VERSIONS]], + "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=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")) + warning_mock.assert_called_with( + "Failed to fetch versions for %r: %s", "nope", "KeyError('nope')" + ) + + @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=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( + "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/" + ) + + @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(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..e08cb6d --- /dev/null +++ b/bumpreqs/vrange.py @@ -0,0 +1,182 @@ +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/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 027ccab..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,11 +0,0 @@ -black==23.3.0 # last version to support 3.7 -checkdeps==0.0.2 -coverage==7.3.2 -flake8==6.1.0 -mypy==1.6.1 -tox==4.11.3 -twine==4.0.2 -ufmt==2.3.0 -usort==1.0.7 -volatile==2.1.0 -wheel==0.41.3 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..db4f592 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +click==8.1.7 +requests==2.31.0 +packaging==23.2 +moreorless==0.4.0 diff --git a/setup.cfg b/setup.cfg index 2b6e507..ae09f9b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,6 +15,15 @@ setup_requires = setuptools >= 65 python_requires = >=3.10 include_package_data = true +install_requires = + requests + packaging + click + moreorless + +[options.entry_points] +console_scripts = + bumpreqs = bumpreqs.cli:main [options.extras_require] dev = @@ -40,7 +49,7 @@ include = bumpreqs/* omit = bumpreqs/tests/* [coverage:report] -fail_under = 70 +fail_under = 98 precision = 1 show_missing = True skip_covered = True