├── .flake8 ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── README.md ├── pyproject.toml ├── semver ├── __init__.py ├── empty_constraint.py ├── exceptions.py ├── patterns.py ├── version.py ├── version_constraint.py ├── version_range.py └── version_union.py ├── tests ├── __init__.py ├── test_main.py ├── test_semver.py ├── test_version.py └── test_version_range.py └── tox.ini /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | ignore = E501, E203, W503 4 | per-file-ignores = __init__.py:F401 5 | exclude = 6 | .git 7 | __pycache__ 8 | setup.py 9 | build 10 | dist 11 | .venv 12 | .tox 13 | .mypy_cache 14 | .pytest_cache 15 | .vscode 16 | .github 17 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | Linting: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v1 11 | - name: Set up Python 3.8 12 | uses: actions/setup-python@v1 13 | with: 14 | python-version: 3.8 15 | - name: Linting 16 | run: | 17 | pip install pre-commit 18 | pre-commit run --all-files 19 | Linux: 20 | needs: Linting 21 | runs-on: ubuntu-latest 22 | strategy: 23 | matrix: 24 | python-version: [2.7, 3.5, 3.6, 3.7, 3.8] 25 | 26 | steps: 27 | - uses: actions/checkout@v1 28 | - name: Set up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v1 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | - name: Install Poetry 33 | run: | 34 | curl -fsS -o get-poetry.py https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py 35 | python get-poetry.py --preview -y 36 | source $HOME/.poetry/env 37 | - name: Install dependencies 38 | run: | 39 | source $HOME/.poetry/env 40 | poetry install 41 | - name: Test 42 | run: | 43 | source $HOME/.poetry/env 44 | poetry run pytest -q tests 45 | MacOS: 46 | needs: Linting 47 | runs-on: macos-latest 48 | strategy: 49 | matrix: 50 | python-version: [2.7, 3.5, 3.6, 3.7, 3.8] 51 | 52 | steps: 53 | - uses: actions/checkout@v1 54 | - name: Set up Python ${{ matrix.python-version }} 55 | uses: actions/setup-python@v1 56 | with: 57 | python-version: ${{ matrix.python-version }} 58 | - name: Install Poetry 59 | run: | 60 | curl -fsS -o get-poetry.py https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py 61 | python get-poetry.py --preview -y 62 | source $HOME/.poetry/env 63 | - name: Install dependencies 64 | run: | 65 | source $HOME/.poetry/env 66 | poetry install 67 | - name: Test 68 | run: | 69 | source $HOME/.poetry/env 70 | poetry run pytest -q tests 71 | Windows: 72 | needs: Linting 73 | runs-on: windows-latest 74 | strategy: 75 | matrix: 76 | python-version: [2.7, 3.5, 3.6, 3.7, 3.8] 77 | 78 | steps: 79 | - uses: actions/checkout@v1 80 | - name: Set up Python ${{ matrix.python-version }} 81 | uses: actions/setup-python@v1 82 | with: 83 | python-version: ${{ matrix.python-version }} 84 | - name: Install Poetry 85 | run: | 86 | Invoke-WebRequest https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py -O get-poetry.py 87 | python get-poetry.py --preview -y 88 | $env:Path += ";$env:Userprofile\.poetry\bin" 89 | - name: Install dependencies 90 | run: | 91 | $env:Path += ";$env:Userprofile\.poetry\bin" 92 | poetry install 93 | - name: Test 94 | run: | 95 | $env:Path += ";$env:Userprofile\.poetry\bin" 96 | poetry run pytest -q tests 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | _build 9 | .cache 10 | *.so 11 | 12 | # Installer logs 13 | pip-log.txt 14 | 15 | # Unit test / coverage reports 16 | .coverage 17 | .tox 18 | .pytest_cache 19 | 20 | .DS_Store 21 | .idea/* 22 | .python-version 23 | 24 | /test.py 25 | /test_*.* 26 | 27 | pip-wheel-metadata 28 | setup.cfg 29 | MANIFEST.in 30 | setup.py 31 | /docs/site/* 32 | poetry.lock 33 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: stable 4 | hooks: 5 | - id: black 6 | 7 | - repo: https://gitlab.com/pycqa/flake8 8 | rev: 3.7.8 9 | hooks: 10 | - id: flake8 11 | 12 | - repo: https://github.com/pre-commit/mirrors-isort 13 | rev: v4.3.21 14 | hooks: 15 | - id: isort 16 | additional_dependencies: [toml] 17 | exclude: ^.*/?setup\.py$ 18 | 19 | - repo: https://github.com/pre-commit/pre-commit-hooks 20 | rev: v2.3.0 21 | hooks: 22 | - id: trailing-whitespace 23 | - id: end-of-file-fixer 24 | - id: debug-statements 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Poetry SemVer 2 | 3 | A semantic versioning library for Python. Initially part of the [Poetry](https://github.com/python-poetry/poetry) codebase. 4 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "poetry-semver" 3 | version = "0.1.0" 4 | description = "A semantic versioning library for Python" 5 | authors = ["Sébastien Eustace "] 6 | license = "MIT" 7 | readme = "README.md" 8 | homepage = "https://github.com/python-poetry/semver" 9 | repository = "https://github.com/python-poetry/semver" 10 | 11 | packages = [ 12 | {include = "semver"}, 13 | {include = "tests", format = "sdist"}, 14 | ] 15 | 16 | [tool.poetry.dependencies] 17 | python = "~2.7 || ^3.4" 18 | 19 | # The typing module is not in the stdlib in Python 2.7 and 3.4 20 | typing = { version = "^3.6", python = "~2.7 || ~3.4" } 21 | 22 | [tool.poetry.dev-dependencies] 23 | pytest = "^4.1" 24 | pytest-cov = "^2.8.1" 25 | tox = "^3.12" 26 | pre-commit = "^1.10" 27 | 28 | [tool.isort] 29 | line_length = 88 30 | force_single_line = true 31 | atomic = true 32 | include_trailing_comma = true 33 | lines_after_imports = 2 34 | lines_between_types = 1 35 | multi_line_output = 3 36 | use_parentheses = true 37 | not_skip = "__init__.py" 38 | skip_glob = ["*/setup.py"] 39 | filter_files = true 40 | 41 | known_first_party = "semver" 42 | known_third_party = [ 43 | "pytest", 44 | ] 45 | 46 | 47 | [build-system] 48 | requires = ["poetry>=1.0.0b8"] 49 | build-backend = "poetry.masonry.api" 50 | -------------------------------------------------------------------------------- /semver/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from .empty_constraint import EmptyConstraint 4 | from .patterns import BASIC_CONSTRAINT 5 | from .patterns import CARET_CONSTRAINT 6 | from .patterns import TILDE_CONSTRAINT 7 | from .patterns import TILDE_PEP440_CONSTRAINT 8 | from .patterns import X_CONSTRAINT 9 | from .version import Version 10 | from .version_constraint import VersionConstraint 11 | from .version_range import VersionRange 12 | from .version_union import VersionUnion 13 | 14 | 15 | __version__ = "0.1.0" 16 | 17 | 18 | def parse_constraint(constraints): # type: (str) -> VersionConstraint 19 | if constraints == "*": 20 | return VersionRange() 21 | 22 | or_constraints = re.split(r"\s*\|\|?\s*", constraints.strip()) 23 | or_groups = [] 24 | for constraints in or_constraints: 25 | and_constraints = re.split( 26 | "(?< ,]) *(? 1: 31 | for constraint in and_constraints: 32 | constraint_objects.append(parse_single_constraint(constraint)) 33 | else: 34 | constraint_objects.append(parse_single_constraint(and_constraints[0])) 35 | 36 | if len(constraint_objects) == 1: 37 | constraint = constraint_objects[0] 38 | else: 39 | constraint = constraint_objects[0] 40 | for next_constraint in constraint_objects[1:]: 41 | constraint = constraint.intersect(next_constraint) 42 | 43 | or_groups.append(constraint) 44 | 45 | if len(or_groups) == 1: 46 | return or_groups[0] 47 | else: 48 | return VersionUnion.of(*or_groups) 49 | 50 | 51 | def parse_single_constraint(constraint): # type: (str) -> VersionConstraint 52 | m = re.match(r"(?i)^v?[xX*](\.[xX*])*$", constraint) 53 | if m: 54 | return VersionRange() 55 | 56 | # Tilde range 57 | m = TILDE_CONSTRAINT.match(constraint) 58 | if m: 59 | version = Version.parse(m.group(1)) 60 | 61 | high = version.stable.next_minor 62 | if len(m.group(1).split(".")) == 1: 63 | high = version.stable.next_major 64 | 65 | return VersionRange( 66 | version, high, include_min=True, always_include_max_prerelease=True 67 | ) 68 | 69 | # PEP 440 Tilde range (~=) 70 | m = TILDE_PEP440_CONSTRAINT.match(constraint) 71 | if m: 72 | precision = 1 73 | if m.group(3): 74 | precision += 1 75 | 76 | if m.group(4): 77 | precision += 1 78 | 79 | version = Version.parse(m.group(1)) 80 | 81 | if precision == 2: 82 | low = version 83 | high = version.stable.next_major 84 | else: 85 | low = Version(version.major, version.minor, version.patch) 86 | high = version.stable.next_minor 87 | 88 | return VersionRange( 89 | low, high, include_min=True, always_include_max_prerelease=True 90 | ) 91 | 92 | # Caret range 93 | m = CARET_CONSTRAINT.match(constraint) 94 | if m: 95 | version = Version.parse(m.group(1)) 96 | 97 | return VersionRange( 98 | version, 99 | version.next_breaking, 100 | include_min=True, 101 | always_include_max_prerelease=True, 102 | ) 103 | 104 | # X Range 105 | m = X_CONSTRAINT.match(constraint) 106 | if m: 107 | op = m.group(1) 108 | major = int(m.group(2)) 109 | minor = m.group(3) 110 | 111 | if minor is not None: 112 | version = Version(major, int(minor), 0) 113 | 114 | result = VersionRange( 115 | version, 116 | version.next_minor, 117 | include_min=True, 118 | always_include_max_prerelease=True, 119 | ) 120 | else: 121 | if major == 0: 122 | result = VersionRange(max=Version(1, 0, 0)) 123 | else: 124 | version = Version(major, 0, 0) 125 | 126 | result = VersionRange( 127 | version, 128 | version.next_major, 129 | include_min=True, 130 | always_include_max_prerelease=True, 131 | ) 132 | 133 | if op == "!=": 134 | result = VersionRange().difference(result) 135 | 136 | return result 137 | 138 | # Basic comparator 139 | m = BASIC_CONSTRAINT.match(constraint) 140 | if m: 141 | op = m.group(1) 142 | version = m.group(2) 143 | 144 | if version == "dev": 145 | version = "0.0-dev" 146 | 147 | try: 148 | version = Version.parse(version) 149 | except ValueError: 150 | raise ValueError( 151 | "Could not parse version constraint: {}".format(constraint) 152 | ) 153 | 154 | if op == "<": 155 | return VersionRange(max=version) 156 | elif op == "<=": 157 | return VersionRange(max=version, include_max=True) 158 | elif op == ">": 159 | return VersionRange(min=version) 160 | elif op == ">=": 161 | return VersionRange(min=version, include_min=True) 162 | elif op == "!=": 163 | return VersionUnion(VersionRange(max=version), VersionRange(min=version)) 164 | else: 165 | return version 166 | 167 | raise ValueError("Could not parse version constraint: {}".format(constraint)) 168 | -------------------------------------------------------------------------------- /semver/empty_constraint.py: -------------------------------------------------------------------------------- 1 | from .version_constraint import VersionConstraint 2 | 3 | 4 | class EmptyConstraint(VersionConstraint): 5 | def is_empty(self): 6 | return True 7 | 8 | def is_any(self): 9 | return False 10 | 11 | def allows(self, version): 12 | return False 13 | 14 | def allows_all(self, other): 15 | return other.is_empty() 16 | 17 | def allows_any(self, other): 18 | return False 19 | 20 | def intersect(self, other): 21 | return self 22 | 23 | def union(self, other): 24 | return other 25 | 26 | def difference(self, other): 27 | return self 28 | 29 | def __str__(self): 30 | return "" 31 | -------------------------------------------------------------------------------- /semver/exceptions.py: -------------------------------------------------------------------------------- 1 | class ParseVersionError(ValueError): 2 | pass 3 | -------------------------------------------------------------------------------- /semver/patterns.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | MODIFIERS = ( 5 | "[._-]?" 6 | r"((?!post)(?:beta|b|c|pre|RC|alpha|a|patch|pl|p|dev)(?:(?:[.-]?\d+)*)?)?" 7 | r"([+-]?([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?" 8 | ) 9 | 10 | _COMPLETE_VERSION = r"v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?{}(?:\+[^\s]+)?".format( 11 | MODIFIERS 12 | ) 13 | 14 | COMPLETE_VERSION = re.compile("(?i)" + _COMPLETE_VERSION) 15 | 16 | CARET_CONSTRAINT = re.compile(r"(?i)^\^({})$".format(_COMPLETE_VERSION)) 17 | TILDE_CONSTRAINT = re.compile("(?i)^~(?!=)({})$".format(_COMPLETE_VERSION)) 18 | TILDE_PEP440_CONSTRAINT = re.compile("(?i)^~=({})$".format(_COMPLETE_VERSION)) 19 | X_CONSTRAINT = re.compile(r"^(!=|==)?\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$") 20 | BASIC_CONSTRAINT = re.compile( 21 | r"(?i)^(<>|!=|>=?|<=?|==?)?\s*({}|dev)".format(_COMPLETE_VERSION) 22 | ) 23 | -------------------------------------------------------------------------------- /semver/version.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from typing import List 4 | from typing import Optional 5 | from typing import Union 6 | 7 | from .empty_constraint import EmptyConstraint 8 | from .exceptions import ParseVersionError 9 | from .patterns import COMPLETE_VERSION 10 | from .version_constraint import VersionConstraint 11 | from .version_range import VersionRange 12 | from .version_union import VersionUnion 13 | 14 | 15 | class Version(VersionRange): 16 | """ 17 | A parsed semantic version number. 18 | """ 19 | 20 | def __init__( 21 | self, 22 | major, # type: int 23 | minor=None, # type: Optional[int] 24 | patch=None, # type: Optional[int] 25 | rest=None, # type: Optional[int] 26 | pre=None, # type: Optional[str] 27 | build=None, # type: Optional[str] 28 | text=None, # type: Optional[str] 29 | precision=None, # type: Optional[int] 30 | ): # type: (...) -> None 31 | self._major = int(major) 32 | self._precision = None 33 | if precision is None: 34 | self._precision = 1 35 | 36 | if minor is None: 37 | minor = 0 38 | else: 39 | if self._precision is not None: 40 | self._precision += 1 41 | 42 | self._minor = int(minor) 43 | 44 | if patch is None: 45 | patch = 0 46 | else: 47 | if self._precision is not None: 48 | self._precision += 1 49 | 50 | if rest is None: 51 | rest = 0 52 | else: 53 | if self._precision is not None: 54 | self._precision += 1 55 | 56 | if precision is not None: 57 | self._precision = precision 58 | 59 | self._patch = int(patch) 60 | self._rest = int(rest) 61 | 62 | if text is None: 63 | parts = [str(major)] 64 | if self._precision >= 2 or minor != 0: 65 | parts.append(str(minor)) 66 | 67 | if self._precision >= 3 or patch != 0: 68 | parts.append(str(patch)) 69 | 70 | if self._precision >= 4 or rest != 0: 71 | parts.append(str(rest)) 72 | 73 | text = ".".join(parts) 74 | if pre: 75 | text += "-{}".format(pre) 76 | 77 | if build: 78 | text += "+{}".format(build) 79 | 80 | self._text = text 81 | 82 | pre = self._normalize_prerelease(pre) 83 | 84 | self._prerelease = [] 85 | if pre is not None: 86 | self._prerelease = self._split_parts(pre) 87 | 88 | build = self._normalize_build(build) 89 | 90 | self._build = [] 91 | if build is not None: 92 | if build.startswith(("-", "+")): 93 | build = build[1:] 94 | 95 | self._build = self._split_parts(build) 96 | 97 | @property 98 | def major(self): # type: () -> int 99 | return self._major 100 | 101 | @property 102 | def minor(self): # type: () -> int 103 | return self._minor 104 | 105 | @property 106 | def patch(self): # type: () -> int 107 | return self._patch 108 | 109 | @property 110 | def rest(self): # type: () -> int 111 | return self._rest 112 | 113 | @property 114 | def prerelease(self): # type: () -> List[str] 115 | return self._prerelease 116 | 117 | @property 118 | def build(self): # type: () -> List[str] 119 | return self._build 120 | 121 | @property 122 | def text(self): 123 | return self._text 124 | 125 | @property 126 | def precision(self): # type: () -> int 127 | return self._precision 128 | 129 | @property 130 | def stable(self): 131 | if not self.is_prerelease(): 132 | return self 133 | 134 | return self.next_patch 135 | 136 | @property 137 | def next_major(self): # type: () -> Version 138 | if self.is_prerelease() and self.minor == 0 and self.patch == 0: 139 | return Version(self.major, self.minor, self.patch) 140 | 141 | return self._increment_major() 142 | 143 | @property 144 | def next_minor(self): # type: () -> Version 145 | if self.is_prerelease() and self.patch == 0: 146 | return Version(self.major, self.minor, self.patch) 147 | 148 | return self._increment_minor() 149 | 150 | @property 151 | def next_patch(self): # type: () -> Version 152 | if self.is_prerelease(): 153 | return Version(self.major, self.minor, self.patch) 154 | 155 | return self._increment_patch() 156 | 157 | @property 158 | def next_breaking(self): # type: () -> Version 159 | if self.major == 0: 160 | if self.minor != 0: 161 | return self._increment_minor() 162 | 163 | if self._precision == 1: 164 | return self._increment_major() 165 | elif self._precision == 2: 166 | return self._increment_minor() 167 | 168 | return self._increment_patch() 169 | 170 | return self._increment_major() 171 | 172 | @property 173 | def first_prerelease(self): # type: () -> Version 174 | return Version.parse( 175 | "{}.{}.{}-alpha.0".format(self.major, self.minor, self.patch) 176 | ) 177 | 178 | @property 179 | def min(self): 180 | return self 181 | 182 | @property 183 | def max(self): 184 | return self 185 | 186 | @property 187 | def full_max(self): 188 | return self 189 | 190 | @property 191 | def include_min(self): 192 | return True 193 | 194 | @property 195 | def include_max(self): 196 | return True 197 | 198 | @classmethod 199 | def parse(cls, text): # type: (str) -> Version 200 | try: 201 | match = COMPLETE_VERSION.match(text) 202 | except TypeError: 203 | match = None 204 | 205 | if match is None: 206 | raise ParseVersionError('Unable to parse "{}".'.format(text)) 207 | 208 | text = text.rstrip(".") 209 | 210 | major = int(match.group(1)) 211 | minor = int(match.group(2)) if match.group(2) else None 212 | patch = int(match.group(3)) if match.group(3) else None 213 | rest = int(match.group(4)) if match.group(4) else None 214 | 215 | pre = match.group(5) 216 | build = match.group(6) 217 | 218 | if build: 219 | build = build.lstrip("+") 220 | 221 | return Version(major, minor, patch, rest, pre, build, text) 222 | 223 | def is_any(self): 224 | return False 225 | 226 | def is_empty(self): 227 | return False 228 | 229 | def is_prerelease(self): # type: () -> bool 230 | return len(self._prerelease) > 0 231 | 232 | def allows(self, version): # type: (Version) -> bool 233 | return self == version 234 | 235 | def allows_all(self, other): # type: (VersionConstraint) -> bool 236 | return other.is_empty() or other == self 237 | 238 | def allows_any(self, other): # type: (VersionConstraint) -> bool 239 | return other.allows(self) 240 | 241 | def intersect(self, other): # type: (VersionConstraint) -> VersionConstraint 242 | if other.allows(self): 243 | return self 244 | 245 | return EmptyConstraint() 246 | 247 | def union(self, other): # type: (VersionConstraint) -> VersionConstraint 248 | from .version_range import VersionRange 249 | 250 | if other.allows(self): 251 | return other 252 | 253 | if isinstance(other, VersionRange): 254 | if other.min == self: 255 | return VersionRange( 256 | other.min, 257 | other.max, 258 | include_min=True, 259 | include_max=other.include_max, 260 | ) 261 | 262 | if other.max == self: 263 | return VersionRange( 264 | other.min, 265 | other.max, 266 | include_min=other.include_min, 267 | include_max=True, 268 | ) 269 | 270 | return VersionUnion.of(self, other) 271 | 272 | def difference(self, other): # type: (VersionConstraint) -> VersionConstraint 273 | if other.allows(self): 274 | return EmptyConstraint() 275 | 276 | return self 277 | 278 | def equals_without_prerelease(self, other): # type: (Version) -> bool 279 | return ( 280 | self.major == other.major 281 | and self.minor == other.minor 282 | and self.patch == other.patch 283 | ) 284 | 285 | def _increment_major(self): # type: () -> Version 286 | return Version(self.major + 1, 0, 0, precision=self._precision) 287 | 288 | def _increment_minor(self): # type: () -> Version 289 | return Version(self.major, self.minor + 1, 0, precision=self._precision) 290 | 291 | def _increment_patch(self): # type: () -> Version 292 | return Version( 293 | self.major, self.minor, self.patch + 1, precision=self._precision 294 | ) 295 | 296 | def _normalize_prerelease(self, pre): # type: (str) -> str 297 | if not pre: 298 | return 299 | 300 | m = re.match(r"(?i)^(a|alpha|b|beta|c|pre|rc|dev)[-.]?(\d+)?$", pre) 301 | if not m: 302 | return 303 | 304 | modifier = m.group(1) 305 | number = m.group(2) 306 | 307 | if number is None: 308 | number = 0 309 | 310 | if modifier == "a": 311 | modifier = "alpha" 312 | elif modifier == "b": 313 | modifier = "beta" 314 | elif modifier in {"c", "pre"}: 315 | modifier = "rc" 316 | elif modifier == "dev": 317 | modifier = "alpha" 318 | 319 | return "{}.{}".format(modifier, number) 320 | 321 | def _normalize_build(self, build): # type: (str) -> str 322 | if not build: 323 | return 324 | 325 | if build.startswith("post"): 326 | build = build.lstrip("post") 327 | 328 | if not build: 329 | return 330 | 331 | return build 332 | 333 | def _split_parts(self, text): # type: (str) -> List[Union[str, int]] 334 | parts = text.split(".") 335 | 336 | for i, part in enumerate(parts): 337 | try: 338 | parts[i] = int(part) 339 | except (TypeError, ValueError): 340 | continue 341 | 342 | return parts 343 | 344 | def __lt__(self, other): 345 | return self._cmp(other) < 0 346 | 347 | def __le__(self, other): 348 | return self._cmp(other) <= 0 349 | 350 | def __gt__(self, other): 351 | return self._cmp(other) > 0 352 | 353 | def __ge__(self, other): 354 | return self._cmp(other) >= 0 355 | 356 | def _cmp(self, other): 357 | if not isinstance(other, VersionConstraint): 358 | return NotImplemented 359 | 360 | if not isinstance(other, Version): 361 | return -other._cmp(self) 362 | 363 | if self.major != other.major: 364 | return self._cmp_parts(self.major, other.major) 365 | 366 | if self.minor != other.minor: 367 | return self._cmp_parts(self.minor, other.minor) 368 | 369 | if self.patch != other.patch: 370 | return self._cmp_parts(self.patch, other.patch) 371 | 372 | if self.rest != other.rest: 373 | return self._cmp_parts(self.rest, other.rest) 374 | 375 | # Pre-releases always come before no pre-release string. 376 | if not self.is_prerelease() and other.is_prerelease(): 377 | return 1 378 | 379 | if not other.is_prerelease() and self.is_prerelease(): 380 | return -1 381 | 382 | comparison = self._cmp_lists(self.prerelease, other.prerelease) 383 | if comparison != 0: 384 | return comparison 385 | 386 | # Builds always come after no build string. 387 | if not self.build and other.build: 388 | return -1 389 | 390 | if not other.build and self.build: 391 | return 1 392 | 393 | return self._cmp_lists(self.build, other.build) 394 | 395 | def _cmp_parts(self, a, b): 396 | if a < b: 397 | return -1 398 | elif a > b: 399 | return 1 400 | 401 | return 0 402 | 403 | def _cmp_lists(self, a, b): # type: (List, List) -> int 404 | for i in range(max(len(a), len(b))): 405 | a_part = None 406 | if i < len(a): 407 | a_part = a[i] 408 | 409 | b_part = None 410 | if i < len(b): 411 | b_part = b[i] 412 | 413 | if a_part == b_part: 414 | continue 415 | 416 | # Missing parts come after present ones. 417 | if a_part is None: 418 | return -1 419 | 420 | if b_part is None: 421 | return 1 422 | 423 | if isinstance(a_part, int): 424 | if isinstance(b_part, int): 425 | return self._cmp_parts(a_part, b_part) 426 | 427 | return -1 428 | else: 429 | if isinstance(b_part, int): 430 | return 1 431 | 432 | return self._cmp_parts(a_part, b_part) 433 | 434 | return 0 435 | 436 | def __eq__(self, other): # type: (Version) -> bool 437 | if not isinstance(other, Version): 438 | return NotImplemented 439 | 440 | return ( 441 | self._major == other.major 442 | and self._minor == other.minor 443 | and self._patch == other.patch 444 | and self._rest == other.rest 445 | and self._prerelease == other.prerelease 446 | and self._build == other.build 447 | ) 448 | 449 | def __ne__(self, other): 450 | return not self == other 451 | 452 | def __str__(self): 453 | return self._text 454 | 455 | def __repr__(self): 456 | return "".format(str(self)) 457 | 458 | def __hash__(self): 459 | return hash( 460 | ( 461 | self.major, 462 | self.minor, 463 | self.patch, 464 | ".".join(str(p) for p in self.prerelease), 465 | ".".join(str(p) for p in self.build), 466 | ) 467 | ) 468 | -------------------------------------------------------------------------------- /semver/version_constraint.py: -------------------------------------------------------------------------------- 1 | import semver 2 | 3 | 4 | class VersionConstraint: 5 | def is_empty(self): # type: () -> bool 6 | raise NotImplementedError() 7 | 8 | def is_any(self): # type: () -> bool 9 | raise NotImplementedError() 10 | 11 | def allows(self, version): # type: (semver.Version) -> bool 12 | raise NotImplementedError() 13 | 14 | def allows_all(self, other): # type: (VersionConstraint) -> bool 15 | raise NotImplementedError() 16 | 17 | def allows_any(self, other): # type: (VersionConstraint) -> bool 18 | raise NotImplementedError() 19 | 20 | def intersect(self, other): # type: (VersionConstraint) -> VersionConstraint 21 | raise NotImplementedError() 22 | 23 | def union(self, other): # type: (VersionConstraint) -> VersionConstraint 24 | raise NotImplementedError() 25 | 26 | def difference(self, other): # type: (VersionConstraint) -> VersionConstraint 27 | raise NotImplementedError() 28 | -------------------------------------------------------------------------------- /semver/version_range.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import semver 4 | 5 | from .empty_constraint import EmptyConstraint 6 | from .version_constraint import VersionConstraint 7 | from .version_union import VersionUnion 8 | 9 | 10 | class VersionRange(VersionConstraint): 11 | def __init__( 12 | self, 13 | min=None, 14 | max=None, 15 | include_min=False, 16 | include_max=False, 17 | always_include_max_prerelease=False, 18 | ): 19 | full_max = max 20 | if ( 21 | always_include_max_prerelease 22 | and not include_max 23 | and not full_max.is_prerelease() 24 | and not full_max.build 25 | and ( 26 | min is None 27 | or not min.is_prerelease() 28 | or not min.equals_without_prerelease(full_max) 29 | ) 30 | ): 31 | full_max = full_max.first_prerelease 32 | 33 | self._min = min 34 | self._max = max 35 | self._full_max = full_max 36 | self._include_min = include_min 37 | self._include_max = include_max 38 | 39 | @property 40 | def min(self): 41 | return self._min 42 | 43 | @property 44 | def max(self): 45 | return self._max 46 | 47 | @property 48 | def full_max(self): 49 | return self._full_max 50 | 51 | @property 52 | def include_min(self): 53 | return self._include_min 54 | 55 | @property 56 | def include_max(self): 57 | return self._include_max 58 | 59 | def is_empty(self): 60 | return False 61 | 62 | def is_any(self): 63 | return self._min is None and self._max is None 64 | 65 | def allows(self, other): # type: (semver.Version) -> bool 66 | if self._min is not None: 67 | if other < self._min: 68 | return False 69 | 70 | if not self._include_min and other == self._min: 71 | return False 72 | 73 | if self._max is not None: 74 | if other > self._max: 75 | return False 76 | 77 | if not self._include_max and other == self._max: 78 | return False 79 | 80 | return True 81 | 82 | def allows_all(self, other): # type: (VersionConstraint) -> bool 83 | from .version import Version 84 | 85 | if other.is_empty(): 86 | return True 87 | 88 | if isinstance(other, Version): 89 | return self.allows(other) 90 | 91 | if isinstance(other, VersionUnion): 92 | return all([self.allows_all(constraint) for constraint in other.ranges]) 93 | 94 | if isinstance(other, VersionRange): 95 | return not other.allows_lower(self) and not other.allows_higher(self) 96 | 97 | raise ValueError("Unknown VersionConstraint type {}.".format(other)) 98 | 99 | def allows_any(self, other): # type: (VersionConstraint) -> bool 100 | from .version import Version 101 | 102 | if other.is_empty(): 103 | return False 104 | 105 | if isinstance(other, Version): 106 | return self.allows(other) 107 | 108 | if isinstance(other, VersionUnion): 109 | return any([self.allows_any(constraint) for constraint in other.ranges]) 110 | 111 | if isinstance(other, VersionRange): 112 | return not other.is_strictly_lower(self) and not other.is_strictly_higher( 113 | self 114 | ) 115 | 116 | raise ValueError("Unknown VersionConstraint type {}.".format(other)) 117 | 118 | def intersect(self, other): # type: (VersionConstraint) -> VersionConstraint 119 | from .version import Version 120 | 121 | if other.is_empty(): 122 | return other 123 | 124 | if isinstance(other, VersionUnion): 125 | return other.intersect(self) 126 | 127 | # A range and a Version just yields the version if it's in the range. 128 | if isinstance(other, Version): 129 | if self.allows(other): 130 | return other 131 | 132 | return EmptyConstraint() 133 | 134 | if not isinstance(other, VersionRange): 135 | raise ValueError("Unknown VersionConstraint type {}.".format(other)) 136 | 137 | if self.allows_lower(other): 138 | if self.is_strictly_lower(other): 139 | return EmptyConstraint() 140 | 141 | intersect_min = other.min 142 | intersect_include_min = other.include_min 143 | else: 144 | if other.is_strictly_lower(self): 145 | return EmptyConstraint() 146 | 147 | intersect_min = self._min 148 | intersect_include_min = self._include_min 149 | 150 | if self.allows_higher(other): 151 | intersect_max = other.max 152 | intersect_include_max = other.include_max 153 | else: 154 | intersect_max = self._max 155 | intersect_include_max = self._include_max 156 | 157 | if intersect_min is None and intersect_max is None: 158 | return VersionRange() 159 | 160 | # If the range is just a single version. 161 | if intersect_min == intersect_max: 162 | # Because we already verified that the lower range isn't strictly 163 | # lower, there must be some overlap. 164 | assert intersect_include_min and intersect_include_max 165 | 166 | return intersect_min 167 | 168 | # If we got here, there is an actual range. 169 | return VersionRange( 170 | intersect_min, intersect_max, intersect_include_min, intersect_include_max 171 | ) 172 | 173 | def union(self, other): # type: (VersionConstraint) -> VersionConstraint 174 | from .version import Version 175 | 176 | if isinstance(other, Version): 177 | if self.allows(other): 178 | return self 179 | 180 | if other == self.min: 181 | return VersionRange( 182 | self.min, self.max, include_min=True, include_max=self.include_max 183 | ) 184 | 185 | if other == self.max: 186 | return VersionRange( 187 | self.min, self.max, include_min=self.include_min, include_max=True 188 | ) 189 | 190 | return VersionUnion.of(self, other) 191 | 192 | if isinstance(other, VersionRange): 193 | # If the two ranges don't overlap, we won't be able to create a single 194 | # VersionRange for both of them. 195 | edges_touch = ( 196 | self.max == other.min and (self.include_max or other.include_min) 197 | ) or (self.min == other.max and (self.include_min or other.include_max)) 198 | 199 | if not edges_touch and not self.allows_any(other): 200 | return VersionUnion.of(self, other) 201 | 202 | if self.allows_lower(other): 203 | union_min = self.min 204 | union_include_min = self.include_min 205 | else: 206 | union_min = other.min 207 | union_include_min = other.include_min 208 | 209 | if self.allows_higher(other): 210 | union_max = self.max 211 | union_include_max = self.include_max 212 | else: 213 | union_max = other.max 214 | union_include_max = other.include_max 215 | 216 | return VersionRange( 217 | union_min, 218 | union_max, 219 | include_min=union_include_min, 220 | include_max=union_include_max, 221 | ) 222 | 223 | return VersionUnion.of(self, other) 224 | 225 | def difference(self, other): # type: (VersionConstraint) -> VersionConstraint 226 | from .version import Version 227 | 228 | if other.is_empty(): 229 | return self 230 | 231 | if isinstance(other, Version): 232 | if not self.allows(other): 233 | return self 234 | 235 | if other == self.min: 236 | if not self.include_min: 237 | return self 238 | 239 | return VersionRange(self.min, self.max, False, self.include_max) 240 | 241 | if other == self.max: 242 | if not self.include_max: 243 | return self 244 | 245 | return VersionRange(self.min, self.max, self.include_min, False) 246 | 247 | return VersionUnion.of( 248 | VersionRange(self.min, other, self.include_min, False), 249 | VersionRange(other, self.max, False, self.include_max), 250 | ) 251 | elif isinstance(other, VersionRange): 252 | if not self.allows_any(other): 253 | return self 254 | 255 | if not self.allows_lower(other): 256 | before = None 257 | elif self.min == other.min: 258 | before = self.min 259 | else: 260 | before = VersionRange( 261 | self.min, other.min, self.include_min, not other.include_min 262 | ) 263 | 264 | if not self.allows_higher(other): 265 | after = None 266 | elif self.max == other.max: 267 | after = self.max 268 | else: 269 | after = VersionRange( 270 | other.max, self.max, not other.include_max, self.include_max 271 | ) 272 | 273 | if before is None and after is None: 274 | return EmptyConstraint() 275 | 276 | if before is None: 277 | return after 278 | 279 | if after is None: 280 | return before 281 | 282 | return VersionUnion.of(before, after) 283 | elif isinstance(other, VersionUnion): 284 | ranges = [] # type: List[VersionRange] 285 | current = self 286 | 287 | for range in other.ranges: 288 | # Skip any ranges that are strictly lower than [current]. 289 | if range.is_strictly_lower(current): 290 | continue 291 | 292 | # If we reach a range strictly higher than [current], no more ranges 293 | # will be relevant so we can bail early. 294 | if range.is_strictly_higher(current): 295 | break 296 | 297 | difference = current.difference(range) 298 | if difference.is_empty(): 299 | return EmptyConstraint() 300 | elif isinstance(difference, VersionUnion): 301 | # If [range] split [current] in half, we only need to continue 302 | # checking future ranges against the latter half. 303 | ranges.append(difference.ranges[0]) 304 | current = difference.ranges[-1] 305 | else: 306 | current = difference 307 | 308 | if not ranges: 309 | return current 310 | 311 | return VersionUnion.of(*(ranges + [current])) 312 | 313 | raise ValueError("Unknown VersionConstraint type {}.".format(other)) 314 | 315 | def allows_lower(self, other): # type: (VersionRange) -> bool 316 | if self.min is None: 317 | return other.min is not None 318 | 319 | if other.min is None: 320 | return False 321 | 322 | if self.min < other.min: 323 | return True 324 | 325 | if self.min > other.min: 326 | return False 327 | 328 | return self.include_min and not other.include_min 329 | 330 | def allows_higher(self, other): # type: (VersionRange) -> bool 331 | if self.max is None: 332 | return other.max is not None 333 | 334 | if other.max is None: 335 | return False 336 | 337 | if self.max < other.max: 338 | return False 339 | 340 | if self.max > other.max: 341 | return True 342 | 343 | return self.include_max and not other.include_max 344 | 345 | def is_strictly_lower(self, other): # type: (VersionRange) -> bool 346 | if self.max is None or other.min is None: 347 | return False 348 | 349 | if self.full_max < other.min: 350 | return True 351 | 352 | if self.full_max > other.min: 353 | return False 354 | 355 | return not self.include_max or not other.include_min 356 | 357 | def is_strictly_higher(self, other): # type: (VersionRange) -> bool 358 | return other.is_strictly_lower(self) 359 | 360 | def is_adjacent_to(self, other): # type: (VersionRange) -> bool 361 | if self.max != other.min: 362 | return False 363 | 364 | return ( 365 | self.include_max 366 | and not other.include_min 367 | or not self.include_max 368 | and other.include_min 369 | ) 370 | 371 | def __eq__(self, other): 372 | if not isinstance(other, VersionRange): 373 | return False 374 | 375 | return ( 376 | self._min == other.min 377 | and self._max == other.max 378 | and self._include_min == other.include_min 379 | and self._include_max == other.include_max 380 | ) 381 | 382 | def __lt__(self, other): 383 | return self._cmp(other) < 0 384 | 385 | def __le__(self, other): 386 | return self._cmp(other) <= 0 387 | 388 | def __gt__(self, other): 389 | return self._cmp(other) > 0 390 | 391 | def __ge__(self, other): 392 | return self._cmp(other) >= 0 393 | 394 | def _cmp(self, other): # type: (VersionRange) -> int 395 | if self.min is None: 396 | if other.min is None: 397 | return self._compare_max(other) 398 | 399 | return -1 400 | elif other.min is None: 401 | return 1 402 | 403 | result = self.min._cmp(other.min) 404 | if result != 0: 405 | return result 406 | 407 | if self.include_min != other.include_min: 408 | return -1 if self.include_min else 1 409 | 410 | return self._compare_max(other) 411 | 412 | def _compare_max(self, other): # type: (VersionRange) -> int 413 | if self.max is None: 414 | if other.max is None: 415 | return 0 416 | 417 | return 1 418 | elif other.max is None: 419 | return -1 420 | 421 | result = self.max._cmp(other.max) 422 | if result != 0: 423 | return result 424 | 425 | if self.include_max != other.include_max: 426 | return 1 if self.include_max else -1 427 | 428 | return 0 429 | 430 | def __str__(self): 431 | text = "" 432 | 433 | if self.min is not None: 434 | text += ">=" if self.include_min else ">" 435 | text += self.min.text 436 | 437 | if self.max is not None: 438 | if self.min is not None: 439 | text += "," 440 | 441 | text += "{}{}".format("<=" if self.include_max else "<", self.max.text) 442 | 443 | if self.min is None and self.max is None: 444 | return "*" 445 | 446 | return text 447 | 448 | def __repr__(self): 449 | return "".format(str(self)) 450 | 451 | def __hash__(self): 452 | return hash((self.min, self.max, self.include_min, self.include_max)) 453 | -------------------------------------------------------------------------------- /semver/version_union.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import semver 4 | 5 | from .empty_constraint import EmptyConstraint 6 | from .version_constraint import VersionConstraint 7 | 8 | 9 | class VersionUnion(VersionConstraint): 10 | """ 11 | A version constraint representing a union of multiple disjoint version 12 | ranges. 13 | 14 | An instance of this will only be created if the version can't be represented 15 | as a non-compound value. 16 | """ 17 | 18 | def __init__(self, *ranges): 19 | self._ranges = list(ranges) 20 | 21 | @property 22 | def ranges(self): 23 | return self._ranges 24 | 25 | @classmethod 26 | def of(cls, *ranges): 27 | from .version_range import VersionRange 28 | 29 | flattened = [] 30 | for constraint in ranges: 31 | if constraint.is_empty(): 32 | continue 33 | 34 | if isinstance(constraint, VersionUnion): 35 | flattened += constraint.ranges 36 | continue 37 | 38 | flattened.append(constraint) 39 | 40 | if not flattened: 41 | return EmptyConstraint() 42 | 43 | if any([constraint.is_any() for constraint in flattened]): 44 | return VersionRange() 45 | 46 | # Only allow Versions and VersionRanges here so we can more easily reason 47 | # about everything in flattened. _EmptyVersions and VersionUnions are 48 | # filtered out above. 49 | for constraint in flattened: 50 | if isinstance(constraint, VersionRange): 51 | continue 52 | 53 | raise ValueError("Unknown VersionConstraint type {}.".format(constraint)) 54 | 55 | flattened.sort() 56 | 57 | merged = [] 58 | for constraint in flattened: 59 | # Merge this constraint with the previous one, but only if they touch. 60 | if not merged or ( 61 | not merged[-1].allows_any(constraint) 62 | and not merged[-1].is_adjacent_to(constraint) 63 | ): 64 | merged.append(constraint) 65 | else: 66 | merged[-1] = merged[-1].union(constraint) 67 | 68 | if len(merged) == 1: 69 | return merged[0] 70 | 71 | return VersionUnion(*merged) 72 | 73 | def is_empty(self): 74 | return False 75 | 76 | def is_any(self): 77 | return False 78 | 79 | def allows(self, version): # type: (semver.Version) -> bool 80 | return any([constraint.allows(version) for constraint in self._ranges]) 81 | 82 | def allows_all(self, other): # type: (VersionConstraint) -> bool 83 | our_ranges = iter(self._ranges) 84 | their_ranges = iter(self._ranges_for(other)) 85 | 86 | our_current_range = next(our_ranges, None) 87 | their_current_range = next(their_ranges, None) 88 | 89 | while our_current_range and their_current_range: 90 | if our_current_range.allows_all(their_current_range): 91 | their_current_range = next(their_ranges, None) 92 | else: 93 | our_current_range = next(our_ranges, None) 94 | 95 | return their_current_range is None 96 | 97 | def allows_any(self, other): # type: (VersionConstraint) -> bool 98 | our_ranges = iter(self._ranges) 99 | their_ranges = iter(self._ranges_for(other)) 100 | 101 | our_current_range = next(our_ranges, None) 102 | their_current_range = next(their_ranges, None) 103 | 104 | while our_current_range and their_current_range: 105 | if our_current_range.allows_any(their_current_range): 106 | return True 107 | 108 | if their_current_range.allows_higher(our_current_range): 109 | our_current_range = next(our_ranges, None) 110 | else: 111 | their_current_range = next(their_ranges, None) 112 | 113 | return False 114 | 115 | def intersect(self, other): # type: (VersionConstraint) -> VersionConstraint 116 | our_ranges = iter(self._ranges) 117 | their_ranges = iter(self._ranges_for(other)) 118 | new_ranges = [] 119 | 120 | our_current_range = next(our_ranges, None) 121 | their_current_range = next(their_ranges, None) 122 | 123 | while our_current_range and their_current_range: 124 | intersection = our_current_range.intersect(their_current_range) 125 | 126 | if not intersection.is_empty(): 127 | new_ranges.append(intersection) 128 | 129 | if their_current_range.allows_higher(our_current_range): 130 | our_current_range = next(our_ranges, None) 131 | else: 132 | their_current_range = next(their_ranges, None) 133 | 134 | return VersionUnion.of(*new_ranges) 135 | 136 | def union(self, other): # type: (VersionConstraint) -> VersionConstraint 137 | return VersionUnion.of(self, other) 138 | 139 | def difference(self, other): # type: (VersionConstraint) -> VersionConstraint 140 | our_ranges = iter(self._ranges) 141 | their_ranges = iter(self._ranges_for(other)) 142 | new_ranges = [] 143 | 144 | state = { 145 | "current": next(our_ranges, None), 146 | "their_range": next(their_ranges, None), 147 | } 148 | 149 | def their_next_range(): 150 | state["their_range"] = next(their_ranges, None) 151 | if state["their_range"]: 152 | return True 153 | 154 | new_ranges.append(state["current"]) 155 | our_current = next(our_ranges, None) 156 | while our_current: 157 | new_ranges.append(our_current) 158 | our_current = next(our_ranges, None) 159 | 160 | return False 161 | 162 | def our_next_range(include_current=True): 163 | if include_current: 164 | new_ranges.append(state["current"]) 165 | 166 | our_current = next(our_ranges, None) 167 | if not our_current: 168 | return False 169 | 170 | state["current"] = our_current 171 | 172 | return True 173 | 174 | while True: 175 | if state["their_range"] is None: 176 | break 177 | 178 | if state["their_range"].is_strictly_lower(state["current"]): 179 | if not their_next_range(): 180 | break 181 | 182 | continue 183 | 184 | if state["their_range"].is_strictly_higher(state["current"]): 185 | if not our_next_range(): 186 | break 187 | 188 | continue 189 | 190 | difference = state["current"].difference(state["their_range"]) 191 | if isinstance(difference, VersionUnion): 192 | assert len(difference.ranges) == 2 193 | new_ranges.append(difference.ranges[0]) 194 | state["current"] = difference.ranges[-1] 195 | 196 | if not their_next_range(): 197 | break 198 | elif difference.is_empty(): 199 | if not our_next_range(False): 200 | break 201 | else: 202 | state["current"] = difference 203 | 204 | if state["current"].allows_higher(state["their_range"]): 205 | if not their_next_range(): 206 | break 207 | else: 208 | if not our_next_range(): 209 | break 210 | 211 | if not new_ranges: 212 | return EmptyConstraint() 213 | 214 | if len(new_ranges) == 1: 215 | return new_ranges[0] 216 | 217 | return VersionUnion.of(*new_ranges) 218 | 219 | def _ranges_for( 220 | self, constraint 221 | ): # type: (VersionConstraint) -> List[semver.VersionRange] 222 | from .version_range import VersionRange 223 | 224 | if constraint.is_empty(): 225 | return [] 226 | 227 | if isinstance(constraint, VersionUnion): 228 | return constraint.ranges 229 | 230 | if isinstance(constraint, VersionRange): 231 | return [constraint] 232 | 233 | raise ValueError("Unknown VersionConstraint type {}".format(constraint)) 234 | 235 | def _excludes_single_version(self): # type: () -> bool 236 | from .version import Version 237 | from .version_range import VersionRange 238 | 239 | return isinstance(VersionRange().difference(self), Version) 240 | 241 | def __eq__(self, other): 242 | if not isinstance(other, VersionUnion): 243 | return False 244 | 245 | return self._ranges == other.ranges 246 | 247 | def __str__(self): 248 | from .version_range import VersionRange 249 | 250 | if self._excludes_single_version(): 251 | return "!={}".format(VersionRange().difference(self)) 252 | 253 | return " || ".join([str(r) for r in self._ranges]) 254 | 255 | def __repr__(self): 256 | return "".format(str(self)) 257 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-poetry/semver/7ef69b00f6bd63539eb01ea8a947ca99a11fde93/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from semver import Version 4 | from semver import VersionRange 5 | from semver import VersionUnion 6 | from semver import parse_constraint 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "input,constraint", 11 | [ 12 | ("*", VersionRange()), 13 | ("*.*", VersionRange()), 14 | ("v*.*", VersionRange()), 15 | ("*.x.*", VersionRange()), 16 | ("x.X.x.*", VersionRange()), 17 | # ('!=1.0.0', Constraint('!=', '1.0.0.0')), 18 | (">1.0.0", VersionRange(min=Version(1, 0, 0))), 19 | ("<1.2.3", VersionRange(max=Version(1, 2, 3))), 20 | ("<=1.2.3", VersionRange(max=Version(1, 2, 3), include_max=True)), 21 | (">=1.2.3", VersionRange(min=Version(1, 2, 3), include_min=True)), 22 | ("=1.2.3", Version(1, 2, 3)), 23 | ("1.2.3", Version(1, 2, 3)), 24 | ("=1.0", Version(1, 0, 0)), 25 | ("1.2.3b5", Version(1, 2, 3, pre="b5")), 26 | (">= 1.2.3", VersionRange(min=Version(1, 2, 3), include_min=True)), 27 | (">dev", VersionRange(min=Version(0, 0, pre="dev"))), # Issue 206 28 | ], 29 | ) 30 | def test_parse_constraint(input, constraint): 31 | assert parse_constraint(input) == constraint 32 | 33 | 34 | @pytest.mark.parametrize( 35 | "input,constraint", 36 | [ 37 | ("v2.*", VersionRange(Version(2, 0, 0), Version(3, 0, 0), True)), 38 | ("2.*.*", VersionRange(Version(2, 0, 0), Version(3, 0, 0), True)), 39 | ("20.*", VersionRange(Version(20, 0, 0), Version(21, 0, 0), True)), 40 | ("20.*.*", VersionRange(Version(20, 0, 0), Version(21, 0, 0), True)), 41 | ("2.0.*", VersionRange(Version(2, 0, 0), Version(2, 1, 0), True)), 42 | ("2.x", VersionRange(Version(2, 0, 0), Version(3, 0, 0), True)), 43 | ("2.x.x", VersionRange(Version(2, 0, 0), Version(3, 0, 0), True)), 44 | ("2.2.X", VersionRange(Version(2, 2, 0), Version(2, 3, 0), True)), 45 | ("0.*", VersionRange(max=Version(1, 0, 0))), 46 | ("0.*.*", VersionRange(max=Version(1, 0, 0))), 47 | ("0.x", VersionRange(max=Version(1, 0, 0))), 48 | ], 49 | ) 50 | def test_parse_constraint_wildcard(input, constraint): 51 | assert parse_constraint(input) == constraint 52 | 53 | 54 | @pytest.mark.parametrize( 55 | "input,constraint", 56 | [ 57 | ("~v1", VersionRange(Version(1, 0, 0), Version(2, 0, 0), True)), 58 | ("~1.0", VersionRange(Version(1, 0, 0), Version(1, 1, 0), True)), 59 | ("~1.0.0", VersionRange(Version(1, 0, 0), Version(1, 1, 0), True)), 60 | ("~1.2", VersionRange(Version(1, 2, 0), Version(1, 3, 0), True)), 61 | ("~1.2.3", VersionRange(Version(1, 2, 3), Version(1, 3, 0), True)), 62 | ( 63 | "~1.2-beta", 64 | VersionRange(Version(1, 2, 0, pre="beta"), Version(1, 3, 0), True), 65 | ), 66 | ("~1.2-b2", VersionRange(Version(1, 2, 0, pre="b2"), Version(1, 3, 0), True)), 67 | ("~0.3", VersionRange(Version(0, 3, 0), Version(0, 4, 0), True)), 68 | ("~3.5", VersionRange(Version(3, 5, 0), Version(3, 6, 0), True)), 69 | ("~=3.5", VersionRange(Version(3, 5, 0), Version(4, 0, 0), True)), # PEP 440 70 | ("~=3.5.3", VersionRange(Version(3, 5, 3), Version(3, 6, 0), True)), # PEP 440 71 | ], 72 | ) 73 | def test_parse_constraint_tilde(input, constraint): 74 | assert parse_constraint(input) == constraint 75 | 76 | 77 | @pytest.mark.parametrize( 78 | "input,constraint", 79 | [ 80 | ("^v1", VersionRange(Version(1, 0, 0), Version(2, 0, 0), True)), 81 | ("^0", VersionRange(Version(0, 0, 0), Version(1, 0, 0), True)), 82 | ("^0.0", VersionRange(Version(0, 0, 0), Version(0, 1, 0), True)), 83 | ("^1.2", VersionRange(Version(1, 2, 0), Version(2, 0, 0), True)), 84 | ( 85 | "^1.2.3-beta.2", 86 | VersionRange(Version(1, 2, 3, pre="beta.2"), Version(2, 0, 0), True), 87 | ), 88 | ("^1.2.3", VersionRange(Version(1, 2, 3), Version(2, 0, 0), True)), 89 | ("^0.2.3", VersionRange(Version(0, 2, 3), Version(0, 3, 0), True)), 90 | ("^0.2", VersionRange(Version(0, 2, 0), Version(0, 3, 0), True)), 91 | ("^0.2.0", VersionRange(Version(0, 2, 0), Version(0, 3, 0), True)), 92 | ("^0.0.3", VersionRange(Version(0, 0, 3), Version(0, 0, 4), True)), 93 | ], 94 | ) 95 | def test_parse_constraint_caret(input, constraint): 96 | assert parse_constraint(input) == constraint 97 | 98 | 99 | @pytest.mark.parametrize( 100 | "input", 101 | [ 102 | ">2.0,<=3.0", 103 | ">2.0 <=3.0", 104 | ">2.0 <=3.0", 105 | ">2.0, <=3.0", 106 | ">2.0 ,<=3.0", 107 | ">2.0 , <=3.0", 108 | ">2.0 , <=3.0", 109 | "> 2.0 <= 3.0", 110 | "> 2.0 , <= 3.0", 111 | " > 2.0 , <= 3.0 ", 112 | ], 113 | ) 114 | def test_parse_constraint_multi(input): 115 | assert parse_constraint(input) == VersionRange( 116 | Version(2, 0, 0), Version(3, 0, 0), include_min=False, include_max=True 117 | ) 118 | 119 | 120 | @pytest.mark.parametrize( 121 | "input", 122 | [">=2.7,!=3.0.*,!=3.1.*", ">=2.7, !=3.0.*, !=3.1.*", ">= 2.7, != 3.0.*, != 3.1.*"], 123 | ) 124 | def test_parse_constraint_multi_wilcard(input): 125 | assert parse_constraint(input) == VersionUnion( 126 | VersionRange(Version(2, 7, 0), Version(3, 0, 0), True, False), 127 | VersionRange(Version(3, 2, 0), None, True, False), 128 | ) 129 | 130 | 131 | @pytest.mark.parametrize( 132 | "input,constraint", 133 | [ 134 | ( 135 | "!=v2.*", 136 | VersionRange(max=Version.parse("2.0")).union( 137 | VersionRange(Version.parse("3.0"), include_min=True) 138 | ), 139 | ), 140 | ( 141 | "!=2.*.*", 142 | VersionRange(max=Version.parse("2.0")).union( 143 | VersionRange(Version.parse("3.0"), include_min=True) 144 | ), 145 | ), 146 | ( 147 | "!=2.0.*", 148 | VersionRange(max=Version.parse("2.0")).union( 149 | VersionRange(Version.parse("2.1"), include_min=True) 150 | ), 151 | ), 152 | ("!=0.*", VersionRange(Version.parse("1.0"), include_min=True)), 153 | ("!=0.*.*", VersionRange(Version.parse("1.0"), include_min=True)), 154 | ], 155 | ) 156 | def test_parse_constraints_negative_wildcard(input, constraint): 157 | assert parse_constraint(input) == constraint 158 | 159 | 160 | @pytest.mark.parametrize( 161 | "input, expected", 162 | [ 163 | ("1", "1"), 164 | ("1.2", "1.2"), 165 | ("1.2.3", "1.2.3"), 166 | ("!=1", "!=1"), 167 | ("!=1.2", "!=1.2"), 168 | ("!=1.2.3", "!=1.2.3"), 169 | ("^1", ">=1,<2"), 170 | ("^1.0", ">=1.0,<2.0"), 171 | ("^1.0.0", ">=1.0.0,<2.0.0"), 172 | ("~1", ">=1,<2"), 173 | ("~1.0", ">=1.0,<1.1"), 174 | ("~1.0.0", ">=1.0.0,<1.1.0"), 175 | ], 176 | ) 177 | def test_constraints_keep_version_precision(input, expected): 178 | assert str(parse_constraint(input)) == expected 179 | 180 | 181 | @pytest.mark.parametrize( 182 | "unsorted, sorted_", 183 | [ 184 | (["1.0.3", "1.0.2", "1.0.1"], ["1.0.1", "1.0.2", "1.0.3"]), 185 | (["1.0.0.2", "1.0.0.0rc2"], ["1.0.0.0rc2", "1.0.0.2"]), 186 | (["1.0.0.0", "1.0.0.0rc2"], ["1.0.0.0rc2", "1.0.0.0"]), 187 | (["1.0.0.0.0", "1.0.0.0rc2"], ["1.0.0.0rc2", "1.0.0.0.0"]), 188 | (["1.0.0rc2", "1.0.0rc1"], ["1.0.0rc1", "1.0.0rc2"]), 189 | (["1.0.0rc2", "1.0.0b1"], ["1.0.0b1", "1.0.0rc2"]), 190 | ], 191 | ) 192 | def test_versions_are_sortable(unsorted, sorted_): 193 | unsorted = [parse_constraint(u) for u in unsorted] 194 | sorted_ = [parse_constraint(s) for s in sorted_] 195 | 196 | assert sorted(unsorted) == sorted_ 197 | -------------------------------------------------------------------------------- /tests/test_semver.py: -------------------------------------------------------------------------------- 1 | from semver import __version__ 2 | 3 | 4 | def test_version(): 5 | assert __version__ == "0.1.0" 6 | -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from semver import EmptyConstraint 4 | from semver import Version 5 | from semver import VersionRange 6 | from semver.exceptions import ParseVersionError 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "input,version", 11 | [ 12 | ("1.0.0", Version(1, 0, 0)), 13 | ("1", Version(1, 0, 0)), 14 | ("1.0", Version(1, 0, 0)), 15 | ("1b1", Version(1, 0, 0, pre="beta1")), 16 | ("1.0b1", Version(1, 0, 0, pre="beta1")), 17 | ("1.0.0b1", Version(1, 0, 0, pre="beta1")), 18 | ("1.0.0-b1", Version(1, 0, 0, pre="beta1")), 19 | ("1.0.0-beta.1", Version(1, 0, 0, pre="beta1")), 20 | ("1.0.0+1", Version(1, 0, 0, build="1")), 21 | ("1.0.0-1", Version(1, 0, 0, build="1")), 22 | ("1.0.0.0", Version(1, 0, 0)), 23 | ("1.0.0-post", Version(1, 0, 0)), 24 | ("1.0.0-post1", Version(1, 0, 0, build="1")), 25 | ("0.6c", Version(0, 6, 0, pre="rc0")), 26 | ("0.6pre", Version(0, 6, 0, pre="rc0")), 27 | ], 28 | ) 29 | def test_parse_valid(input, version): 30 | parsed = Version.parse(input) 31 | 32 | assert parsed == version 33 | assert parsed.text == input 34 | 35 | 36 | @pytest.mark.parametrize("input", [(None, "example")]) 37 | def test_parse_invalid(input): 38 | with pytest.raises(ParseVersionError): 39 | Version.parse(input) 40 | 41 | 42 | def test_comparison(): 43 | versions = [ 44 | "1.0.0-alpha", 45 | "1.0.0-alpha.1", 46 | "1.0.0-beta.2", 47 | "1.0.0-beta.11", 48 | "1.0.0-rc.1", 49 | "1.0.0-rc.1+build.1", 50 | "1.0.0", 51 | "1.0.0+0.3.7", 52 | "1.3.7+build", 53 | "1.3.7+build.2.b8f12d7", 54 | "1.3.7+build.11.e0f985a", 55 | "2.0.0", 56 | "2.1.0", 57 | "2.2.0", 58 | "2.11.0", 59 | "2.11.1", 60 | ] 61 | 62 | for i in range(len(versions)): 63 | for j in range(len(versions)): 64 | a = Version.parse(versions[i]) 65 | b = Version.parse(versions[j]) 66 | 67 | assert (a < b) == (i < j) 68 | assert (a > b) == (i > j) 69 | assert (a <= b) == (i <= j) 70 | assert (a >= b) == (i >= j) 71 | assert (a == b) == (i == j) 72 | assert (a != b) == (i != j) 73 | 74 | 75 | def test_equality(): 76 | assert Version.parse("1.2.3") == Version.parse("01.2.3") 77 | assert Version.parse("1.2.3") == Version.parse("1.02.3") 78 | assert Version.parse("1.2.3") == Version.parse("1.2.03") 79 | assert Version.parse("1.2.3-1") == Version.parse("1.2.3-01") 80 | assert Version.parse("1.2.3+1") == Version.parse("1.2.3+01") 81 | 82 | 83 | def test_allows(): 84 | v = Version.parse("1.2.3") 85 | assert v.allows(v) 86 | assert not v.allows(Version.parse("2.2.3")) 87 | assert not v.allows(Version.parse("1.3.3")) 88 | assert not v.allows(Version.parse("1.2.4")) 89 | assert not v.allows(Version.parse("1.2.3-dev")) 90 | assert not v.allows(Version.parse("1.2.3+build")) 91 | 92 | 93 | def test_allows_all(): 94 | v = Version.parse("1.2.3") 95 | 96 | assert v.allows_all(v) 97 | assert not v.allows_all(Version.parse("0.0.3")) 98 | assert not v.allows_all( 99 | VersionRange(Version.parse("1.1.4"), Version.parse("1.2.4")) 100 | ) 101 | assert not v.allows_all(VersionRange()) 102 | assert v.allows_all(EmptyConstraint()) 103 | 104 | 105 | def test_allows_any(): 106 | v = Version.parse("1.2.3") 107 | 108 | assert v.allows_any(v) 109 | assert not v.allows_any(Version.parse("0.0.3")) 110 | assert v.allows_any(VersionRange(Version.parse("1.1.4"), Version.parse("1.2.4"))) 111 | assert v.allows_any(VersionRange()) 112 | assert not v.allows_any(EmptyConstraint()) 113 | 114 | 115 | def test_intersect(): 116 | v = Version.parse("1.2.3") 117 | 118 | assert v.intersect(v) == v 119 | assert v.intersect(Version.parse("1.1.4")).is_empty() 120 | assert ( 121 | v.intersect(VersionRange(Version.parse("1.1.4"), Version.parse("1.2.4"))) == v 122 | ) 123 | assert ( 124 | Version.parse("1.1.4") 125 | .intersect(VersionRange(v, Version.parse("1.2.4"))) 126 | .is_empty() 127 | ) 128 | 129 | 130 | def test_union(): 131 | v = Version.parse("1.2.3") 132 | 133 | assert v.union(v) == v 134 | 135 | result = v.union(Version.parse("0.8.0")) 136 | assert result.allows(v) 137 | assert result.allows(Version.parse("0.8.0")) 138 | assert not result.allows(Version.parse("1.1.4")) 139 | 140 | range = VersionRange(Version.parse("1.1.4"), Version.parse("1.2.4")) 141 | assert v.union(range) == range 142 | 143 | union = Version.parse("1.1.4").union( 144 | VersionRange(Version.parse("1.1.4"), Version.parse("1.2.4")) 145 | ) 146 | assert union == VersionRange( 147 | Version.parse("1.1.4"), Version.parse("1.2.4"), include_min=True 148 | ) 149 | 150 | result = v.union(VersionRange(Version.parse("0.0.3"), Version.parse("1.1.4"))) 151 | assert result.allows(v) 152 | assert result.allows(Version.parse("0.1.0")) 153 | 154 | 155 | def test_difference(): 156 | v = Version.parse("1.2.3") 157 | 158 | assert v.difference(v).is_empty() 159 | assert v.difference(Version.parse("0.8.0")) == v 160 | assert v.difference( 161 | VersionRange(Version.parse("1.1.4"), Version.parse("1.2.4")) 162 | ).is_empty() 163 | assert ( 164 | v.difference(VersionRange(Version.parse("1.4.0"), Version.parse("3.0.0"))) == v 165 | ) 166 | -------------------------------------------------------------------------------- /tests/test_version_range.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from semver import EmptyConstraint 4 | from semver import Version 5 | from semver import VersionRange 6 | 7 | 8 | @pytest.fixture() 9 | def v003(): 10 | return Version.parse("0.0.3") 11 | 12 | 13 | @pytest.fixture() 14 | def v010(): 15 | return Version.parse("0.1.0") 16 | 17 | 18 | @pytest.fixture() 19 | def v080(): 20 | return Version.parse("0.8.0") 21 | 22 | 23 | @pytest.fixture() 24 | def v072(): 25 | return Version.parse("0.7.2") 26 | 27 | 28 | @pytest.fixture() 29 | def v114(): 30 | return Version.parse("1.1.4") 31 | 32 | 33 | @pytest.fixture() 34 | def v123(): 35 | return Version.parse("1.2.3") 36 | 37 | 38 | @pytest.fixture() 39 | def v124(): 40 | return Version.parse("1.2.4") 41 | 42 | 43 | @pytest.fixture() 44 | def v130(): 45 | return Version.parse("1.3.0") 46 | 47 | 48 | @pytest.fixture() 49 | def v140(): 50 | return Version.parse("1.4.0") 51 | 52 | 53 | @pytest.fixture() 54 | def v200(): 55 | return Version.parse("2.0.0") 56 | 57 | 58 | @pytest.fixture() 59 | def v234(): 60 | return Version.parse("2.3.4") 61 | 62 | 63 | @pytest.fixture() 64 | def v250(): 65 | return Version.parse("2.5.0") 66 | 67 | 68 | @pytest.fixture() 69 | def v300(): 70 | return Version.parse("3.0.0") 71 | 72 | 73 | def test_allows_all(v003, v010, v080, v114, v123, v124, v140, v200, v234, v250, v300): 74 | assert VersionRange(v123, v250).allows_all(EmptyConstraint()) 75 | 76 | range = VersionRange(v123, v250, include_max=True) 77 | assert not range.allows_all(v123) 78 | assert range.allows_all(v124) 79 | assert range.allows_all(v250) 80 | assert not range.allows_all(v300) 81 | 82 | # with no min 83 | range = VersionRange(max=v250) 84 | assert range.allows_all(VersionRange(v080, v140)) 85 | assert not range.allows_all(VersionRange(v080, v300)) 86 | assert range.allows_all(VersionRange(max=v140)) 87 | assert not range.allows_all(VersionRange(max=v300)) 88 | assert range.allows_all(range) 89 | assert not range.allows_all(VersionRange()) 90 | 91 | # with no max 92 | range = VersionRange(min=v010) 93 | assert range.allows_all(VersionRange(v080, v140)) 94 | assert not range.allows_all(VersionRange(v003, v140)) 95 | assert range.allows_all(VersionRange(v080)) 96 | assert not range.allows_all(VersionRange(v003)) 97 | assert range.allows_all(range) 98 | assert not range.allows_all(VersionRange()) 99 | 100 | # Allows bordering range that is not more inclusive 101 | exclusive = VersionRange(v010, v250) 102 | inclusive = VersionRange(v010, v250, True, True) 103 | assert inclusive.allows_all(exclusive) 104 | assert inclusive.allows_all(inclusive) 105 | assert not exclusive.allows_all(inclusive) 106 | assert exclusive.allows_all(exclusive) 107 | 108 | # Allows unions that are completely contained 109 | range = VersionRange(v114, v200) 110 | assert range.allows_all(VersionRange(v123, v124).union(v140)) 111 | assert not range.allows_all(VersionRange(v010, v124).union(v140)) 112 | assert not range.allows_all(VersionRange(v123, v234).union(v140)) 113 | 114 | 115 | def test_allows_any( 116 | v003, v010, v072, v080, v114, v123, v124, v140, v200, v234, v250, v300 117 | ): 118 | # disallows an empty constraint 119 | assert not VersionRange(v123, v250).allows_any(EmptyConstraint()) 120 | 121 | # allows allowed versions 122 | range = VersionRange(v123, v250, include_max=True) 123 | assert not range.allows_any(v123) 124 | assert range.allows_any(v124) 125 | assert range.allows_any(v250) 126 | assert not range.allows_any(v300) 127 | 128 | # with no min 129 | range = VersionRange(max=v200) 130 | assert range.allows_any(VersionRange(v140, v300)) 131 | assert not range.allows_any(VersionRange(v234, v300)) 132 | assert range.allows_any(VersionRange(v140)) 133 | assert not range.allows_any(VersionRange(v234)) 134 | assert range.allows_any(range) 135 | 136 | # with no max 137 | range = VersionRange(min=v072) 138 | assert range.allows_any(VersionRange(v003, v140)) 139 | assert not range.allows_any(VersionRange(v003, v010)) 140 | assert range.allows_any(VersionRange(max=v080)) 141 | assert not range.allows_any(VersionRange(max=v003)) 142 | assert range.allows_any(range) 143 | 144 | # with min and max 145 | range = VersionRange(v072, v200) 146 | assert range.allows_any(VersionRange(v003, v140)) 147 | assert range.allows_any(VersionRange(v140, v300)) 148 | assert not range.allows_any(VersionRange(v003, v010)) 149 | assert not range.allows_any(VersionRange(v234, v300)) 150 | assert not range.allows_any(VersionRange(max=v010)) 151 | assert not range.allows_any(VersionRange(v234)) 152 | assert range.allows_any(range) 153 | 154 | # allows a bordering range when both are inclusive 155 | assert not VersionRange(max=v250).allows_any(VersionRange(min=v250)) 156 | assert not VersionRange(max=v250, include_max=True).allows_any( 157 | VersionRange(min=v250) 158 | ) 159 | assert not VersionRange(max=v250).allows_any( 160 | VersionRange(min=v250, include_min=True) 161 | ) 162 | assert not VersionRange(min=v250).allows_any(VersionRange(max=v250)) 163 | assert VersionRange(max=v250, include_max=True).allows_any( 164 | VersionRange(min=v250, include_min=True) 165 | ) 166 | 167 | # allows unions that are partially contained' 168 | range = VersionRange(v114, v200) 169 | assert range.allows_any(VersionRange(v010, v080).union(v140)) 170 | assert range.allows_any(VersionRange(v123, v234).union(v300)) 171 | assert not range.allows_any(VersionRange(v234, v300).union(v010)) 172 | 173 | # pre-release min does not allow lesser than itself 174 | range = VersionRange(Version.parse("1.9b1"), include_min=True) 175 | assert not range.allows_any( 176 | VersionRange( 177 | Version.parse("1.8.0"), 178 | Version.parse("1.9.0"), 179 | include_min=True, 180 | always_include_max_prerelease=True, 181 | ) 182 | ) 183 | 184 | 185 | def test_intersect(v114, v123, v124, v200, v250, v300): 186 | # two overlapping ranges 187 | assert VersionRange(v123, v250).intersect(VersionRange(v200, v300)) == VersionRange( 188 | v200, v250 189 | ) 190 | 191 | # a non-overlapping range allows no versions 192 | a = VersionRange(v114, v124) 193 | b = VersionRange(v200, v250) 194 | assert a.intersect(b).is_empty() 195 | 196 | # adjacent ranges allow no versions if exclusive 197 | a = VersionRange(v114, v124) 198 | b = VersionRange(v124, v200) 199 | assert a.intersect(b).is_empty() 200 | 201 | # adjacent ranges allow version if inclusive 202 | a = VersionRange(v114, v124, include_max=True) 203 | b = VersionRange(v124, v200, include_min=True) 204 | assert a.intersect(b) == v124 205 | 206 | # with an open range 207 | open = VersionRange() 208 | a = VersionRange(v114, v124) 209 | assert open.intersect(open) == open 210 | assert open.intersect(a) == a 211 | 212 | # returns the version if the range allows it 213 | assert VersionRange(v114, v124).intersect(v123) == v123 214 | assert VersionRange(v123, v124).intersect(v114).is_empty() 215 | 216 | 217 | def test_union( 218 | v003, v010, v072, v080, v114, v123, v124, v130, v140, v200, v234, v250, v300 219 | ): 220 | # with a version returns the range if it contains the version 221 | range = VersionRange(v114, v124) 222 | assert range.union(v123) == range 223 | 224 | # with a version on the edge of the range, expands the range 225 | range = VersionRange(v114, v124) 226 | assert range.union(v124) == VersionRange(v114, v124, include_max=True) 227 | assert range.union(v114) == VersionRange(v114, v124, include_min=True) 228 | 229 | # with a version allows both the range and the version if the range 230 | # doesn't contain the version 231 | result = VersionRange(v003, v114).union(v124) 232 | assert result.allows(v010) 233 | assert not result.allows(v123) 234 | assert result.allows(v124) 235 | 236 | # returns a VersionUnion for a disjoint range 237 | result = VersionRange(v003, v114).union(VersionRange(v130, v200)) 238 | assert result.allows(v080) 239 | assert not result.allows(v123) 240 | assert result.allows(v140) 241 | 242 | # considers open ranges disjoint 243 | result = VersionRange(v003, v114).union(VersionRange(v114, v200)) 244 | assert result.allows(v080) 245 | assert not result.allows(v114) 246 | assert result.allows(v140) 247 | result = VersionRange(v114, v200).union(VersionRange(v003, v114)) 248 | assert result.allows(v080) 249 | assert not result.allows(v114) 250 | assert result.allows(v140) 251 | 252 | # returns a merged range for an overlapping range 253 | result = VersionRange(v003, v114).union(VersionRange(v080, v200)) 254 | assert result == VersionRange(v003, v200) 255 | 256 | # considers closed ranges overlapping 257 | result = VersionRange(v003, v114, include_max=True).union(VersionRange(v114, v200)) 258 | assert result == VersionRange(v003, v200) 259 | result = VersionRange(v003, v114).union(VersionRange(v114, v200, include_min=True)) 260 | assert result == VersionRange(v003, v200) 261 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist = True 3 | envlist = py27, py34, py35, py36, py37, py38 4 | 5 | [testenv] 6 | whitelist_externals = poetry 7 | skip_install = true 8 | commands = 9 | poetry install -vvv 10 | poetry run pytest tests/ 11 | --------------------------------------------------------------------------------