diff --git a/.cirrus.yml b/.cirrus.yml index e2341cdf4b7..c8ccc47cf40 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,5 +1,5 @@ freebsd_instance: - image_family: freebsd-12-1-snap + image_family: freebsd-12-2 test_task: name: "Tests / FreeBSD / " @@ -7,8 +7,8 @@ test_task: skip: "!changesInclude('.cirrus.yml', 'poetry.lock', 'pyproject.toml', '**.json','**.py')" env: matrix: - - PYTHON: python2.7 - - PYTHON: python3.7 + - PYTHON: python3.6 + - PYTHON: python3.8 python_script: - PYPACKAGE=$(printf '%s' $PYTHON | tr -d '.') - SQLPACKAGE=$(printf '%s-sqlite3' $PYPACKAGE | sed 's/thon//') @@ -24,56 +24,3 @@ test_task: path: junit.xml format: junit type: text/xml - -release_task: - name: "Release / FreeBSD" - only_if: $CIRRUS_TAG != '' - env: - GITHUB_TOKEN: ENCRYPTED[2b573a2d28a03523ac6fb5b3c2f513a41c0a98db81e40e50e1d103b171f85c57e58ae38d957499dbf7fd7635cfcfd7be] - PYTHON: python3.8 - PYTHON27: python2.7 - PYTHON36: python3.6 - PYTHON37: python3.7 - PYTHON38: python3.8 - freebsd_instance: - matrix: - - image_family: freebsd-12-1-snap - - image_family: freebsd-13-0-snap - - image_family: freebsd-11-4-snap - python_script: pkg install -y curl bash jq python3 python27 python36 python37 python38 - pip_script: - - python2.7 -m ensurepip - - python3.6 -m ensurepip - - python3.7 -m ensurepip - - python3.8 -m ensurepip - build_script: bash ./make-nix-release.sh - upload_script: | - #!/usr/bin/env bash - - if [[ "$CIRRUS_RELEASE" == "" ]]; then - CIRRUS_RELEASE=$(curl -sL https://api.github.com/repos/$CIRRUS_REPO_FULL_NAME/releases/tags/$CIRRUS_TAG | jq -r '.id') - if [[ "$CIRRUS_RELEASE" == "null" ]]; then - echo "Failed to find a release associated with this tag!" - exit 0 - fi - fi - - if [[ "$GITHUB_TOKEN" == "" ]]; then - echo "Please provide GitHub access token via GITHUB_TOKEN environment variable!" - exit 1 - fi - - for fpath in releases/* - do - echo "Uploading $fpath..." - name=$(basename "$fpath") - url_to_upload="https://uploads.github.com/repos/$CIRRUS_REPO_FULL_NAME/releases/$CIRRUS_RELEASE/assets?name=$name" - echo "Uploading to $url_to_upload" - curl -X POST \ - --data-binary @$fpath \ - --header "Authorization: token $GITHUB_TOKEN" \ - --header "Content-Type: application/octet-stream" \ - $url_to_upload - done - archive_artifacts: - path: "releases/*" diff --git a/.flake8 b/.flake8 index 130c44c6752..6005a30d32d 100644 --- a/.flake8 +++ b/.flake8 @@ -1,7 +1,9 @@ [flake8] max-line-length = 88 ignore = E501, E203, W503 -per-file-ignores = __init__.py:F401 +per-file-ignores = + __init__.py:F401 + tests/console/commands/debug/test_resolve.py:W291 exclude = .git __pycache__ diff --git a/.github/workflows/code-quality.yaml b/.github/workflows/code-quality.yaml index 4c1e4d566ed..86f4939283a 100644 --- a/.github/workflows/code-quality.yaml +++ b/.github/workflows/code-quality.yaml @@ -16,6 +16,6 @@ jobs: name: Linting runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - uses: actions/setup-python@v1 + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 - uses: pre-commit/action@v2.0.0 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index b14884c8fa0..00000000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Documentation - -on: - push: - paths: - - 'docs/**' - - '.github/workflows/docs.yml' - branches: - - master - pull_request: - paths: - - 'docs/**' - - '.github/workflows/docs.yml' - branches: - - '**' - -jobs: - docs: - name: Documentation Build - runs-on: Ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - name: Install tox - run: pip install tox - - name: Build documentation - run: tox -e doc diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 119916ce3d6..608fe4292df 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,15 +19,24 @@ jobs: tests: name: ${{ matrix.os }} / ${{ matrix.python-version }} runs-on: ${{ matrix.os }}-latest + continue-on-error: ${{ matrix.experimental }} strategy: matrix: os: [Ubuntu, MacOS, Windows] - python-version: [2.7, 3.5, 3.6, 3.7, 3.8] + python-version: [3.6, 3.7, 3.8, 3.9] + experimental: [false] + bootstrap-args: [""] + include: + - os: Ubuntu + python-version: "3.10.0-alpha - 3.10.0" + experimental: true + bootstrap-args: "--git https://github.com/python-poetry/poetry.git" + fail-fast: false steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} @@ -39,13 +48,22 @@ jobs: - name: Bootstrap poetry shell: bash run: | - python -m ensurepip - python -m pip install --upgrade pip - python -m pip install . + curl -sL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py \ + | python - -y ${{ matrix.bootstrap-args }} + + - name: Update PATH + if: ${{ matrix.os != 'Windows' }} + shell: bash + run: echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Update Path for Windows + if: ${{ matrix.os == 'Windows' }} + shell: bash + run: echo "$APPDATA\Python\Scripts" >> $GITHUB_PATH - name: Configure poetry shell: bash - run: python -m poetry config virtualenvs.in-project true + run: poetry config virtualenvs.in-project true - name: Set up cache uses: actions/cache@v2 @@ -57,12 +75,12 @@ jobs: - name: Ensure cache is healthy if: steps.cache.outputs.cache-hit == 'true' shell: bash - run: timeout 10s python -m poetry run pip --version || rm -rf .venv + run: timeout 10s poetry run pip --version || rm -rf .venv - name: Install dependencies shell: bash - run: python -m poetry install + run: poetry install - name: Run pytest shell: bash - run: python -m poetry run python -m pytest -v tests + run: poetry run python -m pytest -p no:sugar -q tests/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 43e7d357cd1..d71b0165e3f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,261 +6,46 @@ on: - '*.*.*' jobs: - - Linux: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Get tag - id: tag - run: | - echo ::set-output name=tag::${GITHUB_REF#refs/tags/} - - name: Building release - run: | - make linux_release - - name: Upload release file - uses: actions/upload-artifact@v1 - with: - name: poetry-${{ steps.tag.outputs.tag }}-linux.tar.gz - path: releases/poetry-${{ steps.tag.outputs.tag }}-linux.tar.gz - - name: Upload checksum file - uses: actions/upload-artifact@v1 - with: - name: poetry-${{ steps.tag.outputs.tag }}-linux.sha256sum - path: releases/poetry-${{ steps.tag.outputs.tag }}-linux.sha256sum - - MacOS: - runs-on: macos-latest - - steps: - - uses: actions/checkout@v2 - - name: Get tag - id: tag - run: | - echo ::set-output name=tag::${GITHUB_REF#refs/tags/} - - name: Set up Python 3.8 - uses: actions/setup-python@v2 - with: - python-version: "3.8" - - name: Install Poetry - run: | - python get-poetry.py --preview -y - source $HOME/.poetry/env - - name: Install dependencies - run: | - source $HOME/.poetry/env - poetry install --no-dev - - name: Preparing Python executables - run: | - curl -L https://github.com/sdispater/python-binaries/releases/download/2.7.17/python-2.7.17.macos.tar.xz -o python-2.7.17.tar.xz - curl -L https://github.com/sdispater/python-binaries/releases/download/3.5.9/python-3.5.9.macos.tar.xz -o python-3.5.9.tar.xz - curl -L https://github.com/sdispater/python-binaries/releases/download/3.6.8/python-3.6.8.macos.tar.xz -o python-3.6.8.tar.xz - curl -L https://github.com/sdispater/python-binaries/releases/download/3.7.6/python-3.7.6.macos.tar.xz -o python-3.7.6.tar.xz - curl -L https://github.com/sdispater/python-binaries/releases/download/3.8.3/python-3.8.3.macos.tar.xz -o python-3.8.3.tar.xz - curl -L https://github.com/sdispater/python-binaries/releases/download/3.9.0b4/python-3.9.0b4.macos.tar.xz -o python-3.9.0b4.tar.xz - tar -zxf python-2.7.17.tar.xz - tar -zxf python-3.5.9.tar.xz - tar -zxf python-3.6.8.tar.xz - tar -zxf python-3.7.6.tar.xz - tar -zxf python-3.8.3.tar.xz - tar -zxf python-3.9.0b4.tar.xz - - name: Build specific release - run: | - source $HOME/.poetry/env - poetry run python sonnet make release --ansi -P "2.7:python-2.7.17/bin/python" -P "3.5:python-3.5.9/bin/python" -P "3.6:python-3.6.8/bin/python" -P "3.7:python-3.7.6/bin/python" -P "3.8:python-3.8.3/bin/python" -P "3.9:python-3.9.0b4/bin/python" - - name: Upload release file - uses: actions/upload-artifact@v1 - with: - name: poetry-${{ steps.tag.outputs.tag }}-darwin.tar.gz - path: releases/poetry-${{ steps.tag.outputs.tag }}-darwin.tar.gz - - name: Upload checksum file - uses: actions/upload-artifact@v1 - with: - name: poetry-${{ steps.tag.outputs.tag }}-darwin.sha256sum - path: releases/poetry-${{ steps.tag.outputs.tag }}-darwin.sha256sum - - Windows: - runs-on: windows-latest - - steps: - - uses: actions/checkout@v2 - - name: Get tag - id: tag - shell: bash - run: | - echo ::set-output name=tag::${GITHUB_REF#refs/tags/} - - name: Set up Python 3.8 - uses: actions/setup-python@v2 - with: - python-version: "3.8" - - name: Install Poetry - run: | - python get-poetry.py --preview -y - $env:Path += ";$env:Userprofile\.poetry\bin" - - name: Install dependencies - run: | - $env:Path += ";$env:Userprofile\.poetry\bin" - poetry install --no-dev - - name: Preparing Python executables - run: | - Invoke-WebRequest https://github.com/sdispater/python-binaries/releases/download/2.7.17/python-2.7.17.windows.tar.xz -O python-2.7.17.tar.xz - Invoke-WebRequest https://github.com/sdispater/python-binaries/releases/download/3.5.4/python-3.5.4.windows.tar.xz -O python-3.5.4.tar.xz - Invoke-WebRequest https://github.com/sdispater/python-binaries/releases/download/3.6.8/python-3.6.8.windows.tar.xz -O python-3.6.8.tar.xz - Invoke-WebRequest https://github.com/sdispater/python-binaries/releases/download/3.7.6/python-3.7.6.windows.tar.xz -O python-3.7.6.tar.xz - Invoke-WebRequest https://github.com/sdispater/python-binaries/releases/download/3.8.3/python-3.8.3.windows.tar.xz -O python-3.8.3.tar.xz - Invoke-WebRequest https://github.com/sdispater/python-binaries/releases/download/3.9.0b4/python-3.9.0b4.windows.tar.xz -O python-3.9.0b4.tar.xz - 7z x python-2.7.17.tar.xz - 7z x python-3.5.4.tar.xz - 7z x python-3.6.8.tar.xz - 7z x python-3.7.6.tar.xz - 7z x python-3.8.3.tar.xz - 7z x python-3.9.0b4.tar.xz - 7z x python-2.7.17.tar - 7z x python-3.4.4.tar - 7z x python-3.5.4.tar - 7z x python-3.6.8.tar - 7z x python-3.7.6.tar - 7z x python-3.8.3.tar - 7z x python-3.9.0b4.tar - - name: Build specific release - run: | - $env:Path += ";$env:Userprofile\.poetry\bin" - poetry run python sonnet make release --ansi -P "2.7:python-2.7.17\python.exe" -P "3.5:python-3.5.4\python.exe" -P "3.6:python-3.6.8\python.exe" -P "3.7:python-3.7.6\python.exe" -P "3.8:python-3.8.3\python.exe" -P "3.9:python-3.9.0b4\python.exe" - - name: Upload release file - uses: actions/upload-artifact@v1 - with: - name: poetry-${{ steps.tag.outputs.tag }}-win32.tar.gz - path: releases/poetry-${{ steps.tag.outputs.tag }}-win32.tar.gz - - name: Upload checksum file - uses: actions/upload-artifact@v1 - with: - name: poetry-${{ steps.tag.outputs.tag }}-win32.sha256sum - path: releases/poetry-${{ steps.tag.outputs.tag }}-win32.sha256sum - Release: - needs: [Linux, MacOS, Windows] runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 + - name: Get tag id: tag - run: | - echo ::set-output name=tag::${GITHUB_REF#refs/tags/} - - name: Download Linux release file - uses: actions/download-artifact@master - with: - name: poetry-${{ steps.tag.outputs.tag }}-linux.tar.gz - path: releases/ - - name: Download Linux checksum file - uses: actions/download-artifact@master - with: - name: poetry-${{ steps.tag.outputs.tag }}-linux.sha256sum - path: releases/ - - name: Download MacOS release file - uses: actions/download-artifact@master - with: - name: poetry-${{ steps.tag.outputs.tag }}-darwin.tar.gz - path: releases/ - - name: Download MacOS checksum file - uses: actions/download-artifact@master - with: - name: poetry-${{ steps.tag.outputs.tag }}-darwin.sha256sum - path: releases/ - - name: Download Windows release file - uses: actions/download-artifact@master - with: - name: poetry-${{ steps.tag.outputs.tag }}-win32.tar.gz - path: releases/ - - name: Download Windows checksum file - uses: actions/download-artifact@master - with: - name: poetry-${{ steps.tag.outputs.tag }}-win32.sha256sum - path: releases/ - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ steps.tag.outputs.tag }} - release_name: ${{ steps.tag.outputs.tag }} - draft: false - prerelease: false - - name: Upload Linux release file asset - uses: actions/upload-release-asset@v1.0.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: releases/poetry-${{ steps.tag.outputs.tag }}-linux.tar.gz - asset_name: poetry-${{ steps.tag.outputs.tag }}-linux.tar.gz - asset_content_type: application/gzip - - name: Upload Linux checksum file asset - uses: actions/upload-release-asset@v1.0.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: releases/poetry-${{ steps.tag.outputs.tag }}-linux.sha256sum - asset_name: poetry-${{ steps.tag.outputs.tag }}-linux.sha256sum - asset_content_type: text/plain - - name: Upload MacOS release file asset - uses: actions/upload-release-asset@v1.0.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: releases/poetry-${{ steps.tag.outputs.tag }}-darwin.tar.gz - asset_name: poetry-${{ steps.tag.outputs.tag }}-darwin.tar.gz - asset_content_type: application/gzip - - name: Upload MacOS checksum file asset - uses: actions/upload-release-asset@v1.0.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: releases/poetry-${{ steps.tag.outputs.tag }}-darwin.sha256sum - asset_name: poetry-${{ steps.tag.outputs.tag }}-darwin.sha256sum - asset_content_type: text/plain - - name: Upload Windows release file asset - uses: actions/upload-release-asset@v1.0.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: releases/poetry-${{ steps.tag.outputs.tag }}-win32.tar.gz - asset_name: poetry-${{ steps.tag.outputs.tag }}-win32.tar.gz - asset_content_type: application/gzip - - name: Upload Windows checksum file asset - uses: actions/upload-release-asset@v1.0.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: releases/poetry-${{ steps.tag.outputs.tag }}-win32.sha256sum - asset_name: poetry-${{ steps.tag.outputs.tag }}-win32.sha256sum - asset_content_type: text/plain - - name: Set up Python 3.8 + run: echo ::set-output name=tag::${GITHUB_REF#refs/tags/} + + - name: Set up Python 3.9 uses: actions/setup-python@v2 with: - python-version: "3.8" + python-version: "3.9" + - name: Install Poetry - run: | - python get-poetry.py --preview -y - - name: Install dependencies - run: | - source $HOME/.poetry/env - poetry install --no-dev + run: python install-poetry.py -y + + - name: Update PATH + run: echo "$HOME/.local/bin" >> $GITHUB_PATH + - name: Build project for distribution + run: poetry build + + - name: Check Version + id: check-version run: | - source $HOME/.poetry/env - poetry run poetry build + [[ "$(poetry version --short)" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \ + || echo ::set-output name=prerelease::true + + - name: Create Release + uses: ncipollo/release-action@v1 + with: + artifacts: "dist/*" + token: ${{ secrets.GITHUB_TOKEN }} + draft: false + prerelease: steps.check-version.outputs.prerelease == 'true' + - name: Publish to PyPI env: POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} - run: | - source $HOME/.poetry/env - poetry run poetry publish + run: poetry publish diff --git a/.github/workflows/skip.yml b/.github/workflows/skip.yml index a9b384e3500..752ed7fa911 100644 --- a/.github/workflows/skip.yml +++ b/.github/workflows/skip.yml @@ -32,6 +32,6 @@ jobs: strategy: matrix: os: [Ubuntu, MacOS, Windows] - python-version: [2.7, 3.5, 3.6, 3.7, 3.8] + python-version: [3.6, 3.7, 3.8, 3.9] steps: - run: exit 0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7766f56e108..9377e9a6992 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,26 +1,30 @@ repos: - repo: https://github.com/psf/black - rev: 19.10b0 + rev: 20.8b1 hooks: - id: black - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.3 + rev: 3.8.4 hooks: - id: flake8 - repo: https://github.com/timothycrosley/isort - rev: 5.4.2 + rev: 5.7.0 hooks: - id: isort additional_dependencies: [toml] exclude: ^.*/?setup\.py$ - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 + rev: v3.4.0 hooks: - id: trailing-whitespace - exclude: ^tests/.*/fixtures/.* + exclude: | + (?x)( + ^tests/.*/fixtures/.* + | ^tests/console/commands/debug/test_resolve.py + ) - id: end-of-file-fixer exclude: ^tests/.*/fixtures/.* - id: debug-statements diff --git a/CHANGELOG.md b/CHANGELOG.md index e375aeeffb6..35c22a27f68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,105 @@ # Change Log +## [1.2.0a1] - 2021-05-21 + +This release is the first testing release of the upcoming 1.2.0 version. + +It **drops** support for Python 2.7 and 3.5. + +### Added + +- Poetry now supports a plugin system to alter or expand Poetry's functionality. ([#3733](https://github.com/python-poetry/poetry/pull/3733)) +- Poetry now supports [PEP 610](https://www.python.org/dev/peps/pep-0610/). ([#3876](https://github.com/python-poetry/poetry/pull/3876)) +- Several configuration options to better control the way virtual environments are created are now available. ([#3157](https://github.com/python-poetry/poetry/pull/3157), [#3711](https://github.com/python-poetry/poetry/pull/3711)). +- The `new` command now supports namespace packages. ([#2768](https://github.com/python-poetry/poetry/pull/2768)) +- The `add` command now supports the `--editable` option to add packages in editable mode. ([#3940](https://github.com/python-poetry/poetry/pull/3940)) + +### Changed + +- Python 2.7 and 3.5 are no longer supported. ([#3405](https://github.com/python-poetry/poetry/pull/3405)) +- The usage of the `get-poetry.py` script is now deprecated and is replaced by the `install-poetry.py` script. ([#3706](https://github.com/python-poetry/poetry/pull/3706)) +- Directory dependencies are now in non-develop mode by default. ([poetry-core#98](https://github.com/python-poetry/poetry-core/pull/98)) +- Improved support for PEP 440 specific versions that do not abide by semantic versioning. ([poetry-core#140](https://github.com/python-poetry/poetry-core/pull/140)) +- Improved the CLI experience and performance by migrating to the latest version of Cleo. ([#3618](https://github.com/python-poetry/poetry/pull/3618)) +- Packages previously considered as unsafe (`pip`, `setuptools`, `wheels` and `distribute`) can now be managed as any other package. ([#2826](https://github.com/python-poetry/poetry/pull/2826)) +- The `new` command now defaults to the Markdown format for README files. ([#2768](https://github.com/python-poetry/poetry/pull/2768)) + +### Fixed + +- Fixed an error where command line options were not taken into account when using the `run` command. ([#3618](https://github.com/python-poetry/poetry/pull/3618)) +- Fixed an error in the way custom repositories were resolved. ([#3406](https://github.com/python-poetry/poetry/pull/3406)) + + +## [1.1.5] - 2021-03-04 + +### Fixed +- Fixed an error in the export command when no lock file existed and a verbose flag was passed to the command. (#3310) +- Fixed an error where the pyproject.toml was not reverted when using the add command. (#3622) +- Fixed errors when using non-HTTPS indices. (#3622) +- Fixed errors when handling simple indices redirection. (#3622) +- Fixed errors when trying to handle newer wheels by using the latest version of poetry-core and packaging. (#3677) +- Fixed an error when using some versions of poetry-core due to an incorrect import. (#3696) + +## [1.1.4] - 2020-10-23 + +### Added + +- Added `installer.parallel` boolean flag (defaults to `true`) configuration to enable/disable parallel execution of operations when using the new installer. ([#3088](https://github.com/python-poetry/poetry/pull/3088)) + +### Changed + +- When using system environments as an unprivileged user, user site and bin directories are created if they do not already exist. ([#3107](https://github.com/python-poetry/poetry/pull/3107)) + +### Fixed + +- Fixed editable installation of poetry projects when using system environments. ([#3107](https://github.com/python-poetry/poetry/pull/3107)) +- Fixed locking of nested extra activations. If you were affected by this issue, you will need to regenerate the lock file using `poetry lock --no-update`. ([#3229](https://github.com/python-poetry/poetry/pull/3229)) +- Fixed prioritisation of non-default custom package sources. ([#3251](https://github.com/python-poetry/poetry/pull/3251)) +- Fixed detection of installed editable packages when non-poetry managed `.pth` file exists. ([#3210](https://github.com/python-poetry/poetry/pull/3210)) +- Fixed scripts generated by editable builder to use valid import statements. ([#3214](https://github.com/python-poetry/poetry/pull/3214)) +- Fixed recursion error when locked dependencies contain cyclic dependencies. ([#3237](https://github.com/python-poetry/poetry/pull/3237)) +- Fixed propagation of editable flag for VCS dependencies. ([#3264](https://github.com/python-poetry/poetry/pull/3264)) + +## [1.1.3] - 2020-10-14 + +### Changed + +- Python version support deprecation warning is now written to `stderr`. ([#3131](https://github.com/python-poetry/poetry/pull/3131)) + +### Fixed + +- Fixed `KeyError` when `PATH` is not defined in environment variables. ([#3159](https://github.com/python-poetry/poetry/pull/3159)) +- Fixed error when using `config` command in a directory with an existing `pyproject.toml` without any Poetry configuration. ([#3172](https://github.com/python-poetry/poetry/pull/3172)) +- Fixed incorrect inspection of package requirements when same dependency is specified multiple times with unique markers. ([#3147](https://github.com/python-poetry/poetry/pull/3147)) +- Fixed `show` command to use already resolved package metadata. ([#3117](https://github.com/python-poetry/poetry/pull/3117)) +- Fixed multiple issues with `export` command output when using `requirements.txt` format. ([#3119](https://github.com/python-poetry/poetry/pull/3119)) + +## [1.1.2] - 2020-10-06 + +### Changed +- Dependency installation of editable packages and all uninstall operations are now performed serially within their corresponding priority groups. ([#3099](https://github.com/python-poetry/poetry/pull/3099)) +- Improved package metadata inspection of nested poetry projects within project path dependencies. ([#3105](https://github.com/python-poetry/poetry/pull/3105)) + +### Fixed + +- Fixed export of `requirements.txt` when project dependency contains git dependencies. ([#3100](https://github.com/python-poetry/poetry/pull/3100)) + +## [1.1.1] - 2020-10-05 + +### Added + +- Added `--no-update` option to `lock` command. ([#3034](https://github.com/python-poetry/poetry/pull/3034)) + +### Fixed + +- Fixed resolution of packages with missing required extras. ([#3035](https://github.com/python-poetry/poetry/pull/3035)) +- Fixed export of `requirements.txt` dependencies to include development dependencies. ([#3024](https://github.com/python-poetry/poetry/pull/3024)) +- Fixed incorrect selection of unsupported binary distribution formats when selecting a package artifact to install. ([#3058](https://github.com/python-poetry/poetry/pull/3058)) +- Fixed incorrect use of system executable when building package distributions via `build` command. ([#3056](https://github.com/python-poetry/poetry/pull/3056)) +- Fixed errors in `init` command when specifying `--dependency` in non-interactive mode when a `pyproject.toml` file already exists. ([#3076](https://github.com/python-poetry/poetry/pull/3076)) +- Fixed incorrect selection of configured source url when a publish repository url configuration with the same name already exists. ([#3047](https://github.com/python-poetry/poetry/pull/3047)) +- Fixed dependency resolution issues when the same package is specified in multiple dependency extras. ([#3046](https://github.com/python-poetry/poetry/pull/3046)) + ## [1.1.0] - 2020-10-01 ### Changed @@ -1023,7 +1123,12 @@ Initial release -[Unreleased]: https://github.com/python-poetry/poetry/compare/1.1.0...master +[Unreleased]: https://github.com/python-poetry/poetry/compare/1.2.0a1...master +[1.2.0a1]: https://github.com/python-poetry/poetry/compare/1.2.0a1 +[1.1.4]: https://github.com/python-poetry/poetry/compare/1.1.4 +[1.1.3]: https://github.com/python-poetry/poetry/compare/1.1.3 +[1.1.2]: https://github.com/python-poetry/poetry/releases/tag/1.1.2 +[1.1.1]: https://github.com/python-poetry/poetry/releases/tag/1.1.1 [1.1.0]: https://github.com/python-poetry/poetry/releases/tag/1.1.0 [1.1.0rc1]: https://github.com/python-poetry/poetry/releases/tag/1.1.0rc1 [1.1.0b4]: https://github.com/python-poetry/poetry/releases/tag/1.1.0b4 diff --git a/Makefile b/Makefile index 4633517e587..f8a9ffb361b 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ wheel: @poetry build -v linux_release: - docker pull quay.io/pypa/manylinux2010_x86_64 + docker pull quay.io/pypa/manylinux2010_x86_64:2021-02-06-3d322a5 docker run --rm -i -v `pwd`:/io \ -e PYTHON=/opt/python/cp38-cp38/bin/python \ -e PYTHON27=/opt/python/cp27-cp27m/bin/python \ @@ -56,7 +56,7 @@ linux_release: -e PYTHON37=/opt/python/cp37-cp37m/bin/python \ -e PYTHON38=/opt/python/cp38-cp38/bin/python \ -e PYTHON39=/opt/python/cp39-cp39/bin/python \ - quay.io/pypa/manylinux2010_x86_64 sh -c "cd /io && ./make-nix-release.sh" + quay.io/pypa/manylinux2010_x86_64:2021-02-06-3d322a5 sh -c "cd /io && ./make-nix-release.sh" # run tests against all supported python versions tox: diff --git a/README.md b/README.md index 3ad7b97d5f1..9a456733b0d 100644 --- a/README.md +++ b/README.md @@ -17,47 +17,84 @@ The [complete documentation](https://python-poetry.org/docs/) is available on th ## Installation 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`. +from the rest of your system. +### osx / linux / bashonwindows install instructions ```bash -curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python +curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py | python - ``` +### windows powershell install instructions +```powershell +(Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py -UseBasicParsing).Content | python - +``` + +**Warning**: The previous `get-poetry.py` installer is now deprecated, if you are currently using it +you should migrate to the new, supported, `install-poetry.py` installer. + +The installer installs the `poetry` tool to Poetry's `bin` directory. This location depends on your system: -Alternatively, you can download the `get-poetry.py` file and execute it separately. +- `$HOME/.local/bin` for Unix +- `%APPDATA%\Python\Scripts` on Windows -The setup script must be able to find one of following executables in your shell's path environment: +If this directory is not on your `PATH`, you will need to add it manually +if you want to invoke Poetry with simply `poetry`. -- `python` (which can be a py3 or py2 interpreter) -- `python3` -- `py.exe -3` (Windows) -- `py.exe -2` (Windows) +Alternatively, you can use the full path to `poetry` to use it. -If you want to install prerelease versions, you can do so by passing `--preview` to `get-poetry.py`: +Once Poetry is installed you can execute the following: ```bash -python get-poetry.py --preview +poetry --version ``` -Similarly, if you want to install a specific version, you can use `--version`: +If you see something like `Poetry (version 1.2.0)` then you are ready to use Poetry. +If you decide Poetry isn't your thing, you can completely remove it from your system +by running the installer again with the `--uninstall` option or by setting +the `POETRY_UNINSTALL` environment variable before executing the installer. ```bash -python get-poetry.py --version 0.7.0 +python install-poetry.py --uninstall +POETRY_UNINSTALL=1 python install-poetry.py ``` -Using `pip` to install `poetry` is also possible. +By default, Poetry is installed into the user's platform-specific home directory. +If you wish to change this, you may define the `POETRY_HOME` environment variable: ```bash -pip install --user poetry +POETRY_HOME=/etc/poetry python install-poetry.py ``` -Be aware, however, that it will also install poetry's dependencies -which might cause conflicts. +If you want to install prerelease versions, you can do so by passing `--preview` option to `install-poetry.py` +or by using the `POETRY_PREVIEW` environment variable: + +```bash +python install-poetry.py --preview +POETRY_PREVIEW=1 python install-poetry.py +``` + +Similarly, if you want to install a specific version, you can use `--version` option or the `POETRY_VERSION` +environment variable: + +```bash +python install-poetry.py --version 1.2.0 +POETRY_VERSION=1.2.0 python install-poetry.py +``` + +You can also install Poetry for a `git` repository by using the `--git` option: + +```bash +python install-poetry.py --git https://github.com/python-poetry/poetry.git@master +```` + +**Note**: Note that the installer does not support Python < 3.6. ## Updating `poetry` Updating poetry to the latest stable version is as simple as calling the `self update` command. +**Warning**: Poetry versions installed using the now deprecated `get-poetry.py` installer will not be able to use this +command to update to 1.2 releases or later. Migrate to using the `install-poetry.py` installer or `pipx`. + ```bash poetry self update ``` @@ -72,13 +109,9 @@ And finally, if you want to install a specific version you can pass it as an arg to `self update`. ```bash -poetry self update 1.0.0 +poetry self update 1.2.0 ``` -*Note:* - - If you are still on poetry version < 1.0 use `poetry self:update` instead. - ## Enable tab completion for Bash, Fish, or Zsh @@ -195,8 +228,8 @@ dependency management, packaging and publishing. It takes inspiration in tools that exist in other languages, like `composer` (PHP) or `cargo` (Rust). -And, finally, there is no reliable tool to properly resolve dependencies in Python, so I started `poetry` -to bring an exhaustive dependency resolver to the Python community. +And, finally, I started `poetry` to bring another exhaustive dependency resolver to the Python community apart from +[Conda's](https://conda.io). ### What about Pipenv? @@ -268,4 +301,4 @@ At this point the rest of the resolution is straightforward since there is no mo * [Official Website](https://python-poetry.org) * [Issue Tracker](https://github.com/python-poetry/poetry/issues) -* [Discord](https://discordapp.com/invite/awxPgve) +* [Discord](https://discord.com/invite/awxPgve) diff --git a/docs/docs/basic-usage.md b/docs/basic-usage.md similarity index 72% rename from docs/docs/basic-usage.md rename to docs/basic-usage.md index 178c75954d6..1b7dbf764f5 100644 --- a/docs/docs/basic-usage.md +++ b/docs/basic-usage.md @@ -1,7 +1,18 @@ +--- +title: "Basic usage" +draft: false +type: docs +layout: single + +menu: + docs: + weight: 10 +--- + # Basic usage For the basic usage introduction we will be installing `pendulum`, a datetime library. -If you have not yet installed Poetry, refer to the [Introduction](/docs/) chapter. +If you have not yet installed Poetry, refer to the [Introduction]({{< relref "docs" >}} "Introduction") chapter. ## Project setup @@ -77,8 +88,8 @@ It will automatically find a suitable version constraint **and install** the pac ## Using your virtual environment By default, poetry creates a virtual environment in `{cache-dir}/virtualenvs` (`{cache-dir}\virtualenvs` on Windows). -You can change the [`cache-dir`](/docs/configuration/#cache-dir) value by editing the poetry config. -Additionally, you can use the [`virtualenvs.in-project`](/docs/configuration/#virtualenvs.in-project) configuration variable +You can change the [`cache-dir`]({{< relref "configuration#cache-dir" >}} "cache-dir configuration documentation") value by editing the poetry config. +Additionally, you can use the [`virtualenvs.in-project`]({{< relref "configuration#virtualenvsin-project" >}} "#virtualenvs.in-project configuration documentation") configuration variable to create virtual environment within your project directory. @@ -96,31 +107,32 @@ The easiest way to activate the virtual environment is to create a new shell wit To deactivate the virtual environment and exit this new shell type `exit`. To deactivate the virtual environment without leaving the shell use `deactivate`. -!!!note +{{% note %}} +**Why a new shell?** - **Why a new shell?** - Child processes inherit their environment from their parents, but do not share - them. As such, any modifications made by a child process, is not persisted after - the child process exits. A Python application (Poetry), being a child process, - cannot modify the environment of the shell that it has been called from such - that an activated virtual environment remains active after the Poetry command - has completed execution. +Child processes inherit their environment from their parents, but do not share +them. As such, any modifications made by a child process, is not persisted after +the child process exits. A Python application (Poetry), being a child process, +cannot modify the environment of the shell that it has been called from such +that an activated virtual environment remains active after the Poetry command +has completed execution. - Therefore, Poetry has to create a sub-shell with the virtual envrionment activated - in order for the subsequent commands to run from within the virtual environment. +Therefore, Poetry has to create a sub-shell with the virtual environment activated +in order for the subsequent commands to run from within the virtual environment. +{{% /note %}} Alternatively, to avoid creating a new shell, you can manually activate the -virtual environment by running `source {path_to_venv}/bin/activate` (`source {path_to_venv}\Scripts\activate.bat` on Windows). +virtual environment by running `source {path_to_venv}/bin/activate` (`{path_to_venv}\Scripts\activate.bat` on Windows). To get the path to your virtual environment run `poetry env info --path`. You can also combine these into a nice one-liner, `source $(poetry env info --path)/bin/activate` To deactivate this virtual environment simply use `deactivate`. -| | POSIX Shell | Windows | Exit/Deactivate | -|-------------------|------------------------------------------------|---------------------------------------------|-----------------| -| New Shell | `poetry shell` | `poetry shell` | `exit` | -| Manual Activation | `source {path_to_venv}/bin/activate` | `source {path_to_venv}\Scripts\activate.bat`| `deactivate` | -| One-liner | ```source`poetry env info --path`/bin/activate```| | `deactivate` | +| | POSIX Shell | Windows | Exit/Deactivate | +| ----------------- | -------------------------------------------------- | ------------------------------------- | --------------- | +| New Shell | `poetry shell` | `poetry shell` | `exit` | +| Manual Activation | `source {path_to_venv}/bin/activate` | `{path_to_venv}\Scripts\activate.bat` | `deactivate` | +| One-liner | ```source `poetry env info --path`/bin/activate``` | | `deactivate` | ### Version constraints @@ -128,21 +140,21 @@ To deactivate this virtual environment simply use `deactivate`. 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 [Dependency specification](/docs/dependency-specification) for more in-depth information on versions, +Please read [Dependency specification]({{< relref "dependency-specification" >}} "Dependency specification documentation") for more in-depth information on versions, how versions relate to each other, and on the different ways you can specify dependencies. -!!!note - - **How does Poetry download the right files?** +{{% note %}} +**How does Poetry download the right files?** - When you specify a dependency in `pyproject.toml`, Poetry first takes the name of the package - that you have requested and searches for it in any repository you have registered using the `repositories` key. - If you have not registered any extra repositories, or it does not find a package with that name in the - repositories you have specified, it falls back on PyPI. +When you specify a dependency in `pyproject.toml`, Poetry first takes the name of the package +that you have requested and searches for it in any repository you have registered using the `repositories` key. +If you have not registered any extra repositories, or it does not find a package with that name in the +repositories you have specified, it falls back on PyPI. - When Poetry finds the right package, it then attempts to find the best match - for the version constraint you have specified. +When Poetry finds the right package, it then attempts to find the best match +for the version constraint you have specified. +{{% /note %}} ## Installing dependencies @@ -189,9 +201,9 @@ Even if you develop alone, in six months when reinstalling the project you can f the dependencies installed are still working even if your dependencies released many new versions since then. (See note below about using the update command.) -!!!note - - For libraries it is not necessary to commit the lock file. +{{% note %}} +For libraries it is not necessary to commit the lock file. +{{% /note %}} ### Installing dependencies only @@ -212,7 +224,7 @@ This will fetch the latest matching versions (according to your `pyproject.toml` and update the lock file with the new versions. (This is equivalent to deleting the `poetry.lock` file and running `install` again.) -!!!note - - Poetry will display a **Warning** when executing an install command if `poetry.lock` and `pyproject.toml` - are not synchronized. +{{% note %}} +Poetry will display a **Warning** when executing an install command if `poetry.lock` and `pyproject.toml` +are not synchronized. +{{% /note %}} diff --git a/docs/docs/cli.md b/docs/cli.md similarity index 63% rename from docs/docs/cli.md rename to docs/cli.md index b051e9346bb..0a1b8a265af 100644 --- a/docs/docs/cli.md +++ b/docs/cli.md @@ -1,3 +1,15 @@ +--- +title: "Commands" +draft: false +type: docs +layout: single + +menu: + docs: + weight: 30 +--- + + # Commands You've already learned how to use the command-line interface to do some things. @@ -14,6 +26,7 @@ then `--help` combined with any of those can give you more information. * `--ansi`: Force ANSI output. * `--no-ansi`: Disable ANSI output. * `--version (-V)`: Display this application version. +* `--no-interaction (-n)`: Do not ask any interactive question. ## new @@ -30,12 +43,11 @@ will create a folder as follows: ```text my-package ├── pyproject.toml -├── README.rst +├── README.md ├── my_package │ └── __init__.py └── tests - ├── __init__.py - └── test_my_package.py + └── __init__.py ``` If you want to name your project differently than the folder, you can pass @@ -56,13 +68,33 @@ That will create a folder structure as follows: ```text my-package ├── pyproject.toml -├── README.rst +├── README.md ├── src │ └── my_package │ └── __init__.py └── tests - ├── __init__.py - └── test_my_package.py + └── __init__.py +``` + +The `--name` option is smart enough to detect namespace packages and create +the required structure for you. + +```bash +poetry new --src --name my.package my-package +``` + +will create the following structure: + +```text +my-package +├── pyproject.toml +├── README.md +├── src +│ └── my +│ └── package +│ └── __init__.py +└── tests + └── __init__.py ``` ## init @@ -108,6 +140,13 @@ the `--no-dev` option. poetry install --no-dev ``` +Conversely, you can specify to the command that you only want to install the development dependencies +by passing the `--dev-only` option. Note that `--no-dev` takes priority if both options are passed. + +```bash +poetry install --dev-only +``` + If you want to remove old dependencies no longer present in the lock file, use the `--remove-untracked` option. @@ -116,7 +155,7 @@ poetry install --remove-untracked ``` You can also specify the extras you want installed -by passing the `-E|--extras` option (See [Extras](/docs/pyproject/#extras) for more info) +by passing the `-E|--extras` option (See [Extras]({{< relref "pyproject#extras" >}}) for more info) ```bash poetry install --extras "mysql pgsql" @@ -132,7 +171,6 @@ Installing dependencies from lock file No dependencies to install or update - Installing (x.x.x) - ``` If you want to skip this installation, use the `--no-root` option. @@ -141,10 +179,16 @@ If you want to skip this installation, use the `--no-root` option. poetry install --no-root ``` +Installation of your project's package is also skipped when the `--dev-only` +option is passed. + ### Options * `--no-dev`: Do not install dev dependencies. +* `--dev-only`: Only install dev dependencies. * `--no-root`: Do not install the root package (your project). +* `--dry-run`: Output the operations but do not execute anything (implicitly enables --verbose). +* `--remove-untracked`: Remove dependencies not presented in the lock file * `--extras (-E)`: Features to install (multiple values allowed). ## update @@ -212,6 +256,10 @@ or use ssh instead of https: ```bash poetry add git+ssh://git@github.com/sdispater/pendulum.git + +or alternatively: + +poetry add git+ssh://git@github.com:sdispater/pendulum.git ``` If you need to checkout a specific branch, tag or revision, @@ -220,6 +268,11 @@ 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 + +or using SSH instead: + +poetry add git+ssh://github.com/sdispater/pendulum.git#develop +poetry add git+ssh://github.com/sdispater/pendulum.git#2.0.5 ``` or make them point to a local directory or file: @@ -230,17 +283,24 @@ poetry add ../my-package/dist/my-package-0.1.0.tar.gz poetry add ../my-package/dist/my_package-0.1.0.whl ``` -If you want the dependency to be installed in editable mode you can specify it in the `pyproject.toml` file. It means that changes in the local directory will be reflected directly in environment. +If you want the dependency to be installed in editable mode you can use the `--editable` option. + +```bash +poetry add --editable ./my-package/ +poetry add --editable git+ssh://github.com/sdispater/pendulum.git#develop +``` + +Alternatively, you can specify it in the `pyproject.toml` file. It means that changes in the local directory will be reflected directly in environment. ```toml [tool.poetry.dependencies] my-package = {path = "../my/path", develop = true} ``` -!!!note - - Before poetry 1.1 path dependencies were installed in editable mode by default. You should always set the `develop` attribute explicit, - to make sure the behavior is the same for all poetry versions. +{{% note %}} +Before poetry 1.1 path dependencies were installed in editable mode by default. You should always set the `develop` attribute explicit, +to make sure the behavior is the same for all poetry versions. +{{% /note %}} If the package(s) you want to install provide extras, you can specify them when adding the package: @@ -254,10 +314,15 @@ poetry add "git+https://github.com/pallets/flask.git@1.1.1[dotenv,dev]" ### Options * `--dev (-D)`: Add package as development dependency. -* `--path`: The path to a dependency. -* `--optional` : Add as an optional dependency. -* `--dry-run` : Outputs the operations but will not execute anything (implicitly enables --verbose). -* `--lock` : Do not perform install (only update the lockfile). +* `--editable (-e)`: Add vcs/path dependencies as editable. +* `--extras (-E)`: Extras to activate for the dependency. (multiple values allowed) +* `--optional`: Add as an optional dependency. +* `--python`: Python version for which the dependency must be installed. +* `--platform`: Platforms for which the dependency must be installed. +* `--source`: Name of the source to use to install the package. +* `---allow-prereleases`: Accept prereleases. +* `--dry-run`: Outputs the operations but will not execute anything (implicitly enables --verbose). +* `--lock`: Do not perform install (only update the lockfile). ## remove @@ -355,7 +420,7 @@ poetry config [options] [setting-key] [setting-value1] ... [setting-valueN] ```` `setting-key` is a configuration option name and `setting-value1` is a configuration value. -See [Configuration](/docs/configuration/) for all available settings. +See [Configuration]({{< relref "configuration" >}}) for all available settings. ### Options @@ -419,10 +484,19 @@ poetry search requests pendulum This command locks (without installing) the dependencies specified in `pyproject.toml`. +{{% note %}} +By default, this will lock all dependencies to the latest available compatible versions. To only refresh the lock file, use the `--no-update` option. +{{% /note %}} + ```bash poetry lock ``` +### Options + +* `--check`: Verify that `poetry.lock` is consistent with `pyproject.toml` +* `--no-update`: Do not update locked versions, only refresh lock file. + ## version This command shows the current version of the project or bumps the version of @@ -434,19 +508,19 @@ The new version should ideally be a valid [semver](https://semver.org/) string o The table below illustrates the effect of these rules with concrete examples. -| rule | before | after | -|------------|---------------|---------------| -| major | 1.3.0 | 2.0.0 | -| minor | 2.1.4 | 2.2.0 | -| patch | 4.1.1 | 4.1.2 | -| premajor | 1.0.2 | 2.0.0-alpha.0 | -| preminor | 1.0.2 | 1.1.0-alpha.0 | -| prepatch | 1.0.2 | 1.0.3-alpha.0 | -| prerelease | 1.0.2 | 1.0.3-alpha.0 | +| rule | before | after | +| ---------- | ------------- | ------------- | +| major | 1.3.0 | 2.0.0 | +| minor | 2.1.4 | 2.2.0 | +| patch | 4.1.1 | 4.1.2 | +| premajor | 1.0.2 | 2.0.0-alpha.0 | +| preminor | 1.0.2 | 1.1.0-alpha.0 | +| prepatch | 1.0.2 | 1.0.3-alpha.0 | +| prerelease | 1.0.2 | 1.0.3-alpha.0 | | prerelease | 1.0.3-alpha.0 | 1.0.3-alpha.1 | -| prerelease | 1.0.3-beta.0 | 1.0.3-beta.1 | +| prerelease | 1.0.3-beta.0 | 1.0.3-beta.1 | -## Options +### Options * `--short (-s)`: Output the version number only. @@ -458,9 +532,9 @@ This command exports the lock file to other formats. poetry export -f requirements.txt --output requirements.txt ``` -!!!note - - Only the `requirements.txt` format is currently supported. +{{% note %}} +Only the `requirements.txt` format is currently supported. +{{% /note %}} ### Options @@ -478,7 +552,7 @@ poetry export -f requirements.txt --output requirements.txt The `env` command regroups sub commands to interact with the virtualenvs associated with a specific project. -See [Managing environments](/docs/managing-environments/) for more information about these commands. +See [Managing environments]({{< relref "managing-environments" >}}) for more information about these commands. ## cache @@ -491,3 +565,115 @@ The `cache list` command lists Poetry's available caches. ```bash poetry cache list ``` + +### cache clear + +The `cache clear` command removes packages from a cached repository. + +For example, to clear the whole cache of packages from the `pypi` repository, run: + +```bash +poetry cache clear pypi --all +``` + +To only remove a specific package from a cache, you have to specify the cache entry in the following form `cache:package:version`: + +```bash +poetry cache clear pypi:requests:2.24.0 +``` + +## plugin + +The `plugin` namespace regroups sub commands to manage Poetry plugins. + +### `plugin add` + +The `plugin add` command installs Poetry plugins and make them available at runtime. + +For example, to install the `poetry-plugin` plugin, you can run: + +```bash +poetry plugin add poetry-plugin +``` + +The package specification formats supported by the `plugin add` command are the same as the ones supported +by the [`add` command](#add). + +If you just want to check what would happen by installing a plugin, you can use the `--dry-run` option + +```bash +poetry plugin add poetry-plugin --dry-run +``` + +#### Options + +* `--dry-run`: Outputs the operations but will not execute anything (implicitly enables --verbose). + +### `plugin show` + +The `plugin show` command lists all the currently installed plugins. + +```bash +poetry plugin show +``` + +### `plugin remove` + +The `plugin remove` command removes installed plugins. + +```bash +poetry plugin remove poetry-plugin +``` + +## source + +The `source` namespace regroups sub commands to manage repository sources for a Poetry project. + +### `source add` + +The `source add` command adds source configuration to the project. + +For example, to add the `pypi-test` source, you can run: + +```bash +poetry source add pypi-test https://test.pypi.org/simple/ +``` + +{{% note %}} +You cannot use the name `pypi` as it is reserved for use by the default PyPI source. +{{% /note %}} + +#### Options + +* `--default`: Set this source as the [default]({{< relref "repositories#disabling-the-pypi-repository" >}}) (disable PyPI). +* `--secondary`: Set this source as a [secondary]({{< relref "repositories#install-dependencies-from-a-private-repository" >}}) source. + +{{% note %}} +You cannot set a source as both `default` and `secondary`. +{{% /note %}} + +### `source show` + +The `source show` command displays information on all configured sources for the project. + +```bash +poetry source show +``` + +Optionally, you can show information of one or more sources by specifying their names. + +```bash +poetry source show pypi-test +``` + +{{% note %}} +This command will only show sources configured via the `pyproject.toml` and does not include PyPI. +{{% /note %}} + +### `source remove` + +The `source remove` command removes a configured source from your `pyproject.toml`. + +```bash +poetry source remove pypi-test +``` diff --git a/docs/docs/configuration.md b/docs/configuration.md similarity index 64% rename from docs/docs/configuration.md rename to docs/configuration.md index 87c5232f4be..f1669d76f62 100644 --- a/docs/docs/configuration.md +++ b/docs/configuration.md @@ -1,6 +1,17 @@ +--- +title: "Configuration" +draft: false +type: docs +layout: single + +menu: + docs: + weight: 40 +--- + # Configuration -Poetry can be configured via the `config` command ([see more about its usage here](/docs/cli/#config)) +Poetry can be configured via the `config` command ([see more about its usage here]({{< relref "cli#config" >}} "config command documentation")) or directly in the `config.toml` file that will be automatically be created when you first run that command. This file can typically be found in one of the following directories: @@ -34,6 +45,8 @@ which will give you something similar to this: cache-dir = "/path/to/cache/directory" virtualenvs.create = true virtualenvs.in-project = null +virtualenvs.options.always-copy = true +virtualenvs.options.system-site-packages = false virtualenvs.path = "{cache-dir}/virtualenvs" # /path/to/cache/directory/virtualenvs ``` @@ -89,10 +102,11 @@ This also works for secret settings, like credentials: export POETRY_HTTP_BASIC_MY_REPOSITORY_PASSWORD=secret ``` - ## Available settings -### `cache-dir`: string +### `cache-dir` + +**Type**: string The path to the cache directory used by Poetry. @@ -102,30 +116,70 @@ Defaults to one of the following directories: - Windows: `C:\Users\\AppData\Local\pypoetry\Cache` - Unix: `~/.cache/pypoetry` -### `virtualenvs.create`: boolean +### `installer.parallel` + +**Type**: boolean + +Use parallel execution when using the new (`>=1.1.0`) installer. +Defaults to `true`. + +{{% note %}} +This configuration will be ignored, and parallel execution disabled when running +Python 2.7 under Windows. +{{% /note %}} + +### `virtualenvs.create` + +**Type**: boolean Create a new virtual environment if one doesn't already exist. Defaults to `true`. -!!!note: - When setting this configuration to `false`, the Python environment used must have `pip` - installed and available. +If set to `false`, poetry will install dependencies into the current python environment. -### `virtualenvs.in-project`: boolean +{{% note %}} +When setting this configuration to `false`, the Python environment used must have `pip` +installed and available. +{{% /note %}} + +### `virtualenvs.in-project` + +**Type**: boolean Create the virtualenv inside the project's root directory. Defaults to `None`. -If set to `true`, the virtualenv wil be created and expected in a folder named `.venv` within the root directory of the project. +If set to `true`, the virtualenv will be created and expected in a folder named +`.venv` within the root directory of the project. -If not set explicitly (default), `poetry` will use the virtualenv from the `.venv` directory when one is available. If set to `false`, `poetry` will ignore any existing `.venv` directory. +If not set explicitly (default), `poetry` will use the virtualenv from the `.venv` +directory when one is available. If set to `false`, `poetry` will ignore any +existing `.venv` directory. +### `virtualenvs.path` -### `virtualenvs.path`: string +**Type**: string Directory where virtual environments will be created. Defaults to `{cache-dir}/virtualenvs` (`{cache-dir}\virtualenvs` on Windows). -### `repositories.`: string +### `virtualenvs.options.always-copy` + +**Type**: boolean + +If set to `true` the `--always-copy` parameter is passed to `virtualenv` on creation of the venv. Thus all needed files are copied into the venv instead of symlinked. +Defaults to `false`. + +### `virtualenvs.options.system-site-packages` + +**Type**: boolean + +Give the virtual environment access to the system site-packages directory. +Applies on virtualenv creation. +Defaults to `false`. + +### `repositories.` + +**Type**: string -Set a new alternative repository. See [Repositories](/docs/repositories/) for more information. +Set a new alternative repository. See [Repositories]({{< relref "repositories" >}}) for more information. diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 00000000000..d54359fde70 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,266 @@ +--- +title: "Contributing to Poetry" +draft: false +type: docs +layout: single + +menu: + docs: + weight: 100 +--- + +# Contributing to Poetry + +First off, thanks 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 judgement, 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 documentation](#contributing-to-documentation) + * [Contributing to code](#contributing-to-code) + * [Issue triage](#issue-triage) + * [Git workflow](#git-workflow) + + +## 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/python-poetry/poetry/blob/master/.github/ISSUE_TEMPLATE/---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://python-poetry.org/docs/faq)** 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/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/---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-paste-able 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/python-poetry/poetry/blob/master/.github/ISSUE_TEMPLATE/---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://python-poetry.org/docs/faq)** 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/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. +* **Provide specific examples to demonstrate the steps**.. +* **Describe the current behavior** and **explain which behavior you expected to see instead** and why. + +### Contributing to documentation + +One of the simplest ways to get started contributing to a project is through improving documentation. Poetry is constantly evolving, this means that sometimes our documentation has gaps. You can help by +adding missing sections, editing the existing content so it is more accessible or creating new content (tutorials, FAQs, etc). + +{{% note %}} +A great way to understand Poetry's design and how it all fits together, is to add FAQ entries for commonly +asked questions. Poetry members usually mark issues with [candidate/faq](https://github.com/python-poetry/poetry/issues?q=is%3Aissue+label%3Acandidate%2Ffaq+) to indicate that the issue either contains a response +that explains how something works or might benefit from an entry in the FAQ. +{{% /note %}} + +Issues pertaining to the documentation are usually marked with the [Documentation](https://github.com/python-poetry/poetry/labels/Documentation) label. + +### Contributing to code + +#### Picking an issue + +{{% note %}} +If you are a first time contributor, and are looking for an issue to take on, you might want to look for [Good First Issue](https://github.com/python-poetry/poetry/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+First+Issue%22) +labelled issues. We do our best to label such issues, however we might fall behind at times. So, ask us. +{{% /note %}} + +If you would like to take on an issue, feel free to comment on the issue tagging `@python-poetry/triage`. We are more than happy to discuss solutions on the issue. If you would like help with navigating +the code base, join us on our [Discord Server](https://discordapp.com/invite/awxPgve). + +#### Local development + +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:python-poetry/poetry.git +$ cd poetry +``` + +{{% note %}} +We recommend that you use a personal [fork](https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/fork-a-repo) for this step. If you are new to GitHub collaboration, +you can refer to the [Forking Projects Guide](https://guides.github.com/activities/forking/). +{{% /note %}} + +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/psf/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/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. + +{{% note %}} +Make sure your branch is [rebased](https://docs.github.com/en/free-pro-team@latest/github/using-git/about-git-rebase) against the latest main branch. A maintainer might ask you to ensure the branch is +up-to-date prior to merging your Pull Request if changes have conflicts. +{{% /note %}} + +All pull requests, unless otherwise instructed, need to be first accepted into the main branch (`master`). + +### Issue triage + +{{% note %}} +If you have an issue that hasn't had any attention, you can ping us `@python-poetry/triage` on the issue. Please, give us reasonable time to get to your issue first, spamming us with messages +{{% /note %}} + +If you are helping with the triage of reported issues, this section provides some useful information to assist you in your contribution. + +#### Triage steps + +1. If `pyproject.toml` is missing or `-vvv` debug logs (with stack trace) is not provided and required, request that the issue author provides it. +1. Attempt to reproduce the issue with the reported Poetry version or request further clarification from the issue author. +1. Ensure the issue is not already resolved. You can attempt to reproduce using the latest preview release and/or poetry from the main branch. +1. If the issue cannot be reproduced, + 1. clarify with the issue's author, + 1. close the issue or notify `@python-poetry/triage`. +1. If the issue can be reproduced, + 1. comment on the issue confirming so + 1. notify `@python-poetry/triage`. + 1. if possible, identify the root cause of the issue. + 1. if interested, attempt to fix it via a pull request. + +#### Multiple versions + +Often times you would want to attempt to reproduce issues with multiple versions of `poetry` at the same time. For these use cases, the [pipx project](https://pipxproject.github.io/pipx/) is useful. + +You can set your environment up like so. + +```sh +pipx install --suffix @1.0.10 'poetry==1.0.10' +pipx install --suffix @1.1.0rc1 'poetry==1.1.0rc1' +pipx install --suffix @master 'poetry @ git+https://github.com/python-poetry/poetry' +``` + +{{% note %}} +Do not forget to update your `poetry@master` installation in sync with upstream. +{{% /note %}} + +For `@local` it is recommended that you do something similar to the following as editable installs are not supported for PEP 517 projects. + +```sh +# note this will not work for Windows, and we assume you have already run `poetry install` +cd /path/to/python-poetry/poetry +ln -sf $(poetry run which poetry) ~/.local/bin/poetry@local +``` + +{{% note %}} +This mechanism can also be used to test pull requests. +{{% /note %}} + +### Git Workflow + +All development work is performed against Poetry's main branch (`master`). All changes are expected to be submitted and accepted to this +branch. + +#### Release branch + +When a release is ready, the following are required before a release is tagged. + +1. A release branch with the prefix `release-`, eg: `release-1.1.0rc1`. +1. A pull request from the release branch to the main branch (`master`) if it's a minor or major release. Otherwise, to the bug fix branch (eg: `1.0`). + 1. The pull request description MUST include the change log corresponding to the release (eg: [#2971](https://github.com/python-poetry/poetry/pull/2971)). + 1. The pull request must contain a commit that updates [CHANGELOG.md](CHANGELOG.md) and bumps the project version (eg: [#2971](https://github.com/python-poetry/poetry/pull/2971/commits/824e7b79defca435cf1d765bb633030b71b9a780)). + 1. The pull request must have the `Release` label specified. + +Once the branch pull-request is ready and approved, a member of `@python-poetry/core` will, + +1. Tag the branch with the version identifier (eg: `1.1.0rc1`). +2. Merge the pull request once the release is created and assets are uploaded by the CI. + +{{% note %}} +In this case, we prefer a merge commit instead of squash or rebase merge. +{{% /note %}} + +#### Bug fix branch + +Once a minor version (eg: `1.1.0`) is released, a new branch for the minor version (eg: `1.1`) is created for the bug fix releases. Changes identified +or acknowledged by the Poetry team as requiring a bug fix can be submitted as a pull requests against this branch. + +At the time of writing only issues meeting the following criteria may be accepted into a bug fix branch. Trivial fixes may be accepted on a +case-by-case basis. + +1. The issue breaks a core functionality and/or is a critical regression. +1. The change set does not introduce a new feature or changes an existing functionality. +1. A new minor release is not expected within a reasonable time frame. +1. If the issue affects the next minor/major release, a corresponding fix has been accepted into the main branch. + +{{% note %}} +This is subject to the interpretation of a maintainer within the context of the issue. +{{% /note %}} diff --git a/docs/docs/dependency-specification.md b/docs/dependency-specification.md similarity index 81% rename from docs/docs/dependency-specification.md rename to docs/dependency-specification.md index 2a1b63afdae..e2fa0e4e021 100644 --- a/docs/docs/dependency-specification.md +++ b/docs/dependency-specification.md @@ -1,3 +1,14 @@ +--- +title: "Dependency specification" +draft: false +type: docs +layout: single + +menu: + docs: + weight: 70 +--- + # Dependency specification Dependencies for a project can be specified in various forms, which depend on the type @@ -7,12 +18,7 @@ of the dependency and on the optional constraints that might be needed for it to ### Caret requirements -**Caret requirements** allow SemVer compatible updates to a specified version. -An update is allowed if the new version number does not modify the left-most non-zero digit in the major, minor, patch grouping. -In this case, if we ran `poetry update requests`, poetry would update us to version `2.14.0` if it was available, -but would not update us to `3.0.0`. -If instead we had specified the version string as `^0.1.13`, poetry would update to `0.1.14` but not `0.2.0`. -`0.0.x` is not considered compatible with any other version. +**Caret requirements** allow [SemVer](https://semver.org/) compatible updates to a specified version. An update is allowed if the new version number does not modify the left-most non-zero digit in the major, minor, patch grouping. For instance, if we previously ran `poetry add requests@^2.13.0` and wanted to update the library and ran `poetry update requests`, poetry would update us to version `2.14.0` if it was available, but would not update us to `3.0.0`. If instead we had specified the version string as `^0.1.13`, poetry would update to `0.1.14` but not `0.2.0`. `0.0.x` is not considered compatible with any other version. Here are some more examples of caret requirements and the versions that would be allowed with them: @@ -103,6 +109,13 @@ flask = { git = "https://github.com/pallets/flask.git", rev = "38eb5d3b" } numpy = { git = "https://github.com/numpy/numpy.git", tag = "v0.13.2" } ``` +To use an SSH connection, for example in the case of private repositories, use the following example syntax: + +```toml +[tool.poetry.dependencies] +requests = { git = "git@github.com:requests/requests.git" } +``` + ## `path` dependencies To depend on a library located in a local directory or file, @@ -117,10 +130,10 @@ my-package = { path = "../my-package/", develop = false } my-package = { path = "../my-package/dist/my-package-0.1.0.tar.gz" } ``` -!!!note - - Before poetry 1.1 directory path dependencies were installed in editable mode by default. You should set the `develop` attribute explicitly, - to make sure the behavior is the same for all poetry versions. +{{% note %}} +Before poetry 1.1 directory path dependencies were installed in editable mode by default. You should set the `develop` attribute explicitly, +to make sure the behavior is the same for all poetry versions. +{{% /note %}} ## `url` dependencies @@ -183,6 +196,11 @@ foo = [ ] ``` +{{% note %}} +The constraints **must** have different requirements (like `python`) +otherwise it will cause an error when resolving dependencies. +{{% /note %}} + ## Expanded dependency specification syntax In the case of more complex dependency specifications, you may find that you @@ -210,8 +228,3 @@ markers = "platform_python_implementation == 'CPython'" All of the same information is still present, and ends up providing the exact same specification. It's simply split into multiple, slightly more readable, lines. - -!!!note - - The constraints **must** have different requirements (like `python`) - otherwise it will cause an error when resolving dependencies. diff --git a/docs/docs/contributing.md b/docs/docs/contributing.md deleted file mode 100644 index 568877b4a4f..00000000000 --- a/docs/docs/contributing.md +++ /dev/null @@ -1 +0,0 @@ -{!../CONTRIBUTING.md!} diff --git a/docs/docs/index.md b/docs/docs/index.md deleted file mode 100644 index 64bb512f6cb..00000000000 --- a/docs/docs/index.md +++ /dev/null @@ -1,219 +0,0 @@ -# Introduction - -Poetry is a tool for dependency management and packaging in Python. -It allows you to declare the libraries your project depends on and it will manage (install/update) them for you. - - -## System requirements - -Poetry requires Python 2.7 or 3.5+. It is multi-platform and the goal is to make it work equally well -on Windows, Linux and OSX. - -!!! note - - Python 2.7 and 3.5 will no longer be supported in the next feature release (1.2). - You should consider updating your Python version to a supported one. - - -## Installation - -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/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 - - You only need to install Poetry once. It will automatically pick up the current - Python version and use it to [create virtualenvs](/docs/managing-environments) accordingly. - -The installer installs the `poetry` tool to Poetry's `bin` directory. -On Unix it is located at `$HOME/.poetry/bin` and on Windows at `%USERPROFILE%\.poetry\bin`. - -This directory will be automatically added to your `$PATH` environment variable, -by appending a statement to your `$HOME/.profile` configuration (or equivalent files). -If you do not feel comfortable with this, please pass the `--no-modify-path` flag to -the installer and manually add the Poetry's `bin` directory to your path. - -Finally, open a new shell and type the following: - -```bash -poetry --version -``` - -If you see something like `Poetry 0.12.0` then you are ready to use Poetry. -If you decide Poetry isn't your thing, you can completely remove it from your system -by running the installer again with the `--uninstall` option or by setting -the `POETRY_UNINSTALL` environment variable before executing the installer. - -```bash -python get-poetry.py --uninstall -POETRY_UNINSTALL=1 python get-poetry.py -``` - -By default, Poetry is installed into the user's platform-specific home directory. If you wish to change this, you may define the `POETRY_HOME` environment variable: - -```bash -POETRY_HOME=/etc/poetry python get-poetry.py -``` - -If you want to install prerelease versions, you can do so by passing `--preview` to `get-poetry.py` -or by using the `POETRY_PREVIEW` environment variable: - -```bash -python get-poetry.py --preview -POETRY_PREVIEW=1 python get-poetry.py -``` - -Similarly, if you want to install a specific version, you can use `--version` or the `POETRY_VERSION` -environment variable: - -```bash -python get-poetry.py --version 0.12.0 -POETRY_VERSION=0.12.0 python get-poetry.py -``` - -!!!note - - Note that the installer does not support Poetry releases < 0.12.0. - -!!!note - - The setup script must be able to find one of following executables in your shell's path environment: - - - `python` (which can be a py3 or py2 interpreter) - - `python3` - - `py.exe -3` (Windows) - - `py.exe -2` (Windows) - -### Alternative installation methods (not recommended) - -!!!note - - Using alternative installation methods will make Poetry always - use the Python version for which it has been installed to create - virtualenvs. - - So, you will need to install Poetry for each Python version you - want to use and switch between them. - -#### Installing with `pip` - -Using `pip` to install Poetry is possible. - -```bash -pip install --user poetry -``` - -!!!warning - - Be aware that it will also install Poetry's dependencies - which might cause conflicts with other packages. - -#### Installing with `pipx` - -Using [`pipx`](https://github.com/cs01/pipx) to install Poetry is also possible. [pipx] is used to install Python CLI applications globally while still isolating them in virtual environments. This allows for clean upgrades and uninstalls. pipx supports Python 3.6 and later. If using an earlier version of Python, consider [pipsi](https://github.com/mitsuhiko/pipsi). - -```bash -pipx install poetry -``` - -```bash -pipx upgrade poetry -``` - -```bash -pipx uninstall poetry -``` - -[Github repository](https://github.com/cs01/pipx). - - -## Updating `poetry` - -Updating Poetry to the latest stable version is as simple as calling the `self update` command. - -```bash -poetry self update -``` - -If you want to install pre-release versions, you can use the `--preview` option. - -```bash -poetry self update --preview -``` - -And finally, if you want to install a specific version, you can pass it as an argument -to `self update`. - -```bash -poetry self update 0.8.0 -``` - -!!!note - - 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 - -`poetry` supports generating completion scripts for Bash, Fish, and Zsh. -See `poetry help completions` for full details, but the gist is as simple as using one of the following: - - -```bash -# Bash -poetry completions bash > /etc/bash_completion.d/poetry.bash-completion - -# Bash (Homebrew) -poetry completions bash > $(brew --prefix)/etc/bash_completion.d/poetry.bash-completion - -# Fish -poetry completions fish > ~/.config/fish/completions/poetry.fish - -# Fish (Homebrew) -poetry completions fish > (brew --prefix)/share/fish/vendor_completions.d/poetry.fish - -# Zsh -poetry completions zsh > ~/.zfunc/_poetry - -# Oh-My-Zsh -mkdir $ZSH_CUSTOM/plugins/poetry -poetry completions zsh > $ZSH_CUSTOM/plugins/poetry/_poetry - -# prezto -poetry completions zsh > ~/.zprezto/modules/completion/external/src/_poetry - -``` - -!!! note - - You may need to restart your shell in order for the changes to take effect. - -For `zsh`, you must then add the following line in your `~/.zshrc` before `compinit`: - -```bash -fpath+=~/.zfunc -``` - -For `oh-my-zsh`, you must then enable poetry in your `~/.zshrc` plugins - -``` -plugins( - poetry - ... - ) -``` diff --git a/docs/docs/faq.md b/docs/faq.md similarity index 78% rename from docs/docs/faq.md rename to docs/faq.md index 4d464d7ebc0..122a6856e6b 100644 --- a/docs/docs/faq.md +++ b/docs/faq.md @@ -1,6 +1,17 @@ +--- +title: "FAQ" +draft: false +type: docs +layout: single + +menu: + docs: + weight: 110 +--- + # FAQ -## Why is the dependency resolution process slow? +### Why is the dependency resolution process slow? While the dependency resolver at the heart of Poetry is highly optimized and should be fast enough for most cases, sometimes, with some specific set of dependencies, @@ -13,18 +24,18 @@ operation, both in bandwidth and time, which is why it seems this is a long proc At the moment there is no way around it. -!!!note - - Once Poetry has cached the releases' information, the dependency resolution process - will be much faster. +{{% note %}} +Once Poetry has cached the releases' information, the dependency resolution process +will be much faster. +{{% /note %}} -## Why are unbound version constraints a bad idea? +### Why are unbound version constraints a bad idea? A version constraint without an upper bound such as `*` or `>=3.4` will allow updates to any future version of the dependency. This includes major versions breaking backward compatibility. Once a release of your package is published, you cannot tweak its dependencies anymore in case a dependency breaks BC -- you have to do a new release but the previous one stays broken. +– you have to do a new release but the previous one stays broken. The only good alternative is to define an upper bound on your constraints, which you can increase in a new release after testing that your package is compatible @@ -33,9 +44,9 @@ with the new major version of your dependency. For example instead of using `>=3.4` you should use `~3.4` which allows all versions `<4.0`. The `^` operator works very well with libraries following [semantic versioning](https://semver.org). -## Is tox supported? +### Is tox supported? -Yes. By using the [isolated builds](https://tox.readthedocs.io/en/latest/config.html#conf-isolated_build) `tox` provides, +**Yes**. By using the [isolated builds](https://tox.readthedocs.io/en/latest/config.html#conf-isolated_build) `tox` provides, you can use it in combination with the PEP 517 compliant build system provided by Poetry. So, in your `pyproject.toml` file, add this section if it does not already exist: @@ -60,7 +71,7 @@ commands = poetry run pytest tests/ ``` -## I don't want Poetry to manage my virtual environments. Can I disable it? +### I don't want Poetry to manage my virtual environments. Can I disable it? While Poetry automatically creates virtual environments to always work isolated from the global Python installation, there are valid reasons why it's not necessary diff --git a/docs/docs/libraries.md b/docs/libraries.md similarity index 78% rename from docs/docs/libraries.md rename to docs/libraries.md index db4822c342d..bd7d195bd6c 100644 --- a/docs/docs/libraries.md +++ b/docs/libraries.md @@ -1,3 +1,15 @@ +--- +title: "Libraries" +draft: false +type: docs +layout: "docs" + +menu: + docs: + weight: 20 +--- + + # Libraries This chapter will tell you how to make your library installable through Poetry. @@ -9,7 +21,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/dependency-specification/#version-constraints). +[version constraints]({{< relref "dependency-specification#version-constraints" >}}). ## Lock file @@ -49,14 +61,14 @@ poetry publish ``` This will package and publish the library to PyPI, at the condition that you are a registered user -and you have [configured your credentials](/docs/repositories/#adding-credentials) properly. - -!!!note +and you have [configured your credentials]({{< relref "repositories#configuring-credentials" >}}) properly. - The `publish` command does not execute `build` by default. +{{% note %}} +The `publish` command does not execute `build` by default. - If you want to build and publish your packages together, - just pass the `--build` option. +If you want to build and publish your packages together, +just pass the `--build` option. +{{% /note %}} Once this is done, your library will be available to anyone. @@ -68,7 +80,7 @@ Sometimes, you may want to keep your library private but also being accessible t In this case, you will need to use a private repository. In order to publish to a private repository, you will need to add it to your -global list of repositories. See [Adding a repository](/docs/repositories/#adding-a-repository) +global list of repositories. See [Adding a repository]({{< relref "repositories#adding-a-repository" >}}) for more information. Once this is done, you can actually publish to it like so: diff --git a/docs/docs/managing-environments.md b/docs/managing-environments.md similarity index 86% rename from docs/docs/managing-environments.md rename to docs/managing-environments.md index 8b626cd813a..d9b728825a1 100644 --- a/docs/docs/managing-environments.md +++ b/docs/managing-environments.md @@ -1,3 +1,14 @@ +--- +title: "Managing environments" +draft: false +type: docs +layout: "docs" + +menu: + docs: + weight: 60 +--- + # Managing environments Poetry makes project environment isolation one of its core features. @@ -15,19 +26,19 @@ 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. +{{% 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: +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 - ``` +```bash +pyenv install 2.7.15 +pyenv local 2.7.15 # Activate Python 2.7 for the current project +poetry install +``` +{{% /note %}} ## Switching between environments diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml deleted file mode 100644 index c35ef9579b4..00000000000 --- a/docs/mkdocs.yml +++ /dev/null @@ -1,30 +0,0 @@ -site_name: Poetry documentation - -theme: - name: null - custom_dir: theme - -extra: - version: 2.0 - -nav: - - Introduction: index.md - - Basic Usage: basic-usage.md - - Libraries: libraries.md - - Commands: cli.md - - Configuration: configuration.md - - Repositories: repositories.md - - Managing environments: managing-environments.md - - Dependency specification: dependency-specification.md - - The pyproject.toml file: pyproject.md - - Contributing: contributing.md - - FAQ: faq.md - -markdown_extensions: - - codehilite - - admonition - - pymdownx.superfences - - toc: - permalink:  - - markdown_include.include: - base_path: docs diff --git a/docs/plugins.md b/docs/plugins.md new file mode 100644 index 00000000000..96ca48e191b --- /dev/null +++ b/docs/plugins.md @@ -0,0 +1,249 @@ +--- +title: "Plugins" +draft: false +type: docs +layout: single + +menu: + docs: + weight: 80 +--- + +# Plugins + +Poetry supports using and building plugins if you wish to +alter or expand Poetry's functionality with your own. + +For example if your environment poses special requirements +on the behaviour of Poetry which do not apply to the majority of its users +or if you wish to accomplish something with Poetry in a way that is not desired by most users. + +In these cases you could consider creating a plugin to handle your specific logic. + + +## Creating a plugin + +A plugin is a regular Python package which ships its code as part of the package +and may also depend on further packages. + +### Plugin package + +The plugin package must depend on Poetry +and declare a proper [plugin]({{< relref "pyproject#plugins" >}}) in the `pyproject.toml` file. + +```toml +[tool.poetry] +name = "my-poetry-plugin" +version = "1.0.0" + +# ... +[tool.poetry.dependencies] +python = "~2.7 || ^3.7" +poetry = "^1.0" + +[tool.poetry.plugins."poetry.plugin"] +demo = "poetry_demo_plugin.plugin:MyPlugin" +``` + +### Generic plugins + +Every plugin has to supply a class which implements the `poetry.plugins.Plugin` interface. + +The `activate()` method of the plugin is called after the plugin is loaded +and receives an instance of `Poetry` as well as an instance of `cleo.io.IO`. + +Using these two objects all configuration can be read +and all public internal objects and state can be manipulated as desired. + +Example: + +```python +from cleo.io.io import IO + +from poetry.plugins.plugin import Plugin +from poetry.poetry import Poetry + + +class MyPlugin(Plugin): + + def activate(self, poetry: Poetry, io: IO): + version = self.get_custom_version() + io.write_line(f"Setting package version to {version}") + poetry.package.set_version(version) + + def get_custom_version(self) -> str: + ... +``` + +### Application plugins + +If you want to add commands or options to the `poetry` script you need +to create an application plugin which implements the `poetry.plugins.ApplicationPlugin` interface. + +The `activate()` method of the application plugin is called after the plugin is loaded +and receives an instance of `console.Application`. + +```python +from cleo.commands.command import Command +from poetry.plugins.application_plugin import ApplicationPlugin + + +class CustomCommand(Command): + + name = "my-command" + + def handle(self) -> int: + self.line("My command") + + return 0 + + +def factory(): + return CustomCommand() + + +class MyApplicationPlugin(ApplicationPlugin): + def activate(self, application): + application.command_loader.register_factory("my-command", factory) +``` + +{{% note %}} +It's possible to do the following to register the command: + +```python +application.add(MyCommand()) +``` + +However, it is **strongly** recommended to register a new factory +in the command loader to defer the loading of the command when it's actually +called. + +This will help keep the performances of Poetry good. +{{% /note %}} + +The plugin also must be declared in the `pyproject.toml` file of the plugin package +as an `application.plugin` plugin: + +```toml +[tool.poetry.plugins."poetry.application.plugin"] +foo-command = "poetry_demo_plugin.plugin:MyApplicationPlugin" +``` + +{{% warning %}} +A plugin **must not** remove or modify in any way the core commands of Poetry. +{{% /warning %}} + + +### Event handler + +Plugins can also listen to specific events and act on them if necessary. + +These events are fired by [Cleo](https://github.com/sdispater/cleo) +and are accessible from the `cleo.events.console_events` module. + +- `COMMAND`: this event allows attaching listeners before any command is executed. +- `SIGNAL`: this event allows some actions to be performed after the command execution is interrupted. +- `TERMINATE`: this event allows listeners to be attached after the command. +- `ERROR`: this event occurs when an uncaught exception is raised. + +Let's see how to implement an application event handler. For this example +we will see how to load environment variables from a `.env` file before executing +a command. + + +```python +from cleo.events.console_events import COMMAND +from cleo.events.console_command_event import ConsoleCommandEvent +from cleo.events.event_dispatcher import EventDispatcher +from dotenv import load_dotenv +from poetry.console.application import Application +from poetry.console.commands.env_command import EnvCommand +from poetry.plugins.application_plugin import ApplicationPlugin + + +class MyApplicationPlugin(ApplicationPlugin): + def activate(self, application: Application): + application.event_dispatcher.add_listener( + COMMAND, self.load_dotenv + ) + + def load_dotenv( + self, + event: ConsoleCommandEvent, + event_name: str, + dispatcher: EventDispatcher + ) -> None: + command = event.command + if not isinstance(command, EnvCommand): + return + + io = event.io + + if io.is_debug(): + io.write_line( + "Loading environment variables." + ) + + load_dotenv() +``` + + +## Using plugins + +Installed plugin packages are automatically loaded when Poetry starts up. + +You have multiple ways to install plugins for Poetry + +### The `plugin add` command + +This is the easiest way and should account for all the ways Poetry can be installed. + +```bash +poetry plugin add poetry-plugin +``` + +The `plugin add` command will ensure that the plugin is compatible with the current version of Poetry +and install the needed packages for the plugin to work. + +The package specification formats supported by the `plugin add` command are the same as the ones supported +by the [`add` command]({{< relref "cli#add" >}}). + +If you no longer need a plugin and want to uninstall it, you can use the `plugin remove` command. + +```shell +poetry plugin remove poetry-plugin +``` + +You can also list all currently installed plugins by running: + +```shell +poetry plugin show +``` + +### With `pipx inject` + +If you used `pipx` to install Poetry you can add the plugin packages via the `pipx inject` command. + +```shell +pipx inject poetry poetry-plugin +``` + +If you want to uninstall a plugin, you can run: + +```shell +pipx runpip poetry uninstall poetry-plugin +``` + +### With `pip` + +If you used `pip` to install Poetry you can add the plugin packages via the `pip install` command. + +```shell +pip install --user poetry-plugin +``` + +If you want to uninstall a plugin, you can run: + +```shell +pip uninstall poetry-plugin +``` diff --git a/docs/docs/pyproject.md b/docs/pyproject.md similarity index 68% rename from docs/docs/pyproject.md rename to docs/pyproject.md index 14221b8b03a..e102bffc104 100644 --- a/docs/docs/pyproject.md +++ b/docs/pyproject.md @@ -1,3 +1,14 @@ +--- +title: "The pyproject.toml file" +draft: false +type: docs +layout: single + +menu: + docs: + weight: 90 +--- + # The `pyproject.toml` file The `tool.poetry` section of the `pyproject.toml` file is composed of multiple sections. @@ -40,9 +51,9 @@ The recommended notation for the most common licenses is (alphabetical): Optional, but it is highly recommended to supply this. More identifiers are listed at the [SPDX Open Source License Registry](https://spdx.org/licenses/). -!!!note - - If your project is proprietary and does not use a specific licence, you can set this value as `Proprietary`. +{{% note %}} +If your project is proprietary and does not use a specific licence, you can set this value as `Proprietary`. +{{% /note %}} ## authors @@ -91,11 +102,11 @@ classifiers = [ ] ``` -!!!note +{{% note %}} +Note that Python classifiers are still automatically added for you and are determined by your `python` requirement. - Note that Python classifiers are still automatically added for you and are determined by your `python` requirement. - - The `license` property will also set the License classifier automatically. +The `license` property will also set the License classifier automatically. +{{% /note %}} ## packages @@ -137,26 +148,26 @@ packages = [ From now on, only the `sdist` build archive will include the `my_other_package` package. -!!!note - - Using `packages` disables the package auto-detection feature meaning you have to - **explicitly** specify the "default" package. +{{% note %}} +Using `packages` disables the package auto-detection feature meaning you have to +**explicitly** specify the "default" package. - For instance, if you have a package named `my_package` and you want to also include - another package named `extra_package`, you will need to specify `my_package` explicitly: +For instance, if you have a package named `my_package` and you want to also include +another package named `extra_package`, you will need to specify `my_package` explicitly: - ```toml - packages = [ - { include = "my_package" }, - { include = "extra_package" }, - ] - ``` - -!!!note +```toml +packages = [ + { include = "my_package" }, + { include = "extra_package" }, +] +``` +{{% /note %}} - Poetry is clever enough to detect Python subpackages. +{{% note %}} +Poetry is clever enough to detect Python subpackages. - Thus, you only have to specify the directory where your root package resides. +Thus, you only have to specify the directory where your root package resides. +{{% /note %}} ## include and exclude @@ -173,6 +184,19 @@ If a VCS is being used for a package, the exclude field will be seeded with the include = ["CHANGELOG.md"] ``` +You can also specify the formats for which these patterns have to be included, as shown here: + +```toml +[tool.poetry] +# ... +include = [ + { path = "tests", format = "sdist" }, + { path = "for_wheel.txt", format = ["sdist", "wheel"] } +] +``` + +If no format is specified, it will default to include both `sdist` and `wheel`. + ```toml exclude = ["my_package/excluded.py"] ``` @@ -195,15 +219,15 @@ name = 'private' url = 'http://example.com/simple' ``` -!!!note - - Be aware that declaring the python version for which your package - is compatible is mandatory: +{{% note %}} +Be aware that declaring the python version for which your package +is compatible is mandatory: - ```toml - [tool.poetry.dependencies] - python = "^3.6" - ``` +```toml +[tool.poetry.dependencies] +python = "^3.6" +``` +{{% /note %}} ## `scripts` @@ -216,9 +240,9 @@ poetry = 'poetry.console:run' Here, we will have the `poetry` script installed which will execute `console.run` in the `poetry` package. -!!!note - - When a script is added or updated, run `poetry install` to make them available in the project's virtualenv. +{{% note %}} +When a script is added or updated, run `poetry install` to make them available in the project's virtualenv. +{{% /note %}} ## `extras` @@ -243,15 +267,33 @@ mysqlclient = { version = "^1.3", optional = true } [tool.poetry.extras] mysql = ["mysqlclient"] pgsql = ["psycopg2"] +databases = ["mysqlclient", "psycopg2"] ``` -When installing packages, you can specify extras by using the `-E|--extras` option: +When installing packages with Poetry, you can specify extras by using the `-E|--extras` option: ```bash poetry install --extras "mysql pgsql" poetry install -E mysql -E pgsql ``` +When installing or specifying Poetry-built packages, the extras defined in this section can be activated +as described in [PEP 508](https://www.python.org/dev/peps/pep-0508/#extras). + +For example, when installing the package using `pip`, the dependencies required by +the `databases` extra can be installed as shown below. + +```bash +pip install awesome[databases] +``` + +{{% note %}} +The dependencies specified for each `extra` must already be defined as project dependencies. + +Dependencies listed in the `dev-dependencies` section cannot be specified as extras. +{{% /note %}} + + ## `plugins` Poetry supports arbitrary plugins which work similarly to @@ -275,7 +317,7 @@ any custom url in the `urls` section. "Bug Tracker" = "https://github.com/python-poetry/poetry/issues" ``` -If you publish you package on PyPI, they will appear in the `Project Links` section. +If you publish your package on PyPI, they will appear in the `Project Links` section. ## Poetry and PEP-517 @@ -288,16 +330,15 @@ it in the `build-system` section of the `pyproject.toml` file like so: ```toml [build-system] -requires = ["poetry_core>=1.0.0"] +requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ``` -!!!note - - When using the `new` or `init` command this section will be automatically added. - - -!!!note +{{% note %}} +When using the `new` or `init` command this section will be automatically added. +{{% /note %}} - If your `pyproject.toml` file still references `poetry` directly as a build backend, - you should update it to reference `poetry_core` instead. +{{% note %}} +If your `pyproject.toml` file still references `poetry` directly as a build backend, +you should update it to reference `poetry-core` instead. +{{% /note %}} diff --git a/docs/docs/repositories.md b/docs/repositories.md similarity index 76% rename from docs/docs/repositories.md rename to docs/repositories.md index d281f6ca77a..5b253e023c0 100644 --- a/docs/docs/repositories.md +++ b/docs/repositories.md @@ -1,3 +1,14 @@ +--- +title: "Repositories" +draft: false +type: docs +layout: "docs" + +menu: + docs: + weight: 50 +--- + # Repositories ## Using the PyPI repository @@ -37,24 +48,25 @@ poetry config http-basic.foo username password If you do not specify the password you will be prompted to write it. -!!!note +{{% note %}} +To publish to PyPI, you can set your credentials for the repository named `pypi`. - To publish to PyPI, you can set your credentials for the repository named `pypi`. +Note that it is recommended to use [API tokens](https://pypi.org/help/#apitoken) +when uploading packages to PyPI. +Once you have created a new token, you can tell Poetry to use it: - Note that it is recommended to use [API tokens](https://pypi.org/help/#apitoken) - when uploading packages to PyPI. - Once you have created a new token, you can tell Poetry to use it: +```bash +poetry config pypi-token.pypi my-token +``` - ```bash - poetry config pypi-token.pypi my-token - ``` +If you still want to use your username and password, you can do so with the following +call to `config`. - If you still want to use you username and password, you can do so with the following - call to `config`. +```bash +poetry config http-basic.pypi username password +``` +{{% /note %}} - ```bash - poetry config http-basic.pypi username password - ``` You can also specify the username and password when using the `publish` command with the `--username` and `--password` options. @@ -71,7 +83,7 @@ export POETRY_HTTP_BASIC_PYPI_USERNAME=username export POETRY_HTTP_BASIC_PYPI_PASSWORD=password ``` -See [Using environment variables](/docs/configuration/#using-environment-variables) for more information +See [Using environment variables]({{< relref "configuration#using-environment-variables" >}}) for more information on how to configure Poetry with environment variables. #### Custom certificate authority and mutual TLS authentication @@ -79,9 +91,10 @@ Poetry supports repositories that are secured by a custom certificate authority certificate-based client authentication. The following will configure the "foo" repository to validate the repository's certificate using a custom certificate authority and use a client certificate (note that these config variables do not both need to be set): + ```bash - poetry config certificates.foo.cert /path/to/ca.pem - poetry config certificates.foo.client-cert /path/to/client.pem +poetry config certificates.foo.cert /path/to/ca.pem +poetry config certificates.foo.client-cert /path/to/client.pem ``` ### Install dependencies from a private repository @@ -99,19 +112,19 @@ url = "https://foo.bar/simple/" From now on, Poetry will also look for packages in your private repository. -!!!note +{{% note %}} +Any custom repository will have precedence over PyPI. - Any custom repository will have precedence over PyPI. +If you still want PyPI to be your primary source for your packages +you can declare custom repositories as secondary. - If you still want PyPI to be your primary source for your packages - you can declare custom repositories as secondary. - - ```toml - [[tool.poetry.source]] - name = "foo" - url = "https://foo.bar/simple/" - secondary = true - ``` +```toml +[[tool.poetry.source]] +name = "foo" +url = "https://foo.bar/simple/" +secondary = true +``` +{{% /note %}} If your private repository requires HTTP Basic Auth be sure to add the username and password to your `http-basic` configuration using the example above (be sure to use the diff --git a/docs/theme/main.html b/docs/theme/main.html deleted file mode 100644 index 83151151ff6..00000000000 --- a/docs/theme/main.html +++ /dev/null @@ -1,29 +0,0 @@ ---- -layout: documentation -title: {{ page.title|striptags|e }} ---- - -
-
-
-
-
-
-
    - {% set navlevel = 1 %} - - {% for nav_item in nav %} - - {% endfor %} -
-
-
- {{page.content}} -
-
-
-
-
-
diff --git a/docs/theme/nav.html b/docs/theme/nav.html deleted file mode 100644 index e9d2b383844..00000000000 --- a/docs/theme/nav.html +++ /dev/null @@ -1,18 +0,0 @@ -{{ nav_item.title }} - -{%- if nav_item == page or nav_item.children %} - -{%- endif %} diff --git a/docs/theme/toc.html b/docs/theme/toc.html deleted file mode 100644 index 4f3e8e09ab6..00000000000 --- a/docs/theme/toc.html +++ /dev/null @@ -1,10 +0,0 @@ -{% for toc_item in page.toc %} - -{% if toc_item.children %} - -{% endif %} -{% endfor %} diff --git a/get-poetry.py b/get-poetry.py index 0df27239a9b..dc3c235eeaa 100644 --- a/get-poetry.py +++ b/get-poetry.py @@ -159,15 +159,15 @@ def colorize(style, text): def temporary_directory(*args, **kwargs): try: from tempfile import TemporaryDirectory - - with TemporaryDirectory(*args, **kwargs) as name: - yield name except ImportError: name = tempfile.mkdtemp(*args, **kwargs) yield name shutil.rmtree(name) + else: + with TemporaryDirectory(*args, **kwargs) as name: + yield name def string_to_bool(value): @@ -449,6 +449,33 @@ def _compare_versions(x, y): break + def _is_supported(x): + mx = self.VERSION_REGEX.match(x) + vx = tuple(int(p) for p in mx.groups()[:3]) + (mx.group(5),) + return vx < (1, 2, 0) + + if not _is_supported(version): + print( + colorize( + "error", + "Version {version} does not support this installation method. Please specify a version prior to " + "1.2.0a1 explicitly using the '--version' option.\n" + "Please see " + "https://python-poetry.org/blog/announcing-poetry-1-2-0a1.html#deprecation-of-the-get-poetry-py-script " + "for more information.".format(version=version), + ) + ) + return None, None + + print( + colorize( + "warning", + "This installer is deprecated. " + "Poetry versions installed using this script will not be able to use 'self update' command to upgrade to " + "1.2.0a1 or later.", + ) + ) + current_version = None if os.path.exists(POETRY_LIB): with open( @@ -638,7 +665,7 @@ def extract_lib(self, filename): def _which_python(self): """Decides which python executable we'll embed in the launcher script.""" - allowed_executables = ["python", "python3"] + allowed_executables = ["python3", "python"] if WINDOWS: allowed_executables += ["py.exe -3", "py.exe -2"] @@ -824,7 +851,7 @@ def set_windows_path_var(self, value): HWND_BROADCAST, WM_SETTINGCHANGE, 0, - u"Environment", + "Environment", SMTO_ABORTIFHUNG, 5000, ctypes.byref(result), @@ -903,7 +930,7 @@ def get_unix_profiles(self): if "zsh" in SHELL: zdotdir = os.getenv("ZDOTDIR", HOME) - profiles.append(os.path.join(zdotdir, ".zprofile")) + profiles.append(os.path.join(zdotdir, ".zshrc")) bash_profile = os.path.join(HOME, ".bash_profile") if os.path.exists(bash_profile): diff --git a/install-poetry.py b/install-poetry.py new file mode 100644 index 00000000000..e11bed1f1ba --- /dev/null +++ b/install-poetry.py @@ -0,0 +1,831 @@ +""" +This script will install Poetry and its dependencies. + +It does, in order: + + - Downloads the virtualenv package to a temporary directory and add it to sys.path. + - Creates a virtual environment in the correct OS data dir which will be + - `%APPDATA%\\pypoetry` on Windows + - ~/Library/Application Support/pypoetry on MacOS + - `${XDG_DATA_HOME}/pypoetry` (or `~/.local/share/pypoetry` if it's not set) on UNIX systems + - In `${POETRY_HOME}` if it's set. + - Installs the latest or given version of Poetry inside this virtual environment. + - Installs a `poetry` script in the Python user directory (or `${POETRY_HOME/bin}` if `POETRY_HOME` is set). +""" + +import argparse +import json +import os +import re +import shutil +import site +import subprocess +import sys +import tempfile + +from contextlib import closing +from contextlib import contextmanager +from functools import cmp_to_key +from io import UnsupportedOperation +from pathlib import Path +from typing import Optional +from urllib.request import Request +from urllib.request import urlopen + + +SHELL = os.getenv("SHELL", "") +WINDOWS = sys.platform.startswith("win") or (sys.platform == "cli" and os.name == "nt") +MACOS = sys.platform == "darwin" + +FOREGROUND_COLORS = { + "black": 30, + "red": 31, + "green": 32, + "yellow": 33, + "blue": 34, + "magenta": 35, + "cyan": 36, + "white": 37, +} + +BACKGROUND_COLORS = { + "black": 40, + "red": 41, + "green": 42, + "yellow": 43, + "blue": 44, + "magenta": 45, + "cyan": 46, + "white": 47, +} + +OPTIONS = {"bold": 1, "underscore": 4, "blink": 5, "reverse": 7, "conceal": 8} + + +def style(fg, bg, options): + codes = [] + + if fg: + codes.append(FOREGROUND_COLORS[fg]) + + if bg: + codes.append(BACKGROUND_COLORS[bg]) + + if options: + if not isinstance(options, (list, tuple)): + options = [options] + + for option in options: + codes.append(OPTIONS[option]) + + return "\033[{}m".format(";".join(map(str, codes))) + + +STYLES = { + "info": style("cyan", None, None), + "comment": style("yellow", None, None), + "success": style("green", None, None), + "error": style("red", None, None), + "warning": style("yellow", None, None), + "b": style(None, None, ("bold",)), +} + + +def is_decorated(): + if WINDOWS: + return ( + os.getenv("ANSICON") is not None + or "ON" == os.getenv("ConEmuANSI") + or "xterm" == os.getenv("Term") + ) + + if not hasattr(sys.stdout, "fileno"): + return False + + try: + return os.isatty(sys.stdout.fileno()) + except UnsupportedOperation: + return False + + +def is_interactive(): + if not hasattr(sys.stdin, "fileno"): + return False + + try: + return os.isatty(sys.stdin.fileno()) + except UnsupportedOperation: + return False + + +def colorize(style, text): + if not is_decorated(): + return text + + return "{}{}\033[0m".format(STYLES[style], text) + + +def string_to_bool(value): + value = value.lower() + + return value in {"true", "1", "y", "yes"} + + +def data_dir(version: Optional[str] = None) -> Path: + if os.getenv("POETRY_HOME"): + return Path(os.getenv("POETRY_HOME")).expanduser() + + if WINDOWS: + const = "CSIDL_APPDATA" + path = os.path.normpath(_get_win_folder(const)) + path = os.path.join(path, "pypoetry") + elif MACOS: + path = os.path.expanduser("~/Library/Application Support/pypoetry") + else: + path = os.getenv("XDG_DATA_HOME", os.path.expanduser("~/.local/share")) + path = os.path.join(path, "pypoetry") + + if version: + path = os.path.join(path, version) + + return Path(path) + + +def bin_dir(version: Optional[str] = None) -> Path: + if os.getenv("POETRY_HOME"): + return Path(os.getenv("POETRY_HOME"), "bin").expanduser() + + user_base = site.getuserbase() + + if WINDOWS: + bin_dir = os.path.join(user_base, "Scripts") + else: + bin_dir = os.path.join(user_base, "bin") + + return Path(bin_dir) + + +def _get_win_folder_from_registry(csidl_name): + import winreg as _winreg + + shell_folder_name = { + "CSIDL_APPDATA": "AppData", + "CSIDL_COMMON_APPDATA": "Common AppData", + "CSIDL_LOCAL_APPDATA": "Local AppData", + }[csidl_name] + + key = _winreg.OpenKey( + _winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders", + ) + dir, type = _winreg.QueryValueEx(key, shell_folder_name) + + return dir + + +def _get_win_folder_with_ctypes(csidl_name): + import ctypes + + csidl_const = { + "CSIDL_APPDATA": 26, + "CSIDL_COMMON_APPDATA": 35, + "CSIDL_LOCAL_APPDATA": 28, + }[csidl_name] + + buf = ctypes.create_unicode_buffer(1024) + ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf) + + # Downgrade to short path name if have highbit chars. See + # . + has_high_char = False + for c in buf: + if ord(c) > 255: + has_high_char = True + break + if has_high_char: + buf2 = ctypes.create_unicode_buffer(1024) + if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024): + buf = buf2 + + return buf.value + + +if WINDOWS: + try: + from ctypes import windll # noqa + + _get_win_folder = _get_win_folder_with_ctypes + except ImportError: + _get_win_folder = _get_win_folder_from_registry + + +@contextmanager +def temporary_directory(*args, **kwargs): + try: + from tempfile import TemporaryDirectory + except ImportError: + name = tempfile.mkdtemp(*args, **kwargs) + + yield name + + shutil.rmtree(name) + else: + with TemporaryDirectory(*args, **kwargs) as name: + yield name + + +PRE_MESSAGE = """# Welcome to {poetry}! + +This will download and install the latest version of {poetry}, +a dependency and package manager for Python. + +It will add the `poetry` command to {poetry}'s bin directory, located at: + +{poetry_home_bin} + +You can uninstall at any time by executing this script with the --uninstall option, +and these changes will be reverted. +""" + +POST_MESSAGE = """{poetry} ({version}) is installed now. Great! + +You can test that everything is set up by executing: + +`{test_command}` +""" + +POST_MESSAGE_NOT_IN_PATH = """{poetry} ({version}) is installed now. Great! + +To get started you need {poetry}'s bin directory ({poetry_home_bin}) in your `PATH` +environment variable. +{configure_message} +Alternatively, you can call {poetry} explicitly with `{poetry_executable}`. + +You can test that everything is set up by executing: + +`{test_command}` +""" + +POST_MESSAGE_CONFIGURE_UNIX = """ +Add `export PATH="{poetry_home_bin}:$PATH"` to your shell configuration file. +""" + +POST_MESSAGE_CONFIGURE_FISH = """ +You can execute `set -U fish_user_paths {poetry_home_bin} $fish_user_paths` +""" + +POST_MESSAGE_CONFIGURE_WINDOWS = """""" + + +class Cursor: + def __init__(self) -> None: + self._output = sys.stdout + + def move_up(self, lines: int = 1) -> "Cursor": + self._output.write("\x1b[{}A".format(lines)) + + return self + + def move_down(self, lines: int = 1) -> "Cursor": + self._output.write("\x1b[{}B".format(lines)) + + return self + + def move_right(self, columns: int = 1) -> "Cursor": + self._output.write("\x1b[{}C".format(columns)) + + return self + + def move_left(self, columns: int = 1) -> "Cursor": + self._output.write("\x1b[{}D".format(columns)) + + return self + + def move_to_column(self, column: int) -> "Cursor": + self._output.write("\x1b[{}G".format(column)) + + return self + + def move_to_position(self, column: int, row: int) -> "Cursor": + self._output.write("\x1b[{};{}H".format(row + 1, column)) + + return self + + def save_position(self) -> "Cursor": + self._output.write("\x1b7") + + return self + + def restore_position(self) -> "Cursor": + self._output.write("\x1b8") + + return self + + def hide(self) -> "Cursor": + self._output.write("\x1b[?25l") + + return self + + def show(self) -> "Cursor": + self._output.write("\x1b[?25h\x1b[?0c") + + return self + + def clear_line(self) -> "Cursor": + """ + Clears all the output from the current line. + """ + self._output.write("\x1b[2K") + + return self + + def clear_line_after(self) -> "Cursor": + """ + Clears all the output from the current line after the current position. + """ + self._output.write("\x1b[K") + + return self + + def clear_output(self) -> "Cursor": + """ + Clears all the output from the cursors' current position + to the end of the screen. + """ + self._output.write("\x1b[0J") + + return self + + def clear_screen(self) -> "Cursor": + """ + Clears the entire screen. + """ + self._output.write("\x1b[2J") + + return self + + +class Installer: + METADATA_URL = "https://pypi.org/pypi/poetry/json" + VERSION_REGEX = re.compile( + r"v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?" + "(" + "[._-]?" + r"(?:(stable|beta|b|rc|RC|alpha|a|patch|pl|p)((?:[.-]?\d+)*)?)?" + "([.-]?dev)?" + ")?" + r"(?:\+[^\s]+)?" + ) + + def __init__( + self, + version: Optional[str] = None, + preview: bool = False, + force: bool = False, + accept_all: bool = False, + git: Optional[str] = None, + path: Optional[str] = None, + ) -> None: + self._version = version + self._preview = preview + self._force = force + self._accept_all = accept_all + self._git = git + self._path = path + self._data_dir = data_dir() + self._bin_dir = bin_dir() + self._cursor = Cursor() + + def allows_prereleases(self) -> bool: + return self._preview + + def run(self) -> int: + if self._git: + version = self._git + elif self._path: + version = self._path + else: + version, current_version = self.get_version() + + if version is None: + return 0 + + self.display_pre_message() + self.ensure_directories() + + def _is_self_upgrade_supported(x): + mx = self.VERSION_REGEX.match(x) + + if mx is None: + # the version is not semver, perhaps scm or file, we assume upgrade is supported + return True + + vx = tuple(int(p) for p in mx.groups()[:3]) + (mx.group(5),) + return vx >= (1, 2, 0) + + if version and not _is_self_upgrade_supported(version): + self._write( + colorize( + "warning", + f"You are installing {version}. When using the current installer, this version does not support " + f"updating using the 'self update' command. Please use 1.2.0a1 or later.", + ) + ) + if not self._accept_all: + continue_install = input("Do you want to continue? ([y]/n) ") or "y" + if continue_install.lower() in {"n", "no"}: + return 0 + + try: + self.install(version) + except subprocess.CalledProcessError as e: + print(colorize("error", "An error has occured: {}".format(str(e)))) + print(e.output.decode()) + + return e.returncode + + self._write("") + self.display_post_message(version) + + return 0 + + def install(self, version, upgrade=False): + """ + Installs Poetry in $POETRY_HOME. + """ + self._write( + "Installing {} ({})".format( + colorize("info", "Poetry"), colorize("info", version) + ) + ) + + env_path = self.make_env(version) + self.install_poetry(version, env_path) + self.make_bin(version) + + self._overwrite( + "Installing {} ({}): {}".format( + colorize("info", "Poetry"), + colorize("b", version), + colorize("success", "Done"), + ) + ) + + self._data_dir.joinpath("VERSION").write_text(version) + + return 0 + + def uninstall(self) -> int: + if not self._data_dir.exists(): + self._write( + "{} is not currently installed.".format(colorize("info", "Poetry")) + ) + + return 1 + + version = None + if self._data_dir.joinpath("VERSION").exists(): + version = self._data_dir.joinpath("VERSION").read_text().strip() + + if version: + self._write( + "Removing {} ({})".format( + colorize("info", "Poetry"), colorize("b", version) + ) + ) + else: + self._write("Removing {}".format(colorize("info", "Poetry"))) + + shutil.rmtree(str(self._data_dir)) + for script in ["poetry", "poetry.bat"]: + if self._bin_dir.joinpath(script).exists(): + self._bin_dir.joinpath(script).unlink() + + return 0 + + def make_env(self, version: str) -> Path: + self._overwrite( + "Installing {} ({}): {}".format( + colorize("info", "Poetry"), + colorize("b", version), + colorize("comment", "Creating environment"), + ) + ) + + env_path = self._data_dir.joinpath("venv") + + with temporary_directory() as tmp_dir: + subprocess.call( + [sys.executable, "-m", "pip", "install", "virtualenv", "-t", tmp_dir], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + + sys.path.insert(0, tmp_dir) + + import virtualenv + + virtualenv.cli_run([str(env_path), "--clear"]) + + return env_path + + def make_bin(self, version: str) -> None: + self._overwrite( + "Installing {} ({}): {}".format( + colorize("info", "Poetry"), + colorize("b", version), + colorize("comment", "Creating script"), + ) + ) + + self._bin_dir.mkdir(parents=True, exist_ok=True) + + script = "poetry" + target_script = "venv/bin/poetry" + if WINDOWS: + script = "poetry.exe" + target_script = "venv/Scripts/poetry.exe" + + if self._bin_dir.joinpath(script).exists(): + self._bin_dir.joinpath(script).unlink() + + try: + self._bin_dir.joinpath(script).symlink_to( + self._data_dir.joinpath(target_script) + ) + except OSError: + # This can happen if the user + # does not have the correct permission on Windows + shutil.copy( + self._data_dir.joinpath(target_script), self._bin_dir.joinpath(script) + ) + + def install_poetry(self, version: str, env_path: Path) -> None: + self._overwrite( + "Installing {} ({}): {}".format( + colorize("info", "Poetry"), + colorize("b", version), + colorize("comment", "Installing Poetry"), + ) + ) + + if WINDOWS: + python = env_path.joinpath("Scripts/python.exe") + else: + python = env_path.joinpath("bin/python") + + if self._git: + specification = "git+" + version + elif self._path: + specification = version + else: + specification = f"poetry=={version}" + + subprocess.run( + [str(python), "-m", "pip", "install", specification], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + + def display_pre_message(self) -> None: + kwargs = { + "poetry": colorize("info", "Poetry"), + "poetry_home_bin": colorize("comment", self._bin_dir), + } + self._write(PRE_MESSAGE.format(**kwargs)) + + def display_post_message(self, version: str) -> None: + if WINDOWS: + return self.display_post_message_windows(version) + + if SHELL == "fish": + return self.display_post_message_fish(version) + + return self.display_post_message_unix(version) + + def display_post_message_windows(self, version: str) -> None: + path = self.get_windows_path_var() + + message = POST_MESSAGE_NOT_IN_PATH + if path and str(self._bin_dir) in path: + message = POST_MESSAGE + + self._write( + message.format( + poetry=colorize("info", "Poetry"), + version=colorize("b", version), + poetry_home_bin=colorize("comment", self._bin_dir), + poetry_executable=colorize("b", self._bin_dir.joinpath("poetry")), + configure_message=POST_MESSAGE_CONFIGURE_WINDOWS.format( + poetry_home_bin=colorize("comment", self._bin_dir) + ), + test_command=colorize("b", "poetry --version"), + ) + ) + + def get_windows_path_var(self) -> Optional[str]: + import winreg + + with winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) as root: + with winreg.OpenKey(root, "Environment", 0, winreg.KEY_ALL_ACCESS) as key: + path, _ = winreg.QueryValueEx(key, "PATH") + + return path + + def display_post_message_fish(self, version: str) -> None: + fish_user_paths = subprocess.check_output( + ["fish", "-c", "echo $fish_user_paths"] + ).decode("utf-8") + + message = POST_MESSAGE_NOT_IN_PATH + if fish_user_paths and str(self._bin_dir) in fish_user_paths: + message = POST_MESSAGE + + self._write( + message.format( + poetry=colorize("info", "Poetry"), + version=colorize("b", version), + poetry_home_bin=colorize("comment", self._bin_dir), + poetry_executable=colorize("b", self._bin_dir.joinpath("poetry")), + configure_message=POST_MESSAGE_CONFIGURE_FISH.format( + poetry_home_bin=colorize("comment", self._bin_dir) + ), + test_command=colorize("b", "poetry --version"), + ) + ) + + def display_post_message_unix(self, version: str) -> None: + paths = os.getenv("PATH", "").split(":") + + message = POST_MESSAGE_NOT_IN_PATH + if paths and str(self._bin_dir) in paths: + message = POST_MESSAGE + + self._write( + message.format( + poetry=colorize("info", "Poetry"), + version=colorize("b", version), + poetry_home_bin=colorize("comment", self._bin_dir), + poetry_executable=colorize("b", self._bin_dir.joinpath("poetry")), + configure_message=POST_MESSAGE_CONFIGURE_UNIX.format( + poetry_home_bin=colorize("comment", self._bin_dir) + ), + test_command=colorize("b", "poetry --version"), + ) + ) + + def ensure_directories(self) -> None: + self._data_dir.mkdir(parents=True, exist_ok=True) + self._bin_dir.mkdir(parents=True, exist_ok=True) + + def get_version(self): + current_version = None + if self._data_dir.joinpath("VERSION").exists(): + current_version = self._data_dir.joinpath("VERSION").read_text().strip() + + self._write(colorize("info", "Retrieving Poetry metadata")) + + metadata = json.loads(self._get(self.METADATA_URL).decode()) + + def _compare_versions(x, y): + mx = self.VERSION_REGEX.match(x) + my = self.VERSION_REGEX.match(y) + + vx = tuple(int(p) for p in mx.groups()[:3]) + (mx.group(5),) + vy = tuple(int(p) for p in my.groups()[:3]) + (my.group(5),) + + if vx < vy: + return -1 + elif vx > vy: + return 1 + + return 0 + + self._write("") + releases = sorted( + metadata["releases"].keys(), key=cmp_to_key(_compare_versions) + ) + + if self._version and self._version not in releases: + self._write( + colorize("error", "Version {} does not exist.".format(self._version)) + ) + + return None, None + + version = self._version + if not version: + for release in reversed(releases): + m = self.VERSION_REGEX.match(release) + if m.group(5) and not self.allows_prereleases(): + continue + + version = release + + break + + if current_version == version and not self._force: + self._write( + "The latest version ({}) is already installed.".format( + colorize("b", version) + ) + ) + + return None, current_version + + return version, current_version + + def _write(self, line) -> None: + sys.stdout.write(line + "\n") + + def _overwrite(self, line) -> None: + if not is_decorated(): + return self._write(line) + + self._cursor.move_up() + self._cursor.clear_line() + self._write(line) + + def _get(self, url): + request = Request(url, headers={"User-Agent": "Python Poetry"}) + + with closing(urlopen(request)) as r: + return r.read() + + +def main(): + parser = argparse.ArgumentParser( + description="Installs the latest (or given) version of poetry" + ) + parser.add_argument( + "-p", + "--preview", + help="install preview version", + dest="preview", + action="store_true", + default=False, + ) + parser.add_argument("--version", help="install named version", dest="version") + parser.add_argument( + "-f", + "--force", + help="install on top of existing version", + dest="force", + action="store_true", + default=False, + ) + parser.add_argument( + "-y", + "--yes", + help="accept all prompts", + dest="accept_all", + action="store_true", + default=False, + ) + parser.add_argument( + "--uninstall", + help="uninstall poetry", + dest="uninstall", + action="store_true", + default=False, + ) + parser.add_argument( + "--path", + dest="path", + action="store", + help=( + "Install from a given path (file or directory) instead of " + "fetching the latest version of Poetry available online." + ), + ) + parser.add_argument( + "--git", + dest="git", + action="store", + help=( + "Install from a git repository instead of fetching the latest version " + "of Poetry available online." + ), + ) + + args = parser.parse_args() + + installer = Installer( + version=args.version or os.getenv("POETRY_VERSION"), + preview=args.preview or string_to_bool(os.getenv("POETRY_PREVIEW", "0")), + force=args.force, + accept_all=args.accept_all + or string_to_bool(os.getenv("POETRY_ACCEPT", "0")) + or not is_interactive(), + path=args.path, + git=args.git, + ) + + if args.uninstall or string_to_bool(os.getenv("POETRY_UNINSTALL", "0")): + return installer.uninstall() + + return installer.run() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/make-nix-release.sh b/make-nix-release.sh index b3828db2c38..4872907e193 100755 --- a/make-nix-release.sh +++ b/make-nix-release.sh @@ -11,7 +11,7 @@ RUNTIMES[4]="${PYTHON38:+-P "3.8:$PYTHON38"}" test -n "$PYTHON" || PYTHON="python3" if [ "$OSTYPE" == "linux-gnu" ]; then - $PYTHON get-poetry.py -y --preview + $PYTHON get-poetry.py -y POETRY="$PYTHON $HOME/.poetry/bin/poetry" RUNTIMES[5]="${PYTHON39:+-P "3.9:$PYTHON39"}" else diff --git a/poetry.lock b/poetry.lock index 973733a6d61..26eb85c6f31 100644 --- a/poetry.lock +++ b/poetry.lock @@ -16,29 +16,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "20.2.0" +version = "21.2.0" description = "Classes Without Boilerplate" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] -docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] - -[[package]] -name = "backports.functools-lru-cache" -version = "1.6.1" -description = "Backport of functools.lru_cache" -category = "dev" -optional = false -python-versions = ">=2.6" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-black-multipy", "pytest-cov"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] [[package]] name = "cachecontrol" @@ -72,7 +60,7 @@ msgpack = ["msgpack-python (>=0.5,<0.6)"] [[package]] name = "certifi" -version = "2020.6.20" +version = "2020.12.5" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -80,7 +68,7 @@ python-versions = "*" [[package]] name = "cffi" -version = "1.14.3" +version = "1.14.5" description = "Foreign Function Interface for Python calling C code." category = "main" optional = false @@ -91,7 +79,7 @@ pycparser = "*" [[package]] name = "cfgv" -version = "3.2.0" +version = "3.3.0" description = "Validate configuration and produce human readable error messages." category = "dev" optional = false @@ -99,75 +87,43 @@ python-versions = ">=3.6.1" [[package]] name = "chardet" -version = "3.0.4" +version = "4.0.0" description = "Universal encoding detector for Python 2 and 3" category = "main" optional = false -python-versions = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "cleo" -version = "0.8.1" +version = "1.0.0a3" description = "Cleo allows you to create beautiful and testable command-line interfaces." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -clikit = ">=0.6.0,<0.7.0" - -[[package]] -name = "clikit" -version = "0.6.2" -description = "CliKit is a group of utilities to build beautiful and testable command line interfaces." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6,<4.0" [package.dependencies] -crashtest = {version = ">=0.3.0,<0.4.0", markers = "python_version >= \"3.6\" and python_version < \"4.0\""} -enum34 = {version = ">=1.1,<2.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\""} -pastel = ">=0.2.0,<0.3.0" -pylev = ">=1.3,<2.0" -typing = {version = ">=3.6,<4.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.4\" and python_version < \"3.5\""} -typing-extensions = {version = ">=3.6,<4.0", markers = "python_version >= \"3.5\" and python_full_version < \"3.5.4\""} +crashtest = ">=0.3.1,<0.4.0" +pylev = ">=1.3.0,<2.0.0" [[package]] name = "colorama" -version = "0.4.3" +version = "0.4.4" description = "Cross-platform colored terminal text." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -[[package]] -name = "configparser" -version = "4.0.2" -description = "Updated configparser from Python 3.7 for Python 2.6+." -category = "main" -optional = false -python-versions = ">=2.6" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2)", "pytest-flake8", "pytest-black-multipy"] - -[[package]] -name = "contextlib2" -version = "0.6.0.post1" -description = "Backports and enhancements for the contextlib module" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - [[package]] name = "coverage" -version = "5.3" +version = "5.5" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +[package.dependencies] +toml = {version = "*", optional = true, markers = "extra == \"toml\""} + [package.extras] toml = ["toml"] @@ -181,88 +137,65 @@ python-versions = ">=3.6,<4.0" [[package]] name = "cryptography" -version = "3.1.1" +version = "3.4.7" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +python-versions = ">=3.6" [package.dependencies] -cffi = ">=1.8,<1.11.3 || >1.11.3" -enum34 = {version = "*", markers = "python_version < \"3\""} -ipaddress = {version = "*", markers = "python_version < \"3\""} -six = ">=1.4.1" +cffi = ">=1.12" [package.extras] -docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0,<3.1.0 || >3.1.0,<3.1.1 || >3.1.1)", "sphinx-rtd-theme"] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=3.6.0,<3.9.0 || >3.9.0,<3.9.1 || >3.9.1,<3.9.2 || >3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,<3.79.2 || >3.79.2)"] +test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] [[package]] -name = "distlib" -version = "0.3.1" -description = "Distribution utilities" +name = "dataclasses" +version = "0.8" +description = "A backport of the dataclasses module for Python 3.6" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6, <3.7" [[package]] -name = "entrypoints" -version = "0.3" -description = "Discover and load entry points from installed packages." -category = "main" +name = "deepdiff" +version = "5.5.0" +description = "Deep Difference and Search of any Python object/data." +category = "dev" optional = false -python-versions = ">=2.7" +python-versions = ">=3.6" [package.dependencies] -configparser = {version = ">=3.5", markers = "python_version == \"2.7\""} +ordered-set = "4.0.2" -[[package]] -name = "enum34" -version = "1.1.10" -description = "Python 3.4 Enum backported to 3.3, 3.2, 3.1, 2.7, 2.6, 2.5, and 2.4" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "filelock" -version = "3.0.12" -description = "A platform independent file lock." -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "funcsigs" -version = "1.0.2" -description = "Python function signatures from PEP362 for Python 2.6, 2.7 and 3.2+" -category = "dev" -optional = false -python-versions = "*" +[package.extras] +cli = ["click (==7.1.2)", "pyyaml (==5.4)", "toml (==0.10.2)", "clevercsv (==0.6.7)"] [[package]] -name = "functools32" -version = "3.2.3-2" -description = "Backport of the functools module from Python 3.2.3 for use on 2.7 and PyPy." +name = "distlib" +version = "0.3.1" +description = "Distribution utilities" category = "main" optional = false python-versions = "*" [[package]] -name = "futures" -version = "3.3.0" -description = "Backport of the concurrent.futures package from Python 3" +name = "entrypoints" +version = "0.3" +description = "Discover and load entry points from installed packages." category = "main" optional = false -python-versions = ">=2.6, <3" +python-versions = ">=2.7" [[package]] -name = "glob2" -version = "0.6" -description = "Version of the glob module that can capture patterns and supports recursive wildcards" +name = "filelock" +version = "3.0.12" +description = "A platform independent file lock." category = "main" optional = false python-versions = "*" @@ -287,25 +220,22 @@ lxml = ["lxml"] [[package]] name = "httpretty" -version = "0.9.7" +version = "1.1.2" description = "HTTP client mock for Python" category = "dev" optional = false -python-versions = "*" - -[package.dependencies] -six = "*" +python-versions = ">=3" [[package]] name = "identify" -version = "1.5.5" +version = "2.2.4" description = "File identification library for Python" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +python-versions = ">=3.6.1" [package.extras] -license = ["editdistance"] +license = ["editdistance-s"] [[package]] name = "idna" @@ -324,9 +254,6 @@ optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] -configparser = {version = ">=3.5", markers = "python_version < \"3\""} -contextlib2 = {version = "*", markers = "python_version < \"3\""} -pathlib2 = {version = "*", markers = "python_version < \"3\""} zipp = ">=0.5" [package.extras] @@ -335,92 +262,55 @@ testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] [[package]] name = "importlib-resources" -version = "3.0.0" +version = "5.1.3" description = "Read resources from Python packages" category = "main" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6" [package.dependencies] -contextlib2 = {version = "*", markers = "python_version < \"3\""} -pathlib2 = {version = "*", markers = "python_version < \"3\""} -singledispatch = {version = "*", markers = "python_version < \"3.4\""} -typing = {version = "*", markers = "python_version < \"3.5\""} zipp = {version = ">=0.4", markers = "python_version < \"3.8\""} [package.extras] -docs = ["sphinx", "rst.linker", "jaraco.packaging"] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"] [[package]] -name = "ipaddress" -version = "1.0.23" -description = "IPv4/IPv6 manipulation library" -category = "main" +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" optional = false python-versions = "*" [[package]] name = "jeepney" -version = "0.4.3" +version = "0.6.0" description = "Low-level, pure Python DBus protocol wrapper." category = "main" optional = false -python-versions = ">=3.5" - -[package.extras] -dev = ["testpath"] - -[[package]] -name = "keyring" -version = "18.0.1" -description = "Store and access your passwords safely." -category = "main" -optional = false -python-versions = ">=2.7" - -[package.dependencies] -entrypoints = "*" -pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""} -secretstorage = {version = "<3", markers = "(sys_platform == \"linux2\" or sys_platform == \"linux\") and python_version < \"3.5\""} - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs", "pytest-flake8"] - -[[package]] -name = "keyring" -version = "20.0.1" -description = "Store and access your passwords safely." -category = "main" -optional = false -python-versions = ">=3.5" - -[package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""} -secretstorage = {version = "*", markers = "sys_platform == \"linux\""} +python-versions = ">=3.6" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-black-multipy", "pytest-cov"] +test = ["pytest", "pytest-trio", "pytest-asyncio", "testpath", "trio"] [[package]] name = "keyring" -version = "21.4.0" +version = "22.3.0" description = "Store and access your passwords safely." category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +importlib-metadata = {version = ">=1", markers = "python_version < \"3.8\""} jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""} -SecretStorage = {version = ">=3", markers = "sys_platform == \"linux\""} +SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} [package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-black (>=0.3.7)", "pytest-cov", "pytest-mypy"] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "pytest-black (>=0.3.7)", "pytest-mypy"] [[package]] name = "lockfile" @@ -430,45 +320,9 @@ category = "main" optional = false python-versions = "*" -[[package]] -name = "mock" -version = "3.0.5" -description = "Rolling backport of unittest.mock for all Pythons" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -funcsigs = {version = ">=1", markers = "python_version < \"3.3\""} -six = "*" - -[package.extras] -build = ["twine", "wheel", "blurb"] -docs = ["sphinx"] -test = ["pytest", "pytest-cov"] - -[[package]] -name = "more-itertools" -version = "5.0.0" -description = "More routines for operating on iterables, beyond itertools" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -six = ">=1.0.0,<2.0.0" - -[[package]] -name = "more-itertools" -version = "8.5.0" -description = "More routines for operating on iterables, beyond itertools" -category = "dev" -optional = false -python-versions = ">=3.5" - [[package]] name = "msgpack" -version = "1.0.0" +version = "1.0.2" description = "MessagePack (de)serializer." category = "main" optional = false @@ -476,43 +330,30 @@ python-versions = "*" [[package]] name = "nodeenv" -version = "1.5.0" +version = "1.6.0" description = "Node.js virtual environment builder" category = "dev" optional = false python-versions = "*" [[package]] -name = "packaging" -version = "20.4" -description = "Core utilities for Python packages" -category = "main" +name = "ordered-set" +version = "4.0.2" +description = "A set that remembers its order, and allows looking up its items by their index in that order." +category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -pyparsing = ">=2.0.2" -six = "*" +python-versions = ">=3.5" [[package]] -name = "pastel" -version = "0.2.1" -description = "Bring colors to your terminal." +name = "packaging" +version = "20.9" +description = "Core utilities for Python packages" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -[[package]] -name = "pathlib2" -version = "2.3.5" -description = "Object-oriented filesystem paths" -category = "main" -optional = false -python-versions = "*" - [package.dependencies] -scandir = {version = "*", markers = "python_version < \"3.5\""} -six = "*" +pyparsing = ">=2.0.2" [[package]] name = "pexpect" @@ -527,7 +368,7 @@ ptyprocess = ">=0.5" [[package]] name = "pkginfo" -version = "1.5.0.1" +version = "1.7.0" description = "Query metadatdata from sdists / bdists / installed packages." category = "main" optional = false @@ -552,22 +393,19 @@ dev = ["pre-commit", "tox"] [[package]] name = "poetry-core" -version = "1.0.0" +version = "1.1.0a5" description = "Poetry PEP 517 Build Backend" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6,<4.0" [package.dependencies] -enum34 = {version = ">=1.1.10,<2.0.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\""} -functools32 = {version = ">=3.2.3-2,<4.0.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\""} -importlib-metadata = {version = ">=1.7.0,<2.0.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.5\" and python_version < \"3.8\""} -pathlib2 = {version = ">=2.3.5,<3.0.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\""} -typing = {version = ">=3.7.4.1,<4.0.0.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\""} +dataclasses = {version = ">=0.8", markers = "python_version >= \"3.6\" and python_version < \"3.7\""} +importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""} [[package]] name = "pre-commit" -version = "2.7.1" +version = "2.12.1" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false @@ -585,7 +423,7 @@ virtualenv = ">=20.0.8" [[package]] name = "ptyprocess" -version = "0.6.0" +version = "0.7.0" description = "Run a subprocess in a pseudo terminal" category = "main" optional = false @@ -593,7 +431,7 @@ python-versions = "*" [[package]] name = "py" -version = "1.9.0" +version = "1.10.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" category = "dev" optional = false @@ -625,85 +463,54 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "pytest" -version = "4.6.11" -description = "pytest: simple powerful testing with Python" -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" - -[package.dependencies] -atomicwrites = ">=1.0" -attrs = ">=17.4.0" -colorama = {version = "*", markers = "sys_platform == \"win32\" and python_version != \"3.4\""} -funcsigs = {version = ">=1.0", markers = "python_version < \"3.0\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -more-itertools = [ - {version = ">=4.0.0,<6.0.0", markers = "python_version <= \"2.7\""}, - {version = ">=4.0.0", markers = "python_version > \"2.7\""}, -] -packaging = "*" -pathlib2 = {version = ">=2.2.0", markers = "python_version < \"3.6\""} -pluggy = ">=0.12,<1.0" -py = ">=1.5.0" -six = ">=1.10.0" -wcwidth = "*" - -[package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "nose", "requests", "mock"] - -[[package]] -name = "pytest" -version = "5.4.3" +version = "6.2.4" description = "pytest: simple powerful testing with Python" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=17.4.0" +attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -more-itertools = ">=4.0.0" +iniconfig = "*" packaging = "*" -pathlib2 = {version = ">=2.2.0", markers = "python_version < \"3.6\""} -pluggy = ">=0.12,<1.0" -py = ">=1.5.0" -wcwidth = "*" +pluggy = ">=0.12,<1.0.0a1" +py = ">=1.8.2" +toml = "*" [package.extras] -checkqa-mypy = ["mypy (v0.761)"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] name = "pytest-cov" -version = "2.10.1" +version = "2.12.0" description = "Pytest plugin for measuring coverage." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] -coverage = ">=4.4" +coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"] +testing = ["fields", "hunter", "process-tests (==2.0.2)", "six", "pytest-xdist", "virtualenv"] [[package]] name = "pytest-mock" -version = "1.13.0" -description = "Thin-wrapper around the mock package for easier use with py.test" +version = "3.6.1" +description = "Thin-wrapper around the mock package for easier use with pytest" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.dependencies] -mock = {version = "*", markers = "python_version < \"3.0\""} -pytest = ">=2.7" +pytest = ">=5.0" [package.extras] -dev = ["pre-commit", "tox"] +dev = ["pre-commit", "tox", "pytest-asyncio"] [[package]] name = "pytest-sugar" @@ -728,15 +535,15 @@ python-versions = "*" [[package]] name = "pyyaml" -version = "5.3.1" +version = "5.4.1" description = "YAML parser and emitter for Python" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "requests" -version = "2.24.0" +version = "2.25.1" description = "Python HTTP for Humans." category = "main" optional = false @@ -744,13 +551,13 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] certifi = ">=2017.4.17" -chardet = ">=3.0.2,<4" +chardet = ">=3.0.2,<5" idna = ">=2.5,<3" -urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" +urllib3 = ">=1.21.1,<1.27" [package.extras] security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] -socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] [[package]] name = "requests-toolbelt" @@ -763,75 +570,34 @@ python-versions = "*" [package.dependencies] requests = ">=2.0.1,<3.0.0" -[[package]] -name = "scandir" -version = "1.10.0" -description = "scandir, a better directory iterator and faster os.walk()" -category = "main" -optional = false -python-versions = "*" - [[package]] name = "secretstorage" -version = "2.3.1" +version = "3.3.1" description = "Python bindings to FreeDesktop.org Secret Service API" category = "main" optional = false -python-versions = "*" - -[package.dependencies] -cryptography = "*" - -[package.extras] -dbus-python = ["dbus-python"] - -[[package]] -name = "secretstorage" -version = "3.1.2" -description = "Python bindings to FreeDesktop.org Secret Service API" -category = "main" -optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] -cryptography = "*" -jeepney = ">=0.4.2" +cryptography = ">=2.0" +jeepney = ">=0.6" [[package]] name = "shellingham" -version = "1.3.2" +version = "1.4.0" description = "Tool to Detect Surrounding Shell" category = "main" optional = false python-versions = "!=3.0,!=3.1,!=3.2,!=3.3,>=2.6" -[[package]] -name = "singledispatch" -version = "3.4.0.3" -description = "This library brings functools.singledispatch from Python 3.4 to Python 2.6-3.3." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -six = "*" - [[package]] name = "six" -version = "1.15.0" +version = "1.16.0" description = "Python 2 and 3 compatibility utilities" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -[[package]] -name = "subprocess32" -version = "3.5.4" -description = "A backport of the subprocess module from Python 3 for use on 2.x." -category = "main" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" - [[package]] name = "termcolor" version = "1.1.0" @@ -842,28 +608,23 @@ python-versions = "*" [[package]] name = "toml" -version = "0.10.1" +version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" category = "dev" optional = false -python-versions = "*" +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tomlkit" -version = "0.7.0" +version = "0.7.2" description = "Style preserving TOML library" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -[package.dependencies] -enum34 = {version = ">=1.1,<2.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\""} -functools32 = {version = ">=3.2.3,<4.0.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\""} -typing = {version = ">=3.6,<4.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.4\" and python_version < \"3.5\""} - [[package]] name = "tox" -version = "3.20.0" +version = "3.23.1" description = "tox is a generic virtualenv management and test command line tool" category = "dev" optional = false @@ -872,7 +633,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} filelock = ">=3.0.0" -importlib-metadata = {version = ">=0.12,<2", markers = "python_version < \"3.8\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} packaging = ">=14" pluggy = ">=0.12.0" py = ">=1.4.17" @@ -882,43 +643,24 @@ virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2, [package.extras] docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] -testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "pytest-xdist (>=1.22.2)"] - -[[package]] -name = "typing" -version = "3.7.4.3" -description = "Type Hints for Python" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "typing-extensions" -version = "3.7.4.3" -description = "Backported and Experimental Type Hints for Python 3.5+" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -typing = {version = ">=3.7.4", markers = "python_version < \"3.5\""} +testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "pytest-xdist (>=1.22.2)", "pathlib2 (>=2.3.3)"] [[package]] name = "urllib3" -version = "1.25.10" +version = "1.26.4" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] brotli = ["brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] -socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.0.31" +version = "20.4.4" description = "Virtual Python Environment builder" category = "main" optional = false @@ -928,25 +670,13 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" appdirs = ">=1.4.3,<2" distlib = ">=0.3.1,<1" filelock = ">=3.0.0,<4" -importlib-metadata = {version = ">=0.12,<2", markers = "python_version < \"3.8\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""} -pathlib2 = {version = ">=2.3.3,<3", markers = "python_version < \"3.4\" and sys_platform != \"win32\""} six = ">=1.9.0,<2" [package.extras] docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] -testing = ["coverage (>=5)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "pytest-xdist (>=1.31.0)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] - -[[package]] -name = "wcwidth" -version = "0.2.5" -description = "Measures the displayed width of unicode strings in a terminal" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -"backports.functools-lru-cache" = {version = ">=1.2.1", markers = "python_version < \"3.2\""} +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] [[package]] name = "webencodings" @@ -958,23 +688,20 @@ python-versions = "*" [[package]] name = "zipp" -version = "1.2.0" +version = "3.4.1" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false -python-versions = ">=2.7" - -[package.dependencies] -contextlib2 = {version = "*", markers = "python_version < \"3.4\""} +python-versions = ">=3.6" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pathlib2", "unittest2", "jaraco.itertools", "func-timeout"] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [metadata] lock-version = "1.1" -python-versions = "~2.7 || ^3.5" -content-hash = "1e774c9d8b7f6812d721cff08b51554f9a0cd051e2ae0e884421bcb56718d131" +python-versions = "^3.6" +content-hash = "ac67bc6eacbb6b633f9568d69533d80456f632c7bfb9a2aa61aa9dd98e862473" [metadata.files] appdirs = [ @@ -986,12 +713,8 @@ atomicwrites = [ {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ - {file = "attrs-20.2.0-py2.py3-none-any.whl", hash = "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"}, - {file = "attrs-20.2.0.tar.gz", hash = "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594"}, -] -"backports.functools-lru-cache" = [ - {file = "backports.functools_lru_cache-1.6.1-py2.py3-none-any.whl", hash = "sha256:0bada4c2f8a43d533e4ecb7a12214d9420e66eb206d54bf2d682581ca4b80848"}, - {file = "backports.functools_lru_cache-1.6.1.tar.gz", hash = "sha256:8fde5f188da2d593bd5bc0be98d9abc46c95bb8a9dde93429570192ee6cc2d4a"}, + {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, + {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, ] cachecontrol = [ {file = "CacheControl-0.12.6-py2.py3-none-any.whl", hash = "sha256:10d056fa27f8563a271b345207402a6dcce8efab7e5b377e270329c62471b10d"}, @@ -1002,138 +725,143 @@ cachy = [ {file = "cachy-0.3.0.tar.gz", hash = "sha256:186581f4ceb42a0bbe040c407da73c14092379b1e4c0e327fdb72ae4a9b269b1"}, ] certifi = [ - {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, - {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, + {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, + {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, ] cffi = [ - {file = "cffi-1.14.3-2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc"}, - {file = "cffi-1.14.3-2-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768"}, - {file = "cffi-1.14.3-2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d"}, - {file = "cffi-1.14.3-2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1"}, - {file = "cffi-1.14.3-2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca"}, - {file = "cffi-1.14.3-2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a"}, - {file = "cffi-1.14.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c"}, - {file = "cffi-1.14.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730"}, - {file = "cffi-1.14.3-cp27-cp27m-win32.whl", hash = "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d"}, - {file = "cffi-1.14.3-cp27-cp27m-win_amd64.whl", hash = "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05"}, - {file = "cffi-1.14.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b"}, - {file = "cffi-1.14.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171"}, - {file = "cffi-1.14.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f"}, - {file = "cffi-1.14.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4"}, - {file = "cffi-1.14.3-cp35-cp35m-win32.whl", hash = "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d"}, - {file = "cffi-1.14.3-cp35-cp35m-win_amd64.whl", hash = "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d"}, - {file = "cffi-1.14.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3"}, - {file = "cffi-1.14.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808"}, - {file = "cffi-1.14.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537"}, - {file = "cffi-1.14.3-cp36-cp36m-win32.whl", hash = "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0"}, - {file = "cffi-1.14.3-cp36-cp36m-win_amd64.whl", hash = "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e"}, - {file = "cffi-1.14.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1"}, - {file = "cffi-1.14.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579"}, - {file = "cffi-1.14.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394"}, - {file = "cffi-1.14.3-cp37-cp37m-win32.whl", hash = "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc"}, - {file = "cffi-1.14.3-cp37-cp37m-win_amd64.whl", hash = "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869"}, - {file = "cffi-1.14.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e"}, - {file = "cffi-1.14.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828"}, - {file = "cffi-1.14.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9"}, - {file = "cffi-1.14.3-cp38-cp38-win32.whl", hash = "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522"}, - {file = "cffi-1.14.3-cp38-cp38-win_amd64.whl", hash = "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15"}, - {file = "cffi-1.14.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d"}, - {file = "cffi-1.14.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c"}, - {file = "cffi-1.14.3-cp39-cp39-win32.whl", hash = "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b"}, - {file = "cffi-1.14.3-cp39-cp39-win_amd64.whl", hash = "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3"}, - {file = "cffi-1.14.3.tar.gz", hash = "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591"}, + {file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"}, + {file = "cffi-1.14.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1"}, + {file = "cffi-1.14.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa"}, + {file = "cffi-1.14.5-cp27-cp27m-win32.whl", hash = "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3"}, + {file = "cffi-1.14.5-cp27-cp27m-win_amd64.whl", hash = "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5"}, + {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482"}, + {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6"}, + {file = "cffi-1.14.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045"}, + {file = "cffi-1.14.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa"}, + {file = "cffi-1.14.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406"}, + {file = "cffi-1.14.5-cp35-cp35m-win32.whl", hash = "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369"}, + {file = "cffi-1.14.5-cp35-cp35m-win_amd64.whl", hash = "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315"}, + {file = "cffi-1.14.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132"}, + {file = "cffi-1.14.5-cp36-cp36m-win32.whl", hash = "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53"}, + {file = "cffi-1.14.5-cp36-cp36m-win_amd64.whl", hash = "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813"}, + {file = "cffi-1.14.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49"}, + {file = "cffi-1.14.5-cp37-cp37m-win32.whl", hash = "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62"}, + {file = "cffi-1.14.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4"}, + {file = "cffi-1.14.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827"}, + {file = "cffi-1.14.5-cp38-cp38-win32.whl", hash = "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e"}, + {file = "cffi-1.14.5-cp38-cp38-win_amd64.whl", hash = "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396"}, + {file = "cffi-1.14.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee"}, + {file = "cffi-1.14.5-cp39-cp39-win32.whl", hash = "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396"}, + {file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"}, + {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"}, ] cfgv = [ - {file = "cfgv-3.2.0-py2.py3-none-any.whl", hash = "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d"}, - {file = "cfgv-3.2.0.tar.gz", hash = "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1"}, + {file = "cfgv-3.3.0-py2.py3-none-any.whl", hash = "sha256:b449c9c6118fe8cca7fa5e00b9ec60ba08145d281d52164230a69211c5d597a1"}, + {file = "cfgv-3.3.0.tar.gz", hash = "sha256:9e600479b3b99e8af981ecdfc80a0296104ee610cab48a5ae4ffd0b668650eb1"}, ] chardet = [ - {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, - {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, ] cleo = [ - {file = "cleo-0.8.1-py2.py3-none-any.whl", hash = "sha256:141cda6dc94a92343be626bb87a0b6c86ae291dfc732a57bf04310d4b4201753"}, - {file = "cleo-0.8.1.tar.gz", hash = "sha256:3d0e22d30117851b45970b6c14aca4ab0b18b1b53c8af57bed13208147e4069f"}, -] -clikit = [ - {file = "clikit-0.6.2-py2.py3-none-any.whl", hash = "sha256:71268e074e68082306e23d7369a7b99f824a0ef926e55ba2665e911f7208489e"}, - {file = "clikit-0.6.2.tar.gz", hash = "sha256:442ee5db9a14120635c5990bcdbfe7c03ada5898291f0c802f77be71569ded59"}, + {file = "cleo-1.0.0a3-py3-none-any.whl", hash = "sha256:46b2f970d06caa311d1e12a1013b0ce2a8149502669ac82cbedafb9e0bfdbccd"}, + {file = "cleo-1.0.0a3.tar.gz", hash = "sha256:9c1c8dd06635c936f45e4649aa2f7581517b4d52c7a9414d1b42586e63c2fe5d"}, ] colorama = [ - {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, - {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, -] -configparser = [ - {file = "configparser-4.0.2-py2.py3-none-any.whl", hash = "sha256:254c1d9c79f60c45dfde850850883d5aaa7f19a23f13561243a050d5a7c3fe4c"}, - {file = "configparser-4.0.2.tar.gz", hash = "sha256:c7d282687a5308319bf3d2e7706e575c635b0a470342641c93bea0ea3b5331df"}, -] -contextlib2 = [ - {file = "contextlib2-0.6.0.post1-py2.py3-none-any.whl", hash = "sha256:3355078a159fbb44ee60ea80abd0d87b80b78c248643b49aa6d94673b413609b"}, - {file = "contextlib2-0.6.0.post1.tar.gz", hash = "sha256:01f490098c18b19d2bd5bb5dc445b2054d2fa97f09a4280ba2c5f3c394c8162e"}, + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] coverage = [ - {file = "coverage-5.3-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270"}, - {file = "coverage-5.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4"}, - {file = "coverage-5.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9"}, - {file = "coverage-5.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729"}, - {file = "coverage-5.3-cp27-cp27m-win32.whl", hash = "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d"}, - {file = "coverage-5.3-cp27-cp27m-win_amd64.whl", hash = "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418"}, - {file = "coverage-5.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9"}, - {file = "coverage-5.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5"}, - {file = "coverage-5.3-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822"}, - {file = "coverage-5.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097"}, - {file = "coverage-5.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9"}, - {file = "coverage-5.3-cp35-cp35m-win32.whl", hash = "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636"}, - {file = "coverage-5.3-cp35-cp35m-win_amd64.whl", hash = "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f"}, - {file = "coverage-5.3-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237"}, - {file = "coverage-5.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54"}, - {file = "coverage-5.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7"}, - {file = "coverage-5.3-cp36-cp36m-win32.whl", hash = "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a"}, - {file = "coverage-5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d"}, - {file = "coverage-5.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8"}, - {file = "coverage-5.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f"}, - {file = "coverage-5.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c"}, - {file = "coverage-5.3-cp37-cp37m-win32.whl", hash = "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751"}, - {file = "coverage-5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709"}, - {file = "coverage-5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516"}, - {file = "coverage-5.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f"}, - {file = "coverage-5.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259"}, - {file = "coverage-5.3-cp38-cp38-win32.whl", hash = "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82"}, - {file = "coverage-5.3-cp38-cp38-win_amd64.whl", hash = "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221"}, - {file = "coverage-5.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978"}, - {file = "coverage-5.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21"}, - {file = "coverage-5.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24"}, - {file = "coverage-5.3-cp39-cp39-win32.whl", hash = "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7"}, - {file = "coverage-5.3-cp39-cp39-win_amd64.whl", hash = "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7"}, - {file = "coverage-5.3.tar.gz", hash = "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0"}, + {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, + {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, + {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, + {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, + {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, + {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, + {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, + {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, + {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, + {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, + {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, + {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, + {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, + {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, + {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, + {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, + {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, + {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, + {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, + {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, + {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, + {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, + {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, + {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, ] crashtest = [ {file = "crashtest-0.3.1-py3-none-any.whl", hash = "sha256:300f4b0825f57688b47b6d70c6a31de33512eb2fa1ac614f780939aa0cf91680"}, {file = "crashtest-0.3.1.tar.gz", hash = "sha256:42ca7b6ce88b6c7433e2ce47ea884e91ec93104a4b754998be498a8e6c3d37dd"}, ] cryptography = [ - {file = "cryptography-3.1.1-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:65beb15e7f9c16e15934569d29fb4def74ea1469d8781f6b3507ab896d6d8719"}, - {file = "cryptography-3.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:983c0c3de4cb9fcba68fd3f45ed846eb86a2a8b8d8bc5bb18364c4d00b3c61fe"}, - {file = "cryptography-3.1.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:e97a3b627e3cb63c415a16245d6cef2139cca18bb1183d1b9375a1c14e83f3b3"}, - {file = "cryptography-3.1.1-cp27-cp27m-win32.whl", hash = "sha256:cb179acdd4ae1e4a5a160d80b87841b3d0e0be84af46c7bb2cd7ece57a39c4ba"}, - {file = "cryptography-3.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:b372026ebf32fe2523159f27d9f0e9f485092e43b00a5adacf732192a70ba118"}, - {file = "cryptography-3.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:680da076cad81cdf5ffcac50c477b6790be81768d30f9da9e01960c4b18a66db"}, - {file = "cryptography-3.1.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5d52c72449bb02dd45a773a203196e6d4fae34e158769c896012401f33064396"}, - {file = "cryptography-3.1.1-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:f0e099fc4cc697450c3dd4031791559692dd941a95254cb9aeded66a7aa8b9bc"}, - {file = "cryptography-3.1.1-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:a7597ffc67987b37b12e09c029bd1dc43965f75d328076ae85721b84046e9ca7"}, - {file = "cryptography-3.1.1-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:4549b137d8cbe3c2eadfa56c0c858b78acbeff956bd461e40000b2164d9167c6"}, - {file = "cryptography-3.1.1-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:89aceb31cd5f9fc2449fe8cf3810797ca52b65f1489002d58fe190bfb265c536"}, - {file = "cryptography-3.1.1-cp35-cp35m-win32.whl", hash = "sha256:559d622aef2a2dff98a892eef321433ba5bc55b2485220a8ca289c1ecc2bd54f"}, - {file = "cryptography-3.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:451cdf60be4dafb6a3b78802006a020e6cd709c22d240f94f7a0696240a17154"}, - {file = "cryptography-3.1.1-cp36-abi3-win32.whl", hash = "sha256:762bc5a0df03c51ee3f09c621e1cee64e3a079a2b5020de82f1613873d79ee70"}, - {file = "cryptography-3.1.1-cp36-abi3-win_amd64.whl", hash = "sha256:b12e715c10a13ca1bd27fbceed9adc8c5ff640f8e1f7ea76416352de703523c8"}, - {file = "cryptography-3.1.1-cp36-cp36m-win32.whl", hash = "sha256:21b47c59fcb1c36f1113f3709d37935368e34815ea1d7073862e92f810dc7499"}, - {file = "cryptography-3.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:48ee615a779ffa749d7d50c291761dc921d93d7cf203dca2db663b4f193f0e49"}, - {file = "cryptography-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:b2bded09c578d19e08bd2c5bb8fed7f103e089752c9cf7ca7ca7de522326e921"}, - {file = "cryptography-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f99317a0fa2e49917689b8cf977510addcfaaab769b3f899b9c481bbd76730c2"}, - {file = "cryptography-3.1.1-cp38-cp38-win32.whl", hash = "sha256:ab010e461bb6b444eaf7f8c813bb716be2d78ab786103f9608ffd37a4bd7d490"}, - {file = "cryptography-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:99d4984aabd4c7182050bca76176ce2dbc9fa9748afe583a7865c12954d714ba"}, - {file = "cryptography-3.1.1.tar.gz", hash = "sha256:9d9fc6a16357965d282dd4ab6531013935425d0dc4950df2e0cf2a1b1ac1017d"}, + {file = "cryptography-3.4.7-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1"}, + {file = "cryptography-3.4.7-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250"}, + {file = "cryptography-3.4.7-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2"}, + {file = "cryptography-3.4.7-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6"}, + {file = "cryptography-3.4.7-cp36-abi3-manylinux2014_x86_64.whl", hash = "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959"}, + {file = "cryptography-3.4.7-cp36-abi3-win32.whl", hash = "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d"}, + {file = "cryptography-3.4.7-cp36-abi3-win_amd64.whl", hash = "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca"}, + {file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873"}, + {file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2014_x86_64.whl", hash = "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d"}, + {file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177"}, + {file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2014_x86_64.whl", hash = "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"}, + {file = "cryptography-3.4.7.tar.gz", hash = "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713"}, +] +dataclasses = [ + {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, + {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, +] +deepdiff = [ + {file = "deepdiff-5.5.0-py3-none-any.whl", hash = "sha256:e054fed9dfe0d83d622921cbb3a3d0b3a6dd76acd2b6955433a0a2d35147774a"}, + {file = "deepdiff-5.5.0.tar.gz", hash = "sha256:dd79b81c2d84bfa33aa9d94d456b037b68daff6bb87b80dfaa1eca04da68b349"}, ] distlib = [ {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, @@ -1143,40 +871,20 @@ entrypoints = [ {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, ] -enum34 = [ - {file = "enum34-1.1.10-py2-none-any.whl", hash = "sha256:a98a201d6de3f2ab3db284e70a33b0f896fbf35f8086594e8c9e74b909058d53"}, - {file = "enum34-1.1.10-py3-none-any.whl", hash = "sha256:c3858660960c984d6ab0ebad691265180da2b43f07e061c0f8dca9ef3cffd328"}, - {file = "enum34-1.1.10.tar.gz", hash = "sha256:cce6a7477ed816bd2542d03d53db9f0db935dd013b70f336a95c73979289f248"}, -] filelock = [ {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, ] -funcsigs = [ - {file = "funcsigs-1.0.2-py2.py3-none-any.whl", hash = "sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca"}, - {file = "funcsigs-1.0.2.tar.gz", hash = "sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"}, -] -functools32 = [ - {file = "functools32-3.2.3-2.tar.gz", hash = "sha256:f6253dfbe0538ad2e387bd8fdfd9293c925d63553f5813c4e587745416501e6d"}, - {file = "functools32-3.2.3-2.zip", hash = "sha256:89d824aa6c358c421a234d7f9ee0bd75933a67c29588ce50aaa3acdf4d403fa0"}, -] -futures = [ - {file = "futures-3.3.0-py2-none-any.whl", hash = "sha256:49b3f5b064b6e3afc3316421a3f25f66c137ae88f068abbf72830170033c5e16"}, - {file = "futures-3.3.0.tar.gz", hash = "sha256:7e033af76a5e35f58e56da7a91e687706faf4e7bdfb2cbc3f2cca6b9bcda9794"}, -] -glob2 = [ - {file = "glob2-0.6.tar.gz", hash = "sha256:f5b0a686ff21f820c4d3f0c4edd216704cea59d79d00fa337e244a2f2ff83ed6"}, -] html5lib = [ {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"}, {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"}, ] httpretty = [ - {file = "httpretty-0.9.7.tar.gz", hash = "sha256:66216f26b9d2c52e81808f3e674a6fb65d4bf719721394a1a9be926177e55fbe"}, + {file = "httpretty-1.1.2.tar.gz", hash = "sha256:73d3e342ce8b21a16b0ff6c2b4c2c2d11c2da28784c264fcf7fe8cd2d8a25090"}, ] identify = [ - {file = "identify-1.5.5-py2.py3-none-any.whl", hash = "sha256:da683bfb7669fa749fc7731f378229e2dbf29a1d1337cbde04106f02236eb29d"}, - {file = "identify-1.5.5.tar.gz", hash = "sha256:7c22c384a2c9b32c5cc891d13f923f6b2653aa83e2d75d8f79be240d6c86c4f4"}, + {file = "identify-2.2.4-py2.py3-none-any.whl", hash = "sha256:ad9f3fa0c2316618dc4d840f627d474ab6de106392a4f00221820200f490f5a8"}, + {file = "identify-2.2.4.tar.gz", hash = "sha256:9bcc312d4e2fa96c7abebcdfb1119563b511b5e3985ac52f60d9116277865b2e"}, ] idna = [ {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, @@ -1187,103 +895,93 @@ importlib-metadata = [ {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, ] importlib-resources = [ - {file = "importlib_resources-3.0.0-py2.py3-none-any.whl", hash = "sha256:d028f66b66c0d5732dae86ba4276999855e162a749c92620a38c1d779ed138a7"}, - {file = "importlib_resources-3.0.0.tar.gz", hash = "sha256:19f745a6eca188b490b1428c8d1d4a0d2368759f32370ea8fb89cad2ab1106c3"}, + {file = "importlib_resources-5.1.3-py3-none-any.whl", hash = "sha256:3b9c774e0e7e8d9c069eb2fe6aee7e9ae71759a381dec02eb45249fba7f38713"}, + {file = "importlib_resources-5.1.3.tar.gz", hash = "sha256:0786b216556e53b34156263ab654406e543a8b0d9b1381019e25a36a09263c36"}, ] -ipaddress = [ - {file = "ipaddress-1.0.23-py2.py3-none-any.whl", hash = "sha256:6e0f4a39e66cb5bb9a137b00276a2eff74f93b71dcbdad6f10ff7df9d3557fcc"}, - {file = "ipaddress-1.0.23.tar.gz", hash = "sha256:b7f8e0369580bb4a24d5ba1d7cc29660a4a6987763faf1d8a8046830e020e7e2"}, +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] jeepney = [ - {file = "jeepney-0.4.3-py3-none-any.whl", hash = "sha256:d6c6b49683446d2407d2fe3acb7a368a77ff063f9182fe427da15d622adc24cf"}, - {file = "jeepney-0.4.3.tar.gz", hash = "sha256:3479b861cc2b6407de5188695fa1a8d57e5072d7059322469b62628869b8e36e"}, + {file = "jeepney-0.6.0-py3-none-any.whl", hash = "sha256:aec56c0eb1691a841795111e184e13cad504f7703b9a64f63020816afa79a8ae"}, + {file = "jeepney-0.6.0.tar.gz", hash = "sha256:7d59b6622675ca9e993a6bd38de845051d315f8b0c72cca3aef733a20b648657"}, ] keyring = [ - {file = "keyring-18.0.1-py2.py3-none-any.whl", hash = "sha256:7b29ebfcf8678c4da531b2478a912eea01e80007e5ddca9ee0c7038cb3489ec6"}, - {file = "keyring-18.0.1.tar.gz", hash = "sha256:67d6cc0132bd77922725fae9f18366bb314fd8f95ff4d323a4df41890a96a838"}, - {file = "keyring-20.0.1-py2.py3-none-any.whl", hash = "sha256:c674f032424b4bffc62abeac5523ec49cc84aed07a480c3233e0baf618efc15c"}, - {file = "keyring-20.0.1.tar.gz", hash = "sha256:963bfa7f090269d30bdc5e25589e5fd9dad2cf2a7c6f176a7f2386910e5d0d8d"}, - {file = "keyring-21.4.0-py3-none-any.whl", hash = "sha256:4e34ea2fdec90c1c43d6610b5a5fafa1b9097db1802948e90caf5763974b8f8d"}, - {file = "keyring-21.4.0.tar.gz", hash = "sha256:9aeadd006a852b78f4b4ef7c7556c2774d2432bbef8ee538a3e9089ac8b11466"}, + {file = "keyring-22.3.0-py3-none-any.whl", hash = "sha256:2bc8363ebdd63886126a012057a85c8cb6e143877afa02619ac7dbc9f38a207b"}, + {file = "keyring-22.3.0.tar.gz", hash = "sha256:16927a444b2c73f983520a48dec79ddab49fe76429ea05b8d528d778c8339522"}, ] lockfile = [ {file = "lockfile-0.12.2-py2.py3-none-any.whl", hash = "sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa"}, {file = "lockfile-0.12.2.tar.gz", hash = "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799"}, ] -mock = [ - {file = "mock-3.0.5-py2.py3-none-any.whl", hash = "sha256:d157e52d4e5b938c550f39eb2fd15610db062441a9c2747d3dbfa9298211d0f8"}, - {file = "mock-3.0.5.tar.gz", hash = "sha256:83657d894c90d5681d62155c82bda9c1187827525880eda8ff5df4ec813437c3"}, -] -more-itertools = [ - {file = "more-itertools-5.0.0.tar.gz", hash = "sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4"}, - {file = "more_itertools-5.0.0-py2-none-any.whl", hash = "sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc"}, - {file = "more_itertools-5.0.0-py3-none-any.whl", hash = "sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"}, - {file = "more-itertools-8.5.0.tar.gz", hash = "sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20"}, - {file = "more_itertools-8.5.0-py3-none-any.whl", hash = "sha256:9b30f12df9393f0d28af9210ff8efe48d10c94f73e5daf886f10c4b0b0b4f03c"}, -] msgpack = [ - {file = "msgpack-1.0.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:cec8bf10981ed70998d98431cd814db0ecf3384e6b113366e7f36af71a0fca08"}, - {file = "msgpack-1.0.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aa5c057eab4f40ec47ea6f5a9825846be2ff6bf34102c560bad5cad5a677c5be"}, - {file = "msgpack-1.0.0-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:4233b7f86c1208190c78a525cd3828ca1623359ef48f78a6fea4b91bb995775a"}, - {file = "msgpack-1.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:b3758dfd3423e358bbb18a7cccd1c74228dffa7a697e5be6cb9535de625c0dbf"}, - {file = "msgpack-1.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:25b3bc3190f3d9d965b818123b7752c5dfb953f0d774b454fd206c18fe384fb8"}, - {file = "msgpack-1.0.0-cp36-cp36m-win32.whl", hash = "sha256:e7bbdd8e2b277b77782f3ce34734b0dfde6cbe94ddb74de8d733d603c7f9e2b1"}, - {file = "msgpack-1.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5dba6d074fac9b24f29aaf1d2d032306c27f04187651511257e7831733293ec2"}, - {file = "msgpack-1.0.0-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:908944e3f038bca67fcfedb7845c4a257c7749bf9818632586b53bcf06ba4b97"}, - {file = "msgpack-1.0.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:db685187a415f51d6b937257474ca72199f393dad89534ebbdd7d7a3b000080e"}, - {file = "msgpack-1.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ea41c9219c597f1d2bf6b374d951d310d58684b5de9dc4bd2976db9e1e22c140"}, - {file = "msgpack-1.0.0-cp37-cp37m-win32.whl", hash = "sha256:e35b051077fc2f3ce12e7c6a34cf309680c63a842db3a0616ea6ed25ad20d272"}, - {file = "msgpack-1.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:5bea44181fc8e18eed1d0cd76e355073f00ce232ff9653a0ae88cb7d9e643322"}, - {file = "msgpack-1.0.0-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c901e8058dd6653307906c5f157f26ed09eb94a850dddd989621098d347926ab"}, - {file = "msgpack-1.0.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:271b489499a43af001a2e42f42d876bb98ccaa7e20512ff37ca78c8e12e68f84"}, - {file = "msgpack-1.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7a22c965588baeb07242cb561b63f309db27a07382825fc98aecaf0827c1538e"}, - {file = "msgpack-1.0.0-cp38-cp38-win32.whl", hash = "sha256:002a0d813e1f7b60da599bdf969e632074f9eec1b96cbed8fb0973a63160a408"}, - {file = "msgpack-1.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:39c54fdebf5fa4dda733369012c59e7d085ebdfe35b6cf648f09d16708f1be5d"}, - {file = "msgpack-1.0.0.tar.gz", hash = "sha256:9534d5cc480d4aff720233411a1f765be90885750b07df772380b34c10ecb5c0"}, + {file = "msgpack-1.0.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:b6d9e2dae081aa35c44af9c4298de4ee72991305503442a5c74656d82b581fe9"}, + {file = "msgpack-1.0.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:a99b144475230982aee16b3d249170f1cccebf27fb0a08e9f603b69637a62192"}, + {file = "msgpack-1.0.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1026dcc10537d27dd2d26c327e552f05ce148977e9d7b9f1718748281b38c841"}, + {file = "msgpack-1.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:fe07bc6735d08e492a327f496b7850e98cb4d112c56df69b0c844dbebcbb47f6"}, + {file = "msgpack-1.0.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:9ea52fff0473f9f3000987f313310208c879493491ef3ccf66268eff8d5a0326"}, + {file = "msgpack-1.0.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:26a1759f1a88df5f1d0b393eb582ec022326994e311ba9c5818adc5374736439"}, + {file = "msgpack-1.0.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:497d2c12426adcd27ab83144057a705efb6acc7e85957a51d43cdcf7f258900f"}, + {file = "msgpack-1.0.2-cp36-cp36m-win32.whl", hash = "sha256:e89ec55871ed5473a041c0495b7b4e6099f6263438e0bd04ccd8418f92d5d7f2"}, + {file = "msgpack-1.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a4355d2193106c7aa77c98fc955252a737d8550320ecdb2e9ac701e15e2943bc"}, + {file = "msgpack-1.0.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:d6c64601af8f3893d17ec233237030e3110f11b8a962cb66720bf70c0141aa54"}, + {file = "msgpack-1.0.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f484cd2dca68502de3704f056fa9b318c94b1539ed17a4c784266df5d6978c87"}, + {file = "msgpack-1.0.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f3e6aaf217ac1c7ce1563cf52a2f4f5d5b1f64e8729d794165db71da57257f0c"}, + {file = "msgpack-1.0.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:8521e5be9e3b93d4d5e07cb80b7e32353264d143c1f072309e1863174c6aadb1"}, + {file = "msgpack-1.0.2-cp37-cp37m-win32.whl", hash = "sha256:31c17bbf2ae5e29e48d794c693b7ca7a0c73bd4280976d408c53df421e838d2a"}, + {file = "msgpack-1.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8ffb24a3b7518e843cd83538cf859e026d24ec41ac5721c18ed0c55101f9775b"}, + {file = "msgpack-1.0.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:b28c0876cce1466d7c2195d7658cf50e4730667196e2f1355c4209444717ee06"}, + {file = "msgpack-1.0.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:87869ba567fe371c4555d2e11e4948778ab6b59d6cc9d8460d543e4cfbbddd1c"}, + {file = "msgpack-1.0.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:b55f7db883530b74c857e50e149126b91bb75d35c08b28db12dcb0346f15e46e"}, + {file = "msgpack-1.0.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:ac25f3e0513f6673e8b405c3a80500eb7be1cf8f57584be524c4fa78fe8e0c83"}, + {file = "msgpack-1.0.2-cp38-cp38-win32.whl", hash = "sha256:0cb94ee48675a45d3b86e61d13c1e6f1696f0183f0715544976356ff86f741d9"}, + {file = "msgpack-1.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:e36a812ef4705a291cdb4a2fd352f013134f26c6ff63477f20235138d1d21009"}, + {file = "msgpack-1.0.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:2a5866bdc88d77f6e1370f82f2371c9bc6fc92fe898fa2dec0c5d4f5435a2694"}, + {file = "msgpack-1.0.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:92be4b12de4806d3c36810b0fe2aeedd8d493db39e2eb90742b9c09299eb5759"}, + {file = "msgpack-1.0.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:de6bd7990a2c2dabe926b7e62a92886ccbf809425c347ae7de277067f97c2887"}, + {file = "msgpack-1.0.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5a9ee2540c78659a1dd0b110f73773533ee3108d4e1219b5a15a8d635b7aca0e"}, + {file = "msgpack-1.0.2-cp39-cp39-win32.whl", hash = "sha256:c747c0cc08bd6d72a586310bda6ea72eeb28e7505990f342552315b229a19b33"}, + {file = "msgpack-1.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:d8167b84af26654c1124857d71650404336f4eb5cc06900667a493fc619ddd9f"}, + {file = "msgpack-1.0.2.tar.gz", hash = "sha256:fae04496f5bc150eefad4e9571d1a76c55d021325dcd484ce45065ebbdd00984"}, ] nodeenv = [ - {file = "nodeenv-1.5.0-py2.py3-none-any.whl", hash = "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9"}, - {file = "nodeenv-1.5.0.tar.gz", hash = "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c"}, -] -packaging = [ - {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, - {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, + {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, + {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, ] -pastel = [ - {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, - {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, +ordered-set = [ + {file = "ordered-set-4.0.2.tar.gz", hash = "sha256:ba93b2df055bca202116ec44b9bead3df33ea63a7d5827ff8e16738b97f33a95"}, ] -pathlib2 = [ - {file = "pathlib2-2.3.5-py2.py3-none-any.whl", hash = "sha256:0ec8205a157c80d7acc301c0b18fbd5d44fe655968f5d947b6ecef5290fc35db"}, - {file = "pathlib2-2.3.5.tar.gz", hash = "sha256:6cd9a47b597b37cc57de1c05e56fb1a1c9cc9fab04fe78c29acd090418529868"}, +packaging = [ + {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, + {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, ] pexpect = [ {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, ] pkginfo = [ - {file = "pkginfo-1.5.0.1-py2.py3-none-any.whl", hash = "sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32"}, - {file = "pkginfo-1.5.0.1.tar.gz", hash = "sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb"}, + {file = "pkginfo-1.7.0-py2.py3-none-any.whl", hash = "sha256:9fdbea6495622e022cc72c2e5e1b735218e4ffb2a2a69cde2694a6c1f16afb75"}, + {file = "pkginfo-1.7.0.tar.gz", hash = "sha256:029a70cb45c6171c329dfc890cde0879f8c52d6f3922794796e06f577bb03db4"}, ] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] poetry-core = [ - {file = "poetry-core-1.0.0.tar.gz", hash = "sha256:6a664ff389b9f45382536f8fa1611a0cb4d2de7c5a5c885db1f0c600cd11fbd5"}, - {file = "poetry_core-1.0.0-py2.py3-none-any.whl", hash = "sha256:769288e0e1b88dfcceb3185728f0b7388b26d5f93d6c22d2dcae372da51d200d"}, + {file = "poetry-core-1.1.0a5.tar.gz", hash = "sha256:1b886de26026865325eae86a5d12eb154b80c0add8067c106eb706757594d85f"}, + {file = "poetry_core-1.1.0a5-py3-none-any.whl", hash = "sha256:b347525c1417e9b5c6aee52967eff98c0886853a9e8ab1b9dfb2659913dd37bc"}, ] pre-commit = [ - {file = "pre_commit-2.7.1-py2.py3-none-any.whl", hash = "sha256:810aef2a2ba4f31eed1941fc270e72696a1ad5590b9751839c90807d0fff6b9a"}, - {file = "pre_commit-2.7.1.tar.gz", hash = "sha256:c54fd3e574565fe128ecc5e7d2f91279772ddb03f8729645fa812fe809084a70"}, + {file = "pre_commit-2.12.1-py2.py3-none-any.whl", hash = "sha256:70c5ec1f30406250b706eda35e868b87e3e4ba099af8787e3e8b4b01e84f4712"}, + {file = "pre_commit-2.12.1.tar.gz", hash = "sha256:900d3c7e1bf4cf0374bb2893c24c23304952181405b4d88c9c40b72bda1bb8a9"}, ] ptyprocess = [ - {file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"}, - {file = "ptyprocess-0.6.0.tar.gz", hash = "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0"}, + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, ] py = [ - {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, - {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, + {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, + {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, ] pycparser = [ {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, @@ -1298,18 +996,16 @@ pyparsing = [ {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] pytest = [ - {file = "pytest-4.6.11-py2.py3-none-any.whl", hash = "sha256:a00a7d79cbbdfa9d21e7d0298392a8dd4123316bfac545075e6f8f24c94d8c97"}, - {file = "pytest-4.6.11.tar.gz", hash = "sha256:50fa82392f2120cc3ec2ca0a75ee615be4c479e66669789771f1758332be4353"}, - {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, - {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, + {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, + {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, ] pytest-cov = [ - {file = "pytest-cov-2.10.1.tar.gz", hash = "sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e"}, - {file = "pytest_cov-2.10.1-py2.py3-none-any.whl", hash = "sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191"}, + {file = "pytest-cov-2.12.0.tar.gz", hash = "sha256:8535764137fecce504a49c2b742288e3d34bc09eed298ad65963616cc98fd45e"}, + {file = "pytest_cov-2.12.0-py2.py3-none-any.whl", hash = "sha256:95d4933dcbbacfa377bb60b29801daa30d90c33981ab2a79e9ab4452c165066e"}, ] pytest-mock = [ - {file = "pytest-mock-1.13.0.tar.gz", hash = "sha256:e24a911ec96773022ebcc7030059b57cd3480b56d4f5d19b7c370ec635e6aed5"}, - {file = "pytest_mock-1.13.0-py2.py3-none-any.whl", hash = "sha256:67e414b3caef7bff6fc6bd83b22b5bc39147e4493f483c2679bc9d4dc485a94d"}, + {file = "pytest-mock-3.6.1.tar.gz", hash = "sha256:40217a058c52a63f1042f0784f62009e976ba824c418cced42e88d5f40ab0e62"}, + {file = "pytest_mock-3.6.1-py3-none-any.whl", hash = "sha256:30c2f2cc9759e76eee674b81ea28c9f0b94f8f0445a1b87762cadf774f0df7e3"}, ] pytest-sugar = [ {file = "pytest-sugar-0.9.4.tar.gz", hash = "sha256:b1b2186b0a72aada6859bea2a5764145e3aaa2c1cfbb23c3a19b5f7b697563d3"}, @@ -1319,101 +1015,76 @@ pywin32-ctypes = [ {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, ] pyyaml = [ - {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, - {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, - {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, - {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, - {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, - {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, - {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, - {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, - {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, - {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, - {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, + {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, + {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, + {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, + {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, + {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, + {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, + {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, + {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, + {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, + {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, + {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, + {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, + {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, + {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, + {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, + {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, + {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, ] requests = [ - {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, - {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, ] requests-toolbelt = [ {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, ] -scandir = [ - {file = "scandir-1.10.0-cp27-cp27m-win32.whl", hash = "sha256:92c85ac42f41ffdc35b6da57ed991575bdbe69db895507af88b9f499b701c188"}, - {file = "scandir-1.10.0-cp27-cp27m-win_amd64.whl", hash = "sha256:cb925555f43060a1745d0a321cca94bcea927c50114b623d73179189a4e100ac"}, - {file = "scandir-1.10.0-cp34-cp34m-win32.whl", hash = "sha256:2c712840c2e2ee8dfaf36034080108d30060d759c7b73a01a52251cc8989f11f"}, - {file = "scandir-1.10.0-cp34-cp34m-win_amd64.whl", hash = "sha256:2586c94e907d99617887daed6c1d102b5ca28f1085f90446554abf1faf73123e"}, - {file = "scandir-1.10.0-cp35-cp35m-win32.whl", hash = "sha256:2b8e3888b11abb2217a32af0766bc06b65cc4a928d8727828ee68af5a967fa6f"}, - {file = "scandir-1.10.0-cp35-cp35m-win_amd64.whl", hash = "sha256:8c5922863e44ffc00c5c693190648daa6d15e7c1207ed02d6f46a8dcc2869d32"}, - {file = "scandir-1.10.0-cp36-cp36m-win32.whl", hash = "sha256:2ae41f43797ca0c11591c0c35f2f5875fa99f8797cb1a1fd440497ec0ae4b022"}, - {file = "scandir-1.10.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7d2d7a06a252764061a020407b997dd036f7bd6a175a5ba2b345f0a357f0b3f4"}, - {file = "scandir-1.10.0-cp37-cp37m-win32.whl", hash = "sha256:67f15b6f83e6507fdc6fca22fedf6ef8b334b399ca27c6b568cbfaa82a364173"}, - {file = "scandir-1.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b24086f2375c4a094a6b51e78b4cf7ca16c721dcee2eddd7aa6494b42d6d519d"}, - {file = "scandir-1.10.0.tar.gz", hash = "sha256:4d4631f6062e658e9007ab3149a9b914f3548cb38bfb021c64f39a025ce578ae"}, -] secretstorage = [ - {file = "SecretStorage-2.3.1.tar.gz", hash = "sha256:3af65c87765323e6f64c83575b05393f9e003431959c9395d1791d51497f29b6"}, - {file = "SecretStorage-3.1.2-py3-none-any.whl", hash = "sha256:b5ec909dde94d4ae2fa26af7c089036997030f0cf0a5cb372b4cccabd81c143b"}, - {file = "SecretStorage-3.1.2.tar.gz", hash = "sha256:15da8a989b65498e29be338b3b279965f1b8f09b9668bd8010da183024c8bff6"}, + {file = "SecretStorage-3.3.1-py3-none-any.whl", hash = "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f"}, + {file = "SecretStorage-3.3.1.tar.gz", hash = "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195"}, ] shellingham = [ - {file = "shellingham-1.3.2-py2.py3-none-any.whl", hash = "sha256:7f6206ae169dc1a03af8a138681b3f962ae61cc93ade84d0585cca3aaf770044"}, - {file = "shellingham-1.3.2.tar.gz", hash = "sha256:576c1982bea0ba82fb46c36feb951319d7f42214a82634233f58b40d858a751e"}, -] -singledispatch = [ - {file = "singledispatch-3.4.0.3-py2.py3-none-any.whl", hash = "sha256:833b46966687b3de7f438c761ac475213e53b306740f1abfaa86e1d1aae56aa8"}, - {file = "singledispatch-3.4.0.3.tar.gz", hash = "sha256:5b06af87df13818d14f08a028e42f566640aef80805c3b50c5056b086e3c2b9c"}, + {file = "shellingham-1.4.0-py2.py3-none-any.whl", hash = "sha256:536b67a0697f2e4af32ab176c00a50ac2899c5a05e0d8e2dadac8e58888283f9"}, + {file = "shellingham-1.4.0.tar.gz", hash = "sha256:4855c2458d6904829bd34c299f11fdeed7cfefbf8a2c522e4caea6cd76b3171e"}, ] six = [ - {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, - {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, -] -subprocess32 = [ - {file = "subprocess32-3.5.4-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:88e37c1aac5388df41cc8a8456bb49ebffd321a3ad4d70358e3518176de3a56b"}, - {file = "subprocess32-3.5.4.tar.gz", hash = "sha256:eb2937c80497978d181efa1b839ec2d9622cf9600a039a79d0e108d1f9aec79d"}, + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] termcolor = [ {file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"}, ] toml = [ - {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, - {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tomlkit = [ - {file = "tomlkit-0.7.0-py2.py3-none-any.whl", hash = "sha256:6babbd33b17d5c9691896b0e68159215a9387ebfa938aa3ac42f4a4beeb2b831"}, - {file = "tomlkit-0.7.0.tar.gz", hash = "sha256:ac57f29693fab3e309ea789252fcce3061e19110085aa31af5446ca749325618"}, + {file = "tomlkit-0.7.2-py2.py3-none-any.whl", hash = "sha256:173ad840fa5d2aac140528ca1933c29791b79a374a0861a80347f42ec9328117"}, + {file = "tomlkit-0.7.2.tar.gz", hash = "sha256:d7a454f319a7e9bd2e249f239168729327e4dd2d27b17dc68be264ad1ce36754"}, ] tox = [ - {file = "tox-3.20.0-py2.py3-none-any.whl", hash = "sha256:e6318f404aff16522ff5211c88cab82b39af121735a443674e4e2e65f4e4637b"}, - {file = "tox-3.20.0.tar.gz", hash = "sha256:eb629ddc60e8542fd4a1956b2462e3b8771d49f1ff630cecceacaa0fbfb7605a"}, -] -typing = [ - {file = "typing-3.7.4.3-py2-none-any.whl", hash = "sha256:283d868f5071ab9ad873e5e52268d611e851c870a2ba354193026f2dfb29d8b5"}, - {file = "typing-3.7.4.3.tar.gz", hash = "sha256:1187fb9c82fd670d10aa07bbb6cfcfe4bdda42d6fab8d5134f04e8c4d0b71cc9"}, -] -typing-extensions = [ - {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, - {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, - {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, + {file = "tox-3.23.1-py2.py3-none-any.whl", hash = "sha256:b0b5818049a1c1997599d42012a637a33f24c62ab8187223fdd318fa8522637b"}, + {file = "tox-3.23.1.tar.gz", hash = "sha256:307a81ddb82bd463971a273f33e9533a24ed22185f27db8ce3386bff27d324e3"}, ] urllib3 = [ - {file = "urllib3-1.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"}, - {file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"}, + {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"}, + {file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"}, ] virtualenv = [ - {file = "virtualenv-20.0.31-py2.py3-none-any.whl", hash = "sha256:e0305af10299a7fb0d69393d8f04cb2965dda9351140d11ac8db4e5e3970451b"}, - {file = "virtualenv-20.0.31.tar.gz", hash = "sha256:43add625c53c596d38f971a465553f6318decc39d98512bc100fa1b1e839c8dc"}, -] -wcwidth = [ - {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, - {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, + {file = "virtualenv-20.4.4-py2.py3-none-any.whl", hash = "sha256:a935126db63128861987a7d5d30e23e8ec045a73840eeccb467c148514e29535"}, + {file = "virtualenv-20.4.4.tar.gz", hash = "sha256:09c61377ef072f43568207dc8e46ddeac6bcdcaf288d49011bda0e7f4d38c4a2"}, ] webencodings = [ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, ] zipp = [ - {file = "zipp-1.2.0-py2.py3-none-any.whl", hash = "sha256:e0d9e63797e483a30d27e09fffd308c59a700d365ec34e93cc100844168bf921"}, - {file = "zipp-1.2.0.tar.gz", hash = "sha256:c70410551488251b0fee67b460fb9a536af8d6f9f008ad10ac51f615b6a521b1"}, + {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, + {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, ] diff --git a/poetry/__main__.py b/poetry/__main__.py index b280ed84e4a..1c442536107 100644 --- a/poetry/__main__.py +++ b/poetry/__main__.py @@ -2,6 +2,6 @@ if __name__ == "__main__": - from .console import main + from .console.application import main sys.exit(main()) diff --git a/poetry/__version__.py b/poetry/__version__.py index 6849410aae0..7bfbef15537 100644 --- a/poetry/__version__.py +++ b/poetry/__version__.py @@ -1 +1 @@ -__version__ = "1.1.0" +__version__ = "1.2.0a1" diff --git a/poetry/config/config.py b/poetry/config/config.py index be585575c05..b1b3b3e61bb 100644 --- a/poetry/config/config.py +++ b/poetry/config/config.py @@ -1,17 +1,14 @@ -from __future__ import absolute_import - import os import re from copy import deepcopy +from pathlib import Path from typing import Any from typing import Callable from typing import Dict from typing import Optional from poetry.locations import CACHE_DIR -from poetry.utils._compat import Path -from poetry.utils._compat import basestring from .config_source import ConfigSource from .dict_config_source import DictConfigSource @@ -20,15 +17,15 @@ _NOT_SET = object() -def boolean_validator(val): +def boolean_validator(val: str) -> bool: return val in {"true", "false", "1", "0"} -def boolean_normalizer(val): +def boolean_normalizer(val: str) -> bool: return val in ["true", "1"] -class Config(object): +class Config: default_config = { "cache-dir": str(CACHE_DIR), @@ -36,13 +33,15 @@ class Config(object): "create": True, "in-project": None, "path": os.path.join("{cache-dir}", "virtualenvs"), + "options": {"always-copy": False, "system-site-packages": False}, }, "experimental": {"new-installer": True}, + "installer": {"parallel": True}, } def __init__( - self, use_environment=True, base_dir=None - ): # type: (bool, Optional[Path]) -> None + self, use_environment: bool = True, base_dir: Optional[Path] = None + ) -> None: self._config = deepcopy(self.default_config) self._use_environment = use_environment self._base_dir = base_dir @@ -50,44 +49,48 @@ def __init__( self._auth_config_source = DictConfigSource() @property - def name(self): + def name(self) -> str: return str(self._file.path) @property - def config(self): + def config(self) -> Dict: return self._config @property - def config_source(self): # type: () -> ConfigSource + def config_source(self) -> ConfigSource: return self._config_source @property - def auth_config_source(self): # type: () -> ConfigSource + def auth_config_source(self) -> ConfigSource: return self._auth_config_source - def set_config_source(self, config_source): # type: (ConfigSource) -> Config + def set_config_source(self, config_source: ConfigSource) -> "Config": self._config_source = config_source return self - def set_auth_config_source(self, config_source): # type: (ConfigSource) -> Config + def set_auth_config_source(self, config_source: ConfigSource) -> "Config": self._auth_config_source = config_source return self - def merge(self, config): # type: (Dict[str, Any]) -> None + def merge(self, config: Dict[str, Any]) -> None: from poetry.utils.helpers import merge_dicts merge_dicts(self._config, config) - def all(self): # type: () -> Dict[str, Any] - def _all(config, parent_key=""): + def all(self) -> Dict[str, Any]: + def _all(config: Dict, parent_key: str = "") -> Dict: all_ = {} for key in config: value = self.get(parent_key + key) if isinstance(value, dict): - all_[key] = _all(config[key], parent_key=key + ".") + if parent_key != "": + current_parent = parent_key + key + "." + else: + current_parent = key + "." + all_[key] = _all(config[key], parent_key=current_parent) continue all_[key] = value @@ -96,10 +99,10 @@ def _all(config, parent_key=""): return _all(self.config) - def raw(self): # type: () -> Dict[str, Any] + def raw(self) -> Dict[str, Any]: return self._config - def get(self, setting_name, default=None): # type: (str, Any) -> Any + def get(self, setting_name: str, default: Any = None) -> Any: """ Retrieve a setting value. """ @@ -124,21 +127,20 @@ def get(self, setting_name, default=None): # type: (str, Any) -> Any return self.process(value) - def process(self, value): # type: (Any) -> Any - if not isinstance(value, basestring): + def process(self, value: Any) -> Any: + if not isinstance(value, str): return value return re.sub(r"{(.+?)}", lambda m: self.get(m.group(1)), value) - def _get_validator(self, name): # type: (str) -> Callable - if name in {"virtualenvs.create", "virtualenvs.in-project"}: - return boolean_validator - - if name == "virtualenvs.path": - return str - - def _get_normalizer(self, name): # type: (str) -> Callable - if name in {"virtualenvs.create", "virtualenvs.in-project"}: + def _get_normalizer(self, name: str) -> Callable: + if name in { + "virtualenvs.create", + "virtualenvs.in-project", + "virtualenvs.options.always-copy", + "virtualenvs.options.system-site-packages", + "installer.parallel", + }: return boolean_normalizer if name == "virtualenvs.path": diff --git a/poetry/config/config_source.py b/poetry/config/config_source.py index 63a4ad6b628..2fc9b585ac6 100644 --- a/poetry/config/config_source.py +++ b/poetry/config/config_source.py @@ -1,9 +1,9 @@ from typing import Any -class ConfigSource(object): - def add_property(self, key, value): # type: (str, Any) -> None +class ConfigSource: + def add_property(self, key: str, value: Any) -> None: raise NotImplementedError() - def remove_property(self, key): # type: (str) -> None + def remove_property(self, key: str) -> None: raise NotImplementedError() diff --git a/poetry/config/dict_config_source.py b/poetry/config/dict_config_source.py index aaa6ee3b9d1..941e39e6be6 100644 --- a/poetry/config/dict_config_source.py +++ b/poetry/config/dict_config_source.py @@ -5,14 +5,14 @@ class DictConfigSource(ConfigSource): - def __init__(self): # type: () -> None + def __init__(self) -> None: self._config = {} @property - def config(self): # type: () -> Dict[str, Any] + def config(self) -> Dict[str, Any]: return self._config - def add_property(self, key, value): # type: (str, Any) -> None + def add_property(self, key: str, value: Any) -> None: keys = key.split(".") config = self._config @@ -26,7 +26,7 @@ def add_property(self, key, value): # type: (str, Any) -> None config = config[key] - def remove_property(self, key): # type: (str) -> None + def remove_property(self, key: str) -> None: keys = key.split(".") config = self._config diff --git a/poetry/config/file_config_source.py b/poetry/config/file_config_source.py index ed4e3a8522e..cfcdfced45b 100644 --- a/poetry/config/file_config_source.py +++ b/poetry/config/file_config_source.py @@ -1,6 +1,7 @@ from contextlib import contextmanager from typing import TYPE_CHECKING from typing import Any +from typing import Iterator from tomlkit import document from tomlkit import table @@ -9,23 +10,25 @@ if TYPE_CHECKING: - from poetry.core.toml.file import TOMLFile # noqa + from tomlkit.toml_document import TOMLDocument + + from poetry.core.toml.file import TOMLFile class FileConfigSource(ConfigSource): - def __init__(self, file, auth_config=False): # type: ("TOMLFile", bool) -> None + def __init__(self, file: "TOMLFile", auth_config: bool = False) -> None: self._file = file self._auth_config = auth_config @property - def name(self): # type: () -> str + def name(self) -> str: return str(self._file.path) @property - def file(self): # type: () -> "TOMLFile" + def file(self) -> "TOMLFile": return self._file - def add_property(self, key, value): # type: (str, Any) -> None + def add_property(self, key: str, value: Any) -> None: with self.secure() as config: keys = key.split(".") @@ -39,7 +42,7 @@ def add_property(self, key, value): # type: (str, Any) -> None config = config[key] - def remove_property(self, key): # type: (str) -> None + def remove_property(self, key: str) -> None: with self.secure() as config: keys = key.split(".") @@ -56,7 +59,7 @@ def remove_property(self, key): # type: (str) -> None current_config = current_config[key] @contextmanager - def secure(self): + def secure(self) -> Iterator["TOMLDocument"]: if self.file.exists(): initial_config = self.file.read() config = self.file.read() diff --git a/poetry/config/source.py b/poetry/config/source.py new file mode 100644 index 00000000000..3735b193dfa --- /dev/null +++ b/poetry/config/source.py @@ -0,0 +1,15 @@ +import dataclasses + +from typing import Dict +from typing import Union + + +@dataclasses.dataclass(order=True, eq=True) +class Source: + name: str + url: str + default: bool = dataclasses.field(default=False) + secondary: bool = dataclasses.field(default=False) + + def to_dict(self) -> Dict[str, Union[str, bool]]: + return dataclasses.asdict(self) diff --git a/poetry/console/__init__.py b/poetry/console/__init__.py index c0c25738482..e69de29bb2d 100644 --- a/poetry/console/__init__.py +++ b/poetry/console/__init__.py @@ -1,5 +0,0 @@ -from .application import Application - - -def main(): - return Application().run() diff --git a/poetry/console/application.py b/poetry/console/application.py index e1d7c2aa525..42cc4377043 100644 --- a/poetry/console/application.py +++ b/poetry/console/application.py @@ -1,114 +1,339 @@ -import sys +import logging +import re -from cleo import Application as BaseApplication +from importlib import import_module +from typing import TYPE_CHECKING +from typing import Any +from typing import Callable +from typing import Optional +from typing import Type +from typing import cast + +from cleo.application import Application as BaseApplication +from cleo.events.console_command_event import ConsoleCommandEvent +from cleo.events.console_events import COMMAND +from cleo.events.event_dispatcher import EventDispatcher +from cleo.exceptions import CleoException +from cleo.formatters.style import Style +from cleo.io.inputs.argv_input import ArgvInput +from cleo.io.inputs.input import Input +from cleo.io.io import IO +from cleo.io.outputs.output import Output from poetry.__version__ import __version__ -from .commands.about import AboutCommand -from .commands.add import AddCommand -from .commands.build import BuildCommand -from .commands.cache.cache import CacheCommand -from .commands.check import CheckCommand -from .commands.config import ConfigCommand -from .commands.debug.debug import DebugCommand -from .commands.env.env import EnvCommand -from .commands.export import ExportCommand -from .commands.init import InitCommand -from .commands.install import InstallCommand -from .commands.lock import LockCommand -from .commands.new import NewCommand -from .commands.publish import PublishCommand -from .commands.remove import RemoveCommand -from .commands.run import RunCommand -from .commands.search import SearchCommand -from .commands.self.self import SelfCommand -from .commands.shell import ShellCommand -from .commands.show import ShowCommand -from .commands.update import UpdateCommand -from .commands.version import VersionCommand -from .config import ApplicationConfig +from .command_loader import CommandLoader +from .commands.command import Command -class Application(BaseApplication): - def __init__(self): - super(Application, self).__init__( - "poetry", __version__, config=ApplicationConfig("poetry", __version__) +def load_command(name: str) -> Callable: + def _load() -> Type[Command]: + module = import_module( + "poetry.console.commands.{}".format(".".join(name.split(" "))) + ) + command_class = getattr( + module, "{}Command".format("".join(c.title() for c in name.split(" "))) ) + return command_class() + + return _load + + +COMMANDS = [ + "about", + "add", + "build", + "check", + "config", + "export", + "init", + "install", + "lock", + "new", + "publish", + "remove", + "run", + "search", + "shell", + "show", + "update", + "version", + # Cache commands + "cache clear", + "cache list", + # Debug commands + "debug info", + "debug resolve", + # Env commands + "env info", + "env list", + "env remove", + "env use", + # Plugin commands + "plugin add", + "plugin remove", + "plugin show", + # Self commands + "self update", + # Source commands + "source add", + "source remove", + "source show", +] + +if TYPE_CHECKING: + from cleo.io.inputs.definition import Definition + + from poetry.console.commands.installer_command import InstallerCommand + from poetry.poetry import Poetry + + +class Application(BaseApplication): + def __init__(self) -> None: + super().__init__("poetry", __version__) + self._poetry = None + self._io: Optional[IO] = None + self._disable_plugins = False + self._plugins_loaded = False + + dispatcher = EventDispatcher() + dispatcher.add_listener(COMMAND, self.register_command_loggers) + dispatcher.add_listener(COMMAND, self.configure_env) + dispatcher.add_listener(COMMAND, self.configure_installer) + self.set_event_dispatcher(dispatcher) - for command in self.get_default_commands(): - self.add(command) - - if sys.version_info[:2] < (3, 6): - python_version = "{}".format( - ".".join(str(v) for v in sys.version_info[:2]) - ) - poetry_feature_release = "1.2" - message = ( - "\n" - "Python {} will no longer be supported " - "in the next feature release of Poetry ({}).\n" - "You should consider updating your Python version to a supported one.\n\n" - "" - "Note that you will still be able to manage Python {} projects " - "by using the env command.\n" - "See https://python-poetry.org/docs/managing-environments/ " - "for more information." - ).format(python_version, poetry_feature_release, python_version) - self._preliminary_io.write_line("{}\n".format(message)) + command_loader = CommandLoader({name: load_command(name) for name in COMMANDS}) + self.set_command_loader(command_loader) @property - def poetry(self): + def poetry(self) -> "Poetry": + from pathlib import Path + from poetry.factory import Factory - from poetry.utils._compat import Path if self._poetry is not None: return self._poetry - self._poetry = Factory().create_poetry(Path.cwd()) + self._poetry = Factory().create_poetry( + Path.cwd(), io=self._io, disable_plugins=self._disable_plugins + ) return self._poetry - def reset_poetry(self): # type: () -> None + @property + def command_loader(self) -> CommandLoader: + return self._command_loader + + def reset_poetry(self) -> None: self._poetry = None - def get_default_commands(self): # type: () -> list - commands = [ - AboutCommand(), - AddCommand(), - BuildCommand(), - CheckCommand(), - ConfigCommand(), - ExportCommand(), - InitCommand(), - InstallCommand(), - LockCommand(), - NewCommand(), - PublishCommand(), - RemoveCommand(), - RunCommand(), - SearchCommand(), - ShellCommand(), - ShowCommand(), - UpdateCommand(), - VersionCommand(), + def create_io( + self, + input: Optional[Input] = None, + output: Optional[Output] = None, + error_output: Optional[Output] = None, + ) -> IO: + io = super().create_io(input, output, error_output) + + # Set our own CLI styles + formatter = io.output.formatter + formatter.set_style("c1", Style("cyan")) + formatter.set_style("c2", Style("default", options=["bold"])) + formatter.set_style("info", Style("blue")) + formatter.set_style("comment", Style("green")) + formatter.set_style("warning", Style("yellow")) + formatter.set_style("debug", Style("default", options=["dark"])) + formatter.set_style("success", Style("green")) + + # Dark variants + formatter.set_style("c1_dark", Style("cyan", options=["dark"])) + formatter.set_style("c2_dark", Style("default", options=["bold", "dark"])) + formatter.set_style("success_dark", Style("green", options=["dark"])) + + io.output.set_formatter(formatter) + io.error_output.set_formatter(formatter) + + self._io = io + + return io + + def _run(self, io: IO) -> int: + self._disable_plugins = io.input.parameter_option("--no-plugins") + + self._load_plugins(io) + + return super()._run(io) + + def _configure_io(self, io: IO) -> None: + # We need to check if the command being run + # is the "run" command. + definition = self.definition + try: + io.input.bind(definition) + except CleoException: + pass + + name = io.input.first_argument + if name == "run": + from .io.inputs.run_argv_input import RunArgvInput + + input = cast(ArgvInput, io.input) + run_input = RunArgvInput([self._name or ""] + input._tokens) + # For the run command reset the definition + # with only the set options (i.e. the options given before the command) + for option_name, value in input.options.items(): + if value: + option = definition.option(option_name) + run_input.add_parameter_option("--" + option.name) + if option.shortcut: + shortcuts = re.split(r"\|-?", option.shortcut.lstrip("-")) + shortcuts = [s for s in shortcuts if s] + for shortcut in shortcuts: + run_input.add_parameter_option("-" + shortcut.lstrip("-")) + + try: + run_input.bind(definition) + except CleoException: + pass + + for option_name, value in input.options.items(): + if value: + run_input.set_option(option_name, value) + + io.set_input(run_input) + + return super()._configure_io(io) + + def register_command_loggers( + self, event: ConsoleCommandEvent, event_name: str, _: Any + ) -> None: + from .logging.io_formatter import IOFormatter + from .logging.io_handler import IOHandler + + command = event.command + if not isinstance(command, Command): + return + + io = event.io + + loggers = [ + "poetry.packages.locker", + "poetry.packages.package", + "poetry.utils.password_manager", ] - # Cache commands - commands += [CacheCommand()] + loggers += command.loggers + + handler = IOHandler(io) + handler.setFormatter(IOFormatter()) + + for logger in loggers: + logger = logging.getLogger(logger) + + logger.handlers = [handler] + + level = logging.WARNING + # The builders loggers are special and we can actually + # start at the INFO level. + if logger.name.startswith("poetry.core.masonry.builders"): + level = logging.INFO + + if io.is_debug(): + level = logging.DEBUG + elif io.is_very_verbose() or io.is_verbose(): + level = logging.INFO + + logger.setLevel(level) + + def configure_env( + self, event: ConsoleCommandEvent, event_name: str, _: Any + ) -> None: + from .commands.env_command import EnvCommand + + command: EnvCommand = cast(EnvCommand, event.command) + if not isinstance(command, EnvCommand): + return + + if command.env is not None: + return + + from poetry.utils.env import EnvManager + + io = event.io + poetry = command.poetry + + env_manager = EnvManager(poetry) + env = env_manager.create_venv(io) + + if env.is_venv() and io.is_verbose(): + io.write_line(f"Using virtualenv: {env.path}") + + command.set_env(env) - # Debug command - commands += [DebugCommand()] + def configure_installer( + self, event: ConsoleCommandEvent, event_name: str, _: Any + ) -> None: + from .commands.installer_command import InstallerCommand + + command: InstallerCommand = cast(InstallerCommand, event.command) + if not isinstance(command, InstallerCommand): + return + + # If the command already has an installer + # we skip this step + if command.installer is not None: + return + + self._configure_installer(command, event.io) + + def _configure_installer(self, command: "InstallerCommand", io: "IO") -> None: + from poetry.installation.installer import Installer + + poetry = command.poetry + installer = Installer( + io, + command.env, + poetry.package, + poetry.locker, + poetry.pool, + poetry.config, + ) + installer.use_executor(poetry.config.get("experimental.new-installer", False)) + command.set_installer(installer) + + def _load_plugins(self, io: IO) -> None: + if self._plugins_loaded: + return + + self._disable_plugins = io.input.has_parameter_option("--no-plugins") + + if not self._disable_plugins: + from poetry.plugins.plugin_manager import PluginManager + + manager = PluginManager("application.plugin") + manager.load_plugins() + manager.activate(self) + + self._plugins_loaded = True + + @property + def _default_definition(self) -> "Definition": + from cleo.io.inputs.option import Option + + definition = super()._default_definition + + definition.add_option( + Option("--no-plugins", flag=True, description="Disables plugins.") + ) - # Env command - commands += [EnvCommand()] + return definition - # Self commands - commands += [SelfCommand()] - return commands +def main() -> int: + return Application().run() if __name__ == "__main__": - Application().run() + main() diff --git a/poetry/console/args/run_args_parser.py b/poetry/console/args/run_args_parser.py deleted file mode 100644 index 9f8cff8b209..00000000000 --- a/poetry/console/args/run_args_parser.py +++ /dev/null @@ -1,41 +0,0 @@ -from clikit.api.args import Args -from clikit.api.args import RawArgs -from clikit.api.args.format import ArgsFormat -from clikit.api.args.format import ArgsFormatBuilder -from clikit.args import DefaultArgsParser - - -class RunArgsParser(DefaultArgsParser): - """ - Parser that just parses command names and leave the rest - alone to be passed to the command. - """ - - def parse( - self, args, fmt, lenient=False - ): # type: (RawArgs, ArgsFormat, bool) -> Args - builder = ArgsFormatBuilder() - builder.set_command_names(*fmt.get_command_names()) - builder.set_arguments(*fmt.get_arguments().values()) - fmt = builder.format - - return super(RunArgsParser, self).parse(args, fmt, True) - - def _parse( - self, raw_args, fmt, lenient - ): # type: (RawArgs, ArgsFormat, bool) -> None - """ - Parse everything as a single, multi-valued argument. - """ - tokens = raw_args.tokens[:] - - last_arg = list(fmt.get_arguments().values())[-1] - self._arguments[last_arg.name] = [] - - while True: - try: - token = tokens.pop(0) - except IndexError: - break - - self._arguments[last_arg.name].append(token) diff --git a/poetry/console/command_loader.py b/poetry/console/command_loader.py new file mode 100644 index 00000000000..852abe07dc6 --- /dev/null +++ b/poetry/console/command_loader.py @@ -0,0 +1,12 @@ +from typing import Callable + +from cleo.exceptions import LogicException +from cleo.loaders.factory_command_loader import FactoryCommandLoader + + +class CommandLoader(FactoryCommandLoader): + def register_factory(self, command_name: str, factory: Callable) -> None: + if command_name in self._factories: + raise LogicException(f'The command "{command_name}" already exists.') + + self._factories[command_name] = factory diff --git a/poetry/console/commands/__init__.py b/poetry/console/commands/__init__.py index b8cb3f4e1f4..e69de29bb2d 100644 --- a/poetry/console/commands/__init__.py +++ b/poetry/console/commands/__init__.py @@ -1,18 +0,0 @@ -from .about import AboutCommand -from .add import AddCommand -from .build import BuildCommand -from .check import CheckCommand -from .config import ConfigCommand -from .export import ExportCommand -from .init import InitCommand -from .install import InstallCommand -from .lock import LockCommand -from .new import NewCommand -from .publish import PublishCommand -from .remove import RemoveCommand -from .run import RunCommand -from .search import SearchCommand -from .shell import ShellCommand -from .show import ShowCommand -from .update import UpdateCommand -from .version import VersionCommand diff --git a/poetry/console/commands/about.py b/poetry/console/commands/about.py index a84a2b6fec6..76461852fd7 100644 --- a/poetry/console/commands/about.py +++ b/poetry/console/commands/about.py @@ -7,7 +7,7 @@ class AboutCommand(Command): description = "Shows information about Poetry." - def handle(self): + def handle(self) -> None: self.line( """Poetry - Package Management for Python diff --git a/poetry/console/commands/add.py b/poetry/console/commands/add.py index 3ad77ad686b..777f3744787 100644 --- a/poetry/console/commands/add.py +++ b/poetry/console/commands/add.py @@ -2,8 +2,8 @@ from typing import Dict from typing import List -from cleo import argument -from cleo import option +from cleo.helpers import argument +from cleo.helpers import option from .init import InitCommand from .installer_command import InstallerCommand @@ -17,6 +17,7 @@ class AddCommand(InstallerCommand, InitCommand): arguments = [argument("name", "The packages to add.", multiple=True)] options = [ option("dev", "D", "Add as a development dependency."), + option("editable", "e", "Add vcs/path dependencies as editable."), option( "extras", "E", @@ -59,6 +60,8 @@ class AddCommand(InstallerCommand, InitCommand): " - A name and a constraint (requests@^2.23.0)\n" " - A git url (git+https://github.com/python-poetry/poetry.git)\n" " - A git url with a revision (git+https://github.com/python-poetry/poetry.git#develop)\n" + " - A git SSH url (git+ssh://github.com/python-poetry/poetry.git)\n" + " - A git SSH url with a revision (git+ssh://github.com/python-poetry/poetry.git#develop)\n" " - A file path (../my-package/my-package.whl)\n" " - A directory (../my-package/)\n" " - A url (https://example.com/packages/my-package-0.1.0.tar.gz)\n" @@ -66,10 +69,10 @@ class AddCommand(InstallerCommand, InitCommand): loggers = ["poetry.repositories.pypi_repository", "poetry.inspection.info"] - def handle(self): + def handle(self) -> int: from tomlkit import inline_table - from poetry.core.semver import parse_constraint + from poetry.core.semver.helpers import parse_constraint packages = self.argument("name") is_dev = self.option("dev") @@ -100,7 +103,6 @@ def handle(self): packages = [name for name in packages if name not in existing_packages] if not packages: - self.poetry.file.write(content) self.line("Nothing to add.") return 0 @@ -138,6 +140,19 @@ def handle(self): constraint["extras"] = self.option("extras") + if self.option("editable"): + if "git" in _constraint or "path" in _constraint: + constraint["develop"] = True + else: + self.line_error( + "\n" + "Failed to add packages. " + "Only vcs/path dependencies support editable installs. " + f"{_constraint['name']} is neither." + ) + self.line_error("\nNo changes were applied.") + return 1 + if self.option("python"): constraint["python"] = self.option("python") @@ -152,29 +167,29 @@ def handle(self): poetry_content[section][_constraint["name"]] = constraint - # Write new content - self.poetry.file.write(content) + try: + # Write new content + self.poetry.file.write(content) - # Cosmetic new line - self.line("") + # Cosmetic new line + self.line("") - # Update packages - self.reset_poetry() + # Update packages + self.reset_poetry() - self._installer.set_package(self.poetry.package) - self._installer.dry_run(self.option("dry-run")) - self._installer.verbose(self._io.is_verbose()) - self._installer.update(True) - if self.option("lock"): - self._installer.lock() + self._installer.set_package(self.poetry.package) + self._installer.dry_run(self.option("dry-run")) + self._installer.verbose(self._io.is_verbose()) + self._installer.update(True) + if self.option("lock"): + self._installer.lock() - self._installer.whitelist([r["name"] for r in requirements]) + self._installer.whitelist([r["name"] for r in requirements]) - try: status = self._installer.run() - except Exception: + except BaseException: + # Using BaseException here as some exceptions, eg: KeyboardInterrupt, do not inherit from Exception self.poetry.file.write(original_content) - raise if status != 0 or self.option("dry-run"): @@ -191,8 +206,8 @@ def handle(self): return status def get_existing_packages_from_input( - self, packages, poetry_content, target_section - ): # type: (List[str], Dict, str) -> List[str] + self, packages: List[str], poetry_content: Dict, target_section: str + ) -> List[str]: existing_packages = [] for name in packages: @@ -202,9 +217,7 @@ def get_existing_packages_from_input( return existing_packages - def notify_about_existing_packages( - self, existing_packages - ): # type: (List[str]) -> None + def notify_about_existing_packages(self, existing_packages: List[str]) -> None: self.line( "The following packages are already present in the pyproject.toml and will be skipped:\n" ) diff --git a/poetry/console/commands/build.py b/poetry/console/commands/build.py index 118fb210c53..c12fe9ab13d 100644 --- a/poetry/console/commands/build.py +++ b/poetry/console/commands/build.py @@ -1,4 +1,4 @@ -from cleo import option +from cleo.helpers import option from .env_command import EnvCommand @@ -18,8 +18,8 @@ class BuildCommand(EnvCommand): "poetry.core.masonry.builders.wheel", ] - def handle(self): - from poetry.core.masonry import Builder + def handle(self) -> None: + from poetry.core.masonry.builder import Builder fmt = "all" if self.option("format"): @@ -33,4 +33,4 @@ def handle(self): ) builder = Builder(self.poetry) - builder.build(fmt) + builder.build(fmt, executable=self.env.python) diff --git a/poetry/console/commands/cache/cache.py b/poetry/console/commands/cache/cache.py deleted file mode 100644 index 695e27e0af7..00000000000 --- a/poetry/console/commands/cache/cache.py +++ /dev/null @@ -1,15 +0,0 @@ -from poetry.console.commands.cache.list import CacheListCommand - -from ..command import Command -from .clear import CacheClearCommand - - -class CacheCommand(Command): - - name = "cache" - description = "Interact with Poetry's cache" - - commands = [CacheClearCommand(), CacheListCommand()] - - def handle(self): - return self.call("help", self._config.name) diff --git a/poetry/console/commands/cache/clear.py b/poetry/console/commands/cache/clear.py index 42e71091526..7bacdb891cb 100644 --- a/poetry/console/commands/cache/clear.py +++ b/poetry/console/commands/cache/clear.py @@ -1,20 +1,20 @@ import os -from cleo import argument -from cleo import option +from cleo.helpers import argument +from cleo.helpers import option from ..command import Command class CacheClearCommand(Command): - name = "clear" + name = "cache clear" description = "Clears Poetry's cache." arguments = [argument("cache", description="The name of the cache to clear.")] options = [option("all", description="Clear all entries in the cache.")] - def handle(self): + def handle(self) -> int: from cachy import CacheManager from poetry.locations import REPOSITORY_CACHE_DIR diff --git a/poetry/console/commands/cache/list.py b/poetry/console/commands/cache/list.py index 6a030fa2eba..b22dc9c312c 100644 --- a/poetry/console/commands/cache/list.py +++ b/poetry/console/commands/cache/list.py @@ -1,14 +1,16 @@ import os +from typing import Optional + from ..command import Command class CacheListCommand(Command): - name = "list" + name = "cache list" description = "List Poetry's caches." - def handle(self): + def handle(self) -> Optional[int]: from poetry.locations import REPOSITORY_CACHE_DIR if os.path.exists(str(REPOSITORY_CACHE_DIR)): diff --git a/poetry/console/commands/check.py b/poetry/console/commands/check.py index bb97da14640..f6531dab992 100644 --- a/poetry/console/commands/check.py +++ b/poetry/console/commands/check.py @@ -1,6 +1,7 @@ +from pathlib import Path + from poetry.core.pyproject.toml import PyProjectTOML from poetry.factory import Factory -from poetry.utils._compat import Path from .command import Command @@ -10,7 +11,7 @@ class CheckCommand(Command): name = "check" description = "Checks the validity of the pyproject.toml file." - def handle(self): + def handle(self) -> int: # Load poetry config and display errors, if any poetry_file = Factory.locate(Path.cwd()) config = PyProjectTOML(poetry_file).poetry_config diff --git a/poetry/console/commands/command.py b/poetry/console/commands/command.py index 1e22142341a..a717fa4e666 100644 --- a/poetry/console/commands/command.py +++ b/poetry/console/commands/command.py @@ -1,13 +1,31 @@ -from cleo import Command as BaseCommand +from typing import TYPE_CHECKING +from typing import Optional +from cleo.commands.command import Command as BaseCommand -class Command(BaseCommand): +if TYPE_CHECKING: + from poetry.console.application import Application + from poetry.poetry import Poetry + + +class Command(BaseCommand): loggers = [] + _poetry: Optional["Poetry"] = None + @property - def poetry(self): - return self.application.poetry + def poetry(self) -> "Poetry": + if self._poetry is None: + return self.get_application().poetry + + return self._poetry + + def set_poetry(self, poetry: "Poetry") -> None: + self._poetry = poetry + + def get_application(self) -> "Application": + return self.application - def reset_poetry(self): # type: () -> None - self.application.reset_poetry() + def reset_poetry(self) -> None: + self.get_application().reset_poetry() diff --git a/poetry/console/commands/config.py b/poetry/console/commands/config.py index 551876912c2..300245955d0 100644 --- a/poetry/console/commands/config.py +++ b/poetry/console/commands/config.py @@ -1,15 +1,23 @@ import json import re -from cleo import argument -from cleo import option +from typing import TYPE_CHECKING +from typing import Any +from typing import Dict +from typing import List +from typing import Optional +from typing import Tuple -from poetry.core.toml.file import TOMLFile -from poetry.factory import Factory +from cleo.helpers import argument +from cleo.helpers import option from .command import Command +if TYPE_CHECKING: + from poetry.config.config_source import ConfigSource + + class ConfigCommand(Command): name = "config" @@ -39,11 +47,12 @@ class ConfigCommand(Command): LIST_PROHIBITED_SETTINGS = {"http-basic", "pypi-token"} @property - def unique_config_values(self): + def unique_config_values(self) -> Dict[str, Tuple[Any, Any, Any]]: + from pathlib import Path + from poetry.config.config import boolean_normalizer from poetry.config.config import boolean_validator from poetry.locations import CACHE_DIR - from poetry.utils._compat import Path unique_config_values = { "cache-dir": ( @@ -53,6 +62,16 @@ def unique_config_values(self): ), "virtualenvs.create": (boolean_validator, boolean_normalizer, True), "virtualenvs.in-project": (boolean_validator, boolean_normalizer, False), + "virtualenvs.options.always-copy": ( + boolean_validator, + boolean_normalizer, + False, + ), + "virtualenvs.options.system-site-packages": ( + boolean_validator, + boolean_normalizer, + False, + ), "virtualenvs.path": ( str, lambda val: str(Path(val)), @@ -63,15 +82,23 @@ def unique_config_values(self): boolean_normalizer, True, ), + "installer.parallel": ( + boolean_validator, + boolean_normalizer, + True, + ), } return unique_config_values - def handle(self): + def handle(self) -> Optional[int]: + from pathlib import Path + from poetry.config.file_config_source import FileConfigSource + from poetry.core.pyproject.exceptions import PyProjectException + from poetry.core.toml.file import TOMLFile + from poetry.factory import Factory from poetry.locations import CONFIG_DIR - from poetry.utils._compat import Path - from poetry.utils._compat import basestring config = Factory.create_config(self.io) config_file = TOMLFile(Path(CONFIG_DIR) / "config.toml") @@ -80,7 +107,7 @@ def handle(self): local_config_file = TOMLFile(self.poetry.file.parent / "poetry.toml") if local_config_file.exists(): config.merge(local_config_file.read()) - except RuntimeError: + except (RuntimeError, PyProjectException): local_config_file = TOMLFile(Path.cwd() / "poetry.toml") if self.option("local"): @@ -127,7 +154,7 @@ def handle(self): value = config.get(setting_key) - if not isinstance(value, basestring): + if not isinstance(value, str): value = json.dumps(value) self.line(value) @@ -245,7 +272,13 @@ def handle(self): raise ValueError("Setting {} does not exist".format(self.argument("key"))) - def _handle_single_value(self, source, key, callbacks, values): + def _handle_single_value( + self, + source: "ConfigSource", + key: str, + callbacks: Tuple[Any, Any, Any], + values: List[Any], + ) -> int: validator, normalizer, _ = callbacks if len(values) > 1: @@ -259,9 +292,7 @@ def _handle_single_value(self, source, key, callbacks, values): return 0 - def _list_configuration(self, config, raw, k=""): - from poetry.utils._compat import basestring - + def _list_configuration(self, config: Dict, raw: Dict, k: str = "") -> None: orig_k = k for key, value in sorted(config.items()): if k + key in self.LIST_PROHIBITED_SETTINGS: @@ -286,7 +317,7 @@ def _list_configuration(self, config, raw, k=""): message = "{} = {}".format( k + key, json.dumps(raw_val) ) - elif isinstance(raw_val, basestring) and raw_val != value: + elif isinstance(raw_val, str) and raw_val != value: message = "{} = {} # {}".format( k + key, json.dumps(raw_val), value ) @@ -295,15 +326,13 @@ def _list_configuration(self, config, raw, k=""): self.line(message) - def _list_setting(self, contents, setting=None, k=None, default=None): - values = self._get_setting(contents, setting, k, default) - - for value in values: - self.line( - "{} = {}".format(value[0], value[1]) - ) - - def _get_setting(self, contents, setting=None, k=None, default=None): + def _get_setting( + self, + contents: Dict, + setting: Optional[str] = None, + k: Optional[str] = None, + default: Optional[Any] = None, + ) -> List[Tuple[str, str]]: orig_k = k if setting and setting.split(".")[0] not in contents: @@ -344,11 +373,3 @@ def _get_setting(self, contents, setting=None, k=None, default=None): values.append(((k or "") + key, value)) return values - - def _get_formatted_value(self, value): - if isinstance(value, list): - value = [json.dumps(val) if isinstance(val, list) else val for val in value] - - value = "[{}]".format(", ".join(value)) - - return json.dumps(value) diff --git a/poetry/console/commands/debug/debug.py b/poetry/console/commands/debug/debug.py deleted file mode 100644 index 468e2faad1f..00000000000 --- a/poetry/console/commands/debug/debug.py +++ /dev/null @@ -1,11 +0,0 @@ -from ..command import Command -from .info import DebugInfoCommand -from .resolve import DebugResolveCommand - - -class DebugCommand(Command): - - name = "debug" - description = "Debug various elements of Poetry." - - commands = [DebugInfoCommand().default(), DebugResolveCommand()] diff --git a/poetry/console/commands/debug/info.py b/poetry/console/commands/debug/info.py index 81096a6ffc8..b007d93d4c8 100644 --- a/poetry/console/commands/debug/info.py +++ b/poetry/console/commands/debug/info.py @@ -1,16 +1,14 @@ import sys -from clikit.args import StringArgs - from ..command import Command class DebugInfoCommand(Command): - name = "info" + name = "debug info" description = "Shows debug information." - def handle(self): + def handle(self) -> int: poetry_python_version = ".".join(str(s) for s in sys.version_info[:3]) self.line("") @@ -25,7 +23,6 @@ def handle(self): ] ) ) - args = StringArgs("") - command = self.application.get_command("env").get_sub_command("info") + command = self.application.get("env info") - return command.run(args, self._io) + return command.run(self._io) diff --git a/poetry/console/commands/debug/resolve.py b/poetry/console/commands/debug/resolve.py index 52ae1951b21..06b0fa9d21f 100644 --- a/poetry/console/commands/debug/resolve.py +++ b/poetry/console/commands/debug/resolve.py @@ -1,12 +1,20 @@ -from cleo import argument -from cleo import option +from typing import TYPE_CHECKING +from typing import Optional + +from cleo.helpers import argument +from cleo.helpers import option +from cleo.io.outputs.output import Verbosity from ..init import InitCommand +if TYPE_CHECKING: + from poetry.console.commands.show import ShowCommand + + class DebugResolveCommand(InitCommand): - name = "resolve" + name = "debug resolve" description = "Debugs dependency resolution." arguments = [ @@ -27,10 +35,11 @@ class DebugResolveCommand(InitCommand): loggers = ["poetry.repositories.pypi_repository", "poetry.inspection.info"] - def handle(self): + def handle(self) -> Optional[int]: + from cleo.io.null_io import NullIO + from poetry.core.packages.project_package import ProjectPackage from poetry.factory import Factory - from poetry.io.null_io import NullIO from poetry.puzzle import Solver from poetry.repositories.pool import Pool from poetry.repositories.repository import Repository @@ -49,14 +58,12 @@ def handle(self): ) # Silencing output - is_quiet = self.io.output.is_quiet() - if not is_quiet: - self.io.output.set_quiet(True) + verbosity = self.io.output.verbosity + self.io.output.set_verbosity(Verbosity.QUIET) requirements = self._determine_requirements(packages) - if not is_quiet: - self.io.output.set_quiet(False) + self.io.output.set_verbosity(verbosity) for constraint in requirements: name = constraint.pop("name") @@ -86,7 +93,7 @@ def handle(self): self.line("") if self.option("tree"): - show_command = self.application.find("show") + show_command: ShowCommand = self.application.find("show") show_command.init_styles(self.io) packages = [op.package for op in ops] @@ -101,7 +108,8 @@ def handle(self): return 0 - table = self.table([], style="borderless") + table = self.table([], style="compact") + table.style.set_vertical_border_chars("", " ") rows = [] if self.option("install"): @@ -134,4 +142,4 @@ def handle(self): rows.append(row) table.set_rows(rows) - table.render(self.io) + table.render() diff --git a/poetry/console/commands/env/env.py b/poetry/console/commands/env/env.py deleted file mode 100644 index f979b66e436..00000000000 --- a/poetry/console/commands/env/env.py +++ /dev/null @@ -1,16 +0,0 @@ -from ..command import Command -from .info import EnvInfoCommand -from .list import EnvListCommand -from .remove import EnvRemoveCommand -from .use import EnvUseCommand - - -class EnvCommand(Command): - - name = "env" - description = "Interact with Poetry's project environments." - - commands = [EnvInfoCommand(), EnvListCommand(), EnvRemoveCommand(), EnvUseCommand()] - - def handle(self): # type: () -> int - return self.call("help", self._config.name) diff --git a/poetry/console/commands/env/info.py b/poetry/console/commands/env/info.py index 301d88f9520..aecce3628ac 100644 --- a/poetry/console/commands/env/info.py +++ b/poetry/console/commands/env/info.py @@ -1,16 +1,23 @@ -from cleo import option +from typing import TYPE_CHECKING +from typing import Optional + +from cleo.helpers import option from ..command import Command +if TYPE_CHECKING: + from poetry.utils.env import Env + + class EnvInfoCommand(Command): - name = "info" + name = "env info" description = "Displays information about the current environment." options = [option("path", "p", "Only display the environment's path.")] - def handle(self): + def handle(self) -> Optional[int]: from poetry.utils.env import EnvManager env = EnvManager(self.poetry).get() @@ -25,7 +32,7 @@ def handle(self): self._display_complete_info(env) - def _display_complete_info(self, env): + def _display_complete_info(self, env: "Env") -> None: env_python_version = ".".join(str(s) for s in env.version_info[:3]) self.line("") self.line("Virtualenv") diff --git a/poetry/console/commands/env/list.py b/poetry/console/commands/env/list.py index 272a853b976..e7b1eac6cd5 100644 --- a/poetry/console/commands/env/list.py +++ b/poetry/console/commands/env/list.py @@ -1,16 +1,16 @@ -from cleo import option +from cleo.helpers import option from ..command import Command class EnvListCommand(Command): - name = "list" + name = "env list" description = "Lists all virtualenvs associated with the current project." options = [option("full-path", None, "Output the full paths of the virtualenvs.")] - def handle(self): + def handle(self) -> None: from poetry.utils.env import EnvManager manager = EnvManager(self.poetry) diff --git a/poetry/console/commands/env/remove.py b/poetry/console/commands/env/remove.py index 5f208851deb..9d5153c420b 100644 --- a/poetry/console/commands/env/remove.py +++ b/poetry/console/commands/env/remove.py @@ -1,18 +1,18 @@ -from cleo import argument +from cleo.helpers import argument from ..command import Command class EnvRemoveCommand(Command): - name = "remove" + name = "env remove" description = "Removes a specific virtualenv associated with the project." arguments = [ argument("python", "The python executable to remove the virtualenv for.") ] - def handle(self): + def handle(self) -> None: from poetry.utils.env import EnvManager manager = EnvManager(self.poetry) diff --git a/poetry/console/commands/env/use.py b/poetry/console/commands/env/use.py index ef9cf3def6b..fa8a455bcf7 100644 --- a/poetry/console/commands/env/use.py +++ b/poetry/console/commands/env/use.py @@ -1,16 +1,16 @@ -from cleo import argument +from cleo.helpers import argument from ..command import Command class EnvUseCommand(Command): - name = "use" + name = "env use" description = "Activates or creates a new virtualenv for the current project." arguments = [argument("python", "The python executable to use.")] - def handle(self): + def handle(self) -> None: from poetry.utils.env import EnvManager manager = EnvManager(self.poetry) diff --git a/poetry/console/commands/env_command.py b/poetry/console/commands/env_command.py index 2fb298d7bb2..fd44b415c00 100644 --- a/poetry/console/commands/env_command.py +++ b/poetry/console/commands/env_command.py @@ -1,15 +1,21 @@ +from typing import TYPE_CHECKING + from .command import Command +if TYPE_CHECKING: + from poetry.utils.env import Env + + class EnvCommand(Command): - def __init__(self): + def __init__(self) -> None: self._env = None super(EnvCommand, self).__init__() @property - def env(self): + def env(self) -> "Env": return self._env - def set_env(self, env): + def set_env(self, env: "Env") -> None: self._env = env diff --git a/poetry/console/commands/export.py b/poetry/console/commands/export.py index 126b657b937..8bce95d8af7 100644 --- a/poetry/console/commands/export.py +++ b/poetry/console/commands/export.py @@ -1,4 +1,4 @@ -from cleo import option +from cleo.helpers import option from poetry.utils.exporter import Exporter @@ -31,7 +31,7 @@ class ExportCommand(Command): option("with-credentials", None, "Include credentials for extra indices."), ] - def handle(self): + def handle(self) -> None: fmt = self.option("format") if fmt not in Exporter.ACCEPTED_FORMATS: @@ -44,13 +44,13 @@ def handle(self): self.line("The lock file does not exist. Locking.") options = [] if self.io.is_debug(): - options.append(("-vvv", None)) + options.append("-vvv") elif self.io.is_very_verbose(): - options.append(("-vv", None)) + options.append("-vv") elif self.io.is_verbose(): - options.append(("-v", None)) + options.append("-v") - self.call("lock", options) + self.call("lock", " ".join(options)) if not locker.is_fresh(): self.line( diff --git a/poetry/console/commands/init.py b/poetry/console/commands/init.py index d6817bee975..0d525dbe3e0 100644 --- a/poetry/console/commands/init.py +++ b/poetry/console/commands/init.py @@ -1,27 +1,27 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import os import re import sys +import urllib.parse +from pathlib import Path +from typing import TYPE_CHECKING from typing import Dict from typing import List +from typing import Optional from typing import Tuple from typing import Union -from cleo import option +from cleo.helpers import option from tomlkit import inline_table -from poetry.core.pyproject.toml import PyProjectTOML -from poetry.utils._compat import OrderedDict -from poetry.utils._compat import Path -from poetry.utils._compat import urlparse - from .command import Command from .env_command import EnvCommand +if TYPE_CHECKING: + from poetry.repositories import Pool + + class InitCommand(Command): name = "init" description = ( @@ -56,15 +56,17 @@ class InitCommand(Command): The init command creates a basic pyproject.toml file in the current directory. """ - def __init__(self): + def __init__(self) -> None: super(InitCommand, self).__init__() self._pool = None - def handle(self): + def handle(self) -> int: + from pathlib import Path + + from poetry.core.pyproject.toml import PyProjectTOML from poetry.core.vcs.git import GitConfig from poetry.layouts import layout - from poetry.utils._compat import Path from poetry.utils.env import SystemEnv pyproject = PyProjectTOML(Path.cwd() / "pyproject.toml") @@ -84,11 +86,12 @@ def handle(self): vcs_config = GitConfig() - self.line("") - self.line( - "This command will guide you through creating your pyproject.toml config." - ) - self.line("") + if self.io.is_interactive(): + self.line("") + self.line( + "This command will guide you through creating your pyproject.toml config." + ) + self.line("") name = self.option("name") if not name: @@ -152,7 +155,8 @@ def handle(self): ) python = self.ask(question) - self.line("") + if self.io.is_interactive(): + self.line("") requirements = {} if self.option("dependency"): @@ -173,12 +177,14 @@ def handle(self): ) help_displayed = False if self.confirm(question, True): - self.line(help_message) - help_displayed = True + if self.io.is_interactive(): + self.line(help_message) + help_displayed = True requirements.update( self._format_requirements(self._determine_requirements([])) ) - self.line("") + if self.io.is_interactive(): + self.line("") dev_requirements = {} if self.option("dev-dependency"): @@ -190,13 +196,14 @@ def handle(self): "Would you like to define your development dependencies interactively?" ) if self.confirm(question, True): - if not help_displayed: + if self.io.is_interactive() and not help_displayed: self.line(help_message) dev_requirements.update( self._format_requirements(self._determine_requirements([])) ) - self.line("") + if self.io.is_interactive(): + self.line("") layout_ = layout("standard")( name, @@ -225,8 +232,11 @@ def handle(self): f.write(content) def _determine_requirements( - self, requires, allow_prereleases=False, source=None - ): # type: (List[str], bool) -> List[Dict[str, str]] + self, + requires: List[str], + allow_prereleases: bool = False, + source: Optional[str] = None, + ) -> List[Dict[str, Union[str, List[str]]]]: if not requires: requires = [] @@ -312,7 +322,8 @@ def _determine_requirements( if package is not False: requires.append(constraint) - package = self.ask("\nAdd a package:") + if self.io.is_interactive(): + package = self.ask("\nAdd a package:") return requires @@ -352,8 +363,12 @@ def _determine_requirements( return result def _find_best_version_for_package( - self, name, required_version=None, allow_prereleases=False, source=None - ): # type: (...) -> Tuple[str, str] + self, + name: str, + required_version: Optional[str] = None, + allow_prereleases: bool = False, + source: Optional[str] = None, + ) -> Tuple[str, str]: from poetry.version.version_selector import VersionSelector selector = VersionSelector(self._get_pool()) @@ -369,16 +384,15 @@ def _find_best_version_for_package( return package.pretty_name, selector.find_recommended_require_version(package) - def _parse_requirements( - self, requirements - ): # type: (List[str]) -> List[Dict[str, str]] + def _parse_requirements(self, requirements: List[str]) -> List[Dict[str, str]]: + from poetry.core.pyproject.exceptions import PyProjectException from poetry.puzzle.provider import Provider result = [] try: cwd = self.poetry.file.parent - except RuntimeError: + except (PyProjectException, RuntimeError): cwd = Path.cwd() for requirement in requirements: @@ -389,7 +403,7 @@ def _parse_requirements( extras = [e.strip() for e in extras_m.group(1).split(",")] requirement, _ = requirement.split("[") - url_parsed = urlparse.urlparse(requirement) + url_parsed = urllib.parse.urlparse(requirement) if url_parsed.scheme and url_parsed.netloc: # Url if url_parsed.scheme in ["git+https", "git+ssh"]: @@ -399,7 +413,7 @@ def _parse_requirements( parsed = ParsedUrl.parse(requirement) url = Git.normalize_url(requirement) - pair = OrderedDict([("name", parsed.name), ("git", url.url)]) + pair = dict([("name", parsed.name), ("git", url.url)]) if parsed.rev: pair["rev"] = url.revision @@ -416,28 +430,38 @@ def _parse_requirements( elif url_parsed.scheme in ["http", "https"]: package = Provider.get_package_from_url(requirement) - pair = OrderedDict( - [("name", package.name), ("url", package.source_url)] - ) + pair = dict([("name", package.name), ("url", package.source_url)]) if extras: pair["extras"] = extras result.append(pair) continue - elif (os.path.sep in requirement or "/" in requirement) and cwd.joinpath( - requirement - ).exists(): - path = cwd.joinpath(requirement) + elif (os.path.sep in requirement or "/" in requirement) and ( + cwd.joinpath(requirement).exists() + or Path(requirement).expanduser().exists() + and Path(requirement).expanduser().is_absolute() + ): + path = Path(requirement).expanduser() + is_absolute = path.is_absolute() + + if not path.is_absolute(): + path = cwd.joinpath(requirement) + if path.is_file(): package = Provider.get_package_from_file(path.resolve()) else: - package = Provider.get_package_from_directory(path) + package = Provider.get_package_from_directory(path.resolve()) result.append( - OrderedDict( + dict( [ ("name", package.name), - ("path", path.relative_to(cwd).as_posix()), + ( + "path", + path.relative_to(cwd).as_posix() + if not is_absolute + else path.as_posix(), + ), ] + ([("extras", extras)] if extras else []) ) @@ -450,7 +474,7 @@ def _parse_requirements( ) pair = pair.strip() - require = OrderedDict() + require = dict() if " " in pair: name, version = pair.split(" ", 2) extras_m = re.search(r"\[([\w\d,-_]+)\]$", name) @@ -490,8 +514,8 @@ def _parse_requirements( return result def _format_requirements( - self, requirements - ): # type: (List[Dict[str, str]]) -> Dict[str, Union[str, Dict[str, str]]] + self, requirements: List[Dict[str, str]] + ) -> Dict[str, Union[str, Dict[str, str]]]: requires = {} for requirement in requirements: name = requirement.pop("name") @@ -506,7 +530,7 @@ def _format_requirements( return requires - def _validate_author(self, author, default): + def _validate_author(self, author: str, default: str) -> Optional[str]: from poetry.core.packages.package import AUTHOR_REGEX author = author or default @@ -523,15 +547,15 @@ def _validate_author(self, author, default): return author - def _validate_license(self, license): - from poetry.core.spdx import license_by_id + def _validate_license(self, license: str) -> str: + from poetry.core.spdx.helpers import license_by_id if license: license_by_id(license) return license - def _get_pool(self): + def _get_pool(self) -> "Pool": from poetry.repositories import Pool from poetry.repositories.pypi_repository import PyPiRepository diff --git a/poetry/console/commands/install.py b/poetry/console/commands/install.py index 6a9ef2cb41d..0ffb30a3756 100644 --- a/poetry/console/commands/install.py +++ b/poetry/console/commands/install.py @@ -1,4 +1,4 @@ -from cleo import option +from cleo.helpers import option from .installer_command import InstallerCommand @@ -10,6 +10,7 @@ class InstallCommand(InstallerCommand): options = [ option("no-dev", None, "Do not install the development dependencies."), + option("dev-only", None, "Only install the development dependencies."), option( "no-root", None, "Do not install the root package (the current project)." ), @@ -20,7 +21,9 @@ class InstallCommand(InstallerCommand): "(implicitly enables --verbose).", ), option( - "remove-untracked", None, "Removes packages not present in the lock file.", + "remove-untracked", + None, + "Removes packages not present in the lock file.", ), option( "extras", @@ -47,7 +50,7 @@ class InstallCommand(InstallerCommand): _loggers = ["poetry.repositories.pypi_repository", "poetry.inspection.info"] - def handle(self): + def handle(self) -> int: from poetry.core.masonry.utils.module import ModuleOrPackageNotFound from poetry.masonry.builders import EditableBuilder @@ -64,6 +67,7 @@ def handle(self): self._installer.extras(extras) self._installer.dev_mode(not self.option("no-dev")) + self._installer.dev_only(self.option("dev-only")) self._installer.dry_run(self.option("dry-run")) self._installer.remove_untracked(self.option("remove-untracked")) self._installer.verbose(self._io.is_verbose()) @@ -73,7 +77,7 @@ def handle(self): if return_code != 0: return return_code - if self.option("no-root"): + if self.option("no-root") or self.option("dev-only"): return 0 try: @@ -85,7 +89,7 @@ def handle(self): return 0 self.line("") - if not self._io.supports_ansi() or self.io.is_debug(): + if not self._io.output.is_decorated() or self.io.is_debug(): self.line( "Installing the current project: {} ({})".format( self.poetry.package.pretty_name, self.poetry.package.pretty_version @@ -104,7 +108,7 @@ def handle(self): builder.build() - if self._io.supports_ansi() and not self.io.is_debug(): + if self._io.output.is_decorated() and not self.io.is_debug(): self.overwrite( "Installing the current project: {} ({})".format( self.poetry.package.pretty_name, self.poetry.package.pretty_version diff --git a/poetry/console/commands/installer_command.py b/poetry/console/commands/installer_command.py index 51647eff471..409e2fc456c 100644 --- a/poetry/console/commands/installer_command.py +++ b/poetry/console/commands/installer_command.py @@ -9,20 +9,20 @@ class InstallerCommand(EnvCommand): - def __init__(self): - self._installer = None # type: Optional[Installer] + def __init__(self) -> None: + self._installer: Optional["Installer"] = None super(InstallerCommand, self).__init__() - def reset_poetry(self): + def reset_poetry(self) -> None: super(InstallerCommand, self).reset_poetry() self._installer.set_package(self.poetry.package) self._installer.set_locker(self.poetry.locker) @property - def installer(self): # type: () -> Installer + def installer(self) -> "Installer": return self._installer - def set_installer(self, installer): # type: (Installer) -> None + def set_installer(self, installer: "Installer") -> None: self._installer = installer diff --git a/poetry/console/commands/lock.py b/poetry/console/commands/lock.py index ddedb055d87..bfcdcaf665a 100644 --- a/poetry/console/commands/lock.py +++ b/poetry/console/commands/lock.py @@ -1,3 +1,5 @@ +from cleo.helpers import option + from .installer_command import InstallerCommand @@ -6,6 +8,18 @@ class LockCommand(InstallerCommand): name = "lock" description = "Locks the project dependencies." + options = [ + option( + "no-update", None, "Do not update locked versions, only refresh lock file." + ), + option( + "check", + None, + "Check that the poetry.lock file corresponds to the current version " + "of pyproject.toml.", + ), + ] + help = """ The lock command reads the pyproject.toml file from the current directory, processes it, and locks the dependencies in the poetry.lock @@ -16,11 +30,18 @@ class LockCommand(InstallerCommand): loggers = ["poetry.repositories.pypi_repository"] - def handle(self): + def handle(self) -> int: self._installer.use_executor( self.poetry.config.get("experimental.new-installer", False) ) - self._installer.lock() + if self.option("check"): + return ( + 0 + if self.poetry.locker.is_locked() and self.poetry.locker.is_fresh() + else 1 + ) + + self._installer.lock(update=not self.option("no-update")) return self._installer.run() diff --git a/poetry/console/commands/new.py b/poetry/console/commands/new.py index 481b0577bcc..8158442f741 100644 --- a/poetry/console/commands/new.py +++ b/poetry/console/commands/new.py @@ -1,9 +1,7 @@ import sys -from cleo import argument -from cleo import option - -from poetry.utils.helpers import module_name +from cleo.helpers import argument +from cleo.helpers import option from .command import Command @@ -17,13 +15,19 @@ class NewCommand(Command): options = [ option("name", None, "Set the resulting package name.", flag=False), option("src", None, "Use the src layout for the project."), + option( + "readme", + None, + "Specify the readme file format. One of md (default) or rst", + flag=False, + ), ] - def handle(self): - from poetry.core.semver import parse_constraint + def handle(self) -> None: + from pathlib import Path + from poetry.core.vcs.git import GitConfig from poetry.layouts import layout - from poetry.utils._compat import Path from poetry.utils.env import SystemEnv if self.option("src"): @@ -31,7 +35,11 @@ def handle(self): else: layout_ = layout("standard") - path = Path.cwd() / Path(self.argument("path")) + path = Path(self.argument("path")) + if not path.is_absolute(): + # we do not use resolve here due to compatibility issues for path.resolve(strict=False) + path = Path.cwd().joinpath(path) + name = self.option("name") if not name: name = path.name @@ -44,7 +52,7 @@ def handle(self): "exists and is not empty".format(path) ) - readme_format = "rst" + readme_format = self.option("readme") or "md" config = GitConfig() author = None @@ -59,25 +67,24 @@ def handle(self): ".".join(str(v) for v in current_env.version_info[:2]) ) - dev_dependencies = {} - python_constraint = parse_constraint(default_python) - if parse_constraint("<3.5").allows_any(python_constraint): - dev_dependencies["pytest"] = "^4.6" - if parse_constraint(">=3.5").allows_all(python_constraint): - dev_dependencies["pytest"] = "^5.2" - layout_ = layout_( name, "0.1.0", author=author, readme_format=readme_format, python=default_python, - dev_dependencies=dev_dependencies, ) layout_.create(path) + path = path.resolve() + + try: + path = path.relative_to(Path.cwd()) + except ValueError: + pass + self.line( "Created package {} in {}".format( - module_name(name), path.relative_to(Path.cwd()) + layout_._package_name, path.as_posix() # noqa ) ) diff --git a/poetry/console/args/__init__.py b/poetry/console/commands/plugin/__init__.py similarity index 100% rename from poetry/console/args/__init__.py rename to poetry/console/commands/plugin/__init__.py diff --git a/poetry/console/commands/plugin/add.py b/poetry/console/commands/plugin/add.py new file mode 100644 index 00000000000..c0a61e3843d --- /dev/null +++ b/poetry/console/commands/plugin/add.py @@ -0,0 +1,203 @@ +import os + +from typing import TYPE_CHECKING +from typing import Dict +from typing import List +from typing import cast + +from cleo.helpers import argument +from cleo.helpers import option + +from ..init import InitCommand + + +if TYPE_CHECKING: + from poetry.console.application import Application # noqa + from poetry.console.commands.update import UpdateCommand # noqa + + +class PluginAddCommand(InitCommand): + + name = "plugin add" + + description = "Adds new plugins." + + arguments = [ + argument("plugins", "The names of the plugins to install.", multiple=True), + ] + + options = [ + option( + "dry-run", + None, + "Output the operations but do not execute anything (implicitly enables --verbose).", + ) + ] + + help = """ +The plugin add command installs Poetry plugins globally. + +It works similarly to the add command: + +If you do not specify a version constraint, poetry will choose a suitable one based on the available package versions. + +You can specify a package in the following forms: + + - A single name (requests) + - A name and a constraint (requests@^2.23.0) + - A git url (git+https://github.com/python-poetry/poetry.git) + - A git url with a revision (git+https://github.com/python-poetry/poetry.git#develop) + - A git SSH url (git+ssh://github.com/python-poetry/poetry.git) + - A git SSH url with a revision (git+ssh://github.com/python-poetry/poetry.git#develop) + - A file path (../my-package/my-package.whl) + - A directory (../my-package/) + - A url (https://example.com/packages/my-package-0.1.0.tar.gz)\ +""" + + def handle(self) -> int: + from pathlib import Path + + import tomlkit + + from cleo.io.inputs.string_input import StringInput + from cleo.io.io import IO + + from poetry.core.pyproject.toml import PyProjectTOML + from poetry.core.semver.helpers import parse_constraint + from poetry.factory import Factory + from poetry.packages.project_package import ProjectPackage + from poetry.repositories.installed_repository import InstalledRepository + from poetry.utils.env import EnvManager + + plugins = self.argument("plugins") + + # Plugins should be installed in the system env to be globally available + system_env = EnvManager.get_system_env(naive=True) + + env_dir = Path( + os.getenv("POETRY_HOME") if os.getenv("POETRY_HOME") else system_env.path + ) + + # We check for the plugins existence first. + if env_dir.joinpath("pyproject.toml").exists(): + pyproject = tomlkit.loads( + env_dir.joinpath("pyproject.toml").read_text(encoding="utf-8") + ) + poetry_content = pyproject["tool"]["poetry"] + existing_packages = self.get_existing_packages_from_input( + plugins, poetry_content, "dependencies" + ) + + if existing_packages: + self.notify_about_existing_packages(existing_packages) + + plugins = [plugin for plugin in plugins if plugin not in existing_packages] + + if not plugins: + return 0 + + plugins = self._determine_requirements(plugins) + + # We retrieve the packages installed in the system environment. + # We assume that this environment will be a self contained virtual environment + # built by the official installer or by pipx. + # If not, it might lead to side effects since other installed packages + # might not be required by Poetry but still taken into account when resolving dependencies. + installed_repository = InstalledRepository.load( + system_env, with_dependencies=True + ) + + root_package = None + for package in installed_repository.packages: + if package.name == "poetry": + root_package = ProjectPackage(package.name, package.version) + for dependency in package.requires: + root_package.add_dependency(dependency) + + break + + root_package.python_versions = ".".join( + str(v) for v in system_env.version_info[:3] + ) + # We create a `pyproject.toml` file based on all the information + # we have about the current environment. + if not env_dir.joinpath("pyproject.toml").exists(): + Factory.create_pyproject_from_package(root_package, env_dir) + + # We add the plugins to the dependencies section of the previously + # created `pyproject.toml` file + pyproject = PyProjectTOML(env_dir.joinpath("pyproject.toml")) + poetry_content = pyproject.poetry_config + poetry_dependency_section = poetry_content["dependencies"] + plugin_names = [] + for plugin in plugins: + if "version" in plugin: + # Validate version constraint + parse_constraint(plugin["version"]) + + constraint = tomlkit.inline_table() + for name, value in plugin.items(): + if name == "name": + continue + + constraint[name] = value + + if len(constraint) == 1 and "version" in constraint: + constraint = constraint["version"] + + poetry_dependency_section[plugin["name"]] = constraint + plugin_names.append(plugin["name"]) + + pyproject.save() + + # From this point forward, all the logic will be deferred to + # the update command, by using the previously created `pyproject.toml` + # file. + application = cast("Application", self.application) + update_command: "UpdateCommand" = cast( + "UpdateCommand", application.find("update") + ) + # We won't go through the event dispatching done by the application + # so we need to configure the command manually + update_command.set_poetry(Factory().create_poetry(env_dir)) + update_command.set_env(system_env) + application._configure_installer(update_command, self._io) + + argv = ["update"] + plugin_names + if self.option("dry-run"): + argv.append("--dry-run") + + return update_command.run( + IO( + StringInput(" ".join(argv)), + self._io.output, + self._io.error_output, + ) + ) + + def get_existing_packages_from_input( + self, packages: List[str], poetry_content: Dict, target_section: str + ) -> List[str]: + existing_packages = [] + + for name in packages: + for key in poetry_content[target_section]: + if key.lower() == name.lower(): + existing_packages.append(name) + + return existing_packages + + def notify_about_existing_packages(self, existing_packages: List[str]) -> None: + self.line( + "The following plugins are already present in the " + "pyproject.toml file and will be skipped:\n" + ) + for name in existing_packages: + self.line(" • {name}".format(name=name)) + + self.line( + "\nIf you want to update it to the latest compatible version, " + "you can use `poetry plugin update package`.\n" + "If you prefer to upgrade it to the latest available version, " + "you can use `poetry plugin add package@latest`.\n" + ) diff --git a/poetry/console/commands/plugin/remove.py b/poetry/console/commands/plugin/remove.py new file mode 100644 index 00000000000..9d90e8fea5e --- /dev/null +++ b/poetry/console/commands/plugin/remove.py @@ -0,0 +1,73 @@ +import os + +from typing import TYPE_CHECKING +from typing import cast + +from cleo.helpers import argument +from cleo.helpers import option + +from poetry.console.commands.command import Command + + +if TYPE_CHECKING: + from poetry.console.application import Application # noqa + from poetry.console.commands.remove import RemoveCommand + + +class PluginRemoveCommand(Command): + + name = "plugin remove" + + description = "Removes installed plugins" + + arguments = [ + argument("plugins", "The names of the plugins to install.", multiple=True), + ] + + options = [ + option( + "dry-run", + None, + "Output the operations but do not execute anything (implicitly enables --verbose).", + ) + ] + + def handle(self) -> int: + from pathlib import Path + + from cleo.io.inputs.string_input import StringInput + from cleo.io.io import IO + + from poetry.factory import Factory + from poetry.utils.env import EnvManager + + plugins = self.argument("plugins") + + system_env = EnvManager.get_system_env(naive=True) + env_dir = Path( + os.getenv("POETRY_HOME") if os.getenv("POETRY_HOME") else system_env.path + ) + + # From this point forward, all the logic will be deferred to + # the remove command, by using the global `pyproject.toml` file. + application = cast("Application", self.application) + remove_command: "RemoveCommand" = cast( + "RemoveCommand", application.find("remove") + ) + # We won't go through the event dispatching done by the application + # so we need to configure the command manually + remove_command.set_poetry(Factory().create_poetry(env_dir)) + remove_command.set_env(system_env) + application._configure_installer(remove_command, self._io) + + argv = ["remove"] + plugins + if self.option("dry-run"): + argv.append("--dry-run") + + return remove_command.run( + IO( + StringInput(" ".join(argv)), + self._io.output, + self._io.error_output, + ) + ) diff --git a/poetry/console/commands/plugin/show.py b/poetry/console/commands/plugin/show.py new file mode 100644 index 00000000000..8ca6290d870 --- /dev/null +++ b/poetry/console/commands/plugin/show.py @@ -0,0 +1,95 @@ +from collections import defaultdict +from typing import TYPE_CHECKING +from typing import DefaultDict +from typing import Dict +from typing import List +from typing import Union + +from poetry.console.commands.command import Command + + +if TYPE_CHECKING: + from poetry.core.packages.package import Package + + +class PluginShowCommand(Command): + + name = "plugin show" + + description = "Shows information about the currently installed plugins." + + def handle(self) -> int: + from poetry.plugins.application_plugin import ApplicationPlugin + from poetry.plugins.plugin_manager import PluginManager + from poetry.repositories.installed_repository import InstalledRepository + from poetry.utils.env import EnvManager + from poetry.utils.helpers import canonicalize_name + + plugins: DefaultDict[str, Dict[str, Union["Package", List[str]]]] = defaultdict( + lambda: { + "package": None, + "plugins": [], + "application_plugins": [], + } + ) + + entry_points = ( + PluginManager("application.plugin").get_plugin_entry_points() + + PluginManager("plugin").get_plugin_entry_points() + ) + + system_env = EnvManager.get_system_env(naive=True) + installed_repository = InstalledRepository.load( + system_env, with_dependencies=True + ) + + packages_by_name = {pkg.name: pkg for pkg in installed_repository.packages} + + for entry_point in entry_points: + plugin = entry_point.load() + category = "plugins" + if issubclass(plugin, ApplicationPlugin): + category = "application_plugins" + + package = packages_by_name[canonicalize_name(entry_point.name)] + plugins[package.pretty_name]["package"] = package + plugins[package.pretty_name][category].append(entry_point) + + for name, info in plugins.items(): + package = info["package"] + self.line("") + self.line( + " • {} ({}){}".format( + name, + package.version, + " " + package.description if package.description else "", + ) + ) + provide_line = " " + if info["plugins"]: + provide_line += " {} plugin{}".format( + len(info["plugins"]), "s" if len(info["plugins"]) > 1 else "" + ) + + if info["application_plugins"]: + if info["plugins"]: + provide_line += " and" + + provide_line += " {} application plugin{}".format( + len(info["application_plugins"]), + "s" if len(info["application_plugins"]) > 1 else "", + ) + + self.line(provide_line) + + if package.requires: + self.line("") + self.line(" Dependencies") + for dependency in package.requires: + self.line( + " - {} ({})".format( + dependency.pretty_name, dependency.pretty_constraint + ) + ) + + return 0 diff --git a/poetry/console/commands/publish.py b/poetry/console/commands/publish.py index 557cd1d7ab7..bd777bb65d2 100644 --- a/poetry/console/commands/publish.py +++ b/poetry/console/commands/publish.py @@ -1,6 +1,7 @@ -from cleo import option +from pathlib import Path +from typing import Optional -from poetry.utils._compat import Path +from cleo.helpers import option from .command import Command @@ -40,7 +41,7 @@ class PublishCommand(Command): loggers = ["poetry.masonry.publishing.publisher"] - def handle(self): + def handle(self) -> Optional[int]: from poetry.publishing.publisher import Publisher publisher = Publisher(self.poetry, self.io) diff --git a/poetry/console/commands/remove.py b/poetry/console/commands/remove.py index d9a289cba7f..58af353ccc6 100644 --- a/poetry/console/commands/remove.py +++ b/poetry/console/commands/remove.py @@ -1,6 +1,7 @@ -from cleo import argument -from cleo import option +from cleo.helpers import argument +from cleo.helpers import option +from ...utils.helpers import canonicalize_name from .installer_command import InstallerCommand @@ -27,11 +28,10 @@ class RemoveCommand(InstallerCommand): loggers = ["poetry.repositories.pypi_repository", "poetry.inspection.info"] - def handle(self): + def handle(self) -> int: packages = self.argument("packages") is_dev = self.option("dev") - original_content = self.poetry.file.read() content = self.poetry.file.read() poetry_content = content["tool"]["poetry"] section = "dependencies" @@ -54,12 +54,17 @@ def handle(self): for key in requirements: del poetry_content[section][key] - # Write the new content back - self.poetry.file.write(content) + dependencies = ( + self.poetry.package.requires + if section == "dependencies" + else self.poetry.package.dev_requires + ) - # Update packages - self.reset_poetry() + for i, dependency in enumerate(reversed(dependencies)): + if dependency.name == canonicalize_name(key): + del dependencies[-i] + # Update packages self._installer.use_executor( self.poetry.config.get("experimental.new-installer", False) ) @@ -69,22 +74,9 @@ def handle(self): self._installer.update(True) self._installer.whitelist(requirements) - try: - status = self._installer.run() - except Exception: - self.poetry.file.write(original_content) - - raise - - if status != 0 or self.option("dry-run"): - # Revert changes - if not self.option("dry-run"): - self.line_error( - "\n" - "Removal failed, reverting pyproject.toml " - "to its original content." - ) + status = self._installer.run() - self.poetry.file.write(original_content) + if not self.option("dry-run") and status == 0: + self.poetry.file.write(content) return status diff --git a/poetry/console/commands/run.py b/poetry/console/commands/run.py index fda01114426..fd23f93ddd7 100644 --- a/poetry/console/commands/run.py +++ b/poetry/console/commands/run.py @@ -1,8 +1,16 @@ -from cleo import argument +from typing import TYPE_CHECKING +from typing import Any +from typing import Union + +from cleo.helpers import argument from .env_command import EnvCommand +if TYPE_CHECKING: + from poetry.core.masonry.utils.module import Module + + class RunCommand(EnvCommand): name = "run" @@ -12,14 +20,7 @@ class RunCommand(EnvCommand): argument("args", "The command and arguments/options to run.", multiple=True) ] - def __init__(self): # type: () -> None - from poetry.console.args.run_args_parser import RunArgsParser - - super(RunCommand, self).__init__() - - self.config.set_args_parser(RunArgsParser()) - - def handle(self): + def handle(self) -> Any: args = self.argument("args") script = args[0] scripts = self.poetry.local_config.get("scripts") @@ -29,7 +30,18 @@ def handle(self): return self.env.execute(*args) - def run_script(self, script, args): + @property + def _module(self) -> "Module": + from poetry.core.masonry.utils.module import Module + + poetry = self.poetry + package = poetry.package + path = poetry.file.parent + module = Module(package.name, path.as_posix(), package.packages) + + return module + + def run_script(self, script: Union[str, dict], args: str) -> Any: if isinstance(script, dict): script = script["callable"] @@ -47,14 +59,3 @@ def run_script(self, script, args): ] return self.env.execute(*cmd) - - @property - def _module(self): - from poetry.core.masonry.utils.module import Module - - poetry = self.poetry - package = poetry.package - path = poetry.file.parent - module = Module(package.name, path.as_posix(), package.packages) - - return module diff --git a/poetry/console/commands/search.py b/poetry/console/commands/search.py index 299dee6a96a..85384125bca 100644 --- a/poetry/console/commands/search.py +++ b/poetry/console/commands/search.py @@ -1,4 +1,4 @@ -from cleo import argument +from cleo.helpers import argument from .command import Command @@ -10,7 +10,7 @@ class SearchCommand(Command): arguments = [argument("tokens", "The tokens to search for.", multiple=True)] - def handle(self): + def handle(self) -> None: from poetry.repositories.pypi_repository import PyPiRepository results = PyPiRepository().search(self.argument("tokens")) diff --git a/poetry/console/commands/self/self.py b/poetry/console/commands/self/self.py deleted file mode 100644 index 3e5cafa9180..00000000000 --- a/poetry/console/commands/self/self.py +++ /dev/null @@ -1,13 +0,0 @@ -from ..command import Command -from .update import SelfUpdateCommand - - -class SelfCommand(Command): - - name = "self" - description = "Interact with Poetry directly." - - commands = [SelfUpdateCommand()] - - def handle(self): - return self.call("help", self._config.name) diff --git a/poetry/console/commands/self/update.py b/poetry/console/commands/self/update.py index 8660bea2f09..e40dc225274 100644 --- a/poetry/console/commands/self/update.py +++ b/poetry/console/commands/self/update.py @@ -1,101 +1,104 @@ -from __future__ import unicode_literals - -import hashlib import os -import re import shutil -import stat -import subprocess -import sys -import tarfile +import site from functools import cmp_to_key -from gzip import GzipFile - -from cleo import argument -from cleo import option +from pathlib import Path +from typing import TYPE_CHECKING -from poetry.core.packages import Dependency +from cleo.helpers import argument +from cleo.helpers import option from ..command import Command -try: - from urllib.error import HTTPError - from urllib.request import urlopen -except ImportError: - from urllib2 import HTTPError - from urllib2 import urlopen +if TYPE_CHECKING: + from poetry.core.packages.package import Package + from poetry.core.semver.version import Version + from poetry.repositories.pool import Pool -BIN = """# -*- coding: utf-8 -*- -import glob -import sys -import os +class SelfUpdateCommand(Command): -lib = os.path.normpath(os.path.join(os.path.realpath(__file__), "../..", "lib")) -vendors = os.path.join(lib, "poetry", "_vendor") -current_vendors = os.path.join( - vendors, "py{}".format(".".join(str(v) for v in sys.version_info[:2])) -) -sys.path.insert(0, lib) -sys.path.insert(0, current_vendors) + name = "self update" + description = "Updates Poetry to the latest version." -if __name__ == "__main__": - from poetry.console import main - main() -""" + arguments = [argument("version", "The version to update to.", optional=True)] + options = [ + option("preview", None, "Allow the installation of pre-release versions."), + option( + "dry-run", + None, + "Output the operations but do not execute anything " + "(implicitly enables --verbose).", + ), + ] + + _data_dir = None + _bin_dir = None + _pool = None -BAT = '@echo off\r\n{python_executable} "{poetry_bin}" %*\r\n' + @property + def data_dir(self) -> Path: + if self._data_dir is not None: + return self._data_dir + from poetry.locations import data_dir -class SelfUpdateCommand(Command): + self._data_dir = data_dir() - name = "update" - description = "Updates Poetry to the latest version." + return self._data_dir - arguments = [argument("version", "The version to update to.", optional=True)] - options = [option("preview", None, "Install prereleases.")] + @property + def bin_dir(self) -> Path: + if self._data_dir is not None: + return self._data_dir - REPOSITORY_URL = "https://github.com/python-poetry/poetry" - BASE_URL = REPOSITORY_URL + "/releases/download" + from poetry.utils._compat import WINDOWS - @property - def home(self): - from poetry.utils._compat import Path + if os.getenv("POETRY_HOME"): + return Path(os.getenv("POETRY_HOME"), "bin").expanduser() - return Path(os.environ.get("POETRY_HOME", "~/.poetry")).expanduser() + user_base = site.getuserbase() - @property - def bin(self): - return self.home / "bin" + if WINDOWS: + bin_dir = os.path.join(user_base, "Scripts") + else: + bin_dir = os.path.join(user_base, "bin") - @property - def lib(self): - return self.home / "lib" + self._bin_dir = Path(bin_dir) + + return self._bin_dir @property - def lib_backup(self): - return self.home / "lib-backup" + def pool(self) -> "Pool": + if self._pool is not None: + return self._pool - def handle(self): - from poetry.__version__ import __version__ - from poetry.core.semver import Version + from poetry.repositories.pool import Pool from poetry.repositories.pypi_repository import PyPiRepository - self._check_recommended_installation() + pool = Pool() + pool.add_repository(PyPiRepository()) + + return pool + + def handle(self) -> int: + from poetry.__version__ import __version__ + from poetry.core.packages.dependency import Dependency + from poetry.core.semver.version import Version version = self.argument("version") if not version: version = ">=" + __version__ - repo = PyPiRepository(fallback=False) + repo = self.pool.repositories[0] packages = repo.find_packages( Dependency("poetry", version, allows_prereleases=self.option("preview")) ) if not packages: self.line("No release found for the specified version") - return + return 1 packages.sort( key=cmp_to_key( @@ -121,211 +124,101 @@ def handle(self): if release is None: self.line("No new release found") - return + return 1 if release.version == Version.parse(__version__): self.line("You are using the latest version") - return - - self.update(release) - - def update(self, release): - version = release.version - self.line("Updating to {}".format(version)) - - if self.lib_backup.exists(): - shutil.rmtree(str(self.lib_backup)) - - # Backup the current installation - if self.lib.exists(): - shutil.copytree(str(self.lib), str(self.lib_backup)) - shutil.rmtree(str(self.lib)) - - try: - self._update(version) - except Exception: - if not self.lib_backup.exists(): - raise + return 0 - shutil.copytree(str(self.lib_backup), str(self.lib)) - shutil.rmtree(str(self.lib_backup)) - - raise - finally: - if self.lib_backup.exists(): - shutil.rmtree(str(self.lib_backup)) + self.line("Updating Poetry to {}".format(release.version)) + self.line("") - self.make_bin() + self.update(release) - self.line("") self.line("") self.line( - "Poetry ({}) is installed now. Great!".format( - version + "Poetry ({}) is installed now. Great!".format( + release.version ) ) - def _update(self, version): - from poetry.utils.helpers import temporary_directory - - release_name = self._get_release_name(version) - - checksum = "{}.sha256sum".format(release_name) - - base_url = self.BASE_URL + return 0 - try: - r = urlopen(base_url + "/{}/{}".format(version, checksum)) - except HTTPError as e: - if e.code == 404: - raise RuntimeError("Could not find {} file".format(checksum)) + def update(self, release: "Package") -> None: + from poetry.utils.env import EnvManager - raise + version = release.version - checksum = r.read().decode().strip() + env = EnvManager.get_system_env(naive=True) - # We get the payload from the remote host - name = "{}.tar.gz".format(release_name) + # We can't use is_relative_to() since it's only available in Python 3.9+ try: - r = urlopen(base_url + "/{}/{}".format(version, name)) - except HTTPError as e: - if e.code == 404: - raise RuntimeError("Could not find {} file".format(name)) - - raise - - meta = r.info() - size = int(meta["Content-Length"]) - current = 0 - block_size = 8192 - - bar = self.progress_bar(max=size) - bar.set_format(" - Downloading {} %percent%%".format(name)) - bar.start() - - sha = hashlib.sha256() - with temporary_directory(prefix="poetry-updater-") as dir_: - tar = os.path.join(dir_, name) - with open(tar, "wb") as f: - while True: - buffer = r.read(block_size) - if not buffer: - break - - current += len(buffer) - f.write(buffer) - sha.update(buffer) - - bar.set_progress(current) - - bar.finish() - - # Checking hashes - if checksum != sha.hexdigest(): - raise RuntimeError( - "Hashes for {} do not match: {} != {}".format( - name, checksum, sha.hexdigest() - ) - ) - - gz = GzipFile(tar, mode="rb") - try: - with tarfile.TarFile(tar, fileobj=gz, format=tarfile.PAX_FORMAT) as f: - f.extractall(str(self.lib)) - finally: - gz.close() - - def process(self, *args): - return subprocess.check_output(list(args), stderr=subprocess.STDOUT) - - def _check_recommended_installation(self): - from poetry.utils._compat import Path - - current = Path(__file__) - try: - current.relative_to(self.home) + env.path.relative_to(self.data_dir) except ValueError: - raise RuntimeError( - "Poetry was not installed with the recommended installer. " - "Cannot update automatically." - ) - - def _get_release_name(self, version): - platform = sys.platform - if platform == "linux2": - platform = "linux" - - return "poetry-{}-{}".format(version, platform) - - def _bin_path(self, base_path, bin): - from poetry.utils._compat import WINDOWS + # Poetry was not installed using the recommended installer + from poetry.console.exceptions import PoetrySimpleConsoleException - if WINDOWS: - return (base_path / "Scripts" / bin).with_suffix(".exe") + raise PoetrySimpleConsoleException( + "Poetry was not installed with the recommended installer, " + "so it cannot be updated automatically." + ) - return base_path / "bin" / bin + self._update(version) + self._make_bin() + + def _update(self, version: "Version") -> None: + from poetry.config.config import Config + from poetry.core.packages.dependency import Dependency + from poetry.core.packages.project_package import ProjectPackage + from poetry.installation.installer import Installer + from poetry.packages.locker import NullLocker + from poetry.repositories.installed_repository import InstalledRepository + from poetry.utils.env import EnvManager + + env = EnvManager.get_system_env() + installed = InstalledRepository.load(env) + + root = ProjectPackage("poetry-updater", "0.0.0") + root.python_versions = ".".join(str(c) for c in env.version_info[:3]) + root.add_dependency(Dependency("poetry", version.text)) + + installer = Installer( + self.io, + env, + root, + NullLocker(self.data_dir.joinpath("poetry.lock"), {}), + self.pool, + Config(), + installed=installed, + ) + installer.update(True) + installer.dry_run(self.option("dry-run")) + installer.run() - def make_bin(self): + def _make_bin(self) -> None: from poetry.utils._compat import WINDOWS - self.bin.mkdir(0o755, parents=True, exist_ok=True) - - python_executable = self._which_python() + self.line("") + self.line("Updating the poetry script") - if WINDOWS: - with self.bin.joinpath("poetry.bat").open("w", newline="") as f: - f.write( - BAT.format( - python_executable=python_executable, - poetry_bin=str(self.bin / "poetry").replace( - os.environ["USERPROFILE"], "%USERPROFILE%" - ), - ) - ) - - bin_content = BIN - if not WINDOWS: - bin_content = "#!/usr/bin/env {}\n".format(python_executable) + bin_content - - self.bin.joinpath("poetry").write_text(bin_content, encoding="utf-8") - - if not WINDOWS: - # Making the file executable - st = os.stat(str(self.bin.joinpath("poetry"))) - os.chmod(str(self.bin.joinpath("poetry")), st.st_mode | stat.S_IEXEC) - - def _which_python(self): - """ - Decides which python executable we'll embed in the launcher script. - """ - from poetry.utils._compat import WINDOWS + self.bin_dir.mkdir(parents=True, exist_ok=True) - allowed_executables = ["python", "python3"] + script = "poetry" + target_script = "venv/bin/poetry" if WINDOWS: - allowed_executables += ["py.exe -3", "py.exe -2"] - - # \d in regex ensures we can convert to int later - version_matcher = re.compile(r"^Python (?P\d+)\.(?P\d+)\..+$") - fallback = None - for executable in allowed_executables: - try: - raw_version = subprocess.check_output( - executable + " --version", stderr=subprocess.STDOUT, shell=True - ).decode("utf-8") - except subprocess.CalledProcessError: - continue - - match = version_matcher.match(raw_version.strip()) - if match and tuple(map(int, match.groups())) >= (3, 0): - # favor the first py3 executable we can find. - return executable - - if fallback is None: - # keep this one as the fallback; it was the first valid executable we found. - fallback = executable + script = "poetry.exe" + target_script = "venv/Scripts/poetry.exe" - if fallback is None: - # Avoid breaking existing scripts - fallback = "python" + if self.bin_dir.joinpath(script).exists(): + self.bin_dir.joinpath(script).unlink() - return fallback + try: + self.bin_dir.joinpath(script).symlink_to( + self.data_dir.joinpath(target_script) + ) + except OSError: + # This can happen if the user + # does not have the correct permission on Windows + shutil.copy( + self.data_dir.joinpath(target_script), self.bin_dir.joinpath(script) + ) diff --git a/poetry/console/commands/shell.py b/poetry/console/commands/shell.py index 033ab207e31..f0f7f851fba 100644 --- a/poetry/console/commands/shell.py +++ b/poetry/console/commands/shell.py @@ -16,7 +16,7 @@ class ShellCommand(EnvCommand): If one doesn't exist yet, it will be created. """ - def handle(self): + def handle(self) -> None: from poetry.utils.shell import Shell # Check if it's already activated or doesn't exist and won't be created diff --git a/poetry/console/commands/show.py b/poetry/console/commands/show.py index 808122e64dc..799fa4d78db 100644 --- a/poetry/console/commands/show.py +++ b/poetry/console/commands/show.py @@ -1,10 +1,23 @@ -# -*- coding: utf-8 -*- -from cleo import argument -from cleo import option +from typing import TYPE_CHECKING +from typing import List +from typing import Optional +from typing import Union + +from cleo.helpers import argument +from cleo.helpers import option from .env_command import EnvCommand +if TYPE_CHECKING: + from cleo.io.io import IO # noqa + + from poetry.core.packages.dependency import Dependency + from poetry.core.packages.package import Package + from poetry.repositories import Repository + from poetry.repositories.installed_repository import InstalledRepository + + class ShowCommand(EnvCommand): name = "show" @@ -32,10 +45,10 @@ class ShowCommand(EnvCommand): colors = ["cyan", "yellow", "green", "magenta", "blue"] - def handle(self): - from clikit.utils.terminal import Terminal + def handle(self) -> Optional[int]: + from cleo.io.null_io import NullIO + from cleo.terminal import Terminal - from poetry.io.null_io import NullIO from poetry.puzzle.solver import Solver from poetry.repositories.installed_repository import InstalledRepository from poetry.repositories.pool import Pool @@ -48,14 +61,16 @@ def handle(self): self.init_styles(self.io) if self.option("outdated"): - self._args.set_option("latest", True) + self._io.input.set_option("latest", True) include_dev = not self.option("no-dev") locked_repo = self.poetry.locker.locked_repository(True) # Show tree view if requested if self.option("tree") and not package: - requires = self.poetry.package.requires + self.poetry.package.dev_requires + requires = self.poetry.package.requires + if include_dev: + requires += self.poetry.package.dev_requires packages = locked_repo.packages for package in packages: for require in requires: @@ -66,9 +81,8 @@ def handle(self): return 0 table = self.table(style="compact") - # table.style.line_vc_char = "" locked_packages = locked_repo.packages - pool = Pool() + pool = Pool(ignore_repository_names=True) pool.add_repository(locked_repo) solver = Solver( self.poetry.package, @@ -110,7 +124,7 @@ def handle(self): ] table.add_rows(rows) - table.render(self.io) + table.render() if pkg.requires: self.line("") @@ -139,7 +153,7 @@ def handle(self): continue current_length = len(locked.pretty_name) - if not self._io.output.supports_ansi(): + if not self._io.output.is_decorated(): installed_status = self.get_installed_status(locked, installed_repo) if installed_status == "not-installed": @@ -202,7 +216,7 @@ def handle(self): if installed_status == "not-installed": color = "red" - if not self._io.output.supports_ansi(): + if not self._io.output.is_decorated(): # Non installed in non decorated mode install_marker = " (!)" @@ -255,7 +269,9 @@ def handle(self): self.line(line) - def display_package_tree(self, io, package, installed_repo): + def display_package_tree( + self, io: "IO", package: "Package", installed_repo: "Repository" + ) -> None: io.write("{}".format(package.pretty_name)) description = "" if package.description: @@ -292,13 +308,13 @@ def display_package_tree(self, io, package, installed_repo): def _display_tree( self, - io, - dependency, - installed_repo, - packages_in_tree, - previous_tree_bar="├", - level=1, - ): + io: "IO", + dependency: "Dependency", + installed_repo: "Repository", + packages_in_tree: List[str], + previous_tree_bar: str = "├", + level: int = 1, + ) -> None: previous_tree_bar = previous_tree_bar.replace("├", "│") dependencies = [] @@ -343,8 +359,8 @@ def _display_tree( io, dependency, installed_repo, current_tree, tree_bar, level + 1 ) - def _write_tree_line(self, io, line): - if not io.output.supports_ansi(): + def _write_tree_line(self, io: "IO", line: str) -> None: + if not io.output.supports_utf8(): line = line.replace("└", "`-") line = line.replace("├", "|-") line = line.replace("──", "-") @@ -352,16 +368,18 @@ def _write_tree_line(self, io, line): io.write_line(line) - def init_styles(self, io): - from clikit.api.formatter import Style + def init_styles(self, io: "IO") -> None: + from cleo.formatters.style import Style for color in self.colors: - style = Style(color).fg(color) - io.output.formatter.add_style(style) - io.error_output.formatter.add_style(style) + style = Style(color) + io.output.formatter.set_style(color, style) + io.error_output.formatter.set_style(color, style) - def find_latest_package(self, package, include_dev): - from clikit.io import NullIO + def find_latest_package( + self, package: "Package", include_dev: bool + ) -> Union["Package", bool]: + from cleo.io.null_io import NullIO from poetry.puzzle.provider import Provider from poetry.version.version_selector import VersionSelector @@ -388,8 +406,8 @@ def find_latest_package(self, package, include_dev): return selector.find_best_candidate(name, ">={}".format(package.pretty_version)) - def get_update_status(self, latest, package): - from poetry.core.semver import parse_constraint + def get_update_status(self, latest: "Package", package: "Package") -> str: + from poetry.core.semver.helpers import parse_constraint if latest.full_pretty_version == package.full_pretty_version: return "up-to-date" @@ -403,7 +421,9 @@ def get_update_status(self, latest, package): # it needs an upgrade but has potential BC breaks so is not urgent return "update-possible" - def get_installed_status(self, locked, installed_repo): + def get_installed_status( + self, locked: "Package", installed_repo: "InstalledRepository" + ) -> str: for package in installed_repo.packages: if locked.name == package.name: return "installed" diff --git a/poetry/io/__init__.py b/poetry/console/commands/source/__init__.py similarity index 100% rename from poetry/io/__init__.py rename to poetry/console/commands/source/__init__.py diff --git a/poetry/console/commands/source/add.py b/poetry/console/commands/source/add.py new file mode 100644 index 00000000000..1b097076b84 --- /dev/null +++ b/poetry/console/commands/source/add.py @@ -0,0 +1,108 @@ +from typing import Optional + +from cleo.helpers import argument +from cleo.helpers import option +from cleo.io.null_io import NullIO +from tomlkit import nl +from tomlkit import table +from tomlkit.items import AoT +from tomlkit.items import Table + +from poetry.config.source import Source +from poetry.console.commands.command import Command +from poetry.factory import Factory +from poetry.repositories import Pool + + +class SourceAddCommand(Command): + + name = "source add" + description = "Add source configuration for project." + + arguments = [ + argument( + "name", + "Source repository name.", + ), + argument("url", "Source repository url."), + ] + + options = [ + option( + "default", + "d", + "Set this source as the default (disable PyPI). A " + "default source will also be the fallback source if " + "you add other sources.", + ), + option("secondary", "s", "Set this source as secondary."), + ] + + @staticmethod + def source_to_table(source: Source) -> Table: + source_table: Table = table() + for key, value in source.to_dict().items(): + source_table.add(key, value) + source_table.add(nl()) + return source_table + + def handle(self) -> Optional[int]: + name = self.argument("name") + url = self.argument("url") + is_default = self.option("default") + is_secondary = self.option("secondary") + + if is_default and is_secondary: + self.line_error( + "Cannot configure a source as both default and secondary." + ) + return 1 + + new_source = Source( + name=name, url=url, default=is_default, secondary=is_secondary + ) + existing_sources = self.poetry.get_sources() + + sources = AoT([]) + + for source in existing_sources: + if source == new_source: + self.line( + f"Source with name {name} already exits. Skipping addition." + ) + return 0 + elif source.default and is_default: + self.line_error( + f"Source with name {source.name} is already set to default. " + f"Only one default source can be configured at a time." + ) + return 1 + + if source.name == name: + self.line(f"Source with name {name} already exits. Updating.") + source = new_source + new_source = None + + sources.append(self.source_to_table(source)) + + if new_source is not None: + self.line(f"Adding source with name {name}.") + sources.append(self.source_to_table(new_source)) + + # ensure new source is valid. eg: invalid name etc. + self.poetry._pool = Pool() + try: + Factory.configure_sources( + self.poetry, sources, self.poetry.config, NullIO() + ) + self.poetry.pool.repository(name) + except ValueError as e: + self.line_error( + f"Failed to validate addition of {name}: {e}" + ) + return 1 + + self.poetry.pyproject.poetry_config["source"] = sources + self.poetry.pyproject.save() + + return 0 diff --git a/poetry/console/commands/source/remove.py b/poetry/console/commands/source/remove.py new file mode 100644 index 00000000000..6bc2db11e59 --- /dev/null +++ b/poetry/console/commands/source/remove.py @@ -0,0 +1,55 @@ +from typing import Optional + +from cleo.helpers import argument +from tomlkit import nl +from tomlkit import table +from tomlkit.items import AoT +from tomlkit.items import Table + +from poetry.config.source import Source +from poetry.console.commands.command import Command + + +class SourceRemoveCommand(Command): + + name = "source remove" + description = "Remove source configured for the project." + + arguments = [ + argument( + "name", + "Source repository name.", + ), + ] + + @staticmethod + def source_to_table(source: Source) -> Table: + source_table: Table = table() + for key, value in source.to_dict().items(): + source_table.add(key, value) + source_table.add(nl()) + return source_table + + def handle(self) -> Optional[int]: + name = self.argument("name") + + sources = AoT([]) + removed = False + + for source in self.poetry.get_sources(): + if source.name == name: + self.line(f"Removing source with name {source.name}.") + removed = True + continue + sources.append(self.source_to_table(source)) + + if not removed: + self.line_error( + f"Source with name {name} was not found." + ) + return 1 + + self.poetry.pyproject.poetry_config["source"] = sources + self.poetry.pyproject.save() + + return 0 diff --git a/poetry/console/commands/source/show.py b/poetry/console/commands/source/show.py new file mode 100644 index 00000000000..67e843fcf61 --- /dev/null +++ b/poetry/console/commands/source/show.py @@ -0,0 +1,59 @@ +from typing import Optional + +from cleo.helpers import argument + +from poetry.console.commands.command import Command + + +class SourceShowCommand(Command): + name = "source show" + description = "Show information about sources configured for the project." + + arguments = [ + argument( + "source", + "Source(s) to show information for. Defaults to showing all sources.", + optional=True, + multiple=True, + ), + ] + + def handle(self) -> Optional[int]: + sources = self.poetry.get_sources() + names = self.argument("source") + + if not sources: + self.line("No sources configured for this project.") + return 0 + + if names and not any(map(lambda s: s.name in names, sources)): + self.line_error(f"No source found with name(s): {', '.join(names)}") + return 1 + + bool_string = { + True: "yes", + False: "no", + } + + for source in sources: + if names and source.name not in names: + continue + + table = self.table(style="compact") + rows = [ + ["name", " : {}".format(source.name)], + ["url", " : {}".format(source.url)], + [ + "default", + " : {}".format(bool_string.get(source.default, False)), + ], + [ + "secondary", + " : {}".format(bool_string.get(source.secondary, False)), + ], + ] + table.add_rows(rows) + table.render() + self.line("") + + return 0 diff --git a/.coveragerc b/poetry/console/commands/source/update.py similarity index 100% rename from .coveragerc rename to poetry/console/commands/source/update.py diff --git a/poetry/console/commands/update.py b/poetry/console/commands/update.py index 9e18feb78b6..fd2b421bce2 100644 --- a/poetry/console/commands/update.py +++ b/poetry/console/commands/update.py @@ -1,5 +1,5 @@ -from cleo import argument -from cleo import option +from cleo.helpers import argument +from cleo.helpers import option from .installer_command import InstallerCommand @@ -27,7 +27,7 @@ class UpdateCommand(InstallerCommand): loggers = ["poetry.repositories.pypi_repository"] - def handle(self): + def handle(self) -> int: packages = self.argument("packages") self._installer.use_executor( diff --git a/poetry/console/commands/version.py b/poetry/console/commands/version.py index 21ed676f54f..71c18a0260c 100644 --- a/poetry/console/commands/version.py +++ b/poetry/console/commands/version.py @@ -1,9 +1,15 @@ -from cleo import argument -from cleo import option +from typing import TYPE_CHECKING + +from cleo.helpers import argument +from cleo.helpers import option from .command import Command +if TYPE_CHECKING: + from poetry.core.semver.version import Version + + class VersionCommand(Command): name = "version" @@ -40,7 +46,7 @@ class VersionCommand(Command): "prerelease", } - def handle(self): + def handle(self) -> None: version = self.argument("version") if version: @@ -48,11 +54,14 @@ def handle(self): self.poetry.package.pretty_version, version ) - self.line( - "Bumping version from {} to {}".format( - self.poetry.package.pretty_version, version + if self.option("short"): + self.line("{}".format(version)) + else: + self.line( + "Bumping version from {} to {}".format( + self.poetry.package.pretty_version, version + ) ) - ) content = self.poetry.file.read() poetry_content = content["tool"]["poetry"] @@ -69,8 +78,8 @@ def handle(self): ) ) - def increment_version(self, version, rule): - from poetry.core.semver import Version + def increment_version(self, version: str, rule: str) -> "Version": + from poetry.core.semver.version import Version try: version = Version.parse(version) @@ -78,31 +87,22 @@ def increment_version(self, version, rule): raise ValueError("The project's version doesn't seem to follow semver") if rule in {"major", "premajor"}: - new = version.next_major + new = version.next_major() if rule == "premajor": - new = new.first_prerelease + new = new.first_prerelease() elif rule in {"minor", "preminor"}: - new = version.next_minor + new = version.next_minor() if rule == "preminor": - new = new.first_prerelease + new = new.first_prerelease() elif rule in {"patch", "prepatch"}: - new = version.next_patch + new = version.next_patch() if rule == "prepatch": - new = new.first_prerelease + new = new.first_prerelease() elif rule == "prerelease": - if version.is_prerelease(): - pre = version.prerelease - new_prerelease = int(pre[1]) + 1 - new = Version.parse( - "{}.{}.{}-{}".format( - version.major, - version.minor, - version.patch, - ".".join([pre[0], str(new_prerelease)]), - ) - ) + if version.is_unstable(): + new = Version(version.epoch, version.release, version.pre.next()) else: - new = version.next_patch.first_prerelease + new = version.next_patch().first_prerelease() else: new = Version.parse(rule) diff --git a/poetry/console/config/__init__.py b/poetry/console/config/__init__.py deleted file mode 100644 index 14e86b4365b..00000000000 --- a/poetry/console/config/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .application_config import ApplicationConfig diff --git a/poetry/console/config/application_config.py b/poetry/console/config/application_config.py deleted file mode 100644 index 09cc2cb1b44..00000000000 --- a/poetry/console/config/application_config.py +++ /dev/null @@ -1,249 +0,0 @@ -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 -from clikit.api.event import PRE_HANDLE -from clikit.api.event import PreHandleEvent -from clikit.api.event import PreResolveEvent -from clikit.api.event.event_dispatcher import EventDispatcher -from clikit.api.exceptions import CliKitException -from clikit.api.formatter import Style -from clikit.api.io import Input -from clikit.api.io import InputStream -from clikit.api.io import Output -from clikit.api.io import OutputStream -from clikit.api.io.flags import DEBUG -from clikit.api.io.flags import VERBOSE -from clikit.api.io.flags import VERY_VERBOSE -from clikit.api.io.io import IO -from clikit.formatter import AnsiFormatter -from clikit.formatter import PlainFormatter -from clikit.io.input_stream import StandardInputStream -from clikit.io.output_stream import ErrorOutputStream -from clikit.io.output_stream import StandardOutputStream - -from poetry.console.commands.command import Command -from poetry.console.commands.env_command import EnvCommand -from poetry.console.commands.installer_command import InstallerCommand -from poetry.console.logging.io_formatter import IOFormatter -from poetry.console.logging.io_handler import IOHandler -from poetry.utils._compat import PY36 - - -class ApplicationConfig(BaseApplicationConfig): - def configure(self): - super(ApplicationConfig, self).configure() - - self.add_style(Style("c1").fg("cyan")) - self.add_style(Style("c2").fg("default").bold()) - self.add_style(Style("info").fg("blue")) - self.add_style(Style("comment").fg("green")) - self.add_style(Style("error").fg("red").bold()) - self.add_style(Style("warning").fg("yellow").bold()) - self.add_style(Style("debug").fg("default").dark()) - self.add_style(Style("success").fg("green")) - - # Dark variants - self.add_style(Style("c1_dark").fg("cyan").dark()) - self.add_style(Style("c2_dark").fg("default").bold().dark()) - self.add_style(Style("success_dark").fg("green").dark()) - - self.add_event_listener(PRE_HANDLE, self.register_command_loggers) - self.add_event_listener(PRE_HANDLE, self.set_env) - self.add_event_listener(PRE_HANDLE, self.set_installer) - - if PY36: - from poetry.mixology.solutions.providers import ( - PythonRequirementSolutionProvider, - ) - - self._solution_provider_repository.register_solution_providers( - [PythonRequirementSolutionProvider] - ) - - def register_command_loggers( - 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.locker", - "poetry.packages.package", - "poetry.utils.password_manager", - ] - - loggers += command.loggers - - handler = IOHandler(io) - handler.setFormatter(IOFormatter()) - - for logger in loggers: - logger = logging.getLogger(logger) - - logger.handlers = [handler] - - level = logging.WARNING - # The builders loggers are special and we can actually - # start at the INFO level. - if logger.name.startswith("poetry.core.masonry.builders"): - level = logging.INFO - - if io.is_debug(): - level = logging.DEBUG - elif io.is_very_verbose() or io.is_verbose(): - level = logging.INFO - - logger.setLevel(level) - - 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 - if not isinstance(command, EnvCommand): - return - - if command.env is not None: - return - - io = event.io - poetry = command.poetry - - env_manager = EnvManager(poetry) - env = env_manager.create_venv(io) - - if env.is_venv() and io.is_verbose(): - io.write_line("Using virtualenv: {}".format(env.path)) - - command.set_env(env) - - def set_installer( - self, event, event_name, _ - ): # type: (PreHandleEvent, str, Any) -> None - command = event.command.config.handler # type: InstallerCommand - if not isinstance(command, InstallerCommand): - return - - # If the command already has an installer - # we skip this step - if command.installer is not None: - return - - from poetry.installation.installer import Installer - - poetry = command.poetry - installer = Installer( - event.io, - command.env, - poetry.package, - poetry.locker, - poetry.pool, - poetry.config, - ) - installer.use_executor(poetry.config.get("experimental.new-installer", False)) - command.set_installer(installer) - - def resolve_help_command( - self, event, event_name, dispatcher - ): # type: (PreResolveEvent, str, EventDispatcher) -> None - args = event.raw_args - application = event.application - - if args.has_option_token("-h") or args.has_option_token("--help"): - from clikit.api.resolver import ResolvedCommand - - try: - resolved_command = self.command_resolver.resolve(args, application) - except CliKitException: - # We weren't able to resolve the command, - # due to a parse error most likely, - # so we fall back on the default behavior - return super(ApplicationConfig, self).resolve_help_command( - event, event_name, dispatcher - ) - - # If the current command is the run one, skip option - # check and interpret them as part of the executed command - if resolved_command.command.name == "run": - event.set_resolved_command(resolved_command) - - return event.stop_propagation() - - command = application.get_command("help") - - # Enable lenient parsing - parsed_args = command.parse(args, True) - - event.set_resolved_command(ResolvedCommand(command, parsed_args)) - event.stop_propagation() - - def create_io( - self, - application, - args, - input_stream=None, - output_stream=None, - error_stream=None, - ): # type: (Application, RawArgs, InputStream, OutputStream, OutputStream) -> IO - if input_stream is None: - input_stream = StandardInputStream() - - if output_stream is None: - output_stream = StandardOutputStream() - - if error_stream is None: - error_stream = ErrorOutputStream() - - style_set = application.config.style_set - - if output_stream.supports_ansi(): - output_formatter = AnsiFormatter(style_set) - else: - output_formatter = PlainFormatter(style_set) - - if error_stream.supports_ansi(): - error_formatter = AnsiFormatter(style_set) - else: - error_formatter = PlainFormatter(style_set) - - io = self.io_class( - Input(input_stream), - Output(output_stream, output_formatter), - Output(error_stream, error_formatter), - ) - - resolved_command = application.resolve_command(args) - # If the current command is the run one, skip option - # check and interpret them as part of the executed command - if resolved_command.command.name == "run": - return io - - if args.has_option_token("--no-ansi"): - formatter = PlainFormatter(style_set) - io.output.set_formatter(formatter) - io.error_output.set_formatter(formatter) - elif args.has_option_token("--ansi"): - formatter = AnsiFormatter(style_set, True) - io.output.set_formatter(formatter) - io.error_output.set_formatter(formatter) - - if args.has_option_token("-vvv") or self.is_debug(): - io.set_verbosity(DEBUG) - elif args.has_option_token("-vv"): - io.set_verbosity(VERY_VERBOSE) - elif args.has_option_token("-v"): - io.set_verbosity(VERBOSE) - - if args.has_option_token("--quiet") or args.has_option_token("-q"): - io.set_quiet(True) - - if args.has_option_token("--no-interaction") or args.has_option_token("-n"): - io.set_interactive(False) - - return io diff --git a/poetry/console/events/__init__.py b/poetry/console/events/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/poetry/console/events/console_events.py b/poetry/console/events/console_events.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/poetry/console/exceptions.py b/poetry/console/exceptions.py new file mode 100644 index 00000000000..04e2d84ffa7 --- /dev/null +++ b/poetry/console/exceptions.py @@ -0,0 +1,6 @@ +from cleo.exceptions import CleoSimpleException + + +class PoetrySimpleConsoleException(CleoSimpleException): + + pass diff --git a/poetry/console/io/__init__.py b/poetry/console/io/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/poetry/console/io/inputs/__init__.py b/poetry/console/io/inputs/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/poetry/console/io/inputs/run_argv_input.py b/poetry/console/io/inputs/run_argv_input.py new file mode 100644 index 00000000000..e052f3e9c9a --- /dev/null +++ b/poetry/console/io/inputs/run_argv_input.py @@ -0,0 +1,81 @@ +from typing import List +from typing import Optional +from typing import Union + +from cleo.io.inputs.argv_input import ArgvInput +from cleo.io.inputs.definition import Definition + + +class RunArgvInput(ArgvInput): + def __init__( + self, argv: Optional[List[str]] = None, definition: Optional[Definition] = None + ) -> None: + super().__init__(argv, definition=definition) + + self._parameter_options = [] + + @property + def first_argument(self) -> Optional[str]: + return "run" + + def add_parameter_option(self, name: str) -> None: + self._parameter_options.append(name) + + def has_parameter_option( + self, values: Union[str, List[str]], only_params: bool = False + ) -> bool: + if not isinstance(values, list): + values = [values] + + for token in self._tokens: + if only_params and token == "--": + return False + + for value in values: + if value not in self._parameter_options: + continue + + # Options with values: + # For long options, test for '--option=' at beginning + # For short options, test for '-o' at beginning + if value.find("--") == 0: + leading = value + "=" + else: + leading = value + + if token == value or leading != "" and token.find(leading) == 0: + return True + + return False + + def _parse(self) -> None: + parse_options = True + self._parsed = self._tokens[:] + + try: + token = self._parsed.pop(0) + except IndexError: + token = None + + while token is not None: + if parse_options and token == "": + self._parse_argument(token) + elif parse_options and token == "--": + parse_options = False + elif parse_options and token.find("--") == 0: + if token in self._parameter_options: + self._parse_long_option(token) + else: + self._parse_argument(token) + elif parse_options and token[0] == "-" and token != "-": + if token in self._parameter_options: + self._parse_short_option(token) + else: + self._parse_argument(token) + else: + self._parse_argument(token) + + try: + token = self._parsed.pop(0) + except IndexError: + token = None diff --git a/poetry/console/logging/formatters/builder_formatter.py b/poetry/console/logging/formatters/builder_formatter.py index 56bed9b67a3..69641e7662a 100644 --- a/poetry/console/logging/formatters/builder_formatter.py +++ b/poetry/console/logging/formatters/builder_formatter.py @@ -4,7 +4,7 @@ class BuilderLogFormatter(Formatter): - def format(self, msg): # type: (str) -> str + def format(self, msg: str) -> str: if msg.startswith("Building "): msg = re.sub("Building (.+)", " - Building \\1", msg) elif msg.startswith("Built "): diff --git a/poetry/console/logging/formatters/formatter.py b/poetry/console/logging/formatters/formatter.py index 35b59374be4..f2ab7b18624 100644 --- a/poetry/console/logging/formatters/formatter.py +++ b/poetry/console/logging/formatters/formatter.py @@ -2,5 +2,5 @@ class Formatter(object): - def format(self, record): # type: (logging.LogRecord) -> str + def format(self, record: logging.LogRecord) -> str: raise NotImplementedError() diff --git a/poetry/console/logging/io_formatter.py b/poetry/console/logging/io_formatter.py index 9ff57fec761..c7ca46fef97 100644 --- a/poetry/console/logging/io_formatter.py +++ b/poetry/console/logging/io_formatter.py @@ -1,8 +1,14 @@ import logging +from typing import TYPE_CHECKING + from .formatters import FORMATTERS +if TYPE_CHECKING: + from logging import LogRecord + + class IOFormatter(logging.Formatter): _colors = { @@ -12,7 +18,7 @@ class IOFormatter(logging.Formatter): "info": "fg=blue", } - def format(self, record): + def format(self, record: "LogRecord") -> str: if not record.exc_info: level = record.levelname.lower() msg = record.msg diff --git a/poetry/console/logging/io_handler.py b/poetry/console/logging/io_handler.py index 14fd176986c..9a13e68b0e9 100644 --- a/poetry/console/logging/io_handler.py +++ b/poetry/console/logging/io_handler.py @@ -1,19 +1,27 @@ import logging +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from logging import LogRecord + + from cleo.io.io import IO # noqa + class IOHandler(logging.Handler): - def __init__(self, io): + def __init__(self, io: "IO") -> None: self._io = io super(IOHandler, self).__init__() - def emit(self, record): + def emit(self, record: "LogRecord") -> None: try: msg = self.format(record) level = record.levelname.lower() err = level in ("warning", "error", "exception", "critical") if err: - self._io.error_line(msg) + self._io.write_error_line(msg) else: self._io.write_line(msg) except Exception: diff --git a/poetry/factory.py b/poetry/factory.py old mode 100755 new mode 100644 index 90028a110f2..144bfe35871 --- a/poetry/factory.py +++ b/poetry/factory.py @@ -1,22 +1,30 @@ from __future__ import absolute_import from __future__ import unicode_literals +from pathlib import Path +from typing import TYPE_CHECKING from typing import Dict +from typing import List from typing import Optional -from clikit.api.io.io import IO +from cleo.io.io import IO +from cleo.io.null_io import NullIO from poetry.core.factory import Factory as BaseFactory from poetry.core.toml.file import TOMLFile from .config.config import Config from .config.file_config_source import FileConfigSource -from .io.null_io import NullIO from .locations import CONFIG_DIR from .packages.locker import Locker +from .packages.project_package import ProjectPackage +from .plugins.plugin_manager import PluginManager from .poetry import Poetry from .repositories.pypi_repository import PyPiRepository -from .utils._compat import Path + + +if TYPE_CHECKING: + from .repositories.legacy_repository import LegacyRepository class Factory(BaseFactory): @@ -25,8 +33,11 @@ class Factory(BaseFactory): """ def create_poetry( - self, cwd=None, io=None - ): # type: (Optional[Path], Optional[IO]) -> Poetry + self, + cwd: Optional[Path] = None, + io: Optional[IO] = None, + disable_plugins: bool = False, + ) -> Poetry: if io is None: io = NullIO() @@ -51,11 +62,13 @@ def create_poetry( # Load local sources repositories = {} + existing_repositories = config.get("repositories", {}) for source in base_poetry.pyproject.poetry_config.get("source", []): name = source.get("name") url = source.get("url") if name and url: - repositories[name] = {"url": url} + if name not in existing_repositories: + repositories[name] = {"url": url} config.merge({"repositories": repositories}) @@ -68,35 +81,23 @@ def create_poetry( ) # Configuring sources - for source in poetry.local_config.get("source", []): - repository = self.create_legacy_repository(source, config) - is_default = source.get("default", False) - is_secondary = source.get("secondary", False) - if io.is_debug(): - message = "Adding repository {} ({})".format( - repository.name, repository.url - ) - if is_default: - message += " and setting it as the default one" - elif is_secondary: - message += " and setting it as secondary" - - io.write_line(message) - - poetry.pool.add_repository(repository, is_default, secondary=is_secondary) + self.configure_sources( + poetry, poetry.local_config.get("source", []), config, io + ) - # Always put PyPI last to prefer private repositories - # but only if we have no other default source - if not poetry.pool.has_default(): - poetry.pool.add_repository(PyPiRepository(), True) - else: - if io.is_debug(): - io.write_line("Deactivating the PyPI repository") + plugin_manager = PluginManager("plugin", disable_plugins=disable_plugins) + plugin_manager.load_plugins() + poetry.set_plugin_manager(plugin_manager) + plugin_manager.activate(poetry, io) return poetry @classmethod - def create_config(cls, io=None): # type: (Optional[IO]) -> Config + def get_package(cls, name: str, version: str) -> ProjectPackage: + return ProjectPackage(name, version, version) + + @classmethod + def create_config(cls, io: Optional[IO] = None) -> Config: if io is None: io = NullIO() @@ -131,9 +132,41 @@ def create_config(cls, io=None): # type: (Optional[IO]) -> Config return config + @classmethod + def configure_sources( + cls, poetry: "Poetry", sources: List[Dict[str, str]], config: "Config", io: "IO" + ) -> None: + for source in sources: + repository = cls.create_legacy_repository(source, config) + is_default = source.get("default", False) + is_secondary = source.get("secondary", False) + if io.is_debug(): + message = "Adding repository {} ({})".format( + repository.name, repository.url + ) + if is_default: + message += " and setting it as the default one" + elif is_secondary: + message += " and setting it as secondary" + + io.write_line(message) + + poetry.pool.add_repository(repository, is_default, secondary=is_secondary) + + # Put PyPI last to prefer private repositories + # unless we have no default source AND no primary sources + # (default = false, secondary = false) + if poetry.pool.has_default(): + if io.is_debug(): + io.write_line("Deactivating the PyPI repository") + else: + default = not poetry.pool.has_primary_repositories() + poetry.pool.add_repository(PyPiRepository(), default, not default) + + @classmethod def create_legacy_repository( - self, source, auth_config - ): # type: (Dict[str, str], Config) -> LegacyRepository + cls, source: Dict[str, str], auth_config: Config + ) -> "LegacyRepository": from .repositories.legacy_repository import LegacyRepository from .utils.helpers import get_cert from .utils.helpers import get_client_cert @@ -155,3 +188,49 @@ def create_legacy_repository( cert=get_cert(auth_config, name), client_cert=get_client_cert(auth_config, name), ) + + @classmethod + def create_pyproject_from_package( + cls, package: "ProjectPackage", path: "Path" + ) -> None: + import tomlkit + + from poetry.layouts.layout import POETRY_DEFAULT + + pyproject = tomlkit.loads(POETRY_DEFAULT) + content = pyproject["tool"]["poetry"] + + content["name"] = package.name + content["version"] = package.version.text + content["description"] = package.description + content["authors"] = package.authors + + dependency_section = content["dependencies"] + dependency_section["python"] = package.python_versions + + for dep in package.requires: + constraint = tomlkit.inline_table() + if dep.is_vcs(): + constraint[dep.vcs] = dep.source_url + + if dep.reference: + constraint["rev"] = dep.reference + elif dep.is_file() or dep.is_directory(): + constraint["path"] = dep.source_url + else: + constraint["version"] = dep.pretty_constraint + + if not dep.marker.is_any(): + constraint["markers"] = str(dep.marker) + + if dep.extras: + constraint["extras"] = list(sorted(dep.extras)) + + if len(constraint) == 1 and "version" in constraint: + constraint = constraint["version"] + + dependency_section[dep.name] = constraint + + path.joinpath("pyproject.toml").write_text( + pyproject.as_string(), encoding="utf-8" + ) diff --git a/poetry/inspection/info.py b/poetry/inspection/info.py index a2d3cbeb545..9c602603ba2 100644 --- a/poetry/inspection/info.py +++ b/poetry/inspection/info.py @@ -4,6 +4,7 @@ import tarfile import zipfile +from pathlib import Path from typing import Dict from typing import Iterator from typing import List @@ -13,18 +14,15 @@ import pkginfo from poetry.core.factory import Factory -from poetry.core.packages import Package -from poetry.core.packages import ProjectPackage -from poetry.core.packages import dependency_from_pep_508 +from poetry.core.packages.dependency import Dependency +from poetry.core.packages.package import Package +from poetry.core.packages.project_package import ProjectPackage from poetry.core.pyproject.toml import PyProjectTOML -from poetry.core.utils._compat import PY35 -from poetry.core.utils._compat import Path from poetry.core.utils.helpers import parse_requires from poetry.core.utils.helpers import temporary_directory from poetry.core.version.markers import InvalidMarker from poetry.utils.env import EnvCommandError -from poetry.utils.env import EnvManager -from poetry.utils.env import VirtualEnv +from poetry.utils.env import ephemeral_environment from poetry.utils.setup_reader import SetupReader @@ -44,27 +42,25 @@ class PackageInfoError(ValueError): def __init__( - self, path, *reasons - ): # type: (Union[Path, str], *Union[BaseException, str]) -> None + self, path: Union[Path, str], *reasons: Union[BaseException, str] + ) -> None: reasons = ( "Unable to determine package info for path: {}".format(str(path)), ) + reasons - super(PackageInfoError, self).__init__( - "\n\n".join(str(msg).strip() for msg in reasons if msg) - ) + super().__init__("\n\n".join(str(msg).strip() for msg in reasons if msg)) class PackageInfo: def __init__( self, - name=None, # type: Optional[str] - version=None, # type: Optional[str] - summary=None, # type: Optional[str] - platform=None, # type: Optional[str] - requires_dist=None, # type: Optional[List[str]] - requires_python=None, # type: Optional[str] - files=None, # type: Optional[List[str]] - cache_version=None, # type: Optional[str] + name: Optional[str] = None, + version: Optional[str] = None, + summary: Optional[str] = None, + platform: Optional[str] = None, + requires_dist: Optional[List[str]] = None, + requires_python: Optional[str] = None, + files: Optional[List[str]] = None, + cache_version: Optional[str] = None, ): self.name = name self.version = version @@ -79,10 +75,10 @@ def __init__( self._source_reference = None @property - def cache_version(self): # type: () -> Optional[str] + def cache_version(self) -> Optional[str]: return self._cache_version - def update(self, other): # type: (PackageInfo) -> PackageInfo + def update(self, other: "PackageInfo") -> "PackageInfo": self.name = other.name or self.name self.version = other.version or self.version self.summary = other.summary or self.summary @@ -93,7 +89,7 @@ def update(self, other): # type: (PackageInfo) -> PackageInfo self._cache_version = other.cache_version or self._cache_version return self - def asdict(self): # type: () -> Dict[str, Optional[Union[str, List[str]]]] + def asdict(self) -> Dict[str, Optional[Union[str, List[str]]]]: """ Helper method to convert package info into a dictionary used for caching. """ @@ -109,9 +105,7 @@ def asdict(self): # type: () -> Dict[str, Optional[Union[str, List[str]]]] } @classmethod - def load( - cls, data - ): # type: (Dict[str, Optional[Union[str, List[str]]]]) -> PackageInfo + def load(cls, data: Dict[str, Optional[Union[str, List[str]]]]) -> "PackageInfo": """ Helper method to load data from a dictionary produced by `PackageInfo.asdict()`. @@ -121,13 +115,16 @@ def load( return cls(cache_version=cache_version, **data) @classmethod - def _log(cls, msg, level="info"): + def _log(cls, msg: str, level: str = "info") -> None: """Internal helper method to log information.""" - getattr(logger, level)("{}: {}".format(cls.__name__, msg)) + getattr(logger, level)(f"{cls.__name__}: {msg}") def to_package( - self, name=None, extras=None, root_dir=None - ): # type: (Optional[str], Optional[List[str]], Optional[Path]) -> Package + self, + name: Optional[str] = None, + extras: Optional[List[str]] = None, + root_dir: Optional[Path] = None, + ) -> Package: """ Create a new `poetry.core.packages.package.Package` instance using metadata from this instance. @@ -140,13 +137,11 @@ def to_package( if not self.version: # The version could not be determined, so we raise an error since it is mandatory. - raise RuntimeError( - "Unable to retrieve the package version for {}".format(name) - ) + raise RuntimeError(f"Unable to retrieve the package version for {name}") package = Package( name=name, - version=self.version, + version=self.public_version, source_type=self._source_type, source_url=self._source_url, source_reference=self._source_reference, @@ -156,14 +151,25 @@ def to_package( package.python_versions = self.requires_python or "*" package.files = self.files + if root_dir or (self._source_type in {"directory"} and self._source_url): + # this is a local poetry project, this means we can extract "richer" requirement information + # eg: development requirements etc. + poetry_package = self._get_poetry_package(path=root_dir or self._source_url) + if poetry_package: + package.extras = poetry_package.extras + package.requires = poetry_package.requires + return package + + seen_requirements = set() + for req in self.requires_dist or []: try: # Attempt to parse the PEP-508 requirement string - dependency = dependency_from_pep_508(req, relative_to=root_dir) + dependency = Dependency.create_from_pep_508(req, relative_to=root_dir) except InvalidMarker: # Invalid marker, We strip the markers hoping for the best req = req.split(";")[0] - dependency = dependency_from_pep_508(req, relative_to=root_dir) + dependency = Dependency.create_from_pep_508(req, relative_to=root_dir) except ValueError: # Likely unable to parse constraint so we skip it self._log( @@ -182,15 +188,18 @@ def to_package( package.extras[extra].append(dependency) - if dependency not in package.requires: + req = dependency.to_pep_508(with_extras=True) + + if req not in seen_requirements: package.requires.append(dependency) + seen_requirements.add(req) return package @classmethod def _from_distribution( - cls, dist - ): # type: (Union[pkginfo.BDist, pkginfo.SDist, pkginfo.Wheel]) -> PackageInfo + cls, dist: Union[pkginfo.BDist, pkginfo.SDist, pkginfo.Wheel] + ) -> "PackageInfo": """ Helper method to parse package information from a `pkginfo.Distribution` instance. @@ -221,7 +230,7 @@ def _from_distribution( return info @classmethod - def _from_sdist_file(cls, path): # type: (Path) -> PackageInfo + def _from_sdist_file(cls, path: Path) -> "PackageInfo": """ Helper method to parse package information from an sdist file. We attempt to first inspect the file using `pkginfo.SDist`. If this does not provide us with package requirements, we extract the @@ -282,11 +291,11 @@ def _from_sdist_file(cls, path): # type: (Path) -> PackageInfo return info.update(new_info) @staticmethod - def has_setup_files(path): # type: (Path) -> bool + def has_setup_files(path: Path) -> bool: return any((path / f).exists() for f in SetupReader.FILES) @classmethod - def from_setup_files(cls, path): # type: (Path) -> PackageInfo + def from_setup_files(cls, path: Path) -> "PackageInfo": """ Mechanism to parse package information from a `setup.[py|cfg]` file. This uses the implementation at `poetry.utils.setup_reader.SetupReader` in order to parse the file. This is not reliable for @@ -316,7 +325,7 @@ def from_setup_files(cls, path): # type: (Path) -> PackageInfo requires += "\n" for extra_name, deps in result["extras_require"].items(): - requires += "[{}]\n".format(extra_name) + requires += f"[{extra_name}]\n" for dep in deps: requires += dep + "\n" @@ -343,26 +352,23 @@ def from_setup_files(cls, path): # type: (Path) -> PackageInfo return info @staticmethod - def _find_dist_info(path): # type: (Path) -> Iterator[Path] + def _find_dist_info(path: Path) -> Iterator[Path]: """ Discover all `*.*-info` directories in a given path. :param path: Path to search. """ pattern = "**/*.*-info" - if PY35: - # Sometimes pathlib will fail on recursive symbolic links, so we need to workaround it - # and use the glob module instead. Note that this does not happen with pathlib2 - # so it's safe to use it for Python < 3.4. - directories = glob.iglob(path.joinpath(pattern).as_posix(), recursive=True) - else: - directories = path.glob(pattern) + # Sometimes pathlib will fail on recursive symbolic links, so we need to workaround it + # and use the glob module instead. Note that this does not happen with pathlib2 + # so it's safe to use it for Python < 3.4. + directories = glob.iglob(path.joinpath(pattern).as_posix(), recursive=True) for d in directories: yield Path(d) @classmethod - def from_metadata(cls, path): # type: (Path) -> Optional[PackageInfo] + def from_metadata(cls, path: Path) -> Optional["PackageInfo"]: """ Helper method to parse package information from an unpacked metadata directory. @@ -396,7 +402,7 @@ def from_metadata(cls, path): # type: (Path) -> Optional[PackageInfo] return info @classmethod - def from_package(cls, package): # type: (Package) -> PackageInfo + def from_package(cls, package: Package) -> "PackageInfo": """ Helper method to inspect a `Package` object, in order to generate package info. @@ -419,14 +425,14 @@ def from_package(cls, package): # type: (Package) -> PackageInfo ) @staticmethod - def _get_poetry_package(path): # type: (Path) -> Optional[ProjectPackage] + def _get_poetry_package(path: Path) -> Optional[ProjectPackage]: # Note: we ignore any setup.py file at this step # TODO: add support for handling non-poetry PEP-517 builds if PyProjectTOML(path.joinpath("pyproject.toml")).is_poetry_project(): return Factory().create_poetry(path).package @classmethod - def _pep517_metadata(cls, path): # type (Path) -> PackageInfo + def _pep517_metadata(cls, path: Path) -> "PackageInfo": """ Helper method to use PEP-517 library to build and read package metadata. @@ -440,24 +446,19 @@ def _pep517_metadata(cls, path): # type (Path) -> PackageInfo except PackageInfoError: pass - with temporary_directory() as tmp_dir: + with ephemeral_environment( + with_pip=True, with_wheel=True, with_setuptools=True + ) as venv: # TODO: cache PEP 517 build environment corresponding to each project venv - venv_dir = Path(tmp_dir) / ".venv" - EnvManager.build_venv(venv_dir.as_posix()) - venv = VirtualEnv(venv_dir, venv_dir) - - dest_dir = Path(tmp_dir) / "dist" + dest_dir = venv.path.parent / "dist" dest_dir.mkdir() try: - venv.run( - "python", - "-m", - "pip", + venv.run_pip( "install", "--disable-pip-version-check", "--ignore-installed", - *PEP517_META_BUILD_DEPS + *PEP517_META_BUILD_DEPS, ) venv.run( "python", @@ -470,7 +471,7 @@ def _pep517_metadata(cls, path): # type (Path) -> PackageInfo except EnvCommandError as e: # something went wrong while attempting pep517 metadata build # fallback to egg_info if setup.py available - cls._log("PEP517 build failed: {}".format(e), level="debug") + cls._log(f"PEP517 build failed: {e}", level="debug") setup_py = path / "setup.py" if not setup_py.exists(): raise PackageInfoError( @@ -492,18 +493,14 @@ def _pep517_metadata(cls, path): # type (Path) -> PackageInfo os.chdir(cwd.as_posix()) if info: - cls._log( - "Falling back to parsed setup.py file for {}".format(path), "debug" - ) + cls._log(f"Falling back to parsed setup.py file for {path}", "debug") return info # if we reach here, everything has failed and all hope is lost raise PackageInfoError(path, "Exhausted all core metadata sources.") @classmethod - def from_directory( - cls, path, disable_build=False - ): # type: (Path, bool) -> PackageInfo + def from_directory(cls, path: Path, disable_build: bool = False) -> "PackageInfo": """ Generate package information from a package source directory. If `disable_build` is not `True` and introspection of all available metadata fails, the package is attempted to be build in an isolated @@ -537,7 +534,7 @@ def from_directory( return info @classmethod - def from_sdist(cls, path): # type: (Path) -> PackageInfo + def from_sdist(cls, path: Path) -> "PackageInfo": """ Gather package information from an sdist file, packed or unpacked. @@ -551,7 +548,7 @@ def from_sdist(cls, path): # type: (Path) -> PackageInfo return cls.from_directory(path=path) @classmethod - def from_wheel(cls, path): # type: (Path) -> PackageInfo + def from_wheel(cls, path: Path) -> "PackageInfo": """ Gather package information from a wheel. @@ -563,7 +560,7 @@ def from_wheel(cls, path): # type: (Path) -> PackageInfo return PackageInfo() @classmethod - def from_bdist(cls, path): # type: (Path) -> PackageInfo + def from_bdist(cls, path: Path) -> "PackageInfo": """ Gather package information from a bdist (wheel etc.). @@ -581,7 +578,7 @@ def from_bdist(cls, path): # type: (Path) -> PackageInfo raise PackageInfoError(path, e) @classmethod - def from_path(cls, path): # type: (Path) -> PackageInfo + def from_path(cls, path: Path) -> "PackageInfo": """ Gather package information from a given path (bdist, sdist, directory). @@ -591,3 +588,10 @@ def from_path(cls, path): # type: (Path) -> PackageInfo return cls.from_bdist(path=path) except PackageInfoError: return cls.from_sdist(path=path) + + @property + def public_version(self) -> str: + """ + Removes + from packages. + """ + return self.version.split("+")[0] diff --git a/poetry/installation/authenticator.py b/poetry/installation/authenticator.py index 69adb844809..8c5e1d01222 100644 --- a/poetry/installation/authenticator.py +++ b/poetry/installation/authenticator.py @@ -1,23 +1,22 @@ import logging import time +import urllib.parse from typing import TYPE_CHECKING +from typing import Any +from typing import Optional +from typing import Tuple import requests import requests.auth import requests.exceptions from poetry.exceptions import PoetryException -from poetry.utils._compat import urlparse from poetry.utils.password_manager import PasswordManager if TYPE_CHECKING: - from typing import Any - from typing import Optional - from typing import Tuple - - from clikit.api.io import IO + from cleo.io.io import IO from poetry.config.config import Config @@ -25,15 +24,15 @@ logger = logging.getLogger() -class Authenticator(object): - def __init__(self, config, io=None): # type: (Config, Optional[IO]) -> None +class Authenticator: + def __init__(self, config: "Config", io: Optional["IO"] = None) -> None: self._config = config self._io = io self._session = None self._credentials = {} self._password_manager = PasswordManager(self._config) - def _log(self, message, level="debug"): # type: (str, str) -> None + def _log(self, message: str, level: str = "debug") -> None: if self._io is not None: self._io.write_line( "<{level:s}>{message:s}".format( @@ -44,15 +43,13 @@ def _log(self, message, level="debug"): # type: (str, str) -> None getattr(logger, level, logger.debug)(message) @property - def session(self): # type: () -> requests.Session + def session(self) -> requests.Session: if self._session is None: self._session = requests.Session() return self._session - def request( - self, method, url, **kwargs - ): # type: (str, str, Any) -> requests.Response + def request(self, method: str, url: str, **kwargs: Any) -> requests.Response: request = requests.Request(method, url) username, password = self.get_credentials_for_url(url) @@ -95,19 +92,15 @@ def request( if not is_last_attempt: attempt += 1 delay = 0.5 * attempt - self._log( - "Retrying HTTP request in {} seconds.".format(delay), level="debug" - ) + self._log(f"Retrying HTTP request in {delay} seconds.", level="debug") time.sleep(delay) continue # this should never really be hit under any sane circumstance raise PoetryException("Failed HTTP {} request", method.upper()) - def get_credentials_for_url( - self, url - ): # type: (str) -> Tuple[Optional[str], Optional[str]] - parsed_url = urlparse.urlsplit(url) + def get_credentials_for_url(self, url: str) -> Tuple[Optional[str], Optional[str]]: + parsed_url = urllib.parse.urlsplit(url) netloc = parsed_url.netloc @@ -130,7 +123,7 @@ def get_credentials_for_url( credentials = auth, None credentials = tuple( - None if x is None else urlparse.unquote(x) for x in credentials + None if x is None else urllib.parse.unquote(x) for x in credentials ) if credentials[0] is not None or credentials[1] is not None: @@ -141,14 +134,12 @@ def get_credentials_for_url( return credentials[0], credentials[1] def _get_credentials_for_netloc_from_config( - self, netloc - ): # type: (str) -> Tuple[Optional[str], Optional[str]] + self, netloc: str + ) -> Tuple[Optional[str], Optional[str]]: credentials = (None, None) for repository_name in self._config.get("repositories", []): - repository_config = self._config.get( - "repositories.{}".format(repository_name) - ) + repository_config = self._config.get(f"repositories.{repository_name}") if not repository_config: continue @@ -156,7 +147,7 @@ def _get_credentials_for_netloc_from_config( if not url: continue - parsed_url = urlparse.urlsplit(url) + parsed_url = urllib.parse.urlsplit(url) if netloc == parsed_url.netloc: auth = self._password_manager.get_http_auth(repository_name) diff --git a/poetry/installation/base_installer.py b/poetry/installation/base_installer.py index 1e068d076cd..ecb6dadf3df 100644 --- a/poetry/installation/base_installer.py +++ b/poetry/installation/base_installer.py @@ -1,9 +1,16 @@ +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from poetry.core.packages.package import Package + + class BaseInstaller: - def install(self, package): + def install(self, package: "Package") -> None: raise NotImplementedError - def update(self, source, target): + def update(self, source: "Package", target: "Package") -> None: raise NotImplementedError - def remove(self, package): + def remove(self, package: "Package") -> None: raise NotImplementedError diff --git a/poetry/installation/chef.py b/poetry/installation/chef.py index 669ce17768e..ce095a03d5c 100644 --- a/poetry/installation/chef.py +++ b/poetry/installation/chef.py @@ -1,47 +1,47 @@ import hashlib import json +from pathlib import Path from typing import TYPE_CHECKING +from typing import List +from typing import Optional from poetry.core.packages.utils.link import Link -from poetry.utils._compat import Path from .chooser import InvalidWheelName from .chooser import Wheel if TYPE_CHECKING: - from typing import List - from typing import Optional from poetry.config.config import Config from poetry.utils.env import Env class Chef: - def __init__(self, config, env): # type: (Config, Env) -> None + def __init__(self, config: "Config", env: "Env") -> None: self._config = config self._env = env self._cache_dir = ( Path(config.get("cache-dir")).expanduser().joinpath("artifacts") ) - def prepare(self, archive): # type: (Path) -> Path + def prepare(self, archive: Path) -> Path: return archive - def prepare_sdist(self, archive): # type: (Path) -> Path + def prepare_sdist(self, archive: Path) -> Path: return archive - def prepare_wheel(self, archive): # type: (Path) -> Path + def prepare_wheel(self, archive: Path) -> Path: return archive - def should_prepare(self, archive): # type: (Path) -> bool + def should_prepare(self, archive: Path) -> bool: return not self.is_wheel(archive) - def is_wheel(self, archive): # type: (Path) -> bool + def is_wheel(self, archive: Path) -> bool: return archive.suffix == ".whl" - def get_cached_archive_for_link(self, link): # type: (Link) -> Optional[Link] + def get_cached_archive_for_link(self, link: Link) -> Optional[Link]: # If the archive is already a wheel, there is no need to cache it. if link.is_wheel: pass @@ -74,18 +74,18 @@ def get_cached_archive_for_link(self, link): # type: (Link) -> Optional[Link] return min(candidates)[1] - def get_cached_archives_for_link(self, link): # type: (Link) -> List[Link] + def get_cached_archives_for_link(self, link: Link) -> List[Link]: cache_dir = self.get_cache_directory_for_link(link) archive_types = ["whl", "tar.gz", "tar.bz2", "bz2", "zip"] links = [] for archive_type in archive_types: - for archive in cache_dir.glob("*.{}".format(archive_type)): + for archive in cache_dir.glob(f"*.{archive_type}"): links.append(Link(archive.as_uri())) return links - def get_cache_directory_for_link(self, link): # type: (Link) -> Path + def get_cache_directory_for_link(self, link: Link) -> Path: key_parts = {"url": link.url_without_fragment} if link.hash_name is not None and link.hash is not None: diff --git a/poetry/installation/chooser.py b/poetry/installation/chooser.py index db9e0a9c357..4f9b469d823 100644 --- a/poetry/installation/chooser.py +++ b/poetry/installation/chooser.py @@ -1,6 +1,7 @@ import re from typing import List +from typing import Optional from typing import Tuple from packaging.tags import Tag @@ -16,11 +17,11 @@ class InvalidWheelName(Exception): pass -class Wheel(object): - def __init__(self, filename): # type: (str) -> None +class Wheel: + def __init__(self, filename: str) -> None: wheel_info = wheel_file_re.match(filename) if not wheel_info: - raise InvalidWheelName("{} is not a valid wheel filename.".format(filename)) + raise InvalidWheelName(f"{filename} is not a valid wheel filename.") self.filename = filename self.name = wheel_info.group("name").replace("_", "-") @@ -34,12 +35,12 @@ def __init__(self, filename): # type: (str) -> None Tag(x, y, z) for x in self.pyversions for y in self.abis for z in self.plats } - def get_minimum_supported_index(self, tags): + def get_minimum_supported_index(self, tags: List[Tag]) -> Optional[int]: indexes = [tags.index(t) for t in self.tags if t in tags] return min(indexes) if indexes else None - def is_supported_by_environment(self, env): + def is_supported_by_environment(self, env: Env) -> bool: return bool(set(env.supported_tags).intersection(self.tags)) @@ -48,11 +49,11 @@ class Chooser: A Chooser chooses an appropriate release archive for packages. """ - def __init__(self, pool, env): # type: (Pool, Env) -> None + def __init__(self, pool: Pool, env: Env) -> None: self._pool = pool self._env = env - def choose_for(self, package): # type: (Package) -> Link + def choose_for(self, package: Package) -> Link: """ Return the url of the selected archive for a given package. """ @@ -63,26 +64,22 @@ def choose_for(self, package): # type: (Package) -> Link ): continue - if link.ext == ".egg": + if link.ext in {".egg", ".exe", ".msi", ".rpm", ".srpm"}: continue links.append(link) if not links: - raise RuntimeError( - "Unable to find installation candidates for {}".format(package) - ) + raise RuntimeError(f"Unable to find installation candidates for {package}") # Get the best link chosen = max(links, key=lambda link: self._sort_key(package, link)) if not chosen: - raise RuntimeError( - "Unable to find installation candidates for {}".format(package) - ) + raise RuntimeError(f"Unable to find installation candidates for {package}") return chosen - def _get_links(self, package): # type: (Package) -> List[Link] + def _get_links(self, package: Package) -> List[Link]: if not package.source_type: if not self._pool.has_repository("pypi"): repository = self._pool.repositories[0] @@ -109,9 +106,14 @@ def _get_links(self, package): # type: (Package) -> List[Link] selected_links.append(link) + if links and not selected_links: + raise RuntimeError( + f"Retrieved digest for link {link.filename}({h}) not in poetry.lock metadata {hashes}" + ) + return selected_links - def _sort_key(self, package, link): # type: (Package, Link) -> Tuple + def _sort_key(self, package: Package, link: Link) -> Tuple: """ Function to pass as the `key` argument to a call to sorted() to sort InstallationCandidates by preference. @@ -169,9 +171,7 @@ def _sort_key(self, package, link): # type: (Package, Link) -> Tuple pri, ) - def _is_link_hash_allowed_for_package( - self, link, package - ): # type: (Link, Package) -> bool + def _is_link_hash_allowed_for_package(self, link: Link, package: Package) -> bool: if not link.hash: return True diff --git a/poetry/installation/executor.py b/poetry/installation/executor.py index 9effaf3ebeb..4580656c933 100644 --- a/poetry/installation/executor.py +++ b/poetry/installation/executor.py @@ -1,27 +1,30 @@ -# -*- coding: utf-8 -*- -from __future__ import division - +import csv import itertools +import json import os import threading from concurrent.futures import ThreadPoolExecutor from concurrent.futures import wait +from pathlib import Path from subprocess import CalledProcessError +from typing import TYPE_CHECKING +from typing import Any +from typing import Dict +from typing import List +from typing import Union + +from cleo.io.null_io import NullIO from poetry.core.packages.file_dependency import FileDependency from poetry.core.packages.utils.link import Link from poetry.core.pyproject.toml import PyProjectTOML -from poetry.io.null_io import NullIO -from poetry.utils._compat import PY2 -from poetry.utils._compat import WINDOWS -from poetry.utils._compat import OrderedDict -from poetry.utils._compat import Path -from poetry.utils._compat import cpu_count from poetry.utils._compat import decode from poetry.utils.env import EnvCommandError from poetry.utils.helpers import safe_rmtree +from poetry.utils.pip import pip_editable_install +from ..utils.pip import pip_install from .authenticator import Authenticator from .chef import Chef from .chooser import Chooser @@ -31,8 +34,26 @@ from .operations.update import Update -class Executor(object): - def __init__(self, env, pool, config, io, parallel=True): +if TYPE_CHECKING: + from cleo.io.io import IO # noqa + + from poetry.config.config import Config + from poetry.core.packages.package import Package + from poetry.repositories import Pool + from poetry.utils.env import Env + + from .operations import OperationTypes + + +class Executor: + def __init__( + self, + env: "Env", + pool: "Pool", + config: "Config", + io: "IO", + parallel: bool = None, + ) -> None: self._env = env self._io = io self._dry_run = False @@ -42,13 +63,16 @@ def __init__(self, env, pool, config, io, parallel=True): self._chef = Chef(config, self._env) self._chooser = Chooser(pool, self._env) - if parallel and not (PY2 and WINDOWS): + if parallel is None: + parallel = config.get("installer.parallel", True) + + if parallel: # This should be directly handled by ThreadPoolExecutor # however, on some systems the number of CPUs cannot be determined # (it raises a NotImplementedError), so, in this case, we assume # that the system only has one CPU. try: - self._max_workers = cpu_count() + 4 + self._max_workers = os.cpu_count() + 4 except NotImplementedError: self._max_workers = 5 else: @@ -59,41 +83,62 @@ def __init__(self, env, pool, config, io, parallel=True): self._executed_operations = 0 self._executed = {"install": 0, "update": 0, "uninstall": 0} self._skipped = {"install": 0, "update": 0, "uninstall": 0} - self._sections = OrderedDict() + self._sections = dict() self._lock = threading.Lock() self._shutdown = False + self._hashes: Dict[str, str] = {} @property - def installations_count(self): # type: () -> int + def installations_count(self) -> int: return self._executed["install"] @property - def updates_count(self): # type: () -> int + def updates_count(self) -> int: return self._executed["update"] @property - def removals_count(self): # type: () -> int + def removals_count(self) -> int: return self._executed["uninstall"] - def supports_fancy_output(self): # type: () -> bool - return self._io.supports_ansi() and not self._dry_run + def supports_fancy_output(self) -> bool: + return self._io.output.is_decorated() and not self._dry_run - def disable(self): + def disable(self) -> "Executor": self._enabled = False return self - def dry_run(self, dry_run=True): + def dry_run(self, dry_run: bool = True) -> "Executor": self._dry_run = dry_run return self - def verbose(self, verbose=True): + def verbose(self, verbose: bool = True) -> "Executor": self._verbose = verbose return self - def execute(self, operations): # type: (Operation) -> int + def pip_install( + self, req: Union[Path, str], upgrade: bool = False, editable: bool = False + ) -> int: + func = pip_install + if editable: + func = pip_editable_install + + try: + func(req, self._env, upgrade=upgrade) + except EnvCommandError as e: + output = decode(e.e.output) + if ( + "KeyboardInterrupt" in output + or "ERROR: Operation cancelled by user" in output + ): + return -2 + raise + + return 0 + + def execute(self, operations: List["OperationTypes"]) -> int: self._total_operations = len(operations) for job_type in self._executed: self._executed[job_type] = 0 @@ -104,17 +149,36 @@ def execute(self, operations): # type: (Operation) -> int # We group operations by priority groups = itertools.groupby(operations, key=lambda o: -o.priority) - self._sections = OrderedDict() + self._sections = dict() for _, group in groups: tasks = [] + serial_operations = [] for operation in group: if self._shutdown: break + # Some operations are unsafe, we must execute them serially in a group + # https://github.com/python-poetry/poetry/issues/3086 + # https://github.com/python-poetry/poetry/issues/2658 + # + # We need to explicitly check source type here, see: + # https://github.com/python-poetry/poetry-core/pull/98 + is_parallel_unsafe = operation.job_type == "uninstall" or ( + operation.package.develop + and operation.package.source_type in {"directory", "git"} + ) + if not operation.skipped and is_parallel_unsafe: + serial_operations.append(operation) + continue + tasks.append(self._executor.submit(self._execute_operation, operation)) try: wait(tasks) + + for operation in serial_operations: + wait([self._executor.submit(self._execute_operation, operation)]) + except KeyboardInterrupt: self._shutdown = True @@ -127,7 +191,7 @@ def execute(self, operations): # type: (Operation) -> int return 1 if self._shutdown else 0 - def _write(self, operation, line): + def _write(self, operation: "OperationTypes", line: str) -> None: if not self.supports_fancy_output() or not self._should_write_operation( operation ): @@ -142,10 +206,10 @@ def _write(self, operation, line): with self._lock: section = self._sections[id(operation)] - section.output.clear() + section.clear() section.write(line) - def _execute_operation(self, operation): + def _execute_operation(self, operation: "OperationTypes") -> None: try: if self.supports_fancy_output(): if id(operation) not in self._sections: @@ -191,13 +255,15 @@ def _execute_operation(self, operation): raise KeyboardInterrupt except Exception as e: try: - from clikit.ui.components.exception_trace import ExceptionTrace + from cleo.ui.exception_trace import ExceptionTrace if not self.supports_fancy_output(): io = self._io else: - message = " {message}: Failed".format( - message=self.get_operation_message(operation, error=True), + message = ( + " {message}: Failed".format( + message=self.get_operation_message(operation, error=True), + ) ) self._write(operation, message) io = self._sections.get(id(operation), self._io) @@ -222,7 +288,7 @@ def _execute_operation(self, operation): with self._lock: self._shutdown = True - def _do_execute_operation(self, operation): + def _do_execute_operation(self, operation: "OperationTypes") -> int: method = operation.job_type operation_message = self.get_operation_message(operation) @@ -234,7 +300,8 @@ def _do_execute_operation(self, operation): "Skipped " "for the following reason: " "{reason}".format( - message=operation_message, reason=operation.skip_reason, + message=operation_message, + reason=operation.skip_reason, ), ) @@ -251,7 +318,7 @@ def _do_execute_operation(self, operation): return 0 - result = getattr(self, "_execute_{}".format(method))(operation) + result = getattr(self, f"_execute_{method}")(operation) if result != 0: return result @@ -265,7 +332,9 @@ def _do_execute_operation(self, operation): return result - def _increment_operations_count(self, operation, executed): + def _increment_operations_count( + self, operation: "OperationTypes", executed: bool + ) -> None: with self._lock: if executed: self._executed_operations += 1 @@ -273,7 +342,7 @@ def _increment_operations_count(self, operation, executed): else: self._skipped[operation.job_type] += 1 - def run_pip(self, *args, **kwargs): # type: (...) -> int + def run_pip(self, *args: Any, **kwargs: Any) -> int: try: self._env.run_pip(*args, **kwargs) except EnvCommandError as e: @@ -288,7 +357,13 @@ def run_pip(self, *args, **kwargs): # type: (...) -> int return 0 - def get_operation_message(self, operation, done=False, error=False, warning=False): + def get_operation_message( + self, + operation: "OperationTypes", + done: bool = False, + error: bool = False, + warning: bool = False, + ) -> str: base_tag = "fg=default" operation_color = "c2" source_operation_color = "c2" @@ -342,7 +417,7 @@ def get_operation_message(self, operation, done=False, error=False, warning=Fals return "" - def _display_summary(self, operations): + def _display_summary(self, operations: List["OperationTypes"]) -> None: installs = 0 updates = 0 uninstalls = 0 @@ -378,28 +453,36 @@ def _display_summary(self, operations): "" if updates == 1 else "s", uninstalls, "" if uninstalls == 1 else "s", - ", {} skipped".format(skipped) - if skipped and self._verbose - else "", + f", {skipped} skipped" if skipped and self._verbose else "", ) ) self._io.write_line("") - def _execute_install(self, operation): # type: (Install) -> None - return self._install(operation) + def _execute_install(self, operation: Union[Install, Update]) -> int: + status_code = self._install(operation) + + self._save_url_reference(operation) + + return status_code + + def _execute_update(self, operation: Union[Install, Update]) -> int: + status_code = self._update(operation) + + self._save_url_reference(operation) - def _execute_update(self, operation): # type: (Update) -> None - return self._update(operation) + return status_code - def _execute_uninstall(self, operation): # type: (Uninstall) -> None - message = " • {message}: Removing...".format( - message=self.get_operation_message(operation), + def _execute_uninstall(self, operation: Uninstall) -> int: + message = ( + " • {message}: Removing...".format( + message=self.get_operation_message(operation), + ) ) self._write(operation, message) return self._remove(operation) - def _install(self, operation): + def _install(self, operation: Union[Install, Update]) -> int: package = operation.package if package.source_type == "directory": return self._install_directory(operation) @@ -415,21 +498,18 @@ def _install(self, operation): archive = self._download(operation) operation_message = self.get_operation_message(operation) - message = " • {message}: Installing...".format( - message=operation_message, + message = ( + " • {message}: Installing...".format( + message=operation_message, + ) ) self._write(operation, message) + return self.pip_install(str(archive), upgrade=operation.job_type == "update") - args = ["install", "--no-deps", str(archive)] - if operation.job_type == "update": - args.insert(2, "-U") - - return self.run_pip(*args) - - def _update(self, operation): + def _update(self, operation: Union[Install, Update]) -> int: return self._install(operation) - def _remove(self, operation): + def _remove(self, operation: Uninstall) -> int: package = operation.package # If we have a VCS package, remove its source directory @@ -446,11 +526,13 @@ def _remove(self, operation): raise - def _prepare_file(self, operation): + def _prepare_file(self, operation: Union[Install, Update]) -> Path: package = operation.package - message = " • {message}: Preparing...".format( - message=self.get_operation_message(operation), + message = ( + " • {message}: Preparing...".format( + message=self.get_operation_message(operation), + ) ) self._write(operation, message) @@ -462,23 +544,23 @@ def _prepare_file(self, operation): return archive - def _install_directory(self, operation): + def _install_directory(self, operation: Union[Install, Update]) -> int: from poetry.factory import Factory package = operation.package operation_message = self.get_operation_message(operation) - message = " • {message}: Building...".format( - message=operation_message, + message = ( + " • {message}: Building...".format( + message=operation_message, + ) ) self._write(operation, message) if package.root_dir: - req = os.path.join(str(package.root_dir), package.source_url) + req = package.root_dir / package.source_url else: - req = os.path.realpath(package.source_url) - - args = ["install", "--no-deps", "-U"] + req = Path(package.source_url).resolve(strict=False) pyproject = PyProjectTOML(os.path.join(req, "pyproject.toml")) @@ -487,8 +569,9 @@ def _install_directory(self, operation): # some versions of pip (< 19.0.0) don't understand it # so we need to check the version of pip to know # if we can rely on the build system - legacy_pip = self._env.pip_version < self._env.pip_version.__class__( - 19, 0, 0 + legacy_pip = ( + self._env.pip_version + < self._env.pip_version.__class__.from_parts(19, 0, 0) ) package_poetry = Factory().create_poetry(pyproject.file.path.parent) @@ -513,27 +596,24 @@ def _install_directory(self, operation): with builder.setup_py(): if package.develop: - args.append("-e") - - args.append(req) - - return self.run_pip(*args) + return self.pip_install(req, editable=True) + return self.pip_install(req, upgrade=True) if package.develop: - args.append("-e") + return self.pip_install(req, editable=True) - args.append(req) + return self.pip_install(req, upgrade=True) - return self.run_pip(*args) - - def _install_git(self, operation): + def _install_git(self, operation: Union[Install, Update]) -> int: from poetry.core.vcs import Git package = operation.package operation_message = self.get_operation_message(operation) - message = " • {message}: Cloning...".format( - message=operation_message, + message = ( + " • {message}: Cloning...".format( + message=operation_message, + ) ) self._write(operation, message) @@ -545,19 +625,29 @@ def _install_git(self, operation): git = Git() git.clone(package.source_url, src_dir) - git.checkout(package.source_reference, src_dir) + + reference = package.source_resolved_reference + if not reference: + reference = package.source_reference + + git.checkout(reference, src_dir) # Now we just need to install from the source directory + original_url = package.source_url package._source_url = str(src_dir) - return self._install_directory(operation) + status_code = self._install_directory(operation) + + package._source_url = original_url - def _download(self, operation): # type: (Operation) -> Path + return status_code + + def _download(self, operation: Union[Install, Update]) -> Link: link = self._chooser.choose_for(operation.package) return self._download_link(operation, link) - def _download_link(self, operation, link): + def _download_link(self, operation: Union[Install, Update], link: Link) -> Link: package = operation.package archive = self._chef.get_cached_archive_for_link(link) @@ -581,32 +671,42 @@ def _download_link(self, operation, link): archive = self._chef.prepare(archive) if package.files: - archive_hash = "sha256:" + FileDependency(package.name, archive).hash() + archive_hash = ( + "sha256:" + + FileDependency( + package.name, + Path(archive.path) if isinstance(archive, Link) else archive, + ).hash() + ) if archive_hash not in {f["hash"] for f in package.files}: raise RuntimeError( - "Invalid hash for {} using archive {}".format(package, archive.name) + f"Invalid hash for {package} using archive {archive.name}" ) + self._hashes[package.name] = archive_hash + return archive - def _download_archive(self, operation, link): # type: (Operation, Link) -> Path + def _download_archive(self, operation: Union[Install, Update], link: Link) -> Path: response = self._authenticator.request( "get", link.url, stream=True, io=self._sections.get(id(operation), self._io) ) wheel_size = response.headers.get("content-length") operation_message = self.get_operation_message(operation) - message = " • {message}: Downloading...".format( - message=operation_message, + message = ( + " • {message}: Downloading...".format( + message=operation_message, + ) ) progress = None if self.supports_fancy_output(): if wheel_size is None: self._write(operation, message) else: - from clikit.ui.components.progress_bar import ProgressBar + from cleo.ui.progress_bar import ProgressBar progress = ProgressBar( - self._sections[id(operation)].output, max=int(wheel_size) + self._sections[id(operation)], max=int(wheel_size) ) progress.set_format(message + " %percent%%") @@ -636,8 +736,122 @@ def _download_archive(self, operation, link): # type: (Operation, Link) -> Path return archive - def _should_write_operation(self, operation): # type: (Operation) -> bool - if not operation.skipped: - return True + def _should_write_operation(self, operation: Operation) -> bool: + return not operation.skipped or self._dry_run or self._verbose + + def _save_url_reference(self, operation: "OperationTypes") -> None: + """ + Create and store a PEP-610 `direct_url.json` file, if needed. + """ + if operation.job_type not in {"install", "update"}: + return + + package = operation.package + + if not package.source_url: + # Since we are installing from our own distribution cache + # pip will write a `direct_url.json` file pointing to the cache + # distribution. + # That's not what we want so we remove the direct_url.json file, + # if it exists. + for ( + direct_url_json + ) in self._env.site_packages.find_distribution_direct_url_json_files( + distribution_name=package.name, writable_only=True + ): + # We can't use unlink(missing_ok=True) because it's not always available + if direct_url_json.exists(): + direct_url_json.unlink() + return + + url_reference = None + + if package.source_type == "git": + url_reference = self._create_git_url_reference(package) + elif package.source_type == "url": + url_reference = self._create_url_url_reference(package) + elif package.source_type == "directory": + url_reference = self._create_directory_url_reference(package) + elif package.source_type == "file": + url_reference = self._create_file_url_reference(package) + + if url_reference: + for dist in self._env.site_packages.distributions( + name=package.name, writable_only=True + ): + dist._path.joinpath("direct_url.json").write_text( + json.dumps(url_reference), + encoding="utf-8", + ) + + record = dist._path.joinpath("RECORD") + if record.exists(): + with record.open(mode="a", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerow( + [ + str( + dist._path.joinpath("direct_url.json").relative_to( + record.parent.parent + ) + ), + "", + "", + ] + ) + + def _create_git_url_reference( + self, package: "Package" + ) -> Dict[str, Union[str, Dict[str, str]]]: + reference = { + "url": package.source_url, + "vcs_info": { + "vcs": "git", + "requested_revision": package.source_reference, + "commit_id": package.source_resolved_reference, + }, + } + + return reference + + def _create_url_url_reference( + self, package: "Package" + ) -> Dict[str, Union[str, Dict[str, str]]]: + archive_info = {} + + if package.name in self._hashes: + archive_info["hash"] = self._hashes[package.name] + + reference = {"url": package.source_url, "archive_info": archive_info} + + return reference + + def _create_file_url_reference( + self, package: "Package" + ) -> Dict[str, Union[str, Dict[str, str]]]: + archive_info = {} + + if package.name in self._hashes: + archive_info["hash"] = self._hashes[package.name] + + reference = { + "url": Path(package.source_url).as_uri(), + "archive_info": archive_info, + } + + return reference + + def _create_directory_url_reference( + self, package: "Package" + ) -> Dict[str, Union[str, Dict[str, str]]]: + dir_info = {} + + if package.develop: + dir_info["editable"] = True + + reference = { + "url": Path(package.source_url).as_uri(), + "dir_info": dir_info, + } - return self._dry_run or self._verbose + return reference diff --git a/poetry/installation/installer.py b/poetry/installation/installer.py index f0c9a62d65d..0718eef52d0 100644 --- a/poetry/installation/installer.py +++ b/poetry/installation/installer.py @@ -1,12 +1,14 @@ +from typing import TYPE_CHECKING +from typing import Iterable from typing import List from typing import Optional from typing import Union -from clikit.api.io import IO +from cleo.io.io import IO +from cleo.io.null_io import NullIO from poetry.config.config import Config from poetry.core.packages.project_package import ProjectPackage -from poetry.io.null_io import NullIO from poetry.packages import Locker from poetry.repositories import Pool from poetry.repositories import Repository @@ -23,17 +25,23 @@ from .pip_installer import PipInstaller +if TYPE_CHECKING: + from poetry.utils.env import Env + + from .operations import OperationTypes + + class Installer: def __init__( self, - io, # type: IO - env, - package, # type: ProjectPackage - locker, # type: Locker - pool, # type: Pool - config, # type: Config - installed=None, # type: Union[InstalledRepository, None] - executor=None, # type: Optional[Executor] + io: IO, + env: "Env", + package: ProjectPackage, + locker: Locker, + pool: Pool, + config: Config, + installed: Union[Repository, None] = None, + executor: Optional[Executor] = None, ): self._io = io self._env = env @@ -47,6 +55,7 @@ def __init__( self._verbose = False self._write_lock = True self._dev_mode = True + self._dev_only = False self._execute_operations = True self._lock = False @@ -67,24 +76,28 @@ def __init__( self._installed_repository = installed @property - def executor(self): + def executor(self) -> Executor: return self._executor @property - def installer(self): + def installer(self) -> BaseInstaller: return self._installer - def set_package(self, package): # type: (ProjectPackage) -> Installer + def set_package(self, package: ProjectPackage) -> "Installer": self._package = package return self - def set_locker(self, locker): # type: (Locker) -> Installer + def set_locker(self, locker: Locker) -> "Installer": self._locker = locker return self - def run(self): + def run(self) -> int: + # Check if refresh + if not self._update and self._lock and self._locker.is_locked(): + return self._do_refresh() + # Force update if there is no lock file present if not self._update and not self._locker.is_locked(): self._update = True @@ -98,59 +111,67 @@ def run(self): return self._do_install(local_repo) - def dry_run(self, dry_run=True): # type: (bool) -> Installer + def dry_run(self, dry_run: bool = True) -> "Installer": self._dry_run = dry_run self._executor.dry_run(dry_run) return self - def is_dry_run(self): # type: () -> bool + def is_dry_run(self) -> bool: return self._dry_run - def remove_untracked(self, remove_untracked=True): # type: (bool) -> Installer + def remove_untracked(self, remove_untracked: bool = True) -> "Installer": self._remove_untracked = remove_untracked return self - def is_remove_untracked(self): # type: () -> bool + def is_remove_untracked(self) -> bool: return self._remove_untracked - def verbose(self, verbose=True): # type: (bool) -> Installer + def verbose(self, verbose: bool = True) -> "Installer": self._verbose = verbose self._executor.verbose(verbose) return self - def is_verbose(self): # type: () -> bool + def is_verbose(self) -> bool: return self._verbose - def dev_mode(self, dev_mode=True): # type: (bool) -> Installer + def dev_mode(self, dev_mode: bool = True) -> "Installer": self._dev_mode = dev_mode return self - def is_dev_mode(self): # type: () -> bool + def is_dev_mode(self) -> bool: return self._dev_mode - def update(self, update=True): # type: (bool) -> Installer + def dev_only(self, dev_only: bool = False) -> "Installer": + self._dev_only = dev_only + + return self + + def is_dev_only(self) -> bool: + return self._dev_only + + def update(self, update: bool = True) -> "Installer": self._update = update return self - def lock(self): # type: () -> Installer + def lock(self, update: bool = True) -> "Installer": """ Prepare the installer for locking only. """ - self.update() + self.update(update=update) self.execute_operations(False) self._lock = True return self - def is_updating(self): # type: () -> bool + def is_updating(self) -> bool: return self._update - def execute_operations(self, execute=True): # type: (bool) -> Installer + def execute_operations(self, execute: bool = True) -> "Installer": self._execute_operations = execute if not execute: @@ -158,22 +179,48 @@ def execute_operations(self, execute=True): # type: (bool) -> Installer return self - def whitelist(self, packages): # type: (dict) -> Installer + def whitelist(self, packages: Iterable[str]) -> "Installer": self._whitelist = [canonicalize_name(p) for p in packages] return self - def extras(self, extras): # type: (list) -> Installer + def extras(self, extras: list) -> "Installer": self._extras = extras return self - def use_executor(self, use_executor=True): # type: (bool) -> Installer + def use_executor(self, use_executor: bool = True) -> "Installer": self._use_executor = use_executor return self - def _do_install(self, local_repo): + def _do_refresh(self) -> int: + from poetry.puzzle import Solver + + # Checking extras + for extra in self._extras: + if extra not in self._package.extras: + raise ValueError(f"Extra [{extra}] is not specified.") + + locked_repository = self._locker.locked_repository(True) + solver = Solver( + self._package, + self._pool, + locked_repository, + locked_repository, + self._io, + ) + + ops = solver.solve(use_latest=[]) + + local_repo = Repository() + self._populate_local_repo(local_repo, ops) + + self._write_lock_file(local_repo, force=True) + + return 0 + + def _do_install(self, local_repo: Repository) -> int: from poetry.puzzle import Solver locked_repository = Repository() @@ -190,7 +237,7 @@ def _do_install(self, local_repo): # Checking extras for extra in self._extras: if extra not in self._package.extras: - raise ValueError("Extra [{}] is not specified.".format(extra)) + raise ValueError(f"Extra [{extra}] is not specified.") self._io.write_line("Updating dependencies") solver = Solver( @@ -220,7 +267,7 @@ def _do_install(self, local_repo): for extra in self._extras: if extra not in self._locker.lock_data.get("extras", {}): - raise ValueError("Extra [{}] is not specified.".format(extra)) + raise ValueError(f"Extra [{extra}] is not specified.") # If we are installing from lock # Filter the operations by comparing it with what is @@ -240,6 +287,9 @@ def _do_install(self, local_repo): if not self.is_dev_mode(): root = root.clone() del root.dev_requires[:] + elif self.is_dev_only(): + root = root.clone() + del root.requires[:] if self._io.is_verbose(): self._io.write_line("") @@ -259,12 +309,6 @@ def _do_install(self, local_repo): pool.add_repository(repo) - # We whitelist all packages to be sure - # that the latest ones are picked up - whitelist = [] - for pkg in locked_repository.packages: - whitelist.append(pkg.name) - solver = Solver( root, pool, @@ -273,9 +317,12 @@ def _do_install(self, local_repo): NullIO(), remove_untracked=self._remove_untracked, ) + # Everything is resolved at this point, so we no longer need + # to load deferred dependencies (i.e. VCS, URL and path dependencies) + solver.provider.load_deferred(False) with solver.use_environment(self._env): - ops = solver.solve(use_latest=whitelist) + ops = solver.solve(use_latest=self._whitelist) # We need to filter operations so that packages # not compatible with the current system, @@ -285,15 +332,15 @@ def _do_install(self, local_repo): # Execute operations return self._execute(ops) - def _write_lock_file(self, repo): # type: (Repository) -> None - if self._update and self._write_lock: + def _write_lock_file(self, repo: Repository, force: bool = True) -> None: + if force or (self._update and self._write_lock): updated_lock = self._locker.set_lock_data(self._package, repo.packages) if updated_lock: self._io.write_line("") self._io.write_line("Writing lock file") - def _execute(self, operations): + def _execute(self, operations: List["OperationTypes"]) -> int: if self._use_executor: return self._executor.execute(operations) @@ -328,7 +375,7 @@ def _execute(self, operations): "" if updates == 1 else "s", uninstalls, "" if uninstalls == 1 else "s", - ", {} skipped".format(skipped) + f", {skipped} skipped" if skipped and self.is_verbose() else "", ) @@ -341,15 +388,15 @@ def _execute(self, operations): return 0 - def _execute_operation(self, operation): # type: (Operation) -> None + def _execute_operation(self, operation: Operation) -> None: """ Execute a given operation. """ method = operation.job_type - getattr(self, "_execute_{}".format(method))(operation) + getattr(self, f"_execute_{method}")(operation) - def _execute_install(self, operation): # type: (Install) -> None + def _execute_install(self, operation: Install) -> None: if operation.skipped: if self.is_verbose() and (self._execute_operations or self.is_dry_run()): self._io.write_line( @@ -374,7 +421,7 @@ def _execute_install(self, operation): # type: (Install) -> None self._installer.install(operation.package) - def _execute_update(self, operation): # type: (Update) -> None + def _execute_update(self, operation: Update) -> None: source = operation.initial_package target = operation.target_package @@ -404,7 +451,7 @@ def _execute_update(self, operation): # type: (Update) -> None self._installer.update(source, target) - def _execute_uninstall(self, operation): # type: (Uninstall) -> None + def _execute_uninstall(self, operation: Uninstall) -> None: if operation.skipped: if self.is_verbose() and (self._execute_operations or self.is_dry_run()): self._io.write_line( @@ -429,7 +476,9 @@ def _execute_uninstall(self, operation): # type: (Uninstall) -> None self._installer.remove(operation.package) - def _populate_local_repo(self, local_repo, ops): + def _populate_local_repo( + self, local_repo: Repository, ops: List[Operation] + ) -> None: for op in ops: if isinstance(op, Uninstall): continue @@ -442,8 +491,8 @@ def _populate_local_repo(self, local_repo, ops): local_repo.add_package(package) def _get_operations_from_lock( - self, locked_repository # type: Repository - ): # type: (...) -> List[Operation] + self, locked_repository: Repository + ) -> List[Operation]: installed_repo = self._installed_repository ops = [] @@ -474,9 +523,7 @@ def _get_operations_from_lock( return ops - def _filter_operations( - self, ops, repo - ): # type: (List[Operation], Repository) -> None + def _filter_operations(self, ops: List[Operation], repo: Repository) -> None: extra_packages = self._get_extra_packages(repo) for op in ops: if isinstance(op, Update): @@ -511,7 +558,7 @@ def _filter_operations( if package.category == "dev" and not self.is_dev_mode(): op.skip("Dev dependencies not requested") - def _get_extra_packages(self, repo): # type: (Repository) -> List[str] + def _get_extra_packages(self, repo: Repository) -> List[str]: """ Returns all package names required by extras. @@ -524,8 +571,8 @@ def _get_extra_packages(self, repo): # type: (Repository) -> List[str] return list(get_extra_package_names(repo.packages, extras, self._extras)) - def _get_installer(self): # type: () -> BaseInstaller + def _get_installer(self) -> BaseInstaller: return PipInstaller(self._env, self._io, self._pool) - def _get_installed(self): # type: () -> InstalledRepository + def _get_installed(self) -> InstalledRepository: return InstalledRepository.load(self._env) diff --git a/poetry/installation/noop_installer.py b/poetry/installation/noop_installer.py index 0f0c6cda007..fe0a01e29d4 100644 --- a/poetry/installation/noop_installer.py +++ b/poetry/installation/noop_installer.py @@ -1,29 +1,36 @@ +from typing import TYPE_CHECKING +from typing import List + from .base_installer import BaseInstaller +if TYPE_CHECKING: + from poetry.core.packages.package import Package + + class NoopInstaller(BaseInstaller): - def __init__(self): + def __init__(self) -> None: self._installs = [] self._updates = [] self._removals = [] @property - def installs(self): + def installs(self) -> List["Package"]: return self._installs @property - def updates(self): + def updates(self) -> List["Package"]: return self._updates @property - def removals(self): + def removals(self) -> List["Package"]: return self._removals - def install(self, package): + def install(self, package: "Package") -> None: self._installs.append(package) - def update(self, source, target): + def update(self, source: "Package", target: "Package") -> None: self._updates.append((source, target)) - def remove(self, package): + def remove(self, package: "Package") -> None: self._removals.append(package) diff --git a/poetry/installation/operations/__init__.py b/poetry/installation/operations/__init__.py index 42573c10e8e..d7b27fe2a20 100644 --- a/poetry/installation/operations/__init__.py +++ b/poetry/installation/operations/__init__.py @@ -1,3 +1,8 @@ +from typing import Union + from .install import Install from .uninstall import Uninstall from .update import Update + + +OperationTypes = Union[Install, Uninstall, Update] diff --git a/poetry/installation/operations/install.py b/poetry/installation/operations/install.py index 48097c7c6ce..621ff4a37dc 100644 --- a/poetry/installation/operations/install.py +++ b/poetry/installation/operations/install.py @@ -1,26 +1,35 @@ +from typing import TYPE_CHECKING +from typing import Optional + from .operation import Operation +if TYPE_CHECKING: + from poetry.core.packages.package import Package + + class Install(Operation): - def __init__(self, package, reason=None, priority=0): + def __init__( + self, package: "Package", reason: Optional[str] = None, priority: int = 0 + ) -> None: super(Install, self).__init__(reason, priority=priority) self._package = package @property - def package(self): + def package(self) -> "Package": return self._package @property - def job_type(self): + def job_type(self) -> str: return "install" - def __str__(self): + def __str__(self) -> str: return "Installing {} ({})".format( self.package.pretty_name, self.format_version(self.package) ) - def __repr__(self): + def __repr__(self) -> str: return "".format( self.package.pretty_name, self.format_version(self.package) ) diff --git a/poetry/installation/operations/operation.py b/poetry/installation/operations/operation.py index 0c72cc8c044..581b62959cd 100644 --- a/poetry/installation/operations/operation.py +++ b/poetry/installation/operations/operation.py @@ -1,12 +1,13 @@ -# -*- coding: utf-8 -*- +from typing import TYPE_CHECKING +from typing import Optional -from typing import Union + +if TYPE_CHECKING: + from poetry.core.packages.package import Package class Operation(object): - def __init__( - self, reason=None, priority=0 - ): # type: (Union[str, None], int) -> None + def __init__(self, reason: Optional[str] = None, priority: int = 0) -> None: self._reason = reason self._skipped = False @@ -14,39 +15,39 @@ def __init__( self._priority = priority @property - def job_type(self): # type: () -> str + def job_type(self) -> str: raise NotImplementedError @property - def reason(self): # type: () -> str + def reason(self) -> str: return self._reason @property - def skipped(self): # type: () -> bool + def skipped(self) -> bool: return self._skipped @property - def skip_reason(self): # type: () -> Union[str, None] + def skip_reason(self) -> Optional[str]: return self._skip_reason @property - def priority(self): # type: () -> int + def priority(self) -> int: return self._priority @property - def package(self): + def package(self) -> "Package": raise NotImplementedError() - def format_version(self, package): # type: (...) -> str + def format_version(self, package: "Package") -> str: return package.full_pretty_version - def skip(self, reason): # type: (str) -> Operation + def skip(self, reason: str) -> "Operation": self._skipped = True self._skip_reason = reason return self - def unskip(self): # type: () -> Operation + def unskip(self) -> "Operation": self._skipped = False self._skip_reason = None diff --git a/poetry/installation/operations/uninstall.py b/poetry/installation/operations/uninstall.py index b7e40bc606e..d9f41055d1a 100644 --- a/poetry/installation/operations/uninstall.py +++ b/poetry/installation/operations/uninstall.py @@ -1,26 +1,38 @@ +from typing import TYPE_CHECKING +from typing import Optional + from .operation import Operation +if TYPE_CHECKING: + from poetry.core.packages.package import Package + + class Uninstall(Operation): - def __init__(self, package, reason=None, priority=float("inf")): + def __init__( + self, + package: "Package", + reason: Optional[str] = None, + priority: int = float("inf"), + ) -> None: super(Uninstall, self).__init__(reason, priority=priority) self._package = package @property - def package(self): + def package(self) -> "Package": return self._package @property - def job_type(self): + def job_type(self) -> str: return "uninstall" - def __str__(self): + def __str__(self) -> str: return "Uninstalling {} ({})".format( self.package.pretty_name, self.format_version(self._package) ) - def __repr__(self): + def __repr__(self) -> str: return "".format( self.package.pretty_name, self.format_version(self.package) ) diff --git a/poetry/installation/operations/update.py b/poetry/installation/operations/update.py index 87803fd7a23..26ca853df05 100644 --- a/poetry/installation/operations/update.py +++ b/poetry/installation/operations/update.py @@ -1,30 +1,43 @@ +from typing import TYPE_CHECKING +from typing import Optional + from .operation import Operation +if TYPE_CHECKING: + from poetry.core.packages.package import Package + + class Update(Operation): - def __init__(self, initial, target, reason=None, priority=0): + def __init__( + self, + initial: "Package", + target: "Package", + reason: Optional[str] = None, + priority: int = 0, + ) -> None: self._initial_package = initial self._target_package = target super(Update, self).__init__(reason, priority=priority) @property - def initial_package(self): + def initial_package(self) -> "Package": return self._initial_package @property - def target_package(self): + def target_package(self) -> "Package": return self._target_package @property - def package(self): + def package(self) -> "Package": return self._target_package @property - def job_type(self): + def job_type(self) -> str: return "update" - def __str__(self): + def __str__(self) -> str: return "Updating {} ({}) to {} ({})".format( self.initial_package.pretty_name, self.format_version(self.initial_package), @@ -32,7 +45,7 @@ def __str__(self): self.format_version(self.target_package), ) - def __repr__(self): + def __repr__(self) -> str: return "".format( self.initial_package.pretty_name, self.format_version(self.initial_package), diff --git a/poetry/installation/pip_installer.py b/poetry/installation/pip_installer.py index f5de6642f2d..438864a3bdb 100644 --- a/poetry/installation/pip_installer.py +++ b/poetry/installation/pip_installer.py @@ -1,32 +1,36 @@ import os import tempfile +import urllib.parse +from pathlib import Path from subprocess import CalledProcessError +from typing import TYPE_CHECKING +from typing import Any +from typing import Union -from clikit.api.io import IO +from cleo.io.io import IO from poetry.core.pyproject.toml import PyProjectTOML +from poetry.installation.base_installer import BaseInstaller from poetry.repositories.pool import Pool from poetry.utils._compat import encode from poetry.utils.env import Env from poetry.utils.helpers import safe_rmtree +from poetry.utils.pip import pip_editable_install +from poetry.utils.pip import pip_install -from .base_installer import BaseInstaller - -try: - import urllib.parse as urlparse -except ImportError: - import urlparse +if TYPE_CHECKING: + from poetry.core.packages.package import Package class PipInstaller(BaseInstaller): - def __init__(self, env, io, pool): # type: (Env, IO, Pool) -> None + def __init__(self, env: Env, io: IO, pool: Pool) -> None: self._env = env self._io = io self._pool = pool - def install(self, package, update=False): + def install(self, package: "Package", update: bool = False) -> None: if package.source_type == "directory": self.install_directory(package) @@ -44,9 +48,9 @@ def install(self, package, update=False): and package.source_url ): repository = self._pool.repository(package.source_reference) - parsed = urlparse.urlparse(package.source_url) + parsed = urllib.parse.urlparse(package.source_url) if parsed.scheme == "http": - self._io.error( + self._io.write_error( " Installing from unsecure host: {}".format( parsed.hostname ) @@ -95,7 +99,7 @@ def install(self, package, update=False): self.run(*args) - def update(self, package, target): + def update(self, package: "Package", target: "Package") -> None: 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 @@ -103,7 +107,7 @@ def update(self, package, target): self.install(target, update=True) - def remove(self, package): + def remove(self, package: "Package") -> None: try: self.run("uninstall", package.name, "-y") except CalledProcessError as e: @@ -113,8 +117,9 @@ def remove(self, package): raise # This is a workaround for https://github.com/pypa/pip/issues/4176 - nspkg_pth_file = self._env.site_packages / "{}-nspkg.pth".format(package.name) - if nspkg_pth_file.exists(): + for nspkg_pth_file in self._env.site_packages.find_distribution_nspkg_pth_files( + distribution_name=package.name + ): nspkg_pth_file.unlink() # If we have a VCS package, remove its source directory @@ -123,19 +128,19 @@ def remove(self, package): if src_dir.exists(): safe_rmtree(str(src_dir)) - def run(self, *args, **kwargs): # type: (...) -> str + def run(self, *args: Any, **kwargs: Any) -> str: return self._env.run_pip(*args, **kwargs) - def requirement(self, package, formatted=False): + def requirement(self, package: "Package", formatted: bool = False) -> str: if formatted and not package.source_type: - req = "{}=={}".format(package.name, package.version) + req = f"{package.name}=={package.version}" for f in package.files: hash_type = "sha256" h = f["hash"] if ":" in h: hash_type, h = h.split(":") - req += " --hash {}:{}".format(hash_type, h) + req += f" --hash {hash_type}:{h}" req += "\n" @@ -163,14 +168,12 @@ def requirement(self, package, formatted=False): return req if package.source_type == "url": - return "{}#egg={}".format(package.source_url, package.name) + return f"{package.source_url}#egg={package.name}" - return "{}=={}".format(package.name, package.version) + return f"{package.name}=={package.version}" - def create_temporary_requirement(self, package): - fd, name = tempfile.mkstemp( - "reqs.txt", "{}-{}".format(package.name, package.version) - ) + def create_temporary_requirement(self, package: "Package") -> str: + fd, name = tempfile.mkstemp("reqs.txt", f"{package.name}-{package.version}") try: os.write(fd, encode(self.requirement(package, formatted=True))) @@ -179,16 +182,17 @@ def create_temporary_requirement(self, package): return name - def install_directory(self, package): + def install_directory(self, package: "Package") -> Union[str, int]: + from cleo.io.null_io import NullIO + from poetry.factory import Factory - from poetry.io.null_io import NullIO + + req: Path if package.root_dir: req = (package.root_dir / package.source_url).as_posix() else: - req = os.path.realpath(package.source_url) - - args = ["install", "--no-deps", "-U"] + req = Path(package.source_url).resolve(strict=False) pyproject = PyProjectTOML(os.path.join(req, "pyproject.toml")) @@ -223,22 +227,20 @@ def install_directory(self, package): with builder.setup_py(): if package.develop: - args.append("-e") - - args.append(req) - - return self.run_pip(*args) + return pip_editable_install( + directory=req, environment=self._env + ) + return pip_install( + path=req, environment=self._env, deps=False, upgrade=True + ) if package.develop: - args.append("-e") - - args.append(req) + return pip_editable_install(directory=req, environment=self._env) + return pip_install(path=req, environment=self._env, deps=False, upgrade=True) - return self.run(*args) - - def install_git(self, package): - from poetry.core.packages import Package - from poetry.core.vcs import Git + def install_git(self, package: "Package") -> None: + from poetry.core.packages.package import Package + from poetry.core.vcs.git import Git src_dir = self._env.path / "src" / package.name if src_dir.exists(): @@ -248,7 +250,12 @@ def install_git(self, package): git = Git() git.clone(package.source_url, src_dir) - git.checkout(package.source_reference, src_dir) + + reference = package.source_resolved_reference + if not reference: + reference = package.source_reference + + git.checkout(reference, src_dir) # Now we just need to install from the source directory pkg = Package(package.name, package.version) diff --git a/poetry/io/null_io.py b/poetry/io/null_io.py deleted file mode 100644 index d81cd595527..00000000000 --- a/poetry/io/null_io.py +++ /dev/null @@ -1,11 +0,0 @@ -from cleo.io.io_mixin import IOMixin -from clikit.io import NullIO as BaseNullIO - - -class NullIO(IOMixin, BaseNullIO): - """ - A wrapper around CliKit's NullIO. - """ - - def __init__(self, *args, **kwargs): - super(NullIO, self).__init__(*args, **kwargs) diff --git a/poetry/json/__init__.py b/poetry/json/__init__.py index d50eb7a7500..ad4eafed7a9 100644 --- a/poetry/json/__init__.py +++ b/poetry/json/__init__.py @@ -1,7 +1,6 @@ import json import os -from io import open from typing import List import jsonschema @@ -15,11 +14,11 @@ class ValidationError(ValueError): pass -def validate_object(obj, schema_name): # type: (dict, str) -> List[str] - schema = os.path.join(SCHEMA_DIR, "{}.json".format(schema_name)) +def validate_object(obj: dict, schema_name: str) -> List[str]: + schema = os.path.join(SCHEMA_DIR, f"{schema_name}.json") if not os.path.exists(schema): - raise ValueError("Schema {} does not exist.".format(schema_name)) + raise ValueError(f"Schema {schema_name} does not exist.") with open(schema, encoding="utf-8") as f: schema = json.loads(f.read()) diff --git a/poetry/layouts/__init__.py b/poetry/layouts/__init__.py index 9969ce5e3a7..f7ddf8c4def 100644 --- a/poetry/layouts/__init__.py +++ b/poetry/layouts/__init__.py @@ -2,13 +2,12 @@ from .layout import Layout from .src import SrcLayout -from .standard import StandardLayout -_LAYOUTS = {"src": SrcLayout, "standard": StandardLayout} +_LAYOUTS = {"src": SrcLayout, "standard": Layout} -def layout(name): # type: (str) -> Type[Layout] +def layout(name: str) -> Type[Layout]: if name not in _LAYOUTS: raise ValueError("Invalid layout") diff --git a/poetry/layouts/layout.py b/poetry/layouts/layout.py index 8a74060f06e..f4610564582 100644 --- a/poetry/layouts/layout.py +++ b/poetry/layouts/layout.py @@ -1,22 +1,21 @@ +from pathlib import Path from typing import TYPE_CHECKING +from typing import Dict from typing import Optional from tomlkit import dumps +from tomlkit import inline_table from tomlkit import loads from tomlkit import table +from poetry.utils.helpers import canonicalize_name from poetry.utils.helpers import module_name if TYPE_CHECKING: - from poetry.core.pyproject.toml import PyProjectTOML - -TESTS_DEFAULT = u"""from {package_name} import __version__ - + from tomlkit.items import InlineTable -def test_version(): - assert __version__ == '{version}' -""" + from poetry.core.pyproject.toml import PyProjectTOML POETRY_DEFAULT = """\ @@ -25,47 +24,50 @@ def test_version(): version = "" description = "" authors = [] - -[tool.poetry.dependencies] - -[tool.poetry.dev-dependencies] -""" - -POETRY_WITH_LICENSE = """\ -[tool.poetry] -name = "" -version = "" -description = "" -authors = [] license = "" +readme = "" +packages = [] [tool.poetry.dependencies] [tool.poetry.dev-dependencies] """ -BUILD_SYSTEM_MIN_VERSION = "1.0.0" -BUILD_SYSTEM_MAX_VERSION = None +BUILD_SYSTEM_MIN_VERSION: Optional[str] = None +BUILD_SYSTEM_MAX_VERSION: Optional[str] = None + +class Layout: + ACCEPTED_README_FORMATS = {"md", "rst"} -class Layout(object): def __init__( self, - project, - version="0.1.0", - description="", - readme_format="md", - author=None, - license=None, - python="*", - dependencies=None, - dev_dependencies=None, + project: str, + version: str = "0.1.0", + description: str = "", + readme_format: str = "md", + author: Optional[str] = None, + license: Optional[str] = None, # noqa + python: str = "*", + dependencies: Optional[Dict[str, str]] = None, + dev_dependencies: Optional[Dict[str, str]] = None, ): - self._project = project - self._package_name = module_name(project) + self._project = canonicalize_name(project).replace(".", "-") + self._package_path_relative = Path( + *(module_name(part) for part in canonicalize_name(project).split(".")) + ) + self._package_name = ".".join(self._package_path_relative.parts) self._version = version self._description = description - self._readme_format = readme_format + + self._readme_format = readme_format.lower() + if self._readme_format not in self.ACCEPTED_README_FORMATS: + raise ValueError( + "Invalid readme format '{}', use one of {}.".format( + readme_format, ", ".join(self.ACCEPTED_README_FORMATS) + ) + ) + self._license = license self._python = python self._dependencies = dependencies or {} @@ -76,7 +78,31 @@ def __init__( self._author = author - def create(self, path, with_tests=True): + @property + def basedir(self) -> Path: + return Path() + + @property + def package_path(self) -> Path: + return self.basedir / self._package_path_relative + + def get_package_include(self) -> Optional["InlineTable"]: + package = inline_table() + + include = self._package_path_relative.parts[0] + package.append("include", include) + + if self.basedir != Path(): + package.append("from", self.basedir.as_posix()) + else: + if include == self._project: + # package include and package name are the same, + # packages table is redundant here. + return None + + return package + + def create(self, path: "Path", with_tests: bool = True) -> None: path.mkdir(parents=True, exist_ok=True) self._create_default(path) @@ -88,20 +114,29 @@ def create(self, path, with_tests=True): self._write_poetry(path) def generate_poetry_content( - self, original=None - ): # type: (Optional["PyProjectTOML"]) -> str + self, original: Optional["PyProjectTOML"] = None + ) -> str: template = POETRY_DEFAULT - if self._license: - template = POETRY_WITH_LICENSE content = loads(template) + poetry_content = content["tool"]["poetry"] poetry_content["name"] = self._project poetry_content["version"] = self._version poetry_content["description"] = self._description poetry_content["authors"].append(self._author) + if self._license: poetry_content["license"] = self._license + else: + poetry_content.remove("license") + + poetry_content["readme"] = "README.{}".format(self._readme_format) + packages = self.get_package_include() + if packages: + poetry_content["packages"].append(packages) + else: + poetry_content.remove("packages") poetry_content["dependencies"]["python"] = self._python @@ -113,9 +148,14 @@ def generate_poetry_content( # Add build system build_system = table() - build_system_version = ">=" + BUILD_SYSTEM_MIN_VERSION + build_system_version = "" + + if BUILD_SYSTEM_MIN_VERSION is not None: + build_system_version = ">=" + BUILD_SYSTEM_MIN_VERSION if BUILD_SYSTEM_MAX_VERSION is not None: - build_system_version += ",<" + BUILD_SYSTEM_MAX_VERSION + if build_system_version: + build_system_version += "," + build_system_version += "<" + BUILD_SYSTEM_MAX_VERSION build_system.add("requires", ["poetry-core" + build_system_version]) build_system.add("build-backend", "poetry.core.masonry.api") @@ -129,33 +169,27 @@ def generate_poetry_content( return content - def _create_default(self, path, src=True): - raise NotImplementedError() + def _create_default(self, path: "Path", src: bool = True) -> None: + package_path = path / self.package_path + package_path.mkdir(parents=True) - def _create_readme(self, path): - if self._readme_format == "rst": - readme_file = path / "README.rst" - else: - readme_file = path / "README.md" + package_init = package_path / "__init__.py" + package_init.touch() + def _create_readme(self, path: "Path") -> "Path": + readme_file = path.joinpath("README.{}".format(self._readme_format)) readme_file.touch() + return readme_file - def _create_tests(self, path): + @staticmethod + def _create_tests(path: "Path") -> None: tests = path / "tests" - tests_init = tests / "__init__.py" - tests_default = tests / "test_{}.py".format(self._package_name) - tests.mkdir() - tests_init.touch(exist_ok=False) - with tests_default.open("w", encoding="utf-8") as f: - f.write( - TESTS_DEFAULT.format( - package_name=self._package_name, version=self._version - ) - ) + tests_init = tests / "__init__.py" + tests_init.touch(exist_ok=False) - def _write_poetry(self, path): + def _write_poetry(self, path: "Path") -> None: content = self.generate_poetry_content() poetry = path / "pyproject.toml" diff --git a/poetry/layouts/src.py b/poetry/layouts/src.py index 06db7a71f92..6d10e63296b 100644 --- a/poetry/layouts/src.py +++ b/poetry/layouts/src.py @@ -1,19 +1,9 @@ -# -*- coding: utf-8 -*- +from pathlib import Path from .layout import Layout -DEFAULT = u"""__version__ = '{version}' -""" - - class SrcLayout(Layout): - def _create_default(self, path): - package_path = path / "src" / self._package_name - - package_init = package_path / "__init__.py" - - package_path.mkdir(parents=True) - - with package_init.open("w", encoding="utf-8") as f: - f.write(DEFAULT.format(version=self._version)) + @property + def basedir(self) -> "Path": + return Path("src") diff --git a/poetry/layouts/standard.py b/poetry/layouts/standard.py index eca4c435c40..e69de29bb2d 100644 --- a/poetry/layouts/standard.py +++ b/poetry/layouts/standard.py @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- - -from .layout import Layout - - -DEFAULT = u"""__version__ = '{version}' -""" - - -class StandardLayout(Layout): - def _create_default(self, path): - package_path = path / self._package_name - - package_init = package_path / "__init__.py" - - package_path.mkdir() - - with package_init.open("w", encoding="utf-8") as f: - f.write(DEFAULT.format(version=self._version)) diff --git a/poetry/locations.py b/poetry/locations.py index 003950d500d..b6b6f84681d 100644 --- a/poetry/locations.py +++ b/poetry/locations.py @@ -1,9 +1,21 @@ -from .utils._compat import Path +import os + +from pathlib import Path + from .utils.appdirs import user_cache_dir from .utils.appdirs import user_config_dir +from .utils.appdirs import user_data_dir CACHE_DIR = user_cache_dir("pypoetry") +DATA_DIR = user_data_dir("pypoetry") CONFIG_DIR = user_config_dir("pypoetry") REPOSITORY_CACHE_DIR = Path(CACHE_DIR) / "cache" / "repositories" + + +def data_dir() -> Path: + if os.getenv("POETRY_HOME"): + return Path(os.getenv("POETRY_HOME")).expanduser() + + return Path(user_data_dir("pypoetry", roaming=True)) diff --git a/poetry/masonry/builders/editable.py b/poetry/masonry/builders/editable.py index 11bda4efcbd..2483a5ec704 100644 --- a/poetry/masonry/builders/editable.py +++ b/poetry/masonry/builders/editable.py @@ -5,16 +5,26 @@ import shutil from base64 import urlsafe_b64encode +from pathlib import Path +from typing import TYPE_CHECKING +from typing import List from poetry.core.masonry.builders.builder import Builder from poetry.core.masonry.builders.sdist import SdistBuilder from poetry.core.masonry.utils.package_include import PackageInclude from poetry.core.semver.version import Version from poetry.utils._compat import WINDOWS -from poetry.utils._compat import Path from poetry.utils._compat import decode +from poetry.utils.helpers import is_dir_writable +from poetry.utils.pip import pip_editable_install +if TYPE_CHECKING: + from cleo.io.io import IO # noqa + + from poetry.core.poetry import Poetry + from poetry.utils.env import Env + SCRIPT_TEMPLATE = """\ #!{python} from {module} import {callable_holder} @@ -29,13 +39,13 @@ class EditableBuilder(Builder): - def __init__(self, poetry, env, io): + def __init__(self, poetry: "Poetry", env: "Env", io: "IO") -> None: super(EditableBuilder, self).__init__(poetry) self._env = env self._io = io - def build(self): + def build(self) -> None: self._debug( " - Building package {} in editable mode".format( self._package.name @@ -47,21 +57,29 @@ def build(self): self._debug( " - Falling back on using a setup.py" ) - return self._setup_build() self._run_build_script(self._package.build_script) + for removed in self._env.site_packages.remove_distribution_files( + distribution_name=self._package.name + ): + self._debug( + " - Removed {} directory from {}".format( + removed.name, removed.parent + ) + ) + added_files = [] added_files += self._add_pth() added_files += self._add_scripts() self._add_dist_info(added_files) - def _run_build_script(self, build_script): + def _run_build_script(self, build_script: Path) -> None: self._debug(" - Executing build script: {}".format(build_script)) self._env.run("python", str(self._path.joinpath(build_script)), call=True) - def _setup_build(self): + def _setup_build(self) -> None: builder = SdistBuilder(self._poetry) setup = self._path / "setup.py" has_setup = setup.exists() @@ -75,15 +93,15 @@ def _setup_build(self): f.write(decode(builder.build_setup())) try: - if self._env.pip_version < Version(19, 0): - self._env.run_pip("install", "-e", str(self._path), "--no-deps") + if self._env.pip_version < Version.from_parts(19, 0): + pip_editable_install(self._path, self._env) else: # Temporarily rename pyproject.toml shutil.move( str(self._poetry.file), str(self._poetry.file.with_suffix(".tmp")) ) try: - self._env.run_pip("install", "-e", str(self._path), "--no-deps") + pip_editable_install(self._path, self._env) finally: shutil.move( str(self._poetry.file.with_suffix(".tmp")), @@ -93,8 +111,7 @@ def _setup_build(self): if not has_setup: os.remove(str(setup)) - def _add_pth(self): - pth_file = Path(self._module.name).with_suffix(".pth") + def _add_pth(self) -> List[Path]: paths = set() for include in self._module.includes: if isinstance(include, PackageInclude) and ( @@ -106,40 +123,58 @@ def _add_pth(self): for path in paths: content += decode(path + os.linesep) - for site_package in [self._env.site_packages, self._env.usersite]: - if not site_package: - continue + pth_file = Path(self._module.name).with_suffix(".pth") - try: - site_package.mkdir(parents=True, exist_ok=True) - path = site_package.joinpath(pth_file) - self._debug( - " - Adding {} to {} for {}".format( - path.name, site_package, self._poetry.file.parent - ) + # remove any pre-existing pth files for this package + for file in self._env.site_packages.find(path=pth_file, writable_only=True): + self._debug( + " - Removing existing {} from {} for {}".format( + file.name, file.parent, self._poetry.file.parent ) - path.write_text(content, encoding="utf-8") - return [path] - except PermissionError: - self._debug("- {} is not writable trying next available site") - - self._io.error_line( - " - Failed to create {} for {}".format( - pth_file.name, self._poetry.file.parent ) - ) - return [] + # We can't use unlink(missing_ok=True) because it's not always available + if file.exists(): + file.unlink() + + try: + pth_file = self._env.site_packages.write_text( + pth_file, content, encoding="utf-8" + ) + self._debug( + " - Adding {} to {} for {}".format( + pth_file.name, pth_file.parent, self._poetry.file.parent + ) + ) + return [pth_file] + except OSError: + # TODO: Replace with PermissionError + self._io.write_error_line( + " - Failed to create {} for {}".format( + pth_file.name, self._poetry.file.parent + ) + ) + return [] - def _add_scripts(self): + def _add_scripts(self) -> List[Path]: added = [] entry_points = self.convert_entry_points() - scripts_path = Path(self._env.paths["scripts"]) + + for scripts_path in self._env.script_dirs: + if is_dir_writable(path=scripts_path, create=True): + break + else: + self._io.write_error_line( + " - Failed to find a suitable script installation directory for {}".format( + self._poetry.file.parent + ) + ) + return [] scripts = entry_points.get("console_scripts", []) for script in scripts: name, script = script.split(" = ") module, callable_ = script.split(":") - callable_holder = callable_.rsplit(".", 1)[0] + callable_holder = callable_.split(".", 1)[0] script_file = scripts_path.joinpath(name) self._debug( @@ -151,7 +186,7 @@ def _add_scripts(self): f.write( decode( SCRIPT_TEMPLATE.format( - python=self._env._bin("python"), + python=self._env.python, module=module, callable_holder=callable_holder, callable_=callable_, @@ -165,9 +200,7 @@ def _add_scripts(self): if WINDOWS: cmd_script = script_file.with_suffix(".cmd") - cmd = WINDOWS_CMD_TEMPLATE.format( - python=self._env._bin("python"), script=name - ) + cmd = WINDOWS_CMD_TEMPLATE.format(python=self._env.python, script=name) self._debug( " - Adding the {} script wrapper to {}".format( cmd_script.name, scripts_path @@ -181,25 +214,20 @@ def _add_scripts(self): return added - def _add_dist_info(self, added_files): + def _add_dist_info(self, added_files: List[Path]) -> None: from poetry.core.masonry.builders.wheel import WheelBuilder added_files = added_files[:] builder = WheelBuilder(self._poetry) - dist_info = self._env.site_packages.joinpath(builder.dist_info) + dist_info = self._env.site_packages.mkdir(Path(builder.dist_info)) self._debug( " - Adding the {} directory to {}".format( - dist_info.name, self._env.site_packages + dist_info.name, dist_info.parent ) ) - if dist_info.exists(): - shutil.rmtree(str(dist_info)) - - dist_info.mkdir() - with dist_info.joinpath("METADATA").open("w", encoding="utf-8") as f: builder._write_metadata_file(f) @@ -227,7 +255,7 @@ def _add_dist_info(self, added_files): # RECORD itself is recorded with no hash or size f.write("{},,\n".format(dist_info.joinpath("RECORD"))) - def _get_file_hash(self, filepath): + def _get_file_hash(self, filepath: Path) -> str: hashsum = hashlib.sha256() with filepath.open("rb") as src: while True: @@ -240,6 +268,6 @@ def _get_file_hash(self, filepath): return urlsafe_b64encode(hashsum.digest()).decode("ascii").rstrip("=") - def _debug(self, msg): + def _debug(self, msg: str) -> None: if self._io.is_debug(): self._io.write_line(msg) diff --git a/poetry/mixology/__init__.py b/poetry/mixology/__init__.py index 50fbffb27cb..8cf422203a9 100644 --- a/poetry/mixology/__init__.py +++ b/poetry/mixology/__init__.py @@ -1,7 +1,24 @@ +from typing import TYPE_CHECKING +from typing import Dict +from typing import List + from .version_solver import VersionSolver -def resolve_version(root, provider, locked=None, use_latest=None): +if TYPE_CHECKING: + from poetry.core.packages.project_package import ProjectPackage + from poetry.packages import DependencyPackage + from poetry.puzzle.provider import Provider + + from .result import SolverResult + + +def resolve_version( + root: "ProjectPackage", + provider: "Provider", + locked: Dict[str, "DependencyPackage"] = None, + use_latest: List[str] = None, +) -> "SolverResult": solver = VersionSolver(root, provider, locked=locked, use_latest=use_latest) return solver.solve() diff --git a/poetry/mixology/assignment.py b/poetry/mixology/assignment.py index e288c5da520..e0079d46b1a 100644 --- a/poetry/mixology/assignment.py +++ b/poetry/mixology/assignment.py @@ -1,44 +1,64 @@ +from typing import TYPE_CHECKING from typing import Any +from typing import Optional -from .incompatibility import Incompatibility from .term import Term +if TYPE_CHECKING: + from poetry.core.packages.dependency import Dependency + from poetry.core.packages.package import Package + + from .incompatibility import Incompatibility + + class Assignment(Term): """ A term in a PartialSolution that tracks some additional metadata. """ - def __init__(self, dependency, is_positive, decision_level, index, cause=None): - super(Assignment, self).__init__(dependency, is_positive) + def __init__( + self, + dependency: "Dependency", + is_positive: bool, + decision_level: int, + index: int, + cause: Optional["Incompatibility"] = None, + ) -> None: + super().__init__(dependency, is_positive) self._decision_level = decision_level self._index = index self._cause = cause @property - def decision_level(self): # type: () -> int + def decision_level(self) -> int: return self._decision_level @property - def index(self): # type: () -> int + def index(self) -> int: return self._index @property - def cause(self): # type: () -> Incompatibility + def cause(self) -> "Incompatibility": return self._cause @classmethod def decision( - cls, package, decision_level, index - ): # type: (Any, int, int) -> Assignment + cls, package: "Package", decision_level: int, index: int + ) -> "Assignment": return cls(package.to_dependency(), True, decision_level, index) @classmethod def derivation( - cls, dependency, is_positive, cause, decision_level, index - ): # type: (Any, bool, Incompatibility, int, int) -> Assignment + cls, + dependency: Any, + is_positive: bool, + cause: "Incompatibility", + decision_level: int, + index: int, + ) -> "Assignment": return cls(dependency, is_positive, decision_level, index, cause) - def is_decision(self): # type: () -> bool + def is_decision(self) -> bool: return self._cause is None diff --git a/poetry/mixology/failure.py b/poetry/mixology/failure.py index afa332085b3..bf84348ced1 100644 --- a/poetry/mixology/failure.py +++ b/poetry/mixology/failure.py @@ -1,8 +1,9 @@ from typing import Dict from typing import List +from typing import Optional from typing import Tuple -from poetry.core.semver import parse_constraint +from poetry.core.semver.helpers import parse_constraint from .incompatibility import Incompatibility from .incompatibility_cause import ConflictCause @@ -10,27 +11,27 @@ class SolveFailure(Exception): - def __init__(self, incompatibility): # type: (Incompatibility) -> None + def __init__(self, incompatibility: Incompatibility) -> None: self._incompatibility = incompatibility @property - def message(self): + def message(self) -> str: return str(self) - def __str__(self): + def __str__(self) -> str: return _Writer(self._incompatibility).write() class _Writer: - def __init__(self, root): # type: (Incompatibility) -> None + def __init__(self, root: Incompatibility) -> None: self._root = root - self._derivations = {} # type: Dict[Incompatibility, int] - self._lines = [] # type: List[Tuple[str, int]] - self._line_numbers = {} # type: Dict[Incompatibility, int] + self._derivations: Dict[Incompatibility, int] = {} + self._lines: List[Tuple[str, Optional[int]]] = [] + self._line_numbers: Dict[Incompatibility, int] = {} self._count_derivations(self._root) - def write(self): + def write(self) -> str: buffer = [] required_python_version_notification = False @@ -64,9 +65,7 @@ def write(self): if isinstance(self._root.cause, ConflictCause): self._visit(self._root, {}) else: - self._write( - self._root, "Because {}, version solving failed.".format(self._root) - ) + self._write(self._root, f"Because {self._root}, version solving failed.") padding = ( 0 @@ -88,7 +87,7 @@ def write(self): number = line[-1] if number is not None: - message = "({})".format(number).ljust(padding) + message + message = f"({number})".ljust(padding) + message else: message = " " * padding + message @@ -97,8 +96,8 @@ def write(self): return "\n".join(buffer) def _write( - self, incompatibility, message, numbered=False - ): # type: (Incompatibility, str, bool) -> None + self, incompatibility: Incompatibility, message: str, numbered: bool = False + ) -> None: if numbered: number = len(self._line_numbers) + 1 self._line_numbers[incompatibility] = number @@ -107,13 +106,16 @@ def _write( self._lines.append((message, None)) def _visit( - self, incompatibility, details_for_incompatibility, conclusion=False - ): # type: (Incompatibility, Dict, bool) -> None + self, + incompatibility: Incompatibility, + details_for_incompatibility: Dict, + conclusion: bool = False, + ) -> None: numbered = conclusion or self._derivations[incompatibility] > 1 conjunction = "So," if conclusion or incompatibility == self._root else "And" incompatibility_string = str(incompatibility) - cause = incompatibility.cause # type: ConflictCause + cause = incompatibility.cause details_for_cause = {} if isinstance(cause.conflict.cause, ConflictCause) and isinstance( cause.other.cause, ConflictCause @@ -161,7 +163,7 @@ def _visit( self._visit(second, details_for_cause) self._write( incompatibility, - "Thus, {}.".format(incompatibility_string), + f"Thus, {incompatibility_string}.", numbered=numbered, ) else: @@ -207,7 +209,7 @@ def _visit( numbered=numbered, ) elif self._is_collapsible(derived): - derived_cause = derived.cause # type: ConflictCause + derived_cause: ConflictCause = derived.cause if isinstance(derived_cause.conflict.cause, ConflictCause): collapsed_derived = derived_cause.conflict else: @@ -251,11 +253,11 @@ def _visit( numbered=numbered, ) - def _is_collapsible(self, incompatibility): # type: (Incompatibility) -> bool + def _is_collapsible(self, incompatibility: Incompatibility) -> bool: if self._derivations[incompatibility] > 1: return False - cause = incompatibility.cause # type: ConflictCause + cause: ConflictCause = incompatibility.cause if isinstance(cause.conflict.cause, ConflictCause) and isinstance( cause.other.cause, ConflictCause ): @@ -274,12 +276,12 @@ def _is_collapsible(self, incompatibility): # type: (Incompatibility) -> bool return complex not in self._line_numbers - def _is_single_line(self, cause): # type: (ConflictCause) -> bool + def _is_single_line(self, cause: ConflictCause) -> bool: return not isinstance(cause.conflict.cause, ConflictCause) and not isinstance( cause.other.cause, ConflictCause ) - def _count_derivations(self, incompatibility): # type: (Incompatibility) -> None + def _count_derivations(self, incompatibility: Incompatibility) -> None: if incompatibility in self._derivations: self._derivations[incompatibility] += 1 else: diff --git a/poetry/mixology/incompatibility.py b/poetry/mixology/incompatibility.py index 3cbc867874a..e195e08b7ab 100644 --- a/poetry/mixology/incompatibility.py +++ b/poetry/mixology/incompatibility.py @@ -1,6 +1,8 @@ from typing import Dict -from typing import Generator +from typing import Iterator from typing import List +from typing import Optional +from typing import Union from .incompatibility_cause import ConflictCause from .incompatibility_cause import DependencyCause @@ -14,16 +16,14 @@ class Incompatibility: - def __init__( - self, terms, cause - ): # type: (List[Term], IncompatibilityCause) -> None + def __init__(self, terms: List[Term], cause: IncompatibilityCause) -> None: # Remove the root package from generated incompatibilities, since it will # always be satisfied. This makes error reporting clearer, and may also # make solving more efficient. if ( len(terms) != 1 and isinstance(cause, ConflictCause) - and any([term.is_positive() and term.dependency.is_root for term in terms]) + and any(term.is_positive() and term.dependency.is_root for term in terms) ): terms = [ term @@ -41,7 +41,7 @@ def __init__( pass else: # Coalesce multiple terms about the same package if possible. - by_name = {} # type: Dict[str, Dict[str, Term]] + by_name: Dict[str, Dict[str, Term]] = {} for term in terms: if term.dependency.complete_name not in by_name: by_name[term.dependency.complete_name] = {} @@ -78,35 +78,45 @@ def __init__( self._cause = cause @property - def terms(self): # type: () -> List[Term] + def terms(self) -> List[Term]: return self._terms @property - def cause(self): # type: () -> IncompatibilityCause + def cause( + self, + ) -> Union[ + RootCause, + NoVersionsCause, + DependencyCause, + ConflictCause, + PythonCause, + PlatformCause, + PackageNotFoundCause, + ]: return self._cause @property - def external_incompatibilities(self): # type: () -> Generator[Incompatibility] + def external_incompatibilities( + self, + ) -> Iterator[Union[ConflictCause, "Incompatibility"]]: """ Returns all external incompatibilities in this incompatibility's derivation graph. """ if isinstance(self._cause, ConflictCause): - cause = self._cause # type: ConflictCause - for incompatibility in cause.conflict.external_incompatibilities: - yield incompatibility + cause: ConflictCause = self._cause + yield from cause.conflict.external_incompatibilities - for incompatibility in cause.other.external_incompatibilities: - yield incompatibility + yield from cause.other.external_incompatibilities else: yield self - def is_failure(self): # type: () -> bool + def is_failure(self) -> bool: return len(self._terms) == 0 or ( len(self._terms) == 1 and self._terms[0].dependency.is_root ) - def __str__(self): + def __str__(self) -> str: if isinstance(self._cause, DependencyCause): assert len(self._terms) == 2 @@ -122,18 +132,18 @@ def __str__(self): assert len(self._terms) == 1 assert self._terms[0].is_positive() - cause = self._cause # type: PythonCause + cause: PythonCause = self._cause text = "{} requires ".format(self._terse(self._terms[0], allow_every=True)) - text += "Python {}".format(cause.python_version) + text += f"Python {cause.python_version}" return text elif isinstance(self._cause, PlatformCause): assert len(self._terms) == 1 assert self._terms[0].is_positive() - cause = self._cause # type: PlatformCause + cause: PlatformCause = self._cause text = "{} requires ".format(self._terse(self._terms[0], allow_every=True)) - text += "platform {}".format(cause.platform) + text += f"platform {cause.platform}" return text elif isinstance(self._cause, NoVersionsCause): @@ -189,7 +199,7 @@ def __str__(self): else self._terse(term2) ) - return "{} is incompatible with {}".format(package1, package2) + return f"{package1} is incompatible with {package2}" else: return "either {} or {}".format( self._terse(term1), self._terse(term2) @@ -221,8 +231,12 @@ def __str__(self): return "one of {} must be true".format(" or ".join(negative)) def and_to_string( - self, other, details, this_line, other_line - ): # type: (Incompatibility, dict, int, int) -> str + self, + other: "Incompatibility", + details: dict, + this_line: Optional[int], + other_line: Optional[int], + ) -> str: requires_both = self._try_requires_both(other, details, this_line, other_line) if requires_both is not None: return requires_both @@ -241,18 +255,22 @@ def and_to_string( buffer = [str(self)] if this_line is not None: - buffer.append(" " + this_line) + buffer.append(" " + str(this_line)) buffer.append(" and {}".format(str(other))) if other_line is not None: - buffer.append(" " + other_line) + buffer.append(" " + str(other_line)) return "\n".join(buffer) def _try_requires_both( - self, other, details, this_line, other_line - ): # type: (Incompatibility, dict, int, int) -> str + self, + other: "Incompatibility", + details: dict, + this_line: Optional[int], + other_line: Optional[int], + ) -> Optional[str]: if len(self._terms) == 1 or len(other.terms) == 1: return @@ -285,20 +303,20 @@ def _try_requires_both( else: buffer.append("requires") - buffer.append(" both {}".format(this_negatives)) + buffer.append(f" both {this_negatives}") if this_line is not None: - buffer.append(" ({})".format(this_line)) + buffer.append(f" ({this_line})") - buffer.append(" and {}".format(other_negatives)) + buffer.append(f" and {other_negatives}") if other_line is not None: - buffer.append(" ({})".format(other_line)) + buffer.append(f" ({other_line})") return "".join(buffer) def _try_requires_through( - self, other, details, this_line, other_line - ): # type: (Incompatibility, dict, int, int) -> str + self, other: "Incompatibility", details: dict, this_line: int, other_line: int + ) -> Optional[str]: if len(self._terms) == 1 or len(other.terms) == 1: return @@ -341,7 +359,7 @@ def _try_requires_through( buffer = [] if len(prior_positives) > 1: prior_string = " or ".join([self._terse(term) for term in prior_positives]) - buffer.append("if {} then ".format(prior_string)) + buffer.append(f"if {prior_string} then ") else: if isinstance(prior.cause, DependencyCause): verb = "depends on" @@ -354,7 +372,7 @@ def _try_requires_through( buffer.append(self._terse(prior_negative)) if prior_line is not None: - buffer.append(" ({})".format(prior_line)) + buffer.append(f" ({prior_line})") buffer.append(" which ") @@ -370,13 +388,13 @@ def _try_requires_through( ) if latter_line is not None: - buffer.append(" ({})".format(latter_line)) + buffer.append(f" ({latter_line})") return "".join(buffer) def _try_requires_forbidden( - self, other, details, this_line, other_line - ): # type: (Incompatibility, dict, int, int) -> str + self, other: "Incompatibility", details: dict, this_line: int, other_line: int + ) -> Optional[str]: if len(self._terms) != 1 and len(other.terms) != 1: return None @@ -403,7 +421,7 @@ def _try_requires_forbidden( buffer = [] if len(positives) > 1: prior_string = " or ".join([self._terse(term) for term in positives]) - buffer.append("if {} then ".format(prior_string)) + buffer.append(f"if {prior_string} then ") else: buffer.append(self._terse(positives[0], allow_every=True)) if isinstance(prior.cause, DependencyCause): @@ -413,11 +431,11 @@ def _try_requires_forbidden( buffer.append(self._terse(latter.terms[0]) + " ") if prior_line is not None: - buffer.append("({}) ".format(prior_line)) + buffer.append(f"({prior_line}) ") if isinstance(latter.cause, PythonCause): - cause = latter.cause # type: PythonCause - buffer.append("which requires Python {}".format(cause.python_version)) + cause: PythonCause = latter.cause + buffer.append(f"which requires Python {cause.python_version}") elif isinstance(latter.cause, NoVersionsCause): buffer.append("which doesn't match any versions") elif isinstance(latter.cause, PackageNotFoundCause): @@ -426,17 +444,22 @@ def _try_requires_forbidden( buffer.append("which is forbidden") if latter_line is not None: - buffer.append(" ({})".format(latter_line)) + buffer.append(f" ({latter_line})") return "".join(buffer) - def _terse(self, term, allow_every=False): + def _terse(self, term: Term, allow_every: bool = False) -> str: if allow_every and term.constraint.is_any(): - return "every version of {}".format(term.dependency.complete_name) + return f"every version of {term.dependency.complete_name}" - return str(term.dependency) + if term.dependency.is_root: + return term.dependency.pretty_name - def _single_term_where(self, callable): # type: (callable) -> Term + return "{} ({})".format( + term.dependency.pretty_name, term.dependency.pretty_constraint + ) + + def _single_term_where(self, callable: callable) -> Optional[Term]: found = None for term in self._terms: if not callable(term): @@ -449,5 +472,5 @@ def _single_term_where(self, callable): # type: (callable) -> Term return found - def __repr__(self): + def __repr__(self) -> str: return "".format(str(self)) diff --git a/poetry/mixology/incompatibility_cause.py b/poetry/mixology/incompatibility_cause.py index 8156b4fa42b..3267be56d7a 100644 --- a/poetry/mixology/incompatibility_cause.py +++ b/poetry/mixology/incompatibility_cause.py @@ -1,3 +1,10 @@ +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from poetry.mixology.incompatibility import Incompatibility + + class IncompatibilityCause(Exception): """ The reason and Incompatibility's terms are incompatible. @@ -25,19 +32,19 @@ class ConflictCause(IncompatibilityCause): during conflict resolution. """ - def __init__(self, conflict, other): + def __init__(self, conflict: "Incompatibility", other: "Incompatibility") -> None: self._conflict = conflict self._other = other @property - def conflict(self): + def conflict(self) -> "Incompatibility": return self._conflict @property - def other(self): + def other(self) -> "Incompatibility": return self._other - def __str__(self): + def __str__(self) -> str: return str(self._conflict) @@ -48,16 +55,16 @@ class PythonCause(IncompatibilityCause): with the current python version. """ - def __init__(self, python_version, root_python_version): + def __init__(self, python_version: str, root_python_version: str) -> None: self._python_version = python_version self._root_python_version = root_python_version @property - def python_version(self): + def python_version(self) -> str: return self._python_version @property - def root_python_version(self): + def root_python_version(self) -> str: return self._root_python_version @@ -67,11 +74,11 @@ class PlatformCause(IncompatibilityCause): (OS most likely) being incompatible with the current platform. """ - def __init__(self, platform): + def __init__(self, platform: str) -> None: self._platform = platform @property - def platform(self): + def platform(self) -> str: return self._platform @@ -81,9 +88,9 @@ class PackageNotFoundCause(IncompatibilityCause): source. """ - def __init__(self, error): + def __init__(self, error: Exception) -> None: self._error = error @property - def error(self): + def error(self) -> Exception: return self._error diff --git a/poetry/mixology/partial_solution.py b/poetry/mixology/partial_solution.py old mode 100755 new mode 100644 index df17f7184b2..96a761d4438 --- a/poetry/mixology/partial_solution.py +++ b/poetry/mixology/partial_solution.py @@ -1,16 +1,18 @@ -from collections import OrderedDict +from typing import TYPE_CHECKING from typing import Dict from typing import List -from poetry.core.packages import Dependency -from poetry.core.packages import Package - from .assignment import Assignment from .incompatibility import Incompatibility from .set_relation import SetRelation from .term import Term +if TYPE_CHECKING: + from poetry.core.packages.dependency import Dependency + from poetry.core.packages.package import Package + + class PartialSolution: """ # A list of Assignments that represent the solver's current best guess about @@ -20,19 +22,19 @@ class PartialSolution: # See https://github.com/dart-lang/mixology/tree/master/doc/solver.md#partial-solution. """ - def __init__(self): + def __init__(self) -> None: # The assignments that have been made so far, in the order they were # assigned. - self._assignments = [] # type: List[Assignment] + self._assignments: List[Assignment] = [] # The decisions made for each package. - self._decisions = OrderedDict() # type: Dict[str, Package] + self._decisions: Dict[str, "Package"] = dict() # The intersection of all positive Assignments for each package, minus any # negative Assignments that refer to that package. # # This is derived from self._assignments. - self._positive = OrderedDict() # type: Dict[str, Term] + self._positive: Dict[str, Term] = dict() # The union of all negative Assignments for each package. # @@ -40,7 +42,7 @@ def __init__(self): # map. # # This is derived from self._assignments. - self._negative = OrderedDict() # type: Dict[str, Dict[str, Term]] + self._negative: Dict[str, Dict[str, Term]] = dict() # The number of distinct solutions that have been attempted so far. self._attempted_solutions = 1 @@ -49,26 +51,26 @@ def __init__(self): self._backtracking = False @property - def decisions(self): # type: () -> List[Package] + def decisions(self) -> List["Package"]: return list(self._decisions.values()) @property - def decision_level(self): # type: () -> int + def decision_level(self) -> int: return len(self._decisions) @property - def attempted_solutions(self): # type: () -> int + def attempted_solutions(self) -> int: return self._attempted_solutions @property - def unsatisfied(self): # type: () -> List[Dependency] + def unsatisfied(self) -> List["Dependency"]: return [ term.dependency for term in self._positive.values() if term.dependency.complete_name not in self._decisions ] - def decide(self, package): # type: (Package) -> None + def decide(self, package: "Package") -> None: """ Adds an assignment of package as a decision and increments the decision level. @@ -88,8 +90,8 @@ def decide(self, package): # type: (Package) -> None ) def derive( - self, dependency, is_positive, cause - ): # type: (Dependency, bool, Incompatibility) -> None + self, dependency: "Dependency", is_positive: bool, cause: Incompatibility + ) -> None: """ Adds an assignment of package as a derivation. """ @@ -103,14 +105,14 @@ def derive( ) ) - def _assign(self, assignment): # type: (Assignment) -> None + def _assign(self, assignment: Assignment) -> None: """ Adds an Assignment to _assignments and _positive or _negative. """ self._assignments.append(assignment) self._register(assignment) - def backtrack(self, decision_level): # type: (int) -> None + def backtrack(self, decision_level: int) -> None: """ Resets the current decision level to decision_level, and removes all assignments made after that level. @@ -136,7 +138,7 @@ def backtrack(self, decision_level): # type: (int) -> None if assignment.dependency.complete_name in packages: self._register(assignment) - def _register(self, assignment): # type: (Assignment) -> None + def _register(self, assignment: Assignment) -> None: """ Registers an Assignment in _positive or _negative. """ @@ -166,12 +168,12 @@ def _register(self, assignment): # type: (Assignment) -> None self._negative[name][ref] = term - def satisfier(self, term): # type: (Term) -> Assignment + def satisfier(self, term: Term) -> Assignment: """ Returns the first Assignment in this solution such that the sublist of assignments up to and including that entry collectively satisfies term. """ - assigned_term = None # type: Term + assigned_term = None for assignment in self._assignments: if assignment.dependency.complete_name != term.dependency.complete_name: @@ -197,12 +199,12 @@ def satisfier(self, term): # type: (Term) -> Assignment if assigned_term.satisfies(term): return assignment - raise RuntimeError("[BUG] {} is not satisfied.".format(term)) + raise RuntimeError(f"[BUG] {term} is not satisfied.") - def satisfies(self, term): # type: (Term) -> bool + def satisfies(self, term: Term) -> bool: return self.relation(term) == SetRelation.SUBSET - def relation(self, term): # type: (Term) -> int + def relation(self, term: Term) -> int: positive = self._positive.get(term.dependency.complete_name) if positive is not None: return positive.relation(term) diff --git a/poetry/mixology/result.py b/poetry/mixology/result.py index 5eadeb75ddc..d2ef1a2dade 100644 --- a/poetry/mixology/result.py +++ b/poetry/mixology/result.py @@ -1,13 +1,27 @@ +from typing import TYPE_CHECKING +from typing import List + + +if TYPE_CHECKING: + from poetry.core.packages.package import Package + from poetry.core.packages.project_package import ProjectPackage + + class SolverResult: - def __init__(self, root, packages, attempted_solutions): + def __init__( + self, + root: "ProjectPackage", + packages: List["Package"], + attempted_solutions: int, + ) -> None: self._root = root self._packages = packages self._attempted_solutions = attempted_solutions @property - def packages(self): + def packages(self) -> List["Package"]: return self._packages @property - def attempted_solutions(self): + def attempted_solutions(self) -> int: return self._attempted_solutions diff --git a/poetry/mixology/solutions/providers/python_requirement_solution_provider.py b/poetry/mixology/solutions/providers/python_requirement_solution_provider.py index 4c903677fd0..ac061e20f0f 100644 --- a/poetry/mixology/solutions/providers/python_requirement_solution_provider.py +++ b/poetry/mixology/solutions/providers/python_requirement_solution_provider.py @@ -7,7 +7,7 @@ class PythonRequirementSolutionProvider(HasSolutionsForException): - def can_solve(self, exception): # type: (Exception) -> bool + def can_solve(self, exception: Exception) -> bool: from poetry.puzzle.exceptions import SolverProblemError if not isinstance(exception, SolverProblemError): @@ -24,7 +24,7 @@ def can_solve(self, exception): # type: (Exception) -> bool return True - def get_solutions(self, exception): # type: (Exception) -> List[Solution] + def get_solutions(self, exception: Exception) -> List[Solution]: from ..solutions.python_requirement_solution import PythonRequirementSolution return [PythonRequirementSolution(exception)] diff --git a/poetry/mixology/solutions/solutions/python_requirement_solution.py b/poetry/mixology/solutions/solutions/python_requirement_solution.py index 9ec7cf22301..ed6e2cbd974 100644 --- a/poetry/mixology/solutions/solutions/python_requirement_solution.py +++ b/poetry/mixology/solutions/solutions/python_requirement_solution.py @@ -1,9 +1,16 @@ +from typing import TYPE_CHECKING +from typing import List + from crashtest.contracts.solution import Solution +if TYPE_CHECKING: + from poetry.mixology.incompatibility_cause import PackageNotFoundCause + + class PythonRequirementSolution(Solution): - def __init__(self, exception): - from poetry.core.semver import parse_constraint + def __init__(self, exception: "PackageNotFoundCause") -> None: + from poetry.core.semver.helpers import parse_constraint from poetry.mixology.incompatibility_cause import PythonCause self._title = "Check your dependencies Python requirement." @@ -41,11 +48,11 @@ def solution_title(self) -> str: return self._title @property - def solution_description(self): + def solution_description(self) -> str: return self._description @property - def documentation_links(self): + def documentation_links(self) -> List[str]: return [ "https://python-poetry.org/docs/dependency-specification/#python-restricted-dependencies", "https://python-poetry.org/docs/dependency-specification/#using-environment-markers", diff --git a/poetry/mixology/term.py b/poetry/mixology/term.py old mode 100755 new mode 100644 index 0889e984a3b..9542689e033 --- a/poetry/mixology/term.py +++ b/poetry/mixology/term.py @@ -1,12 +1,16 @@ -# -*- coding: utf-8 -*- -from typing import Union +from typing import TYPE_CHECKING +from typing import Optional -from poetry.core.packages import Dependency +from poetry.core.packages.dependency import Dependency from .set_relation import SetRelation -class Term(object): +if TYPE_CHECKING: + from poetry.core.semver.helpers import VersionTypes + + +class Term: """ A statement about a package which is true or false for a given selection of package versions. @@ -14,26 +18,26 @@ class Term(object): See https://github.com/dart-lang/pub/tree/master/doc/solver.md#term. """ - def __init__(self, dependency, is_positive): # type: (Dependency, bool) -> None + def __init__(self, dependency: Dependency, is_positive: bool) -> None: self._dependency = dependency self._positive = is_positive @property - def inverse(self): # type: () -> Term + def inverse(self) -> "Term": return Term(self._dependency, not self.is_positive()) @property - def dependency(self): + def dependency(self) -> Dependency: return self._dependency @property - def constraint(self): + def constraint(self) -> "VersionTypes": return self._dependency.constraint - def is_positive(self): # type: () -> bool + def is_positive(self) -> bool: return self._positive - def satisfies(self, other): # type: (Term) -> bool + def satisfies(self, other: "Term") -> bool: """ Returns whether this term satisfies another. """ @@ -42,15 +46,13 @@ def satisfies(self, other): # type: (Term) -> bool and self.relation(other) == SetRelation.SUBSET ) - def relation(self, other): # type: (Term) -> int + def relation(self, other: "Term") -> int: """ Returns the relationship between the package versions allowed by this term and another. """ if self.dependency.complete_name != other.dependency.complete_name: - raise ValueError( - "{} should refer to {}".format(other, self.dependency.complete_name) - ) + raise ValueError(f"{other} should refer to {self.dependency.complete_name}") other_constraint = other.constraint @@ -106,15 +108,13 @@ def relation(self, other): # type: (Term) -> int # not foo ^1.5.0 is a superset of not foo ^1.0.0 return SetRelation.OVERLAPPING - def intersect(self, other): # type: (Term) -> Union[Term, None] + def intersect(self, other: "Term") -> Optional["Term"]: """ Returns a Term that represents the packages allowed by both this term and another """ if self.dependency.complete_name != other.dependency.complete_name: - raise ValueError( - "{} should refer to {}".format(other, self.dependency.complete_name) - ) + raise ValueError(f"{other} should refer to {self.dependency.complete_name}") if self._compatible_dependency(other.dependency): if self.is_positive() != other.is_positive(): @@ -140,28 +140,30 @@ def intersect(self, other): # type: (Term) -> Union[Term, None] else: return - def difference(self, other): # type: (Term) -> Term + def difference(self, other: "Term") -> "Term": """ Returns a Term that represents packages allowed by this term and not by the other """ return self.intersect(other.inverse) - def _compatible_dependency(self, other): + def _compatible_dependency(self, other: "Dependency") -> bool: return ( self.dependency.is_root or other.is_root or other.is_same_package_as(self.dependency) ) - def _non_empty_term(self, constraint, is_positive): + def _non_empty_term( + self, constraint: "VersionTypes", is_positive: bool + ) -> Optional["Term"]: if constraint.is_empty(): return return Term(self.dependency.with_constraint(constraint), is_positive) - def __str__(self): + def __str__(self) -> str: return "{}{}".format("not " if not self.is_positive() else "", self._dependency) - def __repr__(self): + def __repr__(self) -> str: return "".format(str(self)) diff --git a/poetry/mixology/version_solver.py b/poetry/mixology/version_solver.py old mode 100755 new mode 100644 index 679ecf07c2d..2ef0c65268a --- a/poetry/mixology/version_solver.py +++ b/poetry/mixology/version_solver.py @@ -1,17 +1,15 @@ -# -*- coding: utf-8 -*- import time from typing import TYPE_CHECKING -from typing import Any from typing import Dict from typing import List +from typing import Optional +from typing import Tuple from typing import Union -from poetry.core.packages import Dependency -from poetry.core.packages import Package -from poetry.core.packages import ProjectPackage -from poetry.core.semver import Version -from poetry.core.semver import VersionRange +from poetry.core.packages.dependency import Dependency +from poetry.core.packages.package import Package +from poetry.core.packages.project_package import ProjectPackage from .failure import SolveFailure from .incompatibility import Incompatibility @@ -43,10 +41,10 @@ class VersionSolver: def __init__( self, - root, # type: ProjectPackage - provider, # type: Provider - locked=None, # type: Dict[str, Package] - use_latest=None, # type: List[str] + root: ProjectPackage, + provider: "Provider", + locked: Dict[str, Package] = None, + use_latest: List[str] = None, ): self._root = root self._provider = provider @@ -57,14 +55,14 @@ def __init__( self._use_latest = use_latest - self._incompatibilities = {} # type: Dict[str, List[Incompatibility]] + self._incompatibilities: Dict[str, List[Incompatibility]] = {} self._solution = PartialSolution() @property - def solution(self): # type: () -> PartialSolution + def solution(self) -> PartialSolution: return self._solution - def solve(self): # type: () -> SolverResult + def solve(self) -> SolverResult: """ Finds a set of dependencies that match the root package's constraints, or raises an error if no such set is available. @@ -94,7 +92,7 @@ def solve(self): # type: () -> SolverResult ) ) - def _propagate(self, package): # type: (str) -> None + def _propagate(self, package: str) -> None: """ Performs unit propagation on incompatibilities transitively related to package to derive new assignments for _solution. @@ -132,8 +130,8 @@ def _propagate(self, package): # type: (str) -> None changed.add(result) def _propagate_incompatibility( - self, incompatibility - ): # type: (Incompatibility) -> Union[str, _conflict, None] + self, incompatibility: Incompatibility + ) -> Optional[Union[str, object]]: """ If incompatibility is almost satisfied by _solution, adds the negation of the unsatisfied term to _solution. @@ -185,9 +183,7 @@ def _propagate_incompatibility( return unsatisfied.dependency.complete_name - def _resolve_conflict( - self, incompatibility - ): # type: (Incompatibility) -> Incompatibility + def _resolve_conflict(self, incompatibility: Incompatibility) -> Incompatibility: """ Given an incompatibility that's satisfied by _solution, The `conflict resolution`_ constructs a new incompatibility that encapsulates the root @@ -198,7 +194,7 @@ def _resolve_conflict( .. _conflict resolution: https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution """ - self._log("conflict: {}".format(incompatibility)) + self._log(f"conflict: {incompatibility}") new_incompatibility = False while not incompatibility.is_failure(): @@ -313,14 +309,12 @@ def _resolve_conflict( bang, most_recent_term, partially, most_recent_satisfier ) ) - self._log( - '{} which is caused by "{}"'.format(bang, most_recent_satisfier.cause) - ) - self._log("{} thus: {}".format(bang, incompatibility)) + self._log(f'{bang} which is caused by "{most_recent_satisfier.cause}"') + self._log(f"{bang} thus: {incompatibility}") raise SolveFailure(incompatibility) - def _choose_package_version(self): # type: () -> Union[str, None] + def _choose_package_version(self) -> Optional[str]: """ Tries to select a version of a required package. @@ -334,19 +328,19 @@ def _choose_package_version(self): # type: () -> Union[str, None] # Prefer packages with as few remaining versions as possible, # so that if a conflict is necessary it's forced quickly. - def _get_min(dependency): + def _get_min(dependency: Dependency) -> Tuple[bool, int]: if dependency.name in self._use_latest: # If we're forced to use the latest version of a package, it effectively # only has one version to choose from. - return 1 + return not dependency.marker.is_any(), 1 locked = self._get_locked(dependency) if locked and ( dependency.constraint.allows(locked.version) or locked.is_prerelease() - and dependency.constraint.allows(locked.version.next_patch) + and dependency.constraint.allows(locked.version.next_patch()) ): - return 1 + return not dependency.marker.is_any(), 1 # VCS, URL, File or Directory dependencies # represent a single version @@ -356,12 +350,15 @@ def _get_min(dependency): or dependency.is_file() or dependency.is_directory() ): - return 1 + return not dependency.marker.is_any(), 1 try: - return len(self._provider.search_for(dependency)) + return ( + not dependency.marker.is_any(), + len(self._provider.search_for(dependency)), + ) except ValueError: - return 0 + return not dependency.marker.is_any(), 0 if len(unsatisfied) == 1: dependency = unsatisfied[0] @@ -406,11 +403,9 @@ def _get_min(dependency): # We'll continue adding its dependencies, then go back to # unit propagation which will guide us to choose a better version. conflict = conflict or all( - [ - term.dependency.complete_name == dependency.complete_name - or self._solution.satisfies(term) - for term in incompatibility.terms - ] + term.dependency.complete_name == dependency.complete_name + or self._solution.satisfies(term) + for term in incompatibility.terms ) if not conflict: @@ -423,10 +418,7 @@ def _get_min(dependency): return dependency.complete_name - def _excludes_single_version(self, constraint): # type: (Any) -> bool - return isinstance(VersionRange().difference(constraint), Version) - - def _result(self): # type: () -> SolverResult + def _result(self) -> SolverResult: """ Creates a #SolverResult from the decisions in _solution """ @@ -438,8 +430,8 @@ def _result(self): # type: () -> SolverResult self._solution.attempted_solutions, ) - def _add_incompatibility(self, incompatibility): # type: (Incompatibility) -> None - self._log("fact: {}".format(incompatibility)) + def _add_incompatibility(self, incompatibility: Incompatibility) -> None: + self._log(f"fact: {incompatibility}") for term in incompatibility.terms: if term.dependency.complete_name not in self._incompatibilities: @@ -455,7 +447,7 @@ def _add_incompatibility(self, incompatibility): # type: (Incompatibility) -> N incompatibility ) - def _get_locked(self, dependency): # type: (Dependency) -> Union[Package, None] + def _get_locked(self, dependency: Dependency) -> Optional[Package]: if dependency.name in self._use_latest: return @@ -468,5 +460,5 @@ def _get_locked(self, dependency): # type: (Dependency) -> Union[Package, None] return locked - def _log(self, text): + def _log(self, text: str) -> None: self._provider.debug(text, self._solution.attempted_solutions) diff --git a/poetry/packages/dependency_package.py b/poetry/packages/dependency_package.py index 60c51007ed8..e215db5b3b2 100644 --- a/poetry/packages/dependency_package.py +++ b/poetry/packages/dependency_package.py @@ -1,50 +1,52 @@ +from typing import Any from typing import List +from typing import Union from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package -class DependencyPackage(object): - def __init__(self, dependency, package): # type: (Dependency, Package) -> None +class DependencyPackage: + def __init__(self, dependency: Dependency, package: Package) -> None: self._dependency = dependency self._package = package @property - def dependency(self): # type: () -> Dependency + def dependency(self) -> Dependency: return self._dependency @property - def package(self): # type: () -> Package + def package(self) -> Package: return self._package - def clone(self): # type: () -> DependencyPackage + def clone(self) -> "DependencyPackage": return self.__class__(self._dependency, self._package.clone()) - def with_features(self, features): # type: (List[str]) -> "DependencyPackage" + def with_features(self, features: List[str]) -> "DependencyPackage": return self.__class__(self._dependency, self._package.with_features(features)) - def without_features(self): # type: () -> "DependencyPackage" + def without_features(self) -> "DependencyPackage": return self.with_features([]) - def __getattr__(self, name): + def __getattr__(self, name: str) -> Any: return getattr(self._package, name) - def __setattr__(self, key, value): + def __setattr__(self, key: str, value: Any) -> None: if key in {"_dependency", "_package"}: - return super(DependencyPackage, self).__setattr__(key, value) + return super().__setattr__(key, value) setattr(self._package, key, value) - def __str__(self): + def __str__(self) -> str: return str(self._package) - def __repr__(self): + def __repr__(self) -> str: return repr(self._package) - def __hash__(self): + def __hash__(self) -> int: return hash(self._package) - def __eq__(self, other): + def __eq__(self, other: Union[Package, "DependencyPackage"]) -> bool: if isinstance(other, DependencyPackage): other = other.package diff --git a/poetry/packages/locker.py b/poetry/packages/locker.py index 2b696b3450a..ad61421a93e 100644 --- a/poetry/packages/locker.py +++ b/poetry/packages/locker.py @@ -1,4 +1,3 @@ -import itertools import json import logging import os @@ -6,9 +5,17 @@ from copy import deepcopy from hashlib import sha256 -from typing import Any +from pathlib import Path +from typing import TYPE_CHECKING +from typing import Dict +from typing import Iterable +from typing import Iterator from typing import List from typing import Optional +from typing import Sequence +from typing import Set +from typing import Tuple +from typing import Union from tomlkit import array from tomlkit import document @@ -19,43 +26,47 @@ import poetry.repositories -from poetry.core.packages.package import Dependency +from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package -from poetry.core.semver import parse_constraint +from poetry.core.semver.helpers import parse_constraint from poetry.core.semver.version import Version from poetry.core.toml.file import TOMLFile from poetry.core.version.markers import parse_marker -from poetry.utils._compat import OrderedDict -from poetry.utils._compat import Path +from poetry.core.version.requirements import InvalidRequirement +from poetry.packages import DependencyPackage +from poetry.utils.extras import get_extra_package_names +if TYPE_CHECKING: + from tomlkit.toml_document import TOMLDocument + logger = logging.getLogger(__name__) -class Locker(object): +class Locker: _VERSION = "1.1" _relevant_keys = ["dependencies", "dev-dependencies", "source", "extras"] - def __init__(self, lock, local_config): # type: (Path, dict) -> None + def __init__(self, lock: Union[str, Path], local_config: dict) -> None: self._lock = TOMLFile(lock) self._local_config = local_config self._lock_data = None self._content_hash = self._get_content_hash() @property - def lock(self): # type: () -> TOMLFile + def lock(self) -> TOMLFile: return self._lock @property - def lock_data(self): + def lock_data(self) -> "TOMLDocument": if self._lock_data is None: self._lock_data = self._get_lock_data() return self._lock_data - def is_locked(self): # type: () -> bool + def is_locked(self) -> bool: """ Checks whether the locker has been locked (lockfile found). """ @@ -64,7 +75,7 @@ def is_locked(self): # type: () -> bool return "package" in self.lock_data - def is_fresh(self): # type: () -> bool + def is_fresh(self) -> bool: """ Checks whether the lock file is still up to date with the current hash. """ @@ -77,8 +88,8 @@ def is_fresh(self): # type: () -> bool return False def locked_repository( - self, with_dev_reqs=False - ): # type: (bool) -> poetry.repositories.Repository + self, with_dev_reqs: bool = False + ) -> poetry.repositories.Repository: """ Searches and returns a repository of locked packages. """ @@ -135,11 +146,18 @@ def locked_repository( package.extras[name] = [] for dep in deps: - m = re.match(r"^(.+?)(?:\s+\((.+)\))?$", dep) - dep_name = m.group(1) - constraint = m.group(2) or "*" - - package.extras[name].append(Dependency(dep_name, constraint)) + try: + dependency = Dependency.create_from_pep_508(dep) + except InvalidRequirement: + # handle lock files with invalid PEP 508 + m = re.match(r"^(.+?)(?:\[(.+?)])?(?:\s+\((.+)\))?$", dep) + dep_name = m.group(1) + extras = m.group(2) or "" + constraint = m.group(3) or "*" + dependency = Dependency( + dep_name, constraint, extras=extras.split(",") + ) + package.extras[name].append(dependency) if "marker" in info: package.marker = parse_marker(info["marker"]) @@ -181,40 +199,120 @@ def locked_repository( return packages - def get_project_dependencies( - self, project_requires, pinned_versions=False, with_nested=False - ): # type: (List[Dependency], bool, bool) -> Any - packages = self.locked_repository().packages + @staticmethod + def __get_locked_package( + _dependency: Dependency, packages_by_name: Dict[str, List[Package]] + ) -> Optional[Package]: + """ + Internal helper to identify corresponding locked package using dependency + version constraints. + """ + for _package in packages_by_name.get(_dependency.name, []): + if _dependency.constraint.allows(_package.version): + return _package + return None + + @classmethod + def __walk_dependency_level( + cls, + dependencies: List[Dependency], + level: int, + pinned_versions: bool, + packages_by_name: Dict[str, List[Package]], + project_level_dependencies: Set[str], + nested_dependencies: Dict[Tuple[str, str], Dependency], + ) -> Dict[Tuple[str, str], Dependency]: + if not dependencies: + return nested_dependencies + + next_level_dependencies = [] + + for requirement in dependencies: + key = (requirement.name, requirement.pretty_constraint) + locked_package = cls.__get_locked_package(requirement, packages_by_name) + + if locked_package: + # create dependency from locked package to retain dependency metadata + # if this is not done, we can end-up with incorrect nested dependencies + marker = requirement.marker + requirement = locked_package.to_dependency() + requirement.marker = requirement.marker.intersect(marker) + + key = (requirement.name, requirement.pretty_constraint) + + if pinned_versions: + requirement.set_constraint( + locked_package.to_dependency().constraint + ) + + if key not in nested_dependencies: + for require in locked_package.requires: + if require.marker.is_empty(): + require.marker = requirement.marker + else: + require.marker = require.marker.intersect( + requirement.marker + ) + + require.marker = require.marker.intersect(locked_package.marker) + next_level_dependencies.append(require) + + if requirement.name in project_level_dependencies and level == 0: + # project level dependencies take precedence + continue - # group packages entries by name, this is required because requirement might use - # different constraints + if not locked_package: + # we make a copy to avoid any side-effects + requirement = deepcopy(requirement) + + if key not in nested_dependencies: + nested_dependencies[key] = requirement + else: + nested_dependencies[key].marker = nested_dependencies[key].marker.union( + requirement.marker + ) + + return cls.__walk_dependency_level( + dependencies=next_level_dependencies, + level=level + 1, + pinned_versions=pinned_versions, + packages_by_name=packages_by_name, + project_level_dependencies=project_level_dependencies, + nested_dependencies=nested_dependencies, + ) + + @classmethod + def get_project_dependencies( + cls, + project_requires: List[Dependency], + locked_packages: List[Package], + pinned_versions: bool = False, + with_nested: bool = False, + ) -> Iterable[Dependency]: + # group packages entries by name, this is required because requirement might use different constraints packages_by_name = {} - for pkg in packages: + for pkg in locked_packages: if pkg.name not in packages_by_name: packages_by_name[pkg.name] = [] packages_by_name[pkg.name].append(pkg) - def __get_locked_package( - _dependency, - ): # type: (Dependency) -> Optional[Package] - """ - Internal helper to identify corresponding locked package using dependency - version constraints. - """ - for _package in packages_by_name.get(_dependency.name, []): - if _dependency.constraint.allows(_package.version): - return _package - return None - project_level_dependencies = set() dependencies = [] for dependency in project_requires: dependency = deepcopy(dependency) - if pinned_versions: - locked_package = __get_locked_package(dependency) - if locked_package: - dependency.set_constraint(locked_package.to_dependency().constraint) + locked_package = cls.__get_locked_package(dependency, packages_by_name) + if locked_package: + locked_dependency = locked_package.to_dependency() + locked_dependency.marker = dependency.marker.intersect( + locked_package.marker + ) + + if not pinned_versions: + locked_dependency.set_constraint(dependency.constraint) + + dependency = locked_dependency + project_level_dependencies.add(dependency.name) dependencies.append(dependency) @@ -222,44 +320,81 @@ def __get_locked_package( # return only with project level dependencies return dependencies - nested_dependencies = list() - - for pkg in packages: # type: Package - for requirement in pkg.requires: # type: Dependency - if requirement.name in project_level_dependencies: - # project level dependencies take precedence - continue + nested_dependencies = cls.__walk_dependency_level( + dependencies=dependencies, + level=0, + pinned_versions=pinned_versions, + packages_by_name=packages_by_name, + project_level_dependencies=project_level_dependencies, + nested_dependencies=dict(), + ) - if pinned_versions: - requirement.set_constraint( - __get_locked_package(requirement).to_dependency().constraint - ) + # Merge same dependencies using marker union + for requirement in dependencies: + key = (requirement.name, requirement.pretty_constraint) + if key not in nested_dependencies: + nested_dependencies[key] = requirement + else: + nested_dependencies[key].marker = nested_dependencies[key].marker.union( + requirement.marker + ) - # dependencies use extra to indicate that it was activated via parent - # package's extras - marker = requirement.marker.without_extras() - for project_requirement in project_requires: - if ( - pkg.name == project_requirement.name - and project_requirement.constraint.allows(pkg.version) - ): - requirement.marker = marker.intersect( - project_requirement.marker - ) - break - else: - # this dependency was not from a project requirement - requirement.marker = marker.intersect(pkg.marker) + return sorted(nested_dependencies.values(), key=lambda x: x.name.lower()) - if requirement not in nested_dependencies: - nested_dependencies.append(requirement) + def get_project_dependency_packages( + self, + project_requires: List[Dependency], + dev: bool = False, + extras: Optional[Union[bool, Sequence[str]]] = None, + ) -> Iterator[DependencyPackage]: + repository = self.locked_repository(with_dev_reqs=dev) - return sorted( - itertools.chain(dependencies, nested_dependencies), - key=lambda x: x.name.lower(), + # Build a set of all packages required by our selected extras + extra_package_names = ( + None if (isinstance(extras, bool) and extras is True) else () ) - def set_lock_data(self, root, packages): # type: (...) -> bool + if extra_package_names is not None: + extra_package_names = set( + get_extra_package_names( + repository.packages, + self.lock_data.get("extras", {}), + extras or (), + ) + ) + + # If a package is optional and we haven't opted in to it, do not select + selected = [] + for dependency in project_requires: + try: + package = repository.find_packages(dependency=dependency)[0] + except IndexError: + continue + + if extra_package_names is not None and ( + package.optional and package.name not in extra_package_names + ): + # a package is locked as optional, but is not activated via extras + continue + + selected.append(dependency) + + for dependency in self.get_project_dependencies( + project_requires=selected, + locked_packages=repository.packages, + with_nested=True, + ): + try: + package = repository.find_packages(dependency=dependency)[0] + except IndexError: + continue + + for extra in dependency.extras: + package.requires_extras.append(extra) + + yield DependencyPackage(dependency=dependency, package=package) + + def set_lock_data(self, root: Package, packages: List[Package]) -> bool: files = table() packages = self._lock_packages(packages) # Retrieving hashes @@ -288,7 +423,7 @@ def set_lock_data(self, root, packages): # type: (...) -> bool for extra, deps in sorted(root.extras.items()) } - lock["metadata"] = OrderedDict( + lock["metadata"] = dict( [ ("lock-version", self._VERSION), ("python-versions", root.python_versions), @@ -304,7 +439,7 @@ def set_lock_data(self, root, packages): # type: (...) -> bool return False - def _write_lock_data(self, data): + def _write_lock_data(self, data: "TOMLDocument") -> None: self.lock.write(data) # Checking lock file data consistency @@ -313,7 +448,7 @@ def _write_lock_data(self, data): self._lock_data = None - def _get_content_hash(self): # type: () -> str + def _get_content_hash(self) -> str: """ Returns the sha256 hash of the sorted content of the pyproject file. """ @@ -329,21 +464,21 @@ def _get_content_hash(self): # type: () -> str return content_hash - def _get_lock_data(self): # type: () -> dict + def _get_lock_data(self) -> "TOMLDocument": if not self._lock.exists(): raise RuntimeError("No lockfile found. Unable to read locked packages") try: lock_data = self._lock.read() except TOMLKitError as e: - raise RuntimeError("Unable to read the lock file ({}).".format(e)) + raise RuntimeError(f"Unable to read the lock file ({e}).") lock_version = Version.parse(lock_data["metadata"].get("lock-version", "1.0")) current_version = Version.parse(self._VERSION) # We expect the locker to be able to read lock files # from the same semantic versioning range accepted_versions = parse_constraint( - "^{}".format(Version(current_version.major, 0)) + "^{}".format(Version.from_parts(current_version.major, 0)) ) lock_version_allowed = accepted_versions.allows(lock_version) if lock_version_allowed and current_version < lock_version: @@ -361,9 +496,7 @@ def _get_lock_data(self): # type: () -> dict return lock_data - def _lock_packages( - self, packages - ): # type: (List['poetry.packages.Package']) -> list + def _lock_packages(self, packages: List[Package]) -> list: locked = [] for package in sorted(packages, key=lambda x: x.name): @@ -373,14 +506,32 @@ def _lock_packages( return locked - def _dump_package(self, package): # type: (Package) -> dict + def _dump_package(self, package: Package) -> dict: dependencies = {} for dependency in sorted(package.requires, key=lambda d: d.name): if dependency.pretty_name not in dependencies: dependencies[dependency.pretty_name] = [] constraint = inline_table() - constraint["version"] = str(dependency.pretty_constraint) + + if dependency.is_directory() or dependency.is_file(): + constraint["path"] = dependency.path.as_posix() + + if dependency.is_directory() and dependency.develop: + constraint["develop"] = True + elif dependency.is_url(): + constraint["url"] = dependency.url + elif dependency.is_vcs(): + constraint[dependency.vcs] = dependency.source + + if dependency.branch: + constraint["branch"] = dependency.branch + elif dependency.tag: + constraint["tag"] = dependency.tag + elif dependency.rev: + constraint["rev"] = dependency.rev + else: + constraint["version"] = str(dependency.pretty_constraint) if dependency.extras: constraint["extras"] = sorted(dependency.extras) @@ -396,12 +547,15 @@ def _dump_package(self, package): # type: (Package) -> dict # All the constraints should have the same type, # but we want to simplify them if it's possible for dependency, constraints in tuple(dependencies.items()): - if all(len(constraint) == 1 for constraint in constraints): + if all( + len(constraint) == 1 and "version" in constraint + for constraint in constraints + ): dependencies[dependency] = [ constraint["version"] for constraint in constraints ] - data = OrderedDict( + data = dict( [ ("name", package.pretty_name), ("version", package.pretty_version), @@ -426,8 +580,10 @@ def _dump_package(self, package): # type: (Package) -> dict if package.extras: extras = {} for name, deps in package.extras.items(): + # TODO: This should use dep.to_pep_508() once this is fixed + # https://github.com/python-poetry/poetry-core/pull/102 extras[name] = [ - str(dep) if not dep.constraint.is_any() else dep.name + dep.base_pep_508_name if not dep.constraint.is_any() else dep.name for dep in deps ] @@ -443,7 +599,7 @@ def _dump_package(self, package): # type: (Package) -> dict ) ).as_posix() - data["source"] = OrderedDict() + data["source"] = dict() if package.source_type: data["source"]["type"] = package.source_type @@ -456,7 +612,12 @@ def _dump_package(self, package): # type: (Package) -> dict if package.source_resolved_reference: data["source"]["resolved_reference"] = package.source_resolved_reference - if package.source_type == "directory": + if package.source_type in ["directory", "git"]: data["develop"] = package.develop return data + + +class NullLocker(Locker): + def set_lock_data(self, root: Package, packages: List[Package]) -> bool: + pass diff --git a/poetry/packages/package_collection.py b/poetry/packages/package_collection.py index e10ea635bca..3386aee855b 100644 --- a/poetry/packages/package_collection.py +++ b/poetry/packages/package_collection.py @@ -1,22 +1,35 @@ +from typing import TYPE_CHECKING +from typing import List +from typing import Union + from .dependency_package import DependencyPackage +if TYPE_CHECKING: + from poetry.core.packages.dependency import Dependency + from poetry.core.packages.package import Package + + class PackageCollection(list): - def __init__(self, dependency, packages=None): + def __init__( + self, + dependency: "Dependency", + packages: List[Union["Package", DependencyPackage]] = None, + ) -> None: self._dependency = dependency if packages is None: packages = [] - super(PackageCollection, self).__init__() + super().__init__() for package in packages: self.append(package) - def append(self, package): + def append(self, package: Union["Package", DependencyPackage]) -> None: if isinstance(package, DependencyPackage): package = package.package package = DependencyPackage(self._dependency, package) - return super(PackageCollection, self).append(package) + return super().append(package) diff --git a/poetry/packages/project_package.py b/poetry/packages/project_package.py new file mode 100644 index 00000000000..22379c2026f --- /dev/null +++ b/poetry/packages/project_package.py @@ -0,0 +1,23 @@ +from typing import TYPE_CHECKING +from typing import Optional +from typing import Union + +from poetry.core.packages.project_package import ProjectPackage as _ProjectPackage + + +if TYPE_CHECKING: + from poetry.core.semver.version import Version # noqa + + +class ProjectPackage(_ProjectPackage): + def set_version( + self, version: Union[str, "Version"], pretty_version: Optional[str] = None + ) -> "ProjectPackage": + from poetry.core.semver.version import Version # noqa + + if not isinstance(version, Version): + self._version = Version.parse(version) + self._pretty_version = pretty_version or version + else: + self._version = version + self._pretty_version = pretty_version or version.text diff --git a/poetry/plugins/__init__.py b/poetry/plugins/__init__.py new file mode 100644 index 00000000000..c81eb48d65f --- /dev/null +++ b/poetry/plugins/__init__.py @@ -0,0 +1,5 @@ +from .application_plugin import ApplicationPlugin +from .plugin import Plugin + + +__all__ = ["ApplicationPlugin", "Plugin"] diff --git a/poetry/plugins/application_plugin.py b/poetry/plugins/application_plugin.py new file mode 100644 index 00000000000..0f896282172 --- /dev/null +++ b/poetry/plugins/application_plugin.py @@ -0,0 +1,12 @@ +from .base_plugin import BasePlugin + + +class ApplicationPlugin(BasePlugin): + """ + Base class for plugins. + """ + + type = "application.plugin" + + def activate(self, application): + raise NotImplementedError() diff --git a/poetry/plugins/base_plugin.py b/poetry/plugins/base_plugin.py new file mode 100644 index 00000000000..de42071b4dc --- /dev/null +++ b/poetry/plugins/base_plugin.py @@ -0,0 +1,6 @@ +class BasePlugin: + """ + Base class for all plugin types + """ + + PLUGIN_API_VERSION = "1.0.0" diff --git a/poetry/plugins/plugin.py b/poetry/plugins/plugin.py new file mode 100644 index 00000000000..0c2f0711a29 --- /dev/null +++ b/poetry/plugins/plugin.py @@ -0,0 +1,14 @@ +from .base_plugin import BasePlugin + + +class Plugin(BasePlugin): + """ + Generic plugin not related to the console application. + The activate() method must be implemented and receives + the Poetry instance. + """ + + type = "plugin" + + def activate(self, poetry, io): + raise NotImplementedError() diff --git a/poetry/plugins/plugin_manager.py b/poetry/plugins/plugin_manager.py new file mode 100644 index 00000000000..c24ad2b20c1 --- /dev/null +++ b/poetry/plugins/plugin_manager.py @@ -0,0 +1,60 @@ +import logging + +from typing import List + +import entrypoints + +from .application_plugin import ApplicationPlugin +from .plugin import Plugin + + +logger = logging.getLogger(__name__) + + +class PluginManager: + """ + This class registers and activates plugins. + """ + + def __init__(self, type, disable_plugins=False): # type: (str, bool) -> None + self._type = type + self._disable_plugins = disable_plugins + self._plugins = [] + + def load_plugins(self): # type: () -> None + if self._disable_plugins: + return + + plugin_entrypoints = self.get_plugin_entry_points() + + for entrypoint in plugin_entrypoints: + self._load_plugin_entrypoint(entrypoint) + + def get_plugin_entry_points(self) -> List[entrypoints.EntryPoint]: + return entrypoints.get_group_all(f"poetry.{self._type}") + + def add_plugin(self, plugin): # type: (Plugin) -> None + if not isinstance(plugin, (Plugin, ApplicationPlugin)): + raise ValueError( + "The Poetry plugin must be an instance of Plugin or ApplicationPlugin" + ) + + self._plugins.append(plugin) + + def activate(self, *args, **kwargs): + for plugin in self._plugins: + plugin.activate(*args, **kwargs) + + def _load_plugin_entrypoint( + self, entrypoint + ): # type: (entrypoints.EntryPoint) -> None + logger.debug(f"Loading the {entrypoint.name} plugin") + + plugin = entrypoint.load() + + if not issubclass(plugin, (Plugin, ApplicationPlugin)): + raise ValueError( + "The Poetry plugin must be an instance of Plugin or ApplicationPlugin" + ) + + self.add_plugin(plugin()) diff --git a/poetry/poetry.py b/poetry/poetry.py index 4878f0a22aa..95814330e4a 100644 --- a/poetry/poetry.py +++ b/poetry/poetry.py @@ -1,14 +1,19 @@ -from __future__ import absolute_import -from __future__ import unicode_literals +from pathlib import Path +from typing import TYPE_CHECKING +from typing import List -from poetry.core.packages import ProjectPackage +from poetry.__version__ import __version__ +from poetry.config.source import Source from poetry.core.poetry import Poetry as BasePoetry -from .__version__ import __version__ -from .config.config import Config -from .packages import Locker -from .repositories.pool import Pool -from .utils._compat import Path + +if TYPE_CHECKING: + from poetry.core.packages.project_package import ProjectPackage + + from .config.config import Config + from .packages.locker import Locker + from .plugins.plugin_manager import PluginManager + from .repositories.pool import Pool class Poetry(BasePoetry): @@ -17,41 +22,55 @@ class Poetry(BasePoetry): def __init__( self, - file, # type: Path - local_config, # type: dict - package, # type: ProjectPackage - locker, # type: Locker - config, # type: Config + file: Path, + local_config: dict, + package: "ProjectPackage", + locker: "Locker", + config: "Config", ): + from .repositories.pool import Pool # noqa + super(Poetry, self).__init__(file, local_config, package) self._locker = locker self._config = config self._pool = Pool() + self._plugin_manager = None @property - def locker(self): # type: () -> Locker + def locker(self) -> "Locker": return self._locker @property - def pool(self): # type: () -> Pool + def pool(self) -> "Pool": return self._pool @property - def config(self): # type: () -> Config + def config(self) -> "Config": return self._config - def set_locker(self, locker): # type: (Locker) -> Poetry + def set_locker(self, locker: "Locker") -> "Poetry": self._locker = locker return self - def set_pool(self, pool): # type: (Pool) -> Poetry + def set_pool(self, pool: "Pool") -> "Poetry": self._pool = pool return self - def set_config(self, config): # type: (Config) -> Poetry + def set_config(self, config: "Config") -> "Poetry": self._config = config return self + + def set_plugin_manager(self, plugin_manager: "PluginManager") -> "Poetry": + self._plugin_manager = plugin_manager + + return self + + def get_sources(self) -> List[Source]: + return [ + Source(**source) + for source in self.pyproject.poetry_config.get("source", []) + ] diff --git a/poetry/publishing/publisher.py b/poetry/publishing/publisher.py index 67515f77f64..cfa5c7d085d 100644 --- a/poetry/publishing/publisher.py +++ b/poetry/publishing/publisher.py @@ -1,8 +1,11 @@ import logging +from pathlib import Path +from typing import TYPE_CHECKING +from typing import List from typing import Optional +from typing import Union -from poetry.utils._compat import Path from poetry.utils.helpers import get_cert from poetry.utils.helpers import get_client_cert from poetry.utils.password_manager import PasswordManager @@ -10,6 +13,12 @@ from .uploader import Uploader +if TYPE_CHECKING: + from cleo.io import BufferedIO + from cleo.io import ConsoleIO + + from ..poetry import Poetry + logger = logging.getLogger(__name__) @@ -18,7 +27,7 @@ class Publisher: Registers and publishes packages to remote repositories. """ - def __init__(self, poetry, io): + def __init__(self, poetry: "Poetry", io: Union["BufferedIO", "ConsoleIO"]) -> None: self._poetry = poetry self._package = poetry.package self._io = io @@ -26,34 +35,32 @@ def __init__(self, poetry, io): self._password_manager = PasswordManager(poetry.config) @property - def files(self): + def files(self) -> List[Path]: return self._uploader.files def publish( self, - repository_name, - username, - password, - cert=None, - client_cert=None, - dry_run=False, - ): # type: (Optional[str], Optional[str], Optional[str], Optional[Path], Optional[Path], Optional[bool]) -> None + repository_name: Optional[str], + username: Optional[str], + password: Optional[str], + cert: Optional[Path] = None, + client_cert: Optional[Path] = None, + dry_run: Optional[bool] = False, + ) -> None: if not repository_name: url = "https://upload.pypi.org/legacy/" repository_name = "pypi" else: # Retrieving config information - url = self._poetry.config.get("repositories.{}.url".format(repository_name)) + url = self._poetry.config.get(f"repositories.{repository_name}.url") if url is None: - raise RuntimeError( - "Repository {} is not defined".format(repository_name) - ) + raise RuntimeError(f"Repository {repository_name} is not defined") if not (username and password): # Check if we have a token first token = self._password_manager.get_pypi_token(repository_name) if token: - logger.debug("Found an API token for {}.".format(repository_name)) + logger.debug(f"Found an API token for {repository_name}.") username = "__token__" password = token else: diff --git a/poetry/publishing/uploader.py b/poetry/publishing/uploader.py index b817676f956..798b2533af8 100644 --- a/poetry/publishing/uploader.py +++ b/poetry/publishing/uploader.py @@ -1,10 +1,13 @@ import hashlib import io +from pathlib import Path +from typing import TYPE_CHECKING from typing import Any from typing import Dict from typing import List from typing import Optional +from typing import Tuple from typing import Union import requests @@ -21,19 +24,23 @@ from poetry.core.masonry.metadata import Metadata from poetry.core.masonry.utils.helpers import escape_name from poetry.core.masonry.utils.helpers import escape_version -from poetry.utils._compat import Path -from poetry.utils.helpers import normalize_version +from poetry.core.utils.helpers import normalize_version from poetry.utils.patterns import wheel_file_re +if TYPE_CHECKING: + from cleo.io.null_io import NullIO + + from poetry.poetry import Poetry + _has_blake2 = hasattr(hashlib, "blake2b") class UploadError(Exception): - def __init__(self, error): # type: (Union[ConnectionError, HTTPError]) -> None + def __init__(self, error: Union[ConnectionError, HTTPError, str]) -> None: if isinstance(error, HTTPError): - message = "HTTP Error {}: {}".format( - error.response.status_code, error.response.reason + message = ( + f"HTTP Error {error.response.status_code}: {error.response.reason}" ) elif isinstance(error, ConnectionError): message = ( @@ -42,11 +49,11 @@ def __init__(self, error): # type: (Union[ConnectionError, HTTPError]) -> None ) else: message = str(error) - super(UploadError, self).__init__(message) + super().__init__(message) class Uploader: - def __init__(self, poetry, io): + def __init__(self, poetry: "Poetry", io: "NullIO") -> None: self._poetry = poetry self._package = poetry.package self._io = io @@ -54,11 +61,11 @@ def __init__(self, poetry, io): self._password = None @property - def user_agent(self): + def user_agent(self) -> str: return user_agent("poetry", __version__) @property - def adapter(self): + def adapter(self) -> adapters.HTTPAdapter: retry = util.Retry( connect=5, total=10, @@ -69,28 +76,24 @@ def adapter(self): return adapters.HTTPAdapter(max_retries=retry) @property - def files(self): # type: () -> List[Path] + def files(self) -> List[Path]: dist = self._poetry.file.parent / "dist" version = normalize_version(self._package.version.text) wheels = list( dist.glob( - "{}-{}-*.whl".format( - escape_name(self._package.pretty_name), escape_version(version) - ) + f"{escape_name(self._package.pretty_name)}-{escape_version(version)}-*.whl" ) ) - tars = list( - dist.glob("{}-{}.tar.gz".format(self._package.pretty_name, version)) - ) + tars = list(dist.glob(f"{self._package.pretty_name}-{version}.tar.gz")) return sorted(wheels + tars) - def auth(self, username, password): + def auth(self, username: str, password: str) -> None: self._username = username self._password = password - def make_session(self): # type: () -> requests.Session + def make_session(self) -> requests.Session: session = requests.session() if self.is_authenticated(): session.auth = (self._username, self._password) @@ -101,12 +104,16 @@ def make_session(self): # type: () -> requests.Session return session - def is_authenticated(self): + def is_authenticated(self) -> bool: return self._username is not None and self._password is not None def upload( - self, url, cert=None, client_cert=None, dry_run=False - ): # type: (str, Optional[Path], Optional[Path], bool) -> None + self, + url: str, + cert: Optional[Path] = None, + client_cert: Optional[Path] = None, + dry_run: bool = False, + ) -> None: session = self.make_session() if cert: @@ -120,7 +127,7 @@ def upload( finally: session.close() - def post_data(self, file): # type: (Path) -> Dict[str, Any] + def post_data(self, file: Path) -> Dict[str, Any]: meta = Metadata.from_package(self._package) file_type = self._get_type(file) @@ -199,8 +206,8 @@ def post_data(self, file): # type: (Path) -> Dict[str, Any] return data def _upload( - self, session, url, dry_run=False - ): # type: (requests.Session, str, Optional[bool]) -> None + self, session: requests.Session, url: str, dry_run: Optional[bool] = False + ) -> None: try: self._do_upload(session, url, dry_run) except HTTPError as e: @@ -216,8 +223,8 @@ def _upload( raise UploadError(e) def _do_upload( - self, session, url, dry_run=False - ): # type: (requests.Session, str, Optional[bool]) -> None + self, session: requests.Session, url: str, dry_run: Optional[bool] = False + ) -> None: for file in self.files: # TODO: Check existence @@ -227,8 +234,14 @@ def _do_upload( resp.raise_for_status() def _upload_file( - self, session, url, file, dry_run=False - ): # type: (requests.Session, str, Path, Optional[bool]) -> requests.Response + self, + session: requests.Session, + url: str, + file: Path, + dry_run: Optional[bool] = False, + ) -> requests.Response: + from cleo.ui.progress_bar import ProgressBar + data = self.post_data(file) data.update( { @@ -245,10 +258,8 @@ def _upload_file( ("content", (file.name, fp, "application/octet-stream")) ) encoder = MultipartEncoder(data_to_send) - bar = self._io.progress_bar(encoder.len) - bar.set_format( - " - Uploading {0} %percent%%".format(file.name) - ) + bar = ProgressBar(self._io, max=encoder.len) + bar.set_format(f" - Uploading {file.name} %percent%%") monitor = MultipartEncoderMonitor( encoder, lambda monitor: bar.set_progress(monitor.bytes_read) ) @@ -265,19 +276,24 @@ def _upload_file( allow_redirects=False, headers={"Content-Type": monitor.content_type}, ) - if dry_run or resp.ok: + if dry_run or 200 <= resp.status_code < 300: bar.set_format( - " - Uploading {0} %percent%%".format( - file.name - ) + f" - Uploading {file.name} %percent%%" ) bar.finish() + elif resp.status_code == 301: + if self._io.output.is_decorated(): + self._io.overwrite( + f" - Uploading {file.name} FAILED" + ) + raise UploadError( + "Redirects are not supported. " + "Is the URL missing a trailing slash?" + ) except (requests.ConnectionError, requests.HTTPError) as e: - if self._io.output.supports_ansi(): + if self._io.output.is_decorated(): self._io.overwrite( - " - Uploading {0} {1}".format( - file.name, "FAILED" - ) + f" - Uploading {file.name} FAILED" ) raise UploadError(e) finally: @@ -285,19 +301,18 @@ def _upload_file( return resp - def _register( - self, session, url - ): # type: (requests.Session, str) -> requests.Response + def _register(self, session: requests.Session, url: str) -> requests.Response: """ Register a package to a repository. """ dist = self._poetry.file.parent / "dist" - file = dist / "{}-{}.tar.gz".format( - self._package.name, normalize_version(self._package.version.text) + file = ( + dist + / f"{self._package.name}-{normalize_version(self._package.version.text)}.tar.gz" ) if not file.exists(): - raise RuntimeError('"{0}" does not exist.'.format(file.name)) + raise RuntimeError(f'"{file.name}" does not exist.') data = self.post_data(file) data.update({":action": "submit", "protocol_version": "1"}) @@ -315,7 +330,7 @@ def _register( return resp - def _prepare_data(self, data): + def _prepare_data(self, data: Dict) -> List[Tuple[str, str]]: data_to_send = [] for key, value in data.items(): if not isinstance(value, (list, tuple)): @@ -326,7 +341,7 @@ def _prepare_data(self, data): return data_to_send - def _get_type(self, file): + def _get_type(self, file: Path) -> str: exts = file.suffixes if exts[-1] == ".whl": return "bdist_wheel" diff --git a/poetry/puzzle/exceptions.py b/poetry/puzzle/exceptions.py index e2e0b0dcce7..4eaf46fda40 100644 --- a/poetry/puzzle/exceptions.py +++ b/poetry/puzzle/exceptions.py @@ -1,18 +1,22 @@ +from typing import Dict +from typing import Tuple + + class SolverProblemError(Exception): - def __init__(self, error): + def __init__(self, error: Exception) -> None: self._error = error - super(SolverProblemError, self).__init__(str(error)) + super().__init__(str(error)) @property - def error(self): + def error(self) -> Exception: return self._error class OverrideNeeded(Exception): - def __init__(self, *overrides): + def __init__(self, *overrides: Dict) -> None: self._overrides = overrides @property - def overrides(self): + def overrides(self) -> Tuple[Dict]: return self._overrides diff --git a/poetry/puzzle/provider.py b/poetry/puzzle/provider.py old mode 100755 new mode 100644 index 44d8b6e3796..c7427161c2c --- a/poetry/puzzle/provider.py +++ b/poetry/puzzle/provider.py @@ -2,22 +2,27 @@ import os import re import time +import urllib.parse from contextlib import contextmanager +from pathlib import Path from tempfile import mkdtemp from typing import Any +from typing import Dict +from typing import Iterator from typing import List from typing import Optional +from typing import Union -from clikit.ui.components import ProgressIndicator +from cleo.ui.progress_indicator import ProgressIndicator -from poetry.core.packages import Dependency -from poetry.core.packages import DirectoryDependency -from poetry.core.packages import FileDependency -from poetry.core.packages import Package -from poetry.core.packages import URLDependency -from poetry.core.packages import VCSDependency +from poetry.core.packages.dependency import Dependency +from poetry.core.packages.directory_dependency import DirectoryDependency +from poetry.core.packages.file_dependency import FileDependency +from poetry.core.packages.package import Package +from poetry.core.packages.url_dependency import URLDependency from poetry.core.packages.utils.utils import get_python_constraint_from_marker +from poetry.core.packages.vcs_dependency import VCSDependency from poetry.core.semver.version import Version from poetry.core.vcs.git import Git from poetry.core.version.markers import MarkerUnion @@ -31,9 +36,6 @@ from poetry.packages.package_collection import PackageCollection from poetry.puzzle.exceptions import OverrideNeeded from poetry.repositories import Pool -from poetry.utils._compat import OrderedDict -from poetry.utils._compat import Path -from poetry.utils._compat import urlparse from poetry.utils.env import Env from poetry.utils.helpers import download_file from poetry.utils.helpers import safe_rmtree @@ -44,19 +46,19 @@ class Indicator(ProgressIndicator): - def _formatter_elapsed(self): + def _formatter_elapsed(self) -> str: elapsed = time.time() - self._start_time - return "{:.1f}s".format(elapsed) + return f"{elapsed:.1f}s" class Provider: - UNSAFE_PACKAGES = {"setuptools", "distribute", "pip", "wheel"} + UNSAFE_PACKAGES = set() def __init__( - self, package, pool, io, env=None - ): # type: (Package, Pool, Any, Optional[Env]) -> None + self, package: Package, pool: Pool, io: Any, env: Optional[Env] = None + ) -> None: self._package = package self._pool = pool self._io = io @@ -70,20 +72,20 @@ def __init__( self._load_deferred = True @property - def pool(self): # type: () -> Pool + def pool(self) -> Pool: return self._pool - def is_debugging(self): + def is_debugging(self) -> bool: return self._is_debugging - def set_overrides(self, overrides): + def set_overrides(self, overrides: Dict) -> None: self._overrides = overrides - def load_deferred(self, load_deferred): # type: (bool) -> None + def load_deferred(self, load_deferred: bool) -> None: self._load_deferred = load_deferred @contextmanager - def use_environment(self, env): # type: (Env) -> Provider + def use_environment(self, env: Env) -> "Provider": original_env = self._env original_python_constraint = self._python_constraint @@ -95,7 +97,16 @@ def use_environment(self, env): # type: (Env) -> Provider self._env = original_env self._python_constraint = original_python_constraint - def search_for(self, dependency): # type: (Dependency) -> List[Package] + def search_for( + self, + dependency: Union[ + Dependency, + VCSDependency, + FileDependency, + DirectoryDependency, + URLDependency, + ], + ) -> List[DependencyPackage]: """ Search for the specifications that match the given dependency. @@ -105,7 +116,7 @@ def search_for(self, dependency): # type: (Dependency) -> List[Package] if dependency.is_root: return PackageCollection(dependency, [self._package]) - for constraint in self._search_for.keys(): + for constraint in self._search_for: if ( constraint.is_same_package_as(dependency) and constraint.constraint.intersect(dependency.constraint) @@ -150,7 +161,7 @@ def search_for(self, dependency): # type: (Dependency) -> List[Package] return PackageCollection(dependency, packages) - def search_for_vcs(self, dependency): # type: (VCSDependency) -> List[Package] + def search_for_vcs(self, dependency: VCSDependency) -> List[Package]: """ Search for the specifications that match the given VCS dependency. @@ -168,6 +179,7 @@ def search_for_vcs(self, dependency): # type: (VCSDependency) -> List[Package] rev=dependency.rev, name=dependency.name, ) + package.develop = dependency.develop dependency._constraint = package.version dependency._pretty_constraint = package.version.text @@ -178,10 +190,16 @@ def search_for_vcs(self, dependency): # type: (VCSDependency) -> List[Package] @classmethod def get_package_from_vcs( - cls, vcs, url, branch=None, tag=None, rev=None, name=None - ): # type: (str, str, Optional[str], Optional[str]) -> Package + cls, + vcs: str, + url: str, + branch: Optional[str] = None, + tag: Optional[str] = None, + rev: Optional[str] = None, + name: Optional[str] = None, + ) -> Package: if vcs != "git": - raise ValueError("Unsupported VCS dependency {}".format(vcs)) + raise ValueError(f"Unsupported VCS dependency {vcs}") tmp_dir = Path( mkdtemp(prefix="pypoetry-git-{}".format(url.split("/")[-1].rstrip(".git"))) @@ -210,7 +228,7 @@ def get_package_from_vcs( return package - def search_for_file(self, dependency): # type: (FileDependency) -> List[Package] + def search_for_file(self, dependency: FileDependency) -> List[Package]: if dependency in self._deferred_cache: dependency, _package = self._deferred_cache[dependency] @@ -241,21 +259,19 @@ def search_for_file(self, dependency): # type: (FileDependency) -> List[Package return [package] @classmethod - def get_package_from_file(cls, file_path): # type: (Path) -> Package + def get_package_from_file(cls, file_path: Path) -> Package: try: package = PackageInfo.from_path(path=file_path).to_package( root_dir=file_path ) except PackageInfoError: raise RuntimeError( - "Unable to determine package info from path: {}".format(file_path) + f"Unable to determine package info from path: {file_path}" ) return package - def search_for_directory( - self, dependency - ): # type: (DirectoryDependency) -> List[Package] + def search_for_directory(self, dependency: DirectoryDependency) -> List[Package]: if dependency in self._deferred_cache: dependency, _package = self._deferred_cache[dependency] @@ -279,8 +295,8 @@ def search_for_directory( @classmethod def get_package_from_directory( - cls, directory, name=None - ): # type: (Path, Optional[str]) -> Package + cls, directory: Path, name: Optional[str] = None + ) -> Package: package = PackageInfo.from_directory(path=directory).to_package( root_dir=directory ) @@ -295,7 +311,7 @@ def get_package_from_directory( return package - def search_for_url(self, dependency): # type: (URLDependency) -> List[Package] + def search_for_url(self, dependency: URLDependency) -> List[Package]: if dependency in self._deferred_cache: return [self._deferred_cache[dependency]] @@ -324,10 +340,10 @@ def search_for_url(self, dependency): # type: (URLDependency) -> List[Package] return [package] @classmethod - def get_package_from_url(cls, url): # type: (str) -> Package + def get_package_from_url(cls, url: str) -> Package: with temporary_directory() as temp_dir: temp_dir = Path(temp_dir) - file_name = os.path.basename(urlparse.urlparse(url).path) + file_name = os.path.basename(urllib.parse.urlparse(url).path) download_file(url, str(temp_dir / file_name)) package = cls.get_package_from_file(temp_dir / file_name) @@ -338,8 +354,8 @@ def get_package_from_url(cls, url): # type: (str) -> Package return package def incompatibilities_for( - self, package - ): # type: (DependencyPackage) -> List[Incompatibility] + self, package: DependencyPackage + ) -> List[Incompatibility]: """ Returns incompatibilities that encapsulate a given package's dependencies, or that it can't be safely selected. @@ -414,10 +430,7 @@ def incompatibilities_for( for dep in dependencies ] - def complete_package( - self, package - ): # type: (DependencyPackage) -> DependencyPackage - + def complete_package(self, package: DependencyPackage) -> DependencyPackage: if package.is_root(): package = package.clone() requires = package.all_requires @@ -432,7 +445,7 @@ def complete_package( self._pool.package( package.name, package.version.text, - extras=package.dependency.extras, + extras=list(package.dependency.extras), repository=package.dependency.source_name, ), ) @@ -453,23 +466,19 @@ def complete_package( self.search_for_url(r) optional_dependencies = [] - activated_extras = [] - for extra in package.dependency.extras: - if extra not in package.extras: - continue - - activated_extras.append(extra) - optional_dependencies += [d.name for d in package.extras[extra]] - _dependencies = [] # If some extras/features were required, we need to # add a special dependency representing the base package # to the current package if package.dependency.extras: - if activated_extras: - package = package.with_features(activated_extras) + for extra in package.dependency.extras: + if extra not in package.extras: + continue + + optional_dependencies += [d.name for d in package.extras[extra]] + package = package.with_features(list(package.dependency.extras)) _dependencies.append(package.without_features().to_dependency()) for dep in requires: @@ -482,12 +491,12 @@ def complete_package( if self._env and not dep.marker.validate(self._env.marker_env): continue - if ( - dep.is_optional() - and dep.name not in optional_dependencies - and not package.is_root() - ): - continue + if not package.is_root(): + if (dep.is_optional() and dep.name not in optional_dependencies) or ( + dep.in_extras + and not set(dep.in_extras).intersection(package.dependency.extras) + ): + continue _dependencies.append(dep) @@ -524,7 +533,7 @@ def complete_package( # An example of this is: # - pypiwin32 (220); sys_platform == "win32" and python_version >= "3.6" # - pypiwin32 (219); sys_platform == "win32" and python_version < "3.6" - duplicates = OrderedDict() + duplicates = dict() for dep in dependencies: if dep.name not in duplicates: duplicates[dep.name] = [] @@ -537,10 +546,10 @@ def complete_package( dependencies.append(deps[0]) continue - self.debug("Duplicate dependencies for {}".format(dep_name)) + self.debug(f"Duplicate dependencies for {dep_name}") # Regrouping by constraint - by_constraint = OrderedDict() + by_constraint = dict() for dep in deps: if dep.constraint not in by_constraint: by_constraint[dep.constraint] = [] @@ -666,8 +675,10 @@ def complete_package( 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() + 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 @@ -692,7 +703,7 @@ def complete_package( return package - def debug(self, message, depth=0): + def debug(self, message: str, depth: int = 0) -> None: if not (self._io.is_very_verbose() or self._io.is_debug()): return @@ -779,8 +790,8 @@ def debug(self, message, depth=0): self._io.write(debug_info) @contextmanager - def progress(self): - if not self._io.output.supports_ansi() or self.is_debugging(): + def progress(self) -> Iterator[None]: + if not self._io.output.is_decorated() or self.is_debugging(): self._io.write_line("Resolving dependencies...") yield else: diff --git a/poetry/puzzle/solver.py b/poetry/puzzle/solver.py index 31858bb3a1d..2e864bc9b20 100644 --- a/poetry/puzzle/solver.py +++ b/poetry/puzzle/solver.py @@ -3,17 +3,21 @@ from collections import defaultdict from contextlib import contextmanager +from typing import TYPE_CHECKING +from typing import Callable +from typing import Dict from typing import List from typing import Optional +from typing import Tuple +from typing import Union -from clikit.io import ConsoleIO +from cleo.io.io import IO -from poetry.core.packages import Package +from poetry.core.packages.package import Package from poetry.core.packages.project_package import ProjectPackage from poetry.installation.operations import Install from poetry.installation.operations import Uninstall from poetry.installation.operations import Update -from poetry.installation.operations.operation import Operation from poetry.mixology import resolve_version from poetry.mixology.failure import SolveFailure from poetry.packages import DependencyPackage @@ -26,16 +30,25 @@ from .provider import Provider +if TYPE_CHECKING: + from poetry.core.packages.dependency import Dependency + from poetry.core.packages.directory_dependency import DirectoryDependency + from poetry.core.packages.file_dependency import FileDependency + from poetry.core.packages.url_dependency import URLDependency + from poetry.core.packages.vcs_dependency import VCSDependency + from poetry.installation.operations import OperationTypes + + class Solver: def __init__( self, - package, # type: ProjectPackage - pool, # type: Pool - installed, # type: Repository - locked, # type: Repository - io, # type: ConsoleIO - remove_untracked=False, # type: bool - provider=None, # type: Optional[Provider] + package: ProjectPackage, + pool: Pool, + installed: Repository, + locked: Repository, + io: IO, + remove_untracked: bool = False, + provider: Optional[Provider] = None, ): self._package = package self._pool = pool @@ -50,16 +63,37 @@ def __init__( self._overrides = [] self._remove_untracked = remove_untracked + self._preserved_package_names = None + @property - def provider(self): # type: () -> Provider + def provider(self) -> Provider: return self._provider + @property + def preserved_package_names(self): + if self._preserved_package_names is None: + self._preserved_package_names = { + self._package.name, + *Provider.UNSAFE_PACKAGES, + } + + deps = {package.name for package in self._locked.packages} + + # preserve pip/setuptools/wheel when not managed by poetry, this is so + # to avoid externally managed virtual environments causing unnecessary + # removals. + for name in {"pip", "wheel", "setuptools"}: + if name not in deps: + self._preserved_package_names.add(name) + + return self._preserved_package_names + @contextmanager - def use_environment(self, env): # type: (Env) -> None + def use_environment(self, env: Env) -> None: with self.provider.use_environment(env): yield - def solve(self, use_latest=None): # type: (...) -> List[Operation] + def solve(self, use_latest: List[str] = None) -> List["OperationTypes"]: with self._provider.progress(): start = time.time() packages, depths = self._solve(use_latest=use_latest) @@ -72,9 +106,7 @@ def solve(self, use_latest=None): # type: (...) -> List[Operation] ) ) self._provider.debug( - "Resolved with overrides: {}".format( - ", ".join("({})".format(b) for b in self._overrides) - ) + f"Resolved with overrides: {', '.join(f'({b})' for b in self._overrides)}" ) operations = [] @@ -179,19 +211,24 @@ def solve(self, use_latest=None): # type: (...) -> List[Operation] locked_names = {locked.name for locked in self._locked.packages} for installed in self._installed.packages: - if installed.name == self._package.name: - continue - if installed.name in Provider.UNSAFE_PACKAGES: - # Never remove pip, setuptools etc. + if installed.name in self.preserved_package_names: continue + if installed.name not in locked_names: operations.append(Uninstall(installed)) return sorted( - operations, key=lambda o: (-o.priority, o.package.name, o.package.version,), + operations, + key=lambda o: ( + -o.priority, + o.package.name, + o.package.version, + ), ) - def solve_in_compatibility_mode(self, overrides, use_latest=None): + def solve_in_compatibility_mode( + self, overrides: Tuple[Dict], use_latest: List[str] = None + ) -> Tuple[List["Package"], List[int]]: locked = {} for package in self._locked.packages: locked[package.name] = DependencyPackage(package.to_dependency(), package) @@ -201,7 +238,7 @@ def solve_in_compatibility_mode(self, overrides, use_latest=None): for override in overrides: self._provider.debug( "Retrying dependency resolution " - "with the following overrides ({}).".format(override) + f"with the following overrides ({override})." ) self._provider.set_overrides(override) _packages, _depths = self._solve(use_latest=use_latest) @@ -221,7 +258,7 @@ def solve_in_compatibility_mode(self, overrides, use_latest=None): return packages, depths - def _solve(self, use_latest=None): + def _solve(self, use_latest: List[str] = None) -> Tuple[List[Package], List[int]]: if self._provider._overrides: self._overrides.append(self._provider._overrides) @@ -273,19 +310,19 @@ def _solve(self, use_latest=None): return final_packages, depths -class DFSNode(object): - def __init__(self, id, name, base_name): +class DFSNode: + def __init__(self, id: Tuple[str, str, bool], name: str, base_name: str) -> None: self.id = id self.name = name self.base_name = base_name - def reachable(self): + def reachable(self) -> List: return [] - def visit(self, parents): + def visit(self, parents: List["PackageNode"]) -> None: pass - def __str__(self): + def __str__(self) -> str: return str(self.id) @@ -295,7 +332,9 @@ class VisitedState(enum.Enum): Visited = 2 -def depth_first_search(source, aggregator): +def depth_first_search( + source: "PackageNode", aggregator: Callable +) -> List[Tuple[Package, int]]: back_edges = defaultdict(list) visited = {} topo_sorted_nodes = [] @@ -322,7 +361,12 @@ def depth_first_search(source, aggregator): return results -def dfs_visit(node, back_edges, visited, sorted_nodes): +def dfs_visit( + node: "PackageNode", + back_edges: Dict[str, List["PackageNode"]], + visited: Dict[str, VisitedState], + sorted_nodes: List["PackageNode"], +) -> bool: if visited.get(node.id, VisitedState.Unvisited) == VisitedState.Visited: return True if visited.get(node.id, VisitedState.Unvisited) == VisitedState.PartiallyVisited: @@ -343,8 +387,29 @@ def dfs_visit(node, back_edges, visited, sorted_nodes): class PackageNode(DFSNode): def __init__( - self, package, packages, previous=None, previous_dep=None, dep=None, - ): + self, + package: Package, + packages: List[Package], + previous: Optional["PackageNode"] = None, + previous_dep: Optional[ + Union[ + "DirectoryDependency", + "FileDependency", + "URLDependency", + "VCSDependency", + "Dependency", + ] + ] = None, + dep: Optional[ + Union[ + "DirectoryDependency", + "FileDependency", + "URLDependency", + "VCSDependency", + "Dependency", + ] + ] = None, + ) -> None: self.package = package self.packages = packages @@ -360,14 +425,14 @@ def __init__( self.category = dep.category self.optional = dep.is_optional() - super(PackageNode, self).__init__( + super().__init__( (package.complete_name, self.category, self.optional), package.complete_name, package.name, ) - def reachable(self): - children = [] # type: List[PackageNode] + def reachable(self) -> List["PackageNode"]: + children: List[PackageNode] = [] if ( self.previous_dep @@ -389,7 +454,7 @@ def reachable(self): if pkg.complete_name == dependency.complete_name and ( dependency.constraint.allows(pkg.version) or dependency.allows_prereleases() - and pkg.version.is_prerelease() + and pkg.version.is_unstable() and dependency.constraint.allows(pkg.version.stable) ): # If there is already a child with this name @@ -413,7 +478,7 @@ def reachable(self): return children - def visit(self, parents): + def visit(self, parents: "PackageNode") -> None: # The root package, which has no parents, is defined as having depth -1 # So that the root package's top-level dependencies have depth 0. self.depth = 1 + max( @@ -425,7 +490,9 @@ def visit(self, parents): ) -def aggregate_package_nodes(nodes, children): +def aggregate_package_nodes( + nodes: List[PackageNode], children: List[PackageNode] +) -> Tuple[Package, int]: package = nodes[0].package depth = max(node.depth for node in nodes) category = ( diff --git a/poetry/repositories/base_repository.py b/poetry/repositories/base_repository.py index 46422ca0edc..3d5e5c5ebcd 100644 --- a/poetry/repositories/base_repository.py +++ b/poetry/repositories/base_repository.py @@ -1,19 +1,31 @@ -class BaseRepository(object): - def __init__(self): +from typing import TYPE_CHECKING +from typing import List +from typing import Optional + + +if TYPE_CHECKING: + from poetry.core.packages.dependency import Dependency + from poetry.core.packages.package import Package + + +class BaseRepository: + def __init__(self) -> None: self._packages = [] @property - def packages(self): + def packages(self) -> List["Package"]: return self._packages - def has_package(self, package): + def has_package(self, package: "Package") -> None: raise NotImplementedError() - def package(self, name, version, extras=None): + def package( + self, name: str, version: str, extras: Optional[List[str]] = None + ) -> None: raise NotImplementedError() - def find_packages(self, dependency): + def find_packages(self, dependency: "Dependency") -> None: raise NotImplementedError() - def search(self, query): + def search(self, query: str) -> None: raise NotImplementedError() diff --git a/poetry/repositories/installed_repository.py b/poetry/repositories/installed_repository.py index a0630116f53..841c0a5e610 100644 --- a/poetry/repositories/installed_repository.py +++ b/poetry/repositories/installed_repository.py @@ -1,11 +1,15 @@ import itertools +import json +from pathlib import Path from typing import Set +from typing import Tuple from typing import Union -from poetry.core.packages import Package +from poetry.core.packages.package import Package +from poetry.core.packages.utils.utils import url_to_path +from poetry.core.utils.helpers import canonicalize_name from poetry.core.utils.helpers import module_name -from poetry.utils._compat import Path from poetry.utils._compat import metadata from poetry.utils.env import Env @@ -23,7 +27,7 @@ class InstalledRepository(Repository): @classmethod - def get_package_paths(cls, env, name): # type: (Env, str) -> Set[Path] + def get_package_paths(cls, env: Env, name: str) -> Set[Path]: """ Process a .pth file within the site-packages directories, and return any valid paths. We skip executable .pth files as there is no reliable means to do this @@ -44,7 +48,8 @@ def get_package_paths(cls, env, name): # type: (Env, str) -> Set[Path] # where the pth file for foo-bar might have been installed as either foo-bar.pth or # foo_bar.pth (expected) in either pure or platform lib directories. candidates = itertools.product( - {env.purelib, env.platlib}, {name, module_name(name)}, + {env.purelib, env.platlib}, + {name, module_name(name)}, ) for lib, module in candidates: @@ -67,26 +72,17 @@ def get_package_paths(cls, env, name): # type: (Env, str) -> Set[Path] return paths @classmethod - def set_package_vcs_properties_from_path( - cls, src, package - ): # type: (Path, Package) -> None + def get_package_vcs_properties_from_path(cls, src: Path) -> Tuple[str, str, str]: from poetry.core.vcs.git import Git git = Git() revision = git.rev_parse("HEAD", src).strip() url = git.remote_url(src) - package._source_type = "git" - package._source_url = url - package._source_reference = revision + return "git", url, revision @classmethod - def set_package_vcs_properties(cls, package, env): # type: (Package, Env) -> None - src = env.path / "src" / package.name - cls.set_package_vcs_properties_from_path(src, package) - - @classmethod - def is_vcs_package(cls, package, env): # type: (Union[Path, Package], Env) -> bool + def is_vcs_package(cls, package: Union[Path, Package], env: Env) -> bool: # A VCS dependency should have been installed # in the src directory. src = env.path / "src" @@ -101,26 +97,146 @@ def is_vcs_package(cls, package, env): # type: (Union[Path, Package], Env) -> b return True @classmethod - def load(cls, env): # type: (Env) -> InstalledRepository + def create_package_from_distribution( + cls, distribution: metadata.Distribution, env: "Env" + ) -> Package: + # We first check for a direct_url.json file to determine + # the type of package. + path = Path(str(distribution._path)) + + if ( + path.name.endswith(".dist-info") + and path.joinpath("direct_url.json").exists() + ): + return cls.create_package_from_pep610(distribution) + + is_standard_package = env.is_path_relative_to_lib(path) + + source_type = None + source_url = None + source_reference = None + source_resolved_reference = None + if is_standard_package: + if path.name.endswith(".dist-info"): + paths = cls.get_package_paths( + env=env, name=distribution.metadata["name"] + ) + if paths: + is_editable_package = False + for src in paths: + if cls.is_vcs_package(src, env): + ( + source_type, + source_url, + source_reference, + ) = cls.get_package_vcs_properties_from_path(src) + break + + if not ( + is_editable_package or env.is_path_relative_to_lib(src) + ): + is_editable_package = True + else: + # TODO: handle multiple source directories? + if is_editable_package: + source_type = "directory" + source_url = paths.pop().as_posix() + else: + if cls.is_vcs_package(path, env): + ( + source_type, + source_url, + source_reference, + ) = cls.get_package_vcs_properties_from_path( + env.path / "src" / canonicalize_name(distribution.metadata["name"]) + ) + else: + # If not, it's a path dependency + source_type = "directory" + source_url = str(path.parent) + + package = Package( + distribution.metadata["name"], + distribution.metadata["version"], + source_type=source_type, + source_url=source_url, + source_reference=source_reference, + source_resolved_reference=source_resolved_reference, + ) + package.description = distribution.metadata.get("summary", "") + + return package + + @classmethod + def create_package_from_pep610(cls, distribution: metadata.Distribution) -> Package: + path = Path(str(distribution._path)) + source_type = None + source_url = None + source_reference = None + source_resolved_reference = None + develop = False + + url_reference = json.loads( + path.joinpath("direct_url.json").read_text(encoding="utf-8") + ) + if "archive_info" in url_reference: + # File or URL distribution + if url_reference["url"].startswith("file:"): + # File distribution + source_type = "file" + source_url = url_to_path(url_reference["url"]).as_posix() + else: + # URL distribution + source_type = "url" + source_url = url_reference["url"] + elif "dir_info" in url_reference: + # Directory distribution + source_type = "directory" + source_url = url_to_path(url_reference["url"]).as_posix() + develop = url_reference["dir_info"].get("editable", False) + elif "vcs_info" in url_reference: + # VCS distribution + source_type = url_reference["vcs_info"]["vcs"] + source_url = url_reference["url"] + source_reference = url_reference["vcs_info"]["requested_revision"] + source_resolved_reference = url_reference["vcs_info"]["commit_id"] + + package = Package( + distribution.metadata["name"], + distribution.metadata["version"], + source_type=source_type, + source_url=source_url, + source_reference=source_reference, + source_resolved_reference=source_resolved_reference, + develop=develop, + ) + + package.description = distribution.metadata.get("summary", "") + + return package + + @classmethod + def load(cls, env: Env, with_dependencies: bool = False) -> "InstalledRepository": """ Load installed packages. """ + from poetry.core.packages.dependency import Dependency + repo = cls() seen = set() for entry in reversed(env.sys_path): for distribution in sorted( - metadata.distributions(path=[entry]), key=lambda d: str(d._path), + metadata.distributions(path=[entry]), + key=lambda d: str(d._path), ): - name = distribution.metadata["name"] - path = Path(str(distribution._path)) - version = distribution.metadata["version"] - package = Package(name, version, version) - package.description = distribution.metadata.get("summary", "") + name = canonicalize_name(distribution.metadata["name"]) - if package.name in seen: + if name in seen: continue + path = Path(str(distribution._path)) + try: path.relative_to(_VENDORS) except ValueError: @@ -128,31 +244,14 @@ def load(cls, env): # type: (Env) -> InstalledRepository else: continue - seen.add(package.name) - - repo.add_package(package) + package = cls.create_package_from_distribution(distribution, env) - is_standard_package = env.is_path_relative_to_lib(path) - - if is_standard_package: - if path.name.endswith(".dist-info"): - paths = cls.get_package_paths(env=env, name=package.pretty_name) - if paths: - for src in paths: - if cls.is_vcs_package(src, env): - cls.set_package_vcs_properties(package, env) - break - else: - # TODO: handle multiple source directories? - package._source_type = "directory" - package._source_url = paths.pop().as_posix() - continue + if with_dependencies: + for require in distribution.metadata.get_all("requires-dist", []): + dep = Dependency.create_from_pep_508(require) + package.add_dependency(dep) - if cls.is_vcs_package(path, env): - cls.set_package_vcs_properties(package, env) - else: - # If not, it's a path dependency - package._source_type = "directory" - package._source_url = str(path.parent) + seen.add(package.name) + repo.add_package(package) return repo diff --git a/poetry/repositories/legacy_repository.py b/poetry/repositories/legacy_repository.py old mode 100755 new mode 100644 index f9963ddc4d4..60fd8c8b6fa --- a/poetry/repositories/legacy_repository.py +++ b/poetry/repositories/legacy_repository.py @@ -1,11 +1,17 @@ import cgi +import hashlib import re +import urllib.parse import warnings from collections import defaultdict -from typing import Generator +from pathlib import Path +from typing import TYPE_CHECKING +from typing import Any +from typing import Dict +from typing import Iterator +from typing import List from typing import Optional -from typing import Union import requests import requests.auth @@ -14,15 +20,16 @@ from cachecontrol.caches.file_cache import FileCache from cachy import CacheManager -from poetry.core.packages import Package +from poetry.core.packages.package import Package from poetry.core.packages.utils.link import Link -from poetry.core.semver import Version -from poetry.core.semver import VersionConstraint -from poetry.core.semver import VersionRange -from poetry.core.semver import parse_constraint +from poetry.core.semver.helpers import parse_constraint +from poetry.core.semver.version import Version +from poetry.core.semver.version_constraint import VersionConstraint +from poetry.core.semver.version_range import VersionRange from poetry.locations import REPOSITORY_CACHE_DIR -from poetry.utils._compat import Path from poetry.utils.helpers import canonicalize_name +from poetry.utils.helpers import download_file +from poetry.utils.helpers import temporary_directory from poetry.utils.patterns import wheel_file_re from ..config.config import Config @@ -33,10 +40,8 @@ from .pypi_repository import PyPiRepository -try: - import urllib.parse as urlparse -except ImportError: - import urlparse +if TYPE_CHECKING: + from poetry.core.packages.dependency import Dependency try: from html import unescape @@ -73,7 +78,7 @@ class Page: ".tar", ] - def __init__(self, url, content, headers): + def __init__(self, url: str, content: str, headers: Dict[str, Any]) -> None: if not url.endswith("/"): url += "/" @@ -95,7 +100,7 @@ def __init__(self, url, content, headers): ) @property - def versions(self): # type: () -> Generator[Version] + def versions(self) -> Iterator[Version]: seen = set() for link in self.links: version = self.link_version(link) @@ -111,11 +116,11 @@ def versions(self): # type: () -> Generator[Version] yield version @property - def links(self): # type: () -> Generator[Link] + def links(self) -> Iterator[Link]: for anchor in self._parsed.findall(".//a"): if anchor.get("href"): href = anchor.get("href") - url = self.clean_link(urlparse.urljoin(self._url, href)) + url = self.clean_link(urllib.parse.urljoin(self._url, href)) pyrequire = anchor.get("data-requires-python") pyrequire = unescape(pyrequire) if pyrequire else None @@ -126,12 +131,12 @@ def links(self): # type: () -> Generator[Link] yield link - def links_for_version(self, version): # type: (Version) -> Generator[Link] + def links_for_version(self, version: Version) -> Iterator[Link]: for link in self.links: if self.link_version(link) == version: yield link - def link_version(self, link): # type: (Link) -> Union[Version, None] + def link_version(self, link: Link) -> Optional[Version]: m = wheel_file_re.match(link.filename) if m: version = m.group("ver") @@ -152,7 +157,7 @@ def link_version(self, link): # type: (Link) -> Union[Version, None] _clean_re = re.compile(r"[^a-z0-9$&+,/:;=?@.#%_\\|-]", re.I) - def clean_link(self, url): + def clean_link(self, url: str) -> str: """Makes sure a link is fully encoded. That is, if a ' ' shows up in the link, it will be rewritten to %20 (while not over-quoting % or other characters).""" @@ -161,8 +166,14 @@ def clean_link(self, url): class LegacyRepository(PyPiRepository): def __init__( - self, name, url, config=None, disable_cache=False, cert=None, client_cert=None - ): # type: (str, str, Optional[Config], bool, Optional[Path], Optional[Path]) -> None + self, + name: str, + url: str, + config: Optional[Config] = None, + disable_cache: bool = False, + cert: Optional[Path] = None, + client_cert: Optional[Path] = None, + ) -> None: if name == "pypi": raise ValueError("The name [pypi] is reserved for repositories") @@ -207,19 +218,19 @@ def __init__( self._disable_cache = disable_cache @property - def cert(self): # type: () -> Optional[Path] + def cert(self) -> Optional[Path]: return self._cert @property - def client_cert(self): # type: () -> Optional[Path] + def client_cert(self) -> Optional[Path]: return self._client_cert @property - def authenticated_url(self): # type: () -> str + def authenticated_url(self) -> str: if not self._session.auth: return self.url - parsed = urlparse.urlparse(self.url) + parsed = urllib.parse.urlparse(self.url) return "{scheme}://{username}:{password}@{netloc}{path}".format( scheme=parsed.scheme, @@ -229,7 +240,7 @@ def authenticated_url(self): # type: () -> str path=parsed.path, ) - def find_packages(self, dependency): + def find_packages(self, dependency: "Dependency") -> List[Package]: packages = [] constraint = dependency.constraint @@ -243,9 +254,9 @@ def find_packages(self, dependency): if isinstance(constraint, VersionRange): if ( constraint.max is not None - and constraint.max.is_prerelease() + and constraint.max.is_unstable() or constraint.min is not None - and constraint.min.is_prerelease() + and constraint.min.is_unstable() ): allow_prereleases = True @@ -264,7 +275,7 @@ def find_packages(self, dependency): versions = [] for version in page.versions: - if version.is_prerelease() and not allow_prereleases: + if version.is_unstable() and not allow_prereleases: if constraint.is_any(): # we need this when all versions of the package are pre-releases ignored_pre_release_versions.append(version) @@ -300,7 +311,9 @@ def find_packages(self, dependency): return packages - def package(self, name, version, extras=None): # type: (...) -> Package + def package( + self, name: str, version: str, extras: Optional[List[str]] = None + ) -> Package: """ Retrieve the release information. @@ -317,24 +330,24 @@ def package(self, name, version, extras=None): # type: (...) -> Package return self._packages[index] except ValueError: - package = super(LegacyRepository, self).package(name, version, extras) + package = super().package(name, version, extras) package._source_type = "legacy" package._source_url = self._url package._source_reference = self.name return package - def find_links_for_package(self, package): + def find_links_for_package(self, package: Package) -> List[Link]: page = self._get("/{}/".format(package.name.replace(".", "-"))) if page is None: return [] return list(page.links_for_version(package.version)) - def _get_release_info(self, name, version): # type: (str, str) -> dict + def _get_release_info(self, name: str, version: str) -> dict: page = self._get("/{}/".format(canonicalize_name(name).replace(".", "-"))) if page is None: - raise PackageNotFound('No package named "{}"'.format(name)) + raise PackageNotFound(f'No package named "{name}"') data = PackageInfo( name=name, @@ -364,10 +377,37 @@ def _get_release_info(self, name, version): # type: (str, str) -> dict ): urls["sdist"].append(link.url) - h = link.hash - if h: - h = link.hash_name + ":" + link.hash - files.append({"file": link.filename, "hash": h}) + file_hash = f"{link.hash_name}:{link.hash}" if link.hash else None + + if not link.hash or ( + link.hash_name not in ("sha256", "sha384", "sha512") + and hasattr(hashlib, link.hash_name) + ): + with temporary_directory() as temp_dir: + filepath = Path(temp_dir) / link.filename + self._download(link.url, str(filepath)) + + known_hash = ( + getattr(hashlib, link.hash_name)() if link.hash_name else None + ) + required_hash = hashlib.sha256() + + chunksize = 4096 + with filepath.open("rb") as f: + while True: + chunk = f.read(chunksize) + if not chunk: + break + if known_hash: + known_hash.update(chunk) + required_hash.update(chunk) + + if not known_hash or known_hash.hexdigest() == link.hash: + file_hash = "{}:{}".format( + required_hash.name, required_hash.hexdigest() + ) + + files.append({"file": link.filename, "hash": file_hash}) data.files = files @@ -379,20 +419,31 @@ def _get_release_info(self, name, version): # type: (str, str) -> dict return data.asdict() - def _get(self, endpoint): # type: (str) -> Union[Page, None] + def _get(self, endpoint: str) -> Optional[Page]: url = self._url + endpoint try: response = self.session.get(url) + if response.status_code in (401, 403): + self._log( + f"Authorization error accessing {url}", + level="warning", + ) + return if response.status_code == 404: return response.raise_for_status() except requests.HTTPError as e: raise RepositoryError(e) - if response.status_code in (401, 403): + if response.url != url: self._log( - "Authorization error accessing {url}".format(url=url), level="warn" + "Response URL {response_url} differs from request URL {url}".format( + response_url=response.url, url=url + ), + level="debug", ) - return - return Page(url, response.content, response.headers) + return Page(response.url, response.content, response.headers) + + def _download(self, url, dest): # type: (str, str) -> None + return download_file(url, dest, session=self.session) diff --git a/poetry/repositories/pool.py b/poetry/repositories/pool.py old mode 100755 new mode 100644 index ac712831d3a..b57ba155ec2 --- a/poetry/repositories/pool.py +++ b/poetry/repositories/pool.py @@ -9,19 +9,23 @@ if TYPE_CHECKING: - from poetry.core.packages import Package + from poetry.core.packages.dependency import Dependency + from poetry.core.packages.package import Package class Pool(BaseRepository): def __init__( - self, repositories=None, ignore_repository_names=False - ): # type: (Optional[List[Repository]], bool) -> None + self, + repositories: Optional[List[Repository]] = None, + ignore_repository_names: bool = False, + ) -> None: if repositories is None: repositories = [] - self._lookup = {} # type: Dict[str, int] - self._repositories = [] # type: List[Repository] + self._lookup: Dict[str, int] = {} + self._repositories: List[Repository] = [] self._default = False + self._has_primary_repositories = False self._secondary_start_idx = None for repository in repositories: @@ -29,32 +33,35 @@ def __init__( self._ignore_repository_names = ignore_repository_names - super(Pool, self).__init__() + super().__init__() @property - def repositories(self): # type: () -> List[Repository] + def repositories(self) -> List[Repository]: return self._repositories - def has_default(self): # type: () -> bool + def has_default(self) -> bool: return self._default - def has_repository(self, name): # type: (str) -> bool + def has_primary_repositories(self) -> bool: + return self._has_primary_repositories + + def has_repository(self, name: str) -> bool: name = name.lower() if name is not None else None return name in self._lookup - def repository(self, name): # type: (str) -> Repository + def repository(self, name: str) -> Repository: if name is not None: name = name.lower() if name in self._lookup: return self._repositories[self._lookup[name]] - raise ValueError('Repository "{}" does not exist.'.format(name)) + raise ValueError(f'Repository "{name}" does not exist.') def add_repository( - self, repository, default=False, secondary=False - ): # type: (Repository, bool, bool) -> Pool + self, repository: Repository, default: bool = False, secondary: bool = False + ) -> "Pool": """ Adds a repository to the pool. """ @@ -81,6 +88,7 @@ def add_repository( self._repositories.append(repository) self._lookup[repository_name] = len(self._repositories) - 1 else: + self._has_primary_repositories = True if self._secondary_start_idx is None: self._repositories.append(repository) self._lookup[repository_name] = len(self._repositories) - 1 @@ -98,7 +106,7 @@ def add_repository( return self - def remove_repository(self, repository_name): # type: (str) -> Pool + def remove_repository(self, repository_name: str) -> "Pool": if repository_name is not None: repository_name = repository_name.lower() @@ -108,12 +116,12 @@ def remove_repository(self, repository_name): # type: (str) -> Pool return self - def has_package(self, package): + def has_package(self, package: "Package") -> bool: raise NotImplementedError() def package( - self, name, version, extras=None, repository=None - ): # type: (str, str, List[str], str) -> Package + self, name: str, version: str, extras: List[str] = None, repository: str = None + ) -> "Package": if repository is not None: repository = repository.lower() @@ -122,7 +130,7 @@ def package( and repository not in self._lookup and not self._ignore_repository_names ): - raise ValueError('Repository "{}" does not exist.'.format(repository)) + raise ValueError(f'Repository "{repository}" does not exist.') if repository is not None and not self._ignore_repository_names: try: @@ -141,11 +149,9 @@ def package( return package - raise PackageNotFound("Package {} ({}) not found.".format(name, version)) + raise PackageNotFound(f"Package {name} ({version}) not found.") - def find_packages( - self, dependency, - ): + def find_packages(self, dependency: "Dependency") -> List["Package"]: repository = dependency.source_name if repository is not None: repository = repository.lower() @@ -155,7 +161,7 @@ def find_packages( and repository not in self._lookup and not self._ignore_repository_names ): - raise ValueError('Repository "{}" does not exist.'.format(repository)) + raise ValueError(f'Repository "{repository}" does not exist.') if repository is not None and not self._ignore_repository_names: return self.repository(repository).find_packages(dependency) @@ -166,7 +172,7 @@ def find_packages( return packages - def search(self, query): + def search(self, query: str) -> List["Package"]: from .legacy_repository import LegacyRepository results = [] diff --git a/poetry/repositories/pypi_repository.py b/poetry/repositories/pypi_repository.py old mode 100755 new mode 100644 index 16105992a1c..1c7b7be316a --- a/poetry/repositories/pypi_repository.py +++ b/poetry/repositories/pypi_repository.py @@ -1,7 +1,10 @@ import logging import os +import urllib.parse from collections import defaultdict +from pathlib import Path +from typing import TYPE_CHECKING from typing import Dict from typing import List from typing import Union @@ -14,44 +17,44 @@ from cachy import CacheManager from html5lib.html5parser import parse -from poetry.core.packages import Dependency -from poetry.core.packages import Package -from poetry.core.packages import dependency_from_pep_508 +from poetry.core.packages.dependency import Dependency +from poetry.core.packages.package import Package from poetry.core.packages.utils.link import Link -from poetry.core.semver import VersionConstraint -from poetry.core.semver import VersionRange -from poetry.core.semver import parse_constraint -from poetry.core.semver.exceptions import ParseVersionError +from poetry.core.semver.helpers import parse_constraint +from poetry.core.semver.version_constraint import VersionConstraint +from poetry.core.semver.version_range import VersionRange +from poetry.core.version.exceptions import InvalidVersion from poetry.core.version.markers import parse_marker from poetry.locations import REPOSITORY_CACHE_DIR -from poetry.utils._compat import Path from poetry.utils._compat import to_str from poetry.utils.helpers import download_file from poetry.utils.helpers import temporary_directory from poetry.utils.patterns import wheel_file_re -from ..inspection.info import PackageInfo from .exceptions import PackageNotFound from .remote_repository import RemoteRepository -try: - import urllib.parse as urlparse -except ImportError: - import urlparse - - cache_control_logger.setLevel(logging.ERROR) logger = logging.getLogger(__name__) +if TYPE_CHECKING: + from poetry.inspection.info import PackageInfo + + class PyPiRepository(RemoteRepository): CACHE_VERSION = parse_constraint("1.0.0") - def __init__(self, url="https://pypi.org/", disable_cache=False, fallback=True): - super(PyPiRepository, self).__init__(url.rstrip("/") + "/simple/") + def __init__( + self, + url: str = "https://pypi.org/", + disable_cache: bool = False, + fallback: bool = True, + ) -> None: + super().__init__(url.rstrip("/") + "/simple/") self._base_url = url self._disable_cache = disable_cache @@ -77,10 +80,10 @@ def __init__(self, url="https://pypi.org/", disable_cache=False, fallback=True): self._name = "PyPI" @property - def session(self): + def session(self) -> CacheControl: return self._session - def find_packages(self, dependency): # type: (Dependency) -> List[Package] + def find_packages(self, dependency: Dependency) -> List[Package]: """ Find packages on the remote server. """ @@ -95,9 +98,9 @@ def find_packages(self, dependency): # type: (Dependency) -> List[Package] if isinstance(constraint, VersionRange): if ( constraint.max is not None - and constraint.max.is_prerelease() + and constraint.max.is_unstable() or constraint.min is not None - and constraint.min.is_prerelease() + and constraint.min.is_unstable() ): allow_prereleases = True @@ -126,7 +129,7 @@ def find_packages(self, dependency): # type: (Dependency) -> List[Package] try: package = Package(info["info"]["name"], version) - except ParseVersionError: + except InvalidVersion: self._log( 'Unable to parse version "{}" for the {} package, skipping'.format( version, dependency.name @@ -155,13 +158,13 @@ def find_packages(self, dependency): # type: (Dependency) -> List[Package] def package( self, - name, # type: str - version, # type: str - extras=None, # type: (Union[list, None]) - ): # type: (...) -> Package + name: str, + version: str, + extras: (Union[list, None]) = None, + ) -> Package: return self.get_release_info(name, version).to_package(name=name, extras=extras) - def search(self, query): + def search(self, query: str) -> List[Package]: results = [] search = {"q": query} @@ -183,7 +186,7 @@ def search(self, query): result = Package(name, version, description) result.description = to_str(description.strip()) results.append(result) - except ParseVersionError: + except InvalidVersion: self._log( 'Unable to parse version "{}" for the {} package, skipping'.format( version, name @@ -193,7 +196,7 @@ def search(self, query): return results - def get_package_info(self, name): # type: (str) -> dict + def get_package_info(self, name: str) -> dict: """ Return the package information given its name. @@ -207,42 +210,44 @@ def get_package_info(self, name): # type: (str) -> dict name, lambda: self._get_package_info(name) ) - def _get_package_info(self, name): # type: (str) -> dict - data = self._get("pypi/{}/json".format(name)) + def _get_package_info(self, name: str) -> dict: + data = self._get(f"pypi/{name}/json") if data is None: - raise PackageNotFound("Package [{}] not found.".format(name)) + raise PackageNotFound(f"Package [{name}] not found.") return data - def get_release_info(self, name, version): # type: (str, str) -> PackageInfo + def get_release_info(self, name: str, version: str) -> "PackageInfo": """ Return the release information given a package name and a version. The information is returned from the cache if it exists or retrieved from the remote server. """ + from poetry.inspection.info import PackageInfo + if self._disable_cache: return PackageInfo.load(self._get_release_info(name, version)) cached = self._cache.remember_forever( - "{}:{}".format(name, version), lambda: self._get_release_info(name, version) + f"{name}:{version}", lambda: self._get_release_info(name, version) ) cache_version = cached.get("_cache_version", "0.0.0") if parse_constraint(cache_version) != self.CACHE_VERSION: # The cache must be updated self._log( - "The cache for {} {} is outdated. Refreshing.".format(name, version), + f"The cache for {name} {version} is outdated. Refreshing.", level="debug", ) cached = self._get_release_info(name, version) - self._cache.forever("{}:{}".format(name, version), cached) + self._cache.forever(f"{name}:{version}", cached) return PackageInfo.load(cached) - def find_links_for_package(self, package): - json_data = self._get("pypi/{}/{}/json".format(package.name, package.version)) + def find_links_for_package(self, package: Package) -> List[Link]: + json_data = self._get(f"pypi/{package.name}/{package.version}/json") if json_data is None: return [] @@ -253,12 +258,14 @@ def find_links_for_package(self, package): return links - def _get_release_info(self, name, version): # type: (str, str) -> dict - self._log("Getting info for {} ({}) from PyPI".format(name, version), "debug") + def _get_release_info(self, name: str, version: str) -> dict: + from poetry.inspection.info import PackageInfo - json_data = self._get("pypi/{}/{}/json".format(name, version)) + self._log(f"Getting info for {name} ({version}) from PyPI", "debug") + + json_data = self._get(f"pypi/{name}/{version}/json") if json_data is None: - raise PackageNotFound("Package [{}] not found.".format(name)) + raise PackageNotFound(f"Package [{name}] not found.") info = json_data["info"] @@ -314,7 +321,7 @@ def _get_release_info(self, name, version): # type: (str, str) -> dict return data.asdict() - def _get(self, endpoint): # type: (str) -> Union[dict, None] + def _get(self, endpoint: str) -> Union[dict, None]: try: json_response = self.session.get(self._base_url + endpoint) except requests.exceptions.TooManyRedirects: @@ -330,7 +337,7 @@ def _get(self, endpoint): # type: (str) -> Union[dict, None] return json_data - def _get_info_from_urls(self, urls): # type: (Dict[str, List[str]]) -> PackageInfo + def _get_info_from_urls(self, urls: Dict[str, List[str]]) -> "PackageInfo": # Checking wheels first as they are more likely to hold # the necessary information if "bdist_wheel" in urls: @@ -376,14 +383,14 @@ def _get_info_from_urls(self, urls): # type: (Dict[str, List[str]]) -> PackageI return info - py2_requires_dist = set( - dependency_from_pep_508(r).to_pep_508() + py2_requires_dist = { + Dependency.create_from_pep_508(r).to_pep_508() for r in info.requires_dist - ) - py3_requires_dist = set( - dependency_from_pep_508(r).to_pep_508() + } + py3_requires_dist = { + Dependency.create_from_pep_508(r).to_pep_508() for r in py3_info.requires_dist - ) + } base_requires_dist = py2_requires_dist & py3_requires_dist py2_only_requires_dist = py2_requires_dist - py3_requires_dist py3_only_requires_dist = py3_requires_dist - py2_requires_dist @@ -391,14 +398,14 @@ def _get_info_from_urls(self, urls): # type: (Dict[str, List[str]]) -> PackageI # Normalizing requires_dist requires_dist = list(base_requires_dist) for requirement in py2_only_requires_dist: - dep = dependency_from_pep_508(requirement) + dep = Dependency.create_from_pep_508(requirement) dep.marker = dep.marker.intersect( parse_marker("python_version == '2.7'") ) requires_dist.append(dep.to_pep_508()) for requirement in py3_only_requires_dist: - dep = dependency_from_pep_508(requirement) + dep = Dependency.create_from_pep_508(requirement) dep.marker = dep.marker.intersect( parse_marker("python_version >= '3'") ) @@ -422,13 +429,17 @@ def _get_info_from_urls(self, urls): # type: (Dict[str, List[str]]) -> PackageI return self._get_info_from_sdist(urls["sdist"][0]) - def _get_info_from_wheel(self, url): # type: (str) -> PackageInfo + def _get_info_from_wheel(self, url: str) -> "PackageInfo": + from poetry.inspection.info import PackageInfo + self._log( - "Downloading wheel: {}".format(urlparse.urlparse(url).path.rsplit("/")[-1]), + "Downloading wheel: {}".format( + urllib.parse.urlparse(url).path.rsplit("/")[-1] + ), level="debug", ) - filename = os.path.basename(urlparse.urlparse(url).path.rsplit("/")[-1]) + filename = os.path.basename(urllib.parse.urlparse(url).path.rsplit("/")[-1]) with temporary_directory() as temp_dir: filepath = Path(temp_dir) / filename @@ -436,13 +447,17 @@ def _get_info_from_wheel(self, url): # type: (str) -> PackageInfo return PackageInfo.from_wheel(filepath) - def _get_info_from_sdist(self, url): # type: (str) -> PackageInfo + def _get_info_from_sdist(self, url: str) -> "PackageInfo": + from poetry.inspection.info import PackageInfo + self._log( - "Downloading sdist: {}".format(urlparse.urlparse(url).path.rsplit("/")[-1]), + "Downloading sdist: {}".format( + urllib.parse.urlparse(url).path.rsplit("/")[-1] + ), level="debug", ) - filename = os.path.basename(urlparse.urlparse(url).path) + filename = os.path.basename(urllib.parse.urlparse(url).path) with temporary_directory() as temp_dir: filepath = Path(temp_dir) / filename @@ -450,8 +465,8 @@ def _get_info_from_sdist(self, url): # type: (str) -> PackageInfo return PackageInfo.from_sdist(filepath) - def _download(self, url, dest): # type: (str, str) -> None + def _download(self, url: str, dest: str) -> None: return download_file(url, dest, session=self.session) - def _log(self, msg, level="info"): - getattr(logger, level)("{}: {}".format(self._name, msg)) + def _log(self, msg: str, level: str = "info") -> None: + getattr(logger, level)(f"{self._name}: {msg}") diff --git a/poetry/repositories/remote_repository.py b/poetry/repositories/remote_repository.py index 7717740d87c..b07321dcd66 100644 --- a/poetry/repositories/remote_repository.py +++ b/poetry/repositories/remote_repository.py @@ -2,15 +2,15 @@ class RemoteRepository(Repository): - def __init__(self, url): # type: (str) -> None + def __init__(self, url: str) -> None: self._url = url - super(RemoteRepository, self).__init__() + super().__init__() @property - def url(self): # type: () -> str + def url(self) -> str: return self._url @property - def authenticated_url(self): # type: () -> str + def authenticated_url(self) -> str: return self._url diff --git a/poetry/repositories/repository.py b/poetry/repositories/repository.py old mode 100755 new mode 100644 index 556c77e9862..c2a36c2dfe1 --- a/poetry/repositories/repository.py +++ b/poetry/repositories/repository.py @@ -1,13 +1,19 @@ -from poetry.core.semver import VersionConstraint -from poetry.core.semver import VersionRange -from poetry.core.semver import parse_constraint +from typing import TYPE_CHECKING +from typing import List +from typing import Optional from .base_repository import BaseRepository +if TYPE_CHECKING: + from poetry.core.packages.dependency import Dependency + from poetry.core.packages.package import Package + from poetry.core.packages.utils.link import Link + + class Repository(BaseRepository): - def __init__(self, packages=None, name=None): - super(Repository, self).__init__() + def __init__(self, packages: List["Package"] = None, name: str = None) -> None: + super().__init__() self._name = name @@ -18,22 +24,23 @@ def __init__(self, packages=None, name=None): self.add_package(package) @property - def name(self): + def name(self) -> str: return self._name - def package(self, name, version, extras=None): + def package( + self, name: str, version: str, extras: Optional[List[str]] = None + ) -> "Package": name = name.lower() - if extras is None: - extras = [] - for package in self.packages: if name == package.name and package.version.text == version: - package = package.with_features(extras) + return package.clone() - return package + def find_packages(self, dependency: "Dependency") -> List["Package"]: + from poetry.core.semver.helpers import parse_constraint + from poetry.core.semver.version_constraint import VersionConstraint + from poetry.core.semver.version_range import VersionRange - def find_packages(self, dependency): constraint = dependency.constraint packages = [] ignored_pre_release_packages = [] @@ -48,9 +55,9 @@ def find_packages(self, dependency): if isinstance(constraint, VersionRange): if ( constraint.max is not None - and constraint.max.is_prerelease() + and constraint.max.is_unstable() or constraint.min is not None - and constraint.min.is_prerelease() + and constraint.min.is_unstable() ): allow_prereleases = True @@ -70,13 +77,13 @@ def find_packages(self, dependency): if constraint.allows(package.version) or ( package.is_prerelease() - and constraint.allows(package.version.next_patch) + and constraint.allows(package.version.next_patch()) ): packages.append(package) return packages or ignored_pre_release_packages - def has_package(self, package): + def has_package(self, package: "Package") -> bool: package_id = package.unique_name for repo_package in self.packages: @@ -85,10 +92,10 @@ def has_package(self, package): return False - def add_package(self, package): + def add_package(self, package: "Package") -> None: self._packages.append(package) - def remove_package(self, package): + def remove_package(self, package: "Package") -> None: package_id = package.unique_name index = None @@ -100,10 +107,10 @@ def remove_package(self, package): if index is not None: del self._packages[index] - def find_links_for_package(self, package): + def find_links_for_package(self, package: "Package") -> List["Link"]: return [] - def search(self, query): + def search(self, query: str) -> List["Package"]: results = [] for package in self.packages: @@ -112,5 +119,5 @@ def search(self, query): return results - def __len__(self): + def __len__(self) -> int: return len(self._packages) diff --git a/poetry/utils/_compat.py b/poetry/utils/_compat.py index 937f9b300e6..977c997ede3 100644 --- a/poetry/utils/_compat.py +++ b/poetry/utils/_compat.py @@ -2,236 +2,16 @@ try: - from functools32 import lru_cache -except ImportError: - from functools import lru_cache - -try: - from glob2 import glob -except ImportError: - from glob import glob - -try: - import zipfile as zipp - from importlib import metadata except ImportError: - import importlib_metadata as metadata - import zipp - -try: - import urllib.parse as urlparse -except ImportError: - import urlparse - -try: - from os import cpu_count -except ImportError: # Python 2 - from multiprocessing import cpu_count - -try: # Python 2 - long = long - unicode = unicode - basestring = basestring -except NameError: # Python 3 - long = int - unicode = str - basestring = str - - -PY2 = sys.version_info[0] == 2 -PY34 = sys.version_info >= (3, 4) -PY35 = sys.version_info >= (3, 5) -PY36 = sys.version_info >= (3, 6) + # compatibility for python <3.8 + import importlib_metadata as metadata # noqa WINDOWS = sys.platform == "win32" -try: - from shlex import quote -except ImportError: - # PY2 - from pipes import quote # noqa - -if PY34: - from importlib.machinery import EXTENSION_SUFFIXES -else: - from imp import get_suffixes - - EXTENSION_SUFFIXES = [suffix[0] for suffix in get_suffixes()] - - -if PY35: - from pathlib import Path -else: - from pathlib2 import Path - -if not PY36: - from collections import OrderedDict -else: - OrderedDict = dict - - -if PY35: - import subprocess as subprocess - - from subprocess import CalledProcessError -else: - import subprocess32 as subprocess - - from subprocess32 import CalledProcessError - - -if PY34: - # subprocess32 pass the calls directly to subprocess - # on Python 3.3+ but Python 3.4 does not provide run() - # so we backport it - import signal - - from subprocess import PIPE - from subprocess import Popen - from subprocess import SubprocessError - from subprocess import TimeoutExpired - - class CalledProcessError(SubprocessError): - """Raised when run() is called with check=True and the process - returns a non-zero exit status. - - Attributes: - cmd, returncode, stdout, stderr, output - """ - - def __init__(self, returncode, cmd, output=None, stderr=None): - self.returncode = returncode - self.cmd = cmd - self.output = output - self.stderr = stderr - - def __str__(self): - if self.returncode and self.returncode < 0: - try: - return "Command '%s' died with %r." % ( - self.cmd, - signal.Signals(-self.returncode), - ) - except ValueError: - return "Command '%s' died with unknown signal %d." % ( - self.cmd, - -self.returncode, - ) - else: - return "Command '%s' returned non-zero exit status %d." % ( - self.cmd, - self.returncode, - ) - - @property - def stdout(self): - """Alias for output attribute, to match stderr""" - return self.output - - @stdout.setter - def stdout(self, value): - # There's no obvious reason to set this, but allow it anyway so - # .stdout is a transparent alias for .output - self.output = value - - class CompletedProcess(object): - """A process that has finished running. - This is returned by run(). - Attributes: - args: The list or str args passed to run(). - returncode: The exit code of the process, negative for signals. - stdout: The standard output (None if not captured). - stderr: The standard error (None if not captured). - """ - - def __init__(self, args, returncode, stdout=None, stderr=None): - self.args = args - self.returncode = returncode - self.stdout = stdout - self.stderr = stderr - - def __repr__(self): - args = [ - "args={!r}".format(self.args), - "returncode={!r}".format(self.returncode), - ] - if self.stdout is not None: - args.append("stdout={!r}".format(self.stdout)) - if self.stderr is not None: - args.append("stderr={!r}".format(self.stderr)) - return "{}({})".format(type(self).__name__, ", ".join(args)) - - def check_returncode(self): - """Raise CalledProcessError if the exit code is non-zero.""" - if self.returncode: - raise CalledProcessError( - self.returncode, self.args, self.stdout, self.stderr - ) - - def run(*popenargs, **kwargs): - """Run command with arguments and return a CompletedProcess instance. - The returned instance will have attributes args, returncode, stdout and - stderr. By default, stdout and stderr are not captured, and those attributes - will be None. Pass stdout=PIPE and/or stderr=PIPE in order to capture them. - If check is True and the exit code was non-zero, it raises a - CalledProcessError. The CalledProcessError object will have the return code - in the returncode attribute, and output & stderr attributes if those streams - were captured. - If timeout is given, and the process takes too long, a TimeoutExpired - exception will be raised. - There is an optional argument "input", allowing you to - pass a string to the subprocess's stdin. If you use this argument - you may not also use the Popen constructor's "stdin" argument, as - it will be used internally. - The other arguments are the same as for the Popen constructor. - If universal_newlines=True is passed, the "input" argument must be a - string and stdout/stderr in the returned object will be strings rather than - bytes. - """ - input = kwargs.pop("input", None) - timeout = kwargs.pop("timeout", None) - check = kwargs.pop("check", False) - if input is not None: - if "stdin" in kwargs: - raise ValueError("stdin and input arguments may not both be used.") - kwargs["stdin"] = PIPE - - process = Popen(*popenargs, **kwargs) - try: - process.__enter__() # No-Op really... illustrate "with in 2.4" - try: - stdout, stderr = process.communicate(input, timeout=timeout) - except TimeoutExpired: - process.kill() - stdout, stderr = process.communicate() - raise TimeoutExpired( - process.args, timeout, output=stdout, stderr=stderr - ) - except: - process.kill() - process.wait() - raise - retcode = process.poll() - if check and retcode: - raise CalledProcessError( - retcode, process.args, output=stdout, stderr=stderr - ) - finally: - # None because our context manager __exit__ does not use them. - process.__exit__(None, None, None) - - return CompletedProcess(process.args, retcode, stdout, stderr) - - subprocess.run = run - subprocess.CalledProcessError = CalledProcessError - def decode(string, encodings=None): - if not PY2 and not isinstance(string, bytes): - return string - - if PY2 and isinstance(string, unicode): + if not isinstance(string, bytes): return string encodings = encodings or ["utf-8", "latin1", "ascii"] @@ -246,10 +26,7 @@ def decode(string, encodings=None): def encode(string, encodings=None): - if not PY2 and isinstance(string, bytes): - return string - - if PY2 and isinstance(string, str): + if isinstance(string, bytes): return string encodings = encodings or ["utf-8", "latin1", "ascii"] @@ -264,27 +41,11 @@ def encode(string, encodings=None): def to_str(string): - if isinstance(string, str) or not isinstance(string, (unicode, bytes)): - return string - - if PY2: - method = "encode" - else: - method = "decode" - - encodings = ["utf-8", "latin1", "ascii"] - - for encoding in encodings: - try: - return getattr(string, method)(encoding) - except (UnicodeEncodeError, UnicodeDecodeError): - pass - - return getattr(string, method)(encodings[0], errors="ignore") + return decode(string) def list_to_shell_command(cmd): return " ".join( - '"{}"'.format(token) if " " in token and token[0] not in {"'", '"'} else token + f'"{token}"' if " " in token and token[0] not in {"'", '"'} else token for token in cmd ) diff --git a/poetry/utils/appdirs.py b/poetry/utils/appdirs.py index 5b9da0cdde3..f5592f6b0da 100644 --- a/poetry/utils/appdirs.py +++ b/poetry/utils/appdirs.py @@ -5,11 +5,19 @@ import os import sys +from typing import TYPE_CHECKING +from typing import List +from typing import Union + + +if TYPE_CHECKING: + from pathlib import Path + WINDOWS = sys.platform.startswith("win") or (sys.platform == "cli" and os.name == "nt") -def expanduser(path): +def expanduser(path: Union[str, "Path"]) -> str: """ Expand ~ and ~user constructions. @@ -21,7 +29,7 @@ def expanduser(path): return expanded -def user_cache_dir(appname): +def user_cache_dir(appname: str) -> str: r""" Return full path to the user-specific cache dir for this application. @@ -64,7 +72,7 @@ def user_cache_dir(appname): return path -def user_data_dir(appname, roaming=False): +def user_data_dir(appname: str, roaming: bool = False) -> str: r""" Return full path to the user-specific data dir for this application. @@ -92,7 +100,7 @@ def user_data_dir(appname, roaming=False): That means, by default "~/.local/share/". """ if WINDOWS: - const = roaming and "CSIDL_APPDATA" or "CSIDL_LOCAL_APPDATA" + const = "CSIDL_APPDATA" if roaming else "CSIDL_LOCAL_APPDATA" path = os.path.join(os.path.normpath(_get_win_folder(const)), appname) elif sys.platform == "darwin": path = os.path.join(expanduser("~/Library/Application Support/"), appname) @@ -104,7 +112,7 @@ def user_data_dir(appname, roaming=False): return path -def user_config_dir(appname, roaming=True): +def user_config_dir(appname: str, roaming: bool = True) -> str: """Return full path to the user-specific config dir for this application. "appname" is the name of application. @@ -137,7 +145,7 @@ def user_config_dir(appname, roaming=True): # for the discussion regarding site_config_dirs locations # see -def site_config_dirs(appname): +def site_config_dirs(appname: str) -> List[str]: r"""Return a list of potential user-shared config dirs for this application. "appname" is the name of application. @@ -178,7 +186,7 @@ def site_config_dirs(appname): # -- Windows support functions -- -def _get_win_folder_from_registry(csidl_name): +def _get_win_folder_from_registry(csidl_name: str) -> str: """ This is a fallback technique at best. I'm not sure if using the registry for this guarantees us the correct answer for all CSIDL_* @@ -200,7 +208,7 @@ def _get_win_folder_from_registry(csidl_name): return directory -def _get_win_folder_with_ctypes(csidl_name): +def _get_win_folder_with_ctypes(csidl_name: str) -> str: csidl_const = { "CSIDL_APPDATA": 26, "CSIDL_COMMON_APPDATA": 35, @@ -234,7 +242,7 @@ def _get_win_folder_with_ctypes(csidl_name): _get_win_folder = _get_win_folder_from_registry -def _win_path_to_bytes(path): +def _win_path_to_bytes(path: str) -> Union[str, bytes]: """Encode Windows paths to bytes. Only used on Python 2. Motivation is to be consistent with other operating systems where paths diff --git a/poetry/utils/env.py b/poetry/utils/env.py index 0a027bd668a..d35fb221313 100644 --- a/poetry/utils/env.py +++ b/poetry/utils/env.py @@ -1,17 +1,25 @@ import base64 import hashlib +import itertools import json import os import platform import re import shutil +import subprocess import sys import sysconfig import textwrap from contextlib import contextmanager +from copy import deepcopy +from pathlib import Path +from subprocess import CalledProcessError from typing import Any +from typing import ContextManager from typing import Dict +from typing import Iterable +from typing import Iterator from typing import List from typing import Optional from typing import Tuple @@ -21,24 +29,26 @@ import tomlkit import virtualenv -from clikit.api.io import IO +from cleo.io.io import IO from packaging.tags import Tag from packaging.tags import interpreter_name from packaging.tags import interpreter_version from packaging.tags import sys_tags +from virtualenv.seed.wheels.embed import get_embed_wheel -from poetry.core.semver import parse_constraint +from poetry.core.semver.helpers import parse_constraint from poetry.core.semver.version import Version from poetry.core.toml.file import TOMLFile from poetry.core.version.markers import BaseMarker from poetry.locations import CACHE_DIR from poetry.poetry import Poetry -from poetry.utils._compat import CalledProcessError -from poetry.utils._compat import Path from poetry.utils._compat import decode from poetry.utils._compat import encode from poetry.utils._compat import list_to_shell_command -from poetry.utils._compat import subprocess +from poetry.utils._compat import metadata +from poetry.utils.helpers import is_dir_writable +from poetry.utils.helpers import paths_csv +from poetry.utils.helpers import temporary_directory GET_ENVIRONMENT_INFO = """\ @@ -143,25 +153,250 @@ def _version_nodot(version): """ +class SitePackages: + def __init__( + self, + purelib: Path, + platlib: Optional[Path] = None, + fallbacks: List[Path] = None, + skip_write_checks: bool = False, + ) -> None: + self._purelib = purelib + self._platlib = platlib or purelib + + if platlib and platlib.resolve() == purelib.resolve(): + self._platlib = purelib + + self._fallbacks = fallbacks or [] + self._skip_write_checks = skip_write_checks + + self._candidates: List[Path] = [] + for path in itertools.chain([self._purelib, self._platlib], self._fallbacks): + if path not in self._candidates: + self._candidates.append(path) + + self._writable_candidates = None if not skip_write_checks else self._candidates + + @property + def path(self) -> Path: + return self._purelib + + @property + def purelib(self) -> Path: + return self._purelib + + @property + def platlib(self) -> Path: + return self._platlib + + @property + def candidates(self) -> List[Path]: + return self._candidates + + @property + def writable_candidates(self) -> List[Path]: + if self._writable_candidates is not None: + return self._writable_candidates + + self._writable_candidates = [] + for candidate in self._candidates: + if not is_dir_writable(path=candidate, create=True): + continue + self._writable_candidates.append(candidate) + + return self._writable_candidates + + def make_candidates( + self, path: Path, writable_only: bool = False, strict: bool = False + ) -> List[Path]: + candidates = self._candidates if not writable_only else self.writable_candidates + if path.is_absolute(): + for candidate in candidates: + try: + path.relative_to(candidate) + return [path] + except ValueError: + pass + else: + raise ValueError( + "{} is not relative to any discovered {}sites".format( + path, "writable " if writable_only else "" + ) + ) + + results = [candidate / path for candidate in candidates if candidate] + + if not results and strict: + raise RuntimeError( + 'Unable to find a suitable destination for "{}" in {}'.format( + str(path), paths_csv(self._candidates) + ) + ) + + return results + + def distributions( + self, name: Optional[str] = None, writable_only: bool = False + ) -> Iterable[metadata.PathDistribution]: + path = list( + map( + str, self._candidates if not writable_only else self.writable_candidates + ) + ) + for distribution in metadata.PathDistribution.discover( + name=name, path=path + ): # type: metadata.PathDistribution + yield distribution + + def find_distribution( + self, name: str, writable_only: bool = False + ) -> Optional[metadata.PathDistribution]: + for distribution in self.distributions(name=name, writable_only=writable_only): + return distribution + else: + return None + + def find_distribution_files_with_suffix( + self, distribution_name: str, suffix: str, writable_only: bool = False + ) -> Iterable[Path]: + for distribution in self.distributions( + name=distribution_name, writable_only=writable_only + ): + for file in distribution.files: + if file.name.endswith(suffix): + yield Path(distribution.locate_file(file)) + + def find_distribution_files_with_name( + self, distribution_name: str, name: str, writable_only: bool = False + ) -> Iterable[Path]: + for distribution in self.distributions( + name=distribution_name, writable_only=writable_only + ): + for file in distribution.files: + if file.name == name: + yield Path(distribution.locate_file(file)) + + def find_distribution_nspkg_pth_files( + self, distribution_name: str, writable_only: bool = False + ) -> Iterable[Path]: + return self.find_distribution_files_with_suffix( + distribution_name=distribution_name, + suffix="-nspkg.pth", + writable_only=writable_only, + ) + + def find_distribution_direct_url_json_files( + self, distribution_name: str, writable_only: bool = False + ) -> Iterable[Path]: + return self.find_distribution_files_with_name( + distribution_name=distribution_name, + name="direct_url.json", + writable_only=writable_only, + ) + + def remove_distribution_files(self, distribution_name: str) -> List[Path]: + paths = [] + + for distribution in self.distributions( + name=distribution_name, writable_only=True + ): + for file in distribution.files: + file = Path(distribution.locate_file(file)) + # We can't use unlink(missing_ok=True) because it's not always available + if file.exists(): + file.unlink() + + if distribution._path.exists(): + shutil.rmtree(str(distribution._path)) + + paths.append(distribution._path) + + return paths + + def _path_method_wrapper( + self, + path: Union[str, Path], + method: str, + *args: Any, + return_first: bool = True, + writable_only: bool = False, + **kwargs: Any, + ) -> Union[Tuple[Path, Any], List[Tuple[Path, Any]]]: + if isinstance(path, str): + path = Path(path) + + candidates = self.make_candidates( + path, writable_only=writable_only, strict=True + ) + + results = [] + + for candidate in candidates: + try: + result = candidate, getattr(candidate, method)(*args, **kwargs) + if return_first: + return result + results.append(result) + except OSError: + # TODO: Replace with PermissionError + pass + + if results: + return results + + raise OSError("Unable to access any of {}".format(paths_csv(candidates))) + + def write_text(self, path: Union[str, Path], *args: Any, **kwargs: Any) -> Path: + return self._path_method_wrapper(path, "write_text", *args, **kwargs)[0] + + def mkdir(self, path: Union[str, Path], *args: Any, **kwargs: Any) -> Path: + return self._path_method_wrapper(path, "mkdir", *args, **kwargs)[0] + + def exists(self, path: Union[str, Path]) -> bool: + return any( + value[-1] + for value in self._path_method_wrapper(path, "exists", return_first=False) + ) + + def find( + self, + path: Union[str, Path], + writable_only: bool = False, + ) -> List[Path]: + return [ + value[0] + for value in self._path_method_wrapper( + path, "exists", return_first=False, writable_only=writable_only + ) + if value[-1] is True + ] + + def __getattr__(self, item: str) -> Any: + try: + return super().__getattribute__(item) + except AttributeError: + return getattr(self.path, item) + + class EnvError(Exception): pass class EnvCommandError(EnvError): - def __init__(self, e, input=None): # type: (CalledProcessError) -> None + def __init__(self, e: CalledProcessError, input: Optional[str] = None) -> None: self.e = e message = "Command {} errored with the following return code {}, and output: \n{}".format( e.cmd, e.returncode, decode(e.output) ) if input: - message += "input was : {}".format(input) - super(EnvCommandError, self).__init__(message) + message += f"input was : {input}" + super().__init__(message) class NoCompatiblePythonVersionFound(EnvError): - def __init__(self, expected, given=None): + def __init__(self, expected: str, given: Optional[str] = None) -> None: if given: message = ( "The specified Python version ({}) " @@ -177,10 +412,10 @@ def __init__(self, expected, given=None): 'via the "env use" command.' ) - super(NoCompatiblePythonVersionFound, self).__init__(message) + super().__init__(message) -class EnvManager(object): +class EnvManager: """ Environments manager """ @@ -189,10 +424,10 @@ class EnvManager(object): ENVS_FILE = "envs.toml" - def __init__(self, poetry): # type: (Poetry) -> None + def __init__(self, poetry: Poetry) -> None: self._poetry = poetry - def activate(self, python, io): # type: (str, IO) -> Env + def activate(self, python: str, io: IO) -> "Env": venv_path = self._poetry.config.get("virtualenvs.path") if venv_path is None: venv_path = Path(CACHE_DIR) / "virtualenvs" @@ -205,9 +440,9 @@ def activate(self, python, io): # type: (str, IO) -> Env try: python_version = Version.parse(python) - python = "python{}".format(python_version.major) + python = f"python{python_version.major}" if python_version.precision > 1: - python += ".{}".format(python_version.minor) + python += f".{python_version.minor}" except ValueError: # Executable in PATH or full executable path pass @@ -229,7 +464,7 @@ def activate(self, python, io): # type: (str, IO) -> Env raise EnvCommandError(e) python_version = Version.parse(python_version.strip()) - minor = "{}.{}".format(python_version.major, python_version.minor) + minor = f"{python_version.major}.{python_version.minor}" patch = python_version.text create = False @@ -264,7 +499,7 @@ def activate(self, python, io): # type: (str, IO) -> Env # We need to recreate create = True - name = "{}-py{}".format(base_env_name, minor) + name = f"{base_env_name}-py{minor}" venv = venv_path / name # Create if needed @@ -289,7 +524,7 @@ def activate(self, python, io): # type: (str, IO) -> Env return self.get(reload=True) - def deactivate(self, io): # type: (IO) -> None + def deactivate(self, io: IO) -> None: venv_path = self._poetry.config.get("virtualenvs.path") if venv_path is None: venv_path = Path(CACHE_DIR) / "virtualenvs" @@ -313,7 +548,7 @@ def deactivate(self, io): # type: (IO) -> None envs_file.write(envs) - def get(self, reload=False): # type: (bool) -> Env + def get(self, reload: bool = False) -> Union["VirtualEnv", "SystemEnv"]: if self._env is not None and not reload: return self._env @@ -363,7 +598,7 @@ def get(self, reload=False): # type: (bool) -> Env else: venv_path = Path(venv_path) - name = "{}-py{}".format(base_env_name, python_minor.strip()) + name = f"{base_env_name}-py{python_minor.strip()}" venv = venv_path / name @@ -381,7 +616,7 @@ def get(self, reload=False): # type: (bool) -> Env return VirtualEnv(prefix, base_prefix) - def list(self, name=None): # type: (Optional[str]) -> List[VirtualEnv] + def list(self, name: Optional[str] = None) -> List["VirtualEnv"]: if name is None: name = self._poetry.package.name @@ -394,8 +629,7 @@ def list(self, name=None): # type: (Optional[str]) -> List[VirtualEnv] venv_path = Path(venv_path) env_list = [ - VirtualEnv(Path(p)) - for p in sorted(venv_path.glob("{}-py*".format(venv_name))) + VirtualEnv(Path(p)) for p in sorted(venv_path.glob(f"{venv_name}-py*")) ] venv = self._poetry.file.parent / ".venv" @@ -407,7 +641,7 @@ def list(self, name=None): # type: (Optional[str]) -> List[VirtualEnv] env_list.insert(0, VirtualEnv(venv)) return env_list - def remove(self, python): # type: (str) -> Env + def remove(self, python: str) -> "Env": venv_path = self._poetry.config.get("virtualenvs.path") if venv_path is None: venv_path = Path(CACHE_DIR) / "virtualenvs" @@ -447,14 +681,14 @@ def remove(self, python): # type: (str) -> Env return venv raise ValueError( - 'Environment "{}" does not exist.'.format(python) + f'Environment "{python}" does not exist.' ) try: python_version = Version.parse(python) - python = "python{}".format(python_version.major) + python = f"python{python_version.major}" if python_version.precision > 1: - python += ".{}".format(python_version.minor) + python += f".{python_version.minor}" except ValueError: # Executable in PATH or full executable path pass @@ -476,15 +710,13 @@ def remove(self, python): # type: (str) -> Env raise EnvCommandError(e) python_version = Version.parse(python_version.strip()) - minor = "{}.{}".format(python_version.major, python_version.minor) + minor = f"{python_version.major}.{python_version.minor}" - name = "{}-py{}".format(base_env_name, minor) + name = f"{base_env_name}-py{minor}" venv = venv_path / name if not venv.exists(): - raise ValueError( - 'Environment "{}" does not exist.'.format(name) - ) + raise ValueError(f'Environment "{name}" does not exist.') if envs_file.exists(): envs = envs_file.read() @@ -501,8 +733,12 @@ def remove(self, python): # type: (str) -> Env return VirtualEnv(venv) def create_venv( - self, io, name=None, executable=None, force=False - ): # type: (IO, Optional[str], Optional[str], bool) -> Env + self, + io: IO, + name: Optional[str] = None, + executable: Optional[str] = None, + force: bool = False, + ) -> Union["SystemEnv", "VirtualEnv"]: if self._env is not None and not force: return self._env @@ -518,8 +754,8 @@ def create_venv( create_venv = self._poetry.config.get("virtualenvs.create") root_venv = self._poetry.config.get("virtualenvs.in-project") - venv_path = self._poetry.config.get("virtualenvs.path") + if root_venv: venv_path = cwd / ".venv" elif venv_path is None: @@ -575,7 +811,7 @@ def create_venv( ) ): if len(python_to_try) == 1: - if not parse_constraint("^{}.0".format(python_to_try)).allows_any( + if not parse_constraint(f"^{python_to_try}.0").allows_any( supported_python ): continue @@ -587,7 +823,7 @@ def create_venv( python = "python" + python_to_try if io.is_debug(): - io.write_line("Trying {}".format(python)) + io.write_line(f"Trying {python}") try: python_patch = decode( @@ -610,7 +846,7 @@ def create_venv( continue if supported_python.allows(Version.parse(python_patch)): - io.write_line("Using {} ({})".format(python, python_patch)) + io.write_line(f"Using {python} ({python_patch})") executable = python python_minor = ".".join(python_patch.split(".")[:2]) break @@ -624,7 +860,7 @@ def create_venv( venv = venv_path else: name = self.generate_env_name(name, str(cwd)) - name = "{}-py{}".format(name, python_minor.strip()) + name = f"{name}-py{python_minor.strip()}" venv = venv_path / name if not venv.exists(): @@ -641,9 +877,8 @@ def create_venv( io.write_line( "Creating virtualenv {} in {}".format(name, str(venv_path)) ) - - self.build_venv(venv, executable=executable) else: + create_venv = False if force: if not env.is_sane(): io.write_line( @@ -655,9 +890,22 @@ def create_venv( "Recreating virtualenv {} in {}".format(name, str(venv)) ) self.remove_venv(venv) - self.build_venv(venv, executable=executable) + create_venv = True elif io.is_very_verbose(): - io.write_line("Virtualenv {} already exists.".format(name)) + io.write_line(f"Virtualenv {name} already exists.") + + if create_venv: + self.build_venv( + venv, + executable=executable, + flags=self._poetry.config.get("virtualenvs.options"), + # TODO: in a future version switch remove pip/setuptools/wheel + # poetry does not need them these exists today to not break developer + # environment assumptions + with_pip=True, + with_setuptools=True, + with_wheel=True, + ) # venv detection: # stdlib venv may symlink sys.executable, so we can't use realpath. @@ -673,28 +921,59 @@ def create_venv( p_venv = os.path.normcase(str(venv)) if any(p.startswith(p_venv) for p in paths): # Running properly in the virtualenv, don't need to do anything - return SystemEnv(Path(sys.prefix), self.get_base_prefix()) + return self.get_system_env() return VirtualEnv(venv) @classmethod def build_venv( - cls, path, executable=None - ): # type: (Union[Path,str], Optional[Union[str, Path]]) -> virtualenv.run.session.Session + cls, + path: Union[Path, str], + executable: Optional[Union[str, Path]] = None, + flags: Dict[str, bool] = None, + with_pip: Optional[bool] = None, + with_wheel: Optional[bool] = None, + with_setuptools: Optional[bool] = None, + ) -> virtualenv.run.session.Session: + flags = flags or {} + + flags["no-pip"] = ( + not with_pip if with_pip is not None else flags.pop("no-pip", True) + ) + + flags["no-setuptools"] = ( + not with_setuptools + if with_setuptools is not None + else flags.pop("no-setuptools", True) + ) + + # we want wheels to be enabled when pip is required and it has not been explicitly disabled + flags["no-wheel"] = ( + not with_wheel + if with_wheel is not None + else flags.pop("no-wheel", flags["no-pip"]) + ) + if isinstance(executable, Path): executable = executable.resolve().as_posix() - return virtualenv.cli_run( - [ - "--no-download", - "--no-periodic-update", - "--python", - executable or sys.executable, - str(path), - ] - ) + + args = [ + "--no-download", + "--no-periodic-update", + "--python", + executable or sys.executable, + ] + + for flag, value in flags.items(): + if value is True: + args.append(f"--{flag}") + + args.append(str(path)) + + return virtualenv.cli_run(args) @classmethod - def remove_venv(cls, path): # type: (Union[Path,str]) -> None + def remove_venv(cls, path: Union[Path, str]) -> None: if isinstance(path, str): path = Path(path) assert path.is_dir() @@ -716,35 +995,67 @@ def remove_venv(cls, path): # type: (Union[Path,str]) -> None elif file_path.is_dir(): shutil.rmtree(str(file_path)) - def get_base_prefix(self): # type: () -> Path + @classmethod + def get_system_env(cls, naive: bool = False) -> "SystemEnv": + """ + Retrieve the current Python environment. + + This can be the base Python environment or an activated virtual environment. + + This method also workaround the issue that the virtual environment + used by Poetry internally (when installed via the custom installer) + is incorrectly detected as the system environment. Note that this workaround + happens only when `naive` is False since there are times where we actually + want to retrieve Poetry's custom virtual environment + (e.g. plugin installation or self update). + """ + prefix, base_prefix = Path(sys.prefix), cls.get_base_prefix() + if naive is False: + from poetry.locations import data_dir + + try: + prefix.relative_to(data_dir()) + except ValueError: + pass + else: + prefix = base_prefix + + return SystemEnv(prefix, base_prefix) + + @classmethod + def get_base_prefix(cls) -> Path: if hasattr(sys, "real_prefix"): - return sys.real_prefix + return Path(sys.real_prefix) if hasattr(sys, "base_prefix"): - return sys.base_prefix + return Path(sys.base_prefix) - return sys.prefix + return Path(sys.prefix) @classmethod - def generate_env_name(cls, name, cwd): # type: (str, str) -> str + def generate_env_name(cls, name: str, cwd: str) -> str: name = name.lower() sanitized_name = re.sub(r'[ $`!*@"\\\r\n\t]', "_", name)[:42] h = hashlib.sha256(encode(cwd)).digest() h = base64.urlsafe_b64encode(h).decode()[:8] - return "{}-{}".format(sanitized_name, h) + return f"{sanitized_name}-{h}" -class Env(object): +class Env: """ An abstract Python environment. """ - def __init__(self, path, base=None): # type: (Path, Optional[Path]) -> None + def __init__(self, path: Path, base: Optional[Path] = None) -> None: self._is_windows = sys.platform == "win32" + self._is_mingw = sysconfig.get_platform() == "mingw" + if not self._is_windows or self._is_mingw: + bin_dir = "bin" + else: + bin_dir = "Scripts" self._path = path - bin_dir = "bin" if not self._is_windows else "Scripts" self._bin_dir = self._path / bin_dir self._base = base or path @@ -756,79 +1067,109 @@ def __init__(self, path, base=None): # type: (Path, Optional[Path]) -> None self._supported_tags = None self._purelib = None self._platlib = None + self._script_dirs = None + + self._embedded_pip_path = None @property - def path(self): # type: () -> Path + def path(self) -> Path: return self._path @property - def base(self): # type: () -> Path + def base(self) -> Path: return self._base @property - def version_info(self): # type: () -> Tuple[int] + def version_info(self) -> Tuple[int]: return tuple(self.marker_env["version_info"]) @property - def python_implementation(self): # type: () -> str + def python_implementation(self) -> str: return self.marker_env["platform_python_implementation"] @property - def python(self): # type: () -> str + def python(self) -> str: """ Path to current python executable """ return self._bin("python") @property - def marker_env(self): + def marker_env(self) -> Dict[str, Any]: if self._marker_env is None: self._marker_env = self.get_marker_env() return self._marker_env + def get_embedded_wheel(self, distribution): + return get_embed_wheel( + distribution, "{}.{}".format(self.version_info[0], self.version_info[1]) + ).path + + @property + def pip_embedded(self) -> str: + if self._embedded_pip_path is None: + self._embedded_pip_path = str(self.get_embedded_wheel("pip") / "pip") + return self._embedded_pip_path + @property - def pip(self): # type: () -> str + def pip(self) -> str: """ Path to current pip executable """ - return self._bin("pip") + # we do not use as_posix() here due to issues with windows pathlib2 implementation + path = self._bin("pip") + if not Path(path).exists(): + return str(self.pip_embedded) + return path @property - def platform(self): # type: () -> str + def platform(self) -> str: return sys.platform @property - def os(self): # type: () -> str + def os(self) -> str: return os.name @property - def pip_version(self): + def pip_version(self) -> Version: if self._pip_version is None: self._pip_version = self.get_pip_version() return self._pip_version @property - def site_packages(self): # type: () -> Path + def site_packages(self) -> SitePackages: if self._site_packages is None: - self._site_packages = self.purelib + # we disable write checks if no user site exist + fallbacks = [self.usersite] if self.usersite else [] + self._site_packages = SitePackages( + self.purelib, + self.platlib, + fallbacks, + skip_write_checks=False if fallbacks else True, + ) return self._site_packages @property - def usersite(self): # type: () -> Optional[Path] + def usersite(self) -> Optional[Path]: if "usersite" in self.paths: return Path(self.paths["usersite"]) @property - def purelib(self): # type: () -> Path + def userbase(self) -> Optional[Path]: + if "userbase" in self.paths: + return Path(self.paths["userbase"]) + + @property + def purelib(self) -> Path: if self._purelib is None: self._purelib = Path(self.paths["purelib"]) return self._purelib @property - def platlib(self): # type: () -> Path + def platlib(self) -> Path: if self._platlib is None: if "platlib" in self.paths: self._platlib = Path(self.paths["platlib"]) @@ -837,7 +1178,7 @@ def platlib(self): # type: () -> Path return self._platlib - def is_path_relative_to_lib(self, path): # type: (Path) -> bool + def is_path_relative_to_lib(self, path: Path) -> bool: for lib_path in [self.purelib, self.platlib]: try: path.relative_to(lib_path) @@ -848,79 +1189,90 @@ def is_path_relative_to_lib(self, path): # type: (Path) -> bool return False @property - def sys_path(self): # type: () -> List[str] + def sys_path(self) -> List[str]: raise NotImplementedError() @property - def paths(self): # type: () -> Dict[str, str] + def paths(self) -> Dict[str, str]: if self._paths is None: self._paths = self.get_paths() return self._paths @property - def supported_tags(self): # type: () -> List[Tag] + def supported_tags(self) -> List[Tag]: if self._supported_tags is None: self._supported_tags = self.get_supported_tags() return self._supported_tags @classmethod - def get_base_prefix(cls): # type: () -> Path + def get_base_prefix(cls) -> Path: if hasattr(sys, "real_prefix"): - return sys.real_prefix + return Path(sys.real_prefix) if hasattr(sys, "base_prefix"): - return sys.base_prefix + return Path(sys.base_prefix) - return sys.prefix + return Path(sys.prefix) - def get_version_info(self): # type: () -> Tuple[int] + def get_version_info(self) -> Tuple[int]: raise NotImplementedError() - def get_python_implementation(self): # type: () -> str + def get_python_implementation(self) -> str: raise NotImplementedError() - def get_marker_env(self): # type: () -> Dict[str, Any] + def get_marker_env(self) -> Dict[str, Any]: raise NotImplementedError() - def get_pip_command(self): # type: () -> List[str] + def get_pip_command(self, embedded: bool = False) -> List[str]: raise NotImplementedError() - def get_supported_tags(self): # type: () -> List[Tag] + def get_supported_tags(self) -> List[Tag]: raise NotImplementedError() - def get_pip_version(self): # type: () -> Version + def get_pip_version(self) -> Version: raise NotImplementedError() - def get_paths(self): # type: () -> Dict[str, str] + def get_paths(self) -> Dict[str, str]: raise NotImplementedError() - def is_valid_for_marker(self, marker): # type: (BaseMarker) -> bool + def is_valid_for_marker(self, marker: BaseMarker) -> bool: return marker.validate(self.marker_env) - def is_sane(self): # type: () -> bool + def is_sane(self) -> bool: """ Checks whether the current environment is sane or not. """ return True - def run(self, bin, *args, **kwargs): - bin = self._bin(bin) - cmd = [bin] + list(args) + def get_command_from_bin(self, bin: str) -> List[str]: + if bin == "pip": + # when pip is required we need to ensure that we fallback to + # embedded pip when pip is not available in the environment + return self.get_pip_command() + + return [self._bin(bin)] + + def run(self, bin: str, *args: str, **kwargs: Any) -> Union[str, int]: + cmd = self.get_command_from_bin(bin) + list(args) return self._run(cmd, **kwargs) - def run_pip(self, *args, **kwargs): - pip = self.get_pip_command() + def run_pip(self, *args: str, **kwargs: Any) -> Union[int, str]: + pip = self.get_pip_command(embedded=True) cmd = pip + list(args) return self._run(cmd, **kwargs) - def _run(self, cmd, **kwargs): + def run_python_script(self, content: str, **kwargs: Any) -> str: + return self.run("python", "-W", "ignore", "-", input_=content, **kwargs) + + def _run(self, cmd: List[str], **kwargs: Any) -> Union[int, str]: """ Run a command inside the Python environment. """ call = kwargs.pop("call", False) input_ = kwargs.pop("input_", None) + env = kwargs.pop("env", {k: v for k, v in os.environ.items()}) try: if self._is_windows: @@ -936,37 +1288,46 @@ def _run(self, cmd, **kwargs): stderr=subprocess.STDOUT, input=encode(input_), check=True, - **kwargs + **kwargs, ).stdout elif call: - return subprocess.call(cmd, stderr=subprocess.STDOUT, **kwargs) + return subprocess.call(cmd, stderr=subprocess.STDOUT, env=env, **kwargs) else: output = subprocess.check_output( - cmd, stderr=subprocess.STDOUT, **kwargs + cmd, stderr=subprocess.STDOUT, env=env, **kwargs ) except CalledProcessError as e: raise EnvCommandError(e, input=input_) return decode(output) - def execute(self, bin, *args, **kwargs): - bin = self._bin(bin) + def execute(self, bin: str, *args: str, **kwargs: Any) -> Optional[int]: + command = self.get_command_from_bin(bin) + list(args) + env = kwargs.pop("env", {k: v for k, v in os.environ.items()}) if not self._is_windows: - args = [bin] + list(args) - if "env" in kwargs: - return os.execvpe(bin, args, kwargs["env"]) - else: - return os.execvp(bin, args) + return os.execvpe(command[0], command, env=env) else: - exe = subprocess.Popen([bin] + list(args), **kwargs) + exe = subprocess.Popen([command[0]] + command[1:], env=env, **kwargs) exe.communicate() return exe.returncode - def is_venv(self): # type: () -> bool + def is_venv(self) -> bool: raise NotImplementedError() - def _bin(self, bin): # type: (str) -> str + @property + def script_dirs(self) -> List[Path]: + if self._script_dirs is None: + self._script_dirs = ( + [Path(self.paths["scripts"])] + if "scripts" in self.paths + else self._bin_dir + ) + if self.userbase: + self._script_dirs.append(self.userbase / self._script_dirs[0].name) + return self._script_dirs + + def _bin(self, bin: str) -> str: """ Return path to the given executable. """ @@ -989,11 +1350,11 @@ def _bin(self, bin): # type: (str) -> str return str(bin_path) - def __eq__(self, other): # type: (Env) -> bool + def __eq__(self, other: "Env") -> bool: return other.__class__ == self.__class__ and other.path == self.path - def __repr__(self): - return '{}("{}")'.format(self.__class__.__name__, self._path) + def __repr__(self) -> str: + return f'{self.__class__.__name__}("{self._path}")' class SystemEnv(Env): @@ -1002,21 +1363,25 @@ class SystemEnv(Env): """ @property - def sys_path(self): # type: () -> List[str] + def python(self) -> str: + return sys.executable + + @property + def sys_path(self) -> List[str]: return sys.path - def get_version_info(self): # type: () -> Tuple[int] + def get_version_info(self) -> Tuple[int]: return sys.version_info - def get_python_implementation(self): # type: () -> str + def get_python_implementation(self) -> str: return platform.python_implementation() - def get_pip_command(self): # type: () -> List[str] + def get_pip_command(self, embedded: bool = False) -> List[str]: # If we're not in a venv, assume the interpreter we're running on # has a pip and use that - return [sys.executable, "-m", "pip"] + return [sys.executable, self.pip_embedded if embedded else self.pip] - def get_paths(self): # type: () -> Dict[str, str] + def get_paths(self) -> Dict[str, str]: # We can't use sysconfig.get_paths() because # on some distributions it does not return the proper paths # (those used by pip for instance). We go through distutils @@ -1037,17 +1402,18 @@ def get_paths(self): # type: () -> Dict[str, str] # headers is not a path returned by sysconfig.get_paths() continue - paths[key] = getattr(obj, "install_{}".format(key)) + paths[key] = getattr(obj, f"install_{key}") if site.check_enableusersite() and hasattr(obj, "install_usersite"): paths["usersite"] = getattr(obj, "install_usersite") + paths["userbase"] = getattr(obj, "install_userbase") return paths - def get_supported_tags(self): # type: () -> List[Tag] + def get_supported_tags(self) -> List[Tag]: return list(sys_tags()) - def get_marker_env(self): # type: () -> Dict[str, Any] + def get_marker_env(self) -> Dict[str, Any]: if hasattr(sys, "implementation"): info = sys.implementation.version iver = "{0.major}.{0.minor}.{0.micro}".format(info) @@ -1080,12 +1446,12 @@ def get_marker_env(self): # type: () -> Dict[str, Any] "interpreter_version": interpreter_version(), } - def get_pip_version(self): # type: () -> Version + def get_pip_version(self) -> Version: from pip import __version__ return Version.parse(__version__) - def is_venv(self): # type: () -> bool + def is_venv(self) -> bool: return self._path != self._base @@ -1094,36 +1460,35 @@ class VirtualEnv(Env): A virtual Python environment. """ - def __init__(self, path, base=None): # type: (Path, Optional[Path]) -> None - super(VirtualEnv, self).__init__(path, base) + def __init__(self, path: Path, base: Optional[Path] = None) -> None: + super().__init__(path, base) # If base is None, it probably means this is # a virtualenv created from VIRTUAL_ENV. # In this case we need to get sys.base_prefix # from inside the virtualenv. if base is None: - self._base = Path(self.run("python", "-", input_=GET_BASE_PREFIX).strip()) + self._base = Path(self.run_python_script(GET_BASE_PREFIX).strip()) @property - def sys_path(self): # type: () -> List[str] - output = self.run("python", "-", input_=GET_SYS_PATH) - + def sys_path(self) -> List[str]: + output = self.run_python_script(GET_SYS_PATH) return json.loads(output) - def get_version_info(self): # type: () -> Tuple[int] - output = self.run("python", "-", input_=GET_PYTHON_VERSION) + def get_version_info(self) -> Tuple[int]: + output = self.run_python_script(GET_PYTHON_VERSION) return tuple([int(s) for s in output.strip().split(".")]) - def get_python_implementation(self): # type: () -> str + def get_python_implementation(self) -> str: return self.marker_env["platform_python_implementation"] - def get_pip_command(self): # type: () -> List[str] + def get_pip_command(self, embedded: bool = False) -> List[str]: # We're in a virtualenv that is known to be sane, # so assume that we have a functional pip - return [self._bin("pip")] + return [self._bin("python"), self.pip_embedded if embedded else self.pip] - def get_supported_tags(self): # type: () -> List[Tag] + def get_supported_tags(self) -> List[Tag]: file_path = Path(packaging.tags.__file__) if file_path.suffix == ".pyc": # Python 2 @@ -1149,16 +1514,16 @@ def get_supported_tags(self): # type: () -> List[Tag] """ ) - output = self.run("python", "-", input_=script) + output = self.run_python_script(script) return [Tag(*t) for t in json.loads(output)] - def get_marker_env(self): # type: () -> Dict[str, Any] - output = self.run("python", "-", input_=GET_ENVIRONMENT_INFO) + def get_marker_env(self) -> Dict[str, Any]: + output = self.run_python_script(GET_ENVIRONMENT_INFO) return json.loads(output) - def get_pip_version(self): # type: () -> Version + def get_pip_version(self) -> Version: output = self.run_pip("--version").strip() m = re.match("pip (.+?)(?: from .+)?$", output) if not m: @@ -1166,40 +1531,50 @@ def get_pip_version(self): # type: () -> Version return Version.parse(m.group(1)) - def get_paths(self): # type: () -> Dict[str, str] - output = self.run("python", "-", input_=GET_PATHS) - + def get_paths(self) -> Dict[str, str]: + output = self.run_python_script(GET_PATHS) return json.loads(output) - def is_venv(self): # type: () -> bool + def is_venv(self) -> bool: return True - def is_sane(self): - # A virtualenv is considered sane if both "python" and "pip" exist. - return os.path.exists(self._bin("python")) and os.path.exists(self._bin("pip")) + def is_sane(self) -> bool: + # A virtualenv is considered sane if "python" exists. + return os.path.exists(self.python) - def _run(self, cmd, **kwargs): - with self.temp_environ(): - os.environ["PATH"] = self._updated_path() - os.environ["VIRTUAL_ENV"] = str(self._path) + def _run(self, cmd: List[str], **kwargs: Any) -> Optional[int]: + kwargs["env"] = self.get_temp_environ(environ=kwargs.get("env")) + return super()._run(cmd, **kwargs) - self.unset_env("PYTHONHOME") - self.unset_env("__PYVENV_LAUNCHER__") + def get_temp_environ( + self, + environ: Optional[Dict[str, str]] = None, + exclude: Optional[List[str]] = None, + **kwargs: str, + ) -> Dict[str, str]: + exclude = exclude or [] + exclude.extend(["PYTHONHOME", "__PYVENV_LAUNCHER__"]) + + if environ: + environ = deepcopy(environ) + for key in exclude: + environ.pop(key, None) + else: + environ = {k: v for k, v in os.environ.items() if k not in exclude} - return super(VirtualEnv, self)._run(cmd, **kwargs) + environ.update(kwargs) - def execute(self, bin, *args, **kwargs): - with self.temp_environ(): - os.environ["PATH"] = self._updated_path() - os.environ["VIRTUAL_ENV"] = str(self._path) + environ["PATH"] = self._updated_path() + environ["VIRTUAL_ENV"] = str(self._path) - self.unset_env("PYTHONHOME") - self.unset_env("__PYVENV_LAUNCHER__") + return environ - return super(VirtualEnv, self).execute(bin, *args, **kwargs) + def execute(self, bin: str, *args: str, **kwargs: Any) -> Optional[int]: + kwargs["env"] = self.get_temp_environ(environ=kwargs.get("env")) + return super().execute(bin, *args, **kwargs) @contextmanager - def temp_environ(self): + def temp_environ(self) -> Iterator[None]: environ = dict(os.environ) try: yield @@ -1207,58 +1582,78 @@ def temp_environ(self): os.environ.clear() os.environ.update(environ) - def unset_env(self, key): - if key in os.environ: - del os.environ[key] - - def _updated_path(self): - return os.pathsep.join([str(self._bin_dir), os.environ["PATH"]]) + def _updated_path(self) -> str: + return os.pathsep.join([str(self._bin_dir), os.environ.get("PATH", "")]) class NullEnv(SystemEnv): - def __init__(self, path=None, base=None, execute=False): + def __init__( + self, path: Path = None, base: Optional[Path] = None, execute: bool = False + ) -> None: if path is None: path = Path(sys.prefix) - super(NullEnv, self).__init__(path, base=base) + super().__init__(path, base=base) self._execute = execute self.executed = [] - def get_pip_command(self): # type: () -> List[str] - return [self._bin("python"), "-m", "pip"] + def get_pip_command(self, embedded: bool = False) -> List[str]: + return [self._bin("python"), self.pip_embedded if embedded else self.pip] - def _run(self, cmd, **kwargs): + def _run(self, cmd: List[str], **kwargs: Any) -> int: self.executed.append(cmd) if self._execute: - return super(NullEnv, self)._run(cmd, **kwargs) + return super()._run(cmd, **kwargs) - def execute(self, bin, *args, **kwargs): + def execute(self, bin: str, *args: str, **kwargs: Any) -> Optional[int]: self.executed.append([bin] + list(args)) if self._execute: - return super(NullEnv, self).execute(bin, *args, **kwargs) + return super().execute(bin, *args, **kwargs) - def _bin(self, bin): + def _bin(self, bin: str) -> str: return bin +@contextmanager +def ephemeral_environment( + executable=None, + flags: Dict[str, bool] = None, + with_pip: bool = False, + with_wheel: Optional[bool] = None, + with_setuptools: Optional[bool] = None, +) -> ContextManager[VirtualEnv]: + with temporary_directory() as tmp_dir: + # TODO: cache PEP 517 build environment corresponding to each project venv + venv_dir = Path(tmp_dir) / ".venv" + EnvManager.build_venv( + path=venv_dir.as_posix(), + executable=executable, + flags=flags, + with_pip=with_pip, + with_wheel=with_wheel, + with_setuptools=with_setuptools, + ) + yield VirtualEnv(venv_dir, venv_dir) + + class MockEnv(NullEnv): def __init__( self, - version_info=(3, 7, 0), - python_implementation="CPython", - platform="darwin", - os_name="posix", - is_venv=False, - pip_version="19.1", - sys_path=None, - marker_env=None, - supported_tags=None, - **kwargs + version_info: Tuple[int, int, int] = (3, 7, 0), + python_implementation: str = "CPython", + platform: str = "darwin", + os_name: str = "posix", + is_venv: bool = False, + pip_version: str = "19.1", + sys_path: Optional[List[str]] = None, + marker_env: Dict[str, Any] = None, + supported_tags: List[Tag] = None, + **kwargs: Any, ): - super(MockEnv, self).__init__(**kwargs) + super().__init__(**kwargs) self._version_info = version_info self._python_implementation = python_implementation @@ -1271,29 +1666,29 @@ def __init__( self._supported_tags = supported_tags @property - def platform(self): # type: () -> str + def platform(self) -> str: return self._platform @property - def os(self): # type: () -> str + def os(self) -> str: return self._os_name @property - def pip_version(self): + def pip_version(self) -> Version: return self._pip_version @property - def sys_path(self): + def sys_path(self) -> List[str]: if self._sys_path is None: - return super(MockEnv, self).sys_path + return super().sys_path return self._sys_path - def get_marker_env(self): # type: () -> Dict[str, Any] + def get_marker_env(self) -> Dict[str, Any]: if self._mock_marker_env is not None: return self._mock_marker_env - marker_env = super(MockEnv, self).get_marker_env() + marker_env = super().get_marker_env() marker_env["python_implementation"] = self._python_implementation marker_env["version_info"] = self._version_info marker_env["python_version"] = ".".join(str(v) for v in self._version_info[:2]) @@ -1306,5 +1701,5 @@ def get_marker_env(self): # type: () -> Dict[str, Any] return marker_env - def is_venv(self): # type: () -> bool + def is_venv(self) -> bool: return self._is_venv diff --git a/poetry/utils/exporter.py b/poetry/utils/exporter.py index 19b48452346..2ce9c6d19fd 100644 --- a/poetry/utils/exporter.py +++ b/poetry/utils/exporter.py @@ -1,14 +1,18 @@ +import urllib.parse + +from pathlib import Path +from typing import Optional +from typing import Sequence from typing import Union -from clikit.api.io import IO +from cleo.io.io import IO +from poetry.core.packages.utils.utils import path_to_url from poetry.poetry import Poetry -from poetry.utils._compat import Path from poetry.utils._compat import decode -from poetry.utils.extras import get_extra_package_names -class Exporter(object): +class Exporter: """ Exporter class to export a lock file to alternative formats. """ @@ -18,21 +22,21 @@ class Exporter(object): ACCEPTED_FORMATS = (FORMAT_REQUIREMENTS_TXT,) ALLOWED_HASH_ALGORITHMS = ("sha256", "sha384", "sha512") - def __init__(self, poetry): # type: (Poetry) -> None + def __init__(self, poetry: Poetry) -> None: self._poetry = poetry def export( self, - fmt, - cwd, - output, - with_hashes=True, - dev=False, - extras=None, - with_credentials=False, - ): # type: (str, Path, Union[IO, str], bool, bool, bool) -> None + fmt: str, + cwd: Path, + output: Union[IO, str], + with_hashes: bool = True, + dev: bool = False, + extras: Optional[Union[bool, Sequence[str]]] = None, + with_credentials: bool = False, + ) -> None: if fmt not in self.ACCEPTED_FORMATS: - raise ValueError("Invalid export format: {}".format(fmt)) + raise ValueError(f"Invalid export format: {fmt}") getattr(self, "_export_{}".format(fmt.replace(".", "_")))( cwd, @@ -45,63 +49,53 @@ def export( def _export_requirements_txt( self, - cwd, - output, - with_hashes=True, - dev=False, - extras=None, - with_credentials=False, - ): # type: (Path, Union[IO, str], bool, bool, bool) -> None + cwd: Path, + output: Union[IO, str], + with_hashes: bool = True, + dev: bool = False, + extras: Optional[Union[bool, Sequence[str]]] = None, + with_credentials: bool = False, + ) -> None: indexes = set() content = "" - repository = self._poetry.locker.locked_repository(dev) - - # Build a set of all packages required by our selected extras - extra_package_names = set( - get_extra_package_names( - repository.packages, - self._poetry.locker.lock_data.get("extras", {}), - extras or (), - ) - ) - dependency_lines = set() - for dependency in self._poetry.locker.get_project_dependencies( - project_requires=self._poetry.package.requires - if not dev - else self._poetry.package.all_requires, - with_nested=True, + for dependency_package in self._poetry.locker.get_project_dependency_packages( + project_requires=self._poetry.package.all_requires, dev=dev, extras=extras ): - package = repository.find_packages(dependency=dependency)[0] - - # If a package is optional and we haven't opted in to it, continue - if package.optional and package.name not in extra_package_names: - continue - line = "" + dependency = dependency_package.dependency + package = dependency_package.package + if package.develop: line += "-e " requirement = dependency.to_pep_508(with_extras=False) - is_direct_reference = ( - dependency.is_vcs() - or dependency.is_url() - or dependency.is_file() - or dependency.is_directory() + is_direct_local_reference = ( + dependency.is_file() or dependency.is_directory() ) + is_direct_remote_reference = dependency.is_vcs() or dependency.is_url() - if is_direct_reference: + if is_direct_remote_reference: line = requirement + elif is_direct_local_reference: + dependency_uri = path_to_url(dependency.source_url) + line = f"{dependency.name} @ {dependency_uri}" else: - line = "{}=={}".format(package.name, package.version) + line = f"{package.name}=={package.version}" + + if not is_direct_remote_reference: if ";" in requirement: markers = requirement.split(";", 1)[1].strip() if markers: - line += "; {}".format(markers) + line += f"; {markers}" - if not is_direct_reference and package.source_url: + if ( + not is_direct_remote_reference + and not is_direct_local_reference + and package.source_url + ): indexes.add(package.source_url) if package.files and with_hashes: @@ -115,7 +109,7 @@ def _export_requirements_txt( if algorithm not in self.ALLOWED_HASH_ALGORITHMS: continue - hashes.append("{}:{}".format(algorithm, h)) + hashes.append(f"{algorithm}:{h}") if hashes: line += " \\\n" @@ -132,11 +126,14 @@ def _export_requirements_txt( # If we have extra indexes, we add them to the beginning of the output indexes_header = "" for index in sorted(indexes): - repository = [ + repositories = [ r for r in self._poetry.pool.repositories if r.url == index.rstrip("/") - ][0] + ] + if not repositories: + continue + repository = repositories[0] if ( self._poetry.pool.has_default() and repository is self._poetry.pool.repositories[0] @@ -146,21 +143,22 @@ def _export_requirements_txt( if with_credentials else repository.url ) - indexes_header = "--index-url {}\n".format(url) + indexes_header = f"--index-url {url}\n" continue url = ( repository.authenticated_url if with_credentials else repository.url ) - indexes_header += "--extra-index-url {}\n".format(url) + parsed_url = urllib.parse.urlsplit(url) + if parsed_url.scheme == "http": + indexes_header += f"--trusted-host {parsed_url.netloc}\n" + indexes_header += f"--extra-index-url {url}\n" content = indexes_header + "\n" + content self._output(content, cwd, output) - def _output( - self, content, cwd, output - ): # type: (str, Path, Union[IO, str]) -> None + def _output(self, content: str, cwd: Path, output: Union[IO, str]) -> None: decoded = decode(content) try: output.write(decoded) diff --git a/poetry/utils/extras.py b/poetry/utils/extras.py index c97d6ee79e5..37f04681eed 100644 --- a/poetry/utils/extras.py +++ b/poetry/utils/extras.py @@ -1,17 +1,20 @@ +from typing import TYPE_CHECKING +from typing import Iterable from typing import Iterator from typing import List from typing import Mapping from typing import Sequence -from poetry.core.packages import Package -from poetry.utils.helpers import canonicalize_name + +if TYPE_CHECKING: + from poetry.core.packages.package import Package # noqa def get_extra_package_names( - packages, # type: Sequence[Package] - extras, # type: Mapping[str, List[str]] - extra_names, # type: Sequence[str] -): # type: (...) -> Iterator[str] + packages: Sequence["Package"], + extras: Mapping[str, List[str]], + extra_names: Sequence[str], +) -> Iterator[str]: """ Returns all package names required by the given extras. @@ -20,6 +23,9 @@ def get_extra_package_names( in the `extras` section of `poetry.lock`. :param extra_names: A list of strings specifying names of extra groups to resolve. """ + from poetry.core.packages.package import Package # noqa + from poetry.utils.helpers import canonicalize_name + if not extra_names: return [] @@ -36,7 +42,7 @@ def get_extra_package_names( # keep record of packages seen during recursion in order to avoid recursion error seen_package_names = set() - def _extra_packages(package_names): + def _extra_packages(package_names: Iterable[str]) -> Iterator[str]: """Recursively find dependencies for packages names""" # for each extra pacakge name for package_name in package_names: diff --git a/poetry/utils/helpers.py b/poetry/utils/helpers.py index 180a90d50f3..d81d0f15b3d 100644 --- a/poetry/utils/helpers.py +++ b/poetry/utils/helpers.py @@ -5,14 +5,18 @@ import tempfile from contextlib import contextmanager +from pathlib import Path +from typing import Any +from typing import Callable +from typing import Dict +from typing import Iterator +from typing import List from typing import Optional import requests from poetry.config.config import Config from poetry.core.packages.package import Package -from poetry.core.version import Version -from poetry.utils._compat import Path try: @@ -24,25 +28,21 @@ _canonicalize_regex = re.compile("[-_]+") -def canonicalize_name(name): # type: (str) -> str +def canonicalize_name(name: str) -> str: return _canonicalize_regex.sub("-", name).lower() -def module_name(name): # type: (str) -> str +def module_name(name: str) -> str: return canonicalize_name(name).replace(".", "_").replace("-", "_") -def normalize_version(version): # type: (str) -> str - return str(Version(version)) - - -def _del_ro(action, name, exc): +def _del_ro(action: Callable, name: str, exc: Exception) -> None: os.chmod(name, stat.S_IWRITE) os.remove(name) @contextmanager -def temporary_directory(*args, **kwargs): +def temporary_directory(*args: Any, **kwargs: Any) -> Iterator[str]: name = tempfile.mkdtemp(*args, **kwargs) yield name @@ -50,23 +50,23 @@ def temporary_directory(*args, **kwargs): shutil.rmtree(name, onerror=_del_ro) -def get_cert(config, repository_name): # type: (Config, str) -> Optional[Path] - cert = config.get("certificates.{}.cert".format(repository_name)) +def get_cert(config: Config, repository_name: str) -> Optional[Path]: + cert = config.get(f"certificates.{repository_name}.cert") if cert: return Path(cert) else: return None -def get_client_cert(config, repository_name): # type: (Config, str) -> Optional[Path] - client_cert = config.get("certificates.{}.client-cert".format(repository_name)) +def get_client_cert(config: Config, repository_name: str) -> Optional[Path]: + client_cert = config.get(f"certificates.{repository_name}.client-cert") if client_cert: return Path(client_cert) else: return None -def _on_rm_error(func, path, exc_info): +def _on_rm_error(func: Callable, path: str, exc_info: Exception) -> None: if not os.path.exists(path): return @@ -74,14 +74,14 @@ def _on_rm_error(func, path, exc_info): func(path) -def safe_rmtree(path): +def safe_rmtree(path: str) -> None: if Path(path).is_symlink(): return os.unlink(str(path)) shutil.rmtree(path, onerror=_on_rm_error) -def merge_dicts(d1, d2): +def merge_dicts(d1: Dict, d2: Dict) -> None: for k, v in d2.items(): if k in d1 and isinstance(d1[k], dict) and isinstance(d2[k], Mapping): merge_dicts(d1[k], d2[k]) @@ -90,8 +90,11 @@ def merge_dicts(d1, d2): def download_file( - url, dest, session=None, chunk_size=1024 -): # type: (str, str, Optional[requests.Session], int) -> None + url: str, + dest: str, + session: Optional[requests.Session] = None, + chunk_size: int = 1024, +) -> None: get = requests.get if not session else session.get with get(url, stream=True) as response: @@ -104,8 +107,8 @@ def download_file( def get_package_version_display_string( - package, root=None -): # type: (Package, Optional[Path]) -> str + package: Package, root: Optional[Path] = None +) -> str: if package.source_type in ["file", "directory"] and root: return "{} {}".format( package.version, @@ -113,3 +116,22 @@ def get_package_version_display_string( ) return package.full_pretty_version + + +def paths_csv(paths: List[Path]) -> str: + return ", ".join('"{}"'.format(str(c)) for c in paths) + + +def is_dir_writable(path: Path, create: bool = False) -> bool: + try: + if not path.exists(): + if not create: + return False + path.mkdir(parents=True, exist_ok=True) + + with tempfile.TemporaryFile(dir=str(path)): + pass + except OSError: + return False + else: + return True diff --git a/poetry/utils/password_manager.py b/poetry/utils/password_manager.py index 24a615a46bf..e5eb05eab51 100644 --- a/poetry/utils/password_manager.py +++ b/poetry/utils/password_manager.py @@ -1,5 +1,12 @@ import logging +from typing import TYPE_CHECKING +from typing import Dict +from typing import Optional + + +if TYPE_CHECKING: + from poetry.config.config import Config logger = logging.getLogger(__name__) @@ -15,16 +22,16 @@ class KeyRingError(Exception): class KeyRing: - def __init__(self, namespace): + def __init__(self, namespace: str) -> None: self._namespace = namespace self._is_available = True self._check() - def is_available(self): + def is_available(self) -> bool: return self._is_available - def get_password(self, name, username): + def get_password(self, name: str, username: str) -> Optional[str]: if not self.is_available(): return @@ -37,10 +44,10 @@ def get_password(self, name, username): 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) + f"Unable to retrieve the password for {name} from the key ring" ) - def set_password(self, name, username, password): + def set_password(self, name: str, username: str, password: str) -> None: if not self.is_available(): return @@ -58,7 +65,7 @@ def set_password(self, name, username, password): ) ) - def delete_password(self, name, username): + def delete_password(self, name: str, username: str) -> None: if not self.is_available(): return @@ -71,13 +78,13 @@ def delete_password(self, name, username): keyring.delete_password(name, username) except (RuntimeError, keyring.errors.KeyringError): raise KeyRingError( - "Unable to delete the password for {} from the key ring".format(name) + f"Unable to delete the password for {name} from the key ring" ) - def get_entry_name(self, name): - return "{}-{}".format(self._namespace, name) + def get_entry_name(self, name: str) -> str: + return f"{self._namespace}-{name}" - def _check(self): + def _check(self) -> None: try: import keyring except Exception as e: @@ -101,11 +108,9 @@ def _check(self): 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 - ] + 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 @@ -115,12 +120,12 @@ def _check(self): class PasswordManager: - def __init__(self, config): + def __init__(self, config: "Config") -> None: self._config = config self._keyring = None @property - def keyring(self): + def keyring(self) -> KeyRing: if self._keyring is None: self._keyring = KeyRing("poetry-repository") if not self._keyring.is_available(): @@ -130,33 +135,29 @@ def keyring(self): return self._keyring - def set_pypi_token(self, name, token): + def set_pypi_token(self, name: str, token: str) -> None: if not self.keyring.is_available(): - self._config.auth_config_source.add_property( - "pypi-token.{}".format(name), token - ) + self._config.auth_config_source.add_property(f"pypi-token.{name}", token) else: self.keyring.set_password(name, "__token__", token) - def get_pypi_token(self, name): + def get_pypi_token(self, name: str) -> str: if not self.keyring.is_available(): - return self._config.get("pypi-token.{}".format(name)) + return self._config.get(f"pypi-token.{name}") return self.keyring.get_password(name, "__token__") - def delete_pypi_token(self, name): + def delete_pypi_token(self, name: str) -> None: if not self.keyring.is_available(): - return self._config.auth_config_source.remove_property( - "pypi-token.{}".format(name) - ) + return self._config.auth_config_source.remove_property(f"pypi-token.{name}") self.keyring.delete_password(name, "__token__") - def get_http_auth(self, name): - auth = self._config.get("http-basic.{}".format(name)) + def get_http_auth(self, name: str) -> Optional[Dict[str, str]]: + auth = self._config.get(f"http-basic.{name}") if not auth: - username = self._config.get("http-basic.{}.username".format(name)) - password = self._config.get("http-basic.{}.password".format(name)) + username = self._config.get(f"http-basic.{name}.username") + password = self._config.get(f"http-basic.{name}.password") if not username and not password: return None else: @@ -169,7 +170,7 @@ def get_http_auth(self, name): "password": password, } - def set_http_password(self, name, username, password): + def set_http_password(self, name: str, username: str, password: str) -> None: auth = {"username": username} if not self.keyring.is_available(): @@ -177,9 +178,9 @@ def set_http_password(self, name, username, password): else: self.keyring.set_password(name, username, password) - self._config.auth_config_source.add_property("http-basic.{}".format(name), auth) + self._config.auth_config_source.add_property(f"http-basic.{name}", auth) - def delete_http_password(self, name): + def delete_http_password(self, name: str) -> None: auth = self.get_http_auth(name) if not auth or "username" not in auth: return @@ -189,4 +190,4 @@ def delete_http_password(self, name): except KeyRingError: pass - self._config.auth_config_source.remove_property("http-basic.{}".format(name)) + self._config.auth_config_source.remove_property(f"http-basic.{name}") diff --git a/poetry/utils/patterns.py b/poetry/utils/patterns.py index 1d6413c26fe..ec6c53d78d7 100644 --- a/poetry/utils/patterns.py +++ b/poetry/utils/patterns.py @@ -2,8 +2,11 @@ wheel_file_re = re.compile( - r"""^(?P(?P.+?)(-(?P\d.+?))?) - ((-(?P\d.*?))?-(?P.+?)-(?P.+?)-(?P.+?) - \.whl|\.dist-info)$""", + r"^(?P(?P.+?)-(?P\d.*?))" + r"(-(?P\d.*?))?" + r"-(?P.+?)" + r"-(?P.+?)" + r"-(?P.+?)" + r"\.whl|\.dist-info$", re.VERBOSE, ) diff --git a/poetry/utils/pip.py b/poetry/utils/pip.py new file mode 100644 index 00000000000..416c56b97ae --- /dev/null +++ b/poetry/utils/pip.py @@ -0,0 +1,66 @@ +import os +import sys + +from pathlib import Path +from typing import Union + +from poetry.exceptions import PoetryException +from poetry.utils.env import Env +from poetry.utils.env import EnvCommandError +from poetry.utils.env import ephemeral_environment + + +def pip_install( + path: Union[Path, str], + environment: Env, + editable: bool = False, + deps: bool = False, + upgrade: bool = False, +) -> Union[int, str]: + path = Path(path) if isinstance(path, str) else path + is_wheel = path.suffix == ".whl" + + # We disable version check here as we are already pinning to version available in either the + # virtual environment or the virtualenv package embedded wheel. Version checks are a wasteful + # network call that adds a lot of wait time when installing a lot of packages. + args = ["install", "--disable-pip-version-check", "--prefix", str(environment.path)] + + if not is_wheel: + args.insert(1, "--use-pep517") + + if upgrade: + args.append("--upgrade") + + if not deps: + args.append("--no-deps") + + if editable: + if not path.is_dir(): + raise PoetryException( + "Cannot install non directory dependencies in editable mode" + ) + args.append("-e") + + args.append(str(path)) + + try: + return environment.run_pip(*args) + except EnvCommandError as e: + if sys.version_info < (3, 7) and not is_wheel: + # Under certain Python3.6 installs vendored pip wheel does not contain zip-safe + # pep517 lib. In this cases we create an isolated ephemeral virtual environment. + with ephemeral_environment( + executable=environment.python, with_pip=True, with_setuptools=True + ) as env: + return environment.run( + *env.get_pip_command(), + *args, + env={**os.environ, "PYTHONPATH": str(env.purelib)}, + ) + raise PoetryException(f"Failed to install {path.as_posix()}") from e + + +def pip_editable_install(directory: Path, environment: Env) -> Union[int, str]: + return pip_install( + path=directory, environment=environment, editable=True, deps=False, upgrade=True + ) diff --git a/poetry/utils/setup_reader.py b/poetry/utils/setup_reader.py index 6a45103c1f4..c85f34cdda3 100644 --- a/poetry/utils/setup_reader.py +++ b/poetry/utils/setup_reader.py @@ -1,5 +1,7 @@ import ast +from configparser import ConfigParser +from pathlib import Path from typing import Any from typing import Dict from typing import Iterable @@ -8,20 +10,10 @@ from typing import Tuple from typing import Union -from poetry.core.semver import Version +from poetry.core.semver.version import Version -from ._compat import PY35 -from ._compat import Path -from ._compat import basestring - -try: - from configparser import ConfigParser -except ImportError: - from ConfigParser import ConfigParser - - -class SetupReader(object): +class SetupReader: """ Class that reads a setup.py file without executing it. """ @@ -38,9 +30,9 @@ class SetupReader(object): @classmethod def read_from_directory( - cls, directory - ): # type: (Union[basestring, Path]) -> Dict[str, Union[List, Dict]] - if isinstance(directory, basestring): + cls, directory: Union[str, Path] + ) -> Dict[str, Union[List, Dict]]: + if isinstance(directory, str): directory = Path(directory) result = cls.DEFAULT.copy() @@ -59,21 +51,8 @@ def read_from_directory( return result - @classmethod - def _is_empty_result(cls, result): # type: (Dict[str, Any]) -> bool - return ( - not result["install_requires"] - and not result["extras_require"] - and not result["python_requires"] - ) - - def read_setup_py( - self, filepath - ): # type: (Union[basestring, Path]) -> Dict[str, Union[List, Dict]] - if not PY35: - return self.DEFAULT - - if isinstance(filepath, basestring): + def read_setup_py(self, filepath: Union[str, Path]) -> Dict[str, Union[List, Dict]]: + if isinstance(filepath, str): filepath = Path(filepath) with filepath.open(encoding="utf-8") as f: @@ -99,8 +78,8 @@ def read_setup_py( return result def read_setup_cfg( - self, filepath - ): # type: (Union[basestring, Path]) -> Dict[str, Union[List, Dict]] + self, filepath: Union[str, Path] + ) -> Dict[str, Union[List, Dict]]: parser = ConfigParser() parser.read(str(filepath)) @@ -148,8 +127,8 @@ def read_setup_cfg( } def _find_setup_call( - self, elements - ): # type: (List[Any]) -> Tuple[Optional[ast.Call], Optional[List[Any]]] + self, elements: List[Any] + ) -> Tuple[Optional[ast.Call], Optional[List[Any]]]: funcdefs = [] for i, element in enumerate(elements): if isinstance(element, ast.If) and i == len(elements) - 1: @@ -197,8 +176,8 @@ def _find_setup_call( return self._find_sub_setup_call(funcdefs) def _find_sub_setup_call( - self, elements - ): # type: (List[Any]) -> Tuple[Optional[ast.Call], Optional[List[Any]]] + self, elements: List[Any] + ) -> Tuple[Optional[ast.Call], Optional[List[Any]]]: for element in elements: if not isinstance(element, (ast.FunctionDef, ast.If)): continue @@ -213,9 +192,7 @@ def _find_sub_setup_call( return None, None - def _find_install_requires( - self, call, body - ): # type: (ast.Call, Iterable[Any]) -> List[str] + def _find_install_requires(self, call: ast.Call, body: Iterable[Any]) -> List[str]: install_requires = [] value = self._find_in_call(call, "install_requires") if value is None: @@ -256,8 +233,8 @@ def _find_install_requires( return install_requires def _find_extras_require( - self, call, body - ): # type: (ast.Call, Iterable[Any]) -> Dict[str, List] + self, call: ast.Call, body: Iterable[Any] + ) -> Dict[str, List]: extras_require = {} value = self._find_in_call(call, "extras_require") if value is None: @@ -308,8 +285,8 @@ def _find_extras_require( return extras_require def _find_single_string( - self, call, body, name - ): # type: (ast.Call, List[Any], str) -> Optional[str] + self, call: ast.Call, body: List[Any], name: str + ) -> Optional[str]: value = self._find_in_call(call, name) if value is None: # Trying to find in kwargs @@ -344,12 +321,12 @@ def _find_single_string( if variable is not None and isinstance(variable, ast.Str): return variable.s - def _find_in_call(self, call, name): # type: (ast.Call, str) -> Optional[Any] + def _find_in_call(self, call: ast.Call, name: str) -> Optional[Any]: for keyword in call.keywords: if keyword.arg == name: return keyword.value - def _find_call_kwargs(self, call): # type: (ast.Call) -> Optional[Any] + def _find_call_kwargs(self, call: ast.Call) -> Optional[Any]: kwargs = None for keyword in call.keywords: if keyword.arg is None: @@ -357,9 +334,7 @@ def _find_call_kwargs(self, call): # type: (ast.Call) -> Optional[Any] return kwargs - def _find_variable_in_body( - self, body, name - ): # type: (Iterable[Any], str) -> Optional[Any] + def _find_variable_in_body(self, body: Iterable[Any], name: str) -> Optional[Any]: found = None for elem in body: if found: @@ -375,7 +350,9 @@ def _find_variable_in_body( if target.id == name: return elem.value - def _find_in_dict(self, dict_, name): # type: (ast.Call, str) -> Optional[Any] + def _find_in_dict( + self, dict_: Union[ast.Dict, ast.Call], name: str + ) -> Optional[Any]: for key, val in zip(dict_.keys, dict_.values): if isinstance(key, ast.Str) and key.s == name: return val diff --git a/poetry/utils/shell.py b/poetry/utils/shell.py index 2b2fe91f946..ce91bfba029 100644 --- a/poetry/utils/shell.py +++ b/poetry/utils/shell.py @@ -2,14 +2,16 @@ import signal import sys +from pathlib import Path +from typing import Any + import pexpect -from clikit.utils.terminal import Terminal +from cleo.terminal import Terminal from shellingham import ShellDetectionFailure from shellingham import detect_shell from ._compat import WINDOWS -from ._compat import Path from .env import VirtualEnv @@ -20,20 +22,20 @@ class Shell: _shell = None - def __init__(self, name, path): # type: (str, str) -> None + def __init__(self, name: str, path: str) -> None: self._name = name self._path = path @property - def name(self): # type: () -> str + def name(self) -> str: return self._name @property - def path(self): # type: () -> str + def path(self) -> str: return self._path @classmethod - def get(cls): # type: () -> Shell + def get(cls) -> "Shell": """ Retrieve the current shell. """ @@ -59,10 +61,12 @@ def get(cls): # type: () -> Shell return cls._shell - def activate(self, env): # type: (VirtualEnv) -> None + def activate(self, env: VirtualEnv) -> None: if WINDOWS: return env.execute(self.path) + import shlex + terminal = Terminal() with env.temp_environ(): c = pexpect.spawn( @@ -75,9 +79,11 @@ def activate(self, env): # type: (VirtualEnv) -> None activate_script = self._get_activate_script() bin_dir = "Scripts" if WINDOWS else "bin" activate_path = env.path / bin_dir / activate_script - c.sendline("{} {}".format(self._get_source_command(), activate_path)) + c.sendline( + "{} {}".format(self._get_source_command(), shlex.quote(str(activate_path))) + ) - def resize(sig, data): + def resize(sig: Any, data: Any) -> None: terminal = Terminal() c.setwinsize(terminal.height, terminal.width) @@ -89,7 +95,7 @@ def resize(sig, data): sys.exit(c.exitstatus) - def _get_activate_script(self): + def _get_activate_script(self) -> str: if "fish" == self._name: suffix = ".fish" elif "csh" == self._name: @@ -101,7 +107,7 @@ def _get_activate_script(self): return "activate" + suffix - def _get_source_command(self): + def _get_source_command(self) -> str: if "fish" == self._name: return "source" elif "csh" == self._name: @@ -111,5 +117,5 @@ def _get_source_command(self): return "." - def __repr__(self): # type: () -> str - return '{}("{}", "{}")'.format(self.__class__.__name__, self._name, self._path) + def __repr__(self) -> str: + return f'{self.__class__.__name__}("{self._name}", "{self._path}")' diff --git a/poetry/version/version_selector.py b/poetry/version/version_selector.py index ea002860938..034294ede9b 100644 --- a/poetry/version/version_selector.py +++ b/poetry/version/version_selector.py @@ -1,20 +1,26 @@ +from typing import TYPE_CHECKING +from typing import Optional from typing import Union -from poetry.core.packages import Package -from poetry.core.semver import Version +from poetry.core.packages.package import Package +from poetry.core.semver.version import Version -class VersionSelector(object): - def __init__(self, pool): +if TYPE_CHECKING: + from poetry.repositories import Pool + + +class VersionSelector: + def __init__(self, pool: "Pool") -> None: self._pool = pool def find_best_candidate( self, - package_name, # type: str - target_package_version=None, # type: Union[str, None] - allow_prereleases=False, # type: bool - source=None, # type: str - ): # type: (...) -> Union[Package, bool] + package_name: str, + target_package_version: Optional[str] = None, + allow_prereleases: bool = False, + source: Optional[str] = None, + ) -> Union[Package, bool]: """ Given a package name and optional version, returns the latest Package that matches @@ -30,7 +36,7 @@ def find_best_candidate( }, ) candidates = self._pool.find_packages(dependency) - only_prereleases = all([c.version.is_prerelease() for c in candidates]) + only_prereleases = all([c.version.is_unstable() for c in candidates]) if not candidates: return False @@ -52,26 +58,13 @@ def find_best_candidate( return False return package - def find_recommended_require_version(self, package): + def find_recommended_require_version(self, package: Package) -> str: version = package.version return self._transform_version(version.text, package.pretty_version) - def _transform_version(self, version, pretty_version): + def _transform_version(self, version: str, pretty_version: str) -> str: try: - parsed = Version.parse(version) - parts = [parsed.major, parsed.minor, parsed.patch] + return f"^{Version.parse(version).to_string()}" except ValueError: return pretty_version - - parts = parts[: parsed.precision] - - # check to see if we have a semver-looking version - if len(parts) < 3: - version = pretty_version - else: - version = ".".join(str(p) for p in parts) - if parsed.is_prerelease(): - version += "-{}".format(".".join(str(p) for p in parsed.prerelease)) - - return "^{}".format(version) diff --git a/pyproject.toml b/pyproject.toml index f099a4cac80..68f78a25a50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "poetry" -version = "1.1.0" +version = "1.2.0a1" description = "Python dependency management and packaging made easy." authors = [ "Sébastien Eustace " @@ -9,6 +9,10 @@ license = "MIT" readme = "README.md" +include = [ + { path = "tests", format = "sdist" } +] + homepage = "https://python-poetry.org/" repository = "https://github.com/python-poetry/poetry" documentation = "https://python-poetry.org/docs" @@ -20,60 +24,46 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules" ] +[tool.poetry.build] +generate-setup-file = false + # Requirements [tool.poetry.dependencies] -python = "~2.7 || ^3.5" +python = "^3.6" -poetry-core = "^1.0.0" -cleo = "^0.8.1" -clikit = "^0.6.2" -crashtest = { version = "^0.3.0", python = "^3.6" } +poetry-core = "~1.1.0a5" +cleo = "^1.0.0a1" +crashtest = "^0.3.0" requests = "^2.18" cachy = "^0.3.0" requests-toolbelt = "^0.9.1" cachecontrol = { version = "^0.12.4", extras = ["filecache"] } -pkginfo = "^1.4" +pkginfo = "^1.5" html5lib = "^1.0" shellingham = "^1.1" tomlkit = ">=0.7.0,<1.0.0" pexpect = "^4.7.0" packaging = "^20.4" -virtualenv = { version = "^20.0.26" } - -# The typing module is not in the stdlib in Python 2.7 -typing = { version = "^3.6", python = "~2.7" } - -# Use pathlib2 for Python 2.7 -pathlib2 = { version = "^2.3", python = "~2.7" } -# Use futures on Python 2.7 -futures = { version = "^3.3.0", python = "~2.7" } -# Use glob2 for Python 2.7 and 3.4 -glob2 = { version = "^0.6", python = "~2.7" } -# functools32 is needed for Python 2.7 -functools32 = { version = "^3.2.3", python = "~2.7" } -keyring = [ - { version = "^18.0.1", python = "~2.7" }, - { version = "^20.0.1", python = "~3.5" }, - { version = "^21.2.0", python = "^3.6" } -] -# Use subprocess32 for Python 2.7 -subprocess32 = { version = "^3.5", python = "~2.7" } +# temporarily clamped due to https://github.com/pypa/pip/issues/9953 +virtualenv = ">=20.4.3,<20.4.5" +keyring = ">=21.2.0" +entrypoints = "^0.3" importlib-metadata = {version = "^1.6.0", python = "<3.8"} +dataclasses = {version = "^0.8", python = "~3.6"} [tool.poetry.dev-dependencies] -pytest = [ - {version = "^4.1", python = "<3.5"}, - {version = "^5.4.3", python = ">=3.5"} -] -pytest-cov = "^2.5" -pytest-mock = "^1.9" +pytest = "^6.2" +pytest-cov = "^2.8" +pytest-mock = "^3.5" pre-commit = { version = "^2.6", python = "^3.6.1" } tox = "^3.0" -pytest-sugar = "^0.9.2" -httpretty = "^0.9.6" +pytest-sugar = "^0.9" +httpretty = "^1.0" +zipp = { version = "^3.4", python = "<3.8"} +deepdiff = "^5.0" [tool.poetry.scripts] -poetry = "poetry.console:main" +poetry = "poetry.console.application:main" [build-system] @@ -113,3 +103,9 @@ exclude = ''' | tests/.*/setup.py )/ ''' + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "if TYPE_CHECKING:" +] diff --git a/sonnet b/sonnet index ecfda99037b..7322d3abed1 100755 --- a/sonnet +++ b/sonnet @@ -7,10 +7,31 @@ import sys import tarfile from gzip import GzipFile +from pathlib import Path +from typing import Optional -from cleo import Application -from cleo import Command -from clikit.api.formatter import Style +from cleo.application import Application as BaseApplication +from cleo.commands.command import Command +from cleo.formatters.style import Style +from cleo.helpers import option +from cleo.io.inputs.input import Input +from cleo.io.io import IO +from cleo.io.outputs.output import Output + + +class Application(BaseApplication): + def create_io( + self, + input: Optional[Input] = None, + output: Optional[Output] = None, + error_output: Optional[Output] = None, + ) -> IO: + io = super(Application, self).create_io(input, output, error_output) + + io.output.formatter.set_style("debug", Style("default", options=["dark"])) + io.error_output.formatter.set_style("debug", Style("default", options=["dark"])) + + return io WINDOWS = sys.platform.startswith("win") or (sys.platform == "cli" and os.name == "nt") @@ -19,18 +40,18 @@ WINDOWS = sys.platform.startswith("win") or (sys.platform == "cli" and os.name = class MakeReleaseCommand(Command): """ Makes a self-contained package of Poetry. - - release - {--P|python=?* : Python version to use} """ + name = "make release" + + options = [option("--python", "-P", flag=False, multiple=True)] + PYTHON = { - "2.7": "python2.7", - "3.5": "python3.5", "3.6": "python3.6", "3.7": "python3.7", "3.8": "python3.8", "3.9": "python3.9", + "3.10": "python3.10", } def handle(self): @@ -54,7 +75,6 @@ class MakeReleaseCommand(Command): from poetry.puzzle import Solver from poetry.repositories.pool import Pool from poetry.repositories.repository import Repository - from poetry.utils._compat import Path from poetry.utils.env import EnvManager from poetry.utils.env import VirtualEnv from poetry.utils.helpers import temporary_directory @@ -246,23 +266,8 @@ class MakeReleaseCommand(Command): return vendor_dir -class MakeCommand(Command): - """ - Build poetry releases. - - make - """ - - commands = [MakeReleaseCommand()] - - def handle(self): - return self.call("help", self.config.name) - - app = Application("sonnet") -app.config.add_style(Style("debug").fg("default").dark()) - -app.add(MakeCommand()) +app.add(MakeReleaseCommand()) if __name__ == "__main__": app.run() diff --git a/tests/compat.py b/tests/compat.py new file mode 100644 index 00000000000..13c5bb7da98 --- /dev/null +++ b/tests/compat.py @@ -0,0 +1,4 @@ +try: + import zipp +except ImportError: + import zipfile as zipp # noqa diff --git a/tests/config/test_config.py b/tests/config/test_config.py index 07373ade0b4..f3b13f23003 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -1,16 +1,29 @@ import os +import pytest -def test_config_get_default_value(config): - assert config.get("virtualenvs.create") is True +@pytest.mark.parametrize( + ("name", "value"), [("installer.parallel", True), ("virtualenvs.create", True)] +) +def test_config_get_default_value(config, name, value): + assert config.get(name) is value -def test_config_get_processes_depended_on_values(config): - assert os.path.join("/foo", "virtualenvs") == config.get("virtualenvs.path") +def test_config_get_processes_depended_on_values(config, config_cache_dir): + assert str(config_cache_dir / "virtualenvs") == config.get("virtualenvs.path") -def test_config_get_from_environment_variable(config, environ): - assert config.get("virtualenvs.create") - os.environ["POETRY_VIRTUALENVS_CREATE"] = "false" - assert not config.get("virtualenvs.create") +@pytest.mark.parametrize( + ("name", "env_value", "value"), + [ + ("installer.parallel", "true", True), + ("installer.parallel", "false", False), + ("virtualenvs.create", "true", True), + ("virtualenvs.create", "false", False), + ], +) +def test_config_get_from_environment_variable(config, environ, name, env_value, value): + env_var = "POETRY_{}".format("_".join(k.upper() for k in name.split("."))) + os.environ[env_var] = env_value + assert config.get(name) is value diff --git a/tests/conftest.py b/tests/conftest.py index 15f7b56e567..d66ed9ba659 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,13 +4,14 @@ import sys import tempfile +from pathlib import Path from typing import Any from typing import Dict import httpretty import pytest -from cleo import CommandTester +from cleo.testers.command_tester import CommandTester from poetry.config.config import Config as BaseConfig from poetry.config.dict_config_source import DictConfigSource @@ -21,7 +22,6 @@ from poetry.layouts import layout from poetry.repositories import Pool from poetry.repositories import Repository -from poetry.utils._compat import Path from poetry.utils.env import EnvManager from poetry.utils.env import SystemEnv from poetry.utils.env import VirtualEnv @@ -34,19 +34,19 @@ class Config(BaseConfig): - def get(self, setting_name, default=None): # type: (str, Any) -> Any + def get(self, setting_name: str, default: Any = None) -> Any: self.merge(self._config_source.config) self.merge(self._auth_config_source.config) return super(Config, self).get(setting_name, default=default) - def raw(self): # type: () -> Dict[str, Any] + def raw(self) -> Dict[str, Any]: self.merge(self._config_source.config) self.merge(self._auth_config_source.config) return super(Config, self).raw() - def all(self): # type: () -> Dict[str, Any] + def all(self) -> Dict[str, Any]: self.merge(self._config_source.config) self.merge(self._auth_config_source.config) @@ -54,9 +54,21 @@ def all(self): # type: () -> Dict[str, Any] @pytest.fixture -def config_source(): +def config_cache_dir(tmp_dir): + path = Path(tmp_dir) / ".cache" / "pypoetry" + path.mkdir(parents=True) + return path + + +@pytest.fixture +def config_virtualenvs_path(config_cache_dir): + return config_cache_dir / "virtualenvs" + + +@pytest.fixture +def config_source(config_cache_dir): source = DictConfigSource() - source.add_property("cache-dir", "/foo") + source.add_property("cache-dir", str(config_cache_dir)) return source @@ -87,6 +99,15 @@ def config(config_source, auth_config_source, mocker): return c +@pytest.fixture(autouse=True) +def mock_user_config_dir(mocker): + config_dir = tempfile.mkdtemp(prefix="poetry_config_") + mocker.patch("poetry.locations.CONFIG_DIR", new=config_dir) + mocker.patch("poetry.factory.CONFIG_DIR", new=config_dir) + yield + shutil.rmtree(config_dir, ignore_errors=True) + + @pytest.fixture(autouse=True) def download_mock(mocker): # Patch download to not download anything but to just copy from fixtures @@ -106,7 +127,8 @@ def _pep517_metadata(cls, path): return PackageInfo(name="demo", version="0.1.2") mocker.patch( - "poetry.inspection.info.PackageInfo._pep517_metadata", _pep517_metadata, + "poetry.inspection.info.PackageInfo._pep517_metadata", + _pep517_metadata, ) @@ -141,9 +163,14 @@ def http(): @pytest.fixture -def fixture_dir(): +def fixture_base(): + return Path(__file__).parent / "fixtures" + + +@pytest.fixture +def fixture_dir(fixture_base): def _fixture_dir(name): - return Path(__file__).parent / "fixtures" / name + return fixture_base / name return _fixture_dir @@ -167,7 +194,7 @@ def mocked_open(self, *args, **kwargs): return mocker.MagicMock() return original(self, *args, **kwargs) - mocker.patch("poetry.utils._compat.Path.open", mocked_open) + mocker.patch("pathlib.Path.open", mocked_open) yield files @@ -207,7 +234,8 @@ def default_python(current_python): @pytest.fixture def repo(http): http.register_uri( - http.GET, re.compile("^https?://foo.bar/(.+?)$"), + http.GET, + re.compile("^https?://foo.bar/(.+?)$"), ) return TestRepository(name="foo") @@ -221,6 +249,7 @@ def _factory( dependencies=None, dev_dependencies=None, pyproject_content=None, + poetry_lock_content=None, install_deps=True, ): project_dir = workspace / "poetry-fixture-{}".format(name) @@ -244,6 +273,10 @@ def _factory( dev_dependencies=dev_dependencies, ).create(project_dir, with_tests=False) + if poetry_lock_content: + lock_file = project_dir / "poetry.lock" + lock_file.write_text(data=poetry_lock_content, encoding="utf-8") + poetry = Factory().create_poetry(project_dir) locker = TestLocker( @@ -277,6 +310,13 @@ def _tester(command, poetry=None, installer=None, executor=None, environment=Non command = app.find(command) tester = CommandTester(command) + # Setting the formatter from the application + # TODO: Find a better way to do this in Cleo + app_io = app.create_io() + formatter = app_io.output.formatter + tester.io.output.set_formatter(formatter) + tester.io.error_output.set_formatter(formatter) + if poetry: app._poetry = poetry diff --git a/tests/console/commands/debug/test_resolve.py b/tests/console/commands/debug/test_resolve.py index ad7e94321d2..9af3e0010d8 100644 --- a/tests/console/commands/debug/test_resolve.py +++ b/tests/console/commands/debug/test_resolve.py @@ -9,14 +9,20 @@ def tester(command_tester_factory): return command_tester_factory("debug resolve") -def test_debug_resolve_gives_resolution_results(tester, repo): - cachy2 = get_package("cachy", "0.2.0") - cachy2.add_dependency(Factory.create_dependency("msgpack-python", ">=0.5 <0.6")) +@pytest.fixture(autouse=True) +def __add_packages(repo): + cachy020 = get_package("cachy", "0.2.0") + cachy020.add_dependency(Factory.create_dependency("msgpack-python", ">=0.5 <0.6")) repo.add_package(get_package("cachy", "0.1.0")) - repo.add_package(cachy2) + repo.add_package(cachy020) repo.add_package(get_package("msgpack-python", "0.5.3")) + repo.add_package(get_package("pendulum", "2.0.3")) + repo.add_package(get_package("cleo", "0.6.5")) + + +def test_debug_resolve_gives_resolution_results(tester): tester.execute("cachy") expected = """\ @@ -24,21 +30,14 @@ def test_debug_resolve_gives_resolution_results(tester, repo): Resolution results: -msgpack-python 0.5.3 -cachy 0.2.0 +msgpack-python 0.5.3 +cachy 0.2.0 """ assert expected == tester.io.fetch_output() -def test_debug_resolve_tree_option_gives_the_dependency_tree(tester, repo): - cachy2 = get_package("cachy", "0.2.0") - cachy2.add_dependency(Factory.create_dependency("msgpack-python", ">=0.5 <0.6")) - - repo.add_package(get_package("cachy", "0.1.0")) - repo.add_package(cachy2) - repo.add_package(get_package("msgpack-python", "0.5.3")) - +def test_debug_resolve_tree_option_gives_the_dependency_tree(tester): tester.execute("cachy --tree") expected = """\ @@ -47,16 +46,13 @@ def test_debug_resolve_tree_option_gives_the_dependency_tree(tester, repo): Resolution results: cachy 0.2.0 -`-- msgpack-python >=0.5 <0.6 +└── msgpack-python >=0.5 <0.6 """ assert expected == tester.io.fetch_output() -def test_debug_resolve_git_dependency(tester, repo): - repo.add_package(get_package("pendulum", "2.0.3")) - repo.add_package(get_package("cleo", "0.6.5")) - +def test_debug_resolve_git_dependency(tester): tester.execute("git+https://github.com/demo/demo.git") expected = """\ @@ -64,8 +60,8 @@ def test_debug_resolve_git_dependency(tester, repo): Resolution results: -pendulum 2.0.3 -demo 0.1.2 +pendulum 2.0.3 +demo 0.1.2 """ assert expected == tester.io.fetch_output() diff --git a/tests/console/commands/env/conftest.py b/tests/console/commands/env/conftest.py index 5fbddf1aa6f..22b12f0d44b 100644 --- a/tests/console/commands/env/conftest.py +++ b/tests/console/commands/env/conftest.py @@ -1,8 +1,9 @@ import os +from pathlib import Path + import pytest -from poetry.utils._compat import Path from poetry.utils.env import EnvManager diff --git a/tests/console/commands/env/helpers.py b/tests/console/commands/env/helpers.py index 17d4c2ac274..b15a68d07a2 100644 --- a/tests/console/commands/env/helpers.py +++ b/tests/console/commands/env/helpers.py @@ -1,16 +1,16 @@ -from typing import Optional +from pathlib import Path +from typing import Any from typing import Union -from poetry.core.semver import Version -from poetry.utils._compat import Path +from poetry.core.semver.version import Version -def build_venv(path, executable=None): # type: (Union[Path,str], Optional[str]) -> () +def build_venv(path: Union[Path, str], **_: Any) -> (): Path(path).mkdir(parents=True, exist_ok=True) def check_output_wrapper(version=Version.parse("3.7.1")): - def check_output(cmd, *args, **kwargs): + def check_output(cmd, *_, **__): if "sys.version_info[:3]" in cmd: return version.text elif "sys.version_info[:2]" in cmd: diff --git a/tests/console/commands/env/test_info.py b/tests/console/commands/env/test_info.py index 9d1a0c8426a..123835ba7b9 100644 --- a/tests/console/commands/env/test_info.py +++ b/tests/console/commands/env/test_info.py @@ -1,6 +1,7 @@ +from pathlib import Path + import pytest -from poetry.utils._compat import Path from poetry.utils.env import MockEnv diff --git a/tests/console/commands/env/test_remove.py b/tests/console/commands/env/test_remove.py index 2b4f3ae775c..b95419bb6fb 100644 --- a/tests/console/commands/env/test_remove.py +++ b/tests/console/commands/env/test_remove.py @@ -13,7 +13,7 @@ def test_remove_by_python_version( mocker, tester, venvs_in_cache_dirs, venv_name, venv_cache ): check_output = mocker.patch( - "poetry.utils._compat.subprocess.check_output", + "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.6")), ) diff --git a/tests/console/commands/env/test_use.py b/tests/console/commands/env/test_use.py index 563e0ac7ffe..8b37c8e726e 100644 --- a/tests/console/commands/env/test_use.py +++ b/tests/console/commands/env/test_use.py @@ -1,11 +1,12 @@ import os +from pathlib import Path + import pytest import tomlkit -from poetry.core.semver import Version +from poetry.core.semver.version import Version from poetry.core.toml.file import TOMLFile -from poetry.utils._compat import Path from poetry.utils.env import MockEnv from tests.console.commands.env.helpers import build_venv from tests.console.commands.env.helpers import check_output_wrapper @@ -21,11 +22,11 @@ def setup(mocker): @pytest.fixture(autouse=True) def mock_subprocess_calls(setup, current_python, mocker): mocker.patch( - "poetry.utils._compat.subprocess.check_output", - side_effect=check_output_wrapper(Version(*current_python)), + "subprocess.check_output", + side_effect=check_output_wrapper(Version.from_parts(*current_python)), ) mocker.patch( - "poetry.utils._compat.subprocess.Popen.communicate", + "subprocess.Popen.communicate", side_effect=[("/prefix", None), ("/prefix", None), ("/prefix", None)], ) @@ -39,7 +40,7 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file( mocker, tester, venv_cache, venv_name, venvs_in_cache_config ): mocker.patch( - "poetry.utils._compat.subprocess.check_output", + "subprocess.check_output", side_effect=check_output_wrapper(), ) @@ -50,7 +51,14 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file( tester.execute("3.7") venv_py37 = venv_cache / "{}-py3.7".format(venv_name) - mock_build_env.assert_called_with(venv_py37, executable="python3.7") + mock_build_env.assert_called_with( + venv_py37, + executable="python3.7", + flags={"always-copy": False, "system-site-packages": False}, + with_pip=True, + with_setuptools=True, + with_wheel=True, + ) envs_file = TOMLFile(venv_cache / "envs.toml") assert envs_file.exists() @@ -62,7 +70,9 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file( Creating virtualenv {} in {} Using virtualenv: {} """.format( - venv_py37.name, venv_py37.parent, venv_py37, + venv_py37.name, + venv_py37.parent, + venv_py37, ) assert expected == tester.io.fetch_output() @@ -120,7 +130,9 @@ def test_get_prefers_explicitly_activated_non_existing_virtualenvs_over_env_var( Creating virtualenv {} in {} Using virtualenv: {} """.format( - venv_dir.name, venv_dir.parent, venv_dir, + venv_dir.name, + venv_dir.parent, + venv_dir, ) assert expected == tester.io.fetch_output() diff --git a/tests/console/commands/plugin/__init__.py b/tests/console/commands/plugin/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/console/commands/plugin/conftest.py b/tests/console/commands/plugin/conftest.py new file mode 100644 index 00000000000..84509c81172 --- /dev/null +++ b/tests/console/commands/plugin/conftest.py @@ -0,0 +1,35 @@ +import pytest + +from poetry.__version__ import __version__ +from poetry.core.packages.package import Package +from poetry.factory import Factory +from poetry.repositories.installed_repository import InstalledRepository +from poetry.repositories.pool import Pool +from poetry.utils.env import EnvManager + + +@pytest.fixture() +def installed(): + repository = InstalledRepository() + + repository.add_package(Package("poetry", __version__)) + + return repository + + +def configure_sources_factory(repo): + def _configure_sources(poetry, sources, config, io): # noqa + pool = Pool() + pool.add_repository(repo) + poetry.set_pool(pool) + + return _configure_sources + + +@pytest.fixture(autouse=True) +def setup_mocks(mocker, env, repo, installed): + mocker.patch.object(EnvManager, "get_system_env", return_value=env) + mocker.patch.object(InstalledRepository, "load", return_value=installed) + mocker.patch.object( + Factory, "configure_sources", side_effect=configure_sources_factory(repo) + ) diff --git a/tests/console/commands/plugin/test_add.py b/tests/console/commands/plugin/test_add.py new file mode 100644 index 00000000000..88c02812afd --- /dev/null +++ b/tests/console/commands/plugin/test_add.py @@ -0,0 +1,233 @@ +import pytest + +from poetry.core.packages.package import Package +from poetry.factory import Factory + + +@pytest.fixture() +def tester(command_tester_factory): + return command_tester_factory("plugin add") + + +def assert_plugin_add_result(tester, app, env, expected, constraint): + assert tester.io.fetch_output() == expected + + update_command = app.find("update") + assert update_command.poetry.file.parent == env.path + assert update_command.poetry.locker.lock.parent == env.path + assert update_command.poetry.locker.lock.exists() + + content = update_command.poetry.file.read()["tool"]["poetry"] + assert "poetry-plugin" in content["dependencies"] + assert content["dependencies"]["poetry-plugin"] == constraint + + +def test_add_no_constraint(app, repo, tester, env, installed): + repo.add_package(Package("poetry-plugin", "0.1.0")) + + tester.execute("poetry-plugin") + + expected = """\ +Using version ^0.1.0 for poetry-plugin +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 1 install, 0 updates, 0 removals + + • Installing poetry-plugin (0.1.0) +""" + assert_plugin_add_result(tester, app, env, expected, "^0.1.0") + + +def test_add_with_constraint(app, repo, tester, env, installed): + repo.add_package(Package("poetry-plugin", "0.1.0")) + repo.add_package(Package("poetry-plugin", "0.2.0")) + + tester.execute("poetry-plugin@^0.2.0") + + expected = """\ +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 1 install, 0 updates, 0 removals + + • Installing poetry-plugin (0.2.0) +""" + + assert_plugin_add_result(tester, app, env, expected, "^0.2.0") + + +def test_add_with_git_constraint(app, repo, tester, env, installed): + repo.add_package(Package("pendulum", "2.0.5")) + + tester.execute("git+https://github.com/demo/poetry-plugin.git") + + expected = """\ +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 2 installs, 0 updates, 0 removals + + • Installing pendulum (2.0.5) + • Installing poetry-plugin (0.1.2 9cf87a2) +""" + + assert_plugin_add_result( + tester, app, env, expected, {"git": "https://github.com/demo/poetry-plugin.git"} + ) + + +def test_add_with_git_constraint_with_extras(app, repo, tester, env, installed): + repo.add_package(Package("pendulum", "2.0.5")) + repo.add_package(Package("tomlkit", "0.7.0")) + + tester.execute("git+https://github.com/demo/poetry-plugin.git[foo]") + + expected = """\ +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 3 installs, 0 updates, 0 removals + + • Installing pendulum (2.0.5) + • Installing tomlkit (0.7.0) + • Installing poetry-plugin (0.1.2 9cf87a2) +""" + + assert_plugin_add_result( + tester, + app, + env, + expected, + { + "git": "https://github.com/demo/poetry-plugin.git", + "extras": ["foo"], + }, + ) + + +def test_add_existing_plugin_warns_about_no_operation( + app, repo, tester, env, installed +): + env.path.joinpath("pyproject.toml").write_text( + """\ +[tool.poetry] +name = "poetry" +version = "1.2.0" +description = "Python dependency management and packaging made easy." +authors = [ + "Sébastien Eustace " +] + +[tool.poetry.dependencies] +python = "^3.6" +poetry-plugin = "^1.2.3" +""", + encoding="utf-8", + ) + + installed.add_package(Package("poetry-plugin", "1.2.3")) + + repo.add_package(Package("poetry-plugin", "1.2.3")) + + tester.execute("poetry-plugin") + + expected = """\ +The following plugins are already present in the pyproject.toml file and will be skipped: + + • poetry-plugin + +If you want to update it to the latest compatible version, you can use `poetry plugin update package`. +If you prefer to upgrade it to the latest available version, you can use `poetry plugin add package@latest`. + +""" + + assert tester.io.fetch_output() == expected + + update_command = app.find("update") + # The update command should not have been called + assert update_command.poetry.file.parent != env.path + + +def test_add_existing_plugin_updates_if_requested( + app, repo, tester, env, installed, mocker +): + env.path.joinpath("pyproject.toml").write_text( + """\ +[tool.poetry] +name = "poetry" +version = "1.2.0" +description = "Python dependency management and packaging made easy." +authors = [ + "Sébastien Eustace " +] + +[tool.poetry.dependencies] +python = "^3.6" +poetry-plugin = "^1.2.3" +""", + encoding="utf-8", + ) + + installed.add_package(Package("poetry-plugin", "1.2.3")) + + repo.add_package(Package("poetry-plugin", "1.2.3")) + repo.add_package(Package("poetry-plugin", "2.3.4")) + + tester.execute("poetry-plugin@latest") + + expected = """\ +Using version ^2.3.4 for poetry-plugin +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 0 installs, 1 update, 0 removals + + • Updating poetry-plugin (1.2.3 -> 2.3.4) +""" + + assert_plugin_add_result(tester, app, env, expected, "^2.3.4") + + +def test_adding_a_plugin_can_update_poetry_dependencies_if_needed( + app, repo, tester, env, installed +): + poetry_package = Package("poetry", "1.2.0") + poetry_package.add_dependency(Factory.create_dependency("tomlkit", "^0.7.0")) + + plugin_package = Package("poetry-plugin", "1.2.3") + plugin_package.add_dependency(Factory.create_dependency("tomlkit", "^0.7.2")) + + installed.add_package(poetry_package) + installed.add_package(Package("tomlkit", "0.7.1")) + + repo.add_package(plugin_package) + repo.add_package(Package("tomlkit", "0.7.1")) + repo.add_package(Package("tomlkit", "0.7.2")) + + tester.execute("poetry-plugin") + + expected = """\ +Using version ^1.2.3 for poetry-plugin +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 1 install, 1 update, 0 removals + + • Updating tomlkit (0.7.1 -> 0.7.2) + • Installing poetry-plugin (1.2.3) +""" + + assert_plugin_add_result(tester, app, env, expected, "^1.2.3") diff --git a/tests/console/commands/plugin/test_remove.py b/tests/console/commands/plugin/test_remove.py new file mode 100644 index 00000000000..75c60422e3a --- /dev/null +++ b/tests/console/commands/plugin/test_remove.py @@ -0,0 +1,123 @@ +import pytest +import tomlkit + +from poetry.__version__ import __version__ +from poetry.core.packages.package import Package +from poetry.layouts.layout import POETRY_DEFAULT + + +@pytest.fixture() +def tester(command_tester_factory): + return command_tester_factory("plugin remove") + + +@pytest.fixture() +def pyproject(env): + pyproject = tomlkit.loads(POETRY_DEFAULT) + content = pyproject["tool"]["poetry"] + + content["name"] = "poetry" + content["version"] = __version__ + content["description"] = "" + content["authors"] = ["Sébastien Eustace "] + + dependency_section = content["dependencies"] + dependency_section["python"] = "^3.6" + + env.path.joinpath("pyproject.toml").write_text( + tomlkit.dumps(pyproject), encoding="utf-8" + ) + + +@pytest.fixture(autouse=True) +def install_plugin(env, installed, pyproject): + lock_content = { + "package": [ + { + "name": "poetry-plugin", + "version": "1.2.3", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + }, + ], + "metadata": { + "python-versions": "^3.6", + "platform": "*", + "content-hash": "123456789", + "hashes": {"poetry-plugin": []}, + }, + } + + env.path.joinpath("poetry.lock").write_text( + tomlkit.dumps(lock_content), encoding="utf-8" + ) + + pyproject = tomlkit.loads( + env.path.joinpath("pyproject.toml").read_text(encoding="utf-8") + ) + content = pyproject["tool"]["poetry"] + + dependency_section = content["dependencies"] + dependency_section["poetry-plugin"] = "^1.2.3" + + env.path.joinpath("pyproject.toml").write_text( + tomlkit.dumps(pyproject), encoding="utf-8" + ) + + installed.add_package(Package("poetry-plugin", "1.2.3")) + + +def test_remove_installed_package(app, tester, env): + tester.execute("poetry-plugin") + + expected = """\ +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 0 installs, 0 updates, 1 removal + + • Removing poetry-plugin (1.2.3) +""" + + assert tester.io.fetch_output() == expected + + remove_command = app.find("remove") + assert remove_command.poetry.file.parent == env.path + assert remove_command.poetry.locker.lock.parent == env.path + assert remove_command.poetry.locker.lock.exists() + assert not remove_command.installer.executor._dry_run + + content = remove_command.poetry.file.read()["tool"]["poetry"] + assert "poetry-plugin" not in content["dependencies"] + + +def test_remove_installed_package_dry_run(app, tester, env): + tester.execute("poetry-plugin --dry-run") + + expected = """\ +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 0 installs, 0 updates, 1 removal + + • Removing poetry-plugin (1.2.3) + • Removing poetry-plugin (1.2.3) +""" + + assert tester.io.fetch_output() == expected + + remove_command = app.find("remove") + assert remove_command.poetry.file.parent == env.path + assert remove_command.poetry.locker.lock.parent == env.path + assert remove_command.poetry.locker.lock.exists() + assert remove_command.installer.executor._dry_run + + content = remove_command.poetry.file.read()["tool"]["poetry"] + assert "poetry-plugin" in content["dependencies"] diff --git a/tests/console/commands/plugin/test_show.py b/tests/console/commands/plugin/test_show.py new file mode 100644 index 00000000000..8e075762dbf --- /dev/null +++ b/tests/console/commands/plugin/test_show.py @@ -0,0 +1,141 @@ +import pytest + +from entrypoints import EntryPoint as _EntryPoint + +from poetry.core.packages.package import Package +from poetry.factory import Factory +from poetry.plugins.application_plugin import ApplicationPlugin +from poetry.plugins.plugin import Plugin + + +class EntryPoint(_EntryPoint): + def load(self): + if "ApplicationPlugin" in self.object_name: + return ApplicationPlugin + + return Plugin + + +@pytest.fixture() +def tester(command_tester_factory): + return command_tester_factory("plugin show") + + +def test_show_displays_installed_plugins(app, tester, installed, mocker): + mocker.patch( + "entrypoints.get_group_all", + side_effect=[ + [ + EntryPoint( + "poetry-plugin", + "poetry_plugin.plugins:ApplicationPlugin", + "FirstApplicationPlugin", + ) + ], + [ + EntryPoint( + "poetry-plugin", + "poetry_plugin.plugins:Plugin", + "FirstPlugin", + ) + ], + ], + ) + + installed.add_package(Package("poetry-plugin", "1.2.3")) + + tester.execute("") + + expected = """ + • poetry-plugin (1.2.3) + 1 plugin and 1 application plugin +""" + + assert tester.io.fetch_output() == expected + + +def test_show_displays_installed_plugins_with_multiple_plugins( + app, tester, installed, mocker +): + mocker.patch( + "entrypoints.get_group_all", + side_effect=[ + [ + EntryPoint( + "poetry-plugin", + "poetry_plugin.plugins:ApplicationPlugin", + "FirstApplicationPlugin", + ), + EntryPoint( + "poetry-plugin", + "poetry_plugin.plugins:ApplicationPlugin", + "SecondApplicationPlugin", + ), + ], + [ + EntryPoint( + "poetry-plugin", + "poetry_plugin.plugins:Plugin", + "FirstPlugin", + ), + EntryPoint( + "poetry-plugin", + "poetry_plugin.plugins:Plugin", + "SecondPlugin", + ), + ], + ], + ) + + installed.add_package(Package("poetry-plugin", "1.2.3")) + + tester.execute("") + + expected = """ + • poetry-plugin (1.2.3) + 2 plugins and 2 application plugins +""" + + assert tester.io.fetch_output() == expected + + +def test_show_displays_installed_plugins_with_dependencies( + app, tester, installed, mocker +): + mocker.patch( + "entrypoints.get_group_all", + side_effect=[ + [ + EntryPoint( + "poetry-plugin", + "poetry_plugin.plugins:ApplicationPlugin", + "FirstApplicationPlugin", + ) + ], + [ + EntryPoint( + "poetry-plugin", + "poetry_plugin.plugins:Plugin", + "FirstPlugin", + ) + ], + ], + ) + + plugin = Package("poetry-plugin", "1.2.3") + plugin.add_dependency(Factory.create_dependency("foo", ">=1.2.3")) + plugin.add_dependency(Factory.create_dependency("bar", "<4.5.6")) + installed.add_package(plugin) + + tester.execute("") + + expected = """ + • poetry-plugin (1.2.3) + 1 plugin and 1 application plugin + + Dependencies + - foo (>=1.2.3) + - bar (<4.5.6) +""" + + assert tester.io.fetch_output() == expected diff --git a/tests/console/commands/self/test_update.py b/tests/console/commands/self/test_update.py index 6e094111ad0..396965e2e68 100644 --- a/tests/console/commands/self/test_update.py +++ b/tests/console/commands/self/test_update.py @@ -1,12 +1,16 @@ -import os +from pathlib import Path import pytest from poetry.__version__ import __version__ +from poetry.console.exceptions import PoetrySimpleConsoleException from poetry.core.packages.package import Package from poetry.core.semver.version import Version -from poetry.utils._compat import WINDOWS -from poetry.utils._compat import Path +from poetry.factory import Factory +from poetry.repositories.installed_repository import InstalledRepository +from poetry.repositories.pool import Pool +from poetry.repositories.repository import Repository +from poetry.utils.env import EnvManager FIXTURES = Path(__file__).parent.joinpath("fixtures") @@ -17,75 +21,87 @@ def tester(command_tester_factory): return command_tester_factory("self update") -def test_self_update_should_install_all_necessary_elements( - tester, http, mocker, environ, tmp_dir +def test_self_update_can_update_from_recommended_installation( + tester, http, mocker, environ, tmp_venv ): - os.environ["POETRY_HOME"] = tmp_dir + mocker.patch.object(EnvManager, "get_system_env", return_value=tmp_venv) - command = tester._command + command = tester.command + command._data_dir = tmp_venv.path.parent - version = Version.parse(__version__).next_minor.text - mocker.patch( - "poetry.repositories.pypi_repository.PyPiRepository.find_packages", - return_value=[Package("poetry", version)], - ) - mocker.patch.object(command, "_check_recommended_installation", return_value=None) - mocker.patch.object( - command, "_get_release_name", return_value="poetry-{}-darwin".format(version) - ) - mocker.patch("subprocess.check_output", return_value=b"Python 3.8.2") + new_version = Version.parse(__version__).next_minor().text - http.register_uri( - "GET", - command.BASE_URL + "/{}/poetry-{}-darwin.sha256sum".format(version, version), - body=FIXTURES.joinpath("poetry-1.0.5-darwin.sha256sum").read_bytes(), - ) - http.register_uri( - "GET", - command.BASE_URL + "/{}/poetry-{}-darwin.tar.gz".format(version, version), - body=FIXTURES.joinpath("poetry-1.0.5-darwin.tar.gz").read_bytes(), - ) + old_poetry = Package("poetry", __version__) + old_poetry.add_dependency(Factory.create_dependency("cleo", "^0.8.2")) + + new_poetry = Package("poetry", new_version) + new_poetry.add_dependency(Factory.create_dependency("cleo", "^1.0.0")) + + installed_repository = Repository() + installed_repository.add_package(old_poetry) + installed_repository.add_package(Package("cleo", "0.8.2")) + + repository = Repository() + repository.add_package(new_poetry) + repository.add_package(Package("cleo", "1.0.0")) + + pool = Pool() + pool.add_repository(repository) + + command._pool = pool + + mocker.patch.object(InstalledRepository, "load", return_value=installed_repository) tester.execute() - bin_ = Path(tmp_dir).joinpath("bin") - lib = Path(tmp_dir).joinpath("lib") - assert bin_.exists() - - script = bin_.joinpath("poetry") - assert script.exists() - - expected_script = """\ -# -*- coding: utf-8 -*- -import glob -import sys -import os - -lib = os.path.normpath(os.path.join(os.path.realpath(__file__), "../..", "lib")) -vendors = os.path.join(lib, "poetry", "_vendor") -current_vendors = os.path.join( - vendors, "py{}".format(".".join(str(v) for v in sys.version_info[:2])) -) -sys.path.insert(0, lib) -sys.path.insert(0, current_vendors) - -if __name__ == "__main__": - from poetry.console import main - main() -""" - if not WINDOWS: - expected_script = "#!/usr/bin/env python\n" + expected_script - - assert expected_script == script.read_text() - - if WINDOWS: - bat = bin_.joinpath("poetry.bat") - expected_bat = '@echo off\r\npython "{}" %*\r\n'.format( - str(script).replace(os.environ.get("USERPROFILE", ""), "%USERPROFILE%") - ) - assert bat.exists() - with bat.open(newline="") as f: - assert expected_bat == f.read() - - assert lib.exists() - assert lib.joinpath("poetry").exists() + expected_output = """\ +Updating Poetry to 1.2.0 + +Updating dependencies +Resolving dependencies... + +Package operations: 0 installs, 2 updates, 0 removals + + - Updating cleo (0.8.2 -> 1.0.0) + - Updating poetry ({} -> {}) + +Updating the poetry script + +Poetry ({}) is installed now. Great! +""".format( + __version__, new_version, new_version + ) + + assert tester.io.fetch_output() == expected_output + + +def test_self_update_does_not_update_non_recommended_installation( + tester, http, mocker, environ, tmp_venv +): + mocker.patch.object(EnvManager, "get_system_env", return_value=tmp_venv) + + command = tester.command + + new_version = Version.parse(__version__).next_minor().text + + old_poetry = Package("poetry", __version__) + old_poetry.add_dependency(Factory.create_dependency("cleo", "^0.8.2")) + + new_poetry = Package("poetry", new_version) + new_poetry.add_dependency(Factory.create_dependency("cleo", "^1.0.0")) + + installed_repository = Repository() + installed_repository.add_package(old_poetry) + installed_repository.add_package(Package("cleo", "0.8.2")) + + repository = Repository() + repository.add_package(new_poetry) + repository.add_package(Package("cleo", "1.0.0")) + + pool = Pool() + pool.add_repository(repository) + + command._pool = pool + + with pytest.raises(PoetrySimpleConsoleException): + tester.execute() diff --git a/tests/console/commands/source/__init__.py b/tests/console/commands/source/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/console/commands/source/conftest.py b/tests/console/commands/source/conftest.py new file mode 100644 index 00000000000..fc3c38b181f --- /dev/null +++ b/tests/console/commands/source/conftest.py @@ -0,0 +1,63 @@ +import pytest + +from poetry.config.source import Source + + +@pytest.fixture +def source_one(): + return Source(name="one", url="https://one.com") + + +@pytest.fixture +def source_two(): + return Source(name="two", url="https://two.com") + + +@pytest.fixture +def source_default(): + return Source(name="default", url="https://default.com", default=True) + + +@pytest.fixture +def source_secondary(): + return Source(name="secondary", url="https://secondary.com", secondary=True) + + +_existing_source = Source(name="existing", url="https://existing.com") + + +@pytest.fixture +def source_existing(): + return _existing_source + + +PYPROJECT_WITH_SOURCES = f""" +[tool.poetry] +name = "source-command-test" +version = "0.1.0" +description = "" +authors = ["Poetry Tester "] + +[tool.poetry.dependencies] +python = "^3.9" + +[tool.poetry.dev-dependencies] + +[[tool.poetry.source]] +name = "{_existing_source.name}" +url = "{_existing_source.url}" +""" + + +@pytest.fixture +def poetry_with_source(project_factory): + return project_factory(pyproject_content=PYPROJECT_WITH_SOURCES) + + +@pytest.fixture +def add_multiple_sources( + command_tester_factory, poetry_with_source, source_one, source_two +): + add = command_tester_factory("source add", poetry=poetry_with_source) + for source in [source_one, source_two]: + add.execute(f"{source.name} {source.url}") diff --git a/tests/console/commands/source/test_add.py b/tests/console/commands/source/test_add.py new file mode 100644 index 00000000000..97a407918dd --- /dev/null +++ b/tests/console/commands/source/test_add.py @@ -0,0 +1,71 @@ +import dataclasses + +import pytest + + +@pytest.fixture +def tester(command_tester_factory, poetry_with_source): + return command_tester_factory("source add", poetry=poetry_with_source) + + +def assert_source_added(tester, poetry, source_existing, source_added): + assert ( + tester.io.fetch_output().strip() + == f"Adding source with name {source_added.name}." + ) + poetry.pyproject.reload() + sources = poetry.get_sources() + assert sources == [source_existing, source_added] + assert tester.status_code == 0 + + +def test_source_add_simple(tester, source_existing, source_one, poetry_with_source): + tester.execute(f"{source_one.name} {source_one.url}") + assert_source_added(tester, poetry_with_source, source_existing, source_one) + + +def test_source_add_default( + tester, source_existing, source_default, poetry_with_source +): + tester.execute(f"--default {source_default.name} {source_default.url}") + assert_source_added(tester, poetry_with_source, source_existing, source_default) + + +def test_source_add_secondary( + tester, source_existing, source_secondary, poetry_with_source +): + tester.execute(f"--secondary {source_secondary.name} {source_secondary.url}") + assert_source_added(tester, poetry_with_source, source_existing, source_secondary) + + +def test_source_add_error_default_and_secondary(tester): + tester.execute("--default --secondary error https://error.com") + assert ( + tester.io.fetch_error().strip() + == "Cannot configure a source as both default and secondary." + ) + assert tester.status_code == 1 + + +def test_source_add_error_pypi(tester): + tester.execute("pypi https://test.pypi.org/simple/") + assert ( + tester.io.fetch_error().strip() + == "Failed to validate addition of pypi: The name [pypi] is reserved for repositories" + ) + assert tester.status_code == 1 + + +def test_source_add_existing(tester, source_existing, poetry_with_source): + tester.execute(f"--default {source_existing.name} {source_existing.url}") + assert ( + tester.io.fetch_output().strip() + == f"Source with name {source_existing.name} already exits. Updating." + ) + + poetry_with_source.pyproject.reload() + sources = poetry_with_source.get_sources() + + assert len(sources) == 1 + assert sources[0] != source_existing + assert sources[0] == dataclasses.replace(source_existing, default=True) diff --git a/tests/console/commands/source/test_remove.py b/tests/console/commands/source/test_remove.py new file mode 100644 index 00000000000..33f35a8080b --- /dev/null +++ b/tests/console/commands/source/test_remove.py @@ -0,0 +1,28 @@ +import pytest + + +@pytest.fixture +def tester(command_tester_factory, poetry_with_source, add_multiple_sources): + return command_tester_factory("source remove", poetry=poetry_with_source) + + +def test_source_remove_simple( + tester, poetry_with_source, source_existing, source_one, source_two +): + tester.execute(f"{source_existing.name}") + assert ( + tester.io.fetch_output().strip() + == f"Removing source with name {source_existing.name}." + ) + + poetry_with_source.pyproject.reload() + sources = poetry_with_source.get_sources() + assert sources == [source_one, source_two] + + assert tester.status_code == 0 + + +def test_source_remove_error(tester): + tester.execute("error") + assert tester.io.fetch_error().strip() == "Source with name error was not found." + assert tester.status_code == 1 diff --git a/tests/console/commands/source/test_show.py b/tests/console/commands/source/test_show.py new file mode 100644 index 00000000000..8aa961cf147 --- /dev/null +++ b/tests/console/commands/source/test_show.py @@ -0,0 +1,75 @@ +import pytest + + +@pytest.fixture +def tester(command_tester_factory, poetry_with_source, add_multiple_sources): + return command_tester_factory("source show", poetry=poetry_with_source) + + +def test_source_show_simple(tester): + tester.execute("") + + expected = """\ +name : existing +url : https://existing.com +default : no +secondary : no + +name : one +url : https://one.com +default : no +secondary : no + +name : two +url : https://two.com +default : no +secondary : no +""".splitlines() + assert ( + list(map(lambda l: l.strip(), tester.io.fetch_output().strip().splitlines())) + == expected + ) + assert tester.status_code == 0 + + +def test_source_show_one(tester, source_one): + tester.execute(f"{source_one.name}") + + expected = """\ +name : one +url : https://one.com +default : no +secondary : no +""".splitlines() + assert ( + list(map(lambda l: l.strip(), tester.io.fetch_output().strip().splitlines())) + == expected + ) + assert tester.status_code == 0 + + +def test_source_show_two(tester, source_one, source_two): + tester.execute(f"{source_one.name} {source_two.name}") + + expected = """\ +name : one +url : https://one.com +default : no +secondary : no + +name : two +url : https://two.com +default : no +secondary : no +""".splitlines() + assert ( + list(map(lambda l: l.strip(), tester.io.fetch_output().strip().splitlines())) + == expected + ) + assert tester.status_code == 0 + + +def test_source_show_error(tester): + tester.execute("error") + assert tester.io.fetch_error().strip() == "No source found with name(s): error" + assert tester.status_code == 1 diff --git a/tests/console/commands/test_add.py b/tests/console/commands/test_add.py index 09fae525aec..26955345457 100644 --- a/tests/console/commands/test_add.py +++ b/tests/console/commands/test_add.py @@ -1,13 +1,11 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import sys +from pathlib import Path + import pytest -from poetry.core.semver import Version +from poetry.core.semver.version import Version from poetry.repositories.legacy_repository import LegacyRepository -from poetry.utils._compat import Path from tests.helpers import get_dependency from tests.helpers import get_package @@ -19,7 +17,7 @@ def tester(command_tester_factory): @pytest.fixture() def old_tester(tester): - tester._command.installer.use_executor(False) + tester.command.installer.use_executor(False) return tester @@ -44,7 +42,7 @@ def test_add_no_constraint(app, repo, tester): """ assert expected == tester.io.fetch_output() - assert 1 == tester._command.installer.executor.installations_count + assert 1 == tester.command.installer.executor.installations_count content = app.poetry.file.read()["tool"]["poetry"] @@ -52,6 +50,24 @@ def test_add_no_constraint(app, repo, tester): assert content["dependencies"]["cachy"] == "^0.2.0" +def test_add_no_constraint_editable_error(app, repo, tester): + content = app.poetry.file.read()["tool"]["poetry"] + + repo.add_package(get_package("cachy", "0.2.0")) + + tester.execute("-e cachy") + + expected = """ +Failed to add packages. Only vcs/path dependencies support editable installs. cachy is neither. + +No changes were applied. +""" + assert 1 == tester.status_code + assert expected == tester.io.fetch_error() + assert 0 == tester.command.installer.executor.installations_count + assert content == app.poetry.file.read()["tool"]["poetry"] + + def test_add_equal_constraint(app, repo, tester): repo.add_package(get_package("cachy", "0.1.0")) repo.add_package(get_package("cachy", "0.2.0")) @@ -71,7 +87,7 @@ def test_add_equal_constraint(app, repo, tester): """ assert expected == tester.io.fetch_output() - assert 1 == tester._command.installer.executor.installations_count + assert 1 == tester.command.installer.executor.installations_count def test_add_greater_constraint(app, repo, tester): @@ -93,7 +109,7 @@ def test_add_greater_constraint(app, repo, tester): """ assert expected == tester.io.fetch_output() - assert 1 == tester._command.installer.executor.installations_count + assert 1 == tester.command.installer.executor.installations_count def test_add_constraint_with_extras(app, repo, tester): @@ -122,7 +138,7 @@ def test_add_constraint_with_extras(app, repo, tester): """ assert expected == tester.io.fetch_output() - assert 2 == tester._command.installer.executor.installations_count + assert 2 == tester.command.installer.executor.installations_count def test_add_constraint_dependencies(app, repo, tester): @@ -150,11 +166,11 @@ def test_add_constraint_dependencies(app, repo, tester): """ assert expected == tester.io.fetch_output() - assert 2 == tester._command.installer.executor.installations_count + assert 2 == tester.command.installer.executor.installations_count def test_add_git_constraint(app, repo, tester, tmp_venv): - tester._command.set_env(tmp_venv) + tester.command.set_env(tmp_venv) repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cleo", "0.6.5")) @@ -175,7 +191,7 @@ def test_add_git_constraint(app, repo, tester, tmp_venv): """ assert expected == tester.io.fetch_output() - assert 2 == tester._command.installer.executor.installations_count + assert 2 == tester.command.installer.executor.installations_count content = app.poetry.file.read()["tool"]["poetry"] @@ -186,7 +202,7 @@ def test_add_git_constraint(app, repo, tester, tmp_venv): def test_add_git_constraint_with_poetry(app, repo, tester, tmp_venv): - tester._command.set_env(tmp_venv) + tester.command.set_env(tmp_venv) repo.add_package(get_package("pendulum", "1.4.4")) @@ -206,11 +222,11 @@ def test_add_git_constraint_with_poetry(app, repo, tester, tmp_venv): """ assert expected == tester.io.fetch_output() - assert 2 == tester._command.installer.executor.installations_count + assert 2 == tester.command.installer.executor.installations_count def test_add_git_constraint_with_extras(app, repo, tester, tmp_venv): - tester._command.set_env(tmp_venv) + tester.command.set_env(tmp_venv) repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cleo", "0.6.5")) @@ -234,7 +250,7 @@ def test_add_git_constraint_with_extras(app, repo, tester, tmp_venv): """ assert expected.strip() == tester.io.fetch_output().strip() - assert 4 == tester._command.installer.executor.installations_count + assert 4 == tester.command.installer.executor.installations_count content = app.poetry.file.read()["tool"]["poetry"] @@ -245,13 +261,15 @@ def test_add_git_constraint_with_extras(app, repo, tester, tmp_venv): } -def test_add_git_ssh_constraint(app, repo, tester, tmp_venv): - tester._command.set_env(tmp_venv) +@pytest.mark.parametrize("editable", [False, True]) +def test_add_git_ssh_constraint(editable, app, repo, tester, tmp_venv): + tester.command.set_env(tmp_venv) 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") + url = "git+ssh://git@github.com/demo/demo.git@develop" + tester.execute(f"{url}" if not editable else f"-e {url}") expected = """\ @@ -267,26 +285,32 @@ def test_add_git_ssh_constraint(app, repo, tester, tmp_venv): """ assert expected == tester.io.fetch_output() - assert 2 == tester._command.installer.executor.installations_count + assert 2 == tester.command.installer.executor.installations_count content = app.poetry.file.read()["tool"]["poetry"] assert "demo" in content["dependencies"] - assert content["dependencies"]["demo"] == { + + expected = { "git": "ssh://git@github.com/demo/demo.git", "rev": "develop", } + if editable: + expected["develop"] = True + + assert content["dependencies"]["demo"] == expected -def test_add_directory_constraint(app, repo, tester, mocker): - p = mocker.patch("poetry.utils._compat.Path.cwd") +@pytest.mark.parametrize("editable", [False, True]) +def test_add_directory_constraint(editable, app, repo, tester, mocker): + p = mocker.patch("pathlib.Path.cwd") p.return_value = Path(__file__).parent repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cleo", "0.6.5")) path = "../git/github.com/demo/demo" - tester.execute("{}".format(path)) + tester.execute(f"{path}" if not editable else f"-e {path}") expected = """\ @@ -304,16 +328,21 @@ def test_add_directory_constraint(app, repo, tester, mocker): ) assert expected == tester.io.fetch_output() - assert 2 == tester._command.installer.executor.installations_count + assert 2 == tester.command.installer.executor.installations_count content = app.poetry.file.read()["tool"]["poetry"] assert "demo" in content["dependencies"] - assert content["dependencies"]["demo"] == {"path": "../git/github.com/demo/demo"} + + expected = {"path": "../git/github.com/demo/demo"} + if editable: + expected["develop"] = True + + assert content["dependencies"]["demo"] == expected def test_add_directory_with_poetry(app, repo, tester, mocker): - p = mocker.patch("poetry.utils._compat.Path.cwd") + p = mocker.patch("pathlib.Path.cwd") p.return_value = Path(__file__) / ".." repo.add_package(get_package("pendulum", "1.4.4")) @@ -337,11 +366,11 @@ def test_add_directory_with_poetry(app, repo, tester, mocker): ) assert expected == tester.io.fetch_output() - assert 2 == tester._command.installer.executor.installations_count + assert 2 == tester.command.installer.executor.installations_count def test_add_file_constraint_wheel(app, repo, tester, mocker, poetry): - p = mocker.patch("poetry.utils._compat.Path.cwd") + p = mocker.patch("pathlib.Path.cwd") p.return_value = poetry.file.parent repo.add_package(get_package("pendulum", "1.4.4")) @@ -365,7 +394,7 @@ def test_add_file_constraint_wheel(app, repo, tester, mocker, poetry): ) assert expected == tester.io.fetch_output() - assert 2 == tester._command.installer.executor.installations_count + assert 2 == tester.command.installer.executor.installations_count content = app.poetry.file.read()["tool"]["poetry"] @@ -376,7 +405,7 @@ def test_add_file_constraint_wheel(app, repo, tester, mocker, poetry): def test_add_file_constraint_sdist(app, repo, tester, mocker): - p = mocker.patch("poetry.utils._compat.Path.cwd") + p = mocker.patch("pathlib.Path.cwd") p.return_value = Path(__file__) / ".." repo.add_package(get_package("pendulum", "1.4.4")) @@ -400,7 +429,7 @@ def test_add_file_constraint_sdist(app, repo, tester, mocker): ) assert expected == tester.io.fetch_output() - assert 2 == tester._command.installer.executor.installations_count + assert 2 == tester.command.installer.executor.installations_count content = app.poetry.file.read()["tool"]["poetry"] @@ -436,7 +465,7 @@ def test_add_constraint_with_extras_option(app, repo, tester): """ assert expected == tester.io.fetch_output() - assert 2 == tester._command.installer.executor.installations_count + assert 2 == tester.command.installer.executor.installations_count content = app.poetry.file.read()["tool"]["poetry"] @@ -448,7 +477,7 @@ def test_add_constraint_with_extras_option(app, repo, tester): def test_add_url_constraint_wheel(app, repo, tester, mocker): - p = mocker.patch("poetry.utils._compat.Path.cwd") + p = mocker.patch("pathlib.Path.cwd") p.return_value = Path(__file__) / ".." repo.add_package(get_package("pendulum", "1.4.4")) @@ -471,7 +500,7 @@ def test_add_url_constraint_wheel(app, repo, tester, mocker): """ assert expected == tester.io.fetch_output() - assert 2 == tester._command.installer.executor.installations_count + assert 2 == tester.command.installer.executor.installations_count content = app.poetry.file.read()["tool"]["poetry"] @@ -508,7 +537,7 @@ def test_add_url_constraint_wheel_with_extras(app, repo, tester, mocker): expected = set(expected.splitlines()) output = set(tester.io.fetch_output().splitlines()) assert expected == output - assert 4 == tester._command.installer.executor.installations_count + assert 4 == tester.command.installer.executor.installations_count content = app.poetry.file.read()["tool"]["poetry"] @@ -540,7 +569,7 @@ def test_add_constraint_with_python(app, repo, tester): """ assert expected == tester.io.fetch_output() - assert 1 == tester._command.installer.executor.installations_count + assert 1 == tester.command.installer.executor.installations_count content = app.poetry.file.read()["tool"]["poetry"] @@ -572,7 +601,7 @@ def test_add_constraint_with_platform(app, repo, tester, env): """ assert expected == tester.io.fetch_output() - assert 1 == tester._command.installer.executor.installations_count + assert 1 == tester.command.installer.executor.installations_count content = app.poetry.file.read()["tool"]["poetry"] @@ -605,7 +634,7 @@ def test_add_constraint_with_source(app, poetry, tester): """ assert expected == tester.io.fetch_output() - assert 1 == tester._command.installer.executor.installations_count + assert 1 == tester.command.installer.executor.installations_count content = app.poetry.file.read()["tool"]["poetry"] @@ -658,7 +687,7 @@ def test_add_to_section_that_does_no_exist_yet(app, repo, tester): """ assert expected == tester.io.fetch_output() - assert 1 == tester._command.installer.executor.installations_count + assert 1 == tester.command.installer.executor.installations_count content = app.poetry.file.read()["tool"]["poetry"] @@ -686,7 +715,7 @@ def test_add_should_not_select_prereleases(app, repo, tester): """ assert expected == tester.io.fetch_output() - assert 1 == tester._command.installer.executor.installations_count + assert 1 == tester.command.installer.executor.installations_count content = app.poetry.file.read()["tool"]["poetry"] @@ -1069,7 +1098,7 @@ def test_add_git_ssh_constraint_old_installer(app, repo, installer, old_tester): def test_add_directory_constraint_old_installer( app, repo, installer, mocker, old_tester ): - p = mocker.patch("poetry.utils._compat.Path.cwd") + p = mocker.patch("pathlib.Path.cwd") p.return_value = Path(__file__) / ".." repo.add_package(get_package("pendulum", "1.4.4")) @@ -1106,7 +1135,7 @@ def test_add_directory_constraint_old_installer( def test_add_directory_with_poetry_old_installer( app, repo, installer, mocker, old_tester ): - p = mocker.patch("poetry.utils._compat.Path.cwd") + p = mocker.patch("pathlib.Path.cwd") p.return_value = Path(__file__) / ".." repo.add_package(get_package("pendulum", "1.4.4")) @@ -1137,7 +1166,7 @@ def test_add_directory_with_poetry_old_installer( def test_add_file_constraint_wheel_old_installer( app, repo, installer, mocker, old_tester ): - p = mocker.patch("poetry.utils._compat.Path.cwd") + p = mocker.patch("pathlib.Path.cwd") p.return_value = Path(__file__) / ".." repo.add_package(get_package("pendulum", "1.4.4")) @@ -1175,7 +1204,7 @@ def test_add_file_constraint_wheel_old_installer( def test_add_file_constraint_sdist_old_installer( app, repo, installer, mocker, old_tester ): - p = mocker.patch("poetry.utils._compat.Path.cwd") + p = mocker.patch("pathlib.Path.cwd") p.return_value = Path(__file__) / ".." repo.add_package(get_package("pendulum", "1.4.4")) @@ -1253,7 +1282,7 @@ def test_add_constraint_with_extras_option_old_installer( def test_add_url_constraint_wheel_old_installer( app, repo, installer, mocker, old_tester ): - p = mocker.patch("poetry.utils._compat.Path.cwd") + p = mocker.patch("pathlib.Path.cwd") p.return_value = Path(__file__) / ".." repo.add_package(get_package("pendulum", "1.4.4")) @@ -1630,3 +1659,26 @@ def test_add_with_lock_old_installer(app, repo, installer, old_tester): """ assert expected == old_tester.io.fetch_output() + + +def test_add_keyboard_interrupt_restore_content(app, repo, installer, tester, mocker): + mocker.patch( + "poetry.installation.installer.Installer.run", side_effect=KeyboardInterrupt() + ) + original_content = app.poetry.file.read() + + repo.add_package(get_package("cachy", "0.2.0")) + + tester.execute("cachy --dry-run") + + assert original_content == app.poetry.file.read() + + +def test_dry_run_restore_original_content(app, repo, installer, tester): + original_content = app.poetry.file.read() + + repo.add_package(get_package("cachy", "0.2.0")) + + tester.execute("cachy --dry-run") + + assert original_content == app.poetry.file.read() diff --git a/tests/console/commands/test_cache.py b/tests/console/commands/test_cache.py index a8d47842d1e..f33fa9ad31a 100644 --- a/tests/console/commands/test_cache.py +++ b/tests/console/commands/test_cache.py @@ -5,9 +5,9 @@ @pytest.fixture def repository_cache_dir(monkeypatch, tmpdir): - import poetry.locations + from pathlib import Path - from poetry.utils._compat import Path + import poetry.locations path = Path(str(tmpdir)) monkeypatch.setattr(poetry.locations, "REPOSITORY_CACHE_DIR", path) diff --git a/tests/console/commands/test_check.py b/tests/console/commands/test_check.py index caa5485154f..90225bd6857 100644 --- a/tests/console/commands/test_check.py +++ b/tests/console/commands/test_check.py @@ -1,7 +1,6 @@ -import pytest +from pathlib import Path -from poetry.utils._compat import PY2 -from poetry.utils._compat import Path +import pytest @pytest.fixture() @@ -30,14 +29,7 @@ def test_check_invalid(mocker, tester): tester.execute() - if PY2: - expected = """\ -Error: u'description' is a required property -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-prereleases" instead. -""" - else: - expected = """\ + expected = """\ Error: 'description' is a required property 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-prereleases" instead. diff --git a/tests/console/commands/test_config.py b/tests/console/commands/test_config.py index 4bf102073d5..9cb692b40aa 100644 --- a/tests/console/commands/test_config.py +++ b/tests/console/commands/test_config.py @@ -4,6 +4,7 @@ import pytest from poetry.config.config_source import ConfigSource +from poetry.core.pyproject.exceptions import PyProjectException from poetry.factory import Factory @@ -12,33 +13,53 @@ def tester(command_tester_factory): return command_tester_factory("config") -def test_list_displays_default_value_if_not_set(tester, config): +def test_show_config_with_local_config_file_empty(tester, mocker): + mocker.patch( + "poetry.factory.Factory.create_poetry", + side_effect=PyProjectException("[tool.poetry] section not found"), + ) + tester.execute() + + assert "" == tester.io.fetch_output() + + +def test_list_displays_default_value_if_not_set(tester, config, config_cache_dir): tester.execute("--list") - expected = """cache-dir = "/foo" + expected = """cache-dir = {cache} experimental.new-installer = true +installer.parallel = true virtualenvs.create = true virtualenvs.in-project = null -virtualenvs.path = {path} # /foo{sep}virtualenvs +virtualenvs.options.always-copy = false +virtualenvs.options.system-site-packages = false +virtualenvs.path = {path} # {virtualenvs} """.format( - path=json.dumps(os.path.join("{cache-dir}", "virtualenvs")), sep=os.path.sep + cache=json.dumps(str(config_cache_dir)), + path=json.dumps(os.path.join("{cache-dir}", "virtualenvs")), + virtualenvs=str(config_cache_dir / "virtualenvs"), ) assert expected == tester.io.fetch_output() -def test_list_displays_set_get_setting(tester, config): +def test_list_displays_set_get_setting(tester, config, config_cache_dir): tester.execute("virtualenvs.create false") tester.execute("--list") - expected = """cache-dir = "/foo" + expected = """cache-dir = {cache} experimental.new-installer = true +installer.parallel = true virtualenvs.create = false virtualenvs.in-project = null -virtualenvs.path = {path} # /foo{sep}virtualenvs +virtualenvs.options.always-copy = false +virtualenvs.options.system-site-packages = false +virtualenvs.path = {path} # {virtualenvs} """.format( - path=json.dumps(os.path.join("{cache-dir}", "virtualenvs")), sep=os.path.sep + cache=json.dumps(str(config_cache_dir)), + path=json.dumps(os.path.join("{cache-dir}", "virtualenvs")), + virtualenvs=str(config_cache_dir / "virtualenvs"), ) assert 0 == config.set_config_source.call_count @@ -66,18 +87,23 @@ def test_display_single_local_setting(command_tester_factory, fixture_dir): assert expected == tester.io.fetch_output() -def test_list_displays_set_get_local_setting(tester, config): +def test_list_displays_set_get_local_setting(tester, config, config_cache_dir): tester.execute("virtualenvs.create false --local") tester.execute("--list") - expected = """cache-dir = "/foo" + expected = """cache-dir = {cache} experimental.new-installer = true +installer.parallel = true virtualenvs.create = false virtualenvs.in-project = null -virtualenvs.path = {path} # /foo{sep}virtualenvs +virtualenvs.options.always-copy = false +virtualenvs.options.system-site-packages = false +virtualenvs.path = {path} # {virtualenvs} """.format( - path=json.dumps(os.path.join("{cache-dir}", "virtualenvs")), sep=os.path.sep + cache=json.dumps(str(config_cache_dir)), + path=json.dumps(os.path.join("{cache-dir}", "virtualenvs")), + virtualenvs=str(config_cache_dir / "virtualenvs"), ) assert 1 == config.set_config_source.call_count @@ -108,3 +134,23 @@ def test_set_cert(tester, auth_config_source, mocker): tester.execute("certificates.foo.cert path/to/ca.pem") assert "path/to/ca.pem" == auth_config_source.config["certificates"]["foo"]["cert"] + + +def test_config_installer_parallel(tester, command_tester_factory): + tester.execute("--local installer.parallel") + assert tester.io.fetch_output().strip() == "true" + + workers = command_tester_factory( + "install" + )._command._installer._executor._max_workers + assert workers > 1 + + tester.io.clear_output() + tester.execute("--local installer.parallel false") + tester.execute("--local installer.parallel") + assert tester.io.fetch_output().strip() == "false" + + workers = command_tester_factory( + "install" + )._command._installer._executor._max_workers + assert workers == 1 diff --git a/tests/console/commands/test_init.py b/tests/console/commands/test_init.py index c64bc23316a..2d6301e05a0 100644 --- a/tests/console/commands/test_init.py +++ b/tests/console/commands/test_init.py @@ -1,26 +1,43 @@ +import os +import shutil import sys +from pathlib import Path + import pytest -from poetry.utils._compat import Path +from cleo.testers.command_tester import CommandTester + +from poetry.repositories import Pool from poetry.utils._compat import decode +from tests.helpers import TestApplication from tests.helpers import get_package @pytest.fixture -def source_dir(tmp_path): # type: (Path) -> Path - yield Path(tmp_path.as_posix()) +def source_dir(tmp_path) -> Path: + cwd = os.getcwd() + + try: + os.chdir(str(tmp_path)) + yield Path(tmp_path.as_posix()) + finally: + os.chdir(cwd) -@pytest.fixture(autouse=True) -def patches(mocker, source_dir): - patch = mocker.patch("poetry.utils._compat.Path.cwd") - patch.return_value = source_dir +@pytest.fixture +def patches(mocker, source_dir, repo): + mocker.patch("pathlib.Path.cwd", return_value=source_dir) + mocker.patch( + "poetry.console.commands.init.InitCommand._get_pool", return_value=Pool([repo]) + ) @pytest.fixture -def tester(command_tester_factory): - return command_tester_factory("init") +def tester(patches): + # we need a test application without poetry here. + app = TestApplication(None) + return CommandTester(app.find("init")) @pytest.fixture @@ -49,6 +66,8 @@ def init_basic_toml(): description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" @@ -62,6 +81,28 @@ def test_basic_interactive(tester, init_basic_inputs, init_basic_toml): assert init_basic_toml in tester.io.fetch_output() +def test_noninteractive(app, mocker, poetry, repo, tmp_path): + command = app.find("init") + command._pool = poetry.pool + + repo.add_package(get_package("pytest", "3.6.0")) + + p = mocker.patch("poetry.utils._compat.Path.cwd") + p.return_value = tmp_path + + tester = CommandTester(command) + args = "--name my-package --dependency pytest" + tester.execute(args=args, interactive=False) + + expected = "Using version ^3.6.0 for pytest\n" + assert tester.io.fetch_output() == expected + assert "" == tester.io.fetch_error() + + toml_content = (tmp_path / "pyproject.toml").read_text() + assert 'name = "my-package"' in toml_content + assert 'pytest = "^3.6.0"' in toml_content + + def test_interactive_with_dependencies(tester, repo): repo.add_package(get_package("django-pendulum", "0.1.6-pre4")) repo.add_package(get_package("pendulum", "2.0.0")) @@ -95,6 +136,8 @@ def test_interactive_with_dependencies(tester, repo): description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" @@ -127,6 +170,8 @@ def test_empty_license(tester): version = "1.2.3" description = "" authors = ["Your Name "] +readme = "README.md" +packages = [{{include = "my_package"}}] [tool.poetry.dependencies] python = "^{python}" @@ -135,7 +180,6 @@ def test_empty_license(tester): """.format( python=".".join(str(c) for c in sys.version_info[:2]) ) - assert expected in tester.io.fetch_output() @@ -169,6 +213,8 @@ def test_interactive_with_git_dependencies(tester, repo): description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" @@ -211,6 +257,8 @@ def test_interactive_with_git_dependencies_with_reference(tester, repo): description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" @@ -253,6 +301,8 @@ def test_interactive_with_git_dependencies_and_other_name(tester, repo): description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" @@ -265,10 +315,13 @@ def test_interactive_with_git_dependencies_and_other_name(tester, repo): assert expected in tester.io.fetch_output() -def test_interactive_with_directory_dependency(tester, repo): +def test_interactive_with_directory_dependency(tester, repo, source_dir, fixture_dir): repo.add_package(get_package("pendulum", "2.0.0")) repo.add_package(get_package("pytest", "3.6.0")) + demo = fixture_dir("git") / "github.com" / "demo" / "demo" + shutil.copytree(str(demo), str(source_dir / "demo")) + inputs = [ "my-package", # Package name "1.2.3", # Version @@ -277,7 +330,7 @@ def test_interactive_with_directory_dependency(tester, repo): "MIT", # License "~2.7 || ^3.6", # Python "", # Interactive packages - "../../fixtures/git/github.com/demo/demo", # Search for package + "./demo", # Search for package "", # Stop searching for packages "", # Interactive dev packages "pytest", # Search for package @@ -295,22 +348,28 @@ def test_interactive_with_directory_dependency(tester, repo): description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" -demo = {path = "../../fixtures/git/github.com/demo/demo"} +demo = {path = "demo"} [tool.poetry.dev-dependencies] pytest = "^3.6.0" """ - assert expected in tester.io.fetch_output() -def test_interactive_with_directory_dependency_and_other_name(tester, repo): +def test_interactive_with_directory_dependency_and_other_name( + tester, repo, source_dir, fixture_dir +): repo.add_package(get_package("pendulum", "2.0.0")) repo.add_package(get_package("pytest", "3.6.0")) + demo = fixture_dir("git") / "github.com" / "demo" / "pyproject-demo" + shutil.copytree(str(demo), str(source_dir / "pyproject-demo")) + inputs = [ "my-package", # Package name "1.2.3", # Version @@ -319,7 +378,7 @@ def test_interactive_with_directory_dependency_and_other_name(tester, repo): "MIT", # License "~2.7 || ^3.6", # Python "", # Interactive packages - "../../fixtures/git/github.com/demo/pyproject-demo", # Search for package + "./pyproject-demo", # Search for package "", # Stop searching for packages "", # Interactive dev packages "pytest", # Search for package @@ -337,10 +396,12 @@ def test_interactive_with_directory_dependency_and_other_name(tester, repo): description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" -demo = {path = "../../fixtures/git/github.com/demo/pyproject-demo"} +demo = {path = "pyproject-demo"} [tool.poetry.dev-dependencies] pytest = "^3.6.0" @@ -349,10 +410,13 @@ def test_interactive_with_directory_dependency_and_other_name(tester, repo): assert expected in tester.io.fetch_output() -def test_interactive_with_file_dependency(tester, repo): +def test_interactive_with_file_dependency(tester, repo, source_dir, fixture_dir): repo.add_package(get_package("pendulum", "2.0.0")) repo.add_package(get_package("pytest", "3.6.0")) + demo = fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl" + shutil.copyfile(str(demo), str(source_dir / demo.name)) + inputs = [ "my-package", # Package name "1.2.3", # Version @@ -361,7 +425,7 @@ def test_interactive_with_file_dependency(tester, repo): "MIT", # License "~2.7 || ^3.6", # Python "", # Interactive packages - "../../fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl", # Search for package + "./demo-0.1.0-py2.py3-none-any.whl", # Search for package "", # Stop searching for packages "", # Interactive dev packages "pytest", # Search for package @@ -379,10 +443,12 @@ def test_interactive_with_file_dependency(tester, repo): description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" -demo = {path = "../../fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl"} +demo = {path = "demo-0.1.0-py2.py3-none-any.whl"} [tool.poetry.dev-dependencies] pytest = "^3.6.0" @@ -411,6 +477,8 @@ def test_python_option(tester): description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" @@ -444,6 +512,8 @@ def test_predefined_dependency(tester, repo): description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" @@ -484,6 +554,8 @@ def test_predefined_and_interactive_dependencies(tester, repo): description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" @@ -518,6 +590,8 @@ def test_predefined_dev_dependency(tester, repo): description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" @@ -558,11 +632,15 @@ def test_predefined_and_interactive_dev_dependencies(tester, repo): description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" [tool.poetry.dev-dependencies] +pytest = "^3.6.0" +pytest-requests = "^0.2.0" """ output = tester.io.fetch_output() @@ -572,7 +650,7 @@ def test_predefined_and_interactive_dev_dependencies(tester, repo): def test_add_package_with_extras_and_whitespace(tester): - result = tester._command._parse_requirements(["databases[postgresql, sqlite]"]) + result = tester.command._parse_requirements(["databases[postgresql, sqlite]"]) assert result[0]["name"] == "databases" assert len(result[0]["extras"]) == 2 @@ -595,6 +673,44 @@ def test_init_existing_pyproject_simple( ) +def test_init_non_interactive_existing_pyproject_add_dependency( + tester, source_dir, init_basic_inputs, repo +): + pyproject_file = source_dir / "pyproject.toml" + existing_section = """ +[tool.black] +line-length = 88 +""" + pyproject_file.write_text(decode(existing_section)) + + repo.add_package(get_package("foo", "1.19.2")) + + tester.execute( + "--author 'Your Name ' " + "--name 'my-package' " + "--python '^3.6' " + "--dependency foo", + interactive=False, + ) + + expected = """\ +[tool.poetry] +name = "my-package" +version = "0.1.0" +description = "" +authors = ["Your Name "] +readme = "README.md" +packages = [{include = "my_package"}] + +[tool.poetry.dependencies] +python = "^3.6" +foo = "^1.19.2" + +[tool.poetry.dev-dependencies] +""" + assert "{}\n{}".format(existing_section, expected) in pyproject_file.read_text() + + def test_init_existing_pyproject_with_build_system_fails( tester, source_dir, init_basic_inputs ): diff --git a/tests/console/commands/test_lock.py b/tests/console/commands/test_lock.py new file mode 100644 index 00000000000..e49e4326555 --- /dev/null +++ b/tests/console/commands/test_lock.py @@ -0,0 +1,113 @@ +from pathlib import Path + +import pytest + +from poetry.packages import Locker +from tests.helpers import get_package + + +@pytest.fixture +def source_dir(tmp_path: Path) -> Path: + yield Path(tmp_path.as_posix()) + + +@pytest.fixture +def tester(command_tester_factory): + return command_tester_factory("lock") + + +def _project_factory(fixture_name, project_factory, fixture_dir): + source = fixture_dir(fixture_name) + pyproject_content = (source / "pyproject.toml").read_text(encoding="utf-8") + poetry_lock_content = (source / "poetry.lock").read_text(encoding="utf-8") + return project_factory( + name="foobar", + pyproject_content=pyproject_content, + poetry_lock_content=poetry_lock_content, + ) + + +@pytest.fixture +def poetry_with_outdated_lockfile(project_factory, fixture_dir): + return _project_factory("outdated_lock", project_factory, fixture_dir) + + +@pytest.fixture +def poetry_with_up_to_date_lockfile(project_factory, fixture_dir): + return _project_factory("up_to_date_lock", project_factory, fixture_dir) + + +@pytest.fixture +def poetry_with_old_lockfile(project_factory, fixture_dir): + return _project_factory("old_lock", project_factory, fixture_dir) + + +def test_lock_check_outdated( + command_tester_factory, poetry_with_outdated_lockfile, http +): + http.disable() + + locker = Locker( + lock=poetry_with_outdated_lockfile.pyproject.file.path.parent / "poetry.lock", + local_config=poetry_with_outdated_lockfile.locker._local_config, + ) + poetry_with_outdated_lockfile.set_locker(locker) + + tester = command_tester_factory("lock", poetry=poetry_with_outdated_lockfile) + status_code = tester.execute("--check") + + # exit with an error + assert status_code == 1 + + +def test_lock_check_up_to_date( + command_tester_factory, poetry_with_up_to_date_lockfile, http +): + http.disable() + + locker = Locker( + lock=poetry_with_up_to_date_lockfile.pyproject.file.path.parent / "poetry.lock", + local_config=poetry_with_up_to_date_lockfile.locker._local_config, + ) + poetry_with_up_to_date_lockfile.set_locker(locker) + + tester = command_tester_factory("lock", poetry=poetry_with_up_to_date_lockfile) + status_code = tester.execute("--check") + + # exit with an error + assert status_code == 0 + + +def test_lock_no_update(command_tester_factory, poetry_with_old_lockfile, repo): + repo.add_package(get_package("sampleproject", "1.3.1")) + repo.add_package(get_package("sampleproject", "2.0.0")) + + locker = Locker( + lock=poetry_with_old_lockfile.pyproject.file.path.parent / "poetry.lock", + local_config=poetry_with_old_lockfile.locker._local_config, + ) + poetry_with_old_lockfile.set_locker(locker) + + locked_repository = poetry_with_old_lockfile.locker.locked_repository( + with_dev_reqs=True + ) + assert ( + poetry_with_old_lockfile.locker.lock_data["metadata"].get("lock-version") + == "1.0" + ) + + tester = command_tester_factory("lock", poetry=poetry_with_old_lockfile) + tester.execute("--no-update") + + locker = Locker( + lock=poetry_with_old_lockfile.pyproject.file.path.parent / "poetry.lock", + local_config={}, + ) + packages = locker.locked_repository(True).packages + + assert len(packages) == len(locked_repository.packages) + + assert locker.lock_data["metadata"].get("lock-version") == "1.1" + + for package in packages: + assert locked_repository.find_packages(package.to_dependency()) diff --git a/tests/console/commands/test_new.py b/tests/console/commands/test_new.py new file mode 100644 index 00000000000..0df5149ae0c --- /dev/null +++ b/tests/console/commands/test_new.py @@ -0,0 +1,155 @@ +from pathlib import Path +from typing import Optional + +import pytest + +from poetry.factory import Factory +from poetry.poetry import Poetry + + +@pytest.fixture +def tester(command_tester_factory): + return command_tester_factory("new") + + +def verify_project_directory( + path: Path, package_name: str, package_path: str, include_from: Optional[str] = None +) -> Poetry: + package_path = Path(package_path) + assert path.is_dir() + + pyproject = path / "pyproject.toml" + assert pyproject.is_file() + + init_file = path / package_path / "__init__.py" + assert init_file.is_file() + + tests_init_file = path / "tests" / "__init__.py" + assert tests_init_file.is_file() + + poetry = Factory().create_poetry(cwd=path) + assert poetry.package.name == package_name + + if include_from: + package_include = { + "include": package_path.relative_to(include_from).parts[0], + "from": include_from, + } + else: + package_include = {"include": package_path.parts[0]} + + packages = poetry.local_config.get("packages") + + if not packages: + assert poetry.local_config.get("name") == package_include.get("include") + else: + assert len(packages) == 1 + assert packages[0] == package_include + + return poetry + + +@pytest.mark.parametrize( + "options,directory,package_name,package_path,include_from", + [ + ([], "package", "package", "package", None), + (["--src"], "package", "package", "src/package", "src"), + ( + ["--name namespace.package"], + "namespace-package", + "namespace-package", + "namespace/package", + None, + ), + ( + ["--src", "--name namespace.package"], + "namespace-package", + "namespace-package", + "src/namespace/package", + "src", + ), + ( + ["--name namespace.package_a"], + "namespace-package_a", + "namespace-package-a", + "namespace/package_a", + None, + ), + ( + ["--src", "--name namespace.package_a"], + "namespace-package_a", + "namespace-package-a", + "src/namespace/package_a", + "src", + ), + ( + ["--name namespace_package"], + "namespace-package", + "namespace-package", + "namespace_package", + None, + ), + ( + ["--name namespace_package", "--src"], + "namespace-package", + "namespace-package", + "src/namespace_package", + "src", + ), + ( + ["--name namespace.package"], + "package", + "namespace-package", + "namespace/package", + None, + ), + ( + ["--name namespace.package", "--src"], + "package", + "namespace-package", + "src/namespace/package", + "src", + ), + ( + ["--name namespace.package"], + "package", + "namespace-package", + "namespace/package", + None, + ), + ( + ["--name namespace.package", "--src"], + "package", + "namespace-package", + "src/namespace/package", + "src", + ), + ([], "namespace_package", "namespace-package", "namespace_package", None), + ( + ["--src", "--name namespace_package"], + "namespace_package", + "namespace-package", + "src/namespace_package", + "src", + ), + ], +) +def test_command_new( + options, directory, package_name, package_path, include_from, tester, tmp_dir +): + path = Path(tmp_dir) / directory + options.append(path.as_posix()) + tester.execute(" ".join(options)) + verify_project_directory(path, package_name, package_path, include_from) + + +@pytest.mark.parametrize("fmt", [(None,), ("md",), ("rst",)]) +def test_command_new_with_readme(fmt, tester, tmp_dir): + fmt = "md" + package = "package" + path = Path(tmp_dir) / package + options = ["--readme {}".format(fmt) if fmt else "md", path.as_posix()] + tester.execute(" ".join(options)) + + poetry = verify_project_directory(path, package, package, None) + assert poetry.local_config.get("readme") == "README.{}".format(fmt or "md") diff --git a/tests/console/commands/test_publish.py b/tests/console/commands/test_publish.py index dccc43ee87d..62671eda624 100644 --- a/tests/console/commands/test_publish.py +++ b/tests/console/commands/test_publish.py @@ -1,14 +1,10 @@ -import pytest +from pathlib import Path + import requests from poetry.publishing.uploader import UploadError -from poetry.utils._compat import PY36 -from poetry.utils._compat import Path -@pytest.mark.skipif( - not PY36, reason="Improved error rendering is only available on Python >=3.6" -) def test_publish_returns_non_zero_code_for_upload_errors(app, app_tester, http): http.register_uri( http.POST, "https://upload.pypi.org/legacy/", status=400, body="Bad Request" @@ -18,16 +14,17 @@ def test_publish_returns_non_zero_code_for_upload_errors(app, app_tester, http): assert 1 == exit_code - expected = """ + expected_output = """ Publishing simple-project (1.2.3) to PyPI - - +""" + expected_error_output = """\ UploadError HTTP Error 400: Bad Request """ - assert expected in app_tester.io.fetch_output() + assert expected_output in app_tester.io.fetch_output() + assert expected_error_output in app_tester.io.fetch_error() def test_publish_returns_non_zero_code_for_connection_errors(app, app_tester, http): @@ -44,33 +41,7 @@ def request_callback(*_, **__): expected = str(UploadError(error=requests.ConnectionError())) - assert expected in app_tester.io.fetch_output() - - -@pytest.mark.skipif( - PY36, reason="Improved error rendering is not available on Python <3.6" -) -def test_publish_returns_non_zero_code_for_upload_errors_older_python( - app, app_tester, http -): - http.register_uri( - http.POST, "https://upload.pypi.org/legacy/", status=400, body="Bad Request" - ) - - exit_code = app_tester.execute("publish --username foo --password bar") - - assert 1 == exit_code - - expected = """ -Publishing simple-project (1.2.3) to PyPI - - -UploadError - -HTTP Error 400: Bad Request -""" - - assert app_tester.io.fetch_output() == expected + assert expected in app_tester.io.fetch_error() def test_publish_with_cert(app_tester, mocker): diff --git a/tests/console/commands/test_remove.py b/tests/console/commands/test_remove.py new file mode 100644 index 00000000000..7ccddb5140b --- /dev/null +++ b/tests/console/commands/test_remove.py @@ -0,0 +1,24 @@ +import pytest + +from poetry.core.packages.package import Package + + +@pytest.fixture() +def tester(command_tester_factory): + return command_tester_factory("remove") + + +def test_remove_command_should_not_write_changes_upon_installer_errors( + tester, app, repo, command_tester_factory, mocker +): + repo.add_package(Package("foo", "2.0.0")) + + command_tester_factory("add").execute("foo") + + mocker.patch("poetry.installation.installer.Installer.run", return_value=1) + + original_content = app.poetry.file.read().as_string() + + tester.execute("foo") + + assert app.poetry.file.read().as_string() == original_content diff --git a/tests/console/commands/test_run.py b/tests/console/commands/test_run.py index 351d869d1a9..33314fa53c0 100644 --- a/tests/console/commands/test_run.py +++ b/tests/console/commands/test_run.py @@ -11,6 +11,16 @@ def patches(mocker, env): mocker.patch("poetry.utils.env.EnvManager.get", return_value=env) -def test_run_passes_all_args(tester, env): - tester.execute("python -V") +def test_run_passes_all_args(app_tester, env): + app_tester.execute("run python -V") assert [["python", "-V"]] == env.executed + + +def test_run_keeps_options_passed_before_command(app_tester, env): + app_tester.execute("-V --no-ansi run python", decorated=True) + + assert not app_tester.io.is_decorated() + assert app_tester.io.fetch_output() == app_tester.io.remove_format( + app_tester.application.long_version + "\n" + ) + assert [] == env.executed diff --git a/tests/console/commands/test_search.py b/tests/console/commands/test_search.py index 9b61476c0b5..ef668454084 100644 --- a/tests/console/commands/test_search.py +++ b/tests/console/commands/test_search.py @@ -1,6 +1,6 @@ -import pytest +from pathlib import Path -from poetry.utils._compat import Path +import pytest TESTS_DIRECTORY = Path(__file__).parent.parent.parent @@ -21,7 +21,8 @@ def tester(command_tester_factory): def test_search( - tester, http, + tester, + http, ): tester.execute("sqlalchemy") diff --git a/tests/console/commands/test_show.py b/tests/console/commands/test_show.py index cf4dffcacc4..2c69d3a3667 100644 --- a/tests/console/commands/test_show.py +++ b/tests/console/commands/test_show.py @@ -1,7 +1,5 @@ import pytest -from clikit.formatter.ansi_formatter import AnsiFormatter - from poetry.factory import Factory from tests.helpers import get_package @@ -86,6 +84,46 @@ def test_show_basic_with_installed_packages(tester, poetry, installed): assert expected == tester.io.fetch_output() +def test_show_basic_with_installed_packages_single(tester, poetry, installed): + poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0")) + + cachy_010 = get_package("cachy", "0.1.0") + cachy_010.description = "Cachy package" + + installed.add_package(cachy_010) + + poetry.locker.mock_lock_data( + { + "package": [ + { + "name": "cachy", + "version": "0.1.0", + "description": "Cachy package", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + }, + ], + "metadata": { + "python-versions": "*", + "platform": "*", + "content-hash": "123456789", + "hashes": {"cachy": []}, + }, + } + ) + + tester.execute("cachy") + + assert [ + "name : cachy", + "version : 0.1.0", + "description : Cachy package", + ] == [line.strip() for line in tester.io.fetch_output().splitlines()] + + def test_show_basic_with_not_installed_packages_non_decorated( tester, poetry, installed ): @@ -186,12 +224,11 @@ def test_show_basic_with_not_installed_packages_decorated(tester, poetry, instal } ) - tester.io.set_formatter(AnsiFormatter(forced=True)) - tester.execute() + tester.execute(decorated=True) expected = """\ -\033[36mcachy \033[0m \033[1m0.1.0\033[0m Cachy package -\033[31mpendulum\033[0m \033[1m2.0.0\033[0m Pendulum package +\033[36mcachy \033[39m \033[39;1m0.1.0\033[39;22m Cachy package +\033[31mpendulum\033[39m \033[39;1m2.0.0\033[39;22m Pendulum package """ assert expected == tester.io.fetch_output() @@ -317,12 +354,11 @@ def test_show_latest_decorated(tester, poetry, installed, repo): } ) - tester.io.set_formatter(AnsiFormatter(forced=True)) - tester.execute("--latest") + tester.execute("--latest", decorated=True) expected = """\ -\033[36mcachy \033[0m \033[1m0.1.0\033[0m \033[33m0.2.0\033[0m Cachy package -\033[36mpendulum\033[0m \033[1m2.0.0\033[0m \033[31m2.0.1\033[0m Pendulum package +\033[36mcachy \033[39m \033[39;1m0.1.0\033[39;22m \033[33m0.2.0\033[39m Cachy package +\033[36mpendulum\033[39m \033[39;1m2.0.0\033[39;22m \033[31m2.0.1\033[39m Pendulum package """ assert expected == tester.io.fetch_output() @@ -1144,7 +1180,7 @@ def test_show_tree(tester, poetry, installed): } ) - tester.execute("--tree") + tester.execute("--tree", supports_utf8=False) expected = """\ cachy 0.2.0 @@ -1152,3 +1188,70 @@ def test_show_tree(tester, poetry, installed): """ assert expected == tester.io.fetch_output() + + +def test_show_tree_no_dev(tester, poetry, installed): + poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.2.0")) + poetry.package.add_dependency( + Factory.create_dependency("pytest", "^6.1.0", category="dev") + ) + + cachy2 = get_package("cachy", "0.2.0") + cachy2.add_dependency(Factory.create_dependency("msgpack-python", ">=0.5 <0.6")) + installed.add_package(cachy2) + + pytest = get_package("pytest", "6.1.1") + installed.add_package(pytest) + + poetry.locker.mock_lock_data( + { + "package": [ + { + "name": "cachy", + "version": "0.2.0", + "description": "", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + "dependencies": {"msgpack-python": ">=0.5 <0.6"}, + }, + { + "name": "msgpack-python", + "version": "0.5.1", + "description": "", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + }, + { + "name": "pytest", + "version": "6.1.1", + "description": "", + "category": "dev", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + }, + ], + "metadata": { + "python-versions": "*", + "platform": "*", + "content-hash": "123456789", + "hashes": {"cachy": [], "msgpack-python": [], "pytest": []}, + }, + } + ) + + tester.execute("--tree --no-dev") + + expected = """\ +cachy 0.2.0 +└── msgpack-python >=0.5 <0.6 +""" + + assert expected == tester.io.fetch_output() diff --git a/tests/console/commands/test_version.py b/tests/console/commands/test_version.py index 77f6d8aa41e..f868d9f1060 100644 --- a/tests/console/commands/test_version.py +++ b/tests/console/commands/test_version.py @@ -1,6 +1,6 @@ import pytest -from poetry.console.commands import VersionCommand +from poetry.console.commands.version import VersionCommand @pytest.fixture() @@ -51,3 +51,13 @@ def test_version_show(tester): def test_short_version_show(tester): tester.execute("--short") assert "1.2.3\n" == tester.io.fetch_output() + + +def test_version_update(tester): + tester.execute("2.0.0") + assert "Bumping version from 1.2.3 to 2.0.0\n" == tester.io.fetch_output() + + +def test_short_version_update(tester): + tester.execute("--short 2.0.0") + assert "2.0.0\n" == tester.io.fetch_output() diff --git a/tests/console/conftest.py b/tests/console/conftest.py index 8a3a5cba8ae..82143358d4d 100644 --- a/tests/console/conftest.py +++ b/tests/console/conftest.py @@ -1,14 +1,15 @@ import os +from pathlib import Path + import pytest -from cleo import ApplicationTester +from cleo.io.null_io import NullIO +from cleo.testers.application_tester import ApplicationTester from poetry.factory import Factory from poetry.installation.noop_installer import NoopInstaller -from poetry.io.null_io import NullIO from poetry.repositories import Pool -from poetry.utils._compat import Path from poetry.utils.env import MockEnv from tests.helpers import TestApplication from tests.helpers import TestExecutor @@ -22,8 +23,10 @@ def installer(): @pytest.fixture -def env(): - return MockEnv(path=Path("/prefix"), base=Path("/base/prefix"), is_venv=True) +def env(tmp_dir): + path = Path(tmp_dir) / ".venv" + path.mkdir(parents=True) + return MockEnv(path=path, is_venv=True) @pytest.fixture(autouse=True) @@ -95,7 +98,6 @@ def poetry(repo, project_directory, config): @pytest.fixture def app(poetry): app_ = TestApplication(poetry) - app_.config.set_terminate_after_run(False) return app_ diff --git a/tests/console/test_application.py b/tests/console/test_application.py new file mode 100644 index 00000000000..6e4e5bab98f --- /dev/null +++ b/tests/console/test_application.py @@ -0,0 +1,101 @@ +import re + +from cleo.testers.application_tester import ApplicationTester +from entrypoints import EntryPoint + +from poetry.console.application import Application +from poetry.console.commands.command import Command +from poetry.plugins.application_plugin import ApplicationPlugin + + +class FooCommand(Command): + name = "foo" + + description = "Foo Command" + + def handle(self): + self.line("foo called") + + return 0 + + +class AddCommandPlugin(ApplicationPlugin): + def activate(self, application: Application): + application.command_loader.register_factory("foo", lambda: FooCommand()) + + +def test_application_with_plugins(mocker): + mocker.patch( + "entrypoints.get_group_all", + return_value=[ + EntryPoint( + "my-plugin", "tests.console.test_application", "AddCommandPlugin" + ) + ], + ) + + app = Application() + + tester = ApplicationTester(app) + tester.execute("") + + assert re.search(r"\s+foo\s+Foo Command", tester.io.fetch_output()) is not None + assert 0 == tester.status_code + + +def test_application_with_plugins_disabled(mocker): + mocker.patch( + "entrypoints.get_group_all", + return_value=[ + EntryPoint( + "my-plugin", "tests.console.test_application", "AddCommandPlugin" + ) + ], + ) + + app = Application() + + tester = ApplicationTester(app) + tester.execute("--no-plugins") + + assert re.search(r"\s+foo\s+Foo Command", tester.io.fetch_output()) is None + assert 0 == tester.status_code + + +def test_application_execute_plugin_command(mocker): + mocker.patch( + "entrypoints.get_group_all", + return_value=[ + EntryPoint( + "my-plugin", "tests.console.test_application", "AddCommandPlugin" + ) + ], + ) + + app = Application() + + tester = ApplicationTester(app) + tester.execute("foo") + + assert "foo called\n" == tester.io.fetch_output() + assert 0 == tester.status_code + + +def test_application_execute_plugin_command_with_plugins_disabled(mocker): + mocker.patch( + "entrypoints.get_group_all", + return_value=[ + EntryPoint( + "my-plugin", "tests.console.test_application", "AddCommandPlugin" + ) + ], + ) + + app = Application() + + tester = ApplicationTester(app) + tester.execute("foo --no-plugins") + + assert "" == tester.io.fetch_output() + assert '\nThe command "foo" does not exist.\n' == tester.io.fetch_error() + assert 1 == tester.status_code diff --git a/tests/fixtures/extended_project/pyproject.toml b/tests/fixtures/extended_project/pyproject.toml index 954b12b3497..ecb7deb9107 100644 --- a/tests/fixtures/extended_project/pyproject.toml +++ b/tests/fixtures/extended_project/pyproject.toml @@ -25,6 +25,3 @@ build = "build.py" # Requirements [tool.poetry.dependencies] python = "~2.7 || ^3.4" - -[tool.poetry.scripts] -foo = "foo:bar" diff --git a/tests/fixtures/extended_project_without_setup/pyproject.toml b/tests/fixtures/extended_project_without_setup/pyproject.toml index 42375c03c25..5c9dc2774c7 100644 --- a/tests/fixtures/extended_project_without_setup/pyproject.toml +++ b/tests/fixtures/extended_project_without_setup/pyproject.toml @@ -27,6 +27,3 @@ generate-setup-file = false # Requirements [tool.poetry.dependencies] python = "~2.7 || ^3.4" - -[tool.poetry.scripts] -foo = "foo:bar" diff --git a/tests/fixtures/git/github.com/demo/demo/demo.egg-info/PKG-INFO b/tests/fixtures/git/github.com/demo/demo/demo.egg-info/PKG-INFO old mode 100755 new mode 100644 diff --git a/tests/fixtures/git/github.com/demo/demo/demo.egg-info/SOURCES.txt b/tests/fixtures/git/github.com/demo/demo/demo.egg-info/SOURCES.txt old mode 100755 new mode 100644 diff --git a/tests/fixtures/git/github.com/demo/demo/demo.egg-info/dependency_links.txt b/tests/fixtures/git/github.com/demo/demo/demo.egg-info/dependency_links.txt old mode 100755 new mode 100644 diff --git a/tests/fixtures/git/github.com/demo/demo/demo.egg-info/requires.txt b/tests/fixtures/git/github.com/demo/demo/demo.egg-info/requires.txt old mode 100755 new mode 100644 diff --git a/tests/fixtures/git/github.com/demo/demo/demo.egg-info/top_level.txt b/tests/fixtures/git/github.com/demo/demo/demo.egg-info/top_level.txt old mode 100755 new mode 100644 diff --git a/tests/fixtures/git/github.com/demo/poetry-plugin/poetry_plugin/__init__.py b/tests/fixtures/git/github.com/demo/poetry-plugin/poetry_plugin/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/git/github.com/demo/poetry-plugin/pyproject.toml b/tests/fixtures/git/github.com/demo/poetry-plugin/pyproject.toml new file mode 100644 index 00000000000..b45d9d976eb --- /dev/null +++ b/tests/fixtures/git/github.com/demo/poetry-plugin/pyproject.toml @@ -0,0 +1,18 @@ +[tool.poetry] +name = "poetry-plugin" +version = "0.1.2" +description = "Some description." +authors = [ + "Sébastien Eustace " +] +license = "MIT" + +[tool.poetry.dependencies] +python = "^3.6" +pendulum = "^2.0" +tomlkit = {version = "^0.7.0", optional = true} + +[tool.poetry.extras] +foo = ["tomlkit"] + +[tool.poetry.dev-dependencies] diff --git a/tests/fixtures/old_lock/poetry.lock b/tests/fixtures/old_lock/poetry.lock new file mode 100644 index 00000000000..498df2edc9f --- /dev/null +++ b/tests/fixtures/old_lock/poetry.lock @@ -0,0 +1,19 @@ +[[package]] +category = "main" +description = "A sample Python project" +name = "sampleproject" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "1.3.1" + +[metadata] +content-hash = "c8c2c9d899e47bac3972e029ef0e71b75d5df98a28eebef25a75640a19aac177" +lock-version = "1.0" +python-versions = "^3.8" + +[metadata.files] +sampleproject = [ + {file = "sampleproject-1.3.1-py2.py3-none-any.whl", hash = "sha256:26c9172e08244873b0e09c574a229bf2c251c67723a05e08fd3ec0c5ee423796"}, + {file = "sampleproject-1.3.1-py3-none-any.whl", hash = "sha256:75bb5bb4e74a1b77dc0cff25ebbacb54fe1318aaf99a86a036cefc86ed885ced"}, + {file = "sampleproject-1.3.1.tar.gz", hash = "sha256:3593ca2f1e057279d70d6144b14472fb28035b1da213dde60906b703d6f82c55"}, +] diff --git a/tests/fixtures/old_lock/pyproject.toml b/tests/fixtures/old_lock/pyproject.toml new file mode 100644 index 00000000000..377aa676be9 --- /dev/null +++ b/tests/fixtures/old_lock/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "foobar" +version = "0.1.0" +description = "" +authors = ["Poetry Developer "] + +[tool.poetry.dependencies] +python = "^3.8" +sampleproject = ">=1.3.1" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/old_lock/updated.lock b/tests/fixtures/old_lock/updated.lock new file mode 100644 index 00000000000..22cd049e68f --- /dev/null +++ b/tests/fixtures/old_lock/updated.lock @@ -0,0 +1,152 @@ +[[package]] +name = "certifi" +version = "2020.6.20" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "chardet" +version = "3.0.4" +description = "Universal encoding detector for Python 2 and 3" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "docker" +version = "4.3.1" +description = "A Python library for the Docker Engine API." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +pywin32 = {version = "227", markers = "sys_platform == \"win32\""} +requests = ">=2.14.2,<2.18.0 || >2.18.0" +six = ">=1.4.0" +websocket-client = ">=0.32.0" + +[package.extras] +ssh = ["paramiko (>=2.4.2)"] +tls = ["pyOpenSSL (>=17.5.0)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"] + +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pywin32" +version = "227" +description = "Python for Window Extensions" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "requests" +version = "2.24.0" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<4" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] + +[[package]] +name = "six" +version = "1.15.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "urllib3" +version = "1.25.10" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] + +[[package]] +name = "websocket-client" +version = "0.57.0" +description = "WebSocket client for Python. hybi13 is supported." +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +six = "*" + +[metadata] +lock-version = "1.1" +python-versions = "^3.8" +content-hash = "bb4c2f3c089b802c1930b6acbeed04711d93e9cdfd9a003eb17518a6d9f350c6" + +[metadata.files] +certifi = [ + {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, + {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, +] +chardet = [ + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, +] +docker = [ + {file = "docker-4.3.1-py2.py3-none-any.whl", hash = "sha256:13966471e8bc23b36bfb3a6fb4ab75043a5ef1dac86516274777576bed3b9828"}, + {file = "docker-4.3.1.tar.gz", hash = "sha256:bad94b8dd001a8a4af19ce4becc17f41b09f228173ffe6a4e0355389eef142f2"}, +] +idna = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] +pywin32 = [ + {file = "pywin32-227-cp27-cp27m-win32.whl", hash = "sha256:371fcc39416d736401f0274dd64c2302728c9e034808e37381b5e1b22be4a6b0"}, + {file = "pywin32-227-cp27-cp27m-win_amd64.whl", hash = "sha256:4cdad3e84191194ea6d0dd1b1b9bdda574ff563177d2adf2b4efec2a244fa116"}, + {file = "pywin32-227-cp35-cp35m-win32.whl", hash = "sha256:f4c5be1a293bae0076d93c88f37ee8da68136744588bc5e2be2f299a34ceb7aa"}, + {file = "pywin32-227-cp35-cp35m-win_amd64.whl", hash = "sha256:a929a4af626e530383a579431b70e512e736e9588106715215bf685a3ea508d4"}, + {file = "pywin32-227-cp36-cp36m-win32.whl", hash = "sha256:300a2db938e98c3e7e2093e4491439e62287d0d493fe07cce110db070b54c0be"}, + {file = "pywin32-227-cp36-cp36m-win_amd64.whl", hash = "sha256:9b31e009564fb95db160f154e2aa195ed66bcc4c058ed72850d047141b36f3a2"}, + {file = "pywin32-227-cp37-cp37m-win32.whl", hash = "sha256:47a3c7551376a865dd8d095a98deba954a98f326c6fe3c72d8726ca6e6b15507"}, + {file = "pywin32-227-cp37-cp37m-win_amd64.whl", hash = "sha256:31f88a89139cb2adc40f8f0e65ee56a8c585f629974f9e07622ba80199057511"}, + {file = "pywin32-227-cp38-cp38-win32.whl", hash = "sha256:7f18199fbf29ca99dff10e1f09451582ae9e372a892ff03a28528a24d55875bc"}, + {file = "pywin32-227-cp38-cp38-win_amd64.whl", hash = "sha256:7c1ae32c489dc012930787f06244426f8356e129184a02c25aef163917ce158e"}, + {file = "pywin32-227-cp39-cp39-win32.whl", hash = "sha256:c054c52ba46e7eb6b7d7dfae4dbd987a1bb48ee86debe3f245a2884ece46e295"}, + {file = "pywin32-227-cp39-cp39-win_amd64.whl", hash = "sha256:f27cec5e7f588c3d1051651830ecc00294f90728d19c3bf6916e6dba93ea357c"}, +] +requests = [ + {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, + {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] +urllib3 = [ + {file = "urllib3-1.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"}, + {file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"}, +] +websocket-client = [ + {file = "websocket_client-0.57.0-py2.py3-none-any.whl", hash = "sha256:0fc45c961324d79c781bab301359d5a1b00b13ad1b10415a4780229ef71a5549"}, + {file = "websocket_client-0.57.0.tar.gz", hash = "sha256:d735b91d6d1692a6a181f2a8c9e0238e5f6373356f561bb9dc4c7af36f452010"}, +] diff --git a/tests/fixtures/outdated_lock/poetry.lock b/tests/fixtures/outdated_lock/poetry.lock new file mode 100644 index 00000000000..1d950ca7c10 --- /dev/null +++ b/tests/fixtures/outdated_lock/poetry.lock @@ -0,0 +1,152 @@ +[[package]] +name = "certifi" +version = "2020.6.20" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "chardet" +version = "3.0.4" +description = "Universal encoding detector for Python 2 and 3" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "docker" +version = "4.3.0" +description = "A Python library for the Docker Engine API." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +pywin32 = {version = "227", markers = "sys_platform == \"win32\""} +requests = ">=2.14.2,<2.18.0 || >2.18.0" +six = ">=1.4.0" +websocket-client = ">=0.32.0" + +[package.extras] +ssh = ["paramiko (>=2.4.2)"] +tls = ["pyOpenSSL (>=17.5.0)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"] + +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pywin32" +version = "227" +description = "Python for Window Extensions" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "requests" +version = "2.24.0" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<4" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] + +[[package]] +name = "six" +version = "1.15.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "urllib3" +version = "1.25.10" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] + +[[package]] +name = "websocket-client" +version = "0.57.0" +description = "WebSocket client for Python. hybi13 is supported." +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +six = "*" + +[metadata] +lock-version = "1.1" +python-versions = "^3.8" +content-hash = "2f47de5e052dabeff3c1362d3a37b5cfcaf9bbe9d9ce1681207e72ca1f4dab55" + +[metadata.files] +certifi = [ + {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, + {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, +] +chardet = [ + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, +] +docker = [ + {file = "docker-4.3.0-py2.py3-none-any.whl", hash = "sha256:ba118607b0ba6bfc1b236ec32019a355c47b5d012d01d976467d4692ef443929"}, + {file = "docker-4.3.0.tar.gz", hash = "sha256:431a268f2caf85aa30613f9642da274c62f6ee8bae7d70d968e01529f7d6af93"}, +] +idna = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] +pywin32 = [ + {file = "pywin32-227-cp27-cp27m-win32.whl", hash = "sha256:371fcc39416d736401f0274dd64c2302728c9e034808e37381b5e1b22be4a6b0"}, + {file = "pywin32-227-cp27-cp27m-win_amd64.whl", hash = "sha256:4cdad3e84191194ea6d0dd1b1b9bdda574ff563177d2adf2b4efec2a244fa116"}, + {file = "pywin32-227-cp35-cp35m-win32.whl", hash = "sha256:f4c5be1a293bae0076d93c88f37ee8da68136744588bc5e2be2f299a34ceb7aa"}, + {file = "pywin32-227-cp35-cp35m-win_amd64.whl", hash = "sha256:a929a4af626e530383a579431b70e512e736e9588106715215bf685a3ea508d4"}, + {file = "pywin32-227-cp36-cp36m-win32.whl", hash = "sha256:300a2db938e98c3e7e2093e4491439e62287d0d493fe07cce110db070b54c0be"}, + {file = "pywin32-227-cp36-cp36m-win_amd64.whl", hash = "sha256:9b31e009564fb95db160f154e2aa195ed66bcc4c058ed72850d047141b36f3a2"}, + {file = "pywin32-227-cp37-cp37m-win32.whl", hash = "sha256:47a3c7551376a865dd8d095a98deba954a98f326c6fe3c72d8726ca6e6b15507"}, + {file = "pywin32-227-cp37-cp37m-win_amd64.whl", hash = "sha256:31f88a89139cb2adc40f8f0e65ee56a8c585f629974f9e07622ba80199057511"}, + {file = "pywin32-227-cp38-cp38-win32.whl", hash = "sha256:7f18199fbf29ca99dff10e1f09451582ae9e372a892ff03a28528a24d55875bc"}, + {file = "pywin32-227-cp38-cp38-win_amd64.whl", hash = "sha256:7c1ae32c489dc012930787f06244426f8356e129184a02c25aef163917ce158e"}, + {file = "pywin32-227-cp39-cp39-win32.whl", hash = "sha256:c054c52ba46e7eb6b7d7dfae4dbd987a1bb48ee86debe3f245a2884ece46e295"}, + {file = "pywin32-227-cp39-cp39-win_amd64.whl", hash = "sha256:f27cec5e7f588c3d1051651830ecc00294f90728d19c3bf6916e6dba93ea357c"}, +] +requests = [ + {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, + {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] +urllib3 = [ + {file = "urllib3-1.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"}, + {file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"}, +] +websocket-client = [ + {file = "websocket_client-0.57.0-py2.py3-none-any.whl", hash = "sha256:0fc45c961324d79c781bab301359d5a1b00b13ad1b10415a4780229ef71a5549"}, + {file = "websocket_client-0.57.0.tar.gz", hash = "sha256:d735b91d6d1692a6a181f2a8c9e0238e5f6373356f561bb9dc4c7af36f452010"}, +] diff --git a/tests/fixtures/outdated_lock/pyproject.toml b/tests/fixtures/outdated_lock/pyproject.toml new file mode 100644 index 00000000000..555147605ea --- /dev/null +++ b/tests/fixtures/outdated_lock/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "foobar" +version = "0.1.0" +description = "" +authors = ["Poetry Developer "] + +[tool.poetry.dependencies] +python = "^3.8" +docker = "4.3.1" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/project_with_nested_local/bar/pyproject.toml b/tests/fixtures/project_with_nested_local/bar/pyproject.toml new file mode 100644 index 00000000000..bf058b8813a --- /dev/null +++ b/tests/fixtures/project_with_nested_local/bar/pyproject.toml @@ -0,0 +1,11 @@ +[tool.poetry] +name = "bar" +version = "1.2.3" +description = "Some description." +authors = ["Poetry Maintainer "] +license = "MIT" + +# Requirements +[tool.poetry.dependencies] +python = "~2.7 || ^3.4" +quix = { path = "../quix", develop = true } diff --git a/tests/fixtures/project_with_nested_local/foo/pyproject.toml b/tests/fixtures/project_with_nested_local/foo/pyproject.toml new file mode 100644 index 00000000000..1aba06effe1 --- /dev/null +++ b/tests/fixtures/project_with_nested_local/foo/pyproject.toml @@ -0,0 +1,11 @@ +[tool.poetry] +name = "foo" +version = "1.2.3" +description = "Some description." +authors = ["Poetry Maintainer "] +license = "MIT" + +# Requirements +[tool.poetry.dependencies] +python = "~2.7 || ^3.4" +bar = { path = "../bar", develop = true } diff --git a/tests/fixtures/project_with_nested_local/pyproject.toml b/tests/fixtures/project_with_nested_local/pyproject.toml new file mode 100644 index 00000000000..c6eb2c6b46c --- /dev/null +++ b/tests/fixtures/project_with_nested_local/pyproject.toml @@ -0,0 +1,12 @@ +[tool.poetry] +name = "project-with-nested-local" +version = "1.2.3" +description = "Some description." +authors = ["Poetry Maintainer "] +license = "MIT" + +# Requirements +[tool.poetry.dependencies] +python = "~2.7 || ^3.4" +foo = { path = "./foo", develop = true } +bar = { path = "./bar", develop = true } diff --git a/tests/fixtures/project_with_nested_local/quix/pyproject.toml b/tests/fixtures/project_with_nested_local/quix/pyproject.toml new file mode 100644 index 00000000000..a90d9bcc41f --- /dev/null +++ b/tests/fixtures/project_with_nested_local/quix/pyproject.toml @@ -0,0 +1,10 @@ +[tool.poetry] +name = "quix" +version = "1.2.3" +description = "Some description." +authors = ["Poetry Maintainer "] +license = "MIT" + +# Requirements +[tool.poetry.dependencies] +python = "~2.7 || ^3.4" diff --git a/tests/fixtures/simple_project/pyproject.toml b/tests/fixtures/simple_project/pyproject.toml index 0b41162c6f4..41a062fc09a 100644 --- a/tests/fixtures/simple_project/pyproject.toml +++ b/tests/fixtures/simple_project/pyproject.toml @@ -27,3 +27,9 @@ python = "~2.7 || ^3.4" [tool.poetry.scripts] foo = "foo:bar" baz = "bar:baz.boom.bim" +fox = "fuz.foo:bar.baz" + + +[build-system] +requires = ["poetry-core>=1.0.2"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/up_to_date_lock/poetry.lock b/tests/fixtures/up_to_date_lock/poetry.lock new file mode 100644 index 00000000000..a896b5d0621 --- /dev/null +++ b/tests/fixtures/up_to_date_lock/poetry.lock @@ -0,0 +1,152 @@ +[[package]] +name = "certifi" +version = "2020.12.5" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "docker" +version = "4.3.1" +description = "A Python library for the Docker Engine API." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +pywin32 = {version = "227", markers = "sys_platform == \"win32\""} +requests = ">=2.14.2,<2.18.0 || >2.18.0" +six = ">=1.4.0" +websocket-client = ">=0.32.0" + +[package.extras] +ssh = ["paramiko (>=2.4.2)"] +tls = ["pyOpenSSL (>=17.5.0)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"] + +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pywin32" +version = "227" +description = "Python for Window Extensions" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "requests" +version = "2.25.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<5" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] + +[[package]] +name = "six" +version = "1.15.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "urllib3" +version = "1.26.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "websocket-client" +version = "0.58.0" +description = "WebSocket client for Python with low level API options" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +six = "*" + +[metadata] +lock-version = "1.1" +python-versions = "^3.8" +content-hash = "0cd068218f235c162f7b74bc8faf4ce3387b82daee1c1bb7a97af034f27ee116" + +[metadata.files] +certifi = [ + {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, + {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, +] +chardet = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] +docker = [ + {file = "docker-4.3.1-py2.py3-none-any.whl", hash = "sha256:13966471e8bc23b36bfb3a6fb4ab75043a5ef1dac86516274777576bed3b9828"}, + {file = "docker-4.3.1.tar.gz", hash = "sha256:bad94b8dd001a8a4af19ce4becc17f41b09f228173ffe6a4e0355389eef142f2"}, +] +idna = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] +pywin32 = [ + {file = "pywin32-227-cp27-cp27m-win32.whl", hash = "sha256:371fcc39416d736401f0274dd64c2302728c9e034808e37381b5e1b22be4a6b0"}, + {file = "pywin32-227-cp27-cp27m-win_amd64.whl", hash = "sha256:4cdad3e84191194ea6d0dd1b1b9bdda574ff563177d2adf2b4efec2a244fa116"}, + {file = "pywin32-227-cp35-cp35m-win32.whl", hash = "sha256:f4c5be1a293bae0076d93c88f37ee8da68136744588bc5e2be2f299a34ceb7aa"}, + {file = "pywin32-227-cp35-cp35m-win_amd64.whl", hash = "sha256:a929a4af626e530383a579431b70e512e736e9588106715215bf685a3ea508d4"}, + {file = "pywin32-227-cp36-cp36m-win32.whl", hash = "sha256:300a2db938e98c3e7e2093e4491439e62287d0d493fe07cce110db070b54c0be"}, + {file = "pywin32-227-cp36-cp36m-win_amd64.whl", hash = "sha256:9b31e009564fb95db160f154e2aa195ed66bcc4c058ed72850d047141b36f3a2"}, + {file = "pywin32-227-cp37-cp37m-win32.whl", hash = "sha256:47a3c7551376a865dd8d095a98deba954a98f326c6fe3c72d8726ca6e6b15507"}, + {file = "pywin32-227-cp37-cp37m-win_amd64.whl", hash = "sha256:31f88a89139cb2adc40f8f0e65ee56a8c585f629974f9e07622ba80199057511"}, + {file = "pywin32-227-cp38-cp38-win32.whl", hash = "sha256:7f18199fbf29ca99dff10e1f09451582ae9e372a892ff03a28528a24d55875bc"}, + {file = "pywin32-227-cp38-cp38-win_amd64.whl", hash = "sha256:7c1ae32c489dc012930787f06244426f8356e129184a02c25aef163917ce158e"}, + {file = "pywin32-227-cp39-cp39-win32.whl", hash = "sha256:c054c52ba46e7eb6b7d7dfae4dbd987a1bb48ee86debe3f245a2884ece46e295"}, + {file = "pywin32-227-cp39-cp39-win_amd64.whl", hash = "sha256:f27cec5e7f588c3d1051651830ecc00294f90728d19c3bf6916e6dba93ea357c"}, +] +requests = [ + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] +urllib3 = [ + {file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"}, + {file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"}, +] +websocket-client = [ + {file = "websocket_client-0.58.0-py2.py3-none-any.whl", hash = "sha256:44b5df8f08c74c3d82d28100fdc81f4536809ce98a17f0757557813275fbb663"}, + {file = "websocket_client-0.58.0.tar.gz", hash = "sha256:63509b41d158ae5b7f67eb4ad20fecbb4eee99434e73e140354dc3ff8e09716f"}, +] diff --git a/tests/fixtures/up_to_date_lock/pyproject.toml b/tests/fixtures/up_to_date_lock/pyproject.toml new file mode 100644 index 00000000000..555147605ea --- /dev/null +++ b/tests/fixtures/up_to_date_lock/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "foobar" +version = "0.1.0" +description = "" +authors = ["Poetry Developer "] + +[tool.poetry.dependencies] +python = "^3.8" +docker = "4.3.1" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/with_non_default_multiple_secondary_sources/pyproject.toml b/tests/fixtures/with_non_default_multiple_secondary_sources/pyproject.toml new file mode 100644 index 00000000000..933bee96912 --- /dev/null +++ b/tests/fixtures/with_non_default_multiple_secondary_sources/pyproject.toml @@ -0,0 +1,24 @@ +[tool.poetry] +name = "my-package" +version = "1.2.3" +description = "Some description." +authors = [ + "Your Name " +] +license = "MIT" + +# Requirements +[tool.poetry.dependencies] +python = "~2.7 || ^3.6" + +[tool.poetry.dev-dependencies] + +[[tool.poetry.source]] +name = "foo" +url = "https://foo.bar/simple/" +secondary = true + +[[tool.poetry.source]] +name = "bar" +url = "https://bar.baz/simple/" +secondary = true diff --git a/tests/fixtures/with_non_default_multiple_sources/pyproject.toml b/tests/fixtures/with_non_default_multiple_sources/pyproject.toml new file mode 100644 index 00000000000..6cacb602e8b --- /dev/null +++ b/tests/fixtures/with_non_default_multiple_sources/pyproject.toml @@ -0,0 +1,23 @@ +[tool.poetry] +name = "my-package" +version = "1.2.3" +description = "Some description." +authors = [ + "Your Name " +] +license = "MIT" + +# Requirements +[tool.poetry.dependencies] +python = "~2.7 || ^3.6" + +[tool.poetry.dev-dependencies] + +[[tool.poetry.source]] +name = "foo" +url = "https://foo.bar/simple/" +secondary = true + +[[tool.poetry.source]] +name = "bar" +url = "https://bar.baz/simple/" diff --git a/tests/fixtures/with_non_default_secondary_source/pyproject.toml b/tests/fixtures/with_non_default_secondary_source/pyproject.toml new file mode 100644 index 00000000000..453e3f9747f --- /dev/null +++ b/tests/fixtures/with_non_default_secondary_source/pyproject.toml @@ -0,0 +1,19 @@ +[tool.poetry] +name = "my-package" +version = "1.2.3" +description = "Some description." +authors = [ + "Your Name " +] +license = "MIT" + +# Requirements +[tool.poetry.dependencies] +python = "~2.7 || ^3.6" + +[tool.poetry.dev-dependencies] + +[[tool.poetry.source]] +name = "foo" +url = "https://foo.bar/simple/" +secondary = true diff --git a/tests/fixtures/with_non_default_source/pyproject.toml b/tests/fixtures/with_non_default_source/pyproject.toml new file mode 100644 index 00000000000..d36abb55a25 --- /dev/null +++ b/tests/fixtures/with_non_default_source/pyproject.toml @@ -0,0 +1,18 @@ +[tool.poetry] +name = "my-package" +version = "1.2.3" +description = "Some description." +authors = [ + "Your Name " +] +license = "MIT" + +# Requirements +[tool.poetry.dependencies] +python = "~2.7 || ^3.6" + +[tool.poetry.dev-dependencies] + +[[tool.poetry.source]] +name = "foo" +url = "https://foo.bar/simple/" diff --git a/tests/helpers.py b/tests/helpers.py index f380bb8de16..f1bafd42700 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,12 +1,15 @@ import os import shutil +import urllib.parse -from poetry.console import Application +from pathlib import Path + +from poetry.console.application import Application from poetry.core.masonry.utils.helpers import escape_name from poetry.core.masonry.utils.helpers import escape_version -from poetry.core.packages import Dependency -from poetry.core.packages import Link -from poetry.core.packages import Package +from poetry.core.packages.dependency import Dependency +from poetry.core.packages.package import Package +from poetry.core.packages.utils.link import Link from poetry.core.toml.file import TOMLFile from poetry.core.vcs.git import ParsedUrl from poetry.factory import Factory @@ -14,10 +17,7 @@ from poetry.packages import Locker from poetry.repositories import Repository 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._compat import urlparse FIXTURE_PATH = Path(__file__).parent / "fixtures" @@ -59,19 +59,13 @@ def copy_or_symlink(source, dest): # os.symlink requires either administrative privileges or developer mode on Win10, # throwing an OSError if neither is active. if WINDOWS: - if PY2: + 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: - 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)) @@ -92,7 +86,7 @@ def mock_clone(_, source, dest): def mock_download(url, dest, **__): - parts = urlparse.urlparse(url) + parts = urllib.parse.urlparse(url) fixtures = Path(__file__).parent / "fixtures" fixture = fixtures / parts.path.lstrip("/") diff --git a/tests/inspection/test_info.py b/tests/inspection/test_info.py index a128469a23f..3163f629a7d 100644 --- a/tests/inspection/test_info.py +++ b/tests/inspection/test_info.py @@ -1,12 +1,13 @@ +from pathlib import Path +from subprocess import CalledProcessError from typing import Set import pytest +from poetry.core.packages.package import Package +from poetry.core.semver.version import Version from poetry.inspection.info import PackageInfo from poetry.inspection.info import PackageInfoError -from poetry.utils._compat import PY35 -from poetry.utils._compat import CalledProcessError -from poetry.utils._compat import Path from poetry.utils._compat import decode from poetry.utils.env import EnvCommandError from poetry.utils.env import VirtualEnv @@ -22,22 +23,22 @@ def pep517_metadata_mock(): @pytest.fixture -def demo_sdist(): # type: () -> Path +def demo_sdist() -> Path: return FIXTURE_DIR_BASE / "distributions" / "demo-0.1.0.tar.gz" @pytest.fixture -def demo_wheel(): # type: () -> Path +def demo_wheel() -> Path: return FIXTURE_DIR_BASE / "distributions" / "demo-0.1.0-py2.py3-none-any.whl" @pytest.fixture -def source_dir(tmp_path): # type: (Path) -> Path +def source_dir(tmp_path: Path) -> Path: yield Path(tmp_path.as_posix()) @pytest.fixture -def demo_setup(source_dir): # type: (Path) -> Path +def demo_setup(source_dir: Path) -> Path: setup_py = source_dir / "setup.py" setup_py.write_text( decode( @@ -51,7 +52,7 @@ def demo_setup(source_dir): # type: (Path) -> Path @pytest.fixture -def demo_setup_cfg(source_dir): # type: (Path) -> Path +def demo_setup_cfg(source_dir: Path) -> Path: setup_cfg = source_dir / "setup.cfg" setup_cfg.write_text( decode( @@ -70,7 +71,7 @@ def demo_setup_cfg(source_dir): # type: (Path) -> Path @pytest.fixture -def demo_setup_complex(source_dir): # type: (Path) -> Path +def demo_setup_complex(source_dir: Path) -> Path: setup_py = source_dir / "setup.py" setup_py.write_text( decode( @@ -84,7 +85,7 @@ def demo_setup_complex(source_dir): # type: (Path) -> Path @pytest.fixture -def demo_setup_complex_pep517_legacy(demo_setup_complex): # type: (Path) -> Path +def demo_setup_complex_pep517_legacy(demo_setup_complex: Path) -> Path: pyproject_toml = demo_setup_complex / "pyproject.toml" pyproject_toml.write_text( decode("[build-system]\n" 'requires = ["setuptools", "wheel"]') @@ -92,7 +93,7 @@ def demo_setup_complex_pep517_legacy(demo_setup_complex): # type: (Path) -> Pat yield demo_setup_complex -def demo_check_info(info, requires_dist=None): # type: (PackageInfo, Set[str]) -> None +def demo_check_info(info: PackageInfo, requires_dist: Set[str] = None) -> None: assert info.name == "demo" assert info.version == "0.1.0" assert info.requires_dist @@ -134,13 +135,11 @@ def test_info_from_requires_txt(): demo_check_info(info) -@pytest.mark.skipif(not PY35, reason="Parsing of setup.py is skipped for Python < 3.5") def test_info_from_setup_py(demo_setup): info = PackageInfo.from_setup_files(demo_setup) demo_check_info(info, requires_dist={"package"}) -@pytest.mark.skipif(not PY35, reason="Parsing of setup.cfg is skipped for Python < 3.5") def test_info_from_setup_cfg(demo_setup_cfg): info = PackageInfo.from_setup_files(demo_setup_cfg) demo_check_info(info, requires_dist={"package"}) @@ -148,14 +147,14 @@ def test_info_from_setup_cfg(demo_setup_cfg): def test_info_no_setup_pkg_info_no_deps(): info = PackageInfo.from_directory( - FIXTURE_DIR_INSPECTIONS / "demo_no_setup_pkg_info_no_deps", disable_build=True, + FIXTURE_DIR_INSPECTIONS / "demo_no_setup_pkg_info_no_deps", + disable_build=True, ) assert info.name == "demo" assert info.version == "0.1.0" assert info.requires_dist is None -@pytest.mark.skipif(not PY35, reason="Parsing of setup.py is skipped for Python < 3.5") def test_info_setup_simple(mocker, demo_setup): spy = mocker.spy(VirtualEnv, "run") info = PackageInfo.from_directory(demo_setup) @@ -163,18 +162,6 @@ def test_info_setup_simple(mocker, demo_setup): demo_check_info(info, requires_dist={"package"}) -@pytest.mark.skipif( - PY35, - reason="For projects with setup.py using Python < 3.5 fallback to pep517 build", -) -def test_info_setup_simple_py2(mocker, demo_setup): - spy = mocker.spy(VirtualEnv, "run") - info = PackageInfo.from_directory(demo_setup) - assert spy.call_count == 2 - demo_check_info(info, requires_dist={"package"}) - - -@pytest.mark.skipif(not PY35, reason="Parsing of setup.cfg is skipped for Python < 3.5") def test_info_setup_cfg(mocker, demo_setup_cfg): spy = mocker.spy(VirtualEnv, "run") info = PackageInfo.from_directory(demo_setup_cfg) @@ -190,7 +177,7 @@ def test_info_setup_complex(demo_setup_complex): def test_info_setup_complex_pep517_error(mocker, demo_setup_complex): mocker.patch( "poetry.utils.env.VirtualEnv.run", - auto_spec=True, + autospec=True, side_effect=EnvCommandError(CalledProcessError(1, "mock", output="mock")), ) @@ -203,7 +190,6 @@ def test_info_setup_complex_pep517_legacy(demo_setup_complex_pep517_legacy): demo_check_info(info, requires_dist={"package"}) -@pytest.mark.skipif(not PY35, reason="Parsing of setup.py is skipped for Python < 3.5") def test_info_setup_complex_disable_build(mocker, demo_setup_complex): spy = mocker.spy(VirtualEnv, "run") info = PackageInfo.from_directory(demo_setup_complex, disable_build=True) @@ -213,7 +199,6 @@ def test_info_setup_complex_disable_build(mocker, demo_setup_complex): assert info.requires_dist is None -@pytest.mark.skipif(not PY35, reason="Parsing of setup.py is skipped for Python < 3.5") @pytest.mark.parametrize("missing", ["version", "name", "install_requires"]) def test_info_setup_missing_mandatory_should_trigger_pep517( mocker, source_dir, missing @@ -242,3 +227,20 @@ def test_info_prefer_poetry_config_over_egg_info(): FIXTURE_DIR_INSPECTIONS / "demo_with_obsolete_egg_info" ) demo_check_info(info) + + +def test_info_public_version(): + package_info = PackageInfo(version="1.2.3+localVersion") + assert package_info.public_version == "1.2.3" + + +def test_info_public_version_no_mutation(): + package_info = PackageInfo(version="1.2.3") + assert package_info.public_version == "1.2.3" + + +def test_package_from_info_with_public_version(): + package_info = PackageInfo(name="test-package", version="1.2.3+local_super_version") + package = package_info.to_package() # type: Package + assert type(package.version) == Version + assert package.version.text == "1.2.3" diff --git a/tests/installation/fixtures/with-dependencies-extras.test b/tests/installation/fixtures/with-dependencies-extras.test index 63560bb4793..042e29670e1 100644 --- a/tests/installation/fixtures/with-dependencies-extras.test +++ b/tests/installation/fixtures/with-dependencies-extras.test @@ -18,7 +18,7 @@ python-versions = "*" C = {version = "^1.0", optional = true} [package.extras] -foo = ["C (^1.0)"] +foo = ["C (>=1.0,<2.0)"] [[package]] name = "C" diff --git a/tests/installation/fixtures/with-dependencies-nested-extras.test b/tests/installation/fixtures/with-dependencies-nested-extras.test new file mode 100644 index 00000000000..48a22a7c7f3 --- /dev/null +++ b/tests/installation/fixtures/with-dependencies-nested-extras.test @@ -0,0 +1,45 @@ +[[package]] +name = "A" +version = "1.0" +description = "" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +B = {version = "^1.0", optional = true, extras = ["C"]} + +[package.extras] +B = ["B[C] (>=1.0,<2.0)"] + +[[package]] +name = "B" +version = "1.0" +description = "" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +C = {version = "^1.0", optional = true} + +[package.extras] +C = ["C (>=1.0,<2.0)"] + +[[package]] +name = "C" +version = "1.0" +description = "" +category = "main" +optional = false +python-versions = "*" + +[metadata] +python-versions = "*" +lock-version = "1.1" +content-hash = "123456789" + +[metadata.files] +"A" = [] +"B" = [] +"C" = [] diff --git a/tests/installation/fixtures/with-directory-dependency-poetry-transitive.test b/tests/installation/fixtures/with-directory-dependency-poetry-transitive.test index 7177191e2c8..fb10b1accea 100644 --- a/tests/installation/fixtures/with-directory-dependency-poetry-transitive.test +++ b/tests/installation/fixtures/with-directory-dependency-poetry-transitive.test @@ -65,8 +65,8 @@ python-versions = "*" version = "1.2.3" [package.dependencies] -project-with-extras = "1.2.3" -project-with-transitive-file-dependencies = "1.2.3" +project-with-extras = { "path" = "../../project_with_extras" } +project-with-transitive-file-dependencies = { "path" = "../project_with_transitive_file_dependencies" } [package.source] type = "directory" @@ -82,8 +82,8 @@ python-versions = "*" version = "1.2.3" [package.dependencies] -demo = "0.1.0" -inner-directory-project = "1.2.4" +demo = { "path" = "../../distributions/demo-0.1.0-py2.py3-none-any.whl" } +inner-directory-project = { "path" = "inner-directory-project" } [package.source] type = "directory" diff --git a/tests/installation/fixtures/with-directory-dependency-poetry.test b/tests/installation/fixtures/with-directory-dependency-poetry.test index 32530cb97f0..12431f62185 100644 --- a/tests/installation/fixtures/with-directory-dependency-poetry.test +++ b/tests/installation/fixtures/with-directory-dependency-poetry.test @@ -16,7 +16,7 @@ python-versions = "*" version = "1.2.3" [package.dependencies] -pendulum = {version = ">=1.4.4", optional = true, markers = "extra == \"extras_a\""} +pendulum = {version = ">=1.4.4", optional = true} [package.extras] extras_a = ["pendulum (>=1.4.4)"] diff --git a/tests/installation/fixtures/with-file-dependency-transitive.test b/tests/installation/fixtures/with-file-dependency-transitive.test index 6e5d92d711a..b882f262640 100644 --- a/tests/installation/fixtures/with-file-dependency-transitive.test +++ b/tests/installation/fixtures/with-file-dependency-transitive.test @@ -48,8 +48,8 @@ python-versions = "*" version = "1.2.3" [package.dependencies] -demo = "0.1.0" -inner-directory-project = "1.2.4" +demo = { "path" = "../../distributions/demo-0.1.0-py2.py3-none-any.whl" } +inner-directory-project = { "path" = "inner-directory-project" } [package.source] type = "directory" diff --git a/tests/installation/fixtures/with-pypi-repository.test b/tests/installation/fixtures/with-pypi-repository.test index 3f3719452d9..d1ed1ae55ed 100644 --- a/tests/installation/fixtures/with-pypi-repository.test +++ b/tests/installation/fixtures/with-pypi-repository.test @@ -66,6 +66,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" py = ">=1.5.0" six = ">=1.10.0" attrs = ">=17.4.0" +setuptools = "*" more-itertools = ">=4.0.0" pluggy = ">=0.5,<0.7" funcsigs = {"version" = "*", "markers" = "python_version < \"3.0\""} @@ -79,6 +80,18 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "setuptools" +version = "39.2.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*" + +[package.extras] +certs = ["certifi (==2016.9.26)"] +ssl = ["wincertstore (==0.2)"] + [metadata] python-versions = "*" lock-version = "1.1" @@ -113,6 +126,10 @@ pytest = [ {file = "pytest-3.5.0-py2.py3-none-any.whl", hash = "sha256:6266f87ab64692112e5477eba395cfedda53b1933ccd29478e671e73b420c19c"}, {file = "pytest-3.5.0.tar.gz", hash = "sha256:fae491d1874f199537fd5872b5e1f0e74a009b979df9d53d1553fd03da1703e1"}, ] +setuptools = [ + {file = "setuptools-39.2.0-py2.py3-none-any.whl", hash = "sha256:8fca9275c89964f13da985c3656cb00ba029d7f3916b37990927ffdf264e7926"}, + {file = "setuptools-39.2.0.zip", hash = "sha256:f7cddbb5f5c640311eb00eab6e849f7701fa70bf6a183fc8a2c33dd1d1672fb2"}, +] six = [ {file = "six-1.11.0-py2.py3-none-any.whl", hash = "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"}, {file = "six-1.11.0.tar.gz", hash = "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9"}, diff --git a/tests/installation/test_authenticator.py b/tests/installation/test_authenticator.py index d19364741d1..5f756c34f91 100644 --- a/tests/installation/test_authenticator.py +++ b/tests/installation/test_authenticator.py @@ -5,14 +5,16 @@ import pytest import requests +from cleo.io.null_io import NullIO + from poetry.installation.authenticator import Authenticator -from poetry.io.null_io import NullIO @pytest.fixture() def mock_remote(http): http.register_uri( - http.GET, re.compile("^https?://foo.bar/(.+?)$"), + http.GET, + re.compile("^https?://foo.bar/(.+?)$"), ) diff --git a/tests/installation/test_chef.py b/tests/installation/test_chef.py index 9fcbbea184c..332d9c4794f 100644 --- a/tests/installation/test_chef.py +++ b/tests/installation/test_chef.py @@ -1,8 +1,9 @@ +from pathlib import Path + from packaging.tags import Tag from poetry.core.packages.utils.link import Link from poetry.installation.chef import Chef -from poetry.utils._compat import Path from poetry.utils.env import MockEnv @@ -47,7 +48,9 @@ def test_get_cached_archives_for_link(config, mocker): distributions = Path(__file__).parent.parent.joinpath("fixtures/distributions") mocker.patch.object( - chef, "get_cache_directory_for_link", return_value=distributions, + chef, + "get_cache_directory_for_link", + return_value=distributions, ) archives = chef.get_cached_archives_for_link( @@ -60,7 +63,7 @@ def test_get_cached_archives_for_link(config, mocker): } -def test_get_cache_directory_for_link(config): +def test_get_cache_directory_for_link(config, config_cache_dir): chef = Chef( config, MockEnv( @@ -71,8 +74,11 @@ def test_get_cache_directory_for_link(config): directory = chef.get_cache_directory_for_link( Link("https://files.python-poetry.org/poetry-1.1.0.tar.gz") ) + expected = Path( - "/foo/artifacts/ba/63/13/283a3b3b7f95f05e9e6f84182d276f7bb0951d5b0cc24422b33f7a4648" + "{}/artifacts/ba/63/13/283a3b3b7f95f05e9e6f84182d276f7bb0951d5b0cc24422b33f7a4648".format( + config_cache_dir.as_posix() + ) ) assert expected == directory diff --git a/tests/installation/test_chooser.py b/tests/installation/test_chooser.py index cf3f931b942..b7c6e155c6f 100644 --- a/tests/installation/test_chooser.py +++ b/tests/installation/test_chooser.py @@ -1,5 +1,7 @@ import re +from pathlib import Path + import pytest from packaging.tags import Tag @@ -9,7 +11,6 @@ from poetry.repositories.legacy_repository import LegacyRepository from poetry.repositories.pool import Pool from poetry.repositories.pypi_repository import PyPiRepository -from poetry.utils._compat import Path from poetry.utils.env import MockEnv @@ -49,7 +50,9 @@ def callback(request, uri, headers): return [200, headers, f.read()] http.register_uri( - http.GET, re.compile("^https://pypi.org/(.+?)/(.+?)/json$"), body=callback, + http.GET, + re.compile("^https://pypi.org/(.+?)/(.+?)/json$"), + body=callback, ) @@ -65,7 +68,9 @@ def callback(request, uri, headers): return [200, headers, f.read()] http.register_uri( - http.GET, re.compile("^https://foo.bar/simple/(.+?)$"), body=callback, + http.GET, + re.compile("^https://foo.bar/simple/(.+?)$"), + body=callback, ) @@ -149,7 +154,11 @@ def test_chooser_chooses_system_specific_wheel_link_if_available( @pytest.mark.parametrize("source_type", ["", "legacy"]) def test_chooser_chooses_sdist_if_no_compatible_wheel_link_is_available( - env, mock_pypi, mock_legacy, source_type, pool, + env, + mock_pypi, + mock_legacy, + source_type, + pool, ): chooser = Chooser(pool, env) @@ -170,7 +179,11 @@ def test_chooser_chooses_sdist_if_no_compatible_wheel_link_is_available( @pytest.mark.parametrize("source_type", ["", "legacy"]) def test_chooser_chooses_distributions_that_match_the_package_hashes( - env, mock_pypi, mock_legacy, source_type, pool, + env, + mock_pypi, + mock_legacy, + source_type, + pool, ): chooser = Chooser(pool, env) @@ -195,3 +208,36 @@ def test_chooser_chooses_distributions_that_match_the_package_hashes( link = chooser.choose_for(package) assert "isort-4.3.4.tar.gz" == link.filename + + +@pytest.mark.parametrize("source_type", ["", "legacy"]) +def test_chooser_throws_an_error_if_package_hashes_do_not_match( + env, + mock_pypi, + mock_legacy, + source_type, + pool, +): + chooser = Chooser(pool, env) + + package = Package("isort", "4.3.4") + files = [ + { + "hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "filename": "isort-4.3.4.tar.gz", + } + ] + if source_type == "legacy": + package = Package( + package.name, + package.version.text, + source_type="legacy", + source_reference="foo", + source_url="https://foo.bar/simple/", + ) + + package.files = files + + with pytest.raises(RuntimeError) as e: + chooser.choose_for(package) + assert files[0]["hash"] in str(e) diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py index bb659321d0f..9b7b413b92b 100644 --- a/tests/installation/test_executor.py +++ b/tests/installation/test_executor.py @@ -1,34 +1,60 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import json import re import shutil +from pathlib import Path + import pytest -from clikit.api.formatter.style import Style -from clikit.io.buffered_io import BufferedIO +from cleo.formatters.style import Style +from cleo.io.buffered_io import BufferedIO from poetry.config.config import Config from poetry.core.packages.package import Package +from poetry.core.utils._compat import PY36 from poetry.installation.executor import Executor from poetry.installation.operations import Install from poetry.installation.operations import Uninstall from poetry.installation.operations import Update from poetry.repositories.pool import Pool -from poetry.utils._compat import PY36 -from poetry.utils._compat import Path from poetry.utils.env import MockEnv from tests.repositories.test_pypi_repository import MockRepository +@pytest.fixture +def env(tmp_dir): + path = Path(tmp_dir) / ".venv" + path.mkdir(parents=True) + + return MockEnv(path=path, is_venv=True) + + @pytest.fixture() def io(): io = BufferedIO() - io.formatter.add_style(Style("c1_dark").fg("cyan").dark()) - io.formatter.add_style(Style("c2_dark").fg("default").bold().dark()) - io.formatter.add_style(Style("success_dark").fg("green").dark()) - io.formatter.add_style(Style("warning").fg("yellow")) + io.output.formatter.set_style("c1_dark", Style("cyan", options=["dark"])) + io.output.formatter.set_style("c2_dark", Style("default", options=["bold", "dark"])) + io.output.formatter.set_style("success_dark", Style("green", options=["dark"])) + io.output.formatter.set_style("warning", Style("yellow")) + + return io + + +@pytest.fixture() +def io_decorated(): + io = BufferedIO(decorated=True) + io.output.formatter.set_style("c1", Style("cyan")) + io.output.formatter.set_style("success", Style("green")) + + return io + + +@pytest.fixture() +def io_not_decorated(): + io = BufferedIO(decorated=False) return io @@ -52,17 +78,22 @@ def callback(request, uri, headers): return [200, headers, f.read()] http.register_uri( - http.GET, re.compile("^https://files.pythonhosted.org/.*$"), body=callback, + http.GET, + re.compile("^https://files.pythonhosted.org/.*$"), + body=callback, ) def test_execute_executes_a_batch_of_operations( - config, pool, io, tmp_dir, mock_file_downloads + mocker, config, pool, io, tmp_dir, mock_file_downloads, env ): + pip_editable_install = mocker.patch( + "poetry.installation.executor.pip_editable_install", unsafe=not PY36 + ) + config = Config() config.merge({"cache-dir": tmp_dir}) - env = MockEnv(path=Path(tmp_dir)) executor = Executor(env, pool, config, io) file_package = Package( @@ -93,9 +124,10 @@ def test_execute_executes_a_batch_of_operations( source_type="git", source_reference="master", source_url="https://github.com/demo/demo.git", + develop=True, ) - assert 0 == executor.execute( + return_code = executor.execute( [ Install(Package("pytest", "3.5.2")), Uninstall(Package("attrs", "17.4.0")), @@ -124,13 +156,16 @@ def test_execute_executes_a_batch_of_operations( output = set(io.fetch_output().splitlines()) assert expected == output assert 5 == len(env.executed) + assert 0 == return_code + pip_editable_install.assert_called_once() -def test_execute_shows_skipped_operations_if_verbose(config, pool, io): +def test_execute_shows_skipped_operations_if_verbose( + config, pool, io, config_cache_dir, env +): config = Config() - config.merge({"cache-dir": "/foo"}) + config.merge({"cache-dir": config_cache_dir.as_posix()}) - env = MockEnv() executor = Executor(env, pool, config, io) executor.verbose() @@ -147,11 +182,7 @@ def test_execute_shows_skipped_operations_if_verbose(config, pool, io): assert 0 == len(env.executed) -@pytest.mark.skipif( - not PY36, reason="Improved error rendering is only available on Python >=3.6" -) -def test_execute_should_show_errors(config, mocker, io): - env = MockEnv() +def test_execute_should_show_errors(config, mocker, io, env): executor = Executor(env, pool, config, io) executor.verbose() @@ -172,10 +203,73 @@ def test_execute_should_show_errors(config, mocker, io): assert expected in io.fetch_output() +def test_execute_works_with_ansi_output( + mocker, config, pool, io_decorated, tmp_dir, mock_file_downloads, env +): + config = Config() + config.merge({"cache-dir": tmp_dir}) + + executor = Executor(env, pool, config, io_decorated) + + install_output = ( + "some string that does not contain a keyb0ard !nterrupt or cance11ed by u$er" + ) + mocker.patch.object(env, "_run", return_value=install_output) + return_code = executor.execute( + [ + Install(Package("pytest", "3.5.2")), + ] + ) + env._run.assert_called_once() + + expected = [ + "\x1b[39;1mPackage operations\x1b[39;22m: \x1b[34m1\x1b[39m install, \x1b[34m0\x1b[39m updates, \x1b[34m0\x1b[39m removals", + "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m3.5.2\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mPending...\x1b[39m", + "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m3.5.2\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mDownloading...\x1b[39m", + "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m3.5.2\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mInstalling...\x1b[39m", + "\x1b[32;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[32m3.5.2\x1b[39m\x1b[39m)\x1b[39m", # finished + ] + output = io_decorated.fetch_output() + # hint: use print(repr(output)) if you need to debug this + + for line in expected: + assert line in output + assert 0 == return_code + + +def test_execute_works_with_no_ansi_output( + mocker, config, pool, io_not_decorated, tmp_dir, mock_file_downloads, env +): + config = Config() + config.merge({"cache-dir": tmp_dir}) + + executor = Executor(env, pool, config, io_not_decorated) + + install_output = ( + "some string that does not contain a keyb0ard !nterrupt or cance11ed by u$er" + ) + mocker.patch.object(env, "_run", return_value=install_output) + return_code = executor.execute( + [ + Install(Package("pytest", "3.5.2")), + ] + ) + env._run.assert_called_once() + + expected = """ +Package operations: 1 install, 0 updates, 0 removals + + • Installing pytest (3.5.2) +""" + expected = set(expected.splitlines()) + output = set(io_not_decorated.fetch_output().splitlines()) + assert expected == output + assert 0 == return_code + + def test_execute_should_show_operation_as_cancelled_on_subprocess_keyboard_interrupt( - config, mocker, io + config, mocker, io, env ): - env = MockEnv() executor = Executor(env, pool, config, io) executor.verbose() @@ -194,17 +288,16 @@ def test_execute_should_show_operation_as_cancelled_on_subprocess_keyboard_inter assert expected == io.fetch_output() -def test_execute_should_gracefully_handle_io_error(config, mocker, io): - env = MockEnv() +def test_execute_should_gracefully_handle_io_error(config, mocker, io, env): executor = Executor(env, pool, config, io) executor.verbose() original_write_line = executor._io.write_line - def write_line(string, flags=None): + def write_line(string, **kwargs): # Simulate UnicodeEncodeError string.encode("ascii") - original_write_line(string, flags) + original_write_line(string, **kwargs) mocker.patch.object(io, "write_line", side_effect=write_line) @@ -221,7 +314,7 @@ def write_line(string, flags=None): def test_executor_should_delete_incomplete_downloads( - config, io, tmp_dir, mocker, pool, mock_file_downloads + config, io, tmp_dir, mocker, pool, mock_file_downloads, env ): fixture = Path(__file__).parent.parent.joinpath( "fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl" @@ -244,10 +337,131 @@ def test_executor_should_delete_incomplete_downloads( config = Config() config.merge({"cache-dir": tmp_dir}) - env = MockEnv(path=Path(tmp_dir)) executor = Executor(env, pool, config, io) with pytest.raises(Exception, match="Download error"): executor._download(Install(Package("tomlkit", "0.5.3"))) assert not destination_fixture.exists() + + +def verify_installed_distribution(venv, package, url_reference=None): + distributions = list(venv.site_packages.distributions(name=package.name)) + assert len(distributions) == 1 + + distribution = distributions[0] + metadata = distribution.metadata + assert metadata["Name"] == package.name + assert metadata["Version"] == package.version.text + + direct_url_file = distribution._path.joinpath("direct_url.json") + + if url_reference is not None: + record_file = distribution._path.joinpath("RECORD") + direct_url_entry = direct_url_file.relative_to(record_file.parent.parent) + assert direct_url_file.exists() + assert str(direct_url_entry) in { + row.split(",")[0] + for row in record_file.read_text(encoding="utf-8").splitlines() + } + assert json.loads(direct_url_file.read_text(encoding="utf-8")) == url_reference + else: + assert not direct_url_file.exists() + + +def test_executor_should_write_pep610_url_references_for_files( + tmp_venv, pool, config, io +): + url = ( + Path(__file__) + .parent.parent.joinpath( + "fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl" + ) + .resolve() + ) + package = Package("demo", "0.1.0", source_type="file", source_url=url.as_posix()) + + executor = Executor(tmp_venv, pool, config, io) + executor.execute([Install(package)]) + verify_installed_distribution( + tmp_venv, package, {"archive_info": {}, "url": url.as_uri()} + ) + + +def test_executor_should_write_pep610_url_references_for_directories( + tmp_venv, pool, config, io +): + url = Path(__file__).parent.parent.joinpath("fixtures/simple_project").resolve() + package = Package( + "simple-project", "1.2.3", source_type="directory", source_url=url.as_posix() + ) + + executor = Executor(tmp_venv, pool, config, io) + executor.execute([Install(package)]) + verify_installed_distribution( + tmp_venv, package, {"dir_info": {}, "url": url.as_uri()} + ) + + +def test_executor_should_write_pep610_url_references_for_editable_directories( + tmp_venv, pool, config, io +): + url = Path(__file__).parent.parent.joinpath("fixtures/simple_project").resolve() + package = Package( + "simple-project", + "1.2.3", + source_type="directory", + source_url=url.as_posix(), + develop=True, + ) + + executor = Executor(tmp_venv, pool, config, io) + executor.execute([Install(package)]) + verify_installed_distribution( + tmp_venv, package, {"dir_info": {"editable": True}, "url": url.as_uri()} + ) + + +def test_executor_should_write_pep610_url_references_for_urls( + tmp_venv, pool, config, io, mock_file_downloads +): + package = Package( + "demo", + "0.1.0", + source_type="url", + source_url="https://files.pythonhosted.org/demo-0.1.0-py2.py3-none-any.whl", + ) + + executor = Executor(tmp_venv, pool, config, io) + executor.execute([Install(package)]) + verify_installed_distribution( + tmp_venv, package, {"archive_info": {}, "url": package.source_url} + ) + + +def test_executor_should_write_pep610_url_references_for_git( + tmp_venv, pool, config, io, mock_file_downloads +): + package = Package( + "demo", + "0.1.2", + source_type="git", + source_reference="master", + source_resolved_reference="123456", + source_url="https://github.com/demo/demo.git", + ) + + executor = Executor(tmp_venv, pool, config, io) + executor.execute([Install(package)]) + verify_installed_distribution( + tmp_venv, + package, + { + "vcs_info": { + "vcs": "git", + "requested_revision": "master", + "commit_id": "123456", + }, + "url": package.source_url, + }, + ) diff --git a/tests/installation/test_installer.py b/tests/installation/test_installer.py index 077f6dcab40..00986905984 100644 --- a/tests/installation/test_installer.py +++ b/tests/installation/test_installer.py @@ -1,13 +1,22 @@ from __future__ import unicode_literals +import itertools import json import sys +from pathlib import Path + import pytest -from clikit.io import NullIO +from cleo.io.inputs.input import Input +from cleo.io.io import IO +from cleo.io.null_io import NullIO +from cleo.io.outputs.buffered_output import BufferedOutput +from cleo.io.outputs.output import Verbosity +from deepdiff import DeepDiff -from poetry.core.packages import ProjectPackage +from poetry.core.packages.package import Package +from poetry.core.packages.project_package import ProjectPackage from poetry.core.toml.file import TOMLFile from poetry.factory import Factory from poetry.installation import Installer as BaseInstaller @@ -17,8 +26,6 @@ from poetry.repositories import Pool from poetry.repositories import Repository from poetry.repositories.installed_repository import InstalledRepository -from poetry.utils._compat import PY2 -from poetry.utils._compat import Path from poetry.utils.env import MockEnv from poetry.utils.env import NullEnv from tests.helpers import get_dependency @@ -29,7 +36,7 @@ from tests.repositories.test_pypi_repository import MockRepository -fixtures_dir = Path("tests/fixtures") +RESERVED_PACKAGES = ("pip", "setuptools", "wheel") class Installer(BaseInstaller): @@ -115,15 +122,6 @@ def _get_content_hash(self): def _write_lock_data(self, data): for package in data["package"]: python_versions = str(package["python-versions"]) - if PY2: - python_versions = python_versions.decode() - if "requirements" in package: - requirements = {} - for key, value in package["requirements"].items(): - requirements[key.decode()] = value.decode() - - package["requirements"] = requirements - package["python-versions"] = python_versions self._written_data = json.loads(json.dumps(data)) @@ -279,7 +277,10 @@ def test_run_update_after_removing_dependencies( assert 1 == installer.executor.removals_count -def test_run_install_no_dev(installer, locker, repo, package, installed): +def _configure_run_install_dev(locker, repo, package, installed): + """ + Perform common test setup for `test_run_install_*dev*()` methods. + """ locker.locked(True) locker.mock_lock_data( { @@ -335,6 +336,10 @@ def test_run_install_no_dev(installer, locker, repo, package, installed): package.add_dependency(Factory.create_dependency("B", "~1.1")) package.add_dependency(Factory.create_dependency("C", "~1.2", category="dev")) + +def test_run_install_no_dev(installer, locker, repo, package, installed): + _configure_run_install_dev(locker, repo, package, installed) + installer.dev_mode(False) installer.run() @@ -343,53 +348,111 @@ def test_run_install_no_dev(installer, locker, repo, package, installed): assert 1 == installer.executor.removals_count -def test_run_install_remove_untracked(installer, locker, repo, package, installed): +def test_run_install_dev_only(installer, locker, repo, package, installed): + _configure_run_install_dev(locker, repo, package, installed) + + installer.dev_only(True) + installer.run() + + assert 0 == installer.executor.installations_count + assert 0 == installer.executor.updates_count + assert 2 == installer.executor.removals_count + + +def test_run_install_no_dev_and_dev_only(installer, locker, repo, package, installed): + _configure_run_install_dev(locker, repo, package, installed) + + installer.dev_mode(False) + installer.dev_only(True) + installer.run() + + assert 0 == installer.executor.installations_count + assert 0 == installer.executor.updates_count + assert 1 == installer.executor.removals_count + + +@pytest.mark.parametrize( + "managed_reserved_package_names", + [ + i + for i in itertools.chain( + [tuple()], + itertools.permutations(RESERVED_PACKAGES, 1), + itertools.permutations(RESERVED_PACKAGES, 2), + [RESERVED_PACKAGES], + ) + ], +) +def test_run_install_remove_untracked( + managed_reserved_package_names, installer, locker, repo, package, installed +): + package_a = get_package("a", "1.0") + package_b = get_package("b", "1.1") + package_c = get_package("c", "1.2") + package_pip = get_package("pip", "20.0.0") + package_setuptools = get_package("setuptools", "20.0.0") + package_wheel = get_package("wheel", "20.0.0") + + all_packages = [ + package_a, + package_b, + package_c, + package_pip, + package_setuptools, + package_wheel, + ] + + managed_reserved_packages = [ + pkg for pkg in all_packages if pkg.name in managed_reserved_package_names + ] + locked_packages = [package_a, *managed_reserved_packages] + + for pkg in all_packages: + repo.add_package(pkg) + installed.add_package(pkg) + + installed.add_package(package) # Root package never removed. + + package.add_dependency(Factory.create_dependency(package_a.name, package_a.version)) + locker.locked(True) locker.mock_lock_data( { "package": [ { - "name": "a", - "version": "1.0", + "name": pkg.name, + "version": pkg.version, "category": "main", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], } + for pkg in locked_packages ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", - "hashes": {"a": []}, + "hashes": {pkg.name: [] for pkg in locked_packages}, }, } ) - package_a = get_package("a", "1.0") - package_b = get_package("b", "1.1") - package_c = get_package("c", "1.2") - package_pip = get_package("pip", "20.0.0") - repo.add_package(package_a) - repo.add_package(package_b) - repo.add_package(package_c) - repo.add_package(package_pip) - - installed.add_package(package_a) - installed.add_package(package_b) - installed.add_package(package_c) - installed.add_package(package_pip) # Always required and never removed. - installed.add_package(package) # Root package never removed. - - package.add_dependency(Factory.create_dependency("A", "~1.0")) installer.dev_mode(True).remove_untracked(True) installer.run() assert 0 == installer.executor.installations_count assert 0 == installer.executor.updates_count - assert 2 == installer.executor.removals_count - assert {"b", "c"} == set(r.name for r in installer.executor.removals) + assert 2 + len(managed_reserved_packages) == installer.executor.removals_count + + expected_removals = { + package_b.name, + package_c.name, + *managed_reserved_package_names, + } + + assert expected_removals == set(r.name for r in installer.executor.removals) def test_run_whitelist_add(installer, locker, repo, package): @@ -642,6 +705,35 @@ def test_run_with_dependencies_extras(installer, locker, repo, package): assert locker.written_data == expected +def test_run_with_dependencies_nested_extras(installer, locker, repo, package): + package_a = get_package("A", "1.0") + package_b = get_package("B", "1.0") + package_c = get_package("C", "1.0") + + dependency_c = Factory.create_dependency("C", {"version": "^1.0", "optional": True}) + dependency_b = Factory.create_dependency( + "B", {"version": "^1.0", "optional": True, "extras": ["C"]} + ) + dependency_a = Factory.create_dependency("A", {"version": "^1.0", "extras": ["B"]}) + + package_b.extras = {"C": [dependency_c]} + package_b.add_dependency(dependency_c) + + package_a.add_dependency(dependency_b) + package_a.extras = {"B": [dependency_b]} + + repo.add_package(package_a) + repo.add_package(package_b) + repo.add_package(package_c) + + package.add_dependency(dependency_a) + + installer.run() + expected = fixture("with-dependencies-nested-extras") + + assert locker.written_data == expected + + def test_run_does_not_install_extras_if_not_requested(installer, locker, repo, package): package.extras["foo"] = [get_dependency("D")] package_a = get_package("A", "1.0") @@ -775,12 +867,11 @@ def test_installer_with_pypi_repository(package, locker, installed, config): installer.run() expected = fixture("with-pypi-repository") + assert not DeepDiff(expected, locker.written_data, ignore_order=True) - assert locker.written_data == expected - -def test_run_installs_with_local_file(installer, locker, repo, package): - file_path = fixtures_dir / "distributions/demo-0.1.0-py2.py3-none-any.whl" +def test_run_installs_with_local_file(installer, locker, repo, package, fixture_dir): + file_path = fixture_dir("distributions/demo-0.1.0-py2.py3-none-any.whl") package.add_dependency(Factory.create_dependency("demo", {"file": str(file_path)})) repo.add_package(get_package("pendulum", "1.4.4")) @@ -793,9 +884,11 @@ def test_run_installs_with_local_file(installer, locker, repo, package): assert 2 == installer.executor.installations_count -def test_run_installs_wheel_with_no_requires_dist(installer, locker, repo, package): - file_path = ( - fixtures_dir / "wheel_with_no_requires_dist/demo-0.1.0-py2.py3-none-any.whl" +def test_run_installs_wheel_with_no_requires_dist( + installer, locker, repo, package, fixture_dir +): + file_path = fixture_dir( + "wheel_with_no_requires_dist/demo-0.1.0-py2.py3-none-any.whl" ) package.add_dependency(Factory.create_dependency("demo", {"file": str(file_path)})) @@ -809,31 +902,29 @@ def test_run_installs_wheel_with_no_requires_dist(installer, locker, repo, packa def test_run_installs_with_local_poetry_directory_and_extras( - installer, locker, repo, package, tmpdir + installer, locker, repo, package, tmpdir, fixture_dir ): - file_path = fixtures_dir / "project_with_extras" + file_path = fixture_dir("project_with_extras") package.add_dependency( Factory.create_dependency( "project-with-extras", {"path": str(file_path), "extras": ["extras_a"]} ) ) - print(package.requires[0].develop) repo.add_package(get_package("pendulum", "1.4.4")) installer.run() expected = fixture("with-directory-dependency-poetry") - assert locker.written_data == expected assert 2 == installer.executor.installations_count def test_run_installs_with_local_poetry_directory_transitive( - installer, locker, repo, package, tmpdir + installer, locker, repo, package, tmpdir, fixture_dir ): - root_dir = fixtures_dir.joinpath("directory") + root_dir = fixture_dir("directory") package.root_dir = root_dir locker.set_lock_path(root_dir) directory = root_dir.joinpath("project_with_transitive_directory_dependencies") @@ -858,12 +949,12 @@ def test_run_installs_with_local_poetry_directory_transitive( def test_run_installs_with_local_poetry_file_transitive( - installer, locker, repo, package, tmpdir + installer, locker, repo, package, tmpdir, fixture_dir ): - root_dir = fixtures_dir.joinpath("directory") + root_dir = fixture_dir("directory") package.root_dir = root_dir locker.set_lock_path(root_dir) - directory = fixtures_dir.joinpath("directory").joinpath( + directory = fixture_dir("directory").joinpath( "project_with_transitive_file_dependencies" ) package.add_dependency( @@ -887,9 +978,9 @@ def test_run_installs_with_local_poetry_file_transitive( def test_run_installs_with_local_setuptools_directory( - installer, locker, repo, package, tmpdir + installer, locker, repo, package, tmpdir, fixture_dir ): - file_path = fixtures_dir / "project_with_setup/" + file_path = fixture_dir("project_with_setup/") package.add_dependency( Factory.create_dependency("project-with-setup", {"path": str(file_path)}) ) @@ -1557,7 +1648,7 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de installer.whitelist(["pytest"]) installer.run() - assert (6 if not PY2 else 7) == installer.executor.installations_count + assert 7 == installer.executor.installations_count assert 0 == installer.executor.updates_count assert 0 == installer.executor.removals_count @@ -1807,7 +1898,12 @@ def test_installer_can_handle_old_lock_files( pool, config, installed=installed, - executor=Executor(MockEnv(version_info=(2, 7, 18)), pool, config, NullIO(),), + executor=Executor( + MockEnv(version_info=(2, 7, 18)), + pool, + config, + NullIO(), + ), ) installer.use_executor() @@ -1825,7 +1921,10 @@ def test_installer_can_handle_old_lock_files( config, installed=installed, executor=Executor( - MockEnv(version_info=(2, 7, 18), platform="win32"), pool, config, NullIO(), + MockEnv(version_info=(2, 7, 18), platform="win32"), + pool, + config, + NullIO(), ), ) installer.use_executor() @@ -1834,3 +1933,91 @@ def test_installer_can_handle_old_lock_files( # colorama will be added assert 8 == installer.executor.installations_count + + +@pytest.mark.parametrize("quiet", [True, False]) +def test_run_with_dependencies_quiet(installer, locker, repo, package, quiet): + package_a = get_package("A", "1.0") + package_b = get_package("B", "1.1") + repo.add_package(package_a) + repo.add_package(package_b) + + installer._io = IO(Input(), BufferedOutput(), BufferedOutput()) + installer._io.set_verbosity(Verbosity.QUIET if quiet else Verbosity.NORMAL) + + package.add_dependency(Factory.create_dependency("A", "~1.0")) + package.add_dependency(Factory.create_dependency("B", "^1.0")) + + installer.run() + expected = fixture("with-dependencies") + + assert locker.written_data == expected + + installer._io.output._buffer.seek(0) + if quiet: + assert installer._io.output._buffer.read() == "" + else: + assert installer._io.output._buffer.read() != "" + + +def test_installer_should_use_the_locked_version_of_git_dependencies( + installer, locker, package, repo +): + locker.locked(True) + locker.mock_lock_data( + { + "package": [ + { + "name": "demo", + "version": "0.1.1", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + "dependencies": {"pendulum": ">=1.4.4"}, + "source": { + "type": "git", + "url": "https://github.com/demo/demo.git", + "reference": "master", + "resolved_reference": "123456", + }, + }, + { + "name": "pendulum", + "version": "1.4.4", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + "dependencies": {}, + }, + ], + "metadata": { + "python-versions": "*", + "platform": "*", + "content-hash": "123456789", + "hashes": {"demo": [], "pendulum": []}, + }, + } + ) + + package.add_dependency( + Factory.create_dependency( + "demo", {"git": "https://github.com/demo/demo.git", "branch": "master"} + ) + ) + + repo.add_package(get_package("pendulum", "1.4.4")) + + installer.run() + + assert installer.executor.installations[-1] == Package( + "demo", + "0.1.1", + source_type="git", + source_url="https://github.com/demo/demo.git", + source_reference="master", + source_resolved_reference="123456", + ) diff --git a/tests/installation/test_installer_old.py b/tests/installation/test_installer_old.py index f8f5f62a469..20936fe5f09 100644 --- a/tests/installation/test_installer_old.py +++ b/tests/installation/test_installer_old.py @@ -1,12 +1,16 @@ from __future__ import unicode_literals +import itertools import sys +from pathlib import Path + import pytest -from clikit.io import NullIO +from cleo.io.null_io import NullIO +from deepdiff import DeepDiff -from poetry.core.packages import ProjectPackage +from poetry.core.packages.project_package import ProjectPackage from poetry.core.toml.file import TOMLFile from poetry.factory import Factory from poetry.installation import Installer as BaseInstaller @@ -15,8 +19,6 @@ from poetry.repositories import Pool from poetry.repositories import Repository from poetry.repositories.installed_repository import InstalledRepository -from poetry.utils._compat import PY2 -from poetry.utils._compat import Path from poetry.utils.env import MockEnv from poetry.utils.env import NullEnv from tests.helpers import get_dependency @@ -27,7 +29,7 @@ from tests.repositories.test_pypi_repository import MockRepository -fixtures_dir = Path("tests/fixtures") +RESERVED_PACKAGES = ("pip", "setuptools", "wheel") class Installer(BaseInstaller): @@ -77,15 +79,6 @@ def _get_content_hash(self): def _write_lock_data(self, data): for package in data["package"]: python_versions = str(package["python-versions"]) - if PY2: - python_versions = python_versions.decode() - if "requirements" in package: - requirements = {} - for key, value in package["requirements"].items(): - requirements[key.decode()] = value.decode() - - package["requirements"] = requirements - package["python-versions"] = python_versions self._written_data = data @@ -303,45 +296,73 @@ def test_run_install_no_dev(installer, locker, repo, package, installed): assert len(removals) == 1 -def test_run_install_remove_untracked(installer, locker, repo, package, installed): +@pytest.mark.parametrize( + "managed_reserved_package_names", + [ + i + for i in itertools.chain( + [tuple()], + itertools.permutations(RESERVED_PACKAGES, 1), + itertools.permutations(RESERVED_PACKAGES, 2), + [RESERVED_PACKAGES], + ) + ], +) +def test_run_install_remove_untracked( + managed_reserved_package_names, installer, locker, repo, package, installed +): + package_a = get_package("a", "1.0") + package_b = get_package("b", "1.1") + package_c = get_package("c", "1.2") + package_pip = get_package("pip", "20.0.0") + package_setuptools = get_package("setuptools", "20.0.0") + package_wheel = get_package("wheel", "20.0.0") + + all_packages = [ + package_a, + package_b, + package_c, + package_pip, + package_setuptools, + package_wheel, + ] + + managed_reserved_packages = [ + pkg for pkg in all_packages if pkg.name in managed_reserved_package_names + ] + locked_packages = [package_a, *managed_reserved_packages] + + for pkg in all_packages: + repo.add_package(pkg) + installed.add_package(pkg) + + installed.add_package(package) # Root package never removed. + + package.add_dependency(Factory.create_dependency(package_a.name, package_a.version)) + locker.locked(True) locker.mock_lock_data( { "package": [ { - "name": "a", - "version": "1.0", + "name": pkg.name, + "version": pkg.version, "category": "main", "optional": False, "platform": "*", "python-versions": "*", "checksum": [], } + for pkg in locked_packages ], "metadata": { "python-versions": "*", "platform": "*", "content-hash": "123456789", - "hashes": {"a": []}, + "hashes": {pkg.name: [] for pkg in locked_packages}, }, } ) - package_a = get_package("a", "1.0") - package_b = get_package("b", "1.1") - package_c = get_package("c", "1.2") - package_pip = get_package("pip", "20.0.0") - repo.add_package(package_a) - repo.add_package(package_b) - repo.add_package(package_c) - repo.add_package(package_pip) - - installed.add_package(package_a) - installed.add_package(package_b) - installed.add_package(package_c) - installed.add_package(package_pip) # Always required and never removed. - installed.add_package(package) # Root package never removed. - - package.add_dependency(Factory.create_dependency("A", "~1.0")) installer.dev_mode(True).remove_untracked(True) installer.run() @@ -353,7 +374,12 @@ def test_run_install_remove_untracked(installer, locker, repo, package, installe assert len(updates) == 0 removals = installer.installer.removals - assert set(r.name for r in removals) == {"b", "c"} + expected_removals = { + package_b.name, + package_c.name, + *managed_reserved_package_names, + } + assert set(r.name for r in removals) == expected_removals def test_run_whitelist_add(installer, locker, repo, package): @@ -745,12 +771,11 @@ def test_installer_with_pypi_repository(package, locker, installed, config): installer.run() expected = fixture("with-pypi-repository") - - assert locker.written_data == expected + assert not DeepDiff(expected, locker.written_data, ignore_order=True) -def test_run_installs_with_local_file(installer, locker, repo, package): - file_path = fixtures_dir / "distributions/demo-0.1.0-py2.py3-none-any.whl" +def test_run_installs_with_local_file(installer, locker, repo, package, fixture_dir): + file_path = fixture_dir("distributions/demo-0.1.0-py2.py3-none-any.whl") package.add_dependency(Factory.create_dependency("demo", {"file": str(file_path)})) repo.add_package(get_package("pendulum", "1.4.4")) @@ -764,9 +789,11 @@ def test_run_installs_with_local_file(installer, locker, repo, package): assert len(installer.installer.installs) == 2 -def test_run_installs_wheel_with_no_requires_dist(installer, locker, repo, package): - file_path = ( - fixtures_dir / "wheel_with_no_requires_dist/demo-0.1.0-py2.py3-none-any.whl" +def test_run_installs_wheel_with_no_requires_dist( + installer, locker, repo, package, fixture_dir +): + file_path = fixture_dir( + "wheel_with_no_requires_dist/demo-0.1.0-py2.py3-none-any.whl" ) package.add_dependency(Factory.create_dependency("demo", {"file": str(file_path)})) @@ -780,9 +807,9 @@ def test_run_installs_wheel_with_no_requires_dist(installer, locker, repo, packa def test_run_installs_with_local_poetry_directory_and_extras( - installer, locker, repo, package, tmpdir + installer, locker, repo, package, tmpdir, fixture_dir ): - file_path = fixtures_dir / "project_with_extras" + file_path = fixture_dir("project_with_extras") package.add_dependency( Factory.create_dependency( "project-with-extras", {"path": str(file_path), "extras": ["extras_a"]} @@ -801,9 +828,9 @@ def test_run_installs_with_local_poetry_directory_and_extras( def test_run_installs_with_local_poetry_directory_transitive( - installer, locker, repo, package, tmpdir + installer, locker, repo, package, tmpdir, fixture_dir ): - root_dir = fixtures_dir.joinpath("directory") + root_dir = fixture_dir("directory") package.root_dir = root_dir locker.set_lock_path(root_dir) directory = root_dir.joinpath("project_with_transitive_directory_dependencies") @@ -828,16 +855,16 @@ def test_run_installs_with_local_poetry_directory_transitive( def test_run_installs_with_local_poetry_file_transitive( - installer, locker, repo, package, tmpdir + installer, locker, repo, package, tmpdir, fixture_dir ): - root_dir = fixtures_dir.joinpath("directory") + root_dir = fixture_dir("directory") package.root_dir = root_dir locker.set_lock_path(root_dir) directory = root_dir.joinpath("project_with_transitive_file_dependencies") package.add_dependency( Factory.create_dependency( "project-with-transitive-file-dependencies", - {"path": str(directory.relative_to(fixtures_dir.joinpath("directory")))}, + {"path": str(directory.relative_to(fixture_dir("directory")))}, root_dir=root_dir, ) ) @@ -855,9 +882,9 @@ def test_run_installs_with_local_poetry_file_transitive( def test_run_installs_with_local_setuptools_directory( - installer, locker, repo, package, tmpdir + installer, locker, repo, package, tmpdir, fixture_dir ): - file_path = fixtures_dir / "project_with_setup/" + file_path = fixture_dir("project_with_setup/") package.add_dependency( Factory.create_dependency("project-with-setup", {"path": str(file_path)}) ) @@ -1515,7 +1542,7 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de installer.whitelist(["pytest"]) installer.run() - assert len(installer.installer.installs) == 6 if not PY2 else 7 + assert len(installer.installer.installs) == 7 assert len(installer.installer.updates) == 0 assert len(installer.installer.removals) == 0 diff --git a/tests/installation/test_pip_installer.py b/tests/installation/test_pip_installer.py index 206bcce59cc..20fc9d0a9d3 100644 --- a/tests/installation/test_pip_installer.py +++ b/tests/installation/test_pip_installer.py @@ -1,13 +1,16 @@ +import re import shutil +from pathlib import Path + import pytest +from cleo.io.null_io import NullIO + from poetry.core.packages.package import Package from poetry.installation.pip_installer import PipInstaller -from poetry.io.null_io import NullIO from poetry.repositories.legacy_repository import LegacyRepository from poetry.repositories.pool import Pool -from poetry.utils._compat import Path from poetry.utils.env import NullEnv @@ -188,9 +191,6 @@ def test_uninstall_git_package_nspkg_pth_cleanup(mocker, tmp_venv, pool): source_reference="master", ) - # we do this here because the virtual env might not be usable if failure case is triggered - pth_file_candidate = tmp_venv.site_packages / "{}-nspkg.pth".format(package.name) - # in order to reproduce the scenario where the git source is removed prior to proper # clean up of nspkg.pth file, we need to make sure the fixture is copied and not # symlinked into the git src directory @@ -209,8 +209,9 @@ def copy_only(source, dest): installer.install(package) installer.remove(package) - assert not Path(pth_file_candidate).exists() + pth_file = f"{package.name}-nspkg.pth" + assert not tmp_venv.site_packages.exists(pth_file) # any command in the virtual environment should trigger the error message output = tmp_venv.run("python", "-m", "site") - assert "Error processing line 1 of {}".format(pth_file_candidate) not in output + assert not re.match(rf"Error processing line 1 of .*{pth_file}", output) diff --git a/tests/masonry/builders/fixtures/pep_561_stub_only/pkg-stubs/module.pyi b/tests/masonry/builders/fixtures/pep_561_stub_only/pkg-stubs/module.pyi index d79e6e39ee0..f85a07d465a 100644 --- a/tests/masonry/builders/fixtures/pep_561_stub_only/pkg-stubs/module.pyi +++ b/tests/masonry/builders/fixtures/pep_561_stub_only/pkg-stubs/module.pyi @@ -1,4 +1,5 @@ """Example module""" from typing import Tuple + version_info = Tuple[int, int, int] diff --git a/tests/masonry/builders/fixtures/pep_561_stub_only_partial/pkg-stubs/module.pyi b/tests/masonry/builders/fixtures/pep_561_stub_only_partial/pkg-stubs/module.pyi index d79e6e39ee0..f85a07d465a 100644 --- a/tests/masonry/builders/fixtures/pep_561_stub_only_partial/pkg-stubs/module.pyi +++ b/tests/masonry/builders/fixtures/pep_561_stub_only_partial/pkg-stubs/module.pyi @@ -1,4 +1,5 @@ """Example module""" from typing import Tuple + version_info = Tuple[int, int, int] diff --git a/tests/masonry/builders/fixtures/pep_561_stub_only_src/src/pkg-stubs/module.pyi b/tests/masonry/builders/fixtures/pep_561_stub_only_src/src/pkg-stubs/module.pyi index d79e6e39ee0..f85a07d465a 100644 --- a/tests/masonry/builders/fixtures/pep_561_stub_only_src/src/pkg-stubs/module.pyi +++ b/tests/masonry/builders/fixtures/pep_561_stub_only_src/src/pkg-stubs/module.pyi @@ -1,4 +1,5 @@ """Example module""" from typing import Tuple + version_info = Tuple[int, int, int] diff --git a/tests/masonry/builders/test_editable_builder.py b/tests/masonry/builders/test_editable_builder.py index e2aac5beb41..6fb0a07a445 100644 --- a/tests/masonry/builders/test_editable_builder.py +++ b/tests/masonry/builders/test_editable_builder.py @@ -4,12 +4,14 @@ import os import shutil +from pathlib import Path + import pytest +from cleo.io.null_io import NullIO + from poetry.factory import Factory -from poetry.io.null_io import NullIO from poetry.masonry.builders.editable import EditableBuilder -from poetry.utils._compat import Path from poetry.utils.env import EnvManager from poetry.utils.env import MockEnv from poetry.utils.env import VirtualEnv @@ -76,15 +78,18 @@ def test_builder_installs_proper_files_for_standard_packages(simple_poetry, tmp_ builder.build() assert tmp_venv._bin_dir.joinpath("foo").exists() - assert tmp_venv.site_packages.joinpath("simple_project.pth").exists() - assert simple_poetry.file.parent.resolve().as_posix() == tmp_venv.site_packages.joinpath( - "simple_project.pth" - ).read_text().strip( - os.linesep + pth_file = "simple_project.pth" + assert tmp_venv.site_packages.exists(pth_file) + assert ( + simple_poetry.file.parent.resolve().as_posix() + == tmp_venv.site_packages.find(pth_file)[0].read_text().strip(os.linesep) ) - dist_info = tmp_venv.site_packages.joinpath("simple_project-1.2.3.dist-info") - assert dist_info.exists() + dist_info = "simple_project-1.2.3.dist-info" + assert tmp_venv.site_packages.exists(dist_info) + + dist_info = tmp_venv.site_packages.find(dist_info)[0] + assert dist_info.joinpath("INSTALLER").exists() assert dist_info.joinpath("METADATA").exists() assert dist_info.joinpath("RECORD").exists() @@ -92,7 +97,7 @@ def test_builder_installs_proper_files_for_standard_packages(simple_poetry, tmp_ assert "poetry" == dist_info.joinpath("INSTALLER").read_text() assert ( - "[console_scripts]\nbaz=bar:baz.boom.bim\nfoo=foo:bar\n\n" + "[console_scripts]\nbaz=bar:baz.boom.bim\nfoo=foo:bar\nfox=fuz.foo:bar.baz\n\n" == dist_info.joinpath("entry_points.txt").read_text() ) @@ -117,6 +122,7 @@ def test_builder_installs_proper_files_for_standard_packages(simple_poetry, tmp_ Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: Software Development :: Libraries :: Python Modules Project-URL: Documentation, https://python-poetry.org/docs @@ -130,7 +136,9 @@ def test_builder_installs_proper_files_for_standard_packages(simple_poetry, tmp_ assert metadata == dist_info.joinpath("METADATA").read_text(encoding="utf-8") records = dist_info.joinpath("RECORD").read_text() - assert str(tmp_venv.site_packages.joinpath("simple_project.pth")) in records + pth_file = "simple_project.pth" + assert tmp_venv.site_packages.exists(pth_file) + assert str(tmp_venv.site_packages.find(pth_file)[0]) in records assert str(tmp_venv._bin_dir.joinpath("foo")) in records assert str(tmp_venv._bin_dir.joinpath("baz")) in records assert str(dist_info.joinpath("METADATA")) in records @@ -140,12 +148,12 @@ def test_builder_installs_proper_files_for_standard_packages(simple_poetry, tmp_ baz_script = """\ #!{python} -from bar import baz.boom +from bar import baz if __name__ == '__main__': baz.boom.bim() """.format( - python=tmp_venv._bin("python") + python=tmp_venv.python ) assert baz_script == tmp_venv._bin_dir.joinpath("baz").read_text() @@ -157,31 +165,38 @@ def test_builder_installs_proper_files_for_standard_packages(simple_poetry, tmp_ if __name__ == '__main__': bar() """.format( - python=tmp_venv._bin("python") + python=tmp_venv.python ) assert foo_script == tmp_venv._bin_dir.joinpath("foo").read_text() + fox_script = """\ +#!{python} +from fuz.foo import bar + +if __name__ == '__main__': + bar.baz() +""".format( + python=tmp_venv.python + ) + + assert fox_script == tmp_venv._bin_dir.joinpath("fox").read_text() + def test_builder_falls_back_on_setup_and_pip_for_packages_with_build_scripts( - extended_poetry, + mocker, extended_poetry, tmp_dir ): - env = MockEnv(path=Path("/foo")) + pip_editable_install = mocker.patch( + "poetry.masonry.builders.editable.pip_editable_install" + ) + env = MockEnv(path=Path(tmp_dir) / "foo") builder = EditableBuilder(extended_poetry, env, NullIO()) builder.build() - - assert [ - [ - "python", - "-m", - "pip", - "install", - "-e", - str(extended_poetry.file.parent), - "--no-deps", - ] - ] == env.executed + pip_editable_install.assert_called_once_with( + extended_poetry.pyproject.file.path.parent, env + ) + assert [] == env.executed def test_builder_installs_proper_files_when_packages_configured( @@ -190,8 +205,10 @@ def test_builder_installs_proper_files_when_packages_configured( builder = EditableBuilder(project_with_include, tmp_venv, NullIO()) builder.build() - pth_file = tmp_venv.site_packages.joinpath("with_include.pth") - assert pth_file.is_file() + pth_file = "with_include.pth" + assert tmp_venv.site_packages.exists(pth_file) + + pth_file = tmp_venv.site_packages.find(pth_file)[0] paths = set() with pth_file.open() as f: @@ -207,8 +224,8 @@ def test_builder_installs_proper_files_when_packages_configured( assert len(paths) == len(expected) -def test_builder_should_execute_build_scripts(extended_without_setup_poetry): - env = MockEnv(path=Path("/foo")) +def test_builder_should_execute_build_scripts(extended_without_setup_poetry, tmp_dir): + env = MockEnv(path=Path(tmp_dir) / "foo") builder = EditableBuilder(extended_without_setup_poetry, env, NullIO()) builder.build() diff --git a/tests/mixology/helpers.py b/tests/mixology/helpers.py index fcde6701516..dc0a48e526b 100644 --- a/tests/mixology/helpers.py +++ b/tests/mixology/helpers.py @@ -1,4 +1,4 @@ -from poetry.core.packages import Package +from poetry.core.packages.package import Package from poetry.factory import Factory from poetry.mixology.failure import SolveFailure from poetry.mixology.version_solver import VersionSolver diff --git a/tests/mixology/solutions/providers/test_python_requirement_solution_provider.py b/tests/mixology/solutions/providers/test_python_requirement_solution_provider.py index 81c11d215d3..14f1b3cb6e1 100644 --- a/tests/mixology/solutions/providers/test_python_requirement_solution_provider.py +++ b/tests/mixology/solutions/providers/test_python_requirement_solution_provider.py @@ -1,5 +1,3 @@ -import pytest - from poetry.core.packages.dependency import Dependency from poetry.mixology.failure import SolveFailure from poetry.mixology.incompatibility import Incompatibility @@ -7,12 +5,8 @@ from poetry.mixology.incompatibility_cause import PythonCause from poetry.mixology.term import Term from poetry.puzzle.exceptions import SolverProblemError -from poetry.utils._compat import PY36 -@pytest.mark.skipif( - not PY36, reason="Error solutions are only available for Python ^3.6" -) def test_it_can_solve_python_incompatibility_solver_errors(): from poetry.mixology.solutions.providers import PythonRequirementSolutionProvider from poetry.mixology.solutions.solutions import PythonRequirementSolution @@ -27,9 +21,6 @@ def test_it_can_solve_python_incompatibility_solver_errors(): assert isinstance(provider.get_solutions(exception)[0], PythonRequirementSolution) -@pytest.mark.skipif( - not PY36, reason="Error solutions are only available for Python ^3.6" -) def test_it_cannot_solve_other_solver_errors(): from poetry.mixology.solutions.providers import PythonRequirementSolutionProvider diff --git a/tests/mixology/solutions/solutions/test_python_requirement_solution.py b/tests/mixology/solutions/solutions/test_python_requirement_solution.py index e264ad8d04b..46646b5ec81 100644 --- a/tests/mixology/solutions/solutions/test_python_requirement_solution.py +++ b/tests/mixology/solutions/solutions/test_python_requirement_solution.py @@ -1,6 +1,4 @@ -import pytest - -from clikit.io.buffered_io import BufferedIO +from cleo.io.buffered_io import BufferedIO from poetry.core.packages.dependency import Dependency from poetry.mixology.failure import SolveFailure @@ -8,12 +6,8 @@ from poetry.mixology.incompatibility_cause import PythonCause from poetry.mixology.term import Term from poetry.puzzle.exceptions import SolverProblemError -from poetry.utils._compat import PY36 -@pytest.mark.skipif( - not PY36, reason="Error solutions are only available for Python ^3.6" -) def test_it_provides_the_correct_solution(): from poetry.mixology.solutions.solutions import PythonRequirementSolution diff --git a/tests/mixology/version_solver/conftest.py b/tests/mixology/version_solver/conftest.py index b31634b85db..ad0dacfbe75 100644 --- a/tests/mixology/version_solver/conftest.py +++ b/tests/mixology/version_solver/conftest.py @@ -1,6 +1,6 @@ import pytest -from clikit.io import NullIO +from cleo.io.null_io import NullIO from poetry.core.packages.project_package import ProjectPackage from poetry.puzzle.provider import Provider as BaseProvider diff --git a/tests/mixology/version_solver/test_backtracking.py b/tests/mixology/version_solver/test_backtracking.py index 1716ca35f70..8fbd8874c7d 100644 --- a/tests/mixology/version_solver/test_backtracking.py +++ b/tests/mixology/version_solver/test_backtracking.py @@ -99,7 +99,7 @@ def test_backjump_to_nearer_unsatisfied_package(root, provider, repo): root.add_dependency(Factory.create_dependency("b", "*")) add_to_repo(repo, "a", "1.0.0", deps={"c": "1.0.0"}) - add_to_repo(repo, "a", "2.0.0", deps={"c": "2.0.0-nonexistent"}) + add_to_repo(repo, "a", "2.0.0", deps={"c": "2.0.0-1"}) add_to_repo(repo, "b", "1.0.0") add_to_repo(repo, "b", "2.0.0") add_to_repo(repo, "b", "3.0.0") diff --git a/tests/packages/test_locker.py b/tests/packages/test_locker.py index 35c584f2767..657eed58635 100644 --- a/tests/packages/test_locker.py +++ b/tests/packages/test_locker.py @@ -1,6 +1,8 @@ import logging import tempfile +from pathlib import Path + import pytest import tomlkit @@ -9,6 +11,7 @@ from poetry.core.semver.version import Version from poetry.factory import Factory from poetry.packages.locker import Locker +from poetry.utils._compat import Path from ..helpers import get_dependency from ..helpers import get_package @@ -73,6 +76,7 @@ def test_lock_file_data_is_ordered(locker, root): category = "main" optional = false python-versions = "*" +develop = false [package.source] type = "git" @@ -142,6 +146,136 @@ def test_locker_properly_loads_extras(locker): assert lockfile_dep.name == "lockfile" +def test_locker_properly_loads_nested_extras(locker): + content = """\ +[[package]] +name = "a" +version = "1.0" +description = "" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +b = {version = "^1.0", optional = true, extras = "c"} + +[package.extras] +b = ["b[c] (>=1.0,<2.0)"] + +[[package]] +name = "b" +version = "1.0" +description = "" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +c = {version = "^1.0", optional = true} + +[package.extras] +c = ["c (>=1.0,<2.0)"] + +[[package]] +name = "c" +version = "1.0" +description = "" +category = "main" +optional = false +python-versions = "*" + +[metadata] +python-versions = "*" +lock-version = "1.1" +content-hash = "123456789" + +[metadata.files] +"a" = [] +"b" = [] +"c" = [] +""" + + locker.lock.write(tomlkit.parse(content)) + + repository = locker.locked_repository() + assert 3 == len(repository.packages) + + packages = repository.find_packages(get_dependency("a", "1.0")) + assert len(packages) == 1 + + package = packages[0] + assert len(package.requires) == 1 + assert len(package.extras) == 1 + + dependency_b = package.extras["b"][0] + assert dependency_b.name == "b" + assert dependency_b.extras == frozenset({"c"}) + + packages = repository.find_packages(dependency_b) + assert len(packages) == 1 + + package = packages[0] + assert len(package.requires) == 1 + assert len(package.extras) == 1 + + dependency_c = package.extras["c"][0] + assert dependency_c.name == "c" + assert dependency_c.extras == frozenset() + + packages = repository.find_packages(dependency_c) + assert len(packages) == 1 + + +def test_locker_properly_loads_extras_legacy(locker): + content = """\ +[[package]] +name = "a" +version = "1.0" +description = "" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +b = {version = "^1.0", optional = true} + +[package.extras] +b = ["b (^1.0)"] + +[[package]] +name = "b" +version = "1.0" +description = "" +category = "main" +optional = false +python-versions = "*" + +[metadata] +python-versions = "*" +lock-version = "1.1" +content-hash = "123456789" + +[metadata.files] +"a" = [] +"b" = [] +""" + + locker.lock.write(tomlkit.parse(content)) + + repository = locker.locked_repository() + assert 2 == len(repository.packages) + + packages = repository.find_packages(get_dependency("a", "1.0")) + assert len(packages) == 1 + + package = packages[0] + assert len(package.requires) == 1 + assert len(package.extras) == 1 + + dependency_b = package.extras["b"][0] + assert dependency_b.name == "b" + + def test_lock_packages_with_null_description(locker, root): package_a = get_package("A", "1.0.0") package_a.description = None @@ -297,7 +431,9 @@ def test_locker_should_emit_warnings_if_lock_version_is_newer_but_allowed( [metadata.files] """.format( - version=".".join(Version.parse(Locker._VERSION).next_minor.text.split(".")[:2]) + version=".".join( + Version.parse(Locker._VERSION).next_minor().text.split(".")[:2] + ) ) caplog.set_level(logging.WARNING, logger="poetry.packages.locker") @@ -398,3 +534,68 @@ def test_locker_should_neither_emit_warnings_nor_raise_error_for_lower_compatibl _ = locker.lock_data assert 0 == len(caplog.records) + + +def test_locker_dumps_dependency_information_correctly(locker, root): + root_dir = Path(__file__).parent.parent.joinpath("fixtures") + package_a = get_package("A", "1.0.0") + package_a.add_dependency( + Factory.create_dependency( + "B", {"path": "project_with_extras", "develop": True}, root_dir=root_dir + ) + ) + package_a.add_dependency( + Factory.create_dependency( + "C", + {"path": "directory/project_with_transitive_directory_dependencies"}, + root_dir=root_dir, + ) + ) + package_a.add_dependency( + Factory.create_dependency( + "D", {"path": "distributions/demo-0.1.0.tar.gz"}, root_dir=root_dir + ) + ) + package_a.add_dependency( + Factory.create_dependency( + "E", {"url": "https://python-poetry.org/poetry-1.2.0.tar.gz"} + ) + ) + package_a.add_dependency( + Factory.create_dependency( + "F", {"git": "https://github.com/python-poetry/poetry.git", "branch": "foo"} + ) + ) + + packages = [package_a] + + locker.set_lock_data(root, packages) + + with locker.lock.open(encoding="utf-8") as f: + content = f.read() + + expected = """[[package]] +name = "A" +version = "1.0.0" +description = "" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +B = {path = "project_with_extras", develop = true} +C = {path = "directory/project_with_transitive_directory_dependencies"} +D = {path = "distributions/demo-0.1.0.tar.gz"} +E = {url = "https://python-poetry.org/poetry-1.2.0.tar.gz"} +F = {git = "https://github.com/python-poetry/poetry.git", branch = "foo"} + +[metadata] +lock-version = "1.1" +python-versions = "*" +content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" + +[metadata.files] +A = [] +""" + + assert expected == content diff --git a/tests/plugins/test_plugin_manager.py b/tests/plugins/test_plugin_manager.py new file mode 100644 index 00000000000..b8a4c5d5230 --- /dev/null +++ b/tests/plugins/test_plugin_manager.py @@ -0,0 +1,112 @@ +from pathlib import Path + +import pytest + +from cleo.io.buffered_io import BufferedIO +from entrypoints import EntryPoint + +from poetry.packages.locker import Locker +from poetry.packages.project_package import ProjectPackage +from poetry.plugins import ApplicationPlugin +from poetry.plugins import Plugin +from poetry.plugins.plugin_manager import PluginManager +from poetry.poetry import Poetry + + +CWD = Path(__file__).parent.parent / "fixtures" / "simple_project" + + +class MyPlugin(Plugin): + def activate(self, poetry, io): + io.write_line("Updating version") + poetry.package.set_version("9.9.9") + + +class MyCommandPlugin(ApplicationPlugin): + @property + def commands(self): + return [] + + +class InvalidPlugin: + def activate(self, poetry, io): + io.write_line("Updating version") + poetry.package.version = "9.9.9" + + +@pytest.fixture() +def poetry(tmp_dir, config): + poetry = Poetry( + CWD / "pyproject.toml", + {}, + ProjectPackage("simple-project", "1.2.3"), + Locker(CWD / "poetry.lock", {}), + config, + ) + + return poetry + + +@pytest.fixture() +def io(): + return BufferedIO() + + +@pytest.fixture() +def manager_factory(poetry, io): + def _manager(type="plugin"): + return PluginManager(type) + + return _manager + + +@pytest.fixture() +def no_plugin_manager(poetry, io): + return PluginManager("plugin", disable_plugins=True) + + +def test_load_plugins_and_activate(manager_factory, poetry, io, mocker): + manager = manager_factory() + + mocker.patch( + "entrypoints.get_group_all", + return_value=[ + EntryPoint("my-plugin", "tests.plugins.test_plugin_manager", "MyPlugin") + ], + ) + + manager.load_plugins() + manager.activate(poetry, io) + + assert "9.9.9" == poetry.package.version.text + assert "Updating version\n" == io.fetch_output() + + +def test_load_plugins_with_invalid_plugin(manager_factory, poetry, io, mocker): + manager = manager_factory() + + mocker.patch( + "entrypoints.get_group_all", + return_value=[ + EntryPoint( + "my-plugin", "tests.plugins.test_plugin_manager", "InvalidPlugin" + ) + ], + ) + + with pytest.raises(ValueError): + manager.load_plugins() + + +def test_load_plugins_with_plugins_disabled(no_plugin_manager, poetry, io, mocker): + mocker.patch( + "entrypoints.get_group_all", + return_value=[ + EntryPoint("my-plugin", "tests.plugins.test_plugin_manager", "MyPlugin") + ], + ) + + no_plugin_manager.load_plugins() + + assert "1.2.3" == poetry.package.version.text + assert "" == io.fetch_output() diff --git a/tests/publishing/test_publisher.py b/tests/publishing/test_publisher.py index a86c482226e..d35dcf8f2ca 100644 --- a/tests/publishing/test_publisher.py +++ b/tests/publishing/test_publisher.py @@ -1,13 +1,14 @@ import os +from pathlib import Path + import pytest -from cleo.io import BufferedIO +from cleo.io.buffered_io import BufferedIO +from cleo.io.null_io import NullIO from poetry.factory import Factory -from poetry.io.null_io import NullIO from poetry.publishing.publisher import Publisher -from poetry.utils._compat import Path def test_publish_publishes_to_pypi_by_default(fixture_dir, mocker, config): @@ -29,28 +30,36 @@ def test_publish_publishes_to_pypi_by_default(fixture_dir, mocker, config): ] == uploader_upload.call_args -def test_publish_can_publish_to_given_repository(fixture_dir, mocker, config): +@pytest.mark.parametrize( + ("fixture_name",), [("sample_project",), ("with_default_source",)] +) +def test_publish_can_publish_to_given_repository( + fixture_dir, mocker, config, fixture_name +): uploader_auth = mocker.patch("poetry.publishing.uploader.Uploader.auth") uploader_upload = mocker.patch("poetry.publishing.uploader.Uploader.upload") - poetry = Factory().create_poetry(fixture_dir("sample_project")) - poetry._config = config - poetry.config.merge( + + config.merge( { - "repositories": {"my-repo": {"url": "http://foo.bar"}}, - "http-basic": {"my-repo": {"username": "foo", "password": "bar"}}, + "repositories": {"foo": {"url": "http://foo.bar"}}, + "http-basic": {"foo": {"username": "foo", "password": "bar"}}, } ) + + mocker.patch("poetry.factory.Factory.create_config", return_value=config) + poetry = Factory().create_poetry(fixture_dir(fixture_name)) + io = BufferedIO() publisher = Publisher(poetry, io) - publisher.publish("my-repo", None, None) + publisher.publish("foo", None, None) assert [("foo", "bar")] == uploader_auth.call_args assert [ ("http://foo.bar",), {"cert": None, "client_cert": None, "dry_run": False}, ] == uploader_upload.call_args - assert "Publishing my-package (1.2.3) to my-repo" in io.fetch_output() + assert "Publishing my-package (1.2.3) to foo" in io.fetch_output() def test_publish_raises_error_for_undefined_repository(fixture_dir, mocker, config): diff --git a/tests/publishing/test_uploader.py b/tests/publishing/test_uploader.py index 8c46057bf17..e4deb372884 100644 --- a/tests/publishing/test_uploader.py +++ b/tests/publishing/test_uploader.py @@ -1,10 +1,12 @@ +from pathlib import Path + import pytest +from cleo.io.null_io import NullIO + from poetry.factory import Factory -from poetry.io.null_io import NullIO from poetry.publishing.uploader import Uploader from poetry.publishing.uploader import UploadError -from poetry.utils._compat import Path fixtures_dir = Path(__file__).parent.parent / "fixtures" @@ -14,9 +16,13 @@ def project(name): return fixtures_dir / name -def test_uploader_properly_handles_400_errors(http): +@pytest.fixture +def uploader(): + return Uploader(Factory().create_poetry(project("simple_project")), NullIO()) + + +def test_uploader_properly_handles_400_errors(http, uploader): http.register_uri(http.POST, "https://foo.com", status=400, body="Bad request") - uploader = Uploader(Factory().create_poetry(project("simple_project")), NullIO()) with pytest.raises(UploadError) as e: uploader.upload("https://foo.com") @@ -24,9 +30,8 @@ def test_uploader_properly_handles_400_errors(http): assert "HTTP Error 400: Bad Request" == str(e.value) -def test_uploader_properly_handles_403_errors(http): +def test_uploader_properly_handles_403_errors(http, uploader): http.register_uri(http.POST, "https://foo.com", status=403, body="Unauthorized") - uploader = Uploader(Factory().create_poetry(project("simple_project")), NullIO()) with pytest.raises(UploadError) as e: uploader.upload("https://foo.com") @@ -34,12 +39,22 @@ def test_uploader_properly_handles_403_errors(http): assert "HTTP Error 403: Forbidden" == str(e.value) -def test_uploader_registers_for_appropriate_400_errors(mocker, http): +def test_uploader_properly_handles_301_redirects(http, uploader): + http.register_uri(http.POST, "https://foo.com", status=301, body="Redirect") + + with pytest.raises(UploadError) as e: + uploader.upload("https://foo.com") + + assert "Redirects are not supported. Is the URL missing a trailing slash?" == str( + e.value + ) + + +def test_uploader_registers_for_appropriate_400_errors(mocker, http, uploader): register = mocker.patch("poetry.publishing.uploader.Uploader._register") http.register_uri( http.POST, "https://foo.com", status=400, body="No package was ever registered" ) - uploader = Uploader(Factory().create_poetry(project("simple_project")), NullIO()) with pytest.raises(UploadError): uploader.upload("https://foo.com") diff --git a/tests/puzzle/conftest.py b/tests/puzzle/conftest.py index e3812530bf5..5848294b765 100644 --- a/tests/puzzle/conftest.py +++ b/tests/puzzle/conftest.py @@ -1,8 +1,8 @@ import shutil -import pytest +from pathlib import Path -from poetry.utils._compat import Path +import pytest try: diff --git a/tests/puzzle/test_provider.py b/tests/puzzle/test_provider.py index 693470156f4..b5add046370 100644 --- a/tests/puzzle/test_provider.py +++ b/tests/puzzle/test_provider.py @@ -1,19 +1,18 @@ +from pathlib import Path from subprocess import CalledProcessError import pytest -from clikit.io import NullIO +from cleo.io.null_io import NullIO -from poetry.core.packages import ProjectPackage from poetry.core.packages.directory_dependency import DirectoryDependency from poetry.core.packages.file_dependency import FileDependency +from poetry.core.packages.project_package import ProjectPackage from poetry.core.packages.vcs_dependency import VCSDependency from poetry.inspection.info import PackageInfo from poetry.puzzle.provider import Provider from poetry.repositories.pool import Pool from poetry.repositories.repository import Repository -from poetry.utils._compat import PY35 -from poetry.utils._compat import Path from poetry.utils.env import EnvCommandError from poetry.utils.env import MockEnv as BaseMockEnv from tests.helpers import get_dependency @@ -47,6 +46,15 @@ def provider(root, pool): return Provider(root, pool, NullIO()) +@pytest.mark.parametrize("value", [True, False]) +def test_search_for_vcs_retains_develop_flag(provider, value): + dependency = VCSDependency( + "demo", "git", "https://github.com/demo/demo.git", develop=value + ) + package = provider.search_for_vcs(dependency)[0] + assert package.develop == value + + def test_search_for_vcs_setup_egg_info(provider): dependency = VCSDependency("demo", "git", "https://github.com/demo/demo.git") @@ -85,7 +93,6 @@ def test_search_for_vcs_setup_egg_info_with_extras(provider): } -@pytest.mark.skipif(not PY35, reason="AST parsing does not work for Python <3.4") def test_search_for_vcs_read_setup(provider, mocker): mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv()) @@ -106,7 +113,6 @@ def test_search_for_vcs_read_setup(provider, mocker): } -@pytest.mark.skipif(not PY35, reason="AST parsing does not work for Python <3.4") def test_search_for_vcs_read_setup_with_extras(provider, mocker): mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv()) @@ -232,7 +238,6 @@ def test_search_for_directory_setup_with_base(provider, directory): ) -@pytest.mark.skipif(not PY35, reason="AST parsing does not work for Python <3.4") def test_search_for_directory_setup_read_setup(provider, mocker): mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv()) @@ -261,7 +266,6 @@ def test_search_for_directory_setup_read_setup(provider, mocker): } -@pytest.mark.skipif(not PY35, reason="AST parsing does not work for Python <3.4") def test_search_for_directory_setup_read_setup_with_extras(provider, mocker): mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv()) @@ -291,7 +295,6 @@ def test_search_for_directory_setup_read_setup_with_extras(provider, mocker): } -@pytest.mark.skipif(not PY35, reason="AST parsing does not work for Python <3.4") def test_search_for_directory_setup_read_setup_with_no_dependencies(provider): dependency = DirectoryDependency( "demo", diff --git a/tests/puzzle/test_solver.py b/tests/puzzle/test_solver.py index bf9075e186a..dd193096d24 100644 --- a/tests/puzzle/test_solver.py +++ b/tests/puzzle/test_solver.py @@ -1,10 +1,12 @@ +from pathlib import Path + import pytest -from clikit.io import NullIO +from cleo.io.null_io import NullIO -from poetry.core.packages import Package -from poetry.core.packages import ProjectPackage -from poetry.core.packages import dependency_from_pep_508 +from poetry.core.packages.dependency import Dependency +from poetry.core.packages.package import Package +from poetry.core.packages.project_package import ProjectPackage from poetry.core.version.markers import parse_marker from poetry.factory import Factory from poetry.puzzle import Solver @@ -13,7 +15,6 @@ from poetry.repositories.installed_repository import InstalledRepository from poetry.repositories.pool import Pool from poetry.repositories.repository import Repository -from poetry.utils._compat import Path from poetry.utils.env import MockEnv from tests.helpers import get_dependency from tests.helpers import get_package @@ -449,6 +450,165 @@ def test_solver_returns_extras_if_requested(solver, repo, package): assert ops[0].package.marker.is_any() +@pytest.mark.parametrize(("enabled_extra",), [("one",), ("two",), (None,)]) +def test_solver_returns_extras_only_requested(solver, repo, package, enabled_extra): + extras = [enabled_extra] if enabled_extra is not None else [] + + package.add_dependency(Factory.create_dependency("A", "*")) + package.add_dependency( + Factory.create_dependency("B", {"version": "*", "extras": extras}) + ) + + package_a = get_package("A", "1.0") + package_b = get_package("B", "1.0") + package_c10 = get_package("C", "1.0") + package_c20 = get_package("C", "2.0") + + dep10 = get_dependency("C", "1.0", optional=True) + dep10._in_extras.append("one") + dep10.marker = parse_marker("extra == 'one'") + + dep20 = get_dependency("C", "2.0", optional=True) + dep20._in_extras.append("two") + dep20.marker = parse_marker("extra == 'two'") + + package_b.extras = {"one": [dep10], "two": [dep20]} + + package_b.requires.append(dep10) + package_b.requires.append(dep20) + + repo.add_package(package_a) + repo.add_package(package_b) + repo.add_package(package_c10) + repo.add_package(package_c20) + + ops = solver.solve() + + expected = [ + {"job": "install", "package": package_a}, + {"job": "install", "package": package_b}, + ] + + if enabled_extra is not None: + expected.insert( + 0, + { + "job": "install", + "package": package_c10 if enabled_extra == "one" else package_c20, + }, + ) + + check_solver_result( + ops, + expected, + ) + + assert ops[-1].package.marker.is_any() + assert ops[0].package.marker.is_any() + + +@pytest.mark.parametrize(("enabled_extra",), [("one",), ("two",), (None,)]) +def test_solver_returns_extras_when_multiple_extras_use_same_dependency( + solver, repo, package, enabled_extra +): + package.add_dependency(Factory.create_dependency("A", "*")) + + package_a = get_package("A", "1.0") + package_b = get_package("B", "1.0") + package_c = get_package("C", "1.0") + + dep = get_dependency("C", "*", optional=True) + dep._in_extras.append("one") + dep._in_extras.append("two") + + package_b.extras = {"one": [dep], "two": [dep]} + + package_b.requires.append(dep) + + extras = [enabled_extra] if enabled_extra is not None else [] + package_a.add_dependency( + Factory.create_dependency("B", {"version": "*", "extras": extras}) + ) + + repo.add_package(package_a) + repo.add_package(package_b) + repo.add_package(package_c) + + ops = solver.solve() + + expected = [ + {"job": "install", "package": package_b}, + {"job": "install", "package": package_a}, + ] + + if enabled_extra is not None: + expected.insert(0, {"job": "install", "package": package_c}) + + check_solver_result( + ops, + expected, + ) + + assert ops[-1].package.marker.is_any() + assert ops[0].package.marker.is_any() + + +@pytest.mark.parametrize(("enabled_extra",), [("one",), ("two",), (None,)]) +def test_solver_returns_extras_only_requested_nested( + solver, repo, package, enabled_extra +): + package.add_dependency(Factory.create_dependency("A", "*")) + + package_a = get_package("A", "1.0") + package_b = get_package("B", "1.0") + package_c10 = get_package("C", "1.0") + package_c20 = get_package("C", "2.0") + + dep10 = get_dependency("C", "1.0", optional=True) + dep10._in_extras.append("one") + dep10.marker = parse_marker("extra == 'one'") + + dep20 = get_dependency("C", "2.0", optional=True) + dep20._in_extras.append("two") + dep20.marker = parse_marker("extra == 'two'") + + package_b.extras = {"one": [dep10], "two": [dep20]} + + package_b.requires.append(dep10) + package_b.requires.append(dep20) + + extras = [enabled_extra] if enabled_extra is not None else [] + package_a.add_dependency( + Factory.create_dependency("B", {"version": "*", "extras": extras}) + ) + + repo.add_package(package_a) + repo.add_package(package_b) + repo.add_package(package_c10) + repo.add_package(package_c20) + + ops = solver.solve() + + expected = [ + {"job": "install", "package": package_b}, + {"job": "install", "package": package_a}, + ] + + if enabled_extra is not None: + expected.insert( + 0, + { + "job": "install", + "package": package_c10 if enabled_extra == "one" else package_c20, + }, + ) + + check_solver_result(ops, expected) + + assert ops[-1].package.marker.is_any() + assert ops[0].package.marker.is_any() + + def test_solver_returns_prereleases_if_requested(solver, repo, package): package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "*")) @@ -620,6 +780,50 @@ def test_solver_sub_dependencies_with_not_supported_python_version( check_solver_result(ops, [{"job": "install", "package": package_a}]) +def test_solver_sub_dependencies_with_not_supported_python_version_transitive( + solver, repo, package +): + solver.provider.set_package_python_versions("^3.4") + + package.add_dependency( + Factory.create_dependency("httpx", {"version": "^0.17.1", "python": "^3.6"}) + ) + + httpx = get_package("httpx", "0.17.1") + httpx.python_versions = ">=3.6" + + httpcore = get_package("httpcore", "0.12.3") + httpcore.python_versions = ">=3.6" + + sniffio_1_1_0 = get_package("sniffio", "1.1.0") + sniffio_1_1_0.python_versions = ">=3.5" + + sniffio = get_package("sniffio", "1.2.0") + sniffio.python_versions = ">=3.5" + + httpx.add_dependency( + Factory.create_dependency("httpcore", {"version": ">=0.12.1,<0.13"}) + ) + httpx.add_dependency(Factory.create_dependency("sniffio", {"version": "*"})) + httpcore.add_dependency(Factory.create_dependency("sniffio", {"version": "==1.*"})) + + repo.add_package(httpx) + repo.add_package(httpcore) + repo.add_package(sniffio) + repo.add_package(sniffio_1_1_0) + + ops = solver.solve() + + check_solver_result( + ops, + [ + {"job": "install", "package": sniffio, "skipped": False}, + {"job": "install", "package": httpcore, "skipped": False}, + {"job": "install", "package": httpx, "skipped": False}, + ], + ) + + def test_solver_with_dependency_in_both_main_and_dev_dependencies( solver, repo, package ): @@ -1202,9 +1406,9 @@ def test_solver_finds_compatible_package_for_dependency_python_not_fully_compati def test_solver_does_not_trigger_new_resolution_on_duplicate_dependencies_if_only_extras( solver, repo, package ): - dep1 = dependency_from_pep_508('B (>=1.0); extra == "foo"') + dep1 = Dependency.create_from_pep_508('B (>=1.0); extra == "foo"') dep1.activate() - dep2 = dependency_from_pep_508('B (>=2.0); extra == "bar"') + dep2 = Dependency.create_from_pep_508('B (>=2.0); extra == "bar"') dep2.activate() package.add_dependency( @@ -1336,7 +1540,7 @@ def test_solver_ignores_dependencies_with_incompatible_python_full_version_marke package_a = get_package("A", "1.0.0") package_a.requires.append( - dependency_from_pep_508( + Dependency.create_from_pep_508( 'B (<2.0); platform_python_implementation == "PyPy" and python_full_version < "2.7.9"' ) ) @@ -1515,6 +1719,60 @@ def test_solver_can_resolve_directory_dependencies(solver, repo, package): assert op.package.source_url == path +def test_solver_can_resolve_directory_dependencies_nested_editable( + solver, repo, pool, installed, locked, io +): + base = Path(__file__).parent.parent / "fixtures" / "project_with_nested_local" + poetry = Factory().create_poetry(cwd=base) + package = poetry.package + + solver = Solver( + package, pool, installed, locked, io, provider=Provider(package, pool, io) + ) + + ops = solver.solve() + + check_solver_result( + ops, + [ + { + "job": "install", + "package": Package( + "quix", + "1.2.3", + source_type="directory", + source_url=(base / "quix").as_posix(), + ), + "skipped": False, + }, + { + "job": "install", + "package": Package( + "bar", + "1.2.3", + source_type="directory", + source_url=(base / "bar").as_posix(), + ), + "skipped": False, + }, + { + "job": "install", + "package": Package( + "foo", + "1.2.3", + source_type="directory", + source_url=(base / "foo").as_posix(), + ), + "skipped": False, + }, + ], + ) + + for op in ops: + assert op.package.source_type == "directory" + assert op.package.develop is True + + def test_solver_can_resolve_directory_dependencies_with_extras(solver, repo, package): pendulum = get_package("pendulum", "2.0.3") cleo = get_package("cleo", "1.0.0") @@ -2141,7 +2399,8 @@ def test_ignore_python_constraint_no_overlap_dependencies(solver, repo, package) ops = solver.solve() check_solver_result( - ops, [{"job": "install", "package": pytest}], + ops, + [{"job": "install", "package": pytest}], ) @@ -2188,11 +2447,12 @@ def test_solver_remove_untracked_single(package, pool, installed, locked, io): check_solver_result(ops, [{"job": "remove", "package": package_a}]) +@pytest.mark.skip(reason="Poetry no longer has critical package requirements") def test_solver_remove_untracked_keeps_critical_package( package, pool, installed, locked, io ): solver = Solver(package, pool, installed, locked, io, remove_untracked=True) - package_pip = get_package("pip", "1.0") + package_pip = get_package("setuptools", "1.0") installed.add_package(package_pip) ops = solver.solve() @@ -2358,7 +2618,8 @@ def test_solver_should_use_the_python_constraint_from_the_environment_if_availab ops = solver.solve() check_solver_result( - ops, [{"job": "install", "package": b}, {"job": "install", "package": a}], + ops, + [{"job": "install", "package": b}, {"job": "install", "package": a}], ) @@ -2435,7 +2696,6 @@ def test_solver_can_resolve_transitive_extras(solver, repo, package): requests = get_package("requests", "2.24.0") requests.add_dependency(Factory.create_dependency("certifi", ">=2017.4.17")) dep = get_dependency("PyOpenSSL", ">=0.14") - dep.in_extras.append("security") requests.add_dependency( Factory.create_dependency("PyOpenSSL", {"version": ">=0.14", "optional": True}) ) @@ -2465,6 +2725,41 @@ def test_solver_can_resolve_transitive_extras(solver, repo, package): ) +def test_solver_can_resolve_for_packages_with_missing_extras(solver, repo, package): + package.add_dependency( + Factory.create_dependency( + "django-anymail", {"version": "^6.0", "extras": ["postmark"]} + ) + ) + + django_anymail = get_package("django-anymail", "6.1.0") + django_anymail.add_dependency(Factory.create_dependency("django", ">=2.0")) + django_anymail.add_dependency(Factory.create_dependency("requests", ">=2.4.3")) + django_anymail.add_dependency( + Factory.create_dependency("boto3", {"version": "*", "optional": True}) + ) + django_anymail.extras["amazon_ses"] = [Factory.create_dependency("boto3", "*")] + django = get_package("django", "2.2.0") + boto3 = get_package("boto3", "1.0.0") + requests = get_package("requests", "2.24.0") + + repo.add_package(django_anymail) + repo.add_package(django) + repo.add_package(boto3) + repo.add_package(requests) + + ops = solver.solve() + + check_solver_result( + ops, + [ + {"job": "install", "package": django}, + {"job": "install", "package": requests}, + {"job": "install", "package": django_anymail}, + ], + ) + + def test_solver_can_resolve_python_restricted_package_dependencies( solver, repo, package, locked ): @@ -2496,3 +2791,53 @@ def test_solver_can_resolve_python_restricted_package_dependencies( {"job": "install", "package": pre_commit}, ], ) + + +def test_solver_should_not_raise_errors_for_irrelevant_transitive_python_constraints( + solver, repo, package +): + package.python_versions = "~2.7 || ^3.5" + solver.provider.set_package_python_versions("~2.7 || ^3.5") + package.add_dependency(Factory.create_dependency("virtualenv", "^20.4.3")) + package.add_dependency( + Factory.create_dependency("pre-commit", {"version": "^2.6", "python": "^3.6.1"}) + ) + + virtualenv = get_package("virtualenv", "20.4.3") + virtualenv.python_versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + virtualenv.add_dependency( + Factory.create_dependency( + "importlib-resources", {"version": "*", "markers": 'python_version < "3.7"'} + ) + ) + + pre_commit = Package("pre-commit", "2.7.1") + pre_commit.python_versions = ">=3.6.1" + pre_commit.add_dependency( + Factory.create_dependency( + "importlib-resources", {"version": "*", "markers": 'python_version < "3.7"'} + ) + ) + + importlib_resources = get_package("importlib-resources", "5.1.2") + importlib_resources.python_versions = ">=3.6" + + importlib_resources_3_2_1 = get_package("importlib-resources", "3.2.1") + importlib_resources_3_2_1.python_versions = ( + "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + ) + + repo.add_package(virtualenv) + repo.add_package(pre_commit) + repo.add_package(importlib_resources) + repo.add_package(importlib_resources_3_2_1) + ops = solver.solve() + + check_solver_result( + ops, + [ + {"job": "install", "package": importlib_resources_3_2_1}, + {"job": "install", "package": pre_commit}, + {"job": "install", "package": virtualenv}, + ], + ) diff --git a/tests/repositories/fixtures/installed/lib/python3.7/site-packages/directory_pep_610-1.2.3.dist-info/METADATA b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/directory_pep_610-1.2.3.dist-info/METADATA new file mode 100644 index 00000000000..30928a39f0c --- /dev/null +++ b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/directory_pep_610-1.2.3.dist-info/METADATA @@ -0,0 +1,6 @@ +Metadata-Version: 2.1 +Name: directory-pep-610 +Version: 1.2.3 +Summary: Foo +License: MIT +Requires-Python: >=3.6 diff --git a/tests/repositories/fixtures/installed/lib/python3.7/site-packages/directory_pep_610-1.2.3.dist-info/direct_url.json b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/directory_pep_610-1.2.3.dist-info/direct_url.json new file mode 100644 index 00000000000..3385611ce74 --- /dev/null +++ b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/directory_pep_610-1.2.3.dist-info/direct_url.json @@ -0,0 +1,4 @@ +{ + "url": "file:///path/to/distributions/directory-pep-610", + "dir_info": {} +} diff --git a/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable_directory_pep_610-1.2.3.dist-info/METADATA b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable_directory_pep_610-1.2.3.dist-info/METADATA new file mode 100644 index 00000000000..337c6fc4301 --- /dev/null +++ b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable_directory_pep_610-1.2.3.dist-info/METADATA @@ -0,0 +1,6 @@ +Metadata-Version: 2.1 +Name: editable-directory-pep-610 +Version: 1.2.3 +Summary: Foo +License: MIT +Requires-Python: >=3.6 diff --git a/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable_directory_pep_610-1.2.3.dist-info/direct_url.json b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable_directory_pep_610-1.2.3.dist-info/direct_url.json new file mode 100644 index 00000000000..e45f7c31a35 --- /dev/null +++ b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable_directory_pep_610-1.2.3.dist-info/direct_url.json @@ -0,0 +1,6 @@ +{ + "url": "file:///path/to/distributions/directory-pep-610", + "dir_info": { + "editable": true + } +} diff --git a/tests/repositories/fixtures/installed/lib/python3.7/site-packages/file_pep_610-1.2.3.dist-info/METADATA b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/file_pep_610-1.2.3.dist-info/METADATA new file mode 100644 index 00000000000..9478ca1f064 --- /dev/null +++ b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/file_pep_610-1.2.3.dist-info/METADATA @@ -0,0 +1,6 @@ +Metadata-Version: 2.1 +Name: file-pep-610 +Version: 1.2.3 +Summary: Foo +License: MIT +Requires-Python: >=3.6 diff --git a/tests/repositories/fixtures/installed/lib/python3.7/site-packages/file_pep_610-1.2.3.dist-info/direct_url.json b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/file_pep_610-1.2.3.dist-info/direct_url.json new file mode 100644 index 00000000000..d481649fa32 --- /dev/null +++ b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/file_pep_610-1.2.3.dist-info/direct_url.json @@ -0,0 +1,6 @@ +{ + "url": "file:///path/to/distributions/file-pep-610-1.2.3.tar.gz", + "archive_info": { + "hash": "sha256=2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" + } +} diff --git a/tests/repositories/fixtures/installed/lib/python3.7/site-packages/git_pep_610-1.2.3.dist-info/METADATA b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/git_pep_610-1.2.3.dist-info/METADATA new file mode 100644 index 00000000000..bfc73cf720c --- /dev/null +++ b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/git_pep_610-1.2.3.dist-info/METADATA @@ -0,0 +1,6 @@ +Metadata-Version: 2.1 +Name: git-pep-610 +Version: 1.2.3 +Summary: Foo +License: MIT +Requires-Python: >=3.6 diff --git a/tests/repositories/fixtures/installed/lib/python3.7/site-packages/git_pep_610-1.2.3.dist-info/direct_url.json b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/git_pep_610-1.2.3.dist-info/direct_url.json new file mode 100644 index 00000000000..a3115254845 --- /dev/null +++ b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/git_pep_610-1.2.3.dist-info/direct_url.json @@ -0,0 +1,8 @@ +{ + "url": "https://github.com/demo/git-pep-610.git", + "vcs_info": { + "vcs": "git", + "requested_revision": "my-branch", + "commit_id": "123456" + } +} diff --git a/tests/repositories/fixtures/installed/lib/python3.7/site-packages/standard-1.2.3.dist-info/METADATA b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/standard-1.2.3.dist-info/METADATA new file mode 100644 index 00000000000..245121d496d --- /dev/null +++ b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/standard-1.2.3.dist-info/METADATA @@ -0,0 +1,22 @@ +Metadata-Version: 2.1 +Name: standard +Version: 1.2.3 +Summary: Standard description. +License: MIT +Keywords: cli,commands +Author: Foo Bar +Author-email: foo@bar.com +Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Description-Content-Type: text/x-rst + +Editable +#### diff --git a/tests/repositories/fixtures/installed/lib/python3.7/site-packages/standard.pth b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/standard.pth new file mode 100644 index 00000000000..aa0bc074b62 --- /dev/null +++ b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/standard.pth @@ -0,0 +1 @@ +standard \ No newline at end of file diff --git a/tests/repositories/fixtures/installed/lib/python3.7/site-packages/url_pep_610-1.2.3.dist-info/METADATA b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/url_pep_610-1.2.3.dist-info/METADATA new file mode 100644 index 00000000000..7b2afd3189b --- /dev/null +++ b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/url_pep_610-1.2.3.dist-info/METADATA @@ -0,0 +1,6 @@ +Metadata-Version: 2.1 +Name: url-pep-610 +Version: 1.2.3 +Summary: Foo +License: MIT +Requires-Python: >=3.6 diff --git a/tests/repositories/fixtures/installed/lib/python3.7/site-packages/url_pep_610-1.2.3.dist-info/direct_url.json b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/url_pep_610-1.2.3.dist-info/direct_url.json new file mode 100644 index 00000000000..b36e4055294 --- /dev/null +++ b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/url_pep_610-1.2.3.dist-info/direct_url.json @@ -0,0 +1,4 @@ +{ + "url": "https://python-poetry.org/distributions/url-pep-610-1.2.3.tar.gz", + "archive_info": {} +} diff --git a/tests/repositories/fixtures/legacy/ipython.html b/tests/repositories/fixtures/legacy/ipython.html index cbdc19e5850..5b61c92d784 100644 --- a/tests/repositories/fixtures/legacy/ipython.html +++ b/tests/repositories/fixtures/legacy/ipython.html @@ -5,10 +5,10 @@

Links for ipython

- ipython-5.7.0-py2-none-any.whl
+ ipython-5.7.0-py2-none-any.whl
ipython-5.7.0-py3-none-any.whl
ipython-5.7.0.tar.gz
- ipython-7.5.0-py3-none-any.whl
+ ipython-7.5.0-py3-none-any.whl
ipython-7.5.0.tar.gz
diff --git a/tests/repositories/test_installed_repository.py b/tests/repositories/test_installed_repository.py index 79ac262671c..8fd3da25ea2 100644 --- a/tests/repositories/test_installed_repository.py +++ b/tests/repositories/test_installed_repository.py @@ -1,16 +1,15 @@ +from pathlib import Path from typing import Optional import pytest -from pytest_mock.plugin import MockFixture +from pytest_mock.plugin import MockerFixture -from poetry.core.packages import Package +from poetry.core.packages.package import Package from poetry.repositories.installed_repository import InstalledRepository -from poetry.utils._compat import PY36 -from poetry.utils._compat import Path from poetry.utils._compat import metadata -from poetry.utils._compat import zipp from poetry.utils.env import MockEnv as BaseMockEnv +from tests.compat import zipp FIXTURES_DIR = Path(__file__).parent / "fixtures" @@ -26,10 +25,18 @@ zipp.Path(str(SITE_PURELIB / "foo-0.1.0-py3.8.egg"), "EGG-INFO") ), metadata.PathDistribution(VENDOR_DIR / "attrs-19.3.0.dist-info"), + metadata.PathDistribution(SITE_PURELIB / "standard-1.2.3.dist-info"), metadata.PathDistribution(SITE_PURELIB / "editable-2.3.4.dist-info"), metadata.PathDistribution(SITE_PURELIB / "editable-with-import-2.3.4.dist-info"), metadata.PathDistribution(SITE_PLATLIB / "lib64-2.3.4.dist-info"), metadata.PathDistribution(SITE_PLATLIB / "bender-2.0.5.dist-info"), + metadata.PathDistribution(SITE_PURELIB / "git_pep_610-1.2.3.dist-info"), + metadata.PathDistribution(SITE_PURELIB / "url_pep_610-1.2.3.dist-info"), + metadata.PathDistribution(SITE_PURELIB / "file_pep_610-1.2.3.dist-info"), + metadata.PathDistribution(SITE_PURELIB / "directory_pep_610-1.2.3.dist-info"), + metadata.PathDistribution( + SITE_PURELIB / "editable_directory_pep_610-1.2.3.dist-info" + ), ] @@ -47,12 +54,12 @@ def sys_path(self): @pytest.fixture -def env(): # type: () -> MockEnv +def env() -> MockEnv: return MockEnv(path=ENV_DIR) @pytest.fixture -def repository(mocker, env): # type: (MockFixture, MockEnv) -> InstalledRepository +def repository(mocker: MockerFixture, env: MockEnv) -> InstalledRepository: mocker.patch( "poetry.utils._compat.metadata.Distribution.discover", return_value=INSTALLED_RESULTS, @@ -73,8 +80,8 @@ def repository(mocker, env): # type: (MockFixture, MockEnv) -> InstalledReposit def get_package_from_repository( - name, repository -): # type: (str, InstalledRepository) -> Optional[Package] + name: str, repository: InstalledRepository +) -> Optional[Package]: for pkg in repository.packages: if pkg.name == name: return pkg @@ -134,9 +141,6 @@ def test_load_platlib_package(repository): assert lib64.version.text == "2.3.4" -@pytest.mark.skipif( - not PY36, reason="pathlib.resolve() does not support strict argument" -) def test_load_editable_package(repository): # test editable package with text .pth file editable = get_package_from_repository("editable", repository) @@ -158,3 +162,70 @@ def test_load_editable_with_import_package(repository): assert editable.version.text == "2.3.4" assert editable.source_type is None assert editable.source_url is None + + +def test_load_standard_package_with_pth_file(repository): + # test standard packages with .pth file is not treated as editable + standard = get_package_from_repository("standard", repository) + assert standard is not None + assert standard.name == "standard" + assert standard.version.text == "1.2.3" + assert standard.source_type is None + assert standard.source_url is None + + +def test_load_pep_610_compliant_git_packages(repository): + package = get_package_from_repository("git-pep-610", repository) + + assert package is not None + assert package.name == "git-pep-610" + assert package.version.text == "1.2.3" + assert package.source_type == "git" + assert package.source_url == "https://github.com/demo/git-pep-610.git" + assert package.source_reference == "my-branch" + assert package.source_resolved_reference == "123456" + + +def test_load_pep_610_compliant_url_packages(repository): + package = get_package_from_repository("url-pep-610", repository) + + assert package is not None + assert package.name == "url-pep-610" + assert package.version.text == "1.2.3" + assert package.source_type == "url" + assert ( + package.source_url + == "https://python-poetry.org/distributions/url-pep-610-1.2.3.tar.gz" + ) + + +def test_load_pep_610_compliant_file_packages(repository): + package = get_package_from_repository("file-pep-610", repository) + + assert package is not None + assert package.name == "file-pep-610" + assert package.version.text == "1.2.3" + assert package.source_type == "file" + assert package.source_url == "/path/to/distributions/file-pep-610-1.2.3.tar.gz" + + +def test_load_pep_610_compliant_directory_packages(repository): + package = get_package_from_repository("directory-pep-610", repository) + + assert package is not None + assert package.name == "directory-pep-610" + assert package.version.text == "1.2.3" + assert package.source_type == "directory" + assert package.source_url == "/path/to/distributions/directory-pep-610" + assert not package.develop + + +def test_load_pep_610_compliant_editable_directory_packages(repository): + package = get_package_from_repository("editable-directory-pep-610", repository) + + assert package is not None + assert package.name == "editable-directory-pep-610" + assert package.version.text == "1.2.3" + assert package.source_type == "directory" + assert package.source_url == "/path/to/distributions/directory-pep-610" + assert package.develop diff --git a/tests/repositories/test_legacy_repository.py b/tests/repositories/test_legacy_repository.py index 7fd131c70e4..fef4b6d2d68 100644 --- a/tests/repositories/test_legacy_repository.py +++ b/tests/repositories/test_legacy_repository.py @@ -1,15 +1,16 @@ import shutil +from pathlib import Path + import pytest +import requests -from poetry.core.packages import Dependency +from poetry.core.packages.dependency import Dependency from poetry.factory import Factory from poetry.repositories.exceptions import PackageNotFound from poetry.repositories.exceptions import RepositoryError from poetry.repositories.legacy_repository import LegacyRepository from poetry.repositories.legacy_repository import Page -from poetry.utils._compat import PY35 -from poetry.utils._compat import Path try: @@ -95,16 +96,6 @@ def test_get_package_information_fallback_read_setup(): == "Jupyter metapackage. Install all the Jupyter components in one go." ) - if PY35: - assert package.requires == [ - Dependency("notebook", "*"), - Dependency("qtconsole", "*"), - Dependency("jupyter-console", "*"), - Dependency("nbconvert", "*"), - Dependency("ipykernel", "*"), - Dependency("ipywidgets", "*"), - ] - def test_get_package_information_skips_dependencies_with_invalid_constraints(): repo = MockRepository() @@ -117,7 +108,7 @@ def test_get_package_information_skips_dependencies_with_invalid_constraints(): package.description == "Python Language Server for the Language Server Protocol" ) - assert 19 == len(package.requires) + assert 25 == len(package.requires) assert sorted( [r for r in package.requires if not r.is_optional()], key=lambda r: r.name ) == [ @@ -216,7 +207,7 @@ def test_get_package_from_both_py2_and_py3_specific_wheels(): assert "ipython" == package.name assert "5.7.0" == package.version.text assert "*" == package.python_versions - assert 26 == len(package.requires) + assert 41 == len(package.requires) expected = [ Dependency("appnope", "*"), @@ -282,7 +273,7 @@ def test_get_package_retrieves_non_sha256_hashes(): expected = [ { "file": "ipython-7.5.0-py3-none-any.whl", - "hash": "md5:dbdc53e3918f28fa335a173432402a00", + "hash": "sha256:78aea20b7991823f6a32d55f4e963a61590820e43f666ad95ad07c7f0c704efa", }, { "file": "ipython-7.5.0.tar.gz", @@ -293,12 +284,40 @@ def test_get_package_retrieves_non_sha256_hashes(): assert expected == package.files +def test_get_package_retrieves_non_sha256_hashes_mismatching_known_hash(): + repo = MockRepository() + + package = repo.package("ipython", "5.7.0") + + expected = [ + { + "file": "ipython-5.7.0-py2-none-any.whl", + "hash": "md5:a10a802ef98da741cd6f4f6289d47ba7", + }, + { + "file": "ipython-5.7.0-py3-none-any.whl", + "hash": "sha256:fc0464e68f9c65cd8c453474b4175432cc29ecb6c83775baedf6dbfcee9275ab", + }, + { + "file": "ipython-5.7.0.tar.gz", + "hash": "sha256:8db43a7fb7619037c98626613ff08d03dda9d5d12c84814a4504c78c0da8323c", + }, + ] + + assert expected == package.files + + def test_get_package_retrieves_packages_with_no_hashes(): repo = MockRepository() package = repo.package("jupyter", "1.0.0") - assert [] == package.files + assert [ + { + "file": "jupyter-1.0.0.tar.gz", + "hash": "sha256:d9dc4b3318f310e34c82951ea5d6683f67bed7def4b259fafbfe4f1beb1d8e5f", + } + ] == package.files class MockHttpRepository(LegacyRepository): @@ -319,16 +338,29 @@ def test_get_200_returns_page(http): assert repo._get("/foo") -def test_get_404_returns_none(http): - repo = MockHttpRepository({"/foo": 404}, http) +@pytest.mark.parametrize("status_code", [401, 403, 404]) +def test_get_40x_and_returns_none(http, status_code): + repo = MockHttpRepository({"/foo": status_code}, http) assert repo._get("/foo") is None -def test_get_4xx_and_5xx_raises(http): - endpoints = {"/{}".format(code): code for code in {401, 403, 500}} - repo = MockHttpRepository(endpoints, http) +def test_get_5xx_raises(http): + repo = MockHttpRepository({"/foo": 500}, http) + + with pytest.raises(RepositoryError): + repo._get("/foo") + + +def test_get_redirected_response_url(http, mocker): + repo = MockHttpRepository({"/foo": 200}, http) + redirect_url = "http://legacy.redirect.bar" + + def get_mock(url): + response = requests.Response() + response.status_code = 200 + response.url = redirect_url + "/foo" + return response - for endpoint in endpoints: - with pytest.raises(RepositoryError): - repo._get(endpoint) + mocker.patch.object(requests.Session, "get", side_effect=get_mock) + assert repo._get("/foo")._url == "http://legacy.redirect.bar/foo/" diff --git a/tests/repositories/test_pypi_repository.py b/tests/repositories/test_pypi_repository.py index ef094f3f1d5..f567314a34c 100644 --- a/tests/repositories/test_pypi_repository.py +++ b/tests/repositories/test_pypi_repository.py @@ -2,17 +2,16 @@ import shutil from io import BytesIO +from pathlib import Path import pytest from requests.exceptions import TooManyRedirects from requests.models import Response -from poetry.core.packages import Dependency +from poetry.core.packages.dependency import Dependency from poetry.factory import Factory from poetry.repositories.pypi_repository import PyPiRepository -from poetry.utils._compat import PY35 -from poetry.utils._compat import Path from poetry.utils._compat import encode @@ -136,7 +135,6 @@ def test_fallback_inspects_sdist_first_if_no_matching_wheels_can_be_found(): assert dep.python_versions == "~2.7" -@pytest.mark.skipif(not PY35, reason="AST parsing does not work for Python <3.4") def test_fallback_can_read_setup_to_get_dependencies(): repo = MockRepository(fallback=True) @@ -165,7 +163,7 @@ def test_pypi_repository_supports_reading_bz2_files(): package = repo.package("twisted", "18.9.0") assert package.name == "twisted" - assert 28 == len(package.requires) + assert 71 == len(package.requires) assert sorted( [r for r in package.requires if not r.is_optional()], key=lambda r: r.name ) == [ diff --git a/tests/test_factory.py b/tests/test_factory.py index d7758e8deaa..e9a9a9a5994 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -2,17 +2,28 @@ from __future__ import absolute_import from __future__ import unicode_literals +from pathlib import Path + import pytest +from entrypoints import EntryPoint + from poetry.core.toml.file import TOMLFile from poetry.factory import Factory -from poetry.utils._compat import PY2 -from poetry.utils._compat import Path +from poetry.plugins.plugin import Plugin +from poetry.repositories.legacy_repository import LegacyRepository +from poetry.repositories.pypi_repository import PyPiRepository fixtures_dir = Path(__file__).parent / "fixtures" +class MyPlugin(Plugin): + def activate(self, poetry, io): + io.write_line("Updating version") + poetry.package.set_version("9.9.9") + + def test_create_poetry(): poetry = Factory().create_poetry(fixtures_dir / "sample_project") @@ -108,6 +119,7 @@ def test_create_poetry(): "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules", ] @@ -150,6 +162,89 @@ def test_poetry_with_default_source(): assert 1 == len(poetry.pool.repositories) +def test_poetry_with_non_default_source(): + poetry = Factory().create_poetry(fixtures_dir / "with_non_default_source") + + assert len(poetry.pool.repositories) == 2 + + assert not poetry.pool.has_default() + + assert poetry.pool.repositories[0].name == "foo" + assert isinstance(poetry.pool.repositories[0], LegacyRepository) + + assert poetry.pool.repositories[1].name == "PyPI" + assert isinstance(poetry.pool.repositories[1], PyPiRepository) + + +def test_poetry_with_non_default_secondary_source(): + poetry = Factory().create_poetry(fixtures_dir / "with_non_default_secondary_source") + + assert len(poetry.pool.repositories) == 2 + + assert poetry.pool.has_default() + + repository = poetry.pool.repositories[0] + assert repository.name == "PyPI" + assert isinstance(repository, PyPiRepository) + + repository = poetry.pool.repositories[1] + assert repository.name == "foo" + assert isinstance(repository, LegacyRepository) + + +def test_poetry_with_non_default_multiple_secondary_sources(): + poetry = Factory().create_poetry( + fixtures_dir / "with_non_default_multiple_secondary_sources" + ) + + assert len(poetry.pool.repositories) == 3 + + assert poetry.pool.has_default() + + repository = poetry.pool.repositories[0] + assert repository.name == "PyPI" + assert isinstance(repository, PyPiRepository) + + repository = poetry.pool.repositories[1] + assert repository.name == "foo" + assert isinstance(repository, LegacyRepository) + + repository = poetry.pool.repositories[2] + assert repository.name == "bar" + assert isinstance(repository, LegacyRepository) + + +def test_poetry_with_non_default_multiple_sources(): + poetry = Factory().create_poetry(fixtures_dir / "with_non_default_multiple_sources") + + assert len(poetry.pool.repositories) == 3 + + assert not poetry.pool.has_default() + + repository = poetry.pool.repositories[0] + assert repository.name == "bar" + assert isinstance(repository, LegacyRepository) + + repository = poetry.pool.repositories[1] + assert repository.name == "foo" + assert isinstance(repository, LegacyRepository) + + repository = poetry.pool.repositories[2] + assert repository.name == "PyPI" + assert isinstance(repository, PyPiRepository) + + +def test_poetry_with_no_default_source(): + poetry = Factory().create_poetry(fixtures_dir / "sample_project") + + assert len(poetry.pool.repositories) == 1 + + assert poetry.pool.has_default() + + assert poetry.pool.repositories[0].name == "PyPI" + assert isinstance(poetry.pool.repositories[0], PyPiRepository) + + def test_poetry_with_two_default_sources(): with pytest.raises(ValueError) as e: Factory().create_poetry(fixtures_dir / "with_two_default_sources") @@ -169,16 +264,10 @@ def test_validate_fails(): content = complete.read()["tool"]["poetry"] content["this key is not in the schema"] = "" - if PY2: - expected = ( - "Additional properties are not allowed " - "(u'this key is not in the schema' was unexpected)" - ) - else: - expected = ( - "Additional properties are not allowed " - "('this key is not in the schema' was unexpected)" - ) + expected = ( + "Additional properties are not allowed " + "('this key is not in the schema' was unexpected)" + ) assert Factory.validate(content) == {"errors": [expected], "warnings": []} @@ -189,13 +278,7 @@ def test_create_poetry_fails_on_invalid_configuration(): Path(__file__).parent / "fixtures" / "invalid_pyproject" / "pyproject.toml" ) - if PY2: - expected = """\ -The Poetry configuration is invalid: - - u'description' is a required property -""" - else: - expected = """\ + expected = """\ The Poetry configuration is invalid: - 'description' is a required property """ @@ -207,3 +290,16 @@ def test_create_poetry_with_local_config(fixture_dir): assert not poetry.config.get("virtualenvs.in-project") assert not poetry.config.get("virtualenvs.create") + assert not poetry.config.get("virtualenvs.options.always-copy") + assert not poetry.config.get("virtualenvs.options.system-site-packages") + + +def test_create_poetry_with_plugins(mocker): + mocker.patch( + "entrypoints.get_group_all", + return_value=[EntryPoint("my-plugin", "tests.test_factory", "MyPlugin")], + ) + + poetry = Factory().create_poetry(fixtures_dir / "sample_project") + + assert "9.9.9" == poetry.package.version.text diff --git a/tests/utils/test_env.py b/tests/utils/test_env.py index 3779623839f..d09012c821f 100644 --- a/tests/utils/test_env.py +++ b/tests/utils/test_env.py @@ -1,20 +1,20 @@ import os import shutil +import subprocess import sys -from typing import Optional +from pathlib import Path +from typing import Any from typing import Union import pytest import tomlkit -from clikit.io import NullIO +from cleo.io.null_io import NullIO -from poetry.core.semver import Version +from poetry.core.semver.version import Version from poetry.core.toml.file import TOMLFile from poetry.factory import Factory -from poetry.utils._compat import PY2 -from poetry.utils._compat import Path from poetry.utils.env import GET_BASE_PREFIX from poetry.utils.env import EnvCommandError from poetry.utils.env import EnvManager @@ -118,7 +118,7 @@ def test_env_get_venv_with_venv_folder_present( assert venv.path == in_project_venv_dir -def build_venv(path, executable=None): # type: (Union[Path,str], Optional[str]) -> () +def build_venv(path: Union[Path, str], **__: Any) -> (): os.mkdir(str(path)) @@ -143,11 +143,11 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file( config.merge({"virtualenvs": {"path": str(tmp_dir)}}) mocker.patch( - "poetry.utils._compat.subprocess.check_output", + "subprocess.check_output", side_effect=check_output_wrapper(), ) mocker.patch( - "poetry.utils._compat.subprocess.Popen.communicate", + "subprocess.Popen.communicate", side_effect=[("/prefix", None), ("/prefix", None)], ) m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv) @@ -156,7 +156,12 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file( venv_name = EnvManager.generate_env_name("simple-project", str(poetry.file.parent)) m.assert_called_with( - Path(tmp_dir) / "{}-py3.7".format(venv_name), executable="python3.7" + Path(tmp_dir) / "{}-py3.7".format(venv_name), + executable="python3.7", + flags={"always-copy": False, "system-site-packages": False}, + with_pip=True, + with_setuptools=True, + with_wheel=True, ) envs_file = TOMLFile(Path(tmp_dir) / "envs.toml") @@ -182,11 +187,11 @@ def test_activate_activates_existing_virtualenv_no_envs_file( config.merge({"virtualenvs": {"path": str(tmp_dir)}}) mocker.patch( - "poetry.utils._compat.subprocess.check_output", + "subprocess.check_output", side_effect=check_output_wrapper(), ) mocker.patch( - "poetry.utils._compat.subprocess.Popen.communicate", + "subprocess.Popen.communicate", side_effect=[("/prefix", None)], ) m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv) @@ -223,11 +228,11 @@ def test_activate_activates_same_virtualenv_with_envs_file( config.merge({"virtualenvs": {"path": str(tmp_dir)}}) mocker.patch( - "poetry.utils._compat.subprocess.check_output", + "subprocess.check_output", side_effect=check_output_wrapper(), ) mocker.patch( - "poetry.utils._compat.subprocess.Popen.communicate", + "subprocess.Popen.communicate", side_effect=[("/prefix", None)], ) m = mocker.patch("poetry.utils.env.EnvManager.create_venv") @@ -262,11 +267,11 @@ def test_activate_activates_different_virtualenv_with_envs_file( config.merge({"virtualenvs": {"path": str(tmp_dir)}}) mocker.patch( - "poetry.utils._compat.subprocess.check_output", + "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.6")), ) mocker.patch( - "poetry.utils._compat.subprocess.Popen.communicate", + "subprocess.Popen.communicate", side_effect=[("/prefix", None), ("/prefix", None), ("/prefix", None)], ) m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv) @@ -274,7 +279,12 @@ def test_activate_activates_different_virtualenv_with_envs_file( env = manager.activate("python3.6", NullIO()) m.assert_called_with( - Path(tmp_dir) / "{}-py3.6".format(venv_name), executable="python3.6" + Path(tmp_dir) / "{}-py3.6".format(venv_name), + executable="python3.6", + flags={"always-copy": False, "system-site-packages": False}, + with_pip=True, + with_setuptools=True, + with_wheel=True, ) assert envs_file.exists() @@ -303,11 +313,11 @@ def test_activate_activates_recreates_for_different_patch( config.merge({"virtualenvs": {"path": str(tmp_dir)}}) mocker.patch( - "poetry.utils._compat.subprocess.check_output", + "subprocess.check_output", side_effect=check_output_wrapper(), ) mocker.patch( - "poetry.utils._compat.subprocess.Popen.communicate", + "subprocess.Popen.communicate", side_effect=[ ("/prefix", None), ('{"version_info": [3, 7, 0]}', None), @@ -326,7 +336,12 @@ def test_activate_activates_recreates_for_different_patch( env = manager.activate("python3.7", NullIO()) build_venv_m.assert_called_with( - Path(tmp_dir) / "{}-py3.7".format(venv_name), executable="python3.7" + Path(tmp_dir) / "{}-py3.7".format(venv_name), + executable="python3.7", + flags={"always-copy": False, "system-site-packages": False}, + with_pip=True, + with_setuptools=True, + with_wheel=True, ) remove_venv_m.assert_called_with(Path(tmp_dir) / "{}-py3.7".format(venv_name)) @@ -358,11 +373,11 @@ def test_activate_does_not_recreate_when_switching_minor( config.merge({"virtualenvs": {"path": str(tmp_dir)}}) mocker.patch( - "poetry.utils._compat.subprocess.check_output", + "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.6")), ) mocker.patch( - "poetry.utils._compat.subprocess.Popen.communicate", + "subprocess.Popen.communicate", side_effect=[("/prefix", None), ("/prefix", None), ("/prefix", None)], ) build_venv_m = mocker.patch( @@ -403,7 +418,7 @@ def test_deactivate_non_activated_but_existing( config.merge({"virtualenvs": {"path": str(tmp_dir)}}) mocker.patch( - "poetry.utils._compat.subprocess.check_output", + "subprocess.check_output", side_effect=check_output_wrapper(), ) @@ -422,7 +437,7 @@ def test_deactivate_activated(tmp_dir, manager, poetry, config, mocker): venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent)) version = Version.parse(".".join(str(c) for c in sys.version_info[:3])) - other_version = Version.parse("3.4") if version.major == 2 else version.next_minor + other_version = Version.parse("3.4") if version.major == 2 else version.next_minor() ( Path(tmp_dir) / "{}-py{}.{}".format(venv_name, version.major, version.minor) ).mkdir() @@ -442,7 +457,7 @@ def test_deactivate_activated(tmp_dir, manager, poetry, config, mocker): config.merge({"virtualenvs": {"path": str(tmp_dir)}}) mocker.patch( - "poetry.utils._compat.subprocess.check_output", + "subprocess.check_output", side_effect=check_output_wrapper(), ) @@ -474,11 +489,11 @@ def test_get_prefers_explicitly_activated_virtualenvs_over_env_var( envs_file.write(doc) mocker.patch( - "poetry.utils._compat.subprocess.check_output", + "subprocess.check_output", side_effect=check_output_wrapper(), ) mocker.patch( - "poetry.utils._compat.subprocess.Popen.communicate", + "subprocess.Popen.communicate", side_effect=[("/prefix", None)], ) @@ -510,7 +525,7 @@ def test_remove_by_python_version(tmp_dir, manager, poetry, config, mocker): (Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir() mocker.patch( - "poetry.utils._compat.subprocess.check_output", + "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.6")), ) @@ -528,7 +543,7 @@ def test_remove_by_name(tmp_dir, manager, poetry, config, mocker): (Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir() mocker.patch( - "poetry.utils._compat.subprocess.check_output", + "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.6")), ) @@ -546,7 +561,7 @@ def test_remove_also_deactivates(tmp_dir, manager, poetry, config, mocker): (Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir() mocker.patch( - "poetry.utils._compat.subprocess.check_output", + "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.6")), ) @@ -583,7 +598,7 @@ def test_remove_keeps_dir_if_not_deleteable(tmp_dir, manager, poetry, config, mo file2_path.touch(exist_ok=False) mocker.patch( - "poetry.utils._compat.subprocess.check_output", + "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.6")), ) @@ -611,9 +626,7 @@ def err_on_rm_venv_only(path, *args, **kwargs): m.side_effect = original_rmtree # Avoid teardown using `err_on_rm_venv_only` -@pytest.mark.skipif( - os.name == "nt" or PY2, reason="Symlinks are not support for Windows" -) +@pytest.mark.skipif(os.name == "nt", reason="Symlinks are not support for Windows") def test_env_has_symlinks_on_nix(tmp_dir, tmp_venv): assert os.path.islink(tmp_venv.python) @@ -633,8 +646,60 @@ def test_run_with_input_non_zero_return(tmp_dir, tmp_venv): assert processError.value.e.returncode == 1 +def test_run_with_keyboard_interrupt(tmp_dir, tmp_venv, mocker): + mocker.patch("subprocess.run", side_effect=KeyboardInterrupt()) + with pytest.raises(KeyboardInterrupt): + tmp_venv.run("python", "-", input_=MINIMAL_SCRIPT) + subprocess.run.assert_called_once() + + +def test_call_with_input_and_keyboard_interrupt(tmp_dir, tmp_venv, mocker): + mocker.patch("subprocess.run", side_effect=KeyboardInterrupt()) + kwargs = {"call": True} + with pytest.raises(KeyboardInterrupt): + tmp_venv.run("python", "-", input_=MINIMAL_SCRIPT, **kwargs) + subprocess.run.assert_called_once() + + +def test_call_no_input_with_keyboard_interrupt(tmp_dir, tmp_venv, mocker): + mocker.patch("subprocess.call", side_effect=KeyboardInterrupt()) + kwargs = {"call": True} + with pytest.raises(KeyboardInterrupt): + tmp_venv.run("python", "-", **kwargs) + subprocess.call.assert_called_once() + + +def test_run_with_called_process_error(tmp_dir, tmp_venv, mocker): + mocker.patch( + "subprocess.run", side_effect=subprocess.CalledProcessError(42, "some_command") + ) + with pytest.raises(EnvCommandError): + tmp_venv.run("python", "-", input_=MINIMAL_SCRIPT) + subprocess.run.assert_called_once() + + +def test_call_with_input_and_called_process_error(tmp_dir, tmp_venv, mocker): + mocker.patch( + "subprocess.run", side_effect=subprocess.CalledProcessError(42, "some_command") + ) + kwargs = {"call": True} + with pytest.raises(EnvCommandError): + tmp_venv.run("python", "-", input_=MINIMAL_SCRIPT, **kwargs) + subprocess.run.assert_called_once() + + +def test_call_no_input_with_called_process_error(tmp_dir, tmp_venv, mocker): + mocker.patch( + "subprocess.call", side_effect=subprocess.CalledProcessError(42, "some_command") + ) + kwargs = {"call": True} + with pytest.raises(EnvCommandError): + tmp_venv.run("python", "-", **kwargs) + subprocess.call.assert_called_once() + + def test_create_venv_tries_to_find_a_compatible_python_executable_using_generic_ones_first( - manager, poetry, config, mocker + manager, poetry, config, mocker, config_virtualenvs_path ): if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] @@ -644,7 +709,7 @@ def test_create_venv_tries_to_find_a_compatible_python_executable_using_generic_ mocker.patch("sys.version_info", (2, 7, 16)) mocker.patch( - "poetry.utils._compat.subprocess.check_output", + "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.7.5")), ) m = mocker.patch( @@ -654,12 +719,17 @@ def test_create_venv_tries_to_find_a_compatible_python_executable_using_generic_ manager.create_venv(NullIO()) m.assert_called_with( - Path("/foo/virtualenvs/{}-py3.7".format(venv_name)), executable="python3" + config_virtualenvs_path / "{}-py3.7".format(venv_name), + executable="python3", + flags={"always-copy": False, "system-site-packages": False}, + with_pip=True, + with_setuptools=True, + with_wheel=True, ) def test_create_venv_tries_to_find_a_compatible_python_executable_using_specific_ones( - manager, poetry, config, mocker + manager, poetry, config, mocker, config_virtualenvs_path ): if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] @@ -668,9 +738,7 @@ def test_create_venv_tries_to_find_a_compatible_python_executable_using_specific venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent)) mocker.patch("sys.version_info", (2, 7, 16)) - mocker.patch( - "poetry.utils._compat.subprocess.check_output", side_effect=["3.5.3", "3.9.0"] - ) + mocker.patch("subprocess.check_output", side_effect=["3.5.3", "3.9.0"]) m = mocker.patch( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) @@ -678,7 +746,12 @@ def test_create_venv_tries_to_find_a_compatible_python_executable_using_specific manager.create_venv(NullIO()) m.assert_called_with( - Path("/foo/virtualenvs/{}-py3.9".format(venv_name)), executable="python3.9" + config_virtualenvs_path / "{}-py3.9".format(venv_name), + executable="python3.9", + flags={"always-copy": False, "system-site-packages": False}, + with_pip=True, + with_setuptools=True, + with_wheel=True, ) @@ -690,9 +763,7 @@ def test_create_venv_fails_if_no_compatible_python_version_could_be_found( poetry.package.python_versions = "^4.8" - mocker.patch( - "poetry.utils._compat.subprocess.check_output", side_effect=["", "", "", ""] - ) + mocker.patch("subprocess.check_output", side_effect=["", "", "", ""]) m = mocker.patch( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) @@ -718,7 +789,7 @@ def test_create_venv_does_not_try_to_find_compatible_versions_with_executable( poetry.package.python_versions = "^4.8" - mocker.patch("poetry.utils._compat.subprocess.check_output", side_effect=["3.8.0"]) + mocker.patch("subprocess.check_output", side_effect=["3.8.0"]) m = mocker.patch( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) @@ -737,7 +808,7 @@ def test_create_venv_does_not_try_to_find_compatible_versions_with_executable( def test_create_venv_uses_patch_version_to_detect_compatibility( - manager, poetry, config, mocker + manager, poetry, config, mocker, config_virtualenvs_path ): if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] @@ -750,7 +821,7 @@ def test_create_venv_uses_patch_version_to_detect_compatibility( mocker.patch("sys.version_info", (version.major, version.minor, version.patch + 1)) check_output = mocker.patch( - "poetry.utils._compat.subprocess.check_output", + "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.9")), ) m = mocker.patch( @@ -761,17 +832,18 @@ def test_create_venv_uses_patch_version_to_detect_compatibility( assert not check_output.called m.assert_called_with( - Path( - "/foo/virtualenvs/{}-py{}.{}".format( - venv_name, version.major, version.minor - ) - ), + config_virtualenvs_path + / "{}-py{}.{}".format(venv_name, version.major, version.minor), executable=None, + flags={"always-copy": False, "system-site-packages": False}, + with_pip=True, + with_setuptools=True, + with_wheel=True, ) def test_create_venv_uses_patch_version_to_detect_compatibility_with_executable( - manager, poetry, config, mocker + manager, poetry, config, mocker, config_virtualenvs_path ): if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] @@ -783,7 +855,7 @@ def test_create_venv_uses_patch_version_to_detect_compatibility_with_executable( venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent)) check_output = mocker.patch( - "poetry.utils._compat.subprocess.check_output", + "subprocess.check_output", side_effect=check_output_wrapper( Version.parse("{}.{}.0".format(version.major, version.minor - 1)) ), @@ -798,12 +870,13 @@ def test_create_venv_uses_patch_version_to_detect_compatibility_with_executable( assert check_output.called m.assert_called_with( - Path( - "/foo/virtualenvs/{}-py{}.{}".format( - venv_name, version.major, version.minor - 1 - ) - ), + config_virtualenvs_path + / "{}-py{}.{}".format(venv_name, version.major, version.minor - 1), executable="python{}.{}".format(version.major, version.minor - 1), + flags={"always-copy": False, "system-site-packages": False}, + with_pip=True, + with_setuptools=True, + with_wheel=True, ) @@ -823,18 +896,25 @@ def test_activate_with_in_project_setting_does_not_fail_if_no_venvs_dir( ) mocker.patch( - "poetry.utils._compat.subprocess.check_output", + "subprocess.check_output", side_effect=check_output_wrapper(), ) mocker.patch( - "poetry.utils._compat.subprocess.Popen.communicate", + "subprocess.Popen.communicate", side_effect=[("/prefix", None), ("/prefix", None)], ) m = mocker.patch("poetry.utils.env.EnvManager.build_venv") manager.activate("python3.7", NullIO()) - m.assert_called_with(poetry.file.parent / ".venv", executable="python3.7") + m.assert_called_with( + poetry.file.parent / ".venv", + executable="python3.7", + flags={"always-copy": False, "system-site-packages": False}, + with_pip=True, + with_setuptools=True, + with_wheel=True, + ) envs_file = TOMLFile(Path(tmp_dir) / "virtualenvs" / "envs.toml") assert not envs_file.exists() @@ -848,11 +928,12 @@ def test_system_env_has_correct_paths(): assert paths.get("purelib") is not None assert paths.get("platlib") is not None assert paths.get("scripts") is not None - assert env.site_packages == Path(paths["purelib"]) + assert env.site_packages.path == Path(paths["purelib"]) @pytest.mark.parametrize( - ("enabled",), [(True,), (False,)], + ("enabled",), + [(True,), (False,)], ) def test_system_env_usersite(mocker, enabled): mocker.patch("site.check_enableusersite", return_value=enabled) @@ -868,4 +949,18 @@ def test_venv_has_correct_paths(tmp_venv): assert paths.get("purelib") is not None assert paths.get("platlib") is not None assert paths.get("scripts") is not None - assert tmp_venv.site_packages == Path(paths["purelib"]) + assert tmp_venv.site_packages.path == Path(paths["purelib"]) + + +def test_env_system_packages(tmp_path, config): + venv_path = tmp_path / "venv" + pyvenv_cfg = venv_path / "pyvenv.cfg" + + EnvManager(config).build_venv(path=venv_path, flags={"system-site-packages": True}) + + if sys.version_info >= (3, 3): + assert "include-system-site-packages = true" in pyvenv_cfg.read_text() + elif (2, 6) < sys.version_info < (3, 0): + assert not venv_path.joinpath( + "lib", "python2.7", "no-global-site-packages.txt" + ).exists() diff --git a/tests/utils/test_env_site.py b/tests/utils/test_env_site.py new file mode 100644 index 00000000000..01abb113771 --- /dev/null +++ b/tests/utils/test_env_site.py @@ -0,0 +1,44 @@ +import uuid + +from pathlib import Path + +from poetry.utils._compat import decode +from poetry.utils.env import SitePackages + + +def test_env_site_simple(tmp_dir, mocker): + # emulate permission error when creating directory + mocker.patch("pathlib.Path.mkdir", side_effect=OSError()) + site_packages = SitePackages(Path("/non-existent"), fallbacks=[Path(tmp_dir)]) + candidates = site_packages.make_candidates(Path("hello.txt"), writable_only=True) + hello = Path(tmp_dir) / "hello.txt" + + assert len(candidates) == 1 + assert candidates[0].as_posix() == hello.as_posix() + + content = decode(str(uuid.uuid4())) + site_packages.write_text(Path("hello.txt"), content, encoding="utf-8") + + assert hello.read_text(encoding="utf-8") == content + + assert not (site_packages.path / "hello.txt").exists() + + +def test_env_site_select_first(tmp_dir): + path = Path(tmp_dir) + fallback = path / "fallback" + fallback.mkdir(parents=True) + + site_packages = SitePackages(path, fallbacks=[fallback]) + candidates = site_packages.make_candidates(Path("hello.txt"), writable_only=True) + + assert len(candidates) == 2 + assert len(site_packages.find(Path("hello.txt"))) == 0 + + content = decode(str(uuid.uuid4())) + site_packages.write_text(Path("hello.txt"), content, encoding="utf-8") + + assert (site_packages.path / "hello.txt").exists() + assert not (fallback / "hello.txt").exists() + + assert len(site_packages.find(Path("hello.txt"))) == 1 diff --git a/tests/utils/test_exporter.py b/tests/utils/test_exporter.py index c2eee0d1f11..eb3e000feed 100644 --- a/tests/utils/test_exporter.py +++ b/tests/utils/test_exporter.py @@ -1,12 +1,14 @@ import sys +from pathlib import Path + import pytest +from poetry.core.packages.dependency import Dependency from poetry.core.toml.file import TOMLFile from poetry.factory import Factory from poetry.packages import Locker as BaseLocker from poetry.repositories.legacy_repository import LegacyRepository -from poetry.utils._compat import Path from poetry.utils.exporter import Exporter @@ -41,9 +43,7 @@ def working_directory(): @pytest.fixture(autouse=True) def mock_path_cwd(mocker, working_directory): - yield mocker.patch( - "poetry.core.utils._compat.Path.cwd", return_value=working_directory - ) + yield mocker.patch("pathlib.Path.cwd", return_value=working_directory) @pytest.fixture() @@ -59,13 +59,18 @@ def poetry(fixture_dir, locker): return p -def set_package_requires(poetry): +def set_package_requires(poetry, skip=None): + skip = skip or set() packages = poetry.locker.locked_repository(with_dev_reqs=True).packages poetry.package.requires = [ - pkg.to_dependency() for pkg in packages if pkg.category == "main" + pkg.to_dependency() + for pkg in packages + if pkg.category == "main" and pkg.name not in skip ] poetry.package.dev_requires = [ - pkg.to_dependency() for pkg in packages if pkg.category == "dev" + pkg.to_dependency() + for pkg in packages + if pkg.category == "dev" and pkg.name not in skip ] @@ -170,6 +175,335 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_markers assert expected == content +def test_exporter_can_export_requirements_txt_poetry(tmp_dir, poetry): + """Regression test for #3254""" + + poetry.locker.mock_lock_data( + { + "package": [ + { + "name": "poetry", + "version": "1.1.4", + "category": "main", + "optional": False, + "python-versions": "*", + "dependencies": {"keyring": "*"}, + }, + { + "name": "junit-xml", + "version": "1.9", + "category": "main", + "optional": False, + "python-versions": "*", + "dependencies": {"six": "*"}, + }, + { + "name": "keyring", + "version": "21.8.0", + "category": "main", + "optional": False, + "python-versions": "*", + "dependencies": { + "SecretStorage": { + "version": "*", + "markers": "sys_platform == 'linux'", + } + }, + }, + { + "name": "secretstorage", + "version": "3.3.0", + "category": "main", + "optional": False, + "python-versions": "*", + "dependencies": {"cryptography": "*"}, + }, + { + "name": "cryptography", + "version": "3.2", + "category": "main", + "optional": False, + "python-versions": "*", + "dependencies": {"six": "*"}, + }, + { + "name": "six", + "version": "1.15.0", + "category": "main", + "optional": False, + "python-versions": "*", + }, + ], + "metadata": { + "python-versions": "*", + "content-hash": "123456789", + "hashes": { + "poetry": [], + "keyring": [], + "secretstorage": [], + "cryptography": [], + "six": [], + "junit-xml": [], + }, + }, + } + ) + set_package_requires( + poetry, skip={"keyring", "secretstorage", "cryptography", "six"} + ) + + exporter = Exporter(poetry) + + exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt") + + with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: + content = f.read() + + # The dependency graph: + # junit-xml 1.9 Creates JUnit XML test result documents that can be read by tools such as Jenkins + # └── six * + # poetry 1.1.4 Python dependency management and packaging made easy. + # ├── keyring >=21.2.0,<22.0.0 + # │ ├── importlib-metadata >=1 + # │ │ └── zipp >=0.5 + # │ ├── jeepney >=0.4.2 + # │ ├── pywin32-ctypes <0.1.0 || >0.1.0,<0.1.1 || >0.1.1 + # │ └── secretstorage >=3.2 -- On linux only + # │ ├── cryptography >=2.0 + # │ │ └── six >=1.4.1 + # │ └── jeepney >=0.6 (circular dependency aborted here) + expected = { + "poetry": Dependency.create_from_pep_508("poetry==1.1.4"), + "junit-xml": Dependency.create_from_pep_508("junit-xml==1.9"), + "keyring": Dependency.create_from_pep_508("keyring==21.8.0"), + "secretstorage": Dependency.create_from_pep_508( + "secretstorage==3.3.0; sys_platform=='linux'" + ), + "cryptography": Dependency.create_from_pep_508( + "cryptography==3.2; sys_platform=='linux'" + ), + "six": Dependency.create_from_pep_508("six==1.15.0"), + } + + for line in content.strip().split("\n"): + dependency = Dependency.create_from_pep_508(line) + assert dependency.name in expected + expected_dependency = expected.pop(dependency.name) + assert dependency == expected_dependency + assert dependency.marker == expected_dependency.marker + + +def test_exporter_can_export_requirements_txt_pyinstaller(tmp_dir, poetry): + """Regression test for #3254""" + + poetry.locker.mock_lock_data( + { + "package": [ + { + "name": "pyinstaller", + "version": "4.0", + "category": "main", + "optional": False, + "python-versions": "*", + "dependencies": { + "altgraph": "*", + "macholib": { + "version": "*", + "markers": "sys_platform == 'darwin'", + }, + }, + }, + { + "name": "altgraph", + "version": "0.17", + "category": "main", + "optional": False, + "python-versions": "*", + }, + { + "name": "macholib", + "version": "1.8", + "category": "main", + "optional": False, + "python-versions": "*", + "dependencies": {"altgraph": ">=0.15"}, + }, + ], + "metadata": { + "python-versions": "*", + "content-hash": "123456789", + "hashes": {"pyinstaller": [], "altgraph": [], "macholib": []}, + }, + } + ) + set_package_requires(poetry, skip={"altgraph", "macholib"}) + + exporter = Exporter(poetry) + + exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt") + + with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: + content = f.read() + + # Rationale for the results: + # * PyInstaller has an explicit dependency on altgraph, so it must always be installed. + # * PyInstaller requires macholib on Darwin, which in turn requires altgraph. + # The dependency graph: + # pyinstaller 4.0 PyInstaller bundles a Python application and all its dependencies into a single package. + # ├── altgraph * + # ├── macholib >=1.8 -- only on Darwin + # │ └── altgraph >=0.15 + expected = { + "pyinstaller": Dependency.create_from_pep_508("pyinstaller==4.0"), + "altgraph": Dependency.create_from_pep_508("altgraph==0.17"), + "macholib": Dependency.create_from_pep_508( + "macholib==1.8; sys_platform == 'darwin'" + ), + } + + for line in content.strip().split("\n"): + dependency = Dependency.create_from_pep_508(line) + assert dependency.name in expected + expected_dependency = expected.pop(dependency.name) + assert dependency == expected_dependency + assert dependency.marker == expected_dependency.marker + + +def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers( + tmp_dir, poetry +): + poetry.locker.mock_lock_data( + { + "package": [ + { + "name": "a", + "version": "1.2.3", + "category": "main", + "optional": False, + "python-versions": "*", + "marker": "python_version < '3.7'", + "dependencies": {"b": ">=0.0.0", "c": ">=0.0.0"}, + }, + { + "name": "b", + "version": "4.5.6", + "category": "main", + "optional": False, + "python-versions": "*", + "marker": "platform_system == 'Windows'", + "dependencies": {"d": ">=0.0.0"}, + }, + { + "name": "c", + "version": "7.8.9", + "category": "main", + "optional": False, + "python-versions": "*", + "marker": "sys_platform == 'win32'", + "dependencies": {"d": ">=0.0.0"}, + }, + { + "name": "d", + "version": "0.0.1", + "category": "main", + "optional": False, + "python-versions": "*", + }, + ], + "metadata": { + "python-versions": "*", + "content-hash": "123456789", + "hashes": {"a": [], "b": [], "c": [], "d": []}, + }, + } + ) + set_package_requires(poetry, skip={"b", "c", "d"}) + + exporter = Exporter(poetry) + + exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt") + + with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: + content = f.read() + + expected = { + "a": Dependency.create_from_pep_508("a==1.2.3; python_version < '3.7'"), + "b": Dependency.create_from_pep_508( + "b==4.5.6; platform_system == 'Windows' and python_version < '3.7'" + ), + "c": Dependency.create_from_pep_508( + "c==7.8.9; sys_platform == 'win32' and python_version < '3.7'" + ), + "d": Dependency.create_from_pep_508( + "d==0.0.1; platform_system == 'Windows' and python_version < '3.7' or sys_platform == 'win32' and python_version < '3.7'" + ), + } + + for line in content.strip().split("\n"): + dependency = Dependency.create_from_pep_508(line) + assert dependency.name in expected + expected_dependency = expected.pop(dependency.name) + assert dependency == expected_dependency + assert dependency.marker == expected_dependency.marker + + assert expected == {} + + +@pytest.mark.parametrize( + "dev,lines", + [(False, ['a==1.2.3; python_version < "3.8"']), (True, ["a==1.2.3", "b==4.5.6"])], +) +def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers_any( + tmp_dir, poetry, dev, lines +): + poetry.locker.mock_lock_data( + { + "package": [ + { + "name": "a", + "version": "1.2.3", + "category": "main", + "optional": False, + "python-versions": "*", + }, + { + "name": "b", + "version": "4.5.6", + "category": "dev", + "optional": False, + "python-versions": "*", + "dependencies": {"a": ">=1.2.3"}, + }, + ], + "metadata": { + "python-versions": "*", + "content-hash": "123456789", + "hashes": {"a": [], "b": []}, + }, + } + ) + + poetry.package.requires = [ + Factory.create_dependency( + name="a", constraint=dict(version="^1.2.3", python="<3.8") + ), + ] + poetry.package.dev_requires = [ + Factory.create_dependency( + name="b", constraint=dict(version="^4.5.6"), category="dev" + ), + ] + + exporter = Exporter(poetry) + + exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt", dev=dev) + + with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: + content = f.read() + + assert content.strip() == "\n".join(lines) + + def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes( tmp_dir, poetry ): @@ -399,8 +733,17 @@ def test_exporter_exports_requirements_txt_without_optional_packages(tmp_dir, po assert expected == content -def test_exporter_exports_requirements_txt_with_optional_packages_if_opted_in( - tmp_dir, poetry +@pytest.mark.parametrize( + "extras,lines", + [ + (None, ["foo==1.2.3"]), + (False, ["foo==1.2.3"]), + (True, ["bar==4.5.6", "foo==1.2.3", "spam==0.1.0"]), + (["feature_bar"], ["bar==4.5.6", "foo==1.2.3", "spam==0.1.0"]), + ], +) +def test_exporter_exports_requirements_txt_with_optional_packages( + tmp_dir, poetry, extras, lines ): poetry.locker.mock_lock_data( { @@ -445,22 +788,16 @@ def test_exporter_exports_requirements_txt_with_optional_packages_if_opted_in( Path(tmp_dir), "requirements.txt", dev=True, - extras=["feature_bar"], + with_hashes=False, + extras=extras, ) with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: content = f.read() - expected = """\ -bar==4.5.6 \\ - --hash=sha256:67890 -foo==1.2.3 \\ - --hash=sha256:12345 -spam==0.1.0 \\ - --hash=sha256:abcde -""" + expected = "\n".join(lines) - assert expected == content + assert content.strip() == expected def test_exporter_can_export_requirements_txt_with_git_packages(tmp_dir, poetry): @@ -503,6 +840,111 @@ def test_exporter_can_export_requirements_txt_with_git_packages(tmp_dir, poetry) assert expected == content +def test_exporter_can_export_requirements_txt_with_nested_packages(tmp_dir, poetry): + poetry.locker.mock_lock_data( + { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "category": "main", + "optional": False, + "python-versions": "*", + "source": { + "type": "git", + "url": "https://github.com/foo/foo.git", + "reference": "123456", + }, + }, + { + "name": "bar", + "version": "4.5.6", + "category": "main", + "optional": False, + "python-versions": "*", + "dependencies": {"foo": "rev 123456"}, + }, + ], + "metadata": { + "python-versions": "*", + "content-hash": "123456789", + "hashes": {"foo": [], "bar": []}, + }, + } + ) + set_package_requires(poetry, skip={"foo"}) + + exporter = Exporter(poetry) + + exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt") + + with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: + content = f.read() + + expected = """\ +bar==4.5.6 +foo @ git+https://github.com/foo/foo.git@123456 +""" + + assert expected == content + + +def test_exporter_can_export_requirements_txt_with_nested_packages_cyclic( + tmp_dir, poetry +): + poetry.locker.mock_lock_data( + { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "category": "main", + "optional": False, + "python-versions": "*", + "dependencies": {"bar": {"version": "4.5.6"}}, + }, + { + "name": "bar", + "version": "4.5.6", + "category": "main", + "optional": False, + "python-versions": "*", + "dependencies": {"baz": {"version": "7.8.9"}}, + }, + { + "name": "baz", + "version": "7.8.9", + "category": "main", + "optional": False, + "python-versions": "*", + "dependencies": {"foo": {"version": "1.2.3"}}, + }, + ], + "metadata": { + "python-versions": "*", + "content-hash": "123456789", + "hashes": {"foo": [], "bar": [], "baz": []}, + }, + } + ) + set_package_requires(poetry, skip={"bar", "baz"}) + + exporter = Exporter(poetry) + + exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt") + + with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: + content = f.read() + + expected = """\ +bar==4.5.6 +baz==7.8.9 +foo==1.2.3 +""" + + assert expected == content + + def test_exporter_can_export_requirements_txt_with_git_packages_and_markers( tmp_dir, poetry ): @@ -584,7 +1026,79 @@ def test_exporter_can_export_requirements_txt_with_directory_packages( expected = """\ foo @ {}/tests/fixtures/sample_project """.format( - working_directory.as_posix() + working_directory.as_uri() + ) + + assert expected == content + + +def test_exporter_can_export_requirements_txt_with_nested_directory_packages( + tmp_dir, poetry, working_directory +): + poetry.locker.mock_lock_data( + { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "category": "main", + "optional": False, + "python-versions": "*", + "source": { + "type": "directory", + "url": "tests/fixtures/sample_project", + "reference": "", + }, + }, + { + "name": "bar", + "version": "4.5.6", + "category": "main", + "optional": False, + "python-versions": "*", + "source": { + "type": "directory", + "url": "tests/fixtures/sample_project/../project_with_nested_local/bar", + "reference": "", + }, + }, + { + "name": "baz", + "version": "7.8.9", + "category": "main", + "optional": False, + "python-versions": "*", + "source": { + "type": "directory", + "url": "tests/fixtures/sample_project/../project_with_nested_local/bar/..", + "reference": "", + }, + }, + ], + "metadata": { + "python-versions": "*", + "content-hash": "123456789", + "hashes": {"foo": [], "bar": [], "baz": []}, + }, + } + ) + set_package_requires(poetry) + + exporter = Exporter(poetry) + + exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt") + + with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: + content = f.read() + + expected = """\ +bar @ {}/tests/fixtures/project_with_nested_local/bar +baz @ {}/tests/fixtures/project_with_nested_local +foo @ {}/tests/fixtures/sample_project +""".format( + working_directory.as_uri(), + working_directory.as_uri(), + working_directory.as_uri(), ) assert expected == content @@ -629,7 +1143,7 @@ def test_exporter_can_export_requirements_txt_with_directory_packages_and_marker expected = """\ foo @ {}/tests/fixtures/sample_project; python_version < "3.7" """.format( - working_directory.as_posix() + working_directory.as_uri() ) assert expected == content @@ -726,7 +1240,10 @@ def test_exporter_can_export_requirements_txt_with_file_packages_and_markers( def test_exporter_exports_requirements_txt_with_legacy_packages(tmp_dir, poetry): poetry.pool.add_repository( - LegacyRepository("custom", "https://example.com/simple",) + LegacyRepository( + "custom", + "https://example.com/simple", + ) ) poetry.locker.mock_lock_data( { @@ -779,13 +1296,134 @@ def test_exporter_exports_requirements_txt_with_legacy_packages(tmp_dir, poetry) assert expected == content +def test_exporter_exports_requirements_txt_with_legacy_packages_trusted_host( + tmp_dir, poetry +): + poetry.pool.add_repository( + LegacyRepository( + "custom", + "http://example.com/simple", + ) + ) + poetry.locker.mock_lock_data( + { + "package": [ + { + "name": "bar", + "version": "4.5.6", + "category": "dev", + "optional": False, + "python-versions": "*", + "source": { + "type": "legacy", + "url": "http://example.com/simple", + "reference": "", + }, + }, + ], + "metadata": { + "python-versions": "*", + "content-hash": "123456789", + "hashes": {"bar": ["67890"]}, + }, + } + ) + set_package_requires(poetry) + exporter = Exporter(poetry) + + exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt", dev=True) + + with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: + content = f.read() + + expected = """\ +--trusted-host example.com +--extra-index-url http://example.com/simple + +bar==4.5.6 \\ + --hash=sha256:67890 +""" + + assert expected == content + + +@pytest.mark.parametrize( + ("dev", "expected"), + [ + (True, ["bar==1.2.2", "baz==1.2.3", "foo==1.2.1"]), + (False, ["bar==1.2.2", "foo==1.2.1"]), + ], +) +def test_exporter_exports_requirements_txt_with_dev_extras( + tmp_dir, poetry, dev, expected +): + poetry.locker.mock_lock_data( + { + "package": [ + { + "name": "foo", + "version": "1.2.1", + "category": "main", + "optional": False, + "python-versions": "*", + }, + { + "name": "bar", + "version": "1.2.2", + "category": "main", + "optional": False, + "python-versions": "*", + "dependencies": { + "baz": { + "version": ">=0.1.0", + "optional": True, + "markers": "extra == 'baz'", + } + }, + "extras": {"baz": ["baz (>=0.1.0)"]}, + }, + { + "name": "baz", + "version": "1.2.3", + "category": "dev", + "optional": False, + "python-versions": "*", + }, + ], + "metadata": { + "python-versions": "*", + "content-hash": "123456789", + "hashes": {"foo": [], "bar": [], "baz": []}, + }, + } + ) + set_package_requires(poetry) + + exporter = Exporter(poetry) + + exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt", dev=dev) + + with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: + content = f.read() + + assert content == "{}\n".format("\n".join(expected)) + + def test_exporter_exports_requirements_txt_with_legacy_packages_and_duplicate_sources( tmp_dir, poetry ): poetry.pool.add_repository( - LegacyRepository("custom", "https://example.com/simple",) + LegacyRepository( + "custom", + "https://example.com/simple", + ) + ) + poetry.pool.add_repository( + LegacyRepository( + "custom", + "https://foobaz.com/simple", + ) ) - poetry.pool.add_repository(LegacyRepository("custom", "https://foobaz.com/simple",)) poetry.locker.mock_lock_data( { "package": [ diff --git a/tests/utils/test_extras.py b/tests/utils/test_extras.py index e06e48096df..ab138c888e7 100644 --- a/tests/utils/test_extras.py +++ b/tests/utils/test_extras.py @@ -1,6 +1,6 @@ import pytest -from poetry.core.packages import Package +from poetry.core.packages.package import Package from poetry.factory import Factory from poetry.utils.extras import get_extra_package_names diff --git a/tests/utils/test_helpers.py b/tests/utils/test_helpers.py index 83bf2030780..fe105df6b74 100644 --- a/tests/utils/test_helpers.py +++ b/tests/utils/test_helpers.py @@ -1,5 +1,6 @@ +from pathlib import Path + from poetry.core.utils.helpers import parse_requires -from poetry.utils._compat import Path from poetry.utils.helpers import get_cert from poetry.utils.helpers import get_client_cert diff --git a/tests/utils/test_patterns.py b/tests/utils/test_patterns.py new file mode 100644 index 00000000000..9f43db27c56 --- /dev/null +++ b/tests/utils/test_patterns.py @@ -0,0 +1,39 @@ +import pytest + +from poetry.utils import patterns + + +@pytest.mark.parametrize( + ["filename", "expected"], + [ + ( + "markdown_captions-2-py3-none-any.whl", + { + "namever": "markdown_captions-2", + "name": "markdown_captions", + "ver": "2", + "build": None, + "pyver": "py3", + "abi": "none", + "plat": "any", + }, + ), + ( + "SQLAlchemy-1.3.20-cp27-cp27mu-manylinux2010_x86_64.whl", + { + "namever": "SQLAlchemy-1.3.20", + "name": "SQLAlchemy", + "ver": "1.3.20", + "build": None, + "pyver": "cp27", + "abi": "cp27mu", + "plat": "manylinux2010_x86_64", + }, + ), + ], +) +def test_wheel_file_re(filename, expected): + match = patterns.wheel_file_re.match(filename) + groups = match.groupdict() + + assert groups == expected diff --git a/tests/utils/test_pip.py b/tests/utils/test_pip.py new file mode 100644 index 00000000000..d13d68992e8 --- /dev/null +++ b/tests/utils/test_pip.py @@ -0,0 +1,20 @@ +import subprocess + +import pytest + +from poetry.utils.pip import pip_install + + +def test_pip_install_successful(tmp_dir, tmp_venv, fixture_dir): + file_path = fixture_dir("distributions/demo-0.1.0-py2.py3-none-any.whl") + result = pip_install(file_path, tmp_venv) + + assert "Successfully installed demo-0.1.0" in result + + +def test_pip_install_with_keyboard_interrupt(tmp_dir, tmp_venv, fixture_dir, mocker): + file_path = fixture_dir("distributions/demo-0.1.0-py2.py3-none-any.whl") + mocker.patch("subprocess.run", side_effect=KeyboardInterrupt()) + with pytest.raises(KeyboardInterrupt): + pip_install(file_path, tmp_venv) + subprocess.run.assert_called_once() diff --git a/tests/utils/test_setup_reader.py b/tests/utils/test_setup_reader.py index 68fc005a496..0112e865793 100644 --- a/tests/utils/test_setup_reader.py +++ b/tests/utils/test_setup_reader.py @@ -2,8 +2,7 @@ import pytest -from poetry.core.semver.exceptions import ParseVersionError -from poetry.utils._compat import PY35 +from poetry.core.version.exceptions import InvalidVersion from poetry.utils.setup_reader import SetupReader @@ -15,7 +14,6 @@ def _setup(name): return _setup -@pytest.mark.skipif(not PY35, reason="AST parsing does not work for Python <3.4") def test_setup_reader_read_first_level_setup_call_with_direct_types(setup): result = SetupReader.read_from_directory(setup("flask")) @@ -48,7 +46,6 @@ def test_setup_reader_read_first_level_setup_call_with_direct_types(setup): assert expected_python_requires == result["python_requires"] -@pytest.mark.skipif(not PY35, reason="AST parsing does not work for Python <3.4") def test_setup_reader_read_first_level_setup_call_with_variables(setup): result = SetupReader.read_from_directory(setup("requests")) @@ -74,7 +71,6 @@ def test_setup_reader_read_first_level_setup_call_with_variables(setup): assert expected_python_requires == result["python_requires"] -@pytest.mark.skipif(not PY35, reason="AST parsing does not work for Python <3.4") def test_setup_reader_read_sub_level_setup_call_with_direct_types(setup): result = SetupReader.read_from_directory(setup("sqlalchemy")) @@ -119,11 +115,10 @@ def test_setup_reader_read_setup_cfg(setup): def test_setup_reader_read_setup_cfg_with_attr(setup): - with pytest.raises(ParseVersionError): + with pytest.raises(InvalidVersion): SetupReader.read_from_directory(setup("with-setup-cfg-attr")) -@pytest.mark.skipif(not PY35, reason="AST parsing does not work for Python <3.4") def test_setup_reader_read_setup_kwargs(setup): result = SetupReader.read_from_directory(setup("pendulum")) @@ -140,7 +135,6 @@ def test_setup_reader_read_setup_kwargs(setup): assert expected_python_requires == result["python_requires"] -@pytest.mark.skipif(not PY35, reason="AST parsing does not work for Python <3.4") def test_setup_reader_read_setup_call_in_main(setup): result = SetupReader.read_from_directory(setup("pyyaml")) @@ -157,7 +151,6 @@ def test_setup_reader_read_setup_call_in_main(setup): assert expected_python_requires == result["python_requires"] -@pytest.mark.skipif(not PY35, reason="AST parsing does not work for Python <3.4") def test_setup_reader_read_extras_require_with_variables(setup): result = SetupReader.read_from_directory(setup("extras_require_with_vars")) @@ -174,7 +167,6 @@ def test_setup_reader_read_extras_require_with_variables(setup): assert expected_python_requires == result["python_requires"] -@pytest.mark.skipif(not PY35, reason="AST parsing does not work for Python <3.4") def test_setup_reader_setuptools(setup): result = SetupReader.read_from_directory(setup("setuptools_setup")) diff --git a/tox.ini b/tox.ini index 190670191da..6e40e8b7f8a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,22 +1,10 @@ [tox] minversion = 3.3.0 isolated_build = True -envlist = py27, py35, py36, py37, py38, doc +envlist = py36, py37, py38, py39, doc [testenv] whitelist_externals = poetry commands = poetry install -vv --no-root poetry run pytest {posargs} tests/ - -[testenv:doc] -whitelist_externals = -skip_install = true -deps = - markdown-include - mkdocs - pygments - pygments-github-lexers - pymdown-extensions -commands = - mkdocs build -f docs/mkdocs.yml