Commit f7d1931a by Sébastien Eustace

Merge branch 'develop'

parents 0667c67a 075d1b2e
# 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
### Fixed
......
......@@ -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.
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/pyproject.bash-completion
poetry completions bash > /etc/bash_completion.d/poetry.bash-completion
# 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
poetry completions fish > ~/.config/fish/completions/pyproject.fish
poetry completions fish > ~/.config/fish/completions/poetry.fish
# Zsh
poetry completions zsh > ~/.zfunc/_poetry
......@@ -209,6 +209,32 @@ results in :
- 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
When you specify a package to the `install` command it will add it as a wildcard
......@@ -264,6 +290,24 @@ the `--name` option:
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
......
......@@ -65,7 +65,7 @@ It will automatically find a suitable version constraint.
### Version constraints
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.
......@@ -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
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
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
for the version constraint you have specified.
......@@ -143,7 +143,7 @@ and update the lock file with the new versions.
!!!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.
......@@ -159,5 +159,5 @@ or create a brand new one for you to always work isolated from your global Pytho
`poetry` has been installed.
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.
......@@ -54,6 +54,26 @@ the `--name` option:
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
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.
## 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.
......@@ -214,6 +234,8 @@ It will automatically register the package before uploading if this is the first
poetry publish
```
It can also build the package if you pass it the `--build` option.
### Options
* `--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.
poetry run python -V
```
Note that this command has no option.
## script
The `script` executes one of the scripts defined in `pyproject.toml`.
It can also executes one of the scripts defined in `pyproject.toml`.
So, if you have a script defined like this:
......@@ -285,7 +303,7 @@ my-script = "my_module:main"
You can execute it like so:
```bash
poetry script my-script
poetry run my-script
```
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.
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`.
```bash
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/" }
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
You can also specify that a dependency should be installed only for specific Python versions:
......
......@@ -15,6 +15,7 @@ pages:
- Repositories: repositories.md
- Versions: versions.md
- The pyproject.toml file: pyproject.md
- FAQ: faq.md
markdown_extensions:
- codehilite
......
......@@ -27,6 +27,7 @@ import tempfile
from contextlib import contextmanager
from email.parser import Parser
from functools import cmp_to_key
from glob import glob
try:
......@@ -88,7 +89,7 @@ def style(fg, bg, options):
STYLES = {
'info': style('green', 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):
return '{}{}\033[0m'.format(STYLES[style], text)
@contextmanager
def temporary_directory(*args, **kwargs):
try:
......@@ -145,13 +147,24 @@ class Installer:
metadata = json.loads(r.read().decode())
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('')
releases = sorted(
metadata['releases'].keys(),
key=lambda r: (
'.'.join(self.VERSION_REGEX.match(r).groups()[:3]),
self.VERSION_REGEX.match(r).group(5)
)
key=cmp_to_key(_compare_versions)
)
if self._version and self._version not in releases:
......@@ -188,7 +201,7 @@ class Installer:
return self.install(version)
except subprocess.CalledProcessError as e:
print(colorize('error', 'An error has occured: {}'.format(str(e))))
print(e.output)
print(e.output.decode())
return e.returncode
......@@ -197,10 +210,30 @@ class Installer:
with temporary_directory(prefix='poetry-installer-') as dir:
dist = os.path.join(dir, 'dist')
print(' - Getting dependencies')
self.call(
self.CURRENT_PYTHON, '-m', 'pip', 'install', 'poetry=={}'.format(version),
'--target', dist
)
try:
self.call(
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')
......@@ -210,7 +243,10 @@ class Installer:
# Everything, except poetry itself, should
# be put in the _vendor directory
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
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
from .commands import BuildCommand
from .commands import CheckCommand
from .commands import ConfigCommand
from .commands import DevelopCommand
from .commands import InitCommand
from .commands import InstallCommand
from .commands import LockCommand
from .commands import NewCommand
......@@ -106,6 +108,8 @@ class Application(BaseApplication):
BuildCommand(),
CheckCommand(),
ConfigCommand(),
DevelopCommand(),
InitCommand(),
InstallCommand(),
LockCommand(),
NewCommand(),
......
......@@ -3,6 +3,8 @@ from .add import AddCommand
from .build import BuildCommand
from .check import CheckCommand
from .config import ConfigCommand
from .develop import DevelopCommand
from .init import InitCommand
from .install import InstallCommand
from .lock import LockCommand
from .new import NewCommand
......
import re
from typing import List
from typing import Tuple
from .init import InitCommand
from .venv_command import VenvCommand
class AddCommand(VenvCommand):
class AddCommand(VenvCommand, InitCommand):
"""
Add a new dependency to <comment>pyproject.toml</>.
......@@ -17,6 +13,8 @@ class AddCommand(VenvCommand):
{ --path= : The path to a dependency. }
{ --E|extras=* : Extras to activate for the 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. }
{ --dry-run : Outputs the operations but will not execute anything
(implicitly enables --verbose). }
......@@ -33,7 +31,7 @@ If you do not specify a version constraint, poetry will choose a suitable one ba
def handle(self):
from poetry.installation import Installer
from poetry.semver.version_parser import VersionParser
from poetry.semver import parse_constraint
packages = self.argument('name')
is_dev = self.option('dev')
......@@ -76,9 +74,8 @@ If you do not specify a version constraint, poetry will choose a suitable one ba
requirements = self._format_requirements(requirements)
# validate requirements format
parser = VersionParser()
for constraint in requirements.values():
parser.parse_constraints(constraint)
parse_constraint(constraint)
for name, constraint in requirements.items():
constraint = {
......@@ -101,8 +98,21 @@ If you do not specify a version constraint, poetry will choose a suitable one ba
constraint['allows-prereleases'] = True
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')
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:
constraint = constraint['version']
......@@ -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)
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):
unique_config_values = {
'settings.virtualenvs.create': (boolean_validator, boolean_normalizer),
'settings.virtualenvs.in-project': (boolean_validator, boolean_normalizer),
'settings.pypi.fallback': (boolean_validator, boolean_normalizer),
}
......
......@@ -11,6 +11,8 @@ class DebugResolveCommand(Command):
debug:resolve
{ package?* : packages to resolve. }
{ --E|extras=* : Extras to activate for the dependency. }
{ --python= : Python version(s) to use for resolution. }
"""
_loggers = [
......@@ -19,39 +21,56 @@ class DebugResolveCommand(Command):
def handle(self):
from poetry.packages import Dependency
from poetry.packages import ProjectPackage
from poetry.puzzle import Solver
from poetry.repositories.repository import Repository
from poetry.semver.version_parser import VersionParser
from poetry.semver import parse_constraint
packages = self.argument('package')
if not packages:
package = self.poetry.package
dependencies = package.requires + package.dev_requires
else:
requirements = self._determine_requirements(packages)
requirements = self._format_requirements(requirements)
# validate requirements format
parser = VersionParser()
for constraint in requirements.values():
parser.parse_constraints(constraint)
parse_constraint(constraint)
dependencies = []
for name, constraint in requirements.items():
dependencies.append(
Dependency(name, constraint)
)
dep = 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(
self.poetry.package,
package,
self.poetry.pool,
Repository(),
Repository(),
self.output
)
ops = solver.solve(dependencies)
ops = solver.solve()
self.line('')
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')
......@@ -10,6 +10,7 @@ class InstallCommand(VenvCommand):
{ --dry-run : Outputs the operations but will not execute anything
(implicitly enables --verbose). }
{ --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
......@@ -35,8 +36,16 @@ exist it will look for <comment>pyproject.toml</> and do the same.
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.develop(self.option('develop'))
installer.dry_run(self.option('dry-run'))
installer.verbose(self.option('verbose'))
......
from poetry.utils._compat import Path
from .command import Command
......@@ -10,12 +8,18 @@ class NewCommand(Command):
new
{ path : The path to create the project at. }
{ --name : Set the resulting package name. }
{ --src : Use the src layout for the project. }
"""
def handle(self):
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'))
name = self.option('name')
......@@ -34,7 +38,15 @@ class NewCommand(Command):
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)
self.line(
......
......@@ -9,7 +9,7 @@ class PublishCommand(Command):
{ --r|repository= : The repository to publish the package to. }
{ --u|username= : The username 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.
......@@ -24,13 +24,32 @@ the config command.
def handle(self):
from poetry.masonry.publishing.publisher import Publisher
# Building package first, unless told otherwise
if not self.option('no-build'):
publisher = Publisher(self.poetry, self.output)
# 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')
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('')
publisher = Publisher(self.poetry, self.output)
publisher.publish(
self.option('repository'),
self.option('username'),
......
......@@ -11,11 +11,47 @@ class RunCommand(VenvCommand):
def handle(self):
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
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):
if self._application is None \
or (self._application_definition_merged
......
import sys
from ...masonry.utils.module import Module
from .venv_command import 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-name : The name of the script to execute }
......@@ -14,6 +11,9 @@ class ScriptCommand(VenvCommand):
"""
def handle(self):
self.line('<warning>script is deprecated use run instead.</warning>')
self.line('')
script = self.argument('script-name')
argv = [script] + self.argument('args')
......@@ -44,6 +44,8 @@ class ScriptCommand(VenvCommand):
@property
def _module(self):
from ...masonry.utils.module import Module
poetry = self.poetry
package = poetry.package
path = poetry.file.parent
......
......@@ -22,7 +22,6 @@ class SelfUpdateCommand(Command):
def handle(self):
from poetry.__version__ import __version__
from poetry.repositories.pypi_repository import PyPiRepository
from poetry.semver.comparison import less_than
version = self.argument('version')
if not version:
......@@ -38,7 +37,7 @@ class SelfUpdateCommand(Command):
key=cmp_to_key(
lambda x, y:
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 -*-
import sys
from .venv_command import VenvCommand
......@@ -12,6 +14,7 @@ class ShowCommand(VenvCommand):
{ --l|latest : Show the latest version. }
{ --o|outdated : Show the latest version
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
......@@ -26,6 +29,11 @@ lists all packages available."""
]
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')
if self.option('tree'):
......@@ -34,23 +42,23 @@ lists all packages available."""
if self.option('outdated'):
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
if self.option('tree') and not package:
requires = self.poetry.package.requires + self.poetry.package.dev_requires
packages = installed_repo.packages
packages = locked_repo.packages
for package in packages:
for require in requires:
if package.name == require.name:
self.display_package_tree(package, installed_repo)
self.display_package_tree(package, locked_repo)
break
return 0
table = self.table(style='compact')
table.get_style().set_vertical_border_char('')
locked_packages = installed_repo.packages
locked_packages = locked_repo.packages
if package:
pkg = None
......@@ -63,7 +71,7 @@ lists all packages available."""
raise ValueError('Package {} not found'.format(package))
if self.option('tree'):
self.display_package_tree(pkg, installed_repo)
self.display_package_tree(pkg, locked_repo)
return 0
......@@ -90,13 +98,38 @@ lists all packages available."""
return 0
show_latest = self.option('latest')
show_all = self.option('all')
terminal = self.get_application().terminal
width = terminal.width
name_length = version_length = latest_length = 0
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
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))
if show_latest:
latest = self.find_latest_package(locked)
......@@ -111,7 +144,24 @@ lists all packages available."""
write_description = name_length + version_length + latest_length + 24 <= width
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:
line += ' {:{}}'.format(
locked.full_pretty_version, version_length
......@@ -253,16 +303,23 @@ lists all packages available."""
)
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:
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
return 'semver-safe-update'
# it needs an upgrade but has potential BC breaks so is not urgent
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.
def handle(self):
version = self.argument('version')
if version in self.RESERVED:
version = self.increment_version(
self.poetry.package.pretty_version, version
)
version = self.increment_version(
self.poetry.package.pretty_version, version
)
self.line(
'Bumping version from <comment>{}</> to <info>{}</>'.format(
......@@ -41,93 +40,47 @@ patch, minor, major, prepatch, preminor, premajor, prerelease.
content = self.poetry.file.read()
poetry_content = content['tool']['poetry']
poetry_content['version'] = version
poetry_content['version'] = version.text
self.poetry.file.write(content)
def increment_version(self, version, rule):
from poetry.semver.version_parser import VersionParser
from poetry.semver import Version
parser = VersionParser()
version_regex = (
'v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?{}(?:\+[^\s]+)?'
).format(parser._modifier_regex)
m = re.match(version_regex, version)
if not m:
try:
version = Version.parse(version)
except ValueError:
raise ValueError(
'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 m.group(1) != '0' or m.group(2) != '0' or not is_prerelease:
position = 0
new = version.next_major
if rule == 'premajor':
new = new.first_prerelease
elif rule in {'minor', 'preminor'}:
if m.group(2) != '0' or not is_prerelease:
position = 1
new = version.next_minor
if rule == 'preminor':
new = new.first_prerelease
elif rule in {'patch', 'prepatch'}:
if not is_prerelease:
position = 2
elif rule == 'prerelease' and not is_prerelease:
position = 2
if position != -1:
extra_matches[0] = None
base = parser._manipulate_version_string(
matches,
position,
increment=increment
)
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 = ''
new = version.next_patch
if rule == 'prepatch':
new = new.first_prerelease
elif rule == 'prerelease':
if version.is_prerelease():
pre = version.prerelease
new_prerelease = int(pre[1]) + 1
new = Version.parse(
'{}.{}.{}-{}'.format(
version.major,
version.minor,
version.patch,
'.'.join([pre[0], str(new_prerelease)])
)
)
else:
if extras.startswith(('.', '_', '-')):
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]
)
new = version.next_patch.first_prerelease
else:
extras = ''
new = rule
return '.'.join(base.split('.')[:max(index, position)+1]) + extras
return new
......@@ -8,8 +8,9 @@ class PoetryStyle(CleoStyle):
super(PoetryStyle, self).__init__(i, o)
self.output.get_formatter().add_style('error', 'red')
self.output.get_formatter().add_style('warning', 'black', 'yellow')
self.output.get_formatter().add_style('question', 'blue')
self.output.get_formatter().add_style('warning', 'yellow')
self.output.get_formatter().add_style('question', 'cyan')
self.output.get_formatter().add_style('comment', 'blue')
def writeln(self, messages,
type=OutputStyle.OUTPUT_NORMAL,
......
......@@ -15,8 +15,9 @@ from poetry.puzzle.operations.operation import Operation
from poetry.repositories import Pool
from poetry.repositories import Repository
from poetry.repositories.installed_repository import InstalledRepository
from poetry.semver.constraints import Constraint
from poetry.semver.version_parser import VersionParser
from poetry.semver import parse_constraint
from poetry.semver import Version
from poetry.utils.helpers import canonicalize_name
from .base_installer import BaseInstaller
from .pip_installer import PipInstaller
......@@ -43,6 +44,7 @@ class Installer:
self._verbose = False
self._write_lock = True
self._dev_mode = True
self._develop = []
self._execute_operations = True
self._whitelist = {}
......@@ -98,6 +100,11 @@ class Installer:
def is_dev_mode(self): # type: () -> bool
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
self._update = update
......@@ -112,7 +119,7 @@ class Installer:
return self
def whitelist(self, packages): # type: (dict) -> Installer
self._whitelist = packages
self._whitelist = [canonicalize_name(p) for p in packages]
return self
......@@ -124,7 +131,14 @@ class Installer:
def _do_install(self, local_repo):
locked_repository = Repository()
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)
# Checking extras
......@@ -135,33 +149,6 @@ class Installer:
)
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(
self._package,
self._pool,
......@@ -170,10 +157,7 @@ class Installer:
self._io
)
request = self._package.requires
request += self._package.dev_requires
ops = solver.solve(request, fixed=fixed)
ops = solver.solve(use_latest=self._whitelist)
else:
self._io.writeln('<info>Installing dependencies from lock file</>')
......@@ -426,13 +410,16 @@ class Installer:
installed, locked
))
if not is_installed:
# If it's optional and not in required extras
# we do not install
if locked.optional and locked.name not in extra_packages:
continue
# If it's optional and not in required extras
# we do not install
if locked.optional and locked.name not in extra_packages:
continue
op = Install(locked)
if is_installed:
op.skip('Already installed')
ops.append(Install(locked))
ops.append(op)
return ops
......@@ -451,18 +438,22 @@ class Installer:
if op.job_type == 'uninstall':
continue
parser = VersionParser()
python = '.'.join([str(i) for i in self._venv.version_info[:3]])
if package.name in self._develop and package.source_type == 'directory':
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:
python_constraint = parser.parse_constraints(
python_constraint = parse_constraint(
package.requirements['python']
)
if not python_constraint.matches(Constraint('=', python)):
if not python_constraint.allows(python):
# Incompatible python versions
op.skip('Not needed for the current python version')
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')
continue
......
......@@ -39,7 +39,11 @@ class PipInstaller(BaseInstaller):
finally:
os.unlink(req)
else:
args.append(self.requirement(package))
req = self.requirement(package)
if not isinstance(req, list):
args.append(req)
else:
args += req
self.run(*args)
......@@ -69,7 +73,15 @@ class PipInstaller(BaseInstaller):
return req
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':
return 'git+{}@{}#egg={}'.format(
......
......@@ -182,6 +182,10 @@
"type": "string",
"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": {
"type": "boolean",
"description": "Whether the dependency allows prereleases or not."
......@@ -225,6 +229,10 @@
"type": "string",
"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": {
"type": "boolean",
"description": "Whether the dependency allows prereleases or not."
......@@ -255,6 +263,10 @@
"type": "string",
"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": {
"type": "boolean",
"description": "Whether the dependency is optional or not."
......@@ -281,6 +293,10 @@
"type": "string",
"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": {
"type": "boolean",
"description": "Whether the dependency is optional or not."
......@@ -291,6 +307,10 @@
"items": {
"type": "string"
}
},
"develop": {
"type": "boolean",
"description": "Whether to install the dependency in development mode."
}
}
},
......
from typing import Type
from .layout import Layout
from .src import SrcLayout
from .standard import StandardLayout
_LAYOUTS = {
'standard': StandardLayout
'src': SrcLayout,
'standard': StandardLayout,
}
......
from poetry.toml import dumps
from poetry.toml import loads
from poetry.utils.helpers import module_name
from poetry.vcs.git import Git
TESTS_DEFAULT = u"""from {package_name} import __version__
......@@ -20,45 +19,52 @@ description = ""
authors = []
[tool.poetry.dependencies]
python = "*"
[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):
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._package_name = module_name(project)
self._version = version
self._description = description
self._readme_format = readme_format
self._dependencies = {}
self._dev_dependencies = {}
self._include = []
self._license = license
self._python = python
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 (
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>'
author = 'Your Name <you@example.com>'
self._author = author
def create(self, path, with_tests=True):
self._dependencies = {}
self._dev_dependencies = {}
self._include = []
path.mkdir(parents=True, exist_ok=True)
self._create_default(path)
......@@ -69,6 +75,30 @@ class Layout(object):
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):
raise NotImplementedError()
......@@ -99,13 +129,9 @@ class Layout(object):
)
def _write_poetry(self, path):
content = loads(POETRY_DEFAULT)
poetry_content = content['tool']['poetry']
poetry_content['name'] = self._project
poetry_content['version'] = self._version
poetry_content['authors'].append(self._author)
content = self.generate_poetry_content()
poetry = path / 'pyproject.toml'
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
from collections import defaultdict
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.vcs import get_vcs
......@@ -106,6 +103,16 @@ class Builder(object):
)
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
# to avoid errors
if 'readme' in self._poetry.local_config:
......@@ -156,35 +163,6 @@ class Builder(object):
'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
@contextmanager
def temporary_directory(cls, *args, **kwargs):
......
......@@ -64,7 +64,7 @@ class SdistBuilder(Builder):
target_dir.mkdir(parents=True)
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')
tar = tarfile.TarFile(target.as_posix(), mode='w', fileobj=gz,
......@@ -72,7 +72,7 @@ class SdistBuilder(Builder):
try:
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)
......
......@@ -13,8 +13,7 @@ from base64 import urlsafe_b64encode
from io import StringIO
from poetry.__version__ import __version__
from poetry.semver.constraints import Constraint
from poetry.semver.constraints import MultiConstraint
from poetry.semver import parse_constraint
from poetry.utils._compat import Path
from ..utils.helpers import normalize_file_permissions
......@@ -181,23 +180,18 @@ class WheelBuilder(Builder):
@property
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
def wheel_filename(self): # type: () -> str
return '{}-{}-{}.whl'.format(
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
)
def supports_python2(self):
return self._package.python_constraint.matches(
MultiConstraint([
Constraint('>=', '2.0.0'),
Constraint('<', '3.0.0')
])
)
return self._package.python_constraint.allows_any(parse_constraint('>=2.0.0 <3.0.0'))
def dist_info_name(self, distribution, version): # type: (...) -> str
escaped_name = re.sub("[^\w\d.]+", "_", distribution, flags=re.UNICODE)
......
from poetry.utils.helpers import canonicalize_name
from poetry.utils.helpers import normalize_version
from poetry.version.helpers import format_python_constraint
......@@ -42,7 +43,7 @@ class Metadata:
meta = cls()
meta.name = canonicalize_name(package.name)
meta.version = package.version
meta.version = normalize_version(package.version.text)
meta.summary = package.description
if package.readme:
with package.readme.open() as f:
......
......@@ -17,6 +17,10 @@ class Publisher:
self._io = io
self._uploader = Uploader(poetry, io)
@property
def files(self):
return self._uploader.files
def publish(self, repository_name, username, password):
if repository_name:
self._io.writeln(
......
......@@ -2,6 +2,8 @@ import hashlib
import io
import re
from typing import List
import requests
from requests import adapters
......@@ -13,6 +15,7 @@ from requests_toolbelt.multipart import (
)
from poetry.__version__ import __version__
from poetry.utils.helpers import normalize_version
from ..metadata import Metadata
......@@ -51,6 +54,23 @@ class Uploader:
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):
self._username = username
self._password = password
......@@ -177,27 +197,7 @@ class Uploader:
raise
def _do_upload(self, session, url):
dist = self._poetry.file.parent / 'dist'
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:
for file in self.files:
# TODO: Check existence
resp = self._upload_file(session, url, file)
......
from .dependency_graph import DependencyGraph
from .resolver import Resolver
from .version_solver import VersionSolver
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
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))
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)]
......@@ -8,6 +8,7 @@ from .directory_dependency import DirectoryDependency
from .file_dependency import FileDependency
from .locker import Locker
from .package import Package
from .project_package import ProjectPackage
from .utils.link import Link
from .utils.utils import convert_markers
from .utils.utils import group_markers
......@@ -20,6 +21,14 @@ from .vcs_dependency import VCSDependency
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)
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):
def matches(self, _):
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):
return '*'
import operator
import re
from poetry.semver.constraints import EmptyConstraint
from poetry.semver.constraints import MultiConstraint
from poetry.semver.constraints.base_constraint import BaseConstraint
from .base_constraint import BaseConstraint
from .empty_constraint import EmptyConstraint
from .multi_constraint import MultiConstraint
class GenericConstraint(BaseConstraint):
......@@ -58,13 +58,8 @@ class GenericConstraint(BaseConstraint):
return self._version
def matches(self, provider):
if not isinstance(provider, (GenericConstraint, EmptyConstraint)):
raise ValueError(
'Generic constraints can only be compared with each other'
)
if isinstance(provider, EmptyConstraint):
return True
if not isinstance(provider, GenericConstraint):
return provider.matches(self)
is_equal_op = self.OP_EQ is self._operator
is_non_equal_op = self.OP_NE is self._operator
......
import poetry.packages
from poetry.semver.constraints import Constraint
from poetry.semver.constraints import EmptyConstraint
from poetry.semver.constraints import MultiConstraint
from poetry.semver.constraints.base_constraint import BaseConstraint
from poetry.semver.version_parser import VersionParser
from poetry.semver import parse_constraint
from poetry.semver import Version
from poetry.semver import VersionConstraint
from poetry.semver import VersionUnion
from poetry.utils.helpers import canonicalize_name
from .constraints.empty_constraint import EmptyConstraint
from .constraints.generic_constraint import GenericConstraint
from .constraints.multi_constraint import MultiConstraint
class Dependency(object):
......@@ -21,29 +22,30 @@ class Dependency(object):
):
self._name = canonicalize_name(name)
self._pretty_name = name
self._parser = VersionParser()
try:
if not isinstance(constraint, BaseConstraint):
self._constraint = self._parser.parse_constraints(constraint)
if not isinstance(constraint, VersionConstraint):
self._constraint = parse_constraint(constraint)
else:
self._constraint = constraint
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._category = category
self._allows_prereleases = allows_prereleases
self._python_versions = '*'
self._python_constraint = self._parser.parse_constraints('*')
self._python_constraint = parse_constraint('*')
self._platform = '*'
self._platform_constraint = EmptyConstraint()
self._extras = []
self._in_extras = []
self.is_root = False
@property
def name(self):
return self._name
......@@ -71,7 +73,7 @@ class Dependency(object):
@python_versions.setter
def python_versions(self, value):
self._python_versions = value
self._python_constraint = self._parser.parse_constraints(value)
self._python_constraint = parse_constraint(value)
@property
def python_constraint(self):
......@@ -119,7 +121,7 @@ class Dependency(object):
"""
return (
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())
)
......@@ -129,11 +131,13 @@ class Dependency(object):
if self.extras:
requirement += '[{}]'.format(','.join(self.extras))
if isinstance(self.constraint, MultiConstraint):
if isinstance(self.constraint, VersionUnion):
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(' ', ''))
# Markers
......@@ -147,6 +151,13 @@ class Dependency(object):
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)
if in_extras and with_extras:
markers.append(
......@@ -185,10 +196,57 @@ class Dependency(object):
parts = [part[1] for part in parts]
marker = glue.join(parts)
else:
elif isinstance(constraint, GenericConstraint):
marker = '{} {} "{}"'.format(
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
......@@ -204,16 +262,43 @@ class Dependency(object):
"""
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):
if not isinstance(other, Dependency):
return NotImplemented
return self._name == other.name and self._constraint == other.constraint
def __ne__(self, other):
return not self == other
def __hash__(self):
return hash((self._name, self._pretty_constraint))
def __str__(self):
if self.is_root:
return self._pretty_name
return '{} ({})'.format(
self._pretty_name, self._pretty_constraint
)
......
......@@ -34,6 +34,7 @@ class DirectoryDependency(Dependency):
develop=False # type: bool
):
from . import dependency_from_pep_508
from .package import Package
self._path = path
self._base = base
......@@ -79,9 +80,13 @@ class DirectoryDependency(Dependency):
with setup.open('w') as f:
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:
from poetry.packages import Package
# Execute egg_info
current_dir = os.getcwd()
os.chdir(str(self._full_path))
......@@ -129,7 +134,7 @@ class DirectoryDependency(Dependency):
self._package = package
self._package.source_type = 'directory'
self._package.source_reference = str(self._path)
self._package.source_url = str(self._path)
super(DirectoryDependency, self).__init__(
self._package.name,
......
......@@ -13,8 +13,6 @@ from poetry.utils.toml_file import TomlFile
class Locker:
_relevant_keys = [
'name',
'version',
'dependencies',
'dev-dependencies',
'source',
......
......@@ -4,16 +4,14 @@ import re
from typing import Union
from poetry.semver.constraints import Constraint
from poetry.semver.constraints import EmptyConstraint
from poetry.semver.helpers import parse_stability
from poetry.semver.version_parser import VersionParser
from poetry.semver import Version
from poetry.semver import parse_constraint
from poetry.spdx import license_by_id
from poetry.spdx import License
from poetry.utils._compat import Path
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 .dependency import Dependency
from .directory_dependency import DirectoryDependency
......@@ -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):
"""
Creates a new in memory package.
......@@ -64,14 +48,15 @@ class Package(object):
self._pretty_name = name
self._name = canonicalize_name(name)
self._version = str(parse_version(version))
self._pretty_version = pretty_version or version
if not isinstance(version, 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._stability = parse_stability(version)
self._dev = self._stability == 'dev'
self._authors = []
self.homepage = None
......@@ -89,8 +74,6 @@ class Package(object):
self.extras = {}
self.requires_extras = []
self._parser = VersionParser()
self.category = 'main'
self.hashes = []
self.optional = False
......@@ -105,12 +88,14 @@ class Package(object):
self.classifiers = []
self._python_versions = '*'
self._python_constraint = self._parser.parse_constraints('*')
self._python_constraint = parse_constraint('*')
self._platform = '*'
self._platform_constraint = EmptyConstraint()
self.root_dir = None
self.develop = False
@property
def name(self):
return self._name
......@@ -129,7 +114,10 @@ class Package(object):
@property
def unique_name(self):
return self.name + '-' + self._version
if self.is_root():
return self._name
return self.name + '-' + self._version.text
@property
def pretty_string(self):
......@@ -137,7 +125,7 @@ class Package(object):
@property
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
# if source reference is a sha1 hash -- truncate
......@@ -159,6 +147,10 @@ class Package(object):
def author_email(self): # type: () -> str
return self._get_author()['email']
@property
def all_requires(self):
return self.requires + self.dev_requires
def _get_author(self): # type: () -> dict
if not self._authors:
return {
......@@ -183,7 +175,7 @@ class Package(object):
@python_versions.setter
def python_versions(self, value):
self._python_versions = value
self._python_constraint = self._parser.parse_constraints(value)
self._python_constraint = parse_constraint(value)
@property
def python_constraint(self):
......@@ -220,19 +212,18 @@ class Package(object):
classifiers = copy.copy(self.classifiers)
# Automatically set python classifiers
parser = VersionParser()
if self.python_versions == '*':
python_constraint = parser.parse_constraints('~2.7 || ^3.4')
python_constraint = parse_constraint('~2.7 || ^3.4')
else:
python_constraint = self.python_constraint
for version in sorted(self.AVAILABLE_PYTHONS):
if len(version) == 1:
constraint = parser.parse_constraints(version + '.*')
constraint = parse_constraint(version + '.*')
else:
constraint = Constraint('=', version)
constraint = Version.parse(version)
if python_constraint.matches(constraint):
if python_constraint.allows_any(constraint):
classifiers.append(
'Programming Language :: Python :: {}'.format(version)
)
......@@ -245,11 +236,11 @@ class Package(object):
return sorted(classifiers)
def is_dev(self):
return self._dev
def is_prerelease(self):
return self._stability != 'stable'
return self._version.is_prerelease()
def is_root(self):
return False
def add_dependency(self,
name, # type: str
......@@ -335,6 +326,9 @@ class Package(object):
return dependency
def to_dependency(self):
return Dependency(self.name, self._version)
def __hash__(self):
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
from .packages import Dependency
from .packages import Locker
from .packages import Package
from .packages import ProjectPackage
from .repositories import Pool
from .repositories.pypi_repository import PyPiRepository
from .spdx import license_by_id
......@@ -95,7 +96,7 @@ class Poetry:
# Load package
name = local_config['name']
version = local_config['version']
package = Package(name, version, version)
package = ProjectPackage(name, version, version)
package.root_dir = poetry_file.parent
for author in local_config['authors']:
......
......@@ -35,3 +35,9 @@ class Operation(object):
self._skip_reason = reason
return self
def unskip(self): # type: () -> Operation
self._skipped = False
self._skip_reason = None
return self
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