Commit be09d9c6 by Sébastien Eustace

Merge branch 'develop' into merge-develop-into-master

# Conflicts:
#	.github/workflows/release.yml
#	CHANGELOG.md
#	Makefile
#	make-linux-release.sh
#	poetry.lock
#	poetry/__version__.py
#	poetry/packages/locker.py
#	pyproject.toml
#	tests/packages/test_locker.py
parents 037c5a50 32c87c9d
freebsd_instance:
image_family: freebsd-12-1-snap
test_task:
name: "Tests / FreeBSD / "
env:
matrix:
- PYTHON: python2.7
- PYTHON: python3.5
- PYTHON: python3.6
- PYTHON: python3.7
- PYTHON: python3.8
python_script:
- PYPACKAGE=$(printf '%s' $PYTHON | tr -d '.')
- SQLPACKAGE=$(printf '%s-sqlite3' $PYPACKAGE | sed 's/thon//')
- pkg install -y git-lite $PYPACKAGE $SQLPACKAGE
pip_script:
- $PYTHON -m ensurepip
- $PYTHON -m pip install -U pip tox
- $PYTHON -m pip install -U --pre poetry
- poetry config virtualenvs.in-project true
tox_script: $PYTHON -m tox -e py -- -q --junitxml=junit.xml tests
on_failure:
annotate_failure_artifacts:
path: junit.xml
format: junit
type: text/xml
release_task:
name: "Release / FreeBSD"
only_if: $CIRRUS_TAG != ''
freebsd_instance:
matrix:
- image_family: freebsd-12-1-snap
- image_family: freebsd-11-3-snap
python_script: pkg install -y python3 python27 python35 python36 python37 python38
pip_script:
- python2.7 -m ensurepip
- python3.5 -m ensurepip
- python3.6 -m ensurepip
- python3.7 -m ensurepip
- python3.8 -m ensurepip
build_script: ./make-nix-release.sh
archive_artifacts:
path: "releases/*"
...@@ -16,6 +16,7 @@ exclude = ...@@ -16,6 +16,7 @@ exclude =
.vscode .vscode
.github .github
poetry/utils/_compat.py poetry/utils/_compat.py
poetry/utils/env_scripts/tags.py
tests/fixtures/ tests/fixtures/
tests/repositories/fixtures/ tests/repositories/fixtures/
tests/utils/fixtures/ tests/utils/fixtures/
name: Tests name: Tests
on: [push, pull_request] on:
push:
branches:
- master
- develop
pull_request:
branches:
- '**'
jobs: jobs:
Linting: Linting:
...@@ -41,7 +48,7 @@ jobs: ...@@ -41,7 +48,7 @@ jobs:
- name: Install poetry - name: Install poetry
shell: bash shell: bash
run: | run: |
python get-poetry.py -y python get-poetry.py -y --preview
echo "::set-env name=PATH::$HOME/.poetry/bin:$PATH" echo "::set-env name=PATH::$HOME/.poetry/bin:$PATH"
- name: Configure poetry - name: Configure poetry
...@@ -60,10 +67,14 @@ jobs: ...@@ -60,10 +67,14 @@ jobs:
shell: bash shell: bash
run: poetry run pip --version >/dev/null 2>&1 || rm -rf .venv run: poetry run pip --version >/dev/null 2>&1 || rm -rf .venv
- name: Upgrade pip
shell: bash
run: poetry run python -m pip install pip -U
- name: Install dependencies - name: Install dependencies
shell: bash shell: bash
run: poetry install run: poetry install
- name: Run pytest - name: Run pytest
shell: bash shell: bash
run: poetry run pytest -q tests run: poetry run pytest -v tests
...@@ -54,14 +54,12 @@ jobs: ...@@ -54,14 +54,12 @@ jobs:
- name: Preparing Python executables - name: Preparing Python executables
run: | run: |
curl -L https://github.com/sdispater/python-binaries/releases/download/2.7.17/python-2.7.17.macos.tar.xz -o python-2.7.17.tar.xz curl -L https://github.com/sdispater/python-binaries/releases/download/2.7.17/python-2.7.17.macos.tar.xz -o python-2.7.17.tar.xz
curl -L https://github.com/sdispater/python-binaries/releases/download/3.4.10/python-3.4.10.macos.tar.xz -o python-3.4.10.tar.xz
curl -L https://github.com/sdispater/python-binaries/releases/download/3.5.9/python-3.5.9.macos.tar.xz -o python-3.5.9.tar.xz curl -L https://github.com/sdispater/python-binaries/releases/download/3.5.9/python-3.5.9.macos.tar.xz -o python-3.5.9.tar.xz
curl -L https://github.com/sdispater/python-binaries/releases/download/3.6.8/python-3.6.8.macos.tar.xz -o python-3.6.8.tar.xz curl -L https://github.com/sdispater/python-binaries/releases/download/3.6.8/python-3.6.8.macos.tar.xz -o python-3.6.8.tar.xz
curl -L https://github.com/sdispater/python-binaries/releases/download/3.7.6/python-3.7.6.macos.tar.xz -o python-3.7.6.tar.xz curl -L https://github.com/sdispater/python-binaries/releases/download/3.7.6/python-3.7.6.macos.tar.xz -o python-3.7.6.tar.xz
curl -L https://github.com/sdispater/python-binaries/releases/download/3.8.3/python-3.8.3.macos.tar.xz -o python-3.8.3.tar.xz curl -L https://github.com/sdispater/python-binaries/releases/download/3.8.3/python-3.8.3.macos.tar.xz -o python-3.8.3.tar.xz
curl -L https://github.com/sdispater/python-binaries/releases/download/3.9.0b4/python-3.9.0b4.macos.tar.xz -o python-3.9.0b4.tar.xz curl -L https://github.com/sdispater/python-binaries/releases/download/3.9.0b4/python-3.9.0b4.macos.tar.xz -o python-3.9.0b4.tar.xz
tar -zxf python-2.7.17.tar.xz tar -zxf python-2.7.17.tar.xz
tar -zxf python-3.4.10.tar.xz
tar -zxf python-3.5.9.tar.xz tar -zxf python-3.5.9.tar.xz
tar -zxf python-3.6.8.tar.xz tar -zxf python-3.6.8.tar.xz
tar -zxf python-3.7.6.tar.xz tar -zxf python-3.7.6.tar.xz
...@@ -70,7 +68,7 @@ jobs: ...@@ -70,7 +68,7 @@ jobs:
- name: Build specific release - name: Build specific release
run: | run: |
source $HOME/.poetry/env source $HOME/.poetry/env
poetry run python sonnet make release --ansi -P "2.7:python-2.7.17/bin/python" -P "3.4:python-3.4.10/bin/python" -P "3.5:python-3.5.9/bin/python" -P "3.6:python-3.6.8/bin/python" -P "3.7:python-3.7.6/bin/python" -P "3.8:python-3.8.3/bin/python" -P "3.9:python-3.9.0b4/bin/python" poetry run python sonnet make release --ansi -P "2.7:python-2.7.17/bin/python" -P "3.5:python-3.5.9/bin/python" -P "3.6:python-3.6.8/bin/python" -P "3.7:python-3.7.6/bin/python" -P "3.8:python-3.8.3/bin/python" -P "3.9:python-3.9.0b4/bin/python"
- name: Upload release file - name: Upload release file
uses: actions/upload-artifact@v1 uses: actions/upload-artifact@v1
with: with:
...@@ -107,14 +105,12 @@ jobs: ...@@ -107,14 +105,12 @@ jobs:
- name: Preparing Python executables - name: Preparing Python executables
run: | run: |
Invoke-WebRequest https://github.com/sdispater/python-binaries/releases/download/2.7.17/python-2.7.17.windows.tar.xz -O python-2.7.17.tar.xz Invoke-WebRequest https://github.com/sdispater/python-binaries/releases/download/2.7.17/python-2.7.17.windows.tar.xz -O python-2.7.17.tar.xz
Invoke-WebRequest https://github.com/sdispater/python-binaries/releases/download/3.4.4/python-3.4.4.windows.tar.xz -O python-3.4.4.tar.xz
Invoke-WebRequest https://github.com/sdispater/python-binaries/releases/download/3.5.4/python-3.5.4.windows.tar.xz -O python-3.5.4.tar.xz Invoke-WebRequest https://github.com/sdispater/python-binaries/releases/download/3.5.4/python-3.5.4.windows.tar.xz -O python-3.5.4.tar.xz
Invoke-WebRequest https://github.com/sdispater/python-binaries/releases/download/3.6.8/python-3.6.8.windows.tar.xz -O python-3.6.8.tar.xz Invoke-WebRequest https://github.com/sdispater/python-binaries/releases/download/3.6.8/python-3.6.8.windows.tar.xz -O python-3.6.8.tar.xz
Invoke-WebRequest https://github.com/sdispater/python-binaries/releases/download/3.7.6/python-3.7.6.windows.tar.xz -O python-3.7.6.tar.xz Invoke-WebRequest https://github.com/sdispater/python-binaries/releases/download/3.7.6/python-3.7.6.windows.tar.xz -O python-3.7.6.tar.xz
Invoke-WebRequest https://github.com/sdispater/python-binaries/releases/download/3.8.3/python-3.8.3.windows.tar.xz -O python-3.8.3.tar.xz Invoke-WebRequest https://github.com/sdispater/python-binaries/releases/download/3.8.3/python-3.8.3.windows.tar.xz -O python-3.8.3.tar.xz
Invoke-WebRequest https://github.com/sdispater/python-binaries/releases/download/3.9.0b4/python-3.9.0b4.windows.tar.xz -O python-3.9.0b4.tar.xz Invoke-WebRequest https://github.com/sdispater/python-binaries/releases/download/3.9.0b4/python-3.9.0b4.windows.tar.xz -O python-3.9.0b4.tar.xz
7z x python-2.7.17.tar.xz 7z x python-2.7.17.tar.xz
7z x python-3.4.4.tar.xz
7z x python-3.5.4.tar.xz 7z x python-3.5.4.tar.xz
7z x python-3.6.8.tar.xz 7z x python-3.6.8.tar.xz
7z x python-3.7.6.tar.xz 7z x python-3.7.6.tar.xz
...@@ -130,7 +126,7 @@ jobs: ...@@ -130,7 +126,7 @@ jobs:
- name: Build specific release - name: Build specific release
run: | run: |
$env:Path += ";$env:Userprofile\.poetry\bin" $env:Path += ";$env:Userprofile\.poetry\bin"
poetry run python sonnet make release --ansi -P "2.7:python-2.7.17\python.exe" -P "3.4:python-3.4.4\python.exe" -P "3.5:python-3.5.4\python.exe" -P "3.6:python-3.6.8\python.exe" -P "3.7:python-3.7.6\python.exe" -P "3.8:python-3.8.3\python.exe" -P "3.9:python-3.9.0b4\python.exe" poetry run python sonnet make release --ansi -P "2.7:python-2.7.17\python.exe" -P "3.5:python-3.5.4\python.exe" -P "3.6:python-3.6.8\python.exe" -P "3.7:python-3.7.6\python.exe" -P "3.8:python-3.8.3\python.exe" -P "3.9:python-3.9.0b4\python.exe"
- name: Upload release file - name: Upload release file
uses: actions/upload-artifact@v1 uses: actions/upload-artifact@v1
with: with:
......
...@@ -15,6 +15,56 @@ ...@@ -15,6 +15,56 @@
- Fixed errors when installing directory or file dependencies in some cases ([#2582](https://github.com/python-poetry/poetry/pull/2582)). - Fixed errors when installing directory or file dependencies in some cases ([#2582](https://github.com/python-poetry/poetry/pull/2582)).
## [1.1.0a3] - 2020-07-10
### Added
- New installer which provides a faster and better experience ([#2595](https://github.com/python-poetry/poetry/pull/2595)).
### Fixed
- Fixed resolution error when handling duplicate dependencies with environment markers ([#2622](https://github.com/python-poetry/poetry/pull/2622)).
- Fixed erroneous resolution errors when resolving packages to install ([#2625](https://github.com/python-poetry/poetry/pull/2625)).
- Fixed errors when detecting installed editable packages ([#2602](https://github.com/python-poetry/poetry/pull/2602)).
## [1.1.0a2] - 2020-06-26
Note that lock files generated with this release are not compatible with previous releases of Poetry.
### Added
- The `install` command now supports a `--remove-untracked` option to ensure only packages from the lock file are present in the environment ([#2172](https://github.com/python-poetry/poetry/pull/2172)).
- Some errors will now be provided with possible solutions and links to the documentation ([#2396](https://github.com/python-poetry/poetry/pull/2396)).
### Changed
- Editable installations of Poetry projects have been improved and are now faster ([#2360](https://github.com/python-poetry/poetry/pull/2360)).
- Improved the accuracy of the dependency resolver in case of dependencies with environment markers ([#2361](https://github.com/python-poetry/poetry/pull/2361))
- Environment markers of dependencies are no longer stored in the lock file ([#2361](https://github.com/python-poetry/poetry/pull/2361)).
- Improved the way connection errors are handled when publishing ([#2285](https://github.com/python-poetry/poetry/pull/2285)).
### Fixed
- Fixed errors when handling duplicate dependencies with environment markers ([#2342](https://github.com/python-poetry/poetry/pull/2342)).
- Fixed the detection of installed packages ([#2360](https://github.com/python-poetry/poetry/pull/2360)).
## [1.1.0a1] - 2020-03-27
This release **must** be downloaded via the `get-poetry.py` script and not via the `self update` command.
### Added
- Added a new `--dry-run` option to the `publish` command ([#2199](https://github.com/python-poetry/poetry/pull/2199)).
### Changed
- The core features of Poetry have been extracted in to a separate library: `poetry-core` ([#2212](https://github.com/python-poetry/poetry/pull/2212)).
- The build backend is no longer `poetry.masonry.api` but `poetry.core.masonry.api` which requires `poetry-core>=1.0.0a5` ([#2212](https://github.com/python-poetry/poetry/pull/2212)).
- The exceptions are now beautifully displayed in the terminal with various level of details depending on the verbosity ([2230](https://github.com/python-poetry/poetry/pull/2230)).
## [1.0.9] - 2020-06-09 ## [1.0.9] - 2020-06-09
### Fixed ### Fixed
...@@ -86,7 +136,6 @@ ...@@ -86,7 +136,6 @@
- Fixed an error when parsing some git URLs ([#2018](https://github.com/python-poetry/poetry/pull/2018)). - Fixed an error when parsing some git URLs ([#2018](https://github.com/python-poetry/poetry/pull/2018)).
## [1.0.3] - 2020-01-31 ## [1.0.3] - 2020-01-31
### Fixed ### Fixed
...@@ -883,7 +932,10 @@ Initial release ...@@ -883,7 +932,10 @@ Initial release
[Unreleased]: https://github.com/python-poetry/poetry/compare/1.0.10...master [Unreleased]: https://github.com/python-poetry/poetry/compare/1.1.0a3...master
[1.1.0a3]: https://github.com/python-poetry/poetry/releases/tag/1.1.0a3
[1.1.0a2]: https://github.com/python-poetry/poetry/releases/tag/1.1.0a2
[1.1.0a1]: https://github.com/python-poetry/poetry/releases/tag/1.1.0a1
[1.0.10]: https://github.com/python-poetry/poetry/releases/tag/1.0.10 [1.0.10]: https://github.com/python-poetry/poetry/releases/tag/1.0.10
[1.0.9]: https://github.com/python-poetry/poetry/releases/tag/1.0.9 [1.0.9]: https://github.com/python-poetry/poetry/releases/tag/1.0.9
[1.0.8]: https://github.com/python-poetry/poetry/releases/tag/1.0.8 [1.0.8]: https://github.com/python-poetry/poetry/releases/tag/1.0.8
......
...@@ -56,7 +56,7 @@ linux_release: ...@@ -56,7 +56,7 @@ linux_release:
-e PYTHON37=/opt/python/cp37-cp37m/bin/python \ -e PYTHON37=/opt/python/cp37-cp37m/bin/python \
-e PYTHON38=/opt/python/cp38-cp38/bin/python \ -e PYTHON38=/opt/python/cp38-cp38/bin/python \
-e PYTHON39=/opt/python/cp39-cp39/bin/python \ -e PYTHON39=/opt/python/cp39-cp39/bin/python \
quay.io/pypa/manylinux2010_x86_64 sh -c "cd /io && ./make-linux-release.sh" quay.io/pypa/manylinux2010_x86_64 sh -c "cd /io && ./make-nix-release.sh"
# run tests against all supported python versions # run tests against all supported python versions
tox: tox:
......
...@@ -5,7 +5,10 @@ ensuring you have the right stack everywhere. ...@@ -5,7 +5,10 @@ ensuring you have the right stack everywhere.
![Poetry Install](https://raw.githubusercontent.com/python-poetry/poetry/master/assets/install.gif) ![Poetry Install](https://raw.githubusercontent.com/python-poetry/poetry/master/assets/install.gif)
It supports Python 2.7 and 3.4+. It supports Python 2.7 and 3.5+.
**Note**: Python 2.7 and 3.5 will no longer be supported in the next feature release (1.2).
You should consider updating your Python version to a supported one.
[![Tests Status](https://github.com/python-poetry/poetry/workflows/Tests/badge.svg?branch=master&event=push)](https://github.com/python-poetry/poetry/actions?query=workflow%3ATests+branch%3Amaster+event%3Apush) [![Tests Status](https://github.com/python-poetry/poetry/workflows/Tests/badge.svg?branch=master&event=push)](https://github.com/python-poetry/poetry/actions?query=workflow%3ATests+branch%3Amaster+event%3Apush)
...@@ -23,6 +26,13 @@ curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poet ...@@ -23,6 +26,13 @@ curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poet
Alternatively, you can download the `get-poetry.py` file and execute it separately. Alternatively, you can download the `get-poetry.py` file and execute it separately.
The setup script must be able to find one of following executables in your shell's path environment:
- `python` (which can be a py3 or py2 interpreter)
- `python3`
- `py.exe -3` (Windows)
- `py.exe -2` (Windows)
If you want to install prerelease versions, you can do so by passing `--preview` to `get-poetry.py`: If you want to install prerelease versions, you can do so by passing `--preview` to `get-poetry.py`:
```bash ```bash
......
...@@ -81,6 +81,7 @@ poetry init ...@@ -81,6 +81,7 @@ poetry init
* `--name`: Name of the package. * `--name`: Name of the package.
* `--description`: Description of the package. * `--description`: Description of the package.
* `--author`: Author of the package. * `--author`: Author of the package.
* `--python` Compatible Python versions.
* `--dependency`: Package to require with a version constraint. Should be in format `foo:1.0.0`. * `--dependency`: Package to require with a version constraint. Should be in format `foo:1.0.0`.
* `--dev-dependency`: Development requirements, see `--require`. * `--dev-dependency`: Development requirements, see `--require`.
...@@ -107,6 +108,13 @@ the `--no-dev` option. ...@@ -107,6 +108,13 @@ the `--no-dev` option.
poetry install --no-dev poetry install --no-dev
``` ```
If you want to remove old dependencies no longer present in the lock file, use the
`--remove-untracked` option.
```bash
poetry install --remove-untracked
```
You can also specify the extras you want installed You can also specify the extras you want installed
by passing the `--E|--extras` option (See [Extras](#extras) for more info) by passing the `--E|--extras` option (See [Extras](#extras) for more info)
...@@ -247,6 +255,7 @@ poetry add "git+https://github.com/pallets/flask.git@1.1.1[dotenv,dev]" ...@@ -247,6 +255,7 @@ poetry add "git+https://github.com/pallets/flask.git@1.1.1[dotenv,dev]"
* `--path`: The path to a dependency. * `--path`: The path to a dependency.
* `--optional` : Add as an optional dependency. * `--optional` : Add as an optional dependency.
* `--dry-run` : Outputs the operations but will not execute anything (implicitly enables --verbose). * `--dry-run` : Outputs the operations but will not execute anything (implicitly enables --verbose).
* `--lock` : Do not perform install (only update the lockfile).
## remove ## remove
...@@ -327,6 +336,7 @@ It can also build the package if you pass it the `--build` option. ...@@ -327,6 +336,7 @@ It can also build the package if you pass it the `--build` option.
Should match a repository name set by the [`config`](#config) command. Should match a repository name set by the [`config`](#config) command.
* `--username (-u)`: The username to access the repository. * `--username (-u)`: The username to access the repository.
* `--password (-p)`: The password to access the repository. * `--password (-p)`: The password to access the repository.
* `--dry-run`: Perform all actions except upload the package.
## config ## config
...@@ -438,7 +448,9 @@ The table below illustrates the effect of these rules with concrete examples. ...@@ -438,7 +448,9 @@ The table below illustrates the effect of these rules with concrete examples.
| prerelease | 1.0.3-alpha.0 | 1.0.3-alpha.1 | | prerelease | 1.0.3-alpha.0 | 1.0.3-alpha.1 |
| prerelease | 1.0.3-beta.0 | 1.0.3-beta.1 | | prerelease | 1.0.3-beta.0 | 1.0.3-beta.1 |
## Options
* `--short (-s)`: Output the version number only.
## export ## export
...@@ -454,8 +466,8 @@ poetry export -f requirements.txt > requirements.txt ...@@ -454,8 +466,8 @@ poetry export -f requirements.txt > requirements.txt
### Options ### Options
* `--format (-f)`: The format to export to. Currently, only * `--format (-f)`: The format to export to (default: `requirements.txt`).
`requirements.txt` is supported. Currently, only `requirements.txt` is supported.
* `--output (-o)`: The name of the output file. If omitted, print to standard * `--output (-o)`: The name of the output file. If omitted, print to standard
output. output.
* `--dev`: Include development dependencies. * `--dev`: Include development dependencies.
...@@ -469,3 +481,15 @@ The `env` command regroups sub commands to interact with the virtualenvs ...@@ -469,3 +481,15 @@ The `env` command regroups sub commands to interact with the virtualenvs
associated with a specific project. associated with a specific project.
See [Managing environments](/docs/managing-environments/) for more information about these commands. See [Managing environments](/docs/managing-environments/) for more information about these commands.
## cache
The `cache` command regroups sub commands to interact with Poetry's cache.
### cache list
The `cache list` command lists Poetry's available caches.
```bash
poetry cache list
```
...@@ -42,8 +42,8 @@ So, in your `pyproject.toml` file, add this section if it does not already exist ...@@ -42,8 +42,8 @@ So, in your `pyproject.toml` file, add this section if it does not already exist
```toml ```toml
[build-system] [build-system]
requires = ["poetry>=0.12"] requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.masonry.api" build-backend = "poetry.core.masonry.api"
``` ```
And use a `tox.ini` configuration file similar to this: And use a `tox.ini` configuration file similar to this:
......
...@@ -6,9 +6,14 @@ It allows you to declare the libraries your project depends on and it will manag ...@@ -6,9 +6,14 @@ It allows you to declare the libraries your project depends on and it will manag
## System requirements ## System requirements
Poetry requires Python 2.7 or 3.4+. It is multi-platform and the goal is to make it work equally well Poetry requires Python 2.7 or 3.5+. It is multi-platform and the goal is to make it work equally well
on Windows, Linux and OSX. on Windows, Linux and OSX.
!!! note
Python 2.7 and 3.5 will no longer be supported in the next feature release (1.2).
You should consider updating your Python version to a supported one.
## Installation ## Installation
......
...@@ -278,16 +278,22 @@ If you publish you package on PyPI, they will appear in the `Project Links` sect ...@@ -278,16 +278,22 @@ If you publish you package on PyPI, they will appear in the `Project Links` sect
[PEP-517](https://www.python.org/dev/peps/pep-0517/) introduces a standard way [PEP-517](https://www.python.org/dev/peps/pep-0517/) introduces a standard way
to define alternative build systems to build a Python project. to define alternative build systems to build a Python project.
Poetry is compliant with PEP-517 so if you use Poetry to manage your Python Poetry is compliant with PEP-517, by providing a lightweight core library,
project you should reference it in the `build-system` section of the `pyproject.toml` so if you use Poetry to manage your Python project you should reference
file like so: it in the `build-system` section of the `pyproject.toml` file like so:
```toml ```toml
[build-system] [build-system]
requires = ["poetry>=0.12"] requires = ["poetry_core>=1.0.0"]
build-backend = "poetry.masonry.api" build-backend = "poetry.core.masonry.api"
``` ```
!!!note !!!note
When using the `new` or `init` command this section will be automatically added. When using the `new` or `init` command this section will be automatically added.
!!!note
If your `pyproject.toml` file still references `poetry` directly as a build backend,
you should update it to reference `poetry_core` instead.
...@@ -207,6 +207,7 @@ vendors = os.path.join(lib, "poetry", "_vendor") ...@@ -207,6 +207,7 @@ vendors = os.path.join(lib, "poetry", "_vendor")
current_vendors = os.path.join( current_vendors = os.path.join(
vendors, "py{}".format(".".join(str(v) for v in sys.version_info[:2])) vendors, "py{}".format(".".join(str(v) for v in sys.version_info[:2]))
) )
sys.path.insert(0, lib) sys.path.insert(0, lib)
sys.path.insert(0, current_vendors) sys.path.insert(0, current_vendors)
...@@ -331,14 +332,17 @@ class Installer: ...@@ -331,14 +332,17 @@ class Installer:
version=None, version=None,
preview=False, preview=False,
force=False, force=False,
modify_path=True,
accept_all=False, accept_all=False,
file=None,
base_url=BASE_URL, base_url=BASE_URL,
): ):
self._version = version self._version = version
self._preview = preview self._preview = preview
self._force = force self._force = force
self._modify_path = True self._modify_path = modify_path
self._accept_all = accept_all self._accept_all = accept_all
self._offline_file = file
self._base_url = base_url self._base_url = base_url
def allows_prereleases(self): def allows_prereleases(self):
...@@ -355,7 +359,9 @@ class Installer: ...@@ -355,7 +359,9 @@ class Installer:
self.ensure_home() self.ensure_home()
try: try:
self.install(version, upgrade=current_version is not None) self.install(
version, upgrade=current_version is not None, file=self._offline_file
)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print(colorize("error", "An error has occured: {}".format(str(e)))) print(colorize("error", "An error has occured: {}".format(str(e))))
print(e.output.decode()) print(e.output.decode())
...@@ -376,6 +382,34 @@ class Installer: ...@@ -376,6 +382,34 @@ class Installer:
self.remove_from_path() self.remove_from_path()
def get_version(self): def get_version(self):
current_version = None
if os.path.exists(POETRY_LIB):
with open(
os.path.join(POETRY_LIB, "poetry", "__version__.py"), encoding="utf-8"
) as f:
version_content = f.read()
current_version_re = re.match(
'(?ms).*__version__ = "(.+)".*', version_content
)
if not current_version_re:
print(
colorize(
"warning",
"Unable to get the current Poetry version. Assuming None",
)
)
else:
current_version = current_version_re.group(1)
# Skip retrieving online release versions if install file is specified
if self._offline_file is not None:
if current_version is not None and not self._force:
print("There is a version of Poetry already installed.")
return None, current_version
return "from an offline file", current_version
print(colorize("info", "Retrieving Poetry metadata")) print(colorize("info", "Retrieving Poetry metadata"))
metadata = json.loads(self._get(self.METADATA_URL).decode()) metadata = json.loads(self._get(self.METADATA_URL).decode())
...@@ -482,11 +516,14 @@ class Installer: ...@@ -482,11 +516,14 @@ class Installer:
shutil.rmtree(POETRY_HOME) shutil.rmtree(POETRY_HOME)
def install(self, version, upgrade=False): def install(self, version, upgrade=False, file=None):
""" """
Installs Poetry in $POETRY_HOME. Installs Poetry in $POETRY_HOME.
""" """
print("Installing version: " + colorize("info", version)) if file is not None:
print("Attempting to install from file: " + colorize("info", file))
else:
print("Installing version: " + colorize("info", version))
self.make_lib(version) self.make_lib(version)
self.make_bin() self.make_bin()
...@@ -522,6 +559,14 @@ class Installer: ...@@ -522,6 +559,14 @@ class Installer:
shutil.rmtree(POETRY_LIB_BACKUP) shutil.rmtree(POETRY_LIB_BACKUP)
def _make_lib(self, version): def _make_lib(self, version):
# Check if an offline installer file has been specified
if self._offline_file is not None:
try:
self.extract_lib(self._offline_file)
return
except Exception:
raise RuntimeError("Could not install from offline file.")
# We get the payload from the remote host # We get the payload from the remote host
platform = sys.platform platform = sys.platform
if platform == "linux2": if platform == "linux2":
...@@ -581,12 +626,15 @@ class Installer: ...@@ -581,12 +626,15 @@ class Installer:
) )
) )
gz = GzipFile(tar, mode="rb") self.extract_lib(tar)
try:
with tarfile.TarFile(tar, fileobj=gz, format=tarfile.PAX_FORMAT) as f: def extract_lib(self, filename):
f.extractall(POETRY_LIB) gz = GzipFile(filename, mode="rb")
finally: try:
gz.close() with tarfile.TarFile(filename, fileobj=gz, format=tarfile.PAX_FORMAT) as f:
f.extractall(POETRY_LIB)
finally:
gz.close()
def _which_python(self): def _which_python(self):
"""Decides which python executable we'll embed in the launcher script.""" """Decides which python executable we'll embed in the launcher script."""
...@@ -606,7 +654,8 @@ class Installer: ...@@ -606,7 +654,8 @@ class Installer:
continue continue
match = version_matcher.match(raw_version.strip()) match = version_matcher.match(raw_version.strip())
if match: if match and tuple(map(int, match.groups())) >= (3, 0):
# favor the first py3 executable we can find.
return executable return executable
if fallback is None: if fallback is None:
...@@ -957,37 +1006,74 @@ def main(): ...@@ -957,37 +1006,74 @@ def main():
description="Installs the latest (or given) version of poetry" description="Installs the latest (or given) version of poetry"
) )
parser.add_argument( parser.add_argument(
"-p", "--preview", dest="preview", action="store_true", default=False "-p",
"--preview",
help="install preview version",
dest="preview",
action="store_true",
default=False,
)
parser.add_argument("--version", help="install named version", dest="version")
parser.add_argument(
"-f",
"--force",
help="install on top of existing version",
dest="force",
action="store_true",
default=False,
) )
parser.add_argument("--version", dest="version")
parser.add_argument( parser.add_argument(
"-f", "--force", dest="force", action="store_true", default=False "--no-modify-path",
help="do not modify $PATH",
dest="no_modify_path",
action="store_true",
default=False,
) )
parser.add_argument( parser.add_argument(
"-y", "--yes", dest="accept_all", action="store_true", default=False "-y",
"--yes",
help="accept all prompts",
dest="accept_all",
action="store_true",
default=False,
) )
parser.add_argument( parser.add_argument(
"--uninstall", dest="uninstall", action="store_true", default=False "--uninstall",
help="uninstall poetry",
dest="uninstall",
action="store_true",
default=False,
)
parser.add_argument(
"--file",
dest="file",
action="store",
help="Install from a local file instead of fetching the latest version "
"of Poetry available online.",
) )
args = parser.parse_args() args = parser.parse_args()
base_url = Installer.BASE_URL base_url = Installer.BASE_URL
try:
urlopen(Installer.REPOSITORY_URL) if args.file is None:
except HTTPError as e: try:
if e.code == 404: urlopen(Installer.REPOSITORY_URL)
base_url = Installer.FALLBACK_BASE_URL except HTTPError as e:
else: if e.code == 404:
raise base_url = Installer.FALLBACK_BASE_URL
else:
raise
installer = Installer( installer = Installer(
version=args.version or os.getenv("POETRY_VERSION"), version=args.version or os.getenv("POETRY_VERSION"),
preview=args.preview or string_to_bool(os.getenv("POETRY_PREVIEW", "0")), preview=args.preview or string_to_bool(os.getenv("POETRY_PREVIEW", "0")),
force=args.force, force=args.force,
modify_path=not args.no_modify_path,
accept_all=args.accept_all accept_all=args.accept_all
or string_to_bool(os.getenv("POETRY_ACCEPT", "0")) or string_to_bool(os.getenv("POETRY_ACCEPT", "0"))
or not is_interactive(), or not is_interactive(),
file=args.file,
base_url=base_url, base_url=base_url,
) )
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
set -e set -e
test -n "$PYTHON" || PYTHON="python3" test -n "$PYTHON" || PYTHON="python3"
$PYTHON get-poetry.py -y $PYTHON get-poetry.py -y --preview
$PYTHON $HOME/.poetry/bin/poetry config virtualenvs.create false $PYTHON $HOME/.poetry/bin/poetry config virtualenvs.create false
$PYTHON $HOME/.poetry/bin/poetry install --no-dev $PYTHON $HOME/.poetry/bin/poetry install --no-dev
$PYTHON $HOME/.poetry/bin/poetry run python sonnet make release \ $PYTHON $HOME/.poetry/bin/poetry run python sonnet make release \
......
import os from pkgutil import extend_path
import sys
from .__version__ import __version__ # noqa
__path__ = extend_path(__path__, __name__)
_ROOT = os.path.dirname(os.path.realpath(__file__))
_VENDOR = os.path.join(_ROOT, "_vendor")
_CURRENT_VENDOR = os.path.join(
_VENDOR, "py{}".format(".".join(str(v) for v in sys.version_info[:2]))
)
# Add vendored dependencies to path.
sys.path.insert(0, _CURRENT_VENDOR)
__version__ = "1.0.10" __version__ = "1.1.0a3"
...@@ -37,6 +37,7 @@ class Config(object): ...@@ -37,6 +37,7 @@ class Config(object):
"in-project": False, "in-project": False,
"path": os.path.join("{cache-dir}", "virtualenvs"), "path": os.path.join("{cache-dir}", "virtualenvs"),
}, },
"experimental": {"new-installer": True},
} }
def __init__( def __init__(
......
import sys
from cleo import Application as BaseApplication from cleo import Application as BaseApplication
from poetry import __version__ from poetry.__version__ import __version__
from .commands.about import AboutCommand from .commands.about import AboutCommand
from .commands.add import AddCommand from .commands.add import AddCommand
...@@ -38,6 +40,24 @@ class Application(BaseApplication): ...@@ -38,6 +40,24 @@ class Application(BaseApplication):
for command in self.get_default_commands(): for command in self.get_default_commands():
self.add(command) self.add(command)
if sys.version_info[:2] < (3, 6):
python_version = "<c1>{}</c1>".format(
".".join(str(v) for v in sys.version_info[:2])
)
poetry_feature_release = "<c1>1.2</c1>"
message = (
"\n"
"Python {} will no longer be supported "
"in the next feature release of Poetry ({}).\n"
"You should consider updating your Python version to a supported one.\n\n"
""
"Note that you will still be able to manage Python {} projects "
"by using the <c1>env</c1> command.\n"
"See <fg=blue>https://python-poetry.org/docs/managing-environments/</> "
"for more information."
).format(python_version, poetry_feature_release, python_version)
self._preliminary_io.write_line("<fg=yellow>{}</>\n".format(message))
@property @property
def poetry(self): def poetry(self):
from poetry.factory import Factory from poetry.factory import Factory
......
from cleo import argument from cleo import argument
from cleo import option from cleo import option
from .env_command import EnvCommand
from .init import InitCommand from .init import InitCommand
from .installer_command import InstallerCommand
class AddCommand(EnvCommand, InitCommand): class AddCommand(InstallerCommand, InitCommand):
name = "add" name = "add"
description = "Adds a new dependency to <comment>pyproject.toml</>." description = "Adds a new dependency to <comment>pyproject.toml</>."
...@@ -33,12 +33,19 @@ class AddCommand(EnvCommand, InitCommand): ...@@ -33,12 +33,19 @@ class AddCommand(EnvCommand, InitCommand):
"Platforms for which the dependency must be installed.", "Platforms for which the dependency must be installed.",
flag=False, flag=False,
), ),
option(
"source",
None,
"Name of the source to use to install the package.",
flag=False,
),
option("allow-prereleases", None, "Accept prereleases."), option("allow-prereleases", None, "Accept prereleases."),
option( option(
"dry-run", "dry-run",
None, None,
"Output the operations but do not execute anything (implicitly enables --verbose).", "Output the operations but do not execute anything (implicitly enables --verbose).",
), ),
option("lock", None, "Do not perform operations (only update the lockfile)."),
] ]
help = ( help = (
"The add command adds required packages to your <comment>pyproject.toml</> and installs them.\n\n" "The add command adds required packages to your <comment>pyproject.toml</> and installs them.\n\n"
...@@ -56,8 +63,7 @@ class AddCommand(EnvCommand, InitCommand): ...@@ -56,8 +63,7 @@ class AddCommand(EnvCommand, InitCommand):
loggers = ["poetry.repositories.pypi_repository"] loggers = ["poetry.repositories.pypi_repository"]
def handle(self): def handle(self):
from poetry.installation.installer import Installer from poetry.core.semver import parse_constraint
from poetry.semver import parse_constraint
from tomlkit import inline_table from tomlkit import inline_table
packages = self.argument("name") packages = self.argument("name")
...@@ -93,7 +99,9 @@ class AddCommand(EnvCommand, InitCommand): ...@@ -93,7 +99,9 @@ class AddCommand(EnvCommand, InitCommand):
raise ValueError("Package {} is already present".format(name)) raise ValueError("Package {} is already present".format(name))
requirements = self._determine_requirements( requirements = self._determine_requirements(
packages, allow_prereleases=self.option("allow-prereleases") packages,
allow_prereleases=self.option("allow-prereleases"),
source=self.option("source"),
) )
for _constraint in requirements: for _constraint in requirements:
...@@ -130,6 +138,9 @@ class AddCommand(EnvCommand, InitCommand): ...@@ -130,6 +138,9 @@ class AddCommand(EnvCommand, InitCommand):
if self.option("platform"): if self.option("platform"):
constraint["platform"] = self.option("platform") constraint["platform"] = self.option("platform")
if self.option("source"):
constraint["source"] = self.option("source")
if len(constraint) == 1 and "version" in constraint: if len(constraint) == 1 and "version" in constraint:
constraint = constraint["version"] constraint = constraint["version"]
...@@ -144,16 +155,17 @@ class AddCommand(EnvCommand, InitCommand): ...@@ -144,16 +155,17 @@ class AddCommand(EnvCommand, InitCommand):
# Update packages # Update packages
self.reset_poetry() self.reset_poetry()
installer = Installer( self._installer.set_package(self.poetry.package)
self.io, self.env, self.poetry.package, self.poetry.locker, self.poetry.pool self._installer.dry_run(self.option("dry-run"))
) self._installer.verbose(self._io.is_verbose())
self._installer.update(True)
if self.option("lock"):
self._installer.lock()
installer.dry_run(self.option("dry-run")) self._installer.whitelist([r["name"] for r in requirements])
installer.update(True)
installer.whitelist([r["name"] for r in requirements])
try: try:
status = installer.run() status = self._installer.run()
except Exception: except Exception:
self.poetry.file.write(original_content) self.poetry.file.write(original_content)
...@@ -162,10 +174,10 @@ class AddCommand(EnvCommand, InitCommand): ...@@ -162,10 +174,10 @@ class AddCommand(EnvCommand, InitCommand):
if status != 0 or self.option("dry-run"): if status != 0 or self.option("dry-run"):
# Revert changes # Revert changes
if not self.option("dry-run"): if not self.option("dry-run"):
self.error( self.line_error(
"\n" "\n"
"Addition failed, reverting pyproject.toml " "<error>Failed to add packages, reverting the pyproject.toml file "
"to its original content." "to its original content.</error>"
) )
self.poetry.file.write(original_content) self.poetry.file.write(original_content)
......
...@@ -12,8 +12,13 @@ class BuildCommand(EnvCommand): ...@@ -12,8 +12,13 @@ class BuildCommand(EnvCommand):
option("format", "f", "Limit the format to either sdist or wheel.", flag=False) option("format", "f", "Limit the format to either sdist or wheel.", flag=False)
] ]
loggers = [
"poetry.core.masonry.builders.sdist",
"poetry.core.masonry.builders.wheel",
]
def handle(self): def handle(self):
from poetry.masonry import Builder from poetry.core.masonry import Builder
fmt = "all" fmt = "all"
if self.option("format"): if self.option("format"):
...@@ -21,10 +26,10 @@ class BuildCommand(EnvCommand): ...@@ -21,10 +26,10 @@ class BuildCommand(EnvCommand):
package = self.poetry.package package = self.poetry.package
self.line( self.line(
"Building <c1>{}</c1> (<b>{}</b>)".format( "Building <c1>{}</c1> (<c2>{}</c2>)".format(
package.pretty_name, package.version package.pretty_name, package.version
) )
) )
builder = Builder(self.poetry, self.env, self.io) builder = Builder(self.poetry)
builder.build(fmt) builder.build(fmt)
from poetry.console.commands.cache.list import CacheListCommand
from ..command import Command from ..command import Command
from .clear import CacheClearCommand from .clear import CacheClearCommand
...@@ -7,7 +9,7 @@ class CacheCommand(Command): ...@@ -7,7 +9,7 @@ class CacheCommand(Command):
name = "cache" name = "cache"
description = "Interact with Poetry's cache" description = "Interact with Poetry's cache"
commands = [CacheClearCommand()] commands = [CacheClearCommand(), CacheListCommand()]
def handle(self): def handle(self):
return self.call("help", self._config.name) return self.call("help", self._config.name)
...@@ -16,19 +16,17 @@ class CacheClearCommand(Command): ...@@ -16,19 +16,17 @@ class CacheClearCommand(Command):
def handle(self): def handle(self):
from cachy import CacheManager from cachy import CacheManager
from poetry.locations import CACHE_DIR from poetry.locations import REPOSITORY_CACHE_DIR
from poetry.utils._compat import Path
cache = self.argument("cache") cache = self.argument("cache")
parts = cache.split(":") parts = cache.split(":")
root = parts[0] root = parts[0]
base_cache = Path(CACHE_DIR) / "cache" / "repositories" cache_dir = REPOSITORY_CACHE_DIR / root
cache_dir = base_cache / root
try: try:
cache_dir.relative_to(base_cache) cache_dir.relative_to(REPOSITORY_CACHE_DIR)
except ValueError: except ValueError:
raise ValueError("{} is not a valid repository cache".format(root)) raise ValueError("{} is not a valid repository cache".format(root))
......
import os
from ..command import Command
class CacheListCommand(Command):
name = "list"
description = "List Poetry's caches."
def handle(self):
from poetry.locations import REPOSITORY_CACHE_DIR
if os.path.exists(str(REPOSITORY_CACHE_DIR)):
caches = list(sorted(REPOSITORY_CACHE_DIR.iterdir()))
if caches:
for cache in caches:
self.line("<info>{}</>".format(cache.name))
return 0
self.line("<warning>No caches found</>")
...@@ -57,6 +57,11 @@ To remove a repository (repo is a short alias for repositories): ...@@ -57,6 +57,11 @@ To remove a repository (repo is a short alias for repositories):
lambda val: str(Path(val)), lambda val: str(Path(val)),
str(Path(CACHE_DIR) / "virtualenvs"), str(Path(CACHE_DIR) / "virtualenvs"),
), ),
"experimental.new-installer": (
boolean_validator,
boolean_normalizer,
True,
),
} }
return unique_config_values return unique_config_values
......
...@@ -28,8 +28,8 @@ class DebugResolveCommand(InitCommand): ...@@ -28,8 +28,8 @@ class DebugResolveCommand(InitCommand):
loggers = ["poetry.repositories.pypi_repository"] loggers = ["poetry.repositories.pypi_repository"]
def handle(self): def handle(self):
from poetry.core.packages.project_package import ProjectPackage
from poetry.io.null_io import NullIO from poetry.io.null_io import NullIO
from poetry.packages import ProjectPackage
from poetry.puzzle import Solver from poetry.puzzle import Solver
from poetry.repositories.pool import Pool from poetry.repositories.pool import Pool
from poetry.repositories.repository import Repository from poetry.repositories.repository import Repository
...@@ -105,7 +105,6 @@ class DebugResolveCommand(InitCommand): ...@@ -105,7 +105,6 @@ class DebugResolveCommand(InitCommand):
if self.option("install"): if self.option("install"):
env = EnvManager(self.poetry).get() env = EnvManager(self.poetry).get()
current_python_version = ".".join(str(v) for v in env.version_info)
pool = Pool() pool = Pool()
locked_repository = Repository() locked_repository = Repository()
for op in ops: for op in ops:
...@@ -113,11 +112,14 @@ class DebugResolveCommand(InitCommand): ...@@ -113,11 +112,14 @@ class DebugResolveCommand(InitCommand):
pool.add_repository(locked_repository) pool.add_repository(locked_repository)
with package.with_python_versions(current_python_version): solver = Solver(package, pool, Repository(), Repository(), NullIO())
solver = Solver(package, pool, Repository(), Repository(), NullIO()) with solver.use_environment(env):
ops = solver.solve() ops = solver.solve()
for op in ops: for op in ops:
if self.option("install") and op.skipped:
continue
pkg = op.package pkg = op.package
row = [ row = [
"<c1>{}</c1>".format(pkg.name), "<c1>{}</c1>".format(pkg.name),
......
...@@ -16,6 +16,7 @@ class ExportCommand(Command): ...@@ -16,6 +16,7 @@ class ExportCommand(Command):
"f", "f",
"Format to export to. Currently, only requirements.txt is supported.", "Format to export to. Currently, only requirements.txt is supported.",
flag=False, flag=False,
default=Exporter.FORMAT_REQUIREMENTS_TXT,
), ),
option("output", "o", "The name of the output file.", flag=False), option("output", "o", "The name of the output file.", flag=False),
option("without-hashes", None, "Exclude hashes from the exported file."), option("without-hashes", None, "Exclude hashes from the exported file."),
......
...@@ -32,6 +32,7 @@ class InitCommand(Command): ...@@ -32,6 +32,7 @@ class InitCommand(Command):
option("name", None, "Name of the package.", flag=False), option("name", None, "Name of the package.", flag=False),
option("description", None, "Description of the package.", flag=False), option("description", None, "Description of the package.", flag=False),
option("author", None, "Author name of the package.", flag=False), option("author", None, "Author name of the package.", flag=False),
option("python", None, "Compatible Python versions.", flag=False),
option( option(
"dependency", "dependency",
None, None,
...@@ -64,7 +65,7 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the ...@@ -64,7 +65,7 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the
from poetry.layouts import layout from poetry.layouts import layout
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils.env import SystemEnv from poetry.utils.env import SystemEnv
from poetry.vcs.git import GitConfig from poetry.core.vcs.git import GitConfig
if (Path.cwd() / "pyproject.toml").exists(): if (Path.cwd() / "pyproject.toml").exists():
self.line("<error>A pyproject.toml file already exists.</error>") self.line("<error>A pyproject.toml file already exists.</error>")
...@@ -126,17 +127,19 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the ...@@ -126,17 +127,19 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the
question.set_validator(self._validate_license) question.set_validator(self._validate_license)
license = self.ask(question) license = self.ask(question)
current_env = SystemEnv(Path(sys.executable)) python = self.option("python")
default_python = "^{}".format( if not python:
".".join(str(v) for v in current_env.version_info[:2]) current_env = SystemEnv(Path(sys.executable))
) default_python = "^{}".format(
question = self.create_question( ".".join(str(v) for v in current_env.version_info[:2])
"Compatible Python versions [<comment>{}</comment>]: ".format( )
default_python question = self.create_question(
), "Compatible Python versions [<comment>{}</comment>]: ".format(
default=default_python, default_python
) ),
python = self.ask(question) default=default_python,
)
python = self.ask(question)
self.line("") self.line("")
...@@ -203,7 +206,7 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the ...@@ -203,7 +206,7 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the
f.write(content) f.write(content)
def _determine_requirements( def _determine_requirements(
self, requires, allow_prereleases=False self, requires, allow_prereleases=False, source=None
): # type: (List[str], bool) -> List[Dict[str, str]] ): # type: (List[str], bool) -> List[Dict[str, str]]
if not requires: if not requires:
requires = [] requires = []
...@@ -299,7 +302,9 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the ...@@ -299,7 +302,9 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the
elif "version" not in requirement: elif "version" not in requirement:
# determine the best version automatically # determine the best version automatically
name, version = self._find_best_version_for_package( name, version = self._find_best_version_for_package(
requirement["name"], allow_prereleases=allow_prereleases requirement["name"],
allow_prereleases=allow_prereleases,
source=source,
) )
requirement["version"] = version requirement["version"] = version
requirement["name"] = name requirement["name"] = name
...@@ -314,6 +319,7 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the ...@@ -314,6 +319,7 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the
requirement["name"], requirement["name"],
requirement["version"], requirement["version"],
allow_prereleases=allow_prereleases, allow_prereleases=allow_prereleases,
source=source,
) )
requirement["name"] = name requirement["name"] = name
...@@ -323,13 +329,13 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the ...@@ -323,13 +329,13 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the
return result return result
def _find_best_version_for_package( def _find_best_version_for_package(
self, name, required_version=None, allow_prereleases=False self, name, required_version=None, allow_prereleases=False, source=None
): # type: (...) -> Tuple[str, str] ): # type: (...) -> Tuple[str, str]
from poetry.version.version_selector import VersionSelector from poetry.version.version_selector import VersionSelector
selector = VersionSelector(self._get_pool()) selector = VersionSelector(self._get_pool())
package = selector.find_best_candidate( package = selector.find_best_candidate(
name, required_version, allow_prereleases=allow_prereleases name, required_version, allow_prereleases=allow_prereleases, source=source
) )
if not package: if not package:
...@@ -364,8 +370,8 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the ...@@ -364,8 +370,8 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the
if url_parsed.scheme and url_parsed.netloc: if url_parsed.scheme and url_parsed.netloc:
# Url # Url
if url_parsed.scheme in ["git+https", "git+ssh"]: if url_parsed.scheme in ["git+https", "git+ssh"]:
from poetry.vcs.git import Git from poetry.core.vcs.git import Git
from poetry.vcs.git import ParsedUrl from poetry.core.vcs.git import ParsedUrl
parsed = ParsedUrl.parse(requirement) parsed = ParsedUrl.parse(requirement)
url = Git.normalize_url(requirement) url = Git.normalize_url(requirement)
...@@ -478,7 +484,7 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the ...@@ -478,7 +484,7 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the
return requires return requires
def _validate_author(self, author, default): def _validate_author(self, author, default):
from poetry.packages.package import AUTHOR_REGEX from poetry.core.packages.package import AUTHOR_REGEX
author = author or default author = author or default
...@@ -495,7 +501,7 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the ...@@ -495,7 +501,7 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the
return author return author
def _validate_license(self, license): def _validate_license(self, license):
from poetry.spdx import license_by_id from poetry.core.spdx import license_by_id
if license: if license:
license_by_id(license) license_by_id(license)
......
from cleo import option from cleo import option
from .env_command import EnvCommand from .installer_command import InstallerCommand
class InstallCommand(EnvCommand): class InstallCommand(InstallerCommand):
name = "install" name = "install"
description = "Installs the project dependencies." description = "Installs the project dependencies."
...@@ -20,6 +20,9 @@ class InstallCommand(EnvCommand): ...@@ -20,6 +20,9 @@ class InstallCommand(EnvCommand):
"(implicitly enables --verbose).", "(implicitly enables --verbose).",
), ),
option( option(
"remove-untracked", None, "Removes packages not present in the lock file.",
),
option(
"extras", "extras",
"E", "E",
"Extra sets of dependencies to install.", "Extra sets of dependencies to install.",
...@@ -45,13 +48,11 @@ dependencies and not including the current project, run the command with the ...@@ -45,13 +48,11 @@ dependencies and not including the current project, run the command with the
_loggers = ["poetry.repositories.pypi_repository"] _loggers = ["poetry.repositories.pypi_repository"]
def handle(self): def handle(self):
from clikit.io import NullIO
from poetry.installation.installer import Installer
from poetry.masonry.builders import EditableBuilder from poetry.masonry.builders import EditableBuilder
from poetry.masonry.utils.module import ModuleOrPackageNotFound from poetry.core.masonry.utils.module import ModuleOrPackageNotFound
installer = Installer( self._installer.use_executor(
self.io, self.env, self.poetry.package, self.poetry.locker, self.poetry.pool self.poetry.config.get("experimental.new-installer", False)
) )
extras = [] extras = []
...@@ -61,12 +62,13 @@ dependencies and not including the current project, run the command with the ...@@ -61,12 +62,13 @@ dependencies and not including the current project, run the command with the
else: else:
extras.append(extra) extras.append(extra)
installer.extras(extras) self._installer.extras(extras)
installer.dev_mode(not self.option("no-dev")) self._installer.dev_mode(not self.option("no-dev"))
installer.dry_run(self.option("dry-run")) self._installer.dry_run(self.option("dry-run"))
installer.verbose(self.option("verbose")) self._installer.remove_untracked(self.option("remove-untracked"))
self._installer.verbose(self._io.is_verbose())
return_code = installer.run() return_code = self._installer.run()
if return_code != 0: if return_code != 0:
return return_code return return_code
...@@ -75,22 +77,39 @@ dependencies and not including the current project, run the command with the ...@@ -75,22 +77,39 @@ dependencies and not including the current project, run the command with the
return 0 return 0
try: try:
builder = EditableBuilder(self.poetry, self._env, NullIO()) builder = EditableBuilder(self.poetry, self._env, self._io)
except ModuleOrPackageNotFound: except ModuleOrPackageNotFound:
# This is likely due to the fact that the project is an application # This is likely due to the fact that the project is an application
# not following the structure expected by Poetry # not following the structure expected by Poetry
# If this is a true error it will be picked up later by build anyway. # If this is a true error it will be picked up later by build anyway.
return 0 return 0
self.line( self.line("")
" - Installing <c1>{}</c1> (<b>{}</b>)".format( if not self._io.supports_ansi() or self.io.is_debug():
self.poetry.package.pretty_name, self.poetry.package.pretty_version self.line(
"<b>Installing</> the current project: <c1>{}</c1> (<c2>{}</c2>)".format(
self.poetry.package.pretty_name, self.poetry.package.pretty_version
)
)
else:
self.write(
"<b>Installing</> the current project: <c1>{}</c1> (<c2>{}</c2>)".format(
self.poetry.package.pretty_name, self.poetry.package.pretty_version
)
) )
)
if self.option("dry-run"): if self.option("dry-run"):
self.line("")
return 0 return 0
builder.build() builder.build()
if self._io.supports_ansi() and not self.io.is_debug():
self.overwrite(
"<b>Installing</> the current project: <c1>{}</c1> (<success>{}</success>)".format(
self.poetry.package.pretty_name, self.poetry.package.pretty_version
)
)
self.line("")
return 0 return 0
from typing import TYPE_CHECKING
from .env_command import EnvCommand
if TYPE_CHECKING:
from poetry.installation.installer import Installer
class InstallerCommand(EnvCommand):
def __init__(self):
self._installer = None
super(InstallerCommand, self).__init__()
@property
def installer(self): # type: () -> Installer
return self._installer
def set_installer(self, installer): # type: (Installer) -> None
self._installer = installer
from .env_command import EnvCommand from .installer_command import InstallerCommand
class LockCommand(EnvCommand): class LockCommand(InstallerCommand):
name = "lock" name = "lock"
description = "Locks the project dependencies." description = "Locks the project dependencies."
...@@ -17,12 +17,10 @@ file. ...@@ -17,12 +17,10 @@ file.
loggers = ["poetry.repositories.pypi_repository"] loggers = ["poetry.repositories.pypi_repository"]
def handle(self): def handle(self):
from poetry.installation.installer import Installer self._installer.use_executor(
self.poetry.config.get("experimental.new-installer", False)
installer = Installer(
self.io, self.env, self.poetry.package, self.poetry.locker, self.poetry.pool
) )
installer.lock() self._installer.lock()
return installer.run() return self._installer.run()
...@@ -21,10 +21,10 @@ class NewCommand(Command): ...@@ -21,10 +21,10 @@ class NewCommand(Command):
def handle(self): def handle(self):
from poetry.layouts import layout from poetry.layouts import layout
from poetry.semver import parse_constraint from poetry.core.semver import parse_constraint
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils.env import SystemEnv from poetry.utils.env import SystemEnv
from poetry.vcs.git import GitConfig from poetry.core.vcs.git import GitConfig
if self.option("src"): if self.option("src"):
layout_ = layout("src") layout_ = layout("src")
......
...@@ -26,6 +26,7 @@ class PublishCommand(Command): ...@@ -26,6 +26,7 @@ class PublishCommand(Command):
flag=False, flag=False,
), ),
option("build", None, "Build the package before publishing."), option("build", None, "Build the package before publishing."),
option("dry-run", None, "Perform all actions except upload the package."),
] ]
help = """The publish command builds and uploads the package to a remote repository. help = """The publish command builds and uploads the package to a remote repository.
...@@ -40,7 +41,7 @@ the config command. ...@@ -40,7 +41,7 @@ the config command.
loggers = ["poetry.masonry.publishing.publisher"] loggers = ["poetry.masonry.publishing.publisher"]
def handle(self): def handle(self):
from poetry.masonry.publishing.publisher import Publisher from poetry.publishing.publisher import Publisher
publisher = Publisher(self.poetry, self.io) publisher = Publisher(self.poetry, self.io)
...@@ -79,4 +80,5 @@ the config command. ...@@ -79,4 +80,5 @@ the config command.
self.option("password"), self.option("password"),
cert, cert,
client_cert, client_cert,
self.option("dry-run"),
) )
from cleo import argument from cleo import argument
from cleo import option from cleo import option
from .env_command import EnvCommand from .installer_command import InstallerCommand
class RemoveCommand(EnvCommand): class RemoveCommand(InstallerCommand):
name = "remove" name = "remove"
description = "Removes a package from the project dependencies." description = "Removes a package from the project dependencies."
...@@ -28,8 +28,6 @@ list of installed packages ...@@ -28,8 +28,6 @@ list of installed packages
loggers = ["poetry.repositories.pypi_repository"] loggers = ["poetry.repositories.pypi_repository"]
def handle(self): def handle(self):
from poetry.installation.installer import Installer
packages = self.argument("packages") packages = self.argument("packages")
is_dev = self.option("dev") is_dev = self.option("dev")
...@@ -62,16 +60,18 @@ list of installed packages ...@@ -62,16 +60,18 @@ list of installed packages
# Update packages # Update packages
self.reset_poetry() self.reset_poetry()
installer = Installer( self._installer.set_package(self.poetry.package)
self.io, self.env, self.poetry.package, self.poetry.locker, self.poetry.pool self._installer.use_executor(
self.poetry.config.get("experimental.new-installer", False)
) )
installer.dry_run(self.option("dry-run")) self._installer.dry_run(self.option("dry-run"))
installer.update(True) self._installer.verbose(self._io.is_verbose())
installer.whitelist(requirements) self._installer.update(True)
self._installer.whitelist(requirements)
try: try:
status = installer.run() status = self._installer.run()
except Exception: except Exception:
self.poetry.file.write(original_content) self.poetry.file.write(original_content)
...@@ -80,7 +80,7 @@ list of installed packages ...@@ -80,7 +80,7 @@ list of installed packages
if status != 0 or self.option("dry-run"): if status != 0 or self.option("dry-run"):
# Revert changes # Revert changes
if not self.option("dry-run"): if not self.option("dry-run"):
self.error( self.line_error(
"\n" "\n"
"Removal failed, reverting pyproject.toml " "Removal failed, reverting pyproject.toml "
"to its original content." "to its original content."
......
...@@ -50,7 +50,7 @@ class RunCommand(EnvCommand): ...@@ -50,7 +50,7 @@ class RunCommand(EnvCommand):
@property @property
def _module(self): def _module(self):
from ...masonry.utils.module import Module from poetry.core.masonry.utils.module import Module
poetry = self.poetry poetry = self.poetry
package = poetry.package package = poetry.package
......
...@@ -61,14 +61,8 @@ class SelfUpdateCommand(Command): ...@@ -61,14 +61,8 @@ class SelfUpdateCommand(Command):
@property @property
def home(self): def home(self):
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils.appdirs import expanduser
if os.environ.get("POETRY_HOME"): return Path(os.environ.get("POETRY_HOME", "~/.poetry")).expanduser()
return Path(expanduser(os.environ["POETRY_HOME"]))
home = Path(expanduser("~"))
return home / ".poetry"
@property @property
def bin(self): def bin(self):
...@@ -85,7 +79,7 @@ class SelfUpdateCommand(Command): ...@@ -85,7 +79,7 @@ class SelfUpdateCommand(Command):
def handle(self): def handle(self):
from poetry.__version__ import __version__ from poetry.__version__ import __version__
from poetry.repositories.pypi_repository import PyPiRepository from poetry.repositories.pypi_repository import PyPiRepository
from poetry.semver import Version from poetry.core.semver import Version
self._check_recommended_installation() self._check_recommended_installation()
......
...@@ -35,7 +35,7 @@ lists all packages available.""" ...@@ -35,7 +35,7 @@ lists all packages available."""
def handle(self): def handle(self):
from clikit.utils.terminal import Terminal from clikit.utils.terminal import Terminal
from poetry.repositories.installed_repository import InstalledRepository from poetry.repositories.installed_repository import InstalledRepository
from poetry.semver import Version from poetry.core.semver import Version
package = self.argument("package") package = self.argument("package")
...@@ -347,7 +347,7 @@ lists all packages available.""" ...@@ -347,7 +347,7 @@ lists all packages available."""
return selector.find_best_candidate(name, ">={}".format(package.pretty_version)) return selector.find_best_candidate(name, ">={}".format(package.pretty_version))
def get_update_status(self, latest, package): def get_update_status(self, latest, package):
from poetry.semver import parse_constraint from poetry.core.semver import parse_constraint
if latest.full_pretty_version == package.full_pretty_version: if latest.full_pretty_version == package.full_pretty_version:
return "up-to-date" return "up-to-date"
......
from cleo import argument from cleo import argument
from cleo import option from cleo import option
from .env_command import EnvCommand from .installer_command import InstallerCommand
class UpdateCommand(EnvCommand): class UpdateCommand(InstallerCommand):
name = "update" name = "update"
description = ( description = (
...@@ -28,22 +28,20 @@ class UpdateCommand(EnvCommand): ...@@ -28,22 +28,20 @@ class UpdateCommand(EnvCommand):
loggers = ["poetry.repositories.pypi_repository"] loggers = ["poetry.repositories.pypi_repository"]
def handle(self): def handle(self):
from poetry.installation.installer import Installer
packages = self.argument("packages") packages = self.argument("packages")
installer = Installer( self._installer.use_executor(
self.io, self.env, self.poetry.package, self.poetry.locker, self.poetry.pool self.poetry.config.get("experimental.new-installer", False)
) )
if packages: if packages:
installer.whitelist({name: "*" for name in packages}) self._installer.whitelist({name: "*" for name in packages})
installer.dev_mode(not self.option("no-dev")) self._installer.dev_mode(not self.option("no-dev"))
installer.dry_run(self.option("dry-run")) self._installer.dry_run(self.option("dry-run"))
installer.execute_operations(not self.option("lock")) self._installer.execute_operations(not self.option("lock"))
# Force update # Force update
installer.update(True) self._installer.update(True)
return installer.run() return self._installer.run()
from cleo import argument from cleo import argument
from cleo import option
from .command import Command from .command import Command
...@@ -18,6 +19,7 @@ class VersionCommand(Command): ...@@ -18,6 +19,7 @@ class VersionCommand(Command):
optional=True, optional=True,
) )
] ]
options = [option("short", "s", "Output the version number only")]
help = """\ help = """\
The version command shows the current version of the project or bumps the version of The version command shows the current version of the project or bumps the version of
...@@ -58,14 +60,17 @@ patch, minor, major, prepatch, preminor, premajor, prerelease. ...@@ -58,14 +60,17 @@ patch, minor, major, prepatch, preminor, premajor, prerelease.
self.poetry.file.write(content) self.poetry.file.write(content)
else: else:
self.line( if self.option("short"):
"<comment>{}</> <info>{}</>".format( self.line("{}".format(self.poetry.package.pretty_version))
self.poetry.package.name, self.poetry.package.pretty_version else:
self.line(
"<comment>{}</> <info>{}</>".format(
self.poetry.package.name, self.poetry.package.pretty_version
)
) )
)
def increment_version(self, version, rule): def increment_version(self, version, rule):
from poetry.semver import Version from poetry.core.semver import Version
try: try:
version = Version.parse(version) version = Version.parse(version)
......
...@@ -27,8 +27,10 @@ from clikit.io.output_stream import StandardOutputStream ...@@ -27,8 +27,10 @@ from clikit.io.output_stream import StandardOutputStream
from poetry.console.commands.command import Command from poetry.console.commands.command import Command
from poetry.console.commands.env_command import EnvCommand from poetry.console.commands.env_command import EnvCommand
from poetry.console.commands.installer_command import InstallerCommand
from poetry.console.logging.io_formatter import IOFormatter from poetry.console.logging.io_formatter import IOFormatter
from poetry.console.logging.io_handler import IOHandler from poetry.console.logging.io_handler import IOHandler
from poetry.utils._compat import PY36
class ApplicationConfig(BaseApplicationConfig): class ApplicationConfig(BaseApplicationConfig):
...@@ -36,14 +38,31 @@ class ApplicationConfig(BaseApplicationConfig): ...@@ -36,14 +38,31 @@ class ApplicationConfig(BaseApplicationConfig):
super(ApplicationConfig, self).configure() super(ApplicationConfig, self).configure()
self.add_style(Style("c1").fg("cyan")) self.add_style(Style("c1").fg("cyan"))
self.add_style(Style("c2").fg("default").bold())
self.add_style(Style("info").fg("blue")) self.add_style(Style("info").fg("blue"))
self.add_style(Style("comment").fg("green")) self.add_style(Style("comment").fg("green"))
self.add_style(Style("error").fg("red").bold()) self.add_style(Style("error").fg("red").bold())
self.add_style(Style("warning").fg("yellow")) self.add_style(Style("warning").fg("yellow").bold())
self.add_style(Style("debug").fg("black").bold()) self.add_style(Style("debug").fg("default").dark())
self.add_style(Style("success").fg("green"))
# Dark variants
self.add_style(Style("c1_dark").fg("cyan").dark())
self.add_style(Style("c2_dark").fg("default").bold().dark())
self.add_style(Style("success_dark").fg("green").dark())
self.add_event_listener(PRE_HANDLE, self.register_command_loggers) self.add_event_listener(PRE_HANDLE, self.register_command_loggers)
self.add_event_listener(PRE_HANDLE, self.set_env) self.add_event_listener(PRE_HANDLE, self.set_env)
self.add_event_listener(PRE_HANDLE, self.set_installer)
if PY36:
from poetry.mixology.solutions.providers import (
PythonRequirementSolutionProvider,
)
self._solution_provider_repository.register_solution_providers(
[PythonRequirementSolutionProvider]
)
def register_command_loggers( def register_command_loggers(
self, event, event_name, _ self, event, event_name, _
...@@ -85,6 +104,9 @@ class ApplicationConfig(BaseApplicationConfig): ...@@ -85,6 +104,9 @@ class ApplicationConfig(BaseApplicationConfig):
if not isinstance(command, EnvCommand): if not isinstance(command, EnvCommand):
return return
if command.env is not None:
return
io = event.io io = event.io
poetry = command.poetry poetry = command.poetry
...@@ -96,6 +118,32 @@ class ApplicationConfig(BaseApplicationConfig): ...@@ -96,6 +118,32 @@ class ApplicationConfig(BaseApplicationConfig):
command.set_env(env) command.set_env(env)
def set_installer(
self, event, event_name, _
): # type: (PreHandleEvent, str, Any) -> None
command = event.command.config.handler # type: InstallerCommand
if not isinstance(command, InstallerCommand):
return
# If the command already has an installer
# we skip this step
if command.installer is not None:
return
from poetry.installation.installer import Installer
poetry = command.poetry
installer = Installer(
event.io,
command.env,
poetry.package,
poetry.locker,
poetry.pool,
poetry.config,
)
installer.use_executor(poetry.config.get("experimental.new-installer", False))
command.set_installer(installer)
def resolve_help_command( def resolve_help_command(
self, event, event_name, dispatcher self, event, event_name, dispatcher
): # type: (PreResolveEvent, str, EventDispatcher) -> None ): # type: (PreResolveEvent, str, EventDispatcher) -> None
......
from .builder_formatter import BuilderLogFormatter
FORMATTERS = {
"poetry.core.masonry.builders.sdist": BuilderLogFormatter(),
"poetry.core.masonry.builders.wheel": BuilderLogFormatter(),
}
import re
from .formatter import Formatter
class BuilderLogFormatter(Formatter):
def format(self, msg): # type: (str) -> str
if msg.startswith(" - Building ") or msg.startswith(" - Built "):
msg = re.sub(r" - (Buil(?:t|ing)) (.+)", " - \\1 <c2>\\2</c2>", msg)
elif msg.startswith(" - Adding: "):
msg = re.sub(r" - Adding: (.+)", " - Adding: <b>\\1</b>", msg)
return msg
import logging
class Formatter(object):
def format(self, record): # type: (logging.LogRecord) -> str
raise NotImplementedError()
import logging import logging
from .formatters import FORMATTERS
class IOFormatter(logging.Formatter): class IOFormatter(logging.Formatter):
...@@ -15,7 +17,9 @@ class IOFormatter(logging.Formatter): ...@@ -15,7 +17,9 @@ class IOFormatter(logging.Formatter):
level = record.levelname.lower() level = record.levelname.lower()
msg = record.msg msg = record.msg
if level in self._colors: if record.name in FORMATTERS:
msg = FORMATTERS[record.name].format(msg)
elif level in self._colors:
msg = "<{}>{}</>".format(self._colors[level], msg) msg = "<{}>{}</>".format(self._colors[level], msg)
return msg return msg
......
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
import shutil
from typing import Dict from typing import Dict
from typing import List
from typing import Optional from typing import Optional
from clikit.api.io.io import IO from clikit.api.io.io import IO
from poetry.core.factory import Factory as BaseFactory
from poetry.core.utils.toml_file import TomlFile
from .config.config import Config from .config.config import Config
from .config.file_config_source import FileConfigSource from .config.file_config_source import FileConfigSource
from .io.null_io import NullIO from .io.null_io import NullIO
from .json import validate_object
from .locations import CONFIG_DIR from .locations import CONFIG_DIR
from .packages.dependency import Dependency
from .packages.locker import Locker from .packages.locker import Locker
from .packages.project_package import ProjectPackage
from .poetry import Poetry from .poetry import Poetry
from .repositories.pypi_repository import PyPiRepository from .repositories.pypi_repository import PyPiRepository
from .spdx import license_by_id
from .utils._compat import Path from .utils._compat import Path
from .utils.toml_file import TomlFile
class Factory: class Factory(BaseFactory):
""" """
Factory class to create various elements needed by Poetry. Factory class to create various elements needed by Poetry.
""" """
...@@ -35,125 +30,17 @@ class Factory: ...@@ -35,125 +30,17 @@ class Factory:
if io is None: if io is None:
io = NullIO() io = NullIO()
poetry_file = self.locate(cwd) base_poetry = super(Factory, self).create_poetry(cwd)
local_config = TomlFile(poetry_file.as_posix()).read()
if "tool" not in local_config or "poetry" not in local_config["tool"]:
raise RuntimeError(
"[tool.poetry] section not found in {}".format(poetry_file.name)
)
local_config = local_config["tool"]["poetry"]
# Checking validity
check_result = self.validate(local_config)
if check_result["errors"]:
message = ""
for error in check_result["errors"]:
message += " - {}\n".format(error)
raise RuntimeError("The Poetry configuration is invalid:\n" + message)
# Load package
name = local_config["name"]
version = local_config["version"]
package = ProjectPackage(name, version, version)
package.root_dir = poetry_file.parent
for author in local_config["authors"]:
package.authors.append(author)
for maintainer in local_config.get("maintainers", []):
package.maintainers.append(maintainer)
package.description = local_config.get("description", "")
package.homepage = local_config.get("homepage")
package.repository_url = local_config.get("repository")
package.documentation_url = local_config.get("documentation")
try:
license_ = license_by_id(local_config.get("license", ""))
except ValueError:
license_ = None
package.license = license_
package.keywords = local_config.get("keywords", [])
package.classifiers = local_config.get("classifiers", [])
if "readme" in local_config:
package.readme = Path(poetry_file.parent) / local_config["readme"]
if "platform" in local_config:
package.platform = local_config["platform"]
if "dependencies" in local_config:
for name, constraint in local_config["dependencies"].items():
if name.lower() == "python":
package.python_versions = constraint
continue
if isinstance(constraint, list):
for _constraint in constraint:
package.add_dependency(name, _constraint)
continue
package.add_dependency(name, constraint)
if "dev-dependencies" in local_config:
for name, constraint in local_config["dev-dependencies"].items():
if isinstance(constraint, list):
for _constraint in constraint:
package.add_dependency(name, _constraint, category="dev")
continue
package.add_dependency(name, constraint, category="dev")
extras = local_config.get("extras", {})
for extra_name, requirements in extras.items():
package.extras[extra_name] = []
# Checking for dependency
for req in requirements:
req = Dependency(req, "*")
for dep in package.requires:
if dep.name == req.name:
dep.in_extras.append(extra_name)
package.extras[extra_name].append(dep)
break
if "build" in local_config:
package.build = local_config["build"]
if "include" in local_config:
package.include = local_config["include"]
if "exclude" in local_config: locker = Locker(
package.exclude = local_config["exclude"] base_poetry.file.parent / "poetry.lock", base_poetry.local_config
)
if "packages" in local_config:
package.packages = local_config["packages"]
# Custom urls
if "urls" in local_config:
package.custom_urls = local_config["urls"]
# Moving lock if necessary (pyproject.lock -> poetry.lock)
lock = poetry_file.parent / "poetry.lock"
if not lock.exists():
# Checking for pyproject.lock
old_lock = poetry_file.with_suffix(".lock")
if old_lock.exists():
shutil.move(str(old_lock), str(lock))
locker = Locker(poetry_file.parent / "poetry.lock", local_config)
# Loading global configuration # Loading global configuration
config = self.create_config(io) config = self.create_config(io)
# Loading local configuration # Loading local configuration
local_config_file = TomlFile(poetry_file.parent / "poetry.toml") local_config_file = TomlFile(base_poetry.file.parent / "poetry.toml")
if local_config_file.exists(): if local_config_file.exists():
if io.is_debug(): if io.is_debug():
io.write_line( io.write_line(
...@@ -162,10 +49,16 @@ class Factory: ...@@ -162,10 +49,16 @@ class Factory:
config.merge(local_config_file.read()) config.merge(local_config_file.read())
poetry = Poetry(poetry_file, local_config, package, locker, config) poetry = Poetry(
base_poetry.file.path,
base_poetry.local_config,
base_poetry.package,
locker,
config,
)
# Configuring sources # Configuring sources
for source in local_config.get("source", []): for source in poetry.local_config.get("source", []):
repository = self.create_legacy_repository(source, config) repository = self.create_legacy_repository(source, config)
is_default = source.get("default", False) is_default = source.get("default", False)
is_secondary = source.get("secondary", False) is_secondary = source.get("secondary", False)
...@@ -259,82 +152,3 @@ class Factory: ...@@ -259,82 +152,3 @@ class Factory:
cert=get_cert(auth_config, name), cert=get_cert(auth_config, name),
client_cert=get_client_cert(auth_config, name), client_cert=get_client_cert(auth_config, name),
) )
@classmethod
def validate(
cls, config, strict=False
): # type: (dict, bool) -> Dict[str, List[str]]
"""
Checks the validity of a configuration
"""
result = {"errors": [], "warnings": []}
# Schema validation errors
validation_errors = validate_object(config, "poetry-schema")
result["errors"] += validation_errors
if strict:
# If strict, check the file more thoroughly
# Checking license
license = config.get("license")
if license:
try:
license_by_id(license)
except ValueError:
result["errors"].append("{} is not a valid license".format(license))
if "dependencies" in config:
python_versions = config["dependencies"]["python"]
if python_versions == "*":
result["warnings"].append(
"A wildcard Python dependency is ambiguous. "
"Consider specifying a more explicit one."
)
for name, constraint in config["dependencies"].items():
if not isinstance(constraint, dict):
continue
if "allows-prereleases" in constraint:
result["warnings"].append(
'The "{}" dependency specifies '
'the "allows-prereleases" property, which is deprecated. '
'Use "allow-prereleases" instead.'.format(name)
)
# Checking for scripts with extras
if "scripts" in config:
scripts = config["scripts"]
for name, script in scripts.items():
if not isinstance(script, dict):
continue
extras = script["extras"]
for extra in extras:
if extra not in config["extras"]:
result["errors"].append(
'Script "{}" requires extra "{}" which is not defined.'.format(
name, extra
)
)
return result
@classmethod
def locate(cls, cwd): # type: (Path) -> Path
candidates = [Path(cwd)]
candidates.extend(Path(cwd).parents)
for path in candidates:
poetry_file = path / "pyproject.toml"
if poetry_file.exists():
return poetry_file
else:
raise RuntimeError(
"Poetry could not find a pyproject.toml file in {} or its parents".format(
cwd
)
)
from typing import TYPE_CHECKING
from poetry.utils._compat import urlparse
from poetry.utils.password_manager import PasswordManager
if TYPE_CHECKING:
from typing import Any
from typing import Optional
from typing import Tuple
from clikit.api.io import IO
from requests import Request # noqa
from requests import Response # noqa
from requests import Session # noqa
from poetry.config.config import Config
class Authenticator(object):
def __init__(self, config, io): # type: (Config, IO) -> None
self._config = config
self._io = io
self._session = None
self._credentials = {}
self._password_manager = PasswordManager(self._config)
@property
def session(self): # type: () -> Session
from requests import Session # noqa
if self._session is None:
self._session = Session()
return self._session
def request(self, method, url, **kwargs): # type: (str, str, Any) -> Response
from requests import Request # noqa
from requests.auth import HTTPBasicAuth
request = Request(method, url)
username, password = self._get_credentials_for_url(url)
if username is not None and password is not None:
request = HTTPBasicAuth(username, password)(request)
session = self.session
prepared_request = session.prepare_request(request)
proxies = kwargs.get("proxies", {})
stream = kwargs.get("stream")
verify = kwargs.get("verify")
cert = kwargs.get("cert")
settings = session.merge_environment_settings(
prepared_request.url, proxies, stream, verify, cert
)
# Send the request.
send_kwargs = {
"timeout": kwargs.get("timeout"),
"allow_redirects": kwargs.get("allow_redirects", True),
}
send_kwargs.update(settings)
resp = session.send(prepared_request, **send_kwargs)
resp.raise_for_status()
return resp
def _get_credentials_for_url(
self, url
): # type: (str) -> Tuple[Optional[str], Optional[str]]
parsed_url = urlparse.urlsplit(url)
netloc = parsed_url.netloc
credentials = self._credentials.get(netloc, (None, None))
if credentials == (None, None):
if "@" not in netloc:
credentials = self._get_credentials_for_netloc_from_config(netloc)
else:
# Split from the right because that's how urllib.parse.urlsplit()
# behaves if more than one @ is present (which can be checked using
# the password attribute of urlsplit()'s return value).
auth, netloc = netloc.rsplit("@", 1)
if ":" in auth:
# Split from the left because that's how urllib.parse.urlsplit()
# behaves if more than one : is present (which again can be checked
# using the password attribute of the return value)
credentials = auth.split(":", 1)
else:
credentials = auth, None
credentials = tuple(
None if x is None else urlparse.unquote(x) for x in credentials
)
if credentials[0] is not None or credentials[1] is not None:
credentials = (credentials[0] or "", credentials[1] or "")
self._credentials[netloc] = credentials
return credentials[0], credentials[1]
def _get_credentials_for_netloc_from_config(
self, netloc
): # type: (str) -> Tuple[Optional[str], Optional[str]]
credentials = (None, None)
for repository_name in self._config.get("http-basic", {}):
repository_config = self._config.get(
"repositories.{}".format(repository_name)
)
if not repository_config:
continue
url = repository_config.get("url")
if not url:
continue
parsed_url = urlparse.urlsplit(url)
if netloc == parsed_url.netloc:
auth = self._password_manager.get_http_auth(repository_name)
if auth is None:
continue
return auth["username"], auth["password"]
return credentials
import hashlib
import json
from typing import TYPE_CHECKING
from poetry.core.packages.utils.link import Link
from poetry.utils._compat import Path
from .chooser import InvalidWheelName
from .chooser import Wheel
if TYPE_CHECKING:
from typing import List
from typing import Optional
from poetry.config.config import Config
from poetry.utils.env import Env
class Chef:
def __init__(self, config, env): # type: (Config, Env) -> None
self._config = config
self._env = env
self._cache_dir = (
Path(config.get("cache-dir")).expanduser().joinpath("artifacts")
)
def prepare(self, archive): # type: (Path) -> Path
return archive
def prepare_sdist(self, archive): # type: (Path) -> Path
return archive
def prepare_wheel(self, archive): # type: (Path) -> Path
return archive
def should_prepare(self, archive): # type: (Path) -> bool
return not self.is_wheel(archive)
def is_wheel(self, archive): # type: (Path) -> bool
return archive.suffix == ".whl"
def get_cached_archive_for_link(self, link): # type: (Link) -> Optional[Link]
# If the archive is already a wheel, there is no need to cache it.
if link.is_wheel:
pass
archives = self.get_cached_archives_for_link(link)
if not archives:
return link
candidates = []
for archive in archives:
if not archive.is_wheel:
candidates.append((float("inf"), archive))
continue
try:
wheel = Wheel(archive.filename)
except InvalidWheelName:
continue
if not wheel.is_supported_by_environment(self._env):
continue
candidates.append(
(wheel.get_minimum_supported_index(self._env.supported_tags), archive),
)
if not candidates:
return link
return min(candidates)[1]
def get_cached_archives_for_link(self, link): # type: (Link) -> List[Link]
cache_dir = self.get_cache_directory_for_link(link)
archive_types = ["whl", "tar.gz", "tar.bz2", "bz2", "zip"]
links = []
for archive_type in archive_types:
for archive in cache_dir.glob("*.{}".format(archive_type)):
links.append(Link("file://{}".format(str(archive))))
return links
def get_cache_directory_for_link(self, link): # type: (Link) -> Path
key_parts = {"url": link.url_without_fragment}
if link.hash_name is not None and link.hash is not None:
key_parts[link.hash_name] = link.hash
if link.subdirectory_fragment:
key_parts["subdirectory"] = link.subdirectory_fragment
key_parts["interpreter_name"] = self._env.marker_env["interpreter_name"]
key_parts["interpreter_version"] = "".join(
self._env.marker_env["interpreter_version"].split(".")[:2]
)
key = hashlib.sha256(
json.dumps(
key_parts, sort_keys=True, separators=(",", ":"), ensure_ascii=True
).encode("ascii")
).hexdigest()
split_key = [key[:2], key[2:4], key[4:6], key[6:]]
return self._cache_dir.joinpath(*split_key)
import re
from typing import List
from typing import Tuple
from packaging.tags import Tag
from poetry.core.packages.package import Package
from poetry.core.packages.utils.link import Link
from poetry.repositories.pool import Pool
from poetry.utils.env import Env
from poetry.utils.patterns import wheel_file_re
class InvalidWheelName(Exception):
pass
class Wheel(object):
def __init__(self, filename): # type: (str) -> None
wheel_info = wheel_file_re.match(filename)
if not wheel_info:
raise InvalidWheelName("{} is not a valid wheel filename.".format(filename))
self.filename = filename
self.name = wheel_info.group("name").replace("_", "-")
self.version = wheel_info.group("ver").replace("_", "-")
self.build_tag = wheel_info.group("build")
self.pyversions = wheel_info.group("pyver").split(".")
self.abis = wheel_info.group("abi").split(".")
self.plats = wheel_info.group("plat").split(".")
self.tags = {
Tag(x, y, z) for x in self.pyversions for y in self.abis for z in self.plats
}
def get_minimum_supported_index(self, tags):
indexes = [tags.index(t) for t in self.tags if t in tags]
return min(indexes) if indexes else None
def is_supported_by_environment(self, env):
return bool(set(env.supported_tags).intersection(self.tags))
class Chooser:
"""
A Chooser chooses an appropriate release archive for packages.
"""
def __init__(self, pool, env): # type: (Pool, Env) -> None
self._pool = pool
self._env = env
def choose_for(self, package): # type: (Package) -> Link
"""
Return the url of the selected archive for a given package.
"""
links = []
for link in self._get_links(package):
if link.is_wheel and not Wheel(link.filename).is_supported_by_environment(
self._env
):
continue
if link.ext == ".egg":
continue
links.append(link)
if not links:
raise RuntimeError(
"Unable to find installation candidates for {}".format(package)
)
# Get the best link
chosen = max(links, key=lambda link: self._sort_key(package, link))
if not chosen:
raise RuntimeError(
"Unable to find installation candidates for {}".format(package)
)
return chosen
def _get_links(self, package): # type: (Package) -> List[Link]
if not package.source_type:
if not self._pool.has_repository("pypi"):
repository = self._pool.repositories[0]
else:
repository = self._pool.repository("pypi")
else:
repository = self._pool.repository(package.source_reference)
links = repository.find_links_for_package(package)
hashes = [f["hash"] for f in package.files]
if not hashes:
return links
selected_links = []
for link in links:
if not link.hash:
selected_links.append(link)
continue
h = link.hash_name + ":" + link.hash
if h not in hashes:
continue
selected_links.append(link)
return selected_links
def _sort_key(self, package, link): # type: (Package, Link) -> Tuple
"""
Function to pass as the `key` argument to a call to sorted() to sort
InstallationCandidates by preference.
Returns a tuple such that tuples sorting as greater using Python's
default comparison operator are more preferred.
The preference is as follows:
First and foremost, candidates with allowed (matching) hashes are
always preferred over candidates without matching hashes. This is
because e.g. if the only candidate with an allowed hash is yanked,
we still want to use that candidate.
Second, excepting hash considerations, candidates that have been
yanked (in the sense of PEP 592) are always less preferred than
candidates that haven't been yanked. Then:
If not finding wheels, they are sorted by version only.
If finding wheels, then the sort order is by version, then:
1. existing installs
2. wheels ordered via Wheel.support_index_min(self._supported_tags)
3. source archives
If prefer_binary was set, then all wheels are sorted above sources.
Note: it was considered to embed this logic into the Link
comparison operators, but then different sdist links
with the same version, would have to be considered equal
"""
support_num = len(self._env.supported_tags)
build_tag = ()
binary_preference = 0
if link.is_wheel:
wheel = Wheel(link.filename)
if not wheel.is_supported_by_environment(self._env):
raise RuntimeError(
"{} is not a supported wheel for this platform. It "
"can't be sorted.".format(wheel.filename)
)
# TODO: Binary preference
pri = -(wheel.get_minimum_supported_index(self._env.supported_tags))
if wheel.build_tag is not None:
match = re.match(r"^(\d+)(.*)$", wheel.build_tag)
build_tag_groups = match.groups()
build_tag = (int(build_tag_groups[0]), build_tag_groups[1])
else: # sdist
pri = -support_num
has_allowed_hash = int(self._is_link_hash_allowed_for_package(link, package))
# TODO: Proper yank value
yank_value = 0
return (
has_allowed_hash,
yank_value,
binary_preference,
package.version,
build_tag,
pri,
)
def _is_link_hash_allowed_for_package(
self, link, package
): # type: (Link, Package) -> bool
if not link.hash:
return True
h = link.hash_name + ":" + link.hash
return h in {f["hash"] for f in package.files}
...@@ -2,8 +2,8 @@ from .operation import Operation ...@@ -2,8 +2,8 @@ from .operation import Operation
class Install(Operation): class Install(Operation):
def __init__(self, package, reason=None): def __init__(self, package, reason=None, priority=0):
super(Install, self).__init__(reason) super(Install, self).__init__(reason, priority=priority)
self._package = package self._package = package
......
...@@ -4,11 +4,14 @@ from typing import Union ...@@ -4,11 +4,14 @@ from typing import Union
class Operation(object): class Operation(object):
def __init__(self, reason=None): # type: (Union[str, None]) -> None def __init__(
self, reason=None, priority=0
): # type: (Union[str, None], int) -> None
self._reason = reason self._reason = reason
self._skipped = False self._skipped = False
self._skip_reason = None self._skip_reason = None
self._priority = priority
@property @property
def job_type(self): # type: () -> str def job_type(self): # type: () -> str
...@@ -27,6 +30,10 @@ class Operation(object): ...@@ -27,6 +30,10 @@ class Operation(object):
return self._skip_reason return self._skip_reason
@property @property
def priority(self): # type: () -> int
return self._priority
@property
def package(self): def package(self):
raise NotImplementedError() raise NotImplementedError()
......
...@@ -2,8 +2,8 @@ from .operation import Operation ...@@ -2,8 +2,8 @@ from .operation import Operation
class Uninstall(Operation): class Uninstall(Operation):
def __init__(self, package, reason=None): def __init__(self, package, reason=None, priority=float("inf")):
super(Uninstall, self).__init__(reason) super(Uninstall, self).__init__(reason, priority=priority)
self._package = package self._package = package
......
...@@ -2,11 +2,11 @@ from .operation import Operation ...@@ -2,11 +2,11 @@ from .operation import Operation
class Update(Operation): class Update(Operation):
def __init__(self, initial, target, reason=None): def __init__(self, initial, target, reason=None, priority=0):
self._initial_package = initial self._initial_package = initial
self._target_package = target self._target_package = target
super(Update, self).__init__(reason) super(Update, self).__init__(reason, priority=priority)
@property @property
def initial_package(self): def initial_package(self):
......
...@@ -4,7 +4,6 @@ import tempfile ...@@ -4,7 +4,6 @@ import tempfile
from subprocess import CalledProcessError from subprocess import CalledProcessError
from clikit.api.io import IO from clikit.api.io import IO
from clikit.io import NullIO
from poetry.repositories.pool import Pool from poetry.repositories.pool import Pool
from poetry.utils._compat import encode from poetry.utils._compat import encode
...@@ -181,6 +180,8 @@ class PipInstaller(BaseInstaller): ...@@ -181,6 +180,8 @@ class PipInstaller(BaseInstaller):
def install_directory(self, package): def install_directory(self, package):
from poetry.factory import Factory from poetry.factory import Factory
from poetry.io.null_io import NullIO
from poetry.masonry.builders.editable import EditableBuilder
from poetry.utils.toml_file import TomlFile from poetry.utils.toml_file import TomlFile
if package.root_dir: if package.root_dir:
...@@ -200,41 +201,53 @@ class PipInstaller(BaseInstaller): ...@@ -200,41 +201,53 @@ class PipInstaller(BaseInstaller):
"tool" in pyproject_content and "poetry" in pyproject_content["tool"] "tool" in pyproject_content and "poetry" in pyproject_content["tool"]
) )
# Even if there is a build system specified # Even if there is a build system specified
# pip as of right now does not support it fully # some versions of pip (< 19.0.0) don't understand it
# TODO: Check for pip version when proper PEP-517 support lands # so we need to check the version of pip to know
# has_build_system = ("build-system" in pyproject_content) # if we can rely on the build system
pip_version = self._env.pip_version
setup = os.path.join(req, "setup.py") pip_version_with_build_system_support = pip_version.__class__(19, 0, 0)
has_setup = os.path.exists(setup) has_build_system = (
if has_poetry and (package.develop or not has_build_system): "build-system" in pyproject_content
# We actually need to rely on creating a temporary setup.py and pip_version >= pip_version_with_build_system_support
# file since pip, as of this comment, does not support
# build-system for editable packages
# We also need it for non-PEP-517 packages
from poetry.masonry.builders.editable import EditableBuilder
builder = EditableBuilder(
Factory().create_poetry(pyproject.parent), self._env, NullIO()
) )
builder.build() if has_poetry:
package_poetry = Factory().create_poetry(pyproject.parent)
if package.develop and not package_poetry.package.build_script:
# This is a Poetry package in editable mode
# we can use the EditableBuilder without going through pip
# to install it, unless it has a build script.
builder = EditableBuilder(package_poetry, self._env, NullIO())
builder.build()
return return
elif not has_build_system or package_poetry.package.build_script:
from poetry.core.masonry.builders.sdist import SdistBuilder
# We need to rely on creating a temporary setup.py
# file since the version of pip does not support
# build-systems
# We also need it for non-PEP-517 packages
builder = SdistBuilder(package_poetry)
with builder.setup_py():
if package.develop:
args.append("-e")
args.append(req)
return self.run(*args)
if package.develop: if package.develop:
args.append("-e") args.append("-e")
args.append(req) args.append(req)
try: return self.run(*args)
return self.run(*args)
finally:
if not has_setup and os.path.exists(setup):
os.remove(setup)
def install_git(self, package): def install_git(self, package):
from poetry.packages import Package from poetry.core.packages import Package
from poetry.vcs import Git from poetry.core.vcs import Git
src_dir = self._env.path / "src" / package.name src_dir = self._env.path / "src" / package.name
if src_dir.exists(): if src_dir.exists():
......
...@@ -38,7 +38,7 @@ license = "" ...@@ -38,7 +38,7 @@ license = ""
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
""" """
BUILD_SYSTEM_MIN_VERSION = "0.12" BUILD_SYSTEM_MIN_VERSION = "1.0.0a5"
BUILD_SYSTEM_MAX_VERSION = None BUILD_SYSTEM_MAX_VERSION = None
...@@ -109,8 +109,8 @@ class Layout(object): ...@@ -109,8 +109,8 @@ class Layout(object):
if BUILD_SYSTEM_MAX_VERSION is not None: if BUILD_SYSTEM_MAX_VERSION is not None:
build_system_version += ",<" + BUILD_SYSTEM_MAX_VERSION build_system_version += ",<" + BUILD_SYSTEM_MAX_VERSION
build_system.add("requires", ["poetry" + build_system_version]) build_system.add("requires", ["poetry-core" + build_system_version])
build_system.add("build-backend", "poetry.masonry.api") build_system.add("build-backend", "poetry.core.masonry.api")
content.add("build-system", build_system) content.add("build-system", build_system)
......
from .utils._compat import Path
from .utils.appdirs import user_cache_dir from .utils.appdirs import user_cache_dir
from .utils.appdirs import user_config_dir from .utils.appdirs import user_config_dir
CACHE_DIR = user_cache_dir("pypoetry") CACHE_DIR = user_cache_dir("pypoetry")
CONFIG_DIR = user_config_dir("pypoetry") CONFIG_DIR = user_config_dir("pypoetry")
REPOSITORY_CACHE_DIR = Path(CACHE_DIR) / "cache" / "repositories"
"""
This module handles the packaging and publishing
of python projects.
A lot of the code used here has been taken from
`flit <https://github.com/takluyver/flit>`__ and adapted
to work with the poetry codebase, so kudos to them for showing the way.
"""
from .builder import Builder
""" from poetry.core.masonry.api import build_sdist
PEP-517 compliant buildsystem API from poetry.core.masonry.api import build_wheel
""" from poetry.core.masonry.api import get_requires_for_build_sdist
import logging from poetry.core.masonry.api import get_requires_for_build_wheel
import sys from poetry.core.masonry.api import prepare_metadata_for_build_wheel
from clikit.io import NullIO
__all__ = [
from poetry.factory import Factory "build_sdist",
from poetry.utils._compat import Path "build_wheel",
from poetry.utils._compat import unicode "get_requires_for_build_sdist",
from poetry.utils.env import SystemEnv "get_requires_for_build_wheel",
"prepare_metadata_for_build_wheel",
from .builders.sdist import SdistBuilder ]
from .builders.wheel import WheelBuilder
log = logging.getLogger(__name__)
def get_requires_for_build_wheel(config_settings=None):
"""
Returns an additional list of requirements for building, as PEP508 strings,
above and beyond those specified in the pyproject.toml file.
This implementation is optional. At the moment it only returns an empty list, which would be the same as if
not define. So this is just for completeness for future implementation.
"""
return []
# For now, we require all dependencies to build either a wheel or an sdist.
get_requires_for_build_sdist = get_requires_for_build_wheel
def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
poetry = Factory().create_poetry(Path("."))
builder = WheelBuilder(poetry, SystemEnv(Path(sys.prefix)), NullIO())
dist_info = Path(metadata_directory, builder.dist_info)
dist_info.mkdir(parents=True, exist_ok=True)
if "scripts" in poetry.local_config or "plugins" in poetry.local_config:
with (dist_info / "entry_points.txt").open("w", encoding="utf-8") as f:
builder._write_entry_points(f)
with (dist_info / "WHEEL").open("w", encoding="utf-8") as f:
builder._write_wheel_file(f)
with (dist_info / "METADATA").open("w", encoding="utf-8") as f:
builder._write_metadata_file(f)
return dist_info.name
def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
"""Builds a wheel, places it in wheel_directory"""
poetry = Factory().create_poetry(Path("."))
return unicode(
WheelBuilder.make_in(
poetry, SystemEnv(Path(sys.prefix)), NullIO(), Path(wheel_directory)
)
)
def build_sdist(sdist_directory, config_settings=None):
"""Builds an sdist, places it in sdist_directory"""
poetry = Factory().create_poetry(Path("."))
path = SdistBuilder(poetry, SystemEnv(Path(sys.prefix)), NullIO()).build(
Path(sdist_directory)
)
return unicode(path.name)
from .builders.complete import CompleteBuilder
from .builders.sdist import SdistBuilder
from .builders.wheel import WheelBuilder
class Builder:
_FORMATS = {"sdist": SdistBuilder, "wheel": WheelBuilder, "all": CompleteBuilder}
def __init__(self, poetry, env, io):
self._poetry = poetry
self._env = env
self._io = io
def build(self, fmt):
if fmt not in self._FORMATS:
raise ValueError("Invalid format: {}".format(fmt))
builder = self._FORMATS[fmt](self._poetry, self._env, self._io)
return builder.build()
from .complete import CompleteBuilder
from .editable import EditableBuilder from .editable import EditableBuilder
from .sdist import SdistBuilder
from .wheel import WheelBuilder
# -*- coding: utf-8 -*-
import re
import shutil
import tempfile
from collections import defaultdict
from contextlib import contextmanager
from typing import Set
from typing import Union
from clikit.api.io.flags import VERY_VERBOSE
from poetry.utils._compat import Path
from poetry.utils._compat import glob
from poetry.utils._compat import lru_cache
from poetry.utils._compat import to_str
from poetry.vcs import get_vcs
from ..metadata import Metadata
from ..utils.module import Module
from ..utils.package_include import PackageInclude
AUTHOR_REGEX = re.compile(r"(?u)^(?P<name>[- .,\w\d'’\"()]+) <(?P<email>.+?)>$")
METADATA_BASE = """\
Metadata-Version: 2.1
Name: {name}
Version: {version}
Summary: {summary}
"""
class Builder(object):
AVAILABLE_PYTHONS = {"2", "2.7", "3", "3.4", "3.5", "3.6", "3.7"}
format = None
def __init__(
self, poetry, env, io, ignore_packages_formats=False
): # type: ("Poetry", "Env", "IO", bool) -> None
self._poetry = poetry
self._env = env
self._io = io
self._package = poetry.package
self._path = poetry.file.parent
self._original_path = self._path
packages = []
for p in self._package.packages:
formats = p.get("format", [])
if not isinstance(formats, list):
formats = [formats]
if (
formats
and self.format
and self.format not in formats
and not ignore_packages_formats
):
continue
packages.append(p)
self._module = Module(
self._package.name,
self._path.as_posix(),
packages=packages,
includes=self._package.include,
)
self._meta = Metadata.from_package(self._package)
def build(self):
raise NotImplementedError()
@lru_cache(maxsize=None)
def find_excluded_files(self): # type: () -> Set[str]
# Checking VCS
vcs = get_vcs(self._original_path)
if not vcs:
vcs_ignored_files = set()
else:
vcs_ignored_files = set(vcs.get_ignored_files())
explicitely_excluded = set()
for excluded_glob in self._package.exclude:
for excluded in glob(
Path(self._path, excluded_glob).as_posix(), recursive=True
):
explicitely_excluded.add(
Path(excluded).relative_to(self._path).as_posix()
)
ignored = vcs_ignored_files | explicitely_excluded
result = set()
for file in ignored:
result.add(file)
# The list of excluded files might be big and we will do a lot
# containment check (x in excluded).
# Returning a set make those tests much much faster.
return result
def is_excluded(self, filepath): # type: (Union[str, Path]) -> bool
exclude_path = Path(filepath)
while True:
if exclude_path.as_posix() in self.find_excluded_files():
return True
if len(exclude_path.parts) > 1:
exclude_path = exclude_path.parent
else:
break
return False
def find_files_to_add(self, exclude_build=True): # type: (bool) -> list
"""
Finds all files to add to the tarball
"""
to_add = []
for include in self._module.includes:
for file in include.elements:
if "__pycache__" in str(file):
continue
if file.is_dir():
continue
file = file.relative_to(self._path)
if self.is_excluded(file) and isinstance(include, PackageInclude):
continue
if file.suffix == ".pyc":
continue
if file in to_add:
# Skip duplicates
continue
self._io.write_line(
" - Adding: <comment>{}</comment>".format(str(file)), VERY_VERBOSE
)
to_add.append(file)
# Include project files
self._io.write_line(
" - Adding: <comment>pyproject.toml</comment>", VERY_VERBOSE
)
to_add.append(Path("pyproject.toml"))
# If a license file exists, add it
for license_file in self._path.glob("LICENSE*"):
self._io.write_line(
" - Adding: <comment>{}</comment>".format(
license_file.relative_to(self._path)
),
VERY_VERBOSE,
)
to_add.append(license_file.relative_to(self._path))
# If a README is specified we need to include it
# to avoid errors
if "readme" in self._poetry.local_config:
readme = self._path / self._poetry.local_config["readme"]
if readme.exists():
self._io.write_line(
" - Adding: <comment>{}</comment>".format(
readme.relative_to(self._path)
),
VERY_VERBOSE,
)
to_add.append(readme.relative_to(self._path))
# If a build script is specified and explicitely required
# we add it to the list of files
if self._package.build and not exclude_build:
to_add.append(Path(self._package.build))
return sorted(to_add)
def get_metadata_content(self): # type: () -> bytes
content = METADATA_BASE.format(
name=self._meta.name,
version=self._meta.version,
summary=to_str(self._meta.summary),
)
# Optional fields
if self._meta.home_page:
content += "Home-page: {}\n".format(self._meta.home_page)
if self._meta.license:
content += "License: {}\n".format(self._meta.license)
if self._meta.keywords:
content += "Keywords: {}\n".format(self._meta.keywords)
if self._meta.author:
content += "Author: {}\n".format(to_str(self._meta.author))
if self._meta.author_email:
content += "Author-email: {}\n".format(to_str(self._meta.author_email))
if self._meta.maintainer:
content += "Maintainer: {}\n".format(to_str(self._meta.maintainer))
if self._meta.maintainer_email:
content += "Maintainer-email: {}\n".format(
to_str(self._meta.maintainer_email)
)
if self._meta.requires_python:
content += "Requires-Python: {}\n".format(self._meta.requires_python)
for classifier in self._meta.classifiers:
content += "Classifier: {}\n".format(classifier)
for extra in sorted(self._meta.provides_extra):
content += "Provides-Extra: {}\n".format(extra)
for dep in sorted(self._meta.requires_dist):
content += "Requires-Dist: {}\n".format(dep)
for url in sorted(self._meta.project_urls, key=lambda u: u[0]):
content += "Project-URL: {}\n".format(to_str(url))
if self._meta.description_content_type:
content += "Description-Content-Type: {}\n".format(
self._meta.description_content_type
)
if self._meta.description is not None:
content += "\n" + to_str(self._meta.description) + "\n"
return content
def convert_entry_points(self): # type: () -> dict
result = defaultdict(list)
# Scripts -> Entry points
for name, ep in self._poetry.local_config.get("scripts", {}).items():
extras = ""
if isinstance(ep, dict):
extras = "[{}]".format(", ".join(ep["extras"]))
ep = ep["callable"]
result["console_scripts"].append("{} = {}{}".format(name, ep, extras))
# Plugins -> entry points
plugins = self._poetry.local_config.get("plugins", {})
for groupname, group in plugins.items():
for name, ep in sorted(group.items()):
result[groupname].append("{} = {}".format(name, ep))
for groupname in result:
result[groupname] = sorted(result[groupname])
return dict(result)
@classmethod
def convert_author(cls, author): # type: (...) -> dict
m = AUTHOR_REGEX.match(author)
name = m.group("name")
email = m.group("email")
return {"name": name, "email": email}
@classmethod
@contextmanager
def temporary_directory(cls, *args, **kwargs):
try:
from tempfile import TemporaryDirectory
with TemporaryDirectory(*args, **kwargs) as name:
yield name
except ImportError:
name = tempfile.mkdtemp(*args, **kwargs)
yield name
shutil.rmtree(name)
import os
import tarfile
from contextlib import contextmanager
from poetry.factory import Factory
from poetry.io.null_io import NullIO
from poetry.utils._compat import Path
from poetry.utils.helpers import temporary_directory
from .builder import Builder
from .sdist import SdistBuilder
from .wheel import WheelBuilder
class CompleteBuilder(Builder):
def build(self):
# We start by building the tarball
# We will use it to build the wheel
sdist_builder = SdistBuilder(self._poetry, self._env, self._io)
build_for_all_formats = False
for p in self._package.packages:
formats = p.get("format", [])
if not isinstance(formats, list):
formats = [formats]
if formats and sdist_builder.format not in formats:
build_for_all_formats = True
break
sdist_file = sdist_builder.build()
self._io.write_line("")
dist_dir = self._path / "dist"
if build_for_all_formats:
sdist_builder = SdistBuilder(
self._poetry, self._env, NullIO(), ignore_packages_formats=True
)
with temporary_directory() as tmp_dir:
sdist_file = sdist_builder.build(Path(tmp_dir))
with self.unpacked_tarball(sdist_file) as tmpdir:
WheelBuilder.make_in(
Factory().create_poetry(tmpdir),
self._env,
self._io,
dist_dir,
original=self._poetry,
)
else:
with self.unpacked_tarball(sdist_file) as tmpdir:
WheelBuilder.make_in(
Factory().create_poetry(tmpdir),
self._env,
self._io,
dist_dir,
original=self._poetry,
)
@classmethod
@contextmanager
def unpacked_tarball(cls, path):
tf = tarfile.open(str(path))
with cls.temporary_directory() as tmpdir:
tf.extractall(tmpdir)
files = os.listdir(tmpdir)
assert len(files) == 1, files
yield Path(tmpdir) / files[0]
from __future__ import unicode_literals from __future__ import unicode_literals
import hashlib
import os import os
import shutil import shutil
from collections import defaultdict from base64 import urlsafe_b64encode
from poetry.semver.version import Version from poetry.core.masonry.builders.builder import Builder
from poetry.core.masonry.builders.sdist import SdistBuilder
from poetry.core.masonry.utils.package_include import PackageInclude
from poetry.core.semver.version import Version
from poetry.utils._compat import WINDOWS
from poetry.utils._compat import Path
from poetry.utils._compat import decode from poetry.utils._compat import decode
from .builder import Builder
from .sdist import SdistBuilder SCRIPT_TEMPLATE = """\
#!{python}
from {module} import {callable_holder}
if __name__ == '__main__':
{callable_}()
"""
WINDOWS_CMD_TEMPLATE = """\
@echo off\r\n"{python}" "%~dp0\\{script}" %*\r\n
"""
class EditableBuilder(Builder): class EditableBuilder(Builder):
def __init__(self, poetry, env, io):
super(EditableBuilder, self).__init__(poetry)
self._env = env
self._io = io
def build(self): def build(self):
return self._setup_build() self._debug(
" - Building package <c1>{}</c1> in <info>editable</info> mode".format(
self._package.name
)
)
if self._package.build_script:
self._debug(
" - <warning>Falling back on using a <b>setup.py</b></warning>"
)
return self._setup_build()
added_files = []
added_files += self._add_pth()
added_files += self._add_scripts()
self._add_dist_info(added_files)
def _setup_build(self): def _setup_build(self):
builder = SdistBuilder(self._poetry, self._env, self._io) builder = SdistBuilder(self._poetry)
setup = self._path / "setup.py" setup = self._path / "setup.py"
has_setup = setup.exists() has_setup = setup.exists()
...@@ -31,14 +68,14 @@ class EditableBuilder(Builder): ...@@ -31,14 +68,14 @@ class EditableBuilder(Builder):
try: try:
if self._env.pip_version < Version(19, 0): if self._env.pip_version < Version(19, 0):
self._env.run_pip("install", "-e", str(self._path)) self._env.run_pip("install", "-e", str(self._path), "--no-deps")
else: else:
# Temporarily rename pyproject.toml # Temporarily rename pyproject.toml
shutil.move( shutil.move(
str(self._poetry.file), str(self._poetry.file.with_suffix(".tmp")) str(self._poetry.file), str(self._poetry.file.with_suffix(".tmp"))
) )
try: try:
self._env.run_pip("install", "--no-deps", "-e", str(self._path)) self._env.run_pip("install", "-e", str(self._path), "--no-deps")
finally: finally:
shutil.move( shutil.move(
str(self._poetry.file.with_suffix(".tmp")), str(self._poetry.file.with_suffix(".tmp")),
...@@ -48,71 +85,137 @@ class EditableBuilder(Builder): ...@@ -48,71 +85,137 @@ class EditableBuilder(Builder):
if not has_setup: if not has_setup:
os.remove(str(setup)) os.remove(str(setup))
def _build_egg_info(self): def _add_pth(self):
egg_info = self._path / "{}.egg-info".format( pth = self._env.site_packages.joinpath(self._module.name).with_suffix(".pth")
self._package.name.replace("-", "_") self._debug(
" - Adding <c2>{}</c2> to <b>{}</b> for {}".format(
pth.name, self._env.site_packages, self._poetry.file.parent
)
) )
egg_info.mkdir(exist_ok=True)
with egg_info.joinpath("PKG-INFO").open("w", encoding="utf-8") as f: paths = set()
f.write(decode(self.get_metadata_content())) for include in self._module.includes:
if isinstance(include, PackageInclude) and (
include.is_module() or include.is_package()
):
paths.add(include.base.resolve().as_posix())
with pth.open("w", encoding="utf-8") as f:
for path in paths:
f.write(decode(path + os.linesep))
return [pth]
def _add_scripts(self):
added = []
entry_points = self.convert_entry_points()
scripts_path = Path(self._env.paths["scripts"])
scripts = entry_points.get("console_scripts", [])
for script in scripts:
name, script = script.split(" = ")
module, callable_ = script.split(":")
callable_holder = callable_.rsplit(".", 1)[0]
script_file = scripts_path.joinpath(name)
self._debug(
" - Adding the <c2>{}</c2> script to <b>{}</b>".format(
name, scripts_path
)
)
with script_file.open("w", encoding="utf-8") as f:
f.write(
decode(
SCRIPT_TEMPLATE.format(
python=self._env._bin("python"),
module=module,
callable_holder=callable_holder,
callable_=callable_,
)
)
)
script_file.chmod(0o755)
added.append(script_file)
if WINDOWS:
cmd_script = script_file.with_suffix(".cmd")
cmd = WINDOWS_CMD_TEMPLATE.format(
python=self._env._bin("python"), script=name
)
self._debug(
" - Adding the <c2>{}</c2> script wrapper to <b>{}</b>".format(
cmd_script.name, scripts_path
)
)
with egg_info.joinpath("entry_points.txt").open("w", encoding="utf-8") as f: with cmd_script.open("w", encoding="utf-8") as f:
entry_points = self.convert_entry_points() f.write(decode(cmd))
for group_name in sorted(entry_points): added.append(cmd_script)
f.write("[{}]\n".format(group_name))
for ep in sorted(entry_points[group_name]):
f.write(ep.replace(" ", "") + "\n")
f.write("\n") return added
def _add_dist_info(self, added_files):
from poetry.core.masonry.builders.wheel import WheelBuilder
added_files = added_files[:]
builder = WheelBuilder(self._poetry)
dist_info = self._env.site_packages.joinpath(builder.dist_info)
self._debug(
" - Adding the <c2>{}</c2> directory to <b>{}</b>".format(
dist_info.name, self._env.site_packages
)
)
with egg_info.joinpath("requires.txt").open("w", encoding="utf-8") as f: if dist_info.exists():
f.write(self._generate_requires()) shutil.rmtree(str(dist_info))
def _build_egg_link(self): dist_info.mkdir()
egg_link = self._env.site_packages / "{}.egg-link".format(self._package.name)
with egg_link.open("w", encoding="utf-8") as f:
f.write(str(self._poetry.file.parent.resolve()) + "\n")
f.write(".")
def _add_easy_install_entry(self): with dist_info.joinpath("METADATA").open("w", encoding="utf-8") as f:
easy_install_pth = self._env.site_packages / "easy-install.pth" builder._write_metadata_file(f)
path = str(self._poetry.file.parent.resolve())
content = ""
if easy_install_pth.exists():
with easy_install_pth.open(encoding="utf-8") as f:
content = f.read()
if path in content: added_files.append(dist_info.joinpath("METADATA"))
return
content += "{}\n".format(path) with dist_info.joinpath("INSTALLER").open("w", encoding="utf-8") as f:
f.write("poetry")
with easy_install_pth.open("w", encoding="utf-8") as f: added_files.append(dist_info.joinpath("INSTALLER"))
f.write(content)
def _generate_requires(self): if self.convert_entry_points():
extras = defaultdict(list) with dist_info.joinpath("entry_points.txt").open(
"w", encoding="utf-8"
) as f:
builder._write_entry_points(f)
requires = "" added_files.append(dist_info.joinpath("entry_points.txt"))
for dep in sorted(self._package.requires, key=lambda d: d.name):
marker = dep.marker
if marker.is_any():
requires += "{}\n".format(dep.base_pep_508_name)
continue
extras[str(marker)].append(dep.base_pep_508_name) with dist_info.joinpath("RECORD").open("w", encoding="utf-8") as f:
for path in added_files:
hash = self._get_file_hash(path)
size = path.stat().st_size
f.write("{},sha256={},{}\n".format(str(path), hash, size))
if extras: # RECORD itself is recorded with no hash or size
requires += "\n" f.write("{},,\n".format(dist_info.joinpath("RECORD")))
for marker, deps in sorted(extras.items()): def _get_file_hash(self, filepath):
requires += "[:{}]\n".format(marker) hashsum = hashlib.sha256()
with filepath.open("rb") as src:
while True:
buf = src.read(1024 * 8)
if not buf:
break
hashsum.update(buf)
for dep in deps: src.seek(0)
requires += dep + "\n"
requires += "\n" return urlsafe_b64encode(hashsum.digest()).decode("ascii").rstrip("=")
return requires def _debug(self, msg):
if self._io.is_debug():
self._io.write_line(msg)
from poetry.utils.helpers import canonicalize_name
from poetry.utils.helpers import normalize_version
from poetry.version.helpers import format_python_constraint
class Metadata:
metadata_version = "2.1"
# version 1.0
name = None
version = None
platforms = ()
supported_platforms = ()
summary = None
description = None
keywords = None
home_page = None
download_url = None
author = None
author_email = None
license = None
# version 1.1
classifiers = ()
requires = ()
provides = ()
obsoletes = ()
# version 1.2
maintainer = None
maintainer_email = None
requires_python = None
requires_external = ()
requires_dist = []
provides_dist = ()
obsoletes_dist = ()
project_urls = ()
# Version 2.1
description_content_type = None
provides_extra = []
@classmethod
def from_package(cls, package): # type: (...) -> Metadata
meta = cls()
meta.name = canonicalize_name(package.name)
meta.version = normalize_version(package.version.text)
meta.summary = package.description
if package.readme:
with package.readme.open(encoding="utf-8") as f:
meta.description = f.read()
meta.keywords = ",".join(package.keywords)
meta.home_page = package.homepage or package.repository_url
meta.author = package.author_name
meta.author_email = package.author_email
if package.license:
meta.license = package.license.id
meta.classifiers = package.all_classifiers
# Version 1.2
meta.maintainer = package.maintainer_name
meta.maintainer_email = package.maintainer_email
# Requires python
if package.python_versions != "*":
meta.requires_python = format_python_constraint(package.python_constraint)
meta.requires_dist = [d.to_pep_508() for d in package.requires]
# Version 2.1
if package.readme:
if package.readme.suffix == ".rst":
meta.description_content_type = "text/x-rst"
elif package.readme.suffix in [".md", ".markdown"]:
meta.description_content_type = "text/markdown"
else:
meta.description_content_type = "text/plain"
meta.provides_extra = [e for e in package.extras]
if package.urls:
for name, url in package.urls.items():
if name == "Homepage" and meta.home_page == url:
continue
meta.project_urls += ("{}, {}".format(name, url),)
return meta
import re
def normalize_file_permissions(st_mode):
"""
Normalizes the permission bits in the st_mode field from stat to 644/755
Popular VCSs only track whether a file is executable or not. The exact
permissions can vary on systems with different umasks. Normalising
to 644 (non executable) or 755 (executable) makes builds more reproducible.
"""
# Set 644 permissions, leaving higher bits of st_mode unchanged
new_mode = (st_mode | 0o644) & ~0o133
if st_mode & 0o100:
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)
from typing import List
from typing import Optional
from poetry.utils._compat import Path
class Include(object):
"""
Represents an "include" entry.
It can be a glob string, a single file or a directory.
This class will then detect the type of this include:
- a package
- a module
- a file
- a directory
"""
def __init__(
self, base, include, formats=None
): # type: (Path, str, Optional[List[str]]) -> None
self._base = base
self._include = str(include)
self._formats = formats
self._elements = sorted(list(self._base.glob(str(self._include))))
@property
def base(self): # type: () -> Path
return self._base
@property
def elements(self): # type: () -> List[Path]
return self._elements
@property
def formats(self): # type: () -> Optional[List[str]]
return self._formats
def is_empty(self): # type: () -> bool
return len(self._elements) == 0
def refresh(self): # type: () -> Include
self._elements = sorted(list(self._base.glob(self._include)))
return self
from typing import List
from poetry.utils._compat import Path
from poetry.utils.helpers import module_name
from .include import Include
from .package_include import PackageInclude
class ModuleOrPackageNotFound(ValueError):
pass
class Module:
def __init__(self, name, directory=".", packages=None, includes=None):
self._name = module_name(name)
self._in_src = False
self._is_package = False
self._path = Path(directory)
self._includes = []
packages = packages or []
includes = includes or []
if not packages:
# It must exist either as a .py file or a directory, but not both
pkg_dir = Path(directory, self._name)
py_file = Path(directory, self._name + ".py")
if pkg_dir.is_dir() and py_file.is_file():
raise ValueError("Both {} and {} exist".format(pkg_dir, py_file))
elif pkg_dir.is_dir():
packages = [{"include": str(pkg_dir.relative_to(self._path))}]
elif py_file.is_file():
packages = [{"include": str(py_file.relative_to(self._path))}]
else:
# Searching for a src module
src = Path(directory, "src")
src_pkg_dir = src / self._name
src_py_file = src / (self._name + ".py")
if src_pkg_dir.is_dir() and src_py_file.is_file():
raise ValueError("Both {} and {} exist".format(pkg_dir, py_file))
elif src_pkg_dir.is_dir():
packages = [
{
"include": str(src_pkg_dir.relative_to(src)),
"from": str(src.relative_to(self._path)),
}
]
elif src_py_file.is_file():
packages = [
{
"include": str(src_py_file.relative_to(src)),
"from": str(src.relative_to(self._path)),
}
]
else:
raise ModuleOrPackageNotFound(
"No file/folder found for package {}".format(name)
)
for package in packages:
formats = package.get("format")
if formats and not isinstance(formats, list):
formats = [formats]
self._includes.append(
PackageInclude(
self._path,
package["include"],
formats=formats,
source=package.get("from"),
)
)
for include in includes:
self._includes.append(Include(self._path, include))
@property
def name(self): # type: () -> str
return self._name
@property
def path(self): # type: () -> Path
return self._path
@property
def file(self): # type: () -> Path
if self._is_package:
return self._path / "__init__.py"
else:
return self._path
@property
def includes(self): # type: () -> List
return self._includes
def is_package(self): # type: () -> bool
return self._is_package
def is_in_src(self): # type: () -> bool
return self._in_src
from .include import Include
class PackageInclude(Include):
def __init__(self, base, include, formats=None, source=None):
self._package = None
self._is_package = False
self._is_module = False
self._source = source
if source is not None:
base = base / source
super(PackageInclude, self).__init__(base, include, formats=formats)
self.check_elements()
@property
def package(self): # type: () -> str
return self._package
@property
def source(self): # type: () -> str
return self._source
def is_package(self): # type: () -> bool
return self._is_package
def is_module(self): # type: () -> bool
return self._is_module
def refresh(self): # type: () -> PackageInclude
super(PackageInclude, self).refresh()
return self.check_elements()
def is_stub_only(self): # type: () -> bool
# returns `True` if this a PEP 561 stub-only package,
# see [PEP 561](https://www.python.org/dev/peps/pep-0561/#stub-only-packages)
return self.package.endswith("-stubs") and all(
el.suffix == ".pyi"
or (el.parent.name == self.package and el.name == "py.typed")
for el in self.elements
if el.is_file()
)
def has_modules(self): # type: () -> bool
# Packages no longer need an __init__.py in python3, but there must
# at least be one .py file for it to be considered a package
return any(element.suffix == ".py" for element in self.elements)
def check_elements(self): # type: () -> PackageInclude
if not self._elements:
raise ValueError(
"{} does not contain any element".format(self._base / self._include)
)
root = self._elements[0]
if len(self._elements) > 1:
# Probably glob
self._is_package = True
self._package = root.parent.name
if not self.is_stub_only() and not self.has_modules():
raise ValueError("{} is not a package.".format(root.name))
else:
if root.is_dir():
# If it's a directory, we include everything inside it
self._package = root.name
self._elements = sorted(list(root.glob("**/*")))
if not self.is_stub_only() and not self.has_modules():
raise ValueError("{} is not a package.".format(root.name))
self._is_package = True
else:
self._package = root.stem
self._is_module = True
return self
"""
Generate and work with PEP 425 Compatibility Tags.
Base implementation taken from
https://github.com/pypa/wheel/blob/master/wheel/pep425tags.py
and adapted to work with poetry's env util.
"""
from __future__ import unicode_literals
import distutils.util
import sys
import warnings
def get_abbr_impl(env):
"""Return abbreviated implementation name."""
impl = env.python_implementation
if impl == "PyPy":
return "pp"
elif impl == "Jython":
return "jy"
elif impl == "IronPython":
return "ip"
elif impl == "CPython":
return "cp"
raise LookupError("Unknown Python implementation: " + impl)
def get_impl_ver(env):
"""Return implementation version."""
impl_ver = env.config_var("py_version_nodot")
if not impl_ver or get_abbr_impl(env) == "pp":
impl_ver = "".join(map(str, get_impl_version_info(env)))
return impl_ver
def get_impl_version_info(env):
"""Return sys.version_info-like tuple for use in decrementing the minor
version."""
if get_abbr_impl(env) == "pp":
# as per https://github.com/pypa/pip/issues/2882
return env.version_info[:3]
else:
return env.version_info[:2]
def get_flag(env, var, fallback, expected=True, warn=True):
"""Use a fallback method for determining SOABI flags if the needed config
var is unset or unavailable."""
val = env.config_var(var)
if val is None:
if warn:
warnings.warn(
"Config variable '{0}' is unset, Python ABI tag may "
"be incorrect".format(var),
RuntimeWarning,
2,
)
return fallback()
return val == expected
def get_abi_tag(env):
"""Return the ABI tag based on SOABI (if available) or emulate SOABI
(CPython 2, PyPy)."""
soabi = env.config_var("SOABI")
impl = get_abbr_impl(env)
if not soabi and impl in ("cp", "pp") and hasattr(sys, "maxunicode"):
d = ""
m = ""
u = ""
if get_flag(
env,
"Py_DEBUG",
lambda: hasattr(sys, "gettotalrefcount"),
warn=(impl == "cp" and env.version_info < (3, 8)),
):
d = "d"
if env.version_info < (3, 8):
if get_flag(
env, "WITH_PYMALLOC", lambda: impl == "cp", warn=(impl == "cp")
):
m = "m"
if get_flag(
env,
"Py_UNICODE_SIZE",
lambda: sys.maxunicode == 0x10FFFF,
expected=4,
warn=(impl == "cp" and env.version_info < (3, 3)),
) and env.version_info < (3, 3):
u = "u"
abi = "%s%s%s%s%s" % (impl, get_impl_ver(env), d, m, u)
elif soabi and soabi.startswith("cpython-"):
abi = "cp" + soabi.split("-")[1]
elif soabi:
abi = soabi.replace(".", "_").replace("-", "_")
else:
abi = None
return abi
def get_platform():
"""Return our platform name 'win32', 'linux_x86_64'"""
# XXX remove distutils dependency
result = distutils.util.get_platform().replace(".", "_").replace("-", "_")
if result == "linux_x86_64" and sys.maxsize == 2147483647:
# pip pull request #3497
result = "linux_i686"
return result
def get_supported(env, versions=None, supplied_platform=None):
"""Return a list of supported tags for each version specified in
`versions`.
:param versions: a list of string versions, of the form ["33", "32"],
or None. The first version will be assumed to support our ABI.
"""
supported = []
# Versions must be given with respect to the preference
if versions is None:
versions = []
version_info = get_impl_version_info(env)
major = version_info[:-1]
# Support all previous minor Python versions.
for minor in range(version_info[-1], -1, -1):
versions.append("".join(map(str, major + (minor,))))
impl = get_abbr_impl(env)
abis = []
abi = get_abi_tag(env)
if abi:
abis[0:0] = [abi]
abi3s = set()
import imp
for suffix in imp.get_suffixes():
if suffix[0].startswith(".abi"):
abi3s.add(suffix[0].split(".", 2)[1])
abis.extend(sorted(list(abi3s)))
abis.append("none")
platforms = []
if supplied_platform:
platforms.append(supplied_platform)
platforms.append(get_platform())
# Current version, current API (built specifically for our Python):
for abi in abis:
for arch in platforms:
supported.append(("%s%s" % (impl, versions[0]), abi, arch))
# abi3 modules compatible with older version of Python
for version in versions[1:]:
# abi3 was introduced in Python 3.2
if version in ("31", "30"):
break
for abi in abi3s: # empty set if not Python 3
for arch in platforms:
supported.append(("%s%s" % (impl, version), abi, arch))
# No abi / arch, but requires our implementation:
for i, version in enumerate(versions):
supported.append(("%s%s" % (impl, version), "none", "any"))
if i == 0:
# Tagged specifically as being cross-version compatible
# (with just the major version specified)
supported.append(("%s%s" % (impl, versions[0][0]), "none", "any"))
# Major Python version + platform; e.g. binaries not using the Python API
supported.append(("py%s" % (versions[0][0]), "none", arch))
# No abi / arch, generic Python
for i, version in enumerate(versions):
supported.append(("py%s" % (version,), "none", "any"))
if i == 0:
supported.append(("py%s" % (version[0]), "none", "any"))
return supported
...@@ -2,6 +2,8 @@ from typing import Dict ...@@ -2,6 +2,8 @@ from typing import Dict
from typing import List from typing import List
from typing import Tuple from typing import Tuple
from poetry.core.semver import parse_constraint
from .incompatibility import Incompatibility from .incompatibility import Incompatibility
from .incompatibility_cause import ConflictCause from .incompatibility_cause import ConflictCause
from .incompatibility_cause import PythonCause from .incompatibility_cause import PythonCause
...@@ -44,10 +46,15 @@ class _Writer: ...@@ -44,10 +46,15 @@ class _Writer:
) )
required_python_version_notification = True required_python_version_notification = True
root_constraint = parse_constraint(
incompatibility.cause.root_python_version
)
constraint = parse_constraint(incompatibility.cause.python_version)
buffer.append( buffer.append(
" - {} requires Python {}".format( " - {} requires Python {}, so it will not be satisfied for Python {}".format(
incompatibility.terms[0].dependency.name, incompatibility.terms[0].dependency.name,
incompatibility.cause.python_version, incompatibility.cause.python_version,
root_constraint.difference(constraint),
) )
) )
......
...@@ -2,8 +2,8 @@ from collections import OrderedDict ...@@ -2,8 +2,8 @@ from collections import OrderedDict
from typing import Dict from typing import Dict
from typing import List from typing import List
from poetry.packages import Dependency from poetry.core.packages import Dependency
from poetry.packages import Package from poetry.core.packages import Package
from .assignment import Assignment from .assignment import Assignment
from .incompatibility import Incompatibility from .incompatibility import Incompatibility
......
from .python_requirement_solution_provider import PythonRequirementSolutionProvider
import re
from typing import List
from crashtest.contracts.has_solutions_for_exception import HasSolutionsForException
from crashtest.contracts.solution import Solution
class PythonRequirementSolutionProvider(HasSolutionsForException):
def can_solve(self, exception): # type: (Exception) -> bool
from poetry.puzzle.exceptions import SolverProblemError
if not isinstance(exception, SolverProblemError):
return False
m = re.match(
"^The current project's Python requirement (.+) is not compatible "
"with some of the required packages Python requirement",
str(exception),
)
if not m:
return False
return True
def get_solutions(self, exception): # type: (Exception) -> List[Solution]
from ..solutions.python_requirement_solution import PythonRequirementSolution
return [PythonRequirementSolution(exception)]
from .python_requirement_solution import PythonRequirementSolution
from crashtest.contracts.solution import Solution
class PythonRequirementSolution(Solution):
def __init__(self, exception):
from poetry.mixology.incompatibility_cause import PythonCause
from poetry.core.semver import parse_constraint
self._title = "Check your dependencies Python requirement."
failure = exception.error
version_solutions = []
for incompatibility in failure._incompatibility.external_incompatibilities:
if isinstance(incompatibility.cause, PythonCause):
root_constraint = parse_constraint(
incompatibility.cause.root_python_version
)
constraint = parse_constraint(incompatibility.cause.python_version)
version_solutions.append(
"For <fg=default;options=bold>{}</>, a possible solution would be "
'to set the `<fg=default;options=bold>python</>` property to <fg=yellow>"{}"</>'.format(
incompatibility.terms[0].dependency.name,
root_constraint.intersect(constraint),
)
)
description = (
"The Python requirement can be specified via the `<fg=default;options=bold>python</>` "
"or `<fg=default;options=bold>markers</>` properties"
)
if version_solutions:
description += "\n\n" + "\n".join(version_solutions)
description += "\n"
self._description = description
@property
def solution_title(self) -> str:
return self._title
@property
def solution_description(self):
return self._description
@property
def documentation_links(self):
return [
"https://python-poetry.org/docs/dependency-specification/#python-restricted-dependencies",
"https://python-poetry.org/docs/dependency-specification/#using-environment-markers",
]
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Union from typing import Union
from poetry.packages import Dependency from poetry.core.packages import Dependency
from .set_relation import SetRelation from .set_relation import SetRelation
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import time import time
from typing import TYPE_CHECKING
from typing import Any from typing import Any
from typing import Dict from typing import Dict
from typing import List from typing import List
from typing import Union from typing import Union
from poetry.packages import Dependency from poetry.core.packages import Dependency
from poetry.packages import Package from poetry.core.packages import Package
from poetry.packages import ProjectPackage from poetry.core.packages import ProjectPackage
from poetry.puzzle.provider import Provider from poetry.core.semver import Version
from poetry.semver import Version from poetry.core.semver import VersionRange
from poetry.semver import VersionRange
from .failure import SolveFailure from .failure import SolveFailure
from .incompatibility import Incompatibility from .incompatibility import Incompatibility
...@@ -25,6 +25,10 @@ from .set_relation import SetRelation ...@@ -25,6 +25,10 @@ from .set_relation import SetRelation
from .term import Term from .term import Term
if TYPE_CHECKING:
from poetry.puzzle.provider import Provider
_conflict = object() _conflict = object()
...@@ -450,6 +454,13 @@ class VersionSolver: ...@@ -450,6 +454,13 @@ class VersionSolver:
if dependency.extras: if dependency.extras:
locked.requires_extras = dependency.extras locked.requires_extras = dependency.extras
if not dependency.transitive_marker.without_extras().is_any():
marker_intersection = dependency.transitive_marker.without_extras().intersect(
locked.dependency.marker.without_extras()
)
if not marker_intersection.is_empty():
locked.dependency.transitive_marker = marker_intersection
return locked return locked
def _log(self, text): def _log(self, text):
......
import os
import re
from poetry.semver import Version
from poetry.utils.patterns import wheel_file_re
from poetry.version.requirements import Requirement
from .dependency import Dependency
from .dependency_package import DependencyPackage from .dependency_package import DependencyPackage
from .directory_dependency import DirectoryDependency
from .file_dependency import FileDependency
from .locker import Locker from .locker import Locker
from .package import Package
from .package_collection import PackageCollection from .package_collection import PackageCollection
from .project_package import ProjectPackage
from .url_dependency import URLDependency
from .utils.link import Link
from .utils.utils import convert_markers
from .utils.utils import group_markers
from .utils.utils import is_archive_file
from .utils.utils import is_installable_dir
from .utils.utils import is_url
from .utils.utils import path_to_url
from .utils.utils import strip_extras
from .vcs_dependency import VCSDependency
def dependency_from_pep_508(name):
from poetry.vcs.git import ParsedUrl
# 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:
markers = convert_markers(req.marker)
else:
markers = {}
name = req.name
path = os.path.normpath(os.path.abspath(name))
link = None
if is_url(name):
link = Link(name)
elif req.url:
link = Link(req.url)
else:
p, extras = strip_extras(path)
if os.path.isdir(p) and (os.path.sep in name or name.startswith(".")):
if not is_installable_dir(p):
raise ValueError(
"Directory {!r} is not installable. File 'setup.py' "
"not found.".format(name)
)
link = Link(path_to_url(p))
elif is_archive_file(p):
link = Link(path_to_url(p))
# it's a local file, dir, or url
if link:
# Handle relative file URLs
if link.scheme == "file" and re.search(r"\.\./", link.url):
link = Link(path_to_url(os.path.normpath(os.path.abspath(link.path))))
# wheel file
if link.is_wheel:
m = wheel_file_re.match(link.filename)
if not m:
raise ValueError("Invalid wheel name: {}".format(link.filename))
name = m.group("name")
version = m.group("ver")
dep = Dependency(name, version)
else:
name = req.name or link.egg_fragment
if link.scheme.startswith("git+"):
url = ParsedUrl.parse(link.url)
dep = VCSDependency(name, "git", url.url, rev=url.rev)
elif link.scheme == "git":
dep = VCSDependency(name, "git", link.url_without_fragment)
elif link.scheme in ["http", "https"]:
dep = URLDependency(name, link.url_without_fragment)
else:
dep = Dependency(name, "*")
else:
if req.pretty_constraint:
constraint = req.constraint
else:
constraint = "*"
dep = Dependency(name, constraint)
if "extra" in markers:
# If we have extras, the dependency is optional
dep.deactivate()
for or_ in markers["extra"]:
for _, extra in or_:
dep.in_extras.append(extra)
if "python_version" in markers:
ors = []
for or_ in markers["python_version"]:
ands = []
for op, version in or_:
# Expand python version
if op == "==":
version = "~" + version
op = ""
elif op == "!=":
version += ".*"
elif op in ("<=", ">"):
parsed_version = Version.parse(version)
if parsed_version.precision == 1:
if op == "<=":
op = "<"
version = parsed_version.next_major.text
elif op == ">":
op = ">="
version = parsed_version.next_major.text
elif parsed_version.precision == 2:
if op == "<=":
op = "<"
version = parsed_version.next_minor.text
elif op == ">":
op = ">="
version = parsed_version.next_minor.text
elif op in ("in", "not in"):
versions = []
for v in re.split("[ ,]+", version):
split = v.split(".")
if len(split) in [1, 2]:
split.append("*")
op_ = "" if op == "in" else "!="
else:
op_ = "==" if op == "in" else "!="
versions.append(op_ + ".".join(split))
glue = " || " if op == "in" else ", "
if versions:
ands.append(glue.join(versions))
continue
ands.append("{}{}".format(op, version))
ors.append(" ".join(ands))
dep.python_versions = " || ".join(ors)
if req.marker:
dep.marker = req.marker
# Extras
for extra in req.extras:
dep.extras.append(extra)
return dep
import re
from .any_constraint import AnyConstraint
from .base_constraint import BaseConstraint
from .constraint import Constraint
from .union_constraint import UnionConstraint
BASIC_CONSTRAINT = re.compile(r"^(!?==?)?\s*([^\s]+?)\s*$")
def parse_constraint(constraints):
if constraints == "*":
return AnyConstraint()
or_constraints = re.split(r"\s*\|\|?\s*", constraints.strip())
or_groups = []
for constraints in or_constraints:
and_constraints = re.split(
r"(?<!^)(?<![=>< ,]) *(?<!-)[, ](?!-) *(?!,|$)", constraints
)
constraint_objects = []
if len(and_constraints) > 1:
for constraint in and_constraints:
constraint_objects.append(parse_single_constraint(constraint))
else:
constraint_objects.append(parse_single_constraint(and_constraints[0]))
if len(constraint_objects) == 1:
constraint = constraint_objects[0]
else:
constraint = constraint_objects[0]
for next_constraint in constraint_objects[1:]:
constraint = constraint.intersect(next_constraint)
or_groups.append(constraint)
if len(or_groups) == 1:
return or_groups[0]
else:
return UnionConstraint(*or_groups)
def parse_single_constraint(constraint): # type: (str) -> BaseConstraint
# Basic comparator
m = BASIC_CONSTRAINT.match(constraint)
if m:
op = m.group(1)
if op is None:
op = "=="
version = m.group(2).strip()
return Constraint(version, op)
raise ValueError("Could not parse version constraint: {}".format(constraint))
from .base_constraint import BaseConstraint
from .empty_constraint import EmptyConstraint
class AnyConstraint(BaseConstraint):
def allows(self, other):
return True
def allows_all(self, other):
return True
def allows_any(self, other):
return True
def difference(self, other):
if other.is_any():
return EmptyConstraint()
return other
def intersect(self, other):
return other
def union(self, other):
return AnyConstraint()
def is_any(self):
return True
def is_empty(self):
return False
def __str__(self):
return "*"
def __eq__(self, other):
return other.is_any()
class BaseConstraint(object):
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 union(self, other):
raise NotImplementedError()
def is_any(self):
return False
def is_empty(self):
return False
def __repr__(self):
return "<{} {}>".format(self.__class__.__name__, str(self))
def __eq__(self, other):
raise NotImplementedError()
import operator
from .base_constraint import BaseConstraint
from .empty_constraint import EmptyConstraint
class Constraint(BaseConstraint):
OP_EQ = operator.eq
OP_NE = operator.ne
_trans_op_str = {"=": OP_EQ, "==": OP_EQ, "!=": OP_NE}
_trans_op_int = {OP_EQ: "==", OP_NE: "!="}
def __init__(self, version, operator="=="):
if operator == "=":
operator = "=="
self._version = version
self._operator = operator
self._op = self._trans_op_str[operator]
@property
def version(self):
return self._version
@property
def operator(self):
return self._operator
def allows(self, other):
is_equal_op = self._operator == "=="
is_non_equal_op = self._operator == "!="
is_other_equal_op = other.operator == "=="
is_other_non_equal_op = other.operator == "!="
if is_equal_op and is_other_equal_op:
return self._version == other.version
if (
is_equal_op
and is_other_non_equal_op
or is_non_equal_op
and is_other_equal_op
or is_non_equal_op
and is_other_non_equal_op
):
return self._version != other.version
return False
def allows_all(self, other):
if not isinstance(other, Constraint):
return other.is_empty()
return other == self
def allows_any(self, other):
if isinstance(other, Constraint):
is_non_equal_op = self._operator == "!="
is_other_non_equal_op = other.operator == "!="
if is_non_equal_op and is_other_non_equal_op:
return self._version != other.version
return other.allows(self)
def difference(self, other):
if other.allows(self):
return EmptyConstraint()
return self
def intersect(self, other):
from .multi_constraint import MultiConstraint
if isinstance(other, Constraint):
if other == self:
return self
if self.operator == "!=" and other.operator == "==" and self.allows(other):
return other
if other.operator == "!=" and self.operator == "==" and other.allows(self):
return self
if other.operator == "!=" and self.operator == "!=":
return MultiConstraint(self, other)
return EmptyConstraint()
return other.intersect(self)
def union(self, other):
if isinstance(other, Constraint):
from .union_constraint import UnionConstraint
return UnionConstraint(self, other)
return other.union(self)
def is_any(self):
return False
def is_empty(self):
return False
def __eq__(self, other):
if not isinstance(other, Constraint):
return NotImplemented
return (self.version, self.operator) == (other.version, other.operator)
def __hash__(self):
return hash((self._operator, self._version))
def __str__(self):
return "{}{}".format(
self._operator if self._operator != "==" else "", self._version
)
from .base_constraint import BaseConstraint
class EmptyConstraint(BaseConstraint):
pretty_string = None
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 __eq__(self, other):
return other.is_empty()
def __str__(self):
return ""
from .base_constraint import BaseConstraint
from .constraint import Constraint
class MultiConstraint(BaseConstraint):
def __init__(self, *constraints):
if any(c.operator == "==" for c in constraints):
raise ValueError(
"A multi-constraint can only be comprised of negative constraints"
)
self._constraints = constraints
@property
def constraints(self):
return self._constraints
def allows(self, other):
for constraint in self._constraints:
if not constraint.allows(other):
return False
return True
def allows_all(self, other):
if other.is_any():
return False
if other.is_empty():
return True
if isinstance(other, Constraint):
return self.allows(other)
our_constraints = iter(self._constraints)
their_constraints = iter(other.constraints)
our_constraint = next(our_constraints, None)
their_constraint = next(their_constraints, None)
while our_constraint and their_constraint:
if our_constraint.allows_all(their_constraint):
their_constraint = next(their_constraints, None)
else:
our_constraint = next(our_constraints, None)
return their_constraint is None
def allows_any(self, other):
if other.is_any():
return True
if other.is_empty():
return True
if isinstance(other, Constraint):
return self.allows(other)
if isinstance(other, MultiConstraint):
for c1 in self.constraints:
for c2 in other.constraints:
if c1.allows(c2):
return True
return False
def intersect(self, other):
if isinstance(other, Constraint):
constraints = self._constraints
if other not in constraints:
constraints += (other,)
else:
constraints = (other,)
if len(constraints) == 1:
return constraints[0]
return MultiConstraint(*constraints)
def __eq__(self, other):
if not isinstance(other, MultiConstraint):
return False
return sorted(
self._constraints, key=lambda c: (c.operator, c.version)
) == sorted(other.constraints, key=lambda c: (c.operator, c.version))
def __str__(self):
constraints = []
for constraint in self._constraints:
constraints.append(str(constraint))
return "{}".format(", ").join(constraints)
from .base_constraint import BaseConstraint
from .constraint import Constraint
from .empty_constraint import EmptyConstraint
class UnionConstraint(BaseConstraint):
def __init__(self, *constraints):
self._constraints = constraints
@property
def constraints(self):
return self._constraints
def allows(self, other):
for constraint in self._constraints:
if constraint.allows(other):
return True
return False
def allows_any(self, other):
if other.is_empty():
return False
if other.is_any():
return True
if isinstance(other, Constraint):
constraints = [other]
else:
constraints = other.constraints
for our_constraint in self._constraints:
for their_constraint in constraints:
if our_constraint.allows_any(their_constraint):
return True
return False
def allows_all(self, other):
if other.is_any():
return False
if other.is_empty():
return True
if isinstance(other, Constraint):
constraints = [other]
else:
constraints = other.constraints
our_constraints = iter(self._constraints)
their_constraints = iter(constraints)
our_constraint = next(our_constraints, None)
their_constraint = next(their_constraints, None)
while our_constraint and their_constraint:
if our_constraint.allows_all(their_constraint):
their_constraint = next(their_constraints, None)
else:
our_constraint = next(our_constraints, None)
return their_constraint is None
def intersect(self, other):
if other.is_any():
return self
if other.is_empty():
return other
if isinstance(other, Constraint):
if self.allows(other):
return other
return EmptyConstraint()
new_constraints = []
for our_constraint in self._constraints:
for their_constraint in other.constraints:
intersection = our_constraint.intersect(their_constraint)
if not intersection.is_empty() and intersection not in new_constraints:
new_constraints.append(intersection)
if not new_constraints:
return EmptyConstraint()
return UnionConstraint(*new_constraints)
def union(self, other):
if isinstance(other, Constraint):
constraints = self._constraints
if other not in self._constraints:
constraints += (other,)
return UnionConstraint(*constraints)
def __eq__(self, other):
if not isinstance(other, UnionConstraint):
return False
return sorted(
self._constraints, key=lambda c: (c.operator, c.version)
) == sorted(other.constraints, key=lambda c: (c.operator, c.version))
def __str__(self):
constraints = []
for constraint in self._constraints:
constraints.append(str(constraint))
return "{}".format(" || ").join(constraints)
import re
from .constraint import Constraint
class WilcardConstraint(Constraint):
def __init__(self, constraint): # type: (str) -> None
m = re.match(
r"^(!= ?|==)?v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$", constraint
)
if not m:
raise ValueError("Invalid value for wildcard constraint")
if not m.group(1):
operator = "=="
else:
operator = m.group(1).strip()
super(WilcardConstraint, self).__init__(
operator, ".".join([g if g else "*" for g in m.groups()[1:]])
)
if m.group(4):
position = 2
elif m.group(3):
position = 1
else:
position = 0
from ..version_parser import VersionParser
parser = VersionParser()
groups = m.groups()[1:]
low_version = parser._manipulate_version_string(groups, position)
high_version = parser._manipulate_version_string(groups, position, 1)
if operator == "!=":
if low_version == "0.0.0.0":
self._constraint = Constraint(">=", high_version)
else:
self._constraint = parser.parse_constraints(
"<{} || >={}".format(low_version, high_version)
)
else:
if low_version == "0.0.0.0":
self._constraint = Constraint("<", high_version)
else:
self._constraint = parser.parse_constraints(
">={},<{}".format(low_version, high_version)
)
@property
def supported_operators(self):
return ["!=", "=="]
@property
def constraint(self):
return self._constraint
def matches(self, provider): # type: (Constraint) -> bool
if isinstance(provider, self.__class__):
return self._constraint.matches(provider.constraint)
return provider.matches(self._constraint)
def __str__(self):
op = ""
if self.string_operator == "!=":
op = "!= "
return "{}{}".format(op, self._version)
from pkginfo.distribution import HEADER_ATTRS
from pkginfo.distribution import HEADER_ATTRS_2_0
from poetry.utils._compat import Path
from poetry.utils.toml_file import TomlFile
from .dependency import Dependency
# Patching pkginfo to support Metadata version 2.1 (PEP 566)
HEADER_ATTRS.update(
{"2.1": HEADER_ATTRS_2_0 + (("Provides-Extra", "provides_extra", True),)}
)
class DirectoryDependency(Dependency):
def __init__(
self,
name,
path, # type: Path
category="main", # type: str
optional=False, # type: bool
base=None, # type: Path
develop=True, # type: bool
):
self._path = path
self._base = base
self._full_path = path
self._develop = develop
self._supports_poetry = False
if self._base and not self._path.is_absolute():
self._full_path = self._base / self._path
if not self._full_path.exists():
raise ValueError("Directory {} does not exist".format(self._path))
if self._full_path.is_file():
raise ValueError("{} is a file, expected a directory".format(self._path))
# Checking content to determine actions
setup = self._full_path / "setup.py"
pyproject = TomlFile(self._full_path / "pyproject.toml")
if pyproject.exists():
pyproject_content = pyproject.read()
self._supports_poetry = (
"tool" in pyproject_content and "poetry" in pyproject_content["tool"]
)
if not setup.exists() and not self._supports_poetry:
raise ValueError(
"Directory {} does not seem to be a Python package".format(
self._full_path
)
)
super(DirectoryDependency, self).__init__(
name, "*", category=category, optional=optional, allows_prereleases=True
)
@property
def path(self):
return self._path
@property
def full_path(self):
return self._full_path.resolve()
@property
def base(self):
return self._base
@property
def develop(self):
return self._develop
def supports_poetry(self):
return self._supports_poetry
def is_directory(self):
return True
def __str__(self):
if self.is_root:
return self._pretty_name
return "{} ({} {})".format(
self._pretty_name, self._pretty_constraint, self._path
)
def __hash__(self):
return hash((self._name, self._full_path))
import hashlib
import io
from pkginfo.distribution import HEADER_ATTRS
from pkginfo.distribution import HEADER_ATTRS_2_0
from poetry.utils._compat import Path
from .dependency import Dependency
# Patching pkginfo to support Metadata version 2.1 (PEP 566)
HEADER_ATTRS.update(
{"2.1": HEADER_ATTRS_2_0 + (("Provides-Extra", "provides_extra", True),)}
)
class FileDependency(Dependency):
def __init__(
self,
name,
path, # type: Path
category="main", # type: str
optional=False, # type: bool
base=None, # type: Path
):
self._path = path
self._base = base
self._full_path = path
if self._base and not self._path.is_absolute():
self._full_path = self._base / self._path
if not self._full_path.exists():
raise ValueError("File {} does not exist".format(self._path))
if self._full_path.is_dir():
raise ValueError("{} is a directory, expected a file".format(self._path))
super(FileDependency, self).__init__(
name, "*", category=category, optional=optional, allows_prereleases=True
)
@property
def base(self):
return self._base
@property
def path(self):
return self._path
@property
def full_path(self):
return self._full_path.resolve()
def is_file(self):
return True
def hash(self):
h = hashlib.sha256()
with self._full_path.open("rb") as fp:
for content in iter(lambda: fp.read(io.DEFAULT_BUFFER_SIZE), b""):
h.update(content)
return h.hexdigest()
def __str__(self):
if self.is_root:
return self._pretty_name
return "{} ({} {})".format(
self._pretty_name, self._pretty_constraint, self._path
)
def __hash__(self):
return hash((self._name, self._full_path))
...@@ -11,14 +11,15 @@ from tomlkit import item ...@@ -11,14 +11,15 @@ from tomlkit import item
from tomlkit import table from tomlkit import table
from tomlkit.exceptions import TOMLKitError from tomlkit.exceptions import TOMLKitError
import poetry.packages
import poetry.repositories import poetry.repositories
from poetry.semver import parse_constraint from poetry.core.packages.package import Dependency
from poetry.semver.version import Version from poetry.core.packages.package import Package
from poetry.core.semver import parse_constraint
from poetry.core.semver.version import Version
from poetry.core.version.markers import parse_marker
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils.toml_file import TomlFile from poetry.utils.toml_file import TomlFile
from poetry.version.markers import parse_marker
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -91,9 +92,7 @@ class Locker(object): ...@@ -91,9 +92,7 @@ class Locker(object):
return packages return packages
for info in locked_packages: for info in locked_packages:
package = poetry.packages.Package( package = Package(info["name"], info["version"], info["version"])
info["name"], info["version"], info["version"]
)
package.description = info.get("description", "") package.description = info.get("description", "")
package.category = info["category"] package.category = info["category"]
package.optional = info["optional"] package.optional = info["optional"]
...@@ -117,16 +116,14 @@ class Locker(object): ...@@ -117,16 +116,14 @@ class Locker(object):
dep_name = m.group(1) dep_name = m.group(1)
constraint = m.group(2) or "*" constraint = m.group(2) or "*"
package.extras[name].append( package.extras[name].append(Dependency(dep_name, constraint))
poetry.packages.Dependency(dep_name, constraint)
)
if "marker" in info: if "marker" in info:
package.marker = parse_marker(info["marker"]) package.marker = parse_marker(info["marker"])
else: else:
# Compatibility for old locks # Compatibility for old locks
if "requirements" in info: if "requirements" in info:
dep = poetry.packages.Dependency("foo", "0.0.0") dep = Dependency("foo", "0.0.0")
for name, value in info["requirements"].items(): for name, value in info["requirements"].items():
if name == "python": if name == "python":
dep.python_versions = value dep.python_versions = value
...@@ -268,7 +265,7 @@ class Locker(object): ...@@ -268,7 +265,7 @@ class Locker(object):
return locked return locked
def _dump_package(self, package): # type: (poetry.packages.Package) -> dict def _dump_package(self, package): # type: (Package) -> dict
dependencies = {} dependencies = {}
for dependency in sorted(package.requires, key=lambda d: d.name): for dependency in sorted(package.requires, key=lambda d: d.name):
if dependency.is_optional() and not dependency.is_activated(): if dependency.is_optional() and not dependency.is_activated():
...@@ -277,16 +274,17 @@ class Locker(object): ...@@ -277,16 +274,17 @@ class Locker(object):
if dependency.pretty_name not in dependencies: if dependency.pretty_name not in dependencies:
dependencies[dependency.pretty_name] = [] dependencies[dependency.pretty_name] = []
constraint = {"version": str(dependency.pretty_constraint)} constraint = inline_table()
constraint["version"] = str(dependency.pretty_constraint)
if dependency.extras: if dependency.extras:
constraint["extras"] = dependency.extras constraint["extras"] = sorted(dependency.extras)
if dependency.is_optional(): if dependency.is_optional():
constraint["optional"] = True constraint["optional"] = True
if not dependency.python_constraint.is_any(): if not dependency.marker.is_any():
constraint["python"] = str(dependency.python_constraint) constraint["markers"] = str(dependency.marker)
dependencies[dependency.pretty_name].append(constraint) dependencies[dependency.pretty_name].append(constraint)
...@@ -307,8 +305,6 @@ class Locker(object): ...@@ -307,8 +305,6 @@ class Locker(object):
"python-versions": package.python_versions, "python-versions": package.python_versions,
"files": sorted(package.files, key=lambda x: x["file"]), "files": sorted(package.files, key=lambda x: x["file"]),
} }
if not package.marker.is_any():
data["marker"] = str(package.marker)
if package.extras: if package.extras:
extras = {} extras = {}
......
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