From e11bae658f8c7e781230c43c0a4a465547c9e181 Mon Sep 17 00:00:00 2001 From: Alan Hamlett Date: Sun, 13 Aug 2023 13:46:14 +0200 Subject: [PATCH] Print warning to stderr if package not found --- README.md | 5 +- pur/__init__.py | 27 ++++-- pur/exceptions.py | 5 ++ pur/utils.py | 5 +- tests/samples/results/test_dry_run_changed | 1 + .../results/test_dry_run_invalid_package | 6 ++ tests/test_pur.py | 88 ++++++++++++++++++- 7 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 tests/samples/results/test_dry_run_changed create mode 100644 tests/samples/results/test_dry_run_invalid_package diff --git a/README.md b/README.md index ceeef32..8593256 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,8 @@ You can also use Pur directly from Python: file. -d, --dry-run Output changes to STDOUT instead of overwriting the requirements.txt file. - --dry-run-changed When running with --dry-run, only output packages - with updates, not packages that are already the - latest. + --dry-run-changed Enable dry run and only output packages with + updates, not packages that are already the latest. -n, --no-recursive Prevents updating nested requirements files. --skip TEXT Comma separated list of packages to skip updating. --skip-gt Skip updating packages using > or >= spec, to allow diff --git a/pur/__init__.py b/pur/__init__.py index 87cd9f0..bc776db 100644 --- a/pur/__init__.py +++ b/pur/__init__.py @@ -14,7 +14,7 @@ from collections import defaultdict import click -from click import echo as _echo +from click import secho as _echo try: from StringIO import StringIO @@ -45,7 +45,7 @@ handle_line) from .__about__ import __version__ -from .exceptions import StopUpdating +from .exceptions import InvalidPackage, StopUpdating from .utils import (ExitCodeException, build_package_finder, can_check_version, current_version, format_list_arg, join_lines, latest_version, old_version, requirements_line, @@ -140,8 +140,8 @@ def pur(**options): minor=options['minor'], patch=options['patch'], pre=options['pre'], - dry_run=options['dry_run'], - dry_run_changed=options['dry_run'] and options['dry_run_changed'], + dry_run=options['dry_run'] or options['dry_run_changed'], + dry_run_changed=options['dry_run_changed'], no_recursive=options['no_recursive'], echo=options['echo'], index_urls=options['index_url'], @@ -156,7 +156,7 @@ def pur(**options): raise ExitCodeException(70, message=traceback.format_exc().rstrip()) raise - if not options['dry_run']: + if not options['dry_run'] and not options['dry_run_changed']: _echo('All requirements up-to-date.') if options['nonzero_exit_code'] and PUR_GLOBAL_UPDATED > 0: @@ -391,7 +391,22 @@ def _get_requirements_and_latest(filename, updates=[], force=False, continue spec_ver = current_version(install_req) if spec_ver or force: - latest_ver = latest_version(install_req, spec_ver, finder, minor=minor, patch=patch, pre=pre) + try: + latest_ver = latest_version(install_req, spec_ver, finder, minor=minor, patch=patch, pre=pre) + except InvalidPackage: + latest_ver = None + + # output warning for invalid package + if not parsed_req.is_editable: + _echo( + 'No matching distribution found for {req_name} from {comes_from}'.format( + req_name=parsed_req.requirement, + comes_from=parsed_req.comes_from, + ), + err=True, + fg='red', + ) + yield (orig_line, install_req, spec_ver, latest_ver) diff --git a/pur/exceptions.py b/pur/exceptions.py index d4a8ba4..3a621b3 100644 --- a/pur/exceptions.py +++ b/pur/exceptions.py @@ -7,6 +7,11 @@ """ +class InvalidPackage(Exception): + """The package has no releases.""" + pass + + class StopUpdating(Exception): """Stop updating a requirements file and exit.""" pass diff --git a/pur/utils.py b/pur/utils.py index 2850a8f..11fe751 100644 --- a/pur/utils.py +++ b/pur/utils.py @@ -19,7 +19,7 @@ from pip._internal.req.req_file import COMMENT_RE from pip._vendor.packaging.version import InvalidVersion, Version, parse -from .exceptions import StopUpdating +from .exceptions import InvalidPackage, StopUpdating def build_package_finder(session=None, index_urls=[]): @@ -141,6 +141,7 @@ def old_version(spec_ver): def latest_version(req, spec_ver, finder, minor=[], patch=[], pre=[]): """Returns a Version instance with the latest version for the package. + Raises InvalidPackage error if no candidates available. :param req: Instance of pip.req.req_install.InstallRequirement. :param spec_ver: Tuple of current versions from the requirements file. @@ -156,6 +157,8 @@ def latest_version(req, spec_ver, finder, minor=[], patch=[], pre=[]): return None all_candidates = finder.find_all_candidates(req.name) + if len(all_candidates) == 0: + raise InvalidPackage() if req.name.lower() in patch or '*' in patch: all_candidates = [c for c in all_candidates diff --git a/tests/samples/results/test_dry_run_changed b/tests/samples/results/test_dry_run_changed new file mode 100644 index 0000000..335836f --- /dev/null +++ b/tests/samples/results/test_dry_run_changed @@ -0,0 +1 @@ +flask==0.10.1 diff --git a/tests/samples/results/test_dry_run_invalid_package b/tests/samples/results/test_dry_run_invalid_package new file mode 100644 index 0000000..a9cae88 --- /dev/null +++ b/tests/samples/results/test_dry_run_invalid_package @@ -0,0 +1,6 @@ +flask==0.9 +flask==12.0 +# an empty line below + +-e git://github.com/kennethreitz/inbox.py.git@551b4f44b144564504c687cebdb4c543cb8e9adf#egg=inbox +flask diff --git a/tests/test_pur.py b/tests/test_pur.py index 38f5155..8b76f54 100644 --- a/tests/test_pur.py +++ b/tests/test_pur.py @@ -559,7 +559,10 @@ def test_invalid_package(self): mock_find_all_candidates.return_value = [] result = self.runner.invoke(pur, args) - expected_output = "All requirements up-to-date.\n" + expected_output = 'No matching distribution found for flask==0.9 from -r ' + tmpfile + ' (line 1)\n' + \ + 'No matching distribution found for flask==12.0 from -r ' + tmpfile + ' (line 2)\n' + \ + 'No matching distribution found for flask from -r ' + tmpfile + ' (line 6)\n' + \ + 'All requirements up-to-date.\n' self.assertEqual(u(result.output), u(expected_output)) self.assertIsNone(result.exception) self.assertEqual(result.exit_code, 0) @@ -873,6 +876,89 @@ def test_dry_run_with_nested_requirements_file(self): expected_requirements = open('tests/samples/requirements-nested.txt').read() self.assertEqual(open(requirements_nested).read(), expected_requirements) + def test_dry_run_invalid_package(self): + requirements = 'tests/samples/requirements.txt' + tempdir = tempfile.mkdtemp() + tmpfile = os.path.join(tempdir, 'requirements.txt') + shutil.copy(requirements, tmpfile) + args = ['-r', tmpfile, '--dry-run'] + + with patch('pip._internal.index.package_finder.PackageFinder.find_all_candidates') as mock_find_all_candidates: + mock_find_all_candidates.return_value = [] + + result = self.runner.invoke(pur, args) + expected_output = 'No matching distribution found for flask==0.9 from -r ' + tmpfile + ' (line 1)\n' + \ + 'No matching distribution found for flask==12.0 from -r ' + tmpfile + ' (line 2)\n' + \ + 'No matching distribution found for flask from -r ' + tmpfile + ' (line 6)\n' + \ + '==> ' + tmpfile + ' <==\n' + \ + open('tests/samples/results/test_dry_run_invalid_package').read() + "\n" + self.assertEqual(u(result.output), u(expected_output)) + self.assertIsNone(result.exception) + self.assertEqual(result.exit_code, 0) + self.assertEqual(open(tmpfile).read(), open(requirements).read()) + + def test_dry_run_changed(self): + requirements = 'tests/samples/requirements.txt' + tempdir = tempfile.mkdtemp() + tmpfile = os.path.join(tempdir, 'requirements.txt') + shutil.copy(requirements, tmpfile) + args = ['-r', tmpfile, '--dry-run-changed'] + + with patch('pip._internal.index.package_finder.PackageFinder.find_all_candidates') as mock_find_all_candidates: + project = 'flask' + version = '0.10.1' + link = Link('') + candidate = InstallationCandidate(project, version, link) + mock_find_all_candidates.return_value = [candidate] + + result = self.runner.invoke(pur, args) + self.assertIsNone(result.exception) + expected_output = '==> ' + tmpfile + ' <==\n' + \ + open('tests/samples/results/test_dry_run_changed').read() + self.assertEqual(u(result.output), u(expected_output)) + self.assertEqual(result.exit_code, 0) + self.assertEqual(open(tmpfile).read(), open(requirements).read()) + + def test_dry_run_changed_no_updates(self): + requirements = 'tests/samples/requirements-up-to-date.txt' + tempdir = tempfile.mkdtemp() + tmpfile = os.path.join(tempdir, 'requirements.txt') + shutil.copy(requirements, tmpfile) + args = ['-r', tmpfile, '--dry-run-changed'] + + with patch('pip._internal.index.package_finder.PackageFinder.find_all_candidates') as mock_find_all_candidates: + project = 'flask' + version = '0.10.1' + link = Link('') + candidate = InstallationCandidate(project, version, link) + mock_find_all_candidates.return_value = [candidate] + + result = self.runner.invoke(pur, args) + self.assertIsNone(result.exception) + expected_output = '' + self.assertEqual(u(result.output), u(expected_output)) + self.assertEqual(result.exit_code, 0) + self.assertEqual(open(tmpfile).read(), open(requirements).read()) + + def test_dry_run_changed_invalid_package(self): + requirements = 'tests/samples/requirements.txt' + tempdir = tempfile.mkdtemp() + tmpfile = os.path.join(tempdir, 'requirements.txt') + shutil.copy(requirements, tmpfile) + args = ['-r', tmpfile, '--dry-run-changed'] + + with patch('pip._internal.index.package_finder.PackageFinder.find_all_candidates') as mock_find_all_candidates: + mock_find_all_candidates.return_value = [] + + result = self.runner.invoke(pur, args) + expected_output = 'No matching distribution found for flask==0.9 from -r ' + tmpfile + ' (line 1)\n' + \ + 'No matching distribution found for flask==12.0 from -r ' + tmpfile + ' (line 2)\n' + \ + 'No matching distribution found for flask from -r ' + tmpfile + ' (line 6)\n' + self.assertEqual(u(result.output), u(expected_output)) + self.assertIsNone(result.exception) + self.assertEqual(result.exit_code, 0) + self.assertEqual(open(tmpfile).read(), open(requirements).read()) + def test_updates_from_alt_index_url(self): requirements = 'tests/samples/requirements-with-alt-index-url.txt' tempdir = tempfile.mkdtemp()