pax_global_header00006660000000000000000000000064147573062450014527gustar00rootroot0000000000000052 comment=59cce1d79d3892bcc3b03887de5e764ffd0e9e66 dep-logic-0.4.11/000077500000000000000000000000001475730624500134555ustar00rootroot00000000000000dep-logic-0.4.11/.github/000077500000000000000000000000001475730624500150155ustar00rootroot00000000000000dep-logic-0.4.11/.github/workflows/000077500000000000000000000000001475730624500170525ustar00rootroot00000000000000dep-logic-0.4.11/.github/workflows/ci.yml000066400000000000000000000011361475730624500201710ustar00rootroot00000000000000name: Tests on: pull_request: paths-ignore: - "*.md" push: branches: - main paths-ignore: - "*.md" jobs: Testing: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.9", "pypy3.10"] steps: - uses: actions/checkout@v4 - name: Set up PDM uses: pdm-project/setup-pdm@v4 with: python-version: ${{ matrix.python-version }} cache: "true" - name: Install packages run: pdm install - name: Run Tests run: pdm run pytest dep-logic-0.4.11/.github/workflows/release.yml000066400000000000000000000022761475730624500212240ustar00rootroot00000000000000name: Release on: push: tags: - "*" jobs: build: name: release-pypi runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-node@v3 with: node-version: 18 - run: npx changelogithub continue-on-error: true env: GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - uses: actions/setup-python@v4 with: python-version: "3.11" - name: Build artifacts run: | pipx run build - name: Store the distribution packages uses: actions/upload-artifact@v4 with: name: python-package-distributions path: dist/ pypi-publish: name: Upload release to PyPI runs-on: ubuntu-latest needs: build environment: name: pypi url: https://pypi.org/p/pkg-logical permissions: id-token: write steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 dep-logic-0.4.11/.gitignore000066400000000000000000000060401475730624500154450ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm-project.org/#use-with-ide .pdm.toml .pdm-python .pdm-build/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ dep-logic-0.4.11/.pre-commit-config.yaml000066400000000000000000000010221475730624500177310ustar00rootroot00000000000000ci: autoupdate_schedule: monthly repos: - repo: https://github.com/asottile/pyupgrade rev: v3.17.0 hooks: - id: pyupgrade args: [--py38-plus] exclude: ^(src/pdm/models/in_process/.*\.py|install-pdm\.py)$ - repo: https://github.com/astral-sh/ruff-pre-commit rev: 'v0.6.3' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix, --show-fixes] - id: ruff-format - repo: https://github.com/RobertCraigie/pyright-python rev: v1.1.378 hooks: - id: pyright dep-logic-0.4.11/LICENSE000066400000000000000000000261141475730624500144660ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2023 Frost Ming Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. dep-logic-0.4.11/README.md000066400000000000000000000041011475730624500147300ustar00rootroot00000000000000# Dep-Logic ![PyPI - Version](https://img.shields.io/pypi/v/dep-logic) ![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Fpdm-project%2Fdep-logic%2Fmain%2Fpyproject.toml) ![GitHub License](https://img.shields.io/github/license/pdm-project/dep-logic) Python dependency specifications supporting logical operations ## Installation ```bash pip install dep-logic ``` This library requires Python 3.8 or later. Currently, it contains two sub-modules: - `dep_logic.specifier` - a module for parsing and calculating PEP 440 version specifiers. - `dep_logic.markers` - a module for parsing and calculating PEP 508 environment markers. ## What does it do? This library allows logic operations on version specifiers and environment markers. For example: ```pycon >>> from dep_logic.specifiers import parse_version_specifier >>> >>> a = parse_version_specifier(">=1.0.0") >>> b = parse_version_specifier("<2.0.0") >>> print(a & b) >=1.0.0,<2.0.0 >>> a = parse_version_specifier(">=1.0.0,<2.0.0") >>> b = parse_version_specifier(">1.5") >>> print(a | b) >=1.0.0 ``` For markers: ```pycon >>> from dep_logic.markers import parse_marker >>> m1 = parse_marker("python_version < '3.8'") >>> m2 = parse_marker("python_version >= '3.6'") >>> print(m1 & m2) python_version < "3.8" and python_version >= "3.6" ``` ## About the project This project is based on @sdispater's [poetry-core](https://github.com/python-poetry/poetry-core) code, but it includes additional packages and a lark parser, which increases the package size and makes it less reusable. Furthermore, `poetry-core` does not always comply with PEP-508. As a result, this project aims to offer a lightweight utility for dependency specification logic using [PyPA's packaging](https://github.com/pypa/packaging). Submodules: - `dep_logic.specifiers` - PEP 440 version specifiers - `dep_logic.markers` - PEP 508 environment markers - `dep_logic.tags` - PEP 425 platform tags ## Caveats Logic operations with `===` specifiers is partially supported. dep-logic-0.4.11/pdm.lock000066400000000000000000000066621475730624500151210ustar00rootroot00000000000000# This file is @generated by PDM. # It is not intended for manual editing. [metadata] groups = ["default", "dev"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.4.2" content_hash = "sha256:7fcb03ae06fee244b9af7cf9efb8e986ad48b8608e7a918f6ca9783f48470b23" [[package]] name = "colorama" version = "0.4.6" requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" summary = "Cross-platform colored terminal text." groups = ["dev"] marker = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] [[package]] name = "exceptiongroup" version = "1.1.3" requires_python = ">=3.7" summary = "Backport of PEP 654 (exception groups)" groups = ["dev"] marker = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, ] [[package]] name = "iniconfig" version = "2.0.0" requires_python = ">=3.7" summary = "brain-dead simple config-ini parsing" groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] [[package]] name = "packaging" version = "23.2" requires_python = ">=3.7" summary = "Core utilities for Python packages" groups = ["default", "dev"] files = [ {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] [[package]] name = "pluggy" version = "1.3.0" requires_python = ">=3.8" summary = "plugin and hook calling mechanisms for python" groups = ["dev"] files = [ {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, ] [[package]] name = "pytest" version = "7.4.3" requires_python = ">=3.7" summary = "pytest: simple powerful testing with Python" groups = ["dev"] dependencies = [ "colorama; sys_platform == \"win32\"", "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", "iniconfig", "packaging", "pluggy<2.0,>=0.12", "tomli>=1.0.0; python_version < \"3.11\"", ] files = [ {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, ] [[package]] name = "tomli" version = "2.0.1" requires_python = ">=3.7" summary = "A lil' TOML parser" groups = ["dev"] marker = "python_version < \"3.11\"" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] dep-logic-0.4.11/pyproject.toml000066400000000000000000000031071475730624500163720ustar00rootroot00000000000000[project] name = "dep-logic" description = "Python dependency specifications supporting logical operations" authors = [ {name = "Frost Ming", email = "me@frostming.com"}, ] dependencies = [ "packaging>=22", ] requires-python = ">=3.8" readme = "README.md" license = {text = "Apache-2.0"} dynamic = ["version"] keywords = ["dependency", "specification", "logic", "packaging"] classifiers = [ "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "License :: OSI Approved :: Apache Software License", ] [build-system] requires = ["pdm-backend"] build-backend = "pdm.backend" [tool.ruff] line-length = 88 src = ["src"] exclude = ["tests/fixtures"] target-version = "py310" [tool.ruff.lint] extend-select = [ "I", # isort "B", # flake8-bugbear "C4", # flake8-comprehensions "PGH", # pygrep-hooks "RUF", # ruff "W", # pycodestyle "YTT", # flake8-2020 ] extend-ignore = ["B018", "B019", "B905"] [tool.ruff.lint.mccabe] max-complexity = 10 [tool.ruff.lint.isort] known-first-party = ["dep_logic"] [tool.pdm.version] source = "scm" [tool.pdm.dev-dependencies] dev = [ "pytest>=7.4.3", ] [tool.pdm.scripts] test = "pytest" [tool.pytest.ini_options] addopts = "-ra" testpaths = [ "src/", "tests/", ] [tool.pyright] venvPath = "." venv = ".venv" pythonVersion = "3.11" reportPrivateImportUsage = "none" dep-logic-0.4.11/src/000077500000000000000000000000001475730624500142445ustar00rootroot00000000000000dep-logic-0.4.11/src/dep_logic/000077500000000000000000000000001475730624500161715ustar00rootroot00000000000000dep-logic-0.4.11/src/dep_logic/__init__.py000066400000000000000000000000001475730624500202700ustar00rootroot00000000000000dep-logic-0.4.11/src/dep_logic/markers/000077500000000000000000000000001475730624500176355ustar00rootroot00000000000000dep-logic-0.4.11/src/dep_logic/markers/__init__.py000066400000000000000000000052551475730624500217550ustar00rootroot00000000000000# Adapted from poetry/core/version/markers.py # The original work is published under the MIT license. # Copyright (c) 2020 Sébastien Eustace # Adapted by Frost Ming (c) 2023 from __future__ import annotations import functools from typing import TYPE_CHECKING from packaging.markers import InvalidMarker as _InvalidMarker from packaging.markers import Marker as _Marker from dep_logic.markers.any import AnyMarker from dep_logic.markers.base import BaseMarker from dep_logic.markers.empty import EmptyMarker from dep_logic.markers.multi import MultiMarker from dep_logic.markers.single import MarkerExpression from dep_logic.markers.union import MarkerUnion from dep_logic.utils import get_reflect_op if TYPE_CHECKING: from typing import List, Literal, Tuple, Union from packaging.markers import Op, Value, Variable _ParsedMarker = Tuple[Variable, Op, Value] _ParsedMarkers = Union[ _ParsedMarker, List[Union["_ParsedMarkers", Literal["or", "and"]]] ] __all__ = [ "parse_marker", "from_pkg_marker", "InvalidMarker", "BaseMarker", "AnyMarker", "EmptyMarker", "MarkerExpression", "MarkerUnion", "MultiMarker", ] class InvalidMarker(ValueError): """ An invalid marker was found, users should refer to PEP 508. """ @functools.lru_cache(maxsize=None) def parse_marker(marker: str) -> BaseMarker: if marker == "": return EmptyMarker() if not marker or marker == "*": return AnyMarker() try: parsed = _Marker(marker) except _InvalidMarker as e: raise InvalidMarker(str(e)) from e markers = _build_markers(parsed._markers) return markers def from_pkg_marker(marker: _Marker) -> BaseMarker: return _build_markers(marker._markers) def _build_markers(markers: _ParsedMarkers) -> BaseMarker: from packaging.markers import Variable if isinstance(markers, tuple): if isinstance(markers[0], Variable): name, op, value, reversed = ( str(markers[0]), str(markers[1]), str(markers[2]), False, ) else: # in reverse order name, op, value, reversed = ( str(markers[2]), get_reflect_op(str(markers[1])), str(markers[0]), True, ) return MarkerExpression(name, op, value, reversed) or_groups: list[BaseMarker] = [AnyMarker()] for item in markers: if item == "or": or_groups.append(AnyMarker()) elif item == "and": continue else: or_groups[-1] &= _build_markers(item) return MarkerUnion.of(*or_groups) dep-logic-0.4.11/src/dep_logic/markers/any.py000066400000000000000000000017671475730624500210110ustar00rootroot00000000000000from __future__ import annotations from dep_logic.markers.base import BaseMarker class AnyMarker(BaseMarker): def __and__(self, other: BaseMarker) -> BaseMarker: return other __rand__ = __and__ def __or__(self, other: BaseMarker) -> BaseMarker: return self __ror__ = __or__ def is_any(self) -> bool: return True def evaluate(self, environment: dict[str, str] | None = None) -> bool: return True def without_extras(self) -> BaseMarker: return self def exclude(self, marker_name: str) -> BaseMarker: return self def only(self, *marker_names: str) -> BaseMarker: return self def __str__(self) -> str: return "" def __repr__(self) -> str: return "" def __hash__(self) -> int: return hash("any") def __eq__(self, other: object) -> bool: if not isinstance(other, BaseMarker): return NotImplemented return isinstance(other, AnyMarker) dep-logic-0.4.11/src/dep_logic/markers/base.py000066400000000000000000000024231475730624500211220ustar00rootroot00000000000000from __future__ import annotations from abc import ABCMeta, abstractmethod from typing import Any class BaseMarker(metaclass=ABCMeta): @property def complexity(self) -> tuple[int, ...]: """ The first number is the number of marker expressions, and the second number is 1 if the marker is single-like. """ return 1, 1 @abstractmethod def __and__(self, other: Any) -> BaseMarker: raise NotImplementedError @abstractmethod def __or__(self, other: Any) -> BaseMarker: raise NotImplementedError def is_any(self) -> bool: return False def is_empty(self) -> bool: return False @abstractmethod def evaluate(self, environment: dict[str, str] | None = None) -> bool: raise NotImplementedError @abstractmethod def without_extras(self) -> BaseMarker: raise NotImplementedError @abstractmethod def exclude(self, marker_name: str) -> BaseMarker: raise NotImplementedError @abstractmethod def only(self, *marker_names: str) -> BaseMarker: raise NotImplementedError def __repr__(self) -> str: return f"<{self.__class__.__name__} {self}>" @abstractmethod def __str__(self) -> str: raise NotImplementedError dep-logic-0.4.11/src/dep_logic/markers/empty.py000066400000000000000000000020111475730624500213370ustar00rootroot00000000000000from __future__ import annotations from dep_logic.markers.base import BaseMarker class EmptyMarker(BaseMarker): def __and__(self, other: BaseMarker) -> BaseMarker: return self __rand__ = __and__ def __or__(self, other: BaseMarker) -> BaseMarker: return other __ror__ = __or__ def is_empty(self) -> bool: return True def evaluate(self, environment: dict[str, str] | None = None) -> bool: return False def without_extras(self) -> BaseMarker: return self def exclude(self, marker_name: str) -> BaseMarker: return self def only(self, *marker_names: str) -> BaseMarker: return self def __str__(self) -> str: return "" def __repr__(self) -> str: return "" def __hash__(self) -> int: return hash("empty") def __eq__(self, other: object) -> bool: if not isinstance(other, BaseMarker): return NotImplemented return isinstance(other, EmptyMarker) dep-logic-0.4.11/src/dep_logic/markers/multi.py000066400000000000000000000135451475730624500213510ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import Iterator from dep_logic.markers.any import AnyMarker from dep_logic.markers.base import BaseMarker from dep_logic.markers.empty import EmptyMarker from dep_logic.markers.single import MarkerExpression, SingleMarker from dep_logic.utils import DATACLASS_ARGS, flatten_items, intersection, union @dataclass(init=False, frozen=True, unsafe_hash=True, **DATACLASS_ARGS) class MultiMarker(BaseMarker): markers: tuple[BaseMarker, ...] def __init__(self, *markers: BaseMarker) -> None: object.__setattr__(self, "markers", tuple(flatten_items(markers, MultiMarker))) def __iter__(self) -> Iterator[BaseMarker]: return iter(self.markers) @property def complexity(self) -> tuple[int, ...]: return tuple(sum(c) for c in zip(*(m.complexity for m in self.markers))) @classmethod def of(cls, *markers: BaseMarker) -> BaseMarker: from dep_logic.markers.union import MarkerUnion new_markers = flatten_items(markers, MultiMarker) old_markers: list[BaseMarker] = [] while old_markers != new_markers: old_markers = new_markers new_markers = [] for marker in old_markers: if marker in new_markers: continue if marker.is_any(): continue intersected = False for i, mark in enumerate(new_markers): # If we have a SingleMarker then with any luck after intersection # it'll become another SingleMarker. if isinstance(mark, SingleMarker): new_marker = mark & marker if new_marker.is_empty(): return EmptyMarker() if isinstance(new_marker, SingleMarker): new_markers[i] = new_marker intersected = True break # If we have a MarkerUnion then we can look for the simplifications # implemented in intersect_simplify(). elif isinstance(mark, MarkerUnion): intersection = mark.intersect_simplify(marker) if intersection is not None: new_markers[i] = intersection intersected = True break if intersected: # flatten again because intersect_simplify may return a multi new_markers = flatten_items(new_markers, MultiMarker) continue new_markers.append(marker) if any(m.is_empty() for m in new_markers): return EmptyMarker() if not new_markers: return AnyMarker() if len(new_markers) == 1: return new_markers[0] return MultiMarker(*new_markers) def __and__(self, other: BaseMarker) -> BaseMarker: return intersection(self, other) def __or__(self, other: BaseMarker) -> BaseMarker: return union(self, other) __rand__ = __and__ __ror__ = __or__ def union_simplify(self, other: BaseMarker) -> BaseMarker | None: """ Finds a couple of easy simplifications for union on MultiMarkers: - union with any marker that appears as part of the multi is just that marker - union between two multimarkers where one is contained by the other is just the larger of the two - union between two multimarkers where there are some common markers and the union of unique markers is a single marker """ if other in self.markers: return other if isinstance(other, MultiMarker): our_markers = set(self.markers) their_markers = set(other.markers) if our_markers.issubset(their_markers): return self if their_markers.issubset(our_markers): return other shared_markers = our_markers.intersection(their_markers) if not shared_markers: return None unique_markers = our_markers - their_markers other_unique_markers = their_markers - our_markers unique_union = MultiMarker(*unique_markers) | ( MultiMarker(*other_unique_markers) ) if isinstance(unique_union, (SingleMarker, AnyMarker)): # Use list instead of set for deterministic order. common_markers = [ marker for marker in self.markers if marker in shared_markers ] return unique_union & MultiMarker(*common_markers) return None def evaluate(self, environment: dict[str, str] | None = None) -> bool: return all(m.evaluate(environment) for m in self.markers) def without_extras(self) -> BaseMarker: return self.exclude("extra") def exclude(self, marker_name: str) -> BaseMarker: new_markers = [] for m in self.markers: if isinstance(m, SingleMarker) and m.name == marker_name: # The marker is not relevant since it must be excluded continue marker = m.exclude(marker_name) if not marker.is_empty(): new_markers.append(marker) return self.of(*new_markers) def only(self, *marker_names: str) -> BaseMarker: return self.of(*(m.only(*marker_names) for m in self.markers)) def __str__(self) -> str: elements = [] for m in self.markers: if isinstance(m, (MarkerExpression, MultiMarker)): elements.append(str(m)) else: elements.append(f"({m})") return " and ".join(elements) dep-logic-0.4.11/src/dep_logic/markers/single.py000066400000000000000000000327451475730624500215030ustar00rootroot00000000000000from __future__ import annotations import functools import typing as t from dataclasses import dataclass, field, replace from packaging.markers import Marker as _Marker from dep_logic.markers.any import AnyMarker from dep_logic.markers.base import BaseMarker from dep_logic.markers.empty import EmptyMarker from dep_logic.specifiers import BaseSpecifier from dep_logic.specifiers.base import VersionSpecifier from dep_logic.specifiers.generic import GenericSpecifier from dep_logic.utils import DATACLASS_ARGS, OrderedSet, get_reflect_op if t.TYPE_CHECKING: from dep_logic.markers.multi import MultiMarker from dep_logic.markers.union import MarkerUnion PYTHON_VERSION_MARKERS = {"python_version", "python_full_version"} class SingleMarker(BaseMarker): name: str _VERSION_LIKE_MARKER_NAME: t.ClassVar[set[str]] = { "python_version", "python_full_version", "platform_release", } def without_extras(self) -> BaseMarker: return self.exclude("extra") def exclude(self, marker_name: str) -> BaseMarker: if self.name == marker_name: return AnyMarker() return self def only(self, *marker_names: str) -> BaseMarker: if self.name not in marker_names: return AnyMarker() return self def evaluate(self, environment: dict[str, str] | None = None) -> bool: pkg_marker = _Marker(str(self)) if self.name != "extra" or not environment or "extra" not in environment: return pkg_marker.evaluate(environment) extras = [extra] if isinstance(extra := environment["extra"], str) else extra assert isinstance(self, MarkerExpression) is_negated = self.op in ("not in", "!=") if is_negated: return all(pkg_marker.evaluate({"extra": extra}) for extra in extras) return any(pkg_marker.evaluate({"extra": extra}) for extra in extras) @dataclass(unsafe_hash=True, **DATACLASS_ARGS) class MarkerExpression(SingleMarker): name: str op: str value: str reversed: bool = field(default=False, compare=False, hash=False) _specifier: BaseSpecifier | None = field(default=None, compare=False, hash=False) @property def specifier(self) -> BaseSpecifier: if self._specifier is None: self._specifier = self._get_specifier() return self._specifier @classmethod def from_specifier(cls, name: str, specifier: BaseSpecifier) -> BaseMarker | None: if specifier.is_any(): return AnyMarker() if specifier.is_empty(): return EmptyMarker() if isinstance(specifier, VersionSpecifier): if not specifier.is_simple(): return None pkg_spec = next(iter(specifier.to_specifierset())) pkg_version = pkg_spec.version if ( dot_num := pkg_version.count(".") ) < 2 and name == "python_full_version": for _ in range(2 - dot_num): pkg_version += ".0" return MarkerExpression( name, pkg_spec.operator, pkg_version, _specifier=specifier ) assert isinstance(specifier, GenericSpecifier) return MarkerExpression( name, specifier.op, specifier.value, _specifier=specifier ) def _get_specifier(self) -> BaseSpecifier: from dep_logic.specifiers import parse_version_specifier if self.name not in self._VERSION_LIKE_MARKER_NAME: return GenericSpecifier(self.op, self.value) if self.op in ("in", "not in"): versions: list[str] = [] op, glue = ("==", "||") if self.op == "in" else ("!=", ",") for part in self.value.split(","): splitted = part.strip().split(".") if part_num := len(splitted) < 3: if self.name == "python_version": splitted.append("*") else: splitted.extend(["0"] * (3 - part_num)) versions.append(op + ".".join(splitted)) return parse_version_specifier(glue.join(versions)) return parse_version_specifier(f"{self.op}{self.value}") def __str__(self) -> str: if self.reversed: return f'"{self.value}" {get_reflect_op(self.op)} {self.name}' return f'{self.name} {self.op} "{self.value}"' def __and__(self, other: t.Any) -> BaseMarker: from dep_logic.markers.multi import MultiMarker if not isinstance(other, MarkerExpression): return NotImplemented merged = _merge_single_markers(self, other, MultiMarker) if merged is not None: return merged return MultiMarker(self, other) def __or__(self, other: t.Any) -> BaseMarker: from dep_logic.markers.union import MarkerUnion if not isinstance(other, MarkerExpression): return NotImplemented merged = _merge_single_markers(self, other, MarkerUnion) if merged is not None: return merged return MarkerUnion(self, other) @dataclass(frozen=True, unsafe_hash=True, **DATACLASS_ARGS) class EqualityMarkerUnion(SingleMarker): name: str values: OrderedSet[str] def __str__(self) -> str: return " or ".join(f'{self.name} == "{value}"' for value in self.values) def replace(self, values: OrderedSet[str]) -> BaseMarker: if not values: return EmptyMarker() if len(values) == 1: return MarkerExpression(self.name, "==", values.peek()) return replace(self, values=values) @property def complexity(self) -> tuple[int, ...]: return len(self.values), 1 def __and__(self, other: t.Any) -> BaseMarker: from dep_logic.markers.multi import MultiMarker if not isinstance(other, SingleMarker): return NotImplemented if self.name != other.name: return MultiMarker(self, other) if isinstance(other, MarkerExpression): new_values = OrderedSet([v for v in self.values if v in other.specifier]) return self.replace(new_values) elif isinstance(other, EqualityMarkerUnion): new_values = self.values & other.values return self.replace(t.cast(OrderedSet, new_values)) else: # intersection with InequalityMarkerUnion will be handled in the other class return NotImplemented def __or__(self, other: t.Any) -> BaseMarker: from dep_logic.markers.union import MarkerUnion if not isinstance(other, SingleMarker): return NotImplemented if self.name != other.name: return MarkerUnion(self, other) if isinstance(other, MarkerExpression): if other.op == "==": if other.value in self.values: return self return replace(self, values=self.values | {other.value}) if other.op == "!=": if other.value in self.values: AnyMarker() return other if all(v in other.specifier for v in self.values): return other else: return MarkerUnion(self, other) elif isinstance(other, EqualityMarkerUnion): return replace(self, values=self.values | other.values) else: # intersection with InequalityMarkerUnion will be handled in the other class return NotImplemented __rand__ = __and__ __ror__ = __or__ @dataclass(frozen=True, unsafe_hash=True, **DATACLASS_ARGS) class InequalityMultiMarker(SingleMarker): name: str values: OrderedSet[str] def __str__(self) -> str: return " and ".join(f'{self.name} != "{value}"' for value in self.values) def replace(self, values: OrderedSet[str]) -> BaseMarker: if not values: return AnyMarker() if len(values) == 1: return MarkerExpression(self.name, "!=", values.peek()) return replace(self, values=values) @property def complexity(self) -> tuple[int, ...]: return len(self.values), 1 def __and__(self, other: t.Any) -> BaseMarker: from dep_logic.markers.multi import MultiMarker if not isinstance(other, SingleMarker): return NotImplemented if self.name != other.name: return MultiMarker(self, other) if isinstance(other, MarkerExpression): if other.op == "==": if other.value in self.values: return EmptyMarker() return other elif other.op == "!=": if other.value in self.values: return self return replace(self, values=self.values | {other.value}) elif not any(v in other.specifier for v in self.values): return other else: return MultiMarker(self, other) elif isinstance(other, EqualityMarkerUnion): new_values = other.values - self.values return other.replace(t.cast(OrderedSet, new_values)) else: assert isinstance(other, InequalityMultiMarker) return replace(self, values=self.values | other.values) def __or__(self, other: t.Any) -> BaseMarker: from dep_logic.markers.union import MarkerUnion if not isinstance(other, SingleMarker): return NotImplemented if self.name != other.name: return MarkerUnion(self, other) if isinstance(other, MarkerExpression): new_values = OrderedSet( [v for v in self.values if v not in other.specifier] ) return self.replace(new_values) elif isinstance(other, EqualityMarkerUnion): new_values = self.values - other.values return self.replace(t.cast(OrderedSet, new_values)) else: assert isinstance(other, InequalityMultiMarker) new_values = self.values & other.values return self.replace(t.cast(OrderedSet, new_values)) __rand__ = __and__ __ror__ = __or__ @functools.lru_cache(maxsize=None) def _merge_single_markers( marker1: MarkerExpression, marker2: MarkerExpression, merge_class: type[MultiMarker | MarkerUnion], ) -> BaseMarker | None: from dep_logic.markers.multi import MultiMarker from dep_logic.markers.union import MarkerUnion if {marker1.name, marker2.name} == PYTHON_VERSION_MARKERS: return _merge_python_version_single_markers(marker1, marker2, merge_class) if marker1.name != marker2.name: return None # "extra" is special because it can have multiple values at the same time. # That's why we can only merge two "extra" markers if they have the same value. if marker1.name == "extra": if marker1.value != marker2.value: # type: ignore[attr-defined] return None try: if merge_class is MultiMarker: result_specifier = marker1.specifier & marker2.specifier else: result_specifier = marker1.specifier | marker2.specifier except NotImplementedError: if marker1.op == marker2.op == "==" and merge_class is MarkerUnion: return EqualityMarkerUnion( marker1.name, OrderedSet([marker1.value, marker2.value]) ) elif marker1.op == marker2.op == "!=" and merge_class is MultiMarker: return InequalityMultiMarker( marker1.name, OrderedSet([marker1.value, marker2.value]) ) return None else: if result_specifier == marker1.specifier: return marker1 if result_specifier == marker2.specifier: return marker2 return MarkerExpression.from_specifier(marker1.name, result_specifier) def _merge_python_version_single_markers( marker1: MarkerExpression, marker2: MarkerExpression, merge_class: type[MultiMarker | MarkerUnion], ) -> BaseMarker | None: from dep_logic.markers.multi import MultiMarker if marker1.name == "python_version": version_marker = marker1 full_version_marker = marker2 else: version_marker = marker2 full_version_marker = marker1 normalized_specifier = _normalize_python_version_specifier(version_marker) if merge_class is MultiMarker: merged = normalized_specifier & full_version_marker.specifier else: merged = normalized_specifier | full_version_marker.specifier if merged == normalized_specifier: # prefer original marker to avoid unnecessary changes return version_marker return MarkerExpression.from_specifier("python_full_version", merged) def _normalize_python_version_specifier(marker: MarkerExpression) -> BaseSpecifier: from dep_logic.specifiers import parse_version_specifier op, value = marker.op, marker.value if op in ("in", "not in"): # skip this case, so in the following code value must be a dotted version string return marker.specifier splitted = [p.strip() for p in value.split(".")] if len(splitted) > 2 or "*" in splitted: return marker.specifier if op in ("==", "!="): splitted.append("*") elif op == ">": # python_version > '3.7' is equal to python_full_version >= '3.8.0' splitted[-1] = str(int(splitted[-1]) + 1) op = ">=" elif op == "<=": # python_version <= '3.7' is equal to python_full_version < '3.8.0' splitted[-1] = str(int(splitted[-1]) + 1) op = "<" spec = parse_version_specifier(f'{op}{".".join(splitted)}') return spec dep-logic-0.4.11/src/dep_logic/markers/union.py000066400000000000000000000133231475730624500213410ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import Iterator from dep_logic.markers.any import AnyMarker from dep_logic.markers.base import BaseMarker from dep_logic.markers.empty import EmptyMarker from dep_logic.markers.multi import MultiMarker from dep_logic.markers.single import SingleMarker from dep_logic.utils import DATACLASS_ARGS, flatten_items, intersection, union @dataclass(init=False, frozen=True, unsafe_hash=True, **DATACLASS_ARGS) class MarkerUnion(BaseMarker): markers: tuple[BaseMarker, ...] def __init__(self, *markers: BaseMarker) -> None: object.__setattr__(self, "markers", tuple(flatten_items(markers, MarkerUnion))) def __iter__(self) -> Iterator[BaseMarker]: return iter(self.markers) @property def complexity(self) -> tuple[int, ...]: return tuple(sum(c) for c in zip(*(m.complexity for m in self.markers))) @classmethod def of(cls, *markers: BaseMarker) -> BaseMarker: new_markers = flatten_items(markers, MarkerUnion) old_markers: list[BaseMarker] = [] while old_markers != new_markers: old_markers = new_markers new_markers = [] for marker in old_markers: if marker in new_markers: continue if marker.is_empty(): continue included = False for i, mark in enumerate(new_markers): # If we have a SingleMarker then with any luck after union it'll # become another SingleMarker. if isinstance(mark, SingleMarker): new_marker = mark | marker if new_marker.is_any(): return AnyMarker() if isinstance(new_marker, SingleMarker): new_markers[i] = new_marker included = True break # If we have a MultiMarker then we can look for the simplifications # implemented in union_simplify(). elif isinstance(mark, MultiMarker): union = mark.union_simplify(marker) if union is not None: new_markers[i] = union included = True break if included: # flatten again because union_simplify may return a union new_markers = flatten_items(new_markers, MarkerUnion) continue new_markers.append(marker) if any(m.is_any() for m in new_markers): return AnyMarker() if not new_markers: return EmptyMarker() if len(new_markers) == 1: return new_markers[0] return MarkerUnion(*new_markers) def __and__(self, other: BaseMarker) -> BaseMarker: return intersection(self, other) def __or__(self, other: BaseMarker) -> BaseMarker: return union(self, other) __rand__ = __and__ __ror__ = __or__ def intersect_simplify(self, other: BaseMarker) -> BaseMarker | None: """ Finds a couple of easy simplifications for intersection on MarkerUnions: - intersection with any marker that appears as part of the union is just that marker - intersection between two markerunions where one is contained by the other is just the smaller of the two - intersection between two markerunions where there are some common markers and the intersection of unique markers is not a single marker """ if other in self.markers: return other if isinstance(other, MarkerUnion): our_markers = set(self.markers) their_markers = set(other.markers) if our_markers.issubset(their_markers): return self if their_markers.issubset(our_markers): return other shared_markers = our_markers.intersection(their_markers) if not shared_markers: return None unique_markers = our_markers - their_markers other_unique_markers = their_markers - our_markers unique_intersection = MarkerUnion(*unique_markers) & MarkerUnion( *other_unique_markers ) if isinstance(unique_intersection, (SingleMarker, EmptyMarker)): # Use list instead of set for deterministic order. common_markers = [ marker for marker in self.markers if marker in shared_markers ] return unique_intersection | MarkerUnion(*common_markers) return None def evaluate(self, environment: dict[str, str] | None = None) -> bool: return any(m.evaluate(environment) for m in self.markers) def without_extras(self) -> BaseMarker: return self.exclude("extra") def exclude(self, marker_name: str) -> BaseMarker: new_markers = [] for m in self.markers: if isinstance(m, SingleMarker) and m.name == marker_name: # The marker is not relevant since it must be excluded continue marker = m.exclude(marker_name) new_markers.append(marker) if not new_markers: # All markers were the excluded marker. return AnyMarker() return self.of(*new_markers) def only(self, *marker_names: str) -> BaseMarker: return self.of(*(m.only(*marker_names) for m in self.markers)) def __str__(self) -> str: return " or ".join(str(m) for m in self.markers) dep-logic-0.4.11/src/dep_logic/markers/utils.py000066400000000000000000000065401475730624500213540ustar00rootroot00000000000000from __future__ import annotations import functools import itertools from typing import AbstractSet, Iterable, Iterator, TypeVar from dep_logic.markers.base import BaseMarker T = TypeVar("T") @functools.lru_cache(maxsize=None) def cnf(marker: BaseMarker) -> BaseMarker: from dep_logic.markers.multi import MultiMarker from dep_logic.markers.union import MarkerUnion """Transforms the marker into CNF (conjunctive normal form).""" if isinstance(marker, MarkerUnion): cnf_markers = [cnf(m) for m in marker.markers] sub_marker_lists = [ m.markers if isinstance(m, MultiMarker) else [m] for m in cnf_markers ] return MultiMarker.of( *[MarkerUnion.of(*c) for c in itertools.product(*sub_marker_lists)] ) if isinstance(marker, MultiMarker): return MultiMarker.of(*[cnf(m) for m in marker.markers]) return marker @functools.lru_cache(maxsize=None) def dnf(marker: BaseMarker) -> BaseMarker: """Transforms the marker into DNF (disjunctive normal form).""" from dep_logic.markers.multi import MultiMarker from dep_logic.markers.union import MarkerUnion if isinstance(marker, MultiMarker): dnf_markers = [dnf(m) for m in marker.markers] sub_marker_lists = [ m.markers if isinstance(m, MarkerUnion) else [m] for m in dnf_markers ] return MarkerUnion.of( *[MultiMarker.of(*c) for c in itertools.product(*sub_marker_lists)] ) if isinstance(marker, MarkerUnion): return MarkerUnion.of(*[dnf(m) for m in marker.markers]) return marker def intersection(*markers: BaseMarker) -> BaseMarker: from dep_logic.markers.multi import MultiMarker return dnf(MultiMarker(*markers)) def union(*markers: BaseMarker) -> BaseMarker: from dep_logic.markers.multi import MultiMarker from dep_logic.markers.union import MarkerUnion # Sometimes normalization makes it more complicate instead of simple # -> choose candidate with the least complexity unnormalized: BaseMarker = MarkerUnion(*markers) while ( isinstance(unnormalized, (MultiMarker, MarkerUnion)) and len(unnormalized.markers) == 1 ): unnormalized = unnormalized.markers[0] conjunction = cnf(unnormalized) if not isinstance(conjunction, MultiMarker): return conjunction disjunction = dnf(conjunction) if not isinstance(disjunction, MarkerUnion): return disjunction return min(disjunction, conjunction, unnormalized, key=lambda x: x.complexity) _op_reflect_map = { "<": ">", "<=": ">=", ">": "<", ">=": "<=", "==": "==", "!=": "!=", "===": "===", "~=": "~=", "in": "in", "not in": "not in", } def get_reflect_op(op: str) -> str: return _op_reflect_map[op] class OrderedSet(AbstractSet[T]): def __init__(self, iterable: Iterable[T]) -> None: self._data: list[T] = [] for item in iterable: if item in self._data: continue self._data.append(item) def __hash__(self) -> int: return self._hash() def __contains__(self, obj: object) -> bool: return obj in self._data def __iter__(self) -> Iterator[T]: return iter(self._data) def __len__(self) -> int: return len(self._data) def peek(self) -> T: return self._data[0] dep-logic-0.4.11/src/dep_logic/py.typed000066400000000000000000000000001475730624500176560ustar00rootroot00000000000000dep-logic-0.4.11/src/dep_logic/specifiers/000077500000000000000000000000001475730624500203255ustar00rootroot00000000000000dep-logic-0.4.11/src/dep_logic/specifiers/__init__.py000066400000000000000000000102731475730624500224410ustar00rootroot00000000000000from __future__ import annotations import functools import itertools import operator from packaging.specifiers import InvalidSpecifier as PkgInvalidSpecifier from packaging.specifiers import Specifier, SpecifierSet from packaging.version import Version from dep_logic.specifiers.arbitrary import ArbitrarySpecifier from dep_logic.specifiers.base import ( BaseSpecifier, InvalidSpecifier, VersionSpecifier, ) from dep_logic.specifiers.generic import GenericSpecifier from dep_logic.specifiers.range import RangeSpecifier from dep_logic.specifiers.special import AnySpecifier, EmptySpecifier from dep_logic.specifiers.union import UnionSpecifier from dep_logic.utils import is_not_suffix, version_split def from_specifierset(spec: SpecifierSet) -> VersionSpecifier: """Convert from a packaging.specifiers.SpecifierSet object.""" return functools.reduce( operator.and_, map(_from_pkg_specifier, spec), RangeSpecifier() ) def _from_pkg_specifier(spec: Specifier) -> VersionSpecifier: version = spec.version min: Version | None = None max: Version | None = None include_min = False include_max = False if (op := spec.operator) in (">", ">="): min = Version(version) include_min = spec.operator == ">=" elif op in ("<", "<="): max = Version(version) include_max = spec.operator == "<=" elif op == "==": if "*" not in version: min = Version(version) max = Version(version) include_min = True include_max = True else: version_parts = list( itertools.takewhile(lambda x: x != "*", version_split(version)) ) min = Version(".".join([*version_parts, "0"])) version_parts[-1] = str(int(version_parts[-1]) + 1) max = Version(".".join([*version_parts, "0"])) include_min = True include_max = False elif op == "~=": min = Version(version) version_parts = list( itertools.takewhile(is_not_suffix, version_split(version)) )[:-1] version_parts[-1] = str(int(version_parts[-1]) + 1) max = Version(".".join([*version_parts, "0"])) include_min = True include_max = False elif op == "!=": if "*" not in version: v = Version(version) return UnionSpecifier( ( RangeSpecifier(max=v, include_max=False), RangeSpecifier(min=v, include_min=False), ), simplified=str(spec), ) else: version_parts = list( itertools.takewhile(lambda x: x != "*", version_split(version)) ) left = Version(".".join([*version_parts, "0"])) version_parts[-1] = str(int(version_parts[-1]) + 1) right = Version(".".join([*version_parts, "0"])) return UnionSpecifier( ( RangeSpecifier(max=left, include_max=False), RangeSpecifier(min=right, include_min=True), ), simplified=str(spec), ) elif op == "===": return ArbitrarySpecifier(target=version) else: raise InvalidSpecifier(f'Unsupported operator "{op}" in specifier "{spec}"') return RangeSpecifier( min=min, max=max, include_min=include_min, include_max=include_max, simplified=str(spec), ) def parse_version_specifier(spec: str) -> BaseSpecifier: """Parse a specifier string.""" if spec == "": return EmptySpecifier() if "||" in spec: return functools.reduce( operator.or_, map(parse_version_specifier, spec.split("||")) ) try: pkg_spec = SpecifierSet(spec) except PkgInvalidSpecifier as e: raise InvalidSpecifier(str(e)) from e else: return from_specifierset(pkg_spec) __all__ = [ "from_specifierset", "parse_version_specifier", "VersionSpecifier", "EmptySpecifier", "AnySpecifier", "RangeSpecifier", "UnionSpecifier", "BaseSpecifier", "GenericSpecifier", "ArbitrarySpecifier", "InvalidSpecifier", ] dep-logic-0.4.11/src/dep_logic/specifiers/arbitrary.py000066400000000000000000000036601475730624500227030ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import Any from packaging.specifiers import SpecifierSet from dep_logic.specifiers.base import BaseSpecifier, UnparsedVersion, VersionSpecifier from dep_logic.specifiers.special import EmptySpecifier from dep_logic.utils import DATACLASS_ARGS @dataclass(frozen=True, unsafe_hash=True, **DATACLASS_ARGS) class ArbitrarySpecifier(VersionSpecifier): """`===` specifier.""" target: str def to_specifierset(self) -> SpecifierSet: return SpecifierSet(f"==={self.target}") def __str__(self) -> str: return f"==={self.target}" def contains( self, version: UnparsedVersion, prereleases: bool | None = None ) -> bool: return str(version) == self.target @property def num_parts(self) -> int: return 1 def is_simple(self) -> bool: return True def __invert__(self) -> BaseSpecifier: raise ValueError("Cannot invert an ArbitrarySpecifier") def __and__(self, other: Any) -> BaseSpecifier: if not isinstance(other, VersionSpecifier): return NotImplemented if other.is_empty(): return other try: if other.is_any() or self.target in other: return self return EmptySpecifier() except ValueError: raise ValueError( f"Unsupported intersection of '{self}' and '{other}'" ) from None __rand__ = __and__ def __or__(self, other: Any) -> BaseSpecifier: if not isinstance(other, VersionSpecifier): return NotImplemented if other.is_empty(): return self try: if other.is_any() or self.target in other: return other except ValueError: pass raise ValueError(f"Unsupported union of '{self}' and '{other}'") from None __ror__ = __or__ dep-logic-0.4.11/src/dep_logic/specifiers/base.py000066400000000000000000000033411475730624500216120ustar00rootroot00000000000000from __future__ import annotations import abc import typing as t from packaging.specifiers import SpecifierSet from packaging.version import Version UnparsedVersion = t.Union[Version, str] class InvalidSpecifier(ValueError): pass class BaseSpecifier(metaclass=abc.ABCMeta): @abc.abstractmethod def __str__(self) -> str: """ Returns the str representation of this Specifier-like object. This should be representative of the Specifier itself. """ @abc.abstractmethod def __and__(self, other: t.Any) -> BaseSpecifier: raise NotImplementedError @abc.abstractmethod def __or__(self, other: t.Any) -> BaseSpecifier: raise NotImplementedError @abc.abstractmethod def __invert__(self) -> BaseSpecifier: raise NotImplementedError def is_simple(self) -> bool: return False def __repr__(self) -> str: return f"<{self.__class__.__name__} {self}>" def is_empty(self) -> bool: return False def is_any(self) -> bool: return False @abc.abstractmethod def __contains__(self, value: str) -> bool: raise NotImplementedError class VersionSpecifier(BaseSpecifier): @abc.abstractmethod def contains( self, version: UnparsedVersion, prereleases: bool | None = None ) -> bool: raise NotImplementedError @property @abc.abstractmethod def num_parts(self) -> int: raise NotImplementedError def __contains__(self, version: UnparsedVersion) -> bool: return self.contains(version) @abc.abstractmethod def to_specifierset(self) -> SpecifierSet: """Convert to a packaging.specifiers.SpecifierSet object.""" raise NotImplementedError dep-logic-0.4.11/src/dep_logic/specifiers/generic.py000066400000000000000000000070741475730624500223230ustar00rootroot00000000000000from __future__ import annotations import operator import typing as t from dataclasses import dataclass from dep_logic.specifiers.base import BaseSpecifier, InvalidSpecifier from dep_logic.specifiers.special import AnySpecifier, EmptySpecifier from dep_logic.utils import DATACLASS_ARGS Operator = t.Callable[[str, str], bool] @dataclass(frozen=True, unsafe_hash=True, **DATACLASS_ARGS) class GenericSpecifier(BaseSpecifier): op: str value: str op_order: t.ClassVar[dict[str, int]] = {"==": 0, "!=": 1, "in": 2, "not in": 3} _op_map: t.ClassVar[dict[str, Operator]] = { "==": operator.eq, "!=": operator.ne, "in": lambda lhs, rhs: lhs in rhs, "not in": lambda lhs, rhs: lhs not in rhs, ">": operator.gt, ">=": operator.ge, "<": operator.lt, "<=": operator.le, } def __post_init__(self) -> None: if self.op not in self._op_map: raise InvalidSpecifier(f"Invalid operator: {self.op!r}") def __str__(self) -> str: return f'{self.op} "{self.value}"' def __invert__(self) -> BaseSpecifier: invert_map = { "==": "!=", "!=": "==", "not in": "in", "in": "not in", "<": ">=", "<=": ">", ">": "<=", ">=": "<", } op = invert_map[self.op] return GenericSpecifier(op, self.value) def __and__(self, other: t.Any) -> BaseSpecifier: if not isinstance(other, GenericSpecifier): return NotImplemented if self == other: return self this, that = sorted( (self, other), key=lambda x: self.op_order.get(x.op, len(self.op_order)) ) if this.op == that.op == "==": # left must be different from right return EmptySpecifier() elif (this.op, that.op) == ("==", "!="): if this.value == that.value: return EmptySpecifier() return this elif (this.op, that.op) == ("in", "not in") and this.value == that.value: return EmptySpecifier() elif (this.op, that.op) == ("==", "in"): if this.value in that.value: return this return EmptySpecifier() elif (this.op, that.op) == ("!=", "not in") and this.value in that.value: return that else: raise NotImplementedError def __or__(self, other: t.Any) -> BaseSpecifier: if not isinstance(other, GenericSpecifier): return NotImplemented if self == other: return self this, that = sorted( (self, other), key=lambda x: self.op_order.get(x.op, len(self.op_order)) ) if this.op == "==" and that.op == "!=": if this.value == that.value: return AnySpecifier() return that elif this.op == "!=" and that.op == "!=": return AnySpecifier() elif this.op == "in" and that.op == "not in" and this.value == that.value: return AnySpecifier() elif this.op == "!=" and that.op == "in" and this.value in that.value: return AnySpecifier() elif this.op == "!=" and that.op == "not in": if this.value in that.value: return this return AnySpecifier() elif this.op == "==" and that.op == "in" and this.value in that.value: return that else: raise NotImplementedError def __contains__(self, value: str) -> bool: return self._op_map[self.op](value, self.value) dep-logic-0.4.11/src/dep_logic/specifiers/range.py000066400000000000000000000220061475730624500217730ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, field from functools import cached_property from typing import Any from packaging.specifiers import SpecifierSet from packaging.version import Version from dep_logic.specifiers.base import ( BaseSpecifier, InvalidSpecifier, UnparsedVersion, VersionSpecifier, ) from dep_logic.specifiers.special import EmptySpecifier from dep_logic.utils import DATACLASS_ARGS, first_different_index, pad_zeros @dataclass(frozen=True, unsafe_hash=True, **DATACLASS_ARGS) class RangeSpecifier(VersionSpecifier): min: Version | None = None max: Version | None = None include_min: bool = False include_max: bool = False simplified: str | None = field(default=None, compare=False, hash=False) def __post_init__(self) -> None: if self.min is None and self.include_min: raise InvalidSpecifier("Cannot include min when min is None") if self.max is None and self.include_max: raise InvalidSpecifier("Cannot include max when max is None") def to_specifierset(self) -> SpecifierSet: return SpecifierSet(str(self)) @property def num_parts(self) -> int: return len(self.to_specifierset()) @cached_property def _simplified_form(self) -> str | None: if self.simplified is not None: return self.simplified if self.min is None and self.max is None: return "" elif self.min is None: return f"{'<=' if self.include_max else '<'}{self.max}" elif self.max is None: return f"{'>=' if self.include_min else '>'}{self.min}" else: # First, try to make a equality specifier if self.min == self.max: # include_min and include_max are always True here return f"=={self.min}" if not self.include_min or self.include_max: return None # Next, try to make a ~= specifier min_stable = [self.min.epoch, *self.min.release] max_stable = [self.max.epoch, *self.max.release] max_length = max(len(min_stable), len(max_stable)) min_stable = pad_zeros(min_stable, max_length) max_stable = pad_zeros(max_stable, max_length) first_different = first_different_index(min_stable, max_stable) if first_different >= len(min_stable) - 1 or first_different == 0: # either they are all equal or the last one is different(>=2.3.1,<2.3.3) return None if max_stable[first_different] - min_stable[first_different] != 1: # The different part must be larger than 1 return None # What's more, we need the max version to be a stable with a suffix of 0 if ( all(p == 0 for p in max_stable[first_different + 1 :]) and not self.max.is_prerelease and len(self.min.release) == first_different + 1 ): return f"~={self.min}" return None def __str__(self) -> str: simplified = self._simplified_form if simplified is not None: return simplified return f'{">=" if self.include_min else ">"}{self.min},{"<=" if self.include_max else "<"}{self.max}' def contains( self, version: UnparsedVersion, prereleases: bool | None = None ) -> bool: return self.to_specifierset().contains(version, prereleases) def __invert__(self) -> BaseSpecifier: from dep_logic.specifiers.union import UnionSpecifier if self.min is None and self.max is None: return EmptySpecifier() specs: list[RangeSpecifier] = [] if self.min is not None: specs.append(RangeSpecifier(max=self.min, include_max=not self.include_min)) if self.max is not None: specs.append(RangeSpecifier(min=self.max, include_min=not self.include_max)) if len(specs) == 1: return specs[0] return UnionSpecifier(tuple(specs)) def is_simple(self) -> bool: return self._simplified_form is not None def is_any(self) -> bool: return self.min is None and self.max is None def allows_lower(self, other: RangeSpecifier) -> bool: if other.min is None: return False if self.min is None: return True return ( self.min < other.min or self.min == other.min and self.include_min and not other.include_min ) def allows_higher(self, other: RangeSpecifier) -> bool: if other.max is None: return False if self.max is None: return True return ( self.max > other.max or self.max == other.max and self.include_max and not other.include_max ) def is_strictly_lower(self, other: RangeSpecifier) -> bool: """Return True if this range is lower than the other range and they have no overlap. """ if self.max is None or other.min is None: return False return ( self.max < other.min or self.max == other.min and False in (self.include_max, other.include_min) ) def is_adjacent_to(self, other: RangeSpecifier) -> bool: if self.max is None or other.min is None: return False return ( self.max == other.min and [self.include_max, other.include_min].count(True) == 1 ) def __lt__(self, other: Any) -> bool: if not isinstance(other, RangeSpecifier): return NotImplemented return self.allows_lower(other) def is_superset(self, other: RangeSpecifier) -> bool: min_lower = ( self.min is None or other.min is not None and ( self.min < other.min or self.min == other.min and not (not self.include_min and other.include_min) ) ) max_higher = ( self.max is None or other.max is not None and ( self.max > other.max or self.max == other.max and not (not self.include_max and other.include_max) ) ) return min_lower and max_higher def is_subset(self, other: RangeSpecifier) -> bool: return other.is_superset(self) def can_combine(self, other: RangeSpecifier) -> bool: """Return True if the two ranges can be combined into one range.""" if self.allows_lower(other): return not self.is_strictly_lower(other) or self.is_adjacent_to(other) else: return not other.is_strictly_lower(self) or other.is_adjacent_to(self) def __and__(self, other: Any) -> RangeSpecifier | EmptySpecifier: if not isinstance(other, RangeSpecifier): return NotImplemented if self.is_superset(other): return other if other.is_superset(self): return self if self.allows_lower(other): if self.is_strictly_lower(other): return EmptySpecifier() intersect_min = other.min intersect_include_min = other.include_min else: if other.is_strictly_lower(self): return EmptySpecifier() intersect_min = self.min intersect_include_min = self.include_min if self.allows_higher(other): intersect_max = other.max intersect_include_max = other.include_max else: intersect_max = self.max intersect_include_max = self.include_max return type(self)( min=intersect_min, max=intersect_max, include_min=intersect_include_min, include_max=intersect_include_max, ) def __or__(self, other: Any) -> VersionSpecifier: from dep_logic.specifiers.union import UnionSpecifier if not isinstance(other, RangeSpecifier): return NotImplemented if self.is_superset(other): return self if other.is_superset(self): return other if self.allows_lower(other): if self.is_strictly_lower(other) and not self.is_adjacent_to(other): return UnionSpecifier((self, other)) union_min = self.min union_include_min = self.include_min else: if other.is_strictly_lower(self) and not other.is_adjacent_to(self): return UnionSpecifier((other, self)) union_min = other.min union_include_min = other.include_min if self.allows_higher(other): union_max = self.max union_include_max = self.include_max else: union_max = other.max union_include_max = other.include_max return type(self)( min=union_min, max=union_max, include_min=union_include_min, include_max=union_include_max, ) dep-logic-0.4.11/src/dep_logic/specifiers/special.py000066400000000000000000000034471475730624500223270ustar00rootroot00000000000000import typing as t from dep_logic.specifiers.base import BaseSpecifier class EmptySpecifier(BaseSpecifier): def __invert__(self) -> BaseSpecifier: return AnySpecifier() def __and__(self, other: t.Any) -> BaseSpecifier: if not isinstance(other, BaseSpecifier): return NotImplemented return self __rand__ = __and__ def __or__(self, other: t.Any) -> BaseSpecifier: if not isinstance(other, BaseSpecifier): return NotImplemented return other __ror__ = __or__ def __str__(self) -> str: return "" def __hash__(self) -> int: return hash(str(self)) def __eq__(self, other: object) -> bool: if not isinstance(other, BaseSpecifier): return NotImplemented return isinstance(other, EmptySpecifier) def is_empty(self) -> bool: return True def __contains__(self, value: str) -> bool: return True class AnySpecifier(BaseSpecifier): def __invert__(self) -> BaseSpecifier: return EmptySpecifier() def __and__(self, other: t.Any) -> BaseSpecifier: if not isinstance(other, BaseSpecifier): return NotImplemented return other __rand__ = __and__ def __or__(self, other: t.Any) -> BaseSpecifier: if not isinstance(other, BaseSpecifier): return NotImplemented return self __ror__ = __or__ def __str__(self) -> str: return "" def __hash__(self) -> int: return hash(str(self)) def __eq__(self, other: object) -> bool: if not isinstance(other, BaseSpecifier): return NotImplemented return other.is_any() def is_any(self) -> bool: return True def __contains__(self, value: str) -> bool: return False dep-logic-0.4.11/src/dep_logic/specifiers/union.py000066400000000000000000000151121475730624500220270ustar00rootroot00000000000000from __future__ import annotations import itertools import typing as t from dataclasses import dataclass, field from functools import cached_property from packaging.specifiers import SpecifierSet from dep_logic.specifiers.base import BaseSpecifier, UnparsedVersion, VersionSpecifier from dep_logic.specifiers.range import RangeSpecifier from dep_logic.specifiers.special import EmptySpecifier from dep_logic.utils import DATACLASS_ARGS, first_different_index, pad_zeros @dataclass(frozen=True, unsafe_hash=True, **DATACLASS_ARGS) class UnionSpecifier(VersionSpecifier): ranges: tuple[RangeSpecifier, ...] simplified: str | None = field(default=None, compare=False, hash=False) def to_specifierset(self) -> SpecifierSet: if (simplified := self._simplified_form) is None: raise ValueError("Cannot convert UnionSpecifier to SpecifierSet") return SpecifierSet(simplified) @property def num_parts(self) -> int: return sum(range.num_parts for range in self.ranges) @cached_property def _simplified_form(self) -> str | None: if self.simplified is not None: return self.simplified # try to get a not-equals form(!=) if possible left, right, *rest = self.ranges if rest: return None if ( left.min is None and right.max is None and left.max == right.min and left.max is not None ): # (-inf, version) | (version, inf) => != version return f"!={left.max}" if ( left.min is None and right.max is None and not left.include_max and right.include_min and left.max is not None and right.min is not None ): # (-inf, X.Y.0) | [X.Y+1.0, inf) => != X.Y.* if left.max.is_prerelease or right.min.is_prerelease: return None left_stable = [left.max.epoch, *left.max.release] right_stable = [right.min.epoch, *right.min.release] max_length = max(len(left_stable), len(right_stable)) left_stable = pad_zeros(left_stable, max_length) right_stable = pad_zeros(right_stable, max_length) first_different = first_different_index(left_stable, right_stable) if ( first_different > 0 and right_stable[first_different] - left_stable[first_different] == 1 and set( left_stable[first_different + 1 :] + right_stable[first_different + 1 :] ) == {0} ): epoch = "" if left.max.epoch == 0 else f"{left.max.epoch}!" version = ".".join(map(str, left.max.release[:first_different])) + ".*" return f"!={epoch}{version}" return None def __str__(self) -> str: if self._simplified_form is not None: return self._simplified_form return "||".join(map(str, self.ranges)) @staticmethod def _from_ranges(ranges: t.Sequence[RangeSpecifier]) -> BaseSpecifier: if (ranges_number := len(ranges)) == 0: return EmptySpecifier() elif ranges_number == 1: return ranges[0] else: return UnionSpecifier(tuple(ranges)) def is_simple(self) -> bool: return self._simplified_form is not None def contains( self, version: UnparsedVersion, prereleases: bool | None = None ) -> bool: return any( specifier.contains(version, prereleases) for specifier in self.ranges ) def __invert__(self) -> BaseSpecifier: to_union: list[RangeSpecifier] = [] if (first := self.ranges[0]).min is not None: to_union.append( RangeSpecifier(max=first.min, include_max=not first.include_min) ) for a, b in zip(self.ranges, self.ranges[1:]): to_union.append( RangeSpecifier( min=a.max, include_min=not a.include_max, max=b.min, include_max=not b.include_min, ) ) if (last := self.ranges[-1]).max is not None: to_union.append( RangeSpecifier(min=last.max, include_min=not last.include_max) ) return self._from_ranges(to_union) def __and__(self, other: t.Any) -> BaseSpecifier: if isinstance(other, RangeSpecifier): if other.is_any(): return self to_intersect: list[RangeSpecifier] = [other] elif isinstance(other, UnionSpecifier): to_intersect = list(other.ranges) else: return NotImplemented # Expand the ranges to be intersected, and discard the empty ones # (a | b) & (c | d) = (a & c) | (a & d) | (b & c) | (b & d) # Since each subrange doesn't overlap with each other and intersection # only makes it smaller, so the result is also guaranteed to be a set # of non-overlapping ranges, just build a new union from them. new_ranges = [ range for (a, b) in itertools.product(self.ranges, to_intersect) if not isinstance(range := a & b, EmptySpecifier) ] return self._from_ranges(new_ranges) __rand__ = __and__ def __or__(self, other: t.Any) -> BaseSpecifier: if isinstance(other, RangeSpecifier): if other.is_any(): return other new_ranges: list[RangeSpecifier] = [] ranges = iter(self.ranges) for range in ranges: if range.can_combine(other): other = t.cast(RangeSpecifier, other | range) elif other.allows_lower(range): # all following ranges are higher than the input, quit the loop # and copy the rest ranges. new_ranges.extend([other, range, *ranges]) break else: # range is strictly lower than other, nothing to do here new_ranges.append(range) else: # we have consumed all ranges or no range is merged, # just append to the last. new_ranges.append(other) return self._from_ranges(new_ranges) elif isinstance(other, UnionSpecifier): result = self for range in other.ranges: result = result | range return result else: return NotImplemented __ror__ = __or__ dep-logic-0.4.11/src/dep_logic/tags/000077500000000000000000000000001475730624500171275ustar00rootroot00000000000000dep-logic-0.4.11/src/dep_logic/tags/__init__.py000066400000000000000000000006021475730624500212360ustar00rootroot00000000000000from .platform import Platform, PlatformError from .tags import ( EnvCompatibility, EnvSpec, Implementation, InvalidWheelFilename, TagsError, UnsupportedImplementation, ) __all__ = [ "Platform", "PlatformError", "TagsError", "UnsupportedImplementation", "InvalidWheelFilename", "EnvSpec", "Implementation", "EnvCompatibility", ] dep-logic-0.4.11/src/dep_logic/tags/os.py000066400000000000000000000027761475730624500201360ustar00rootroot00000000000000from dataclasses import dataclass class Os: def __str__(self) -> str: return self.__class__.__name__.lower() @dataclass(frozen=True) class Manylinux(Os): major: int minor: int def __str__(self) -> str: return f"manylinux_{self.major}_{self.minor}" @dataclass(frozen=True) class Musllinux(Os): major: int minor: int def __str__(self) -> str: return f"musllinux_{self.major}_{self.minor}" @dataclass(frozen=True) class Windows(Os): pass @dataclass(frozen=True) class Macos(Os): major: int minor: int def __str__(self) -> str: return f"macos_{self.major}_{self.minor}" @dataclass(frozen=True) class FreeBsd(Os): release: str def __str__(self) -> str: return f"freebsd_{self.release}" @dataclass(frozen=True) class NetBsd(Os): release: str def __str__(self) -> str: return f"netbsd_{self.release}" @dataclass(frozen=True) class OpenBsd(Os): release: str @dataclass(frozen=True) class Dragonfly(Os): release: str def __str__(self) -> str: return f"dragonfly_{self.release}" @dataclass(frozen=True) class Illumos(Os): release: str arch: str def __str__(self) -> str: return f"illumos_{self.release}_{self.arch}" @dataclass(frozen=True) class Haiku(Os): release: str def __str__(self) -> str: return f"haiku_{self.release}" @dataclass(frozen=True) class Generic(Os): name: str def __str__(self) -> str: return self.name.lower() dep-logic-0.4.11/src/dep_logic/tags/platform.py000066400000000000000000000343271475730624500213360ustar00rootroot00000000000000# Abstractions for understanding the current platform (operating system and architecture). from __future__ import annotations import re import struct import sys from dataclasses import dataclass from enum import Enum from functools import cached_property from typing import TYPE_CHECKING from . import os if TYPE_CHECKING: from typing import Self class PlatformError(Exception): pass _platform_major_minor_re = re.compile( r"(?Pmanylinux|macos|musllinux)_(?P\d+?)_(?P\d+?)_(?P[a-z0-9_]+)$" ) _os_mapping = { "freebsd": os.FreeBsd, "netbsd": os.NetBsd, "openbsd": os.OpenBsd, "dragonfly": os.Dragonfly, "illumos": os.Illumos, "haiku": os.Haiku, } @dataclass(frozen=True) class Platform: os: os.Os arch: Arch @classmethod def parse(cls, platform: str) -> Self: """Parse a platform string (e.g., `linux_x86_64`, `macosx_10_9_x86_64`, or `win_amd64`) Available operating systems: - `linux`: an alias for `manylinux_2_17_x86_64` - `windows`: an alias for `win_amd64` - `macos`: an alias for `macos_14_0_arm64` - `alpine`: an alias for `musllinux_1_2_x86_64` - `windows_amd64` - `windows_x86` - `windows_arm64` - `macos_arm64`: an alias for `macos_14_0_arm64` - `macos_x86_64`: an alias for `macos_14_0_x86_64` - `macos_X_Y_arm64` - `macos_X_Y_x86_64` - `manylinux_X_Y_x86_64` - `manylinux_X_Y_aarch64` - `musllinux_X_Y_x86_64` - `musllinux_X_Y_aarch64` """ if platform == "linux": return cls(os.Manylinux(2, 17), Arch.X86_64) elif platform == "windows": return cls(os.Windows(), Arch.X86_64) elif platform == "macos": return cls(os.Macos(14, 0), Arch.Aarch64) elif platform == "alpine": return cls(os.Musllinux(1, 2), Arch.X86_64) elif platform.startswith("windows_"): return cls(os.Windows(), Arch.parse(platform.split("_", 1)[1])) elif platform == "macos_arm64": return cls(os.Macos(14, 0), Arch.Aarch64) elif platform == "macos_x86_64": return cls(os.Macos(14, 0), Arch.X86_64) elif (m := _platform_major_minor_re.match(platform)) is not None: os_name, major, minor, arch = m.groups() if os_name == "manylinux": return cls(os.Manylinux(int(major), int(minor)), Arch.parse(arch)) elif os_name == "macos": return cls(os.Macos(int(major), int(minor)), Arch.parse(arch)) else: # os_name == "musllinux" return cls(os.Musllinux(int(major), int(minor)), Arch.parse(arch)) else: os_, arch = platform.split("_", 1) if os_ in _os_mapping: release, _, arch = arch.partition("_") return cls(_os_mapping[os_](release), Arch.parse(arch)) try: return cls(os.Generic(os_), Arch.parse(arch)) except ValueError as e: raise PlatformError(f"Unsupported platform {platform}") from e def __str__(self) -> str: if isinstance(self.os, os.Windows) and self.arch == Arch.X86_64: return "windows_amd64" if isinstance(self.os, (os.Macos, os.Windows)) and self.arch == Arch.Aarch64: return f"{self.os}_arm64" return f"{self.os}_{self.arch}" @classmethod def current(cls) -> Self: """Return the current platform.""" import platform import sysconfig platform_ = sysconfig.get_platform() platform_info = platform_.split("-", 1) if len(platform_info) == 1: if platform_info[0] == "win32": return cls(os.Windows(), Arch.X86) operating_system, _, version_arch = ( platform_.replace(".", "_").replace(" ", "_").partition("_") ) else: operating_system, version_arch = platform_info if "-" in version_arch: # Ex: macosx-11.2-arm64 version, architecture = version_arch.rsplit("-", 1) else: # Ex: linux-x86_64 version = None architecture = version_arch if operating_system == "linux": from packaging._manylinux import _get_glibc_version from packaging._musllinux import _get_musl_version musl_version = _get_musl_version(sys.executable) glibc_version = _get_glibc_version() if musl_version: os_ = os.Musllinux(musl_version[0], musl_version[1]) else: os_ = os.Manylinux(glibc_version[0], glibc_version[1]) elif operating_system == "win": os_ = os.Windows() elif operating_system == "macosx": # Apparently, Mac OS is reporting i386 sometimes in sysconfig.get_platform even # though that's not a thing anymore. # https://github.com/astral-sh/uv/issues/2450 version, _, architecture = platform.mac_ver() # https://github.com/pypa/packaging/blob/cc938f984bbbe43c5734b9656c9837ab3a28191f/src/packaging/tags.py#L356-L363 is_32bit = struct.calcsize("P") == 4 if is_32bit: if architecture.startswith("ppc"): architecture = "ppc" else: architecture = "i386" version = version.split(".") os_ = os.Macos(int(version[0]), int(version[1])) elif operating_system in _os_mapping: os_ = _os_mapping[operating_system](version) else: os_ = os.Generic(operating_system) return cls(os_, Arch.parse(architecture)) @classmethod def choices(cls) -> list[str]: return [ "linux", "windows", "macos", "alpine", "windows_amd64", "windows_x86", "windows_arm64", "macos_arm64", "macos_x86_64", "macos_X_Y_arm64", "macos_X_Y_x86_64", "manylinux_X_Y_x86_64", "manylinux_X_Y_aarch64", "musllinux_X_Y_x86_64", "musllinux_X_Y_aarch64", ] @cached_property def compatible_tags(self) -> list[str]: """Returns the compatible tags for the current [`Platform`] (e.g., `manylinux_2_17`, `macosx_11_0_arm64`, or `win_amd64`). We have two cases: Actual platform specific tags (including "merged" tags such as universal2) and "any". Bit of a mess, needs to be cleaned up. """ os_ = self.os arch = self.arch platform_tags: list[str] = [] if isinstance(os_, os.Manylinux): if (min_minor := arch.get_minimum_manylinux_minor()) is not None: for minor in range(os_.minor, min_minor - 1, -1): platform_tags.append(f"manylinux_{os_.major}_{minor}_{arch}") # Support legacy manylinux tags with lower priority # if minor == 12: platform_tags.append(f"manylinux2010_{arch}") if minor == 17: platform_tags.append(f"manylinux2014_{arch}") if minor == 5: platform_tags.append(f"manylinux1_{arch}") # Non-manylinux is lowest priority # platform_tags.append(f"linux_{arch}") elif isinstance(os_, os.Musllinux): platform_tags.append(f"linux_{arch}") for minor in range(1, os_.minor + 1): # musl 1.1 is the lowest supported version in musllinux platform_tags.append(f"musllinux_{os_.major}_{minor}_{arch}") elif isinstance(os_, os.Macos) and arch == Arch.X86_64: if os_.major == 10: for minor in range(os_.minor, 3, -1): for binary_format in arch.get_mac_binary_formats(): platform_tags.append(f"macosx_10_{minor}_{binary_format}") elif isinstance(os_.major, int) and os_.major >= 11: # Starting with Mac OS 11, each yearly release bumps the major version number. # The minor versions are now the midyear updates. for major in range(os_.major, 10, -1): for binary_format in arch.get_mac_binary_formats(): platform_tags.append(f"macosx_{major}_0_{binary_format}") # The "universal2" binary format can have a macOS version earlier than 11.0 # when the x86_64 part of the binary supports that version of macOS. for minor in range(16, 3, -1): for binary_format in arch.get_mac_binary_formats(): platform_tags.append(f"macosx_10_{minor}_{binary_format}") else: raise PlatformError(f"Unsupported macOS version {os_.major}") elif isinstance(os_, os.Macos) and arch == Arch.Aarch64: # Starting with Mac OS 11, each yearly release bumps the major version number. # The minor versions are now the midyear updates. for major in range(os_.major, 10, -1): for binary_format in arch.get_mac_binary_formats(): platform_tags.append(f"macosx_{major}_0_{binary_format}") # The "universal2" binary format can have a macOS version earlier than 11.0 # when the x86_64 part of the binary supports that version of macOS. for minor in range(16, 3, -1): platform_tags.append(f"macosx_10_{minor}_universal2") elif isinstance(os_, os.Windows): if arch == Arch.X86: platform_tags.append("win32") elif arch == Arch.X86_64: platform_tags.append("win_amd64") elif arch == Arch.Aarch64: platform_tags.append("win_arm64") else: raise PlatformError(f"Unsupported Windows architecture {arch}") elif isinstance( os_, (os.FreeBsd, os.NetBsd, os.OpenBsd, os.Dragonfly, os.Haiku) ): release = os_.release.replace(".", "_").replace("-", "_") platform_tags.append(f"{str(os_).lower()}_{release}_{arch}") elif isinstance(os_, os.Illumos): # See https://github.com/python/cpython/blob/46c8d915715aa2bd4d697482aa051fe974d440e1/Lib/sysconfig.py#L722-L730 try: major, other = os_.release.split("_", 1) except ValueError: platform_tags.append(f"{str(os_).lower()}_{os_.release}_{arch}") else: major_ver = int(major) if major_ver >= 5: # SunOS 5 == Solaris 2 release = f"{major_ver - 3}_{other}" arch = f"{arch}_64bit" platform_tags.append(f"solaris_{release}_{arch}") elif isinstance(os_, os.Generic): platform_tags.append(f"{os_}_{arch}") else: raise PlatformError( f"Unsupported operating system and architecture combination: {os_} {arch}" ) return platform_tags @cached_property def os_name(self) -> str: return "nt" if isinstance(self.os, os.Windows) else "posix" @cached_property def sys_platform(self) -> str: if isinstance(self.os, os.Windows): return "win32" elif isinstance(self.os, (os.Macos, os.Illumos)): return "darwin" else: return "linux" @cached_property def platform_machine(self) -> str: if isinstance(self.os, (os.Windows, os.Macos)) and self.arch == Arch.Aarch64: return "arm64" if isinstance(self.os, os.Windows) and self.arch == Arch.X86_64: return "AMD64" return str(self.arch) @cached_property def platform_release(self) -> str: return "" @cached_property def platform_version(self) -> str: return "" @cached_property def platform_system(self) -> str: if isinstance(self.os, os.Macos): return "Darwin" if isinstance(self.os, os.Windows): return "Windows" return "Linux" def is_current(self) -> bool: current = self.current() return isinstance(self.os, type(current.os)) and self.arch == current.arch def markers(self) -> dict[str, str]: if self.is_current(): return {} return { "os_name": self.os_name, "platform_machine": self.platform_machine, "platform_release": self.platform_release, "platform_system": self.platform_system, "platform_version": self.platform_version, "sys_platform": self.sys_platform, } class Arch(Enum): Aarch64 = "aarch64" Armv6L = "armv6l" Armv7L = "armv7l" Powerpc64Le = "ppc64le" Powerpc64 = "ppc64" X86 = "x86" X86_64 = "x86_64" S390X = "s390x" RISCV64 = "riscv64" LoongArch64 = "loongarch64" def __str__(self) -> str: return self.value def get_minimum_manylinux_minor(self) -> int | None: if self in [ Arch.Aarch64, Arch.Armv7L, Arch.Powerpc64, Arch.Powerpc64Le, Arch.S390X, Arch.RISCV64, ]: return 17 elif self in [Arch.X86, Arch.X86_64]: return 5 else: return None def get_mac_binary_formats(self) -> list[str]: if self == Arch.Aarch64: formats = ["arm64"] else: formats = [self.value] if self == Arch.X86_64: formats.extend(["intel", "fat64", "fat32"]) if self in [Arch.X86_64, Arch.Aarch64]: formats.append("universal2") if self == Arch.X86_64: formats.append("universal") return formats @classmethod def parse(cls, arch: str) -> Arch: if arch in ("i386", "i686"): return cls.X86 if arch == "amd64": return cls.X86_64 if arch == "arm64": return cls.Aarch64 return cls(arch) dep-logic-0.4.11/src/dep_logic/tags/tags.py000066400000000000000000000241131475730624500204400ustar00rootroot00000000000000from __future__ import annotations import sys from dataclasses import dataclass from enum import IntEnum, auto from platform import python_implementation from typing import TYPE_CHECKING from dep_logic.specifiers.range import RangeSpecifier from ..specifiers import InvalidSpecifier, VersionSpecifier, parse_version_specifier from .platform import Platform if TYPE_CHECKING: from typing import Literal, Self def parse_wheel_tags(filename: str) -> tuple[list[str], list[str], list[str]]: if not filename.endswith(".whl"): raise InvalidWheelFilename( f"Invalid wheel filename (extension must be '.whl'): {filename}" ) filename = filename[:-4] dashes = filename.count("-") if dashes not in (4, 5): raise InvalidWheelFilename( f"Invalid wheel filename (wrong number of parts): {filename}" ) parts = filename.split("-") python, abi, platform = parts[-3:] return python.split("."), abi.split("."), platform.split(".") def _ensure_version_specifier(spec: str) -> VersionSpecifier: parsed = parse_version_specifier(spec) if not isinstance(parsed, VersionSpecifier): raise InvalidSpecifier(f"Invalid version specifier {spec}") return parsed class TagsError(Exception): pass class InvalidWheelFilename(TagsError, ValueError): pass class UnsupportedImplementation(TagsError, ValueError): pass @dataclass(frozen=True) class Implementation: name: Literal["cpython", "pypy", "pyston"] gil_disabled: bool = False @property def short(self) -> str: if self.name == "cpython": return "cp" elif self.name == "pypy": return "pp" else: return "pt" @property def capitalized(self) -> str: if self.name == "pypy": return "PyPy" elif self.name == "pyston": return "Pyston" else: return "CPython" @classmethod def current(cls) -> Self: import sysconfig implementation = python_implementation() return cls.parse( implementation.lower(), sysconfig.get_config_var("Py_GIL_DISABLED") or False ) @classmethod def parse(cls, name: str, gil_disabled: bool = False) -> Self: if gil_disabled and name != "cpython": raise UnsupportedImplementation("Only CPython supports GIL disabled mode") if name in ("cpython", "pypy", "pyston"): return cls(name, gil_disabled) else: raise UnsupportedImplementation( f"Unsupported implementation: {name}, expected cpython, pypy, or pyston" ) class EnvCompatibility(IntEnum): INCOMPATIBLE = auto() LOWER_OR_EQUAL = auto() HIGHER = auto() @dataclass(frozen=True) class EnvSpec: requires_python: VersionSpecifier platform: Platform | None = None implementation: Implementation | None = None def __str__(self) -> str: parts = [str(self.requires_python)] if self.platform is not None: parts.append(str(self.platform)) if self.implementation is not None: parts.append(self.implementation.name) return f"({', '.join(parts)})" def as_dict(self) -> dict[str, str | bool]: result: dict[str, str | bool] = {"requires_python": str(self.requires_python)} if self.platform is not None: result["platform"] = str(self.platform) if self.implementation is not None: result["implementation"] = self.implementation.name result["gil_disabled"] = self.implementation.gil_disabled return result @classmethod def from_spec( cls, requires_python: str, platform: str | None = None, implementation: str | None = None, gil_disabled: bool = False, ) -> Self: return cls( _ensure_version_specifier(requires_python), Platform.parse(platform) if platform else None, Implementation.parse(implementation, gil_disabled=gil_disabled) if implementation else None, ) @classmethod def current(cls) -> Self: # XXX: Strip pre-release and post-release tags python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" requires_python = _ensure_version_specifier(f"=={python_version}") platform = Platform.current() implementation = Implementation.current() return cls(requires_python, platform, implementation) def _evaluate_python( self, python_tag: str, abi_tag: str ) -> tuple[int, int, int] | None: """Return a tuple of (major, minor, abi) if the wheel is compatible with the environment, or None otherwise.""" impl, major, minor = python_tag[:2], python_tag[2:3], python_tag[3:] if self.implementation is not None and impl not in [ self.implementation.short, "py", ]: return None abi_impl = ( abi_tag.split("_", 1)[0] .replace("pypy", "pp") .replace("pyston", "pt") .lower() ) allow_abi3 = impl == "cp" and ( self.implementation is None or not self.implementation.gil_disabled ) free_threaded: bool | None = None if self.implementation is not None: free_threaded = self.implementation.gil_disabled try: if abi_impl == "abi3": if not allow_abi3: return None if ( parse_version_specifier(f">={major}.{minor or 0}") & self.requires_python ).is_empty(): return None return (int(major), int(minor or 0), 1) # 1 for abi3 # cp36-cp36m-* # cp312-cp312m-* # pp310-pypy310_pp75-* if abi_impl != "none": if not abi_impl.startswith(python_tag.lower()): return None if ( free_threaded is not None and abi_impl.endswith("t") is not free_threaded ): return None if major and minor: wheel_range = parse_version_specifier(f"=={major}.{minor}.*") else: wheel_range = parse_version_specifier(f"=={major}.*") except InvalidSpecifier: return None if (wheel_range & self.requires_python).is_empty(): return None return (int(major), int(minor or 0), 0 if abi_impl == "none" else 2) def _evaluate_platform(self, platform_tag: str) -> int | None: if self.platform is None: return -1 platform_tags = [*self.platform.compatible_tags, "any"] if platform_tag not in platform_tags: return None return len(platform_tags) - platform_tags.index(platform_tag) def compatibility( self, wheel_python_tags: list[str], wheel_abi_tags: list[str], wheel_platform_tags: list[str], ) -> tuple[int, int, int, int] | None: python_abi_combinations = ( (python_tag, abi_tag) for python_tag in wheel_python_tags for abi_tag in wheel_abi_tags ) python_compat = max( filter( None, (self._evaluate_python(*comb) for comb in python_abi_combinations) ), default=None, ) if python_compat is None: return None platform_compat = max( filter(None, map(self._evaluate_platform, wheel_platform_tags)), default=None, ) if platform_compat is None: return None return (*python_compat, platform_compat) def wheel_compatibility( self, wheel_filename: str ) -> tuple[int, int, int, int] | None: wheel_python_tags, wheel_abi_tags, wheel_platform_tags = parse_wheel_tags( wheel_filename ) return self.compatibility( wheel_python_tags, wheel_abi_tags, wheel_platform_tags ) def markers(self) -> dict[str, str]: result = {} if ( isinstance(self.requires_python, RangeSpecifier) and (version := self.requires_python.min) is not None and version == self.requires_python.max ): result.update( python_version=f"{version.major}.{version.minor}", python_full_version=str(version), ) if self.platform is not None: result.update(self.platform.markers()) if self.implementation is not None: result.update( implementation_name=self.implementation.name, platform_python_implementation=self.implementation.capitalized, ) return result def compare(self, target: EnvSpec) -> EnvCompatibility: if self == target: return EnvCompatibility.LOWER_OR_EQUAL if (self.requires_python & target.requires_python).is_empty(): return EnvCompatibility.INCOMPATIBLE if ( self.implementation is not None and target.implementation is not None and self.implementation != target.implementation ): return EnvCompatibility.INCOMPATIBLE if self.platform is None or target.platform is None: return EnvCompatibility.LOWER_OR_EQUAL if self.platform.arch != target.platform.arch: return EnvCompatibility.INCOMPATIBLE if type(self.platform.os) is not type(target.platform.os): return EnvCompatibility.INCOMPATIBLE if hasattr(self.platform.os, "major") and hasattr(self.platform.os, "minor"): if (self.platform.os.major, self.platform.os.minor) <= ( # type: ignore[attr-defined] target.platform.os.major, # type: ignore[attr-defined] target.platform.os.minor, # type: ignore[attr-defined] ): return EnvCompatibility.LOWER_OR_EQUAL else: return EnvCompatibility.HIGHER return EnvCompatibility.LOWER_OR_EQUAL dep-logic-0.4.11/src/dep_logic/utils.py000066400000000000000000000121711475730624500177050ustar00rootroot00000000000000from __future__ import annotations import functools import itertools import re import sys from typing import TYPE_CHECKING, AbstractSet, Iterable, Iterator, Protocol, TypeVar _prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$") if TYPE_CHECKING: from typing import TypedDict from dep_logic.markers.base import BaseMarker class _DataClassArgs(TypedDict, total=False): slots: bool repr: bool if sys.version_info >= (3, 10): DATACLASS_ARGS: _DataClassArgs = {"slots": True, "repr": False} else: DATACLASS_ARGS = {"repr": False} class Ident(Protocol): def __hash__(self) -> int: ... def __eq__(self, __value: object) -> bool: ... T = TypeVar("T", bound=Ident) V = TypeVar("V") def version_split(version: str) -> list[str]: result: list[str] = [] for item in version.split("."): match = _prefix_regex.search(item) if match: result.extend(match.groups()) else: result.append(item) return result def is_not_suffix(segment: str) -> bool: return not any( segment.startswith(prefix) for prefix in ("dev", "a", "b", "rc", "post") ) def flatten_items(items: Iterable[T], flatten_cls: type[Iterable[T]]) -> list[T]: flattened: list[T] = [] for item in items: if isinstance(item, flatten_cls): for subitem in flatten_items(item, flatten_cls): if subitem not in flattened: flattened.append(subitem) elif item not in flattened: flattened.append(item) return flattened def first_different_index( iterable1: Iterable[object], iterable2: Iterable[object] ) -> int: index = 0 for index, (item1, item2) in enumerate(zip(iterable1, iterable2)): if item1 != item2: return index return index + 1 def pad_zeros(parts: list[int], to_length: int) -> list[int]: if len(parts) >= to_length: return parts return parts + [0] * (to_length - len(parts)) @functools.lru_cache(maxsize=None) def cnf(marker: BaseMarker) -> BaseMarker: from dep_logic.markers.multi import MultiMarker from dep_logic.markers.union import MarkerUnion """Transforms the marker into CNF (conjunctive normal form).""" if isinstance(marker, MarkerUnion): cnf_markers = [cnf(m) for m in marker.markers] sub_marker_lists = [ m.markers if isinstance(m, MultiMarker) else [m] for m in cnf_markers ] return MultiMarker.of( *[MarkerUnion.of(*c) for c in itertools.product(*sub_marker_lists)] ) if isinstance(marker, MultiMarker): return MultiMarker.of(*[cnf(m) for m in marker.markers]) return marker @functools.lru_cache(maxsize=None) def dnf(marker: BaseMarker) -> BaseMarker: """Transforms the marker into DNF (disjunctive normal form).""" from dep_logic.markers.multi import MultiMarker from dep_logic.markers.union import MarkerUnion if isinstance(marker, MultiMarker): dnf_markers = [dnf(m) for m in marker.markers] sub_marker_lists = [ m.markers if isinstance(m, MarkerUnion) else [m] for m in dnf_markers ] return MarkerUnion.of( *[MultiMarker.of(*c) for c in itertools.product(*sub_marker_lists)] ) if isinstance(marker, MarkerUnion): return MarkerUnion.of(*[dnf(m) for m in marker.markers]) return marker def intersection(*markers: BaseMarker) -> BaseMarker: from dep_logic.markers.multi import MultiMarker return dnf(MultiMarker(*markers)) def union(*markers: BaseMarker) -> BaseMarker: from dep_logic.markers.multi import MultiMarker from dep_logic.markers.union import MarkerUnion # Sometimes normalization makes it more complicate instead of simple # -> choose candidate with the least complexity unnormalized: BaseMarker = MarkerUnion(*markers) while ( isinstance(unnormalized, (MultiMarker, MarkerUnion)) and len(unnormalized.markers) == 1 ): unnormalized = unnormalized.markers[0] conjunction = cnf(unnormalized) if not isinstance(conjunction, MultiMarker): return conjunction disjunction = dnf(conjunction) if not isinstance(disjunction, MarkerUnion): return disjunction return min(disjunction, conjunction, unnormalized, key=lambda x: x.complexity) _op_reflect_map = { "<": ">", "<=": ">=", ">": "<", ">=": "<=", "==": "==", "!=": "!=", "===": "===", "~=": "~=", "in": "in", "not in": "not in", } def get_reflect_op(op: str) -> str: return _op_reflect_map[op] class OrderedSet(AbstractSet[T]): def __init__(self, iterable: Iterable[T]) -> None: self._data: list[T] = [] for item in iterable: if item in self._data: continue self._data.append(item) def __hash__(self) -> int: return self._hash() def __contains__(self, obj: object) -> bool: return obj in self._data def __iter__(self) -> Iterator[T]: return iter(self._data) def __len__(self) -> int: return len(self._data) def peek(self) -> T: return self._data[0] dep-logic-0.4.11/tests/000077500000000000000000000000001475730624500146175ustar00rootroot00000000000000dep-logic-0.4.11/tests/__init__.py000066400000000000000000000000001475730624500167160ustar00rootroot00000000000000dep-logic-0.4.11/tests/marker/000077500000000000000000000000001475730624500161005ustar00rootroot00000000000000dep-logic-0.4.11/tests/marker/test_common.py000066400000000000000000000124351475730624500210060ustar00rootroot00000000000000from __future__ import annotations import pytest from dep_logic.markers import parse_marker @pytest.mark.parametrize( "marker, expected", [ ('python_version >= "3.6"', 'python_version >= "3.6"'), ('python_version >= "3.6" and extra == "foo"', 'python_version >= "3.6"'), ( 'python_version >= "3.6" and (extra == "foo" or extra == "bar")', 'python_version >= "3.6"', ), ( ( 'python_version >= "3.6" and (extra == "foo" or extra == "bar") or' ' implementation_name == "pypy"' ), 'python_version >= "3.6" or implementation_name == "pypy"', ), ( ( 'python_version >= "3.6" and extra == "foo" or implementation_name ==' ' "pypy" and extra == "bar"' ), 'python_version >= "3.6" or implementation_name == "pypy"', ), ( ( 'python_version >= "3.6" or extra == "foo" and implementation_name ==' ' "pypy" or extra == "bar"' ), 'python_version >= "3.6" or implementation_name == "pypy"', ), ('extra == "foo"', ""), ('extra == "foo" or extra == "bar"', ""), ], ) def test_without_extras(marker: str, expected: str) -> None: m = parse_marker(marker) assert str(m.without_extras()) == expected @pytest.mark.parametrize( "marker, excluded, expected", [ ('python_version >= "3.6"', "implementation_name", 'python_version >= "3.6"'), ('python_version >= "3.6"', "python_version", "*"), ('python_version >= "3.6" and python_version < "3.11"', "python_version", "*"), ( 'python_version >= "3.6" and extra == "foo"', "extra", 'python_version >= "3.6"', ), ( 'python_version >= "3.6" and (extra == "foo" or extra == "bar")', "python_version", 'extra == "foo" or extra == "bar"', ), ( ( 'python_version >= "3.6" and (extra == "foo" or extra == "bar") or' ' implementation_name == "pypy"' ), "python_version", 'extra == "foo" or extra == "bar" or implementation_name == "pypy"', ), ( ( 'python_version >= "3.6" and extra == "foo" or implementation_name ==' ' "pypy" and extra == "bar"' ), "implementation_name", 'python_version >= "3.6" and extra == "foo" or extra == "bar"', ), ( ( 'python_version >= "3.6" or extra == "foo" and implementation_name ==' ' "pypy" or extra == "bar"' ), "implementation_name", 'python_version >= "3.6" or extra == "foo" or extra == "bar"', ), ( 'extra == "foo" and python_version >= "3.6" or python_version >= "3.6"', "extra", 'python_version >= "3.6"', ), ], ) def test_exclude(marker: str, excluded: str, expected: str) -> None: m = parse_marker(marker) if expected == "*": assert m.exclude(excluded).is_any() else: assert str(m.exclude(excluded)) == expected @pytest.mark.parametrize( "marker, only, expected", [ ('python_version >= "3.6"', ["python_version"], 'python_version >= "3.6"'), ('python_version >= "3.6"', ["sys_platform"], ""), ( 'python_version >= "3.6" and extra == "foo"', ["python_version"], 'python_version >= "3.6"', ), ('python_version >= "3.6" and extra == "foo"', ["sys_platform"], ""), ('python_version >= "3.6" or extra == "foo"', ["sys_platform"], ""), ('python_version >= "3.6" or extra == "foo"', ["python_version"], ""), ( 'python_version >= "3.6" and (extra == "foo" or extra == "bar")', ["extra"], 'extra == "foo" or extra == "bar"', ), ( ( 'python_version >= "3.6" and (extra == "foo" or extra == "bar") or' ' implementation_name == "pypy"' ), ["implementation_name"], "", ), ( ( 'python_version >= "3.6" and (extra == "foo" or extra == "bar") or' ' implementation_name == "pypy"' ), ["implementation_name", "extra"], 'extra == "foo" or extra == "bar" or implementation_name == "pypy"', ), ( ( 'python_version >= "3.6" and (extra == "foo" or extra == "bar") or' ' implementation_name == "pypy"' ), ["implementation_name", "python_version"], 'python_version >= "3.6" or implementation_name == "pypy"', ), ( ( 'python_version >= "3.6" and extra == "foo" or implementation_name ==' ' "pypy" and extra == "bar"' ), ["implementation_name", "extra"], 'extra == "foo" or implementation_name == "pypy" and extra == "bar"', ), ], ) def test_only(marker: str, only: list[str], expected: str) -> None: m = parse_marker(marker) assert str(m.only(*only)) == expected dep-logic-0.4.11/tests/marker/test_compound.py000066400000000000000000000722001475730624500213360ustar00rootroot00000000000000import pytest from dep_logic.markers import MarkerUnion, MultiMarker, parse_marker from dep_logic.markers.empty import EmptyMarker from dep_logic.utils import union EMPTY = "" def test_multi_marker() -> None: m = parse_marker('sys_platform == "darwin" and implementation_name == "cpython"') assert isinstance(m, MultiMarker) assert m.markers == ( parse_marker('sys_platform == "darwin"'), parse_marker('implementation_name == "cpython"'), ) def test_multi_marker_is_empty_is_contradictory() -> None: m = parse_marker( 'sys_platform == "linux" and python_version >= "3.5" and python_version < "2.8"' ) assert m.is_empty() m = parse_marker('sys_platform == "linux" and sys_platform == "win32"') assert m.is_empty() def test_multi_complex_multi_marker_is_empty() -> None: m1 = parse_marker( 'python_full_version >= "3.0.0" and python_full_version < "3.4.0"' ) m2 = parse_marker( 'python_version >= "3.6" and python_full_version < "3.0.0" and python_version <' ' "3.7"' ) m3 = parse_marker( 'python_version >= "3.6" and python_version < "3.7" and python_full_version >=' ' "3.5.0"' ) m = m1 & (m2 | m3) assert m.is_empty() def test_multi_marker_is_any() -> None: m1 = parse_marker('python_version != "3.6" or python_version == "3.6"') m2 = parse_marker('python_version != "3.7" or python_version == "3.7"') assert m1 & m2.is_any() assert m2 & m1.is_any() def test_multi_marker_intersect_multi() -> None: m = parse_marker('sys_platform == "darwin" and implementation_name == "cpython"') intersection = m & ( parse_marker('python_version >= "3.6" and os_name == "Windows"') ) assert ( str(intersection) == 'sys_platform == "darwin" and implementation_name == "cpython" ' 'and python_version >= "3.6" and os_name == "Windows"' ) def test_multi_marker_intersect_multi_with_overlapping_constraints() -> None: m = parse_marker('sys_platform == "darwin" and python_version < "3.6"') intersection = m & ( parse_marker( 'python_version <= "3.4" and os_name == "Windows" and sys_platform ==' ' "darwin"' ) ) assert ( str(intersection) == 'sys_platform == "darwin" and python_version <= "3.4" and os_name ==' ' "Windows"' ) def test_multi_marker_intersect_with_union_drops_union() -> None: m = parse_marker('python_version >= "3" and python_version < "4"') m2 = parse_marker('python_version < "2" or python_version >= "3"') assert str(m & m2) == str(m) assert str(m2 & m) == str(m) def test_multi_marker_intersect_with_multi_union_leads_to_empty_in_one_step() -> None: # empty marker in one step # py == 2 and (py < 2 or py >= 3) -> empty m = parse_marker('sys_platform == "darwin" and python_version == "2"') m2 = parse_marker( 'sys_platform == "darwin" and (python_version < "2" or python_version >= "3")' ) assert (m & m2).is_empty() assert (m2 & m).is_empty() def test_multi_marker_intersect_with_multi_union_leads_to_empty_in_two_steps() -> None: # empty marker in two steps # py >= 2 and (py < 2 or py >= 3) -> py >= 3 # py < 3 and py >= 3 -> empty m = parse_marker('python_version >= "2" and python_version < "3"') m2 = parse_marker( 'sys_platform == "darwin" and (python_version < "2" or python_version >= "3")' ) assert (m & m2).is_empty() assert (m2 & m).is_empty() def test_multi_marker_union_multi() -> None: m = parse_marker('sys_platform == "darwin" and implementation_name == "cpython"') union = m | parse_marker('python_version >= "3.6" and os_name == "Windows"') assert ( str(union) == 'sys_platform == "darwin" and implementation_name == "cpython" ' 'or python_version >= "3.6" and os_name == "Windows"' ) def test_multi_marker_union_multi_is_single_marker() -> None: m = parse_marker('python_version >= "3" and sys_platform == "win32"') m2 = parse_marker('sys_platform != "win32" and python_version >= "3"') assert str(m | m2) == 'python_version >= "3"' assert str(m2 | m) == 'python_version >= "3"' @pytest.mark.parametrize( "marker1, marker2, expected", [ ( 'python_version >= "3" and sys_platform == "win32"', ( 'python_version >= "3" and sys_platform != "win32" and sys_platform !=' ' "linux"' ), 'python_version >= "3" and sys_platform != "linux"', ), ( ( 'python_version >= "3.8" and python_version < "4.0" and sys_platform ==' ' "win32"' ), 'python_version >= "3.8" and python_version < "4.0"', 'python_version ~= "3.8"', ), ], ) def test_multi_marker_union_multi_is_multi( marker1: str, marker2: str, expected: str ) -> None: m1 = parse_marker(marker1) m2 = parse_marker(marker2) assert str(m1 | m2) == expected assert str(m2 | m1) == expected @pytest.mark.parametrize( "marker1, marker2, expected", [ # Ranges with same start ( 'python_version >= "3.6" and python_full_version < "3.6.2"', 'python_version >= "3.6" and python_version < "3.7"', 'python_version >= "3.6" and python_version < "3.7"', ), ( 'python_version > "3.6" and python_full_version < "3.6.2"', 'python_version > "3.6" and python_version < "3.7"', 'python_version > "3.6" and python_version < "3.7"', ), # Ranges meet exactly ( 'python_version >= "3.6" and python_full_version < "3.6.2"', 'python_full_version >= "3.6.2" and python_version < "3.7"', 'python_version >= "3.6" and python_full_version < "3.7.0"', ), ( 'python_version >= "3.6" and python_full_version <= "3.6.2"', 'python_full_version > "3.6.2" and python_version < "3.7"', 'python_version >= "3.6" and python_version < "3.7"', ), # Ranges overlap ( 'python_version >= "3.6" and python_full_version <= "3.6.8"', 'python_full_version >= "3.6.2" and python_version < "3.7"', 'python_version >= "3.6" and python_full_version < "3.7.0"', ), # Ranges with same end. ( 'python_version >= "3.6" and python_version < "3.7"', 'python_full_version >= "3.6.2" and python_version < "3.7"', 'python_version >= "3.6" and python_version < "3.7"', ), ( 'python_version >= "3.6" and python_version <= "3.7"', 'python_full_version >= "3.6.2" and python_version <= "3.7"', 'python_version >= "3.6" and python_version <= "3.7"', ), # A range covers an exact marker. ( 'python_version >= "3.6" and python_version <= "3.7"', 'python_version == "3.6"', 'python_version >= "3.6" and python_version <= "3.7"', ), ( 'python_version >= "3.6" and python_version <= "3.7"', 'python_version == "3.6" and implementation_name == "cpython"', 'python_version >= "3.6" and python_version <= "3.7"', ), ( 'python_version >= "3.6" and python_version <= "3.7"', 'python_full_version == "3.6.2"', 'python_version >= "3.6" and python_version <= "3.7"', ), ( 'python_version >= "3.6" and python_version <= "3.7"', 'python_full_version == "3.6.2" and implementation_name == "cpython"', 'python_version >= "3.6" and python_version <= "3.7"', ), ( 'python_version >= "3.6" and python_version <= "3.7"', 'python_version == "3.7"', 'python_version >= "3.6" and python_version <= "3.7"', ), ( 'python_version >= "3.6" and python_version <= "3.7"', 'python_version == "3.7" and implementation_name == "cpython"', 'python_version >= "3.6" and python_version <= "3.7"', ), ], ) def test_version_ranges_collapse_on_union( marker1: str, marker2: str, expected: str ) -> None: m1 = parse_marker(marker1) m2 = parse_marker(marker2) assert str(m1 | m2) == expected assert str(m2 | m1) == expected def test_multi_marker_union_with_union() -> None: m1 = parse_marker('sys_platform == "darwin" and implementation_name == "cpython"') m2 = parse_marker('python_version >= "3.6" or os_name == "Windows"') # Union isn't _quite_ symmetrical. expected1 = ( 'sys_platform == "darwin" and implementation_name == "cpython" or' ' python_version >= "3.6" or os_name == "Windows"' ) assert str(m1 | m2) == expected1 expected2 = ( 'python_version >= "3.6" or os_name == "Windows" or' ' sys_platform == "darwin" and implementation_name == "cpython"' ) assert str(m2 | m1) == expected2 def test_multi_marker_union_with_multi_union_is_single_marker() -> None: m = parse_marker('sys_platform == "darwin" and python_version == "3"') m2 = parse_marker( 'sys_platform == "darwin" and python_version < "3" or sys_platform == "darwin"' ' and python_version > "3"' ) assert str(m | m2) == 'sys_platform == "darwin"' assert str(m2 | m) == 'sys_platform == "darwin"' def test_multi_marker_union_with_union_multi_is_single_marker() -> None: m = parse_marker('sys_platform == "darwin" and python_version == "3"') m2 = parse_marker( 'sys_platform == "darwin" and (python_version < "3" or python_version > "3")' ) assert str(m | m2) == 'sys_platform == "darwin"' assert str(m2 | m) == 'sys_platform == "darwin"' def test_marker_union() -> None: m = parse_marker('sys_platform == "darwin" or implementation_name == "cpython"') assert isinstance(m, MarkerUnion) assert m.markers == ( parse_marker('sys_platform == "darwin"'), parse_marker('implementation_name == "cpython"'), ) def test_marker_union_deduplicate() -> None: m = parse_marker( 'sys_platform == "darwin" or implementation_name == "cpython" or sys_platform' ' == "darwin"' ) assert str(m) == 'sys_platform == "darwin" or implementation_name == "cpython"' def test_marker_union_intersect_single_marker() -> None: m = parse_marker('sys_platform == "darwin" or python_version < "3.4"') intersection = m & parse_marker('implementation_name == "cpython"') assert ( str(intersection) == 'sys_platform == "darwin" and implementation_name == "cpython" ' 'or python_version < "3.4" and implementation_name == "cpython"' ) def test_marker_union_intersect_single_with_overlapping_constraints() -> None: m = parse_marker('sys_platform == "darwin" or python_version < "3.4"') intersection = m & parse_marker('python_version <= "3.6"') assert ( str(intersection) == 'sys_platform == "darwin" and python_version <= "3.6" or python_version <' ' "3.4"' ) m = parse_marker('sys_platform == "darwin" or python_version < "3.4"') intersection = m & parse_marker('sys_platform == "darwin"') assert str(intersection) == 'sys_platform == "darwin"' def test_marker_union_intersect_marker_union() -> None: m = parse_marker('sys_platform == "darwin" or python_version < "3.4"') intersection = m & ( parse_marker('implementation_name == "cpython" or os_name == "Windows"') ) assert ( str(intersection) == 'sys_platform == "darwin" and implementation_name == "cpython" ' 'or sys_platform == "darwin" and os_name == "Windows" or ' 'python_version < "3.4" and implementation_name == "cpython" or ' 'python_version < "3.4" and os_name == "Windows"' ) def test_marker_union_intersect_marker_union_drops_unnecessary_markers() -> None: m = parse_marker( 'python_version >= "2.7" and python_version < "2.8" ' 'or python_version >= "3.4" and python_version < "4.0"' ) m2 = parse_marker( 'python_version >= "2.7" and python_version < "2.8" ' 'or python_version >= "3.4" and python_version < "4.0"' ) intersection = m & m2 expected = ( 'python_version >= "2.7" and python_version < "2.8" ' 'or python_version ~= "3.4"' ) assert str(intersection) == expected def test_marker_union_intersect_multi_marker() -> None: m1 = parse_marker('sys_platform == "darwin" or python_version < "3.4"') m2 = parse_marker('implementation_name == "cpython" and os_name == "Windows"') # Intersection isn't _quite_ symmetrical. expected1 = ( 'sys_platform == "darwin" and implementation_name == "cpython" and os_name ==' ' "Windows" or python_version < "3.4" and implementation_name == "cpython" and' ' os_name == "Windows"' ) intersection = m1 & m2 assert str(intersection) == expected1 expected2 = ( 'implementation_name == "cpython" and os_name == "Windows" and sys_platform' ' == "darwin" or implementation_name == "cpython" and os_name == "Windows"' ' and python_version < "3.4"' ) intersection = m2 & m1 assert str(intersection) == expected2 def test_marker_union_union_with_union() -> None: m = parse_marker('sys_platform == "darwin" or python_version < "3.4"') union = m | ( parse_marker('implementation_name == "cpython" or os_name == "Windows"') ) assert ( str(union) == 'sys_platform == "darwin" or python_version < "3.4" ' 'or implementation_name == "cpython" or os_name == "Windows"' ) def test_marker_union_union_duplicates() -> None: m = parse_marker('sys_platform == "darwin" or python_version < "3.4"') union = m | parse_marker('sys_platform == "darwin" or os_name == "Windows"') assert ( str(union) == 'sys_platform == "darwin" or python_version < "3.4" or os_name == "Windows"' ) m = parse_marker('sys_platform == "darwin" or python_version < "3.4"') union = m | ( parse_marker( 'sys_platform == "darwin" or os_name == "Windows" or python_version <=' ' "3.6"' ) ) assert ( str(union) == 'sys_platform == "darwin" or python_version <= "3.6" or os_name == "Windows"' ) def test_marker_union_all_any() -> None: union = MarkerUnion.of(parse_marker(""), parse_marker("")) assert union.is_any() def test_marker_union_not_all_any() -> None: union = MarkerUnion.of(parse_marker(""), parse_marker(""), EmptyMarker()) assert union.is_any() def test_marker_union_all_empty() -> None: union = MarkerUnion.of(EmptyMarker(), EmptyMarker()) assert union.is_empty() def test_marker_union_not_all_empty() -> None: union = MarkerUnion.of(EmptyMarker(), EmptyMarker(), parse_marker("")) assert not union.is_empty() def test_intersect_compacts_constraints() -> None: m = parse_marker('python_version < "4.0"') intersection = m & parse_marker('python_version < "5.0"') assert str(intersection) == 'python_version < "4.0"' def test_multi_marker_removes_duplicates() -> None: m = parse_marker('sys_platform == "win32" and sys_platform == "win32"') assert str(m) == 'sys_platform == "win32"' m = parse_marker( 'sys_platform == "darwin" and implementation_name == "cpython" ' 'and sys_platform == "darwin" and implementation_name == "cpython"' ) assert str(m) == 'sys_platform == "darwin" and implementation_name == "cpython"' def test_union_of_a_single_marker_is_the_single_marker() -> None: union = MarkerUnion.of(m := parse_marker("python_version>= '2.7'")) assert m == union def test_union_of_multi_with_a_containing_single() -> None: single = parse_marker('python_version >= "2.7"') multi = parse_marker('python_version >= "2.7" and extra == "foo"') union = multi | single assert union == single def test_single_markers_are_found_in_complex_intersection() -> None: m1 = parse_marker('implementation_name != "pypy" and python_version <= "3.6"') m2 = parse_marker( 'python_version >= "3.6" and python_version < "4.0" and implementation_name ==' ' "cpython"' ) intersection = m1 & m2 assert ( str(intersection) == 'implementation_name == "cpython" and python_version == "3.6"' ) @pytest.mark.parametrize( "marker1, marker2", [ ( ( '(platform_system != "Windows" or platform_machine != "x86") and' ' python_version == "3.8"' ), 'platform_system == "Windows" and platform_machine == "x86"', ), # Following example via # https://github.com/python-poetry/poetry-plugin-export/issues/163 ( ( 'python_version >= "3.8" and python_version < "3.11" and' ' (python_version > "3.9" or platform_system != "Windows" or' ' platform_machine != "x86") or python_version >= "3.11" and' ' python_version < "3.12"' ), ( 'python_version == "3.8" and platform_system == "Windows" and' ' platform_machine == "x86" or python_version == "3.9" and' ' platform_system == "Windows" and platform_machine == "x86"' ), ), ], ) def test_empty_marker_is_found_in_complex_intersection( marker1: str, marker2: str ) -> None: m1 = parse_marker(marker1) m2 = parse_marker(marker2) assert (m1 & m2).is_empty() assert (m2 & m1).is_empty() def test_empty_marker_is_found_in_complex_parse() -> None: marker = parse_marker( '(python_implementation != "pypy" or python_version != "3.6") and ' '((python_implementation != "pypy" and python_version != "3.6") or' ' (python_implementation == "pypy" and python_version == "3.6")) and ' '(python_implementation == "pypy" or python_version == "3.6")' ) assert marker.is_empty() def test_complex_union() -> None: """ real world example on the way to get mutually exclusive markers for numpy(>=1.21.2) of https://pypi.org/project/opencv-python/4.6.0.66/ """ markers = [ parse_marker(m) for m in [ ( 'python_version < "3.7" and python_version >= "3.6"' ' and platform_system == "Darwin" and platform_machine == "arm64"' ), ( 'python_version >= "3.10" or python_version >= "3.9"' ' and platform_system == "Darwin" and platform_machine == "arm64"' ), ( 'python_version >= "3.8" and platform_system == "Darwin"' ' and platform_machine == "arm64" and python_version < "3.9"' ), ( 'python_version >= "3.7" and platform_system == "Darwin"' ' and platform_machine == "arm64" and python_version < "3.8"' ), ] ] assert ( str(union(*markers)) == 'platform_system == "Darwin" and platform_machine == "arm64"' ' and python_version >= "3.6" or python_version >= "3.10"' ) def test_union_avoids_combinatorial_explosion() -> None: """ combinatorial explosion without AtomicMultiMarker and AtomicMarkerUnion based gevent constraint of sqlalchemy 2.0.7 see https://github.com/python-poetry/poetry/issues/7689 for details """ expected = ( 'python_full_version >= "3.11.0" and python_version < "4.0"' ' and platform_machine in "WIN32,win32,AMD64,amd64,x86_64,aarch64,ppc64le"' ) m1 = parse_marker(expected) m2 = parse_marker( 'python_full_version >= "3.11.0" and python_full_version < "4.0.0"' ' and (platform_machine == "aarch64" or platform_machine == "ppc64le"' ' or platform_machine == "x86_64" or platform_machine == "amd64"' ' or platform_machine == "AMD64" or platform_machine == "win32"' ' or platform_machine == "WIN32")' ) assert str(m1 | m2) == expected assert str(m2 | m1) == expected def test_intersection_avoids_combinatorial_explosion() -> None: """ combinatorial explosion without AtomicMultiMarker and AtomicMarkerUnion based gevent constraint of sqlalchemy 2.0.7 see https://github.com/python-poetry/poetry/issues/7689 for details """ m1 = parse_marker( 'python_full_version >= "3.11.0" and python_full_version < "4.0.0"' ) m2 = parse_marker( 'python_version >= "3" and (platform_machine == "aarch64" ' 'or platform_machine == "ppc64le" or platform_machine == "x86_64" ' 'or platform_machine == "amd64" or platform_machine == "AMD64" ' 'or platform_machine == "win32" or platform_machine == "WIN32")' ) assert ( str(m1 & m2) == 'python_full_version >= "3.11.0" and python_full_version < "4.0.0"' ' and (platform_machine == "aarch64" or platform_machine == "ppc64le"' ' or platform_machine == "x86_64" or platform_machine == "amd64"' ' or platform_machine == "AMD64" or platform_machine == "win32"' ' or platform_machine == "WIN32")' ) assert ( str(m2 & m1) == '(platform_machine == "aarch64" or platform_machine == "ppc64le"' ' or platform_machine == "x86_64" or platform_machine == "amd64"' ' or platform_machine == "AMD64" or platform_machine == "win32"' ' or platform_machine == "WIN32") and python_full_version >= "3.11.0" ' 'and python_full_version < "4.0.0"' ) @pytest.mark.parametrize( "python_version, python_full_version, " "expected_intersection_version, expected_union_version", [ # python_version > 3.6 (equal to python_full_version >= 3.7.0) ('> "3.6"', '> "3.5.2"', '> "3.6"', '> "3.5.2"'), ('> "3.6"', '>= "3.5.2"', '> "3.6"', '>= "3.5.2"'), ('> "3.6"', '> "3.6.2"', '> "3.6"', '> "3.6.2"'), ('> "3.6"', '>= "3.6.2"', '> "3.6"', '>= "3.6.2"'), ('> "3.6"', '> "3.7.0"', '> "3.7.0"', '> "3.6"'), ('> "3.6"', '>= "3.7.0"', '> "3.6"', '> "3.6"'), ('> "3.6"', '> "3.7.1"', '> "3.7.1"', '> "3.6"'), ('> "3.6"', '>= "3.7.1"', '>= "3.7.1"', '> "3.6"'), ('> "3.6"', '== "3.6.2"', EMPTY, None), ('> "3.6"', '== "3.7.0"', '== "3.7.0"', '> "3.6"'), ('> "3.6"', '== "3.7.1"', '== "3.7.1"', '> "3.6"'), ('> "3.6"', '!= "3.6.2"', '> "3.6"', '!= "3.6.2"'), ('> "3.6"', '!= "3.7.0"', '> "3.7.0"', ""), ('> "3.6"', '!= "3.7.1"', None, ""), ('> "3.6"', '< "3.7.0"', EMPTY, ""), ('> "3.6"', '<= "3.7.0"', '== "3.7.0"', ""), ('> "3.6"', '< "3.7.1"', None, ""), ('> "3.6"', '<= "3.7.1"', None, ""), # python_version >= 3.6 (equal to python_full_version >= 3.6.0) ('>= "3.6"', '> "3.5.2"', '>= "3.6"', '> "3.5.2"'), ('>= "3.6"', '>= "3.5.2"', '>= "3.6"', '>= "3.5.2"'), ('>= "3.6"', '> "3.6.0"', '> "3.6.0"', '>= "3.6"'), ('>= "3.6"', '>= "3.6.0"', '>= "3.6"', '>= "3.6"'), ('>= "3.6"', '> "3.6.1"', '> "3.6.1"', '>= "3.6"'), ('>= "3.6"', '>= "3.6.1"', '>= "3.6.1"', '>= "3.6"'), ('>= "3.6"', '== "3.5.2"', EMPTY, None), ('>= "3.6"', '== "3.6.0"', '== "3.6.0"', '>= "3.6"'), ('>= "3.6"', '!= "3.5.2"', '>= "3.6"', '!= "3.5.2"'), ('>= "3.6"', '!= "3.6.0"', '> "3.6.0"', ""), ('>= "3.6"', '!= "3.6.1"', None, ""), ('>= "3.6"', '!= "3.7.1"', None, ""), ('>= "3.6"', '< "3.6.0"', EMPTY, ""), ('>= "3.6"', '<= "3.6.0"', '== "3.6.0"', ""), ('>= "3.6"', '< "3.6.1"', None, ""), # '== "3.6.0"' ('>= "3.6"', '<= "3.6.1"', None, ""), # python_version < 3.6 (equal to python_full_version < 3.6.0) ('< "3.6"', '< "3.5.2"', '< "3.5.2"', '< "3.6"'), ('< "3.6"', '<= "3.5.2"', '<= "3.5.2"', '< "3.6"'), ('< "3.6"', '< "3.6.0"', '< "3.6"', '< "3.6"'), ('< "3.6"', '<= "3.6.0"', '< "3.6"', '<= "3.6.0"'), ('< "3.6"', '< "3.6.1"', '< "3.6"', '< "3.6.1"'), ('< "3.6"', '<= "3.6.1"', '< "3.6"', '<= "3.6.1"'), ('< "3.6"', '== "3.5.2"', '== "3.5.2"', '< "3.6"'), ('< "3.6"', '== "3.6.0"', EMPTY, '<= "3.6.0"'), ('< "3.6"', '!= "3.5.2"', None, ""), ('< "3.6"', '!= "3.6.0"', '< "3.6"', '!= "3.6.0"'), ('< "3.6"', '> "3.6.0"', EMPTY, '!= "3.6.0"'), ('< "3.6"', '>= "3.6.0"', EMPTY, ""), ('< "3.6"', '> "3.5.2"', None, ""), ('< "3.6"', '>= "3.5.2"', '~= "3.5.2"', ""), # python_version <= 3.6 (equal to python_full_version < 3.7.0) ('<= "3.6"', '< "3.6.1"', '< "3.6.1"', '<= "3.6"'), ('<= "3.6"', '<= "3.6.1"', '<= "3.6.1"', '<= "3.6"'), ('<= "3.6"', '< "3.7.0"', '<= "3.6"', '<= "3.6"'), ('<= "3.6"', '<= "3.7.0"', '<= "3.6"', '<= "3.7.0"'), ('<= "3.6"', '== "3.6.1"', '== "3.6.1"', '<= "3.6"'), ('<= "3.6"', '== "3.7.0"', EMPTY, '<= "3.7.0"'), ('<= "3.6"', '!= "3.6.1"', None, ""), ('<= "3.6"', '!= "3.7.0"', '<= "3.6"', '!= "3.7.0"'), ('<= "3.6"', '> "3.7.0"', EMPTY, '!= "3.7.0"'), ('<= "3.6"', '>= "3.7.0"', EMPTY, ""), ('<= "3.6"', '> "3.6.2"', None, ""), ('<= "3.6"', '>= "3.6.2"', '~= "3.6.2"', ""), # python_version == 3.6 # (equal to python_full_version >= 3.6.0 and python_full_version < 3.7.0) ('== "3.6"', '< "3.5.2"', EMPTY, None), ('== "3.6"', '<= "3.5.2"', EMPTY, None), ('== "3.6"', '> "3.5.2"', '== "3.6"', '> "3.5.2"'), ('== "3.6"', '>= "3.5.2"', '== "3.6"', '>= "3.5.2"'), ('== "3.6"', '!= "3.5.2"', '== "3.6"', '!= "3.5.2"'), ('== "3.6"', '< "3.6.0"', EMPTY, '< "3.7.0"'), ('== "3.6"', '<= "3.6.0"', '== "3.6.0"', '< "3.7.0"'), ('== "3.6"', '> "3.6.0"', None, '>= "3.6.0"'), ('== "3.6"', '>= "3.6.0"', '== "3.6"', '>= "3.6.0"'), ('== "3.6"', '!= "3.6.0"', None, ""), ('== "3.6"', '< "3.6.1"', None, '< "3.7.0"'), ('== "3.6"', '<= "3.6.1"', None, '< "3.7.0"'), ('== "3.6"', '> "3.6.1"', None, '>= "3.6.0"'), ('== "3.6"', '>= "3.6.1"', '~= "3.6.1"', '>= "3.6.0"'), ('== "3.6"', '!= "3.6.1"', None, ""), ('== "3.6"', '< "3.7.0"', '== "3.6"', '< "3.7.0"'), ('== "3.6"', '<= "3.7.0"', '== "3.6"', '<= "3.7.0"'), ('== "3.6"', '> "3.7.0"', EMPTY, None), ('== "3.6"', '>= "3.7.0"', EMPTY, '>= "3.6.0"'), ('== "3.6"', '!= "3.7.0"', '== "3.6"', '!= "3.7.0"'), ('== "3.6"', '<= "3.7.1"', '== "3.6"', '<= "3.7.1"'), ('== "3.6"', '< "3.7.1"', '== "3.6"', '< "3.7.1"'), ('== "3.6"', '> "3.7.1"', EMPTY, None), ('== "3.6"', '>= "3.7.1"', EMPTY, None), ('== "3.6"', '!= "3.7.1"', '== "3.6"', '!= "3.7.1"'), # python_version != 3.6 # (equal to python_full_version < 3.6.0 or python_full_version >= 3.7.0) ('!= "3.6"', '< "3.5.2"', '< "3.5.2"', '!= "3.6"'), ('!= "3.6"', '<= "3.5.2"', '<= "3.5.2"', '!= "3.6"'), ('!= "3.6"', '> "3.5.2"', None, ""), ('!= "3.6"', '>= "3.5.2"', None, ""), ('!= "3.6"', '!= "3.5.2"', None, ""), ('!= "3.6"', '< "3.6.0"', '< "3.6.0"', '!= "3.6"'), ('!= "3.6"', '<= "3.6.0"', '< "3.6.0"', None), ('!= "3.6"', '> "3.6.0"', '>= "3.7.0"', '!= "3.6.0"'), ('!= "3.6"', '>= "3.6.0"', '>= "3.7.0"', ""), ('!= "3.6"', '!= "3.6.0"', '!= "3.6"', '!= "3.6.0"'), ('!= "3.6"', '< "3.6.1"', '< "3.6.0"', None), ('!= "3.6"', '<= "3.6.1"', '< "3.6.0"', None), ('!= "3.6"', '> "3.6.1"', '>= "3.7.0"', None), ('!= "3.6"', '>= "3.6.1"', '>= "3.7.0"', None), ('!= "3.6"', '!= "3.6.1"', '!= "3.6"', '!= "3.6.1"'), ('!= "3.6"', '< "3.7.0"', '< "3.6.0"', ""), ('!= "3.6"', '<= "3.7.0"', None, ""), ('!= "3.6"', '> "3.7.0"', '> "3.7.0"', '!= "3.6"'), ('!= "3.6"', '>= "3.7.0"', '>= "3.7.0"', '!= "3.6"'), ('!= "3.6"', '!= "3.7.0"', None, ""), ('!= "3.6"', '<= "3.7.1"', None, ""), ('!= "3.6"', '< "3.7.1"', None, ""), ('!= "3.6"', '> "3.7.1"', '> "3.7.1"', '!= "3.6"'), ('!= "3.6"', '>= "3.7.1"', '>= "3.7.1"', '!= "3.6"'), ('!= "3.6"', '!= "3.7.1"', None, ""), ], ) def test_merging_python_version_and_python_full_version( python_version: str, python_full_version: str, expected_intersection_version: str, expected_union_version: str, ) -> None: m = f"python_version {python_version}" m2 = f"python_full_version {python_full_version}" def get_expected_marker(expected_version: str, op: str) -> str: if expected_version is None: expected = f"{m} {op} {m2}" elif expected_version in ("", EMPTY): expected = expected_version else: expected_marker_name = ( "python_version" if expected_version.count(".") < 2 else "python_full_version" ) expected = f"{expected_marker_name} {expected_version}" return expected expected_intersection = get_expected_marker(expected_intersection_version, "and") expected_union = get_expected_marker(expected_union_version, "or") intersection = parse_marker(m) & parse_marker(m2) assert str(intersection) == expected_intersection union = parse_marker(m) | parse_marker(m2) assert str(union) == expected_union dep-logic-0.4.11/tests/marker/test_evaluation.py000066400000000000000000000143051475730624500216630ustar00rootroot00000000000000from __future__ import annotations import os import pytest from dep_logic.markers import parse_marker @pytest.mark.parametrize( ("marker_string", "environment", "expected"), [ (f"os_name == '{os.name}'", None, True), ("os_name == 'foo'", {"os_name": "foo"}, True), ("os_name == 'foo'", {"os_name": "bar"}, False), ("'2.7' in python_version", {"python_version": "2.7.5"}, True), ("'2.7' not in python_version", {"python_version": "2.7"}, False), ( "os_name == 'foo' and python_version ~= '2.7.0'", {"os_name": "foo", "python_version": "2.7.6"}, True, ), ( "python_version ~= '2.7.0' and (os_name == 'foo' or " "os_name == 'bar')", {"os_name": "foo", "python_version": "2.7.4"}, True, ), ( "python_version ~= '2.7.0' and (os_name == 'foo' or " "os_name == 'bar')", {"os_name": "bar", "python_version": "2.7.4"}, True, ), ( "python_version ~= '2.7.0' and (os_name == 'foo' or " "os_name == 'bar')", {"os_name": "other", "python_version": "2.7.4"}, False, ), ("extra == 'security'", {"extra": "quux"}, False), ("extra == 'security'", {"extra": "security"}, True), ("extra == 'SECURITY'", {"extra": "security"}, True), ("extra == 'security'", {"extra": "SECURITY"}, True), ("extra == 'pep-685-norm'", {"extra": "PEP_685...norm"}, True), ( "extra == 'Different.punctuation..is...equal'", {"extra": "different__punctuation_is_EQUAL"}, True, ), ], ) def test_evaluates( marker_string: str, environment: dict[str, str], expected: bool ) -> None: args = [] if environment is None else [environment] assert parse_marker(marker_string).evaluate(*args) == expected @pytest.mark.parametrize( ("marker_string", "environment", "expected"), [ (f"os.name == '{os.name}'", None, True), ("sys.platform == 'win32'", {"sys_platform": "linux2"}, False), ("platform.version in 'Ubuntu'", {"platform_version": "#39"}, False), ("platform.machine=='x86_64'", {"platform_machine": "x86_64"}, True), ( "platform.python_implementation=='Jython'", {"platform_python_implementation": "CPython"}, False, ), ( "python_version == '2.5' and platform.python_implementation!= 'Jython'", {"python_version": "2.7"}, False, ), ( ( "platform_machine in 'x86_64 X86_64 aarch64 AARCH64 ppc64le PPC64LE" " amd64 AMD64 win32 WIN32'" ), {"platform_machine": "foo"}, False, ), ( ( "platform_machine in 'x86_64 X86_64 aarch64 AARCH64 ppc64le PPC64LE" " amd64 AMD64 win32 WIN32'" ), {"platform_machine": "x86_64"}, True, ), ( ( "platform_machine not in 'x86_64 X86_64 aarch64 AARCH64 ppc64le PPC64LE" " amd64 AMD64 win32 WIN32'" ), {"platform_machine": "foo"}, True, ), ( ( "platform_machine not in 'x86_64 X86_64 aarch64 AARCH64 ppc64le PPC64LE" " amd64 AMD64 win32 WIN32'" ), {"platform_machine": "x86_64"}, False, ), # extras # single extra ("extra != 'security'", {"extra": "quux"}, True), ("extra != 'security'", {"extra": "security"}, False), ("extra != 'security'", {}, True), ("extra != 'security'", {"platform_machine": "x86_64"}, True), # normalization ("extra == 'Security.1'", {"extra": "security-1"}, True), ("extra == 'a'", {}, False), ("extra != 'a'", {}, True), ("extra == 'a' and extra == 'b'", {}, False), ("extra == 'a' or extra == 'b'", {}, False), ("extra != 'a' and extra != 'b'", {}, True), ("extra != 'a' or extra != 'b'", {}, True), ("extra != 'a' and extra == 'b'", {}, False), ("extra != 'a' or extra == 'b'", {}, True), # multiple extras ("extra == 'a'", {"extra": ("a", "b")}, True), ("extra == 'a'", {"extra": ("b", "c")}, False), ("extra != 'a'", {"extra": ("a", "b")}, False), ("extra != 'a'", {"extra": ("b", "c")}, True), ("extra == 'a' and extra == 'b'", {"extra": ("a", "b", "c")}, True), ("extra == 'a' and extra == 'b'", {"extra": ("a", "c")}, False), ("extra == 'a' or extra == 'b'", {"extra": ("a", "c")}, True), ("extra == 'a' or extra == 'b'", {"extra": ("b", "c")}, True), ("extra == 'a' or extra == 'b'", {"extra": ("c", "d")}, False), ("extra != 'a' and extra != 'b'", {"extra": ("a", "c")}, False), ("extra != 'a' and extra != 'b'", {"extra": ("b", "c")}, False), ("extra != 'a' and extra != 'b'", {"extra": ("c", "d")}, True), ("extra != 'a' or extra != 'b'", {"extra": ("a", "b", "c")}, False), ("extra != 'a' or extra != 'b'", {"extra": ("a", "c")}, True), ("extra != 'a' or extra != 'b'", {"extra": ("b", "c")}, True), ("extra != 'a' and extra == 'b'", {"extra": ("a", "b")}, False), ("extra != 'a' and extra == 'b'", {"extra": ("b", "c")}, True), ("extra != 'a' and extra == 'b'", {"extra": ("c", "d")}, False), ("extra != 'a' or extra == 'b'", {"extra": ("a", "b")}, True), ("extra != 'a' or extra == 'b'", {"extra": ("c", "d")}, True), ("extra != 'a' or extra == 'b'", {"extra": ("a", "c")}, False), ], ) def test_evaluate_extra( marker_string: str, environment: dict[str, str] | None, expected: bool ) -> None: m = parse_marker(marker_string) assert m.evaluate(environment) is expected @pytest.mark.parametrize( "marker, env", [ ( 'platform_release >= "9.0" and platform_release < "11.0"', {"platform_release": "10.0"}, ) ], ) def test_parse_version_like_markers(marker: str, env: dict[str, str]) -> None: m = parse_marker(marker) assert m.evaluate(env) dep-logic-0.4.11/tests/marker/test_expression.py000066400000000000000000000232271475730624500217160ustar00rootroot00000000000000import pytest from dep_logic.markers import parse_marker def test_single_marker_normalisation() -> None: m1 = parse_marker("python_version>='3.6'") m2 = parse_marker("python_version >= '3.6'") assert m1 == m2 assert hash(m1) == hash(m2) def test_single_marker_intersect() -> None: m = parse_marker('sys_platform == "darwin"') intersection = m & parse_marker('implementation_name == "cpython"') assert ( str(intersection) == 'sys_platform == "darwin" and implementation_name == "cpython"' ) m = parse_marker('python_version >= "3.4"') intersection = m & parse_marker('python_version < "3.6"') assert str(intersection) == 'python_version >= "3.4" and python_version < "3.6"' def test_single_marker_intersect_compacts_constraints() -> None: m = parse_marker('python_version < "3.6"') intersection = m & parse_marker('python_version < "3.4"') assert str(intersection) == 'python_version < "3.4"' def test_single_marker_intersect_with_multi() -> None: m = parse_marker('sys_platform == "darwin"') intersection = m & ( parse_marker('implementation_name == "cpython" and python_version >= "3.6"') ) assert ( str(intersection) == 'implementation_name == "cpython" and python_version >= "3.6" and' ' sys_platform == "darwin"' ) def test_single_marker_intersect_with_multi_with_duplicate() -> None: m = parse_marker('python_version < "4.0"') intersection = m & ( parse_marker('sys_platform == "darwin" and python_version < "4.0"') ) assert str(intersection) == 'sys_platform == "darwin" and python_version < "4.0"' def test_single_marker_intersect_with_multi_compacts_constraint() -> None: m = parse_marker('python_version < "3.6"') intersection = m & ( parse_marker('implementation_name == "cpython" and python_version < "3.4"') ) assert ( str(intersection) == 'implementation_name == "cpython" and python_version < "3.4"' ) def test_single_marker_intersect_with_union_leads_to_single_marker() -> None: m = parse_marker('python_version >= "3.6"') intersection = m & ( parse_marker('python_version < "3.6" or python_version >= "3.7"') ) assert str(intersection) == 'python_version >= "3.7"' def test_single_marker_intersect_with_union_leads_to_empty() -> None: m = parse_marker('python_version == "3.7"') intersection = m & ( parse_marker('python_version < "3.7" or python_version >= "3.8"') ) assert intersection.is_empty() def test_single_marker_not_in_python_intersection() -> None: m = parse_marker('python_version not in "2.7, 3.0, 3.1"') intersection = m & (parse_marker('python_version not in "2.7, 3.0, 3.1, 3.2"')) assert str(intersection) == 'python_version not in "2.7, 3.0, 3.1, 3.2"' @pytest.mark.parametrize( ("marker1", "marker2", "expected"), [ # same value ('extra == "a"', 'extra == "a"', 'extra == "a"'), ('extra == "a"', 'extra != "a"', ""), ('extra != "a"', 'extra == "a"', ""), ('extra != "a"', 'extra != "a"', 'extra != "a"'), # different values ('extra == "a"', 'extra == "b"', 'extra == "a" and extra == "b"'), ('extra == "a"', 'extra != "b"', 'extra == "a" and extra != "b"'), ('extra != "a"', 'extra == "b"', 'extra != "a" and extra == "b"'), ('extra != "a"', 'extra != "b"', 'extra != "a" and extra != "b"'), ], ) def test_single_marker_intersect_extras( marker1: str, marker2: str, expected: str ) -> None: assert str(parse_marker(marker1) & parse_marker(marker2)) == expected def test_single_marker_union() -> None: m = parse_marker('sys_platform == "darwin"') union = m | (parse_marker('implementation_name == "cpython"')) assert str(union) == 'sys_platform == "darwin" or implementation_name == "cpython"' def test_single_marker_union_is_any() -> None: m = parse_marker('python_version >= "3.4"') union = m | (parse_marker('python_version < "3.6"')) assert union.is_any() @pytest.mark.parametrize( ("marker1", "marker2", "expected"), [ ( 'python_version < "3.6"', 'python_version < "3.4"', 'python_version < "3.6"', ), ( 'sys_platform == "linux"', 'sys_platform != "win32"', 'sys_platform != "win32"', ), ( 'python_version == "3.6"', 'python_version > "3.6"', 'python_version >= "3.6"', ), ( 'python_version == "3.6"', 'python_version < "3.6"', 'python_version <= "3.6"', ), ( 'python_version < "3.6"', 'python_version > "3.6"', 'python_version != "3.6"', ), ], ) def test_single_marker_union_is_single_marker( marker1: str, marker2: str, expected: str ) -> None: m = parse_marker(marker1) union = m | (parse_marker(marker2)) assert str(union) == expected def test_single_marker_union_with_multi() -> None: m = parse_marker('sys_platform == "darwin"') union = m | ( parse_marker('implementation_name == "cpython" and python_version >= "3.6"') ) assert ( str(union) == 'implementation_name == "cpython" and python_version >= "3.6" or' ' sys_platform == "darwin"' ) def test_single_marker_union_with_multi_duplicate() -> None: m = parse_marker('sys_platform == "darwin" and python_version >= "3.6"') union = m | (parse_marker('sys_platform == "darwin" and python_version >= "3.6"')) assert str(union) == 'sys_platform == "darwin" and python_version >= "3.6"' @pytest.mark.parametrize( ("single_marker", "multi_marker", "expected"), [ ( 'python_version >= "3.6"', 'python_version >= "3.7" and sys_platform == "win32"', 'python_version >= "3.6"', ), ( 'sys_platform == "linux"', 'sys_platform != "linux" and sys_platform != "win32"', 'sys_platform != "win32"', ), ], ) def test_single_marker_union_with_multi_is_single_marker( single_marker: str, multi_marker: str, expected: str ) -> None: m1 = parse_marker(single_marker) m2 = parse_marker(multi_marker) assert str(m1 | (m2)) == expected assert str(m2 | (m1)) == expected def test_single_marker_union_with_multi_cannot_be_simplified() -> None: m = parse_marker('python_version >= "3.7"') union = m | (parse_marker('python_version >= "3.6" and sys_platform == "win32"')) assert ( str(union) == 'python_version >= "3.6" and sys_platform == "win32" or python_version >=' ' "3.7"' ) def test_single_marker_union_with_multi_is_union_of_single_markers() -> None: m = parse_marker('python_version >= "3.6"') union = m | (parse_marker('python_version < "3.6" and sys_platform == "win32"')) assert str(union) == 'sys_platform == "win32" or python_version >= "3.6"' def test_single_marker_union_with_multi_union_is_union_of_single_markers() -> None: m = parse_marker('python_version >= "3.6"') union = m | ( parse_marker( 'python_version < "3.6" and sys_platform == "win32" or python_version <' ' "3.6" and sys_platform == "linux"' ) ) assert ( str(union) == 'sys_platform == "win32" or sys_platform == "linux" or python_version >=' ' "3.6"' ) def test_single_marker_union_with_union() -> None: m = parse_marker('sys_platform == "darwin"') union = m | ( parse_marker('implementation_name == "cpython" or python_version >= "3.6"') ) assert ( str(union) == 'implementation_name == "cpython" or python_version >= "3.6" or sys_platform' ' == "darwin"' ) def test_single_marker_not_in_python_union() -> None: m = parse_marker('python_version not in "2.7, 3.0, 3.1"') union = m | parse_marker('python_version not in "2.7, 3.0, 3.1, 3.2"') assert str(union) == 'python_version not in "2.7, 3.0, 3.1"' def test_single_marker_union_with_union_duplicate() -> None: m = parse_marker('sys_platform == "darwin"') union = m | (parse_marker('sys_platform == "darwin" or python_version >= "3.6"')) assert str(union) == 'sys_platform == "darwin" or python_version >= "3.6"' m = parse_marker('python_version >= "3.7"') union = m | (parse_marker('sys_platform == "darwin" or python_version >= "3.6"')) assert str(union) == 'sys_platform == "darwin" or python_version >= "3.6"' m = parse_marker('python_version <= "3.6"') union = m | (parse_marker('sys_platform == "darwin" or python_version < "3.4"')) assert str(union) == 'sys_platform == "darwin" or python_version <= "3.6"' def test_single_marker_union_with_inverse() -> None: m = parse_marker('sys_platform == "darwin"') union = m | (parse_marker('sys_platform != "darwin"')) assert union.is_any() @pytest.mark.parametrize( ("marker1", "marker2", "expected"), [ # same value ('extra == "a"', 'extra == "a"', 'extra == "a"'), ('extra == "a"', 'extra != "a"', ""), ('extra != "a"', 'extra == "a"', ""), ('extra != "a"', 'extra != "a"', 'extra != "a"'), # different values ('extra == "a"', 'extra == "b"', 'extra == "a" or extra == "b"'), ('extra == "a"', 'extra != "b"', 'extra == "a" or extra != "b"'), ('extra != "a"', 'extra == "b"', 'extra != "a" or extra == "b"'), ('extra != "a"', 'extra != "b"', 'extra != "a" or extra != "b"'), ], ) def test_single_marker_union_extras(marker1: str, marker2: str, expected: str) -> None: assert str(parse_marker(marker1) | (parse_marker(marker2))) == expected dep-logic-0.4.11/tests/marker/test_parsing.py000066400000000000000000000035661475730624500211660ustar00rootroot00000000000000import itertools import pytest from dep_logic.markers import InvalidMarker, parse_marker VARIABLES = [ "extra", "implementation_name", "implementation_version", "os_name", "platform_machine", "platform_release", "platform_system", "platform_version", "python_full_version", "python_version", "platform_python_implementation", "sys_platform", ] PEP_345_VARIABLES = [ "os.name", "sys.platform", "platform.version", "platform.machine", "platform.python_implementation", ] OPERATORS = ["===", "==", ">=", "<=", "!=", "~=", ">", "<", "in", "not in"] VALUES = [ "1.0", "5.6a0", "dog", "freebsd", "literally any string can go here", "things @#4 dsfd (((", ] @pytest.mark.parametrize( "marker_string", ["{} {} {!r}".format(*i) for i in itertools.product(VARIABLES, OPERATORS, VALUES)] + [ "{2!r} {1} {0}".format(*i) for i in itertools.product(VARIABLES, OPERATORS, VALUES) ], ) def test_parses_valid(marker_string: str): parse_marker(marker_string) @pytest.mark.parametrize( "marker_string", [ "this_isnt_a_real_variable >= '1.0'", "python_version", "(python_version)", "python_version >= 1.0 and (python_version)", '(python_version == "2.7" and os_name == "linux"', '(python_version == "2.7") with random text', ], ) def test_parses_invalid(marker_string: str): with pytest.raises(InvalidMarker): parse_marker(marker_string) @pytest.mark.parametrize( "marker_string", [ "{} {} {!r}".format(*i) for i in itertools.product(PEP_345_VARIABLES, OPERATORS, VALUES) ] + [ "{2!r} {1} {0}".format(*i) for i in itertools.product(PEP_345_VARIABLES, OPERATORS, VALUES) ], ) def test_parses_pep345_valid(marker_string: str) -> None: parse_marker(marker_string) dep-logic-0.4.11/tests/specifier/000077500000000000000000000000001475730624500165705ustar00rootroot00000000000000dep-logic-0.4.11/tests/specifier/test_arbitrary.py000066400000000000000000000026671475730624500222130ustar00rootroot00000000000000import pytest from dep_logic.specifiers import parse_version_specifier @pytest.mark.parametrize( "a, b, expected", [ ("===abc", "", "===abc"), ("", "===abc", "===abc"), ("===abc", "===abc", "===abc"), ("===abc", "===def", ""), ("===abc", "", ""), ("", "===abc", ""), ("===1.0.0", ">=1", "===1.0.0"), ("===1.0.0", "<1", ""), ], ) def test_arbitrary_intersection(a: str, b: str, expected: str) -> None: assert str(parse_version_specifier(a) & parse_version_specifier(b)) == expected @pytest.mark.parametrize( "a, b, expected", [ ("===abc", "", ""), ("", "===abc", ""), ("===abc", "===abc", "===abc"), ("===abc", "", "===abc"), ("", "===abc", "===abc"), ("===1.0.0", ">=1", ">=1"), ], ) def test_arbitrary_union(a: str, b: str, expected: str) -> None: assert str(parse_version_specifier(a) | parse_version_specifier(b)) == expected @pytest.mark.parametrize( "a, b, operand", [("===abc", ">=1", "and"), ("===1.0.0", "<1", "or"), ("===abc", "==1.*", "or")], ) def test_arbitrary_unsupported(a: str, b: str, operand: str) -> None: with pytest.raises(ValueError): if operand == "and": _ = parse_version_specifier(a) & parse_version_specifier(b) else: _ = parse_version_specifier(a) | parse_version_specifier(b) dep-logic-0.4.11/tests/specifier/test_range.py000066400000000000000000000143011475730624500212740ustar00rootroot00000000000000from typing import cast import pytest from packaging.version import Version from dep_logic.specifiers import RangeSpecifier, parse_version_specifier @pytest.mark.parametrize( "value,parsed", [ ("", RangeSpecifier()), (">2.0.0", RangeSpecifier(min=Version("2.0.0"), include_min=False)), (">=2.0.0", RangeSpecifier(min=Version("2.0.0"), include_min=True)), ("<2.0.0", RangeSpecifier(max=Version("2.0.0"), include_max=False)), ("<=2.0.0", RangeSpecifier(max=Version("2.0.0"), include_max=True)), ( "==2.0.0", RangeSpecifier( min=Version("2.0.0"), max=Version("2.0.0"), include_min=True, include_max=True, ), ), ( "==2.0.0a1", RangeSpecifier( min=Version("2.0.0a1"), max=Version("2.0.0a1"), include_min=True, include_max=True, ), ), ( "==2.0.*", RangeSpecifier( min=Version("2.0.0"), max=Version("2.1.0"), include_min=True, include_max=False, ), ), ( "~=2.0.1", RangeSpecifier( min=Version("2.0.1"), max=Version("2.1.0"), include_min=True, include_max=False, ), ), ( "~=2.0.1dev2", RangeSpecifier( min=Version("2.0.1.dev2"), max=Version("2.1.0"), include_min=True, include_max=False, ), ), ], ) def test_parse_simple_range(value: str, parsed: RangeSpecifier) -> None: spec = parse_version_specifier(value) assert spec == parsed assert str(spec) == value assert spec.is_simple() @pytest.mark.parametrize( "a,b,expected", [ (">2.0.0", ">2.0.0", False), (">1.0.0", ">=1.0.1", True), ("", ">=2.0.0", True), (">=1.0.0", "", False), ("<1.0", ">=1.0", True), (">=1.0.0", ">1.0.0", True), (">1.0.0", ">=1.0.0", False), (">=1.0.0", ">=1.0.0", False), ], ) def test_range_compare_lower(a: str, b: str, expected: bool) -> None: assert ( cast(RangeSpecifier, parse_version_specifier(a)).allows_lower( cast(RangeSpecifier, parse_version_specifier(b)) ) is expected ) @pytest.mark.parametrize( "value,expected", [ (RangeSpecifier(), ""), (RangeSpecifier(min=Version("1.0")), ">1.0"), (RangeSpecifier(min=Version("1.0"), include_min=True), ">=1.0"), (RangeSpecifier(max=Version("1.0")), "<1.0"), (RangeSpecifier(max=Version("1.0")), "<1.0"), ( RangeSpecifier( min=Version("1.0"), max=Version("1.0"), include_min=True, include_max=True, ), "==1.0", ), (RangeSpecifier(min=Version("1.2"), max=Version("1.4")), ">1.2,<1.4"), ( RangeSpecifier(min=Version("1.2a2"), max=Version("1.4"), include_min=True), ">=1.2a2,<1.4", ), ( RangeSpecifier( min=Version("1.2"), max=Version("2"), include_min=True, ), "~=1.2", ), ( RangeSpecifier( min=Version("1.2r3"), max=Version("2"), include_min=True, ), "~=1.2.post3", ), ( RangeSpecifier( min=Version("1.2"), max=Version("2.0post1"), include_min=True, ), "~=1.2", ), ( RangeSpecifier( min=Version("1.2"), max=Version("1!2"), include_min=True, ), ">=1.2,<1!2", ), ( RangeSpecifier( min=Version("1.2"), max=Version("2"), ), ">1.2,<2", ), ( RangeSpecifier( min=Version("1.2"), max=Version("2"), include_min=True, include_max=True, ), ">=1.2,<=2", ), ], ) def test_range_str_normalization(value: RangeSpecifier, expected: str) -> None: assert str(value) == expected @pytest.mark.parametrize( "a,b,expected", [ ("", ">=1.0", ""), ("<1.0", "", ""), ("<1.0", ">=1.0", ""), (">=1.0", "<0.5", ""), (">=1.0,<1.5", ">1.5,<2", ""), (">=1.0", ">1.0", ">1.0"), (">=1.0,<2", ">=1.0", "~=1.0"), ("~=1.2", ">=1.3", "~=1.3"), (">=1.2,<1.8", "~=1.3", ">=1.3,<1.8"), (">=1.2", "<=1.2", "==1.2"), ], ) def test_range_intersection(a: str, b: str, expected: str) -> None: assert str(parse_version_specifier(a) & parse_version_specifier(b)) == expected @pytest.mark.parametrize( "value,inverted", [ ("", ""), (">1.0", "<=1.0"), (">=1.0", "<1.0"), ("~=1.2", "<1.2||>=2.0"), ("==1.2", "!=1.2"), ("~=1.2.0", "!=1.2.*"), ("<2||>=2.2", ">=2,<2.2"), ("<2||>=2.2,<2.4||>=3.0", ">=2,<2.2||~=2.4"), ], ) def test_range_invert(value: str, inverted: str) -> None: assert str(~parse_version_specifier(value)) == inverted assert str(~parse_version_specifier(inverted)) == value @pytest.mark.parametrize( "a,b,expected", [ ("", ">=1.0", ">=1.0"), (">1.0", "", ">1.0"), ("", "==1.0", ""), (">=1.0", "<0.6", "<0.6||>=1.0"), ("<2.0", "<=1.4", "<2.0"), ("==1.4", ">=1,<2", ">=1,<2"), (">=1.0,<2", ">=1.8,<2.2", ">=1.0,<2.2"), (">=1.0,<2.2", "==2.2", ">=1.0,<=2.2"), ("==1.2.*", "==1.4.4", "==1.2.*||==1.4.4"), (">=1.2.3", "<1.3", ""), ("<1.0", ">1.0", "!=1.0"), ], ) def test_range_union(a: str, b: str, expected: str) -> None: assert str(parse_version_specifier(a) | parse_version_specifier(b)) == expected dep-logic-0.4.11/tests/specifier/test_union.py000066400000000000000000000053671475730624500213440ustar00rootroot00000000000000import pytest from packaging.version import Version from dep_logic.specifiers import ( RangeSpecifier, UnionSpecifier, parse_version_specifier, ) @pytest.mark.parametrize( "spec,parsed", [ ( "!=1.2.3", UnionSpecifier( ( RangeSpecifier(max=Version("1.2.3")), RangeSpecifier(min=Version("1.2.3")), ) ), ), ( "!=1.2.*", UnionSpecifier( ( RangeSpecifier(max=Version("1.2.0")), RangeSpecifier(min=Version("1.3.0"), include_min=True), ) ), ), ], ) def test_parse_simple_union_specifier(spec: str, parsed: UnionSpecifier) -> None: value = parse_version_specifier(spec) assert value.is_simple() assert value == parsed assert str(value) == spec @pytest.mark.parametrize( "spec,parsed", [ ( "<3.0||>=3.6", UnionSpecifier( ( RangeSpecifier(max=Version("3.0.0")), RangeSpecifier(min=Version("3.6.0"), include_min=True), ) ), ), ( ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*", UnionSpecifier( ( RangeSpecifier( min=Version("2.7.0"), max=Version("3.0.0"), include_min=True ), RangeSpecifier(min=Version("3.7.0"), include_min=True), ) ), ), ], ) def test_parse_union_specifier(spec: str, parsed: UnionSpecifier) -> None: value = parse_version_specifier(spec) assert not value.is_simple() assert value == parsed @pytest.mark.parametrize( "a,b,expected", [ ("!=2.0", ">=1.0", "~=1.0||>2.0"), ("!=2.0", ">=2.0", ">2.0"), ("~=2.7||>=3.6", "==3.3", ""), ("~=2.7||>=3.6", "<3.0", "~=2.7"), ("~=2.7||==3.7.*", "<2.8||>=3.6", ">=2.7,<2.8||==3.7.*"), ], ) def test_union_intesection(a: str, b: str, expected: str) -> None: assert str(parse_version_specifier(a) & parse_version_specifier(b)) == expected @pytest.mark.parametrize( "a,b,expected", [ ("!=2.0", ">=1.0", ""), ("~=2.7||>=3.6", ">=3.0,<3.3", ">=2.7,<3.3||>=3.6"), ("~=2.7||>=3.6", ">=3.1,<3.3", "~=2.7||>=3.1,<3.3||>=3.6"), ("~=2.7||>=3.6", ">=3.0,<3.3||==3.4.*", ">=2.7,<3.3||==3.4.*||>=3.6"), ("~=2.7||>=3.6", "", "~=2.7||>=3.6"), ("~=2.7||>=3.6", "", ""), ], ) def test_union_union(a: str, b: str, expected: str) -> None: assert str(parse_version_specifier(a) | parse_version_specifier(b)) == expected dep-logic-0.4.11/tests/tags/000077500000000000000000000000001475730624500155555ustar00rootroot00000000000000dep-logic-0.4.11/tests/tags/test_platform.py000066400000000000000000000253441475730624500210220ustar00rootroot00000000000000import pytest from dep_logic.tags import os from dep_logic.tags.platform import Arch, Platform def test_platform_tags_manylinux(): tags = Platform(os.Manylinux(2, 20), Arch.X86_64).compatible_tags assert tags == [ "manylinux_2_20_x86_64", "manylinux_2_19_x86_64", "manylinux_2_18_x86_64", "manylinux_2_17_x86_64", "manylinux2014_x86_64", "manylinux_2_16_x86_64", "manylinux_2_15_x86_64", "manylinux_2_14_x86_64", "manylinux_2_13_x86_64", "manylinux_2_12_x86_64", "manylinux2010_x86_64", "manylinux_2_11_x86_64", "manylinux_2_10_x86_64", "manylinux_2_9_x86_64", "manylinux_2_8_x86_64", "manylinux_2_7_x86_64", "manylinux_2_6_x86_64", "manylinux_2_5_x86_64", "manylinux1_x86_64", "linux_x86_64", ] def test_platform_tags_macos(): tags = Platform(os.Macos(21, 6), Arch.X86_64).compatible_tags assert tags == [ "macosx_21_0_x86_64", "macosx_21_0_intel", "macosx_21_0_fat64", "macosx_21_0_fat32", "macosx_21_0_universal2", "macosx_21_0_universal", "macosx_20_0_x86_64", "macosx_20_0_intel", "macosx_20_0_fat64", "macosx_20_0_fat32", "macosx_20_0_universal2", "macosx_20_0_universal", "macosx_19_0_x86_64", "macosx_19_0_intel", "macosx_19_0_fat64", "macosx_19_0_fat32", "macosx_19_0_universal2", "macosx_19_0_universal", "macosx_18_0_x86_64", "macosx_18_0_intel", "macosx_18_0_fat64", "macosx_18_0_fat32", "macosx_18_0_universal2", "macosx_18_0_universal", "macosx_17_0_x86_64", "macosx_17_0_intel", "macosx_17_0_fat64", "macosx_17_0_fat32", "macosx_17_0_universal2", "macosx_17_0_universal", "macosx_16_0_x86_64", "macosx_16_0_intel", "macosx_16_0_fat64", "macosx_16_0_fat32", "macosx_16_0_universal2", "macosx_16_0_universal", "macosx_15_0_x86_64", "macosx_15_0_intel", "macosx_15_0_fat64", "macosx_15_0_fat32", "macosx_15_0_universal2", "macosx_15_0_universal", "macosx_14_0_x86_64", "macosx_14_0_intel", "macosx_14_0_fat64", "macosx_14_0_fat32", "macosx_14_0_universal2", "macosx_14_0_universal", "macosx_13_0_x86_64", "macosx_13_0_intel", "macosx_13_0_fat64", "macosx_13_0_fat32", "macosx_13_0_universal2", "macosx_13_0_universal", "macosx_12_0_x86_64", "macosx_12_0_intel", "macosx_12_0_fat64", "macosx_12_0_fat32", "macosx_12_0_universal2", "macosx_12_0_universal", "macosx_11_0_x86_64", "macosx_11_0_intel", "macosx_11_0_fat64", "macosx_11_0_fat32", "macosx_11_0_universal2", "macosx_11_0_universal", "macosx_10_16_x86_64", "macosx_10_16_intel", "macosx_10_16_fat64", "macosx_10_16_fat32", "macosx_10_16_universal2", "macosx_10_16_universal", "macosx_10_15_x86_64", "macosx_10_15_intel", "macosx_10_15_fat64", "macosx_10_15_fat32", "macosx_10_15_universal2", "macosx_10_15_universal", "macosx_10_14_x86_64", "macosx_10_14_intel", "macosx_10_14_fat64", "macosx_10_14_fat32", "macosx_10_14_universal2", "macosx_10_14_universal", "macosx_10_13_x86_64", "macosx_10_13_intel", "macosx_10_13_fat64", "macosx_10_13_fat32", "macosx_10_13_universal2", "macosx_10_13_universal", "macosx_10_12_x86_64", "macosx_10_12_intel", "macosx_10_12_fat64", "macosx_10_12_fat32", "macosx_10_12_universal2", "macosx_10_12_universal", "macosx_10_11_x86_64", "macosx_10_11_intel", "macosx_10_11_fat64", "macosx_10_11_fat32", "macosx_10_11_universal2", "macosx_10_11_universal", "macosx_10_10_x86_64", "macosx_10_10_intel", "macosx_10_10_fat64", "macosx_10_10_fat32", "macosx_10_10_universal2", "macosx_10_10_universal", "macosx_10_9_x86_64", "macosx_10_9_intel", "macosx_10_9_fat64", "macosx_10_9_fat32", "macosx_10_9_universal2", "macosx_10_9_universal", "macosx_10_8_x86_64", "macosx_10_8_intel", "macosx_10_8_fat64", "macosx_10_8_fat32", "macosx_10_8_universal2", "macosx_10_8_universal", "macosx_10_7_x86_64", "macosx_10_7_intel", "macosx_10_7_fat64", "macosx_10_7_fat32", "macosx_10_7_universal2", "macosx_10_7_universal", "macosx_10_6_x86_64", "macosx_10_6_intel", "macosx_10_6_fat64", "macosx_10_6_fat32", "macosx_10_6_universal2", "macosx_10_6_universal", "macosx_10_5_x86_64", "macosx_10_5_intel", "macosx_10_5_fat64", "macosx_10_5_fat32", "macosx_10_5_universal2", "macosx_10_5_universal", "macosx_10_4_x86_64", "macosx_10_4_intel", "macosx_10_4_fat64", "macosx_10_4_fat32", "macosx_10_4_universal2", "macosx_10_4_universal", ] tags = Platform(os.Macos(14, 0), Arch.X86_64).compatible_tags assert tags == [ "macosx_14_0_x86_64", "macosx_14_0_intel", "macosx_14_0_fat64", "macosx_14_0_fat32", "macosx_14_0_universal2", "macosx_14_0_universal", "macosx_13_0_x86_64", "macosx_13_0_intel", "macosx_13_0_fat64", "macosx_13_0_fat32", "macosx_13_0_universal2", "macosx_13_0_universal", "macosx_12_0_x86_64", "macosx_12_0_intel", "macosx_12_0_fat64", "macosx_12_0_fat32", "macosx_12_0_universal2", "macosx_12_0_universal", "macosx_11_0_x86_64", "macosx_11_0_intel", "macosx_11_0_fat64", "macosx_11_0_fat32", "macosx_11_0_universal2", "macosx_11_0_universal", "macosx_10_16_x86_64", "macosx_10_16_intel", "macosx_10_16_fat64", "macosx_10_16_fat32", "macosx_10_16_universal2", "macosx_10_16_universal", "macosx_10_15_x86_64", "macosx_10_15_intel", "macosx_10_15_fat64", "macosx_10_15_fat32", "macosx_10_15_universal2", "macosx_10_15_universal", "macosx_10_14_x86_64", "macosx_10_14_intel", "macosx_10_14_fat64", "macosx_10_14_fat32", "macosx_10_14_universal2", "macosx_10_14_universal", "macosx_10_13_x86_64", "macosx_10_13_intel", "macosx_10_13_fat64", "macosx_10_13_fat32", "macosx_10_13_universal2", "macosx_10_13_universal", "macosx_10_12_x86_64", "macosx_10_12_intel", "macosx_10_12_fat64", "macosx_10_12_fat32", "macosx_10_12_universal2", "macosx_10_12_universal", "macosx_10_11_x86_64", "macosx_10_11_intel", "macosx_10_11_fat64", "macosx_10_11_fat32", "macosx_10_11_universal2", "macosx_10_11_universal", "macosx_10_10_x86_64", "macosx_10_10_intel", "macosx_10_10_fat64", "macosx_10_10_fat32", "macosx_10_10_universal2", "macosx_10_10_universal", "macosx_10_9_x86_64", "macosx_10_9_intel", "macosx_10_9_fat64", "macosx_10_9_fat32", "macosx_10_9_universal2", "macosx_10_9_universal", "macosx_10_8_x86_64", "macosx_10_8_intel", "macosx_10_8_fat64", "macosx_10_8_fat32", "macosx_10_8_universal2", "macosx_10_8_universal", "macosx_10_7_x86_64", "macosx_10_7_intel", "macosx_10_7_fat64", "macosx_10_7_fat32", "macosx_10_7_universal2", "macosx_10_7_universal", "macosx_10_6_x86_64", "macosx_10_6_intel", "macosx_10_6_fat64", "macosx_10_6_fat32", "macosx_10_6_universal2", "macosx_10_6_universal", "macosx_10_5_x86_64", "macosx_10_5_intel", "macosx_10_5_fat64", "macosx_10_5_fat32", "macosx_10_5_universal2", "macosx_10_5_universal", "macosx_10_4_x86_64", "macosx_10_4_intel", "macosx_10_4_fat64", "macosx_10_4_fat32", "macosx_10_4_universal2", "macosx_10_4_universal", ] tags = Platform(os.Macos(10, 6), Arch.X86_64).compatible_tags assert tags == [ "macosx_10_6_x86_64", "macosx_10_6_intel", "macosx_10_6_fat64", "macosx_10_6_fat32", "macosx_10_6_universal2", "macosx_10_6_universal", "macosx_10_5_x86_64", "macosx_10_5_intel", "macosx_10_5_fat64", "macosx_10_5_fat32", "macosx_10_5_universal2", "macosx_10_5_universal", "macosx_10_4_x86_64", "macosx_10_4_intel", "macosx_10_4_fat64", "macosx_10_4_fat32", "macosx_10_4_universal2", "macosx_10_4_universal", ] def test_platform_tags_windows(): tags = Platform(os.Windows(), Arch.X86_64).compatible_tags assert tags == ["win_amd64"] def test_platform_tags_musl(): tags = Platform(os.Musllinux(1, 2), Arch.Aarch64).compatible_tags assert tags == ["linux_aarch64", "musllinux_1_1_aarch64", "musllinux_1_2_aarch64"] @pytest.mark.parametrize( "text,expected,normalized", [ ("linux", Platform(os.Manylinux(2, 17), Arch.X86_64), "manylinux_2_17_x86_64"), ("macos", Platform(os.Macos(14, 0), Arch.Aarch64), "macos_14_0_arm64"), ("windows", Platform(os.Windows(), Arch.X86_64), "windows_amd64"), ("alpine", Platform(os.Musllinux(1, 2), Arch.X86_64), "musllinux_1_2_x86_64"), ( "manylinux_2_20_aarch64", Platform(os.Manylinux(2, 20), Arch.Aarch64), "manylinux_2_20_aarch64", ), ( "macos_14_0_arm64", Platform(os.Macos(14, 0), Arch.Aarch64), "macos_14_0_arm64", ), ("windows_amd64", Platform(os.Windows(), Arch.X86_64), "windows_amd64"), ("windows_arm64", Platform(os.Windows(), Arch.Aarch64), "windows_arm64"), ( "macos_12_0_x86_64", Platform(os.Macos(12, 0), Arch.X86_64), "macos_12_0_x86_64", ), ( "mingw_x86_64", Platform(os.Generic("mingw"), Arch.X86_64), "mingw_x86_64", ), ], ) def test_parse_platform(text, expected, normalized): platform = Platform.parse(text) assert platform == expected assert str(platform) == normalized dep-logic-0.4.11/tests/tags/test_tags.py000066400000000000000000000132531475730624500201300ustar00rootroot00000000000000import pytest from dep_logic.tags import EnvSpec from dep_logic.tags.tags import EnvCompatibility def test_check_wheel_tags(): wheels = [ "protobuf-5.27.2-cp313-cp313t-macosx_14_0_arm64.whl", "protobuf-5.27.2-cp313-cp313-macosx_14_0_arm64.whl", "protobuf-5.27.2-cp310-abi3-win32.whl", "protobuf-5.27.2-cp310-abi3-win_amd64.whl", "protobuf-5.27.2-cp310-cp310-macosx_12_0_arm64.whl", "protobuf-5.27.2-cp38-abi3-macosx_10_9_universal2.whl", "protobuf-5.27.2-cp38-abi3-manylinux2014_aarch64.whl", "protobuf-5.27.2-cp38-abi3-manylinux2014_x86_64.whl", "protobuf-5.27.2-cp38-cp38-win32.whl", "protobuf-5.27.2-cp38-cp38-win_amd64.whl", "protobuf-5.27.2-cp39-cp39-win32.whl", "protobuf-5.27.2-cp39-cp39-win_amd64.whl", "protobuf-5.27.2-py3-none-any.whl", ] linux_env = EnvSpec.from_spec(">=3.9", "linux", "cpython") wheel_compats = { f: c for f, c in {f: linux_env.wheel_compatibility(f) for f in wheels}.items() if c is not None } filtered_wheels = sorted(wheel_compats, key=wheel_compats.__getitem__, reverse=True) assert filtered_wheels == [ "protobuf-5.27.2-cp38-abi3-manylinux2014_x86_64.whl", "protobuf-5.27.2-py3-none-any.whl", ] windows_env = EnvSpec.from_spec(">=3.9", "windows", "cpython") wheel_compats = { f: c for f, c in {f: windows_env.wheel_compatibility(f) for f in wheels}.items() if c is not None } filtered_wheels = sorted(wheel_compats, key=wheel_compats.__getitem__, reverse=True) assert filtered_wheels == [ "protobuf-5.27.2-cp310-abi3-win_amd64.whl", "protobuf-5.27.2-cp39-cp39-win_amd64.whl", "protobuf-5.27.2-py3-none-any.whl", ] macos_env = EnvSpec.from_spec(">=3.9", "macos", "cpython") wheel_compats = { f: c for f, c in {f: macos_env.wheel_compatibility(f) for f in wheels}.items() if c is not None } filtered_wheels = sorted(wheel_compats, key=wheel_compats.__getitem__, reverse=True) assert filtered_wheels == [ "protobuf-5.27.2-cp313-cp313-macosx_14_0_arm64.whl", "protobuf-5.27.2-cp310-cp310-macosx_12_0_arm64.whl", "protobuf-5.27.2-cp38-abi3-macosx_10_9_universal2.whl", "protobuf-5.27.2-py3-none-any.whl", ] macos_free_threaded_env = EnvSpec.from_spec(">=3.9", "macos", "cpython", True) wheel_compats = { f: c for f, c in { f: macos_free_threaded_env.wheel_compatibility(f) for f in wheels }.items() if c is not None } filtered_wheels = sorted(wheel_compats, key=wheel_compats.__getitem__, reverse=True) assert filtered_wheels == [ "protobuf-5.27.2-cp313-cp313t-macosx_14_0_arm64.whl", "protobuf-5.27.2-py3-none-any.whl", ] python_env = EnvSpec.from_spec(">=3.9") wheel_compats = { f: c for f, c in {f: python_env.wheel_compatibility(f) for f in wheels}.items() if c is not None } filtered_wheels = sorted(wheel_compats, key=wheel_compats.__getitem__, reverse=True) assert filtered_wheels == [ "protobuf-5.27.2-cp313-cp313t-macosx_14_0_arm64.whl", "protobuf-5.27.2-cp313-cp313-macosx_14_0_arm64.whl", "protobuf-5.27.2-cp310-cp310-macosx_12_0_arm64.whl", "protobuf-5.27.2-cp310-abi3-win32.whl", "protobuf-5.27.2-cp310-abi3-win_amd64.whl", "protobuf-5.27.2-cp39-cp39-win32.whl", "protobuf-5.27.2-cp39-cp39-win_amd64.whl", "protobuf-5.27.2-cp38-abi3-macosx_10_9_universal2.whl", "protobuf-5.27.2-cp38-abi3-manylinux2014_aarch64.whl", "protobuf-5.27.2-cp38-abi3-manylinux2014_x86_64.whl", "protobuf-5.27.2-py3-none-any.whl", ] @pytest.mark.parametrize( "left,right,expected", [ ( EnvSpec.from_spec(">=3.9", "macos", "cpython"), EnvSpec.from_spec(">=3.9", "macos", "cpython"), EnvCompatibility.LOWER_OR_EQUAL, ), ( EnvSpec.from_spec(">=3.9", "macos", "cpython"), EnvSpec.from_spec(">=3.9", "macos"), EnvCompatibility.LOWER_OR_EQUAL, ), ( EnvSpec.from_spec(">=3.9", "macos"), EnvSpec.from_spec(">=3.9", "macos", "cpython"), EnvCompatibility.LOWER_OR_EQUAL, ), ( EnvSpec.from_spec(">=3.9", "macos", "cpython"), EnvSpec.from_spec(">=3.7,<3.10"), EnvCompatibility.LOWER_OR_EQUAL, ), ( EnvSpec.from_spec(">=3.7,<3.10"), EnvSpec.from_spec(">=3.9", "macos", "cpython"), EnvCompatibility.LOWER_OR_EQUAL, ), ( EnvSpec.from_spec(">=3.9", "macos", "cpython"), EnvSpec.from_spec("<3.8", "macos", "cpython"), EnvCompatibility.INCOMPATIBLE, ), ( EnvSpec.from_spec(">=3.9", "macos", "cpython"), EnvSpec.from_spec("<3.10", "linux", "cpython"), EnvCompatibility.INCOMPATIBLE, ), ( EnvSpec.from_spec(">=3.9", "macos", "cpython"), EnvSpec.from_spec("<3.10", "macos", "pypy"), EnvCompatibility.INCOMPATIBLE, ), ( EnvSpec.from_spec(">=3.9", "macos_x86_64", "cpython"), EnvSpec.from_spec("<3.10", "macos_10_9_x86_64", "cpython"), EnvCompatibility.HIGHER, ), ( EnvSpec.from_spec("<3.10", "macos_10_9_x86_64", "cpython"), EnvSpec.from_spec(">=3.9", "macos_x86_64", "cpython"), EnvCompatibility.LOWER_OR_EQUAL, ), ], ) def test_env_spec_comparison(left, right, expected): assert left.compare(right) == expected