Commit 0cf4bd79 by Sébastien Eustace

Merge branch 'master' into develop

# Conflicts:
#	poetry/masonry/builders/builder.py
#	poetry/masonry/builders/wheel.py
#	tests/masonry/builders/fixtures/default_with_excluded_data_toml/pyproject.toml
#	tests/masonry/builders/fixtures/exclude_nested_data_toml/pyproject.toml
#	tests/masonry/builders/test_wheel.py
parents 39c57cc3 01ec4a16
......@@ -16,8 +16,8 @@ assignees: ''
-->
<!-- Checked checkbox should look like this: [x] -->
- [ ] I am on the [latest](https://github.com/sdispater/poetry/releases/latest) Poetry version.
- [ ] I have searched the [issues](https://github.com/sdispater/poetry/issues) of this repo and believe that this is not a duplicate.
- [ ] I am on the [latest](https://github.com/python-poetry/poetry/releases/latest) Poetry version.
- [ ] I have searched the [issues](https://github.com/python-poetry/poetry/issues) of this repo and believe that this is not a duplicate.
- [ ] If an exception occurs when executing a command, I executed it again in debug mode (`-vvv` option).
<!--
......
---
name: "\U0001F4DA Documentation"
about: Did you find errors, problems, or anything unintelligible in the docs (https://poetry.eustace.io/docs)?
about: Did you find errors, problems, or anything unintelligible in the docs (https://python-poetry.org/docs)?
title: ''
labels: 'Documentation'
assignees: ''
......@@ -16,7 +16,7 @@ assignees: ''
-->
<!-- Checked checkbox should look like this: [x] -->
- [ ] I have searched the [issues](https://github.com/sdispater/poetry/issues) of this repo and believe that this is not a duplicate.
- [ ] I have searched the [issues](https://github.com/python-poetry/poetry/issues) of this repo and believe that this is not a duplicate.
## Issue
<!-- Now feel free to write your issue, but please be descriptive! Thanks again 🙌 ❤️ -->
......@@ -13,8 +13,8 @@ assignees: ''
<!-- Checked checkbox should look like this: [x] -->
- [ ] I have searched the [issues](https://github.com/sdispater/poetry/issues) of this repo and believe that this is not a duplicate.
- [ ] I have searched the [documentation](https://poetry.eustace.io/docs/) and believe that my question is not covered.
- [ ] I have searched the [issues](https://github.com/python-poetry/poetry/issues) of this repo and believe that this is not a duplicate.
- [ ] I have searched the [documentation](https://python-poetry.org/docs/) and believe that my question is not covered.
## Issue
<!-- Now feel free to write your issue, but please be descriptive! Thanks again 🙌 ❤️ -->
......@@ -16,8 +16,8 @@ assignees: ''
-->
<!-- Checked checkbox should look like this: [x] -->
- [ ] I have searched the [issues](https://github.com/sdispater/poetry/issues) of this repo and believe that this is not a duplicate.
- [ ] I have searched the [documentation](https://poetry.eustace.io/docs/) and believe that my question is not covered.
- [ ] I have searched the [issues](https://github.com/python-poetry/poetry/issues) of this repo and believe that this is not a duplicate.
- [ ] I have searched the [documentation](https://python-poetry.org/docs/) and believe that my question is not covered.
## Feature Request
<!-- Now feel free to write your idea for improvement. Thanks again 🙌 ❤️ -->
# Pull Request Check List
This is just a reminder about the most common mistakes. Please make sure that you tick all *appropriate* boxes. But please read our [contribution guide](https://poetry.eustace.io/docs/contributing/) at least once, it will save you unnecessary review cycles!
This is just a reminder about the most common mistakes. Please make sure that you tick all *appropriate* boxes. But please read our [contribution guide](https://python-poetry.org/docs/contributing/) at least once, it will save you unnecessary review cycles!
- [ ] Added **tests** for changed code.
- [ ] Updated **documentation** for changed code.
......
exemptProjects: true
exemptMilestones: true
staleLabel: stale
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
closeComment: >
Closing this issue automatically because it has not had any activity since
it has been marked as stale. If you think it is still relevant and should
be addressed, feel free to open a new one.
pulls:
markComment: >
This pull request has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
closeComment: >
Closing this pull request automatically because it has not had any activity since
it has been marked as stale. If you think it is still relevant and should
be addressed, feel free to open a new one.
......@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [2.7, 3.5, 3.6, 3.7]
python-version: [2.7, 3.5, 3.6, 3.7, 3.8]
steps:
- uses: actions/checkout@v1
......@@ -30,10 +30,16 @@ jobs:
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install Poetry
- name: Install and set up Poetry
run: |
python get-poetry.py --preview -y
source $HOME/.poetry/env
poetry config virtualenvs.in-project true
- name: Set up cache
uses: actions/cache@v1
with:
path: .venv
key: ${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }}
- name: Install dependencies
run: |
source $HOME/.poetry/env
......@@ -48,7 +54,7 @@ jobs:
runs-on: macos-latest
strategy:
matrix:
python-version: [2.7, 3.5, 3.6, 3.7]
python-version: [2.7, 3.5, 3.6, 3.7, 3.8]
steps:
- uses: actions/checkout@v1
......@@ -56,10 +62,16 @@ jobs:
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install Poetry
- name: Install and set up Poetry
run: |
python get-poetry.py --preview -y
source $HOME/.poetry/env
poetry config virtualenvs.in-project true
- name: Set up cache
uses: actions/cache@v1
with:
path: .venv
key: ${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }}
- name: Install dependencies
run: |
source $HOME/.poetry/env
......@@ -74,7 +86,7 @@ jobs:
runs-on: windows-latest
strategy:
matrix:
python-version: [2.7, 3.5, 3.6, 3.7]
python-version: [2.7, 3.5, 3.6, 3.7, 3.8]
steps:
- uses: actions/checkout@v1
......@@ -82,10 +94,16 @@ jobs:
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install Poetry
- name: Install and setup Poetry
run: |
python get-poetry.py --preview -y
$env:Path += ";$env:Userprofile\.poetry\bin"
poetry config virtualenvs.in-project true
- name: Set up cache
uses: actions/cache@v1
with:
path: .venv
key: ${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }}
- name: Install dependencies
run: |
$env:Path += ";$env:Userprofile\.poetry\bin"
......
......@@ -2,7 +2,9 @@
# Packages
*.egg
*.egg-info
!/tests/**/*.egg
/*.egg-info
/tests/fixtures/**/*.egg-info
/dist/*
build
_build
......
......@@ -20,5 +20,7 @@ repos:
rev: v2.3.0
hooks:
- id: trailing-whitespace
exclude: ^tests/.*/fixtures/.*
- id: end-of-file-fixer
exclude: ^tests/.*/fixtures/.*
- id: debug-statements
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at sebastien@eustace.io. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
......@@ -20,18 +20,18 @@ The following is a set of guidelines for contributing to Poetry on GitHub. These
This section guides you through submitting a bug report for Poetry.
Following these guidelines helps maintainers and the community understand your report, reproduce the behavior, and find related reports.
Before creating bug reports, please check [this list](#before-submitting-a-bug-report) to be sure that you need to create one. When you are creating a bug report, please include as many details as possible. Fill out the [required template](https://github.com/sdispater/poetry/blob/master/.github/ISSUE_TEMPLATE/1_Bug_report.md), the information it asks helps the maintainers resolve the issue faster.
Before creating bug reports, please check [this list](#before-submitting-a-bug-report) to be sure that you need to create one. When you are creating a bug report, please include as many details as possible. Fill out the [required template](https://github.com/python-poetry/poetry/blob/master/.github/ISSUE_TEMPLATE/1_Bug_report.md), the information it asks helps the maintainers resolve the issue faster.
> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one.
#### Before submitting a bug report
* **Check the [FAQs on the official website](https://poetry.eustace.io)** for a list of common questions and problems.
* **Check that your issue does not already exist in the [issue tracker](https://github.com/sdispater/poetry/issues)**.
* **Check the [FAQs on the official website](https://python-poetry.org)** for a list of common questions and problems.
* **Check that your issue does not already exist in the [issue tracker](https://github.com/python-poetry/poetry/issues)**.
#### How do I submit a bug report?
Bugs are tracked on the [official issue tracker](https://github.com/sdispater/poetry/issues) where you can create a new one and provide the following information by filling in [the template](https://github.com/sdispater/poetry/blob/master/.github/ISSUE_TEMPLATE/1_Bug_report.md).
Bugs are tracked on the [official issue tracker](https://github.com/python-poetry/poetry/issues) where you can create a new one and provide the following information by filling in [the template](https://github.com/python-poetry/poetry/blob/master/.github/ISSUE_TEMPLATE/1_Bug_report.md).
Explain the problem and include additional details to help maintainers reproduce the problem:
......@@ -60,17 +60,17 @@ Include details about your configuration and environment:
This section guides you through submitting an enhancement suggestion for Poetry, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion and find related suggestions.
Before creating enhancement suggestions, please check [this list](#before-submitting-an-enhancement-suggestion) as you might find out that you don't need to create one. When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-an-enhancement-suggestion). Fill in [the template](https://github.com/sdispater/poetry/blob/master/.github/ISSUE_TEMPLATE/2_Feature_request.md), including the steps that you imagine you would take if the feature you're requesting existed.
Before creating enhancement suggestions, please check [this list](#before-submitting-an-enhancement-suggestion) as you might find out that you don't need to create one. When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-an-enhancement-suggestion). Fill in [the template](https://github.com/python-poetry/poetry/blob/master/.github/ISSUE_TEMPLATE/2_Feature_request.md), including the steps that you imagine you would take if the feature you're requesting existed.
#### Before submitting an enhancement suggestion
* **Check the [FAQs on the official website](https://poetry.eustace.io) for a list of common questions and problems.
* **Check that your issue does not already exist in the [issue tracker](https://github.com/sdispater/poetry/issues).
* **Check the [FAQs on the official website](https://python-poetry.org) for a list of common questions and problems.
* **Check that your issue does not already exist in the [issue tracker](https://github.com/python-poetry/poetry/issues).
#### How do I submit an Enhancement suggestion?
Enhancement suggestions are tracked on the [official issue tracker](https://github.com/sdispater/poetry/issues) where you can create a new one and provide the following information:
Enhancement suggestions are tracked on the [official issue tracker](https://github.com/python-poetry/poetry/issues) where you can create a new one and provide the following information:
* **Use a clear and descriptive title** for the issue to identify the suggestion.
* **Provide a step-by-step description of the suggested enhancement** in as many details as possible.
......@@ -82,12 +82,12 @@ Enhancement suggestions are tracked on the [official issue tracker](https://gith
#### Local development
You will need Poetry to start contributing on the Poetry codebase. Refer to the [documentation](https://poetry.eustace.io/docs/#introduction) to start using Poetry.
You will need Poetry to start contributing on the Poetry codebase. Refer to the [documentation](https://python-poetry.org/docs/#introduction) to start using Poetry.
You will first need to clone the repository using `git` and place yourself in its directory:
```bash
$ git clone git@github.com:sdispater/poetry.git
$ git clone git@github.com:python-poetry/poetry.git
$ cd poetry
```
......@@ -123,6 +123,6 @@ will not be merged.
#### Pull requests
* Fill in [the required template](https://github.com/sdispater/poetry/blob/master/.github/PULL_REQUEST_TEMPLATE.md)
* Fill in [the required template](https://github.com/python-poetry/poetry/blob/master/.github/PULL_REQUEST_TEMPLATE.md)
* Be sure that your pull request contains tests that cover the changed or added code.
* If your changes warrant a documentation change, the pull request must also update the documentation.
# This file is part of Poetry
# https://github.com/sdispater/poetry
# https://github.com/python-poetry/poetry
# Licensed under the MIT license:
# http://www.opensource.org/licenses/MIT-license
......@@ -48,7 +48,7 @@ wheel:
linux_release:
docker pull quay.io/pypa/manylinux2010_x86_64
docker run --rm -t -i -v `pwd`:/io quay.io/pypa/manylinux2010_x86_64 /io/make-linux-release.sh
docker run --rm -i -v `pwd`:/io quay.io/pypa/manylinux2010_x86_64 /io/make-linux-release.sh
# run tests against all supported python versions
tox:
......
......@@ -69,7 +69,8 @@ It will automatically find a suitable version constraint **and install** the pac
In our example, we are requesting the `pendulum` package with the version constraint `^1.4`.
This means any version greater or equal to 1.4.0 and less than 2.0.0 (`>=1.4.0 <2.0.0`).
Please read [versions](/docs/versions/) for more in-depth information on versions, how versions relate to each other, and on version constraints.
Please read [Dependency specification](/docs/dependency-specification) for more in-depth information on versions,
how versions relate to each other, and on the different ways you can specify dependencies.
!!!note
......@@ -147,107 +148,3 @@ and update the lock file with the new versions.
Poetry will display a **Warning** when executing an install command if `poetry.lock` and `pyproject.toml`
are not synchronized.
## Poetry and virtualenvs
When you execute the `install` command (or any other "install" commands like `add` or `remove`),
Poetry will check if it's currently inside a virtualenv and, if not, will use an existing one
or create a brand new one for you to always work isolated from your global Python installation.
By default, Poetry will use the currently activated Python version
to create the virtualenv for the current project.
To easily switch between Python versions, it is recommended to
use [pyenv](https://github.com/pyenv/pyenv) or similar tools.
For instance, if your project is Python 2.7 only, a standard workflow
would be:
```bash
pyenv install 2.7.15
pyenv local 2.7.15 # Activate Python 2.7 for the current project
poetry install
```
However, this might not be feasible for your system, especially Windows where `pyenv`,
is not available. To circumvent that you can use the `env use` command to tell
Poetry which Python version to use for the current project.
```bash
poetry env use /full/path/to/python
```
If you have the python executable in your `PATH` you can use it:
```bash
poetry env use python3.7
```
You can even just use the minor Python version in this case:
```bash
poetry env use 3.7
```
If you want to disable the explicitly activated virtualenv, you can use the
special `system` Python version to retrieve the default behavior:
```bash
poetry env use system
```
If you want to get basic information about the currently activated virtualenv,
you can use the `env info` command:
```bash
poetry env info
```
will output something similar to this:
```text
Virtualenv
Python: 3.7.1
Implementation: CPython
Path: /path/to/poetry/cache/virtualenvs/test-O3eWbxRl-py3.7
Valid: True
System
Platform: darwin
OS: posix
Python: /path/to/main/python
```
If you only want to know the path to the virtualenv, you can pass the `--path` option
to `env info`:
```bash
poetry env info --path
```
You can also list all the virtualenvs associated with the current virtualenv
with the `env list` command:
```bash
poetry env list
```
will output something like the following:
```text
test-O3eWbxRl-py2.7
test-O3eWbxRl-py3.6
test-O3eWbxRl-py3.7 (Activated)
```
Finally, you can delete existing virtualenvs by using `env remove`:
```bash
poetry env remove /full/path/to/python
poetry env remove python3.7
poetry env remove 3.7
poetry env remove test-O3eWbxRl-py3.7
```
If your remove the currently activated virtualenv, it will be automatically deactivated.
......@@ -3,7 +3,7 @@
You've already learned how to use the command-line interface to do some things.
This chapter documents all the available commands.
To get help from the command-line, simply call `poetry` or `poetry list` to see the complete list of commands,
To get help from the command-line, simply call `poetry` to see the complete list of commands,
then `--help` combined with any of those can give you more information.
As `Poetry` uses [cleo](https://github.com/sdispater/cleo) you can call commands by short name if it's not ambiguous.
......@@ -130,7 +130,7 @@ By default `poetry` will install your project's package everytime you run `insta
$ poetry install
Installing dependencies from lock file
Nothing to install or update
No dependencies to install or update
- Installing <your-package-name> (x.x.x)
......@@ -208,8 +208,8 @@ If you need to checkout a specific branch, tag or revision,
you can specify it when using `add`:
```bash
poetry add git+https://github.com/sdispater/pendulum.git@develop
poetry add git+https://github.com/sdispater/pendulum.git@2.0.5
poetry add git+https://github.com/sdispater/pendulum.git#develop
poetry add git+https://github.com/sdispater/pendulum.git#2.0.5
```
or make them point to a local directory or file:
......@@ -451,106 +451,4 @@ poetry export -f requirements.txt > requirements.txt
The `env` command regroups sub commands to interact with the virtualenvs
associated with a specific project.
### env use
The `env use` command tells Poetry which Python version
to use for the current project.
```bash
poetry env use /full/path/to/python
```
If you have the python executable in your `PATH` you can use it:
```bash
poetry env use python3.7
```
You can even just use the minor Python version in this case:
```bash
poetry env use 3.7
```
If you want to disable the explicitly activated virtualenv, you can use the
special `system` Python version to retrieve the default behavior:
```bash
poetry env use system
```
### env info
The `env info` command displays basic information about the currently activated virtualenv:
```bash
poetry env info
```
will output something similar to this:
```text
Virtualenv
Python: 3.7.1
Implementation: CPython
Path: /path/to/poetry/cache/virtualenvs/test-O3eWbxRl-py3.7
Valid: True
System
Platform: darwin
OS: posix
Python: /path/to/main/python
```
If you only want to know the path to the virtualenv, you can pass the `--path` option
to `env info`:
```bash
poetry env info --path
```
#### Options
* `--path`: Only display the path of the virtualenv.
### env list
The `env list` command lists all the virtualenvs associated with the current virtualenv.
```bash
poetry env list
```
will output something like the following:
```text
test-O3eWbxRl-py2.7
test-O3eWbxRl-py3.6
test-O3eWbxRl-py3.7 (Activated)
```
#### Options
* `--full-path`: Display the full path of the virtualenvs.
### env remove
The `env remove` command deletes virtualenvs associated with the current project:
```bash
poetry env remove /full/path/to/python
```
Similarly to `env use`, you can either pass `python3.7`, `3.7` or the name of
the virtualenv (as returned by `env list`):
```bash
poetry env remove python3.7
poetry env remove 3.7
poetry env remove test-O3eWbxRl-py3.7
```
!!!note
If your remove the currently activated virtualenv, it will be automatically deactivated.
See [Managing environments](./managing-environments.md) for more information about these commands.
......@@ -100,11 +100,11 @@ Defaults to one of the following directories:
- macOS: `~/Library/Caches/pypoetry`
- Windows: `C:\Users\<username>\AppData\Local\pypoetry\Cache`
- Unix: `~/.cache/pypoetry/virtualenvs`
- Unix: `~/.cache/pypoetry`
### `virtualenvs.create`: boolean
Create a new virtualenv if one doesn't already exist.
Create a new virtual environment if one doesn't already exist.
Defaults to `true`.
### `virtualenvs.in-project`: boolean
......@@ -114,7 +114,7 @@ Defaults to `false`.
### `virtualenvs.path`: string
Directory where virtualenvs will be created.
Directory where virtual environments will be created.
Defaults to `{cache-dir}/virtualenvs` (`{cache-dir}\virtualenvs` on Windows).
### `repositories.<name>`: string
......
# Contributing to Poetry
First off, thank for taking the time to contribute!
The following is a set of guidelines for contributing to Poetry on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
#### Table of Contents
[How to contribute](#how-to-contribute)
* [Reporting bugs](#reporting-bugs)
* [Suggesting enhancements](#suggesting-enhancements)
* [Contributing to code](#contributing-to-code)
## How to contribute
### Reporting bugs
This section guides you through submitting a bug report for Poetry.
Following these guidelines helps maintainers and the community understand your report, reproduce the behavior, and find related reports.
Before creating bug reports, please check [this list](#before-submitting-a-bug-report) to be sure that you need to create one. When you are creating a bug report, please include as many details as possible. Fill out the [required template](https://github.com/sdispater/poetry/blob/master/.github/ISSUE_TEMPLATE/1_Bug_report.md), the information it asks helps the maintainers resolve the issue faster.
!!!note
If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one.
#### Before submitting a bug report
* **Check the [FAQs on the official website](https://poetry.eustace.io)** for a list of common questions and problems.
* **Check that your issue does not already exist in the [issue tracker](https://github.com/sdispater/poetry/issues)**.
#### How do I submit a bug report
Bugs are tracked on the [official issue tracker](https://github.com/sdispater/poetry/issues) where you can create a new one and provide the following information by filling in [the template](https://github.com/sdispater/poetry/blob/master/.github/ISSUE_TEMPLATE/1_Bug_report.md).
Explain the problem and include additional details to help maintainers reproduce the problem:
* **Use a clear and descriptive title** for the issue to identify the problem.
* **Describe the exact steps which reproduce the problem** in as many details as possible.
* **Provide your pyproject.toml file** in a [Gist](https://gist.github.com) after removing potential private information (like private package repositories).
* **Provide specific examples to demonstrate the steps to reproduce the issue**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples.
* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior.
* **Explain which behavior you expected to see instead and why.**
* **If the problem is an unexpected error being raised**, execute the corresponding command in **debug** mode (the `-vvv` option).
Provide more context by answering these questions:
* **Did the problem start happening recently** (e.g. after updating to a new version of Poetry) or was this always a problem?
* If the problem started happening recently, **can you reproduce the problem in an older version of Poetry?** What's the most recent version in which the problem doesn't happen?
* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens.
Include details about your configuration and environment:
* **Which version of Poetry are you using?** You can get the exact version by running `poetry -V` in your terminal.
* **Which Python version Poetry has been installed for?** Execute the `debug:info` to get the information.
* **What's the name and version of the OS you're using**?
### Suggesting enhancements
This section guides you through submitting an enhancement suggestion for Poetry, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion and find related suggestions.
Before creating enhancement suggestions, please check [this list](#before-submitting-an-enhancement-suggestion) as you might find out that you don't need to create one. When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-an-enhancement-suggestion). Fill in [the template](https://github.com/sdispater/poetry/blob/master/.github/ISSUE_TEMPLATE/2_Feature_request.md), including the steps that you imagine you would take if the feature you're requesting existed.
#### Before submitting an enhancement suggestion
* **Check the [FAQs on the official website](https://poetry.eustace.io)** for a list of common questions and problems.
* **Check that your issue does not already exist in the [issue tracker](https://github.com/sdispater/poetry/issues)**.
#### How do I submit an enhancement suggestion?
Enhancement suggestions are tracked on the [official issue tracker](https://github.com/sdispater/poetry/issues) where you can create a new one and provide the following information:
* **Use a clear and descriptive title** for the issue to identify the suggestion.
* **Provide a step-by-step description of the suggested enhancement** in as many details as possible.
* **Provide specific examples to demonstrate the steps**..
* **Describe the current behavior** and **explain which behavior you expected to see instead** and why.
### Contributing to code
#### Local development
You will need Poetry to start contributing on the Poetry codebase. Refer to the [documentation](https://poetry.eustace.io/docs/#introduction) to start using Poetry.
You will first need to clone the repository using `git` and place yourself in its directory:
```bash
$ git clone git@github.com:sdispater/poetry.git
$ cd poetry
```
Now, you will need to install the required dependency for Poetry and be sure that the current
tests are passing on your machine:
```bash
$ poetry install
$ poetry run pytest tests/
```
Poetry uses the [black](https://github.com/ambv/black) coding style and you must ensure that your
code follows it. If not, the CI will fail and your Pull Request will not be merged.
Similarly, the import statements are sorted with [isort](https://github.com/timothycrosley/isort)
and special care must be taken to respect it. If you don't, the CI will fail as well.
To make sure that you don't accidentally commit code that does not follow the coding style, you can
install a pre-commit hook that will check that everything is in order:
```bash
$ poetry run pre-commit install
```
You can also run it anytime using:
```bash
$ poetry run pre-commit run --all-files
```
Your code must always be accompanied by corresponding tests, if tests are not present your code
will not be merged.
#### Pull requests
* Fill in [the required template](https://github.com/sdispater/poetry/blob/master/.github/PULL_REQUEST_TEMPLATE.md)
* Be sure that you pull request contains tests that cover the changed or added code.
* If you changes warrant a documentation change, the pull request must also update the documentation.
{!../CONTRIBUTING.md!}
# Versions and constraints
# Dependency specification
Poetry recommends following [semantic versioning](https://semver.org) but will not enforce it.
Dependencies for a project can be specified in various forms, which depend on the type
of the dependency and on the optional constraints that might be needed for it to be installed.
## Version constraints
......@@ -74,7 +75,7 @@ If other dependencies require a different version, the solver will ultimately fa
Multiple version requirements can also be separated with a comma, e.g. `>= 1.2, < 1.5`.
### `git` dependencies
## `git` dependencies
To depend on a library located in a `git` repository,
the minimum information you need to specify is the location of the repository with the git key:
......@@ -85,16 +86,24 @@ requests = { git = "https://github.com/requests/requests.git" }
```
Since we haven’t specified any other information,
Poetry assumes that we intend to use the latest commit on the `master` branch to build our project.
You can combine the `git` key with the `rev`, `tag`, or `branch` keys to specify something else.
Here's an example of specifying that you want to use the latest commit on a branch named `next`:
Poetry assumes that we intend to use the latest commit on the `master` branch
to build our project.
You can combine the `git` key with the `branch` key to use another branch.
Alternatively, use `rev` or `tag` to pin a dependency to a specific commit hash
or tagged ref, respectively. For example:
```toml
[tool.poetry.dependencies]
# Get the latest revision on the branch named "next"
requests = { git = "https://github.com/kennethreitz/requests.git", branch = "next" }
# Get a revision by its commit hash
flask = { git = "https://github.com/pallets/flask.git", rev = "38eb5d3b" }
# Get a revision by its tag
numpy = { git = "https://github.com/numpy/numpy.git", tag = "v0.13.2" }
```
### `path` dependencies
## `path` dependencies
To depend on a library located in a local directory or file,
you can use the `path` property:
......@@ -109,7 +118,7 @@ my-package = { path = "../my-package/dist/my-package-0.1.0.tar.gz" }
```
### `url` dependencies
## `url` dependencies
To depend on a library located on a remote archive,
you can use the `url` property:
......@@ -127,7 +136,7 @@ poetry add https://example.com/my-package-0.1.0.tar.gz
```
### Python restricted dependencies
## Python restricted dependencies
You can also specify that a dependency should be installed only for specific Python versions:
......@@ -141,7 +150,7 @@ pathlib2 = { version = "^2.2", python = "~2.7" }
pathlib2 = { version = "^2.2", python = "~2.7 || ^3.2" }
```
### Using environment markers
## Using environment markers
If you need more complex install conditions for your dependencies,
Poetry supports [environment markers](https://www.python.org/dev/peps/pep-0508/#environment-markers)
......@@ -153,7 +162,7 @@ pathlib2 = { version = "^2.2", markers = "python_version ~= '2.7' or sys_platfor
```
### Multiple constraints dependencies
## Multiple constraints dependencies
Sometimes, one of your dependency may have different version ranges depending
on the target Python versions.
......
......@@ -60,14 +60,14 @@ commands =
poetry run pytest tests/
```
## I don't want Poetry to manage my virtualenvs. Can I disable it?
## I don't want Poetry to manage my virtual environments. Can I disable it?
While Poetry automatically creates virtualenvs to always work isolated
While Poetry automatically creates virtual environments to always work isolated
from the global Python installation, there are valid reasons why it's not necessary
and is an overhead, like when working with containers.
In this case, you can disable this feature by setting the `virtualenvs.create` setting to `false`:
```bash
poetry config settings.virtualenvs.create false
poetry config virtualenvs.create false
```
......@@ -16,8 +16,13 @@ Poetry provides a custom installer that will install `poetry` isolated
from the rest of your system by vendorizing its dependencies. This is the
recommended way of installing `poetry`.
### osx / linux / bashonwindows install instructions
```bash
curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python
```
### windows powershell install instructions
```powershell
(Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py -UseBasicParsing).Content | python
```
!!! note
......@@ -141,6 +146,10 @@ poetry self update 0.8.0
The `self update` command will only work if you used the recommended
installer to install Poetry.
!!!note
If you are still on poetry version < 1.0 use `poetry self:update` instead.
## Enable tab completion for Bash, Fish, or Zsh
......
......@@ -2,11 +2,6 @@
This chapter will tell you how to make your library installable through Poetry.
## Every project is a package
As soon as you have a `pyproject.toml` in a directory, that directory is a package.
However, to make it accessible to others you will need to package and publish it.
## Versioning
......@@ -14,7 +9,7 @@ While Poetry does not enforce any convention regarding package versioning,
it **strongly** recommends to follow [semantic versioning](https://semver.org).
This has many advantages for the end users and allows them to set appropriate
[version constraints](/docs/versions/).
[version constraints](/docs/dependency-specification/#version-constraints).
## Lock file
......
# Managing environments
Poetry makes project environment isolation one of its core feature.
What this means is that it will always work isolated from your global Python installation.
To achieve this, it will first check if it's currently running inside a virtual environment.
If it is, it will use it directly without creating a new one. But if it's not, it will use
one that it has already created or create a brand new one for you.
By default, Poetry will try to use the currently activated Python version
to create the virtual environment for the current project.
However, for various reasons, this Python version might not be compatible
with the `python` requirement of the project. In this case, Poetry will try
to find one that is and use it. If it's unable to do so then you will be prompted
to activate one explicitly, see [Switching environments](#switching-between-environments).
!!!note
To easily switch between Python versions, it is recommended to
use [pyenv](https://github.com/pyenv/pyenv) or similar tools.
For instance, if your project is Python 2.7 only, a standard workflow
would be:
```bash
pyenv install 2.7.15
pyenv local 2.7.15 # Activate Python 2.7 for the current project
poetry install
```
## Switching between environments
Sometimes this might not be feasible for your system, especially Windows where `pyenv`
is not available, or you simply prefer to have a more explicit control over your environment.
For this specific purpose, you can use the `env use` command to tell Poetry
which Python version to use for the current project.
```bash
poetry env use /full/path/to/python
```
If you have the python executable in your `PATH` you can use it:
```bash
poetry env use python3.7
```
You can even just use the minor Python version in this case:
```bash
poetry env use 3.7
```
If you want to disable the explicitly activated virtual environment, you can use the
special `system` Python version to retrieve the default behavior:
```bash
poetry env use system
```
## Displaying the environment information
If you want to get basic information about the currently activated virtual environment,
you can use the `env info` command:
```bash
poetry env info
```
will output something similar to this:
```text
Virtual environment
Python: 3.7.1
Implementation: CPython
Path: /path/to/poetry/cache/virtualenvs/test-O3eWbxRl-py3.7
Valid: True
System
Platform: darwin
OS: posix
Python: /path/to/main/python
```
If you only want to know the path to the virtual environment, you can pass the `--path` option
to `env info`:
```bash
poetry env info --path
```
## Listing the environments associated with the project
You can also list all the virtual environments associated with the current virtual environment
with the `env list` command:
```bash
poetry env list
```
will output something like the following:
```text
test-O3eWbxRl-py2.7
test-O3eWbxRl-py3.6
test-O3eWbxRl-py3.7 (Activated)
```
## Deleting the environments
Finally, you can delete existing virtual environments by using `env remove`:
```bash
poetry env remove /full/path/to/python
poetry env remove python3.7
poetry env remove 3.7
poetry env remove test-O3eWbxRl-py3.7
```
If your remove the currently activated virtual environment, it will be automatically deactivated.
......@@ -27,14 +27,14 @@ The recommended notation for the most common licenses is (alphabetical):
* BSD-2-Clause
* BSD-3-Clause
* BSD-4-Clause
* GPL-2.0
* GPL-2.0+
* GPL-3.0
* GPL-3.0+
* LGPL-2.1
* LGPL-2.1+
* LGPL-3.0
* LGPL-3.0+
* GPL-2.0-only
* GPL-2.0-or-later
* GPL-3.0-only
* GPL-3.0-or-later
* LGPL-2.1-only
* LGPL-2.1-or-later
* LGPL-3.0-only
* LGPL-3.0-or-later
* MIT
Optional, but it is highly recommended to supply this.
......@@ -264,7 +264,7 @@ any custom url in the `urls` section.
```toml
[tool.poetry.urls]
"Bug Tracker" = "https://github.com/sdispater/poetry/issues"
"Bug Tracker" = "https://github.com/python-poetry/poetry/issues"
```
If you publish you package on PyPI, they will appear in the `Project Links` section.
......
......@@ -71,7 +71,7 @@ export POETRY_HTTP_BASIC_PYPI_USERNAME=username
export POETRY_HTTP_BASIC_PYPI_PASSWORD=password
```
See [Using environment variables](/configuration#using-environment-variables) for more information
See [Using environment variables](/docs/configuration/#using-environment-variables) for more information
on how to configure Poetry with environment variables.
#### Custom certificate authority and mutual TLS authentication
......
......@@ -14,7 +14,8 @@ nav:
- Commands: cli.md
- Configuration: configuration.md
- Repositories: repositories.md
- Versions: versions.md
- Managing environments: managing-environments.md
- Dependency specification: dependency-specification.md
- The pyproject.toml file: pyproject.md
- Contributing: contributing.md
- FAQ: faq.md
......@@ -25,3 +26,5 @@ markdown_extensions:
- pymdownx.superfences
- toc:
permalink:
- markdown_include.include:
base_path: docs
"""
This script will install poetry and its dependencies
This script will install Poetry and its dependencies
in isolation from the rest of the system.
It does, in order:
......@@ -65,6 +65,7 @@ try:
except NameError:
u = str
SHELL = os.getenv("SHELL", "")
WINDOWS = sys.platform.startswith("win") or (sys.platform == "cli" and os.name == "nt")
......@@ -226,8 +227,7 @@ It will add the `poetry` command to {poetry}'s bin directory, located at:
{platform_msg}
You can uninstall at any time with `poetry self:uninstall`,
or by executing this script with the --uninstall option,
You can uninstall at any time by executing this script with the --uninstall option,
and these changes will be reverted.
"""
......@@ -249,6 +249,9 @@ modifying the profile file{plural} located at:
{rcfiles}"""
PRE_MESSAGE_FISH = """This path will then be added to your `PATH` environment variable by
modifying the `fish_user_paths` universal variable."""
PRE_MESSAGE_WINDOWS = """This path will then be added to your `PATH` environment variable by
modifying the `HKEY_CURRENT_USER/Environment/PATH` registry key."""
......@@ -264,6 +267,12 @@ automatically.
To configure your current shell run `source {poetry_home_env}`
"""
POST_MESSAGE_FISH = """{poetry} ({version}) is installed now. Great!
{poetry}'s bin directory ({poetry_home_bin}) has been added to your `PATH`
environment variable by modifying the `fish_user_paths` universal variable.
"""
POST_MESSAGE_WINDOWS = """{poetry} ({version}) is installed now. Great!
To get started you need Poetry's bin directory ({poetry_home_bin}) in your `PATH`
......@@ -279,6 +288,15 @@ environment variable.
To configure your current shell run `source {poetry_home_env}`
"""
POST_MESSAGE_FISH_NO_MODIFY_PATH = """{poetry} ({version}) is installed now. Great!
To get started you need {poetry}'s bin directory ({poetry_home_bin})
in your `PATH` environment variable, which you can add by running
the following command:
set -U fish_user_paths {poetry_home_bin} $fish_user_paths
"""
POST_MESSAGE_WINDOWS_NO_MODIFY_PATH = """{poetry} ({version}) is installed now. Great!
To get started you need Poetry's bin directory ({poetry_home_bin}) in your `PATH`
......@@ -605,6 +623,9 @@ class Installer:
if not self._modify_path:
return
if "fish" in SHELL:
return self.add_to_fish_path()
if WINDOWS:
return self.add_to_windows_path()
......@@ -625,6 +646,40 @@ class Installer:
with open(profile, "a") as f:
f.write(u(addition))
def add_to_fish_path(self):
"""
Ensure POETRY_BIN directory is on Fish shell $PATH
"""
current_path = os.environ.get("PATH", None)
if current_path is None:
print(
colorize(
"warning",
"\nUnable to get the PATH value. It will not be updated automatically.",
)
)
self._modify_path = False
return
if POETRY_BIN not in current_path:
fish_user_paths = subprocess.check_output(
["fish", "-c", "echo $fish_user_paths"]
).decode("utf-8")
if POETRY_BIN not in fish_user_paths:
cmd = "set -U fish_user_paths {} $fish_user_paths".format(POETRY_BIN)
set_fish_user_path = ["fish", "-c", "{}".format(cmd)]
subprocess.check_output(set_fish_user_path)
else:
print(
colorize(
"warning",
"\nPATH already contains {} and thus was not modified.".format(
POETRY_BIN
),
)
)
def add_to_windows_path(self):
try:
old_path = self.get_windows_path_var()
......@@ -685,11 +740,25 @@ class Installer:
)
def remove_from_path(self):
if WINDOWS:
if "fish" in SHELL:
return self.remove_from_fish_path()
elif WINDOWS:
return self.remove_from_windows_path()
return self.remove_from_unix_path()
def remove_from_fish_path(self):
fish_user_paths = subprocess.check_output(
["fish", "-c", "echo $fish_user_paths"]
).decode("utf-8")
if POETRY_BIN in fish_user_paths:
cmd = "set -U fish_user_paths (string match -v {} $fish_user_paths)".format(
POETRY_BIN
)
set_fish_user_path = ["fish", "-c", "{}".format(cmd)]
subprocess.check_output(set_fish_user_path)
def remove_from_windows_path(self):
path = self.get_windows_path_var()
......@@ -741,8 +810,7 @@ class Installer:
def get_unix_profiles(self):
profiles = [os.path.join(HOME, ".profile")]
shell = os.getenv("SHELL", "")
if "zsh" in shell:
if "zsh" in SHELL:
zdotdir = os.getenv("ZDOTDIR", HOME)
profiles.append(os.path.join(zdotdir, ".zprofile"))
......@@ -766,7 +834,9 @@ class Installer:
if not self._modify_path:
kwargs["platform_msg"] = PRE_MESSAGE_NO_MODIFY_PATH
else:
if WINDOWS:
if "fish" in SHELL:
kwargs["platform_msg"] = PRE_MESSAGE_FISH
elif WINDOWS:
kwargs["platform_msg"] = PRE_MESSAGE_WINDOWS
else:
profiles = [
......@@ -809,6 +879,12 @@ class Installer:
poetry_home_bin = POETRY_BIN.replace(
os.getenv("USERPROFILE", ""), "%USERPROFILE%"
)
elif "fish" in SHELL:
message = POST_MESSAGE_FISH
if not self._modify_path:
message = POST_MESSAGE_FISH_NO_MODIFY_PATH
poetry_home_bin = POETRY_BIN.replace(os.getenv("HOME", ""), "$HOME")
else:
message = POST_MESSAGE_UNIX
if not self._modify_path:
......
......@@ -3,8 +3,8 @@ PYTHON_VERSIONS="cp27-cp27m cp34-cp34m cp35-cp35m cp36-cp36m cp37-cp37m cp38-cp3
cd /io
/opt/python/cp37-cp37m/bin/pip install pip -U
/opt/python/cp37-cp37m/bin/pip install poetry -U
/opt/python/cp37-cp37m/bin/poetry config settings.virtualenvs.create false
/opt/python/cp37-cp37m/bin/pip install poetry -U --pre
/opt/python/cp37-cp37m/bin/poetry config virtualenvs.create false
/opt/python/cp37-cp37m/bin/poetry install --no-dev
/opt/python/cp37-cp37m/bin/python sonnet make release --ansi \
-P "2.7:/opt/python/cp27-cp27m/bin/python" \
......
__version__ = "1.0.0b3"
__version__ = "1.0.1"
......@@ -12,5 +12,5 @@ class AboutCommand(Command):
"""<info>Poetry - Package Management for Python</info>
<comment>Poetry is a dependency manager tracking local dependencies of your projects and libraries.
See <fg=blue>https://github.com/sdispater/poetry</> for more information.</comment>"""
See <fg=blue>https://github.com/python-poetry/poetry</> for more information.</comment>"""
)
......@@ -5,8 +5,6 @@ from cleo import argument
from cleo import option
from poetry.factory import Factory
from poetry.utils.helpers import keyring_repository_password_del
from poetry.utils.helpers import keyring_repository_password_set
from .command import Command
......@@ -181,11 +179,14 @@ To remove a repository (repo is a short alias for repositories):
# handle auth
m = re.match(r"^(http-basic|pypi-token)\.(.+)", self.argument("key"))
if m:
from poetry.utils.password_manager import PasswordManager
password_manager = PasswordManager(config)
if self.option("unset"):
keyring_repository_password_del(config, m.group(2))
config.auth_config_source.remove_property(
"{}.{}".format(m.group(1), m.group(2))
)
if m.group(1) == "http-basic":
password_manager.delete_http_password(m.group(2))
elif m.group(1) == "pypi-token":
password_manager.delete_pypi_token(m.group(2))
return 0
......@@ -203,15 +204,7 @@ To remove a repository (repo is a short alias for repositories):
username = values[0]
password = values[1]
property_value = dict(username=username)
try:
keyring_repository_password_set(m.group(2), username, password)
except RuntimeError:
property_value.update(password=password)
config.auth_config_source.add_property(
"{}.{}".format(m.group(1), m.group(2)), property_value
)
password_manager.set_http_password(m.group(2), username, password)
elif m.group(1) == "pypi-token":
if len(values) != 1:
raise ValueError(
......@@ -220,9 +213,7 @@ To remove a repository (repo is a short alias for repositories):
token = values[0]
config.auth_config_source.add_property(
"{}.{}".format(m.group(1), m.group(2)), token
)
password_manager.set_pypi_token(m.group(2), token)
return 0
......
......@@ -28,10 +28,11 @@ class DebugResolveCommand(InitCommand):
loggers = ["poetry.repositories.pypi_repository"]
def handle(self):
from poetry.io.null_io import NullIO
from poetry.packages import ProjectPackage
from poetry.puzzle import Solver
from poetry.repositories.pool import Pool
from poetry.repositories.repository import Repository
from poetry.semver import parse_constraint
from poetry.utils.env import EnvManager
packages = self.argument("package")
......@@ -99,20 +100,30 @@ class DebugResolveCommand(InitCommand):
return 0
env = EnvManager(self.poetry).get()
current_python_version = parse_constraint(
".".join(str(v) for v in env.version_info)
)
table = self.table([], style="borderless")
rows = []
if self.option("install"):
env = EnvManager(self.poetry).get()
current_python_version = ".".join(str(v) for v in env.version_info)
pool = Pool()
locked_repository = Repository()
for op in ops:
locked_repository.add_package(op.package)
pool.add_repository(locked_repository)
with package.with_python_versions(current_python_version):
solver = Solver(package, pool, Repository(), Repository(), NullIO())
ops = solver.solve()
for op in ops:
pkg = op.package
if self.option("install"):
if not pkg.python_constraint.allows(
current_python_version
) or not env.is_valid_for_marker(pkg.marker):
continue
row = ["<c1>{}</c1>".format(pkg.name), "<b>{}</b>".format(pkg.version), ""]
row = [
"<c1>{}</c1>".format(pkg.name),
"<b>{}</b>".format(pkg.version),
"",
]
if not pkg.marker.is_any():
row[2] = str(pkg.marker)
......
......@@ -147,8 +147,8 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the
"You can specify a package in the following forms:\n"
" - A single name (<b>requests</b>)\n"
" - A name and a constraint (<b>requests ^2.23.0</b>)\n"
" - A git url (<b>https://github.com/sdispater/poetry.git</b>)\n"
" - A git url with a revision (<b>https://github.com/sdispater/poetry.git@develop</b>)\n"
" - A git url (<b>git+https://github.com/python-poetry/poetry.git</b>)\n"
" - A git url with a revision (<b>git+https://github.com/python-poetry/poetry.git#develop</b>)\n"
" - A file path (<b>../my-package/my-package.whl</b>)\n"
" - A directory (<b>../my-package/</b>)\n"
" - An url (<b>https://example.com/packages/my-package-0.1.0.tar.gz</b>)\n"
......@@ -365,22 +365,21 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the
if url_parsed.scheme and url_parsed.netloc:
# Url
if url_parsed.scheme in ["git+https", "git+ssh"]:
url = requirement.lstrip("git+")
rev = None
if "@" in url:
url, rev = url.split("@")
from poetry.vcs.git import Git
from poetry.vcs.git import ParsedUrl
pair = OrderedDict(
[("name", url.split("/")[-1].rstrip(".git")), ("git", url)]
)
if rev:
pair["rev"] = rev
parsed = ParsedUrl.parse(requirement)
url = Git.normalize_url(requirement)
pair = OrderedDict([("name", parsed.name), ("git", url.url)])
if parsed.rev:
pair["rev"] = url.revision
if extras:
pair["extras"] = extras
package = Provider.get_package_from_vcs(
"git", url, reference=pair.get("rev")
"git", url.url, reference=pair.get("rev")
)
pair["name"] = package.name
result.append(pair)
......@@ -426,6 +425,11 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the
require = OrderedDict()
if " " in pair:
name, version = pair.split(" ", 2)
extras_m = re.search(r"\[([\w\d,-_]+)\]$", name)
if extras_m:
extras = [e.strip() for e in extras_m.group(1).split(",")]
name, _ = name.split("[")
require["name"] = name
if version != "latest":
require["version"] = version
......
......@@ -6,8 +6,10 @@ class LockCommand(EnvCommand):
name = "lock"
description = "Locks the project dependencies."
help = """The <info>lock</info> command reads the <comment>pyproject.toml</> file from
the current directory, processes it, and locks the depdencies in the <comment>poetry.lock</> file.
help = """
The <info>lock</info> command reads the <comment>pyproject.toml</> file from the
current directory, processes it, and locks the dependencies in the <comment>poetry.lock</>
file.
<info>poetry lock</info>
"""
......
......@@ -3,6 +3,8 @@ import sys
from cleo import argument
from cleo import option
from poetry.utils.helpers import module_name
from .command import Command
......@@ -76,6 +78,6 @@ class NewCommand(Command):
self.line(
"Created package <info>{}</> in <fg=blue>{}</>".format(
name, path.relative_to(Path.cwd())
module_name(name), path.relative_to(Path.cwd())
)
)
......@@ -172,6 +172,13 @@ lists all packages available."""
# Non installed in non decorated mode
install_marker = " (!)"
if (
show_latest
and self.option("outdated")
and latest_statuses[locked.pretty_name] == "up-to-date"
):
continue
line = "<fg={}>{:{}}{}</>".format(
color, name, name_length - len(install_marker), install_marker
)
......@@ -183,9 +190,6 @@ lists all packages available."""
latest = latest_packages[locked.pretty_name]
update_status = latest_statuses[locked.pretty_name]
if self.option("outdated") and update_status == "up-to-date":
continue
if write_latest:
color = "green"
if update_status == "semver-safe-update":
......
......@@ -7,7 +7,7 @@ class VersionCommand(Command):
name = "version"
description = (
"Shows the version of the project or bumps it when a valid"
"Shows the version of the project or bumps it when a valid "
"bump rule is provided."
)
......@@ -59,7 +59,7 @@ patch, minor, major, prepatch, preminor, premajor, prerelease.
self.poetry.file.write(content)
else:
self.line(
"Project (<comment>{}</>) version is <info>{}</>".format(
"<comment>{}</> <info>{}</>".format(
self.poetry.package.name, self.poetry.package.pretty_version
)
)
......
import logging
from typing import Any
from cleo.config import ApplicationConfig as BaseApplicationConfig
from clikit.api.application.application import Application
from clikit.api.args.raw_args import RawArgs
......@@ -43,15 +45,15 @@ class ApplicationConfig(BaseApplicationConfig):
self.add_event_listener(PRE_HANDLE, self.set_env)
def register_command_loggers(
self, event, event_name, _ # type: PreHandleEvent # type: str
): # type: (...) -> None
self, event, event_name, _
): # type: (PreHandleEvent, str, Any) -> None
command = event.command.config.handler
if not isinstance(command, Command):
return
io = event.io
loggers = ["poetry.packages.package"]
loggers = ["poetry.packages.package", "poetry.utils.password_manager"]
loggers += command.loggers
......@@ -72,7 +74,7 @@ class ApplicationConfig(BaseApplicationConfig):
logger.setLevel(level)
def set_env(self, event, event_name, _): # type: (PreHandleEvent, str, _) -> None
def set_env(self, event, event_name, _): # type: (PreHandleEvent, str, Any) -> None
from poetry.utils.env import EnvManager
command = event.command.config.handler # type: EnvCommand
......
......@@ -233,7 +233,8 @@ class Factory:
): # type: (Dict[str, str], Config) -> LegacyRepository
from .repositories.auth import Auth
from .repositories.legacy_repository import LegacyRepository
from .utils.helpers import get_client_cert, get_cert, get_http_basic_auth
from .utils.helpers import get_client_cert, get_cert
from .utils.password_manager import PasswordManager
if "url" in source:
# PyPI-like repository
......@@ -242,11 +243,12 @@ class Factory:
else:
raise RuntimeError("Unsupported source specified")
password_manager = PasswordManager(auth_config)
name = source["name"]
url = source["url"]
credentials = get_http_basic_auth(auth_config, name)
credentials = password_manager.get_http_auth(name)
if credentials:
auth = Auth(url, credentials[0], credentials[1])
auth = Auth(url, credentials["username"], credentials["password"])
else:
auth = None
......@@ -298,7 +300,7 @@ class Factory:
result["warnings"].append(
'The "{}" dependency specifies '
'the "allows-prereleases" property, which is deprecated. '
'Use "allow-preleases" instead.'.format(name)
'Use "allow-prereleases" instead.'.format(name)
)
# Checking for scripts with extras
......
......@@ -234,7 +234,7 @@ class Installer:
# Execute operations
actual_ops = [op for op in ops if not op.skipped]
if not actual_ops and (self._execute_operations or self._dry_run):
self._io.write_line("Nothing to install or update")
self._io.write_line("No dependencies to install or update")
if actual_ops and (self._execute_operations or self._dry_run):
installs = []
......
......@@ -40,7 +40,10 @@ class PipInstaller(BaseInstaller):
args = ["install", "--no-deps"]
if package.source_type == "legacy" and package.source_url:
if (
package.source_type not in {"git", "directory", "file", "url"}
and package.source_url
):
repository = self._pool.repository(package.source_reference)
parsed = urlparse.urlparse(package.source_url)
if parsed.scheme == "http":
......@@ -70,7 +73,7 @@ class PipInstaller(BaseInstaller):
if update:
args.append("-U")
if package.files and not package.source_type:
if package.files and not package.source_url:
# Format as a requirements.txt
# We need to create a requirements.txt file
# for each package in order to check hashes.
......@@ -93,7 +96,12 @@ class PipInstaller(BaseInstaller):
self.run(*args)
def update(self, _, target):
def update(self, package, target):
if package.source_type != target.source_type:
# If the source type has changed, we remove the current
# package to avoid perpetual updates in some cases
self.remove(package)
self.install(target, update=True)
def remove(self, package):
......@@ -112,7 +120,7 @@ class PipInstaller(BaseInstaller):
raise
def run(self, *args, **kwargs): # type: (...) -> str
return self._env.run("python", "-m", "pip", *args, **kwargs)
return self._env.run_pip(*args, **kwargs)
def requirement(self, package, formatted=False):
if formatted and not package.source_type:
......
......@@ -11,7 +11,6 @@ from typing import Union
from clikit.api.io.flags import VERY_VERBOSE
from poetry.utils._compat import Path
from poetry.utils._compat import basestring
from poetry.utils._compat import glob
from poetry.utils._compat import lru_cache
from poetry.utils._compat import to_str
......@@ -84,15 +83,6 @@ class Builder(object):
explicitely_excluded = set()
for excluded_glob in self._package.exclude:
excluded_path = Path(self._path, excluded_glob)
try:
is_dir = excluded_path.is_dir()
except OSError:
# On Windows, testing if a path with a glob is a directory will raise an OSError
is_dir = False
if is_dir:
excluded_glob = Path(excluded_glob, "**/*")
for excluded in glob(
Path(self._path, excluded_glob).as_posix(), recursive=True
......@@ -112,10 +102,18 @@ class Builder(object):
return result
def is_excluded(self, filepath): # type: (Union[str, Path]) -> bool
if not isinstance(filepath, basestring):
filepath = filepath.as_posix()
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 filepath in self.find_excluded_files()
return False
def find_files_to_add(self, exclude_build=True): # type: (bool) -> list
"""
......@@ -164,7 +162,7 @@ class Builder(object):
)
to_add.append(license_file.relative_to(self._path))
# If a README is specificed we need to include it
# 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"]
......
......@@ -31,16 +31,14 @@ class EditableBuilder(Builder):
try:
if self._env.pip_version < Version(19, 0):
self._env.run("python", "-m", "pip", "install", "-e", str(self._path))
self._env.run_pip("install", "-e", str(self._path))
else:
# Temporarily rename pyproject.toml
shutil.move(
str(self._poetry.file), str(self._poetry.file.with_suffix(".tmp"))
)
try:
self._env.run(
"python", "-m", "pip", "install", "-e", str(self._path)
)
self._env.run_pip("install", "-e", str(self._path))
finally:
shutil.move(
str(self._poetry.file.with_suffix(".tmp")),
......
......@@ -2,6 +2,7 @@
import os
import re
import tarfile
import time
from collections import defaultdict
from copy import copy
......@@ -83,12 +84,14 @@ class SdistBuilder(Builder):
setup = self.build_setup()
tar_info = tarfile.TarInfo(pjoin(tar_dir, "setup.py"))
tar_info.size = len(setup)
tar_info.mtime = time.time()
tar.addfile(tar_info, BytesIO(setup))
pkg_info = self.build_pkg_info()
tar_info = tarfile.TarInfo(pjoin(tar_dir, "PKG-INFO"))
tar_info.size = len(pkg_info)
tar_info.mtime = time.time()
tar.addfile(tar_info, BytesIO(pkg_info))
finally:
tar.close()
......@@ -234,7 +237,7 @@ class SdistBuilder(Builder):
if from_top_level == ".":
continue
is_subpkg = "__init__.py" in filenames
is_subpkg = any([filename.endswith(".py") for filename in filenames])
if is_subpkg:
subpkg_paths.add(from_top_level)
parts = from_top_level.split(os.sep)
......
......@@ -115,9 +115,9 @@ class WheelBuilder(Builder):
return
lib = lib[0]
excluded = self.find_excluded_files()
for pkg in lib.glob("**/*"):
if pkg.is_dir() or pkg in excluded:
if pkg.is_dir() or self.is_excluded(pkg):
continue
rel_path = str(pkg.relative_to(lib))
......@@ -132,7 +132,7 @@ class WheelBuilder(Builder):
self._add_file(wheel, pkg, rel_path)
def _copy_module(self, wheel):
excluded = self.find_excluded_files()
to_add = []
for include in self._module.includes:
......@@ -153,7 +153,9 @@ class WheelBuilder(Builder):
else:
rel_file = file.relative_to(self._path)
if rel_file.as_posix() in excluded:
if self.is_excluded(rel_file.as_posix()) and isinstance(
include, PackageInclude
):
continue
if file.suffix == ".pyc":
......
......@@ -2,7 +2,7 @@ import logging
from poetry.utils.helpers import get_cert
from poetry.utils.helpers import get_client_cert
from poetry.utils.helpers import get_http_basic_auth
from poetry.utils.password_manager import PasswordManager
from .uploader import Uploader
......@@ -20,6 +20,7 @@ class Publisher:
self._package = poetry.package
self._io = io
self._uploader = Uploader(poetry, io)
self._password_manager = PasswordManager(poetry.config)
@property
def files(self):
......@@ -60,21 +61,21 @@ class Publisher:
if not (username and password):
# Check if we have a token first
token = self._poetry.config.get("pypi-token.{}".format(repository_name))
token = self._password_manager.get_pypi_token(repository_name)
if token:
logger.debug("Found an API token for {}.".format(repository_name))
username = "__token__"
password = token
else:
auth = get_http_basic_auth(self._poetry.config, repository_name)
auth = self._password_manager.get_http_auth(repository_name)
if auth:
logger.debug(
"Found authentication information for {}.".format(
repository_name
)
)
username = auth[0]
password = auth[1]
username = auth["username"]
password = auth["password"]
resolved_client_cert = client_cert or get_client_cert(
self._poetry.config, repository_name
......
......@@ -12,7 +12,6 @@ class PackageInclude(Include):
base = base / source
super(PackageInclude, self).__init__(base, include, formats=formats)
self.check_elements()
@property
......@@ -35,6 +34,8 @@ class PackageInclude(Include):
return self.check_elements()
def check_elements(self): # type: () -> PackageInclude
root = self._elements[0]
if not self._elements:
raise ValueError(
"{} does not contain any element".format(self._base / self._include)
......@@ -44,20 +45,24 @@ class PackageInclude(Include):
# Probably glob
self._is_package = True
# The __init__.py file should be first
root = self._elements[0]
if root.name != "__init__.py":
raise ValueError("{} is not a package.".format(root))
# 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
if not any([element.suffix == ".py" for element in self._elements]):
raise ValueError("{} is not a package.".format(root.name))
self._package = root.parent.name
else:
if self._elements[0].is_dir():
if root.is_dir():
# If it's a directory, we include everything inside it
self._package = self._elements[0].name
self._elements = sorted(list(self._elements[0].glob("**/*")))
self._package = root.name
self._elements = sorted(list(root.glob("**/*")))
if not any([element.suffix == ".py" for element in self._elements]):
raise ValueError("{} is not a package.".format(root.name))
self._is_package = True
else:
self._package = self._elements[0].stem
self._package = root.stem
self._is_module = True
return self
......@@ -31,18 +31,27 @@ class _Writer:
def write(self):
buffer = []
required_python_version = None
required_python_version_notification = False
for incompatibility in self._root.external_incompatibilities:
if isinstance(incompatibility.cause, PythonCause):
required_python_version = incompatibility.cause.root_python_version
break
if not required_python_version_notification:
buffer.append(
"The current project's Python requirement ({}) "
"is not compatible with some of the required "
"packages Python requirement:".format(
incompatibility.cause.root_python_version
)
)
required_python_version_notification = True
if required_python_version is not None:
buffer.append(
"The current project must support the following Python versions: {}".format(
required_python_version
buffer.append(
" - {} requires Python {}".format(
incompatibility.terms[0].dependency.name,
incompatibility.cause.python_version,
)
)
)
if required_python_version_notification:
buffer.append("")
if isinstance(self._root.cause, ConflictCause):
......
......@@ -25,6 +25,8 @@ 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()
......@@ -46,6 +48,8 @@ def dependency_from_pep_508(name):
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(".")):
......@@ -74,10 +78,15 @@ def dependency_from_pep_508(name):
version = m.group("ver")
dep = Dependency(name, version)
else:
name = link.egg_fragment
name = req.name or link.egg_fragment
if link.scheme == "git":
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:
......
......@@ -55,6 +55,7 @@ class Dependency(object):
self._python_constraint = parse_constraint("*")
self._transitive_python_versions = None
self._transitive_python_constraint = None
self._transitive_marker = None
self._extras = []
self._in_extras = []
......@@ -118,6 +119,17 @@ class Dependency(object):
self._transitive_python_constraint = parse_constraint(value)
@property
def transitive_marker(self):
if self._transitive_marker is None:
return self.marker
return self._transitive_marker
@transitive_marker.setter
def transitive_marker(self, value):
self._transitive_marker = value
@property
def python_constraint(self):
return self._python_constraint
......
......@@ -74,6 +74,17 @@ class DirectoryDependency(Dependency):
def develop(self):
return self._develop
@property
def base_pep_508_name(self): # type: () -> str
requirement = self.pretty_name
if self.extras:
requirement += "[{}]".format(",".join(self.extras))
requirement += " @ {}".format(self._path)
return requirement
def supports_poetry(self):
return self._supports_poetry
......
......@@ -49,6 +49,17 @@ class FileDependency(Dependency):
def full_path(self):
return self._full_path.resolve()
@property
def base_pep_508_name(self): # type: () -> str
requirement = self.pretty_name
if self.extras:
requirement += "[{}]".format(",".join(self.extras))
requirement += " @ {}".format(self._path)
return requirement
def is_file(self):
return True
......
......@@ -8,6 +8,7 @@ from tomlkit import document
from tomlkit import inline_table
from tomlkit import item
from tomlkit import table
from tomlkit.exceptions import TOMLKitError
import poetry.packages
import poetry.repositories
......@@ -137,8 +138,11 @@ class Locker(object):
package.add_dependency(dep_name, constraint)
if "develop" in info:
package.develop = info["develop"]
if "source" in info:
package.source_type = info["source"]["type"]
package.source_type = info["source"].get("type", "")
package.source_url = info["source"]["url"]
package.source_reference = info["source"]["reference"]
......@@ -217,7 +221,10 @@ class Locker(object):
if not self._lock.exists():
raise RuntimeError("No lockfile found. Unable to read locked packages")
return self._lock.read()
try:
return self._lock.read()
except TOMLKitError as e:
raise RuntimeError("Unable to read the lock file ({}).".format(e))
def _lock_packages(
self, packages
......@@ -290,11 +297,14 @@ class Locker(object):
data["dependencies"] = dependencies
if package.source_type:
if package.source_url:
data["source"] = {
"type": package.source_type,
"url": package.source_url,
"reference": package.source_reference,
}
if package.source_type:
data["source"]["type"] = package.source_type
if package.source_type == "directory":
data["develop"] = package.develop
return data
......@@ -279,7 +279,7 @@ class Package(object):
message = (
'The "{}" dependency specifies '
'the "allows-prereleases" property, which is deprecated. '
'Use "allow-preleases" instead.'.format(name)
'Use "allow-prereleases" instead.'.format(name)
)
warn(message, DeprecationWarning)
logger.warning(message)
......@@ -297,6 +297,7 @@ class Package(object):
branch=constraint.get("branch", None),
tag=constraint.get("tag", None),
rev=constraint.get("rev", None),
category=category,
optional=optional,
)
elif "file" in constraint:
......
......@@ -5,8 +5,13 @@ import re
from poetry.packages.constraints.constraint import Constraint
from poetry.packages.constraints.multi_constraint import MultiConstraint
from poetry.packages.constraints.union_constraint import UnionConstraint
from poetry.semver import EmptyConstraint
from poetry.semver import Version
from poetry.semver import VersionConstraint
from poetry.semver import VersionRange
from poetry.semver import VersionUnion
from poetry.semver import parse_constraint
from poetry.version.markers import BaseMarker
from poetry.version.markers import MarkerUnion
from poetry.version.markers import MultiMarker
from poetry.version.markers import SingleMarker
......@@ -236,3 +241,66 @@ def create_nested_marker(name, constraint):
marker = '{} {} "{}"'.format(name, op, version)
return marker
def get_python_constraint_from_marker(
marker,
): # type: (BaseMarker) -> VersionConstraint
python_marker = marker.only("python_version")
if python_marker.is_any():
return VersionRange()
if python_marker.is_empty():
return EmptyConstraint()
markers = convert_markers(marker)
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))
return parse_constraint(" || ".join(ors))
from poetry.vcs import git
from .dependency import Dependency
......@@ -7,7 +9,15 @@ class VCSDependency(Dependency):
"""
def __init__(
self, name, vcs, source, branch=None, tag=None, rev=None, optional=False
self,
name,
vcs,
source,
branch=None,
tag=None,
rev=None,
category="main",
optional=False,
):
self._vcs = vcs
self._source = source
......@@ -21,7 +31,7 @@ class VCSDependency(Dependency):
self._rev = rev
super(VCSDependency, self).__init__(
name, "*", optional=optional, allows_prereleases=True
name, "*", category=category, optional=optional, allows_prereleases=True
)
@property
......@@ -65,11 +75,17 @@ class VCSDependency(Dependency):
@property
def base_pep_508_name(self): # type: () -> str
requirement = self.pretty_name
parsed_url = git.ParsedUrl.parse(self._source)
if self.extras:
requirement += "[{}]".format(",".join(self.extras))
requirement += " @ {}+{}@{}".format(self._vcs, self._source, self.reference)
if parsed_url.protocol is not None:
requirement += " @ {}+{}@{}".format(self._vcs, self._source, self.reference)
else:
requirement += " @ {}+ssh://{}@{}".format(
self._vcs, parsed_url.format(), self.reference
)
return requirement
......
......@@ -6,6 +6,7 @@ import time
from contextlib import contextmanager
from tempfile import mkdtemp
from typing import Any
from typing import List
from typing import Optional
......@@ -27,6 +28,7 @@ from poetry.packages import PackageCollection
from poetry.packages import URLDependency
from poetry.packages import VCSDependency
from poetry.packages import dependency_from_pep_508
from poetry.packages.utils.utils import get_python_constraint_from_marker
from poetry.repositories import Pool
from poetry.utils._compat import PY35
from poetry.utils._compat import OrderedDict
......@@ -61,9 +63,7 @@ class Provider:
UNSAFE_PACKAGES = {"setuptools", "distribute", "pip"}
def __init__(
self, package, pool, io # type: Package # type: Pool
): # type: (...) -> None
def __init__(self, package, pool, io): # type: (Package, Pool, Any) -> None
self._package = package
self._pool = pool
self._io = io
......@@ -275,6 +275,7 @@ class Provider:
)
package.source_url = dependency.path.as_posix()
package.develop = dependency.develop
if dependency.base is not None:
package.root_dir = dependency.base.as_posix()
......@@ -489,14 +490,15 @@ class Provider:
if not package.python_constraint.allows_all(
self._package.python_constraint
):
intersection = package.python_constraint.intersect(
package.dependency.transitive_python_constraint
transitive_python_constraint = get_python_constraint_from_marker(
package.dependency.transitive_marker
)
difference = package.dependency.transitive_python_constraint.difference(
intersection
intersection = package.python_constraint.intersect(
transitive_python_constraint
)
difference = transitive_python_constraint.difference(intersection)
if (
package.dependency.transitive_python_constraint.is_any()
transitive_python_constraint.is_any()
or self._package.python_constraint.intersect(
package.dependency.python_constraint
).is_empty()
......@@ -604,7 +606,7 @@ class Provider:
new_markers = []
for dep in _deps:
marker = dep.marker.without_extras()
if marker.is_empty():
if marker.is_any():
# No marker or only extras
continue
......@@ -671,13 +673,28 @@ class Provider:
raise CompatibilityError(*python_constraints)
# Modifying dependencies as needed
clean_dependencies = []
for dep in dependencies:
if not package.dependency.transitive_marker.without_extras().is_any():
marker_intersection = package.dependency.transitive_marker.without_extras().intersect(
dep.marker.without_extras()
)
if marker_intersection.is_empty():
# The dependency is not needed, since the markers specified
# for the current package selection are not compatible with
# the markers for the current dependency, so we skip it
continue
dep.transitive_marker = marker_intersection
if not package.dependency.python_constraint.is_any():
dep.transitive_python_versions = str(
dep.python_constraint.intersect(
package.dependency.python_constraint
)
python_constraint_intersection = dep.python_constraint.intersect(
package.dependency.python_constraint
)
if python_constraint_intersection.is_empty():
# This dependency is not needed under current python constraint.
continue
dep.transitive_python_versions = str(python_constraint_intersection)
if (package.dependency.is_directory() or package.dependency.is_file()) and (
dep.is_directory() or dep.is_file()
......@@ -691,8 +708,9 @@ class Provider:
# TODO: Improve the way we set the correct relative path for dependencies
dep._path = relative
clean_dependencies.append(dep)
package.requires = dependencies
package.requires = clean_dependencies
return package
......
......@@ -56,12 +56,20 @@ class Solver:
installed = True
if pkg.source_type == "git" and package.source_type == "git":
from poetry.vcs.git import Git
# Trying to find the currently installed version
pkg_source_url = Git.normalize_url(pkg.source_url)
package_source_url = Git.normalize_url(package.source_url)
for locked in self._locked.packages:
if locked.name != pkg.name or locked.source_type != "git":
continue
locked_source_url = Git.normalize_url(locked.source_url)
if (
locked.name == pkg.name
and locked.source_type == pkg.source_type
and locked.source_url == pkg.source_url
and locked_source_url == pkg_source_url
and locked.source_reference == pkg.source_reference
):
pkg = Package(pkg.name, locked.version)
......@@ -70,7 +78,7 @@ class Solver:
pkg.source_reference = locked.source_reference
break
if pkg.source_url != package.source_url or (
if pkg_source_url != package_source_url or (
pkg.source_reference != package.source_reference
and not pkg.source_reference.startswith(
package.source_reference
......@@ -84,6 +92,8 @@ class Solver:
elif package.version != pkg.version:
# Checking version
operations.append(Update(pkg, package))
elif package.source_type != pkg.source_type:
operations.append(Update(pkg, package))
else:
operations.append(Install(package).skip("Already installed"))
......@@ -212,8 +222,12 @@ class Solver:
else:
category = dep.category
optional = dep.is_optional() and not dep.is_activated()
intersection = previous["marker"].intersect(previous_dep.marker)
intersection = intersection.intersect(package.marker)
intersection = (
previous["marker"]
.without_extras()
.intersect(previous_dep.transitive_marker.without_extras())
)
intersection = intersection.intersect(package.marker.without_extras())
marker = intersection
......
import re
from poetry.packages import Package
from poetry.utils._compat import Path
from poetry.utils._compat import metadata
from poetry.utils.env import Env
from .repository import Repository
......@@ -15,30 +15,52 @@ class InstalledRepository(Repository):
For now, it uses the pip "freeze" command.
"""
repo = cls()
seen = set()
for entry in env.sys_path:
for distribution in sorted(
metadata.distributions(path=[entry]), key=lambda d: str(d._path),
):
name = distribution.metadata["name"]
version = distribution.metadata["version"]
package = Package(name, version, version)
package.description = distribution.metadata.get("summary", "")
if package.name in seen:
continue
seen.add(package.name)
repo.add_package(package)
path = Path(str(distribution._path))
is_standard_package = True
try:
path.relative_to(env.site_packages)
except ValueError:
is_standard_package = False
if is_standard_package:
continue
src_path = env.path / "src"
# A VCS dependency should have been installed
# in the src directory. If not, it's a path dependency
try:
path.relative_to(src_path)
from poetry.vcs.git import Git
git = Git()
revision = git.rev_parse("HEAD", src_path / package.name).strip()
url = git.remote_url(src_path / package.name)
freeze_output = env.run("python", "-m", "pip", "freeze")
for line in freeze_output.split("\n"):
if "==" in line:
name, version = re.split("={2,3}", line)
repo.add_package(Package(name, version, version))
elif line.startswith("-e "):
line = line[3:].strip()
if line.startswith("git+"):
url = line.lstrip("git+")
if "@" in url:
url, rev = url.rsplit("@", 1)
else:
rev = "master"
name = url.split("/")[-1].rstrip(".git")
if "#egg=" in rev:
rev, name = rev.split("#egg=")
package = Package(name, "0.0.0")
package.source_type = "git"
package.source_url = url
package.source_reference = rev
repo.add_package(package)
package.source_reference = revision
except ValueError:
package.source_type = "directory"
package.source_url = str(path.parent)
return repo
......@@ -269,7 +269,6 @@ class LegacyRepository(PyPiRepository):
for version in versions:
package = Package(name, version)
package.source_type = "legacy"
package.source_url = self._url
if extras is not None:
......@@ -314,7 +313,6 @@ class LegacyRepository(PyPiRepository):
if release_info["requires_python"]:
package.python_versions = release_info["requires_python"]
package.source_type = "legacy"
package.source_url = self._url
package.source_reference = self.name
......
......@@ -8,10 +8,12 @@ from typing import Union
from cachecontrol import CacheControl
from cachecontrol.caches.file_cache import FileCache
from cachecontrol.controller import logger as cache_control_logger
from cachy import CacheManager
from html5lib.html5parser import parse
from requests import get
from requests import session
from requests.exceptions import TooManyRedirects
from poetry.locations import CACHE_DIR
from poetry.packages import Package
......@@ -39,12 +41,14 @@ except ImportError:
import urlparse
cache_control_logger.setLevel(logging.ERROR)
logger = logging.getLogger(__name__)
class PyPiRepository(Repository):
CACHE_VERSION = parse_constraint("1.0.0b2")
CACHE_VERSION = parse_constraint("1.0.0")
def __init__(self, url="https://pypi.org/", disable_cache=False, fallback=True):
self._url = url
......@@ -63,9 +67,8 @@ class PyPiRepository(Repository):
}
)
self._session = CacheControl(
session(), cache=FileCache(str(release_cache_dir / "_http"))
)
self._cache_control_cache = FileCache(str(release_cache_dir / "_http"))
self._session = CacheControl(session(), cache=self._cache_control_cache)
self._inspector = Inspector()
super(PyPiRepository, self).__init__()
......@@ -357,7 +360,14 @@ class PyPiRepository(Repository):
return data
def _get(self, endpoint): # type: (str) -> Union[dict, None]
json_response = self._session.get(self._url + endpoint)
try:
json_response = self._session.get(self._url + endpoint)
except TooManyRedirects:
# Cache control redirect loop.
# We try to remove the cache and try again
self._cache_control_cache.delete(self._url + endpoint)
json_response = self._session.get(self._url + endpoint)
if json_response.status_code == 404:
return None
......
......@@ -76,14 +76,12 @@ def parse_single_constraint(constraint): # type: (str) -> VersionConstraint
version = Version.parse(m.group(1))
if precision == 2:
low = version
high = version.stable.next_major
else:
low = Version(version.major, version.minor, version.patch)
high = version.stable.next_minor
return VersionRange(
low, high, include_min=True, always_include_max_prerelease=True
version, high, include_min=True, always_include_max_prerelease=True
)
# Caret range
......
......@@ -12,6 +12,13 @@ except ImportError:
from glob import glob
try:
from importlib import metadata
import zipfile as zipp
except ImportError:
import importlib_metadata as metadata
import zipp
try:
import urllib.parse as urlparse
except ImportError:
import urlparse
......
......@@ -54,7 +54,7 @@ class Exporter(object):
extras=None,
with_credentials=False,
): # type: (Path, Union[IO, str], bool, bool, bool) -> None
indexes = []
indexes = set()
content = ""
packages = self._poetry.locker.locked_repository(dev).packages
......@@ -94,7 +94,7 @@ class Exporter(object):
dependency.marker = package.marker
line = "{}".format(package.source_url)
if package.develop:
if package.develop and package.source_type == "directory":
line = "-e " + line
else:
dependency = package.to_dependency()
......@@ -104,8 +104,11 @@ class Exporter(object):
if ";" in requirement:
line += "; {}".format(requirement.split(";")[1].strip())
if package.source_type == "legacy" and package.source_url:
indexes.append(package.source_url)
if (
package.source_type not in {"git", "directory", "file", "url"}
and package.source_url
):
indexes.add(package.source_url)
if package.files and with_hashes:
hashes = []
......@@ -131,10 +134,9 @@ class Exporter(object):
content += line
if indexes:
# If we have extra indexes, we add them to the begin
# of the output
# If we have extra indexes, we add them to the beginning of the output
indexes_header = ""
for index in indexes:
for index in sorted(indexes):
repository = [
r
for r in self._poetry.pool.repositories
......
......@@ -8,11 +8,6 @@ from contextlib import contextmanager
from typing import List
from typing import Optional
from keyring import delete_password
from keyring import get_password
from keyring import set_password
from keyring.errors import KeyringError
from poetry.config.config import Config
from poetry.utils._compat import Path
from poetry.version import Version
......@@ -95,53 +90,6 @@ def parse_requires(requires): # type: (str) -> List[str]
return requires_dist
def keyring_service_name(repository_name): # type: (str) -> str
return "{}-{}".format("poetry-repository", repository_name)
def keyring_repository_password_get(
repository_name, username
): # type: (str, str) -> Optional[str]
try:
return get_password(keyring_service_name(repository_name), username)
except (RuntimeError, KeyringError):
return None
def keyring_repository_password_set(
repository_name, username, password
): # type: (str, str, str) -> None
try:
set_password(keyring_service_name(repository_name), username, password)
except (RuntimeError, KeyringError):
raise RuntimeError("Failed to store password in keyring")
def keyring_repository_password_del(
config, repository_name
): # type: (Config, str) -> None
try:
repo_auth = config.get("http-basic.{}".format(repository_name))
if repo_auth and "username" in repo_auth:
delete_password(
keyring_service_name(repository_name), repo_auth["username"]
)
except (RuntimeError, KeyringError):
pass
def get_http_basic_auth(
config, repository_name
): # type: (Config, str) -> Optional[tuple]
repo_auth = config.get("http-basic.{}".format(repository_name))
if repo_auth:
username, password = repo_auth["username"], repo_auth.get("password")
if password is None:
password = keyring_repository_password_get(repository_name, username)
return username, password
return None
def get_cert(config, repository_name): # type: (Config, str) -> Optional[Path]
cert = config.get("certificates.{}.cert".format(repository_name))
if cert:
......@@ -159,11 +107,17 @@ def get_client_cert(config, repository_name): # type: (Config, str) -> Optional
def _on_rm_error(func, path, exc_info):
if not os.path.exists(path):
return
os.chmod(path, stat.S_IWRITE)
func(path)
def safe_rmtree(path):
if Path(path).is_symlink():
return os.unlink(str(path))
shutil.rmtree(path, onerror=_on_rm_error)
......
import logging
logger = logging.getLogger(__name__)
class PasswordManagerError(Exception):
pass
class KeyRingError(Exception):
pass
class KeyRing:
def __init__(self, namespace):
self._namespace = namespace
self._is_available = True
self._check()
def is_available(self):
return self._is_available
def get_password(self, name, username):
if not self.is_available():
return
import keyring
import keyring.errors
name = self.get_entry_name(name)
try:
return keyring.get_password(name, username)
except (RuntimeError, keyring.errors.KeyringError):
raise KeyRingError(
"Unable to retrieve the password for {} from the key ring".format(name)
)
def set_password(self, name, username, password):
if not self.is_available():
return
import keyring
import keyring.errors
name = self.get_entry_name(name)
try:
keyring.set_password(name, username, password)
except (RuntimeError, keyring.errors.KeyringError) as e:
raise KeyRingError(
"Unable to store the password for {} in the key ring: {}".format(
name, str(e)
)
)
def delete_password(self, name, username):
if not self.is_available():
return
import keyring
import keyring.errors
name = self.get_entry_name(name)
try:
keyring.delete_password(name, username)
except (RuntimeError, keyring.errors.KeyringError):
raise KeyRingError(
"Unable to delete the password for {} from the key ring".format(name)
)
def get_entry_name(self, name):
return "{}-{}".format(self._namespace, name)
def _check(self):
try:
import keyring
except Exception as e:
logger.debug("An error occurred while importing keyring: {}".format(str(e)))
self._is_available = False
return
backend = keyring.get_keyring()
name = backend.name.split(" ")[0]
if name == "fail":
logger.debug("No suitable keyring backend found")
self._is_available = False
elif "plaintext" in backend.name.lower():
logger.debug("Only a plaintext keyring backend is available. Not using it.")
self._is_available = False
elif name == "chainer":
try:
import keyring.backend
backends = keyring.backend.get_all_keyring()
self._is_available = any(
[
b.name.split(" ")[0] not in ["chainer", "fail"]
and "plaintext" not in b.name.lower()
for b in backends
]
)
except Exception:
self._is_available = False
if not self._is_available:
logger.warning("No suitable keyring backends were found")
class PasswordManager:
def __init__(self, config):
self._config = config
self._keyring = KeyRing("poetry-repository")
if not self._keyring.is_available():
logger.warning("Using a plaintext file to store and retrieve credentials")
@property
def keyring(self):
return self._keyring
def set_pypi_token(self, name, token):
if not self._keyring.is_available():
self._config.auth_config_source.add_property(
"pypi-token.{}".format(name), token
)
else:
self._keyring.set_password(name, "__token__", token)
def get_pypi_token(self, name):
if not self._keyring.is_available():
return self._config.get("pypi-token.{}".format(name))
return self._keyring.get_password(name, "__token__")
def delete_pypi_token(self, name):
if not self._keyring.is_available():
return self._config.auth_config_source.remove_property(
"pypi-token.{}".format(name)
)
self._keyring.delete_password(name, "__token__")
def get_http_auth(self, name):
auth = self._config.get("http-basic.{}".format(name))
if not auth:
return None
username, password = auth["username"], auth.get("password")
if password is None:
password = self._keyring.get_password(name, username)
return {
"username": username,
"password": password,
}
def set_http_password(self, name, username, password):
auth = {"username": username}
if not self._keyring.is_available():
auth["password"] = password
else:
self._keyring.set_password(name, username, password)
self._config.auth_config_source.add_property("http-basic.{}".format(name), auth)
def delete_http_password(self, name):
auth = self.get_http_auth(name)
if not auth or "username" not in auth:
return
try:
self._keyring.delete_password(name, auth["username"])
except KeyRingError:
pass
self._config.auth_config_source.remove_property("http-basic.{}".format(name))
......@@ -181,10 +181,11 @@ class SetupReader(object):
continue
func = value.func
if not isinstance(func, ast.Name):
continue
if func.id != "setup":
if not (isinstance(func, ast.Name) and func.id == "setup") and not (
isinstance(func, ast.Attribute)
and func.value.id == "setuptools"
and func.attr == "setup"
):
continue
return value, elements
......
......@@ -58,7 +58,9 @@ class Shell:
self._path, ["-i"], dimensions=(terminal.height, terminal.width)
)
c.setecho(False)
if self._name == "zsh":
c.setecho(False)
activate_script = self._get_activate_script()
bin_dir = "Scripts" if WINDOWS else "bin"
activate_path = env.path / bin_dir / activate_script
......
......@@ -2,9 +2,103 @@
import re
import subprocess
from collections import namedtuple
from poetry.utils._compat import decode
PATTERNS = [
re.compile(
r"(git\+)?"
r"((?P<protocol>\w+)://)"
r"((?P<user>\w+)@)?"
r"(?P<resource>[\w.\-]+)"
r"(:(?P<port>\d+))?"
r"(?P<pathname>(/(?P<owner>\w+)/)"
r"((?P<projects>([\w\-/]+)/)?(?P<name>[\w\-]+)(\.git|/)?)?)"
r"([@#](?P<rev>[^@#]+))?"
r"$"
),
re.compile(
r"^(git\+)?"
r"(?P<protocol>https?|git|ssh|rsync|file)://"
r"(?:(?P<user>.+)@)*"
r"(?P<resource>[a-z0-9_.-]*)"
r"(:?P<port>[\d]+)?"
r"(?P<pathname>[:/]((?P<owner>[\w\-]+)/(?P<projects>([\w\-/]+)/)?)?"
r"((?P<name>[\w\-.]+?)(\.git|/)?)?)"
r"([@#](?P<rev>[^@#]+))?"
r"$"
),
re.compile(
r"^(?:(?P<user>.+)@)*"
r"(?P<resource>[a-z0-9_.-]*)[:]*"
r"(?P<port>[\d]+)?"
r"(?P<pathname>/?(?P<owner>.+)/(?P<projects>([\w\-/]+)/)?(?P<name>.+).git)"
r"([@#](?P<rev>[^@#]+))?"
r"$"
),
re.compile(
r"((?P<user>\w+)@)?"
r"(?P<resource>[\w.\-]+)"
r"[:/]{1,2}"
r"(?P<pathname>((?P<owner>\w+)/)?"
r"(?P<projects>([\w\-/]+)/)?"
r"((?P<name>[\w\-]+)(\.git|/)?)?)"
r"([@#](?P<rev>[^@#]+))?"
r"$"
),
]
class ParsedUrl:
def __init__(self, protocol, resource, pathname, user, port, name, rev):
self.protocol = protocol
self.resource = resource
self.pathname = pathname
self.user = user
self.port = port
self.name = name
self.rev = rev
@classmethod
def parse(cls, url): # type: () -> ParsedUrl
for pattern in PATTERNS:
m = pattern.match(url)
if m:
groups = m.groupdict()
return ParsedUrl(
groups.get("protocol"),
groups.get("resource"),
groups.get("pathname"),
groups.get("user"),
groups.get("port"),
groups.get("name"),
groups.get("rev"),
)
raise ValueError('Invalid git url "{}"'.format(url))
@property
def url(self): # type: () -> str
return "{}{}{}{}{}".format(
"{}://".format(self.protocol) if self.protocol else "",
"{}@".format(self.user) if self.user else "",
self.resource,
":{}".format(self.port) if self.port else "",
"/" + self.pathname.lstrip(":/"),
)
def format(self):
return "{}".format(self.url, "#{}".format(self.rev) if self.rev else "",)
def __str__(self): # type: () -> str
return self.format()
GitUrl = namedtuple("GitUrl", ["url", "revision"])
class GitConfig:
def __init__(self, requires_git_presence=False):
self._config = {}
......@@ -36,6 +130,30 @@ class Git:
self._config = GitConfig(requires_git_presence=True)
self._work_dir = work_dir
@classmethod
def normalize_url(cls, url): # type: (str) -> GitUrl
parsed = ParsedUrl.parse(url)
formatted = re.sub(r"^git\+", "", url)
if parsed.rev:
formatted = re.sub(r"[#@]{}$".format(parsed.rev), "", formatted)
altered = parsed.format() != formatted
if altered:
if re.match(r"^git\+https?", url) and re.match(
r"^/?:[^0-9]", parsed.pathname
):
normalized = re.sub(r"git\+(.*:[^:]+):(.*)", "\\1/\\2", url)
elif re.match(r"^git\+file", url):
normalized = re.sub(r"git\+", "", url)
else:
normalized = re.sub(r"^(?:git\+)?ssh://", "", url)
else:
normalized = parsed.format()
return GitUrl(re.sub(r"#[^#]*$", "", normalized), parsed.rev)
@property
def config(self): # type: () -> GitConfig
return self._config
......@@ -93,9 +211,35 @@ class Git:
args += ["ls-files", "--others", "-i", "--exclude-standard"]
output = self.run(*args)
return output.split("\n")
return output.strip().split("\n")
def remote_urls(self, folder=None): # type: (...) -> dict
output = self.run(
"config", "--get-regexp", r"remote\..*\.url", folder=folder
).strip()
urls = {}
for url in output.splitlines():
name, url = url.split(" ", 1)
urls[name.strip()] = url.strip()
return urls
def remote_url(self, folder=None): # type: (...) -> str
urls = self.remote_urls(folder=folder)
return urls.get("remote.origin.url", urls[list(urls.keys())[0]])
def run(self, *args, **kwargs): # type: (...) -> str
folder = kwargs.pop("folder", None)
if folder:
args = (
"--git-dir",
(folder / ".git").as_posix(),
"--work-tree",
folder.as_posix(),
) + args
def run(self, *args): # type: (...) -> str
return decode(
subprocess.check_output(["git"] + list(args), stderr=subprocess.STDOUT)
)
).strip()
......@@ -175,6 +175,12 @@ class BaseMarker(object):
def without_extras(self): # type: () -> BaseMarker
raise NotImplementedError()
def exclude(self, marker_name): # type: (str) -> BaseMarker
raise NotImplementedError()
def only(self, marker_name): # type: (str) -> BaseMarker
raise NotImplementedError()
def __repr__(self):
return "<{} {}>".format(self.__class__.__name__, str(self))
......@@ -198,12 +204,27 @@ class AnyMarker(BaseMarker):
def without_extras(self):
return self
def exclude(self, marker_name): # type: (str) -> AnyMarker
return self
def only(self, marker_name): # type: (str) -> AnyMarker
return self
def __str__(self):
return ""
def __repr__(self):
return "<AnyMarker>"
def __hash__(self):
return hash(("<any>", "<any>"))
def __eq__(self, other):
if not isinstance(other, BaseMarker):
return NotImplemented
return isinstance(other, AnyMarker)
class EmptyMarker(BaseMarker):
def intersect(self, other):
......@@ -224,12 +245,27 @@ class EmptyMarker(BaseMarker):
def without_extras(self):
return self
def exclude(self, marker_name): # type: (str) -> EmptyMarker
return self
def only(self, marker_name): # type: (str) -> EmptyMarker
return self
def __str__(self):
return "<empty>"
def __repr__(self):
return "<EmptyMarker>"
def __hash__(self):
return hash(("<empty>", "<empty>"))
def __eq__(self, other):
if not isinstance(other, BaseMarker):
return NotImplemented
return isinstance(other, EmptyMarker)
class SingleMarker(BaseMarker):
......@@ -329,7 +365,7 @@ class SingleMarker(BaseMarker):
if self == other:
return self
return MarkerUnion(self, other)
return MarkerUnion.of(self, other)
return other.union(self)
......@@ -343,7 +379,16 @@ class SingleMarker(BaseMarker):
return self._constraint.allows(self._parser(environment[self._name]))
def without_extras(self):
if self.name == "extra":
return self.exclude("extra")
def exclude(self, marker_name): # type: (str) -> BaseMarker
if self.name == marker_name:
return AnyMarker()
return self
def only(self, marker_name): # type: (str) -> BaseMarker
if self.name != marker_name:
return EmptyMarker()
return self
......@@ -392,7 +437,7 @@ class MultiMarker(BaseMarker):
markers = _flatten_markers(markers, MultiMarker)
for marker in markers:
if marker in new_markers or marker.is_empty():
if marker in new_markers:
continue
if isinstance(marker, SingleMarker):
......@@ -408,11 +453,9 @@ class MultiMarker(BaseMarker):
intersection = mark.constraint.intersect(marker.constraint)
if intersection == mark.constraint:
intersected = True
break
elif intersection == marker.constraint:
new_markers[i] = marker
intersected = True
break
elif intersection.is_empty():
return EmptyMarker()
......@@ -421,9 +464,12 @@ class MultiMarker(BaseMarker):
new_markers.append(marker)
if not new_markers:
if any(m.is_empty() for m in new_markers) or not new_markers:
return EmptyMarker()
if len(new_markers) == 1 and new_markers[0].is_any():
return AnyMarker()
return MultiMarker(*new_markers)
@property
......@@ -443,7 +489,7 @@ class MultiMarker(BaseMarker):
def union(self, other):
if isinstance(other, (SingleMarker, MultiMarker)):
return MarkerUnion(self, other)
return MarkerUnion.of(self, other)
return other.union(self)
......@@ -455,10 +501,32 @@ class MultiMarker(BaseMarker):
return True
def without_extras(self):
return self.exclude("extra")
def exclude(self, marker_name): # type: (str) -> BaseMarker
new_markers = []
for m in self._markers:
marker = m.without_extras()
if isinstance(m, SingleMarker) and m.name == marker_name:
# The marker is not relevant since it must be excluded
continue
marker = m.exclude(marker_name)
if not marker.is_empty():
new_markers.append(marker)
return self.of(*new_markers)
def only(self, marker_name): # type: (str) -> BaseMarker
new_markers = []
for m in self._markers:
if isinstance(m, SingleMarker) and m.name != marker_name:
# The marker is not relevant since it's not one we want
continue
marker = m.only(marker_name)
if not marker.is_empty():
new_markers.append(marker)
......@@ -493,17 +561,24 @@ class MultiMarker(BaseMarker):
class MarkerUnion(BaseMarker):
def __init__(self, *markers):
self._markers = []
self._markers = list(markers)
@property
def markers(self):
return self._markers
markers = _flatten_markers(markers, MarkerUnion)
@classmethod
def of(cls, *markers): # type: (tuple) -> MarkerUnion
flattened_markers = _flatten_markers(markers, MarkerUnion)
for marker in markers:
if marker in self._markers:
markers = []
for marker in flattened_markers:
if marker in markers:
continue
if isinstance(marker, SingleMarker) and marker.name == "python_version":
intersected = False
for i, mark in enumerate(self._markers):
for i, mark in enumerate(markers):
if (
not isinstance(mark, SingleMarker)
or isinstance(mark, SingleMarker)
......@@ -516,18 +591,19 @@ class MarkerUnion(BaseMarker):
intersected = True
break
elif intersection == marker.constraint:
self._markers[i] = marker
markers[i] = marker
intersected = True
break
if intersected:
continue
self._markers.append(marker)
markers.append(marker)
@property
def markers(self):
return self._markers
if any(m.is_any() for m in markers):
return AnyMarker()
return MarkerUnion(*markers)
def append(self, marker):
if marker in self._markers:
......@@ -557,7 +633,7 @@ class MarkerUnion(BaseMarker):
if not intersection.is_empty():
new_markers.append(intersection)
return MarkerUnion(*new_markers)
return MarkerUnion.of(*new_markers)
def union(self, other):
if other.is_any():
......@@ -568,7 +644,7 @@ class MarkerUnion(BaseMarker):
new_markers = self._markers + [other]
return MarkerUnion(*new_markers)
return MarkerUnion.of(*new_markers)
def validate(self, environment):
for m in self._markers:
......@@ -578,15 +654,37 @@ class MarkerUnion(BaseMarker):
return False
def without_extras(self):
return self.exclude("extra")
def exclude(self, marker_name): # type: (str) -> BaseMarker
new_markers = []
for m in self._markers:
marker = m.without_extras()
if isinstance(m, SingleMarker) and m.name == marker_name:
# The marker is not relevant since it must be excluded
continue
marker = m.exclude(marker_name)
if not marker.is_empty():
new_markers.append(marker)
return self.of(*new_markers)
def only(self, marker_name): # type: (str) -> BaseMarker
new_markers = []
for m in self._markers:
if isinstance(m, SingleMarker) and m.name != marker_name:
# The marker is not relevant since it's not one we want
continue
marker = m.only(marker_name)
if not marker.is_empty():
new_markers.append(marker)
return MarkerUnion(*new_markers)
return self.of(*new_markers)
def __eq__(self, other):
if not isinstance(other, MarkerUnion):
......@@ -602,7 +700,15 @@ class MarkerUnion(BaseMarker):
return h
def __str__(self):
return " or ".join(str(m) for m in self._markers)
return " or ".join(
str(m) for m in self._markers if not m.is_any() and not m.is_empty()
)
def is_any(self):
return any(m.is_any() for m in self._markers)
def is_empty(self):
return all(m.is_empty() for m in self._markers)
def parse_marker(marker):
......@@ -654,4 +760,4 @@ def _compact_markers(markers):
if len(groups) == 1:
return groups[0]
return MarkerUnion(*groups)
return MarkerUnion.of(*groups)
......@@ -28,7 +28,7 @@ from .markers import parse_marker
try:
import urllib.parse as urlparse
except ImportError:
from urlparse import urlparse
import urlparse
LEGACY_REGEX = r"""
......
......@@ -35,8 +35,7 @@ class VersionSelector(object):
dependency = Dependency(package_name, constraint)
# Select highest version if we have many
package = candidates[0]
package = None
for candidate in candidates:
if (
candidate.is_prerelease()
......@@ -47,9 +46,11 @@ class VersionSelector(object):
continue
# Select highest version of the two
if package.version < candidate.version:
if package is None or package.version < candidate.version:
package = candidate
if package is None:
return False
return package
def find_recommended_require_version(self, package):
......
[tool.poetry]
name = "poetry"
version = "1.0.0b3"
version = "1.0.1"
description = "Python dependency management and packaging made easy."
authors = [
"Sébastien Eustace <sebastien@eustace.io>"
......@@ -9,9 +9,9 @@ license = "MIT"
readme = "README.md"
homepage = "https://poetry.eustace.io/"
repository = "https://github.com/sdispater/poetry"
documentation = "https://poetry.eustace.io/docs"
homepage = "https://python-poetry.org/"
repository = "https://github.com/python-poetry/poetry"
documentation = "https://python-poetry.org/docs"
keywords = ["packaging", "dependency", "poetry"]
......@@ -24,7 +24,7 @@ classifiers = [
[tool.poetry.dependencies]
python = "~2.7 || ^3.4"
cleo = "^0.7.6"
clikit = "^0.4.0"
clikit = "^0.4.1"
requests = "^2.18"
cachy = "^0.3.0"
requests-toolbelt = "^0.8.0"
......@@ -46,15 +46,16 @@ pathlib2 = { version = "^2.3", python = "~2.7 || ~3.4" }
# Use glob2 for Python 2.7 and 3.4
glob2 = { version = "^0.6", python = "~2.7 || ~3.4" }
# Use virtualenv for Python 2.7 since venv does not exist
virtualenv = { version = "^16.0", python = "~2.7" }
virtualenv = { version = "^16.7.9", python = "~2.7" }
# functools32 is needed for Python 2.7
functools32 = { version = "^3.2.3", python = "~2.7" }
keyring = [
{ version = "^18.0", python = "~2.7 || ~3.4" },
{ version = "^19.0", python = "^3.5" }
{ version = "^18.0.1", python = "~2.7 || ~3.4" },
{ version = "^20.0.1", python = "^3.5" }
]
# Use subprocess32 for Python 2.7 and 3.4
subprocess32 = { version = "^3.5", python = "~2.7 || ~3.4" }
importlib-metadata = {version = "~1.1.3", python = "<3.8"}
[tool.poetry.dev-dependencies]
pytest = "^4.1"
......@@ -69,11 +70,20 @@ pre-commit = "^1.10"
tox = "^3.0"
pytest-sugar = "^0.9.2"
httpretty = "^0.9.6"
markdown-include = "^0.5.1"
[tool.poetry.scripts]
poetry = "poetry.console:main"
[build-system]
requires = ["intreehooks"]
build-backend = "intreehooks:loader"
[tool.intreehooks]
build-backend = "poetry.masonry.api"
[tool.isort]
line_length = 88
force_single_line = true
......
......@@ -219,6 +219,9 @@ class MakeReleaseCommand(Command):
subprocess.check_output(
[python, "-V"], stderr=subprocess.STDOUT, shell=WINDOWS
)
if version == "3.4" and WINDOWS:
continue
subprocess.check_output([python, "-m", "pip", "install", "pip", "-U"])
except subprocess.CalledProcessError:
raise RuntimeError("Python {} is not available".format(version))
......
......@@ -10,15 +10,9 @@ import pytest
from poetry.config.config import Config as BaseConfig
from poetry.config.dict_config_source import DictConfigSource
from poetry.utils._compat import PY2
from poetry.utils._compat import WINDOWS
from poetry.utils._compat import Path
try:
import urllib.parse as urlparse
except ImportError:
import urlparse
from tests.helpers import mock_clone
from tests.helpers import mock_download
class Config(BaseConfig):
......@@ -58,6 +52,11 @@ def auth_config_source():
@pytest.fixture
def config(config_source, auth_config_source, mocker):
import keyring
from keyring.backends.fail import Keyring
keyring.set_keyring(Keyring())
c = Config()
c.merge(config_source.config)
c.set_config_source(config_source)
......@@ -69,47 +68,6 @@ def config(config_source, auth_config_source, mocker):
return c
def mock_clone(_, source, dest):
# Checking source to determine which folder we need to copy
parts = urlparse.urlparse(source)
folder = (
Path(__file__).parent
/ "fixtures"
/ "git"
/ parts.netloc
/ parts.path.lstrip("/").rstrip(".git")
)
if dest.exists():
shutil.rmtree(str(dest))
shutil.copytree(str(folder), str(dest))
def mock_download(self, url, dest):
parts = urlparse.urlparse(url)
fixtures = Path(__file__).parent / "fixtures"
fixture = fixtures / parts.path.lstrip("/")
if dest.exists():
os.unlink(str(dest))
# Python2 does not support os.symlink on Windows whereas Python3 does. os.symlink requires either administrative
# privileges or developer mode on Win10, throwing an OSError is neither is active.
if WINDOWS:
if PY2:
shutil.copyfile(str(fixture), str(dest))
else:
try:
os.symlink(str(fixture), str(dest))
except OSError:
shutil.copyfile(str(fixture), str(dest))
else:
os.symlink(str(fixture), str(dest))
@pytest.fixture(autouse=True)
def download_mock(mocker):
# Patch download to not download anything but to just copy from fixtures
......
......@@ -11,7 +11,7 @@ def test_about(app):
Poetry - Package Management for Python
Poetry is a dependency manager tracking local dependencies of your projects and libraries.
See https://github.com/sdispater/poetry for more information.
See https://github.com/python-poetry/poetry for more information.
"""
assert expected == tester.io.fetch_output()
......@@ -109,7 +109,7 @@ def test_add_constraint_with_extras(app, repo, installer):
repo.add_package(cachy1)
repo.add_package(get_package("msgpack-python", "0.5.3"))
tester.execute("cachy[msgpack]^0.1.0")
tester.execute("cachy[msgpack]>=0.1.0,<0.2.0")
expected = """\
......@@ -264,6 +264,42 @@ Package operations: 4 installs, 0 updates, 0 removals
}
def test_add_git_ssh_constraint(app, repo, installer):
command = app.find("add")
tester = CommandTester(command)
repo.add_package(get_package("pendulum", "1.4.4"))
repo.add_package(get_package("cleo", "0.6.5"))
tester.execute("git+ssh://git@github.com/demo/demo.git@develop")
expected = """\
Updating dependencies
Resolving dependencies...
Writing lock file
Package operations: 2 installs, 0 updates, 0 removals
- Installing pendulum (1.4.4)
- Installing demo (0.1.2 9cf87a2)
"""
assert expected == tester.io.fetch_output()
assert len(installer.installs) == 2
content = app.poetry.file.read()["tool"]["poetry"]
assert "demo" in content["dependencies"]
assert content["dependencies"]["demo"] == {
"git": "ssh://git@github.com/demo/demo.git",
"rev": "develop",
}
def test_add_directory_constraint(app, repo, installer, mocker):
p = mocker.patch("poetry.utils._compat.Path.cwd")
p.return_value = Path(__file__) / ".."
......@@ -456,7 +492,7 @@ def test_add_url_constraint_wheel(app, repo, installer, mocker):
repo.add_package(get_package("pendulum", "1.4.4"))
tester.execute(
"https://poetry.eustace.io/distributions/demo-0.1.0-py2.py3-none-any.whl"
"https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl"
)
expected = """\
......@@ -470,7 +506,7 @@ Writing lock file
Package operations: 2 installs, 0 updates, 0 removals
- Installing pendulum (1.4.4)
- Installing demo (0.1.0 https://poetry.eustace.io/distributions/demo-0.1.0-py2.py3-none-any.whl)
- Installing demo (0.1.0 https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl)
"""
assert expected == tester.io.fetch_output()
......@@ -481,7 +517,7 @@ Package operations: 2 installs, 0 updates, 0 removals
assert "demo" in content["dependencies"]
assert content["dependencies"]["demo"] == {
"url": "https://poetry.eustace.io/distributions/demo-0.1.0-py2.py3-none-any.whl"
"url": "https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl"
}
......@@ -494,7 +530,7 @@ def test_add_url_constraint_wheel_with_extras(app, repo, installer, mocker):
repo.add_package(get_package("tomlkit", "0.5.5"))
tester.execute(
"https://poetry.eustace.io/distributions/demo-0.1.0-py2.py3-none-any.whl[foo,bar]"
"https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl[foo,bar]"
)
expected = """\
......@@ -510,7 +546,7 @@ Package operations: 4 installs, 0 updates, 0 removals
- Installing cleo (0.6.5)
- Installing pendulum (1.4.4)
- Installing tomlkit (0.5.5)
- Installing demo (0.1.0 https://poetry.eustace.io/distributions/demo-0.1.0-py2.py3-none-any.whl)
- Installing demo (0.1.0 https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl)
"""
assert expected == tester.io.fetch_output()
......@@ -521,7 +557,7 @@ Package operations: 4 installs, 0 updates, 0 removals
assert "demo" in content["dependencies"]
assert content["dependencies"]["demo"] == {
"url": "https://poetry.eustace.io/distributions/demo-0.1.0-py2.py3-none-any.whl",
"url": "https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl",
"extras": ["foo", "bar"],
}
......
......@@ -36,14 +36,14 @@ def test_check_invalid(app, mocker):
Error: u'description' is a required property
Error: INVALID is not a valid license
Warning: A wildcard Python dependency is ambiguous. Consider specifying a more explicit one.
Warning: The "pendulum" dependency specifies the "allows-prereleases" property, which is deprecated. Use "allow-preleases" instead.
Warning: The "pendulum" dependency specifies the "allows-prereleases" property, which is deprecated. Use "allow-prereleases" instead.
"""
else:
expected = """\
Error: 'description' is a required property
Error: INVALID is not a valid license
Warning: A wildcard Python dependency is ambiguous. Consider specifying a more explicit one.
Warning: The "pendulum" dependency specifies the "allows-prereleases" property, which is deprecated. Use "allow-preleases" instead.
Warning: The "pendulum" dependency specifies the "allows-prereleases" property, which is deprecated. Use "allow-prereleases" instead.
"""
assert expected == tester.io.fetch_output()
......@@ -26,9 +26,9 @@ license = "MIT"
readme = "README.rst"
homepage = "https://poetry.eustace.io"
repository = "https://github.com/sdispater/poetry"
documentation = "https://poetry.eustace.io/docs"
homepage = "https://python-poetry.org"
repository = "https://github.com/python-poetry/poetry"
documentation = "https://python-poetry.org/docs"
keywords = ["packaging", "dependency", "poetry"]
......
......@@ -379,6 +379,178 @@ cachy 0.1.0 0.2.0 Cachy package
assert expected == tester.io.fetch_output()
def test_show_outdated_with_only_up_to_date_packages(app, poetry, installed, repo):
command = app.find("show")
tester = CommandTester(command)
cachy_020 = get_package("cachy", "0.2.0")
cachy_020.description = "Cachy package"
installed.add_package(cachy_020)
repo.add_package(cachy_020)
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "cachy",
"version": "0.2.0",
"description": "Cachy package",
"category": "main",
"optional": False,
"platform": "*",
"python-versions": "*",
"checksum": [],
},
],
"metadata": {
"python-versions": "*",
"platform": "*",
"content-hash": "123456789",
"hashes": {"cachy": []},
},
}
)
tester.execute("--outdated")
expected = ""
assert expected == tester.io.fetch_output()
def test_show_outdated_has_prerelease_but_not_allowed(app, poetry, installed, repo):
command = app.find("show")
tester = CommandTester(command)
cachy_010 = get_package("cachy", "0.1.0")
cachy_010.description = "Cachy package"
cachy_020 = get_package("cachy", "0.2.0")
cachy_020.description = "Cachy package"
cachy_030dev = get_package("cachy", "0.3.0.dev123")
cachy_030dev.description = "Cachy package"
pendulum_200 = get_package("pendulum", "2.0.0")
pendulum_200.description = "Pendulum package"
installed.add_package(cachy_010)
installed.add_package(pendulum_200)
# sorting isn't used, so this has to be the first element to
# replicate the issue in PR #1548
repo.add_package(cachy_030dev)
repo.add_package(cachy_010)
repo.add_package(cachy_020)
repo.add_package(pendulum_200)
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "cachy",
"version": "0.1.0",
"description": "Cachy package",
"category": "main",
"optional": False,
"platform": "*",
"python-versions": "*",
"checksum": [],
},
{
"name": "pendulum",
"version": "2.0.0",
"description": "Pendulum package",
"category": "main",
"optional": False,
"platform": "*",
"python-versions": "*",
"checksum": [],
},
],
"metadata": {
"python-versions": "*",
"platform": "*",
"content-hash": "123456789",
"hashes": {"cachy": [], "pendulum": []},
},
}
)
tester.execute("--outdated")
expected = """\
cachy 0.1.0 0.2.0 Cachy package
"""
assert expected == tester.io.fetch_output()
def test_show_outdated_has_prerelease_and_allowed(app, poetry, installed, repo):
command = app.find("show")
tester = CommandTester(command)
cachy_010dev = get_package("cachy", "0.1.0.dev1")
cachy_010dev.description = "Cachy package"
cachy_020 = get_package("cachy", "0.2.0")
cachy_020.description = "Cachy package"
cachy_030dev = get_package("cachy", "0.3.0.dev123")
cachy_030dev.description = "Cachy package"
pendulum_200 = get_package("pendulum", "2.0.0")
pendulum_200.description = "Pendulum package"
installed.add_package(cachy_010dev)
installed.add_package(pendulum_200)
# sorting isn't used, so this has to be the first element to
# replicate the issue in PR #1548
repo.add_package(cachy_030dev)
repo.add_package(cachy_010dev)
repo.add_package(cachy_020)
repo.add_package(pendulum_200)
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "cachy",
"version": "0.1.0.dev1",
"description": "Cachy package",
"category": "main",
"optional": False,
"platform": "*",
"python-versions": "*",
"checksum": [],
},
{
"name": "pendulum",
"version": "2.0.0",
"description": "Pendulum package",
"category": "main",
"optional": False,
"platform": "*",
"python-versions": "*",
"checksum": [],
},
],
"metadata": {
"python-versions": "*",
"platform": "*",
"content-hash": "123456789",
"hashes": {"cachy": [], "pendulum": []},
},
}
)
tester.execute("--outdated")
expected = """\
cachy 0.1.0.dev1 0.3.0.dev123 Cachy package
"""
assert expected == tester.io.fetch_output()
def test_show_outdated_formatting(app, poetry, installed, repo):
command = app.find("show")
tester = CommandTester(command)
......
......@@ -44,4 +44,4 @@ def test_version_show(app):
command = app.find("version")
tester = CommandTester(command)
tester.execute()
assert "Project (simple-project) version is 1.2.3\n" == tester.io.fetch_output()
assert "simple-project 1.2.3\n" == tester.io.fetch_output()
import os
import shutil
import pytest
......@@ -13,16 +12,10 @@ from poetry.poetry import Poetry as BasePoetry
from poetry.repositories import Pool
from poetry.repositories import Repository as BaseRepository
from poetry.repositories.exceptions import PackageNotFound
from poetry.utils._compat import PY2
from poetry.utils._compat import WINDOWS
from poetry.utils._compat import Path
from poetry.utils.toml_file import TomlFile
try:
import urllib.parse as urlparse
except ImportError:
import urlparse
from tests.helpers import mock_clone
from tests.helpers import mock_download
@pytest.fixture()
......@@ -30,45 +23,6 @@ def installer():
return NoopInstaller()
def mock_clone(self, source, dest):
# Checking source to determine which folder we need to copy
parts = urlparse.urlparse(source)
folder = (
Path(__file__).parent.parent
/ "fixtures"
/ "git"
/ parts.netloc
/ parts.path.lstrip("/").rstrip(".git")
)
shutil.rmtree(str(dest))
shutil.copytree(str(folder), str(dest))
def mock_download(self, url, dest):
parts = urlparse.urlparse(url)
fixtures = Path(__file__).parent.parent / "fixtures"
fixture = fixtures / parts.path.lstrip("/")
if dest.exists():
shutil.rmtree(str(dest))
# Python2 does not support os.symlink on Windows whereas Python3 does. os.symlink requires either administrative
# privileges or developer mode on Win10, throwing an OSError is neither is active.
if WINDOWS:
if PY2:
shutil.copyfile(str(fixture), str(dest))
else:
try:
os.symlink(str(fixture), str(dest))
except OSError:
shutil.copyfile(str(fixture), str(dest))
else:
os.symlink(str(fixture), str(dest))
@pytest.fixture
def installed():
return BaseRepository()
......
......@@ -9,9 +9,9 @@ license = "MIT"
readme = "README.rst"
homepage = "https://poetry.eustace.io/"
repository = "https://github.com/sdispater/poetry"
documentation = "https://poetry.eustace.io/docs"
homepage = "https://python-poetry.org/"
repository = "https://github.com/python-poetry/poetry"
documentation = "https://python-poetry.org/docs"
keywords = ["packaging", "dependency", "poetry"]
......
......@@ -7,9 +7,9 @@ authors = [
]
license = "MIT"
homepage = "https://poetry.eustace.io"
repository = "https://github.com/sdispater/poetry"
documentation = "https://poetry.eustace.io/docs"
homepage = "https://python-poetry.org"
repository = "https://github.com/python-poetry/poetry"
documentation = "https://python-poetry.org/docs"
keywords = ["packaging", "dependency", "poetry"]
......
......@@ -9,9 +9,9 @@ license = "MIT"
readme = "README.rst"
homepage = "https://poetry.eustace.io"
repository = "https://github.com/sdispater/poetry"
documentation = "https://poetry.eustace.io/docs"
homepage = "https://python-poetry.org"
repository = "https://github.com/python-poetry/poetry"
documentation = "https://python-poetry.org/docs"
keywords = ["packaging", "dependency", "poetry"]
......
......@@ -9,9 +9,9 @@ license = "MIT"
readme = "README.rst"
homepage = "https://poetry.eustace.io"
repository = "https://github.com/sdispater/poetry"
documentation = "https://poetry.eustace.io/docs"
homepage = "https://python-poetry.org"
repository = "https://github.com/python-poetry/poetry"
documentation = "https://python-poetry.org/docs"
keywords = ["packaging", "dependency", "poetry"]
......
......@@ -9,9 +9,9 @@ license = "MIT"
readme = "README.rst"
homepage = "https://poetry.eustace.io"
repository = "https://github.com/sdispater/poetry"
documentation = "https://poetry.eustace.io/docs"
homepage = "https://python-poetry.org"
repository = "https://github.com/python-poetry/poetry"
documentation = "https://python-poetry.org/docs"
keywords = ["packaging", "dependency", "poetry"]
......
......@@ -9,9 +9,9 @@ license = "MIT"
readme = "README.rst"
homepage = "https://poetry.eustace.io"
repository = "https://github.com/sdispater/poetry"
documentation = "https://poetry.eustace.io/docs"
homepage = "https://python-poetry.org"
repository = "https://github.com/python-poetry/poetry"
documentation = "https://python-poetry.org/docs"
keywords = ["packaging", "dependency", "poetry"]
......
[virtualenvs]
in-project = false
create = false
......@@ -9,9 +9,9 @@ license = "MIT"
readme = "README.rst"
homepage = "https://poetry.eustace.io"
repository = "https://github.com/sdispater/poetry"
documentation = "https://poetry.eustace.io/docs"
homepage = "https://python-poetry.org"
repository = "https://github.com/python-poetry/poetry"
documentation = "https://python-poetry.org/docs"
keywords = ["packaging", "dependency", "poetry"]
......
......@@ -9,9 +9,9 @@ license = "MIT"
readme = "README.rst"
homepage = "https://poetry.eustace.io"
repository = "https://github.com/sdispater/poetry"
documentation = "https://poetry.eustace.io/docs"
homepage = "https://python-poetry.org"
repository = "https://github.com/python-poetry/poetry"
documentation = "https://python-poetry.org/docs"
keywords = ["packaging", "dependency", "poetry"]
......
import os
import shutil
from poetry.packages import Dependency
from poetry.packages import Package
from poetry.utils._compat import PY2
from poetry.utils._compat import WINDOWS
from poetry.utils._compat import Path
from poetry.utils._compat import urlparse
from poetry.vcs.git import ParsedUrl
FIXTURE_PATH = Path(__file__).parent / "fixtures"
......@@ -27,3 +34,57 @@ def fixture(path=None):
return FIXTURE_PATH / path
else:
return FIXTURE_PATH
def copy_or_symlink(source, dest):
if dest.exists():
if dest.is_symlink():
os.unlink(str(dest))
elif dest.is_dir():
shutil.rmtree(str(dest))
else:
os.unlink(str(dest))
# Python2 does not support os.symlink on Windows whereas Python3 does.
# os.symlink requires either administrative privileges or developer mode on Win10,
# throwing an OSError if neither is active.
if WINDOWS:
if PY2:
if source.is_dir():
shutil.copytree(str(source), str(dest))
else:
shutil.copyfile(str(source), str(dest))
else:
try:
os.symlink(str(source), str(dest), target_is_directory=source.is_dir())
except OSError:
if source.is_dir():
shutil.copytree(str(source), str(dest))
else:
shutil.copyfile(str(source), str(dest))
else:
os.symlink(str(source), str(dest))
def mock_clone(_, source, dest):
# Checking source to determine which folder we need to copy
parsed = ParsedUrl.parse(source)
folder = (
Path(__file__).parent
/ "fixtures"
/ "git"
/ parsed.resource
/ parsed.pathname.lstrip("/").rstrip(".git")
)
copy_or_symlink(folder, dest)
def mock_download(self, url, dest):
parts = urlparse.urlparse(url)
fixtures = Path(__file__).parent / "fixtures"
fixture = fixtures / parts.path.lstrip("/")
copy_or_symlink(fixture, dest)
[[package]]
category = "main"
description = ""
develop = true
name = "project-with-extras"
optional = false
python-versions = "*"
......@@ -18,6 +19,7 @@ url = "tests/fixtures/directory/project_with_transitive_directory_dependencies/.
[[package]]
category = "main"
description = ""
develop = true
name = "project-with-transitive-directory-dependencies"
optional = false
python-versions = "*"
......
......@@ -9,6 +9,7 @@ version = "1.4.4"
[[package]]
category = "main"
description = ""
develop = true
name = "project-with-extras"
optional = false
python-versions = "*"
......
......@@ -9,6 +9,7 @@ python-versions = "*"
[[package]]
name = "my-package"
version = "0.1.2"
develop = true
description = "Demo project."
category = "main"
optional = false
......
......@@ -23,6 +23,7 @@ C = "1.5"
[[package]]
name = "C"
version = "1.5"
marker = "python_version >= \"2.7\""
description = ""
category = "main"
optional = false
......
......@@ -29,6 +29,7 @@ version = "1.4.4"
[[package]]
category = "main"
description = ""
develop = true
name = "project-with-transitive-file-dependencies"
optional = false
python-versions = "*"
......
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