diff --git a/.github/workflows/docs-ci.yml b/.github/workflows/docs-ci.yml index 511b7c28..8c2abfe9 100644 --- a/.github/workflows/docs-ci.yml +++ b/.github/workflows/docs-ci.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: max-parallel: 4 @@ -20,9 +20,6 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Give permission to run scripts - run: chmod +x ./docs/scripts/doc8_style_check.sh - - name: Install Dependencies run: pip install -e .[docs] diff --git a/.github/workflows/pypi-release.yml b/.github/workflows/pypi-release.yml index 188497e7..bc5d0e01 100644 --- a/.github/workflows/pypi-release.yml +++ b/.github/workflows/pypi-release.yml @@ -13,7 +13,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v1 with: - python-version: 3.8 + python-version: 3.9 - name: Install pypa/build run: python -m pip install build --user - name: Build a binary wheel and a source tarball diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 80aee3fe..b7e21c8e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -11,7 +11,7 @@ jobs: parameters: job_name: ubuntu20_cpython image_name: ubuntu-20.04 - python_versions: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python_versions: ['3.8', '3.9', '3.10', '3.11', '3.12'] test_suites: all: | source venv/bin/activate @@ -21,17 +21,7 @@ jobs: parameters: job_name: ubuntu22_cpython image_name: ubuntu-22.04 - python_versions: ['3.7', '3.8', '3.9', '3.10', '3.11'] - test_suites: - all: | - source venv/bin/activate - pytest -n 2 -vvs - - - template: etc/ci/azure-posix.yml - parameters: - job_name: macos11_cpython - image_name: macOS-11 - python_versions: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python_versions: ['3.8', '3.9', '3.10', '3.11', '3.12'] test_suites: all: | source venv/bin/activate @@ -41,7 +31,7 @@ jobs: parameters: job_name: macos12_cpython image_name: macOS-12 - python_versions: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python_versions: ['3.8', '3.9', '3.10', '3.11', '3.12'] test_suites: all: | source venv/bin/activate @@ -51,17 +41,23 @@ jobs: parameters: job_name: macos13_cpython image_name: macOS-13 - python_versions: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python_versions: ['3.8', '3.9', '3.10', '3.11', '3.12'] test_suites: - all: | - source venv/bin/activate - pytest -n 2 -vvs + all: venv/bin/pytest -n 2 -vvs + + - template: etc/ci/azure-posix.yml + parameters: + job_name: macos14_cpython_arm64 + image_name: macOS-14 + python_versions: ['3.8', '3.9', '3.10', '3.11', '3.12'] + test_suites: + all: venv/bin/pytest -n 2 -vvs - template: etc/ci/azure-win.yml parameters: job_name: win2019_cpython image_name: windows-2019 - python_versions: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python_versions: ['3.8', '3.9', '3.10', '3.11', '3.12'] test_suites: all: | call venv\Scripts\activate.bat @@ -71,7 +67,7 @@ jobs: parameters: job_name: win2022_cpython image_name: windows-2022 - python_versions: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python_versions: ['3.8', '3.9', '3.10', '3.11', '3.12'] test_suites: all: | call venv\Scripts\activate.bat diff --git a/docs/Makefile b/docs/Makefile index d0c3cbf1..788b0396 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -5,6 +5,7 @@ # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build +SPHINXAUTOBUILD = sphinx-autobuild SOURCEDIR = source BUILDDIR = build @@ -14,6 +15,13 @@ help: .PHONY: help Makefile +# Run the development server using sphinx-autobuild +docs: + @echo + @echo "Starting up the docs server..." + @echo + $(SPHINXAUTOBUILD) --port 8000 --watch ${SOURCEDIR} $(SOURCEDIR) "$(BUILDDIR)/html" $(SPHINXOPTS) $(O) + # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile diff --git a/docs/make.bat b/docs/make.bat index 6247f7e2..4a3c1a48 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -7,11 +7,16 @@ REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) +if "%SPHINXAUTOBUILD%" == "" ( + set SPHINXAUTOBUILD=sphinx-autobuild +) set SOURCEDIR=source set BUILDDIR=build if "%1" == "" goto help +if "%1" == "docs" goto docs + %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. @@ -28,6 +33,13 @@ if errorlevel 9009 ( %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end +:docs +@echo +@echo Starting up the docs server... +@echo +%SPHINXAUTOBUILD% --port 8000 --watch %SOURCEDIR% %SOURCEDIR% %BUILDDIR%\html %SPHINXOPTS% %O% +goto end + :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% diff --git a/docs/scripts/doc8_style_check.sh b/docs/scripts/doc8_style_check.sh old mode 100644 new mode 100755 diff --git a/docs/source/_static/theme_overrides.css b/docs/source/_static/theme_overrides.css index 9662d63a..5863ccf5 100644 --- a/docs/source/_static/theme_overrides.css +++ b/docs/source/_static/theme_overrides.css @@ -1,353 +1,26 @@ -body { - color: #000000; -} - -p { - margin-bottom: 10px; -} - -.wy-plain-list-disc, .rst-content .section ul, .rst-content .toctree-wrapper ul, article ul { - margin-bottom: 10px; -} - -.custom_header_01 { - color: #cc0000; - font-size: 22px; - font-weight: bold; - line-height: 50px; -} - -h1, h2, h3, h4, h5, h6 { - margin-bottom: 20px; - margin-top: 20px; -} - -h5 { - font-size: 18px; - color: #000000; - font-style: italic; - margin-bottom: 10px; -} - -h6 { - font-size: 15px; - color: #000000; - font-style: italic; - margin-bottom: 10px; -} - -/* custom admonitions */ -/* success */ -.custom-admonition-success .admonition-title { - color: #000000; - background: #ccffcc; - border-radius: 5px 5px 0px 0px; -} -div.custom-admonition-success.admonition { - color: #000000; - background: #ffffff; - border: solid 1px #cccccc; - border-radius: 5px; - box-shadow: 1px 1px 5px 3px #d8d8d8; - margin: 20px 0px 30px 0px; -} - -/* important */ -.custom-admonition-important .admonition-title { - color: #000000; - background: #ccffcc; - border-radius: 5px 5px 0px 0px; - border-bottom: solid 1px #000000; -} -div.custom-admonition-important.admonition { - color: #000000; - background: #ffffff; - border: solid 1px #cccccc; - border-radius: 5px; - box-shadow: 1px 1px 5px 3px #d8d8d8; - margin: 20px 0px 30px 0px; -} - -/* caution */ -.custom-admonition-caution .admonition-title { - color: #000000; - background: #ffff99; - border-radius: 5px 5px 0px 0px; - border-bottom: solid 1px #e8e8e8; -} -div.custom-admonition-caution.admonition { - color: #000000; - background: #ffffff; - border: solid 1px #cccccc; - border-radius: 5px; - box-shadow: 1px 1px 5px 3px #d8d8d8; - margin: 20px 0px 30px 0px; -} - -/* note */ -.custom-admonition-note .admonition-title { - color: #ffffff; - background: #006bb3; - border-radius: 5px 5px 0px 0px; -} -div.custom-admonition-note.admonition { - color: #000000; - background: #ffffff; - border: solid 1px #cccccc; - border-radius: 5px; - box-shadow: 1px 1px 5px 3px #d8d8d8; - margin: 20px 0px 30px 0px; -} - -/* todo */ -.custom-admonition-todo .admonition-title { - color: #000000; - background: #cce6ff; - border-radius: 5px 5px 0px 0px; - border-bottom: solid 1px #99ccff; -} -div.custom-admonition-todo.admonition { - color: #000000; - background: #ffffff; - border: solid 1px #99ccff; - border-radius: 5px; - box-shadow: 1px 1px 5px 3px #d8d8d8; - margin: 20px 0px 30px 0px; -} - -/* examples */ -.custom-admonition-examples .admonition-title { - color: #000000; - background: #ffe6cc; - border-radius: 5px 5px 0px 0px; - border-bottom: solid 1px #d8d8d8; -} -div.custom-admonition-examples.admonition { - color: #000000; - background: #ffffff; - border: solid 1px #cccccc; - border-radius: 5px; - box-shadow: 1px 1px 5px 3px #d8d8d8; - margin: 20px 0px 30px 0px; -} - +/* this is the container for the pages */ .wy-nav-content { max-width: 100%; - padding-right: 100px; - padding-left: 100px; - background-color: #f2f2f2; -} - -div.rst-content { - background-color: #ffffff; - border: solid 1px #e5e5e5; - padding: 20px 40px 20px 40px; -} - -.rst-content .guilabel { - border: 1px solid #ffff99; - background: #ffff99; - font-size: 100%; - font-weight: normal; - border-radius: 4px; - padding: 2px 0px; - margin: auto 2px; - vertical-align: middle; -} - -.rst-content kbd { - font-family: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace; - border: solid 1px #d8d8d8; - background-color: #f5f5f5; - padding: 0px 3px; - border-radius: 3px; -} - -.wy-nav-content-wrap a { - color: #0066cc; - text-decoration: none; -} -.wy-nav-content-wrap a:hover { - color: #0099cc; - text-decoration: underline; -} - -.wy-nav-top a { - color: #ffffff; -} - -/* Based on numerous similar approaches e.g., https://github.com/readthedocs/sphinx_rtd_theme/issues/117 and https://rackerlabs.github.io/docs-rackspace/tools/rtd-tables.html -- but remove form-factor limits to enable table wrap on full-size and smallest-size form factors */ -.wy-table-responsive table td { - white-space: normal !important; -} - -.rst-content table.docutils td, -.rst-content table.docutils th { - padding: 5px 10px 5px 10px; -} -.rst-content table.docutils td p, -.rst-content table.docutils th p { - font-size: 14px; - margin-bottom: 0px; -} -.rst-content table.docutils td p cite, -.rst-content table.docutils th p cite { - font-size: 14px; - background-color: transparent; -} - -.colwidths-given th { - border: solid 1px #d8d8d8 !important; -} -.colwidths-given td { - border: solid 1px #d8d8d8 !important; -} - -/*handles single-tick inline code*/ -.wy-body-for-nav cite { - color: #000000; - background-color: transparent; - font-style: normal; - font-family: "Courier New"; - font-size: 13px; - padding: 3px 3px 3px 3px; -} - -.rst-content pre.literal-block, .rst-content div[class^="highlight"] pre, .rst-content .linenodiv pre { - font-family: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace; - font-size: 13px; - overflow: visible; - white-space: pre-wrap; - color: #000000; -} - -.rst-content pre.literal-block, .rst-content div[class^='highlight'] { - background-color: #f8f8f8; - border: solid 1px #e8e8e8; -} - -/* This enables inline code to wrap. */ -code, .rst-content tt, .rst-content code { - white-space: pre-wrap; - padding: 2px 3px 1px; - border-radius: 3px; - font-size: 13px; - background-color: #ffffff; -} - -/* use this added class for code blocks attached to bulleted list items */ -.highlight-top-margin { - margin-top: 20px !important; -} - -/* change color of inline code block */ -span.pre { - color: #e01e5a; -} - -.wy-body-for-nav blockquote { - margin: 1em 0; - padding-left: 1em; - border-left: 4px solid #ddd; - color: #000000; -} - -/* Fix the unwanted top and bottom padding inside a nested bulleted/numbered list */ -.rst-content .section ol p, .rst-content .section ul p { - margin-bottom: 0px; -} - -/* add spacing between bullets for legibility */ -.rst-content .section ol li, .rst-content .section ul li { - margin-bottom: 5px; -} - -.rst-content .section ol li:first-child, .rst-content .section ul li:first-child { - margin-top: 5px; -} - -/* but exclude the toctree bullets */ -.rst-content .toctree-wrapper ul li, .rst-content .toctree-wrapper ul li:first-child { + padding: 0px 40px 0px 0px; margin-top: 0px; - margin-bottom: 0px; } -/* remove extra space at bottom of multine list-table cell */ -.rst-content .line-block { - margin-left: 0px; - margin-bottom: 0px; - line-height: 24px; +.wy-nav-content-wrap { + border-right: solid 1px; } -/* fix extra vertical spacing in page toctree */ -.rst-content .toctree-wrapper ul li ul, article ul li ul { - margin-top: 0; - margin-bottom: 0; -} - -/* this is used by the genindex added via layout.html (see source/_templates/) to sidebar toc */ -.reference.internal.toc-index { - color: #d9d9d9; -} - -.reference.internal.toc-index.current { - background-color: #ffffff; - color: #000000; - font-weight: bold; -} - -.toc-index-div { - border-top: solid 1px #000000; - margin-top: 10px; - padding-top: 5px; -} - -.indextable ul li { - font-size: 14px; - margin-bottom: 5px; -} - -/* The next 2 fix the poor vertical spacing in genindex.html (the alphabetized index) */ -.indextable.genindextable { - margin-bottom: 20px; -} - -div.genindex-jumpbox { - margin-bottom: 10px; -} - -/* rst image classes */ - -.clear-both { - clear: both; - } - -.float-left { - float: left; - margin-right: 20px; -} - -img { - border: solid 1px #e8e8e8; -} - -/* These are custom and need to be defined in conf.py to access in all pages, e.g., '.. role:: red' */ -.img-title { - color: #000000; - /* neither padding nor margin works for vertical spacing bc it's a span -- line-height does, sort of */ - line-height: 3.0; - font-style: italic; - font-weight: 600; -} - -.img-title-para { - color: #000000; - margin-top: 20px; - margin-bottom: 0px; - font-style: italic; - font-weight: 500; -} - -.red { - color: red; +div.rst-content { + max-width: 1300px; + border: 0; + padding: 10px 80px 10px 80px; + margin-left: 50px; +} + +@media (max-width: 768px) { + div.rst-content { + max-width: 1300px; + border: 0; + padding: 0px 10px 10px 10px; + margin-left: 0px; + } } diff --git a/docs/source/conf.py b/docs/source/conf.py index 918d62c1..7771ff09 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -30,6 +30,10 @@ extensions = [ "sphinx.ext.intersphinx", "sphinx_reredirects", + "sphinx_rtd_theme", + "sphinx_rtd_dark_mode", + "sphinx.ext.extlinks", + "sphinx_copybutton", ] @@ -43,7 +47,10 @@ intersphinx_mapping = { "aboutcode": ("https://aboutcode.readthedocs.io/en/latest/", None), - "scancode-workbench": ("https://scancode-workbench.readthedocs.io/en/develop/", None), + "scancode-workbench": ( + "https://scancode-workbench.readthedocs.io/en/develop/", + None, + ), } @@ -78,7 +85,9 @@ "conf_py_path": "/docs/source/", # path in the checkout to the docs root } -html_css_files = ["_static/theme_overrides.css"] +html_css_files = [ + "theme_overrides.css", +] # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. @@ -104,6 +113,4 @@ # -- Options for LaTeX output ------------------------------------------------- -latex_elements = { - 'classoptions': ',openany,oneside' -} \ No newline at end of file +latex_elements = {"classoptions": ",openany,oneside"} diff --git a/setup.cfg b/setup.cfg index 8bb877e6..614979dd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -55,7 +55,7 @@ zip_safe = false setup_requires = setuptools_scm[toml] >= 4 -python_requires = >=3.7 +python_requires = >=3.8 install_requires = attrs @@ -84,4 +84,7 @@ docs = sphinx-rtd-theme>=1.0.0 sphinx-reredirects >= 0.1.2 doc8>=0.11.2 + sphinx-autobuild + sphinx-rtd-dark-mode>=1.3.0 + sphinx-copybutton diff --git a/src/univers/version_constraint.py b/src/univers/version_constraint.py index 17a3ebde..a96e667a 100644 --- a/src/univers/version_constraint.py +++ b/src/univers/version_constraint.py @@ -510,6 +510,10 @@ def contains_version(version, constraints): if not constraints: return False + # If we end up with constraints list contains only one item. + if len(constraints) == 1: + return version in constraints[0] + # Iterate over the current and next contiguous constraints pairs (aka. pairwise) # in the second list. # For each current and next constraint: diff --git a/src/univers/version_range.py b/src/univers/version_range.py index c62d5b21..422d2196 100644 --- a/src/univers/version_range.py +++ b/src/univers/version_range.py @@ -4,6 +4,9 @@ # # Visit https://aboutcode.org and https://github.com/nexB/univers for support and download. +from typing import List +from typing import Union + import attr import semantic_version from packaging.specifiers import InvalidSpecifier @@ -232,6 +235,37 @@ def __eq__(self, other): and self.constraints == other.constraints ) + def normalize(self, known_versions: List[str]): + """ + Return a new VersionRange normalized and simplified using the universe of + ``known_versions`` list of version strings. + """ + versions = sorted([self.version_class(i) for i in known_versions]) + + resolved = [] + contiguous = [] + for kv in versions: + if self.__contains__(kv): + contiguous.append(kv) + elif contiguous: + resolved.append(contiguous) + contiguous = [] + + if contiguous: + resolved.append(contiguous) + + version_constraints = [] + for contiguous_segment in resolved: + lower_bound = contiguous_segment[0] + upper_bound = contiguous_segment[-1] + if lower_bound == upper_bound: + version_constraints.append(VersionConstraint(version=lower_bound)) + else: + version_constraints.append(VersionConstraint(comparator=">=", version=lower_bound)) + version_constraints.append(VersionConstraint(comparator="<=", version=upper_bound)) + + return self.__class__(constraints=version_constraints) + def from_cve_v4(data, scheme): """ @@ -769,7 +803,8 @@ def from_native(cls, string): comparator = ">" constraints.append( VersionConstraint( - comparator=comparator, version=cls.version_class(str(lower_bound)) + comparator=comparator, + version=cls.version_class(str(lower_bound)), ) ) @@ -780,7 +815,8 @@ def from_native(cls, string): comparator = "<" constraints.append( VersionConstraint( - comparator=comparator, version=cls.version_class(str(upper_bound)) + comparator=comparator, + version=cls.version_class(str(upper_bound)), ) ) @@ -1142,19 +1178,31 @@ class MattermostVersionRange(VersionRange): def from_gitlab_native(gitlab_scheme, string): - purl_scheme = PURL_TYPE_BY_GITLAB_SCHEME[gitlab_scheme] + purl_scheme = gitlab_scheme + if gitlab_scheme not in PURL_TYPE_BY_GITLAB_SCHEME.values(): + purl_scheme = PURL_TYPE_BY_GITLAB_SCHEME[gitlab_scheme] + vrc = RANGE_CLASS_BY_SCHEMES[purl_scheme] supported_native_implementations = [ ConanVersionRange, + MavenVersionRange, + NugetVersionRange, ] if vrc in supported_native_implementations: return vrc.from_native(string) constraint_items = [] constraints = [] + split = " " - split_by_comma_schemes = ["pypi", "composer"] - if purl_scheme in split_by_comma_schemes: + if purl_scheme == "pypi": + split = "," + + # GitLab advisory for composer uses both `,` and space for separating constraints. + # https://gitlab.com/gitlab-org/security-products/gemnasium-db/-/blob/8ba4872b659cf5a306e0d47abdd0e428948bf41c/packagist/illuminate/cookie/GHSA-2867-6rrm-38gr.yml + # https://gitlab.com/gitlab-org/security-products/gemnasium-db/-/blob/8ba4872b659cf5a306e0d47abdd0e428948bf41c/packagist/contao-components/mediaelement/CVE-2016-4567.yml + if purl_scheme == "composer" and "," in string: split = "," + pipe_separated_constraints = string.split("||") for pipe_separated_constraint in pipe_separated_constraints: space_seperated_constraints = pipe_separated_constraint.split(split) @@ -1220,7 +1268,7 @@ def build_constraint_from_github_advisory_string(scheme: str, string: str): return VersionConstraint(comparator=comparator, version=version) -def build_range_from_github_advisory_constraint(scheme: str, string: str): +def build_range_from_github_advisory_constraint(scheme: str, string: Union[str, List]): """ Github has a special syntax for version ranges. For example: @@ -1229,7 +1277,7 @@ def build_range_from_github_advisory_constraint(scheme: str, string: str): Github native version range looks like: ``>= 1.0.0, < 1.0.1`` - Return a VersionRange built from a ``string`` single github-native + Return a VersionRange built from a ``string`` single or multiple github-native version relationship string. For example:: @@ -1243,14 +1291,112 @@ def build_range_from_github_advisory_constraint(scheme: str, string: str): >>> vr = build_range_from_github_advisory_constraint("pypi","= 9.0") >>> assert str(vr) == "vers:pypi/9.0" """ - constraint_strings = string.split(",") + if isinstance(string, str): + string = [string] + constraints = [] vrc = RANGE_CLASS_BY_SCHEMES[scheme] - for constraint in constraint_strings: - constraints.append(build_constraint_from_github_advisory_string(scheme, constraint)) + for item in string: + constraint_strings = item.split(",") + + for constraint in constraint_strings: + constraints.append(build_constraint_from_github_advisory_string(scheme, constraint)) return vrc(constraints=constraints) +vers_by_snyk_native_comparators = { + "==": "=", + "=": "=", + "!=": "!=", + "<=": "<=", + ">=": ">=", + "<": "<", + ">": ">", +} + + +def split_req_bracket_notation(string): + """ + Return a tuple of (vers comparator, version) strings given an bracket notation + version requirement ``string`` such as "(2.3" or "3.9]" + + For example:: + + >>> assert split_req_bracket_notation(" 2.3 ]") == ("<=", "2.3") + >>> assert split_req_bracket_notation("( 3.9") == (">", "3.9") + """ + comparators_front = {"(": ">", "[": ">="} + comparators_rear = {")": "<", "]": "<="} + + constraint_string = remove_spaces(string).strip() + + for native_comparator, vers_comparator in comparators_front.items(): + if constraint_string.startswith(native_comparator): + version = constraint_string.lstrip(native_comparator) + return vers_comparator, version + + for native_comparator, vers_comparator in comparators_rear.items(): + if constraint_string.endswith(native_comparator): + version = constraint_string.rstrip(native_comparator) + return vers_comparator, version + + raise ValueError(f"Unknown comparator in version requirement: {string!r} ") + + +def build_range_from_snyk_advisory_string(scheme: str, string: Union[str, List]): + """ + Return a VersionRange built from a ``string`` single or multiple snyk + version relationship string. + Snyk version range looks like: + ">=4.0.0, <4.0.10.16" + ">=4.1.0 <4.4.15.7" + "[3.0.0,3.1.25)" + "(,9.21]" + "[1.4.5,)" + + For example:: + + >>> vr = build_range_from_snyk_advisory_string("pypi", ">=4.0.0, <4.0.10") + >>> assert str(vr) == "vers:pypi/>=4.0.0|<4.0.10" + >>> vr = build_range_from_snyk_advisory_string("golang", ">=9.6.0-rc1 <9.8.1-rc1") + >>> assert str(vr) == "vers:golang/>=9.6.0-rc1|<9.8.1-rc1" + >>> vr = build_range_from_snyk_advisory_string("pypi", "(,9.21]") + >>> assert str(vr) == "vers:pypi/<=9.21" + """ + version_constraints = [] + vrc = RANGE_CLASS_BY_SCHEMES[scheme] + + if isinstance(string, str): + string = [string] + + for item in string: + delimiter = "," if "," in item else " " + if delimiter == ",": + snyk_constraints = item.strip().replace(" ", "") + constraints = snyk_constraints.split(",") + else: + snyk_constraints = item.strip() + constraints = snyk_constraints.split(" ") + + for constraint in constraints: + if any(comp in constraint for comp in "[]()"): + comparator, version = split_req_bracket_notation(string=constraint) + else: + comparator, version = split_req( + string=constraint, + comparators=vers_by_snyk_native_comparators, + ) + if comparator and version: + version = vrc.version_class(version) + version_constraints.append( + VersionConstraint( + comparator=comparator, + version=version, + ) + ) + return vrc(constraints=version_constraints) + + RANGE_CLASS_BY_SCHEMES = { "npm": NpmVersionRange, "deb": DebianVersionRange, diff --git a/tests/data/composer_gitlab.json b/tests/data/composer_gitlab.json index 61c5dbca..60fe38a2 100644 --- a/tests/data/composer_gitlab.json +++ b/tests/data/composer_gitlab.json @@ -7533,7 +7533,7 @@ "test_index": 1256, "scheme": "packagist", "gitlab_native": "<=3.8.0||>=4.0.0-alpha <=4.0.0-rc2", - "expected_vers": "vers:composer/<=3.8.0|>=4.0.0-alpha--4.0.0-rc2" + "expected_vers": "vers:composer/<=3.8.0|>=4.0.0-alpha|<=4.0.0-rc2" }, { "test_index": 1257, diff --git a/tests/test_codestyle.py b/tests/test_codestyle.py index d63b20ee..c0fbd5e0 100644 --- a/tests/test_codestyle.py +++ b/tests/test_codestyle.py @@ -5,14 +5,18 @@ # Visit https://aboutcode.org and https://github.com/nexB/univers for support and download. import subprocess +import sys import unittest +import pytest + +@pytest.mark.skipif(sys.platform == "darwin", reason="Test does not run on macOS13 and older.") class BaseTests(unittest.TestCase): def test_codestyle(self): args = "black --check -l 100 setup.py src tests" try: - subprocess.check_output(args.split()) + subprocess.check_output(args.split(), shell=False) except subprocess.CalledProcessError as e: print("===========================================================") print(e.output) @@ -25,7 +29,7 @@ def test_codestyle(self): args = "isort --check-only src tests setup.py" try: - subprocess.check_output(args.split()) + subprocess.check_output(args.split(), shell=False) except Exception as e: print("===========================================================") print(e.output) diff --git a/tests/test_version_range.py b/tests/test_version_range.py index c92de0b7..8361467c 100644 --- a/tests/test_version_range.py +++ b/tests/test_version_range.py @@ -20,6 +20,7 @@ from univers.version_range import OpensslVersionRange from univers.version_range import PypiVersionRange from univers.version_range import VersionRange +from univers.version_range import build_range_from_snyk_advisory_string from univers.version_range import from_gitlab_native from univers.versions import InvalidVersion from univers.versions import NugetVersion @@ -43,7 +44,7 @@ def test_VersionRange_to_string(self): assert str(version_range) == "vers:pypi/>=0.0.0|0.0.1|0.0.2|0.0.3|0.0.4|0.0.5|0.0.6" def test_VersionRange_pypi_does_not_contain_basic(self): - vers = "vers:pypi/0.0.2|0.0.6|>=0.0.0|0.0.1|0.0.4|0.0.5|0.0.3" + vers = "vers:pypi/0.0.2|0.0.6|>=3.0.0|0.0.1|0.0.4|0.0.5|0.0.3" version_range = VersionRange.from_string(vers) assert not version_range.contains(PypiVersion("2.0.3")) @@ -85,6 +86,11 @@ def test_VersionRange_contains_version_in_between(self): version_range = VersionRange.from_string(vers) assert version_range.contains(PypiVersion("1.5")) + def test_VersionRange_contains_filterd_constraint_edge_case(self): + vers = "vers:pypi/<=1.3.0|3.0.0" + version_range = VersionRange.from_string(vers) + assert version_range.contains(PypiVersion("1.0.0")) + def test_VersionRange_from_string_pypi(self): vers = "vers:pypi/0.0.2|0.0.6|0.0.0|0.0.1|0.0.4|0.0.5|0.0.3" version_range = VersionRange.from_string(vers) @@ -489,3 +495,54 @@ def test_mattermost_version_range(): VersionConstraint(comparator=">=", version=SemverVersion("5.0")), ] ) == VersionRange.from_string("vers:mattermost/>=5.0") + + +def test_build_range_from_snyk_advisory_string(): + expression = [">=4.0.0, <4.0.10", ">7.0.0, <8.0.1"] + vr = build_range_from_snyk_advisory_string("pypi", expression) + expected = "vers:pypi/>=4.0.0|<4.0.10|>7.0.0|<8.0.1" + + assert str(vr) == expected + + +def test_build_range_from_snyk_advisory_string_bracket(): + expression = ["[3.0.0,3.1.25)", "[1.0.0,1.0.5)"] + vr = build_range_from_snyk_advisory_string("nuget", expression) + expected = "vers:nuget/>=1.0.0|<1.0.5|>=3.0.0|<3.1.25" + + assert str(vr) == expected + + +def test_build_range_from_snyk_advisory_string_spaced(): + expression = [">=4.1.0 <4.4.1", ">2.1.0 <=3.2.7"] + vr = build_range_from_snyk_advisory_string("composer", expression) + expected = "vers:composer/>2.1.0|<=3.2.7|>=4.1.0|<4.4.1" + + assert str(vr) == expected + + +def test_version_range_normalize_case1(): + known_versions = ["4.0.0", "3.0.0", "1.0.0", "2.0.0", "1.3.0", "1.1.0", "1.2.0"] + + vr = VersionRange.from_string("vers:pypi/<=1.1.0|>=1.2.0|<=1.3.0|3.0.0") + nvr = vr.normalize(known_versions=known_versions) + + assert str(nvr) == "vers:pypi/>=1.0.0|<=1.3.0|3.0.0" + + +def test_version_range_normalize_case2(): + known_versions = ["4.0.0", "3.0.0", "1.0.0", "2.0.0", "1.3.0", "1.1.0", "1.2.0"] + + vr = VersionRange.from_string("vers:pypi/<=1.3.0|3.0.0") + nvr = vr.normalize(known_versions=known_versions) + + assert str(nvr) == "vers:pypi/>=1.0.0|<=1.3.0|3.0.0" + + +def test_version_range_normalize_case3(): + known_versions = ["4.0.0", "3.0.0", "1.0.0", "2.0.0", "1.3.0", "1.1.0", "1.2.0"] + + vr = VersionRange.from_string("vers:pypi/<2.0.0|3.0.0") + nvr = vr.normalize(known_versions=known_versions) + + assert str(nvr) == "vers:pypi/>=1.0.0|<=1.3.0|3.0.0"