diff --git a/lib/galaxy/dependencies/dev-requirements.txt b/lib/galaxy/dependencies/dev-requirements.txt index f3f6c8e21faa..930f6226a30b 100644 --- a/lib/galaxy/dependencies/dev-requirements.txt +++ b/lib/galaxy/dependencies/dev-requirements.txt @@ -66,7 +66,7 @@ myst-parser==1.0.0 ; python_version >= "3.7" and python_version < "3.12" numpy==1.21.6 ; python_version >= "3.7" and python_version < "3.8" numpy==1.24.2 ; python_version >= "3.8" and python_version < "3.12" outcome==1.2.0 ; python_version >= "3.7" and python_version < "3.12" -packaging==21.3 ; python_version >= "3.7" and python_version < "3.12" +packaging==23.1 ; python_version >= "3.7" and python_version < "3.12" pathspec==0.11.1 ; python_version >= "3.7" and python_version < "3.12" pillow==9.5.0 ; python_version >= "3.7" and python_version < "3.12" pkce==1.0.3 ; python_version >= "3.7" and python_version < "3.12" diff --git a/lib/galaxy/dependencies/pinned-requirements.txt b/lib/galaxy/dependencies/pinned-requirements.txt index 88a92eb9774e..c5d9f2803628 100644 --- a/lib/galaxy/dependencies/pinned-requirements.txt +++ b/lib/galaxy/dependencies/pinned-requirements.txt @@ -110,7 +110,7 @@ numpy==1.21.6 ; python_version >= "3.7" and python_version < "3.8" numpy==1.24.2 ; python_version >= "3.8" and python_version < "3.12" oauthlib==3.2.2 ; python_version >= "3.7" and python_version < "3.12" oyaml==1.0 ; python_version >= "3.7" and python_version < "3.12" -packaging==21.3 ; python_version >= "3.7" and python_version < "3.12" +packaging==23.1 ; python_version >= "3.7" and python_version < "3.12" paramiko==3.1.0 ; python_version >= "3.7" and python_version < "3.12" parse==1.19.0 ; python_version >= "3.7" and python_version < "3.12" parsley==1.3 ; python_version >= "3.7" and python_version < "3.12" diff --git a/lib/galaxy/jobs/__init__.py b/lib/galaxy/jobs/__init__.py index 69bb8eee38f9..ce03f507d1d8 100644 --- a/lib/galaxy/jobs/__init__.py +++ b/lib/galaxy/jobs/__init__.py @@ -22,8 +22,8 @@ TYPE_CHECKING, ) -import packaging.version import yaml +from packaging.version import Version from pulsar.client.staging import COMMAND_VERSION_FILENAME from galaxy import ( @@ -1089,7 +1089,7 @@ def job_io(self): @property def outputs_directory(self): """Default location of ``outputs_to_working_directory``.""" - return None if self.created_with_galaxy_version < packaging.version.parse("20.01") else "outputs" + return None if self.created_with_galaxy_version < Version("20.01") else "outputs" @property def outputs_to_working_directory(self): @@ -1098,7 +1098,7 @@ def outputs_to_working_directory(self): @property def created_with_galaxy_version(self): galaxy_version = self.get_job().galaxy_version or "19.05" - return packaging.version.parse(galaxy_version) + return Version(galaxy_version) @property def dependency_shell_commands(self): diff --git a/lib/galaxy/jobs/runners/pulsar.py b/lib/galaxy/jobs/runners/pulsar.py index d2f0905810cd..21650ab08152 100644 --- a/lib/galaxy/jobs/runners/pulsar.py +++ b/lib/galaxy/jobs/runners/pulsar.py @@ -14,9 +14,9 @@ Dict, ) -import packaging.version import pulsar.core import yaml +from packaging.version import Version from pulsar.client import ( build_client_manager, CLIENT_INPUT_PATH_TYPES, @@ -64,9 +64,9 @@ ) MINIMUM_PULSAR_VERSIONS = { - "_default_": packaging.version.parse("0.7.0.dev3"), - "remote_metadata": packaging.version.parse("0.8.0"), - "remote_container_handling": packaging.version.parse("0.9.1.dev0"), # probably 0.10 ultimately? + "_default_": Version("0.7.0.dev3"), + "remote_metadata": Version("0.8.0"), + "remote_container_handling": Version("0.9.1.dev0"), # probably 0.10 ultimately? } NO_REMOTE_GALAXY_FOR_METADATA_MESSAGE = "Pulsar misconfiguration - Pulsar client configured to set metadata remotely, but remote Pulsar isn't properly configured with a galaxy_home directory." @@ -498,7 +498,7 @@ def __prepare_job(self, job_wrapper, job_destination): pulsar_version=pulsar_version, ) rewrite_paths = not PulsarJobRunner.__rewrite_parameters(client) - if pulsar_version < packaging.version.parse("0.14.999") and rewrite_paths: + if pulsar_version < Version("0.14.999") and rewrite_paths: job_wrapper.disable_commands_in_new_shell() container = None if remote_container is None: @@ -810,7 +810,7 @@ def __client_outputs(self, client, job_wrapper): @staticmethod def pulsar_version(remote_job_config): - pulsar_version = packaging.version.parse(remote_job_config.get("pulsar_version", "0.6.0")) + pulsar_version = Version(remote_job_config.get("pulsar_version", "0.6.0")) return pulsar_version @staticmethod @@ -818,7 +818,7 @@ def check_job_config(remote_job_config, check_features=None): check_features = check_features or {} # 0.6.0 was newest Pulsar version that did not report it's version. pulsar_version = PulsarJobRunner.pulsar_version(remote_job_config) - needed_version = packaging.version.parse("0.0.0") + needed_version = Version("0.0.0") log.info(f"pulsar_version is {pulsar_version}") for feature, needed in list(check_features.items()) + [("_default_", True)]: if not needed: diff --git a/lib/galaxy/tool_util/deps/conda_util.py b/lib/galaxy/tool_util/deps/conda_util.py index f1ea923f5426..7baba7d5a7fb 100644 --- a/lib/galaxy/tool_util/deps/conda_util.py +++ b/lib/galaxy/tool_util/deps/conda_util.py @@ -20,8 +20,9 @@ Union, ) -import packaging.version +from packaging.version import Version +from galaxy.tool_util.version import parse_version from galaxy.util import ( commands, download_to_file, @@ -87,7 +88,7 @@ def find_conda_prefix() -> str: class CondaContext(installable.InstallableContext): installable_description = "Conda" _conda_build_available: Optional[bool] - _conda_version: Optional[Union[packaging.version.Version, packaging.version.LegacyVersion]] + _conda_version: Optional[Version] _experimental_solver_available: Optional[bool] def __init__( @@ -132,10 +133,10 @@ def _reset_conda_properties(self) -> None: self._experimental_solver_available = None @property - def conda_version(self) -> Union[packaging.version.Version, packaging.version.LegacyVersion]: + def conda_version(self) -> Version: if self._conda_version is None: self._guess_conda_properties() - assert isinstance(self._conda_version, (packaging.version.Version, packaging.version.LegacyVersion)) + assert isinstance(self._conda_version, Version) return self._conda_version @property @@ -147,12 +148,12 @@ def conda_build_available(self) -> bool: def _guess_conda_properties(self) -> None: info = self.conda_info() - self._conda_version = packaging.version.parse(info["conda_version"]) + self._conda_version = Version(info["conda_version"]) self._conda_build_available = False conda_build_version = info.get("conda_build_version") if conda_build_version and conda_build_version != "not installed": try: - packaging.version.parse(conda_build_version) + Version(conda_build_version) self._conda_build_available = True except Exception: pass @@ -169,9 +170,9 @@ def _override_channels_args(self) -> List[str]: @property def _experimental_solver_args(self) -> List[str]: if self._experimental_solver_available is None: - self._experimental_solver_available = self.conda_version >= packaging.version.parse( - "4.12.0" - ) and self.is_package_installed("conda-libmamba-solver") + self._experimental_solver_available = self.conda_version >= Version("4.12.0") and self.is_package_installed( + "conda-libmamba-solver" + ) if self._experimental_solver_available: return ["--experimental-solver", "libmamba"] else: @@ -282,7 +283,7 @@ def exec_create(self, args: Iterable[str], allow_local: bool = True, stdout_path for try_strict in [True, False]: create_args = ["-y", "--quiet"] if try_strict: - if self.conda_version >= packaging.version.parse("4.7.5"): + if self.conda_version >= Version("4.7.5"): create_args.append("--strict-channel-priority") else: continue @@ -313,7 +314,7 @@ def exec_install(self, args: Iterable[str], allow_local: bool = True, stdout_pat for try_strict in [True, False]: install_args = ["-y"] if try_strict: - if self.conda_version >= packaging.version.parse("4.7.5"): + if self.conda_version >= Version("4.7.5"): install_args.append("--strict-channel-priority") else: continue @@ -567,7 +568,7 @@ def best_search_result( # the latest update time. hits = json.loads(res).get(conda_target.package, [])[::-1] hits = sorted(hits, key=lambda hit: hit["build_number"], reverse=True) - hits = sorted(hits, key=lambda hit: packaging.version.parse(hit["version"]), reverse=True) + hits = sorted(hits, key=lambda hit: parse_version(hit["version"]), reverse=True) except commands.CommandLineException as e: log.error(f"Could not execute: '{e.command}'\n{e}") hits = [] @@ -632,8 +633,8 @@ def build_isolated_environment( # Adjust fix if they fix Conda - xref # - https://github.com/galaxyproject/galaxy/issues/3635 # - https://github.com/conda/conda/issues/2035 - offline_works = (conda_context.conda_version < packaging.version.parse("4.3")) or ( - conda_context.conda_version >= packaging.version.parse("4.4") + offline_works = (conda_context.conda_version < Version("4.3")) or ( + conda_context.conda_version >= Version("4.4") ) if offline_works: create_args.append("--offline") diff --git a/lib/galaxy/tool_util/deps/mulled/mulled_build.py b/lib/galaxy/tool_util/deps/mulled/mulled_build.py index ae54c6d0d61e..ad75b0732daa 100644 --- a/lib/galaxy/tool_util/deps/mulled/mulled_build.py +++ b/lib/galaxy/tool_util/deps/mulled/mulled_build.py @@ -19,6 +19,8 @@ import sys from sys import platform as _platform from typing import ( + Any, + Dict, List, TYPE_CHECKING, ) @@ -157,7 +159,7 @@ def conda_versions(pkg_name, file_name): return ret -def get_conda_hits_for_targets(targets, conda_context): +def get_conda_hits_for_targets(targets, conda_context: CondaContext) -> List[Dict[str, Any]]: search_results = (best_search_result(t, conda_context, platform="linux-64")[0] for t in targets) return [r for r in search_results if r] diff --git a/lib/galaxy/tool_util/deps/mulled/util.py b/lib/galaxy/tool_util/deps/mulled/util.py index 6586a205707d..a832f7389502 100644 --- a/lib/galaxy/tool_util/deps/mulled/util.py +++ b/lib/galaxy/tool_util/deps/mulled/util.py @@ -17,11 +17,12 @@ Union, ) -import packaging.version import requests from conda_package_streaming.package_streaming import stream_conda_info from conda_package_streaming.url import stream_conda_info as stream_conda_info_from_url +from galaxy.tool_util.version import parse_version + if TYPE_CHECKING: from galaxy.tool_util.deps.container_resolvers import ResolutionCache @@ -189,8 +190,8 @@ def parse_tag(tag): build_number = -1 return PARSED_TAG( tag=tag, - version=packaging.version.parse(version), - build_string=packaging.version.parse(build_string), + version=parse_version(version), + build_string=parse_version(build_string), build_number=build_number, ) diff --git a/lib/galaxy/tool_util/linters/general.py b/lib/galaxy/tool_util/linters/general.py index ed87a611da2c..0b59185996c2 100644 --- a/lib/galaxy/tool_util/linters/general.py +++ b/lib/galaxy/tool_util/linters/general.py @@ -1,7 +1,10 @@ """This module contains linting functions for general aspects of the tool.""" import re -import packaging.version +from galaxy.tool_util.version import ( + LegacyVersion, + parse_version, +) ERROR_VERSION_MSG = "Tool version is missing or empty." WARN_VERSION_MSG = "Tool version [%s] is not compliant with PEP 440." @@ -34,10 +37,10 @@ def lint_general(tool_source, lint_ctx): else: tool_node = None version = tool_source.parse_version() or "" - parsed_version = packaging.version.parse(version) + parsed_version = parse_version(version) if not version: lint_ctx.error(ERROR_VERSION_MSG, node=tool_node) - elif isinstance(parsed_version, packaging.version.LegacyVersion): + elif isinstance(parsed_version, LegacyVersion): lint_ctx.warn(WARN_VERSION_MSG % version, node=tool_node) elif version != version.strip(): lint_ctx.warn(WARN_WHITESPACE_PRESUFFIX % ("Tool version", version), node=tool_node) diff --git a/lib/galaxy/tool_util/parser/xml.py b/lib/galaxy/tool_util/parser/xml.py index e7a335a1456c..f6fd3aabde0b 100644 --- a/lib/galaxy/tool_util/parser/xml.py +++ b/lib/galaxy/tool_util/parser/xml.py @@ -9,7 +9,7 @@ Optional, ) -import packaging.version +from packaging.version import Version from galaxy.tool_util.deps import requirements from galaxy.tool_util.parser.util import ( @@ -525,7 +525,7 @@ def parse_stdio(self): def parse_strict_shell(self): command_el = self._command_el - if packaging.version.parse(self.parse_profile()) < packaging.version.parse("20.09"): + if Version(self.parse_profile()) < Version("20.09"): default = "False" else: default = "True" @@ -558,7 +558,7 @@ def parse_tests_to_dict(self) -> ToolSourceTests: return rval - def parse_profile(self): + def parse_profile(self) -> str: # Pre-16.04 or default XML defaults # - Use standard error for error detection. # - Don't run shells with -e @@ -573,7 +573,7 @@ def parse_license(self): def parse_python_template_version(self): python_template_version = self.root.get("python_template_version") if python_template_version is not None: - python_template_version = packaging.version.Version(python_template_version) + python_template_version = Version(python_template_version) return python_template_version def parse_creator(self): diff --git a/lib/galaxy/tool_util/toolbox/lineages/interface.py b/lib/galaxy/tool_util/toolbox/lineages/interface.py index 3722148e3b17..71fdb97611d3 100644 --- a/lib/galaxy/tool_util/toolbox/lineages/interface.py +++ b/lib/galaxy/tool_util/toolbox/lineages/interface.py @@ -5,9 +5,9 @@ List, ) -import packaging.version from sortedcontainers import SortedSet +from galaxy.tool_util.version import parse_version from galaxy.util.tool_version import remove_version_from_guid @@ -46,7 +46,7 @@ class ToolLineage: def __init__(self, tool_id, **kwds): self.tool_id = tool_id - self.tool_versions = SortedSet(key=packaging.version.parse) + self.tool_versions = SortedSet(key=parse_version) @property def tool_ids(self) -> List[str]: diff --git a/lib/galaxy/tool_util/verify/interactor.py b/lib/galaxy/tool_util/verify/interactor.py index ae7ede25198b..411d3d8a1bb1 100644 --- a/lib/galaxy/tool_util/verify/interactor.py +++ b/lib/galaxy/tool_util/verify/interactor.py @@ -25,10 +25,7 @@ ) import requests -from packaging.version import ( - parse as parse_version, - Version, -) +from packaging.version import Version from requests import Response from requests.cookies import RequestsCookieJar from typing_extensions import ( @@ -200,7 +197,7 @@ def __init__(self, **kwds): @property def target_galaxy_version(self): if self._target_galaxy_version is None: - self._target_galaxy_version = parse_version(self._get("version").json()["version_major"]) + self._target_galaxy_version = Version(self._get("version").json()["version_major"]) return self._target_galaxy_version @property diff --git a/lib/galaxy/tool_util/version.py b/lib/galaxy/tool_util/version.py new file mode 100644 index 000000000000..f1b3f01e3b47 --- /dev/null +++ b/lib/galaxy/tool_util/version.py @@ -0,0 +1,164 @@ +# Copyright (c) Donald Stufft and individual contributors. +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# The following code derives from packaging 21.3 before the LegacyVersion class +# was removed: https://github.com/pypa/packaging/blob/21.3/packaging/version.py + +import re +from typing import ( + Iterator, + List, + Tuple, + Union, +) + +from packaging.version import ( + _BaseVersion, + InvalidVersion, + Version, +) + +__all__ = ["parse_version", "LegacyVersion"] + +LegacyCmpKey = Tuple[int, Tuple[str, ...]] + + +def parse_version(version: str) -> Union["LegacyVersion", "Version"]: + """ + Parse the given version string and return either a :class:`Version` object + or a :class:`LegacyVersion` object depending on if the given version is + a valid PEP 440 version or a legacy version. + """ + try: + return Version(version) + except InvalidVersion: + return LegacyVersion(version) + + +class LegacyVersion(_BaseVersion): + def __init__(self, version: str) -> None: + self._version = str(version) + self._key = _legacy_cmpkey(self._version) + + def __str__(self) -> str: + return self._version + + def __repr__(self) -> str: + return f"" + + @property + def public(self) -> str: + return self._version + + @property + def base_version(self) -> str: + return self._version + + @property + def epoch(self) -> int: + return -1 + + @property + def release(self) -> None: + return None + + @property + def pre(self) -> None: + return None + + @property + def post(self) -> None: + return None + + @property + def dev(self) -> None: + return None + + @property + def local(self) -> None: + return None + + @property + def is_prerelease(self) -> bool: + return False + + @property + def is_postrelease(self) -> bool: + return False + + @property + def is_devrelease(self) -> bool: + return False + + +_legacy_version_component_re = re.compile(r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE) + +_legacy_version_replacement_map = { + "pre": "c", + "preview": "c", + "-": "final-", + "rc": "c", + "dev": "@", +} + + +def _parse_version_parts(s: str) -> Iterator[str]: + for part in _legacy_version_component_re.split(s): + part = _legacy_version_replacement_map.get(part, part) + + if not part or part == ".": + continue + + if part[:1] in "0123456789": + # pad for numeric comparison + yield part.zfill(8) + else: + yield "*" + part + + # ensure that alpha/beta/candidate are before final + yield "*final" + + +def _legacy_cmpkey(version: str) -> LegacyCmpKey: + # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch + # greater than or equal to 0. This will effectively put the LegacyVersion, + # which uses the defacto standard originally implemented by setuptools, + # as before all PEP 440 versions. + epoch = -1 + + # This scheme is taken from pkg_resources.parse_version setuptools prior to + # it's adoption of the packaging library. + parts: List[str] = [] + for part in _parse_version_parts(version.lower()): + if part.startswith("*"): + # remove "-" before a prerelease tag + if part < "*final": + while parts and parts[-1] == "*final-": + parts.pop() + + # remove trailing zeros from each series of numeric parts + while parts and parts[-1] == "00000000": + parts.pop() + + parts.append(part) + + return epoch, tuple(parts) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index dcc54ee9325c..94ec48c91974 100644 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -25,10 +25,10 @@ ) from urllib.parse import unquote_plus -import packaging.version import webob.exc from lxml import etree from mako.template import Template +from packaging.version import Version from galaxy import ( exceptions, @@ -79,6 +79,10 @@ ) from galaxy.tool_util.toolbox.views.sources import StaticToolBoxViewSources from galaxy.tool_util.verify.test_data import TestDataNotFoundError +from galaxy.tool_util.version import ( + LegacyVersion, + parse_version, +) from galaxy.tools import expressions from galaxy.tools.actions import ( DefaultToolAction, @@ -223,28 +227,28 @@ # Tools that needed galaxy on the PATH in the past but no longer do along # with the version at which they were fixed. GALAXY_LIB_TOOLS_VERSIONED = { - "meme_fimo": packaging.version.parse("5.0.5"), - "Extract genomic DNA 1": packaging.version.parse("3.0.0"), - "fetchflank": packaging.version.parse("1.0.1"), - "gops_intersect_1": packaging.version.parse("1.0.0"), - "lastz_wrapper_2": packaging.version.parse("1.3"), - "PEsortedSAM2readprofile": packaging.version.parse("1.1.1"), - "sam_to_bam": packaging.version.parse("1.1.3"), - "sam_pileup": packaging.version.parse("1.1.3"), - "vcf_to_maf_customtrack1": packaging.version.parse("1.0.1"), - "secure_hash_message_digest": packaging.version.parse("0.0.2"), - "join1": packaging.version.parse("2.1.3"), - "wiggle2simple1": packaging.version.parse("1.0.1"), - "CONVERTER_wiggle_to_interval_0": packaging.version.parse("1.0.1"), - "aggregate_scores_in_intervals2": packaging.version.parse("1.1.4"), - "CONVERTER_fastq_to_fqtoc0": packaging.version.parse("1.0.1"), - "CONVERTER_tar_to_directory": packaging.version.parse("1.0.1"), - "tabular_to_dbnsfp": packaging.version.parse("1.0.1"), - "cufflinks": packaging.version.parse("2.2.1.3"), - "Convert characters1": packaging.version.parse("1.0.1"), - "substitutions1": packaging.version.parse("1.0.1"), - "winSplitter": packaging.version.parse("1.0.1"), - "Interval2Maf1": packaging.version.parse("1.0.1+galaxy0"), + "meme_fimo": parse_version("5.0.5"), + "Extract genomic DNA 1": parse_version("3.0.0"), + "fetchflank": parse_version("1.0.1"), + "gops_intersect_1": parse_version("1.0.0"), + "lastz_wrapper_2": parse_version("1.3"), + "PEsortedSAM2readprofile": parse_version("1.1.1"), + "sam_to_bam": parse_version("1.1.3"), + "sam_pileup": parse_version("1.1.3"), + "vcf_to_maf_customtrack1": parse_version("1.0.1"), + "secure_hash_message_digest": parse_version("0.0.2"), + "join1": parse_version("2.1.3"), + "wiggle2simple1": parse_version("1.0.1"), + "CONVERTER_wiggle_to_interval_0": parse_version("1.0.1"), + "aggregate_scores_in_intervals2": parse_version("1.1.4"), + "CONVERTER_fastq_to_fqtoc0": parse_version("1.0.1"), + "CONVERTER_tar_to_directory": parse_version("1.0.1"), + "tabular_to_dbnsfp": parse_version("1.0.1"), + "cufflinks": parse_version("2.2.1.3"), + "Convert characters1": parse_version("1.0.1"), + "substitutions1": parse_version("1.0.1"), + "winSplitter": parse_version("1.0.1"), + "Interval2Maf1": parse_version("1.0.1+galaxy0"), } REQUIRE_FULL_DIRECTORY = { @@ -252,7 +256,7 @@ } IMPLICITLY_REQUIRED_TOOL_FILES: Dict[str, Dict] = { "deseq2": { - "version": packaging.version.parse("2.11.40.6"), + "version": parse_version("2.11.40.6"), "required": {"includes": [{"path": "*.R", "path_type": "glob"}]}, }, # minimum example: @@ -277,21 +281,21 @@ class safe_update(NamedTuple): - min_version: Union[packaging.version.LegacyVersion, packaging.version.Version] - current_version: Union[packaging.version.LegacyVersion, packaging.version.Version] + min_version: Union[LegacyVersion, Version] + current_version: Union[LegacyVersion, Version] # Tool updates that did not change parameters in a way that requires rebuilding workflows WORKFLOW_SAFE_TOOL_VERSION_UPDATES = { - "Filter1": safe_update(packaging.version.parse("1.1.0"), packaging.version.parse("1.1.1")), - "__BUILD_LIST__": safe_update(packaging.version.parse("1.0.0"), packaging.version.parse("1.1.0")), - "__APPLY_RULES__": safe_update(packaging.version.parse("1.0.0"), packaging.version.parse("1.1.0")), - "__EXTRACT_DATASET__": safe_update(packaging.version.parse("1.0.0"), packaging.version.parse("1.0.1")), - "Grep1": safe_update(packaging.version.parse("1.0.1"), packaging.version.parse("1.0.4")), - "Show beginning1": safe_update(packaging.version.parse("1.0.0"), packaging.version.parse("1.0.2")), - "Show tail1": safe_update(packaging.version.parse("1.0.0"), packaging.version.parse("1.0.1")), - "sort1": safe_update(packaging.version.parse("1.1.0"), packaging.version.parse("1.2.0")), - "CONVERTER_interval_to_bgzip_0": safe_update(packaging.version.parse("1.0.1"), packaging.version.parse("1.0.2")), + "Filter1": safe_update(parse_version("1.1.0"), parse_version("1.1.1")), + "__BUILD_LIST__": safe_update(parse_version("1.0.0"), parse_version("1.1.0")), + "__APPLY_RULES__": safe_update(parse_version("1.0.0"), parse_version("1.1.0")), + "__EXTRACT_DATASET__": safe_update(parse_version("1.0.0"), parse_version("1.0.1")), + "Grep1": safe_update(parse_version("1.0.1"), parse_version("1.0.4")), + "Show beginning1": safe_update(parse_version("1.0.0"), parse_version("1.0.2")), + "Show tail1": safe_update(parse_version("1.0.0"), parse_version("1.0.1")), + "sort1": safe_update(parse_version("1.1.0"), parse_version("1.2.0")), + "CONVERTER_interval_to_bgzip_0": safe_update(parse_version("1.0.1"), parse_version("1.0.2")), } @@ -757,7 +761,7 @@ def __init__( # guid attribute since it is useful to have. self.guid = guid self.old_id: Optional[str] = None - self.python_template_version: Optional[packaging.version.Version] = None + self.python_template_version: Optional[Version] = None self._lineage = None self.dependencies: List = [] # populate toolshed repository info, if available @@ -835,7 +839,7 @@ def _view(self): @property def version_object(self): - return packaging.version.parse(self.version) + return parse_version(self.version) @property def sa_session(self): @@ -1005,12 +1009,8 @@ def parse(self, tool_source: ToolSource, guid=None, dynamic=False): if not dynamic and not self.id: raise Exception(f"Missing tool 'id' for tool at '{tool_source}'") - profile = packaging.version.parse(str(self.profile)) - if ( - self.app.name == "galaxy" - and profile >= packaging.version.parse("16.04") - and packaging.version.parse(VERSION_MAJOR) < profile - ): + profile = Version(str(self.profile)) + if self.app.name == "galaxy" and profile >= Version("16.04") and Version(VERSION_MAJOR) < profile: message = f"The tool [{self.id}] targets version {self.profile} of Galaxy, you should upgrade Galaxy to ensure proper functioning of this tool." raise Exception(message) @@ -1018,9 +1018,9 @@ def parse(self, tool_source: ToolSource, guid=None, dynamic=False): if self.python_template_version is None: # If python_template_version not specified we assume tools with profile versions >= 19.05 are python 3 ready if self.profile >= 19.05: - self.python_template_version = packaging.version.Version("3.5") + self.python_template_version = Version("3.5") else: - self.python_template_version = packaging.version.Version("2.7") + self.python_template_version = Version("2.7") # Get the (user visible) name of the tool self.name = tool_source.parse_name() diff --git a/lib/galaxy/tools/parameters/wrapped_json.py b/lib/galaxy/tools/parameters/wrapped_json.py index 912ea3eb2c74..7be2f9423fba 100644 --- a/lib/galaxy/tools/parameters/wrapped_json.py +++ b/lib/galaxy/tools/parameters/wrapped_json.py @@ -7,7 +7,7 @@ Sequence, ) -import packaging.version +from packaging.version import Version log = logging.getLogger(__name__) @@ -147,7 +147,7 @@ def _json_wrap_input(input, value_wrapper, profile, handle_files="skip"): else: json_value = _cast_if_not_none(value_wrapper, bool, empty_to_none=input.optional) elif input_type == "select": - if packaging.version.parse(str(profile)) < packaging.version.parse("20.05"): + if Version(str(profile)) < Version("20.05"): json_value = _cast_if_not_none(value_wrapper, str) else: if input.multiple: diff --git a/lib/galaxy/util/template.py b/lib/galaxy/util/template.py index 64b7cf954c5d..536b09e9e119 100644 --- a/lib/galaxy/util/template.py +++ b/lib/galaxy/util/template.py @@ -3,11 +3,11 @@ import traceback from lib2to3.refactor import RefactoringTool -import packaging.version from Cheetah.Compiler import Compiler from Cheetah.NameMapper import NotFound from Cheetah.Parser import ParseError from Cheetah.Template import Template +from packaging.version import Version from past.translation import myfixes from . import unicodify @@ -55,7 +55,7 @@ def fill_template( if not context: context = kwargs if isinstance(python_template_version, str): - python_template_version = packaging.version.parse(python_template_version) + python_template_version = Version(python_template_version) try: klass = Template.compile(source=template_text, compilerClass=compiler_class) except ParseError as e: diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py index 1ec0afef118d..55b22a1c5118 100644 --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -17,7 +17,6 @@ Union, ) -import packaging.version from cwl_utils.expression import do_eval from typing_extensions import TypedDict @@ -48,6 +47,7 @@ ) from galaxy.tool_util.cwl.util import set_basename_and_derived_properties from galaxy.tool_util.parser.output_objects import ToolExpressionOutput +from galaxy.tool_util.version import parse_version from galaxy.tools import ( DatabaseOperationTool, DefaultToolState, @@ -1679,11 +1679,7 @@ def __init__(self, trans, tool_id, tool_version=None, exact_tools=True, tool_uui if safe_version and self.tool.lineage: # tool versions are sorted from old to new, so check newest version first for lineage_version in reversed(self.tool.lineage.tool_versions): - if ( - safe_version.current_version - >= packaging.version.parse(lineage_version) - >= safe_version.min_version - ): + if safe_version.current_version >= parse_version(lineage_version) >= safe_version.min_version: self.tool = trans.app.toolbox.get_tool( tool_id, tool_version=lineage_version, exact=True, tool_uuid=tool_uuid ) diff --git a/pyproject.toml b/pyproject.toml index 4e5cfa2196cb..8d6f0c681fbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,7 @@ Mercurial = "*" mrcfile = "*" nodeenv = "*" numpy = "*" -packaging = "<22" # packaging 22.0 dropped LegacyVersion +packaging = "*" paramiko = "!=2.9.0, !=2.9.1" # https://github.com/paramiko/paramiko/issues/1961 Parsley = "*" Paste = "*" diff --git a/test/unit/tool_util/test_version.py b/test/unit/tool_util/test_version.py new file mode 100644 index 000000000000..8f1ec8d48483 --- /dev/null +++ b/test/unit/tool_util/test_version.py @@ -0,0 +1,100 @@ +import pytest +from packaging.version import Version + +from galaxy.tool_util.version import ( + LegacyVersion, + parse_version, +) + +# This list must be in the correct sorting order +VERSIONS = [ + # Implicit epoch of 0 + "1.0.dev456", + "1.0a1", + "1.0a2.dev456", + "1.0a12.dev456", + "1.0a12", + "1.0b1.dev456", + "1.0b2", + "1.0b2.post345.dev456", + "1.0b2.post345", + "1.0b2-346", + "1.0c1.dev456", + "1.0c1", + "1.0rc2", + "1.0c3", + "1.0", + "1.0.post456.dev34", + "1.0.post456", + "1.1.dev1", + "1.2+123abc", + "1.2+123abc456", + "1.2+abc", + "1.2+abc123", + "1.2+abc123def", + "1.2+1234.abc", + "1.2+123456", + "1.2.r32+123456", + "1.2.rev33+123456", + # Explicit epoch of 1 + "1!1.0.dev456", + "1!1.0a1", + "1!1.0a2.dev456", + "1!1.0a12.dev456", + "1!1.0a12", + "1!1.0b1.dev456", + "1!1.0b2", + "1!1.0b2.post345.dev456", + "1!1.0b2.post345", + "1!1.0b2-346", + "1!1.0c1.dev456", + "1!1.0c1", + "1!1.0rc2", + "1!1.0c3", + "1!1.0", + "1!1.0.post456.dev34", + "1!1.0.post456", + "1!1.1.dev1", + "1!1.2+123abc", + "1!1.2+123abc456", + "1!1.2+abc", + "1!1.2+abc123", + "1!1.2+abc123def", + "1!1.2+1234.abc", + "1!1.2+123456", + "1!1.2.r32+123456", + "1!1.2.rev33+123456", +] + +LEGACY_VERSIONS = ["foobar", "a cat is fine too", "lolwut", "1.8.1+2+galaxy0"] + + +@pytest.mark.parametrize("version", VERSIONS) +def test_parse_pep440_versions(version: str) -> None: + assert isinstance(parse_version(version), Version) + + +@pytest.mark.parametrize("version", LEGACY_VERSIONS) +def test_legacy_version_fields(version: str) -> None: + parsed_version = parse_version(version) + assert isinstance(parsed_version, LegacyVersion) + assert parsed_version.public == version + assert parsed_version.base_version == version + assert parsed_version.epoch == -1 + assert parsed_version.release is None + assert parsed_version.pre is None + assert parsed_version.post is None + assert parsed_version.dev is None + assert parsed_version.local is None + assert parsed_version.is_prerelease is False + assert parsed_version.is_postrelease is False + assert parsed_version.is_devrelease is False + + +@pytest.mark.parametrize( + "lower_ver,greater_ver", + [(VERSIONS[i], VERSIONS[i + 1]) for i in range(len(VERSIONS) - 1)] + + [(lv, v) for v in VERSIONS for lv in LEGACY_VERSIONS], +) +def test_version_cmp(lower_ver: str, greater_ver: str) -> None: + assert parse_version(lower_ver) < parse_version(greater_ver)