Commit fedecf6d by Sébastien Eustace

Merge branch 'master' into develop

parents 3a39e5ac 5e34b9e2
name: Main
name: Tests
on: [push, pull_request]
......@@ -85,12 +85,12 @@ jobs:
- name: Install Poetry
run: |
python get-poetry.py --preview -y
SET PATH=%USERPROFILE%\\.poetry\\bin;%PATH%
$env:Path += ";$env:Userprofile\.poetry\bin"
- name: Install dependencies
run: |
SET PATH=%USERPROFILE%\\.poetry\\bin;%PATH%
$env:Path += ";$env:Userprofile\.poetry\bin"
poetry install
- name: Test
run: |
SET PATH=%USERPROFILE%\\.poetry\\bin;%PATH%
$env:Path += ";$env:Userprofile\.poetry\bin"
poetry run pytest -q tests
......@@ -3,4 +3,3 @@ repos:
rev: stable
hooks:
- id: black
language_version: python3.6
......@@ -21,6 +21,8 @@
- Added support for url dependencies ([#1260](https://github.com/sdispater/poetry/pull/1260)).
- Publishing to PyPI using [API tokens](https://pypi.org/help/#apitoken) is now supported ([#1275](https://github.com/sdispater/poetry/pull/1275)).
- Licenses can now be identified by their full name.
- Added support for support for custom certificate authority and client certificates for private repositories.
- Poetry can now detect and use Conda environments.
### Changed
......@@ -35,6 +37,9 @@
- The `add` command will now automatically select the latest prerelease if only prereleases are available.
- The `add` command can now update a dependencies if an explicit constraint is given ([#1221](https://github.com/sdispater/poetry/pull/1221)).
- Removed the `--develop` option from the `install` command.
- Improved UX when searching for packages in the `init` command.
- The `shell` has been improved.
- The `poetry run` command now uses `os.execvp()` rather than spawning a new subprocess.
### Fixed
......@@ -42,6 +47,15 @@
- The `pyproject.toml` configuration is now properly validated.
- Fixed installing Poetry-based packages breaking with `pip`.
- Fixed packages with empty markers being added to the lock file.
- Fixed invalid lock file generation in some cases.
- Fixed local version identifier handling in wheel file names.
- Fixed packages with invalid metadata triggering an error instead of being skipped.
- Fixed the generation of invalid lock files in some cases.
- Git dependencies are now properly locked to a specific revision when specifying a branch or a tag.
- Fixed the behavior of the `~=` operator.
- Fixed dependency resolution for conditional development dependencies.
- Fixed generated dependency constraints when they contain inequality operators.
- The `run` command now properly handles the `--` separator.
## [0.12.17] - 2019-07-03
......@@ -725,7 +739,7 @@ Initial release
[Unreleased]: https://github.com/sdispater/poetry/compare/0.12.17...develop
[Unreleased]: https://github.com/sdispater/poetry/compare/0.12.17...master
[0.12.17]: https://github.com/sdispater/poetry/releases/tag/0.12.17
[0.12.16]: https://github.com/sdispater/poetry/releases/tag/0.12.16
[0.12.15]: https://github.com/sdispater/poetry/releases/tag/0.12.15
......
......@@ -47,8 +47,8 @@ wheel:
@poetry build -v
linux_release:
docker pull quay.io/pypa/manylinux1_x86_64
docker run --rm -v `pwd`:/io quay.io/pypa/manylinux1_x86_64 /io/make-linux-release.sh
docker pull quay.io/pypa/manylinux2010_x86_64
docker run --rm -t -i -v `pwd`:/io quay.io/pypa/manylinux2010_x86_64 /io/make-linux-release.sh
# run tests against all supported python versions
tox:
......
......@@ -7,8 +7,7 @@ ensuring you have the right stack everywhere.
It supports Python 2.7 and 3.4+.
[![Unix Build Status](https://img.shields.io/travis/sdispater/poetry.svg?label=Unix)](https://travis-ci.org/sdispater/poetry)
[![Windows Build Status](https://img.shields.io/appveyor/ci/sdispater/poetry.svg?label=Windows)](https://ci.appveyor.com/project/sdispater/poetry)
![Tests Status](https://github.com/sdispater/poetry/workflows/Tests/badge.svg)
## Installation
......
......@@ -74,6 +74,16 @@ export POETRY_HTTP_BASIC_PYPI_PASSWORD=password
See [Using environment variables](/configuration#using-environment-variables) for more information
on how to configure Poetry with environment variables.
#### Custom certificate authority and mutual TLS authentication
Poetry supports repositories that are secured by a custom certificate authority as well as those that require
certificate-based client authentication. The following will configure the "foo" repository to validate the repository's
certificate using a custom certificate authority and use a client certificate (note that these config variables do not
both need to be set):
```bash
poetry config certificates.foo.cert /path/to/ca.pem
poetry config certificates.foo.client-cert /path/to/client.pem
```
### Install dependencies from a private repository
Now that you can publish to your private repository, you need to be able to
......@@ -105,8 +115,10 @@ From now on, Poetry will also look for packages in your private repository.
If your private repository requires HTTP Basic Auth be sure to add the username and
password to your `http-basic` configuration using the example above (be sure to use the
same name that is in the `tool.poetry.source` section). Poetry will use these values
to authenticate to your private repository when downloading or looking for packages.
same name that is in the `tool.poetry.source` section). If your repository requires either
a custom certificate authority or client certificates, similarly refer to the example above to configure the
`certificates` section. Poetry will use these values to authenticate to your private repository when downloading or
looking for packages.
### Disabling the PyPI repository
......
......@@ -6,7 +6,7 @@ title: {{ page.title|striptags|e }}
<section class="p-b-50 p-t-50 documentation-content">
<div class="container">
<div class="panel panel-transparent">
<div class="bg-white row p-l-20 p-r-20 p-b-20 p-t-5 xs-no-padding">
<div class="row p-l-20 p-r-20 p-b-20 p-t-5 xs-no-padding">
<div class="row">
<div class="col-md-3 documentation-toc">
<ul class="current">
......
{%- if nav_item.url %}
<a class="{% if nav_item.active%}current{%endif%}" href="{{ base_url }}/{{ nav_item.url }}">{{ nav_item.title }}</a>
{%- else %}
<span class="caption-text">{{ nav_item.title }}</span>
{%- endif %}
{%- if nav_item == page or nav_item.children %}
<ul class="subnav">
......
......@@ -299,7 +299,9 @@ class Installer:
r"(?:\+[^\s]+)?"
)
BASE_URL = "https://github.com/sdispater/poetry/releases/download/"
REPOSITORY_URL = "https://github.com/python-poetry/poetry"
BASE_URL = REPOSITORY_URL + "/releases/download/"
FALLBACK_BASE_URL = "https://github.com/sdispater/poetry/releases/download/"
def __init__(
self,
......@@ -598,6 +600,9 @@ class Installer:
"""
Tries to update the $PATH automatically.
"""
if not self._modify_path:
return
if WINDOWS:
return self.add_to_windows_path()
......@@ -606,7 +611,6 @@ class Installer:
addition = "\n{}\n".format(export_string)
updated = []
profiles = self.get_unix_profiles()
for profile in profiles:
if not os.path.exists(profile):
......@@ -619,8 +623,6 @@ class Installer:
with open(profile, "a") as f:
f.write(u(addition))
updated.append(os.path.relpath(profile, HOME))
def add_to_windows_path(self):
try:
old_path = self.get_windows_path_var()
......@@ -849,6 +851,15 @@ def main():
args = parser.parse_args()
base_url = Installer.BASE_URL
try:
r = urlopen(Installer.REPOSITORY_URL)
except HTTPError as e:
if e.code == 404:
base_url = Installer.FALLBACK_BASE_URL
else:
raise
installer = Installer(
version=args.version or os.getenv("POETRY_VERSION"),
preview=args.preview or string_to_bool(os.getenv("POETRY_PREVIEW", "0")),
......@@ -856,6 +867,7 @@ def main():
accept_all=args.accept_all
or string_to_bool(os.getenv("POETRY_ACCEPT", "0"))
or not is_interactive(),
base_url=base_url,
)
if args.uninstall or string_to_bool(os.getenv("POETRY_UNINSTALL", "0")):
......
#!/bin/bash
PYTHON_VERSIONS="cp27-cp27m cp34-cp34m cp35-cp35m cp36-cp36m cp37-cp37m"
PYTHON_VERSIONS="cp27-cp27m cp34-cp34m cp35-cp35m cp36-cp36m cp37-cp37m cp38-cp38"
cd /io
/opt/python/cp37-cp37m/bin/pip install pip -U
/opt/python/cp37-cp37m/bin/pip install poetry -U
/opt/python/cp37-cp37m/bin/poetry config settings.virtualenvs.create false
/opt/python/cp37-cp37m/bin/poetry install --no-dev
......@@ -10,5 +11,6 @@ cd /io
-P "3.4:/opt/python/cp34-cp34m/bin/python" \
-P "3.5:/opt/python/cp35-cp35m/bin/python" \
-P "3.6:/opt/python/cp36-cp36m/bin/python" \
-P "3.7:/opt/python/cp37-cp37m/bin/python"
-P "3.7:/opt/python/cp37-cp37m/bin/python" \
-P "3.8:/opt/python/cp38-cp38/bin/python"
cd -
__version__ = "1.0.0b1"
__version__ = "1.0.0b3"
......@@ -21,7 +21,7 @@ class BuildCommand(EnvCommand):
package = self.poetry.package
self.line(
"Building <info>{}</> (<comment>{}</>)".format(
"Building <c1>{}</c1> (<b>{}</b>)".format(
package.pretty_name, package.version
)
)
......
......@@ -228,6 +228,27 @@ To remove a repository (repo is a short alias for repositories):
return 0
# handle certs
m = re.match(
r"(?:certificates)\.([^.]+)\.(cert|client-cert)", self.argument("key")
)
if m:
if self.option("unset"):
config.auth_config_source.remove_property(
"certificates.{}.{}".format(m.group(1), m.group(2))
)
return 0
if len(values) == 1:
config.auth_config_source.add_property(
"certificates.{}.{}".format(m.group(1), m.group(2)), values[0]
)
else:
raise ValueError("You must pass exactly 1 value")
return 0
raise ValueError("Setting {} does not exist".format(self.argument("key")))
def _handle_single_value(self, source, key, callbacks, values):
......
......@@ -5,10 +5,10 @@ from typing import List
from cleo import argument
from cleo import option
from ..command import Command
from ..init import InitCommand
class DebugResolveCommand(Command):
class DebugResolveCommand(InitCommand):
name = "resolve"
description = "Debugs dependency resolution."
......@@ -43,12 +43,25 @@ class DebugResolveCommand(Command):
if not packages:
package = self.poetry.package
else:
# Using current pool for determine_requirements()
self._pool = self.poetry.pool
package = ProjectPackage(
self.poetry.package.name, self.poetry.package.version
)
requirements = self._format_requirements(packages)
for name, constraint in requirements.items():
# Silencing output
is_quiet = self.io.output.is_quiet()
if not is_quiet:
self.io.output.set_quiet(True)
requirements = self._determine_requirements(packages)
if not is_quiet:
self.io.output.set_quiet(False)
for constraint in requirements:
name = constraint.pop("name")
dep = package.add_dependency(name, constraint)
extras = []
for extra in self.option("extras"):
......@@ -90,7 +103,7 @@ class DebugResolveCommand(Command):
return 0
env = EnvManager(self.poetry.config).get(self.poetry.file.parent)
env = EnvManager(self.poetry).get()
current_python_version = parse_constraint(
".".join(str(v) for v in env.version_info)
)
......@@ -103,11 +116,7 @@ class DebugResolveCommand(Command):
current_python_version
) or not env.is_valid_for_marker(pkg.marker):
continue
row = [
"<info>{}</info>".format(pkg.name),
"<b>{}</b>".format(pkg.version),
"",
]
row = ["<c1>{}</c1>".format(pkg.name), "<b>{}</b>".format(pkg.version), ""]
if not pkg.marker.is_any():
row[2] = str(pkg.marker)
......@@ -116,55 +125,3 @@ class DebugResolveCommand(Command):
table.set_rows(rows)
table.render(self.io)
def _determine_requirements(self, requires): # type: (List[str]) -> List[str]
from poetry.semver import parse_constraint
if not requires:
return []
requires = self._parse_name_version_pairs(requires)
for requirement in requires:
if "version" in requirement:
parse_constraint(requirement["version"])
return requires
def _parse_name_version_pairs(self, pairs): # type: (list) -> list
result = []
for i in range(len(pairs)):
if pairs[i].startswith("git+https://"):
url = pairs[i].lstrip("git+")
rev = None
if "@" in url:
url, rev = url.split("@")
pair = {"name": url.split("/")[-1].rstrip(".git"), "git": url}
if rev:
pair["rev"] = rev
result.append(pair)
continue
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, "version": "*"})
return result
def _format_requirements(self, requirements): # type: (List[str]) -> dict
requires = {}
requirements = self._determine_requirements(requirements)
for requirement in requirements:
name = requirement.pop("name")
requires[name] = requirement
return requires
......@@ -13,8 +13,7 @@ class EnvInfoCommand(Command):
def handle(self):
from poetry.utils.env import EnvManager
poetry = self.poetry
env = EnvManager(poetry.config).get(cwd=poetry.file.parent)
env = EnvManager(self.poetry).get()
if self.option("path"):
if not env.is_venv():
......
......@@ -13,11 +13,10 @@ class EnvListCommand(Command):
def handle(self):
from poetry.utils.env import EnvManager
poetry = self.poetry
manager = EnvManager(poetry.config)
current_env = manager.get(self.poetry.file.parent)
manager = EnvManager(self.poetry)
current_env = manager.get()
for venv in manager.list(self.poetry.file.parent):
for venv in manager.list():
name = venv.path.name
if self.option("full-path"):
name = str(venv.path)
......
......@@ -15,8 +15,7 @@ class EnvRemoveCommand(Command):
def handle(self):
from poetry.utils.env import EnvManager
poetry = self.poetry
manager = EnvManager(poetry.config)
venv = manager.remove(self.argument("python"), poetry.file.parent)
manager = EnvManager(self.poetry)
venv = manager.remove(self.argument("python"))
self.line("Deleted virtualenv: <comment>{}</comment>".format(venv.path))
......@@ -13,14 +13,13 @@ class EnvUseCommand(Command):
def handle(self):
from poetry.utils.env import EnvManager
poetry = self.poetry
manager = EnvManager(poetry.config)
manager = EnvManager(self.poetry)
if self.argument("python") == "system":
manager.deactivate(poetry.file.parent, self._io)
manager.deactivate(self._io)
return
env = manager.activate(self.argument("python"), poetry.file.parent, self._io)
env = manager.activate(self.argument("python"), self._io)
self.line("Using virtualenv: <comment>{}</>".format(env.path))
......@@ -3,6 +3,7 @@ from __future__ import unicode_literals
import os
import re
import sys
from typing import Dict
from typing import List
......@@ -15,7 +16,6 @@ from tomlkit import inline_table
from poetry.utils._compat import Path
from poetry.utils._compat import OrderedDict
from poetry.utils._compat import urlparse
from poetry.utils.helpers import temporary_directory
from .command import Command
from .env_command import EnvCommand
......@@ -52,7 +52,7 @@ class InitCommand(Command):
]
help = """\
The <info>init</info> command creates a basic <comment>pyproject.toml</> file in the current directory.
The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the current directory.
"""
def __init__(self):
......@@ -63,7 +63,7 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in
def handle(self):
from poetry.layouts import layout
from poetry.utils._compat import Path
from poetry.utils.env import EnvManager
from poetry.utils.env import SystemEnv
from poetry.vcs.git import GitConfig
if (Path.cwd() / "pyproject.toml").exists():
......@@ -126,7 +126,7 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in
question.set_validator(self._validate_license)
license = self.ask(question)
current_env = EnvManager().get(Path.cwd())
current_env = SystemEnv(Path(sys.executable))
default_python = "^{}".format(
".".join(str(v) for v in current_env.version_info[:2])
)
......@@ -209,7 +209,9 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in
if not requires:
requires = []
package = self.ask("Add a package:")
package = self.ask(
"Search for package to add (or leave blank to continue):"
)
while package is not None:
constraint = self._parse_requirements([package])[0]
if (
......@@ -247,7 +249,7 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in
choices.append(found_package.pretty_name)
self.line(
"Found <info>{}</info> packages matching <info>{}</info>".format(
"Found <info>{}</info> packages matching <c1>{}</c1>".format(
len(matches), package
)
)
......@@ -275,7 +277,7 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in
)
self.line(
"Using version <info>{}</info> for <info>{}</info>".format(
"Using version <b>{}</b> for <c1>{}</c1>".format(
package_constraint, package
)
)
......@@ -304,7 +306,7 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in
requirement["name"] = name
self.line(
"Using version <info>{}</> for <info>{}</>".format(version, name)
"Using version <b>{}</b> for <c1>{}</c1>".format(version, name)
)
else:
# check that the specified version/constraint exists
......
......@@ -77,7 +77,7 @@ exist it will look for <comment>pyproject.toml</> and do the same.
return 0
self.line(
" - Installing <info>{}</info> (<comment>{}</comment>)".format(
" - Installing <c1>{}</c1> (<b>{}</b>)".format(
self.poetry.package.pretty_name, self.poetry.package.pretty_version
)
)
......
import sys
from cleo import argument
from cleo import option
......@@ -17,8 +19,9 @@ class NewCommand(Command):
def handle(self):
from poetry.layouts import layout
from poetry.semver import parse_constraint
from poetry.utils._compat import Path
from poetry.utils.env import EnvManager
from poetry.utils.env import SystemEnv
from poetry.vcs.git import GitConfig
if self.option("src"):
......@@ -49,16 +52,25 @@ class NewCommand(Command):
if author_email:
author += " <{}>".format(author_email)
current_env = EnvManager().get(Path.cwd())
current_env = SystemEnv(Path(sys.executable))
default_python = "^{}".format(
".".join(str(v) for v in current_env.version_info[:2])
)
dev_dependencies = {}
python_constraint = parse_constraint(default_python)
if parse_constraint("<3.5").allows_any(python_constraint):
dev_dependencies["pytest"] = "^4.6"
if parse_constraint(">=3.5").allows_all(python_constraint):
dev_dependencies["pytest"] = "^5.2"
layout_ = layout_(
name,
"0.1.0",
author=author,
readme_format=readme_format,
python=default_python,
dev_dependencies=dev_dependencies,
)
layout_.create(path)
......
from cleo import option
from poetry.utils._compat import Path
from .command import Command
......@@ -14,6 +16,15 @@ class PublishCommand(Command):
),
option("username", "u", "The username to access the repository.", flag=False),
option("password", "p", "The password to access the repository.", flag=False),
option(
"cert", None, "Certificate authority to access the repository.", flag=False
),
option(
"client-cert",
None,
"Client certificate to access the repository.",
flag=False,
),
option("build", None, "Build the package before publishing."),
]
......@@ -57,6 +68,15 @@ the config command.
self.line("")
cert = Path(self.option("cert")) if self.option("cert") else None
client_cert = (
Path(self.option("client-cert")) if self.option("client-cert") else None
)
publisher.publish(
self.option("repository"), self.option("username"), self.option("password")
self.option("repository"),
self.option("username"),
self.option("password"),
cert,
client_cert,
)
......@@ -40,15 +40,13 @@ class RunCommand(EnvCommand):
cmd = ["python", "-c"]
cmd += [
'"import sys; '
"import sys; "
"from importlib import import_module; "
"sys.argv = {!r}; {}"
"import_module('{}').{}()\"".format(
args, src_in_sys_path, module, callable_
)
"import_module('{}').{}()".format(args, src_in_sys_path, module, callable_)
]
return self.env.run(*cmd, shell=True, call=True)
return self.env.execute(*cmd)
@property
def _module(self):
......
......@@ -29,7 +29,9 @@ class SelfUpdateCommand(Command):
arguments = [argument("version", "The version to update to.", optional=True)]
options = [option("preview", None, "Install prereleases.")]
BASE_URL = "https://github.com/sdispater/poetry/releases/download"
REPOSITORY_URL = "https://github.com/python-poetry/poetry"
BASE_URL = REPOSITORY_URL + "/releases/download"
FALLBACK_BASE_URL = "https://github.com/sdispater/poetry/releases/download"
@property
def home(self):
......@@ -150,8 +152,17 @@ class SelfUpdateCommand(Command):
checksum = "poetry-{}-{}.sha256sum".format(version, platform)
base_url = self.BASE_URL
try:
r = urlopen(self.BASE_URL + "/{}/{}".format(version, checksum))
urlopen(self.REPOSITORY_URL)
except HTTPError as e:
if e.code == 404:
base_url = self.FALLBACK_BASE_URL
else:
raise
try:
r = urlopen(base_url + "/{}/{}".format(version, checksum))
except HTTPError as e:
if e.code == 404:
raise RuntimeError("Could not find {} file".format(checksum))
......@@ -163,7 +174,7 @@ class SelfUpdateCommand(Command):
# We get the payload from the remote host
name = "poetry-{}-{}.tar.gz".format(version, platform)
try:
r = urlopen(self.BASE_URL + "/{}/{}".format(version, name))
r = urlopen(base_url + "/{}/{}".format(version, name))
except HTTPError as e:
if e.code == 404:
raise RuntimeError("Could not find {} file".format(name))
......
......@@ -80,20 +80,20 @@ lists all packages available."""
return 0
rows = [
["<info>name</>", " : <info>{}</>".format(pkg.pretty_name)],
["<info>version</>", " : <comment>{}</>".format(pkg.pretty_version)],
["<info>name</>", " : <c1>{}</>".format(pkg.pretty_name)],
["<info>version</>", " : <b>{}</b>".format(pkg.pretty_version)],
["<info>description</>", " : {}".format(pkg.description)],
]
table.add_rows(rows)
table.render()
table.render(self.io)
if pkg.requires:
self.line("")
self.line("<info>dependencies</info>")
for dependency in pkg.requires:
self.line(
" - {} <comment>{}</>".format(
" - <c1>{}</c1> <b>{}</b>".format(
dependency.pretty_name, dependency.pretty_constraint
)
)
......@@ -211,7 +211,7 @@ lists all packages available."""
self.line(line)
def display_package_tree(self, io, package, installed_repo):
io.write("<info>{}</info>".format(package.pretty_name))
io.write("<c1>{}</c1>".format(package.pretty_name))
description = ""
if package.description:
description = " " + package.description
......
......@@ -47,7 +47,7 @@ patch, minor, major, prepatch, preminor, premajor, prerelease.
)
self.line(
"Bumping version from <comment>{}</> to <info>{}</>".format(
"Bumping version from <b>{}</> to <fg=green>{}</>".format(
self.poetry.package.pretty_version, version
)
)
......
import logging
from cleo.config import ApplicationConfig as BaseApplicationConfig
from clikit.api.event import ConsoleEvents
from clikit.api.event import PRE_HANDLE
from clikit.api.event import PreHandleEvent
from clikit.api.formatter import Style
from clikit.api.io import Input
from clikit.api.io import InputStream
from clikit.api.io import Output
from clikit.api.io import OutputStream
from clikit.api.io.flags import DEBUG
from clikit.api.io.flags import VERBOSE
from clikit.api.io.flags import VERY_VERBOSE
from clikit.formatter import AnsiFormatter
from clikit.formatter import PlainFormatter
from clikit.io.input_stream import StandardInputStream
from clikit.io.output_stream import ErrorOutputStream
from clikit.io.output_stream import StandardOutputStream
from poetry.console.commands.command import Command
from poetry.console.commands.env_command import EnvCommand
......@@ -16,22 +28,17 @@ class ApplicationConfig(BaseApplicationConfig):
super(ApplicationConfig, self).configure()
self.add_style(Style("c1").fg("cyan"))
self.add_style(Style("info").fg("cyan"))
self.add_style(Style("info").fg("blue"))
self.add_style(Style("comment").fg("green"))
self.add_style(Style("error").fg("red").bold())
self.add_style(Style("warning").fg("yellow"))
self.add_style(Style("debug").fg("black").bold())
self.add_event_listener(
ConsoleEvents.PRE_HANDLE.value, self.register_command_loggers
)
self.add_event_listener(ConsoleEvents.PRE_HANDLE.value, self.set_env)
self.add_event_listener(PRE_HANDLE, self.register_command_loggers)
self.add_event_listener(PRE_HANDLE, self.set_env)
def register_command_loggers(
self,
event, # type: PreHandleEvent
event_name, # type: str
_,
self, event, event_name, _ # type: PreHandleEvent # type: str
): # type: (...) -> None
command = event.command.config.handler
if not isinstance(command, Command):
......@@ -70,27 +77,100 @@ class ApplicationConfig(BaseApplicationConfig):
io = event.io
poetry = command.poetry
env_manager = EnvManager(poetry.config)
# Checking compatibility of the current environment with
# the python dependency specified in pyproject.toml
current_env = env_manager.get(poetry.file.parent)
supported_python = poetry.package.python_constraint
current_python = parse_constraint(
".".join(str(v) for v in current_env.version_info[:3])
)
if not supported_python.allows(current_python):
raise RuntimeError(
"The current Python version ({}) is not supported by the project ({})\n"
"Please activate a compatible Python version.".format(
current_python, poetry.package.python_versions
)
)
env = env_manager.create_venv(poetry.file.parent, io, poetry.package.name)
env_manager = EnvManager(poetry)
env = env_manager.create_venv(io)
if env.is_venv() and io.is_verbose():
io.write_line("Using virtualenv: <comment>{}</>".format(env.path))
command.set_env(env)
def resolve_help_command(
self, event, event_name, dispatcher
): # type: (PreResolveEvent, str, EventDispatcher) -> None
args = event.raw_args
application = event.application
if args.has_option_token("-h") or args.has_option_token("--help"):
from clikit.api.resolver import ResolvedCommand
resolved_command = self.command_resolver.resolve(args, application)
# If the current command is the run one, skip option
# check and interpret them as part of the executed command
if resolved_command.command.name == "run":
event.set_resolved_command(resolved_command)
return event.stop_propagation()
command = application.get_command("help")
# Enable lenient parsing
parsed_args = command.parse(args, True)
event.set_resolved_command(ResolvedCommand(command, parsed_args))
event.stop_propagation()
def create_io(
self,
application,
args,
input_stream=None,
output_stream=None,
error_stream=None,
): # type: (Application, RawArgs, InputStream, OutputStream, OutputStream) -> IO
if input_stream is None:
input_stream = StandardInputStream()
if output_stream is None:
output_stream = StandardOutputStream()
if error_stream is None:
error_stream = ErrorOutputStream()
style_set = application.config.style_set
if output_stream.supports_ansi():
output_formatter = AnsiFormatter(style_set)
else:
output_formatter = PlainFormatter(style_set)
if error_stream.supports_ansi():
error_formatter = AnsiFormatter(style_set)
else:
error_formatter = PlainFormatter(style_set)
io = self.io_class(
Input(input_stream),
Output(output_stream, output_formatter),
Output(error_stream, error_formatter),
)
resolved_command = application.resolve_command(args)
# If the current command is the run one, skip option
# check and interpret them as part of the executed command
if resolved_command.command.name == "run":
return io
if args.has_option_token("--no-ansi"):
formatter = PlainFormatter(style_set)
io.output.set_formatter(formatter)
io.error_output.set_formatter(formatter)
elif args.has_option_token("--ansi"):
formatter = AnsiFormatter(style_set, True)
io.output.set_formatter(formatter)
io.error_output.set_formatter(formatter)
if args.has_option_token("-vvv") or self.is_debug():
io.set_verbosity(DEBUG)
elif args.has_option_token("-vv"):
io.set_verbosity(VERY_VERBOSE)
elif args.has_option_token("-v"):
io.set_verbosity(VERBOSE)
if args.has_option_token("--quiet") or args.has_option_token("-q"):
io.set_quiet(True)
if args.has_option_token("--no-interaction") or args.has_option_token("-n"):
io.set_interactive(False)
return io
......@@ -233,7 +233,7 @@ class Factory:
): # type: (Dict[str, str], Config) -> LegacyRepository
from .repositories.auth import Auth
from .repositories.legacy_repository import LegacyRepository
from .utils.helpers import get_http_basic_auth
from .utils.helpers import get_client_cert, get_cert, get_http_basic_auth
if "url" in source:
# PyPI-like repository
......@@ -245,12 +245,18 @@ class Factory:
name = source["name"]
url = source["url"]
credentials = get_http_basic_auth(auth_config, name)
if not credentials:
return LegacyRepository(name, url)
if credentials:
auth = Auth(url, credentials[0], credentials[1])
return LegacyRepository(name, url, auth=auth)
else:
auth = None
return LegacyRepository(
name,
url,
auth=auth,
cert=get_cert(auth_config, name),
client_cert=get_client_cert(auth_config, name),
)
@classmethod
def validate(
......
......@@ -306,7 +306,7 @@ class Installer:
if operation.skipped:
if self.is_verbose() and (self._execute_operations or self.is_dry_run()):
self._io.write_line(
" - Skipping <info>{}</> (<comment>{}</>) {}".format(
" - Skipping <c1>{}</c1> (<b>{}</b>) {}".format(
operation.package.pretty_name,
operation.package.full_pretty_version,
operation.skip_reason,
......@@ -317,7 +317,7 @@ class Installer:
if self._execute_operations or self.is_dry_run():
self._io.write_line(
" - Installing <info>{}</> (<comment>{}</>)".format(
" - Installing <c1>{}</c1> (<b>{}</b>)".format(
operation.package.pretty_name, operation.package.full_pretty_version
)
)
......@@ -334,7 +334,7 @@ class Installer:
if operation.skipped:
if self.is_verbose() and (self._execute_operations or self.is_dry_run()):
self._io.write_line(
" - Skipping <info>{}</> (<comment>{}</>) {}".format(
" - Skipping <c1>{}</c1> (<b>{}</b>) {}".format(
target.pretty_name,
target.full_pretty_version,
operation.skip_reason,
......@@ -345,7 +345,7 @@ class Installer:
if self._execute_operations or self.is_dry_run():
self._io.write_line(
" - Updating <info>{}</> (<comment>{}</> -> <comment>{}</>)".format(
" - Updating <c1>{}</c1> (<b>{}</b> -> <b>{}</b>)".format(
target.pretty_name,
source.full_pretty_version,
target.full_pretty_version,
......@@ -361,7 +361,7 @@ class Installer:
if operation.skipped:
if self.is_verbose() and (self._execute_operations or self.is_dry_run()):
self._io.write_line(
" - Not removing <info>{}</> (<comment>{}</>) {}".format(
" - Not removing <c1>{}</c1> (<b>{}</b>) {}".format(
operation.package.pretty_name,
operation.package.full_pretty_version,
operation.skip_reason,
......@@ -372,7 +372,7 @@ class Installer:
if self._execute_operations or self.is_dry_run():
self._io.write_line(
" - Removing <info>{}</> (<comment>{}</>)".format(
" - Removing <c1>{}</c1> (<b>{}</b>)".format(
operation.package.pretty_name, operation.package.full_pretty_version
)
)
......
......@@ -52,6 +52,12 @@ class PipInstaller(BaseInstaller):
)
args += ["--trusted-host", parsed.hostname]
if repository.cert:
args += ["--cert", str(repository.cert)]
if repository.client_cert:
args += ["--client-cert", str(repository.client_cert)]
index_url = repository.authenticated_url
args += ["--index-url", index_url]
......@@ -65,7 +71,7 @@ class PipInstaller(BaseInstaller):
if update:
args.append("-U")
if package.hashes and not package.source_type:
if package.files and not package.source_type:
# Format as a requirements.txt
# We need to create a requirements.txt file
# for each package in order to check hashes.
......@@ -112,8 +118,9 @@ class PipInstaller(BaseInstaller):
def requirement(self, package, formatted=False):
if formatted and not package.source_type:
req = "{}=={}".format(package.name, package.version)
for h in package.hashes:
for f in package.files:
hash_type = "sha256"
h = f["hash"]
if ":" in h:
hash_type, h = h.split(":")
......@@ -163,7 +170,7 @@ class PipInstaller(BaseInstaller):
def install_directory(self, package):
from poetry.masonry.builder import SdistBuilder
from poetry.poetry import Poetry
from poetry.factory import Factory
from poetry.utils._compat import decode
from poetry.utils.env import NullEnv
from poetry.utils.toml_file import TomlFile
......@@ -196,7 +203,9 @@ class PipInstaller(BaseInstaller):
# file since pip, as of this comment, does not support
# build-system for editable packages
# We also need it for non-PEP-517 packages
builder = SdistBuilder(Poetry.create(pyproject.parent), NullEnv(), NullIO())
builder = SdistBuilder(
Factory().create_poetry(pyproject.parent), NullEnv(), NullIO()
)
with open(setup, "w", encoding="utf-8") as f:
f.write(decode(builder.build_setup()))
......
......@@ -31,7 +31,9 @@ def validate_object(obj, schema_name): # type: (dict, str) -> List[str]
for error in validation_errors:
message = error.message
if error.path:
message = "[{}] {}".format(".".join(error.path), message)
message = "[{}] {}".format(
".".join(str(x) for x in error.absolute_path), message
)
errors.append(message)
......
......@@ -63,10 +63,7 @@ class Layout(object):
self._license = license
self._python = python
self._dependencies = dependencies or {}
if dev_dependencies is None:
dev_dependencies = {"pytest": "^3.5"}
self._dev_dependencies = dev_dependencies
self._dev_dependencies = dev_dependencies or {}
if not author:
author = "Your Name <you@example.com>"
......@@ -131,8 +128,6 @@ class Layout(object):
readme_file.touch()
def _create_tests(self, path):
self._dev_dependencies["pytest"] = "^3.0"
tests = path / "tests"
tests_init = tests / "__init__.py"
tests_default = tests / "test_{}.py".format(self._package_name)
......
......@@ -22,7 +22,7 @@ from .builder import Builder
SETUP = """\
# -*- coding: utf-8 -*-
from distutils.core import setup
from setuptools import setup
{before}
setup_kwargs = {{
......
......@@ -19,7 +19,7 @@ from poetry.__version__ import __version__
from poetry.semver import parse_constraint
from poetry.utils._compat import decode
from ..utils.helpers import normalize_file_permissions
from ..utils.helpers import normalize_file_permissions, escape_name, escape_version
from ..utils.package_include import PackageInclude
from ..utils.tags import get_abbr_impl
from ..utils.tags import get_abi_tag
......@@ -206,8 +206,8 @@ class WheelBuilder(Builder):
@property
def wheel_filename(self): # type: () -> str
return "{}-{}-{}.whl".format(
re.sub(r"[^\w\d.]+", "_", self._package.pretty_name, flags=re.UNICODE),
re.sub(r"[^\w\d.\+]+", "_", self._meta.version, flags=re.UNICODE),
escape_name(self._package.pretty_name),
escape_version(self._meta.version),
self.tag,
)
......@@ -217,8 +217,8 @@ class WheelBuilder(Builder):
)
def dist_info_name(self, distribution, version): # type: (...) -> str
escaped_name = re.sub(r"[^\w\d.]+", "_", distribution, flags=re.UNICODE)
escaped_version = re.sub(r"[^\w\d.+]+", "_", version, flags=re.UNICODE)
escaped_name = escape_name(distribution)
escaped_version = escape_version(version)
return "{}-{}.dist-info".format(escaped_name, escaped_version)
......
import logging
from poetry.utils.helpers import get_http_basic_auth
from poetry.utils.helpers import get_client_cert, get_cert, get_http_basic_auth
from .uploader import Uploader
......@@ -23,11 +23,11 @@ class Publisher:
def files(self):
return self._uploader.files
def publish(self, repository_name, username, password):
def publish(self, repository_name, username, password, cert=None, client_cert=None):
if repository_name:
self._io.write_line(
"Publishing <info>{}</info> (<comment>{}</comment>) "
"to <fg=cyan>{}</>".format(
"Publishing <c1>{}</c1> (<b>{}</b>) "
"to <info>{}</info>".format(
self._package.pretty_name,
self._package.pretty_version,
repository_name,
......@@ -35,8 +35,8 @@ class Publisher:
)
else:
self._io.write_line(
"Publishing <info>{}</info> (<comment>{}</comment>) "
"to <fg=cyan>PyPI</>".format(
"Publishing <c1>{}</c1> (<b>{}</b>) "
"to <info>PyPI</info>".format(
self._package.pretty_name, self._package.pretty_version
)
)
......@@ -74,15 +74,21 @@ class Publisher:
username = auth[0]
password = auth[1]
# Requesting missing credentials
if not username:
resolved_client_cert = client_cert or get_client_cert(
self._poetry.config, repository_name
)
# Requesting missing credentials but only if there is not a client cert defined.
if not resolved_client_cert:
if username is None:
username = self._io.ask("Username:")
if password is None:
password = self._io.ask_hidden("Password:")
# TODO: handle certificates
self._uploader.auth(username, password)
return self._uploader.upload(url)
return self._uploader.upload(
url,
cert=cert or get_cert(self._poetry.config, repository_name),
client_cert=resolved_client_cert,
)
......@@ -3,7 +3,7 @@ import io
import math
import re
from typing import List
from typing import List, Optional
import requests
......@@ -14,10 +14,12 @@ from requests_toolbelt import user_agent
from requests_toolbelt.multipart import MultipartEncoder, MultipartEncoderMonitor
from poetry.__version__ import __version__
from poetry.utils._compat import Path
from poetry.utils.helpers import normalize_version
from poetry.utils.patterns import wheel_file_re
from ..metadata import Metadata
from ..utils.helpers import escape_name, escape_version
_has_blake2 = hasattr(hashlib, "blake2b")
......@@ -63,10 +65,7 @@ class Uploader:
wheels = list(
dist.glob(
"{}-{}-*.whl".format(
re.sub(
r"[^\w\d.]+", "_", self._package.pretty_name, flags=re.UNICODE
),
re.sub(r"[^\w\d.]+", "_", version, flags=re.UNICODE),
escape_name(self._package.pretty_name), escape_version(version)
)
)
)
......@@ -94,9 +93,17 @@ class Uploader:
def is_authenticated(self):
return self._username is not None and self._password is not None
def upload(self, url):
def upload(
self, url, cert=None, client_cert=None
): # type: (str, Optional[Path], Optional[Path]) -> None
session = self.make_session()
if cert:
session.verify = str(cert)
if client_cert:
session.cert = str(client_cert)
try:
self._upload(session, url)
finally:
......@@ -222,7 +229,7 @@ class Uploader:
encoder = MultipartEncoder(data_to_send)
bar = self._io.progress_bar(encoder.len)
bar.set_format(
" - Uploading <info>{0}</> <comment>%percent%%</>".format(file.name)
" - Uploading <c1>{0}</c1> <b>%percent%%</b>".format(file.name)
)
monitor = MultipartEncoderMonitor(
encoder, lambda monitor: bar.set_progress(monitor.bytes_read)
......@@ -238,13 +245,18 @@ class Uploader:
)
if resp.ok:
bar.set_format(
" - Uploading <c1>{0}</c1> <fg=green>%percent%%</>".format(
file.name
)
)
bar.finish()
self._io.write_line("")
else:
if self._io.output.supports_ansi():
self._io.overwrite(
" - Uploading <info>{0}</> <error>{1}%</>".format(
" - Uploading <c1>{0}</c1> <error>{1}%</>".format(
file.name, int(math.floor(bar._percent * 100))
)
)
......
import re
def normalize_file_permissions(st_mode):
"""
Normalizes the permission bits in the st_mode field from stat to 644/755
......@@ -12,3 +15,17 @@ def normalize_file_permissions(st_mode):
new_mode |= 0o111 # Executable: 644 -> 755
return new_mode
def escape_version(version):
"""
Escaped version in wheel filename. Doesn't exactly follow
the escaping specification in :pep:`427#escaping-and-unicode`
because this conflicts with :pep:`440#local-version-identifiers`.
"""
return re.sub(r"[^\w\d.+]+", "_", version, flags=re.UNICODE)
def escape_name(name):
"""Escaped wheel name as specified in :pep:`427#escaping-and-unicode`."""
return re.sub(r"[^\w\d.]+", "_", name, flags=re.UNICODE)
......@@ -144,9 +144,10 @@ class Dependency(object):
requirement += "[{}]".format(",".join(self.extras))
if isinstance(self.constraint, VersionUnion):
requirement += " ({})".format(
",".join([str(c).replace(" ", "") for c in self.constraint.ranges])
)
if self.constraint.excludes_single_version():
requirement += " ({})".format(str(self.constraint))
else:
requirement += " ({})".format(self.pretty_constraint)
elif isinstance(self.constraint, Version):
requirement += " (=={})".format(self.constraint.text)
elif not self.constraint.is_any():
......
......@@ -6,6 +6,9 @@ import poetry.repositories
from hashlib import sha256
from tomlkit import document
from tomlkit import inline_table
from tomlkit import item
from tomlkit import table
from typing import List
from poetry.utils._compat import Path
......@@ -84,7 +87,15 @@ class Locker(object):
package.description = info.get("description", "")
package.category = info["category"]
package.optional = info["optional"]
package.hashes = lock_data["metadata"]["hashes"][info["name"]]
if "hashes" in lock_data["metadata"]:
# Old lock so we create dummy files from the hashes
package.files = [
{"name": h, "hash": h}
for h in lock_data["metadata"]["hashes"][info["name"]]
]
else:
package.files = lock_data["metadata"]["files"][info["name"]]
package.python_versions = info["python-versions"]
extras = info.get("extras", {})
if extras:
......@@ -135,15 +146,24 @@ class Locker(object):
return packages
def set_lock_data(self, root, packages): # type: (...) -> bool
hashes = {}
files = table()
packages = self._lock_packages(packages)
# Retrieving hashes
for package in packages:
if package["name"] not in hashes:
hashes[package["name"]] = []
if package["name"] not in files:
files[package["name"]] = []
for f in package["files"]:
file_metadata = inline_table()
for k, v in sorted(f.items()):
file_metadata[k] = v
files[package["name"]].append(file_metadata)
hashes[package["name"]] += package["hashes"]
del package["hashes"]
if files[package["name"]]:
files[package["name"]] = item(files[package["name"]]).multiline(True)
del package["files"]
lock = document()
lock["package"] = packages
......@@ -157,7 +177,7 @@ class Locker(object):
lock["metadata"] = {
"python-versions": root.python_versions,
"content-hash": self._content_hash,
"hashes": hashes,
"files": files,
}
if not self.is_locked() or lock != self.lock_data:
......@@ -230,11 +250,16 @@ class Locker(object):
if not dependency.python_constraint.is_any():
constraint["python"] = str(dependency.python_constraint)
if len(constraint) == 1:
dependencies[dependency.pretty_name].append(constraint["version"])
else:
dependencies[dependency.pretty_name].append(constraint)
# All the constraints should have the same type,
# but we want to simplify them if it's possible
for dependency, constraints in tuple(dependencies.items()):
if all(len(constraint) == 1 for constraint in constraints):
dependencies[dependency] = [
constraint["version"] for constraint in constraints
]
data = {
"name": package.pretty_name,
"version": package.pretty_version,
......@@ -242,7 +267,7 @@ class Locker(object):
"category": package.category,
"optional": package.optional,
"python-versions": package.python_versions,
"hashes": sorted(package.hashes),
"files": sorted(package.files, key=lambda x: x["file"]),
}
if not package.marker.is_any():
data["marker"] = str(package.marker)
......
......@@ -27,7 +27,7 @@ AUTHOR_REGEX = re.compile(r"(?u)^(?P<name>[- .,\w\d'’\"()]+)(?: <(?P<email>.+?
class Package(object):
AVAILABLE_PYTHONS = {"2", "2.7", "3", "3.4", "3.5", "3.6", "3.7"}
AVAILABLE_PYTHONS = {"2", "2.7", "3", "3.4", "3.5", "3.6", "3.7", "3.8"}
def __init__(self, name, version, pretty_version=None):
"""
......@@ -66,7 +66,7 @@ class Package(object):
self.requires_extras = []
self.category = "main"
self.hashes = []
self.files = []
self.optional = False
self.classifiers = []
......
......@@ -38,6 +38,7 @@ from poetry.utils.helpers import safe_rmtree
from poetry.utils.helpers import temporary_directory
from poetry.utils.env import EnvManager
from poetry.utils.env import EnvCommandError
from poetry.utils.env import VirtualEnv
from poetry.utils.inspector import Inspector
from poetry.utils.setup_reader import SetupReader
from poetry.utils.toml_file import TomlFile
......@@ -172,9 +173,6 @@ class Provider:
name=dependency.name,
)
if dependency.tag or dependency.rev:
package.source_reference = dependency.reference
for extra in dependency.extras:
if extra in package.extras:
for dep in package.extras[extra]:
......@@ -229,7 +227,9 @@ class Provider:
)
package.source_url = dependency.path.as_posix()
package.hashes = [dependency.hash()]
package.files = [
{"file": dependency.path.name, "hash": "sha256:" + dependency.hash()}
]
for extra in dependency.extras:
if extra in package.extras:
......@@ -278,6 +278,9 @@ class Provider:
package.source_url = dependency.path.as_posix()
if dependency.base != None:
package.root_dir = dependency.base.as_posix()
for extra in dependency.extras:
if extra in package.extras:
for dep in package.extras[extra]:
......@@ -324,8 +327,9 @@ class Provider:
os.chdir(str(directory))
try:
cwd = directory
venv = EnvManager().get(cwd)
with temporary_directory() as tmp_dir:
EnvManager.build_venv(tmp_dir)
venv = VirtualEnv(Path(tmp_dir), Path(tmp_dir))
venv.run("python", "setup.py", "egg_info")
except EnvCommandError:
result = SetupReader.read_from_directory(directory)
......@@ -530,8 +534,8 @@ class Provider:
): # type: (DependencyPackage) -> DependencyPackage
if package.is_root():
package = package.clone()
if not package.is_root() and package.source_type not in {
requires = package.all_requires
elif not package.is_root() and package.source_type not in {
"directory",
"file",
"url",
......@@ -546,10 +550,13 @@ class Provider:
repository=package.dependency.source_name,
),
)
requires = package.requires
else:
requires = package.requires
dependencies = [
r
for r in package.requires
for r in requires
if self._package.python_constraint.allows_any(r.python_constraint)
]
......@@ -701,44 +708,42 @@ class Provider:
m2 = re.match(r"(.+?) \((.+?)\)", m.group(1))
if m2:
name = m2.group(1)
version = " (<comment>{}</comment>)".format(m2.group(2))
version = " (<b>{}</b>)".format(m2.group(2))
else:
name = m.group(1)
version = ""
message = (
"<fg=blue>fact</>: <info>{}</info>{} "
"depends on <info>{}</info> (<comment>{}</comment>)".format(
"<fg=blue>fact</>: <c1>{}</c1>{} "
"depends on <c1>{}</c1> (<b>{}</b>)".format(
name, version, m.group(2), m.group(3)
)
)
elif " is " in message:
message = re.sub(
"fact: (.+) is (.+)",
"<fg=blue>fact</>: <info>\\1</info> is <comment>\\2</comment>",
"<fg=blue>fact</>: <c1>\\1</c1> is <b>\\2</b>",
message,
)
else:
message = re.sub(
r"(?<=: )(.+?) \((.+?)\)",
"<info>\\1</info> (<comment>\\2</comment>)",
message,
r"(?<=: )(.+?) \((.+?)\)", "<c1>\\1</c1> (<b>\\2</b>)", message
)
message = "<fg=blue>fact</>: {}".format(message.split("fact: ")[1])
elif message.startswith("selecting "):
message = re.sub(
r"selecting (.+?) \((.+?)\)",
"<fg=blue>selecting</> <info>\\1</info> (<comment>\\2</comment>)",
"<fg=blue>selecting</> <c1>\\1</c1> (<b>\\2</b>)",
message,
)
elif message.startswith("derived:"):
m = re.match(r"derived: (.+?) \((.+?)\)$", message)
if m:
message = "<fg=blue>derived</>: <info>{}</info> (<comment>{}</comment>)".format(
message = "<fg=blue>derived</>: <c1>{}</c1> (<b>{}</b>)".format(
m.group(1), m.group(2)
)
else:
message = "<fg=blue>derived</>: <info>{}</info>".format(
message = "<fg=blue>derived</>: <c1>{}</c1>".format(
message.split("derived: ")[1]
)
elif message.startswith("conflict:"):
......@@ -747,14 +752,14 @@ class Provider:
m2 = re.match(r"(.+?) \((.+?)\)", m.group(1))
if m2:
name = m2.group(1)
version = " (<comment>{}</comment>)".format(m2.group(2))
version = " (<b>{}</b>)".format(m2.group(2))
else:
name = m.group(1)
version = ""
message = (
"<fg=red;options=bold>conflict</>: <info>{}</info>{} "
"depends on <info>{}</info> (<comment>{}</comment>)".format(
"<fg=red;options=bold>conflict</>: <c1>{}</c1>{} "
"depends on <c1>{}</c1> (<b>{}</b>)".format(
name, version, m.group(2), m.group(3)
)
)
......
......@@ -21,6 +21,11 @@ from typing import Generator
from typing import Optional
from typing import Union
try:
from urllib.parse import quote
except ImportError:
from urllib import quote
import requests
from cachecontrol import CacheControl
......@@ -155,8 +160,8 @@ class Page:
class LegacyRepository(PyPiRepository):
def __init__(
self, name, url, auth=None, disable_cache=False
): # type: (str, str, Optional[Auth], bool) -> None
self, name, url, auth=None, disable_cache=False, cert=None, client_cert=None
): # type: (str, str, Optional[Auth], bool, Optional[Path], Optional[Path]) -> None
if name == "pypi":
raise ValueError("The name [pypi] is reserved for repositories")
......@@ -164,6 +169,8 @@ class LegacyRepository(PyPiRepository):
self._name = name
self._url = url.rstrip("/")
self._auth = auth
self._client_cert = client_cert
self._cert = cert
self._inspector = Inspector()
self._cache_dir = Path(CACHE_DIR) / "cache" / "repositories" / name
self._cache = CacheManager(
......@@ -186,9 +193,23 @@ class LegacyRepository(PyPiRepository):
if not url_parts.username and self._auth:
self._session.auth = self._auth
if self._cert:
self._session.verify = str(self._cert)
if self._client_cert:
self._session.cert = str(self._client_cert)
self._disable_cache = disable_cache
@property
def cert(self): # type: () -> Optional[Path]
return self._cert
@property
def client_cert(self): # type: () -> Optional[Path]
return self._client_cert
@property
def authenticated_url(self): # type: () -> str
if not self._auth:
return self.url
......@@ -197,8 +218,8 @@ class LegacyRepository(PyPiRepository):
return "{scheme}://{username}:{password}@{netloc}{path}".format(
scheme=parsed.scheme,
username=self._auth.auth.username,
password=self._auth.auth.password,
username=quote(self._auth.auth.username),
password=quote(self._auth.auth.password),
netloc=parsed.netloc,
path=parsed.path,
)
......@@ -328,7 +349,7 @@ class LegacyRepository(PyPiRepository):
package.description = release_info.get("summary", "")
# Adding hashes information
package.hashes = release_info["digests"]
package.files = release_info["files"]
# Activate extra dependencies
for extra in extras:
......@@ -353,7 +374,7 @@ class LegacyRepository(PyPiRepository):
"summary": "",
"requires_dist": [],
"requires_python": None,
"digests": [],
"files": [],
"_cache_version": str(self.CACHE_VERSION),
}
......@@ -365,7 +386,7 @@ class LegacyRepository(PyPiRepository):
)
)
urls = defaultdict(list)
hashes = []
files = []
for link in links:
if link.is_wheel:
urls["bdist_wheel"].append(link.url)
......@@ -374,13 +395,12 @@ class LegacyRepository(PyPiRepository):
):
urls["sdist"].append(link.url)
hash = link.hash
if link.hash_name == "sha256":
hashes.append(hash)
elif hash:
hashes.append(link.hash_name + ":" + hash)
h = link.hash
if h:
h = link.hash_name + ":" + link.hash
files.append({"file": link.filename, "hash": h})
data["digests"] = hashes
data["files"] = files
info = self._get_info_from_urls(urls)
......
......@@ -50,7 +50,7 @@ logger = logging.getLogger(__name__)
class PyPiRepository(Repository):
CACHE_VERSION = parse_constraint("0.12.0")
CACHE_VERSION = parse_constraint("1.0.0b2")
def __init__(self, url="https://pypi.org/", disable_cache=False, fallback=True):
self._url = url
......@@ -210,7 +210,7 @@ class PyPiRepository(Repository):
package.platform = release_info["platform"]
# Adding hashes information
package.hashes = release_info["digests"]
package.files = release_info["files"]
# Activate extra dependencies
for extra in extras:
......@@ -311,7 +311,7 @@ class PyPiRepository(Repository):
"platform": info["platform"],
"requires_dist": info["requires_dist"],
"requires_python": info["requires_python"],
"digests": [],
"files": [],
"_cache_version": str(self.CACHE_VERSION),
}
......@@ -321,7 +321,12 @@ class PyPiRepository(Repository):
version_info = []
for file_info in version_info:
data["digests"].append(file_info["digests"]["sha256"])
data["files"].append(
{
"file": file_info["filename"],
"hash": "sha256:" + file_info["digests"]["sha256"],
}
)
if self._fallback and data["requires_dist"] is None:
self._log("No dependencies found, downloading archives", level="debug")
......
......@@ -64,7 +64,13 @@ class Repository(BaseRepository):
for package in self.packages:
if name == package.name:
if package.is_prerelease() and not allow_prereleases:
if (
package.is_prerelease()
and not allow_prereleases
and not package.source_type
):
# If prereleases are not allowed and the package is a prerelease
# and is a standard package then we skip it
continue
if constraint.allows(package.version):
......
......@@ -79,7 +79,7 @@ def parse_single_constraint(constraint): # type: (str) -> VersionConstraint
low = version
high = version.stable.next_major
else:
low = Version(version.major, version.minor, 0)
low = Version(version.major, version.minor, version.patch)
high = version.stable.next_minor
return VersionRange(
......
......@@ -228,7 +228,7 @@ class VersionUnion(VersionConstraint):
raise ValueError("Unknown VersionConstraint type {}".format(constraint))
def _excludes_single_version(self): # type: () -> bool
def excludes_single_version(self): # type: () -> bool
from .version import Version
from .version_range import VersionRange
......@@ -243,7 +243,7 @@ class VersionUnion(VersionConstraint):
def __str__(self):
from .version_range import VersionRange
if self._excludes_single_version():
if self.excludes_single_version():
return "!={}".format(VersionRange().difference(self))
return " || ".join([str(r) for r in self._ranges])
......
......@@ -107,9 +107,10 @@ class Exporter(object):
if package.source_type == "legacy" and package.source_url:
indexes.append(package.source_url)
if package.hashes and with_hashes:
if package.files and with_hashes:
hashes = []
for h in package.hashes:
for f in package.files:
h = f["hash"]
algorithm = "sha256"
if ":" in h:
algorithm, h = h.split(":")
......@@ -123,7 +124,7 @@ class Exporter(object):
line += " \\\n"
for i, h in enumerate(hashes):
line += " --hash={}{}".format(
h, " \\\n" if i < len(package.hashes) - 1 else ""
h, " \\\n" if i < len(hashes) - 1 else ""
)
line += "\n"
......
import collections
import os
import re
import shutil
import stat
import tempfile
try:
from collections.abc import Mapping
except ImportError:
from collections import Mapping
from contextlib import contextmanager
from typing import List
from typing import Optional
......@@ -14,6 +18,7 @@ from keyring.errors import KeyringError
from poetry.config.config import Config
from poetry.version import Version
from poetry.utils._compat import Path
_canonicalize_regex = re.compile("[-_]+")
......@@ -133,6 +138,22 @@ def get_http_basic_auth(
return None
def get_cert(config, repository_name): # type: (Config, str) -> Optional[Path]
cert = config.get("certificates.{}.cert".format(repository_name))
if cert:
return Path(cert)
else:
return None
def get_client_cert(config, repository_name): # type: (Config, str) -> Optional[Path]
client_cert = config.get("certificates.{}.client-cert".format(repository_name))
if client_cert:
return Path(client_cert)
else:
return None
def _on_rm_error(func, path, exc_info):
os.chmod(path, stat.S_IWRITE)
func(path)
......@@ -144,11 +165,7 @@ def safe_rmtree(path):
def merge_dicts(d1, d2):
for k, v in d2.items():
if (
k in d1
and isinstance(d1[k], dict)
and isinstance(d2[k], collections.Mapping)
):
if k in d1 and isinstance(d1[k], dict) and isinstance(d2[k], Mapping):
merge_dicts(d1[k], d2[k])
else:
d1[k] = d2[k]
......@@ -144,11 +144,11 @@ class Inspector:
pyproject = TomlFile(sdist_dir / "pyproject.toml")
if pyproject.exists():
from poetry.poetry import Poetry
from poetry.factory import Factory
pyproject_content = pyproject.read()
if "tool" in pyproject_content and "poetry" in pyproject_content["tool"]:
package = Poetry.create(sdist_dir).package
package = Factory().create_poetry(sdist_dir).package
return {
"name": package.name,
"version": package.version.text,
......
[tool.poetry]
name = "poetry"
version = "1.0.0b1"
version = "1.0.0b3"
description = "Python dependency management and packaging made easy."
authors = [
"Sébastien Eustace <sebastien@eustace.io>"
......@@ -23,18 +23,19 @@ classifiers = [
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.4"
cleo = "^0.7.5"
cleo = "^0.7.6"
clikit = "^0.4.0"
requests = "^2.18"
cachy = "^0.3.0"
requests-toolbelt = "^0.8.0"
jsonschema = "^3.0a3"
jsonschema = "^3.1"
pyrsistent = "^0.14.2"
pyparsing = "^2.2"
cachecontrol = { version = "^0.12.4", extras = ["filecache"] }
pkginfo = "^1.4"
html5lib = "^1.0"
shellingham = "^1.1"
tomlkit = "^0.5.5"
tomlkit = "^0.5.8"
pexpect = "^4.7.0"
# The typing module is not in the stdlib in Python 2.7 and 3.4
......
......@@ -28,6 +28,7 @@ class MakeReleaseCommand(Command):
"3.5": "python3.5",
"3.6": "python3.6",
"3.7": "python3.7",
"3.8": "python3.8",
}
def handle(self):
......@@ -46,7 +47,7 @@ class MakeReleaseCommand(Command):
self.check_system(pythons)
from poetry import __version__
from poetry.poetry import Poetry
from poetry.factory import Factory
from poetry.puzzle import Solver
from poetry.repositories.pool import Pool
from poetry.repositories.repository import Repository
......@@ -59,7 +60,7 @@ class MakeReleaseCommand(Command):
from poetry.utils.helpers import temporary_directory
from poetry.vcs import get_vcs
project = Poetry.create(Path.cwd())
project = Factory().create_poetry(Path.cwd())
package = project.package
del package.dev_requires[:]
......@@ -217,6 +218,7 @@ class MakeReleaseCommand(Command):
subprocess.check_output(
[python, "-V"], stderr=subprocess.STDOUT, shell=WINDOWS
)
subprocess.check_output([python, "-m", "pip", "install", "pip", "-U"])
except subprocess.CalledProcessError:
raise RuntimeError("Python {} is not available".format(version))
......
......@@ -81,7 +81,7 @@ def mock_clone(_, source, dest):
parts = urlparse.urlparse(source)
folder = (
Path(__file__).parent.parent
Path(__file__).parent
/ "fixtures"
/ "git"
/ parts.netloc
......@@ -91,7 +91,6 @@ def mock_clone(_, source, dest):
if dest.exists():
shutil.rmtree(str(dest))
shutil.rmtree(str(dest))
shutil.copytree(str(folder), str(dest))
......
......@@ -11,7 +11,7 @@ def test_none_activated(app, tmp_dir):
app.poetry.config.merge({"virtualenvs": {"path": str(tmp_dir)}})
venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent)
"simple-project", str(app.poetry.file.parent)
)
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
(Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir()
......@@ -34,7 +34,7 @@ def test_activated(app, tmp_dir):
app.poetry.config.merge({"virtualenvs": {"path": str(tmp_dir)}})
venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent)
"simple-project", str(app.poetry.file.parent)
)
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
(Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir()
......
......@@ -11,7 +11,7 @@ def test_remove_by_python_version(app, tmp_dir, mocker):
app.poetry.config.merge({"virtualenvs": {"path": str(tmp_dir)}})
venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent)
"simple-project", str(app.poetry.file.parent)
)
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
(Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir()
......@@ -39,7 +39,7 @@ def test_remove_by_name(app, tmp_dir):
app.poetry.config.merge({"virtualenvs": {"path": str(tmp_dir)}})
venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent)
"simple-project", str(app.poetry.file.parent)
)
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
(Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir()
......
......@@ -57,7 +57,7 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file(app, tmp_dir, m
tester.execute("3.7")
venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent)
"simple-project", str(app.poetry.file.parent)
)
m.assert_called_with(
......@@ -88,7 +88,7 @@ def test_get_prefers_explicitly_activated_virtualenvs_over_env_var(
os.environ["VIRTUAL_ENV"] = "/environment/prefix"
venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent)
"simple-project", str(app.poetry.file.parent)
)
current_python = sys.version_info[:3]
python_minor = ".".join(str(v) for v in current_python[:2])
......@@ -130,7 +130,7 @@ def test_get_prefers_explicitly_activated_non_existing_virtualenvs_over_env_var(
os.environ["VIRTUAL_ENV"] = "/environment/prefix"
venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent)
"simple-project", str(app.poetry.file.parent)
)
current_python = sys.version_info[:3]
python_minor = ".".join(str(v) for v in current_python[:2])
......
......@@ -99,3 +99,26 @@ def test_set_pypi_token(app, config, config_source, auth_config_source):
tester.execute("--list")
assert "mytoken" == auth_config_source.config["pypi-token"]["pypi"]
def test_set_client_cert(app, config_source, auth_config_source, mocker):
init = mocker.spy(ConfigSource, "__init__")
command = app.find("config")
tester = CommandTester(command)
tester.execute("certificates.foo.client-cert path/to/cert.pem")
assert (
"path/to/cert.pem"
== auth_config_source.config["certificates"]["foo"]["client-cert"]
)
def test_set_cert(app, config_source, auth_config_source, mocker):
init = mocker.spy(ConfigSource, "__init__")
command = app.find("config")
tester = CommandTester(command)
tester.execute("certificates.foo.cert path/to/ca.pem")
assert "path/to/ca.pem" == auth_config_source.config["certificates"]["foo"]["cert"]
from poetry.utils._compat import Path
def test_publish_returns_non_zero_code_for_upload_errors(app, app_tester, http):
http.register_uri(
http.POST, "https://upload.pypi.org/legacy/", status=400, body="Bad Request"
......@@ -16,3 +19,22 @@ HTTP Error 400: Bad Request
"""
assert app_tester.io.fetch_output() == expected
def test_publish_with_cert(app_tester, mocker):
publisher_publish = mocker.patch("poetry.masonry.publishing.Publisher.publish")
app_tester.execute("publish --cert path/to/ca.pem")
assert [
(None, None, None, Path("path/to/ca.pem"), None)
] == publisher_publish.call_args
def test_publish_with_client_cert(app_tester, mocker):
publisher_publish = mocker.patch("poetry.masonry.publishing.Publisher.publish")
app_tester.execute("publish --client-cert path/to/client.pem")
assert [
(None, None, None, None, Path("path/to/client.pem"))
] == publisher_publish.call_args
[tool.poetry]
name = "prerelease"
version = "1.0.0.dev0"
description = "Some description."
authors = [
"Sébastien Eustace <sebastien@eustace.io>"
]
license = "MIT"
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.4"
[tool.poetry.dev-dependencies]
......@@ -40,7 +40,7 @@ foo = ["C"]
python-versions = "*"
content-hash = "123456789"
[metadata.hashes]
[metadata.files]
A = []
B = []
C = []
......
......@@ -37,7 +37,7 @@ foo = ["D"]
python-versions = "*"
content-hash = "123456789"
[metadata.hashes]
[metadata.files]
A = []
B = []
C = []
......
......@@ -26,7 +26,7 @@ python-versions = "*"
python-versions = "*"
content-hash = "123456789"
[metadata.hashes]
[metadata.files]
"A" = []
"B" = []
"C" = []
......@@ -4,4 +4,4 @@ package = []
python-versions = "*"
content-hash = "123456789"
[metadata.hashes]
[metadata.files]
......@@ -10,5 +10,5 @@ python-versions = "*"
python-versions = "*"
content-hash = "123456789"
[metadata.hashes]
[metadata.files]
"A" = []
......@@ -10,5 +10,5 @@ python-versions = "*"
python-versions = "*"
content-hash = "123456789"
[metadata.hashes]
[metadata.files]
"A" = []
......@@ -42,7 +42,7 @@ python-versions = "*"
python-versions = "*"
content-hash = "123456789"
[metadata.hashes]
[metadata.files]
"A" = []
"B" = []
"C" = []
......
......@@ -21,6 +21,6 @@ A = "^1.0"
python-versions = "*"
content-hash = "123456789"
[metadata.hashes]
[metadata.files]
"A" = []
"B" = []
......@@ -24,5 +24,5 @@ python = ">=3.6,<4.0"
python-versions = "~2.7 || ^3.4"
content-hash = "123456789"
[metadata.hashes]
[metadata.files]
A = []
......@@ -32,7 +32,7 @@ python-versions = "*"
python-versions = "*"
content-hash = "123456789"
[metadata.hashes]
[metadata.files]
"A" = []
"B" = []
"C" = []
......@@ -18,6 +18,6 @@ python-versions = "*"
python-versions = "*"
content-hash = "123456789"
[metadata.hashes]
[metadata.files]
"A" = []
"B" = []
......@@ -35,6 +35,6 @@ url = "tests/fixtures/directory/project_with_transitive_directory_dependencies"
content-hash = "123456789"
python-versions = "*"
[metadata.hashes]
[metadata.files]
project-with-extras = []
project-with-transitive-directory-dependencies = []
......@@ -30,6 +30,6 @@ url = "tests/fixtures/project_with_extras"
content-hash = "123456789"
python-versions = "*"
[metadata.hashes]
[metadata.files]
project-with-extras = []
pendulum = []
......@@ -35,7 +35,7 @@ python-versions = "*"
python-versions = "*"
content-hash = "123456789"
[metadata.hashes]
[metadata.files]
cachy = []
my-package = []
pendulum = []
......@@ -32,7 +32,7 @@ python-versions = "*"
python-versions = "*"
content-hash = "123456789"
[metadata.hashes]
[metadata.files]
A = []
B = []
C = []
......@@ -58,7 +58,7 @@ python-versions = "*"
python-versions = "*"
content-hash = "123456789"
[metadata.hashes]
[metadata.files]
A = []
B = []
C = []
......@@ -46,7 +46,9 @@ url = "tests/fixtures/directory/project_with_transitive_file_dependencies"
content-hash = "123456789"
python-versions = "*"
[metadata.hashes]
demo = ["70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a"]
[metadata.files]
demo = [
{file = "demo-0.1.0-py2.py3-none-any.whl", hash = "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a"},
]
pendulum = []
project-with-transitive-file-dependencies = []
......@@ -30,6 +30,8 @@ python-versions = "*"
python-versions = "*"
content-hash = "123456789"
[metadata.hashes]
demo = ["70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a"]
[metadata.files]
demo = [
{file = "demo-0.1.0-py2.py3-none-any.whl", hash = "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a"},
]
pendulum = []
......@@ -51,7 +51,7 @@ python-versions = "*"
python-versions = "~2.7 || ^3.4"
content-hash = "123456789"
[metadata.hashes]
[metadata.files]
A = []
B = []
C = []
......@@ -34,7 +34,7 @@ foo = ["A"]
python-versions = "~2.7 || ^3.4"
content-hash = "123456789"
[metadata.hashes]
[metadata.files]
A = []
C = []
D = []
......@@ -43,7 +43,7 @@ foo = ["A"]
python-versions = "*"
content-hash = "123456789"
[metadata.hashes]
[metadata.files]
A = []
B = []
C = []
......
......@@ -18,6 +18,6 @@ python-versions = "*"
python-versions = "*"
content-hash = "123456789"
[metadata.hashes]
[metadata.files]
"A" = []
"B" = []
......@@ -86,12 +86,36 @@ python-versions = "*"
python-versions = "*"
content-hash = "123456789"
[metadata.hashes]
attrs = ["1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9", "a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450"]
colorama = ["463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda", "48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"]
funcsigs = ["330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca", "a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"]
more-itertools = ["0dd8f72eeab0d2c3bd489025bb2f6a1b8342f9b198f6fc37b52d15cfa4531fea", "11a625025954c20145b37ff6309cd54e39ca94f72f6bb9576d1195db6fa2442e", "c9ce7eccdcb901a2c75d326ea134e0886abfbea5f93e91cc95de9507c0816c44"]
pluggy = ["7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff"]
py = ["29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881", "983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a"]
pytest = ["6266f87ab64692112e5477eba395cfedda53b1933ccd29478e671e73b420c19c", "fae491d1874f199537fd5872b5e1f0e74a009b979df9d53d1553fd03da1703e1"]
six = ["70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", "832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"]
[metadata.files]
attrs = [
{file = "attrs-17.4.0-py2.py3-none-any.whl", hash = "sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450"},
{file = "attrs-17.4.0.tar.gz", hash = "sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9"},
]
colorama = [
{file = "colorama-0.3.9-py2.py3-none-any.whl", hash = "sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda"},
{file = "colorama-0.3.9.tar.gz", hash = "sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"},
]
funcsigs = [
{file = "funcsigs-1.0.2-py2.py3-none-any.whl", hash = "sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca"},
{file = "funcsigs-1.0.2.tar.gz", hash = "sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"},
]
more-itertools = [
{file = "more-itertools-4.1.0.tar.gz", hash = "sha256:c9ce7eccdcb901a2c75d326ea134e0886abfbea5f93e91cc95de9507c0816c44"},
{file = "more_itertools-4.1.0-py2-none-any.whl", hash = "sha256:11a625025954c20145b37ff6309cd54e39ca94f72f6bb9576d1195db6fa2442e"},
{file = "more_itertools-4.1.0-py3-none-any.whl", hash = "sha256:0dd8f72eeab0d2c3bd489025bb2f6a1b8342f9b198f6fc37b52d15cfa4531fea"},
]
pluggy = [
{file = "pluggy-0.6.0.tar.gz", hash = "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff"},
]
py = [
{file = "py-1.5.3-py2.py3-none-any.whl", hash = "sha256:983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a"},
{file = "py-1.5.3.tar.gz", hash = "sha256:29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881"},
]
pytest = [
{file = "pytest-3.5.0-py2.py3-none-any.whl", hash = "sha256:6266f87ab64692112e5477eba395cfedda53b1933ccd29478e671e73b420c19c"},
{file = "pytest-3.5.0.tar.gz", hash = "sha256:fae491d1874f199537fd5872b5e1f0e74a009b979df9d53d1553fd03da1703e1"},
]
six = [
{file = "six-1.11.0-py2.py3-none-any.whl", hash = "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"},
{file = "six-1.11.0.tar.gz", hash = "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9"},
]
......@@ -26,7 +26,7 @@ python-versions = "~2.7 || ^3.3"
python-versions = "~2.7 || ^3.4"
content-hash = "123456789"
[metadata.hashes]
[metadata.files]
A = []
B = []
C = []
......@@ -40,7 +40,7 @@ python-versions = "*"
python-versions = "*"
content-hash = "123456789"
[metadata.hashes]
[metadata.files]
A = []
B = []
C = []
......
......@@ -30,6 +30,6 @@ python-versions = "*"
python-versions = "*"
content-hash = "123456789"
[metadata.hashes]
[metadata.files]
demo = []
pendulum = []
......@@ -15,5 +15,7 @@ url = "tests/fixtures/wheel_with_no_requires_dist/demo-0.1.0-py2.py3-none-any.wh
python-versions = "*"
content-hash = "123456789"
[metadata.hashes]
demo = ["c25eb81459126848a1788eb3520d1a32014eb51ce3d3bae88c56bfdde4ce02db"]
[metadata.files]
demo = [
{file = "demo-0.1.0-py2.py3-none-any.whl", hash = "sha256:c25eb81459126848a1788eb3520d1a32014eb51ce3d3bae88c56bfdde4ce02db"},
]
......@@ -1529,3 +1529,31 @@ def test_run_installs_with_url_file(installer, locker, repo, package):
assert locker.written_data == expected
assert len(installer.installer.installs) == 2
def test_installer_uses_prereleases_if_they_are_compatible(
installer, locker, package, repo
):
package.python_versions = "~2.7 || ^3.4"
package.add_dependency(
"prerelease", {"git": "https://github.com/demo/prerelease.git"}
)
package_b = get_package("b", "2.0.0")
package_b.add_dependency("prerelease", ">=0.19")
repo.add_package(package_b)
installer.run()
del installer.installer.installs[:]
locker.locked(True)
locker.mock_lock_data(locker.written_data)
package.add_dependency("b", "^2.0.0")
installer.whitelist(["b"])
installer.update(True)
installer.run()
assert len(installer.installer.installs) == 2
......@@ -5,6 +5,7 @@ from poetry.io.null_io import NullIO
from poetry.packages.package import Package
from poetry.repositories.legacy_repository import LegacyRepository
from poetry.repositories.pool import Pool
from poetry.utils._compat import Path
from poetry.utils.env import NullEnv
......@@ -29,9 +30,12 @@ def installer(pool):
def test_requirement(installer):
package = Package("ipython", "7.5.0")
package.hashes = [
"md5:dbdc53e3918f28fa335a173432402a00",
"e840810029224b56cd0d9e7719dc3b39cf84d577f8ac686547c8ba7a06eeab26",
package.files = [
{"file": "foo-0.1.0.tar.gz", "hash": "md5:dbdc53e3918f28fa335a173432402a00"},
{
"file": "foo.0.1.0.whl",
"hash": "e840810029224b56cd0d9e7719dc3b39cf84d577f8ac686547c8ba7a06eeab26",
},
]
result = installer.requirement(package, formatted=True)
......@@ -86,6 +90,62 @@ def test_install_with_non_pypi_default_repository(pool, installer):
installer.install(bar)
def test_install_with_cert():
ca_path = "path/to/cert.pem"
pool = Pool()
default = LegacyRepository("default", "https://foo.bar", cert=Path(ca_path))
pool.add_repository(default, default=True)
null_env = NullEnv()
installer = PipInstaller(null_env, NullIO(), pool)
foo = Package("foo", "0.0.0")
foo.source_type = "legacy"
foo.source_reference = default._name
foo.source_url = default._url
installer.install(foo)
assert len(null_env.executed) == 1
cmd = null_env.executed[0]
assert "--cert" in cmd
cert_index = cmd.index("--cert")
# Need to do the str(Path()) bit because Windows paths get modified by Path
assert cmd[cert_index + 1] == str(Path(ca_path))
def test_install_with_client_cert():
client_path = "path/to/client.pem"
pool = Pool()
default = LegacyRepository(
"default", "https://foo.bar", client_cert=Path(client_path)
)
pool.add_repository(default, default=True)
null_env = NullEnv()
installer = PipInstaller(null_env, NullIO(), pool)
foo = Package("foo", "0.0.0")
foo.source_type = "legacy"
foo.source_reference = default._name
foo.source_url = default._url
installer.install(foo)
assert len(null_env.executed) == 1
cmd = null_env.executed[0]
assert "--client-cert" in cmd
cert_index = cmd.index("--client-cert")
# Need to do the str(Path()) bit because Windows paths get modified by Path
assert cmd[cert_index + 1] == str(Path(client_path))
def test_requirement_git_develop_true(installer, package_git):
package_git.develop = True
result = installer.requirement(package_git)
......
......@@ -88,6 +88,7 @@ def test_get_metadata_content():
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Libraries :: Python Modules",
]
......
......@@ -79,7 +79,7 @@ def test_wheel_c_extension():
Wheel-Version: 1.0
Generator: poetry {}
Root-Is-Purelib: false
Tag: cp[23]\\d-cp[23]\\dmu?-.+
Tag: cp[23]\\d-cp[23]\\dm?u?-.+
$""".format(
__version__
),
......@@ -136,7 +136,7 @@ def test_wheel_c_extension_src_layout():
Wheel-Version: 1.0
Generator: poetry {}
Root-Is-Purelib: false
Tag: cp[23]\\d-cp[23]\\dmu?-.+
Tag: cp[23]\\d-cp[23]\\dm?u?-.+
$""".format(
__version__
),
......@@ -216,6 +216,7 @@ Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Provides-Extra: time
......@@ -318,6 +319,7 @@ Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Provides-Extra: time
......
......@@ -7,6 +7,7 @@ from clikit.io import NullIO
from poetry.factory import Factory
from poetry.masonry.builders import WheelBuilder
from poetry.masonry.publishing.uploader import Uploader
from poetry.utils._compat import Path
from poetry.utils.env import NullEnv
......@@ -99,7 +100,8 @@ def test_wheel_excluded_nested_data():
def test_wheel_localversionlabel():
module_path = fixtures_dir / "localversionlabel"
WheelBuilder.make(Factory().create_poetry(module_path), NullEnv(), NullIO())
project = Factory().create_poetry(module_path)
WheelBuilder.make(project, NullEnv(), NullIO())
local_version_string = "localversionlabel-0.1b1+gitbranch.buildno.1"
whl = module_path / "dist" / (local_version_string + "-py2.py3-none-any.whl")
......@@ -108,6 +110,9 @@ def test_wheel_localversionlabel():
with zipfile.ZipFile(str(whl)) as z:
assert local_version_string + ".dist-info/METADATA" in z.namelist()
uploader = Uploader(project, NullIO())
assert whl in uploader.files
def test_wheel_package_src():
module_path = fixtures_dir / "source_package"
......
......@@ -3,6 +3,7 @@ import pytest
from poetry.factory import Factory
from poetry.io.null_io import NullIO
from poetry.masonry.publishing.publisher import Publisher
from poetry.utils._compat import Path
def test_publish_publishes_to_pypi_by_default(fixture_dir, mocker, config):
......@@ -18,7 +19,10 @@ def test_publish_publishes_to_pypi_by_default(fixture_dir, mocker, config):
publisher.publish(None, None, None)
assert [("foo", "bar")] == uploader_auth.call_args
assert [("https://upload.pypi.org/legacy/",)] == uploader_upload.call_args
assert [
("https://upload.pypi.org/legacy/",),
{"cert": None, "client_cert": None},
] == uploader_upload.call_args
def test_publish_can_publish_to_given_repository(fixture_dir, mocker, config):
......@@ -37,7 +41,10 @@ def test_publish_can_publish_to_given_repository(fixture_dir, mocker, config):
publisher.publish("my-repo", None, None)
assert [("foo", "bar")] == uploader_auth.call_args
assert [("http://foo.bar",)] == uploader_upload.call_args
assert [
("http://foo.bar",),
{"cert": None, "client_cert": None},
] == uploader_upload.call_args
def test_publish_raises_error_for_undefined_repository(fixture_dir, mocker, config):
......@@ -63,4 +70,52 @@ def test_publish_uses_token_if_it_exists(fixture_dir, mocker, config):
publisher.publish(None, None, None)
assert [("__token__", "my-token")] == uploader_auth.call_args
assert [("https://upload.pypi.org/legacy/",)] == uploader_upload.call_args
assert [
("https://upload.pypi.org/legacy/",),
{"cert": None, "client_cert": None},
] == uploader_upload.call_args
def test_publish_uses_cert(fixture_dir, mocker, config):
cert = "path/to/ca.pem"
uploader_auth = mocker.patch("poetry.masonry.publishing.uploader.Uploader.auth")
uploader_upload = mocker.patch("poetry.masonry.publishing.uploader.Uploader.upload")
poetry = Factory().create_poetry(fixture_dir("sample_project"))
poetry._config = config
poetry.config.merge(
{
"repositories": {"foo": {"url": "https://foo.bar"}},
"http-basic": {"foo": {"username": "foo", "password": "bar"}},
"certificates": {"foo": {"cert": cert}},
}
)
publisher = Publisher(poetry, NullIO())
publisher.publish("foo", None, None)
assert [("foo", "bar")] == uploader_auth.call_args
assert [
("https://foo.bar",),
{"cert": Path(cert), "client_cert": None},
] == uploader_upload.call_args
def test_publish_uses_client_cert(fixture_dir, mocker, config):
client_cert = "path/to/client.pem"
uploader_upload = mocker.patch("poetry.masonry.publishing.uploader.Uploader.upload")
poetry = Factory().create_poetry(fixture_dir("sample_project"))
poetry._config = config
poetry.config.merge(
{
"repositories": {"foo": {"url": "https://foo.bar"}},
"certificates": {"foo": {"client-cert": client_cert}},
}
)
publisher = Publisher(poetry, NullIO())
publisher.publish("foo", None, None)
assert [
("https://foo.bar",),
{"cert": None, "client_cert": Path(client_cert)},
] == uploader_upload.call_args
......@@ -94,6 +94,7 @@ Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Provides-Extra: time
......
......@@ -102,3 +102,9 @@ def test_to_pep_508_in_extras():
") "
'and (extra == "foo" or extra == "bar")'
)
def test_to_pep_508_with_single_version_excluded():
dependency = Dependency("foo", "!=1.2.3")
assert "foo (!=1.2.3)" == dependency.to_pep_508()
......@@ -6,6 +6,7 @@ import tomlkit
from poetry.packages.locker import Locker
from poetry.packages.project_package import ProjectPackage
from ..helpers import get_dependency
from ..helpers import get_package
......@@ -26,7 +27,7 @@ def root():
def test_lock_file_data_is_ordered(locker, root):
package_a = get_package("A", "1.0.0")
package_a.add_dependency("B", "^1.0")
package_a.hashes = ["456", "123"]
package_a.files = [{"file": "foo", "hash": "456"}, {"file": "bar", "hash": "123"}]
packages = [package_a, get_package("B", "1.2")]
locker.set_lock_data(root, packages)
......@@ -57,8 +58,11 @@ version = "1.2"
content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8"
python-versions = "*"
[metadata.hashes]
A = ["123", "456"]
[metadata.files]
A = [
{file = "bar", hash = "123"},
{file = "foo", hash = "456"},
]
B = []
"""
......@@ -91,7 +95,7 @@ redis = ["redis (>=2.10.5)"]
content-hash = "c3d07fca33fba542ef2b2a4d75bf5b48d892d21a830e2ad9c952ba5123a52f77"
python-versions = "~2.7 || ^3.4"
[metadata.hashes]
[metadata.files]
cachecontrol = []
"""
......@@ -130,8 +134,50 @@ version = "1.0.0"
content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8"
python-versions = "*"
[metadata.hashes]
[metadata.files]
A = []
"""
assert expected == content
def test_lock_file_should_not_have_mixed_types(locker, root):
package_a = get_package("A", "1.0.0")
package_a.add_dependency("B", "^1.0.0")
package_a.add_dependency("B", {"version": ">=1.0.0", "optional": True})
package_a.requires[-1].activate()
package_a.extras["foo"] = [get_dependency("B", ">=1.0.0")]
locker.set_lock_data(root, [package_a])
expected = """[[package]]
category = "main"
description = ""
name = "A"
optional = false
python-versions = "*"
version = "1.0.0"
[package.dependencies]
[[package.dependencies.B]]
version = "^1.0.0"
[[package.dependencies.B]]
optional = true
version = ">=1.0.0"
[package.extras]
foo = ["B (>=1.0.0)"]
[metadata]
content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8"
python-versions = "*"
[metadata.files]
A = []
"""
with locker.lock.open(encoding="utf-8") as f:
content = f.read()
assert expected == content
......@@ -118,7 +118,10 @@ def test_search_for_vcs_read_setup_with_extras(provider, mocker):
def test_search_for_vcs_read_setup_raises_error_if_no_version(provider, mocker):
mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv())
mocker.patch(
"poetry.utils.env.VirtualEnv.run",
side_effect=EnvCommandError(CalledProcessError(1, "python", output="")),
)
dependency = VCSDependency("demo", "git", "https://github.com/demo/no-version.git")
......@@ -175,6 +178,46 @@ def test_search_for_directory_setup_egg_info_with_extras(provider):
}
@pytest.mark.parametrize("directory", ["demo", "non-canonical-name"])
def test_search_for_directory_setup_with_base(provider, directory):
dependency = DirectoryDependency(
"demo",
Path(__file__).parent.parent
/ "fixtures"
/ "git"
/ "github.com"
/ "demo"
/ directory,
base=Path(__file__).parent.parent
/ "fixtures"
/ "git"
/ "github.com"
/ "demo"
/ directory,
)
package = provider.search_for_directory(dependency)[0]
assert package.name == "demo"
assert package.version.text == "0.1.2"
assert package.requires == [get_dependency("pendulum", ">=1.4.4")]
assert package.extras == {
"foo": [get_dependency("cleo")],
"bar": [get_dependency("tomlkit")],
}
assert (
package.root_dir
== (
Path(__file__).parent.parent
/ "fixtures"
/ "git"
/ "github.com"
/ "demo"
/ directory
).as_posix()
)
@pytest.mark.skipif(not PY35, reason="AST parsing does not work for Python <3.4")
def test_search_for_directory_setup_read_setup(provider, mocker):
mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv())
......
......@@ -928,6 +928,11 @@ def test_solver_can_resolve_git_dependencies(solver, repo, package):
],
)
op = ops[1]
assert op.package.source_type == "git"
assert op.package.source_reference.startswith("9cf87a2")
def test_solver_can_resolve_git_dependencies_with_extras(solver, repo, package):
pendulum = get_package("pendulum", "2.0.3")
......@@ -951,6 +956,37 @@ def test_solver_can_resolve_git_dependencies_with_extras(solver, repo, package):
)
@pytest.mark.parametrize(
"ref",
[{"branch": "a-branch"}, {"tag": "a-tag"}, {"rev": "9cf8"}],
ids=["branch", "tag", "rev"],
)
def test_solver_can_resolve_git_dependencies_with_ref(solver, repo, package, ref):
pendulum = get_package("pendulum", "2.0.3")
cleo = get_package("cleo", "1.0.0")
repo.add_package(pendulum)
repo.add_package(cleo)
git_config = {"git": "https://github.com/demo/demo.git"}
git_config.update(ref)
package.add_dependency("demo", git_config)
ops = solver.solve()
check_solver_result(
ops,
[
{"job": "install", "package": pendulum},
{"job": "install", "package": get_package("demo", "0.1.2")},
],
)
op = ops[1]
assert op.package.source_type == "git"
assert op.package.source_reference.startswith("9cf87a2")
def test_solver_does_not_trigger_conflict_for_python_constraint_if_python_requirement_is_compatible(
solver, repo, package
):
......@@ -1775,3 +1811,27 @@ def test_solver_discards_packages_with_empty_markers(
{"job": "install", "package": package_a},
],
)
def test_solver_does_not_raise_conflict_for_conditional_dev_dependencies(
solver, repo, package
):
package.python_versions = "~2.7 || ^3.5"
package.add_dependency("A", {"version": "^1.0", "python": "~2.7"}, category="dev")
package.add_dependency("A", {"version": "^2.0", "python": "^3.5"}, category="dev")
package_a100 = get_package("A", "1.0.0")
package_a200 = get_package("A", "2.0.0")
repo.add_package(package_a100)
repo.add_package(package_a200)
ops = solver.solve()
check_solver_result(
ops,
[
{"job": "install", "package": package_a100},
{"job": "install", "package": package_a200},
],
)
......@@ -7,6 +7,7 @@ except ImportError:
import urlparse
from poetry.packages import Dependency
from poetry.repositories.auth import Auth
from poetry.repositories.exceptions import PackageNotFound
from poetry.repositories.legacy_repository import LegacyRepository
from poetry.repositories.legacy_repository import Page
......@@ -18,9 +19,9 @@ class MockRepository(LegacyRepository):
FIXTURES = Path(__file__).parent / "fixtures" / "legacy"
def __init__(self):
def __init__(self, auth=None):
super(MockRepository, self).__init__(
"legacy", url="http://foo.bar", disable_cache=True
"legacy", url="http://foo.bar", auth=auth, disable_cache=True
)
def _get(self, endpoint):
......@@ -242,11 +243,17 @@ def test_get_package_retrieves_non_sha256_hashes():
package = repo.package("ipython", "7.5.0")
expected = [
"md5:dbdc53e3918f28fa335a173432402a00",
"e840810029224b56cd0d9e7719dc3b39cf84d577f8ac686547c8ba7a06eeab26",
{
"file": "ipython-7.5.0-py3-none-any.whl",
"hash": "md5:dbdc53e3918f28fa335a173432402a00",
},
{
"file": "ipython-7.5.0.tar.gz",
"hash": "sha256:e840810029224b56cd0d9e7719dc3b39cf84d577f8ac686547c8ba7a06eeab26",
},
]
assert expected == package.hashes
assert expected == package.files
def test_get_package_retrieves_packages_with_no_hashes():
......@@ -254,4 +261,11 @@ def test_get_package_retrieves_packages_with_no_hashes():
package = repo.package("jupyter", "1.0.0")
assert [] == package.hashes
assert [] == package.files
def test_username_password_special_chars():
auth = Auth("http://foo.bar", "user:", "p@ssword")
repo = MockRepository(auth=auth)
assert "http://user%3A:p%40ssword@foo.bar" == repo.authenticated_url
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