diff --git a/upgrade/scripts/find_compatible_versions.py b/upgrade/scripts/find_compatible_versions.py index 1ca5302..777d6f3 100644 --- a/upgrade/scripts/find_compatible_versions.py +++ b/upgrade/scripts/find_compatible_versions.py @@ -17,7 +17,11 @@ parse_requirements_txt, to_requirements_obj, ) -from upgrade.scripts.utils import get_venv_executable, is_package_already_installed +from upgrade.scripts.utils import ( + get_venv_executable, + is_development_cloudsmith, + is_package_already_installed, +) from upgrade.scripts.validations import is_cloudsmith_url_valid @@ -48,12 +52,16 @@ def get_compatible_upgrade_versions( tree = et.HTML(package_index_html) anchor_tags_el = tree.xpath("//a") parsed_packages_versions = [ - parse_wheel_filename(tag_el.text)[1] for tag_el in anchor_tags_el + parse_wheel_filename(tag_el.text)[1] + for tag_el in anchor_tags_el + if tag_el.text and tag_el.text.endswith(".whl") ] logging.debug(f"Parsed packages versions: {parsed_packages_versions}") compatible_versions = filter_versions( - requirements_obj.specifier, parsed_packages_versions + requirements_obj.specifier, + parsed_packages_versions, + prereleases=is_development_cloudsmith(cloudsmith_url), ) logging.debug(f"Found compatible versions: {compatible_versions}") diff --git a/upgrade/scripts/requirements.py b/upgrade/scripts/requirements.py index 8775d77..fa264e7 100644 --- a/upgrade/scripts/requirements.py +++ b/upgrade/scripts/requirements.py @@ -9,7 +9,7 @@ def filter_versions( - specifier_set: Any, parsed_packages_versions: List[Any] + specifier_set: Any, parsed_packages_versions: List[Any], prereleases: bool = False ) -> List[str]: """Returns a list of versions that are compatible with the `SpecifierSet`. @@ -21,8 +21,15 @@ def filter_versions( or: SpecifierSet("==2.5.14").filter(["2.5.14", "2.5.15", "2.6.0", "3.0.0"]) returns ["2.5.14"] + + prereleases defaults to False, matching SpecifierSet.filter()'s own default - pass True + for a dev/pre-release channel whose versions are all pre-releases, or every version + would otherwise be filtered out and no upgrade would ever be found. """ - return [str(version) for version in specifier_set.filter(parsed_packages_versions)] + return [ + str(version) + for version in specifier_set.filter(parsed_packages_versions, prereleases=prereleases) + ] def parse_requirements_txt( diff --git a/upgrade/tests/manage_venv/conftest.py b/upgrade/tests/manage_venv/conftest.py index 397ce98..bd0e369 100644 --- a/upgrade/tests/manage_venv/conftest.py +++ b/upgrade/tests/manage_venv/conftest.py @@ -124,6 +124,35 @@ def mock_package_index_html(): yield +@pytest.fixture() +def mock_package_index_html_with_sdist_entry(): + """Mirrors a real Cloudsmith index page that also lists a non-wheel sdist + (e.g. a stray oll-test-top-level-0.0.0.tar.gz from an earlier bad publish), + to guard against parse_wheel_filename crashing on it. + """ + index_html_page = """ + + + + Mock package index html page for testing compatible dependencies script. + + +

Links for oll-top-level-package

+ oll-test-top-level-0.0.0.tar.gz
+ oll_test_top_level-1.0.0-py3-none-any.whl
+ oll_test_top_level-2.0.0-py3-none-any.whl
+ oll_test_top_level-2.0.1-py3-none-any.whl
+ oll_test_top_level-2.1.0-py3-none-any.whl
+ + + """ + with patch( + "upgrade.scripts.find_compatible_versions._get_package_index_html", + lambda *_,: index_html_page, + ): + yield + + def install_upgrade_python_package(venv_executable, *rest): run( *([ diff --git a/upgrade/tests/manage_venv/test_get_compatible_upgrade_versions.py b/upgrade/tests/manage_venv/test_get_compatible_upgrade_versions.py index 066d987..f13e62b 100644 --- a/upgrade/tests/manage_venv/test_get_compatible_upgrade_versions.py +++ b/upgrade/tests/manage_venv/test_get_compatible_upgrade_versions.py @@ -59,3 +59,22 @@ def test_get_compatible_version_where_current_is_2_0_0_and_venv_without_specifie expected = "2.1.0" assert actual == expected + + +def test_get_compatible_version_where_index_page_lists_a_non_wheel_sdist_expect_it_is_ignored( + initial_v2_0_0_venv, + envs_home, + mock_package_index_html_with_sdist_entry, +): + cut = get_compatible_version + + requirements = "oll-test-top-level~=2.0.0" + venv_path = Path(envs_home, requirements) + + actual = cut( + requirements_obj=to_requirements_obj(requirements), + venv_path=str(venv_path), + ) + expected = "2.0.1" + + assert actual == expected diff --git a/upgrade/tests/upgrade_package/test_filter_versions.py b/upgrade/tests/upgrade_package/test_filter_versions.py index f94201a..0436cee 100644 --- a/upgrade/tests/upgrade_package/test_filter_versions.py +++ b/upgrade/tests/upgrade_package/test_filter_versions.py @@ -25,3 +25,21 @@ def test_filter_versions_with_different_specifiers( actual = cut(specifier_set, available_versions) assert actual == expected + + +def test_filter_versions_where_channel_is_dev_only_expect_none_found_by_default(): + cut = filter_versions + + actual = cut(SpecifierSet("~=2.21"), ["2.21.6.dev184", "2.21.6.dev200"]) + + assert actual == [] + + +def test_filter_versions_where_channel_is_dev_only_and_prereleases_true_expect_all_found(): + cut = filter_versions + + actual = cut( + SpecifierSet("~=2.21"), ["2.21.6.dev184", "2.21.6.dev200"], prereleases=True + ) + + assert actual == ["2.21.6.dev184", "2.21.6.dev200"] diff --git a/upgrade/tests/upgrade_package/test_integ_find_compatible_versions_live.py b/upgrade/tests/upgrade_package/test_integ_find_compatible_versions_live.py new file mode 100644 index 0000000..f01d583 --- /dev/null +++ b/upgrade/tests/upgrade_package/test_integ_find_compatible_versions_live.py @@ -0,0 +1,32 @@ +import os + +import pytest +from packaging.version import Version + +from upgrade.scripts.find_compatible_versions import get_compatible_upgrade_versions +from upgrade.scripts.requirements import to_requirements_obj + +# Set to a live openlawlibrary/development Cloudsmith index URL (with the entitlement +# token embedded, e.g. https://dl.cloudsmith.io//openlawlibrary/development/python/index/) +# to run this against the real index instead of a mocked one. Skipped otherwise, since it +# needs real network access and a real credential this repo doesn't store. +CLOUDSMITH_DEV_URL = os.environ.get("CLOUDSMITH_DEV_URL_TEST") + +pytestmark = pytest.mark.skipif( + not CLOUDSMITH_DEV_URL, + reason="CLOUDSMITH_DEV_URL_TEST is not set - export a live development Cloudsmith " + "index URL to exercise this against the real index", +) + + +def test_get_compatible_upgrade_versions_where_dev_channel_expect_prereleases_included(): + cut = get_compatible_upgrade_versions + + requirements_obj = to_requirements_obj("oll-cls~=2.21") + versions = cut(requirements_obj, CLOUDSMITH_DEV_URL) + + assert versions, "expected at least one dev version from the live development index" + assert any(Version(v) > Version("2.21.6.dev184") for v in versions), ( + "expected a version newer than the known-stale 2.21.6.dev184 to be present - " + "if this fails, prereleases are being filtered out again (the bug this test guards)" + )