Repository: conan-io/conan
Pull request #17658 (2025-01-29 13:03:12)

Added in_range method to Version class
Changelog: Feature: Introducing `in_range` method in Version which allows comparing against version ranges
Docs: TODO

- [ ] Refer to the issue that supports this Pull Request.
- [x] If the issue has missing info, explain the purpose/use case/pain/need that covers this Pull Request.
- [x] I've read the [Contributing guide](https://github.com/conan-io/conan/blob/develop2/.github/CONTRIBUTING.md).
- [x] I've followed the PEP8 style guides for Python code.
- [ ] I've opened another PR in the Conan docs repo to the ``develop`` branch, documenting this one.



Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 000000000..02a8ad199
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,103 @@
+name: Main Workflow
+on:
+  push:
+    branches:
+      - develop2
+      - release/*
+      - '*'
+  pull_request:
+    branches:
+      - '*'
+      - 'release/*'
+  workflow_dispatch:
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  linux_suite:
+    uses: ./.github/workflows/linux-tests.yml
+    name: Linux test suite
+
+  osx_suite:
+    uses: ./.github/workflows/osx-tests.yml
+    name: OSX test suite
+
+  windows_suite:
+    uses: ./.github/workflows/win-tests.yml
+    name: Windows test suite
+
+  code_coverage:
+    runs-on: ubuntu-latest
+    name: Code coverage
+    if: github.ref == 'refs/heads/develop2'         # Only measure code coverage on main branch
+    needs: [linux_suite, osx_suite, windows_suite]
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+
+      - name: Download coverage artifacts
+        uses: actions/download-artifact@v4
+        with:
+          merge-multiple: true
+
+      - name: Merge coverage reports
+        run: |
+          pip install coverage
+          coverage combine
+          coverage report
+          coverage xml
+
+      - name: Code coverage
+        uses: codecov/codecov-action@v5
+        with:
+          token: ${{ secrets.CODECOV_TOKEN }}
+
+      - uses: geekyeggo/delete-artifact@v5
+        with:
+          name: |
+            .coverage.*
+
+  deploy_to_pypi_test:
+    runs-on: ubuntu-latest
+    name: Deploy to TestPyPI
+    if: github.ref == 'refs/heads/develop2'
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+
+      - name: Set up Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: 3.9
+
+      - name: Install dependencies
+        run: |
+          pip install --upgrade pip
+          pip install twine
+
+      - name: Bump Dev Version
+        run: |
+          python .ci/bump_dev_version.py
+
+      - name: Build Package
+        run: |
+          python setup.py sdist
+
+      - name: Upload to TestPyPI
+        env:
+          TWINE_USERNAME: ${{ secrets.TEST_PYPI_USERNAME }}
+          TWINE_PASSWORD: ${{ secrets.TEST_PYPI_PASSWORD }}
+        run: |
+          python -m twine upload --verbose --repository-url https://test.pypi.org/legacy/ dist/*
+
+      - name: Deploy conan-server to TestPyPI
+        env:
+          TWINE_USERNAME: ${{ secrets.TEST_PYPI_SERVER_USERNAME }}
+          TWINE_PASSWORD: ${{ secrets.TEST_PYPI_SERVER_PASSWORD }}
+        run: |
+          rm -rf dist/
+          mv setup_server.py setup.py
+          python setup.py sdist
+          python -m twine upload --verbose --repository-url https://test.pypi.org/legacy/ dist/*
diff --git a/.gitignore b/.gitignore
index 87824d101..593c94534 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,7 @@ parts/
 sdist/
 var/
 venv/
+.venv/
 *.egg-info/
 .installed.cfg
 *.egg
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 000000000..561bf760e
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,1 @@
+comment: false # Disable codecov PR comments -> leave only the checks
diff --git a/conan/internal/model/version.py b/conan/internal/model/version.py
index dd87b3586..ab7518c3f 100644
--- a/conan/internal/model/version.py
+++ b/conan/internal/model/version.py
@@ -1,4 +1,5 @@
 from functools import total_ordering
+from typing import Optional
 
 from conan.errors import ConanException
 
@@ -193,3 +194,8 @@ class Version:
                     return self._nonzero_items < other._nonzero_items
             else:  # None of them is pre-release
                 return (self._nonzero_items, self._build) < (other._nonzero_items, other._build)
+
+    def in_range(self, version_range: str, resolve_prerelease: Optional[bool] = None):
+        """ Check if the version is in the specified range """
+        from conan.internal.model.version_range import VersionRange
+        return VersionRange(version_range).contains(self, resolve_prerelease=resolve_prerelease)
diff --git a/conans/requirements_dev.txt b/conans/requirements_dev.txt
index e9e964c45..b2876e233 100644
--- a/conans/requirements_dev.txt
+++ b/conans/requirements_dev.txt
@@ -8,3 +8,4 @@ PyJWT
 pluginbase
 docker
 setuptools
+pytest-cov
diff --git a/setup.cfg b/setup.cfg
index 6ba9f60b4..ca70e3f2f 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -3,3 +3,11 @@ count = False
 max-line-length = 100
 statistics = False
 
+; Coverage files are generated across different OS and machines, thus, coverage
+; reports will contain different absolute paths to reference to the same
+; project relative paths.
+; This option makes coverage to use relative paths to the project root,
+; removing the need for setting arbitrary runners paths described in here:
+; https://coverage.readthedocs.io/en/coverage-4.0.3/config.html#paths
+[coverage:run]
+relative_files = True


Test patch:
diff --git a/.github/actions/test-coverage/action.yml b/.github/actions/test-coverage/action.yml
new file mode 100644
index 000000000..2b0575757
--- /dev/null
+++ b/.github/actions/test-coverage/action.yml
@@ -0,0 +1,46 @@
+
+description: 'Run tests enabling coverage in certain conditions and upload coverage artifacts for later process'
+
+inputs:
+  python-version:
+    description: 'Python version in which the tests was ran'
+    required: true
+
+  test-type:
+    description: 'Test suite name'
+    required: true
+
+  duration:
+    description: 'Tests maximum duration'
+    required: true
+    default: '20'
+
+  tests:
+    description: 'Tests folder and options to run'
+    required: false
+
+  workers:
+    description: 'Number of workers to run tests'
+    default: auto
+
+runs:
+  using: 'composite'
+  steps:
+      - name: Run tests with coverage
+        if: ${{ inputs.tests }}
+        shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }}
+        run: |
+          pytest ${{ inputs.tests }} --durations=${{ inputs.duration }} -n ${{ inputs.workers }} ${{ github.ref == 'refs/heads/develop2' && '--cov=conan --cov=conans --cov=test --cov-report=term-missing:skip-covered' || '' }}
+
+      - name: Rename coverage file
+        if: github.ref == 'refs/heads/develop2'
+        shell: bash
+        run: mv .coverage .coverage.${{ runner.os }}-${{ inputs.python-version }}-${{ inputs.test-type }}
+
+      - name: Upload coverage artifact
+        if: github.ref == 'refs/heads/develop2'
+        uses: actions/upload-artifact@v4
+        with:
+          name: .coverage.${{ runner.os }}-${{ inputs.python-version }}-${{ inputs.test-type }}
+          path: .coverage.${{ runner.os }}-${{ inputs.python-version }}-${{ inputs.test-type }}
+          include-hidden-files: true
diff --git a/.github/workflows/linux-tests.yml b/.github/workflows/linux-tests.yml
index 558039da0..1df2d43d1 100644
--- a/.github/workflows/linux-tests.yml
+++ b/.github/workflows/linux-tests.yml
@@ -1,23 +1,12 @@
 name: Linux tests
 
 on:
-  push:
-    branches:
-      - develop2
-      - release/*
-  pull_request:
-    branches:
-      - '*'
-      - 'release/*'
-  workflow_dispatch:
-
-concurrency:
-  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
-  cancel-in-progress: true
+  workflow_call:
 
 jobs:
   build_container:
     runs-on: ubuntu-latest
+    name: Build Linux container
     outputs:
       image_tag: ${{ steps.dockerfile_hash.outputs.tag }}
     steps:
@@ -52,7 +41,7 @@ jobs:
           docker build -t ghcr.io/${{ github.repository_owner }}/conan-tests:${{ steps.dockerfile_hash.outputs.tag }} -f ./.ci/docker/conan-tests .
           docker push ghcr.io/${{ github.repository_owner }}/conan-tests:${{ steps.dockerfile_hash.outputs.tag }}
 
-  linux_test_suite:
+  linux_tests:
     needs: build_container
     runs-on: ubuntu-latest
     container:
@@ -60,9 +49,16 @@ jobs:
       options: --user conan
     strategy:
       matrix:
-        python-version: ['3.12.3', '3.9.2', '3.8.6', '3.6.15']
-        test-type: ['unittests', 'integration', 'functional']
-    name: Conan ${{ matrix.test-type }} (${{ matrix.python-version }})
+        python-version: [3.12.3, 3.9.2, 3.8.6, 3.6.15]
+        test-type: [unittests, integration, functional]
+        include:
+        - test-type: unittests
+          test-name: Unit
+        - test-type: integration
+          test-name: Integration
+        - test-type: functional
+          test-name: Functional
+    name: ${{ matrix.test-name }} Tests (${{ matrix.python-version }})
     steps:
       - name: Checkout code
         uses: actions/checkout@v4
@@ -87,23 +83,20 @@ jobs:
           pip install meson
 
       - name: Run tests
-        shell: bash
-        run: |
-          if [ "${{ matrix.test-type }}" == "unittests" ]; then
-            pytest test/unittests --durations=20 -n 4
-          elif [ "${{ matrix.test-type }}" == "integration" ]; then
-            pytest test/integration --durations=20 -n 4
-          elif [ "${{ matrix.test-type }}" == "functional" ]; then
-            pytest test/functional --durations=20 -n 4
-          fi
+        uses: ./.github/actions/test-coverage
+        with:
+          python-version: ${{ matrix.python-version }}
+          test-type: ${{ matrix.test-type }}
+          tests: test/${{ matrix.test-type }}
+          duration: 20
 
   linux_docker_tests:
     needs: build_container
     runs-on: ubuntu-latest
-    name: Docker Runner Tests
     strategy:
       matrix:
         python-version: [3.12, 3.9]
+    name: Docker Runner Tests (${{ matrix.python-version }})
     steps:
       - name: Set up Python ${{ matrix.python-version }}
         uses: actions/setup-python@v5
@@ -122,50 +115,10 @@ jobs:
           pip install -r conans/requirements_runner.txt
 
       - name: Run tests
-        shell: bash
-        run: |
-          pytest -m docker_runner --durations=20 -rs
-
-  deploy_to_pypi_test:
-    needs: [build_container, linux_test_suite, linux_docker_tests]
-    runs-on: ubuntu-latest
-    if: github.ref == 'refs/heads/develop2'
-    name: Deploy to TestPyPI
-    steps:
-      - name: Checkout code
-        uses: actions/checkout@v4
-
-      - name: Set up Python
-        uses: actions/setup-python@v5
+        uses: ./.github/actions/test-coverage
         with:
-          python-version: '3.9'
-
-      - name: Install dependencies
-        run: |
-          pip install --upgrade pip
-          pip install twine
-
-      - name: Bump Dev Version
-        run: |
-          python .ci/bump_dev_version.py
-
-      - name: Build Package
-        run: |
-          python setup.py sdist
-
-      - name: Upload to TestPyPI
-        env:
-          TWINE_USERNAME: ${{ secrets.TEST_PYPI_USERNAME }}
-          TWINE_PASSWORD: ${{ secrets.TEST_PYPI_PASSWORD }}
-        run: |
-          python -m twine upload --verbose --repository-url https://test.pypi.org/legacy/ dist/*
-
-      - name: Deploy conan-server to TestPyPI
-        env:
-          TWINE_USERNAME: ${{ secrets.TEST_PYPI_SERVER_USERNAME }}
-          TWINE_PASSWORD: ${{ secrets.TEST_PYPI_SERVER_PASSWORD }}
-        run: |
-          rm -rf dist/
-          mv setup_server.py setup.py
-          python setup.py sdist
-          python -m twine upload --verbose --repository-url https://test.pypi.org/legacy/ dist/*
+          python-version: ${{ matrix.python-version }}
+          test-type: docker
+          tests: '-m docker_runner -rs'
+          duration: 20
+          workers: 1
diff --git a/.github/workflows/osx-tests.yml b/.github/workflows/osx-tests.yml
index 671f8d6bf..065afbc33 100644
--- a/.github/workflows/osx-tests.yml
+++ b/.github/workflows/osx-tests.yml
@@ -1,21 +1,10 @@
 name: OSX Tests
 
 on:
-  push:
-    branches:
-      - develop2
-      - release/*
-  pull_request:
-    branches:
-      - '*'
-      - 'release/*'
-
-concurrency:
-  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
-  cancel-in-progress: true
+  workflow_call:
 
 jobs:
-  setup-caches:
+  osx_setup:
     runs-on: macos-14
     name: Setup and Cache Dependencies
     steps:
@@ -118,18 +107,22 @@ jobs:
             chmod +x ${HOME}/Applications/bazel/${version}/bazel
           done
 
-  testing:
-    needs: setup-caches
+  osx_tests:
+    needs: osx_setup
+    runs-on: macos-14
     strategy:
       fail-fast: true
       matrix:
-        python-version: ['3.8', '3.12', '3.13']
+        python-version: [3.8, 3.12, 3.13]
         test-type: [unittests, integration, functional]
-
-    runs-on: macos-14
-
-    name: Conan (${{ matrix.test-type }}) (${{ matrix.python-version }})
-
+        include:
+        - test-type: unittests
+          test-name: Unit
+        - test-type: integration
+          test-name: Integration
+        - test-type: functional
+          test-name: Functional
+    name: ${{ matrix.test-name }} Tests (${{ matrix.python-version }})
     steps:
       - name: Checkout code
         uses: actions/checkout@v4
@@ -168,11 +161,14 @@ jobs:
       - name: Install homebrew dependencies
         run: |
           brew install xcodegen make libtool zlib autoconf automake ninja
-
-      - name: Run Tests
-        run: |
           export PATH=${HOME}/Applications/CMake/3.15.7/bin:$PATH:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin
           cmake --version
           bazel --version
 
-          python -m pytest test/${{ matrix.test-type }} --durations=20 -n auto
+      - name: Run tests
+        uses: ./.github/actions/test-coverage
+        with:
+          python-version: ${{ matrix.python-version }}
+          test-type: ${{ matrix.test-type }}
+          tests: test/${{ matrix.test-type }}
+          duration: 20
diff --git a/.github/workflows/win-tests.yml b/.github/workflows/win-tests.yml
index 2b0df32c8..35214637a 100644
--- a/.github/workflows/win-tests.yml
+++ b/.github/workflows/win-tests.yml
@@ -1,27 +1,14 @@
 name: Windows Tests
 
 on:
-  push:
-    branches:
-      - develop2
-      - release/*
-  pull_request:
-    branches:
-      - '*'
-      - 'release/*'
-  workflow_dispatch:
-
-concurrency:
-  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
-  cancel-in-progress: true
+  workflow_call:
 
 jobs:
   unit_integration_tests:
     runs-on: windows-2022
     strategy:
       matrix:
-        python-version: ['3.13', '3.8', '3.6']
-
+        python-version: [3.13, 3.8, 3.6]
     name: Unit & Integration Tests (${{ matrix.python-version }})
     steps:
       - name: Checkout code
@@ -45,7 +32,6 @@ jobs:
         shell: pwsh
         run: echo "PIP_CACHE_DIR=$(pip cache dir)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
 
-
       - name: Cache pip packages
         uses: actions/cache@v4
         with:
@@ -60,18 +46,21 @@ jobs:
           pip install -r conans/requirements.txt
           pip install -r conans/requirements_dev.txt
           pip install -r conans/requirements_server.txt
+          git config --global core.autocrlf false
 
       - name: Run Unit & Integration Tests
-        shell: pwsh
-        run: |
-          git config --global core.autocrlf false
-          pytest test/unittests test/integration --durations=100 -n=auto
+        uses: ./.github/actions/test-coverage
+        with:
+          python-version: ${{ matrix.python-version }}
+          test-type: unit-integration
+          tests: test/unittests test/integration
+          duration: 100
 
   functional_tests:
     runs-on: windows-2022
     strategy:
       matrix:
-        python-version: ['3.13', '3.8', '3.6']
+        python-version: [3.13, 3.8, 3.6]
 
     name: Functional Tests (${{ matrix.python-version }})
     steps:
@@ -227,7 +216,7 @@ jobs:
               Remove-Item $zipFile
           }
 
-      - name: Run Functional Tests
+      - name: Prepare environment for functional tests
         run: |
           git config --global core.autocrlf false
 
@@ -260,4 +249,15 @@ jobs:
           [System.Environment]::SetEnvironmentVariable('MSYS2_PATH', $msys2Path, [System.EnvironmentVariableTarget]::Process)
           Write-Host "Added MSYS2_PATH environment variable: $msys2Path"
 
-          pytest test/functional -n=auto --durations=100
+          # Export variables so they persist in the next steps
+          echo "PATH=$env:Path" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
+          echo "CONAN_TEST_FOLDER=$env:CONAN_TEST_FOLDER" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
+          echo "MSYS2_PATH=$msys2Path" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
+
+      - name: Run Functional Tests
+        uses: ./.github/actions/test-coverage
+        with:
+          python-version: ${{ matrix.python-version }}
+          test-type: functional
+          tests: test/functional
+          duration: 100
diff --git a/test/unittests/model/version/test_version_comparison.py b/test/unittests/model/version/test_version_comparison.py
index f35cd9796..a0d6341b9 100644
--- a/test/unittests/model/version/test_version_comparison.py
+++ b/test/unittests/model/version/test_version_comparison.py
@@ -1,6 +1,7 @@
 import pytest
 
 from conan.internal.model.version import Version
+from conan.internal.model.version_range import VersionRange
 
 v = [("1", "2"),
      ("1.0", "1.1"),
@@ -155,3 +156,11 @@ def test_elem_comparison():
     assert micro > 3
     assert micro < 5
     assert micro == 4
+
+def test_version_in_range():
+    v = Version("1.0")
+    assert not v.in_range(">1.0 <2")
+    assert v.in_range(">=1.0 <2")
+
+    assert not Version("1.0-rc").in_range(">=1.0 <2.0")
+    assert Version("1.0-rc").in_range(">=1.0 <2.0", resolve_prerelease=True)