Commit c5c7624e by Sébastien Eustace

Initial commit (semver)

parents
*.pyc
# Packages
*.egg
*.egg-info
dist
build
_build
.cache
*.so
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
nosetests.xml
.pytest_cache
.DS_Store
.idea/*
.python-version
poetry.lock
# Poetry: Dependency Management for Python
Poetry helps you declare, manage and install dependencies of Python projects,
ensuring you have the right stack everywhere.
The package is **highly experimental** at the moment so expect things to change and break.
However, if you feel adventurous feedback and pull requests are greatly appreciated.
## Installation
```bash
pip install poetry
```
### Enable tab completion for Bash, Fish, or Zsh
`poetry` supports generating completion scripts for Bash, Fish, and Zsh.
See `poet help completions` for full details, but the gist is as simple as using one of the following:
```bash
# Bash
$ poet completions bash > /etc/bash_completion.d/poet.bash-completion
# Bash (macOS/Homebrew)
$ poet completions bash > $(brew --prefix)/etc/bash_completion.d/poet.bash-completion
# Fish
$ poet completions fish > ~/.config/fish/completions/poet.fish
# Zsh
$ poet completions zsh > ~/.zfunc/_poet
```
*Note:* you may need to restart your shell in order for the changes to take
effect.
For `zsh`, you must then add the following line in your `~/.zshrc` before
`compinit`:
```zsh
fpath+=~/.zfunc
```
## Introduction
`poetry` is a tool to handle dependencies installation, building and packaging of Python packages.
It only needs one file to do all of that: `poetry.toml`.
```toml
[package]
name = "pypoet"
version = "0.1.0"
description = "Poet helps you declare, manage and install dependencies of Python projects, ensuring you have the right stack everywhere."
license = "MIT"
authors = [
"Sébastien Eustace <sebastien@eustace.io>"
]
readme = 'README.md'
repository = "https://github.com/sdispater/poet"
homepage = "https://github.com/sdispater/poet"
keywords = ['packaging', 'poet']
include = ['poet/**/*', 'LICENSE']
python = ["~2.7", "^3.2"]
[dependencies]
toml = "^0.9"
requests = "^2.13"
semantic_version = "^2.6"
pygments = "^2.2"
twine = "^1.8"
wheel = "^0.29"
pip-tools = "^1.8.2"
cleo = { git = "https://github.com/sdispater/cleo.git", branch = "master" }
[dev-dependencies]
pytest = "^3.0"
pytest-cov = "^2.4"
coverage = "<4.0"
httpretty = "^0.8.14"
[scripts]
poet = 'poet:app.run'
```
There are some things we can notice here:
* It will try to enforce [semantic versioning](<http://semver.org>) as the best practice in version naming.
* You can specify the readme, included and excluded files: no more `MANIFEST.in`.
`poetry` will also use VCS ignore files (like `.gitignore`) to populate the `exclude` section.
* Keywords (up to 5) can be specified and will act as tags on the packaging site.
* The dependencies sections support caret, tilde, wildcard, inequality and multiple requirements.
* You must specify the python versions for which your package is compatible.
`poetry` will also detect if you are inside a virtualenv and install the packages accordingly.
If not it will create one for you. So, `poetry` can be installed globally and used everywhere.
## Why?
Packaging system and dependency management in Python is rather convoluted and hard to understand for newcomers.
Even for seasoned developers it might be cumbersome at times to create all files needed in a Python project: `setup.py`,
`requirements.txt`, `setup.cfg`, `MANIFEST.in`.
So I wanted a tool that would limit everything to a single configuration file to do everything: dependency management, packaging
and publishing.
It takes inspiration in tools that exist in other languages, like `composer` (PHP) or `cargo` (Rust).
Note that there is no magic here, `poet` uses existing tools (`pip`, `twine`, `setuptools`, `distutils`, `pip-tools`) under the hood
to achieve that in a more intuitive way.
## Commands
### init
This command will help you create a `poetry.toml` file interactively
by prompting you to provide basic information about your package.
It will interactively ask you to fill in the fields, while using some smart defaults.
```bash
poet init
```
However, if you just want a basic template and fill the information directly, you can just do:
```bash
poet init default
```
#### Options
* `--name`: Name of the package.
* `--description`: Description of the package.
* `--author`: Author of the package.
* `--require`: Package to require with a version constraint. Should be in format `foo:1.0.0`.
* `--require-dev`: Development requirements, see `--require`.
* `--index`: Index to use when searching for packages.
### install
The `install` command reads the `poetry.toml` file from the current directory, resolves the dependencies,
and installs them.
```bash
poet install
```
If there is a `poetry.lock` file in the current directory,
it will use the exact versions from there instead of resolving them.
This ensures that everyone using the library will get the same versions of the dependencies.
If there is no `poetry.lock` file, Poet will create one after dependency resolution.
You can specify to the command that yo do not want the development dependencies installed by passing
the `--no-dev` option.
```bash
poet install --no-dev
```
You can also specify the features you want installed by passing the `--f|--features` option (See [Features](#features) for more info)
```bash
poet install --features "mysql pgsql"
poet install -f mysql -f pgsql
```
#### Options
* `--no-dev`: Do not install dev dependencies.
* `-f|--features`: Features to install (multiple values allowed).
* `--no-progress`: Removes the progress display that can mess with some terminals or scripts which don't handle backspace characters.
* `--index`: The index to use when installing packages.
### update
In order to get the latest versions of the dependencies and to update the `poetry.lock` file,
you should use the `update` command.
```bash
poet update
```
This will resolve all dependencies of the project and write the exact versions into `poetry.lock`.
If you just want to update a few packages and not all, you can list them as such:
```bash
poet update requests toml
```
#### Options
* `--no-progress`: Removes the progress display that can mess with some terminals or scripts which don't handle backspace characters.
* `--index`: The index to use when installing packages.
### package
The `package` command builds the source and wheels archives.
#### Options
* `--no-universal`: Do not build a universal wheel.
* `--no-wheels`: Build only the source package.
* `-c|--clean`: Make a clean package.
### publish
This command builds (if not already built) and publishes the package to the remote repository.
It will automatically register the package before uploading if this is the first time it is submitted.
#### Options
* `-r|--repository`: The repository to register the package to (default: `pypi`). Should match a section of your `~/.pypirc` file.
### search
This command searches for packages on a remote index.
```bash
poet search requests pendulum
```
#### Options
* `-i|--index`: The index to use.
* `-N|--only-name`: Search only in name.
### lock
This command locks (without installing) the dependencies specified in `poetry.toml`.
```bash
poet lock
```
#### Options
* `--no-progress`: Removes the progress display that can mess with some terminals or scripts which don't handle backspace characters.
* `-i|--index`: The index to use.
* `-f|--force`: Force locking.
### check
The `check` command will check if the `poetry.toml` file is valid.
```bash
poet check
```
## The `poetry.toml` file
A `poetry.toml` file is composed of multiple sections.
### package
This section describes the specifics of the package
#### name
The name of the package. **Required**
#### version
The version of the package. **Required**
This should follow [semantic versioning](http://semver.org/). However it will not be enforced and you remain
free to follow another specification.
#### description
A short description of the package. **Required**
#### license
The license of the package.
The recommended notation for the most common licenses is (alphabetical):
* Apache-2.0
* BSD-2-Clause
* BSD-3-Clause
* BSD-4-Clause
* GPL-2.0
* GPL-2.0+
* GPL-3.0
* GPL-3.0+
* LGPL-2.1
* LGPL-2.1+
* LGPL-3.0
* LGPL-3.0+
* MIT
Optional, but it is highly recommended to supply this.
More identifiers are listed at the [SPDX Open Source License Registry](https://www.spdx.org/licenses/).
#### authors
The authors of the package. This is a list of authors and should contain at least one author.
Authors must be in the form `name <email>`.
#### readme
The readme file of the package. **Required**
The file can be either `README.rst` or `README.md`.
If it's a markdown file you have to install the [pandoc](https://github.com/jgm/pandoc) utility so that it can be automatically
converted to a RestructuredText file.
You also need to have the [pypandoc](https://pypi.python.org/pypi/pypandoc/) package installed. If you install `poet` via
`pip` you can use the `markdown-readme` extra to do so.
```bash
pip install pypoet[markdown-readme]
```
#### homepage
An URL to the website of the project. **Optional**
#### repository
An URL to the repository of the project. **Optional**
#### documentation
An URL to the documentation of the project. **Optional**
#### keywords
A list of keywords (max: 5) that the package is related to. **Optional**
#### python
A list of Python versions for which the package is compatible. **Required**
#### include and exclude
A list of patterns that will be included in the final package.
You can explicitly specify to Poet that a set of globs should be ignored or included for the purposes of packaging.
The globs specified in the exclude field identify a set of files that are not included when a package is built.
If a VCS is being used for a package, the exclude field will be seeded with the VCS’ ignore settings (`.gitignore` for git for example).
```toml
[package]
# ...
include = ["package/**/*.py", "package/**/.c"]
```
```toml
exclude = ["package/excluded.py"]
```
If you packages lies elsewhere (say in a `src` directory), you can tell `poet` to find them from there:
```toml
include = { from = 'src', include = '**/*' }
```
Similarly, you can tell that the `src` directory represent the `foo` package:
```toml
include = { from = 'src', include = '**/*', as = 'foo' }
```
### `dependencies` and `dev-dependencies`
Poet is configured to look for dependencies on [PyPi](https://pypi.python.org/pypi) by default.
Only the name and a version string are required in this case.
```toml
[dependencies]
requests = "^2.13.0"
```
#### Caret requirement
**Caret requirements** allow SemVer compatible updates to a specified version.
An update is allowed if the new version number does not modify the left-most non-zero digit in the major, minor, patch grouping.
In this case, if we ran `poet update requests`, poet would update us to version `2.14.0` if it was available,
but would not update us to `3.0.0`.
If instead we had specified the version string as `^0.1.13`, poet would update to `0.1.14` but not `0.2.0`.
`0.0.x` is not considered compatible with any other version.
Here are some more examples of caret requirements and the versions that would be allowed with them:
```text
^1.2.3 := >=1.2.3 <2.0.0
^1.2 := >=1.2.0 <2.0.0
^1 := >=1.0.0 <2.0.0
^0.2.3 := >=0.2.3 <0.3.0
^0.0.3 := >=0.0.3 <0.0.4
^0.0 := >=0.0.0 <0.1.0
^0 := >=0.0.0 <1.0.0
```
#### Tilde requirements
**Tilde requirements** specify a minimal version with some ability to update.
If you specify a major, minor, and patch version or only a major and minor version, only patch-level changes are allowed.
If you only specify a major version, then minor- and patch-level changes are allowed.
`~1.2.3` is an example of a tilde requirement.
```text
~1.2.3 := >=1.2.3 <1.3.0
~1.2 := >=1.2.0 <1.3.0
~1 := >=1.0.0 <2.0.0
```
#### Wildcard requirements
**Wildcard requirements** allow for any version where the wildcard is positioned.
`*`, `1.*` and `1.2.*` are examples of wildcard requirements.
```text
* := >=0.0.0
1.* := >=1.0.0 <2.0.0
1.2.* := >=1.2.0 <1.3.0
```
#### Inequality requirements
**Inequality requirements** allow manually specifying a version range or an exact version to depend on.
Here are some examples of inequality requirements:
```text
>= 1.2.0
> 1
< 2
!= 1.2.3
```
#### Multiple requirements
Multiple version requirements can also be separated with a comma, e.g. `>= 1.2, < 1.5`.
#### `git` dependencies
To depend on a library located in a `git` repository,
the minimum information you need to specify is the location of the repository with the git key:
```toml
[dependencies]
requests = { git = "https://github.com/kennethreitz/requests.git" }
```
Since we haven’t specified any other information, Poet assumes that we intend to use the latest commit on the `master` branch
to build our project.
You can combine the `git` key with the `rev`, `tag`, or `branch` keys to specify something else.
Here's an example of specifying that you want to use the latest commit on a branch named `next`:
```toml
[dependencies]
requests = { git = "https://github.com/kennethreitz/requests.git", branch = "next" }
```
#### Python restricted dependencies
You can also specify that a dependency should be installed only for specific Python versions:
```toml
[dependencies]
pathlib2 = { version = "^2.2", python = "~2.7" }
```
```toml
[dependencies]
pathlib2 = { version = "^2.2", python = ["~2.7", "^3.2"] }
```
### `scripts`
This section describe the scripts or executable that will be installed when installing the package
```toml
[scripts]
poet = 'poet:app.run'
```
Here, we will have the `poet` script installed which will execute `app.run` in the `poet` package.
### `features`
Poet supports features to allow expression of:
* optional dependencies, which enhance a package, but are not required; and
* clusters of optional dependencies.
```toml
[package]
name = "awesome"
[features]
mysql = ["mysqlclient"]
pgsql = ["psycopg2"]
[dependencies]
# These packages are mandatory and form the core of this package’s distribution.
mandatory = "^1.0"
# A list of all of the optional dependencies, some of which are included in the
# above `features`. They can be opted into by apps.
psycopg2 = { version = "^2.7", optional = true }
mysqlclient = { version = "^1.3", optional = true }
```
When installing packages, you can specify features by using the `-f|--features` option:
```bash
poet install --features "mysql pgsql"
poet install -f mysql -f pgsql
```
### `entry_points`
Poet supports arbitrary [setuptools entry points](http://setuptools.readthedocs.io/en/latest/setuptools.html). To match the example in the setuptools documentation, you would use the following:
```toml
[entry-points] # Optional super table
[entry-points."blogtool.parsers"]
".rst" = "some_module::SomeClass"
```
## Resources
* [Official Website](https://github.com/sdispater/poet)
* [Issue Tracker](https://github.com/sdispater/poet/issues)
[package]
name = "poetry"
version = "0.1.0"
description = ""
# Compatibe Python versions
python = ["~2.7", "^3.5"]
license = "MIT"
authors = [
"Sébastien Eustace <sebastien@eustace.io>"
]
# The readme file of the package.
#
# The file can be either README.rst or README.md.
#
# If it's a markdown file you have to install the pandoc utility
# so that it can be automatically converted to a RestructuredText file.
#
# You also need to have the pypandoc package installed.
# If you install poet via pip you can use the markdown-readme extra to do so.
readme = "README.md"
# An URL to the homepage of the project.
# homepage = "https://github.com/sdispater/poet"
# An URL to the repository of the project.
# repository = "https://github.com/sdispater/poet"
# An URL to the documentation of the project.
# documentation = "https://github.com/sdispater/poet"
# A list of keywords (max: 5) that the package is related to.
# keywords = ["packaging", "poet"]
# A list of patterns that will be included in the final package.
#
# Python packages, modules and package data will be automatically detected
# include = ['poet/**/*', 'LICENSE']
#
# If you packages lies elsewhere (like in a src directory),
# you can tell poet to find them from there:
# include = { from = 'src', include = '**/*' }
#
# Similarly, you can tell that the src directory represent the foo package:
# include = { from = 'src', include = '**/*', as = 'foo' }
# Features are sets of optional dependencies, which enhance a package,
# but are not required
# [features]
# markdown-readme = ["pypandoc"] # Adds support for markdown readmes
[dependencies]
# Main dependencies of the project
# See https://github.com/sdispater/poet#dependencies-and-dev-dependencies
cleo = "^0.6"
pendulum = "^1.3"
[dev-dependencies]
# Development dependencies of the package
# See https://github.com/sdispater/poet#dependencies-and-dev-dependencies
pytest = "^3.3"
# CLI scripts
#
# They must follow the following convention:
# {script_name} = "{my.package:executable}"
#
# [scripts]
# poet = 'poet:app.run'
# -*- coding: utf-8 -*-
from functools import cmp_to_key
from .comparison import less_than
from .constraints import Constraint
from .helpers import normalize_version
from .version_parser import VersionParser
SORT_ASC = 1
SORT_DESC = -1
_parser = VersionParser()
def statisfies(version, constraints):
"""
Determine if given version satisfies given constraints.
:type version: str
:type constraints: str
:rtype: bool
"""
provider = Constraint('==', normalize_version(version))
constraints = _parser.parse_constraints(constraints)
return constraints.matches(provider)
def satisfied_by(versions, constraints):
"""
Return all versions that satisfy given constraints.
:type versions: List[str]
:type constraints: str
:rtype: List[str]
"""
return [version for version in versions if statisfies(version, constraints)]
def sort(versions):
return _sort(versions, SORT_ASC)
def rsort(versions):
return _sort(versions, SORT_DESC)
def _sort(versions, direction):
normalized = [
(i, normalize_version(version))
for i, version in enumerate(versions)
]
normalized.sort(
key=cmp_to_key(
lambda x, y:
0 if x[1] == y[1]
else -direction * int(less_than(x[1], y[1]) or -1)
)
)
return [versions[i] for i, _ in normalized]
from .constraints.constraint import Constraint
def greater_than(version1, version2):
"""
Evaluates the expression: version1 > version2.
:type version1: str
:type version2: str
:rtype: bool
"""
return compare(version1, '>', version2)
def greater_than_or_equal(version1, version2):
"""
Evaluates the expression: version1 >= version2.
:type version1: str
:type version2: str
:rtype: bool
"""
return compare(version1, '>=', version2)
def less_than(version1, version2):
"""
Evaluates the expression: version1 < version2.
:type version1: str
:type version2: str
:rtype: bool
"""
return compare(version1, '<', version2)
def less_than_or_equal(version1, version2):
"""
Evaluates the expression: version1 <= version2.
:type version1: str
:type version2: str
:rtype: bool
"""
return compare(version1, '<=', version2)
def equal(version1, version2):
"""
Evaluates the expression: version1 == version2.
:type version1: str
:type version2: str
:rtype: bool
"""
return compare(version1, '==', version2)
def not_equal(version1, version2):
"""
Evaluates the expression: version1 != version2.
:type version1: str
:type version2: str
:rtype: bool
"""
return compare(version1, '!=', version2)
def compare(version1, operator, version2):
"""
Evaluates the expression: $version1 $operator $version2
:type version1: str
:type operator: str
:type version2: str
:rtype: bool
"""
constraint = Constraint(operator, version2)
return constraint.matches(Constraint('==', version1))
from .constraint import Constraint
from .empty_constraint import EmptyConstraint
from .multi_constraint import MultiConstraint
import operator
from pkg_resources import parse_version
from ..helpers import normalize_version
class Constraint:
OP_EQ = operator.eq
OP_LT = operator.lt
OP_LE = operator.le
OP_GT = operator.gt
OP_GE = operator.ge
OP_NE = operator.ne
_trans_op_str = {
'=': OP_EQ,
'==': OP_EQ,
'<': OP_LT,
'<=': OP_LE,
'>': OP_GT,
'>=': OP_GE,
'<>': OP_NE,
'!=': OP_NE
}
_trans_op_int = {
OP_EQ: '==',
OP_LT: '<',
OP_LE: '<=',
OP_GT: '>',
OP_GE: '>=',
OP_NE: '!='
}
def __init__(self, operator: str, version: str):
if operator not in self._trans_op_str:
raise ValueError(
f'Invalid operator "{operator}" given, '
f'expected one of: {", ".join(self.supported_operators)}'
)
self._operator = self._trans_op_str[operator]
self._version = version
@property
def supported_operators(self) -> list:
return list(self._trans_op_str.keys())
@property
def operator(self):
return self._operator
@property
def version(self) -> str:
return self._version
def matches(self, provider):
if isinstance(provider, self.__class__):
return self.match_specific(provider)
# turn matching around to find a match
return provider.matches(self)
def version_compare(self, a: str, b: str, operator: str) -> bool:
if operator not in self._trans_op_str:
raise ValueError(
f'Invalid operator "{operator}" given, '
f'expected one of: {", ".join(self.supported_operators)}'
)
return self._trans_op_str[operator](
parse_version(normalize_version(a)),
parse_version(normalize_version(b))
)
def match_specific(self, provider: 'Constraint') -> bool:
no_equal_op = self._trans_op_int[self._operator].replace('=', '')
provider_no_equal_op = self._trans_op_int[provider.operator].replace('=', '')
is_equal_op = self.OP_EQ is self._operator
is_non_equal_op = self.OP_NE is self._operator
is_provider_equal_op = self.OP_EQ is provider.operator
is_provider_non_equal_op = self.OP_NE is provider.operator
# '!=' operator is match when other operator
# is not '==' operator or version is not match
# these kinds of comparisons always have a solution
if is_non_equal_op or is_provider_non_equal_op:
return (not is_equal_op and not is_provider_equal_op
or self.version_compare(provider.version,
self._version,
'!='))
# An example for the condition is <= 2.0 & < 1.0
# These kinds of comparisons always have a solution
if (self._operator is not self.OP_EQ
and no_equal_op == provider_no_equal_op):
return True
if self.version_compare(
provider.version,
self.version,
self._trans_op_int[self._operator]
):
# special case, e.g. require >= 1.0 and provide < 1.0
# 1.0 >= 1.0 but 1.0 is outside of the provided interval
if (
provider.version == self.version
and self._trans_op_int[provider.operator] == provider_no_equal_op
and self._trans_op_int[self.operator] != no_equal_op
):
return False
return True
return False
def __str__(self):
return '{} {}'.format(
self._trans_op_int[self._operator],
self._version
)
def __repr__(self):
return '<Constraint \'{}\'>'.format(str(self))
class EmptyConstraint:
pretty_string = None
def matches(self, _):
return True
def __str__(self):
return '[]'
class MultiConstraint:
def __init__(self, constraints, conjunctive=True):
self._constraints = tuple(constraints)
self._conjunctive = conjunctive
@property
def constraints(self):
return self._constraints
def is_conjunctive(self):
return self._conjunctive
def is_disjunctive(self):
return not self._conjunctive
def matches(self, provider):
if self.is_disjunctive():
for constraint in self._constraints:
if constraint.matches(provider):
return True
return False
for constraint in self._constraints:
if not constraint.matches(provider):
return False
return True
def __str__(self):
constraints = []
for constraint in self._constraints:
constraints.append(str(constraint))
return '[{}]'.format(
(' ' if self._conjunctive else ' || ').join(constraints)
)
import re
_modifier_regex = (
'[._-]?'
'(?:(stable|beta|b|RC|alpha|a|patch|pl|p)((?:[.-]?\d+)*)?)?'
'([.-]?dev)?'
)
def normalize_version(version):
"""
Normalizes a version string to be able to perform comparisons on it.
"""
version = version.strip()
# strip off build metadata
m = re.match('^([^,\s+]+)\+[^\s]+$', version)
if m:
version = m.group(1)
index = None
# Match classic versioning
m = re.match(
'(?i)^v?(\d{{1,5}})(\.\d+)?(\.\d+)?(\.\d+)?{}$'.format(
_modifier_regex
),
version
)
if m:
version = f'{m.group(1)}' \
f'{m.group(2) if m.group(2) else ".0"}' \
f'{m.group(3) if m.group(3) else ".0"}' \
f'{m.group(4) if m.group(4) else ".0"}'
index = 5
else:
# Match date(time) based versioning
m = re.match(
'(?i)^v?(\d{{4}}(?:[.:-]?\d{{2}}){{1,6}}(?:[.:-]?\d{{1,3}})?){}$'.format(
_modifier_regex
),
version
)
if m:
version = re.sub('\D', '.', m.group(1))
index = 2
# add version modifiers if a version was matched
if index is not None:
if len(m.groups()) - 1 >= index and m.group(index):
version = f'{version}' \
f'-{_expand_stability(m.group(index))}'
if m.group(index + 1):
version = f'{version}.{m.group(index + 1).lstrip(".-")}'
return version
raise ValueError(f'Invalid version string "{version}"')
def normalize_stability(stability: str) -> str:
stability = stability.lower()
if stability == 'rc':
return 'RC'
return stability
def _expand_stability(stability: str) -> str:
stability = stability.lower()
if stability == 'a':
return 'alpha'
elif stability == 'b':
return 'beta'
elif stability in ['p', 'pl']:
return 'patch'
return stability
import re
from .constraints.constraint import Constraint
from .constraints.empty_constraint import EmptyConstraint
from .constraints.multi_constraint import MultiConstraint
from .helpers import normalize_version, _expand_stability
class VersionParser:
_modifier_regex = (
'[._-]?'
'(?:(stable|beta|b|RC|alpha|a|patch|pl|p)((?:[.-]?\d+)*)?)?'
'([.-]?dev)?'
)
_stabilities = [
'stable', 'RC', 'beta', 'alpha', 'dev'
]
@classmethod
def parse_stability(cls, version: str) -> str:
"""
Returns the stability of a version.
"""
version = re.sub('#.+$i', '', version)
if 'dev-' == version[:4] or '-dev' == version[-4:]:
return 'dev'
m = re.match(f'(?i){cls._modifier_regex}(?:\+.*)?$', version)
if m.group(3):
return 'dev'
if m.group(1):
if m.group(1) in ['beta', 'b']:
return 'beta'
if m.group(1) in ['alpha', 'a']:
return 'alpha'
if m.group(1) == 'rc':
return 'RC'
return 'stable'
def parse_constraints(self, constraints: str):
"""
Parses a constraint string into
MultiConstraint and/or Constraint objects.
"""
pretty_constraint = constraints
m = re.match(
'(?i)([^,\s]*?)@({})$'.format('|'.join(self._stabilities)),
constraints
)
if m:
constraints = m.group(1)
if not constraints:
constraints = '*'
or_constraints = re.split('\s*\|\|?\s*', constraints.strip())
or_groups = []
for constraints in or_constraints:
and_constraints = re.split(
'(?<!^)(?<![=>< ,]) *(?<!-)[, ](?!-) *(?!,|$)',
constraints
)
if len(and_constraints) > 1:
constraint_objects = []
for constraint in and_constraints:
for parsed_constraint in self._parse_constraint(constraint):
constraint_objects.append(parsed_constraint)
else:
constraint_objects = self._parse_constraint(and_constraints[0])
if len(constraint_objects) == 1:
constraint = constraint_objects[0]
else:
constraint = MultiConstraint(constraint_objects)
or_groups.append(constraint)
if len(or_groups) == 1:
constraint = or_groups[0]
elif len(or_groups) == 2:
# parse the two OR groups and if they are contiguous we collapse
# them into one constraint
a = str(or_groups[0])
b = str(or_groups[1])
pos_a = a.find('<', 4)
pos_b = a.find('<', 4)
if (
isinstance(or_groups[0], MultiConstraint)
and isinstance(or_groups[1], MultiConstraint)
and len(or_groups[0].constraints)
and len(or_groups[1].constraints)
and a[:3] == '[>=' and pos_a != -1
and b[:3] == '[>=' and pos_b != -1
and a[pos_a + 2:-1] == b[4:pos_b - 5]
):
constraint = MultiConstraint(
Constraint('>=', a[4:pos_a - 5]),
Constraint('<', b[pos_b + 2:-1])
)
else:
constraint = MultiConstraint(or_groups, False)
else:
constraint = MultiConstraint(or_groups, False)
constraint.pretty_string = pretty_constraint
return constraint
def _parse_constraint(self, constraint):
m = re.match('(?i)^v?[xX*](\.[xX*])*$', constraint)
if m:
return EmptyConstraint(),
version_regex = (
'v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?{}(?:\+[^\s]+)?'
).format(self._modifier_regex)
# Tilde range
#
# Like wildcard constraints, unsuffixed tilde constraints
# say that they must be greater than the previous version,
# to ensure that unstable instances of the current version are allowed.
# However, if a stability suffix is added to the constraint,
# then a >= match on the current version is used instead.
m = re.match('(?i)^~{}$'.format(version_regex), constraint)
if m:
# Work out which position in the version we are operating at
if m.group(4):
position = 3
elif m.group(3):
position = 2
elif m.group(2):
position = 1
else:
position = 0
# Calculate the stability suffix
stability_suffix = ''
if m.group(5):
stability_suffix += '-{}{}'.format(
_expand_stability(m.group(5)),
'.' + m.group(6) if m.group(6) else ''
)
low_version = self._manipulate_version_string(
m.groups(), position, 0
) + stability_suffix
lower_bound = Constraint('>=', low_version)
# For upper bound,
# we increment the position of one more significance,
# but high_position = 0 would be illegal
high_position = max(0, position - 1)
high_version = self._manipulate_version_string(
m.groups(), high_position, 1
)
upper_bound = Constraint('<', high_version)
return lower_bound, upper_bound
# Caret range
#
# Allows changes that do not modify
# the left-most non-zero digit in the [major, minor, patch] tuple.
# In other words, this allows:
# - patch and minor updatesfor versions 1.0.0 and above,
# - patch updates for versions 0.X >=0.1.0,
# - and no updates for versions 0.0.X
m = re.match('^\^{}($)'.format(version_regex), constraint)
if m:
if m.group(1) != '0' or not m.group(2):
position = 0
elif m.group(2) != '0' or not m.group(3):
position = 1
else:
position = 2
low_version = normalize_version(constraint[1:])
lower_bound = Constraint('>=', low_version)
# For upper bound,
# we increment the position of one more significance,
# but high_position = 0 would be illegal
high_version = self._manipulate_version_string(
m.groups(), position, 1
)
upper_bound = Constraint('<', high_version)
return lower_bound, upper_bound
# X range
#
# Any of X, x, or * may be used to "stand in"
# for one of the numeric values in the [major, minor, patch] tuple.
# A partial version range is treated as an X-Range,
# so the special character is in fact optional.
m = re.match(
'^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$',
constraint
)
if m:
if m.group(3):
position = 2
elif m.group(2):
position = 1
else:
position = 0
low_version = self._manipulate_version_string(
m.groups(), position
)
high_version = self._manipulate_version_string(
m.groups(), position, 1
)
if low_version == '0.0.0.0':
return Constraint('<', high_version),
return Constraint('>=', low_version), Constraint('<', high_version)
# Basic Comparators
m = re.match('^(<>|!=|>=?|<=?|==?)?\s*(.*)', constraint)
if m:
try:
version = normalize_version(m.group(2))
return Constraint(m.group(1) or '=', version),
except ValueError:
pass
raise ValueError(
'Could not parse version constraint: {}'.format(constraint)
)
def _manipulate_version_string(self, matches, position,
increment=0, pad='0'):
"""
Increment, decrement, or simply pad a version number.
"""
matches = [matches[i]
if i < len(matches) - 1 and matches[i] is not None else '0'
for i in range(4)]
for i in range(3, -1, -1):
if i > position:
matches[i] = pad
elif i == position and increment:
matches[i] = int(matches[i]) + increment
# If $matches[i] was 0, carry the decrement
if matches[i] < 0:
matches[i] = pad
position -= 1
# Return null on a carry overflow
if i == 1:
return
return '{}.{}.{}.{}'.format(matches[0], matches[1],
matches[2], matches[3])
import pytest
from poetry.semver.constraints.constraint import Constraint
@pytest.mark.parametrize(
'require_op, require_version, provide_op, provide_version',
[
('==', '1', '==', '1'),
('>=', '1', '>=', '2'),
('>=', '2', '>=', '1'),
('>=', '2', '>', '1'),
('<=', '2', '>=', '1'),
('>=', '1', '<=', '2'),
('==', '2', '>=', '2'),
('!=', '1', '!=', '1'),
('!=', '1', '==', '2'),
('!=', '1', '<', '1'),
('!=', '1', '<=', '1'),
('!=', '1', '>', '1'),
('!=', '1', '>=', '1')
]
)
def test_version_match_succeeds(require_op, require_version,
provide_op, provide_version):
require = Constraint(require_op, require_version)
provide = Constraint(provide_op, provide_version)
assert require.matches(provide)
@pytest.mark.parametrize(
'require_op, require_version, provide_op, provide_version',
[
('==', '1', '==', '2'),
('>=', '2', '<=', '1'),
('>=', '2', '<', '2'),
('<=', '2', '>', '2'),
('>', '2', '<=', '2'),
('<=', '1', '>=', '2'),
('>=', '2', '<=', '1'),
('==', '2', '<', '2'),
('!=', '1', '==', '1'),
('==', '1', '!=', '1'),
]
)
def test_version_match_fails(require_op, require_version,
provide_op, provide_version):
require = Constraint(require_op, require_version)
provide = Constraint(provide_op, provide_version)
assert not require.matches(provide)
def test_invalid_operators():
with pytest.raises(ValueError):
Constraint('invalid', '1.2.3')
from poetry.semver.constraints.constraint import Constraint
from poetry.semver.constraints.multi_constraint import MultiConstraint
def test_multi_version_match_succeeds():
require_start = Constraint('>', '1.0')
require_end = Constraint('<', '1.2')
provider = Constraint('==', '1.1')
multi = MultiConstraint((require_start, require_end))
assert multi.matches(provider)
def test_multi_version_provided_match_succeeds():
require_start = Constraint('>', '1.0')
require_end = Constraint('<', '1.2')
provide_start = Constraint('>=', '1.1')
provide_end = Constraint('<', '2.0')
multi_require = MultiConstraint((require_start, require_end))
multi_provide = MultiConstraint((provide_start, provide_end))
assert multi_require.matches(multi_provide)
def test_multi_version_match_fails():
require_start = Constraint('>', '1.0')
require_end = Constraint('<', '1.2')
provider = Constraint('==', '1.2')
multi = MultiConstraint((require_start, require_end))
assert not multi.matches(provider)
import pytest
from poetry.semver.comparison import compare
from poetry.semver.comparison import equal
from poetry.semver.comparison import greater_than
from poetry.semver.comparison import greater_than_or_equal
from poetry.semver.comparison import less_than
from poetry.semver.comparison import less_than_or_equal
from poetry.semver.comparison import not_equal
@pytest.mark.parametrize(
'version1, version2, expected',
[
('1.25.0', '1.24.0', True),
('1.25.0', '1.25.0', False),
('1.25.0', '1.26.0', False),
]
)
def test_greater_than(version1, version2, expected):
if expected is True:
assert greater_than(version1, version2)
else:
assert not greater_than(version1, version2)
@pytest.mark.parametrize(
'version1, version2, expected',
[
('1.25.0', '1.24.0', True),
('1.25.0', '1.25.0', True),
('1.25.0', '1.26.0', False),
]
)
def test_greater_than_or_equal(version1, version2, expected):
if expected is True:
assert greater_than_or_equal(version1, version2)
else:
assert not greater_than_or_equal(version1, version2)
@pytest.mark.parametrize(
'version1, version2, expected',
[
('1.25.0', '1.24.0', False),
('1.25.0', '1.25.0', False),
('1.25.0', '1.26.0', True),
('1.25.0', '1.26.0-beta', True),
('1.25.0', '1.25.0-beta', False),
]
)
def test_less_than(version1, version2, expected):
if expected is True:
assert less_than(version1, version2)
else:
assert not less_than(version1, version2)
@pytest.mark.parametrize(
'version1, version2, expected',
[
('1.25.0', '1.24.0', False),
('1.25.0', '1.25.0', True),
('1.25.0', '1.26.0', True),
]
)
def test_less_than_or_equal(version1, version2, expected):
if expected is True:
assert less_than_or_equal(version1, version2)
else:
assert not less_than_or_equal(version1, version2)
@pytest.mark.parametrize(
'version1, version2, expected',
[
('1.25.0', '1.24.0', False),
('1.25.0', '1.25.0', True),
('1.25.0', '1.26.0', False),
]
)
def test_equal(version1, version2, expected):
if expected is True:
assert equal(version1, version2)
else:
assert not equal(version1, version2)
@pytest.mark.parametrize(
'version1, version2, expected',
[
('1.25.0', '1.24.0', True),
('1.25.0', '1.25.0', False),
('1.25.0', '1.26.0', True),
]
)
def test_not_equal(version1, version2, expected):
if expected is True:
assert not_equal(version1, version2)
else:
assert not not_equal(version1, version2)
@pytest.mark.parametrize(
'version1, operator, version2, expected',
[
('1.25.0', '>', '1.24.0', True),
('1.25.0', '>', '1.25.0', False),
('1.25.0', '>', '1.26.0', False),
('1.25.0', '>=', '1.24.0', True),
('1.25.0', '>=', '1.25.0', True),
('1.25.0', '>=', '1.26.0', False),
('1.25.0', '<', '1.24.0', False),
('1.25.0', '<', '1.25.0', False),
('1.25.0', '<', '1.26.0', True),
('1.25.0-beta2.1', '<', '1.25.0-b.3', True),
('1.25.0-b2.1', '<', '1.25.0beta.3', True),
('1.25.0-b-2.1', '<', '1.25.0-rc', True),
('1.25.0', '<=', '1.24.0', False),
('1.25.0', '<=', '1.25.0', True),
('1.25.0', '<=', '1.26.0', True),
('1.25.0', '==', '1.24.0', False),
('1.25.0', '==', '1.25.0', True),
('1.25.0', '==', '1.26.0', False),
('1.25.0-beta2.1', '==', '1.25.0-b.2.1', True),
('1.25.0beta2.1', '==', '1.25.0-b2.1', True),
('1.25.0', '=', '1.24.0', False),
('1.25.0', '=', '1.25.0', True),
('1.25.0', '=', '1.26.0', False),
('1.25.0', '!=', '1.24.0', True),
('1.25.0', '!=', '1.25.0', False),
('1.25.0', '!=', '1.26.0', True),
]
)
def test_compare(version1, operator, version2, expected):
if expected is True:
assert compare(version1, operator, version2)
else:
assert not compare(version1, operator, version2)
import pytest
from poetry.semver.helpers import normalize_version
@pytest.mark.parametrize(
'version,expected',
[
('1.0.0', '1.0.0.0'),
('1.2.3.4', '1.2.3.4'),
('1.0.0RC1', '1.0.0.0-rc.1'),
('1.0.0rC13', '1.0.0.0-rc.13'),
('1.0.0.RC.15-dev', '1.0.0.0-rc.15'),
('1.0.0-rc1', '1.0.0.0-rc.1'),
('1.0.0.pl3', '1.0.0.0-patch.3'),
('1.0', '1.0.0.0'),
('0', '0.0.0.0'),
('10.4.13-b', '10.4.13.0-beta'),
('10.4.13-b5', '10.4.13.0-beta.5'),
('v1.0.0', '1.0.0.0'),
('2010.01', '2010.01.0.0'),
('2010.01.02', '2010.01.02.0'),
('v20100102', '20100102'),
('2010-01-02', '2010.01.02'),
('2010-01-02.5', '2010.01.02.5'),
('20100102-203040', '20100102.203040'),
('20100102203040-10', '20100102203040.10'),
('20100102-203040-p1', '20100102.203040-patch.1'),
('1.0.0-beta.5+foo', '1.0.0.0-beta.5'),
]
)
def test_normalize(version, expected):
assert normalize_version(version) == expected
@pytest.mark.parametrize(
'version',
[
'',
'1.0.0-meh',
'1.0.0.0.0',
'1.0.0+foo bar',
]
)
def test_normalize_fail(version):
with pytest.raises(ValueError):
normalize_version(version)
import pytest
from poetry.semver import sort, rsort, statisfies, satisfied_by
@pytest.mark.parametrize(
'version, constraint',
[
('1.2.3', '^1.2.3+build'),
('1.3.0', '^1.2.3+build'),
('1.3.0-beta', '>1.2'),
('1.2.3-beta', '<=1.2.3'),
('1.0.0', '1.0.0'),
('1.2.3', '*'),
('v1.2.3', '*'),
('1.0.0', '>=1.0.0'),
('1.0.1', '>=1.0.0'),
('1.1.0', '>=1.0.0'),
('1.0.1', '>1.0.0'),
('1.1.0', '>1.0.0'),
('2.0.0', '<=2.0.0'),
('1.9999.9999', '<=2.0.0'),
('0.2.9', '<=2.0.0'),
('1.9999.9999', '<2.0.0'),
('0.2.9', '<2.0.0'),
('1.0.0', '>= 1.0.0'),
('1.0.1', '>= 1.0.0'),
('1.1.0', '>= 1.0.0'),
('1.0.1', '> 1.0.0'),
('1.1.0', '> 1.0.0'),
('2.0.0', '<= 2.0.0'),
('1.9999.9999', '<= 2.0.0'),
('0.2.9', '<= 2.0.0'),
('1.9999.9999', '< 2.0.0'),
('0.2.9', "<\t2.0.0"),
('v0.1.97', '>=0.1.97'),
('0.1.97', '>=0.1.97'),
('1.2.4', '0.1.20 || 1.2.4'),
('0.0.0', '>=0.2.3 || <0.0.1'),
('0.2.3', '>=0.2.3 || <0.0.1'),
('0.2.4', '>=0.2.3 || <0.0.1'),
('2.1.3', '2.x.x'),
('1.2.3', '1.2.x'),
('2.1.3', '1.2.x || 2.x'),
('1.2.3', '1.2.x || 2.x'),
('1.2.3', 'x'),
('2.1.3', '2.*.*'),
('1.2.3', '1.2.*'),
('2.1.3', '1.2.* || 2.*'),
('1.2.3', '1.2.* || 2.*'),
('1.2.3', '*'),
('2.9.0', '~2.4'), # >= 2.4.0 < 3.0.0
('2.4.5', '~2.4'),
('1.2.3', '~1'), # >= 1.0.0 < 2.0.0
('1.4.7', '~1.0'), # >= 1.0.0 < 2.0.0
('1.0.0', '>=1'),
('1.0.0', '>= 1'),
('1.2.8', '>1.2'), # > 1.2.0
('1.1.1', '<1.2'), # < 1.2.0
('1.1.1', '< 1.2'),
('1.2.3', '~1.2.1 >=1.2.3'),
('1.2.3', '~1.2.1 =1.2.3'),
('1.2.3', '~1.2.1 1.2.3'),
('1.2.3', '~1.2.1 >=1.2.3 1.2.3'),
('1.2.3', '~1.2.1 1.2.3 >=1.2.3'),
('1.2.3', '~1.2.1 1.2.3'),
('1.2.3', '>=1.2.1 1.2.3'),
('1.2.3', '1.2.3 >=1.2.1'),
('1.2.3', '>=1.2.3 >=1.2.1'),
('1.2.3', '>=1.2.1 >=1.2.3'),
('1.2.8', '>=1.2'),
('1.8.1', '^1.2.3'),
('0.1.2', '^0.1.2'),
('0.1.2', '^0.1'),
('1.4.2', '^1.2'),
('1.4.2', '^1.2 ^1'),
('0.0.1-beta', '^0.0.1-alpha'),
]
)
def test_statisfies_positive(version, constraint):
assert statisfies(version, constraint)
@pytest.mark.parametrize(
'version, constraint',
[
('2.0.0', '^1.2.3+build'),
('1.2.0', '^1.2.3+build'),
('1.0.0beta', '1'),
('1.0.1', '1.0.0'),
('0.0.0', '>=1.0.0'),
('0.0.1', '>=1.0.0'),
('0.1.0', '>=1.0.0'),
('0.0.1', '>1.0.0'),
('0.1.0', '>1.0.0'),
('3.0.0', '<=2.0.0'),
('2.9999.9999', '<=2.0.0'),
('2.2.9', '<=2.0.0'),
('2.9999.9999', '<2.0.0'),
('2.2.9', '<2.0.0'),
('v0.1.93', '>=0.1.97'),
('0.1.93', '>=0.1.97'),
('1.2.3', '0.1.20 || 1.2.4'),
('0.0.3', '>=0.2.3 || <0.0.1'),
('0.2.2', '>=0.2.3 || <0.0.1'),
('1.1.3', '2.x.x'),
('3.1.3', '2.x.x'),
('1.3.3', '1.2.x'),
('3.1.3', '1.2.x || 2.x'),
('1.1.3', '1.2.x || 2.x'),
('1.1.3', '2.*.*'),
('3.1.3', '2.*.*'),
('1.3.3', '1.2.*'),
('3.1.3', '1.2.* || 2.*'),
('1.1.3', '1.2.* || 2.*'),
('1.1.2', '2'),
('2.4.1', '2.3'),
('3.0.0', '~2.4'), # >= 2.4.0 < 3.0.0
('2.3.9', '~2.4'),
('0.2.3', '~1'), # >= 1.0.0 < 2.0.0
('1.0.0', '<1'),
('1.1.1', '>=1.2'),
('2.0.0beta', '1'),
('0.5.4-alpha', '~v0.5.4-beta'),
('1.2.2', '^1.2.3'),
('1.1.9', '^1.2'),
]
)
def test_statisfies_negative(version, constraint):
assert not statisfies(version, constraint)
@pytest.mark.parametrize(
'constraint, versions, expected',
[
(
'~1.0',
['1.0', '1.2', '1.9999.9999', '2.0', '2.1', '0.9999.9999'],
['1.0', '1.2', '1.9999.9999'],
),
(
'>1.0 <3.0 || >=4.0',
['1.0', '1.1', '2.9999.9999', '3.0', '3.1', '3.9999.9999', '4.0', '4.1'],
['1.1', '2.9999.9999', '4.0', '4.1'],
),
(
'^0.2.0',
['0.1.1', '0.1.9999', '0.2.0', '0.2.1', '0.3.0'],
['0.2.0', '0.2.1'],
),
]
)
def test_satisfied_by(constraint, versions, expected):
assert satisfied_by(versions, constraint) == expected
@pytest.mark.parametrize(
'versions, sorted, rsorted',
[
(
['1.0', '0.1', '0.1', '3.2.1', '2.4.0-alpha', '2.4.0'],
['0.1', '0.1', '1.0', '2.4.0-alpha', '2.4.0', '3.2.1'],
['3.2.1', '2.4.0', '2.4.0-alpha', '1.0', '0.1', '0.1'],
)
]
)
def test_sort(versions, sorted, rsorted):
assert sort(versions) == sorted
assert rsort(versions) == rsorted
import pytest
from poetry.semver.version_parser import VersionParser
from poetry.semver.constraints.constraint import Constraint
from poetry.semver.constraints.empty_constraint import EmptyConstraint
from poetry.semver.constraints.multi_constraint import MultiConstraint
@pytest.fixture
def parser():
return VersionParser()
@pytest.mark.parametrize(
'input,constraint',
[
('*', EmptyConstraint()),
('*.*', EmptyConstraint()),
('v*.*', EmptyConstraint()),
('*.x.*', EmptyConstraint()),
('x.X.x.*', EmptyConstraint()),
('!=1.0.0', Constraint('!=', '1.0.0.0')),
('>1.0.0', Constraint('>', '1.0.0.0')),
('<1.2.3.4', Constraint('<', '1.2.3.4')),
('<=1.2.3', Constraint('<=', '1.2.3.0')),
('>=1.2.3', Constraint('>=', '1.2.3.0')),
('=1.2.3', Constraint('=', '1.2.3.0')),
('1.2.3', Constraint('=', '1.2.3.0')),
('=1.0', Constraint('=', '1.0.0.0')),
('1.2.3b5', Constraint('=', '1.2.3.0-beta.5')),
('>= 1.2.3', Constraint('>=', '1.2.3.0'))
]
)
def test_parse_constraints_simple(parser, input, constraint):
assert str(parser.parse_constraints(input)) == str(constraint)
@pytest.mark.parametrize(
'input,min,max',
[
('v2.*', Constraint('>=', '2.0.0.0'), Constraint('<', '3.0.0.0')),
('2.*.*', Constraint('>=', '2.0.0.0'), Constraint('<', '3.0.0.0')),
('20.*', Constraint('>=', '20.0.0.0'), Constraint('<', '21.0.0.0')),
('20.*.*', Constraint('>=', '20.0.0.0'), Constraint('<', '21.0.0.0')),
('2.0.*', Constraint('>=', '2.0.0.0'), Constraint('<', '2.1.0.0')),
('2.x', Constraint('>=', '2.0.0.0'), Constraint('<', '3.0.0.0')),
('2.x.x', Constraint('>=', '2.0.0.0'), Constraint('<', '3.0.0.0')),
('2.2.X', Constraint('>=', '2.2.0.0'), Constraint('<', '2.3.0.0')),
('0.*', None, Constraint('<', '1.0.0.0')),
('0.*.*', None, Constraint('<', '1.0.0.0')),
('0.x', None, Constraint('<', '1.0.0.0')),
]
)
def test_parse_constraints_wildcard(parser, input, min, max):
if min:
expected = MultiConstraint((min, max))
else:
expected = max
assert str(parser.parse_constraints(input)) == str(expected)
@pytest.mark.parametrize(
'input,min,max',
[
('~v1', Constraint('>=', '1.0.0.0'), Constraint('<', '2.0.0.0')),
('~1.0', Constraint('>=', '1.0.0.0'), Constraint('<', '2.0.0.0')),
('~1.0.0', Constraint('>=', '1.0.0.0'), Constraint('<', '1.1.0.0')),
('~1.2', Constraint('>=', '1.2.0.0'), Constraint('<', '2.0.0.0')),
('~1.2.3', Constraint('>=', '1.2.3.0'), Constraint('<', '1.3.0.0')),
('~1.2.3.4', Constraint('>=', '1.2.3.4'), Constraint('<', '1.2.4.0')),
('~1.2-beta', Constraint('>=', '1.2.0.0-beta'), Constraint('<', '2.0.0.0')),
('~1.2-b2', Constraint('>=', '1.2.0.0-beta.2'), Constraint('<', '2.0.0.0')),
]
)
def test_parse_constraints_tilde(parser, input, min, max):
if min:
expected = MultiConstraint((min, max))
else:
expected = max
assert str(parser.parse_constraints(input)) == str(expected)
@pytest.mark.parametrize(
'input,min,max',
[
('^v1', Constraint('>=', '1.0.0.0'), Constraint('<', '2.0.0.0')),
('^0', Constraint('>=', '0.0.0.0'), Constraint('<', '1.0.0.0')),
('^0.0', Constraint('>=', '0.0.0.0'), Constraint('<', '0.1.0.0')),
('^1.2', Constraint('>=', '1.2.0.0'), Constraint('<', '2.0.0.0')),
('^1.2.3-beta.2', Constraint('>=', '1.2.3.0-beta.2'), Constraint('<', '2.0.0.0')),
('^1.2.3.4', Constraint('>=', '1.2.3.4'), Constraint('<', '2.0.0.0')),
('^1.2.3', Constraint('>=', '1.2.3.0'), Constraint('<', '2.0.0.0')),
('^0.2.3', Constraint('>=', '0.2.3.0'), Constraint('<', '0.3.0.0')),
('^0.2', Constraint('>=', '0.2.0.0'), Constraint('<', '0.3.0.0')),
('^0.2.0', Constraint('>=', '0.2.0.0'), Constraint('<', '0.3.0.0')),
('^0.0.3', Constraint('>=', '0.0.3.0'), Constraint('<', '0.0.4.0')),
]
)
def test_parse_constraints_caret(parser, input, min, max):
if min:
expected = MultiConstraint((min, max))
else:
expected = max
assert str(parser.parse_constraints(input)) == str(expected)
@pytest.mark.parametrize(
'input',
[
'>2.0,<=3.0',
'>2.0 <=3.0',
'>2.0 <=3.0',
'>2.0, <=3.0',
'>2.0 ,<=3.0',
'>2.0 , <=3.0',
'>2.0 , <=3.0',
'> 2.0 <= 3.0',
'> 2.0 , <= 3.0',
' > 2.0 , <= 3.0 ',
]
)
def test_parse_constraints_multi(parser, input):
first = Constraint('>', '2.0.0.0')
second = Constraint('<=', '3.0.0.0')
multi = MultiConstraint((first, second))
assert str(parser.parse_constraints(input)) == str(multi)
@pytest.mark.parametrize(
'input',
[
'>2.0,<2.0.5 | >2.0.6',
'>2.0,<2.0.5 || >2.0.6',
'> 2.0 , <2.0.5 | > 2.0.6',
]
)
def test_parse_constraints_multi2(parser, input):
first = Constraint('>', '2.0.0.0')
second = Constraint('<', '2.0.5.0')
third = Constraint('>', '2.0.6.0')
multi1 = MultiConstraint((first, second))
multi2 = MultiConstraint((multi1, third), False)
assert str(parser.parse_constraints(input)) == str(multi2)
@pytest.mark.parametrize(
'input',
[
'',
'1.0.0-meh',
'>2.0,,<=3.0',
'>2.0 ,, <=3.0',
'>2.0 ||| <=3.0',
]
)
def test_parse_constraints_fail(parser, input):
with pytest.raises(ValueError):
parser.parse_constraints(input)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment