Commit f7d1931a by Sébastien Eustace

Merge branch 'develop'

parents 0667c67a 075d1b2e
# Change Log # Change Log
## [Unreleased]
### Added
- Added a new, more efficient dependency resolver.
- Added a new `init` command to generate a `pyproject.toml` file in existing projects.
- Added a new setting `settings.virtualenvs.in-project` to make `poetry` create the project's virtualenv inside the project's directory.
- Added the `--extras` and `--python` options to `debug:resolve` to help debug dependency resolution.
- Added a `--src` option to new to create an `src` layout.
- Added support for specifying the `platform` for dependencies.
- Added the `--python` option to the `add` command.
- Added the `--platform` option to the `add` command.
- Added a `--develop` option to the install command to install path dependencies in development/editable mode.
- Added a `develop` command to install the current project in development mode.
### Changed
- Improved the `show` command to make it easier to check if packages are properly installed.
- The `script` command has been deprecated, use `run` instead.
- The `publish` command no longer build packages by default. Use `--build` to retrieve the previous behavior.
- Improved support for private repositories.
- Expanded version constraints now keep the original version's precision.
- The lock file hash no longer use the project's name and version.
- The `LICENSE` file, or similar, is now automatically added to the built packages.
### Fixed
- Fixed the dependency resolver selecting incompatible packages.
- Fixed override of dependency with dependency with extras in `dev-dependencies`.
## [0.9.1] - 2018-05-18 ## [0.9.1] - 2018-05-18
### Fixed ### Fixed
......
...@@ -64,20 +64,20 @@ poetry self:update 0.8.0 ...@@ -64,20 +64,20 @@ poetry self:update 0.8.0
``` ```
### Enable tab completion for Bash, Fish, or Zsh ## Enable tab completion for Bash, Fish, or Zsh
`poetry` supports generating completion scripts for Bash, Fish, and Zsh. `poetry` supports generating completion scripts for Bash, Fish, and Zsh.
See `poetry help completions` for full details, but the gist is as simple as using one of the following: See `poetry help completions` for full details, but the gist is as simple as using one of the following:
```bash ```bash
# Bash # Bash
poetry completions bash > /etc/bash_completion.d/pyproject.bash-completion poetry completions bash > /etc/bash_completion.d/poetry.bash-completion
# Bash (macOS/Homebrew) # Bash (macOS/Homebrew)
poetry completions bash > $(brew --prefix)/etc/bash_completion.d/pyproject.bash-completion poetry completions bash > $(brew --prefix)/etc/bash_completion.d/poetry.bash-completion
# Fish # Fish
poetry completions fish > ~/.config/fish/completions/pyproject.fish poetry completions fish > ~/.config/fish/completions/poetry.fish
# Zsh # Zsh
poetry completions zsh > ~/.zfunc/_poetry poetry completions zsh > ~/.zfunc/_poetry
...@@ -209,6 +209,32 @@ results in : ...@@ -209,6 +209,32 @@ results in :
- Installing oslo.utils (1.4.0) - Installing oslo.utils (1.4.0)
``` ```
This is possible thanks to the efficient dependency resolver at the heart of Poetry.
Here is a breakdown of what exactly happens here:
`oslo.utils (1.4.0)` depends on:
- `pbr (>=0.6,!=0.7,<1.0)`
- `Babel (>=1.3)`
- `six (>=1.9.0)`
- `iso8601 (>=0.1.9)`
- `oslo.i18n (>=1.3.0)`
- `netaddr (>=0.7.12)`
- `netifaces (>=0.10.4)`
What interests us is `pbr (>=0.6,!=0.7,<1.0)`.
At his point, poetry will choose `pbr==0.11.1` which is the latest version that matches the constraint.
Next it will try to select `oslo.i18n==3.20.0` which is the latest version that matches `oslo.i18n (>=1.3.0)`.
However this version requires `pbr (!=2.1.0,>=2.0.0)` which is incompatible with `pbr==0.11.1`,
so `poetry` will try to find a version of `oslo.i18n` that satisfies `pbr (>=0.6,!=0.7,<1.0)`.
By analyzing the releases of `oslo.i18n`, it will find `oslo.i18n==2.1.0` which requires `pbr (>=0.11,<2.0)`.
At this point the rest of the resolution is straightforward since there is no more conflict.
#### Install command #### Install command
When you specify a package to the `install` command it will add it as a wildcard When you specify a package to the `install` command it will add it as a wildcard
...@@ -264,6 +290,24 @@ the `--name` option: ...@@ -264,6 +290,24 @@ the `--name` option:
poetry new my-folder --name my-package poetry new my-folder --name my-package
``` ```
### init
This command will help you create a `pyproject.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
poetry init
```
#### Options
* `--name`: Name of the package.
* `--description`: Description of the package.
* `--author`: Author of the package.
* `--dependency`: Package to require with a version constraint. Should be in format `foo:1.0.0`.
* `--dev-dependency`: Development requirements, see `--require`.
### install ### install
......
...@@ -65,7 +65,7 @@ It will automatically find a suitable version constraint. ...@@ -65,7 +65,7 @@ It will automatically find a suitable version constraint.
### Version constraints ### Version constraints
In our example, we are requesting the `pendulum` package with the version constraint `^1.4`. In our example, we are requesting the `pendulum` package with the version constraint `^1.4`.
This means any version geater or equal to 1.4.0 and less than 2.0.0 (`>=1.4.0 <2.0.0`). This means any version greater or equal to 1.4.0 and less than 2.0.0 (`>=1.4.0 <2.0.0`).
Please read [versions](/versions/) for more in-depth information on versions, how versions relate to each other, and on version constraints. Please read [versions](/versions/) for more in-depth information on versions, how versions relate to each other, and on version constraints.
...@@ -77,7 +77,7 @@ Please read [versions](/versions/) for more in-depth information on versions, ho ...@@ -77,7 +77,7 @@ Please read [versions](/versions/) for more in-depth information on versions, ho
When you specify a dependency in `pyproject.toml`, Poetry first take the name of the package When you specify a dependency in `pyproject.toml`, Poetry first take the name of the package
that you have requested and searches for it in any repository you have registered using the `repositories` key. that you have requested and searches for it in any repository you have registered using the `repositories` key.
If you have not registered any extra repositories, or it does not find a package with that name in the If you have not registered any extra repositories, or it does not find a package with that name in the
repositories you have specified, it falls bask on PyPI. repositories you have specified, it falls back on PyPI.
When Poetry finds the right package, it then attempts to find the best match When Poetry finds the right package, it then attempts to find the best match
for the version constraint you have specified. for the version constraint you have specified.
...@@ -143,7 +143,7 @@ and update the lock file with the new versions. ...@@ -143,7 +143,7 @@ and update the lock file with the new versions.
!!!note !!!note
Poetry will display a Warning when executing an install command if `pyproject.lock` and `pyproject.toml` Poetry will display a **Warning** when executing an install command if `pyproject.lock` and `pyproject.toml`
are not synchronized. are not synchronized.
...@@ -159,5 +159,5 @@ or create a brand new one for you to always work isolated from your global Pytho ...@@ -159,5 +159,5 @@ or create a brand new one for you to always work isolated from your global Pytho
`poetry` has been installed. `poetry` has been installed.
What this means is if you project is Python 2.7 only you should What this means is if you project is Python 2.7 only you should
install `poetry` for you global Python 2.7 executable and use install `poetry` for your global Python 2.7 executable and use
it to manage your project. it to manage your project.
...@@ -54,6 +54,26 @@ the `--name` option: ...@@ -54,6 +54,26 @@ the `--name` option:
poetry new my-folder --name my-package poetry new my-folder --name my-package
``` ```
## init
This command will help you create a `pyproject.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
poetry init
```
### Options
* `--name`: Name of the package.
* `--description`: Description of the package.
* `--author`: Author of the package.
* `--dependency`: Package to require with a version constraint. Should be in format `foo:1.0.0`.
* `--dev-dependency`: Development requirements, see `--require`.
## install ## install
The `install` command reads the `pyproject.toml` file from the current project, The `install` command reads the `pyproject.toml` file from the current project,
...@@ -206,7 +226,7 @@ Note that, at the moment, only pure python wheels are supported. ...@@ -206,7 +226,7 @@ Note that, at the moment, only pure python wheels are supported.
## publish ## publish
This command builds (if not already built) and publishes the package to the remote repository. This command publishes the package, previously built with the [`build`](#build) command, to the remote repository.
It will automatically register the package before uploading if this is the first time it is submitted. It will automatically register the package before uploading if this is the first time it is submitted.
...@@ -214,6 +234,8 @@ It will automatically register the package before uploading if this is the first ...@@ -214,6 +234,8 @@ It will automatically register the package before uploading if this is the first
poetry publish poetry publish
``` ```
It can also build the package if you pass it the `--build` option.
### Options ### Options
* `--repository (-r)`: The repository to register the package to (default: `pypi`). * `--repository (-r)`: The repository to register the package to (default: `pypi`).
...@@ -269,11 +291,7 @@ The `run` command executes the given command inside the project's virtualenv. ...@@ -269,11 +291,7 @@ The `run` command executes the given command inside the project's virtualenv.
poetry run python -V poetry run python -V
``` ```
Note that this command has no option. It can also executes one of the scripts defined in `pyproject.toml`.
## script
The `script` executes one of the scripts defined in `pyproject.toml`.
So, if you have a script defined like this: So, if you have a script defined like this:
...@@ -285,7 +303,7 @@ my-script = "my_module:main" ...@@ -285,7 +303,7 @@ my-script = "my_module:main"
You can execute it like so: You can execute it like so:
```bash ```bash
poetry script my-script poetry run my-script
``` ```
Note that this command has no option. Note that this command has no option.
......
# FAQ
## Why is the dependency resolution process slow?
While the dependency resolver at the heart of Poetry is highly optimized and
should be fast enough for most cases, sometimes, with some specific set of dependencies,
it can take time to find a valid solution.
This is due to the fact that not all libraries on PyPI have properly declared their metadata
and, as such, they are not available via the PyPI JSON API. At this point, Poetry has no choice
but downloading the packages and inspect them to get the necessary information. This is an expensive
operation, both in bandwidth and time, which is why it seems this is a long process.
At the moment there is not way around it.
!!!note
Once Poetry has cached the releases' information, the dependency resolution process
will be much faster.
## Why are unbound version constraints a bad idea?
A version constraint without an upper bound such as `*` or `>=3.4` will allow updates to any future version of the dependency.
This includes major versions breaking backward compatibility.
Once a release of your package is published, you cannot tweak its dependencies anymore in case a dependency breaks BC
- you have to do a new release but the previous one stays broken.
The only good alternative is to define an upper bound on your constraints,
which you can increase in a new release after testing that your package is compatible
with the new major version of your dependency.
For example instead of using `>=3.4` you should use `~3.4` which allows all versions `<4.0`.
The `^` operator works very well with libraries following [semantic versioning](https://semver.org).
...@@ -60,9 +60,40 @@ If you want to install prerelease versions, you can use the `--preview` option. ...@@ -60,9 +60,40 @@ If you want to install prerelease versions, you can use the `--preview` option.
poetry self:update --preview poetry self:update --preview
``` ```
And finally, if you want to install a spcific version you can pass it as an argument And finally, if you want to install a specific version you can pass it as an argument
to `self:update`. to `self:update`.
```bash ```bash
poetry self:update 0.8.0 poetry self:update 0.8.0
``` ```
## Enable tab completion for Bash, Fish, or Zsh
`poetry` supports generating completion scripts for Bash, Fish, and Zsh.
See `poetry help completions` for full details, but the gist is as simple as using one of the following:
```bash
# Bash
poetry completions bash > /etc/bash_completion.d/poetry.bash-completion
# Bash (macOS/Homebrew)
poetry completions bash > $(brew --prefix)/etc/bash_completion.d/poetry.bash-completion
# Fish
poetry completions fish > ~/.config/fish/completions/poetry.fish
# Zsh
poetry completions zsh > ~/.zfunc/_poetry
```
!!! 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`:
```bash
fpath+=~/.zfunc
```
...@@ -108,6 +108,13 @@ my-package = { path = "../my-package/" } ...@@ -108,6 +108,13 @@ my-package = { path = "../my-package/" }
my-package = { path = "../my-package/dist/my-package-0.1.0.tar.gz" } my-package = { path = "../my-package/dist/my-package-0.1.0.tar.gz" }
``` ```
!!!note
You can install path dependencies in editable/development mode.
Just pass `--develop my-package` (repeatable as much as you want) to
the `install` command.
### Python restricted dependencies ### Python restricted dependencies
You can also specify that a dependency should be installed only for specific Python versions: You can also specify that a dependency should be installed only for specific Python versions:
......
...@@ -15,6 +15,7 @@ pages: ...@@ -15,6 +15,7 @@ pages:
- Repositories: repositories.md - Repositories: repositories.md
- Versions: versions.md - Versions: versions.md
- The pyproject.toml file: pyproject.md - The pyproject.toml file: pyproject.md
- FAQ: faq.md
markdown_extensions: markdown_extensions:
- codehilite - codehilite
......
...@@ -27,6 +27,7 @@ import tempfile ...@@ -27,6 +27,7 @@ import tempfile
from contextlib import contextmanager from contextlib import contextmanager
from email.parser import Parser from email.parser import Parser
from functools import cmp_to_key
from glob import glob from glob import glob
try: try:
...@@ -88,7 +89,7 @@ def style(fg, bg, options): ...@@ -88,7 +89,7 @@ def style(fg, bg, options):
STYLES = { STYLES = {
'info': style('green', None, None), 'info': style('green', None, None),
'comment': style('yellow', None, None), 'comment': style('yellow', None, None),
'error': style('white', 'red', None) 'error': style('red', None, None)
} }
...@@ -102,6 +103,7 @@ def colorize(style, text): ...@@ -102,6 +103,7 @@ def colorize(style, text):
return '{}{}\033[0m'.format(STYLES[style], text) return '{}{}\033[0m'.format(STYLES[style], text)
@contextmanager @contextmanager
def temporary_directory(*args, **kwargs): def temporary_directory(*args, **kwargs):
try: try:
...@@ -145,13 +147,24 @@ class Installer: ...@@ -145,13 +147,24 @@ class Installer:
metadata = json.loads(r.read().decode()) metadata = json.loads(r.read().decode())
r.close() r.close()
def _compare_versions(x, y):
mx = self.VERSION_REGEX.match(x)
my = self.VERSION_REGEX.match(y)
vx = tuple(int(p) for p in mx.groups()[:3]) + (mx.group(5),)
vy = tuple(int(p) for p in my.groups()[:3]) + (my.group(5),)
if vx < vy:
return -1
elif vx > vy:
return 1
return 0
print('') print('')
releases = sorted( releases = sorted(
metadata['releases'].keys(), metadata['releases'].keys(),
key=lambda r: ( key=cmp_to_key(_compare_versions)
'.'.join(self.VERSION_REGEX.match(r).groups()[:3]),
self.VERSION_REGEX.match(r).group(5)
)
) )
if self._version and self._version not in releases: if self._version and self._version not in releases:
...@@ -188,7 +201,7 @@ class Installer: ...@@ -188,7 +201,7 @@ class Installer:
return self.install(version) return self.install(version)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print(colorize('error', 'An error has occured: {}'.format(str(e)))) print(colorize('error', 'An error has occured: {}'.format(str(e))))
print(e.output) print(e.output.decode())
return e.returncode return e.returncode
...@@ -197,10 +210,30 @@ class Installer: ...@@ -197,10 +210,30 @@ class Installer:
with temporary_directory(prefix='poetry-installer-') as dir: with temporary_directory(prefix='poetry-installer-') as dir:
dist = os.path.join(dir, 'dist') dist = os.path.join(dir, 'dist')
print(' - Getting dependencies') print(' - Getting dependencies')
self.call( try:
self.CURRENT_PYTHON, '-m', 'pip', 'install', 'poetry=={}'.format(version), self.call(
'--target', dist self.CURRENT_PYTHON, '-m', 'pip', 'install', 'poetry=={}'.format(version),
) '--target', dist
)
except subprocess.CalledProcessError as e:
if 'must supply either home or prefix/exec-prefix' in e.output.decode():
# Homebrew Python and possible other installations
# We workaround this issue by temporarily changing
# the --user directory
os.environ['PYTHONUSERBASE'] = dir
self.call(
self.CURRENT_PYTHON, '-m', 'pip', 'install', 'poetry=={}'.format(version),
'--user',
'--ignore-installed'
)
# Finding site-package directory
lib = os.path.join(dir, 'lib')
lib_python = list(glob(os.path.join(lib, 'python*')))[0]
site_packages = os.path.join(lib_python, 'site-packages')
shutil.copytree(site_packages, dist)
else:
raise
print(' - Vendorizing dependencies') print(' - Vendorizing dependencies')
...@@ -210,7 +243,10 @@ class Installer: ...@@ -210,7 +243,10 @@ class Installer:
# Everything, except poetry itself, should # Everything, except poetry itself, should
# be put in the _vendor directory # be put in the _vendor directory
for file in glob(os.path.join(dist, '*')): for file in glob(os.path.join(dist, '*')):
if os.path.basename(file).startswith('poetry'): if (
os.path.basename(file).startswith('poetry')
or os.path.basename(file) == '__pycache__'
):
continue continue
dest = os.path.join(vendor_dir, os.path.basename(file)) dest = os.path.join(vendor_dir, os.path.basename(file))
......
__version__ = '0.9.1' __version__ = '0.10.0-alpha.3'
...@@ -18,6 +18,8 @@ from .commands import AddCommand ...@@ -18,6 +18,8 @@ from .commands import AddCommand
from .commands import BuildCommand from .commands import BuildCommand
from .commands import CheckCommand from .commands import CheckCommand
from .commands import ConfigCommand from .commands import ConfigCommand
from .commands import DevelopCommand
from .commands import InitCommand
from .commands import InstallCommand from .commands import InstallCommand
from .commands import LockCommand from .commands import LockCommand
from .commands import NewCommand from .commands import NewCommand
...@@ -106,6 +108,8 @@ class Application(BaseApplication): ...@@ -106,6 +108,8 @@ class Application(BaseApplication):
BuildCommand(), BuildCommand(),
CheckCommand(), CheckCommand(),
ConfigCommand(), ConfigCommand(),
DevelopCommand(),
InitCommand(),
InstallCommand(), InstallCommand(),
LockCommand(), LockCommand(),
NewCommand(), NewCommand(),
......
...@@ -3,6 +3,8 @@ from .add import AddCommand ...@@ -3,6 +3,8 @@ from .add import AddCommand
from .build import BuildCommand from .build import BuildCommand
from .check import CheckCommand from .check import CheckCommand
from .config import ConfigCommand from .config import ConfigCommand
from .develop import DevelopCommand
from .init import InitCommand
from .install import InstallCommand from .install import InstallCommand
from .lock import LockCommand from .lock import LockCommand
from .new import NewCommand from .new import NewCommand
......
import re from .init import InitCommand
from typing import List
from typing import Tuple
from .venv_command import VenvCommand from .venv_command import VenvCommand
class AddCommand(VenvCommand): class AddCommand(VenvCommand, InitCommand):
""" """
Add a new dependency to <comment>pyproject.toml</>. Add a new dependency to <comment>pyproject.toml</>.
...@@ -17,6 +13,8 @@ class AddCommand(VenvCommand): ...@@ -17,6 +13,8 @@ class AddCommand(VenvCommand):
{ --path= : The path to a dependency. } { --path= : The path to a dependency. }
{ --E|extras=* : Extras to activate for the dependency. } { --E|extras=* : Extras to activate for the dependency. }
{ --optional : Add as an optional dependency. } { --optional : Add as an optional dependency. }
{ --python= : Python version( for which the dependencies must be installed. }
{ --platform= : Platforms for which the dependencies must be installed. }
{ --allow-prereleases : Accept prereleases. } { --allow-prereleases : Accept prereleases. }
{ --dry-run : Outputs the operations but will not execute anything { --dry-run : Outputs the operations but will not execute anything
(implicitly enables --verbose). } (implicitly enables --verbose). }
...@@ -33,7 +31,7 @@ If you do not specify a version constraint, poetry will choose a suitable one ba ...@@ -33,7 +31,7 @@ If you do not specify a version constraint, poetry will choose a suitable one ba
def handle(self): def handle(self):
from poetry.installation import Installer from poetry.installation import Installer
from poetry.semver.version_parser import VersionParser from poetry.semver import parse_constraint
packages = self.argument('name') packages = self.argument('name')
is_dev = self.option('dev') is_dev = self.option('dev')
...@@ -76,9 +74,8 @@ If you do not specify a version constraint, poetry will choose a suitable one ba ...@@ -76,9 +74,8 @@ If you do not specify a version constraint, poetry will choose a suitable one ba
requirements = self._format_requirements(requirements) requirements = self._format_requirements(requirements)
# validate requirements format # validate requirements format
parser = VersionParser()
for constraint in requirements.values(): for constraint in requirements.values():
parser.parse_constraints(constraint) parse_constraint(constraint)
for name, constraint in requirements.items(): for name, constraint in requirements.items():
constraint = { constraint = {
...@@ -101,8 +98,21 @@ If you do not specify a version constraint, poetry will choose a suitable one ba ...@@ -101,8 +98,21 @@ If you do not specify a version constraint, poetry will choose a suitable one ba
constraint['allows-prereleases'] = True constraint['allows-prereleases'] = True
if self.option('extras'): if self.option('extras'):
extras = []
for extra in self.option('extras'):
if ' ' in extra:
extras += [e.strip() for e in extra.split(' ')]
else:
extras.append(extra)
constraint['extras'] = self.option('extras') constraint['extras'] = self.option('extras')
if self.option('python'):
constraint['python'] = self.option('python')
if self.option('platform'):
constraint['platform'] = self.option('platform')
if len(constraint) == 1 and 'version' in constraint: if len(constraint) == 1 and 'version' in constraint:
constraint = constraint['version'] constraint = constraint['version']
...@@ -148,94 +158,3 @@ If you do not specify a version constraint, poetry will choose a suitable one ba ...@@ -148,94 +158,3 @@ If you do not specify a version constraint, poetry will choose a suitable one ba
self.poetry.file.write(original_content) self.poetry.file.write(original_content)
return status return status
def _determine_requirements(self,
requires, # type: List[str]
allow_prereleases=False, # type: bool
): # type: (...) -> List[str]
if not requires:
return []
requires = self._parse_name_version_pairs(requires)
result = []
for requirement in requires:
if 'version' not in requirement:
# determine the best version automatically
name, version = self._find_best_version_for_package(
requirement['name'],
allow_prereleases=allow_prereleases
)
requirement['version'] = version
requirement['name'] = name
self.line(
'Using version <info>{}</> for <info>{}</>'
.format(version, name)
)
else:
# check that the specified version/constraint exists
# before we proceed
name, _ = self._find_best_version_for_package(
requirement['name'], requirement['version'],
allow_prereleases=allow_prereleases
)
requirement['name'] = name
result.append(
'{} {}'.format(requirement['name'], requirement['version'])
)
return result
def _find_best_version_for_package(self,
name,
required_version=None,
allow_prereleases=False
): # type: (...) -> Tuple[str, str]
from poetry.version.version_selector import VersionSelector
selector = VersionSelector(self.poetry.pool)
package = selector.find_best_candidate(
name, required_version,
allow_prereleases=allow_prereleases
)
if not package:
# TODO: find similar
raise ValueError(
'Could not find a matching version of package {}'.format(name)
)
return (
package.pretty_name,
selector.find_recommended_require_version(package)
)
def _parse_name_version_pairs(self, pairs): # type: (list) -> list
result = []
for i in range(len(pairs)):
pair = re.sub('^([^=: ]+)[=: ](.*)$', '\\1 \\2', pairs[i].strip())
pair = pair.strip()
if ' ' in pair:
name, version = pair.split(' ', 2)
result.append({
'name': name,
'version': version
})
else:
result.append({
'name': pair
})
return result
def _format_requirements(self, requirements): # type: (List[str]) -> dict
requires = {}
requirements = self._parse_name_version_pairs(requirements)
for requirement in requirements:
requires[requirement['name']] = requirement['version']
return requires
...@@ -101,6 +101,7 @@ To remove a repository (repo is a short alias for repositories): ...@@ -101,6 +101,7 @@ To remove a repository (repo is a short alias for repositories):
unique_config_values = { unique_config_values = {
'settings.virtualenvs.create': (boolean_validator, boolean_normalizer), 'settings.virtualenvs.create': (boolean_validator, boolean_normalizer),
'settings.virtualenvs.in-project': (boolean_validator, boolean_normalizer),
'settings.pypi.fallback': (boolean_validator, boolean_normalizer), 'settings.pypi.fallback': (boolean_validator, boolean_normalizer),
} }
......
...@@ -11,6 +11,8 @@ class DebugResolveCommand(Command): ...@@ -11,6 +11,8 @@ class DebugResolveCommand(Command):
debug:resolve debug:resolve
{ package?* : packages to resolve. } { package?* : packages to resolve. }
{ --E|extras=* : Extras to activate for the dependency. }
{ --python= : Python version(s) to use for resolution. }
""" """
_loggers = [ _loggers = [
...@@ -19,39 +21,56 @@ class DebugResolveCommand(Command): ...@@ -19,39 +21,56 @@ class DebugResolveCommand(Command):
def handle(self): def handle(self):
from poetry.packages import Dependency from poetry.packages import Dependency
from poetry.packages import ProjectPackage
from poetry.puzzle import Solver from poetry.puzzle import Solver
from poetry.repositories.repository import Repository from poetry.repositories.repository import Repository
from poetry.semver.version_parser import VersionParser from poetry.semver import parse_constraint
packages = self.argument('package') packages = self.argument('package')
if not packages: if not packages:
package = self.poetry.package package = self.poetry.package
dependencies = package.requires + package.dev_requires
else: else:
requirements = self._determine_requirements(packages) requirements = self._determine_requirements(packages)
requirements = self._format_requirements(requirements) requirements = self._format_requirements(requirements)
# validate requirements format # validate requirements format
parser = VersionParser()
for constraint in requirements.values(): for constraint in requirements.values():
parser.parse_constraints(constraint) parse_constraint(constraint)
dependencies = [] dependencies = []
for name, constraint in requirements.items(): for name, constraint in requirements.items():
dependencies.append( dep = Dependency(name, constraint)
Dependency(name, constraint) extras = []
) for extra in self.option('extras'):
if ' ' in extra:
extras += [e.strip() for e in extra.split(' ')]
else:
extras.append(extra)
for ex in extras:
dep.extras.append(ex)
dependencies.append(dep)
package = ProjectPackage(
self.poetry.package.name,
self.poetry.package.version
)
package.python_versions = self.option('python') or self.poetry.package.python_versions
for dep in dependencies:
package.requires.append(dep)
solver = Solver( solver = Solver(
self.poetry.package, package,
self.poetry.pool, self.poetry.pool,
Repository(), Repository(),
Repository(), Repository(),
self.output self.output
) )
ops = solver.solve(dependencies) ops = solver.solve()
self.line('') self.line('')
self.line('Resolution results:') self.line('Resolution results:')
......
import os
from .venv_command import VenvCommand
class DevelopCommand(VenvCommand):
"""
Installs the current project in development mode.
develop
"""
help = """\
The <info>develop</info> command installs the current project in development mode.
"""
def handle(self):
from poetry.masonry.builders import SdistBuilder
from poetry.io import NullIO
from poetry.utils._compat import decode
from poetry.utils.venv import NullVenv
setup = self.poetry.file.parent / 'setup.py'
has_setup = setup.exists()
if has_setup:
self.line('<warning>A setup.py file already exists. Using it.</warning>')
else:
builder = SdistBuilder(self.poetry, NullVenv(), NullIO())
with setup.open('w') as f:
f.write(decode(builder.build_setup()))
try:
self._install(setup)
finally:
if not has_setup:
os.remove(str(setup))
def _install(self, setup):
self.call('install')
self.line(
'Installing <info>{}</info> (<comment>{}</comment>)'.format(
self.poetry.package.pretty_name,
self.poetry.package.pretty_version
)
)
self.venv.run('pip', 'install', '-e', str(setup.parent), '--no-deps')
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import re
from typing import List
from typing import Tuple
from .command import Command
class InitCommand(Command):
"""
Creates a basic <comment>pyproject.toml</> file in the current directory.
init
{--name= : Name of the package}
{--description= : Description of the package}
{--author= : Author name of the package}
{--dependency=* : Package to require with an optional version constraint,
e.g. requests:^2.10.0 or requests=2.11.1}
{--dev-dependency=* : Package to require for development with an optional version constraint,
e.g. requests:^2.10.0 or requests=2.11.1}
{--l|license= : License of the package}
"""
help = """\
The <info>init</info> command creates a basic <comment>pyproject.toml</> file in the current directory.
"""
def __init__(self):
super(InitCommand, self).__init__()
self.pool = None
def handle(self):
from poetry.layouts import layout
from poetry.utils._compat import Path
from poetry.vcs.git import GitConfig
if (Path.cwd() / 'pyproject.toml').exists():
self.error('A pyproject.toml file already exists.')
return 1
vcs_config = GitConfig()
self.line([
'',
'This command will guide you through creating your <info>poetry.toml</> config.',
''
])
name = self.option('name')
if not name:
name = Path.cwd().name.lower()
question = self.create_question(
'Package name [<comment>{}</comment>]: '.format(name),
default=name
)
name = self.ask(question)
version = '0.1.0'
question = self.create_question(
'Version [<comment>{}</comment>]: '.format(version),
default=version
)
version = self.ask(question)
description = self.option('description') or ''
question = self.create_question(
'Description [<comment>{}</comment>]: '.format(description),
default=description
)
description = self.ask(question)
author = self.option('author')
if not author and vcs_config and vcs_config.get('user.name'):
author = vcs_config['user.name']
author_email = vcs_config.get('user.email')
if author_email:
author += ' <{}>'.format(author_email)
question = self.create_question(
'Author [<comment>{}</comment>, n to skip]: '.format(author),
default=author
)
question.validator = lambda v: self._validate_author(v, author)
author = self.ask(question)
if not author:
authors = []
else:
authors = [author]
license = self.option('license') or ''
question = self.create_question(
'License [<comment>{}</comment>]: '.format(license),
default=license
)
license = self.ask(question)
question = self.create_question(
'Compatible Python versions [*]: ',
default='*'
)
python = self.ask(question)
self.line('')
requirements = []
question = 'Would you like to define your dependencies' \
' (require) interactively?'
if self.confirm(question, True):
requirements = self._format_requirements(
self._determine_requirements(self.option('dependency'))
)
dev_requirements = []
question = 'Would you like to define your dev dependencies' \
' (require-dev) interactively'
if self.confirm(question, True):
dev_requirements = self._format_requirements(
self._determine_requirements(self.option('dev-dependency'))
)
layout_ = layout('standard')(
name,
version,
description=description,
author=authors[0] if authors else None,
license=license,
python=python,
dependencies=requirements,
dev_dependencies=dev_requirements
)
content = layout_.generate_poetry_content()
if self.input.is_interactive():
self.line('<info>Generated file</info>')
self.line(['', content, ''])
if not self.confirm('Do you confirm generation?', True):
self.line('<error>Command aborted</error>')
return 1
with (Path.cwd() / 'pyproject.toml').open('w') as f:
f.write(content)
def _determine_requirements(self,
requires, # type: List[str]
allow_prereleases=False, # type: bool
): # type: (...) -> List[str]
if not requires:
requires = []
package = self.ask('Search for package:')
while package is not None:
matches = self._get_pool().search(package)
if not matches:
self.line('<error>Unable to find package</error>')
package = False
else:
choices = []
for found_package in matches:
choices.append(found_package.pretty_name)
self.line(
'Found <info>{}</info> packages matching <info>{}</info>'
.format(
len(matches),
package
)
)
package = self.choice(
'\nEnter package # to add, or the complete package name if it is not listed',
choices,
attempts=3
)
# no constraint yet, determine the best version automatically
if package is not False and ' ' not in package:
question = self.create_question(
'Enter the version constraint to require '
'(or leave blank to use the latest version):'
)
question.attempts = 3
question.validator = lambda x: (x or '').strip() or False
constraint = self.ask(question)
if constraint is False:
_, constraint = self._find_best_version_for_package(package)
self.line(
'Using version <info>{}</info> for <info>{}</info>'
.format(constraint, package)
)
package += ' {}'.format(constraint)
if package is not False:
requires.append(package)
package = self.ask('\nSearch for a package:')
return requires
requires = self._parse_name_version_pairs(requires)
result = []
for requirement in requires:
if 'version' not in requirement:
# determine the best version automatically
name, version = self._find_best_version_for_package(
requirement['name'],
allow_prereleases=allow_prereleases
)
requirement['version'] = version
requirement['name'] = name
self.line(
'Using version <info>{}</> for <info>{}</>'
.format(version, name)
)
else:
# check that the specified version/constraint exists
# before we proceed
name, _ = self._find_best_version_for_package(
requirement['name'], requirement['version'],
allow_prereleases=allow_prereleases
)
requirement['name'] = name
result.append(
'{} {}'.format(requirement['name'], requirement['version'])
)
return result
def _find_best_version_for_package(self,
name,
required_version=None,
allow_prereleases=False
): # type: (...) -> Tuple[str, str]
from poetry.version.version_selector import VersionSelector
selector = VersionSelector(self._get_pool())
package = selector.find_best_candidate(
name, required_version,
allow_prereleases=allow_prereleases
)
if not package:
# TODO: find similar
raise ValueError(
'Could not find a matching version of package {}'.format(name)
)
return (
package.pretty_name,
selector.find_recommended_require_version(package)
)
def _parse_name_version_pairs(self, pairs): # type: (list) -> list
result = []
for i in range(len(pairs)):
pair = re.sub('^([^=: ]+)[=: ](.*)$', '\\1 \\2', pairs[i].strip())
pair = pair.strip()
if ' ' in pair:
name, version = pair.split(' ', 2)
result.append({
'name': name,
'version': version
})
else:
result.append({
'name': pair
})
return result
def _format_requirements(self, requirements): # type: (List[str]) -> dict
requires = {}
requirements = self._parse_name_version_pairs(requirements)
for requirement in requirements:
requires[requirement['name']] = requirement['version']
return requires
def _validate_author(self, author, default):
from poetry.packages.package import AUTHOR_REGEX
author = author or default
if author in ['n', 'no']:
return
m = AUTHOR_REGEX.match(author)
if not m:
raise ValueError(
'Invalid author string. Must be in the format: '
'John Smith <john@example.com>'
)
return author
def _get_pool(self):
if self.pool is None:
self.pool = self.poetry.pool
return self.pool
...@@ -10,6 +10,7 @@ class InstallCommand(VenvCommand): ...@@ -10,6 +10,7 @@ class InstallCommand(VenvCommand):
{ --dry-run : Outputs the operations but will not execute anything { --dry-run : Outputs the operations but will not execute anything
(implicitly enables --verbose). } (implicitly enables --verbose). }
{ --E|extras=* : Extra sets of dependencies to install. } { --E|extras=* : Extra sets of dependencies to install. }
{ --develop=* : Install given packages in development mode. }
""" """
help = """The <info>install</info> command reads the <comment>pyproject.toml</> file from help = """The <info>install</info> command reads the <comment>pyproject.toml</> file from
...@@ -35,8 +36,16 @@ exist it will look for <comment>pyproject.toml</> and do the same. ...@@ -35,8 +36,16 @@ exist it will look for <comment>pyproject.toml</> and do the same.
self.poetry.pool self.poetry.pool
) )
installer.extras(self.option('extras')) extras = []
for extra in self.option('extras'):
if ' ' in extra:
extras += [e.strip() for e in extra.split(' ')]
else:
extras.append(extra)
installer.extras(extras)
installer.dev_mode(not self.option('no-dev')) installer.dev_mode(not self.option('no-dev'))
installer.develop(self.option('develop'))
installer.dry_run(self.option('dry-run')) installer.dry_run(self.option('dry-run'))
installer.verbose(self.option('verbose')) installer.verbose(self.option('verbose'))
......
from poetry.utils._compat import Path
from .command import Command from .command import Command
...@@ -10,12 +8,18 @@ class NewCommand(Command): ...@@ -10,12 +8,18 @@ class NewCommand(Command):
new new
{ path : The path to create the project at. } { path : The path to create the project at. }
{ --name : Set the resulting package name. } { --name : Set the resulting package name. }
{ --src : Use the src layout for the project. }
""" """
def handle(self): def handle(self):
from poetry.layouts import layout from poetry.layouts import layout
from poetry.utils._compat import Path
from poetry.vcs.git import GitConfig
layout_ = layout('standard') if self.option('src'):
layout_ = layout('src')
else:
layout_ = layout('standard')
path = Path.cwd() / Path(self.argument('path')) path = Path.cwd() / Path(self.argument('path'))
name = self.option('name') name = self.option('name')
...@@ -34,7 +38,15 @@ class NewCommand(Command): ...@@ -34,7 +38,15 @@ class NewCommand(Command):
readme_format = 'rst' readme_format = 'rst'
layout_ = layout_(name, '0.1.0', readme_format=readme_format) config = GitConfig()
author = None
if config.get('user.name'):
author = config['user.name']
author_email = config.get('user.email')
if author_email:
author += ' <{}>'.format(author_email)
layout_ = layout_(name, '0.1.0', author=author, readme_format=readme_format)
layout_.create(path) layout_.create(path)
self.line( self.line(
......
...@@ -9,7 +9,7 @@ class PublishCommand(Command): ...@@ -9,7 +9,7 @@ class PublishCommand(Command):
{ --r|repository= : The repository to publish the package to. } { --r|repository= : The repository to publish the package to. }
{ --u|username= : The username to access the repository. } { --u|username= : The username to access the repository. }
{ --p|password= : The password to access the repository. } { --p|password= : The password to access the repository. }
{ --no-build : Do not build the package before publishing. } { --build : Build the package before publishing. }
""" """
help = """The publish command builds and uploads the package to a remote repository. help = """The publish command builds and uploads the package to a remote repository.
...@@ -24,13 +24,32 @@ the config command. ...@@ -24,13 +24,32 @@ the config command.
def handle(self): def handle(self):
from poetry.masonry.publishing.publisher import Publisher from poetry.masonry.publishing.publisher import Publisher
# Building package first, unless told otherwise publisher = Publisher(self.poetry, self.output)
if not self.option('no-build'):
# Building package first, if told
if self.option('build'):
if publisher.files:
if not self.confirm(
'There are <info>{}</info> files ready for publishing. '
'Build anyway?'.format(len(publisher.files))
):
self.line_error('<error>Aborted!</error>')
return 1
self.call('build') self.call('build')
files = publisher.files
if not files:
self.line_error(
'<error>No files to publish. '
'Run poetry build first or use the --build option.</error>'
)
return 1
self.line('') self.line('')
publisher = Publisher(self.poetry, self.output)
publisher.publish( publisher.publish(
self.option('repository'), self.option('repository'),
self.option('username'), self.option('username'),
......
...@@ -11,11 +11,47 @@ class RunCommand(VenvCommand): ...@@ -11,11 +11,47 @@ class RunCommand(VenvCommand):
def handle(self): def handle(self):
args = self.argument('args') args = self.argument('args')
script = args[0]
scripts = self.poetry.local_config.get('scripts')
if scripts and script in scripts:
return self.run_script(scripts[script], args)
venv = self.venv venv = self.venv
return venv.execute(*args) return venv.execute(*args)
def run_script(self, script, args):
module, callable_ = script.split(':')
src_in_sys_path = 'sys.path.append(\'src\'); ' \
if self._module.is_in_src() else ''
cmd = ['python', '-c']
cmd += [
'"import sys; '
'from importlib import import_module; '
'sys.argv = {!r}; {}'
'import_module(\'{}\').{}()"'.format(
args, src_in_sys_path, module, callable_
)
]
return self.venv.run(*cmd, shell=True, call=True)
@property
def _module(self):
from ...masonry.utils.module import Module
poetry = self.poetry
package = poetry.package
path = poetry.file.parent
module = Module(
package.name, path.as_posix()
)
return module
def merge_application_definition(self, merge_args=True): def merge_application_definition(self, merge_args=True):
if self._application is None \ if self._application is None \
or (self._application_definition_merged or (self._application_definition_merged
......
import sys
from ...masonry.utils.module import Module
from .venv_command import VenvCommand from .venv_command import VenvCommand
class ScriptCommand(VenvCommand): class ScriptCommand(VenvCommand):
""" """
Executes a script defined in <comment>pyproject.toml</comment> Executes a script defined in <comment>pyproject.toml</comment>. (<error>Deprecated</error>)
script script
{ script-name : The name of the script to execute } { script-name : The name of the script to execute }
...@@ -14,6 +11,9 @@ class ScriptCommand(VenvCommand): ...@@ -14,6 +11,9 @@ class ScriptCommand(VenvCommand):
""" """
def handle(self): def handle(self):
self.line('<warning>script is deprecated use run instead.</warning>')
self.line('')
script = self.argument('script-name') script = self.argument('script-name')
argv = [script] + self.argument('args') argv = [script] + self.argument('args')
...@@ -44,6 +44,8 @@ class ScriptCommand(VenvCommand): ...@@ -44,6 +44,8 @@ class ScriptCommand(VenvCommand):
@property @property
def _module(self): def _module(self):
from ...masonry.utils.module import Module
poetry = self.poetry poetry = self.poetry
package = poetry.package package = poetry.package
path = poetry.file.parent path = poetry.file.parent
......
...@@ -22,7 +22,6 @@ class SelfUpdateCommand(Command): ...@@ -22,7 +22,6 @@ class SelfUpdateCommand(Command):
def handle(self): def handle(self):
from poetry.__version__ import __version__ from poetry.__version__ import __version__
from poetry.repositories.pypi_repository import PyPiRepository from poetry.repositories.pypi_repository import PyPiRepository
from poetry.semver.comparison import less_than
version = self.argument('version') version = self.argument('version')
if not version: if not version:
...@@ -38,7 +37,7 @@ class SelfUpdateCommand(Command): ...@@ -38,7 +37,7 @@ class SelfUpdateCommand(Command):
key=cmp_to_key( key=cmp_to_key(
lambda x, y: lambda x, y:
0 if x.version == y.version 0 if x.version == y.version
else -1 * int(less_than(x.version, y.version) or -1) else int(x.version < y.version or -1)
) )
) )
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import sys
from .venv_command import VenvCommand from .venv_command import VenvCommand
...@@ -12,6 +14,7 @@ class ShowCommand(VenvCommand): ...@@ -12,6 +14,7 @@ class ShowCommand(VenvCommand):
{ --l|latest : Show the latest version. } { --l|latest : Show the latest version. }
{ --o|outdated : Show the latest version { --o|outdated : Show the latest version
but only for packages that are outdated. } but only for packages that are outdated. }
{ --a|all : Show all packages (even those not compatible with current system). }
""" """
help = """The show command displays detailed information about a package, or help = """The show command displays detailed information about a package, or
...@@ -26,6 +29,11 @@ lists all packages available.""" ...@@ -26,6 +29,11 @@ lists all packages available."""
] ]
def handle(self): def handle(self):
from poetry.packages.constraints.generic_constraint import GenericConstraint
from poetry.repositories.installed_repository import InstalledRepository
from poetry.semver import Version
from poetry.semver import parse_constraint
package = self.argument('package') package = self.argument('package')
if self.option('tree'): if self.option('tree'):
...@@ -34,23 +42,23 @@ lists all packages available.""" ...@@ -34,23 +42,23 @@ lists all packages available."""
if self.option('outdated'): if self.option('outdated'):
self.input.set_option('latest', True) self.input.set_option('latest', True)
installed_repo = self.poetry.locker.locked_repository(True) locked_repo = self.poetry.locker.locked_repository(True)
# Show tree view if requested # Show tree view if requested
if self.option('tree') and not package: if self.option('tree') and not package:
requires = self.poetry.package.requires + self.poetry.package.dev_requires requires = self.poetry.package.requires + self.poetry.package.dev_requires
packages = installed_repo.packages packages = locked_repo.packages
for package in packages: for package in packages:
for require in requires: for require in requires:
if package.name == require.name: if package.name == require.name:
self.display_package_tree(package, installed_repo) self.display_package_tree(package, locked_repo)
break break
return 0 return 0
table = self.table(style='compact') table = self.table(style='compact')
table.get_style().set_vertical_border_char('') table.get_style().set_vertical_border_char('')
locked_packages = installed_repo.packages locked_packages = locked_repo.packages
if package: if package:
pkg = None pkg = None
...@@ -63,7 +71,7 @@ lists all packages available.""" ...@@ -63,7 +71,7 @@ lists all packages available."""
raise ValueError('Package {} not found'.format(package)) raise ValueError('Package {} not found'.format(package))
if self.option('tree'): if self.option('tree'):
self.display_package_tree(pkg, installed_repo) self.display_package_tree(pkg, locked_repo)
return 0 return 0
...@@ -90,13 +98,38 @@ lists all packages available.""" ...@@ -90,13 +98,38 @@ lists all packages available."""
return 0 return 0
show_latest = self.option('latest') show_latest = self.option('latest')
show_all = self.option('all')
terminal = self.get_application().terminal terminal = self.get_application().terminal
width = terminal.width width = terminal.width
name_length = version_length = latest_length = 0 name_length = version_length = latest_length = 0
latest_packages = {} latest_packages = {}
installed_repo = InstalledRepository.load(self.venv)
skipped = []
platform = sys.platform
python = Version.parse('.'.join([str(i) for i in self._venv.version_info[:3]]))
# Computing widths # Computing widths
for locked in locked_packages: for locked in locked_packages:
name_length = max(name_length, len(locked.pretty_name)) python_constraint = parse_constraint(locked.requirements.get('python', '*'))
platform_constraint = GenericConstraint.parse(locked.requirements.get('platform', '*'))
if (
not python_constraint.allows(python)
or not platform_constraint.matches(GenericConstraint('=', platform))
):
skipped.append(locked)
if not show_all:
continue
current_length = len(locked.pretty_name)
if not self.output.is_decorated():
installed_status = self.get_installed_status(locked, installed_repo)
if installed_status == 'not-installed':
current_length += 4
name_length = max(name_length, current_length)
version_length = max(version_length, len(locked.full_pretty_version)) version_length = max(version_length, len(locked.full_pretty_version))
if show_latest: if show_latest:
latest = self.find_latest_package(locked) latest = self.find_latest_package(locked)
...@@ -111,7 +144,24 @@ lists all packages available.""" ...@@ -111,7 +144,24 @@ lists all packages available."""
write_description = name_length + version_length + latest_length + 24 <= width write_description = name_length + version_length + latest_length + 24 <= width
for locked in locked_packages: for locked in locked_packages:
line = '<fg=cyan>{:{}}</>'.format(locked.pretty_name, name_length) color = 'green'
name = locked.pretty_name
install_marker = ''
if locked in skipped:
if not show_all:
continue
color = 'black;options=bold'
else:
installed_status = self.get_installed_status(locked, installed_repo)
if installed_status == 'not-installed':
color = 'red'
if not self.output.is_decorated():
# Non installed in non decorated mode
install_marker = ' (!)'
line = '<fg={}>{:{}}{}</>'.format(color, name, name_length - len(install_marker), install_marker)
if write_version: if write_version:
line += ' {:{}}'.format( line += ' {:{}}'.format(
locked.full_pretty_version, version_length locked.full_pretty_version, version_length
...@@ -253,16 +303,23 @@ lists all packages available.""" ...@@ -253,16 +303,23 @@ lists all packages available."""
) )
def get_update_status(self, latest, package): def get_update_status(self, latest, package):
from poetry.semver import statisfies from poetry.semver import parse_constraint
if latest.full_pretty_version == package.full_pretty_version: if latest.full_pretty_version == package.full_pretty_version:
return 'up-to-date' return 'up-to-date'
constraint = '^' + package.pretty_version constraint = parse_constraint('^' + package.pretty_version)
if latest.version and statisfies(latest.version, constraint): if latest.version and constraint.allows(latest.version):
# It needs an immediate semver-compliant upgrade # It needs an immediate semver-compliant upgrade
return 'semver-safe-update' return 'semver-safe-update'
# it needs an upgrade but has potential BC breaks so is not urgent # it needs an upgrade but has potential BC breaks so is not urgent
return 'update-possible' return 'update-possible'
def get_installed_status(self, locked, installed_repo):
for package in installed_repo.packages:
if locked.name == package.name:
return 'installed'
return 'not-installed'
...@@ -28,10 +28,9 @@ patch, minor, major, prepatch, preminor, premajor, prerelease. ...@@ -28,10 +28,9 @@ patch, minor, major, prepatch, preminor, premajor, prerelease.
def handle(self): def handle(self):
version = self.argument('version') version = self.argument('version')
if version in self.RESERVED: version = self.increment_version(
version = self.increment_version( self.poetry.package.pretty_version, version
self.poetry.package.pretty_version, version )
)
self.line( self.line(
'Bumping version from <comment>{}</> to <info>{}</>'.format( 'Bumping version from <comment>{}</> to <info>{}</>'.format(
...@@ -41,93 +40,47 @@ patch, minor, major, prepatch, preminor, premajor, prerelease. ...@@ -41,93 +40,47 @@ patch, minor, major, prepatch, preminor, premajor, prerelease.
content = self.poetry.file.read() content = self.poetry.file.read()
poetry_content = content['tool']['poetry'] poetry_content = content['tool']['poetry']
poetry_content['version'] = version poetry_content['version'] = version.text
self.poetry.file.write(content) self.poetry.file.write(content)
def increment_version(self, version, rule): def increment_version(self, version, rule):
from poetry.semver.version_parser import VersionParser from poetry.semver import Version
parser = VersionParser() try:
version_regex = ( version = Version.parse(version)
'v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?{}(?:\+[^\s]+)?' except ValueError:
).format(parser._modifier_regex)
m = re.match(version_regex, version)
if not m:
raise ValueError( raise ValueError(
'The project\'s version doesn\'t seem to follow semver' 'The project\'s version doesn\'t seem to follow semver'
) )
if m.group(3):
index = 2
elif m.group(2):
index = 1
else:
index = 0
matches = m.groups()[:index+1]
base = '.'.join(matches)
extra_matches = list(g or '' for g in m.groups()[4:])
extras = version[len('.'.join(matches)):]
increment = 1
is_prerelease = (extra_matches[0] or extra_matches[1]) != ''
bump_prerelease = rule in {
'premajor', 'preminor', 'prepatch', 'prerelease'
}
position = -1
if rule in {'major', 'premajor'}: if rule in {'major', 'premajor'}:
if m.group(1) != '0' or m.group(2) != '0' or not is_prerelease: new = version.next_major
position = 0 if rule == 'premajor':
new = new.first_prerelease
elif rule in {'minor', 'preminor'}: elif rule in {'minor', 'preminor'}:
if m.group(2) != '0' or not is_prerelease: new = version.next_minor
position = 1 if rule == 'preminor':
new = new.first_prerelease
elif rule in {'patch', 'prepatch'}: elif rule in {'patch', 'prepatch'}:
if not is_prerelease: new = version.next_patch
position = 2 if rule == 'prepatch':
elif rule == 'prerelease' and not is_prerelease: new = new.first_prerelease
position = 2 elif rule == 'prerelease':
if version.is_prerelease():
if position != -1: pre = version.prerelease
extra_matches[0] = None new_prerelease = int(pre[1]) + 1
new = Version.parse(
base = parser._manipulate_version_string( '{}.{}.{}-{}'.format(
matches, version.major,
position, version.minor,
increment=increment version.patch,
) '.'.join([pre[0], str(new_prerelease)])
)
if bump_prerelease: )
# We bump the prerelease part of the version
sep = ''
if not extra_matches[0]:
extra_matches[0] = 'a'
extra_matches[1] = '0'
sep = ''
else: else:
if extras.startswith(('.', '_', '-')): new = version.next_patch.first_prerelease
sep = extras[0]
prerelease = extra_matches[1]
if not prerelease:
prerelease = '.1'
psep = ''
if prerelease.startswith(('.', '-')):
psep = prerelease[0]
prerelease = prerelease[1:]
new_prerelease = str(int(prerelease) + 1)
extra_matches[1] = '{}{}'.format(psep, new_prerelease)
extras = '{}{}{}{}'.format(
sep,
extra_matches[0],
extra_matches[1],
extra_matches[2]
)
else: else:
extras = '' new = rule
return '.'.join(base.split('.')[:max(index, position)+1]) + extras return new
...@@ -8,8 +8,9 @@ class PoetryStyle(CleoStyle): ...@@ -8,8 +8,9 @@ class PoetryStyle(CleoStyle):
super(PoetryStyle, self).__init__(i, o) super(PoetryStyle, self).__init__(i, o)
self.output.get_formatter().add_style('error', 'red') self.output.get_formatter().add_style('error', 'red')
self.output.get_formatter().add_style('warning', 'black', 'yellow') self.output.get_formatter().add_style('warning', 'yellow')
self.output.get_formatter().add_style('question', 'blue') self.output.get_formatter().add_style('question', 'cyan')
self.output.get_formatter().add_style('comment', 'blue')
def writeln(self, messages, def writeln(self, messages,
type=OutputStyle.OUTPUT_NORMAL, type=OutputStyle.OUTPUT_NORMAL,
......
...@@ -15,8 +15,9 @@ from poetry.puzzle.operations.operation import Operation ...@@ -15,8 +15,9 @@ from poetry.puzzle.operations.operation import Operation
from poetry.repositories import Pool from poetry.repositories import Pool
from poetry.repositories import Repository from poetry.repositories import Repository
from poetry.repositories.installed_repository import InstalledRepository from poetry.repositories.installed_repository import InstalledRepository
from poetry.semver.constraints import Constraint from poetry.semver import parse_constraint
from poetry.semver.version_parser import VersionParser from poetry.semver import Version
from poetry.utils.helpers import canonicalize_name
from .base_installer import BaseInstaller from .base_installer import BaseInstaller
from .pip_installer import PipInstaller from .pip_installer import PipInstaller
...@@ -43,6 +44,7 @@ class Installer: ...@@ -43,6 +44,7 @@ class Installer:
self._verbose = False self._verbose = False
self._write_lock = True self._write_lock = True
self._dev_mode = True self._dev_mode = True
self._develop = []
self._execute_operations = True self._execute_operations = True
self._whitelist = {} self._whitelist = {}
...@@ -98,6 +100,11 @@ class Installer: ...@@ -98,6 +100,11 @@ class Installer:
def is_dev_mode(self): # type: () -> bool def is_dev_mode(self): # type: () -> bool
return self._dev_mode return self._dev_mode
def develop(self, packages): # type: (dict) -> Installer
self._develop = [canonicalize_name(p) for p in packages]
return self
def update(self, update=True): # type: (bool) -> Installer def update(self, update=True): # type: (bool) -> Installer
self._update = update self._update = update
...@@ -112,7 +119,7 @@ class Installer: ...@@ -112,7 +119,7 @@ class Installer:
return self return self
def whitelist(self, packages): # type: (dict) -> Installer def whitelist(self, packages): # type: (dict) -> Installer
self._whitelist = packages self._whitelist = [canonicalize_name(p) for p in packages]
return self return self
...@@ -124,7 +131,14 @@ class Installer: ...@@ -124,7 +131,14 @@ class Installer:
def _do_install(self, local_repo): def _do_install(self, local_repo):
locked_repository = Repository() locked_repository = Repository()
if self._update: if self._update:
if self._locker.is_locked(): if self._locker.is_locked() and self._whitelist:
# If we update with a lock file present and
# we have whitelisted packages (the ones we want to update)
# we get the lock file packages to only update
# what is strictly needed.
#
# Otherwise, the lock file information is irrelevant
# since we want to update everything.
locked_repository = self._locker.locked_repository(True) locked_repository = self._locker.locked_repository(True)
# Checking extras # Checking extras
...@@ -135,33 +149,6 @@ class Installer: ...@@ -135,33 +149,6 @@ class Installer:
) )
self._io.writeln('<info>Updating dependencies</>') self._io.writeln('<info>Updating dependencies</>')
fixed = []
# If the whitelist is enabled, packages not in it are fixed
# to the version specified in the lock
if self._whitelist:
# collect packages to fixate from root requirements
candidates = []
for package in locked_repository.packages:
candidates.append(package)
# fix them to the version in lock if they are not updateable
for candidate in candidates:
to_fix = True
for require in self._whitelist.keys():
if require == candidate.name:
to_fix = False
if to_fix:
dependency = Dependency(
candidate.name,
candidate.version,
optional=candidate.optional,
category=candidate.category,
allows_prereleases=candidate.is_prerelease()
)
fixed.append(dependency)
solver = Solver( solver = Solver(
self._package, self._package,
self._pool, self._pool,
...@@ -170,10 +157,7 @@ class Installer: ...@@ -170,10 +157,7 @@ class Installer:
self._io self._io
) )
request = self._package.requires ops = solver.solve(use_latest=self._whitelist)
request += self._package.dev_requires
ops = solver.solve(request, fixed=fixed)
else: else:
self._io.writeln('<info>Installing dependencies from lock file</>') self._io.writeln('<info>Installing dependencies from lock file</>')
...@@ -426,13 +410,16 @@ class Installer: ...@@ -426,13 +410,16 @@ class Installer:
installed, locked installed, locked
)) ))
if not is_installed: # If it's optional and not in required extras
# If it's optional and not in required extras # we do not install
# we do not install if locked.optional and locked.name not in extra_packages:
if locked.optional and locked.name not in extra_packages: continue
continue
op = Install(locked)
if is_installed:
op.skip('Already installed')
ops.append(Install(locked)) ops.append(op)
return ops return ops
...@@ -451,18 +438,22 @@ class Installer: ...@@ -451,18 +438,22 @@ class Installer:
if op.job_type == 'uninstall': if op.job_type == 'uninstall':
continue continue
parser = VersionParser() if package.name in self._develop and package.source_type == 'directory':
python = '.'.join([str(i) for i in self._venv.version_info[:3]]) package.develop = True
if op.skipped:
op.unskip()
python = Version.parse('.'.join([str(i) for i in self._venv.version_info[:3]]))
if 'python' in package.requirements: if 'python' in package.requirements:
python_constraint = parser.parse_constraints( python_constraint = parse_constraint(
package.requirements['python'] package.requirements['python']
) )
if not python_constraint.matches(Constraint('=', python)): if not python_constraint.allows(python):
# Incompatible python versions # Incompatible python versions
op.skip('Not needed for the current python version') op.skip('Not needed for the current python version')
continue continue
if not package.python_constraint.matches(Constraint('=', python)): if not package.python_constraint.allows(python):
op.skip('Not needed for the current python version') op.skip('Not needed for the current python version')
continue continue
......
...@@ -39,7 +39,11 @@ class PipInstaller(BaseInstaller): ...@@ -39,7 +39,11 @@ class PipInstaller(BaseInstaller):
finally: finally:
os.unlink(req) os.unlink(req)
else: else:
args.append(self.requirement(package)) req = self.requirement(package)
if not isinstance(req, list):
args.append(req)
else:
args += req
self.run(*args) self.run(*args)
...@@ -69,7 +73,15 @@ class PipInstaller(BaseInstaller): ...@@ -69,7 +73,15 @@ class PipInstaller(BaseInstaller):
return req return req
if package.source_type in ['file', 'directory']: if package.source_type in ['file', 'directory']:
return os.path.realpath(package.source_reference) if package.root_dir:
req = os.path.join(package.root_dir, package.source_url)
else:
req = os.path.realpath(package.source_url)
if package.develop:
req = ['-e', req]
return req
if package.source_type == 'git': if package.source_type == 'git':
return 'git+{}@{}#egg={}'.format( return 'git+{}@{}#egg={}'.format(
......
...@@ -182,6 +182,10 @@ ...@@ -182,6 +182,10 @@
"type": "string", "type": "string",
"description": "The python versions for which the dependency should be installed." "description": "The python versions for which the dependency should be installed."
}, },
"platform": {
"type": "string",
"description": "The platform(s) for which the dependency should be installed."
},
"allows-prereleases": { "allows-prereleases": {
"type": "boolean", "type": "boolean",
"description": "Whether the dependency allows prereleases or not." "description": "Whether the dependency allows prereleases or not."
...@@ -225,6 +229,10 @@ ...@@ -225,6 +229,10 @@
"type": "string", "type": "string",
"description": "The python versions for which the dependency should be installed." "description": "The python versions for which the dependency should be installed."
}, },
"platform": {
"type": "string",
"description": "The platform(s) for which the dependency should be installed."
},
"allows-prereleases": { "allows-prereleases": {
"type": "boolean", "type": "boolean",
"description": "Whether the dependency allows prereleases or not." "description": "Whether the dependency allows prereleases or not."
...@@ -255,6 +263,10 @@ ...@@ -255,6 +263,10 @@
"type": "string", "type": "string",
"description": "The python versions for which the dependency should be installed." "description": "The python versions for which the dependency should be installed."
}, },
"platform": {
"type": "string",
"description": "The platform(s) for which the dependency should be installed."
},
"optional": { "optional": {
"type": "boolean", "type": "boolean",
"description": "Whether the dependency is optional or not." "description": "Whether the dependency is optional or not."
...@@ -281,6 +293,10 @@ ...@@ -281,6 +293,10 @@
"type": "string", "type": "string",
"description": "The python versions for which the dependency should be installed." "description": "The python versions for which the dependency should be installed."
}, },
"platform": {
"type": "string",
"description": "The platform(s) for which the dependency should be installed."
},
"optional": { "optional": {
"type": "boolean", "type": "boolean",
"description": "Whether the dependency is optional or not." "description": "Whether the dependency is optional or not."
...@@ -291,6 +307,10 @@ ...@@ -291,6 +307,10 @@
"items": { "items": {
"type": "string" "type": "string"
} }
},
"develop": {
"type": "boolean",
"description": "Whether to install the dependency in development mode."
} }
} }
}, },
......
from typing import Type from typing import Type
from .layout import Layout from .layout import Layout
from .src import SrcLayout
from .standard import StandardLayout from .standard import StandardLayout
_LAYOUTS = { _LAYOUTS = {
'standard': StandardLayout 'src': SrcLayout,
'standard': StandardLayout,
} }
......
from poetry.toml import dumps from poetry.toml import dumps
from poetry.toml import loads from poetry.toml import loads
from poetry.utils.helpers import module_name from poetry.utils.helpers import module_name
from poetry.vcs.git import Git
TESTS_DEFAULT = u"""from {package_name} import __version__ TESTS_DEFAULT = u"""from {package_name} import __version__
...@@ -20,45 +19,52 @@ description = "" ...@@ -20,45 +19,52 @@ description = ""
authors = [] authors = []
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "*"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pytest = "^3.5" """
POETRY_WITH_LICENSE = """\
[tool.poetry]
name = ""
version = ""
description = ""
authors = []
license = ""
[tool.poetry.dependencies]
[tool.poetry.dev-dependencies]
""" """
class Layout(object): class Layout(object):
def __init__(self, project, version='0.1.0', readme_format='md', author=None): def __init__(self,
project,
version='0.1.0',
description='',
readme_format='md',
author=None,
license=None,
python='*',
dependencies=None,
dev_dependencies=None):
self._project = project self._project = project
self._package_name = module_name(project) self._package_name = module_name(project)
self._version = version self._version = version
self._description = description
self._readme_format = readme_format self._readme_format = readme_format
self._dependencies = {} self._license = license
self._dev_dependencies = {} self._python = python
self._include = [] self._dependencies = dependencies or {}
self._dev_dependencies = dev_dependencies or {'pytest': '^3.5'}
self._git = Git()
git_config = self._git.config
if not author: if not author:
if ( author = 'Your Name <you@example.com>'
git_config.get('user.name')
and git_config.get('user.email')
):
author = u'{} <{}>'.format(
git_config['user.name'],
git_config['user.email']
)
else:
author = 'Your Name <you@example.com>'
self._author = author self._author = author
def create(self, path, with_tests=True): def create(self, path, with_tests=True):
self._dependencies = {}
self._dev_dependencies = {}
self._include = []
path.mkdir(parents=True, exist_ok=True) path.mkdir(parents=True, exist_ok=True)
self._create_default(path) self._create_default(path)
...@@ -69,6 +75,30 @@ class Layout(object): ...@@ -69,6 +75,30 @@ class Layout(object):
self._write_poetry(path) self._write_poetry(path)
def generate_poetry_content(self):
template = POETRY_DEFAULT
if self._license:
template = POETRY_WITH_LICENSE
content = loads(template)
poetry_content = content['tool']['poetry']
poetry_content['name'] = self._project
poetry_content['version'] = self._version
poetry_content['description'] = self._description
poetry_content['authors'].append(self._author)
if self._license:
poetry_content['license'] = self._license
poetry_content['dependencies']['python'] = self._python
for dep_name, dep_constraint in self._dependencies.items():
poetry_content['dependencies'][dep_name] = dep_constraint
for dep_name, dep_constraint in self._dev_dependencies.items():
poetry_content['dev-dependencies'][dep_name] = dep_constraint
return dumps(content)
def _create_default(self, path, src=True): def _create_default(self, path, src=True):
raise NotImplementedError() raise NotImplementedError()
...@@ -99,13 +129,9 @@ class Layout(object): ...@@ -99,13 +129,9 @@ class Layout(object):
) )
def _write_poetry(self, path): def _write_poetry(self, path):
content = loads(POETRY_DEFAULT) content = self.generate_poetry_content()
poetry_content = content['tool']['poetry']
poetry_content['name'] = self._project
poetry_content['version'] = self._version
poetry_content['authors'].append(self._author)
poetry = path / 'pyproject.toml' poetry = path / 'pyproject.toml'
with poetry.open('w') as f: with poetry.open('w') as f:
f.write(dumps(content)) f.write(content)
# -*- coding: utf-8 -*-
from .layout import Layout
DEFAULT = u"""__version__ = '{version}'
"""
class SrcLayout(Layout):
def _create_default(self, path):
package_path = path / 'src' / self._package_name
package_init = package_path / '__init__.py'
package_path.mkdir(parents=True)
with package_init.open('w') as f:
f.write(DEFAULT.format(version=self._version))
...@@ -7,9 +7,6 @@ import tempfile ...@@ -7,9 +7,6 @@ import tempfile
from collections import defaultdict from collections import defaultdict
from contextlib import contextmanager from contextlib import contextmanager
from poetry.semver.constraints import Constraint
from poetry.semver.constraints import MultiConstraint
from poetry.semver.version_parser import VersionParser
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.vcs import get_vcs from poetry.vcs import get_vcs
...@@ -106,6 +103,16 @@ class Builder(object): ...@@ -106,6 +103,16 @@ class Builder(object):
) )
to_add.append(Path('pyproject.toml')) to_add.append(Path('pyproject.toml'))
# If a license file exists, add it
for license_file in self._path.glob('LICENSE*'):
self._io.writeln(
' - Adding: <comment>{}</comment>'.format(
license_file.relative_to(self._path)
),
verbosity=self._io.VERBOSITY_VERY_VERBOSE
)
to_add.append(license_file.relative_to(self._path))
# If a README is specificed we need to include it # If a README is specificed we need to include it
# to avoid errors # to avoid errors
if 'readme' in self._poetry.local_config: if 'readme' in self._poetry.local_config:
...@@ -156,35 +163,6 @@ class Builder(object): ...@@ -156,35 +163,6 @@ class Builder(object):
'email': email 'email': email
} }
def get_classifers(self):
classifiers = []
# Automatically set python classifiers
parser = VersionParser()
if self._package.python_versions == '*':
python_constraint = parser.parse_constraints('~2.7 || ^3.4')
else:
python_constraint = self._package.python_constraint
for version in sorted(self.AVAILABLE_PYTHONS):
if python_constraint.matches(Constraint('=', version)):
classifiers.append(
'Programming Language :: Python :: {}'.format(version)
)
return classifiers
def convert_python_version(self):
constraint = self._package.python_constraint
if isinstance(constraint, MultiConstraint):
python_requires = ','.join(
[str(c).replace(' ', '') for c in constraint.constraints]
)
else:
python_requires = str(constraint).replace(' ', '')
return python_requires
@classmethod @classmethod
@contextmanager @contextmanager
def temporary_directory(cls, *args, **kwargs): def temporary_directory(cls, *args, **kwargs):
......
...@@ -64,7 +64,7 @@ class SdistBuilder(Builder): ...@@ -64,7 +64,7 @@ class SdistBuilder(Builder):
target_dir.mkdir(parents=True) target_dir.mkdir(parents=True)
target = target_dir / '{}-{}.tar.gz'.format( target = target_dir / '{}-{}.tar.gz'.format(
self._package.pretty_name, self._package.version self._package.pretty_name, self._meta.version
) )
gz = GzipFile(target.as_posix(), mode='wb') gz = GzipFile(target.as_posix(), mode='wb')
tar = tarfile.TarFile(target.as_posix(), mode='w', fileobj=gz, tar = tarfile.TarFile(target.as_posix(), mode='w', fileobj=gz,
...@@ -72,7 +72,7 @@ class SdistBuilder(Builder): ...@@ -72,7 +72,7 @@ class SdistBuilder(Builder):
try: try:
tar_dir = '{}-{}'.format( tar_dir = '{}-{}'.format(
self._package.pretty_name, self._package.version self._package.pretty_name, self._meta.version
) )
files_to_add = self.find_files_to_add(exclude_build=False) files_to_add = self.find_files_to_add(exclude_build=False)
......
...@@ -13,8 +13,7 @@ from base64 import urlsafe_b64encode ...@@ -13,8 +13,7 @@ from base64 import urlsafe_b64encode
from io import StringIO from io import StringIO
from poetry.__version__ import __version__ from poetry.__version__ import __version__
from poetry.semver.constraints import Constraint from poetry.semver import parse_constraint
from poetry.semver.constraints import MultiConstraint
from poetry.utils._compat import Path from poetry.utils._compat import Path
from ..utils.helpers import normalize_file_permissions from ..utils.helpers import normalize_file_permissions
...@@ -181,23 +180,18 @@ class WheelBuilder(Builder): ...@@ -181,23 +180,18 @@ class WheelBuilder(Builder):
@property @property
def dist_info(self): # type: () -> str def dist_info(self): # type: () -> str
return self.dist_info_name(self._package.name, self._package.version) return self.dist_info_name(self._package.name, self._meta.version)
@property @property
def wheel_filename(self): # type: () -> str def wheel_filename(self): # type: () -> str
return '{}-{}-{}.whl'.format( return '{}-{}-{}.whl'.format(
re.sub("[^\w\d.]+", "_", self._package.pretty_name, flags=re.UNICODE), re.sub("[^\w\d.]+", "_", self._package.pretty_name, flags=re.UNICODE),
re.sub("[^\w\d.]+", "_", self._package.version, flags=re.UNICODE), re.sub("[^\w\d.]+", "_", self._meta.version, flags=re.UNICODE),
self.tag self.tag
) )
def supports_python2(self): def supports_python2(self):
return self._package.python_constraint.matches( return self._package.python_constraint.allows_any(parse_constraint('>=2.0.0 <3.0.0'))
MultiConstraint([
Constraint('>=', '2.0.0'),
Constraint('<', '3.0.0')
])
)
def dist_info_name(self, distribution, version): # type: (...) -> str def dist_info_name(self, distribution, version): # type: (...) -> str
escaped_name = re.sub("[^\w\d.]+", "_", distribution, flags=re.UNICODE) escaped_name = re.sub("[^\w\d.]+", "_", distribution, flags=re.UNICODE)
......
from poetry.utils.helpers import canonicalize_name from poetry.utils.helpers import canonicalize_name
from poetry.utils.helpers import normalize_version
from poetry.version.helpers import format_python_constraint from poetry.version.helpers import format_python_constraint
...@@ -42,7 +43,7 @@ class Metadata: ...@@ -42,7 +43,7 @@ class Metadata:
meta = cls() meta = cls()
meta.name = canonicalize_name(package.name) meta.name = canonicalize_name(package.name)
meta.version = package.version meta.version = normalize_version(package.version.text)
meta.summary = package.description meta.summary = package.description
if package.readme: if package.readme:
with package.readme.open() as f: with package.readme.open() as f:
......
...@@ -17,6 +17,10 @@ class Publisher: ...@@ -17,6 +17,10 @@ class Publisher:
self._io = io self._io = io
self._uploader = Uploader(poetry, io) self._uploader = Uploader(poetry, io)
@property
def files(self):
return self._uploader.files
def publish(self, repository_name, username, password): def publish(self, repository_name, username, password):
if repository_name: if repository_name:
self._io.writeln( self._io.writeln(
......
...@@ -2,6 +2,8 @@ import hashlib ...@@ -2,6 +2,8 @@ import hashlib
import io import io
import re import re
from typing import List
import requests import requests
from requests import adapters from requests import adapters
...@@ -13,6 +15,7 @@ from requests_toolbelt.multipart import ( ...@@ -13,6 +15,7 @@ from requests_toolbelt.multipart import (
) )
from poetry.__version__ import __version__ from poetry.__version__ import __version__
from poetry.utils.helpers import normalize_version
from ..metadata import Metadata from ..metadata import Metadata
...@@ -51,6 +54,23 @@ class Uploader: ...@@ -51,6 +54,23 @@ class Uploader:
return adapters.HTTPAdapter(max_retries=retry) return adapters.HTTPAdapter(max_retries=retry)
@property
def files(self): # type: () -> List[str]
dist = self._poetry.file.parent / 'dist'
version = normalize_version(self._package.version.text)
wheels = list(dist.glob(
'{}-{}-*.whl'.format(
re.sub("[^\w\d.]+", "_", self._package.pretty_name, flags=re.UNICODE),
re.sub("[^\w\d.]+", "_", version, flags=re.UNICODE),
)
))
tars = list(dist.glob(
'{}-{}.tar.gz'.format(self._package.pretty_name, version)
))
return sorted(wheels + tars)
def auth(self, username, password): def auth(self, username, password):
self._username = username self._username = username
self._password = password self._password = password
...@@ -177,27 +197,7 @@ class Uploader: ...@@ -177,27 +197,7 @@ class Uploader:
raise raise
def _do_upload(self, session, url): def _do_upload(self, session, url):
dist = self._poetry.file.parent / 'dist' for file in self.files:
packages = dist.glob(
'{}-{}*'.format(self._package.name, self._package.version)
)
files = (
i for i in packages if (
i.match(
'{}-{}-*.whl'.format(
self._package.name, self._package.version
)
)
or
i.match(
'{}-{}.tar.gz'.format(
self._package.name, self._package.version
)
)
)
)
for file in files:
# TODO: Check existence # TODO: Check existence
resp = self._upload_file(session, url, file) resp = self._upload_file(session, url, file)
......
from .dependency_graph import DependencyGraph from .version_solver import VersionSolver
from .resolver import Resolver
def resolve_version(root, provider, locked=None, use_latest=None):
solver = VersionSolver(root, provider, locked=locked, use_latest=use_latest)
with provider.progress():
return solver.solve()
from typing import Any
from .incompatibility import Incompatibility
from .term import Term
class Assignment(Term):
"""
A term in a PartialSolution that tracks some additional metadata.
"""
def __init__(self, dependency, is_positive,
decision_level, index,
cause=None):
super(Assignment, self).__init__(dependency, is_positive)
self._decision_level = decision_level
self._index = index
self._cause = cause
@property
def decision_level(self): # type: () -> int
return self._decision_level
@property
def index(self): # type: () -> int
return self._index
@property
def cause(self): # type: () -> Incompatibility
return self._cause
@classmethod
def decision(cls, package, decision_level, index
): # type: (Any, int, int) -> Assignment
return cls(package.to_dependency(), True, decision_level, index)
@classmethod
def derivation(cls, dependency, is_positive, cause, decision_level, index
): # type: (Any, bool, Incompatibility, int, int) -> Assignment
return cls(dependency, is_positive, decision_level, index, cause)
def is_decision(self): # type: () -> bool
return self._cause is None
class Conflict:
def __init__(self,
requirement,
requirements,
existing,
possibility_set,
locked_requirement,
requirement_trees,
activated_by_name,
underlying_error):
self.requirement = requirement
self.requirements = requirements
self.existing = existing
self.possibility_set = possibility_set
self.locked_requirement = locked_requirement
self.requirement_trees = requirement_trees,
self.activated_by_name = activated_by_name
self.underlying_error = underlying_error
@property
def possibility(self):
if self.possibility_set and self.possibility_set.latest_version:
return self.possibility_set.latest_version
from .specification_provider import SpecificationProvider
from .ui import UI
from typing import Any
from typing import Dict
from typing import List
from ..conflict import Conflict
from ..dependency_graph import DependencyGraph
class SpecificationProvider(object):
"""
Provides information about specifcations and dependencies to the resolver,
allowing the Resolver class to remain generic while still providing power
and flexibility.
This contract contains the methods
that users of Molinillo must implement
using knowledge of their own model classes.
"""
@property
def name_for_explicit_dependency_source(self): # type: () -> str
return 'user-specified dependency'
@property
def name_for_locking_dependency_source(self): # type: () -> str
return 'Lockfile'
def search_for(self, dependency): # type: (Any) -> List[Any]
"""
Search for the specifications that match the given dependency.
The specifications in the returned list will be considered in reverse
order, so the latest version ought to be last.
"""
return []
def dependencies_for(self, specification): # type: (Any) -> List[Any]
"""
Returns the dependencies of specification.
"""
return []
def is_requirement_satisfied_by(self,
requirement, # type: Any
activated, # type: DependencyGraph
spec # type: Any
): # type: (...) -> Any
"""
Determines whether the given requirement is satisfied by the given
spec, in the context of the current activated dependency graph.
"""
return True
def name_for(self, dependency): # type: (Any) -> str
"""
Returns the name for the given dependency.
"""
return str(dependency)
def sort_dependencies(self,
dependencies, # type: List[Any]
activated, # type: DependencyGraph
conflicts # type: Dict[str, List[Conflict]]
): # type: (...) -> List[Any]
"""
Sort dependencies so that the ones
that are easiest to resolve are first.
Easiest to resolve is (usually) defined by:
1) Is this dependency already activated?
2) How relaxed are the requirements?
3) Are there any conflicts for this dependency?
4) How many possibilities are there to satisfy this dependency?
"""
return sorted(
dependencies,
key=lambda dep: (
activated.vertex_named(self.name_for(dep)).payload is None,
conflicts.get(self.name_for(dep) is None)
)
)
def allow_missing(self, dependency): # type: (Any) -> bool
"""
Returns whether this dependency, which has no possible matching
specifications, can safely be ignored.
"""
return False
import sys
class UI(object):
def __init__(self, debug=False):
self._debug = debug
@property
def output(self):
return sys.stdout
@property
def progress_rate(self): # type: () -> float
return 0.33
def is_debugging(self): # type: () -> bool
return self._debug
def indicate_progress(self): # type: () -> None
self.output.write('.')
def before_resolution(self): # type: () -> None
self.output.write('Resolving dependencies...\n')
def after_resolution(self): # type: () -> None
self.output.write('')
def debug(self, message, depth): # type: (...) -> None
if self.is_debugging():
debug_info = str(message)
debug_info = '\n'.join([
':{}: {}'.format(str(depth).rjust(4), s)
for s in debug_info.split('\n')
]) + '\n'
self.output.write(debug_info)
from .exceptions import CircularDependencyError
from .graph.log import Log
class DependencyGraph:
def __init__(self):
self._vertices = {}
self._log = Log()
@property
def vertices(self):
return self._vertices
@property
def log(self):
return self._log
def tag(self, tag):
return self._log.tag(self, tag)
def rewind_to(self, tag):
return self._log.rewind_to(self, tag)
def add_child_vertex(self, name, payload, parent_names, requirement):
root = True
try:
parent_names.index(None)
except ValueError:
root = False
parent_names = [n for n in parent_names if n is not None]
vertex = self.add_vertex(name, payload, root)
if root:
vertex.explicit_requirements.append(requirement)
for parent_name in parent_names:
parent_vertex = self.vertex_named(parent_name)
self.add_edge(parent_vertex, vertex, requirement)
return vertex
def add_vertex(self, name, payload, root=False):
return self._log.add_vertex(self, name, payload, root)
def detach_vertex_named(self, name):
return self._log.detach_vertex_named(self, name)
def vertex_named(self, name):
return self.vertices.get(name)
def root_vertex_named(self, name):
vertex = self.vertex_named(name)
if vertex and vertex.root:
return vertex
def add_edge(self, origin, destination, requirement):
if destination.has_path_to(origin):
raise CircularDependencyError([origin, destination])
return self.add_edge_no_circular(origin, destination, requirement)
def add_edge_no_circular(self, origin, destination, requirement):
self._log.add_edge_no_circular(
self,
origin.name, destination.name,
requirement
)
def delete_edge(self, edge):
return self._log.delete_edge(
self,
edge.origin.name,
edge.destination.name,
edge.requirement
)
def set_payload(self, name, payload):
return self._log.set_payload(self, name, payload)
def to_dot(self):
dot_vertices = []
dot_edges = []
for n, v in self.vertices.items():
dot_vertices.append(
' {} [label="{}|{}"]'.format(n, n, v.payload or '')
)
for e in v.outgoing_edges:
label = e.requirement
dot_edges.append(
' {} -> {} [label="{}"]'.format(
e.origin.name,
e.destination.name,
label
)
)
dot_vertices = sorted(set(dot_vertices))
dot_edges = sorted(set(dot_edges))
dot_vertices.insert(0, 'digraph G {')
dot_vertices.append('')
dot_edges.append('}')
dot = dot_vertices + dot_edges
return '\n'.join(dot)
def __iter__(self):
return iter(self.vertices.values())
from .helpers import flat_map
class ResolverError(Exception):
pass
class NoSuchDependencyError(ResolverError):
def __init__(self, dependency, required_by=None):
if required_by is None:
required_by = []
sources = ' and '.join(['"{}"'.format(r) for r in required_by])
message = 'Unable to find a specification for "{}"'.format(dependency)
if sources:
message += ' depended upon by {}'.format(sources)
super(NoSuchDependencyError, self).__init__(message)
class CircularDependencyError(ResolverError):
def __init__(self, vertices):
super(CircularDependencyError, self).__init__(
'There is a circular dependency between {}'.format(
' and '.join([v.name for v in vertices])
)
)
self._dependencies = [v.payload.possibilities[-1] for v in vertices]
@property
def dependencies(self):
return self._dependencies
class VersionConflict(ResolverError):
def __init__(self, conflicts, specification_provider):
pairs = []
for conflicting in flat_map(
list(conflicts.values()), lambda x: x.requirements
):
for source, conflict_requirements in conflicting.items():
for c in conflict_requirements:
pairs.append((c, source))
super(VersionConflict, self).__init__(
'Unable to satisfy the following requirements:\n\n'
'{}'.format(
'\n'.join('- "{}" required by "{}"'.format(r, d)
for r, d in pairs)
)
)
self._conflicts = conflicts
self._specification_provider = specification_provider
@property
def conflicts(self):
return self._conflicts
@property
def specification_provider(self):
return self._specification_provider
def message_with_trees(self,
solver_name='Poetry',
possibility_type='possibility named',
reduce_trees=lambda trees: sorted(set(trees), key=str),
printable_requirement=str,
message_for_conflict=None,
version_for_spec=str):
o = []
for name, conflict in sorted(self._conflicts):
o.append(
'\n{} could not find compatible versions for {} "{}"_n'.format(
solver_name, possibility_type, name
)
)
if conflict.locked_requirement:
o.append(
' In snapshot ({}):\n'.format(
self._specification_provider.name_for_locking_dependency_source
)
)
o.append(
' {}\n'.format(
printable_requirement(conflict.locked_requirement)
)
)
o.append('\n')
o.append(
' In {}:\n'.format(
self._specification_provider.name_for_explicit_dependency_source
)
)
trees = reduce_trees(conflict.requirement_trees)
ot = []
for tree in trees:
t = ''
depth = 2
for req in tree:
t += ' ' * depth + str(req)
if tree[-1] != req:
spec = conflict.activated_by_name.get(
self._specification_provider.name_for(req)
)
if spec:
t += ' was resolved to {}, which'.format(
version_for_spec(spec)
)
t += ' depends on'
t += '\n'
depth += 1
ot.append(t)
o.append('\n'.join(ot))
if message_for_conflict:
message_for_conflict(o, name, conflict)
return ''.join(o).strip()
from typing import Dict
from typing import List
from typing import Tuple
from .incompatibility import Incompatibility
from .incompatibility_cause import ConflictCause
class SolveFailure(Exception):
def __init__(self, incompatibility): # type: (Incompatibility) -> None
assert incompatibility.terms[0].dependency.is_root
self._incompatibility = incompatibility
@property
def message(self):
return str(self)
def __str__(self):
return _Writer(self._incompatibility).write()
class _Writer:
def __init__(self, root): # type: (Incompatibility) -> None
self._root = root
self._derivations = {} # type: Dict[Incompatibility, int]
self._lines = [] # type: List[Tuple[str, int]]
self._line_numbers = {} # type: Dict[Incompatibility, int]
self._count_derivations(self._root)
def write(self):
buffer = []
if isinstance(self._root.cause, ConflictCause):
self._visit(self._root, {})
else:
self._write(self._root, 'Because {}, version solving failed.'.format(self._root))
padding = 0 if not self._line_numbers else len('({})'.format(list(self._line_numbers.values())[-1]))
last_was_empty = False
for line in self._lines:
message = line[0]
if not message:
if not last_was_empty:
buffer.append('')
last_was_empty = True
continue
last_was_empty = False
number = line[-1]
if number is not None:
message = '({})'.format(number).ljust(padding) + message
else:
message = ' ' * padding + message
buffer.append(message)
return '\n'.join(buffer)
def _write(self, incompatibility, message, numbered=False
): # type: (Incompatibility, str, bool) -> None
if numbered:
number = len(self._line_numbers) + 1
self._line_numbers[incompatibility] = number
self._lines.append((message, number))
else:
self._lines.append((message, None))
def _visit(self, incompatibility, details_for_incompatibility, conclusion=False
): # type: (Incompatibility, Dict, bool) -> None
numbered = conclusion or self._derivations[incompatibility] > 1
conjunction = conclusion or ('So,' if incompatibility == self._root else 'And')
incompatibility_string = str(incompatibility)
cause = incompatibility.cause # type: ConflictCause
details_for_cause = {}
if isinstance(cause.conflict.cause, ConflictCause) and isinstance(cause.other.cause, ConflictCause):
conflict_line = self._line_numbers.get(cause.conflict)
other_line = self._line_numbers.get(cause.other)
if conflict_line is not None and other_line is not None:
self._write(
incompatibility,
'Because {}, {}.'.format(
cause.conflict.and_to_string(
cause.other, details_for_cause,
conflict_line, other_line
),
incompatibility_string
),
numbered=numbered
)
elif conflict_line is not None or other_line is not None:
if conflict_line is not None:
with_line = cause.conflict
without_line = cause.other
line = conflict_line
else:
with_line = cause.other
without_line = cause.conflict
line = other_line
self._visit(without_line, details_for_cause)
self._write(
incompatibility,
'{} because {} ({}), {}.'.format(
conjunction, str(with_line), line, incompatibility_string
),
numbered=numbered
)
else:
single_line_conflict = self._is_single_line(cause.conflict.cause)
single_line_other = self._is_single_line(cause.other.cause)
if single_line_other or single_line_conflict:
first = cause.conflict if single_line_other else cause.other
second = cause.other if single_line_other else cause.conflict
self._visit(first, details_for_cause)
self._visit(second, details_for_cause)
self._write(
incompatibility,
'Thus, {}.'.format(incompatibility_string),
numbered=numbered
)
else:
self._visit(cause.conflict, {}, conclusion=True)
self._lines.append(('', None))
self._visit(cause.other, details_for_cause)
self._write(
incompatibility,
'{} because {} ({}), {}'.format(
conjunction,
str(cause.conflict),
self._line_numbers[cause.conflict],
incompatibility_string
),
numbered=numbered
)
elif isinstance(cause.conflict.cause, ConflictCause) or isinstance(cause.other.cause, ConflictCause):
derived = cause.conflict if isinstance(cause.conflict.cause, ConflictCause) else cause.other
ext = cause.other if isinstance(cause.conflict.cause, ConflictCause) else cause.conflict
derived_line = self._line_numbers.get(derived)
if derived_line is not None:
self._write(
incompatibility,
'Because {}, {}.'.format(
ext.and_to_string(derived, details_for_cause, None, derived_line),
incompatibility_string
),
numbered=numbered
)
elif self._is_collapsible(derived):
derived_cause = derived.cause # type: ConflictCause
if isinstance(derived_cause.conflict.cause, ConflictCause):
collapsed_derived = derived_cause.conflict
else:
collapsed_derived = derived_cause.other
if isinstance(derived_cause.conflict.cause, ConflictCause):
collapsed_ext = derived_cause.other
else:
collapsed_ext = derived_cause.conflict
details_for_cause = {}
self._visit(collapsed_derived, details_for_cause)
self._write(
incompatibility,
'{} because {}, {}.'.format(
conjunction,
collapsed_ext.and_to_string(ext, details_for_cause, None, None),
incompatibility_string
),
numbered=numbered
)
else:
self._visit(derived, details_for_cause)
self._write(
incompatibility,
'{} because {}, {}.'.format(
conjunction,
str(ext),
incompatibility_string
),
numbered=numbered
)
else:
self._write(
incompatibility,
'Because {}, {}.'.format(
cause.conflict.and_to_string(cause.other, details_for_cause, None, None),
incompatibility_string
),
numbered=numbered
)
def _is_collapsible(self, incompatibility): # type: (Incompatibility) -> bool
if self._derivations[incompatibility] > 1:
return False
cause = incompatibility.cause # type: ConflictCause
if isinstance(cause.conflict.cause, ConflictCause) and isinstance(cause.other.cause, ConflictCause):
return False
if not isinstance(cause.conflict.cause, ConflictCause) and not isinstance(cause.other.cause, ConflictCause):
return False
complex = cause.conflict if isinstance(cause.conflict.cause, ConflictCause) else cause.other
return complex not in self._line_numbers
def _is_single_line(self, cause): # type: (ConflictCause) -> bool
return not isinstance(cause.conflict.cause, ConflictCause) and not isinstance(cause.other.cause, ConflictCause)
def _count_derivations(self, incompatibility): # type: (Incompatibility) -> None
if incompatibility in self._derivations:
self._derivations[incompatibility] += 1
else:
self._derivations[incompatibility] = 1
cause = incompatibility.cause
if isinstance(cause, ConflictCause):
self._count_derivations(cause.conflict)
self._count_derivations(cause.other)
from typing import Any
class Action(object):
def __init__(self):
self.previous = None
self.next = None
@property
def action_name(self): # type: () -> str
raise NotImplementedError()
def up(self, graph): # type: (DependencyGraph) -> Any
"""
Performs the action on the given graph.
"""
raise NotImplementedError()
def down(self, graph): # type: (DependencyGraph) -> None
"""
Reverses the action on the given graph.
"""
raise NotImplementedError()
from .action import Action
from .edge import Edge
class AddEdgeNoCircular(Action):
def __init__(self, origin, destination, requirement):
super(AddEdgeNoCircular, self).__init__()
self._origin = origin
self._destination = destination
self._requirement = requirement
@property
def action_name(self):
return 'add_edge_no_circular'
@property
def origin(self):
return self._origin
@property
def destination(self):
return self._destination
@property
def requirement(self):
return self._requirement
def up(self, graph):
edge = self.make_edge(graph)
edge.origin.outgoing_edges.append(edge)
edge.destination.incoming_edges.append(edge)
return edge
def down(self, graph):
edge = self.make_edge(graph)
self._delete_first(edge.origin.outgoing_edges, edge)
self._delete_first(edge.destination.incoming_edges, edge)
def make_edge(self, graph):
return Edge(
graph.vertex_named(self._origin),
graph.vertex_named(self._destination),
self._requirement
)
def _delete_first(self, elements, element):
"""
:type elements: list
"""
try:
index = elements.index(element)
except ValueError:
return
del elements[index]
from .action import Action
from .vertex import Vertex
_NULL = object()
class AddVertex(Action):
def __init__(self, name, payload, root):
super(AddVertex, self).__init__()
self._name = name
self._payload = payload
self._root = root
self._existing_payload = _NULL
self._existing_root = None
@property
def action_name(self):
return 'add_vertex'
@property
def name(self):
return self._name
@property
def payload(self):
return self._payload
@property
def root(self):
return self._root
def up(self, graph):
existing = graph.vertices.get(self._name)
if existing:
self._existing_payload = existing.payload
self._existing_root = existing.root
vertex = existing or Vertex(self._name, self._payload)
graph.vertices[vertex.name] = vertex
if not vertex.payload:
vertex.payload = self.payload
if not vertex.root:
vertex.root = self.root
return vertex
def down(self, graph):
if self._existing_payload is not _NULL:
vertex = graph.vertices[self._name]
vertex.payload = self._existing_payload
vertex.root = self._existing_root
else:
del graph.vertices[self._name]
from .action import Action
from .edge import Edge
class DeleteEdge(Action):
def __init__(self, origin, destination, requirement):
super(DeleteEdge, self).__init__()
self._origin = origin
self._destination = destination
self._requirement = requirement
@property
def action_name(self):
return 'delete_edge'
@property
def origin(self):
return self._origin
@property
def destination(self):
return self._destination
@property
def requirement(self):
return self._requirement
def up(self, graph):
edge = self.make_edge(graph)
self._delete_first(edge.origin.outgoing_edges, edge)
self._delete_first(edge.destination.incoming_edges, edge)
return edge
def down(self, graph):
edge = self.make_edge(graph)
edge.origin.outgoing_edges.append(edge)
edge.origin.incoming_edges.append(edge)
def make_edge(self, graph):
return Edge(
graph.vertex_named(self._origin),
graph.vertex_named(self._destination),
self._requirement
)
def _delete_first(self, elements, element):
"""
:type elements: list
"""
try:
index = elements.index(element)
except ValueError:
return
del elements[index]
from .action import Action
class DetachVertexNamed(Action):
def __init__(self, name):
super(DetachVertexNamed, self).__init__()
self._name = name
self._vertex = None
@property
def action_name(self):
return 'detach_vertex'
@property
def name(self):
return self._name
def up(self, graph):
if self._name not in graph.vertices:
return []
self._vertex = graph.vertices[self._name]
del graph.vertices[self._name]
removed_vertices = [self._vertex]
for e in self._vertex.outgoing_edges:
v = e.destination
try:
v.incoming_edges.remove(e)
except ValueError:
pass
if not v.root and not v.incoming_edges:
removed_vertices += graph.detach_vertex_named(v.name)
for e in self._vertex.incoming_edges:
v = e.origin
try:
v.outgoing_edges.remove(e)
except ValueError:
pass
return removed_vertices
def down(self, graph):
if self._vertex is None:
return
graph.vertices[self._vertex.name] = self._vertex
for e in self._vertex.outgoing_edges:
e.destination.incoming_edges.append(e)
for e in self._vertex.incoming_edges:
e.origin.outgoing_edges.append(e)
class Edge:
"""
A directed edge of a DependencyGraph
"""
def __init__(self, origin, destination, requirement):
self._origin = origin
self._destination = destination
self._requirement = requirement
@property
def origin(self):
return self._origin
@property
def destination(self):
return self._destination
@property
def requirement(self):
return self._requirement
def __eq__(self, other):
return self._origin == other.origin and self._destination == other.destination
def __repr__(self):
return '<Edge {} -> {}>'.format(
self._origin.name, self._destination.name
)
from .add_edge_no_circular import AddEdgeNoCircular
from .add_vertex import AddVertex
from .delete_edge import DeleteEdge
from .detach_vertex_named import DetachVertexNamed
from .set_payload import SetPayload
from .tag import Tag
class Log:
"""
A log for dependency graph actions.
"""
def __init__(self):
self._current_action = None
self._first_action = None
def tag(self, graph, tag):
"""
Tags the current state of the dependency as the given tag.
"""
return self._push_action(graph, Tag(tag))
def add_vertex(self, graph, name, payload, root):
return self._push_action(graph, AddVertex(name, payload, root))
def detach_vertex_named(self, graph, name):
return self._push_action(graph, DetachVertexNamed(name))
def add_edge_no_circular(self, graph, origin, destination, requirement):
action = AddEdgeNoCircular(origin, destination, requirement)
return self._push_action(graph, action)
def delete_edge(self, graph, origin, destination, requirement):
action = DeleteEdge(origin, destination, requirement)
return self._push_action(graph, action)
def set_payload(self, graph, name, payload):
return self._push_action(graph, SetPayload(name, payload))
def pop(self, graph):
action = self._current_action
if not action:
return
self._current_action = action.previous
if not self._current_action:
self._first_action = None
action.down(graph)
return action
def rewind_to(self, graph, tag):
while True:
action = self.pop(graph)
if not action:
raise ValueError('No tag "{}" found'.format(tag))
if isinstance(action, Tag) and action.tag == tag:
break
def _push_action(self, graph, action):
"""
Adds the given action to the log, running the action
:param graph: The graph
:param action: The action
:type action: Action
"""
action.previous = self._current_action
if self._current_action:
self._current_action.next = action
self._current_action = action
if not self._first_action:
self._first_action = action
return action.up(graph)
from .action import Action
class SetPayload(Action):
def __init__(self, name, payload):
super(SetPayload, self).__init__()
self._name = name
self._payload = payload
self._old_payload = None
@property
def action_name(self):
return 'set_payload'
@property
def name(self):
return self._name
@property
def payload(self):
return self._payload
def up(self, graph):
vertex = graph.vertex_named(self._name)
self._old_payload = vertex.payload
vertex.payload = self._payload
def down(self, graph):
graph.vertex_named(self._name).payload = self._old_payload
from .action import Action
class Tag(Action):
def __init__(self, tag):
super(Tag, self).__init__()
self._tag = tag
@property
def action_name(self):
return 'tag'
@property
def tag(self):
return self._tag
def up(self, graph):
pass
def down(self, graph):
pass
from ..utils import unique
class Vertex:
def __init__(self, name, payload):
self.name = name
self.payload = payload
self.root = False
self._explicit_requirements = []
self.outgoing_edges = []
self.incoming_edges = []
@property
def explicit_requirements(self):
return self._explicit_requirements
@property
def requirements(self):
return unique([
edge.requirement for edge in self.incoming_edges
] + self._explicit_requirements)
@property
def predecessors(self):
return [edge.origin for edge in self.incoming_edges]
@property
def recursive_predecessors(self):
return self._recursive_predecessors()
def _recursive_predecessors(self, vertices=None):
if vertices is None:
vertices = set()
for edge in self.incoming_edges:
vertex = edge.origin
if vertex in vertices:
continue
vertices.add(vertex)
vertex._recursive_predecessors(vertices)
return vertices
@property
def successors(self):
return [
edge.destination for edge in self.outgoing_edges
]
@property
def recursive_successors(self):
return self._recursive_successors()
def _recursive_successors(self, vertices=None):
if vertices is None:
vertices = set()
for edge in self.outgoing_edges:
vertex = edge.destination
if vertex in vertices:
continue
vertices.add(vertex)
vertex._recursive_successors(vertices)
return vertices
def __eq__(self, other):
if not isinstance(other, Vertex):
return NotImplemented
if self is other:
return True
return (
self.name == other.name
and self.payload == other.payload
and set(self.successors) == set(other.successors)
)
def __hash__(self):
return hash(self.name)
def has_path_to(self, other):
return (
self == other
or any([v.has_path_to(other) for v in self.successors])
)
def is_ancestor(self, other):
return other.path_to(self)
def __repr__(self):
return '<Vertex {} ({})>'.format(self.name, self.payload)
def flat_map(iter, callable):
if not isinstance(iter, (list, tuple)):
yield callable(iter)
else:
for v in iter:
for i in flat_map(v, callable):
yield i
from typing import Dict
from typing import List
from .incompatibility_cause import ConflictCause
from .incompatibility_cause import DependencyCause
from .incompatibility_cause import IncompatibilityCause
from .incompatibility_cause import NoVersionsCause
from .incompatibility_cause import PackageNotFoundCause
from .incompatibility_cause import PlatformCause
from .incompatibility_cause import PythonCause
from .incompatibility_cause import RootCause
from .term import Term
class Incompatibility:
def __init__(self, terms, cause): # type: (List[Term], IncompatibilityCause) -> None
# Remove the root package from generated incompatibilities, since it will
# always be satisfied. This makes error reporting clearer, and may also
# make solving more efficient.
if (
len(terms) != 1
and isinstance(cause, ConflictCause)
and any([term.is_positive() and term.dependency.is_root for term in terms])
):
terms = [
term for term in terms
if not term.is_positive() or not term.dependency.is_root
]
if (
len(terms) == 1
# Short-circuit in the common case of a two-term incompatibility with
# two different packages (for example, a dependency).
or len(terms) == 2 and terms[0].dependency.name != terms[-1].dependency.name
):
pass
else:
# Coalesce multiple terms about the same package if possible.
by_name = {} # type: Dict[str, Dict[str, Term]]
for term in terms:
if term.dependency.name not in by_name:
by_name[term.dependency.name] = {}
by_ref = by_name[term.dependency.name]
ref = term.dependency.name
if ref in by_ref:
by_ref[ref] = by_ref[ref].intersect(term)
# If we have two terms that refer to the same package but have a null
# intersection, they're mutually exclusive, making this incompatibility
# irrelevant, since we already know that mutually exclusive version
# ranges are incompatible. We should never derive an irrelevant
# incompatibility.
assert by_ref[ref] is not None
else:
by_ref[ref] = term
new_terms = []
for by_ref in by_name.values():
positive_terms = [term for term in by_ref.values() if term.is_positive()]
if positive_terms:
new_terms += positive_terms
continue
new_terms += list(by_ref.values())
terms = new_terms
self._terms = terms
self._cause = cause
@property
def terms(self): # type: () -> List[Term]
return self._terms
@property
def cause(self): # type: () -> IncompatibilityCause
return self._cause
def is_failure(self): # type: () -> bool
return len(self._terms) == 0 or (len(self._terms) == 1 and self._terms[0].dependency.is_root)
def __str__(self):
if isinstance(self._cause, DependencyCause):
assert len(self._terms) == 2
depender = self._terms[0]
dependee = self._terms[1]
assert depender.is_positive()
assert not dependee.is_positive()
return '{} depends on {}'.format(
self._terse(depender, allow_every=True),
self._terse(dependee)
)
elif isinstance(self._cause, PythonCause):
assert len(self._terms) == 1
assert self._terms[0].is_positive()
cause = self._cause # type: PythonCause
text = '{} requires '.format(self._terse(self._terms[0], allow_every=True))
text += 'Python {}'.format(cause.python_version)
return text
elif isinstance(self._cause, PlatformCause):
assert len(self._terms) == 1
assert self._terms[0].is_positive()
cause = self._cause # type: PlatformCause
text = '{} requires '.format(self._terse(self._terms[0], allow_every=True))
text += 'platform {}'.format(cause.platform)
return text
elif isinstance(self._cause, NoVersionsCause):
assert len(self._terms) == 1
assert self._terms[0].is_positive()
return 'no versions of {} match {}'.format(
self._terms[0].dependency.name,
self._terms[0].constraint
)
elif isinstance(self._cause, PackageNotFoundCause):
assert len(self._terms) == 1
assert self._terms[0].is_positive()
return '{} doesn\'t exist'.format(self._terms[0].dependency.name)
elif isinstance(self._cause, RootCause):
assert len(self._terms) == 1
assert not self._terms[0].is_positive()
assert self._terms[0].dependency.is_root
return '{} is {}'.format(
self._terms[0].dependency.name,
self._terms[0].dependency.constraint
)
elif self.is_failure():
return 'version solving failed'
if len(self._terms) == 1:
term = self._terms[0]
if term.constraint.is_any():
return '{} is {}'.format(
term.dependency.name,
'forbidden' if term.is_positive() else 'required'
)
else:
return '{} is {}'.format(
term.dependency.name,
'forbidden' if term.is_positive() else 'required'
)
if len(self._terms) == 2:
term1 = self._terms[0]
term2 = self._terms[1]
if term1.is_positive() == term2.is_positive():
if term1.is_positive():
package1 = term1.dependency.name if term1.constraint.is_any() else self._terse(term1)
package2 = term2.dependency.name if term2.constraint.is_any() else self._terse(term2)
return '{} is incompatible with {}'.format(package1, package2)
else:
return 'either {} or {}'.format(
self._terse(term1),
self._terse(term2)
)
positive = []
negative = []
for term in self._terms:
if term.is_positive():
positive.append(self._terse(term))
else:
negative.append(self._terse(term))
if positive and negative:
if len(positive) == 1:
positive_term = [term for term in self._terms if term.is_positive()][0]
return '{} requires {}'.format(
self._terse(positive_term, allow_every=True),
' or '.join(negative)
)
else:
return 'if {} then {}'.format(
' and '.join(positive),
' or '.join(negative)
)
elif positive:
return 'one of {} must be false'.format(' or '.join(positive))
else:
return 'one of {} must be true'.format(' or '.join(negative))
def and_to_string(self, other, details, this_line, other_line
): # type: (Incompatibility, dict, int, int) -> str
requires_both = self._try_requires_both(other, details, this_line, other_line)
if requires_both is not None:
return requires_both
requires_through = self._try_requires_through(other, details, this_line, other_line)
if requires_through is not None:
return requires_through
requires_forbidden = self._try_requires_forbidden(other, details, this_line, other_line)
if requires_forbidden is not None:
return requires_forbidden
buffer = [str(self)]
if this_line is not None:
buffer.append(' ' + this_line)
buffer.append(' and {}'.format(str(other)))
if other_line is not None:
buffer.append(' ' + other_line)
return '\n'.join(buffer)
def _try_requires_both(self, other, details, this_line, other_line
): # type: (Incompatibility, dict, int, int) -> str
if len(self._terms) == 1 or len(other.terms) == 1:
return
this_positive = self._single_term_where(lambda term: term.is_positive())
if this_positive is None:
return
other_positive = other._single_term_where(lambda term: term.is_positive())
if other_positive is None:
return
if this_positive.dependency != other_positive.dependency:
return
this_negatives = ' or '.join([
self._terse(term)
for term in self._terms
if not term.is_positive()
])
other_negatives = ' or '.join([
self._terse(term)
for term in other.terms
if not term.is_positive()
])
buffer = [self._terse(this_positive, allow_every=True) + ' ']
is_dependency = isinstance(self.cause, DependencyCause) and isinstance(other.cause, DependencyCause)
if is_dependency:
buffer.append('depends on')
else:
buffer.append('requires')
buffer.append(' both {}'.format(this_negatives))
if this_line is not None:
buffer.append(' ({})'.format(this_line))
buffer.append(' and {}'.format(other_negatives))
if other_line is not None:
buffer.append(' ({})'.format(other_line))
return ''.join(buffer)
def _try_requires_through(self, other, details, this_line, other_line
): # type: (Incompatibility, dict, int, int) -> str
if len(self._terms) == 1 or len(other.terms) == 1:
return
this_negative = self._single_term_where(lambda term: not term.is_positive())
other_negative = other._single_term_where(lambda term: not term.is_positive())
if this_negative is None and other_negative is None:
return
this_positive = self._single_term_where(lambda term: term.is_positive())
other_positive = self._single_term_where(lambda term: term.is_positive())
if (
this_negative is not None
and other_positive is not None
and this_negative.dependency.name == other_positive.dependency.name
and this_negative.inverse.satisfies(other_positive)
):
prior = self
prior_negative = this_negative
prior_line = this_line
latter = other
latter_line = other_line
elif (
other_negative is not None
and this_positive is not None
and other_negative.dependency.name == this_positive.dependency.name
and other_negative.inverse.satisfies(this_positive)
):
prior = other
prior_negative = other_negative
prior_line = other_line
latter = self
latter_line = this_line
else:
return
prior_positives = [term for term in prior.terms if term.is_positive()]
buffer = []
if len(prior_positives) > 1:
prior_string = ' or '.join([
self._terse(term) for term in prior_positives
])
buffer.append('if {} then '.format(prior_string))
else:
if isinstance(prior.cause, DependencyCause):
verb = 'depends on'
else:
verb = 'requires'
buffer.append(
'{} {} '.format(
self._terse(prior_positives[0], allow_every=True),
verb
)
)
buffer.append(self._terse(prior_negative))
if prior_line is not None:
buffer.append(' ({})'.format(prior_line))
buffer.append(' which ')
if isinstance(latter.cause, DependencyCause):
buffer.append('depends on ')
else:
buffer.append('requires ')
buffer.append(
' or '.join([
self._terse(term) for term in latter.terms
if not term.is_positive()
])
)
if latter_line is not None:
buffer.append(' ({})'.format(latter_line))
return ''.join(buffer)
def _try_requires_forbidden(self, other, details, this_line, other_line
): # type: (Incompatibility, dict, int, int) -> str
if len(self._terms) != 1 and len(other.terms) != 1:
return None
if len(self.terms) == 1:
prior = other
latter = self
prior_line = other_line
latter_line = this_line
else:
prior = self
latter = other
prior_line = this_line
latter_line = other_line
negative = prior._single_term_where(lambda term: not term.is_positive())
if negative is None:
return
if not negative.inverse.satisfies(latter.terms[0]):
return
positives = [t for t in prior.terms if t.is_positive()]
buffer = []
if len(positives) > 1:
prior_string = ' or '.join([
self._terse(term) for term in positives
])
buffer.append('if {} then '.format(prior_string))
else:
buffer.append(self._terse(positives[0], allow_every=True))
if isinstance(prior.cause, DependencyCause):
buffer.append(' depends on ')
else:
buffer.append(' requires ')
buffer.append(self._terse(latter.terms[0]) + ' ')
if prior_line is not None:
buffer.append('({}) '.format(prior_line))
if isinstance(latter.cause, PythonCause):
cause = latter.cause # type: PythonCause
buffer.append('which requires Python {}'.format(cause.python_version))
elif isinstance(latter.cause, NoVersionsCause):
buffer.append('which doesn\'t match any versions')
elif isinstance(latter.cause, PackageNotFoundCause):
buffer.append('which doesn\'t exist')
else:
buffer.append('which is forbidden')
if latter_line is not None:
buffer.append(' ({})'.format(latter_line))
return ''.join(buffer)
def _terse(self, term, allow_every=False):
if allow_every and term.constraint.is_any():
return 'every version of {}'.format(term.dependency.name)
return str(term.dependency)
def _single_term_where(self, callable): # type: (callable) -> Term
found = None
for term in self._terms:
if not callable(term):
continue
if found is not None:
return
found = term
return found
def __repr__(self):
return '<Incompatibility {}>'.format(str(self))
class IncompatibilityCause(Exception):
"""
The reason and Incompatibility's terms are incompatible.
"""
class RootCause(IncompatibilityCause):
pass
class NoVersionsCause(IncompatibilityCause):
pass
class DependencyCause(IncompatibilityCause):
pass
class ConflictCause(IncompatibilityCause):
"""
The incompatibility was derived from two existing incompatibilities
during conflict resolution.
"""
def __init__(self, conflict, other):
self._conflict = conflict
self._other = other
@property
def conflict(self):
return self._conflict
@property
def other(self):
return self._other
def __str__(self):
return str(self._conflict)
class PythonCause(IncompatibilityCause):
"""
The incompatibility represents a package's python constraint
(Python versions) being incompatible
with the current python version.
"""
def __init__(self, python_version):
self._python_version = python_version
@property
def python_version(self):
return self._python_version
class PlatformCause(IncompatibilityCause):
"""
The incompatibility represents a package's platform constraint
(OS most likely) being incompatible with the current platform.
"""
def __init__(self, platform):
self._platform = platform
@property
def platform(self):
return self._platform
class PackageNotFoundCause(IncompatibilityCause):
"""
The incompatibility represents a package that couldn't be found by its
source.
"""
def __init__(self, error):
self._error = error
@property
def error(self):
return self._error
from collections import OrderedDict
from typing import Any
from typing import Dict
from typing import List
from poetry.packages import Dependency
from poetry.packages import Package
from .assignment import Assignment
from .incompatibility import Incompatibility
from .set_relation import SetRelation
from .term import Term
class PartialSolution:
"""
# A list of Assignments that represent the solver's current best guess about
# what's true for the eventual set of package versions that will comprise the
# total solution.
#
# See https://github.com/dart-lang/mixology/tree/master/doc/solver.md#partial-solution.
"""
def __init__(self):
# The assignments that have been made so far, in the order they were
# assigned.
self._assignments = [] # type: List[Assignment]
# The decisions made for each package.
self._decisions = OrderedDict() # type: Dict[str, Package]
# The intersection of all positive Assignments for each package, minus any
# negative Assignments that refer to that package.
#
# This is derived from self._assignments.
self._positive = OrderedDict() # type: Dict[str, Term]
# The union of all negative Assignments for each package.
#
# If a package has any positive Assignments, it doesn't appear in this
# map.
#
# This is derived from self._assignments.
self._negative = OrderedDict() # type: Dict[str, Dict[str, Term]]
# The number of distinct solutions that have been attempted so far.
self._attempted_solutions = 1
# Whether the solver is currently backtracking.
self._backtracking = False
@property
def decisions(self): # type: () -> List[Package]
return list(self._decisions.values())
@property
def decision_level(self): # type: () -> int
return len(self._decisions)
@property
def attempted_solutions(self): # type: () -> int
return self._attempted_solutions
@property
def unsatisfied(self): # type: () -> List[Dependency]
return [
term.dependency
for term in self._positive.values()
if term.dependency.name not in self._decisions
]
def decide(self, package): # type: (Package) -> None
"""
Adds an assignment of package as a decision
and increments the decision level.
"""
# When we make a new decision after backtracking, count an additional
# attempted solution. If we backtrack multiple times in a row, though, we
# only want to count one, since we haven't actually started attempting a
# new solution.
if self._backtracking:
self._attempted_solutions += 1
self._backtracking = False
self._decisions[package.name] = package
self._assign(Assignment.decision(package, self.decision_level, len(self._assignments)))
def derive(self, dependency, is_positive, cause
): # type: (Dependency, bool, Incompatibility) -> None
"""
Adds an assignment of package as a derivation.
"""
self._assign(
Assignment.derivation(
dependency, is_positive, cause,
self.decision_level, len(self._assignments)
)
)
def _assign(self, assignment): # type: (Assignment) -> None
"""
Adds an Assignment to _assignments and _positive or _negative.
"""
self._assignments.append(assignment)
self._register(assignment)
def backtrack(self, decision_level): # type: (int) -> None
"""
Resets the current decision level to decision_level, and removes all
assignments made after that level.
"""
self._backtracking = True
packages = set()
while self._assignments[-1].decision_level > decision_level:
removed = self._assignments.pop(-1)
packages.add(removed.dependency.name)
if removed.is_decision():
del self._decisions[removed.dependency.name]
# Re-compute _positive and _negative for the packages that were removed.
for package in packages:
if package in self._positive:
del self._positive[package]
if package in self._negative:
del self._negative[package]
for assignment in self._assignments:
if assignment.dependency.name in packages:
self._register(assignment)
def _register(self, assignment): # type: (Assignment) -> None
"""
Registers an Assignment in _positive or _negative.
"""
name = assignment.dependency.name
old_positive = self._positive.get(name)
if old_positive is not None:
self._positive[name] = old_positive.intersect(assignment)
return
ref = assignment.dependency.name
negative_by_ref = self._negative.get(name)
old_negative = None if negative_by_ref is None else negative_by_ref.get(ref)
if old_negative is None:
term = assignment
else:
term = assignment.intersect(old_negative)
if term.is_positive():
if name in self._negative:
del self._negative[name]
self._positive[name] = term
else:
if name not in self._negative:
self._negative[name] = {}
self._negative[name][ref] = term
def satisfier(self, term): # type: (Term) -> Assignment
"""
Returns the first Assignment in this solution such that the sublist of
assignments up to and including that entry collectively satisfies term.
"""
assigned_term = None # type: Term
for assignment in self._assignments:
if assignment.dependency.name != term.dependency.name:
continue
if (
not assignment.dependency.is_root
and not assignment.dependency.name == term.dependency.name
):
if not assignment.is_positive():
continue
assert not term.is_positive()
return assignment
if assigned_term is None:
assigned_term = assignment
else:
assigned_term = assigned_term.intersect(assignment)
# As soon as we have enough assignments to satisfy term, return them.
if assigned_term.satisfies(term):
return assignment
raise RuntimeError('[BUG] {} is not satisfied.'.format(term))
def satisfies(self, term): # type: (Term) -> bool
return self.relation(term) == SetRelation.SUBSET
def relation(self, term): # type: (Term) -> int
positive = self._positive.get(term.dependency.name)
if positive is not None:
return positive.relation(term)
by_ref = self._negative.get(term.dependency.name)
if by_ref is None:
return SetRelation.OVERLAPPING
negative = by_ref[term.dependency.name]
if negative is None:
return SetRelation.OVERLAPPING
return negative.relation(term)
class PossibilitySet:
def __init__(self, dependencies, possibilities):
self.dependencies = dependencies
self.possibilities = possibilities
@property
def latest_version(self):
if self.possibilities:
return self.possibilities[-1]
def __str__(self):
return '[{}]'.format(', '.join([str(p) for p in self.possibilities]))
def __repr__(self):
return '<PossibilitySet {}>'.format(str(self))
# -*- coding: utf-8 -*-
import logging
from copy import copy
from datetime import datetime
from typing import Any
from typing import List
from .contracts import SpecificationProvider
from .contracts import UI
from .exceptions import CircularDependencyError
from .exceptions import VersionConflict
from .conflict import Conflict
from .dependency_graph import DependencyGraph
from .helpers import flat_map
from .possibility_set import PossibilitySet
from .state import DependencyState
from .unwind_details import UnwindDetails
from .utils import unique
logger = logging.getLogger(__name__)
class Resolution:
def __init__(self,
provider, # type: SpecificationProvider
ui, # type: UI
requested, # type: List[Any]
base # type: DependencyGraph
):
self._provider = provider
self._ui = ui
self._requested = requested
self._original_requested = copy(requested)
self._base = base
self._states = []
self._iteration_counter = 0
self._progress_rate = 0.33
self._iteration_rate = None
self._parents_of = {}
self._started_at = None
@property
def provider(self): # type: () -> SpecificationProvider
return self._provider
@property
def ui(self): # type: () -> UI
return self._ui
@property
def requested(self): # type: () -> List[Any]
return self._requested
@property
def base(self): # type: () -> DependencyGraph
return self._base
@property
def activated(self): # type: () -> DependencyGraph
return self.state.activated
def resolve(self): # type: () -> DependencyGraph
"""
Resolve the original requested dependencies into a full
dependency graph.
"""
self._start()
try:
while self.state:
if not self.state.requirement and not self.state.requirements:
break
self._indicate_progress()
if hasattr(self.state, 'pop_possibility_state'):
self._debug(
'Creating possibility state for {} ({} remaining)'
.format(
str(self.state.requirement),
len(self.state.possibilities)
)
)
s = self.state.pop_possibility_state()
if s:
self._states.append(s)
self.activated.tag(s)
self._process_topmost_state()
return self._resolve_activated_specs()
finally:
self._end()
def _start(self): # type: () -> None
"""
Set up the resolution process.
"""
self._started_at = datetime.now()
self._debug(
'Starting resolution ({})\nRequested dependencies: {}'.format(
self._started_at,
[str(d) for d in self._original_requested]
)
)
self._ui.before_resolution()
self._handle_missing_or_push_dependency_state(self._initial_state())
def _resolve_activated_specs(self): # type: () -> DependencyGraph
for vertex in self.activated.vertices.values():
if not vertex.payload:
continue
latest_version = None
for possibility in reversed(list(vertex.payload.possibilities)):
if all(
[
self._provider.is_requirement_satisfied_by(
req, self.activated, possibility
)
for req in vertex.requirements
]
):
latest_version = possibility
break
self.activated.set_payload(vertex.name, latest_version)
return self.activated
def _end(self): # type: () -> None
"""
Ends the resolution process
"""
elapsed = (datetime.now() - self._started_at).total_seconds()
self._ui.after_resolution()
self._debug(
'Finished resolution ({} steps) '
'in {:.3f} seconds'.format(
self._iteration_counter, elapsed
)
)
def _process_topmost_state(self): # type: () -> None
"""
Processes the topmost available RequirementState on the stack.
"""
try:
if self.possibility:
self._attempt_to_activate()
else:
self._create_conflict()
self._unwind_for_conflict()
except CircularDependencyError as e:
self._create_conflict(e)
self._unwind_for_conflict()
@property
def possibility(self): # type: () -> PossibilitySet
"""
The current possibility that the resolution is trying.
"""
if self.state.possibilities:
return self.state.possibilities[-1]
@property
def state(self): # type: () -> DependencyState
"""
The current state the resolution is operating upon.
"""
if self._states:
return self._states[-1]
@property
def name(self): # type: () -> str
return self.state.name
@property
def requirement(self): # type: () -> Any
return self.state.requirement
def _initial_state(self): # type: () -> DependencyState
"""
Create the initial state for the resolution, based upon the
requested dependencies.
"""
graph = DependencyGraph()
for requested in self._original_requested:
vertex = graph.add_vertex(
self._provider.name_for(requested), None, True
)
vertex.explicit_requirements.append(requested)
graph.tag('initial_state')
requirements = self._provider.sort_dependencies(
self._original_requested, graph, {}
)
initial_requirement = None
if requirements:
initial_requirement = requirements.pop(0)
name = None
if initial_requirement:
name = self._provider.name_for(initial_requirement)
return DependencyState(
name,
requirements,
graph,
initial_requirement,
self._possibilities_for_requirement(initial_requirement, graph),
0,
{},
[]
)
def _unwind_for_conflict(self): # type: () -> None
"""
Unwinds the states stack because a conflict has been encountered
"""
details_for_unwind = self._build_details_for_unwind()
unwind_options = self.state.unused_unwind_options
self._debug(
'Unwinding for conflict: '
'{} to {}'.format(
str(self.state.requirement),
details_for_unwind.state_index // 2
),
self.state.depth
)
conflicts = self.state.conflicts
sliced_states = self._states[details_for_unwind.state_index + 1:]
self._states = self._states[:details_for_unwind.state_index + 1]
self._raise_error_unless_state(conflicts)
if sliced_states:
self.activated.rewind_to(
sliced_states[0] or 'initial_state'
)
self.state.conflicts = conflicts
self.state.unused_unwind_options = unwind_options
self._filter_possibilities_after_unwind(details_for_unwind)
index = len(self._states) - 1
for k, l in self._parents_of.items():
self._parents_of[k] = [x for x in l if x < index]
self.state.unused_unwind_options = [
uw
for uw in self.state.unused_unwind_options
if uw.state_index < index
]
def _raise_error_unless_state(self, conflicts): # type: (dict) -> None
"""
Raise a VersionConflict error, or any underlying error,
if there is no current state
"""
if self.state:
return
errors = [c.underlying_error
for c in conflicts.values()
if c.underlying_error is not None]
if errors:
error = errors[0]
else:
error = VersionConflict(conflicts, self._provider)
raise error
def _build_details_for_unwind(self): # type: () -> UnwindDetails
"""
Return the details of the nearest index to which we could unwind.
"""
# Get the possible unwinds for the current conflict
current_conflict = self.state.conflicts[self.state.name]
binding_requirements = self._binding_requirements_for_conflict(
current_conflict
)
unwind_details = self._unwind_options_for_requirements(
binding_requirements
)
last_detail_for_current_unwind = sorted(unwind_details)[-1]
current_detail = last_detail_for_current_unwind
# Look for past conflicts that could be unwound to affect the
# requirement tree for the current conflict
relevant_unused_unwinds = []
for alternative in self.state.unused_unwind_options:
intersecting_requirements = (
set(last_detail_for_current_unwind.all_requirements)
&
set(alternative.requirements_unwound_to_instead)
)
if not intersecting_requirements:
continue
# Find the highest index unwind whilst looping through
if alternative > current_detail:
current_detail = alternative
relevant_unused_unwinds.append(alternative)
# Add the current unwind options to the `unused_unwind_options` array.
# The "used" option will be filtered out during `unwind_for_conflict`.
self.state.unused_unwind_options += [
detail
for detail in unwind_details
if detail.state_index != -1
]
# Update the requirements_unwound
# to_instead on any relevant unused unwinds
for d in relevant_unused_unwinds:
d.requirements_unwound_to_instead.append(
current_detail.state_requirement
)
for d in unwind_details:
d.requirements_unwound_to_instead.append(
current_detail.state_requirement
)
return current_detail
def _unwind_options_for_requirements(self, binding_requirements
): # type: (list) -> List[UnwindDetails]
unwind_details = []
trees = []
for r in reversed(binding_requirements):
partial_tree = [r]
trees.append(partial_tree)
unwind_details.append(
UnwindDetails(
-1, None, partial_tree, binding_requirements, trees, []
)
)
# If this requirement has alternative possibilities,
# check if any would satisfy the other requirements
# that created this conflict
requirement_state = self._find_state_for(r)
if self._conflict_fixing_possibilities(requirement_state,
binding_requirements):
unwind_details.append(
UnwindDetails(
self._states.index(requirement_state),
r,
partial_tree,
binding_requirements,
trees,
[]
)
)
# Next, look at the parent of this requirement,
# and check if the requirement could have been avoided
# if an alternative PossibilitySet had been chosen
parent_r = self._parent_of(r)
if parent_r is None:
continue
partial_tree.insert(0, parent_r)
requirement_state = self._find_state_for(parent_r)
possibilities = [
r.name in map(lambda x: x.name, set_.dependencies)
for set_ in requirement_state.possibilities
]
if any(possibilities):
unwind_details.append(
UnwindDetails(
self._states.index(requirement_state),
parent_r,
partial_tree,
binding_requirements,
trees,
[]
)
)
# Finally, look at the grandparent and up of this requirement,
# looking for any possibilities that wouldn't
# create their parent requirement
grandparent_r = self._parent_of(parent_r)
while grandparent_r is not None:
partial_tree.insert(0, grandparent_r)
requirement_state = self._find_state_for(grandparent_r)
possibilities = [
parent_r.name in map(lambda x: x.name, set_.dependencies)
for set_ in requirement_state.possibilities
]
if any(possibilities):
unwind_details.append(
UnwindDetails(
self._states.index(requirement_state),
grandparent_r,
partial_tree,
binding_requirements,
trees,
[]
)
)
parent_r = grandparent_r
grandparent_r = self._parent_of(parent_r)
return unwind_details
def _conflict_fixing_possibilities(self, state, binding_requirements):
"""
Return whether or not the given state has any possibilities
that could satisfy the given requirements
:rtype: bool
"""
if not state:
return False
return any([
any([
self._possibility_satisfies_requirements(
poss, binding_requirements
)
])
for possibility_set in state.possibilities
for poss in possibility_set.possibilities
])
def _filter_possibilities_after_unwind(self, unwind_details):
"""
Filter a state's possibilities to remove any that would not fix the
conflict we've just rewound from
:type unwind_details: UnwindDetails
"""
if not self.state or not self.state.possibilities:
return
if unwind_details.unwinding_to_primary_requirement():
self._filter_possibilities_for_primary_unwind(unwind_details)
else:
self._filter_possibilities_for_parent_unwind(unwind_details)
def _filter_possibilities_for_primary_unwind(self, unwind_details):
"""
Filter a state's possibilities to remove any that would not satisfy
the requirements in the conflict we've just rewound from.
:type unwind_details: UnwindDetails
"""
unwinds_to_state = [
uw
for uw in self.state.unused_unwind_options
if uw.state_index == unwind_details.state_index
]
unwinds_to_state.append(unwind_details)
unwind_requirement_sets = [
uw.conflicting_requirements
for uw in unwinds_to_state
]
possibilities = []
for possibility_set in self.state.possibilities:
if not any([
any([
self._possibility_satisfies_requirements(
poss, requirements
)
])
for poss in possibility_set.possibilities
for requirements in unwind_requirement_sets
]):
continue
possibilities.append(possibility_set)
self.state.possibilities = possibilities
def _possibility_satisfies_requirements(self, possibility, requirements):
name = self._provider.name_for(possibility)
self.activated.tag('swap')
if self.activated.vertex_named(name):
self.activated.set_payload(name, possibility)
satisfied = all([
self._provider.is_requirement_satisfied_by(
r, self.activated, possibility
)
for r in requirements
])
self.activated.rewind_to('swap')
return satisfied
def _filter_possibilities_for_parent_unwind(self,
unwind_details # type: UnwindDetails
):
"""
Filter a state's possibilities to remove any that would (eventually)
the requirements in the conflict we've just rewound from.
"""
unwinds_to_state = [
uw
for uw in self.state.unused_unwind_options
if uw.state_index == unwind_details.state_index
]
unwinds_to_state.append(unwind_details)
primary_unwinds = unique([
uw
for uw in unwinds_to_state
if uw.unwinding_to_primary_requirement()
])
parent_unwinds = unique(unwinds_to_state)
parent_unwinds = [uw for uw in parent_unwinds if uw not in primary_unwinds]
allowed_possibility_sets = []
for unwind in primary_unwinds:
for possibility_set in self._states[unwind.state_index].possibilities:
if any([
self._possibility_satisfies_requirements(
poss, unwind.conflicting_requirements
)
for poss in possibility_set.possibilities
]):
allowed_possibility_sets.append(possibility_set)
requirements_to_avoid = list(flat_map(
parent_unwinds,
lambda x: x.sub_dependencies_to_avoid
))
possibilities = []
for possibility_set in self.state.possibilities:
if (
possibility_set in allowed_possibility_sets
or [
r
for r in requirements_to_avoid
if r not in possibility_set.dependencies
]
):
possibilities.append(possibility_set)
self.state.possibilities = possibilities
def _binding_requirements_for_conflict(self, conflict):
"""
Return the minimal list of requirements that would cause the passed
conflict to occur.
:rtype: list
"""
if conflict.possibility is None:
return [conflict.requirement]
possible_binding_requirements_set = list(conflict.requirements.values())
possible_binding_requirements = []
for reqs in possible_binding_requirements_set:
if isinstance(reqs, list):
possible_binding_requirements += reqs
else:
possible_binding_requirements.append(reqs)
possible_binding_requirements = unique(possible_binding_requirements)
# When there’s a `CircularDependency` error the conflicting requirement
# (the one causing the circular) won’t be `conflict.requirement`
# (which won’t be for the right state, because we won’t have created it,
# because it’s circular).
# We need to make sure we have that requirement in the conflict’s list,
# otherwise we won’t be able to unwind properly, so we just return all
# the requirements for the conflict.
if conflict.underlying_error:
return possible_binding_requirements
possibilities = self._provider.search_for(conflict.requirement)
# If all the requirements together don't filter out all possibilities,
# then the only two requirements we need to consider are the initial one
# (where the dependency's version was first chosen) and the last
if self._binding_requirement_in_set(
None, possible_binding_requirements,
possibilities
):
return list(filter(None, [
conflict.requirement,
self._requirement_for_existing_name(
self._provider.name_for(conflict.requirement)
)
]))
# Loop through the possible binding requirements, removing each one
# that doesn't bind. Use a reversed as we want the earliest set of
# binding requirements.
binding_requirements = copy(possible_binding_requirements)
for req in reversed(possible_binding_requirements):
if req == conflict.requirement:
continue
if not self._binding_requirement_in_set(
req, binding_requirements, possibilities
):
index = binding_requirements.index(req)
del binding_requirements[index]
return binding_requirements
def _binding_requirement_in_set(self,
requirement,
possible_binding_requirements,
possibilities): # type: () -> bool
"""
Return whether or not the given requirement is required
to filter out all elements of the list of possibilities.
"""
return any([
self._possibility_satisfies_requirements(
poss,
set(possible_binding_requirements) - set([requirement])
)
for poss in possibilities
])
def _parent_of(self, requirement):
if not requirement:
return
if requirement not in self._parents_of:
self._parents_of[requirement] = []
if not self._parents_of[requirement]:
return
try:
index = self._parents_of[requirement][-1]
except ValueError:
return
try:
parent_state = self._states[index]
except ValueError:
return
return parent_state.requirement
def _requirement_for_existing_name(self, name):
vertex = self.activated.vertex_named(name)
if not vertex:
return
if not vertex.payload:
return
for s in self._states:
if s.name == name:
return s.requirement
def _find_state_for(self, requirement):
if not requirement:
return
for s in self._states:
if s.requirement == requirement:
return s
def _create_conflict(self, underlying_error=None):
vertex = self.activated.vertex_named(self.state.name)
locked_requirement = self._locked_requirement_named(self.state.name)
requirements = {}
if vertex.explicit_requirements:
requirements[self._provider.name_for_explicit_dependency_source] = vertex.explicit_requirements
if locked_requirement:
requirements[self._provider.name_for_locking_dependency_source] = [locked_requirement]
for edge in vertex.incoming_edges:
if edge.origin.payload.latest_version not in requirements:
requirements[edge.origin.payload.latest_version] = []
requirements[edge.origin.payload.latest_version].insert(0, edge.requirement)
activated_by_name = {}
for v in self.activated:
if v.payload:
activated_by_name[v.name] = v.payload.latest_version
conflict = Conflict(
self.requirement,
requirements,
vertex.payload.latest_version if vertex.payload else None,
self.possibility,
locked_requirement,
self.requirement_trees,
activated_by_name,
underlying_error
)
self.state.conflicts[self.name] = conflict
return conflict
@property
def requirement_trees(self):
vertex = self.activated.vertex_named(self.state.name)
return [self._requirement_tree_for(r) for r in vertex.requirements]
def _requirement_tree_for(self, requirement):
tree = []
while requirement:
tree.insert(0, requirement)
requirement = self._parent_of(requirement)
return tree
def _indicate_progress(self):
self._iteration_counter += 1
progress_rate = self._ui.progress_rate or self._progress_rate
if self._iteration_rate is None:
if (datetime.now() - self._started_at).total_seconds() >= progress_rate:
self._iteration_rate = self._iteration_counter
if self._iteration_rate and (self._iteration_counter % self._iteration_rate) == 0:
self._ui.indicate_progress()
def _debug(self, message, depth=0):
self._ui.debug(message, depth)
def _attempt_to_activate(self):
self._debug(
'Attempting to activate {}'.format(str(self.possibility)),
self.state.depth,
)
existing_vertex = self.activated.vertex_named(self.state.name)
if existing_vertex.payload:
self._debug(
'Found existing spec ({})'.format(existing_vertex.payload),
self.state.depth
)
self._attempt_to_filter_existing_spec(existing_vertex)
else:
latest = self.possibility.latest_version
possibilities = []
for possibility in self.possibility.possibilities:
if self._provider.is_requirement_satisfied_by(
self.requirement, self.activated, possibility
):
possibilities.append(possibility)
self.possibility.possibilities = possibilities
if self.possibility.latest_version is None:
# ensure there's a possibility for better error messages
if latest:
self.possibility.possibilities.append(latest)
self._create_conflict()
self._unwind_for_conflict()
else:
self._activate_new_spec()
def _attempt_to_filter_existing_spec(self, vertex):
"""
Attempt to update the existing vertex's
`PossibilitySet` with a filtered version.
"""
filtered_set = self._filtered_possibility_set(vertex)
if filtered_set.possibilities:
self.activated.set_payload(self.name, filtered_set)
new_requirements = copy(self.state.requirements)
self._push_state_for_requirements(new_requirements, False)
else:
self._create_conflict()
self._debug(
'Unsatisfied by existing spec ({})'.format(str(vertex.payload)),
self.state.depth
)
self._unwind_for_conflict()
def _filtered_possibility_set(self, vertex):
possibilities = [
p
for p in vertex.payload.possibilities
if p in self.possibility.possibilities
]
return PossibilitySet(
vertex.payload.dependencies,
possibilities
)
def _locked_requirement_named(self, requirement_name):
vertex = self.base.vertex_named(requirement_name)
if vertex:
return vertex.payload
def _activate_new_spec(self):
if self.state.name in self.state.conflicts:
del self.state.conflicts[self.name]
self._debug(
'Activated {} at {}'.format(self.state.name, str(self.possibility)),
self.state.depth
)
self.activated.set_payload(self.state.name, self.possibility)
self._require_nested_dependencies_for(self.possibility)
def _require_nested_dependencies_for(self, possibility_set):
nested_dependencies = self._provider.dependencies_for(
possibility_set.latest_version
)
self._debug(
'Requiring nested dependencies '
'({})'.format(', '.join([str(d) for d in nested_dependencies])),
self.state.depth
)
for d in nested_dependencies:
self.activated.add_child_vertex(
self._provider.name_for(d),
None,
[self._provider.name_for(possibility_set.latest_version)],
d
)
parent_index = len(self._states) - 1
if d not in self._parents_of:
self._parents_of[d] = []
parents = self._parents_of[d]
if not parents:
parents.append(parent_index)
self._push_state_for_requirements(
self.state.requirements + nested_dependencies,
len(nested_dependencies) > 0
)
def _push_state_for_requirements(self,
new_requirements,
requires_sort=True,
new_activated=None):
if new_activated is None:
new_activated = self.activated
if requires_sort:
new_requirements = self._provider.sort_dependencies(
unique(new_requirements), new_activated, self.state.conflicts
)
while True:
new_requirement = None
if new_requirements:
new_requirement = new_requirements.pop(0)
if (
new_requirement is None
or not any([
s.requirement == new_requirement
for s in self._states
])
):
break
new_name = ''
if new_requirement:
new_name = self._provider.name_for(new_requirement)
possibilities = self._possibilities_for_requirement(new_requirement)
self._handle_missing_or_push_dependency_state(
DependencyState(
new_name, new_requirements, new_activated,
new_requirement, possibilities, self.state.depth,
copy(self.state.conflicts),
copy(self.state.unused_unwind_options)
)
)
def _possibilities_for_requirement(self, requirement, activated=None):
if activated is None:
activated = self.activated
if not requirement:
return []
if self._locked_requirement_named(self._provider.name_for(requirement)):
return self._locked_requirement_possibility_set(
requirement, activated
)
return self._group_possibilities(
self._provider.search_for(requirement)
)
def _locked_requirement_possibility_set(self, requirement, activated=None):
if activated is None:
activated = self.activated
all_possibilities = self._provider.search_for(requirement)
locked_requirement = self._locked_requirement_named(
self._provider.name_for(requirement)
)
# Longwinded way to build a possibilities list with either the locked
# requirement or nothing in it. Required, since the API for
# locked_requirement isn't guaranteed.
locked_possibilities = [
possibility
for possibility in all_possibilities
if self._provider.is_requirement_satisfied_by(
locked_requirement, activated, possibility
)
]
return self._group_possibilities(locked_possibilities)
def _group_possibilities(self, possibilities):
possibility_sets = []
current_possibility_set = None
for possibility in reversed(possibilities):
dependencies = self._provider.dependencies_for(possibility)
if current_possibility_set and current_possibility_set.dependencies == dependencies:
current_possibility_set.possibilities.insert(0, possibility)
else:
possibility_sets.insert(
0, PossibilitySet(dependencies, [possibility])
)
current_possibility_set = possibility_sets[0]
return possibility_sets
def _handle_missing_or_push_dependency_state(self, state):
if (
state.requirement
and not state.possibilities
and self._provider.allow_missing(state.requirement)
):
state.activated.detach_vertex_named(state.name)
self._push_state_for_requirements(
copy(state.requirements), False, state.activated
)
else:
self._states.append(state)
state.activated.tag(state)
from typing import Any
from typing import List
from typing import Union
from .contracts import SpecificationProvider
from .contracts import UI
from .dependency_graph import DependencyGraph
from .resolution import Resolution
class Resolver:
def __init__(self,
specification_provider, # type: SpecificationProvider
resolver_ui # type: UI
):
self._specification_provider = specification_provider
self._resolver_ui = resolver_ui
@property
def specification_provider(self): # type: () -> SpecificationProvider
return self._specification_provider
@property
def ui(self): # type: () -> UI
return self._resolver_ui
def resolve(self,
requested, # type: List[Any]
base=None # type: Union[DependencyGraph, None]
): # type: (...) -> DependencyGraph
if base is None:
base = DependencyGraph()
return Resolution(
self._specification_provider,
self._resolver_ui,
requested,
base
).resolve()
class SolverResult:
def __init__(self, root, packages, attempted_solutions):
self._root = root
self._packages = packages
self._attempted_solutions = attempted_solutions
@property
def packages(self):
return self._packages
@property
def attempted_solutions(self):
return self._attempted_solutions
class SetRelation:
"""
An enum of possible relationships between two sets.
"""
SUBSET = 'subset'
DISJOINT = 'disjoint'
OVERLAPPING = 'overlapping'
from copy import copy
from .dependency_graph import DependencyGraph
class ResolutionState:
def __init__(self, name, requirements, activated,
requirement, possibilities, depth,
conflicts, unused_unwind_options):
self._name = name
self._requirements = requirements
self._activated = activated
self._requirement = requirement
self.possibilities = possibilities
self._depth = depth
self.conflicts = conflicts
self.unused_unwind_options = unused_unwind_options
@property
def name(self):
return self._name
@property
def requirements(self):
return self._requirements
@property
def activated(self):
return self._activated
@property
def requirement(self):
return self._requirement
@property
def depth(self):
return self._depth
@classmethod
def empty(cls):
return cls(None, [], DependencyGraph(), None, None, 0, {}, [])
def __repr__(self):
return '<{} {} ({})>'.format(
self.__class__.__name__,
self._name,
str(self.requirement)
)
class PossibilityState(ResolutionState):
pass
class DependencyState(ResolutionState):
def pop_possibility_state(self):
state = PossibilityState(
self._name,
copy(self._requirements),
self._activated,
self._requirement,
[self.possibilities.pop() if self.possibilities else None],
self._depth + 1,
copy(self.conflicts),
copy(self.unused_unwind_options)
)
state.activated.tag(state)
return state
# -*- coding: utf-8 -*-
from typing import Union
from poetry.packages import Dependency
from .set_relation import SetRelation
class Term(object):
"""
A statement about a package which is true or false for a given selection of
package versions.
See https://github.com/dart-lang/pub/tree/master/doc/solver.md#term.
"""
def __init__(self,
dependency, # type: Dependency
is_positive # type: bool
):
self._dependency = dependency
self._positive = is_positive
@property
def inverse(self): # type: () -> Term
return Term(self._dependency, not self.is_positive())
@property
def dependency(self):
return self._dependency
@property
def constraint(self):
return self._dependency.constraint
def is_positive(self): # type: () -> bool
return self._positive
def satisfies(self, other): # type: (Term) -> bool
"""
Returns whether this term satisfies another.
"""
return (
self.dependency.name == other.dependency.name
and self.relation(other) == SetRelation.SUBSET
)
def relation(self, other): # type: (Term) -> int
"""
Returns the relationship between the package versions
allowed by this term and another.
"""
if self.dependency.name != other.dependency.name:
raise ValueError('{} should refer to {}'.format(other, self.dependency.name))
other_constraint = other.constraint
if other.is_positive():
if self.is_positive():
if not self._compatible_dependency(other.dependency):
return SetRelation.DISJOINT
# foo ^1.5.0 is a subset of foo ^1.0.0
if other_constraint.allows_all(self.constraint):
return SetRelation.SUBSET
# foo ^2.0.0 is disjoint with foo ^1.0.0
if not self.constraint.allows_any(other_constraint):
return SetRelation.DISJOINT
return SetRelation.OVERLAPPING
else:
if not self._compatible_dependency(other.dependency):
return SetRelation.OVERLAPPING
# not foo ^1.0.0 is disjoint with foo ^1.5.0
if self.constraint.allows_all(other_constraint):
return SetRelation.DISJOINT
# not foo ^1.5.0 overlaps foo ^1.0.0
# not foo ^2.0.0 is a superset of foo ^1.5.0
return SetRelation.OVERLAPPING
else:
if self.is_positive():
if not self._compatible_dependency(other.dependency):
return SetRelation.SUBSET
# foo ^2.0.0 is a subset of not foo ^1.0.0
if not other_constraint.allows_any(self.constraint):
return SetRelation.SUBSET
# foo ^1.5.0 is disjoint with not foo ^1.0.0
if other_constraint.allows_all(self.constraint):
return SetRelation.DISJOINT
# foo ^1.0.0 overlaps not foo ^1.5.0
return SetRelation.OVERLAPPING
else:
if not self._compatible_dependency(other.dependency):
return SetRelation.OVERLAPPING
# not foo ^1.0.0 is a subset of not foo ^1.5.0
if self.constraint.allows_all(other_constraint):
return SetRelation.SUBSET
# not foo ^2.0.0 overlaps not foo ^1.0.0
# not foo ^1.5.0 is a superset of not foo ^1.0.0
return SetRelation.OVERLAPPING
def intersect(self, other): # type: (Term) -> Union[Term, None]
"""
Returns a Term that represents the packages
allowed by both this term and another
"""
if self.dependency.name != other.dependency.name:
raise ValueError('{} should refer to {}'.format(other, self.dependency.name))
if self._compatible_dependency(other.dependency):
if self.is_positive() != other.is_positive():
# foo ^1.0.0 ∩ not foo ^1.5.0 → foo >=1.0.0 <1.5.0
positive = self if self.is_positive() else other
negative = other if self.is_positive() else self
return self._non_empty_term(
positive.constraint.difference(negative.constraint),
True
)
elif self.is_positive():
# foo ^1.0.0 ∩ foo >=1.5.0 <3.0.0 → foo ^1.5.0
return self._non_empty_term(
self.constraint.intersect(other.constraint),
True
)
else:
# not foo ^1.0.0 ∩ not foo >=1.5.0 <3.0.0 → not foo >=1.0.0 <3.0.0
return self._non_empty_term(
self.constraint.union(other.constraint),
False
)
elif self.is_positive() != other.is_positive():
return self if self.is_positive() else other
else:
return
def difference(self, other): # type: (Term) -> Term
"""
Returns a Term that represents packages
allowed by this term and not by the other
"""
return self.intersect(other.inverse)
def _compatible_dependency(self, other):
return (
self.dependency.is_root
or other.is_root
or (
other.name == self.dependency.name
)
)
def _non_empty_term(self, constraint, is_positive):
if constraint.is_empty():
return
return Term(Dependency(self.dependency.name, constraint), is_positive)
def __str__(self):
return '{}{}'.format(
'not ' if not self.is_positive() else '',
self._dependency
)
def __repr__(self):
return '<Term {}>'.format(str(self))
class UnwindDetails:
def __init__(self,
state_index,
state_requirement,
requirement_tree,
conflicting_requirements,
requirement_trees,
requirements_unwound_to_instead):
self.state_index = state_index
self.state_requirement = state_requirement
self.requirement_tree = requirement_tree
self.conflicting_requirements = conflicting_requirements
self.requirement_trees = requirement_trees
self.requirements_unwound_to_instead = requirements_unwound_to_instead
self._reversed_requirement_tree_index = None
self._sub_dependencies_to_avoid = None
self._all_requirements = None
@property
def reversed_requirement_tree_index(self):
if self._reversed_requirement_tree_index is None:
if self.state_requirement:
self._reversed_requirement_tree_index = list(reversed(
self.requirement_tree
)).index(self.state_requirement)
else:
self._reversed_requirement_tree_index = 999999
return self._reversed_requirement_tree_index
def unwinding_to_primary_requirement(self):
return self.requirement_tree[-1] == self.state_requirement
@property
def sub_dependencies_to_avoid(self):
if self._sub_dependencies_to_avoid is None:
self._sub_dependencies_to_avoid = []
for tree in self.requirement_trees:
try:
index = tree.index(self.state_requirement)
except ValueError:
continue
if tree[index + 1] is not None:
self._sub_dependencies_to_avoid.append(tree[index + 1])
return self._sub_dependencies_to_avoid
@property
def all_requirements(self):
if self._all_requirements is None:
self._all_requirements = [
x
for tree in self.requirement_trees
for x in tree
]
return self._all_requirements
def __eq__(self, other):
if not isinstance(other, UnwindDetails):
return NotImplemented
return (
self.state_index == other.state_index
and (
self.reversed_requirement_tree_index
== other.reversed_requirement_tree_index
)
)
def __lt__(self, other):
if not isinstance(other, UnwindDetails):
return NotImplemented
return self.state_index < other.state_index
def __le__(self, other):
if not isinstance(other, UnwindDetails):
return NotImplemented
return self.state_index <= other.state_index
def __gt__(self, other):
if not isinstance(other, UnwindDetails):
return NotImplemented
return self.state_index > other.state_index
def __ge__(self, other):
if not isinstance(other, UnwindDetails):
return NotImplemented
return self.state_index >= other.state_index
def __hash__(self):
return hash((id(self), self.state_index, self.state_requirement))
def unique(l):
used = set()
return [x for x in l if x not in used and (used.add(x) or True)]
# -*- coding: utf-8 -*-
import time
from typing import Dict
from typing import List
from typing import Union
from poetry.packages import Dependency
from poetry.packages import ProjectPackage
from poetry.packages import Package
from poetry.puzzle.provider import Provider
from poetry.semver import Version
from poetry.semver import VersionRange
from .failure import SolveFailure
from .incompatibility import Incompatibility
from .incompatibility_cause import ConflictCause
from .incompatibility_cause import NoVersionsCause
from .incompatibility_cause import PackageNotFoundCause
from .incompatibility_cause import RootCause
from .partial_solution import PartialSolution
from .result import SolverResult
from .set_relation import SetRelation
from .term import Term
_conflict = object()
class VersionSolver:
"""
The version solver that finds a set of package versions that satisfy the
root package's dependencies.
See https://github.com/dart-lang/pub/tree/master/doc/solver.md for details
on how this solver works.
"""
def __init__(self,
root, # type: ProjectPackage
provider, # type: Provider
locked=None, # type: Dict[str, Package]
use_latest=None # type: List[str]
):
self._root = root
self._provider = provider
self._locked = locked or {}
if use_latest is None:
use_latest = []
self._use_latest = use_latest
self._incompatibilities = {} # type: Dict[str, List[Incompatibility]]
self._solution = PartialSolution()
@property
def solution(self): # type: () -> PartialSolution
return self._solution
def solve(self): # type: () -> SolverResult
"""
Finds a set of dependencies that match the root package's constraints,
or raises an error if no such set is available.
"""
start = time.time()
root_dependency = Dependency(self._root.name, self._root.version)
root_dependency.is_root = True
self._add_incompatibility(
Incompatibility(
[Term(root_dependency, False)],
RootCause()
)
)
try:
next = self._root.name
while next is not None:
self._propagate(next)
next = self._choose_package_version()
return self._result()
except Exception:
raise
finally:
self._log(
'Version solving took {:.3f} seconds.\n'
'Tried {} solutions.'
.format(
time.time() - start,
self._solution.attempted_solutions
)
)
def _propagate(self, package): # type: (str) -> None
"""
Performs unit propagation on incompatibilities transitively
related to package to derive new assignments for _solution.
"""
changed = set()
changed.add(package)
while changed:
package = changed.pop()
# Iterate in reverse because conflict resolution tends to produce more
# general incompatibilities as time goes on. If we look at those first,
# we can derive stronger assignments sooner and more eagerly find
# conflicts.
for incompatibility in reversed(self._incompatibilities[package]):
result = self._propagate_incompatibility(incompatibility)
if result is _conflict:
# If the incompatibility is satisfied by the solution, we use
# _resolve_conflict() to determine the root cause of the conflict as a
# new incompatibility.
#
# It also backjumps to a point in the solution
# where that incompatibility will allow us to derive new assignments
# that avoid the conflict.
root_cause = self._resolve_conflict(incompatibility)
# Back jumping erases all the assignments we did at the previous
# decision level, so we clear [changed] and refill it with the
# newly-propagated assignment.
changed.clear()
changed.add(str(self._propagate_incompatibility(root_cause)))
break
elif result is not None:
changed.add(result)
def _propagate_incompatibility(self, incompatibility
): # type: (Incompatibility) -> Union[str, _conflict, None]
"""
If incompatibility is almost satisfied by _solution, adds the
negation of the unsatisfied term to _solution.
If incompatibility is satisfied by _solution, returns _conflict. If
incompatibility is almost satisfied by _solution, returns the
unsatisfied term's package name.
Otherwise, returns None.
"""
# The first entry in incompatibility.terms that's not yet satisfied by
# _solution, if one exists. If we find more than one, _solution is
# inconclusive for incompatibility and we can't deduce anything.
unsatisfied = None
for term in incompatibility.terms:
relation = self._solution.relation(term)
if relation == SetRelation.DISJOINT:
# If term is already contradicted by _solution, then
# incompatibility is contradicted as well and there's nothing new we
# can deduce from it.
return
elif relation == SetRelation.OVERLAPPING:
# If more than one term is inconclusive, we can't deduce anything about
# incompatibility.
if unsatisfied is not None:
return
# If exactly one term in incompatibility is inconclusive, then it's
# almost satisfied and [term] is the unsatisfied term. We can add the
# inverse of the term to _solution.
unsatisfied = term
# If *all* terms in incompatibility are satisfied by _solution, then
# incompatibility is satisfied and we have a conflict.
if unsatisfied is None:
return _conflict
self._log(
'<fg=blue>derived</>: {}{}'.format(
'not ' if unsatisfied.is_positive() else '',
unsatisfied.dependency
)
)
self._solution.derive(
unsatisfied.dependency,
not unsatisfied.is_positive(),
incompatibility
)
return unsatisfied.dependency.name
def _resolve_conflict(self, incompatibility
): # type: (Incompatibility) -> Incompatibility
"""
Given an incompatibility that's satisfied by _solution,
The `conflict resolution`_ constructs a new incompatibility that encapsulates the root
cause of the conflict and backtracks _solution until the new
incompatibility will allow _propagate() to deduce new assignments.
Adds the new incompatibility to _incompatibilities and returns it.
.. _conflict resolution: https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution
"""
self._log('<fg=red;options=bold>conflict</>: {}'.format(incompatibility))
new_incompatibility = False
while not incompatibility.is_failure():
# The term in incompatibility.terms that was most recently satisfied by
# _solution.
most_recent_term = None
# The earliest assignment in _solution such that incompatibility is
# satisfied by _solution up to and including this assignment.
most_recent_satisfier = None
# The difference between most_recent_satisfier and most_recent_term;
# that is, the versions that are allowed by most_recent_satisfier and not
# by most_recent_term. This is None if most_recent_satisfier totally
# satisfies most_recent_term.
difference = None
# The decision level of the earliest assignment in _solution *before*
# most_recent_satisfier such that incompatibility is satisfied by
# _solution up to and including this assignment plus
# most_recent_satisfier.
#
# Decision level 1 is the level where the root package was selected. It's
# safe to go back to decision level 0, but stopping at 1 tends to produce
# better error messages, because references to the root package end up
# closer to the final conclusion that no solution exists.
previous_satisfier_level = 1
for term in incompatibility.terms:
satisfier = self._solution.satisfier(term)
if most_recent_satisfier is None:
most_recent_term = term
most_recent_satisfier= satisfier
elif most_recent_satisfier.index < satisfier.index:
previous_satisfier_level = max(
previous_satisfier_level,
most_recent_satisfier.decision_level
)
most_recent_term = term
most_recent_satisfier = satisfier
difference = None
else:
previous_satisfier_level = max(
previous_satisfier_level, satisfier.decision_level
)
if most_recent_term == term:
# If most_recent_satisfier doesn't satisfy most_recent_term on its
# own, then the next-most-recent satisfier may be the one that
# satisfies the remainder.
difference = most_recent_satisfier.difference(most_recent_term)
if difference is not None:
previous_satisfier_level = max(
previous_satisfier_level,
self._solution.satisfier(difference.inverse).decision_level
)
# If most_recent_identifier is the only satisfier left at its decision
# level, or if it has no cause (indicating that it's a decision rather
# than a derivation), then incompatibility is the root cause. We then
# backjump to previous_satisfier_level, where incompatibility is
# guaranteed to allow _propagate to produce more assignments.
if (
previous_satisfier_level < most_recent_satisfier.decision_level
or most_recent_satisfier.cause is None
):
self._solution.backtrack(previous_satisfier_level)
if new_incompatibility:
self._add_incompatibility(incompatibility)
return incompatibility
# Create a new incompatibility by combining incompatibility with the
# incompatibility that caused most_recent_satisfier to be assigned. Doing
# this iteratively constructs an incompatibility that's guaranteed to be
# true (that is, we know for sure no solution will satisfy the
# incompatibility) while also approximating the intuitive notion of the
# "root cause" of the conflict.
new_terms = []
for term in incompatibility.terms:
if term != most_recent_term:
new_terms.append(term)
for term in most_recent_satisfier.cause.terms:
if term.dependency != most_recent_satisfier.dependency:
new_terms.append(term)
# The most_recent_satisfier may not satisfy most_recent_term on its own
# if there are a collection of constraints on most_recent_term that
# only satisfy it together. For example, if most_recent_term is
# `foo ^1.0.0` and _solution contains `[foo >=1.0.0,
# foo <2.0.0]`, then most_recent_satisfier will be `foo <2.0.0` even
# though it doesn't totally satisfy `foo ^1.0.0`.
#
# In this case, we add `not (most_recent_satisfier \ most_recent_term)` to
# the incompatibility as well, See the `algorithm documentation`_ for
# details.
#
# .. _algorithm documentation: https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution
if difference is not None:
new_terms.append(difference.inverse)
incompatibility = Incompatibility(
new_terms, ConflictCause(incompatibility, most_recent_satisfier.cause)
)
new_incompatibility = True
partially = '' if difference is None else ' partially'
bang = '<fg=red>!</>'
self._log('{} {} is{} satisfied by {}'.format(
bang, most_recent_term, partially, most_recent_satisfier)
)
self._log('{} which is caused by "{}"'.format(bang, most_recent_satisfier.cause))
self._log('{} thus: {}'.format(bang, incompatibility))
raise SolveFailure(incompatibility)
def _choose_package_version(self): # type: () -> Union[str, None]
"""
Tries to select a version of a required package.
Returns the name of the package whose incompatibilities should be
propagated by _propagate(), or None indicating that version solving is
complete and a solution has been found.
"""
unsatisfied = self._solution.unsatisfied
if not unsatisfied:
return
# Prefer packages with as few remaining versions as possible,
# so that if a conflict is necessary it's forced quickly.
def _get_min(dependency):
if dependency.name in self._use_latest:
# If we're forced to use the latest version of a package, it effectively
# only has one version to choose from.
return 1
if dependency.name in self._locked:
return 1
try:
return len(self._provider.search_for(dependency))
except ValueError:
return 0
if len(unsatisfied) == 1:
dependency = unsatisfied[0]
else:
dependency = min(*unsatisfied, key=_get_min)
locked = self._get_locked(dependency.name)
if locked is None or not dependency.constraint.allows(locked.version):
try:
packages = self._provider.search_for(dependency)
except ValueError as e:
self._add_incompatibility(
Incompatibility([Term(dependency, True)], PackageNotFoundCause(e))
)
return dependency.name
try:
version = packages[0]
except IndexError:
version = None
else:
version = locked
if version is None:
# If there are no versions that satisfy the constraint,
# add an incompatibility that indicates that.
self._add_incompatibility(
Incompatibility([Term(dependency, True)], NoVersionsCause())
)
return dependency.name
conflict = False
for incompatibility in self._provider.incompatibilities_for(version):
self._add_incompatibility(incompatibility)
# If an incompatibility is already satisfied, then selecting version
# would cause a conflict.
#
# We'll continue adding its dependencies, then go back to
# unit propagation which will guide us to choose a better version.
conflict = conflict or all([
term.dependency.name == dependency.name or self._solution.satisfies(term)
for term in incompatibility.terms
])
if not conflict:
self._solution.decide(version)
self._log('<fg=blue>selecting</> {} ({})'.format(version.name, version.full_pretty_version))
return dependency.name
def _excludes_single_version(self, constraint): # type: (Any) -> bool
return isinstance(VersionRange().difference(constraint), Version)
def _result(self): # type: () -> SolverResult
"""
Creates a #SolverResult from the decisions in _solution
"""
decisions = self._solution.decisions
return SolverResult(
self._root,
[p for p in decisions if not p.is_root()],
self._solution.attempted_solutions
)
def _add_incompatibility(self, incompatibility): # type: (Incompatibility) -> None
self._log("<fg=blue>fact</>: {}".format(incompatibility))
for term in incompatibility.terms:
if term.dependency.name not in self._incompatibilities:
self._incompatibilities[term.dependency.name] = []
if incompatibility in self._incompatibilities[term.dependency.name]:
continue
self._incompatibilities[term.dependency.name].append(incompatibility)
def _get_locked(self, package_name): # type: (str) -> Union[Package, None]
if package_name in self._use_latest:
return
locked = self._locked.get(package_name)
if not locked:
return
for dep in self._root.all_requires:
if dep.name == locked.name:
locked.requires_extras = dep.extras
return locked
def _log(self, text):
self._provider.debug(text, self._solution.attempted_solutions)
...@@ -8,6 +8,7 @@ from .directory_dependency import DirectoryDependency ...@@ -8,6 +8,7 @@ from .directory_dependency import DirectoryDependency
from .file_dependency import FileDependency from .file_dependency import FileDependency
from .locker import Locker from .locker import Locker
from .package import Package from .package import Package
from .project_package import ProjectPackage
from .utils.link import Link from .utils.link import Link
from .utils.utils import convert_markers from .utils.utils import convert_markers
from .utils.utils import group_markers from .utils.utils import group_markers
...@@ -20,6 +21,14 @@ from .vcs_dependency import VCSDependency ...@@ -20,6 +21,14 @@ from .vcs_dependency import VCSDependency
def dependency_from_pep_508(name): def dependency_from_pep_508(name):
# Removing comments
parts = name.split('#', 1)
name = parts[0].strip()
if len(parts) > 1:
rest = parts[1]
if ';' in rest:
name += ';' + rest.split(';', 1)[1]
req = Requirement(name) req = Requirement(name)
if req.marker: if req.marker:
......
class BaseConstraint(object):
def matches(self, provider):
raise NotImplementedError()
def allows_all(self, other):
raise NotImplementedError()
def allows_any(self, other):
raise NotImplementedError()
def difference(self, other):
raise NotImplementedError()
def intersect(self, other):
raise NotImplementedError()
def is_empty(self):
return False
...@@ -8,5 +8,20 @@ class EmptyConstraint(BaseConstraint): ...@@ -8,5 +8,20 @@ class EmptyConstraint(BaseConstraint):
def matches(self, _): def matches(self, _):
return True return True
def is_empty(self):
return True
def allows_all(self, other):
return True
def allows_any(self, other):
return True
def intersect(self, other):
return other
def difference(self, other):
return
def __str__(self): def __str__(self):
return '*' return '*'
import operator import operator
import re import re
from poetry.semver.constraints import EmptyConstraint from .base_constraint import BaseConstraint
from poetry.semver.constraints import MultiConstraint from .empty_constraint import EmptyConstraint
from poetry.semver.constraints.base_constraint import BaseConstraint from .multi_constraint import MultiConstraint
class GenericConstraint(BaseConstraint): class GenericConstraint(BaseConstraint):
...@@ -58,13 +58,8 @@ class GenericConstraint(BaseConstraint): ...@@ -58,13 +58,8 @@ class GenericConstraint(BaseConstraint):
return self._version return self._version
def matches(self, provider): def matches(self, provider):
if not isinstance(provider, (GenericConstraint, EmptyConstraint)): if not isinstance(provider, GenericConstraint):
raise ValueError( return provider.matches(self)
'Generic constraints can only be compared with each other'
)
if isinstance(provider, EmptyConstraint):
return True
is_equal_op = self.OP_EQ is self._operator is_equal_op = self.OP_EQ is self._operator
is_non_equal_op = self.OP_NE is self._operator is_non_equal_op = self.OP_NE is self._operator
......
import poetry.packages import poetry.packages
from poetry.semver.constraints import Constraint from poetry.semver import parse_constraint
from poetry.semver.constraints import EmptyConstraint from poetry.semver import Version
from poetry.semver.constraints import MultiConstraint from poetry.semver import VersionConstraint
from poetry.semver.constraints.base_constraint import BaseConstraint from poetry.semver import VersionUnion
from poetry.semver.version_parser import VersionParser
from poetry.utils.helpers import canonicalize_name from poetry.utils.helpers import canonicalize_name
from .constraints.empty_constraint import EmptyConstraint
from .constraints.generic_constraint import GenericConstraint from .constraints.generic_constraint import GenericConstraint
from .constraints.multi_constraint import MultiConstraint
class Dependency(object): class Dependency(object):
...@@ -21,29 +22,30 @@ class Dependency(object): ...@@ -21,29 +22,30 @@ class Dependency(object):
): ):
self._name = canonicalize_name(name) self._name = canonicalize_name(name)
self._pretty_name = name self._pretty_name = name
self._parser = VersionParser()
try: try:
if not isinstance(constraint, BaseConstraint): if not isinstance(constraint, VersionConstraint):
self._constraint = self._parser.parse_constraints(constraint) self._constraint = parse_constraint(constraint)
else: else:
self._constraint = constraint self._constraint = constraint
except ValueError: except ValueError:
self._constraint = self._parser.parse_constraints('*') self._constraint = parse_constraint('*')
self._pretty_constraint = constraint self._pretty_constraint = str(constraint)
self._optional = optional self._optional = optional
self._category = category self._category = category
self._allows_prereleases = allows_prereleases self._allows_prereleases = allows_prereleases
self._python_versions = '*' self._python_versions = '*'
self._python_constraint = self._parser.parse_constraints('*') self._python_constraint = parse_constraint('*')
self._platform = '*' self._platform = '*'
self._platform_constraint = EmptyConstraint() self._platform_constraint = EmptyConstraint()
self._extras = [] self._extras = []
self._in_extras = [] self._in_extras = []
self.is_root = False
@property @property
def name(self): def name(self):
return self._name return self._name
...@@ -71,7 +73,7 @@ class Dependency(object): ...@@ -71,7 +73,7 @@ class Dependency(object):
@python_versions.setter @python_versions.setter
def python_versions(self, value): def python_versions(self, value):
self._python_versions = value self._python_versions = value
self._python_constraint = self._parser.parse_constraints(value) self._python_constraint = parse_constraint(value)
@property @property
def python_constraint(self): def python_constraint(self):
...@@ -119,7 +121,7 @@ class Dependency(object): ...@@ -119,7 +121,7 @@ class Dependency(object):
""" """
return ( return (
self._name == package.name self._name == package.name
and self._constraint.matches(Constraint('=', package.version)) and self._constraint.allows(package.version)
and (not package.is_prerelease() or self.allows_prereleases()) and (not package.is_prerelease() or self.allows_prereleases())
) )
...@@ -129,11 +131,13 @@ class Dependency(object): ...@@ -129,11 +131,13 @@ class Dependency(object):
if self.extras: if self.extras:
requirement += '[{}]'.format(','.join(self.extras)) requirement += '[{}]'.format(','.join(self.extras))
if isinstance(self.constraint, MultiConstraint): if isinstance(self.constraint, VersionUnion):
requirement += ' ({})'.format(','.join( requirement += ' ({})'.format(','.join(
[str(c).replace(' ', '') for c in self.constraint.constraints] [str(c).replace(' ', '') for c in self.constraint.ranges]
)) ))
elif str(self.constraint) != '*': elif isinstance(self.constraint, Version):
requirement += ' (=={})'.format(self.constraint.text)
elif not self.constraint.is_any():
requirement += ' ({})'.format(str(self.constraint).replace(' ', '')) requirement += ' ({})'.format(str(self.constraint).replace(' ', ''))
# Markers # Markers
...@@ -147,6 +151,13 @@ class Dependency(object): ...@@ -147,6 +151,13 @@ class Dependency(object):
self._create_nested_marker('python_version', python_constraint) self._create_nested_marker('python_version', python_constraint)
) )
if self.platform != '*':
platform_constraint = self.platform_constraint
markers.append(
self._create_nested_marker('sys_platform', platform_constraint)
)
in_extras = ' || '.join(self._in_extras) in_extras = ' || '.join(self._in_extras)
if in_extras and with_extras: if in_extras and with_extras:
markers.append( markers.append(
...@@ -185,10 +196,57 @@ class Dependency(object): ...@@ -185,10 +196,57 @@ class Dependency(object):
parts = [part[1] for part in parts] parts = [part[1] for part in parts]
marker = glue.join(parts) marker = glue.join(parts)
else: elif isinstance(constraint, GenericConstraint):
marker = '{} {} "{}"'.format( marker = '{} {} "{}"'.format(
name, constraint.string_operator, constraint.version name, constraint.string_operator, constraint.version
) )
elif isinstance(constraint, VersionUnion):
parts = []
for c in constraint.ranges:
parts.append(self._create_nested_marker(name, c))
glue = ' or '
parts = [
'({})'.format(part)
for part in parts
]
marker = glue.join(parts)
elif isinstance(constraint, Version):
marker = '{} == "{}"'.format(
name, constraint.text
)
else:
if constraint.min is not None:
op = '>='
if not constraint.include_min:
op = '>'
version = constraint.min.text
if constraint.max is not None:
text = '{} {} "{}"'.format(name, op, version)
op = '<='
if not constraint.include_max:
op = '<'
version = constraint.max
text += ' and {} {} "{}"'.format(name, op, version)
return text
elif constraint.max is not None:
op = '<='
if not constraint.include_max:
op = '<'
version = constraint.max
else:
return ''
marker = '{} {} "{}"'.format(
name, op, version
)
return marker return marker
...@@ -204,16 +262,43 @@ class Dependency(object): ...@@ -204,16 +262,43 @@ class Dependency(object):
""" """
self._optional = True self._optional = True
def with_constraint(self, constraint):
new = Dependency(
self.pretty_name,
constraint,
optional=self.is_optional(),
category=self.category,
allows_prereleases=self.allows_prereleases()
)
new.is_root = self.is_root
new.python_versions = self.python_versions
new.platform = self.platform
for extra in self.extras:
new.extras.append(extra)
for in_extra in self.in_extras:
new.in_extras.append(in_extra)
return new
def __eq__(self, other): def __eq__(self, other):
if not isinstance(other, Dependency): if not isinstance(other, Dependency):
return NotImplemented return NotImplemented
return self._name == other.name and self._constraint == other.constraint return self._name == other.name and self._constraint == other.constraint
def __ne__(self, other):
return not self == other
def __hash__(self): def __hash__(self):
return hash((self._name, self._pretty_constraint)) return hash((self._name, self._pretty_constraint))
def __str__(self): def __str__(self):
if self.is_root:
return self._pretty_name
return '{} ({})'.format( return '{} ({})'.format(
self._pretty_name, self._pretty_constraint self._pretty_name, self._pretty_constraint
) )
......
...@@ -34,6 +34,7 @@ class DirectoryDependency(Dependency): ...@@ -34,6 +34,7 @@ class DirectoryDependency(Dependency):
develop=False # type: bool develop=False # type: bool
): ):
from . import dependency_from_pep_508 from . import dependency_from_pep_508
from .package import Package
self._path = path self._path = path
self._base = base self._base = base
...@@ -79,9 +80,13 @@ class DirectoryDependency(Dependency): ...@@ -79,9 +80,13 @@ class DirectoryDependency(Dependency):
with setup.open('w') as f: with setup.open('w') as f:
f.write(decode(builder.build_setup())) f.write(decode(builder.build_setup()))
self._package = poetry.package package = poetry.package
self._package = Package(package.pretty_name, package.version)
self._package.requires += package.requires
self._package.dev_requires += package.dev_requires
self._package.python_versions = package.python_versions
self._package.platform = package.platform
else: else:
from poetry.packages import Package
# Execute egg_info # Execute egg_info
current_dir = os.getcwd() current_dir = os.getcwd()
os.chdir(str(self._full_path)) os.chdir(str(self._full_path))
...@@ -129,7 +134,7 @@ class DirectoryDependency(Dependency): ...@@ -129,7 +134,7 @@ class DirectoryDependency(Dependency):
self._package = package self._package = package
self._package.source_type = 'directory' self._package.source_type = 'directory'
self._package.source_reference = str(self._path) self._package.source_url = str(self._path)
super(DirectoryDependency, self).__init__( super(DirectoryDependency, self).__init__(
self._package.name, self._package.name,
......
...@@ -13,8 +13,6 @@ from poetry.utils.toml_file import TomlFile ...@@ -13,8 +13,6 @@ from poetry.utils.toml_file import TomlFile
class Locker: class Locker:
_relevant_keys = [ _relevant_keys = [
'name',
'version',
'dependencies', 'dependencies',
'dev-dependencies', 'dev-dependencies',
'source', 'source',
......
...@@ -4,16 +4,14 @@ import re ...@@ -4,16 +4,14 @@ import re
from typing import Union from typing import Union
from poetry.semver.constraints import Constraint from poetry.semver import Version
from poetry.semver.constraints import EmptyConstraint from poetry.semver import parse_constraint
from poetry.semver.helpers import parse_stability
from poetry.semver.version_parser import VersionParser
from poetry.spdx import license_by_id from poetry.spdx import license_by_id
from poetry.spdx import License from poetry.spdx import License
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils.helpers import canonicalize_name from poetry.utils.helpers import canonicalize_name
from poetry.version import parse as parse_version
from .constraints.empty_constraint import EmptyConstraint
from .constraints.generic_constraint import GenericConstraint from .constraints.generic_constraint import GenericConstraint
from .dependency import Dependency from .dependency import Dependency
from .directory_dependency import DirectoryDependency from .directory_dependency import DirectoryDependency
...@@ -43,20 +41,6 @@ class Package(object): ...@@ -43,20 +41,6 @@ class Package(object):
} }
} }
STABILITY_STABLE = 0
STABILITY_RC = 5
STABILITY_BETA = 10
STABILITY_ALPHA = 15
STABILITY_DEV = 20
stabilities = {
'stable': STABILITY_STABLE,
'rc': STABILITY_RC,
'beta': STABILITY_BETA,
'alpha': STABILITY_ALPHA,
'dev': STABILITY_DEV,
}
def __init__(self, name, version, pretty_version=None): def __init__(self, name, version, pretty_version=None):
""" """
Creates a new in memory package. Creates a new in memory package.
...@@ -64,14 +48,15 @@ class Package(object): ...@@ -64,14 +48,15 @@ class Package(object):
self._pretty_name = name self._pretty_name = name
self._name = canonicalize_name(name) self._name = canonicalize_name(name)
self._version = str(parse_version(version)) if not isinstance(version, Version):
self._pretty_version = pretty_version or version self._version = Version.parse(version)
self._pretty_version = pretty_version or version
else:
self._version = version
self._pretty_version = pretty_version or self._version.text
self.description = '' self.description = ''
self._stability = parse_stability(version)
self._dev = self._stability == 'dev'
self._authors = [] self._authors = []
self.homepage = None self.homepage = None
...@@ -89,8 +74,6 @@ class Package(object): ...@@ -89,8 +74,6 @@ class Package(object):
self.extras = {} self.extras = {}
self.requires_extras = [] self.requires_extras = []
self._parser = VersionParser()
self.category = 'main' self.category = 'main'
self.hashes = [] self.hashes = []
self.optional = False self.optional = False
...@@ -105,12 +88,14 @@ class Package(object): ...@@ -105,12 +88,14 @@ class Package(object):
self.classifiers = [] self.classifiers = []
self._python_versions = '*' self._python_versions = '*'
self._python_constraint = self._parser.parse_constraints('*') self._python_constraint = parse_constraint('*')
self._platform = '*' self._platform = '*'
self._platform_constraint = EmptyConstraint() self._platform_constraint = EmptyConstraint()
self.root_dir = None self.root_dir = None
self.develop = False
@property @property
def name(self): def name(self):
return self._name return self._name
...@@ -129,7 +114,10 @@ class Package(object): ...@@ -129,7 +114,10 @@ class Package(object):
@property @property
def unique_name(self): def unique_name(self):
return self.name + '-' + self._version if self.is_root():
return self._name
return self.name + '-' + self._version.text
@property @property
def pretty_string(self): def pretty_string(self):
...@@ -137,7 +125,7 @@ class Package(object): ...@@ -137,7 +125,7 @@ class Package(object):
@property @property
def full_pretty_version(self): def full_pretty_version(self):
if not self._dev and self.source_type not in ['hg', 'git']: if self.source_type not in ['hg', 'git']:
return self._pretty_version return self._pretty_version
# if source reference is a sha1 hash -- truncate # if source reference is a sha1 hash -- truncate
...@@ -159,6 +147,10 @@ class Package(object): ...@@ -159,6 +147,10 @@ class Package(object):
def author_email(self): # type: () -> str def author_email(self): # type: () -> str
return self._get_author()['email'] return self._get_author()['email']
@property
def all_requires(self):
return self.requires + self.dev_requires
def _get_author(self): # type: () -> dict def _get_author(self): # type: () -> dict
if not self._authors: if not self._authors:
return { return {
...@@ -183,7 +175,7 @@ class Package(object): ...@@ -183,7 +175,7 @@ class Package(object):
@python_versions.setter @python_versions.setter
def python_versions(self, value): def python_versions(self, value):
self._python_versions = value self._python_versions = value
self._python_constraint = self._parser.parse_constraints(value) self._python_constraint = parse_constraint(value)
@property @property
def python_constraint(self): def python_constraint(self):
...@@ -220,19 +212,18 @@ class Package(object): ...@@ -220,19 +212,18 @@ class Package(object):
classifiers = copy.copy(self.classifiers) classifiers = copy.copy(self.classifiers)
# Automatically set python classifiers # Automatically set python classifiers
parser = VersionParser()
if self.python_versions == '*': if self.python_versions == '*':
python_constraint = parser.parse_constraints('~2.7 || ^3.4') python_constraint = parse_constraint('~2.7 || ^3.4')
else: else:
python_constraint = self.python_constraint python_constraint = self.python_constraint
for version in sorted(self.AVAILABLE_PYTHONS): for version in sorted(self.AVAILABLE_PYTHONS):
if len(version) == 1: if len(version) == 1:
constraint = parser.parse_constraints(version + '.*') constraint = parse_constraint(version + '.*')
else: else:
constraint = Constraint('=', version) constraint = Version.parse(version)
if python_constraint.matches(constraint): if python_constraint.allows_any(constraint):
classifiers.append( classifiers.append(
'Programming Language :: Python :: {}'.format(version) 'Programming Language :: Python :: {}'.format(version)
) )
...@@ -245,11 +236,11 @@ class Package(object): ...@@ -245,11 +236,11 @@ class Package(object):
return sorted(classifiers) return sorted(classifiers)
def is_dev(self):
return self._dev
def is_prerelease(self): def is_prerelease(self):
return self._stability != 'stable' return self._version.is_prerelease()
def is_root(self):
return False
def add_dependency(self, def add_dependency(self,
name, # type: str name, # type: str
...@@ -335,6 +326,9 @@ class Package(object): ...@@ -335,6 +326,9 @@ class Package(object):
return dependency return dependency
def to_dependency(self):
return Dependency(self.name, self._version)
def __hash__(self): def __hash__(self):
return hash((self._name, self._version)) return hash((self._name, self._version))
......
from .package import Package
class ProjectPackage(Package):
def is_root(self):
return True
def to_dependency(self):
dependency = super(ProjectPackage, self).to_dependency()
dependency.is_root = True
return dependency
...@@ -11,6 +11,7 @@ from .exceptions import InvalidProjectFile ...@@ -11,6 +11,7 @@ from .exceptions import InvalidProjectFile
from .packages import Dependency from .packages import Dependency
from .packages import Locker from .packages import Locker
from .packages import Package from .packages import Package
from .packages import ProjectPackage
from .repositories import Pool from .repositories import Pool
from .repositories.pypi_repository import PyPiRepository from .repositories.pypi_repository import PyPiRepository
from .spdx import license_by_id from .spdx import license_by_id
...@@ -95,7 +96,7 @@ class Poetry: ...@@ -95,7 +96,7 @@ class Poetry:
# Load package # Load package
name = local_config['name'] name = local_config['name']
version = local_config['version'] version = local_config['version']
package = Package(name, version, version) package = ProjectPackage(name, version, version)
package.root_dir = poetry_file.parent package.root_dir = poetry_file.parent
for author in local_config['authors']: for author in local_config['authors']:
......
...@@ -35,3 +35,9 @@ class Operation(object): ...@@ -35,3 +35,9 @@ class Operation(object):
self._skip_reason = reason self._skip_reason = reason
return self return self
def unskip(self): # type: () -> Operation
self._skipped = False
self._skip_reason = None
return self
import os import os
import pkginfo import pkginfo
import shutil import shutil
import time
from cleo import ProgressIndicator
from contextlib import contextmanager
from functools import cmp_to_key from functools import cmp_to_key
from tempfile import mkdtemp from tempfile import mkdtemp
from typing import Dict
from typing import List from typing import List
from typing import Union from typing import Union
from poetry.mixology import DependencyGraph
from poetry.mixology.conflict import Conflict
from poetry.mixology.contracts import SpecificationProvider
from poetry.mixology.contracts import UI
from poetry.packages import Dependency from poetry.packages import Dependency
from poetry.packages import DirectoryDependency from poetry.packages import DirectoryDependency
from poetry.packages import FileDependency from poetry.packages import FileDependency
...@@ -20,9 +17,13 @@ from poetry.packages import Package ...@@ -20,9 +17,13 @@ from poetry.packages import Package
from poetry.packages import VCSDependency from poetry.packages import VCSDependency
from poetry.packages import dependency_from_pep_508 from poetry.packages import dependency_from_pep_508
from poetry.repositories import Pool from poetry.mixology.incompatibility import Incompatibility
from poetry.mixology.incompatibility_cause import DependencyCause
from poetry.mixology.incompatibility_cause import PlatformCause
from poetry.mixology.incompatibility_cause import PythonCause
from poetry.mixology.term import Term
from poetry.semver import less_than from poetry.repositories import Pool
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils.helpers import parse_requires from poetry.utils.helpers import parse_requires
...@@ -34,7 +35,27 @@ from poetry.vcs.git import Git ...@@ -34,7 +35,27 @@ from poetry.vcs.git import Git
from .dependencies import Dependencies from .dependencies import Dependencies
class Provider(SpecificationProvider, UI): class Indicator(ProgressIndicator):
def __init__(self, output):
super(Indicator, self).__init__(output)
self.format = '%message% <fg=black;options=bold>(%elapsed:2s%)</>'
@contextmanager
def auto(self):
message = '<info>Resolving dependencies</info>...'
with super(Indicator, self).auto(message, message):
yield
def _formatter_elapsed(self):
elapsed = time.time() - self.start_time
return '{:.1f}s'.format(elapsed)
class Provider:
UNSAFE_PACKAGES = {'setuptools', 'distribute', 'pip'} UNSAFE_PACKAGES = {'setuptools', 'distribute', 'pip'}
...@@ -47,11 +68,8 @@ class Provider(SpecificationProvider, UI): ...@@ -47,11 +68,8 @@ class Provider(SpecificationProvider, UI):
self._pool = pool self._pool = pool
self._io = io self._io = io
self._python_constraint = package.python_constraint self._python_constraint = package.python_constraint
self._base_dg = DependencyGraph()
self._search_for = {} self._search_for = {}
self._constraints = {} self._is_debugging = self._io.is_debug() or self._io.is_very_verbose()
super(Provider, self).__init__(debug=self._io.is_debug())
@property @property
def pool(self): # type: () -> Pool def pool(self): # type: () -> Pool
...@@ -65,6 +83,9 @@ class Provider(SpecificationProvider, UI): ...@@ -65,6 +83,9 @@ class Provider(SpecificationProvider, UI):
def name_for_locking_dependency_source(self): # type: () -> str def name_for_locking_dependency_source(self): # type: () -> str
return 'pyproject.lock' return 'pyproject.lock'
def is_debugging(self):
return self._is_debugging
def name_for(self, dependency): # type: (Dependency) -> str def name_for(self, dependency): # type: (Dependency) -> str
""" """
Returns the name for the given dependency. Returns the name for the given dependency.
...@@ -78,6 +99,9 @@ class Provider(SpecificationProvider, UI): ...@@ -78,6 +99,9 @@ class Provider(SpecificationProvider, UI):
The specifications in the returned list will be considered in reverse The specifications in the returned list will be considered in reverse
order, so the latest version ought to be last. order, so the latest version ought to be last.
""" """
if dependency.is_root:
return [self._package]
if dependency in self._search_for: if dependency in self._search_for:
return self._search_for[dependency] return self._search_for[dependency]
...@@ -90,17 +114,6 @@ class Provider(SpecificationProvider, UI): ...@@ -90,17 +114,6 @@ class Provider(SpecificationProvider, UI):
else: else:
constraint = dependency.constraint constraint = dependency.constraint
# If we have already seen this dependency
# we take the most restrictive constraint
if dependency.name in self._constraints:
current_constraint = self._constraints[dependency.name]
if str(dependency.constraint) == '*':
# The new constraint accepts anything
# so we take the previous one
constraint = current_constraint
self._constraints[dependency.name] = constraint
packages = self._pool.find_packages( packages = self._pool.find_packages(
dependency.name, dependency.name,
constraint, constraint,
...@@ -112,7 +125,7 @@ class Provider(SpecificationProvider, UI): ...@@ -112,7 +125,7 @@ class Provider(SpecificationProvider, UI):
key=cmp_to_key( key=cmp_to_key(
lambda x, y: lambda x, y:
0 if x.version == y.version 0 if x.version == y.version
else -1 * int(less_than(x.version, y.version) or -1) else int(x.version < y.version or -1)
) )
) )
...@@ -179,11 +192,12 @@ class Provider(SpecificationProvider, UI): ...@@ -179,11 +192,12 @@ class Provider(SpecificationProvider, UI):
# to figure the information we need # to figure the information we need
# We need to place ourselves in the proper # We need to place ourselves in the proper
# folder for it to work # folder for it to work
venv = Venv.create(self._io)
current_dir = os.getcwd() current_dir = os.getcwd()
os.chdir(tmp_dir.as_posix()) os.chdir(tmp_dir.as_posix())
try: try:
venv = Venv.create(self._io)
venv.run( venv.run(
'python', 'setup.py', 'egg_info' 'python', 'setup.py', 'egg_info'
) )
...@@ -224,7 +238,7 @@ class Provider(SpecificationProvider, UI): ...@@ -224,7 +238,7 @@ class Provider(SpecificationProvider, UI):
): # type: (FileDependency) -> List[Package] ): # type: (FileDependency) -> List[Package]
package = Package(dependency.name, dependency.pretty_constraint) package = Package(dependency.name, dependency.pretty_constraint)
package.source_type = 'file' package.source_type = 'file'
package.source_reference = str(dependency.path) package.source_url = str(dependency.path)
package.description = dependency.metadata.summary package.description = dependency.metadata.summary
for req in dependency.metadata.requires_dist: for req in dependency.metadata.requires_dist:
...@@ -251,6 +265,47 @@ class Provider(SpecificationProvider, UI): ...@@ -251,6 +265,47 @@ class Provider(SpecificationProvider, UI):
return [package] return [package]
def incompatibilities_for(self, package): # type: (Package) -> List[Incompatibility]
"""
Returns incompatibilities that encapsulate a given package's dependencies,
or that it can't be safely selected.
If multiple subsequent versions of this package have the same
dependencies, this will return incompatibilities that reflect that. It
won't return incompatibilities that have already been returned by a
previous call to _incompatibilities_for().
"""
if package.source_type in ['git', 'file', 'directory']:
dependencies = package.requires
elif package.is_root():
dependencies = package.all_requires
else:
dependencies = self._dependencies_for(package)
if not self._package.python_constraint.allows_any(package.python_constraint):
return [
Incompatibility(
[Term(package.to_dependency(), True)],
PythonCause(package.python_versions)
)
]
if not self._package.platform_constraint.matches(package.platform_constraint):
return [
Incompatibility(
[Term(package.to_dependency(), True)],
PlatformCause(package.platform)
)
]
return [
Incompatibility([
Term(package.to_dependency(), True),
Term(dep, False)
], DependencyCause())
for dep in dependencies
]
def dependencies_for(self, package def dependencies_for(self, package
): # type: (Package) -> Union[List[Dependency], Dependencies] ): # type: (Package) -> Union[List[Dependency], Dependencies]
if package.source_type in ['git', 'file', 'directory']: if package.source_type in ['git', 'file', 'directory']:
...@@ -265,7 +320,7 @@ class Provider(SpecificationProvider, UI): ...@@ -265,7 +320,7 @@ class Provider(SpecificationProvider, UI):
def _dependencies_for(self, package): # type: (Package) -> List[Dependency] def _dependencies_for(self, package): # type: (Package) -> List[Dependency]
complete_package = self._pool.package( complete_package = self._pool.package(
package.name, package.version, package.name, package.version.text,
extras=package.requires_extras extras=package.requires_extras
) )
...@@ -275,53 +330,16 @@ class Provider(SpecificationProvider, UI): ...@@ -275,53 +330,16 @@ class Provider(SpecificationProvider, UI):
package.python_versions = complete_package.python_versions package.python_versions = complete_package.python_versions
package.platform = complete_package.platform package.platform = complete_package.platform
package.hashes = complete_package.hashes package.hashes = complete_package.hashes
package.extras = complete_package.extras
return [ return [
r for r in package.requires r for r in package.requires
if not r.is_optional() if not r.is_optional()
and self._package.python_constraint.matches(r.python_constraint) and self._package.python_constraint.allows_any(r.python_constraint)
and self._package.platform_constraint.matches(package.platform_constraint) and self._package.platform_constraint.matches(package.platform_constraint)
and r.name not in self.UNSAFE_PACKAGES and r.name not in self.UNSAFE_PACKAGES
] ]
def is_requirement_satisfied_by(self,
requirement, # type: Dependency
activated, # type: DependencyGraph
package # type: Package
): # type: (...) -> bool
"""
Determines whether the given requirement is satisfied by the given
spec, in the context of the current activated dependency graph.
"""
if isinstance(requirement, Package):
return requirement == package
if not requirement.accepts(package):
return False
if package.is_prerelease() and not requirement.allows_prereleases():
vertex = activated.vertex_named(package.name)
if not any([r.allows_prereleases() for r in vertex.requirements]):
return False
return (
self._package.python_constraint.matches(package.python_constraint)
and self._package.platform_constraint.matches(package.platform_constraint)
)
def sort_dependencies(self,
dependencies, # type: List[Dependency]
activated, # type: DependencyGraph
conflicts # type: Dict[str, List[Conflict]]
): # type: (...) -> List[Dependency]
return sorted(dependencies, key=lambda d: [
0 if activated.vertex_named(d.name).payload else 1,
0 if activated.vertex_named(d.name).root else 1,
0 if d.allows_prereleases() else 1,
0 if d.name in conflicts else 1
])
# UI # UI
@property @property
...@@ -341,12 +359,23 @@ class Provider(SpecificationProvider, UI): ...@@ -341,12 +359,23 @@ class Provider(SpecificationProvider, UI):
def after_resolution(self): def after_resolution(self):
self._io.new_line() self._io.new_line()
def debug(self, message, depth): def debug(self, message, depth=0):
if self.is_debugging(): if self.is_debugging():
debug_info = str(message) debug_info = str(message)
debug_info = '\n'.join([ debug_info = '\n'.join([
'<comment>:{}:</> {}'.format(str(depth).rjust(4), s) '<comment>{}:</> {}'.format(str(depth).rjust(4), s)
for s in debug_info.split('\n') for s in debug_info.split('\n')
]) + '\n' ]) + '\n'
self.output.write(debug_info) self.output.write(debug_info)
@contextmanager
def progress(self):
if not self._io.is_decorated() or self.is_debugging():
self.output.writeln('Resolving dependencies...')
yield
else:
indicator = Indicator(self._io)
with indicator.auto():
yield
from typing import List from typing import List
from poetry.mixology import Resolver from poetry.mixology import resolve_version
from poetry.mixology.dependency_graph import DependencyGraph from poetry.mixology.failure import SolveFailure
from poetry.mixology.exceptions import ResolverError
from poetry.packages.constraints.generic_constraint import GenericConstraint from poetry.packages.constraints.generic_constraint import GenericConstraint
from poetry.semver.version_parser import VersionParser from poetry.semver import parse_constraint
from .exceptions import SolverProblemError from .exceptions import SolverProblemError
from .operations import Install from .operations import Install
from .operations import Uninstall from .operations import Uninstall
from .operations import Update from .operations import Update
...@@ -25,31 +25,27 @@ class Solver: ...@@ -25,31 +25,27 @@ class Solver:
self._locked = locked self._locked = locked
self._io = io self._io = io
def solve(self, requested, fixed=None): # type: (...) -> List[Operation] def solve(self, use_latest=None): # type: (...) -> List[Operation]
provider = Provider(self._package, self._pool, self._io) provider = Provider(self._package, self._pool, self._io)
resolver = Resolver(provider, provider) locked = {}
for package in self._locked.packages:
base = None locked[package.name] = package
if fixed is not None:
base = DependencyGraph()
for fixed_req in fixed:
base.add_vertex(fixed_req.name, fixed_req, True)
try: try:
graph = resolver.resolve(requested, base=base) result = resolve_version(self._package, provider, locked=locked, use_latest=use_latest)
except ResolverError as e: except SolveFailure as e:
raise SolverProblemError(e) raise SolverProblemError(e)
packages = [v.payload for v in graph.vertices.values()] packages = result.packages
requested = self._package.all_requires
# Setting info for package in packages:
for vertex in graph.vertices.values(): category, optional, python, platform = self._get_tags_for_package(
category, optional, python, platform = self._get_tags_for_vertex( package, packages, requested
vertex, requested
) )
vertex.payload.category = category package.category = category
vertex.payload.optional = optional package.optional = optional
# If requirements are empty, drop them # If requirements are empty, drop them
requirements = {} requirements = {}
...@@ -59,7 +55,7 @@ class Solver: ...@@ -59,7 +55,7 @@ class Solver:
if platform is not None and platform != '*': if platform is not None and platform != '*':
requirements['platform'] = platform requirements['platform'] = platform
vertex.payload.requirements = requirements package.requirements = requirements
operations = [] operations = []
for package in packages: for package in packages:
...@@ -101,7 +97,7 @@ class Solver: ...@@ -101,7 +97,7 @@ class Solver:
operations.append(op) operations.append(op)
requested_names = [r.name for r in requested] requested_names = [r.name for r in self._package.all_requires]
return sorted( return sorted(
operations, operations,
...@@ -111,95 +107,171 @@ class Solver: ...@@ -111,95 +107,171 @@ class Solver:
) )
) )
def _get_tags_for_vertex(self, vertex, requested): def _get_graph_for_package(self, package, packages, requested, original=None):
category = 'dev' graph = {
optional = True package.name: {
python_version = None 'category': 'dev',
platform = None 'optional': True,
'python_version': None,
'platform': None,
'dependencies': {},
'parents': {},
},
}
roots = []
for dep in requested:
if dep.name == package.name:
roots.append(dep)
origins = []
for pkg in packages:
for dep in pkg.all_requires:
if original and original.name == pkg.name:
# Circular dependency
continue
if not vertex.incoming_edges: if dep.name == package.name:
# Original dependency origins.append((pkg, dep))
for req in requested:
if vertex.payload.name == req.name: if roots and (not origins or len(roots) > 1):
category = req.category # Root dependency
optional = req.is_optional() if len(roots) == 1:
root = roots[0]
else:
root1 = [r for r in roots if r.category == 'main'][0]
root2 = [r for r in roots if r.category == 'dev'][0]
if root1.extras == root2.extras or original is None:
root = root1
else:
root1_extra_dependencies = []
for extra in root1.extras:
if extra in package.extras:
for dep in package.extras[extra]:
root1_extra_dependencies.append(dep.name)
root2_extra_dependencies = []
for extra in root2.extras:
if extra in package.extras:
for dep in package.extras[extra]:
root2_extra_dependencies.append(dep.name)
if (
original.name in root1_extra_dependencies
and original.name in root2_extra_dependencies
):
root = root1
elif original.name in root2_extra_dependencies:
root = root2
else:
root = root1
category = root.category
optional = root.is_optional()
python_version = str(root.python_constraint)
platform = str(root.platform_constraint)
graph[package.name]['category'] = category
graph[package.name]['optional'] = optional
graph[package.name]['python_version'] = python_version
graph[package.name]['platform'] = platform
return graph
for pkg, dep in origins:
graph[package.name]['dependencies'][pkg.name] = {
'constraint': dep.pretty_constraint,
'python_version': dep.python_versions,
'platform': dep.platform,
}
graph[package.name]['parents'].update(
self._get_graph_for_package(
pkg, packages, requested, original=package
)
)
python_version = str(req.python_constraint) return graph
platform = str(req.platform_constraint) def _get_tags_for_package(self, package, packages, requested):
graph = self._get_graph_for_package(package, packages, requested)[package.name]
break return self._get_tags_from_graph(graph, packages)
def _get_tags_from_graph(self, graph, packages):
category = graph['category']
optional = graph['optional']
python_version = graph['python_version']
platform = graph['platform']
if not graph['parents']:
# Root dependency
return category, optional, python_version, platform return category, optional, python_version, platform
parser = VersionParser()
python_versions = [] python_versions = []
platforms = [] platforms = []
for edge in vertex.incoming_edges:
python_version = None
platform = None
for req in edge.origin.payload.requires:
if req.name == vertex.payload.name:
python_version = req.python_versions
platform = req.platform
break for parent_name, parent_graph in graph['parents'].items():
dep_python_version = graph['dependencies'][parent_name]['python_version']
dep_platform = graph['dependencies'][parent_name]['platform']
for pkg in packages:
if pkg.name == parent_name:
(top_category,
top_optional,
top_python_version,
top_platform) = self._get_tags_from_graph(parent_graph, packages)
if category is None or category != 'main':
category = top_category
optional = optional and top_optional
# Take the most restrictive constraints
if top_python_version is not None:
if dep_python_version is not None:
previous = parse_constraint(dep_python_version)
current = parse_constraint(top_python_version)
if previous.allows_all(current):
python_versions.append(top_python_version)
else:
python_versions.append(dep_python_version)
else:
python_versions.append(top_python_version)
elif dep_python_version is not None:
python_versions.append(dep_python_version)
if top_platform is not None:
if dep_platform is not None:
previous = GenericConstraint.parse(dep_platform)
current = GenericConstraint.parse(top_platform)
if top_platform != '*' and previous.matches(current):
platforms.append(top_platform)
else:
platforms.append(dep_platform)
else:
platforms.append(top_platform)
elif dep_platform is not None:
platforms.append(dep_platform)
(top_category, break
top_optional,
top_python_version,
top_platform) = self._get_tags_for_vertex(
edge.origin, requested
)
if top_category == 'main':
category = top_category
optional = optional and top_optional
# Take the most restrictive constraints
if top_python_version is not None:
if python_version is not None:
previous = parser.parse_constraints(python_version)
current = parser.parse_constraints(top_python_version)
if top_python_version != '*' and previous.matches(current):
python_versions.append(top_python_version)
else:
python_versions.append(python_version)
else:
python_versions.append(top_python_version)
elif python_version is not None:
python_versions.append(python_version)
if top_platform is not None:
if platform is not None:
previous = GenericConstraint.parse(platform)
current = GenericConstraint.parse(top_platform)
if top_platform != '*' and previous.matches(current):
platforms.append(top_platform)
else:
platforms.append(platform)
else:
platforms.append(top_platform)
elif platform is not None:
platforms.append(platform)
if not python_versions: if not python_versions:
python_version = None python_version = None
else: else:
# Find the least restrictive constraint # Find the least restrictive constraint
python_version = python_versions[0] python_version = python_versions[0]
previous = parser.parse_constraints(python_version) previous = parse_constraint(python_version)
for constraint in python_versions[1:]: for constraint in python_versions[1:]:
current = parser.parse_constraints(constraint) current = parse_constraint(constraint)
if python_version == '*': if python_version == '*':
continue continue
elif constraint == '*': elif constraint == '*':
python_version = constraint python_version = constraint
elif current.matches(previous): elif current.allows_all(previous):
python_version = constraint python_version = constraint
if not platforms: if not platforms:
......
from pip._vendor.pkg_resources import RequirementParseError import cgi
import re
try: try:
from pip._internal.exceptions import InstallationError import urllib.parse as urlparse
from pip._internal.req import InstallRequirement
except ImportError: except ImportError:
from pip.exceptions import InstallationError import urlparse
from pip.req import InstallRequirement
from piptools.cache import DependencyCache try:
from piptools.repositories import PyPIRepository from html import unescape
from piptools.resolver import Resolver except ImportError:
from piptools.scripts.compile import get_pip_command try:
from html.parser import HTMLParser
except ImportError:
from HTMLParser import HTMLParser
unescape = HTMLParser().unescape
from typing import Generator
from typing import Union
import html5lib
import requests
from cachecontrol import CacheControl
from cachecontrol.caches.file_cache import FileCache
from cachy import CacheManager from cachy import CacheManager
import poetry.packages import poetry.packages
from poetry.locations import CACHE_DIR from poetry.locations import CACHE_DIR
from poetry.masonry.publishing.uploader import wheel_file_re
from poetry.packages import Package from poetry.packages import Package
from poetry.packages import dependency_from_pep_508 from poetry.packages import dependency_from_pep_508
from poetry.semver.constraints import Constraint from poetry.packages.utils.link import Link
from poetry.semver.constraints.base_constraint import BaseConstraint from poetry.semver import parse_constraint
from poetry.semver.version_parser import VersionParser from poetry.semver import Version
from poetry.semver import VersionConstraint
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils.helpers import canonicalize_name
from poetry.version.markers import InvalidMarker from poetry.version.markers import InvalidMarker
from .pypi_repository import PyPiRepository from .pypi_repository import PyPiRepository
class Page:
VERSION_REGEX = re.compile('(?i)([a-z0-9_\-.]+?)-(?=\d)([a-z0-9_.!+-]+)')
def __init__(self, url, content, headers):
self._url = url
encoding = None
if headers and "Content-Type" in headers:
content_type, params = cgi.parse_header(headers["Content-Type"])
if "charset" in params:
encoding = params['charset']
self._content = content
self._parsed = html5lib.parse(
content,
transport_encoding=encoding,
namespaceHTMLElements=False,
)
@property
def versions(self): # type: () -> Generator[Version]
seen = set()
for link in self.links:
version = self.link_version(link)
if not version:
continue
if version in seen:
continue
seen.add(version)
yield version
@property
def links(self): # type: () -> Generator[Link]
for anchor in self._parsed.findall(".//a"):
if anchor.get("href"):
href = anchor.get("href")
url = self.clean_link(
urlparse.urljoin(self._url, href)
)
pyrequire = anchor.get('data-requires-python')
pyrequire = unescape(pyrequire) if pyrequire else None
yield Link(url, self, requires_python=pyrequire)
def links_for_version(self, version): # type: (Version) -> Generator[Link]
for link in self.links:
if self.link_version(link) == version:
yield link
def link_version(self, link): # type: (Link) -> Union[Version, None]
m = wheel_file_re.match(link.filename)
if m:
version = m.group('ver')
else:
info, ext = link.splitext()
match = self.VERSION_REGEX.match(info)
if not match:
return
version = match.group(2)
try:
version = Version.parse(version)
except ValueError:
return
return version
_clean_re = re.compile(r'[^a-z0-9$&+,/:;=?@.#%_\\|-]', re.I)
def clean_link(self, url):
"""Makes sure a link is fully encoded. That is, if a ' ' shows up in
the link, it will be rewritten to %20 (while not over-quoting
% or other characters)."""
return self._clean_re.sub(
lambda match: '%%%2x' % ord(match.group(0)), url)
class LegacyRepository(PyPiRepository): class LegacyRepository(PyPiRepository):
def __init__(self, name, url): def __init__(self, name, url):
...@@ -36,11 +134,7 @@ class LegacyRepository(PyPiRepository): ...@@ -36,11 +134,7 @@ class LegacyRepository(PyPiRepository):
self._packages = [] self._packages = []
self._name = name self._name = name
self._url = url self._url = url.rstrip('/')
command = get_pip_command()
opts, _ = command.parse_args([])
self._session = command._build_session(opts)
self._repository = PyPIRepository(opts, self._session)
self._cache_dir = Path(CACHE_DIR) / 'cache' / 'repositories' / name self._cache_dir = Path(CACHE_DIR) / 'cache' / 'repositories' / name
self._cache = CacheManager({ self._cache = CacheManager({
...@@ -60,6 +154,11 @@ class LegacyRepository(PyPiRepository): ...@@ -60,6 +154,11 @@ class LegacyRepository(PyPiRepository):
} }
}) })
self._session = CacheControl(
requests.session(),
cache=FileCache(str(self._cache_dir / '_http'))
)
@property @property
def name(self): def name(self):
return self._name return self._name
...@@ -70,9 +169,8 @@ class LegacyRepository(PyPiRepository): ...@@ -70,9 +169,8 @@ class LegacyRepository(PyPiRepository):
packages = [] packages = []
if constraint is not None and not isinstance(constraint, if constraint is not None and not isinstance(constraint,
BaseConstraint): VersionConstraint):
version_parser = VersionParser() constraint = parse_constraint(constraint)
constraint = version_parser.parse_constraints(constraint)
key = name key = name
if constraint: if constraint:
...@@ -81,23 +179,26 @@ class LegacyRepository(PyPiRepository): ...@@ -81,23 +179,26 @@ class LegacyRepository(PyPiRepository):
if self._cache.store('matches').has(key): if self._cache.store('matches').has(key):
versions = self._cache.store('matches').get(key) versions = self._cache.store('matches').get(key)
else: else:
candidates = [str(c.version) for c in self._repository.find_all_candidates(name)] page = self._get('/{}'.format(canonicalize_name(name).replace('.', '-')))
if page is None:
raise ValueError('No package named "{}"'.format(name))
versions = [] versions = []
for version in candidates: for version in page.versions:
if version in versions:
continue
if ( if (
not constraint not constraint
or (constraint and constraint.matches(Constraint('=', version))) or (constraint and constraint.allows(version))
): ):
versions.append(version) versions.append(version)
self._cache.store('matches').put(key, versions, 5) self._cache.store('matches').put(key, versions, 5)
for version in versions: for version in versions:
packages.append(Package(name, version, extras=extras)) package = Package(name, version)
if extras is not None:
package.requires_extras = extras
packages.append(package)
return packages return packages
...@@ -125,8 +226,10 @@ class LegacyRepository(PyPiRepository): ...@@ -125,8 +226,10 @@ class LegacyRepository(PyPiRepository):
extras = [] extras = []
release_info = self.get_release_info(name, version) release_info = self.get_release_info(name, version)
package = poetry.packages.Package(name, version, version) package = poetry.packages.Package(name, version, version)
for req in release_info['requires_dist']: requires_dist = release_info['requires_dist'] or []
for req in requires_dist:
try: try:
dependency = dependency_from_pep_508(req) dependency = dependency_from_pep_508(req)
except InvalidMarker: except InvalidMarker:
...@@ -177,43 +280,63 @@ class LegacyRepository(PyPiRepository): ...@@ -177,43 +280,63 @@ class LegacyRepository(PyPiRepository):
) )
def _get_release_info(self, name, version): # type: (str, str) -> dict def _get_release_info(self, name, version): # type: (str, str) -> dict
ireq = InstallRequirement.from_line('{}=={}'.format(name, version)) page = self._get('/{}'.format(canonicalize_name(name).replace('.', '-')))
resolver = Resolver( if page is None:
[ireq], self._repository, raise ValueError('No package named "{}"'.format(name))
cache=DependencyCache(self._cache_dir.as_posix())
)
try:
requirements = list(resolver._iter_dependencies(ireq))
except (InstallationError, RequirementParseError):
# setup.py egg-info error most likely
# So we assume no dependencies
requirements = []
requires = []
for dep in requirements:
constraint = str(dep.req.specifier)
require = dep.name
if constraint:
require += ' ({})'.format(constraint)
requires.append(require)
try:
hashes = resolver.resolve_hashes([ireq])[ireq]
except IndexError:
# Sometimes pip-tools fails when getting indices
hashes = []
hashes = [h.split(':')[1] for h in hashes]
data = { data = {
'name': name, 'name': name,
'version': version, 'version': version,
'summary': '', 'summary': '',
'requires_dist': requires, 'requires_dist': [],
'digests': hashes 'requires_python': [],
'digests': []
} }
resolver.repository.freshen_build_caches() links = list(page.links_for_version(Version.parse(version)))
urls = {}
hashes = []
default_link = links[0]
for link in links:
if link.is_wheel:
urls['bdist_wheel'] = link.url
elif link.filename.endswith('.tar.gz'):
urls['sdist'] = link.url
elif link.filename.endswith(('.zip', '.bz2')) and 'sdist' not in urls:
urls['sdist'] = link.url
hash = link.hash
if link.hash_name == 'sha256':
hashes.append(hash)
data['digests'] = hashes
if not urls:
if default_link.is_wheel:
m = wheel_file_re.match(default_link.filename)
python = m.group('pyver')
platform = m.group('plat')
if python == 'py2.py3' and platform == 'any':
urls['bdist_wheel'] = default_link.url
elif default_link.filename.endswith('.tar.gz'):
urls['sdist'] = default_link.url
elif default_link.filename.endswith(('.zip', '.bz2')) and 'sdist' not in urls:
urls['sdist'] = default_link.url
else:
return data
info = self._get_info_from_urls(urls)
data['summary'] = info['summary']
data['requires_dist'] = info['requires_dist']
data['requires_python'] = info['requires_python']
return data return data
def _get(self, endpoint): # type: (str) -> Union[Page, None]
url = self._url + endpoint
response = self._session.get(url)
if response.status_code == 404:
return
return Page(url, response.content, response.headers)
...@@ -7,6 +7,7 @@ import pkginfo ...@@ -7,6 +7,7 @@ import pkginfo
from bz2 import BZ2File from bz2 import BZ2File
from gzip import GzipFile from gzip import GzipFile
from typing import Dict
from typing import List from typing import List
from typing import Union from typing import Union
...@@ -26,16 +27,17 @@ from cachy import CacheManager ...@@ -26,16 +27,17 @@ from cachy import CacheManager
from requests import get from requests import get
from requests import session from requests import session
from poetry.io import NullIO
from poetry.locations import CACHE_DIR from poetry.locations import CACHE_DIR
from poetry.packages import dependency_from_pep_508 from poetry.packages import dependency_from_pep_508
from poetry.packages import Package from poetry.packages import Package
from poetry.semver.constraints import Constraint from poetry.semver import parse_constraint
from poetry.semver.constraints.base_constraint import BaseConstraint from poetry.semver import VersionConstraint
from poetry.semver.version_parser import VersionParser
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils._compat import to_str from poetry.utils._compat import to_str
from poetry.utils.helpers import parse_requires from poetry.utils.helpers import parse_requires
from poetry.utils.helpers import temporary_directory from poetry.utils.helpers import temporary_directory
from poetry.utils.venv import Venv
from poetry.version.markers import InvalidMarker from poetry.version.markers import InvalidMarker
from .repository import Repository from .repository import Repository
...@@ -79,16 +81,15 @@ class PyPiRepository(Repository): ...@@ -79,16 +81,15 @@ class PyPiRepository(Repository):
def find_packages(self, def find_packages(self,
name, # type: str name, # type: str
constraint=None, # type: Union[Constraint, str, None] constraint=None, # type: Union[VersionConstraint, str, None]
extras=None, # type: Union[list, None] extras=None, # type: Union[list, None]
allow_prereleases=False # type: bool allow_prereleases=False # type: bool
): # type: (...) -> List[Package] ): # type: (...) -> List[Package]
""" """
Find packages on the remote server. Find packages on the remote server.
""" """
if constraint is not None and not isinstance(constraint, BaseConstraint): if constraint is not None and not isinstance(constraint, VersionConstraint):
version_parser = VersionParser() constraint = parse_constraint(constraint)
constraint = version_parser.parse_constraints(constraint)
info = self.get_package_info(name) info = self.get_package_info(name)
...@@ -112,7 +113,7 @@ class PyPiRepository(Repository): ...@@ -112,7 +113,7 @@ class PyPiRepository(Repository):
if ( if (
not constraint not constraint
or (constraint and constraint.matches(Constraint('=', version))) or (constraint and constraint.allows(package.version))
): ):
if extras is not None: if extras is not None:
package.requires_extras = extras package.requires_extras = extras
...@@ -142,20 +143,6 @@ class PyPiRepository(Repository): ...@@ -142,20 +143,6 @@ class PyPiRepository(Repository):
extras = [] extras = []
release_info = self.get_release_info(name, version) release_info = self.get_release_info(name, version)
if (
self._fallback
and release_info['requires_dist'] is None
and not release_info['requires_python']
and '_fallback' not in release_info
):
# Force cache update
self._log(
'No dependencies found, downloading archives',
level='debug'
)
self._cache.forget('{}:{}'.format(name, version))
release_info = self.get_release_info(name, version)
package = Package(name, version, version) package = Package(name, version, version)
requires_dist = release_info['requires_dist'] or [] requires_dist = release_info['requires_dist'] or []
for req in requires_dist: for req in requires_dist:
...@@ -270,6 +257,8 @@ class PyPiRepository(Repository): ...@@ -270,6 +257,8 @@ class PyPiRepository(Repository):
) )
def _get_release_info(self, name, version): # type: (str, str) -> dict def _get_release_info(self, name, version): # type: (str, str) -> dict
self._log('Getting info for {} ({}) from PyPI'.format(name, version), 'debug')
json_data = self._get('pypi/{}/{}/json'.format(name, version)) json_data = self._get('pypi/{}/{}/json'.format(name, version))
if json_data is None: if json_data is None:
raise ValueError('Package [{}] not found.'.format(name)) raise ValueError('Package [{}] not found.'.format(name))
...@@ -297,15 +286,16 @@ class PyPiRepository(Repository): ...@@ -297,15 +286,16 @@ class PyPiRepository(Repository):
if ( if (
self._fallback self._fallback
and data['requires_dist'] is None and data['requires_dist'] is None
and not data['requires_python']
): ):
self._log(
'No dependencies found, downloading archives',
level='debug'
)
# No dependencies set (along with other information) # No dependencies set (along with other information)
# This might be due to actually no dependencies # This might be due to actually no dependencies
# or badly set metadata when uploading # or badly set metadata when uploading
# So, we need to make sure there is actually no # So, we need to make sure there is actually no
# dependencies by introspecting packages # dependencies by introspecting packages
data['_fallback'] = True
urls = {} urls = {}
for url in json_data['urls']: for url in json_data['urls']:
# Only get sdist and universal wheels # Only get sdist and universal wheels
...@@ -334,9 +324,12 @@ class PyPiRepository(Repository): ...@@ -334,9 +324,12 @@ class PyPiRepository(Repository):
if not urls: if not urls:
return data return data
requires_dist = self._get_requires_dist_from_urls(urls) info = self._get_info_from_urls(urls)
data['requires_dist'] = requires_dist data['requires_dist'] = info['requires_dist']
if not data['requires_python']:
data['requires_python'] = info['requires_python']
return data return data
...@@ -349,15 +342,21 @@ class PyPiRepository(Repository): ...@@ -349,15 +342,21 @@ class PyPiRepository(Repository):
return json_data return json_data
def _get_requires_dist_from_urls(self, urls def _get_info_from_urls(self, urls
): # type: (dict) -> Union[list, None] ): # type: (Dict[str, str]) -> Dict[str, Union[str, List, None]]
if 'bdist_wheel' in urls: if 'bdist_wheel' in urls:
return self._get_requires_dist_from_wheel(urls['bdist_wheek']) return self._get_info_from_wheel(urls['bdist_wheel'])
return self._get_info_from_sdist(urls['sdist'])
return self._get_requires_dist_from_sdist(urls['sdist']) def _get_info_from_wheel(self, url
): # type: (str) -> Dict[str, Union[str, List, None]]
info = {
'summary': '',
'requires_python': None,
'requires_dist': None,
}
def _get_requires_dist_from_wheel(self, url
): # type: (str) -> Union[list, None]
filename = os.path.basename(urlparse.urlparse(url).path) filename = os.path.basename(urlparse.urlparse(url).path)
with temporary_directory() as temp_dir: with temporary_directory() as temp_dir:
...@@ -369,13 +368,26 @@ class PyPiRepository(Repository): ...@@ -369,13 +368,26 @@ class PyPiRepository(Repository):
except ValueError: except ValueError:
# Unable to determine dependencies # Unable to determine dependencies
# Assume none # Assume none
return return info
if meta.summary:
info['summary'] = meta.summary or ''
info['requires_python'] = meta.requires_python
if meta.requires_dist: if meta.requires_dist:
return meta.requires_dist info['requires_dist'] = meta.requires_dist
return info
def _get_info_from_sdist(self, url
): # type: (str) -> Dict[str, Union[str, List, None]]
info = {
'summary': '',
'requires_python': None,
'requires_dist': None,
}
def _get_requires_dist_from_sdist(self, url
): # type: (str) -> Union[list, None]
filename = os.path.basename(urlparse.urlparse(url).path) filename = os.path.basename(urlparse.urlparse(url).path)
with temporary_directory() as temp_dir: with temporary_directory() as temp_dir:
...@@ -384,9 +396,16 @@ class PyPiRepository(Repository): ...@@ -384,9 +396,16 @@ class PyPiRepository(Repository):
try: try:
meta = pkginfo.SDist(str(filepath)) meta = pkginfo.SDist(str(filepath))
if meta.summary:
info['summary'] = meta.summary
if meta.requires_python:
info['requires_python'] = meta.requires_python
if meta.requires_dist: if meta.requires_dist:
return meta.requires_dist info['requires_dist'] = list(meta.requires_dist)
return info
except ValueError: except ValueError:
# Unable to determine dependencies # Unable to determine dependencies
# We pass and go deeper # We pass and go deeper
...@@ -417,7 +436,7 @@ class PyPiRepository(Repository): ...@@ -417,7 +436,7 @@ class PyPiRepository(Repository):
unpacked = Path(temp_dir) / 'unpacked' unpacked = Path(temp_dir) / 'unpacked'
sdist_dir = unpacked / Path(filename).name.rstrip('.tar.gz') sdist_dir = unpacked / Path(filename).name.rstrip('.tar.gz')
# Checking for .egg-info # Checking for .egg-info at root
eggs = list(sdist_dir.glob('*.egg-info')) eggs = list(sdist_dir.glob('*.egg-info'))
if eggs: if eggs:
egg_info = eggs[0] egg_info = eggs[0]
...@@ -425,16 +444,68 @@ class PyPiRepository(Repository): ...@@ -425,16 +444,68 @@ class PyPiRepository(Repository):
requires = egg_info / 'requires.txt' requires = egg_info / 'requires.txt'
if requires.exists(): if requires.exists():
with requires.open() as f: with requires.open() as f:
return parse_requires(f.read()) info['requires_dist'] = parse_requires(f.read())
return info
return # Searching for .egg-info in sub directories
eggs = list(sdist_dir.glob('**/*.egg-info'))
if eggs:
egg_info = eggs[0]
requires = egg_info / 'requires.txt'
if requires.exists():
with requires.open() as f:
info['requires_dist'] = parse_requires(f.read())
return info
# Still nothing, assume no dependencies # Still nothing, assume no dependencies
# We could probably get them by executing # We could probably get them by executing
# python setup.py egg-info but I don't feel # python setup.py egg-info but I don't feel
# confortable executing a file just for the sake # confortable executing a file just for the sake
# of getting dependencies. # of getting dependencies.
return return info
def _inspect_sdist_with_setup(self, sdist_dir):
info = {
'requires_python': None,
'requires_dist': None,
}
setup = sdist_dir / 'setup.py'
if not setup.exists():
return info
venv = Venv.create(NullIO())
current_dir = os.getcwd()
os.chdir(sdist_dir.as_posix())
try:
venv.run(
'python', 'setup.py', 'egg_info'
)
egg_info = list(sdist_dir.glob('**/*.egg-info'))[0]
meta = pkginfo.UnpackedSDist(str(egg_info))
if meta.requires_python:
info['requires_python'] = meta.requires_python
if meta.requires_dist:
info['requires_dist'] = list(meta.requires_dist)
else:
requires = egg_info / 'requires.txt'
if requires.exists():
with requires.open() as f:
info['requires_dist'] = parse_requires(f.read())
except Exception:
pass
os.chdir(current_dir)
return info
def _download(self, url, dest): # type: (str, str) -> None def _download(self, url, dest): # type: (str, str) -> None
r = get(url, stream=True) r = get(url, stream=True)
......
from poetry.semver.constraints import Constraint from poetry.semver import parse_constraint
from poetry.semver.constraints.base_constraint import BaseConstraint from poetry.semver import VersionConstraint
from poetry.semver.version_parser import VersionParser
from poetry.version import parse as parse_version
from .base_repository import BaseRepository from .base_repository import BaseRepository
...@@ -20,10 +17,20 @@ class Repository(BaseRepository): ...@@ -20,10 +17,20 @@ class Repository(BaseRepository):
def package(self, name, version, extras=None): def package(self, name, version, extras=None):
name = name.lower() name = name.lower()
version = str(parse_version(version))
if extras is None:
extras = []
for package in self.packages: for package in self.packages:
if name == package.name and package.version == version: if name == package.name and package.version.text == version:
# Activate extra dependencies
for extra in extras:
if extra in package.extras:
for extra_dep in package.extras[extra]:
for dep in package.requires:
if dep.name == extra_dep.lower():
dep.activate()
return package return package
def find_packages(self, name, constraint=None, def find_packages(self, name, constraint=None,
...@@ -37,15 +44,15 @@ class Repository(BaseRepository): ...@@ -37,15 +44,15 @@ class Repository(BaseRepository):
if constraint is None: if constraint is None:
constraint = '*' constraint = '*'
if not isinstance(constraint, BaseConstraint): if not isinstance(constraint, VersionConstraint):
parser = VersionParser() constraint = parse_constraint(constraint)
constraint = parser.parse_constraints(constraint)
for package in self.packages: for package in self.packages:
if name == package.name: if name == package.name:
pkg_constraint = Constraint('==', package.version) if package.is_prerelease() and not allow_prereleases:
continue
if constraint is None or constraint.matches(pkg_constraint): if constraint is None or constraint.allows(package.version):
for dep in package.requires: for dep in package.requires:
for extra in extras: for extra in extras:
if extra not in package.extras: if extra not in package.extras:
...@@ -84,5 +91,14 @@ class Repository(BaseRepository): ...@@ -84,5 +91,14 @@ class Repository(BaseRepository):
if index is not None: if index is not None:
del self._packages[index] del self._packages[index]
def search(self, query, mode=0):
results = []
for package in self.packages:
if query in package.name:
results.append(package)
return results
def __len__(self): def __len__(self):
return len(self._packages) return len(self._packages)
from functools import cmp_to_key import re
from .comparison import less_than from .empty_constraint import EmptyConstraint
from .constraints import Constraint from .patterns import BASIC_CONSTRAINT
from .helpers import normalize_version from .patterns import CARET_CONSTRAINT
from .version_parser import VersionParser from .patterns import TILDE_CONSTRAINT
from .patterns import X_CONSTRAINT
SORT_ASC = 1 from .version import Version
SORT_DESC = -1 from .version_constraint import VersionConstraint
from .version_range import VersionRange
_parser = VersionParser() from .version_union import VersionUnion
def statisfies(version, constraints): def parse_constraint(constraints): # type: (str) -> VersionConstraint
""" if constraints == '*':
Determine if given version satisfies given constraints. return VersionRange()
:type version: str or_constraints = re.split('\s*\|\|?\s*', constraints.strip())
:type constraints: str or_groups = []
for constraints in or_constraints:
:rtype: bool and_constraints = re.split(
""" '(?<!^)(?<![=>< ,]) *(?<!-)[, ](?!-) *(?!,|$)',
provider = Constraint('==', normalize_version(version)) constraints
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)
) )
) constraint_objects = []
return [versions[i] for i, _ in normalized] if len(and_constraints) > 1:
for constraint in and_constraints:
constraint_objects.append(parse_single_constraint(constraint))
else:
constraint_objects.append(parse_single_constraint(and_constraints[0]))
if len(constraint_objects) == 1:
constraint = constraint_objects[0]
else:
constraint = constraint_objects[0]
for next_constraint in constraint_objects[1:]:
constraint = constraint.intersect(next_constraint)
or_groups.append(constraint)
if len(or_groups) == 1:
return or_groups[0]
else:
return VersionUnion.of(*or_groups)
def parse_single_constraint(constraint): # type: (str) -> VersionConstraint
m = re.match('(?i)^v?[xX*](\.[xX*])*$', constraint)
if m:
return VersionRange()
# Tilde range
m = TILDE_CONSTRAINT.match(constraint)
if m:
version = Version.parse(m.group(1))
high = version.stable.next_minor
if len(m.group(1).split('.')) == 1:
high = version.stable.next_major
return VersionRange(version, high, include_min=True)
# Caret range
m = CARET_CONSTRAINT.match(constraint)
if m:
version = Version.parse(m.group(1))
return VersionRange(version, version.next_breaking, include_min=True)
# X Range
m = X_CONSTRAINT.match(constraint)
if m:
op = m.group(1)
major = int(m.group(2))
minor = m.group(3)
if minor is not None:
version = Version(major, int(minor), 0)
result = VersionRange(version, version.next_minor, include_min=True)
else:
if major == 0:
result = VersionRange(max=Version(1, 0, 0))
else:
version = Version(major, 0, 0)
result = VersionRange(version, version.next_major, include_min=True)
if op == '!=':
result = VersionRange().difference(result)
return result
# Basic comparator
m = BASIC_CONSTRAINT.match(constraint)
if m:
op = m.group(1)
version = m.group(2)
try:
version = Version.parse(version)
except ValueError:
raise ValueError('Could not parse version constraint: {}'.format(constraint))
if op == '<':
return VersionRange(max=version)
elif op == '<=':
return VersionRange(max=version, include_max=True)
elif op == '>':
return VersionRange(min=version)
elif op == '>=':
return VersionRange(min=version, include_min=True)
elif op == '!=':
return VersionUnion(
VersionRange(max=version),
VersionRange(min=version)
)
else:
return version
raise ValueError('Could not parse version constraint: {}'.format(constraint))
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
class BaseConstraint(object):
def matches(self, provider):
raise NotImplementedError()
import operator
from poetry.version import parse as parse_version
from poetry.version import version_compare
from .base_constraint import BaseConstraint
class Constraint(BaseConstraint):
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
}
_trans_op_int = {
OP_EQ: '==',
OP_LT: '<',
OP_LE: '<=',
OP_GT: '>',
OP_GE: '>=',
OP_NE: '!='
}
def __init__(self, operator, version): # type: (str, str) -> None
if operator not in self.supported_operators:
raise ValueError(
'Invalid operator "{}" given, '
'expected one of: {}'
.format(
operator, ', '.join(self.supported_operators)
)
)
self._operator = self._trans_op_str[operator]
self._string_operator = operator
self._version = str(parse_version(version))
@property
def supported_operators(self): # type: () -> list
return list(self._trans_op_str.keys())
@property
def operator(self):
return self._operator
@property
def string_operator(self):
return self._string_operator
@property
def version(self): # type: () -> str
return self._version
def matches(self, provider):
if (
isinstance(provider, self.__class__)
and provider.__class__ is self.__class__
):
return self.match_specific(provider)
# turn matching around to find a match
return provider.matches(self)
def version_compare(self, a, b, operator
): # type: (str, str, str) -> bool
if operator not in self._trans_op_str:
raise ValueError(
'Invalid operator "{}" given, '
'expected one of: {}'
.format(
operator, ', '.join(self.supported_operators)
)
)
return version_compare(a, b, operator)
def match_specific(self, provider): # type: (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))
from .version_constraint import VersionConstraint
class EmptyConstraint(VersionConstraint):
def is_empty(self):
return True
def is_any(self):
return False
def allows(self, version):
return False
def allows_all(self, other):
return other.is_empty()
def allows_any(self, other):
return False
def intersect(self, other):
return self
def union(self, other):
return other
def difference(self, other):
return self
def __str__(self):
return '<empty>'
import re
_modifier_regex = (
'[._-]?'
'(?:(stable|beta|b|RC|c|pre|alpha|a|patch|pl|p|post|[a-z])'
'((?:[.-]?\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 = '{}{}{}{}'.format(
m.group(1),
m.group(2) if m.group(2) else '.0',
m.group(3) if m.group(3) else '.0',
m.group(4) if m.group(4) else '.0',
)
index = 5
else:
# Some versions have the form M.m.p-\d+
# which means M.m.p-post\d+
m = re.match(
'(?i)^v?(\d{1,5})(\.\d+)?(\.\d+)?(\.\d+)?-(\d+)$',
version
)
if m:
version = '{}{}{}{}'.format(
m.group(1),
m.group(2) if m.group(2) else '.0',
m.group(3) if m.group(3) else '.0',
m.group(4) if m.group(4) else '.0',
)
if m.group(5):
version += '-post.' + m.group(5)
m = re.match(
'(?i)^v?(\d{{1,5}})(\.\d+)?(\.\d+)?(\.\d+)?{}$'.format(
_modifier_regex
),
version
)
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):
if m.group(index) == 'post':
# Post releases should be considered
# stable releases
if '-post' in version:
return version
version = '{}-post'.format(version)
else:
version = '{}-{}'.format(
version, _expand_stability(m.group(index))
)
if m.group(index + 1):
version = '{}.{}'.format(
version, m.group(index + 1).lstrip('.-')
)
return version
raise ValueError('Invalid version string "{}"'.format(version))
def normalize_stability(stability): # type: (str) -> str
stability = stability.lower()
if stability == 'rc':
return 'RC'
return stability
def parse_stability(version): # type: (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.search('(?i){}(?:\+.*)?$'.format(_modifier_regex), version.lower())
if m:
if m.group(3):
return 'dev'
if m.group(1):
if m.group(1) in ['beta', 'b']:
return 'beta'
elif m.group(1) in ['alpha', 'a']:
return 'alpha'
elif m.group(1) in ['rc', 'c']:
return 'RC'
elif m.group(1) == 'post':
return 'stable'
else:
return 'dev'
return 'stable'
def _expand_stability(stability): # type: (str) -> str
stability = stability.lower()
if stability == 'a':
return 'alpha'
elif stability == 'b':
return 'beta'
elif stability in ['c', 'pre']:
return 'rc'
elif stability in ['p', 'pl']:
return 'patch'
elif stability in ['post']:
return 'post'
return stability
import re
MODIFIERS = (
'[._-]?'
'((?!post)(?:beta|b|c|pre|RC|alpha|a|patch|pl|p|dev)(?:(?:[.-]?\d+)*)?)?'
'([+-]?([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?'
)
_COMPLETE_VERSION = 'v?(\d+)(?:\.(\d+))?(?:\.(\d+))?{}(?:\+[^\s]+)?'.format(MODIFIERS)
COMPLETE_VERSION = re.compile('(?i)' + _COMPLETE_VERSION)
CARET_CONSTRAINT = re.compile('(?i)^\^({})$'.format(_COMPLETE_VERSION))
TILDE_CONSTRAINT = re.compile('(?i)^~=?({})$'.format(_COMPLETE_VERSION))
X_CONSTRAINT = re.compile('^(!= ?|==)?v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$')
BASIC_CONSTRAINT = re.compile('(?i)^(<>|!=|>=?|<=?|==?)?\s*({})'.format(_COMPLETE_VERSION))
import re
from typing import List
from typing import Union
from .empty_constraint import EmptyConstraint
from .patterns import COMPLETE_VERSION
from .version_constraint import VersionConstraint
from .version_range import VersionRange
from .version_union import VersionUnion
class Version(VersionRange):
"""
A parsed semantic version number.
"""
def __init__(self,
major, # type: int
minor=None, # type: Union[int, None]
patch=None, # type: Union[int, None]
pre=None, # type: Union[str, None]
build=None, # type: Union[str, None]
text=None, # type: Union[str, None]
precision=None # type: Union[str, None]
): # type: () -> None
self._major = int(major)
self._precision = None
if precision is None:
self._precision = 1
if minor is None:
minor = 0
else:
if self._precision is not None:
self._precision += 1
self._minor = int(minor)
if patch is None:
patch = 0
else:
if self._precision is not None:
self._precision += 1
if precision is not None:
self._precision = precision
self._patch = int(patch)
if text is None:
parts = [str(major)]
if self._precision >= 2 or minor != 0:
parts.append(str(minor))
if self._precision >= 3 or patch != 0:
parts.append(str(patch))
text = '.'.join(parts)
if pre:
text += '-{}'.format(pre)
if build:
text += '+{}'.format(build)
self._text = text
pre = self._normalize_prerelease(pre)
self._prerelease = []
if pre is not None:
self._prerelease = self._split_parts(pre)
build = self._normalize_build(build)
self._build = []
if build is not None:
if build.startswith(('-', '+')):
build = build[1:]
self._build = self._split_parts(build)
@property
def major(self): # type: () -> int
return self._major
@property
def minor(self): # type: () -> int
return self._minor
@property
def patch(self): # type: () -> int
return self._patch
@property
def prerelease(self): # type: () -> List[str]
return self._prerelease
@property
def build(self): # type: () -> List[str]
return self._build
@property
def text(self):
return self._text
@property
def stable(self):
if not self.is_prerelease():
return self
return self.next_patch
@property
def next_major(self): # type: () -> Version
if self.is_prerelease() and self.minor == 0 and self.patch == 0:
return Version(self.major, self.minor, self.patch)
return self._increment_major()
@property
def next_minor(self): # type: () -> Version
if self.is_prerelease() and self.patch == 0:
return Version(self.major, self.minor, self.patch)
return self._increment_minor()
@property
def next_patch(self): # type: () -> Version
if self.is_prerelease():
return Version(self.major, self.minor, self.patch)
return self._increment_patch()
@property
def next_breaking(self): # type: () -> Version
if self.major == 0:
if self.minor != 0:
return self._increment_minor()
if self._precision == 1:
return self._increment_major()
elif self._precision == 2:
return self._increment_minor()
return self._increment_patch()
return self._increment_major()
@property
def first_prerelease(self): # type: () -> Version
return Version.parse('{}.{}.{}-alpha.0'.format(self.major, self.minor, self.patch))
@property
def min(self):
return self
@property
def max(self):
return self
@property
def include_min(self):
return True
@property
def include_max(self):
return True
@classmethod
def parse(cls, text): # type: (str) -> Version
match = COMPLETE_VERSION.match(text)
if match is None:
raise ValueError('Unable to parse "{}".'.format(text))
text = text.rstrip('.')
major = int(match.group(1))
minor = int(match.group(2)) if match.group(2) else None
patch = int(match.group(3)) if match.group(3) else None
pre = match.group(4)
build = match.group(5)
if build:
build = build.lstrip('+')
return Version(major, minor, patch, pre, build, text)
def is_any(self):
return False
def is_empty(self):
return False
def is_prerelease(self): # type: () -> bool
return len(self._prerelease) > 0
def allows(self, version): # type: (Version) -> bool
return self == version
def allows_all(self, other): # type: (VersionConstraint) -> bool
return other.is_empty() or other == self
def allows_any(self, other): # type: (VersionConstraint) -> bool
return other.allows(self)
def intersect(self, other): # type: (VersionConstraint) -> VersionConstraint
if other.allows(self):
return self
return EmptyConstraint()
def union(self, other): # type: (VersionConstraint) -> VersionConstraint
from .version_range import VersionRange
if other.allows(self):
return other
if isinstance(other, VersionRange):
if other.min == self:
return VersionRange(
other.min,
other.max,
include_min=True,
include_max=other.include_max
)
if other.max == self:
return VersionRange(
other.min,
other.max,
include_min=other.include_min,
include_max=True
)
return VersionUnion.of(self, other)
def difference(self, other): # type: (VersionConstraint) -> VersionConstraint
if other.allows(self):
return EmptyConstraint()
return self
def _increment_major(self): # type: () -> Version
return Version(self.major + 1, 0, 0, precision=self._precision)
def _increment_minor(self): # type: () -> Version
return Version(self.major, self.minor + 1, 0, precision=self._precision)
def _increment_patch(self): # type: () -> Version
return Version(self.major, self.minor, self.patch + 1, precision=self._precision)
def _normalize_prerelease(self, pre): # type: (str) -> str
if not pre:
return
m = re.match('(?i)^(a|alpha|b|beta|c|pre|rc|dev)[-.]?(\d+)?$', pre)
if not m:
return
modifier = m.group(1)
number = m.group(2)
if number is None:
number = 0
if modifier == 'a':
modifier = 'alpha'
elif modifier == 'b':
modifier = 'beta'
elif modifier in {'c', 'pre'}:
modifier = 'rc'
elif modifier == 'dev':
modifier = 'alpha'
return '{}.{}'.format(modifier, number)
def _normalize_build(self, build): # type: (str) -> str
if not build:
return
if build == '0':
return
if build.startswith('post'):
build = build.lstrip('post')
if not build:
return
return build
def _split_parts(self, text): # type: (str) -> List[Union[str, int]]
parts = text.split('.')
for i, part in enumerate(parts):
try:
parts[i] = int(part)
except (TypeError, ValueError):
continue
return parts
def __lt__(self, other):
return self._cmp(other) < 0
def __le__(self, other):
return self._cmp(other) <= 0
def __gt__(self, other):
return self._cmp(other) > 0
def __ge__(self, other):
return self._cmp(other) >= 0
def _cmp(self, other):
if not isinstance(other, VersionConstraint):
return NotImplemented
if not isinstance(other, Version):
return -other._cmp(self)
if self.major != other.major:
return self._cmp_parts(self.major, other.major)
if self.minor != other.minor:
return self._cmp_parts(self.minor, other.minor)
if self.patch != other.patch:
return self._cmp_parts(self.patch, other.patch)
# Pre-releases always come before no pre-release string.
if not self.is_prerelease() and other.is_prerelease():
return 1
if not other.is_prerelease() and self.is_prerelease():
return -1
comparison = self._cmp_lists(self.prerelease, other.prerelease)
if comparison != 0:
return comparison
# Builds always come after no build string.
if not self.build and other.build:
return -1
if not other.build and self.build:
return 1
return self._cmp_lists(self.build, other.build)
def _cmp_parts(self, a, b):
if a < b:
return -1
elif a > b:
return 1
return 0
def _cmp_lists(self, a, b): # type: (List, List) -> int
for i in range(max(len(a), len(b))):
a_part = None
if i < len(a):
a_part = a[i]
b_part = None
if i < len(b):
b_part = b[i]
if a_part == b_part:
continue
# Missing parts come before present ones.
if a_part is None:
return -1
if b_part is None:
return 1
if isinstance(a_part, int):
if isinstance(b_part, int):
return self._cmp_parts(a_part, b_part)
return -1
else:
if isinstance(b_part, int):
return 1
return self._cmp_parts(a_part, b_part)
return 0
def __eq__(self, other): # type: (Version) -> bool
if not isinstance(other, Version):
return NotImplemented
return (
self._major == other.major
and self._minor == other.minor
and self._patch == other.patch
and self._prerelease == other.prerelease
and self._build == other.build
)
def __ne__(self, other):
return not self == other
def __str__(self):
return self._text
def __repr__(self):
return '<Version {}>'.format(str(self))
def __hash__(self):
return hash(
(self.major,
self.minor,
self.patch,
'.'.join(str(p) for p in self.prerelease),
'.'.join(str(p) for p in self.build)))
class VersionConstraint:
def is_empty(self): # type: () -> bool
raise NotImplementedError()
def is_any(self): # type: () -> bool
raise NotImplementedError()
def allows(self, version): # type: (Version) -> bool
raise NotImplementedError()
def allows_all(self, other): # type: (VersionConstraint) -> bool
raise NotImplementedError()
def allows_any(self, other): # type: (VersionConstraint) -> bool
raise NotImplementedError()
def intersect(self, other): # type: (VersionConstraint) -> VersionConstraint
raise NotImplementedError()
def union(self, other): # type: (VersionConstraint) -> VersionConstraint
raise NotImplementedError()
def difference(self, other): # type: (VersionConstraint) -> VersionConstraint
raise NotImplementedError()
import re
from typing import Tuple
from typing import Union
from .constraints.constraint import Constraint
from .constraints.base_constraint import BaseConstraint
from .constraints.empty_constraint import EmptyConstraint
from .constraints.multi_constraint import MultiConstraint
from .constraints.wildcard_constraint import WilcardConstraint
from .helpers import normalize_version, _expand_stability, parse_stability
class VersionParser:
_modifier_regex = (
'[._-]?'
'(?:(stable|beta|b|RC|alpha|a|patch|post|pl|p)((?:[.-]?\d+)*)?)?'
'([.-]?dev)?'
)
_stabilities = [
'stable', 'RC', 'beta', 'alpha', 'dev'
]
def parse_constraints(
self, constraints
): # type: (str) -> Union[Constraint, MultiConstraint]
"""
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
): # type: (str) -> Union[Tuple[BaseConstraint], Tuple[BaseConstraint, BaseConstraint]]
m = re.match('(?i)^v?[xX*](\.[xX*])*$', constraint)
if m:
return EmptyConstraint(),
# Some versions have the form M.m.p-\d+
# which means M.m.p-post\d+
m = re.match(
'(?i)^(~=?|\^|<> ?|!= ?|>=? ?|<=? ?|==? ?)v?(\d{{1,5}})(\.\d+)?(\.\d+)?(\.\d+)?-(\d+){}$'.format(
self._modifier_regex
),
constraint
)
if m:
constraint = '{}{}{}{}{}'.format(
m.group(1),
m.group(2),
m.group(3) if m.group(3) else '.0',
m.group(4) if m.group(4) else '.0',
m.group(5) if m.group(5) else '.0',
)
if m.group(6):
constraint += '-post.' + m.group(6)
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 = 2
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 updates for 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
# 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 = 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:
# We just leave it as is
return WilcardConstraint(constraint),
# Basic Comparators
m = re.match('^(<>|!=|>=?|<=?|==?)?\s*(.*)', constraint)
if m:
try:
version = normalize_version(m.group(2))
stability = parse_stability(version)
stability_re = re.match(
'(?:[^-]*)(-{})$'.format(self._modifier_regex),
m.group(2).lower()
)
if stability == 'stable' and stability_re:
version = version.split('-')[0] + stability_re.group(1)
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 pad
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])
from .empty_constraint import EmptyConstraint
from .version_constraint import VersionConstraint
from .version_union import VersionUnion
class VersionRange(VersionConstraint):
def __init__(self,
min=None,
max=None,
include_min=False,
include_max=False,
always_include_max_prerelease=False):
self._min = min
self._max = max
self._include_min = include_min
self._include_max = include_max
@property
def min(self):
return self._min
@property
def max(self):
return self._max
@property
def include_min(self):
return self._include_min
@property
def include_max(self):
return self._include_max
def is_empty(self):
return False
def is_any(self):
return self._min is None and self._max is None
def allows(self, other): # type: (Version) -> bool
if self._min is not None:
if other < self._min:
return False
if not self._include_min and other == self._min:
return False
if self._max is not None:
if other > self._max:
return False
if not self._include_max and other == self._max:
return False
return True
def allows_all(self, other): # type: (VersionConstraint) -> bool
from .version import Version
if other.is_empty():
return True
if isinstance(other, Version):
return self.allows(other)
if isinstance(other, VersionUnion):
return all([self.allows_all(constraint) for constraint in other.ranges])
if isinstance(other, VersionRange):
return not other.allows_lower(self) and not other.allows_higher(self)
raise ValueError('Unknown VersionConstraint type {}.'.format(other))
def allows_any(self, other): # type: (VersionConstraint) -> bool
from .version import Version
if other.is_empty():
return False
if isinstance(other, Version):
return self.allows(other)
if isinstance(other, VersionUnion):
return any([self.allows_any(constraint) for constraint in other.ranges])
if isinstance(other, VersionRange):
return not other.is_strictly_lower(self) and not other.is_strictly_higher(self)
raise ValueError('Unknown VersionConstraint type {}.'.format(other))
def intersect(self, other): # type: (VersionConstraint) -> VersionConstraint
from .version import Version
if other.is_empty():
return other
if isinstance(other, VersionUnion):
return other.intersect(self)
# A range and a Version just yields the version if it's in the range.
if isinstance(other, Version):
if self.allows(other):
return other
return EmptyConstraint()
if not isinstance(other, VersionRange):
raise ValueError('Unknown VersionConstraint type {}.'.format(other))
if self.allows_lower(other):
if self.is_strictly_lower(other):
return EmptyConstraint()
intersect_min = other.min
intersect_include_min = other.include_min
else:
if other.is_strictly_lower(self):
return EmptyConstraint()
intersect_min = self._min
intersect_include_min = self._include_min
if self.allows_higher(other):
intersect_max = other.max
intersect_include_max = other.include_max
else:
intersect_max = self._max
intersect_include_max = self._include_max
if intersect_min is None and intersect_max is None:
return VersionRange()
# If the range is just a single version.
if intersect_min == intersect_max:
# Because we already verified that the lower range isn't strictly
# lower, there must be some overlap.
assert intersect_include_min and intersect_include_max
return intersect_min
# If we got here, there is an actual range.
return VersionRange(
intersect_min,
intersect_max,
intersect_include_min,
intersect_include_max
)
def union(self, other): # type: (VersionConstraint) -> VersionConstraint
from .version import Version
if isinstance(other, Version):
if self.allows(other):
return self
if other == self.min:
return VersionRange(
self.min,
self.max,
include_min=True,
include_max=self.include_max
)
if other == self.max:
return VersionRange(
self.min,
self.max,
include_min=self.include_min,
include_max=True
)
return VersionUnion.of(self, other)
if isinstance(other, VersionRange):
# If the two ranges don't overlap, we won't be able to create a single
# VersionRange for both of them.
edges_touch = (
(self.max == other.min and (self.include_max or other.include_min))
or (self.min == other.max and (self.include_min or other.include_max))
)
if not edges_touch and not self.allows_any(other):
return VersionUnion.of(self, other)
if self.allows_lower(other):
union_min = self.min
union_include_min = self.include_min
else:
union_min = other.min
union_include_min = other.include_min
if self.allows_higher(other):
union_max = self.max
union_include_max = self.include_max
else:
union_max = other.max
union_include_max = other.include_max
return VersionRange(
union_min,
union_max,
include_min=union_include_min,
include_max=union_include_max
)
return VersionUnion.of(self, other)
def difference(self, other): # type: (VersionConstraint) -> VersionConstraint
from .version import Version
if other.is_empty():
return self
if isinstance(other, Version):
if not self.allows(other):
return self
if other == self.min:
if not self.include_min:
return self
return VersionRange(
self.min,
self.max,
False,
self.include_max
)
if other == self.max:
if not self.include_max:
return self
return VersionRange(
self.min,
self.max,
self.include_min,
False
)
return VersionUnion.of(
VersionRange(
self.min,
other,
self.include_min,
False
),
VersionRange(
other,
self.max,
False,
self.include_max
)
)
elif isinstance(other, VersionRange):
if not self.allows_any(other):
return self
if not self.allows_lower(other):
before = None
elif self.min == other.min:
before = self.min
else:
before = VersionRange(
self.min,
other.min,
self.include_min,
not other.include_min
)
if not self.allows_higher(other):
after = None
elif self.max == other.max:
after = self.max
else:
after = VersionRange(
other.max,
self.max,
not other.include_max,
self.include_max
)
if before is None and after is None:
return EmptyConstraint()
if before is None:
return after
if after is None:
return before
return VersionUnion.of(before, after)
elif isinstance(other, VersionUnion):
ranges = [] # type: List[VersionRange]
current = self
for range in other.ranges:
# Skip any ranges that are strictly lower than [current].
if range.is_strictly_lower(current):
continue
# If we reach a range strictly higher than [current], no more ranges
# will be relevant so we can bail early.
if range.is_strictly_higher(current):
break
difference = current.difference(range)
if difference.is_empty():
return EmptyConstraint()
elif isinstance(difference, VersionUnion):
# If [range] split [current] in half, we only need to continue
# checking future ranges against the latter half.
ranges.append(difference.ranges[0])
current = difference.ranges[-1]
else:
current = difference
if not ranges:
return current
return VersionUnion.of(*(ranges + [current]))
raise ValueError('Unknown VersionConstraint type {}.'.format(other))
def allows_lower(self, other): # type: (VersionRange) -> bool
if self.min is None:
return other.min is not None
if other.min is None:
return False
if self.min < other.min:
return True
if self.min > other.min:
return False
return self.include_min and not other.include_min
def allows_higher(self, other): # type: (VersionRange) -> bool
if self.max is None:
return other.max is not None
if other.max is None:
return False
if self.max < other.max:
return False
if self.max > other.max:
return True
return self.include_max and not other.include_max
def is_strictly_lower(self, other): # type: (VersionRange) -> bool
if self.max is None or other.min is None:
return False
if self.max < other.min:
return True
if self.max > other.min:
return False
return not self.include_max or not other.include_min
def is_strictly_higher(self, other): # type: (VersionRange) -> bool
return other.is_strictly_lower(self)
def is_adjacent_to(self, other): # type: (VersionRange) -> bool
if self.max != other.min:
return False
return (
self.include_max and not other.include_min
or not self.include_max and other.include_min
)
def __eq__(self, other):
if not isinstance(other, VersionRange):
return False
return (
self._min == other.min
and self._max == other.max
and self._include_min == other.include_min
and self._include_max == other.include_max
)
def __lt__(self, other):
return self._cmp(other) < 0
def __le__(self, other):
return self._cmp(other) <= 0
def __gt__(self, other):
return self._cmp(other) > 0
def __ge__(self, other):
return self._cmp(other) >= 0
def _cmp(self, other): # type: (VersionRange) -> int
if self.min is None:
if other.min is None:
return self._compare_max(other)
return -1
elif other.min is None:
return 1
result = self.min._cmp(other.min)
if result != 0:
return result
if self.include_min != other.include_min:
return -1 if self.include_min else 1
return self._compare_max(other)
def _compare_max(self, other): # type: (VersionRange) -> int
if self.max is None:
if other.max is None:
return 0
return 1
elif other.max is None:
return -1
result = self.max._cmp(other.max)
if result != 0:
return result
if self.include_max != other.include_max:
return 1 if self.include_max else -1
return 0
def __str__(self):
text = ''
if self.min is not None:
text += '>=' if self.include_min else '>'
text += self.min.text
if self.max is not None:
if self.min is not None:
text += ','
text += '{}{}'.format('<=' if self.include_max else '<', self.max.text)
if self.min is None and self.max is None:
return '*'
return text
def __repr__(self):
return '<VersionRange ({})>'.format(str(self))
def __hash__(self):
return hash((self.min, self.max, self.include_min, self.include_max))
from .empty_constraint import EmptyConstraint
from .version_constraint import VersionConstraint
class VersionUnion(VersionConstraint):
"""
A version constraint representing a union of multiple disjoint version
ranges.
An instance of this will only be created if the version can't be represented
as a non-compound value.
"""
def __init__(self, *ranges):
self._ranges = list(ranges)
@property
def ranges(self):
return self._ranges
@classmethod
def of(cls, *ranges):
from .version_range import VersionRange
flattened = []
for constraint in ranges:
if constraint.is_empty():
continue
if isinstance(constraint, VersionUnion):
flattened += constraint.ranges
continue
flattened.append(constraint)
if not flattened:
return EmptyConstraint()
if any([constraint.is_any() for constraint in flattened]):
return VersionRange()
# Only allow Versions and VersionRanges here so we can more easily reason
# about everything in flattened. _EmptyVersions and VersionUnions are
# filtered out above.
for constraint in flattened:
if isinstance(constraint, VersionRange):
continue
raise ValueError('Unknown VersionConstraint type {}.'.format(constraint))
flattened.sort()
merged = []
for constraint in flattened:
# Merge this constraint with the previous one, but only if they touch.
if (
not merged
or (
not merged[-1].allows_any(constraint)
and not merged[-1].is_adjacent_to(constraint)
)
):
merged.append(constraint)
else:
merged[-1] = merged[-1].union(constraint)
if len(merged) == 1:
return merged[0]
return VersionUnion(*merged)
def is_empty(self):
return False
def is_any(self):
return False
def allows(self, version): # type: (Version) -> bool
return any([
constraint.allows(version) for constraint in self._ranges
])
def allows_all(self, other): # type: (VersionConstraint) -> bool
our_ranges = iter(self._ranges)
their_ranges = iter(self._ranges_for(other))
our_current_range = next(our_ranges, None)
their_current_range = next(their_ranges, None)
while our_current_range and their_current_range:
if our_current_range.allows_all(their_current_range):
their_current_range = next(their_ranges, None)
else:
our_current_range = next(our_ranges, None)
return their_current_range is None
def allows_any(self, other): # type: (VersionConstraint) -> bool
our_ranges = iter(self._ranges)
their_ranges = iter(self._ranges_for(other))
our_current_range = next(our_ranges, None)
their_current_range = next(their_ranges, None)
while our_current_range and their_current_range:
if our_current_range.allows_any(their_current_range):
return True
if their_current_range.allows_higher(our_current_range):
our_current_range = next(our_ranges, None)
else:
their_current_range = next(their_ranges, None)
return False
def intersect(self, other): # type: (VersionConstraint) -> VersionConstraint
our_ranges = iter(self._ranges)
their_ranges = iter(self._ranges_for(other))
new_ranges = []
our_current_range = next(our_ranges, None)
their_current_range = next(their_ranges, None)
while our_current_range and their_current_range:
intersection = our_current_range.intersect(their_current_range)
if not intersection.is_empty():
new_ranges.append(intersection)
if their_current_range.allows_higher(our_current_range):
our_current_range = next(our_ranges, None)
else:
their_current_range = next(their_ranges, None)
return VersionUnion.of(*new_ranges)
def union(self, other): # type: (VersionConstraint) -> VersionConstraint
return VersionUnion.of(self, other)
def difference(self, other): # type: (VersionConstraint) -> VersionConstraint
our_ranges = iter(self._ranges)
their_ranges = iter(self._ranges_for(other))
new_ranges = []
state = {
'current': next(our_ranges, None),
'their_range': next(their_ranges, None),
}
def their_next_range():
state['their_range'] = next(their_ranges, None)
if state['their_range']:
return True
new_ranges.append(state['current'])
our_current = next(our_ranges, None)
while our_current:
new_ranges.append(our_current)
our_current = next(our_ranges, None)
return False
def our_next_range(include_current=True):
if include_current:
new_ranges.append(state['current'])
our_current = next(our_ranges, None)
if not our_current:
return False
state['current'] = our_current
return True
while True:
if state['their_range'].is_strictly_lower(state['current']):
if not their_next_range():
break
continue
if state['their_range'].is_strictly_higher(state['current']):
if not our_next_range():
break
continue
difference = state['current'].difference(state['their_range'])
if isinstance(difference, VersionUnion):
assert len(difference.ranges) == 2
new_ranges.append(difference.ranges[0])
state['current'] = difference.ranges[-1]
if not their_next_range():
break
elif difference.is_empty():
if not our_next_range(False):
break
else:
state['current'] = difference
if state['current'].allows_higher(state['their_range']):
if not their_next_range():
break
else:
if not our_next_range():
break
if not new_ranges:
return EmptyConstraint()
if len(new_ranges) == 1:
return new_ranges[0]
return VersionUnion.of(*new_ranges)
def _ranges_for(self, constraint): # type: (VersionConstraint) -> List[VersionRange]
from .version_range import VersionRange
if constraint.is_empty():
return []
if isinstance(constraint, VersionUnion):
return constraint.ranges
if isinstance(constraint, VersionRange):
return [constraint]
raise ValueError('Unknown VersionConstraint type {}'.format(constraint))
def __eq__(self, other):
if not isinstance(other, VersionUnion):
return False
return self._ranges == other.ranges
def __str__(self):
return ' || '.join([str(r) for r in self._ranges])
def __repr__(self):
return '<VersionUnion {}>'.format(str(self))
...@@ -5,6 +5,8 @@ import tempfile ...@@ -5,6 +5,8 @@ import tempfile
from contextlib import contextmanager from contextlib import contextmanager
from typing import Union from typing import Union
from poetry.version import Version
_canonicalize_regex = re.compile('[-_]+') _canonicalize_regex = re.compile('[-_]+')
...@@ -16,6 +18,10 @@ def module_name(name): # type: (str) -> str ...@@ -16,6 +18,10 @@ def module_name(name): # type: (str) -> str
return canonicalize_name(name).replace('.', '_').replace('-', '_') return canonicalize_name(name).replace('.', '_').replace('-', '_')
def normalize_version(version): # type: (str) -> str
return str(Version(version))
@contextmanager @contextmanager
def temporary_directory(*args, **kwargs): def temporary_directory(*args, **kwargs):
try: try:
......
...@@ -60,9 +60,15 @@ class Venv(object): ...@@ -60,9 +60,15 @@ class Venv(object):
config = Config.create('config.toml') config = Config.create('config.toml')
create_venv = config.setting('settings.virtualenvs.create') create_venv = config.setting('settings.virtualenvs.create')
root_venv = config.setting('settings.virtualenvs.in-project')
venv_path = config.setting('settings.virtualenvs.path') venv_path = config.setting('settings.virtualenvs.path')
if venv_path is None: if root_venv:
if not cwd:
raise RuntimeError('Unbale to determine the project\'s directory')
venv_path = (cwd / '.venv')
elif venv_path is None:
venv_path = Path(CACHE_DIR) / 'virtualenvs' venv_path = Path(CACHE_DIR) / 'virtualenvs'
else: else:
venv_path = Path(venv_path) venv_path = Path(venv_path)
...@@ -74,7 +80,11 @@ class Venv(object): ...@@ -74,7 +80,11 @@ class Venv(object):
name, '.'.join([str(v) for v in sys.version_info[:2]]) name, '.'.join([str(v) for v in sys.version_info[:2]])
) )
venv = venv_path / name if root_venv:
venv = venv_path
else:
venv = venv_path / name
if not venv.exists(): if not venv.exists():
if create_venv is False: if create_venv is False:
io.writeln( io.writeln(
......
...@@ -9,17 +9,20 @@ from poetry.utils._compat import decode ...@@ -9,17 +9,20 @@ from poetry.utils._compat import decode
class GitConfig: class GitConfig:
def __init__(self): def __init__(self):
config_list = decode(subprocess.check_output(
['git', 'config', '-l'],
stderr=subprocess.STDOUT
))
self._config = {} self._config = {}
m = re.findall('(?ms)^([^=]+)=(.*?)$', config_list) try:
if m: config_list = decode(subprocess.check_output(
for group in m: ['git', 'config', '-l'],
self._config[group[0]] = group[1] stderr=subprocess.STDOUT
))
m = re.findall('(?ms)^([^=]+)=(.*?)$', config_list)
if m:
for group in m:
self._config[group[0]] = group[1]
except subprocess.CalledProcessError:
pass
def get(self, key, default=None): def get(self, key, default=None):
return self._config.get(key, default) return self._config.get(key, default)
......
...@@ -42,30 +42,3 @@ def parse(version, # type: str ...@@ -42,30 +42,3 @@ def parse(version, # type: str
raise raise
return LegacyVersion(version) return LegacyVersion(version)
def version_compare(version1, version2, operator
): # type: (str, str, str) -> bool
from poetry.semver.helpers import normalize_version
if operator in _trans_op:
operator = _trans_op[operator]
elif operator in _trans_op.values():
pass
else:
raise ValueError('Invalid operator')
version1 = parse(version1)
version2 = parse(version2)
try:
version1 = parse(normalize_version(str(version1)))
except ValueError:
pass
try:
version2 = parse(normalize_version(str(version2)))
except ValueError:
pass
return operator(version1, version2)
from poetry.semver.constraints import MultiConstraint from poetry.semver import parse_constraint
from poetry.semver.version_parser import VersionParser from poetry.semver import VersionUnion
PYTHON_VERSION = [ PYTHON_VERSION = [
'2.7.*', '2.7.*',
...@@ -14,24 +13,15 @@ def format_python_constraint(constraint): ...@@ -14,24 +13,15 @@ def format_python_constraint(constraint):
This helper will help in transforming This helper will help in transforming
disjunctive constraint into proper constraint. disjunctive constraint into proper constraint.
""" """
if not isinstance(constraint, MultiConstraint): if not isinstance(constraint, VersionUnion):
return str(constraint) return str(constraint)
has_disjunctive = False
for c in constraint.constraints:
if isinstance(c, MultiConstraint) and c.is_disjunctive():
has_disjunctive = True
break
parser = VersionParser()
formatted = [] formatted = []
accepted = [] accepted = []
if not constraint.is_disjunctive() and not has_disjunctive:
return str(constraint)
for version in PYTHON_VERSION: for version in PYTHON_VERSION:
version_constraint = parser.parse_constraints(version) version_constraint = parse_constraint(version)
matches = constraint.matches(version_constraint) matches = constraint.allows_any(version_constraint)
if not matches: if not matches:
formatted.append('!=' + version) formatted.append('!=' + version)
else: else:
......
...@@ -17,7 +17,7 @@ from pyparsing import ( ...@@ -17,7 +17,7 @@ from pyparsing import (
from pyparsing import ZeroOrMore, Word, Optional, Regex, Combine from pyparsing import ZeroOrMore, Word, Optional, Regex, Combine
from pyparsing import Literal as L # noqa from pyparsing import Literal as L # noqa
from poetry.semver.version_parser import VersionParser from poetry.semver import parse_constraint
from .markers import MARKER_EXPR, Marker from .markers import MARKER_EXPR, Marker
...@@ -221,7 +221,7 @@ class Requirement(object): ...@@ -221,7 +221,7 @@ class Requirement(object):
if not constraint: if not constraint:
constraint = '*' constraint = '*'
self.constraint = VersionParser().parse_constraints(constraint) self.constraint = parse_constraint(constraint)
self.pretty_constraint = constraint self.pretty_constraint = constraint
self.marker = req.marker if req.marker else None self.marker = req.marker if req.marker else None
......
import re
from typing import Union from typing import Union
from poetry.packages import Package from poetry.packages import Package
from poetry.semver.comparison import less_than from poetry.semver import parse_constraint
from poetry.semver.helpers import normalize_version from poetry.semver import Version
from poetry.semver.version_parser import VersionParser
class VersionSelector(object): class VersionSelector(object):
def __init__(self, pool, parser=VersionParser()): def __init__(self, pool):
self._pool = pool self._pool = pool
self._parser = parser
def find_best_candidate(self, def find_best_candidate(self,
package_name, # type: str package_name, # type: str
...@@ -23,11 +20,14 @@ class VersionSelector(object): ...@@ -23,11 +20,14 @@ class VersionSelector(object):
returns the latest Package that matches returns the latest Package that matches
""" """
if target_package_version: if target_package_version:
constraint = self._parser.parse_constraints(target_package_version) constraint = parse_constraint(target_package_version)
else: else:
constraint = None constraint = None
candidates = self._pool.find_packages(package_name, constraint) candidates = self._pool.find_packages(
package_name, constraint,
allow_prereleases=allow_prereleases
)
if not candidates: if not candidates:
return False return False
...@@ -39,7 +39,7 @@ class VersionSelector(object): ...@@ -39,7 +39,7 @@ class VersionSelector(object):
continue continue
# Select highest version of the two # Select highest version of the two
if less_than(package.version, candidate.version): if package.version < candidate.version:
package = candidate package = candidate
return package return package
...@@ -47,26 +47,26 @@ class VersionSelector(object): ...@@ -47,26 +47,26 @@ class VersionSelector(object):
def find_recommended_require_version(self, package): def find_recommended_require_version(self, package):
version = package.version version = package.version
return self._transform_version(version, package.pretty_version) return self._transform_version(version.text, package.pretty_version)
def _transform_version(self, version, pretty_version): def _transform_version(self, version, pretty_version):
# attempt to transform 2.1.1 to 2.1 # attempt to transform 2.1.1 to 2.1
# this allows you to upgrade through minor versions # this allows you to upgrade through minor versions
try: try:
parts = normalize_version(version).split('.') parsed = Version.parse(version)
parts = [parsed.major, parsed.minor, parsed.patch]
except ValueError: except ValueError:
return pretty_version return pretty_version
# check to see if we have a semver-looking version # check to see if we have a semver-looking version
if len(parts) == 4 and re.match('^0\D?', parts[3]): if len(parts) == 3:
# remove the last parts (the patch version number and any extra) # remove the last parts (the patch version number and any extra)
if parts[0] == '0': if parts[0] != 0:
del parts[3]
else:
del parts[3]
del parts[2] del parts[2]
version = '.'.join(parts) version = '.'.join(str(p) for p in parts)
if parsed.is_prerelease():
version += '-{}'.format('.'.join(str(p) for p in parsed.prerelease))
else: else:
return pretty_version return pretty_version
......
[tool.poetry] [tool.poetry]
name = "poetry" name = "poetry"
version = "0.9.1" version = "0.10.0-alpha.4"
description = "Python dependency management and packaging made easy." description = "Python dependency management and packaging made easy."
authors = [ authors = [
"Sébastien Eustace <sebastien@eustace.io>" "Sébastien Eustace <sebastien@eustace.io>"
...@@ -23,24 +23,24 @@ classifiers = [ ...@@ -23,24 +23,24 @@ classifiers = [
# Requirements # Requirements
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "~2.7 || ^3.4" python = "~2.7 || ^3.4"
cleo = "^0.6" cleo = "^0.6.6"
requests = "^2.18" requests = "^2.18"
toml = "^0.9" toml = "^0.9"
cachy = "^0.2" cachy = "^0.2"
pip-tools = "^2.0"
requests-toolbelt = "^0.8.0" requests-toolbelt = "^0.8.0"
jsonschema = "^2.6" jsonschema = "^2.6"
pyrsistent = "^0.14.2" pyrsistent = "^0.14.2"
pyparsing = "^2.2" pyparsing = "^2.2"
cachecontrol = { version = "^0.12.4", extras = ["filecache"] } cachecontrol = { version = "^0.12.4", extras = ["filecache"] }
pkginfo = "^1.4" pkginfo = "^1.4"
html5lib = "^1.0"
# The typing module is not in the stdlib in Python 2.7 and 3.4 # The typing module is not in the stdlib in Python 2.7 and 3.4
typing = { version = "^3.6", python = "~2.7 || ~3.4" } typing = { version = "^3.6", python = "~2.7 || ~3.4" }
# Use pathlib2 and virtualenv for Python 2.7 # Use pathlib2 and virtualenv for Python 2.7
pathlib2 = { version = "^2.3", python = "~2.7" } pathlib2 = { version = "^2.3", python = "~2.7" }
virtualenv = { version = "^15.2", python = "~2.7" } virtualenv = { version = "^16.0", python = "~2.7" }
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pytest = "^3.4" pytest = "^3.4"
...@@ -49,6 +49,7 @@ mkdocs = "^0.17.3" ...@@ -49,6 +49,7 @@ mkdocs = "^0.17.3"
pymdown-extensions = "^4.9" pymdown-extensions = "^4.9"
pygments = "^2.2" pygments = "^2.2"
pytest-mock = "^1.9" pytest-mock = "^1.9"
pygments-github-lexers = "^0.0.5"
[tool.poetry.scripts] [tool.poetry.scripts]
......
import sys
from cleo.testers import CommandTester from cleo.testers import CommandTester
from tests.helpers import get_dependency from tests.helpers import get_dependency
...@@ -21,7 +23,7 @@ def test_add_no_constraint(app, repo, installer): ...@@ -21,7 +23,7 @@ def test_add_no_constraint(app, repo, installer):
Using version ^0.2.0 for cachy Using version ^0.2.0 for cachy
Updating dependencies Updating dependencies
Resolving dependencies Resolving dependencies...
Package operations: 1 install, 0 updates, 0 removals Package operations: 1 install, 0 updates, 0 removals
...@@ -57,7 +59,7 @@ def test_add_constraint(app, repo, installer): ...@@ -57,7 +59,7 @@ def test_add_constraint(app, repo, installer):
expected = """\ expected = """\
Updating dependencies Updating dependencies
Resolving dependencies Resolving dependencies...
Package operations: 1 install, 0 updates, 0 removals Package operations: 1 install, 0 updates, 0 removals
...@@ -94,7 +96,7 @@ def test_add_constraint_dependencies(app, repo, installer): ...@@ -94,7 +96,7 @@ def test_add_constraint_dependencies(app, repo, installer):
expected = """\ expected = """\
Updating dependencies Updating dependencies
Resolving dependencies Resolving dependencies...
Package operations: 2 installs, 0 updates, 0 removals Package operations: 2 installs, 0 updates, 0 removals
...@@ -125,7 +127,7 @@ def test_add_git_constraint(app, repo, installer): ...@@ -125,7 +127,7 @@ def test_add_git_constraint(app, repo, installer):
expected = """\ expected = """\
Updating dependencies Updating dependencies
Resolving dependencies Resolving dependencies...
Package operations: 2 installs, 0 updates, 0 removals Package operations: 2 installs, 0 updates, 0 removals
...@@ -163,7 +165,7 @@ def test_add_git_constraint_with_poetry(app, repo, installer): ...@@ -163,7 +165,7 @@ def test_add_git_constraint_with_poetry(app, repo, installer):
expected = """\ expected = """\
Updating dependencies Updating dependencies
Resolving dependencies Resolving dependencies...
Package operations: 2 installs, 0 updates, 0 removals Package operations: 2 installs, 0 updates, 0 removals
...@@ -194,7 +196,7 @@ def test_add_file_constraint_wheel(app, repo, installer): ...@@ -194,7 +196,7 @@ def test_add_file_constraint_wheel(app, repo, installer):
expected = """\ expected = """\
Updating dependencies Updating dependencies
Resolving dependencies Resolving dependencies...
Package operations: 2 installs, 0 updates, 0 removals Package operations: 2 installs, 0 updates, 0 removals
...@@ -232,7 +234,7 @@ def test_add_file_constraint_sdist(app, repo, installer): ...@@ -232,7 +234,7 @@ def test_add_file_constraint_sdist(app, repo, installer):
expected = """\ expected = """\
Updating dependencies Updating dependencies
Resolving dependencies Resolving dependencies...
Package operations: 2 installs, 0 updates, 0 removals Package operations: 2 installs, 0 updates, 0 removals
...@@ -281,7 +283,7 @@ def test_add_constraint_with_extras(app, repo, installer): ...@@ -281,7 +283,7 @@ def test_add_constraint_with_extras(app, repo, installer):
expected = """\ expected = """\
Updating dependencies Updating dependencies
Resolving dependencies Resolving dependencies...
Package operations: 2 installs, 0 updates, 0 removals Package operations: 2 installs, 0 updates, 0 removals
...@@ -303,3 +305,86 @@ Writing lock file ...@@ -303,3 +305,86 @@ Writing lock file
'version': '0.2.0', 'version': '0.2.0',
'extras': ['msgpack'] 'extras': ['msgpack']
} }
def test_add_constraint_with_python(app, repo, installer):
command = app.find('add')
tester = CommandTester(command)
cachy2 = get_package('cachy', '0.2.0')
repo.add_package(get_package('cachy', '0.1.0'))
repo.add_package(cachy2)
tester.execute([
('command', command.get_name()),
('name', ['cachy=0.2.0']),
('--python', '>=2.7')
])
expected = """\
Updating dependencies
Resolving dependencies...
Package operations: 1 install, 0 updates, 0 removals
Writing lock file
- Installing cachy (0.2.0)
"""
assert tester.get_display() == expected
assert len(installer.installs) == 1
content = app.poetry.file.read(raw=True)['tool']['poetry']
assert 'cachy' in content['dependencies']
assert content['dependencies']['cachy'] == {
'version': '0.2.0',
'python': '>=2.7'
}
def test_add_constraint_with_platform(app, repo, installer):
platform = sys.platform
command = app.find('add')
tester = CommandTester(command)
cachy2 = get_package('cachy', '0.2.0')
repo.add_package(get_package('cachy', '0.1.0'))
repo.add_package(cachy2)
tester.execute([
('command', command.get_name()),
('name', ['cachy=0.2.0']),
('--platform', platform)
])
expected = """\
Updating dependencies
Resolving dependencies...
Package operations: 1 install, 0 updates, 0 removals
Writing lock file
- Installing cachy (0.2.0)
"""
assert tester.get_display() == expected
assert len(installer.installs) == 1
content = app.poetry.file.read(raw=True)['tool']['poetry']
assert 'cachy' in content['dependencies']
assert content['dependencies']['cachy'] == {
'version': '0.2.0',
'platform': platform
}
import pytest
import shutil
import tempfile
from cleo.testers import CommandTester
from poetry.utils._compat import Path
from tests.helpers import get_package
@pytest.fixture
def tmp_dir():
dir_ = tempfile.mkdtemp(prefix='poetry_')
yield dir_
shutil.rmtree(dir_)
def test_basic_interactive(app, mocker):
command = app.find('init')
mocker.patch('poetry.utils._compat.Path.open')
p = mocker.patch('poetry.utils._compat.Path.cwd')
p.return_value = Path(__file__)
tester = CommandTester(command)
tester.set_inputs([
'my-package', # Package name
'1.2.3', # Version
'This is a description', # Description
'n', # Author
'MIT', # License
'~2.7 || ^3.6', # Python
'n', # Interactive packages
'n', # Interactive dev packages
'\n' # Generate
])
tester.execute([('command', command.name)])
output = tester.get_display()
expected = """\
[tool.poetry]
name = "my-package"
version = "1.2.3"
description = "This is a description"
authors = ["Your Name <you@example.com>"]
license = "MIT"
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
[tool.poetry.dev-dependencies]
pytest = "^3.5"
"""
assert expected in output
def test_interactive_with_dependencies(app, repo, mocker):
repo.add_package(get_package('pendulum', '2.0.0'))
command = app.find('init')
mocker.patch('poetry.utils._compat.Path.open')
p = mocker.patch('poetry.utils._compat.Path.cwd')
p.return_value = Path(__file__).parent
tester = CommandTester(command)
tester.set_inputs([
'my-package', # Package name
'1.2.3', # Version
'This is a description', # Description
'n', # Author
'MIT', # License
'~2.7 || ^3.6', # Python
'', # Interactive packages
'pendulum', # Search for package
'0', # First option
'', # Do not set constraint
'', # Stop searching for packages
'n', # Interactive dev packages
'\n' # Generate
])
tester.execute([('command', command.name)])
output = tester.get_display()
expected = """\
[tool.poetry]
name = "my-package"
version = "1.2.3"
description = "This is a description"
authors = ["Your Name <you@example.com>"]
license = "MIT"
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
pendulum = "^2.0"
[tool.poetry.dev-dependencies]
pytest = "^3.5"
"""
print(output)
assert expected in output
from cleo.testers import CommandTester
from tests.helpers import get_package
def test_show_basic_with_installed_packages(app, poetry, installed):
command = app.find('show')
tester = CommandTester(command)
cachy_010 = get_package('cachy', '0.1.0')
cachy_010.description = 'Cachy package'
pendulum_200 = get_package('pendulum', '2.0.0')
pendulum_200.description = 'Pendulum package'
installed.add_package(cachy_010)
installed.add_package(pendulum_200)
poetry.locker.mock_lock_data({
'package': [{
'name': 'cachy',
'version': '0.1.0',
'description': 'Cachy package',
'category': 'main',
'optional': False,
'platform': '*',
'python-versions': '*',
'checksum': []
}, {
'name': 'pendulum',
'version': '2.0.0',
'description': 'Pendulum package',
'category': 'main',
'optional': False,
'platform': '*',
'python-versions': '*',
'checksum': []
}],
'metadata': {
'python-versions': '*',
'platform': '*',
'content-hash': '123456789',
'hashes': {
'cachy': [],
'pendulum': [],
}
}
})
tester.execute([
('command', command.get_name()),
])
expected = """\
cachy 0.1.0 Cachy package
pendulum 2.0.0 Pendulum package
"""
assert tester.get_display() == expected
def test_show_basic_with_not_installed_packages_non_decorated(app, poetry, installed):
command = app.find('show')
tester = CommandTester(command)
cachy_010 = get_package('cachy', '0.1.0')
cachy_010.description = 'Cachy package'
pendulum_200 = get_package('pendulum', '2.0.0')
pendulum_200.description = 'Pendulum package'
installed.add_package(cachy_010)
poetry.locker.mock_lock_data({
'package': [{
'name': 'cachy',
'version': '0.1.0',
'description': 'Cachy package',
'category': 'main',
'optional': False,
'platform': '*',
'python-versions': '*',
'checksum': []
}, {
'name': 'pendulum',
'version': '2.0.0',
'description': 'Pendulum package',
'category': 'main',
'optional': False,
'platform': '*',
'python-versions': '*',
'checksum': []
}],
'metadata': {
'python-versions': '*',
'platform': '*',
'content-hash': '123456789',
'hashes': {
'cachy': [],
'pendulum': [],
}
}
})
tester.execute([
('command', command.get_name()),
])
expected = """\
cachy 0.1.0 Cachy package
pendulum (!) 2.0.0 Pendulum package
"""
assert tester.get_display() == expected
def test_show_basic_with_not_installed_packages_decorated(app, poetry, installed):
command = app.find('show')
tester = CommandTester(command)
cachy_010 = get_package('cachy', '0.1.0')
cachy_010.description = 'Cachy package'
pendulum_200 = get_package('pendulum', '2.0.0')
pendulum_200.description = 'Pendulum package'
installed.add_package(cachy_010)
poetry.locker.mock_lock_data({
'package': [{
'name': 'cachy',
'version': '0.1.0',
'description': 'Cachy package',
'category': 'main',
'optional': False,
'platform': '*',
'python-versions': '*',
'checksum': []
}, {
'name': 'pendulum',
'version': '2.0.0',
'description': 'Pendulum package',
'category': 'main',
'optional': False,
'platform': '*',
'python-versions': '*',
'checksum': []
}],
'metadata': {
'python-versions': '*',
'platform': '*',
'content-hash': '123456789',
'hashes': {
'cachy': [],
'pendulum': [],
}
}
})
tester.execute([
('command', command.get_name()),
], {'decorated': True})
expected = """\
\033[32mcachy \033[0m 0.1.0 Cachy package
\033[31mpendulum\033[0m 2.0.0 Pendulum package
"""
assert tester.get_display() == expected
def test_show_latest_non_decorated(app, poetry, installed, repo):
command = app.find('show')
tester = CommandTester(command)
cachy_010 = get_package('cachy', '0.1.0')
cachy_010.description = 'Cachy package'
cachy_020 = get_package('cachy', '0.2.0')
cachy_020.description = 'Cachy package'
pendulum_200 = get_package('pendulum', '2.0.0')
pendulum_200.description = 'Pendulum package'
pendulum_201 = get_package('pendulum', '2.0.1')
pendulum_201.description = 'Pendulum package'
installed.add_package(cachy_010)
installed.add_package(pendulum_200)
repo.add_package(cachy_010)
repo.add_package(cachy_020)
repo.add_package(pendulum_200)
repo.add_package(pendulum_201)
poetry.locker.mock_lock_data({
'package': [{
'name': 'cachy',
'version': '0.1.0',
'description': 'Cachy package',
'category': 'main',
'optional': False,
'platform': '*',
'python-versions': '*',
'checksum': []
}, {
'name': 'pendulum',
'version': '2.0.0',
'description': 'Pendulum package',
'category': 'main',
'optional': False,
'platform': '*',
'python-versions': '*',
'checksum': []
}],
'metadata': {
'python-versions': '*',
'platform': '*',
'content-hash': '123456789',
'hashes': {
'cachy': [],
'pendulum': [],
}
}
})
tester.execute([
('command', command.get_name()),
('--latest', True),
])
expected = """\
cachy 0.1.0 0.2.0 Cachy package
pendulum 2.0.0 2.0.1 Pendulum package
"""
assert tester.get_display() == expected
def test_show_latest_decorated(app, poetry, installed, repo):
command = app.find('show')
tester = CommandTester(command)
cachy_010 = get_package('cachy', '0.1.0')
cachy_010.description = 'Cachy package'
cachy_020 = get_package('cachy', '0.2.0')
cachy_020.description = 'Cachy package'
pendulum_200 = get_package('pendulum', '2.0.0')
pendulum_200.description = 'Pendulum package'
pendulum_201 = get_package('pendulum', '2.0.1')
pendulum_201.description = 'Pendulum package'
installed.add_package(cachy_010)
installed.add_package(pendulum_200)
repo.add_package(cachy_010)
repo.add_package(cachy_020)
repo.add_package(pendulum_200)
repo.add_package(pendulum_201)
poetry.locker.mock_lock_data({
'package': [{
'name': 'cachy',
'version': '0.1.0',
'description': 'Cachy package',
'category': 'main',
'optional': False,
'platform': '*',
'python-versions': '*',
'checksum': []
}, {
'name': 'pendulum',
'version': '2.0.0',
'description': 'Pendulum package',
'category': 'main',
'optional': False,
'platform': '*',
'python-versions': '*',
'checksum': []
}],
'metadata': {
'python-versions': '*',
'platform': '*',
'content-hash': '123456789',
'hashes': {
'cachy': [],
'pendulum': [],
}
}
})
tester.execute([
('command', command.get_name()),
('--latest', True),
], {
'decorated': True
})
expected = """\
\033[32mcachy \033[0m 0.1.0 \033[33m0.2.0\033[0m Cachy package
\033[32mpendulum\033[0m 2.0.0 \033[31m2.0.1\033[0m Pendulum package
"""
assert tester.get_display() == expected
def test_show_outdated(app, poetry, installed, repo):
command = app.find('show')
tester = CommandTester(command)
cachy_010 = get_package('cachy', '0.1.0')
cachy_010.description = 'Cachy package'
cachy_020 = get_package('cachy', '0.2.0')
cachy_020.description = 'Cachy package'
pendulum_200 = get_package('pendulum', '2.0.0')
pendulum_200.description = 'Pendulum package'
installed.add_package(cachy_010)
installed.add_package(pendulum_200)
repo.add_package(cachy_010)
repo.add_package(cachy_020)
repo.add_package(pendulum_200)
poetry.locker.mock_lock_data({
'package': [{
'name': 'cachy',
'version': '0.1.0',
'description': 'Cachy package',
'category': 'main',
'optional': False,
'platform': '*',
'python-versions': '*',
'checksum': []
}, {
'name': 'pendulum',
'version': '2.0.0',
'description': 'Pendulum package',
'category': 'main',
'optional': False,
'platform': '*',
'python-versions': '*',
'checksum': []
}],
'metadata': {
'python-versions': '*',
'platform': '*',
'content-hash': '123456789',
'hashes': {
'cachy': [],
'pendulum': [],
}
}
})
tester.execute([
('command', command.get_name()),
('--outdated', True),
])
expected = """\
cachy 0.1.0 0.2.0 Cachy package
"""
assert tester.get_display() == expected
def test_show_hides_incompatible_package(app, poetry, installed, repo):
command = app.find('show')
tester = CommandTester(command)
cachy_010 = get_package('cachy', '0.1.0')
cachy_010.description = 'Cachy package'
pendulum_200 = get_package('pendulum', '2.0.0')
pendulum_200.description = 'Pendulum package'
installed.add_package(pendulum_200)
poetry.locker.mock_lock_data({
'package': [{
'name': 'cachy',
'version': '0.1.0',
'description': 'Cachy package',
'category': 'main',
'optional': False,
'platform': '*',
'python-versions': '*',
'checksum': [],
'requirements': {
'python': '1.0'
}
}, {
'name': 'pendulum',
'version': '2.0.0',
'description': 'Pendulum package',
'category': 'main',
'optional': False,
'platform': '*',
'python-versions': '*',
'checksum': []
}],
'metadata': {
'python-versions': '*',
'platform': '*',
'content-hash': '123456789',
'hashes': {
'cachy': [],
'pendulum': [],
}
}
})
tester.execute([
('command', command.get_name()),
])
expected = """\
pendulum 2.0.0 Pendulum package
"""
assert tester.get_display() == expected
def test_show_all_shows_incompatible_package(app, poetry, installed, repo):
command = app.find('show')
tester = CommandTester(command)
cachy_010 = get_package('cachy', '0.1.0')
cachy_010.description = 'Cachy package'
pendulum_200 = get_package('pendulum', '2.0.0')
pendulum_200.description = 'Pendulum package'
installed.add_package(pendulum_200)
poetry.locker.mock_lock_data({
'package': [{
'name': 'cachy',
'version': '0.1.0',
'description': 'Cachy package',
'category': 'main',
'optional': False,
'platform': '*',
'python-versions': '*',
'checksum': [],
'requirements': {
'python': '1.0'
}
}, {
'name': 'pendulum',
'version': '2.0.0',
'description': 'Pendulum package',
'category': 'main',
'optional': False,
'platform': '*',
'python-versions': '*',
'checksum': []
}],
'metadata': {
'python-versions': '*',
'platform': '*',
'content-hash': '123456789',
'hashes': {
'cachy': [],
'pendulum': [],
}
}
})
tester.execute([
('command', command.get_name()),
('--all', True),
])
expected = """\
cachy 0.1.0 Cachy package
pendulum 2.0.0 Pendulum package
"""
assert tester.get_display() == expected
...@@ -20,19 +20,19 @@ def command(): ...@@ -20,19 +20,19 @@ def command():
('1.2.3', 'patch', '1.2.4'), ('1.2.3', 'patch', '1.2.4'),
('1.2.3', 'minor', '1.3.0'), ('1.2.3', 'minor', '1.3.0'),
('1.2.3', 'major', '2.0.0'), ('1.2.3', 'major', '2.0.0'),
('1.2.3', 'prepatch', '1.2.4a0'), ('1.2.3', 'prepatch', '1.2.4-alpha.0'),
('1.2.3', 'preminor', '1.3.0a0'), ('1.2.3', 'preminor', '1.3.0-alpha.0'),
('1.2.3', 'premajor', '2.0.0a0'), ('1.2.3', 'premajor', '2.0.0-alpha.0'),
('1.2.3-beta.1', 'patch', '1.2.3'), ('1.2.3-beta.1', 'patch', '1.2.3'),
('1.2.3-beta.1', 'minor', '1.3.0'), ('1.2.3-beta.1', 'minor', '1.3.0'),
('1.2.3-beta.1', 'major', '2.0.0'), ('1.2.3-beta.1', 'major', '2.0.0'),
('1.2.3-beta.1', 'prerelease', '1.2.3-beta.2'), ('1.2.3-beta.1', 'prerelease', '1.2.3-beta.2'),
('1.2.3-beta1', 'prerelease', '1.2.3-beta2'), ('1.2.3-beta1', 'prerelease', '1.2.3-beta.2'),
('1.2.3beta1', 'prerelease', '1.2.3beta2'), ('1.2.3beta1', 'prerelease', '1.2.3-beta.2'),
('1.2.3b1', 'prerelease', '1.2.3b2'), ('1.2.3b1', 'prerelease', '1.2.3-beta.2'),
('1.2.3', 'prerelease', '1.2.4a0') ('1.2.3', 'prerelease', '1.2.4-alpha.0')
] ]
) )
def test_increment_version(version, rule, expected, command): def test_increment_version(version, rule, expected, command):
assert expected == command.increment_version(version, rule) assert expected == command.increment_version(version, rule).text
import os
import pytest import pytest
import shutil import shutil
...@@ -37,14 +38,22 @@ def mock_clone(self, source, dest): ...@@ -37,14 +38,22 @@ def mock_clone(self, source, dest):
shutil.copytree(str(folder), str(dest)) shutil.copytree(str(folder), str(dest))
@pytest.fixture
def installed():
return Repository()
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def setup(mocker, installer): def setup(mocker, installer, installed):
# Set Installer's installer # Set Installer's installer
p = mocker.patch('poetry.installation.installer.Installer._get_installer') p = mocker.patch('poetry.installation.installer.Installer._get_installer')
p.return_value = installer p.return_value = installer
p = mocker.patch('poetry.installation.installer.Installer._get_installed') p = mocker.patch('poetry.installation.installer.Installer._get_installed')
p.return_value = Repository() p.return_value = installed
p = mocker.patch('poetry.repositories.installed_repository.InstalledRepository.load')
p.return_value = installed
# Patch git module to not actually clone projects # Patch git module to not actually clone projects
mocker.patch('poetry.vcs.git.Git.clone', new=mock_clone) mocker.patch('poetry.vcs.git.Git.clone', new=mock_clone)
...@@ -52,13 +61,14 @@ def setup(mocker, installer): ...@@ -52,13 +61,14 @@ def setup(mocker, installer):
p = mocker.patch('poetry.vcs.git.Git.rev_parse') p = mocker.patch('poetry.vcs.git.Git.rev_parse')
p.return_value = '9cf87a285a2d3fbb0b9fa621997b3acc3631ed24' p.return_value = '9cf87a285a2d3fbb0b9fa621997b3acc3631ed24'
# Patch provider progress rate to have a consistent # Setting terminal width
# dependency resolution output environ = dict(os.environ)
p = mocker.patch( os.environ['COLUMNS'] = '80'
'poetry.puzzle.provider.Provider.progress_rate',
new_callable=mocker.PropertyMock yield
)
p.return_value = 3600 os.environ.clear()
os.environ.update(environ)
class Application(BaseApplication): class Application(BaseApplication):
...@@ -88,6 +98,24 @@ class Locker(BaseLocker): ...@@ -88,6 +98,24 @@ class Locker(BaseLocker):
self._local_config = local_config self._local_config = local_config
self._lock_data = None self._lock_data = None
self._content_hash = self._get_content_hash() self._content_hash = self._get_content_hash()
self._locked = False
self._lock_data = None
def is_locked(self):
return self._locked
def locked(self, is_locked=True):
self._locked = is_locked
return self
def mock_lock_data(self, data):
self.locked()
self._lock_data = data
def is_fresh(self):
return True
def _write_lock_data(self, data): def _write_lock_data(self, data):
self._lock_data = None self._lock_data = None
......
from poetry.packages import Dependency from poetry.packages import Dependency
from poetry.packages import Package from poetry.packages import Package
from poetry.semver.helpers import normalize_version
from poetry.utils._compat import Path from poetry.utils._compat import Path
...@@ -9,7 +8,7 @@ FIXTURE_PATH = Path(__file__).parent / 'fixtures' ...@@ -9,7 +8,7 @@ FIXTURE_PATH = Path(__file__).parent / 'fixtures'
def get_package(name, version): def get_package(name, version):
return Package(name, normalize_version(version), version) return Package(name, version)
def get_dependency(name, def get_dependency(name,
......
[[package]]
name = "A"
version = "1.1"
description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[metadata]
python-versions = "*"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
"A" = []
[[package]]
name = "A"
version = "1.0"
description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[package.dependencies]
"B" = "^1.0"
"C" = "^1.0"
[[package]]
name = "B"
version = "1.0"
description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[[package]]
name = "C"
version = "1.1"
description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[package.requirements]
python = "~2.7"
[[package]]
name = "D"
version = "1.1"
description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[metadata]
python-versions = "*"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
"A" = []
"B" = []
"C" = []
"D" = []
...@@ -18,12 +18,12 @@ platform = "*" ...@@ -18,12 +18,12 @@ platform = "*"
[package.source] [package.source]
type = "directory" type = "directory"
reference = "tests/fixtures/project_with_setup" reference = ""
url = "" url = "tests/fixtures/project_with_setup"
[package.dependencies] [package.dependencies]
cachy = ">= 0.2.0.0" cachy = ">=0.2.0"
pendulum = ">= 1.4.4.0" pendulum = ">=1.4.4"
[[package]] [[package]]
name = "pendulum" name = "pendulum"
......
...@@ -9,11 +9,11 @@ platform = "*" ...@@ -9,11 +9,11 @@ platform = "*"
[package.source] [package.source]
type = "file" type = "file"
reference = "tests/fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl" reference = ""
url = "" url = "tests/fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl"
[package.dependencies] [package.dependencies]
pendulum = ">= 1.4.0.0, < 2.0.0.0" pendulum = ">=1.4.0.0,<2.0.0.0"
[[package]] [[package]]
name = "pendulum" name = "pendulum"
......
...@@ -17,7 +17,7 @@ python-versions = "*" ...@@ -17,7 +17,7 @@ python-versions = "*"
platform = "*" platform = "*"
[package.requirements] [package.requirements]
python = ">= 2.4.0.0, < 2.5.0.0" python = ">=2.4,<2.5"
[[package]] [[package]]
name = "C" name = "C"
...@@ -32,7 +32,7 @@ platform = "*" ...@@ -32,7 +32,7 @@ platform = "*"
D = "^1.2" D = "^1.2"
[package.requirements] [package.requirements]
python = ">= 2.7.0.0, < 2.8.0.0 || >= 3.4.0.0, < 4.0.0.0" python = ">=2.7,<2.8 || >=3.4,<4.0"
[[package]] [[package]]
name = "D" name = "D"
...@@ -44,7 +44,7 @@ python-versions = "*" ...@@ -44,7 +44,7 @@ python-versions = "*"
platform = "*" platform = "*"
[package.requirements] [package.requirements]
python = ">= 2.7.0.0, < 2.8.0.0 || >= 3.4.0.0, < 4.0.0.0" python = ">=2.7,<2.8 || >=3.4,<4.0"
[metadata] [metadata]
python-versions = "~2.7 || ^3.4" python-versions = "~2.7 || ^3.4"
......
...@@ -41,7 +41,7 @@ python-versions = "*" ...@@ -41,7 +41,7 @@ python-versions = "*"
platform = "*" platform = "*"
[package.dependencies] [package.dependencies]
six = "< 2.0.0.0, >= 1.0.0.0" six = ">=1.0.0,<2.0.0"
[[package]] [[package]]
name = "pluggy" name = "pluggy"
...@@ -71,12 +71,12 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" ...@@ -71,12 +71,12 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
platform = "unix" platform = "unix"
[package.dependencies] [package.dependencies]
py = ">= 1.5.0.0" py = ">=1.5.0"
six = ">= 1.10.0.0" six = ">=1.10.0"
setuptools = "*" setuptools = "*"
attrs = ">= 17.4.0.0" attrs = ">=17.4.0"
more-itertools = ">= 4.0.0.0" more-itertools = ">=4.0.0"
pluggy = "< 0.7.0.0, >= 0.5.0.0" pluggy = ">=0.5,<0.7"
funcsigs = "*" funcsigs = "*"
colorama = "*" colorama = "*"
......
...@@ -9,6 +9,7 @@ from poetry.installation import Installer as BaseInstaller ...@@ -9,6 +9,7 @@ from poetry.installation import Installer as BaseInstaller
from poetry.installation.noop_installer import NoopInstaller from poetry.installation.noop_installer import NoopInstaller
from poetry.io import NullIO from poetry.io import NullIO
from poetry.packages import Locker as BaseLocker from poetry.packages import Locker as BaseLocker
from poetry.packages import ProjectPackage
from poetry.repositories import Pool from poetry.repositories import Pool
from poetry.repositories import Repository from poetry.repositories import Repository
from poetry.repositories.installed_repository import InstalledRepository from poetry.repositories.installed_repository import InstalledRepository
...@@ -98,7 +99,7 @@ def setup(): ...@@ -98,7 +99,7 @@ def setup():
@pytest.fixture() @pytest.fixture()
def package(): def package():
return get_package('root', '1.0') return ProjectPackage('root', '1.0')
@pytest.fixture() @pytest.fixture()
...@@ -195,7 +196,7 @@ def test_run_whitelist_add(installer, locker, repo, package): ...@@ -195,7 +196,7 @@ def test_run_whitelist_add(installer, locker, repo, package):
package.add_dependency('B', '^1.0') package.add_dependency('B', '^1.0')
installer.update(True) installer.update(True)
installer.whitelist({'B': '^1.1'}) installer.whitelist(['B'])
installer.run() installer.run()
expected = fixture('with-dependencies') expected = fixture('with-dependencies')
...@@ -241,7 +242,7 @@ def test_run_whitelist_remove(installer, locker, repo, package): ...@@ -241,7 +242,7 @@ def test_run_whitelist_remove(installer, locker, repo, package):
package.add_dependency('A', '~1.0') package.add_dependency('A', '~1.0')
installer.update(True) installer.update(True)
installer.whitelist({'B': '^1.1'}) installer.whitelist(['B'])
installer.run() installer.run()
expected = fixture('remove') expected = fixture('remove')
...@@ -643,9 +644,116 @@ def test_run_changes_category_if_needed(installer, locker, repo, package): ...@@ -643,9 +644,116 @@ def test_run_changes_category_if_needed(installer, locker, repo, package):
package.add_dependency('B', '^1.1') package.add_dependency('B', '^1.1')
installer.update(True) installer.update(True)
installer.whitelist({'B': '^1.1'}) installer.whitelist(['B'])
installer.run() installer.run()
expected = fixture('with-category-change') expected = fixture('with-category-change')
assert locker.written_data == expected assert locker.written_data == expected
def test_run_update_all_with_lock(installer, locker, repo, package):
locker.locked(True)
locker.mock_lock_data({
'package': [{
'name': 'A',
'version': '1.0',
'category': 'dev',
'optional': True,
'platform': '*',
'python-versions': '*',
'checksum': []
}],
'metadata': {
'python-versions': '*',
'platform': '*',
'content-hash': '123456789',
'hashes': {
'A': [],
}
}
})
package_a = get_package('A', '1.1')
repo.add_package(get_package('A', '1.0'))
repo.add_package(package_a)
package.add_dependency('A')
installer.update(True)
installer.run()
expected = fixture('update-with-lock')
assert locker.written_data == expected
def test_run_update_with_locked_extras(installer, locker, repo, package):
locker.locked(True)
locker.mock_lock_data({
'package': [{
'name': 'A',
'version': '1.0',
'category': 'main',
'optional': False,
'platform': '*',
'python-versions': '*',
'checksum': [],
'dependencies': {
'B': '^1.0',
'C': '^1.0',
}
}, {
'name': 'B',
'version': '1.0',
'category': 'dev',
'optional': False,
'platform': '*',
'python-versions': '*',
'checksum': []
}, {
'name': 'C',
'version': '1.1',
'category': 'dev',
'optional': False,
'platform': '*',
'python-versions': '*',
'checksum': [],
'requirements': {
'python': '~2.7'
}
}],
'metadata': {
'python-versions': '*',
'platform': '*',
'content-hash': '123456789',
'hashes': {
'A': [],
'B': [],
'C': [],
}
}
})
package_a = get_package('A', '1.0')
package_a.extras['foo'] = ['B']
b_dependency = get_dependency('B', '^1.0', optional=True)
b_dependency.in_extras.append('foo')
c_dependency = get_dependency('C', '^1.0')
c_dependency.python_versions = '~2.7'
package_a.requires.append(b_dependency)
package_a.requires.append(c_dependency)
repo.add_package(package_a)
repo.add_package(get_package('B', '1.0'))
repo.add_package(get_package('C', '1.1'))
repo.add_package(get_package('D', '1.1'))
package.add_dependency('A', {'version': '^1.0', 'extras': ['foo']})
package.add_dependency('D', '^1.0')
installer.update(True)
installer.whitelist('D')
installer.run()
expected = fixture('update-with-locked-extras')
assert locker.written_data == expected
Copyright (c) 2018 Sébastien Eustace
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
...@@ -114,7 +114,7 @@ License: MIT ...@@ -114,7 +114,7 @@ License: MIT
Keywords: packaging,dependency,poetry Keywords: packaging,dependency,poetry
Author: Sébastien Eustace Author: Sébastien Eustace
Author-email: sebastien@eustace.io Author-email: sebastien@eustace.io
Requires-Python: >= 3.6.0.0, < 4.0.0.0 Requires-Python: >=3.6,<4.0
Classifier: License :: OSI Approved :: MIT License Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.6
...@@ -122,9 +122,9 @@ Classifier: Programming Language :: Python :: 3.7 ...@@ -122,9 +122,9 @@ Classifier: Programming Language :: Python :: 3.7
Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Software Development :: Libraries :: Python Modules
Provides-Extra: time Provides-Extra: time
Requires-Dist: cachy[msgpack] (>=0.2.0.0,<0.3.0.0) Requires-Dist: cachy[msgpack] (>=0.2.0,<0.3.0)
Requires-Dist: cleo (>=0.6.0.0,<0.7.0.0) Requires-Dist: cleo (>=0.6,<0.7)
Requires-Dist: pendulum (>=1.4.0.0,<2.0.0.0); extra == "time" Requires-Dist: pendulum (>=1.4,<2.0); extra == "time"
Description-Content-Type: text/x-rst Description-Content-Type: text/x-rst
My Package My Package
......
...@@ -48,9 +48,9 @@ def test_convert_dependencies(): ...@@ -48,9 +48,9 @@ def test_convert_dependencies():
] ]
) )
main = [ main = [
'A>=1.0.0.0,<2.0.0.0', 'A>=1.0,<2.0',
'B>=1.0.0.0,<1.1.0.0', 'B>=1.0,<1.1',
'C==1.2.3.0', 'C==1.2.3',
] ]
extras = {} extras = {}
...@@ -70,11 +70,11 @@ def test_convert_dependencies(): ...@@ -70,11 +70,11 @@ def test_convert_dependencies():
] ]
) )
main = [ main = [
'B>=1.0.0.0,<1.1.0.0', 'B>=1.0,<1.1',
'C==1.2.3.0', 'C==1.2.3',
] ]
extras = { extras = {
'bar': ['A>=1.2.0.0'] 'bar': ['A>=1.2']
} }
assert result == (main, extras) assert result == (main, extras)
...@@ -98,20 +98,20 @@ def test_convert_dependencies(): ...@@ -98,20 +98,20 @@ def test_convert_dependencies():
] ]
) )
main = [ main = [
'B>=1.0.0.0,<1.1.0.0', 'B>=1.0,<1.1',
] ]
extra_python = ( extra_python = (
':(python_version >= "2.7.0.0" and python_version < "2.8.0.0") ' ':(python_version >= "2.7" and python_version < "2.8") '
'or (python_version >= "3.6.0.0" and python_version < "4.0.0.0")' 'or (python_version >= "3.6" and python_version < "4.0")'
) )
extra_d_dependency = ( extra_d_dependency = (
'baz:(python_version >= "2.7.0.0" and python_version < "2.8.0.0") ' 'baz:(python_version >= "2.7" and python_version < "2.8") '
'or (python_version >= "3.4.0.0" and python_version < "4.0.0.0")' 'or (python_version >= "3.4" and python_version < "4.0")'
) )
extras = { extras = {
extra_python: ['C==1.2.3.0'], extra_python: ['C==1.2.3'],
extra_d_dependency: ['D==3.4.5.0'], extra_d_dependency: ['D==3.4.5'],
} }
assert result == (main, extras) assert result == (main, extras)
...@@ -133,8 +133,8 @@ def test_make_setup(): ...@@ -133,8 +133,8 @@ def test_make_setup():
'my_package.sub_pkg2' 'my_package.sub_pkg2'
] ]
assert ns['install_requires'] == [ assert ns['install_requires'] == [
'cachy[msgpack]>=0.2.0.0,<0.3.0.0', 'cachy[msgpack]>=0.2.0,<0.3.0',
'cleo>=0.6.0.0,<0.7.0.0', 'cleo>=0.6,<0.7',
] ]
assert ns['entry_points'] == { assert ns['entry_points'] == {
'console_scripts': [ 'console_scripts': [
...@@ -144,7 +144,7 @@ def test_make_setup(): ...@@ -144,7 +144,7 @@ def test_make_setup():
} }
assert ns['extras_require'] == { assert ns['extras_require'] == {
'time': [ 'time': [
'pendulum>=1.4.0.0,<2.0.0.0' 'pendulum>=1.4,<2.0'
] ]
} }
...@@ -156,6 +156,7 @@ def test_find_files_to_add(): ...@@ -156,6 +156,7 @@ def test_find_files_to_add():
result = builder.find_files_to_add() result = builder.find_files_to_add()
assert result == [ assert result == [
Path('LICENSE'),
Path('README.rst'), Path('README.rst'),
Path('my_package/__init__.py'), Path('my_package/__init__.py'),
Path('my_package/data1/test.json'), Path('my_package/data1/test.json'),
...@@ -176,6 +177,10 @@ def test_package(): ...@@ -176,6 +177,10 @@ def test_package():
assert sdist.exists() assert sdist.exists()
tar = tarfile.open(str(sdist), 'r')
assert 'my-package-1.2.3/LICENSE' in tar.getnames()
def test_module(): def test_module():
poetry = Poetry.create(project('module1')) poetry = Poetry.create(project('module1'))
......
{
"name": "detects circular dependencies",
"index": "circular",
"requested": {
"circular_app": "*"
},
"base": [],
"resolved": [],
"conflicts": [
"foo",
"bar"
]
}
{
"name": "resolves a simple conflict index",
"index": "conflict",
"requested": {
"my_app": "*"
},
"base": [],
"resolved": [
{
"name": "my_app",
"version": "1.0.0",
"dependencies": [
{
"name": "activemodel",
"version": "3.2.11",
"dependencies": [
{
"name": "builder",
"version": "3.0.4",
"dependencies": []
}
]
},
{
"name": "grape",
"version": "0.2.6",
"dependencies": [
{
"name": "builder",
"version": "3.0.4",
"dependencies": []
}
]
}
]
}
],
"conflicts": []
}
{
"name": "resolves a single dependency",
"index": "django",
"requested": {
"django": "~1.4.0",
"django-debug-toolbar": ""
},
"base": [],
"resolved": [
{
"name": "django",
"version": "1.4.3",
"dependencies": []
}, {
"name": "django-debug-toolbar",
"version": "1.3.2",
"dependencies": [
{
"name": "django",
"version": "1.4.3",
"dependencies": [
]
}
]
}
],
"conflicts": []
}
{
"name": "resolves an empty list of dependencies",
"requested": {
},
"base": [],
"resolved": [
],
"conflicts": []
}
{
"name": "resolves a single dependency",
"requested": {
"rack": "*"
},
"base": [],
"resolved": [
{
"name": "rack",
"version": "1.1",
"dependencies": []
}
],
"conflicts": []
}
{
"name": "resolves a single locked dependency",
"requested": {
"rack": "*"
},
"base": [
{
"name": "rack",
"version": "1.0",
"dependencies": []
}
],
"resolved": [
{
"name": "rack",
"version": "1.0",
"dependencies": []
}
],
"conflicts": []
}
{
"name": "resolves a single dependency with dependencies",
"requested": {
"actionpack": "*"
},
"base": [],
"resolved": [
{
"name": "actionpack",
"version": "2.3.5",
"dependencies": [
{
"name": "activesupport",
"version": "2.3.5",
"dependencies": []
},
{
"name": "rack",
"version": "1.0",
"dependencies": []
}
]
}
],
"conflicts": []
}
{
"name": "resolves dependencies with shared dependencies",
"requested": {
"actionpack": "*",
"activerecord": "2.3.5"
},
"base": [],
"resolved": [
{
"name": "actionpack",
"version": "2.3.5",
"dependencies": [
{
"name": "activesupport",
"version": "2.3.5",
"dependencies": []
},
{
"name": "rack",
"version": "1.0",
"dependencies": []
}
]
},
{
"name": "activerecord",
"version": "2.3.5",
"dependencies": [
{
"name": "activesupport",
"version": "2.3.5",
"dependencies": []
}
]
}
],
"conflicts": []
}
{
"name": "yields conflicts if a child dependency is not resolved",
"index": "unresolvable_child",
"requested": {
"chef_app_error": "*"
},
"base": [],
"resolved": [],
"conflicts": [
"json"
]
}
{
"rack": [
{
"name": "rack",
"version": "0.8",
"dependencies": {
}
},
{
"name": "rack",
"version": "0.9",
"dependencies": {
}
},
{
"name": "rack",
"version": "0.9.1",
"dependencies": {
}
},
{
"name": "rack",
"version": "0.9.2",
"dependencies": {
}
},
{
"name": "rack",
"version": "1.0",
"dependencies": {
}
},
{
"name": "rack",
"version": "1.1",
"dependencies": {
}
}
],
"rack-mount": [
{
"name": "rack-mount",
"version": "0.4",
"dependencies": {
}
},
{
"name": "rack-mount",
"version": "0.5",
"dependencies": {
}
},
{
"name": "rack-mount",
"version": "0.5.1",
"dependencies": {
}
},
{
"name": "rack-mount",
"version": "0.5.2",
"dependencies": {
}
},
{
"name": "rack-mount",
"version": "0.6",
"dependencies": {
}
}
],
"activesupport": [
{
"name": "activesupport",
"version": "1.2.3",
"dependencies": {
}
},
{
"name": "activesupport",
"version": "2.2.3",
"dependencies": {
}
},
{
"name": "activesupport",
"version": "2.3.5",
"dependencies": {
}
},
{
"name": "activesupport",
"version": "3.0.0-beta",
"dependencies": {
}
},
{
"name": "activesupport",
"version": "3.0.0-beta1",
"dependencies": {
}
}
],
"actionpack": [
{
"name": "actionpack",
"version": "1.2.3",
"dependencies": {
"activesupport": "= 1.2.3"
}
},
{
"name": "actionpack",
"version": "2.2.3",
"dependencies": {
"activesupport": "= 2.2.3",
"rack": "~0.9.0"
}
},
{
"name": "actionpack",
"version": "2.3.5",
"dependencies": {
"activesupport": "= 2.3.5",
"rack": "~1.0.0"
}
},
{
"name": "actionpack",
"version": "3.0.0-beta",
"dependencies": {
"activesupport": "= 3.0.0-beta",
"rack": "~1.1",
"rack-mount": ">= 0.5"
}
},
{
"name": "actionpack",
"version": "3.0.0-beta1",
"dependencies": {
"activesupport": "= 3.0.0-beta1",
"rack": "~1.1",
"rack-mount": ">= 0.5"
}
}
],
"activerecord": [
{
"name": "activerecord",
"version": "1.2.3",
"dependencies": {
"activesupport": "= 1.2.3"
}
},
{
"name": "activerecord",
"version": "2.2.3",
"dependencies": {
"activesupport": "= 2.2.3"
}
},
{
"name": "activerecord",
"version": "2.3.5",
"dependencies": {
"activesupport": "= 2.3.5"
}
},
{
"name": "activerecord",
"version": "3.0.0-beta",
"dependencies": {
"activesupport": "= 3.0.0-beta",
"arel": ">= 0.2"
}
},
{
"name": "activerecord",
"version": "3.0.0-beta1",
"dependencies": {
"activesupport": "= 3.0.0-beta1",
"arel": ">= 0.2"
}
}
],
"actionmailer": [
{
"name": "actionmailer",
"version": "1.2.3",
"dependencies": {
"activesupport": "= 1.2.3",
"actionmailer": "= 1.2.3"
}
},
{
"name": "actionmailer",
"version": "2.2.3",
"dependencies": {
"activesupport": "= 2.2.3",
"actionmailer": "= 2.2.3"
}
},
{
"name": "actionmailer",
"version": "2.3.5",
"dependencies": {
"activesupport": "= 2.3.5",
"actionmailer": "= 2.3.5"
}
},
{
"name": "actionmailer",
"version": "3.0.0-beta",
"dependencies": {
"activesupport": "= 3.0.0-beta",
"actionmailer": "= 3.0.0-beta"
}
},
{
"name": "actionmailer",
"version": "3.0.0-beta1",
"dependencies": {
"activesupport": "= 3.0.0-beta1",
"actionmailer": "= 3.0.0-beta1"
}
}
],
"railties": [
{
"name": "railties",
"version": "1.2.3",
"dependencies": {
"activerecord": "= 1.2.3",
"actionpack": "= 1.2.3",
"actionmailer": "= 1.2.3",
"activesupport": "= 1.2.3"
}
},
{
"name": "railties",
"version": "2.2.3",
"dependencies": {
"activerecord": "= 2.2.3",
"actionpack": "= 2.2.3",
"actionmailer": "= 2.2.3",
"activesupport": "= 2.2.3"
}
},
{
"name": "railties",
"version": "2.3.5",
"dependencies": {
"activerecord": "= 2.3.5",
"actionpack": "= 2.3.5",
"actionmailer": "= 2.3.5",
"activesupport": "= 2.3.5"
}
},
{
"name": "railties",
"version": "3.0.0-beta",
"dependencies": {
}
},
{
"name": "railties",
"version": "3.0.0-beta1",
"dependencies": {
}
}
],
"rails": [
{
"name": "rails",
"version": "3.0.0-beta",
"dependencies": {
"activerecord": "= 3.0.0-beta",
"actionpack": "= 3.0.0-beta",
"actionmailer": "= 3.0.0-beta",
"activesupport": "= 3.0.0-beta",
"railties": "= 3.0.0-beta"
}
},
{
"name": "rails",
"version": "3.0.0-beta1",
"dependencies": {
"activerecord": "= 3.0.0-beta1",
"actionpack": "= 3.0.0-beta1",
"actionmailer": "= 3.0.0-beta1",
"activesupport": "= 3.0.0-beta1",
"railties": "= 3.0.0-beta1"
}
}
],
"nokogiri": [
{
"name": "nokogiri",
"version": "1.0",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.2",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.2.1",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.2.2",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.3",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.3.0-1",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.3.5",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.4.0",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.4.2",
"dependencies": {
}
}
],
"weakling": [
{
"name": "weakling",
"version": "0.0.1",
"dependencies": {
}
},
{
"name": "weakling",
"version": "0.0.2",
"dependencies": {
}
},
{
"name": "weakling",
"version": "0.0.3",
"dependencies": {
}
}
],
"activemerchant": [
{
"name": "activemerchant",
"version": "1.2.3",
"dependencies": {
"activesupport": ">= 1.2.3"
}
},
{
"name": "activemerchant",
"version": "2.2.3",
"dependencies": {
"activesupport": ">= 2.2.3"
}
},
{
"name": "activemerchant",
"version": "2.3.5",
"dependencies": {
"activesupport": ">= 2.3.5"
}
}
]
}
{
"rack": [
{
"name": "rack",
"version": "1.0.1",
"dependencies": {
}
}
],
"foo": [
{
"name": "foo",
"version": "0.2.6",
"dependencies": {
"bar": ">= 0"
}
}
],
"bar": [
{
"name": "bar",
"version": "1.0.0",
"dependencies": {
"foo": ">= 0"
}
}
],
"circular-app": [
{
"name": "circular-app",
"version": "1.0.0",
"dependencies": {
"foo": ">= 0",
"bar": ">= 0"
}
}
]
}
{
"builder": [
{
"name": "builder",
"version": "3.0.4",
"dependencies": {
}
},
{
"name": "builder",
"version": "3.1.4",
"dependencies": {
}
}
],
"grape": [
{
"name": "grape",
"version": "0.2.6",
"dependencies": {
"builder": ">=0"
}
}
],
"activemodel": [
{
"name": "activemodel",
"version": "3.2.8",
"dependencies": {
"builder": "~3.0.0"
}
},
{
"name": "activemodel",
"version": "3.2.9",
"dependencies": {
"builder": "~3.0.0"
}
},
{
"name": "activemodel",
"version": "3.2.10",
"dependencies": {
"builder": "~3.0.0"
}
},
{
"name": "activemodel",
"version": "3.2.11",
"dependencies": {
"builder": "~3.0.0"
}
}
],
"my-app": [
{
"name": "my-app",
"version": "1.0.0",
"dependencies": {
"activemodel": ">=0",
"grape": ">=0"
}
}
]
}
{
"django": [
{
"name": "django",
"version": "1.4.1",
"dependencies": {
}
},
{
"name": "django",
"version": "1.4.3",
"dependencies": {
}
}, {
"name": "django",
"version": "2.0.1",
"dependencies": {
}
}
],
"django-debug-toolbar": [
{
"name": "django-debug-toolbar",
"version": "1.3.2",
"dependencies": {
"django": ">=1.4.2"
}
},
{
"name": "django-debug-toolbar",
"version": "1.6.1",
"dependencies": {
"django": ">=1.8"
}
}, {
"name": "django-debug-toolbar",
"version": "1.7.2",
"dependencies": {
"django": ">=1.9"
}
}
]
}
{
"json": [
{
"name": "json",
"version": "1.8.0",
"dependencies": {
}
}
],
"chef": [
{
"name": "chef",
"version": "10.26",
"dependencies": {
"json": "<= 1.7.7, >= 1.4.4"
}
}
],
"berkshelf": [
{
"name": "berkshelf",
"version": "2.0.7",
"dependencies": {
"json": ">= 1.7.7"
}
}
],
"chef-app-error": [
{
"name": "chef-app-error",
"version": "1.0.0",
"dependencies": {
"berkshelf": "~2.0",
"chef": "~10.26"
}
}
]
}
from poetry.packages import Package
from poetry.mixology.failure import SolveFailure
from poetry.mixology.version_solver import VersionSolver
def add_to_repo(repository, name, version, deps=None):
package = Package(name, version)
if deps:
for dep_name, dep_constraint in deps.items():
package.add_dependency(dep_name, dep_constraint)
repository.add_package(package)
def check_solver_result(root, provider,
result=None,
error=None,
tries=None,
locked=None,
use_latest=None):
solver = VersionSolver(root, provider, locked=locked, use_latest=use_latest)
try:
solution = solver.solve()
except SolveFailure as e:
if error:
assert str(e) == error
if tries is not None:
assert solver.solution.attempted_solutions == tries
return
raise
packages = {}
for package in solution.packages:
packages[package.name] = str(package.version)
assert result == packages
if tries is not None:
assert solution.attempted_solutions == tries
import json
import os
from functools import cmp_to_key
from poetry.mixology.contracts import SpecificationProvider
from poetry.packages import Package, Dependency
from poetry.semver import less_than
from poetry.semver.constraints import Constraint
FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'fixtures')
FIXTURE_INDEX_DIR = os.path.join(FIXTURE_DIR, 'index')
class Index(SpecificationProvider):
_specs_from_fixtures = {}
def __init__(self, packages_by_name):
self._packages = packages_by_name
self._search_for = {}
@property
def packages(self):
return self._packages
@classmethod
def from_fixture(cls, fixture_name):
return cls(cls.specs_from_fixtures(fixture_name))
@classmethod
def specs_from_fixtures(cls, fixture_name):
if fixture_name in cls._specs_from_fixtures:
return cls._specs_from_fixtures[fixture_name]
packages_by_name = {}
with open(os.path.join(FIXTURE_INDEX_DIR, fixture_name + '.json')) as fd:
content = json.load(fd)
for name, releases in content.items():
packages_by_name[name] = []
for release in releases:
package = Package(
name,
release['version'],
release['version']
)
for dependency_name, requirements in release['dependencies'].items():
package.requires.append(
Dependency(dependency_name, requirements)
)
packages_by_name[name].append(package)
packages_by_name[name].sort(
key=cmp_to_key(
lambda x, y:
0 if x.version[1] == y.version[1]
else -1 * int(less_than(x[1], y[1]) or -1)
)
)
return packages_by_name
def is_requirement_satisfied_by(self, requirement, activated, package):
if isinstance(requirement, Package):
return requirement == package
if package.is_prerelease() and not requirement.accepts_prereleases():
vertex = activated.vertex_named(package.name)
if not any([r.allows_prereleases() for r in vertex.requirements]):
return False
return requirement.constraint.matches(Constraint('==', package.version))
def search_for(self, dependency):
if dependency in self._search_for:
return self._search_for[dependency]
results = []
for spec in self._packages[dependency.name]:
if not dependency.allows_prereleases() and spec.is_prerelease():
continue
if dependency.constraint.matches(Constraint('==', spec.version)):
results.append(spec)
return results
def name_for(self, dependency):
return dependency.name
def dependencies_for(self, dependency):
return dependency.requires
def sort_dependencies(self,
dependencies,
activated,
conflicts):
return sorted(dependencies, key=lambda d: [
0 if activated.vertex_named(d.name).payload else 1,
0 if d.allows_prereleases() else 1,
0 if d.name in conflicts else 1,
0 if activated.vertex_named(d.name).payload else len(self.search_for(d))
])
import pytest
from poetry.mixology import DependencyGraph
@pytest.fixture()
def graph():
graph = DependencyGraph()
return graph
@pytest.fixture()
def root(graph):
return graph.add_vertex('Root', 'Root', True)
@pytest.fixture()
def root2(graph):
return graph.add_vertex('Root2', 'Root2', True)
@pytest.fixture()
def child(graph):
return graph.add_child_vertex('Child', 'Child', ['Root'], 'Child')
def test_root_vertex_named(graph, root, root2, child):
assert graph.root_vertex_named('Root') is root
def test_vertex_named(graph, root, root2, child):
assert graph.vertex_named('Root') is root
assert graph.vertex_named('Root2') is root2
assert graph.vertex_named('Child') is child
def test_root_vertex_named_non_existent(graph):
assert graph.root_vertex_named('missing') is None
def test_vertex_named_non_existent(graph):
assert graph.vertex_named('missing') is None
def test_detach_vertex_without_successors(graph):
root = graph.add_vertex('root', 'root', True)
graph.detach_vertex_named(root.name)
assert graph.vertex_named(root.name) is None
assert len(graph.vertices) == 0
def test_detach_vertex_with_successors(graph):
root = graph.add_vertex('root', 'root', True)
child = graph.add_child_vertex('child', 'child', ['root'], 'child')
graph.detach_vertex_named(root.name)
assert graph.vertex_named(root.name) is None
assert graph.vertex_named(child.name) is None
assert len(graph.vertices) == 0
def test_detach_vertex_with_successors_with_other_parents(graph):
root = graph.add_vertex('root', 'root', True)
root2 = graph.add_vertex('root2', 'root2', True)
child = graph.add_child_vertex('child', 'child', ['root', 'root2'], 'child')
graph.detach_vertex_named(root.name)
assert graph.vertex_named(root.name) is None
assert graph.vertex_named(child.name) is child
assert child.predecessors == [root2]
assert len(graph.vertices) == 2
def test_detach_vertex_with_predecessors(graph):
parent = graph.add_vertex('parent', 'parent', True)
child = graph.add_child_vertex('child', 'child', ['parent'], 'child')
graph.detach_vertex_named(child.name)
assert graph.vertex_named(child.name) is None
assert graph.vertices == {parent.name: parent}
assert len(parent.outgoing_edges) == 0
import json
import os
import pytest
from poetry.mixology import DependencyGraph
from poetry.mixology import Resolver
from poetry.mixology.exceptions import CircularDependencyError
from poetry.mixology.exceptions import ResolverError
from poetry.mixology.exceptions import VersionConflict
from poetry.packages import Dependency
from .index import Index
from .ui import UI
FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'fixtures')
FIXTURE_CASE_DIR = os.path.join(FIXTURE_DIR, 'case')
@pytest.fixture()
def resolver():
return Resolver(Index.from_fixture('awesome'), UI(True))
class Case:
def __init__(self, fixture):
self._fixture = fixture
self.name = fixture['name']
self._requested = None
self._result = None
self._index = None
self._base = None
self._conflicts = None
@property
def requested(self):
if self._requested is not None:
return self._requested
requested = []
for name, requirement in self._fixture['requested'].items():
requested.append(Dependency(name, requirement))
self._requested = requested
return self._requested
@property
def result(self):
if self._result is not None:
return self._result
graph = DependencyGraph()
for resolved in self._fixture['resolved']:
self.add_dependencies_to_graph(graph, None, resolved)
self._result = graph
return self._result
@property
def index(self):
if self._index is None:
self._index = Index.from_fixture(
self._fixture.get('index', 'awesome')
)
return self._index
@property
def base(self):
if self._base is not None:
return self._base
graph = DependencyGraph()
for r in self._fixture['base']:
self.add_dependencies_to_graph(graph, None, r)
self._base = graph
return self._base
@property
def conflicts(self):
if self._conflicts is None:
self._conflicts = self._fixture['conflicts']
return self._conflicts
def add_dependencies_to_graph(self, graph, parent, data, all_parents=None):
if all_parents is None:
all_parents = set()
name = data['name']
version = data['version']
dependency = [s for s in self.index.packages[name] if s.version == version][0]
if parent:
vertex = graph.add_vertex(name, dependency)
graph.add_edge(parent, vertex, dependency)
else:
vertex = graph.add_vertex(name, dependency, True)
if vertex in all_parents:
return
for dep in data['dependencies']:
self.add_dependencies_to_graph(graph, vertex, dep, all_parents)
def case(name):
with open(os.path.join(FIXTURE_CASE_DIR, name + '.json')) as fd:
return Case(json.load(fd))
def assert_graph(dg, result):
packages = sorted(dg.vertices.values(), key=lambda x: x.name)
expected_packages = sorted(result.vertices.values(), key=lambda x: x.name)
assert packages == expected_packages
@pytest.mark.parametrize(
'fixture',
[
'empty',
'simple',
'simple_with_base',
'simple_with_dependencies',
'simple_with_shared_dependencies',
'django',
]
)
def test_resolver(fixture):
c = case(fixture)
resolver = Resolver(c.index, UI(True))
dg = resolver.resolve(c.requested, base=c.base)
assert_graph(dg, c.result)
@pytest.mark.parametrize(
'fixture',
[
'circular',
'unresolvable_child'
]
)
def test_resolver_fail(fixture):
c = case(fixture)
resolver = Resolver(c.index, UI())
with pytest.raises(ResolverError) as e:
resolver.resolve(c.requested, base=c.base)
names = []
e = e.value
if isinstance(e, CircularDependencyError):
names = [d.name for d in e.dependencies]
elif isinstance(e, VersionConflict):
names = [n for n in e.conflicts.keys()]
assert sorted(names) == sorted(c.conflicts)
import sys
from io import StringIO
from poetry.mixology.contracts import UI as BaseUI
class UI(BaseUI):
def __init__(self, debug=False):
super(UI, self).__init__(debug)
self._output = None
@property
def output(self):
if self._output is None:
if self.debug:
self._output = sys.stderr
else:
self._output = StringIO()
return self._output
import pytest
from poetry.io import NullIO
from poetry.packages.project_package import ProjectPackage
from poetry.repositories import Pool
from poetry.repositories import Repository
from poetry.puzzle.provider import Provider
@pytest.fixture
def repo():
return Repository()
@pytest.fixture
def pool(repo):
pool = Pool()
pool.add_repository(repo)
return pool
@pytest.fixture
def root():
return ProjectPackage('myapp', '0.0.0')
@pytest.fixture
def provider(pool, root):
return Provider(root, pool, NullIO())
from ..helpers import add_to_repo
from ..helpers import check_solver_result
def test_circular_dependency_on_older_version(root, provider, repo):
root.add_dependency('a', '>=1.0.0')
add_to_repo(repo, 'a', '1.0.0')
add_to_repo(repo, 'a', '2.0.0', deps={'b': '1.0.0'})
add_to_repo(repo, 'b', '1.0.0', deps={'a': '1.0.0'})
check_solver_result(root, provider, {
'a': '1.0.0'
}, tries=2)
def test_diamond_dependency_graph(root, provider, repo):
root.add_dependency('a', '*')
root.add_dependency('b', '*')
add_to_repo(repo, 'a', '2.0.0', deps={'c': '^1.0.0'})
add_to_repo(repo, 'a', '1.0.0')
add_to_repo(repo, 'b', '2.0.0', deps={'c': '^3.0.0'})
add_to_repo(repo, 'b', '1.0.0', deps={'c': '^2.0.0'})
add_to_repo(repo, 'c', '3.0.0')
add_to_repo(repo, 'c', '2.0.0')
add_to_repo(repo, 'c', '1.0.0')
check_solver_result(
root, provider,
{
'a': '1.0.0',
'b': '2.0.0',
'c': '3.0.0',
}
)
def test_backjumps_after_partial_satisfier(root, provider, repo):
# c 2.0.0 is incompatible with y 2.0.0 because it requires x 1.0.0, but that
# requirement only exists because of both a and b. The solver should be able
# to deduce c 2.0.0's incompatibility and select c 1.0.0 instead.
root.add_dependency('c', '*')
root.add_dependency('y', '^2.0.0')
add_to_repo(repo, 'a', '1.0.0', deps={'x': '>=1.0.0'})
add_to_repo(repo, 'b', '1.0.0', deps={'x': '<2.0.0'})
add_to_repo(repo, 'c', '1.0.0')
add_to_repo(repo, 'c', '2.0.0', deps={'a': '*', 'b': '*'})
add_to_repo(repo, 'x', '0.0.0')
add_to_repo(repo, 'x', '1.0.0', deps={'y': '1.0.0'})
add_to_repo(repo, 'x', '2.0.0')
add_to_repo(repo, 'y', '1.0.0')
add_to_repo(repo, 'y', '2.0.0')
check_solver_result(
root, provider,
{
'c': '1.0.0',
'y': '2.0.0'
},
tries=2
)
from ..helpers import add_to_repo
from ..helpers import check_solver_result
def test_simple_dependencies(root, provider, repo):
root.add_dependency('a', '1.0.0')
root.add_dependency('b', '1.0.0')
add_to_repo(repo, 'a', '1.0.0', deps={'aa': '1.0.0', 'ab': '1.0.0'})
add_to_repo(repo, 'b', '1.0.0', deps={'ba': '1.0.0', 'bb': '1.0.0'})
add_to_repo(repo, 'aa', '1.0.0')
add_to_repo(repo, 'ab', '1.0.0')
add_to_repo(repo, 'ba', '1.0.0')
add_to_repo(repo, 'bb', '1.0.0')
check_solver_result(root, provider, {
'a': '1.0.0',
'aa': '1.0.0',
'ab': '1.0.0',
'b': '1.0.0',
'ba': '1.0.0',
'bb': '1.0.0'
})
def test_shared_dependencies_with_overlapping_constraints(root, provider, repo):
root.add_dependency('a', '1.0.0')
root.add_dependency('b', '1.0.0')
add_to_repo(repo, 'a', '1.0.0', deps={'shared': '>=2.0.0 <4.0.0'})
add_to_repo(repo, 'b', '1.0.0', deps={'shared': '>=3.0.0 <5.0.0'})
add_to_repo(repo, 'shared', '2.0.0')
add_to_repo(repo, 'shared', '3.0.0')
add_to_repo(repo, 'shared', '3.6.9')
add_to_repo(repo, 'shared', '4.0.0')
add_to_repo(repo, 'shared', '5.0.0')
check_solver_result(root, provider, {
'a': '1.0.0',
'b': '1.0.0',
'shared': '3.6.9',
})
def test_shared_dependency_where_dependent_version_affects_other_dependencies(root, provider, repo):
root.add_dependency('foo', '<=1.0.2')
root.add_dependency('bar', '1.0.0')
add_to_repo(repo, 'foo', '1.0.0')
add_to_repo(repo, 'foo', '1.0.1', deps={'bang': '1.0.0'})
add_to_repo(repo, 'foo', '1.0.2', deps={'whoop': '1.0.0'})
add_to_repo(repo, 'foo', '1.0.3', deps={'zoop': '1.0.0'})
add_to_repo(repo, 'bar', '1.0.0', deps={'foo': '<=1.0.1'})
add_to_repo(repo, 'bang', '1.0.0')
add_to_repo(repo, 'whoop', '1.0.0')
add_to_repo(repo, 'zoop', '1.0.0')
check_solver_result(root, provider, {
'foo': '1.0.1',
'bar': '1.0.0',
'bang': '1.0.0',
})
def test_circular_dependency(root, provider, repo):
root.add_dependency('foo', '1.0.0')
add_to_repo(repo, 'foo', '1.0.0', deps={'bar': '1.0.0'})
add_to_repo(repo, 'bar', '1.0.0', deps={'foo': '1.0.0'})
check_solver_result(root, provider, {
'foo': '1.0.0',
'bar': '1.0.0'
})
from ..helpers import add_to_repo
from ..helpers import check_solver_result
def test_no_version_matching_constraint(root, provider, repo):
root.add_dependency('foo', '^1.0')
add_to_repo(repo, 'foo', '2.0.0')
add_to_repo(repo, 'foo', '2.1.3')
check_solver_result(
root, provider, error=(
"Because myapp depends on foo (^1.0) "
"which doesn't match any versions, version solving failed."
)
)
def test_no_version_that_matches_combined_constraints(root, provider, repo):
root.add_dependency('foo', '1.0.0')
root.add_dependency('bar', '1.0.0')
add_to_repo(repo, 'foo', '1.0.0', deps={'shared': '>=2.0.0 <3.0.0'})
add_to_repo(repo, 'bar', '1.0.0', deps={'shared': '>=2.9.0 <4.0.0'})
add_to_repo(repo, 'shared', '2.5.0')
add_to_repo(repo, 'shared', '3.5.0')
error = """\
Because foo (1.0.0) depends on shared (>=2.0.0 <3.0.0)
and no versions of shared match >=2.9.0,<3.0.0, foo (1.0.0) requires shared (>=2.0.0,<2.9.0).
And because bar (1.0.0) depends on shared (>=2.9.0 <4.0.0), bar (1.0.0) is incompatible with foo (1.0.0).
So, because myapp depends on both foo (1.0.0) and bar (1.0.0), version solving failed."""
check_solver_result(root, provider, error=error)
def test_disjoint_constraints(root, provider, repo):
root.add_dependency('foo', '1.0.0')
root.add_dependency('bar', '1.0.0')
add_to_repo(repo, 'foo', '1.0.0', deps={'shared': '<=2.0.0'})
add_to_repo(repo, 'bar', '1.0.0', deps={'shared': '>3.0.0'})
add_to_repo(repo, 'shared', '2.0.0')
add_to_repo(repo, 'shared', '4.0.0')
error = """\
Because bar (1.0.0) depends on shared (>3.0.0)
and foo (1.0.0) depends on shared (<=2.0.0), bar (1.0.0) is incompatible with foo (1.0.0).
So, because myapp depends on both foo (1.0.0) and bar (1.0.0), version solving failed."""
check_solver_result(root, provider, error=error)
def test_no_valid_solution(root, provider, repo):
root.add_dependency('a')
root.add_dependency('b')
add_to_repo(repo, 'a', '1.0.0', deps={'b': '1.0.0'})
add_to_repo(repo, 'a', '2.0.0', deps={'b': '2.0.0'})
add_to_repo(repo, 'b', '1.0.0', deps={'a': '2.0.0'})
add_to_repo(repo, 'b', '2.0.0', deps={'a': '1.0.0'})
error = """\
Because no versions of b match <1.0.0 || >1.0.0,<2.0.0 || >2.0.0
and b (1.0.0) depends on a (2.0.0), b (<2.0.0 || >2.0.0) requires a (2.0.0).
And because a (2.0.0) depends on b (2.0.0), b is forbidden.
Because b (2.0.0) depends on a (1.0.0) which depends on b (1.0.0), b is forbidden.
Thus, b is forbidden.
So, because myapp depends on b (*), version solving failed."""
check_solver_result(root, provider, error=error, tries=2)
from ...helpers import get_package
from ..helpers import add_to_repo
from ..helpers import check_solver_result
def test_with_compatible_locked_dependencies(root, provider, repo):
root.add_dependency('foo', '*')
add_to_repo(repo, 'foo', '1.0.0', deps={'bar': '1.0.0'})
add_to_repo(repo, 'foo', '1.0.1', deps={'bar': '1.0.1'})
add_to_repo(repo, 'foo', '1.0.2', deps={'bar': '1.0.2'})
add_to_repo(repo, 'bar', '1.0.0')
add_to_repo(repo, 'bar', '1.0.1')
add_to_repo(repo, 'bar', '1.0.2')
check_solver_result(
root, provider,
result={
'foo': '1.0.1',
'bar': '1.0.1',
},
locked={
'foo': get_package('foo', '1.0.1'),
'bar': get_package('bar', '1.0.1'),
}
)
def test_with_incompatible_locked_dependencies(root, provider, repo):
root.add_dependency('foo', '>1.0.1')
add_to_repo(repo, 'foo', '1.0.0', deps={'bar': '1.0.0'})
add_to_repo(repo, 'foo', '1.0.1', deps={'bar': '1.0.1'})
add_to_repo(repo, 'foo', '1.0.2', deps={'bar': '1.0.2'})
add_to_repo(repo, 'bar', '1.0.0')
add_to_repo(repo, 'bar', '1.0.1')
add_to_repo(repo, 'bar', '1.0.2')
check_solver_result(
root, provider,
result={
'foo': '1.0.2',
'bar': '1.0.2',
},
locked={
'foo': get_package('foo', '1.0.1'),
'bar': get_package('bar', '1.0.1'),
}
)
def test_with_unrelated_locked_dependencies(root, provider, repo):
root.add_dependency('foo', '*')
add_to_repo(repo, 'foo', '1.0.0', deps={'bar': '1.0.0'})
add_to_repo(repo, 'foo', '1.0.1', deps={'bar': '1.0.1'})
add_to_repo(repo, 'foo', '1.0.2', deps={'bar': '1.0.2'})
add_to_repo(repo, 'bar', '1.0.0')
add_to_repo(repo, 'bar', '1.0.1')
add_to_repo(repo, 'bar', '1.0.2')
add_to_repo(repo, 'baz', '1.0.0')
check_solver_result(
root, provider,
result={
'foo': '1.0.2',
'bar': '1.0.2',
},
locked={
'baz': get_package('baz', '1.0.1'),
}
)
def test_unlocks_dependencies_if_necessary_to_ensure_that_a_new_dependency_is_statisfied(root, provider, repo):
root.add_dependency('foo')
root.add_dependency('newdep', '2.0.0')
add_to_repo(repo, 'foo', '1.0.0', deps={'bar': '<2.0.0'})
add_to_repo(repo, 'bar', '1.0.0', deps={'baz': '<2.0.0'})
add_to_repo(repo, 'baz', '1.0.0', deps={'qux': '<2.0.0'})
add_to_repo(repo, 'qux', '1.0.0')
add_to_repo(repo, 'foo', '2.0.0', deps={'bar': '<3.0.0'})
add_to_repo(repo, 'bar', '2.0.0', deps={'baz': '<3.0.0'})
add_to_repo(repo, 'baz', '2.0.0', deps={'qux': '<3.0.0'})
add_to_repo(repo, 'qux', '2.0.0')
add_to_repo(repo, 'newdep', '2.0.0', deps={'baz': '>=1.5.0'})
check_solver_result(
root, provider,
result={
'foo': '2.0.0',
'bar': '2.0.0',
'baz': '2.0.0',
'qux': '1.0.0',
'newdep': '2.0.0',
},
locked={
'foo': get_package('foo', '2.0.0'),
'bar': get_package('bar', '1.0.0'),
'baz': get_package('baz', '1.0.0'),
'qux': get_package('qux', '1.0.0'),
}
)
def test_with_compatible_locked_dependencies_use_latest(root, provider, repo):
root.add_dependency('foo', '*')
root.add_dependency('baz', '*')
add_to_repo(repo, 'foo', '1.0.0', deps={'bar': '1.0.0'})
add_to_repo(repo, 'foo', '1.0.1', deps={'bar': '1.0.1'})
add_to_repo(repo, 'foo', '1.0.2', deps={'bar': '1.0.2'})
add_to_repo(repo, 'bar', '1.0.0')
add_to_repo(repo, 'bar', '1.0.1')
add_to_repo(repo, 'bar', '1.0.2')
add_to_repo(repo, 'baz', '1.0.0')
add_to_repo(repo, 'baz', '1.0.1')
check_solver_result(
root, provider,
result={
'foo': '1.0.2',
'bar': '1.0.2',
'baz': '1.0.0',
},
locked={
'foo': get_package('foo', '1.0.1'),
'bar': get_package('bar', '1.0.1'),
'baz': get_package('baz', '1.0.0'),
},
use_latest=['foo']
)
...@@ -59,15 +59,30 @@ def test_to_pep_508(): ...@@ -59,15 +59,30 @@ def test_to_pep_508():
dependency = Dependency('Django', '^1.23') dependency = Dependency('Django', '^1.23')
result = dependency.to_pep_508() result = dependency.to_pep_508()
assert result == 'Django (>=1.23.0.0,<2.0.0.0)' assert result == 'Django (>=1.23,<2.0)'
dependency = Dependency('Django', '^1.23') dependency = Dependency('Django', '^1.23')
dependency.python_versions = '~2.7 || ^3.6' dependency.python_versions = '~2.7 || ^3.6'
result = dependency.to_pep_508() result = dependency.to_pep_508()
assert result == 'Django (>=1.23.0.0,<2.0.0.0); ' \ assert result == 'Django (>=1.23,<2.0); ' \
'(python_version >= "2.7.0.0" and python_version < "2.8.0.0") ' \ '(python_version >= "2.7" and python_version < "2.8") ' \
'or (python_version >= "3.6.0.0" and python_version < "4.0.0.0")' 'or (python_version >= "3.6" and python_version < "4.0")'
def test_to_pep_508_with_platform():
dependency = Dependency('Django', '^1.23')
dependency.python_versions = '~2.7 || ^3.6'
dependency.platform = 'linux || linux2'
result = dependency.to_pep_508()
assert result == (
'Django (>=1.23,<2.0); '
'((python_version >= "2.7" and python_version < "2.8") '
'or (python_version >= "3.6" and python_version < "4.0"))'
' and (sys_platform == "linux" or sys_platform == "linux2")'
)
def test_to_pep_508_wilcard(): def test_to_pep_508_wilcard():
...@@ -82,21 +97,21 @@ def test_to_pep_508_in_extras(): ...@@ -82,21 +97,21 @@ def test_to_pep_508_in_extras():
dependency.in_extras.append('foo') dependency.in_extras.append('foo')
result = dependency.to_pep_508() result = dependency.to_pep_508()
assert result == 'Django (>=1.23.0.0,<2.0.0.0); extra == "foo"' assert result == 'Django (>=1.23,<2.0); extra == "foo"'
dependency.in_extras.append('bar') dependency.in_extras.append('bar')
result = dependency.to_pep_508() result = dependency.to_pep_508()
assert result == 'Django (>=1.23.0.0,<2.0.0.0); extra == "foo" or extra == "bar"' assert result == 'Django (>=1.23,<2.0); extra == "foo" or extra == "bar"'
dependency.python_versions = '~2.7 || ^3.6' dependency.python_versions = '~2.7 || ^3.6'
result = dependency.to_pep_508() result = dependency.to_pep_508()
assert result == ( assert result == (
'Django (>=1.23.0.0,<2.0.0.0); ' 'Django (>=1.23,<2.0); '
'(' '('
'(python_version >= "2.7.0.0" and python_version < "2.8.0.0") ' '(python_version >= "2.7" and python_version < "2.8") '
'or (python_version >= "3.6.0.0" and python_version < "4.0.0.0")' 'or (python_version >= "3.6" and python_version < "4.0")'
') ' ') '
'and (extra == "foo" or extra == "bar")' 'and (extra == "foo" or extra == "bar")'
) )
...@@ -14,7 +14,7 @@ def test_dependency_from_pep_508_with_version(): ...@@ -14,7 +14,7 @@ def test_dependency_from_pep_508_with_version():
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == 'requests' assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0' assert str(dep.constraint) == '2.18.0'
def test_dependency_from_pep_508_with_parens(): def test_dependency_from_pep_508_with_parens():
...@@ -22,7 +22,7 @@ def test_dependency_from_pep_508_with_parens(): ...@@ -22,7 +22,7 @@ def test_dependency_from_pep_508_with_parens():
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == 'requests' assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0' assert str(dep.constraint) == '2.18.0'
def test_dependency_from_pep_508_with_constraint(): def test_dependency_from_pep_508_with_constraint():
...@@ -30,7 +30,7 @@ def test_dependency_from_pep_508_with_constraint(): ...@@ -30,7 +30,7 @@ def test_dependency_from_pep_508_with_constraint():
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == 'requests' assert dep.name == 'requests'
assert str(dep.constraint) == '>= 2.12.0.0, != 2.17.*, < 3.0.0.0' assert str(dep.constraint) == '>=2.12.0,<2.17.0 || >=2.18.0,<3.0'
def test_dependency_from_pep_508_with_extras(): def test_dependency_from_pep_508_with_extras():
...@@ -38,7 +38,7 @@ def test_dependency_from_pep_508_with_extras(): ...@@ -38,7 +38,7 @@ def test_dependency_from_pep_508_with_extras():
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == 'requests' assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0' assert str(dep.constraint) == '2.18.0'
assert dep.extras == ['foo', 'bar'] assert dep.extras == ['foo', 'bar']
...@@ -50,7 +50,7 @@ def test_dependency_from_pep_508_with_python_version(): ...@@ -50,7 +50,7 @@ def test_dependency_from_pep_508_with_python_version():
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == 'requests' assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0' assert str(dep.constraint) == '2.18.0'
assert dep.extras == [] assert dep.extras == []
assert dep.python_versions == '~2.7 || ~2.6' assert dep.python_versions == '~2.7 || ~2.6'
...@@ -63,7 +63,7 @@ def test_dependency_from_pep_508_with_single_python_version(): ...@@ -63,7 +63,7 @@ def test_dependency_from_pep_508_with_single_python_version():
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == 'requests' assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0' assert str(dep.constraint) == '2.18.0'
assert dep.extras == [] assert dep.extras == []
assert dep.python_versions == '~2.7' assert dep.python_versions == '~2.7'
...@@ -76,7 +76,7 @@ def test_dependency_from_pep_508_with_platform(): ...@@ -76,7 +76,7 @@ def test_dependency_from_pep_508_with_platform():
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == 'requests' assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0' assert str(dep.constraint) == '2.18.0'
assert dep.extras == [] assert dep.extras == []
assert dep.python_versions == '*' assert dep.python_versions == '*'
assert dep.platform == 'win32 || darwin' assert dep.platform == 'win32 || darwin'
...@@ -92,7 +92,7 @@ def test_dependency_from_pep_508_complex(): ...@@ -92,7 +92,7 @@ def test_dependency_from_pep_508_complex():
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == 'requests' assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0' assert str(dep.constraint) == '2.18.0'
assert dep.extras == ['foo'] assert dep.extras == ['foo']
assert dep.python_versions == '>=2.7 !=3.2.*' assert dep.python_versions == '>=2.7 !=3.2.*'
assert dep.platform == 'win32 || darwin' assert dep.platform == 'win32 || darwin'
...@@ -106,7 +106,7 @@ def test_dependency_python_version_in(): ...@@ -106,7 +106,7 @@ def test_dependency_python_version_in():
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == 'requests' assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0' assert str(dep.constraint) == '2.18.0'
assert dep.python_versions == '3.3.* || 3.4.* || 3.5.*' assert dep.python_versions == '3.3.* || 3.4.* || 3.5.*'
...@@ -118,7 +118,7 @@ def test_dependency_platform_in(): ...@@ -118,7 +118,7 @@ def test_dependency_platform_in():
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == 'requests' assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0' assert str(dep.constraint) == '2.18.0'
assert dep.platform == 'win32 || darwin' assert dep.platform == 'win32 || darwin'
...@@ -127,7 +127,7 @@ def test_dependency_with_extra(): ...@@ -127,7 +127,7 @@ def test_dependency_with_extra():
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == 'requests' assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0' assert str(dep.constraint) == '2.18.0'
assert len(dep.extras) == 1 assert len(dep.extras) == 1
assert dep.extras[0] == 'security' assert dep.extras[0] == 'security'
...@@ -3,7 +3,7 @@ import pytest ...@@ -3,7 +3,7 @@ import pytest
from cleo.outputs.null_output import NullOutput from cleo.outputs.null_output import NullOutput
from cleo.styles import OutputStyle from cleo.styles import OutputStyle
from poetry.packages import Package from poetry.packages import ProjectPackage
from poetry.repositories.installed_repository import InstalledRepository from poetry.repositories.installed_repository import InstalledRepository
from poetry.repositories.pool import Pool from poetry.repositories.pool import Pool
from poetry.repositories.repository import Repository from poetry.repositories.repository import Repository
...@@ -21,7 +21,7 @@ def io(): ...@@ -21,7 +21,7 @@ def io():
@pytest.fixture() @pytest.fixture()
def package(): def package():
return Package('root', '1.0') return ProjectPackage('root', '1.0')
@pytest.fixture() @pytest.fixture()
...@@ -77,7 +77,9 @@ def check_solver_result(ops, expected): ...@@ -77,7 +77,9 @@ def check_solver_result(ops, expected):
assert result == expected assert result == expected
def test_solver_install_single(solver, repo): def test_solver_install_single(solver, repo, package):
package.add_dependency('A')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
repo.add_package(package_a) repo.add_package(package_a)
...@@ -93,7 +95,7 @@ def test_solver_remove_if_no_longer_locked(solver, locked, installed): ...@@ -93,7 +95,7 @@ def test_solver_remove_if_no_longer_locked(solver, locked, installed):
installed.add_package(package_a) installed.add_package(package_a)
locked.add_package(package_a) locked.add_package(package_a)
ops = solver.solve([]) ops = solver.solve()
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'remove', 'package': package_a} {'job': 'remove', 'package': package_a}
...@@ -115,15 +117,19 @@ def test_remove_non_installed(solver, repo, locked): ...@@ -115,15 +117,19 @@ def test_remove_non_installed(solver, repo, locked):
]) ])
def test_install_non_existing_package_fail(solver, repo): def test_install_non_existing_package_fail(solver, repo, package):
package.add_dependency('B', '1')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
repo.add_package(package_a) repo.add_package(package_a)
with pytest.raises(SolverProblemError): with pytest.raises(SolverProblemError):
solver.solve([get_dependency('B', '1')]) solver.solve()
def test_solver_with_deps(solver, repo): def test_solver_with_deps(solver, repo, package):
package.add_dependency('A')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
new_package_b = get_package('B', '1.1') new_package_b = get_package('B', '1.1')
...@@ -134,7 +140,7 @@ def test_solver_with_deps(solver, repo): ...@@ -134,7 +140,7 @@ def test_solver_with_deps(solver, repo):
package_a.requires.append(get_dependency('B', '<1.1')) package_a.requires.append(get_dependency('B', '<1.1'))
ops = solver.solve([get_dependency('a')]) ops = solver.solve()
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_b}, {'job': 'install', 'package': package_b},
...@@ -142,7 +148,9 @@ def test_solver_with_deps(solver, repo): ...@@ -142,7 +148,9 @@ def test_solver_with_deps(solver, repo):
]) ])
def test_install_honours_not_equal(solver, repo): def test_install_honours_not_equal(solver, repo, package):
package.add_dependency('A')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
new_package_b11 = get_package('B', '1.1') new_package_b11 = get_package('B', '1.1')
...@@ -157,7 +165,7 @@ def test_install_honours_not_equal(solver, repo): ...@@ -157,7 +165,7 @@ def test_install_honours_not_equal(solver, repo):
package_a.requires.append(get_dependency('B', '<=1.3,!=1.3,!=1.2')) package_a.requires.append(get_dependency('B', '<=1.3,!=1.3,!=1.2'))
ops = solver.solve([get_dependency('a')]) ops = solver.solve()
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': new_package_b11}, {'job': 'install', 'package': new_package_b11},
...@@ -165,7 +173,11 @@ def test_install_honours_not_equal(solver, repo): ...@@ -165,7 +173,11 @@ def test_install_honours_not_equal(solver, repo):
]) ])
def test_install_with_deps_in_order(solver, repo): def test_install_with_deps_in_order(solver, repo, package):
package.add_dependency('A')
package.add_dependency('B')
package.add_dependency('C')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_c = get_package('C', '1.0') package_c = get_package('C', '1.0')
...@@ -178,13 +190,7 @@ def test_install_with_deps_in_order(solver, repo): ...@@ -178,13 +190,7 @@ def test_install_with_deps_in_order(solver, repo):
package_c.requires.append(get_dependency('A', '>=1.0')) package_c.requires.append(get_dependency('A', '>=1.0'))
request = [ ops = solver.solve()
get_dependency('A'),
get_dependency('B'),
get_dependency('C'),
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_a}, {'job': 'install', 'package': package_a},
...@@ -193,23 +199,23 @@ def test_install_with_deps_in_order(solver, repo): ...@@ -193,23 +199,23 @@ def test_install_with_deps_in_order(solver, repo):
]) ])
def test_install_installed(solver, repo, installed): def test_install_installed(solver, repo, installed, package):
package.add_dependency('A')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
installed.add_package(package_a) installed.add_package(package_a)
repo.add_package(package_a) repo.add_package(package_a)
request = [ ops = solver.solve()
get_dependency('A'),
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_a, 'skipped': True}, {'job': 'install', 'package': package_a, 'skipped': True},
]) ])
def test_update_installed(solver, repo, installed): def test_update_installed(solver, repo, installed, package):
package.add_dependency('A')
installed.add_package(get_package('A', '1.0')) installed.add_package(get_package('A', '1.0'))
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
...@@ -217,52 +223,53 @@ def test_update_installed(solver, repo, installed): ...@@ -217,52 +223,53 @@ def test_update_installed(solver, repo, installed):
repo.add_package(package_a) repo.add_package(package_a)
repo.add_package(new_package_a) repo.add_package(new_package_a)
request = [ ops = solver.solve()
get_dependency('A'),
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'update', 'from': package_a, 'to': new_package_a} {'job': 'update', 'from': package_a, 'to': new_package_a}
]) ])
def test_update_with_fixed(solver, repo, installed): def test_update_with_use_latest(solver, repo, installed, package, locked):
package.add_dependency('A')
package.add_dependency('B')
installed.add_package(get_package('A', '1.0')) installed.add_package(get_package('A', '1.0'))
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
new_package_a = get_package('A', '1.1') new_package_a = get_package('A', '1.1')
package_b = get_package('B', '1.0')
new_package_b = get_package('B', '1.1')
repo.add_package(package_a) repo.add_package(package_a)
repo.add_package(new_package_a) repo.add_package(new_package_a)
repo.add_package(package_b)
repo.add_package(new_package_b)
request = [ locked.add_package(package_a)
get_dependency('A'), locked.add_package(package_b)
]
ops = solver.solve(request, fixed=[get_dependency('A', '1.0')]) ops = solver.solve(use_latest=[package_b.name])
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_a, 'skipped': True}, {'job': 'install', 'package': package_a, 'skipped': True},
{'job': 'install', 'package': new_package_b},
]) ])
def test_solver_sets_categories(solver, repo): def test_solver_sets_categories(solver, repo, package):
package.add_dependency('A')
package.add_dependency('B', category='dev')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_c = get_package('C', '1.0') package_c = get_package('C', '1.0')
package_b.requires.append(get_dependency('C', '~1.0')) package_b.add_dependency('C', '~1.0')
repo.add_package(package_a) repo.add_package(package_a)
repo.add_package(package_b) repo.add_package(package_b)
repo.add_package(package_c) repo.add_package(package_c)
request = [ ops = solver.solve()
get_dependency('A'),
get_dependency('B', category='dev')
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_c}, {'job': 'install', 'package': package_c},
...@@ -277,6 +284,9 @@ def test_solver_sets_categories(solver, repo): ...@@ -277,6 +284,9 @@ def test_solver_sets_categories(solver, repo):
def test_solver_respects_root_package_python_versions(solver, repo, package): def test_solver_respects_root_package_python_versions(solver, repo, package):
package.python_versions = '^3.4' package.python_versions = '^3.4'
package.add_dependency('A')
package.add_dependency('B')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_b.python_versions = '^3.6' package_b.python_versions = '^3.6'
...@@ -291,12 +301,7 @@ def test_solver_respects_root_package_python_versions(solver, repo, package): ...@@ -291,12 +301,7 @@ def test_solver_respects_root_package_python_versions(solver, repo, package):
repo.add_package(package_c) repo.add_package(package_c)
repo.add_package(package_c11) repo.add_package(package_c11)
request = [ ops = solver.solve()
get_dependency('A'),
get_dependency('B')
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_c}, {'job': 'install', 'package': package_c},
...@@ -307,6 +312,9 @@ def test_solver_respects_root_package_python_versions(solver, repo, package): ...@@ -307,6 +312,9 @@ def test_solver_respects_root_package_python_versions(solver, repo, package):
def test_solver_fails_if_mismatch_root_python_versions(solver, repo, package): def test_solver_fails_if_mismatch_root_python_versions(solver, repo, package):
package.python_versions = '^3.4' package.python_versions = '^3.4'
package.add_dependency('A')
package.add_dependency('B')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_b.python_versions = '^3.6' package_b.python_versions = '^3.6'
...@@ -318,17 +326,15 @@ def test_solver_fails_if_mismatch_root_python_versions(solver, repo, package): ...@@ -318,17 +326,15 @@ def test_solver_fails_if_mismatch_root_python_versions(solver, repo, package):
repo.add_package(package_b) repo.add_package(package_b)
repo.add_package(package_c) repo.add_package(package_c)
request = [
get_dependency('A'),
get_dependency('B')
]
with pytest.raises(SolverProblemError): with pytest.raises(SolverProblemError):
solver.solve(request) solver.solve()
def test_solver_solves_optional_and_compatible_packages(solver, repo, package): def test_solver_solves_optional_and_compatible_packages(solver, repo, package):
package.python_versions = '^3.4' package.python_versions = '^3.4'
package.add_dependency('A', {'version': '*', 'python': '~3.5'})
package.add_dependency('B', {'version': '*', 'optional': True})
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_b.python_versions = '^3.6' package_b.python_versions = '^3.6'
...@@ -340,16 +346,7 @@ def test_solver_solves_optional_and_compatible_packages(solver, repo, package): ...@@ -340,16 +346,7 @@ def test_solver_solves_optional_and_compatible_packages(solver, repo, package):
repo.add_package(package_b) repo.add_package(package_b)
repo.add_package(package_c) repo.add_package(package_c)
dependency_a = get_dependency('A') ops = solver.solve()
dependency_a.python_versions = '~3.5'
dependency_b = get_dependency('B', optional=True)
request = [
dependency_a,
dependency_b
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_c}, {'job': 'install', 'package': package_c},
...@@ -360,6 +357,9 @@ def test_solver_solves_optional_and_compatible_packages(solver, repo, package): ...@@ -360,6 +357,9 @@ def test_solver_solves_optional_and_compatible_packages(solver, repo, package):
def test_solver_solves_while_respecting_root_platforms(solver, repo, package): def test_solver_solves_while_respecting_root_platforms(solver, repo, package):
package.platform = 'darwin' package.platform = 'darwin'
package.add_dependency('A')
package.add_dependency('B')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_b.python_versions = '^3.6' package_b.python_versions = '^3.6'
...@@ -374,12 +374,7 @@ def test_solver_solves_while_respecting_root_platforms(solver, repo, package): ...@@ -374,12 +374,7 @@ def test_solver_solves_while_respecting_root_platforms(solver, repo, package):
repo.add_package(package_c10) repo.add_package(package_c10)
repo.add_package(package_c12) repo.add_package(package_c12)
request = [ ops = solver.solve()
get_dependency('A'),
get_dependency('B')
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_c10}, {'job': 'install', 'package': package_c10},
...@@ -388,7 +383,10 @@ def test_solver_solves_while_respecting_root_platforms(solver, repo, package): ...@@ -388,7 +383,10 @@ def test_solver_solves_while_respecting_root_platforms(solver, repo, package):
]) ])
def test_solver_does_not_return_extras_if_not_requested(solver, repo): def test_solver_does_not_return_extras_if_not_requested(solver, repo, package):
package.add_dependency('A')
package.add_dependency('B')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_c = get_package('C', '1.0') package_c = get_package('C', '1.0')
...@@ -401,14 +399,7 @@ def test_solver_does_not_return_extras_if_not_requested(solver, repo): ...@@ -401,14 +399,7 @@ def test_solver_does_not_return_extras_if_not_requested(solver, repo):
repo.add_package(package_b) repo.add_package(package_b)
repo.add_package(package_c) repo.add_package(package_c)
dependency_a = get_dependency('A') ops = solver.solve()
dependency_b = get_dependency('B')
request = [
dependency_a,
dependency_b
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_a}, {'job': 'install', 'package': package_a},
...@@ -416,7 +407,10 @@ def test_solver_does_not_return_extras_if_not_requested(solver, repo): ...@@ -416,7 +407,10 @@ def test_solver_does_not_return_extras_if_not_requested(solver, repo):
]) ])
def test_solver_returns_extras_if_requested(solver, repo): def test_solver_returns_extras_if_requested(solver, repo, package):
package.add_dependency('A')
package.add_dependency('B', {'version': '*', 'extras': ['foo']})
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_c = get_package('C', '1.0') package_c = get_package('C', '1.0')
...@@ -430,15 +424,7 @@ def test_solver_returns_extras_if_requested(solver, repo): ...@@ -430,15 +424,7 @@ def test_solver_returns_extras_if_requested(solver, repo):
repo.add_package(package_b) repo.add_package(package_b)
repo.add_package(package_c) repo.add_package(package_c)
dependency_a = get_dependency('A') ops = solver.solve()
dependency_b = get_dependency('B')
dependency_b.extras.append('foo')
request = [
dependency_a,
dependency_b
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_c}, {'job': 'install', 'package': package_c},
...@@ -447,7 +433,11 @@ def test_solver_returns_extras_if_requested(solver, repo): ...@@ -447,7 +433,11 @@ def test_solver_returns_extras_if_requested(solver, repo):
]) ])
def test_solver_returns_prereleases_if_requested(solver, repo): def test_solver_returns_prereleases_if_requested(solver, repo, package):
package.add_dependency('A')
package.add_dependency('B')
package.add_dependency('C', {'version': '*', 'allows-prereleases': True})
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_c = get_package('C', '1.0') package_c = get_package('C', '1.0')
...@@ -458,16 +448,7 @@ def test_solver_returns_prereleases_if_requested(solver, repo): ...@@ -458,16 +448,7 @@ def test_solver_returns_prereleases_if_requested(solver, repo):
repo.add_package(package_c) repo.add_package(package_c)
repo.add_package(package_c_dev) repo.add_package(package_c_dev)
dependency_a = get_dependency('A') ops = solver.solve()
dependency_b = get_dependency('B')
dependency_c = get_dependency('C', allows_prereleases=True)
request = [
dependency_a,
dependency_b,
dependency_c
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_a}, {'job': 'install', 'package': package_a},
...@@ -476,7 +457,11 @@ def test_solver_returns_prereleases_if_requested(solver, repo): ...@@ -476,7 +457,11 @@ def test_solver_returns_prereleases_if_requested(solver, repo):
]) ])
def test_solver_does_not_return_prereleases_if_not_requested(solver, repo): def test_solver_does_not_return_prereleases_if_not_requested(solver, repo, package):
package.add_dependency('A')
package.add_dependency('B')
package.add_dependency('C')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_c = get_package('C', '1.0') package_c = get_package('C', '1.0')
...@@ -487,16 +472,7 @@ def test_solver_does_not_return_prereleases_if_not_requested(solver, repo): ...@@ -487,16 +472,7 @@ def test_solver_does_not_return_prereleases_if_not_requested(solver, repo):
repo.add_package(package_c) repo.add_package(package_c)
repo.add_package(package_c_dev) repo.add_package(package_c_dev)
dependency_a = get_dependency('A') ops = solver.solve()
dependency_b = get_dependency('B')
dependency_c = get_dependency('C')
request = [
dependency_a,
dependency_b,
dependency_c
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_a}, {'job': 'install', 'package': package_a},
...@@ -505,7 +481,10 @@ def test_solver_does_not_return_prereleases_if_not_requested(solver, repo): ...@@ -505,7 +481,10 @@ def test_solver_does_not_return_prereleases_if_not_requested(solver, repo):
]) ])
def test_solver_sub_dependencies_with_requirements(solver, repo): def test_solver_sub_dependencies_with_requirements(solver, repo, package):
package.add_dependency('A')
package.add_dependency('B')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_c = get_package('C', '1.0') package_c = get_package('C', '1.0')
...@@ -520,14 +499,7 @@ def test_solver_sub_dependencies_with_requirements(solver, repo): ...@@ -520,14 +499,7 @@ def test_solver_sub_dependencies_with_requirements(solver, repo):
repo.add_package(package_c) repo.add_package(package_c)
repo.add_package(package_d) repo.add_package(package_d)
dependency_a = get_dependency('A') ops = solver.solve()
dependency_b = get_dependency('B')
request = [
dependency_a,
dependency_b,
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_c}, {'job': 'install', 'package': package_c},
...@@ -540,7 +512,11 @@ def test_solver_sub_dependencies_with_requirements(solver, repo): ...@@ -540,7 +512,11 @@ def test_solver_sub_dependencies_with_requirements(solver, repo):
assert op.package.requirements == {} assert op.package.requirements == {}
def test_solver_sub_dependencies_with_requirements_complex(solver, repo): def test_solver_sub_dependencies_with_requirements_complex(solver, repo, package):
package.add_dependency('A')
package.add_dependency('B')
package.add_dependency('C')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_c = get_package('C', '1.0') package_c = get_package('C', '1.0')
...@@ -562,16 +538,7 @@ def test_solver_sub_dependencies_with_requirements_complex(solver, repo): ...@@ -562,16 +538,7 @@ def test_solver_sub_dependencies_with_requirements_complex(solver, repo):
repo.add_package(package_e) repo.add_package(package_e)
repo.add_package(package_f) repo.add_package(package_f)
dependency_a = get_dependency('A') ops = solver.solve()
dependency_b = get_dependency('B')
dependency_c = get_dependency('C')
request = [
dependency_a,
dependency_b,
dependency_c,
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_d}, {'job': 'install', 'package': package_d},
...@@ -591,6 +558,7 @@ def test_solver_sub_dependencies_with_requirements_complex(solver, repo): ...@@ -591,6 +558,7 @@ def test_solver_sub_dependencies_with_requirements_complex(solver, repo):
def test_solver_sub_dependencies_with_not_supported_python_version(solver, repo, package): def test_solver_sub_dependencies_with_not_supported_python_version(solver, repo, package):
package.python_versions = '^3.5' package.python_versions = '^3.5'
package.add_dependency('A')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
...@@ -601,13 +569,99 @@ def test_solver_sub_dependencies_with_not_supported_python_version(solver, repo, ...@@ -601,13 +569,99 @@ def test_solver_sub_dependencies_with_not_supported_python_version(solver, repo,
repo.add_package(package_a) repo.add_package(package_a)
repo.add_package(package_b) repo.add_package(package_b)
dependency_a = get_dependency('A') ops = solver.solve()
request = [
dependency_a,
]
ops = solver.solve(request) check_solver_result(ops, [
{'job': 'install', 'package': package_a},
])
def test_solver_with_dependency_in_both_main_and_dev_dependencies(solver, repo, package):
package.python_versions = '^3.5'
package.add_dependency('A')
package.add_dependency('A', {'version': '*', 'extras': ['foo']}, category='dev')
package_a = get_package('A', '1.0')
package_a.extras['foo'] = [get_dependency('C')]
package_a.add_dependency('C', {'version': '^1.0', 'optional': True})
package_a.add_dependency('B', {'version': '^1.0'})
package_b = get_package('B', '1.0')
package_c = get_package('C', '1.0')
package_c.add_dependency('D', '^1.0')
package_d = get_package('D', '1.0')
repo.add_package(package_a)
repo.add_package(package_b)
repo.add_package(package_c)
repo.add_package(package_d)
ops = solver.solve()
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_b},
{'job': 'install', 'package': package_c},
{'job': 'install', 'package': package_d},
{'job': 'install', 'package': package_a}, {'job': 'install', 'package': package_a},
]) ])
b = ops[0].package
c = ops[1].package
d = ops[2].package
a = ops[3].package
assert d.category == 'dev'
assert c.category == 'dev'
assert b.category == 'main'
assert a.category == 'main'
def test_solver_with_dependency_in_both_main_and_dev_dependencies_with_one_more_dependent(solver, repo, package):
package.add_dependency('A')
package.add_dependency('E')
package.add_dependency('A', {'version': '*', 'extras': ['foo']}, category='dev')
package_a = get_package('A', '1.0')
package_a.extras['foo'] = [get_dependency('C')]
package_a.add_dependency('C', {'version': '^1.0', 'optional': True})
package_a.add_dependency('B', {'version': '^1.0'})
package_b = get_package('B', '1.0')
package_c = get_package('C', '1.0')
package_c.add_dependency('D', '^1.0')
package_d = get_package('D', '1.0')
package_e = get_package('E', '1.0')
package_e.add_dependency('A', '^1.0')
repo.add_package(package_a)
repo.add_package(package_b)
repo.add_package(package_c)
repo.add_package(package_d)
repo.add_package(package_e)
ops = solver.solve()
check_solver_result(ops, [
{'job': 'install', 'package': package_b},
{'job': 'install', 'package': package_c},
{'job': 'install', 'package': package_d},
{'job': 'install', 'package': package_a},
{'job': 'install', 'package': package_e},
])
b = ops[0].package
c = ops[1].package
d = ops[2].package
a = ops[3].package
e = ops[4].package
assert d.category == 'dev'
assert c.category == 'dev'
assert b.category == 'main'
assert a.category == 'main'
assert e.category == 'main'
...@@ -55,12 +55,3 @@ def test_package(): ...@@ -55,12 +55,3 @@ def test_package():
assert win_inet.name == 'win-inet-pton' assert win_inet.name == 'win-inet-pton'
assert win_inet.python_versions == '~2.7 || ~2.6' assert win_inet.python_versions == '~2.7 || ~2.6'
assert win_inet.platform == 'win32' assert win_inet.platform == 'win32'
def test_package_drops_malformed_dependencies():
repo = MockRepository()
package = repo.package('ipython', '4.1.0rc1')
dependency_names = [d.name for d in package.requires]
assert 'setuptools' not in dependency_names
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'),
('0.6c', '0.6.0.0-rc'),
('3.0.17-20140602', '3.0.17.0-post.20140602'),
('3.0pre', '3.0.0.0-rc')
]
)
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 import pytest
from poetry.semver import sort, rsort, statisfies, satisfied_by from poetry.semver import parse_constraint
from poetry.semver import Version
from poetry.semver import VersionRange
@pytest.mark.parametrize( @pytest.mark.parametrize(
'version, constraint', 'input,constraint',
[ [
('1.2.3', '^1.2.3+build'), ('*', VersionRange()),
('1.3.0', '^1.2.3+build'), ('*.*', VersionRange()),
('1.3.0-beta', '>1.2'), ('v*.*', VersionRange()),
('1.2.3-beta', '<=1.2.3'), ('*.x.*', VersionRange()),
('1.0.0', '1.0.0'), ('x.X.x.*', VersionRange()),
('1.2.3', '*'), # ('!=1.0.0', Constraint('!=', '1.0.0.0')),
('v1.2.3', '*'), ('>1.0.0', VersionRange(min=Version(1, 0, 0))),
('1.0.0', '>=1.0.0'), ('<1.2.3', VersionRange(max=Version(1, 2, 3))),
('1.0.1', '>=1.0.0'), ('<=1.2.3', VersionRange(max=Version(1, 2, 3), include_max=True)),
('1.1.0', '>=1.0.0'), ('>=1.2.3', VersionRange(min=Version(1, 2, 3), include_min=True)),
('1.0.1', '>1.0.0'), ('=1.2.3', Version(1, 2, 3)),
('1.1.0', '>1.0.0'), ('1.2.3', Version(1, 2, 3)),
('2.0.0', '<=2.0.0'), ('=1.0', Version(1, 0, 0)),
('1.9999.9999', '<=2.0.0'), ('1.2.3b5', Version(1, 2, 3, 'b5')),
('0.2.9', '<=2.0.0'), ('>= 1.2.3', VersionRange(min=Version(1, 2, 3), include_min=True))
('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.0.7', '~1.0'), # >= 1.0.0 < 1.1.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): def test_parse_constraint(input, constraint):
assert statisfies(version, constraint) assert parse_constraint(input) == constraint
@pytest.mark.parametrize( @pytest.mark.parametrize(
'version, constraint', 'input,constraint',
[ [
('2.0.0', '^1.2.3+build'), ('v2.*', VersionRange(Version(2, 0, 0), Version(3, 0, 0), True)),
('1.2.0', '^1.2.3+build'), ('2.*.*', VersionRange(Version(2, 0, 0), Version(3, 0, 0), True)),
('1.0.0beta', '1'), ('20.*', VersionRange(Version(20, 0, 0), Version(21, 0, 0), True)),
('1.0.1', '1.0.0'), ('20.*.*', VersionRange(Version(20, 0, 0), Version(21, 0, 0), True)),
('0.0.0', '>=1.0.0'), ('2.0.*', VersionRange(Version(2, 0, 0), Version(2, 1, 0), True)),
('0.0.1', '>=1.0.0'), ('2.x', VersionRange(Version(2, 0, 0), Version(3, 0, 0), True)),
('0.1.0', '>=1.0.0'), ('2.x.x', VersionRange(Version(2, 0, 0), Version(3, 0, 0), True)),
('0.0.1', '>1.0.0'), ('2.2.X', VersionRange(Version(2, 2, 0), Version(2, 3, 0), True)),
('0.1.0', '>1.0.0'), ('0.*', VersionRange(max=Version(1, 0, 0))),
('3.0.0', '<=2.0.0'), ('0.*.*', VersionRange(max=Version(1, 0, 0))),
('2.9999.9999', '<=2.0.0'), ('0.x', VersionRange(max=Version(1, 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): def test_parse_constraint_wildcard(input, constraint):
assert not statisfies(version, constraint) assert parse_constraint(input) == constraint
@pytest.mark.parametrize( @pytest.mark.parametrize(
'constraint, versions, expected', 'input,constraint',
[ [
( ('~v1', VersionRange(Version(1, 0, 0), Version(2, 0, 0), True)),
'~1.0', ('~1.0', VersionRange(Version(1, 0, 0), Version(1, 1, 0), True)),
['1.0', '1.0.9', '1.2', '2.0', '2.1', '0.9999.9999'], ('~1.0.0', VersionRange(Version(1, 0, 0), Version(1, 1, 0), True)),
['1.0', '1.0.9'], ('~1.2', VersionRange(Version(1, 2, 0), Version(1, 3, 0), True)),
), ('~1.2.3', VersionRange(Version(1, 2, 3), Version(1, 3, 0), True)),
( ('~1.2-beta', VersionRange(Version(1, 2, 0, 'beta'), Version(1, 3, 0), True)),
'>1.0 <3.0 || >=4.0', ('~1.2-b2', VersionRange(Version(1, 2, 0, 'b2'), Version(1, 3, 0), True)),
['1.0', '1.1', '2.9999.9999', '3.0', '3.1', '3.9999.9999', '4.0', '4.1'], ('~0.3', VersionRange(Version(0, 3, 0), Version(0, 4, 0), True)),
['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): def test_parse_constraint_tilde(input, constraint):
assert satisfied_by(versions, constraint) == expected assert parse_constraint(input) == constraint
@pytest.mark.parametrize( @pytest.mark.parametrize(
'versions, sorted, rsorted', 'input,constraint',
[ [
( ('^v1', VersionRange(Version(1, 0, 0), Version(2, 0, 0), True)),
['1.0', '0.1', '0.1', '3.2.1', '2.4.0-alpha', '2.4.0'], ('^0', VersionRange(Version(0, 0, 0), Version(1, 0, 0), True)),
['0.1', '0.1', '1.0', '2.4.0-alpha', '2.4.0', '3.2.1'], ('^0.0', VersionRange(Version(0, 0, 0), Version(0, 1, 0), True)),
['3.2.1', '2.4.0', '2.4.0-alpha', '1.0', '0.1', '0.1'], ('^1.2', VersionRange(Version(1, 2, 0), Version(2, 0, 0), True)),
) ('^1.2.3-beta.2', VersionRange(Version(1, 2, 3, 'beta.2'), Version(2, 0, 0), True)),
('^1.2.3', VersionRange(Version(1, 2, 3), Version(2, 0, 0), True)),
('^0.2.3', VersionRange(Version(0, 2, 3), Version(0, 3, 0), True)),
('^0.2', VersionRange(Version(0, 2, 0), Version(0, 3, 0), True)),
('^0.2.0', VersionRange(Version(0, 2, 0), Version(0, 3, 0), True)),
('^0.0.3', VersionRange(Version(0, 0, 3), Version(0, 0, 4), True)),
] ]
) )
def test_sort(versions, sorted, rsorted): def test_parse_constraint_caret(input, constraint):
assert sort(versions) == sorted assert parse_constraint(input) == constraint
assert rsort(versions) == rsorted
@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_constraint_multi(input):
assert parse_constraint(input) == VersionRange(
Version(2, 0, 0), Version(3, 0, 0),
include_min=False,
include_max=True
)
@pytest.mark.parametrize(
'input,constraint',
[
('!=v2.*', VersionRange(max=Version.parse('2.0')).union(VersionRange(Version.parse('3.0'), include_min=True))),
('!=2.*.*', VersionRange(max=Version.parse('2.0')).union(VersionRange(Version.parse('3.0'), include_min=True))),
('!=2.0.*', VersionRange(max=Version.parse('2.0')).union(VersionRange(Version.parse('2.1'), include_min=True))),
('!=0.*', VersionRange(Version.parse('1.0'), include_min=True)),
('!=0.*.*', VersionRange(Version.parse('1.0'), include_min=True)),
]
)
def test_parse_constraints_negative_wildcard(input, constraint):
assert parse_constraint(input) == constraint
@pytest.mark.parametrize(
'input, expected',
[
('1', '1'),
('1.2', '1.2'),
('1.2.3', '1.2.3'),
('!=1', '<1 || >1'),
('!=1.2', '<1.2 || >1.2'),
('!=1.2.3', '<1.2.3 || >1.2.3'),
('^1', '>=1,<2'),
('^1.0', '>=1.0,<2.0'),
('^1.0.0', '>=1.0.0,<2.0.0'),
('~1', '>=1,<2'),
('~1.0', '>=1.0,<1.1'),
('~1.0.0', '>=1.0.0,<1.1.0'),
]
)
def test_constraints_keep_version_precision(input, expected):
assert str(parse_constraint(input)) == expected
import pytest
from poetry.semver import EmptyConstraint
from poetry.semver import Version
from poetry.semver import VersionRange
@pytest.mark.parametrize(
'input,version',
[
('1.0.0', Version(1, 0, 0)),
('1', Version(1, 0, 0)),
('1.0', Version(1, 0, 0)),
('1b1', Version(1, 0, 0, 'beta1')),
('1.0b1', Version(1, 0, 0, 'beta1')),
('1.0.0b1', Version(1, 0, 0, 'beta1')),
('1.0.0-b1', Version(1, 0, 0, 'beta1')),
('1.0.0-beta.1', Version(1, 0, 0, 'beta1')),
('1.0.0+1', Version(1, 0, 0, None, '1')),
('1.0.0-1', Version(1, 0, 0, None, '1')),
('1.0.0.0', Version(1, 0, 0)),
('1.0.0-post', Version(1, 0, 0)),
('1.0.0-post1', Version(1, 0, 0, None, '1')),
('0.6c', Version(0, 6, 0, 'rc0')),
('0.6pre', Version(0, 6, 0, 'rc0')),
]
)
def test_parse_valid(input, version):
parsed = Version.parse(input)
print(parsed.build)
assert parsed == version
assert parsed.text == input
def test_comparison():
versions = [
'1.0.0-alpha',
'1.0.0-alpha.1',
'1.0.0-beta.2',
'1.0.0-beta.11',
'1.0.0-rc.1',
'1.0.0-rc.1+build.1',
'1.0.0',
'1.0.0+0.3.7',
'1.3.7+build',
'1.3.7+build.2.b8f12d7',
'1.3.7+build.11.e0f985a',
'2.0.0',
'2.1.0',
'2.2.0',
'2.11.0',
'2.11.1'
]
for i in range(len(versions)):
for j in range(len(versions)):
a = Version.parse(versions[i])
b = Version.parse(versions[j])
assert (a < b) == (i < j)
assert (a > b) == (i > j)
assert (a <= b) == (i <= j)
assert (a >= b) == (i >= j)
assert (a == b) == (i == j)
assert (a != b) == (i != j)
def test_equality():
assert Version.parse('1.2.3') == Version.parse('01.2.3')
assert Version.parse('1.2.3') == Version.parse('1.02.3')
assert Version.parse('1.2.3') == Version.parse('1.2.03')
assert Version.parse('1.2.3-1') == Version.parse('1.2.3-01')
assert Version.parse('1.2.3+1') == Version.parse('1.2.3+01')
def test_allows():
v = Version.parse('1.2.3')
assert v.allows(v)
assert not v.allows(Version.parse('2.2.3'))
assert not v.allows(Version.parse('1.3.3'))
assert not v.allows(Version.parse('1.2.4'))
assert not v.allows(Version.parse('1.2.3-dev'))
assert not v.allows(Version.parse('1.2.3+build'))
def test_allows_all():
v = Version.parse('1.2.3')
assert v.allows_all(v)
assert not v.allows_all(Version.parse('0.0.3'))
assert not v.allows_all(VersionRange(Version.parse('1.1.4'), Version.parse('1.2.4')))
assert not v.allows_all(VersionRange())
assert v.allows_all(EmptyConstraint())
def test_allows_any():
v = Version.parse('1.2.3')
assert v.allows_any(v)
assert not v.allows_any(Version.parse('0.0.3'))
assert v.allows_any(VersionRange(Version.parse('1.1.4'), Version.parse('1.2.4')))
assert v.allows_any(VersionRange())
assert not v.allows_any(EmptyConstraint())
def test_intersect():
v = Version.parse('1.2.3')
assert v.intersect(v) == v
assert v.intersect(Version.parse('1.1.4')).is_empty()
assert v.intersect(VersionRange(Version.parse('1.1.4'), Version.parse('1.2.4'))) == v
assert Version.parse('1.1.4').intersect(VersionRange(v, Version.parse('1.2.4'))).is_empty()
def test_union():
v = Version.parse('1.2.3')
assert v.union(v) == v
result = v.union(Version.parse('0.8.0'))
assert result.allows(v)
assert result.allows(Version.parse('0.8.0'))
assert not result.allows(Version.parse('1.1.4'))
range = VersionRange(Version.parse('1.1.4'), Version.parse('1.2.4'))
assert v.union(range) == range
union = Version.parse('1.1.4').union(VersionRange(Version.parse('1.1.4'), Version.parse('1.2.4')))
assert union == VersionRange(Version.parse('1.1.4'), Version.parse('1.2.4'), include_min=True)
result = v.union(VersionRange(Version.parse('0.0.3'), Version.parse('1.1.4')))
assert result.allows(v)
assert result.allows(Version.parse('0.1.0'))
def test_difference():
v = Version.parse('1.2.3')
assert v.difference(v).is_empty()
assert v.difference(Version.parse('0.8.0')) == v
assert v.difference(VersionRange(Version.parse('1.1.4'), Version.parse('1.2.4'))).is_empty()
assert v.difference(VersionRange(Version.parse('1.4.0'), Version.parse('3.0.0'))) == v
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
constraint = parser.parse_constraints(input)
assert str(constraint.constraint) == str(expected)
@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')),
('!=2.0.*', Constraint('<', '2.0.0.0'), Constraint('>=', '2.1.0.0')),
('!=0.*', None, Constraint('>=', '1.0.0.0')),
('!=0.*.*', None, Constraint('>=', '1.0.0.0')),
]
)
def test_parse_constraints_negative_wildcard(parser, input, min, max):
if min:
expected = MultiConstraint((min, max), conjunctive=False)
else:
expected = max
constraint = parser.parse_constraints(input)
assert str(constraint.constraint) == 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('<', '1.1.0.0')),
('~1.0.0', Constraint('>=', '1.0.0.0'), Constraint('<', '1.1.0.0')),
('~1.2', Constraint('>=', '1.2.0.0'), Constraint('<', '1.3.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('<', '1.3.0.0')),
('~1.2-b2', Constraint('>=', '1.2.0.0-beta.2'), Constraint('<', '1.3.0.0')),
('~0.3', Constraint('>=', '0.3.0.0'), Constraint('<', '0.4.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)
import pytest
from poetry.semver import EmptyConstraint
from poetry.semver import Version
from poetry.semver import VersionRange
@pytest.fixture()
def v003():
return Version.parse('0.0.3')
@pytest.fixture()
def v010():
return Version.parse('0.1.0')
@pytest.fixture()
def v080():
return Version.parse('0.8.0')
@pytest.fixture()
def v072():
return Version.parse('0.7.2')
@pytest.fixture()
def v114():
return Version.parse('1.1.4')
@pytest.fixture()
def v123():
return Version.parse('1.2.3')
@pytest.fixture()
def v124():
return Version.parse('1.2.4')
@pytest.fixture()
def v130():
return Version.parse('1.3.0')
@pytest.fixture()
def v140():
return Version.parse('1.4.0')
@pytest.fixture()
def v200():
return Version.parse('2.0.0')
@pytest.fixture()
def v234():
return Version.parse('2.3.4')
@pytest.fixture()
def v250():
return Version.parse('2.5.0')
@pytest.fixture()
def v300():
return Version.parse('3.0.0')
def test_allows_all(v003, v010, v080, v114, v123, v124, v140, v200, v234, v250, v300):
assert VersionRange(v123, v250).allows_all(EmptyConstraint())
range = VersionRange(v123, v250, include_max=True)
assert not range.allows_all(v123)
assert range.allows_all(v124)
assert range.allows_all(v250)
assert not range.allows_all(v300)
# with no min
range = VersionRange(max=v250)
assert range.allows_all(VersionRange(v080, v140))
assert not range.allows_all(VersionRange(v080, v300))
assert range.allows_all(VersionRange(max=v140))
assert not range.allows_all(VersionRange(max=v300))
assert range.allows_all(range)
assert not range.allows_all(VersionRange())
# with no max
range = VersionRange(min=v010)
assert range.allows_all(VersionRange(v080, v140))
assert not range.allows_all(VersionRange(v003, v140))
assert range.allows_all(VersionRange(v080))
assert not range.allows_all(VersionRange(v003))
assert range.allows_all(range)
assert not range.allows_all(VersionRange())
# Allows bordering range that is not more inclusive
exclusive = VersionRange(v010, v250)
inclusive = VersionRange(v010, v250, True, True)
assert inclusive.allows_all(exclusive)
assert inclusive.allows_all(inclusive)
assert not exclusive.allows_all(inclusive)
assert exclusive.allows_all(exclusive)
# Allows unions that are completely contained
range = VersionRange(v114, v200)
assert range.allows_all(VersionRange(v123, v124).union(v140))
assert not range.allows_all(VersionRange(v010, v124).union(v140))
assert not range.allows_all(VersionRange(v123, v234).union(v140))
def test_allows_any(v003, v010, v072, v080, v114, v123, v124, v140, v200, v234, v250, v300):
# disallows an empty constraint
assert not VersionRange(v123, v250).allows_any(EmptyConstraint())
# allows allowed versions
range = VersionRange(v123, v250, include_max=True)
assert not range.allows_any(v123)
assert range.allows_any(v124)
assert range.allows_any(v250)
assert not range.allows_any(v300)
# with no min
range = VersionRange(max=v200)
assert range.allows_any(VersionRange(v140, v300))
assert not range.allows_any(VersionRange(v234, v300))
assert range.allows_any(VersionRange(v140))
assert not range.allows_any(VersionRange(v234))
assert range.allows_any(range)
# with no max
range = VersionRange(min=v072)
assert range.allows_any(VersionRange(v003, v140))
assert not range.allows_any(VersionRange(v003, v010))
assert range.allows_any(VersionRange(max=v080))
assert not range.allows_any(VersionRange(max=v003))
assert range.allows_any(range)
# with min and max
range = VersionRange(v072, v200)
assert range.allows_any(VersionRange(v003, v140))
assert range.allows_any(VersionRange(v140, v300))
assert not range.allows_any(VersionRange(v003, v010))
assert not range.allows_any(VersionRange(v234, v300))
assert not range.allows_any(VersionRange(max=v010))
assert not range.allows_any(VersionRange(v234))
assert range.allows_any(range)
# allows a bordering range when both are inclusive
assert not VersionRange(max=v250).allows_any(VersionRange(min=v250))
assert not VersionRange(max=v250, include_max=True).allows_any(VersionRange(min=v250))
assert not VersionRange(max=v250).allows_any(VersionRange(min=v250, include_min=True))
assert not VersionRange(min=v250).allows_any(VersionRange(max=v250))
assert VersionRange(max=v250, include_max=True).allows_any(VersionRange(min=v250, include_min=True))
# allows unions that are partially contained'
range = VersionRange(v114, v200)
assert range.allows_any(VersionRange(v010, v080).union(v140))
assert range.allows_any(VersionRange(v123, v234).union(v300))
assert not range.allows_any(VersionRange(v234, v300).union(v010))
def test_intersect(v114, v123, v124, v200, v250, v300):
# two overlapping ranges
assert VersionRange(v123, v250).intersect(VersionRange(v200, v300)) == VersionRange(v200, v250)
# a non-overlapping range allows no versions
a = VersionRange(v114, v124)
b = VersionRange(v200, v250)
assert a.intersect(b).is_empty()
# adjacent ranges allow no versions if exclusive
a = VersionRange(v114, v124)
b = VersionRange(v124, v200)
assert a.intersect(b).is_empty()
# adjacent ranges allow version if inclusive
a = VersionRange(v114, v124, include_max=True)
b = VersionRange(v124, v200, include_min=True)
assert a.intersect(b) == v124
# with an open range
open = VersionRange()
a = VersionRange(v114, v124)
assert open.intersect(open) == open
assert open.intersect(a) == a
# returns the version if the range allows it
assert VersionRange(v114, v124).intersect(v123) == v123
assert VersionRange(v123, v124).intersect(v114).is_empty()
def test_union(v003, v010, v072, v080, v114, v123, v124, v130, v140, v200, v234, v250, v300):
# with a version returns the range if it contains the version
range = VersionRange(v114, v124)
assert range.union(v123) == range
# with a version on the edge of the range, expands the range
range = VersionRange(v114, v124)
assert range.union(v124) == VersionRange(v114, v124, include_max=True)
assert range.union(v114) == VersionRange(v114, v124, include_min=True)
# with a version allows both the range and the version if the range
# doesn't contain the version
result = VersionRange(v003, v114).union(v124)
assert result.allows(v010)
assert not result.allows(v123)
assert result.allows(v124)
# returns a VersionUnion for a disjoint range
result = VersionRange(v003, v114).union(VersionRange(v130, v200))
assert result.allows(v080)
assert not result.allows(v123)
assert result.allows(v140)
# considers open ranges disjoint
result = VersionRange(v003, v114).union(VersionRange(v114, v200))
assert result.allows(v080)
assert not result.allows(v114)
assert result.allows(v140)
result = VersionRange(v114, v200).union(VersionRange(v003, v114))
assert result.allows(v080)
assert not result.allows(v114)
assert result.allows(v140)
# returns a merged range for an overlapping range
result = VersionRange(v003, v114).union(VersionRange(v080, v200))
assert result == VersionRange(v003, v200)
# considers closed ranges overlapping
result = VersionRange(v003, v114, include_max=True).union(VersionRange(v114, v200))
assert result == VersionRange(v003, v200)
result = VersionRange(v003, v114).union(VersionRange(v114, v200, include_min=True))
assert result == VersionRange(v003, v200)
...@@ -17,7 +17,7 @@ def test_poetry(): ...@@ -17,7 +17,7 @@ def test_poetry():
package = poetry.package package = poetry.package
assert package.name == 'my-package' assert package.name == 'my-package'
assert package.version == '1.2.3' assert package.version.text == '1.2.3'
assert package.description == 'Some description.' assert package.description == 'Some description.'
assert package.authors == ['Sébastien Eustace <sebastien@eustace.io>'] assert package.authors == ['Sébastien Eustace <sebastien@eustace.io>']
assert package.license.id == 'MIT' assert package.license.id == 'MIT'
...@@ -27,7 +27,7 @@ def test_poetry(): ...@@ -27,7 +27,7 @@ def test_poetry():
assert package.keywords == ["packaging", "dependency", "poetry"] assert package.keywords == ["packaging", "dependency", "poetry"]
assert package.python_versions == '~2.7 || ^3.6' assert package.python_versions == '~2.7 || ^3.6'
assert str(package.python_constraint) == '>= 2.7.0.0, < 2.8.0.0 || >= 3.6.0.0, < 4.0.0.0' assert str(package.python_constraint) == '>=2.7,<2.8 || >=3.6,<4.0'
dependencies = {} dependencies = {}
for dep in package.requires: for dep in package.requires:
......
from poetry.version.helpers import format_python_constraint from poetry.version.helpers import format_python_constraint
from poetry.semver.version_parser import VersionParser from poetry.semver import parse_constraint
def test_format_python_constraint(): def test_format_python_constraint():
parser = VersionParser() constraint = parse_constraint('~2.7 || ^3.6')
constraint = parser.parse_constraints('~2.7 || ^3.6')
result = format_python_constraint(constraint) result = format_python_constraint(constraint)
......
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