diff --git a/.cirrus.yml b/.cirrus.yml index 450fe74a08a..2ce67499911 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -16,6 +16,7 @@ test_task: - PYTHON: python3.8 - PYTHON: python3.9 - PYTHON: python3.10 + - PYTHON: python3.11 install_prereqs_script: - V=$(printf '%s' $PYTHON | tr -d '.[:alpha:]') - pkg install -y python${V} py${V}-sqlite3 diff --git a/.flake8 b/.flake8 index 2d16cf2abc3..2780c9c145e 100644 --- a/.flake8 +++ b/.flake8 @@ -6,7 +6,7 @@ ban-relative-imports = true format-greedy = 1 inline-quotes = double enable-extensions = TC, TC1 -type-checking-exempt-modules = typing, typing-extensions +type-checking-strict = true eradicate-whitelist-extend = ^-.*; extend-ignore = # E203: Whitespace before ':' (pycqa/pycodestyle#373) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c4e363a2d8f..df018e67b32 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,7 +28,7 @@ jobs: strategy: matrix: os: [Ubuntu, macOS, Windows] - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11-dev"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] include: - os: Ubuntu image: ubuntu-22.04 @@ -50,11 +50,11 @@ jobs: - name: Get full Python version id: full-python-version - run: echo ::set-output name=version::$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))") + run: echo version=$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))") >> $GITHUB_OUTPUT - name: Bootstrap poetry run: | - curl -sL https://install.python-poetry.org | python - -y + curl -sSL https://install.python-poetry.org | python - -y - name: Update PATH if: ${{ matrix.os != 'Windows' }} @@ -64,6 +64,12 @@ jobs: if: ${{ matrix.os == 'Windows' }} run: echo "$APPDATA\Python\Scripts" >> $GITHUB_PATH + - name: Enable long paths for git on Windows + if: ${{ matrix.os == 'Windows' }} + # Enable handling long path names (+260 char) on the Windows platform + # https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation + run: git config --system core.longpaths true + - name: Configure poetry run: poetry config virtualenvs.in-project true @@ -83,10 +89,8 @@ jobs: # Using `timeout` is a safeguard against the Poetry command hanging for some reason. timeout 10s poetry run pip --version || rm -rf .venv - # XXX: https://github.com/pypa/pip/issues/11352 causes random failures -- remove once fixed in a release. - - name: Upgrade pip on Python 3.11 - if: ${{ matrix.python-version == '3.11-dev' }} - run: poetry run pip install git+https://github.com/pypa/pip.git@f8a25921e5c443b07483017b0ffdeb08b9ba2fdf + - name: Check lock file + run: poetry lock --check - name: Install dependencies run: poetry install --with github-actions @@ -103,8 +107,7 @@ jobs: - name: Get Plugin Version (poetry-plugin-export) id: poetry-plugin-export-version run: | - echo ::set-output name=version::$(\ - poetry show poetry-plugin-export | grep version | cut -d : -f 2 | xargs) + echo version=$(poetry show poetry-plugin-export | grep version | cut -d : -f 2 | xargs) >> $GITHUB_OUTPUT - name: Checkout Plugin Source (poetry-plugin-export) uses: actions/checkout@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cbbd930cab5..0556a26f086 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,8 @@ jobs: python-version: "3.10" - name: Install Poetry - run: python install-poetry.py -y + run: | + curl -sSL https://install.python-poetry.org | python - -y - name: Update PATH run: echo "$HOME/.local/bin" >> $GITHUB_PATH @@ -30,8 +31,7 @@ jobs: - name: Check Version id: check-version run: | - [[ "$(poetry version --short)" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \ - || echo ::set-output name=prerelease::true + [[ "$(poetry version --short)" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || echo prerelease=true >> $GITHUB_OUTPUT - name: Create Release uses: ncipollo/release-action@v1 diff --git a/.github/workflows/skip.yml b/.github/workflows/skip.yml index 40225d5adfd..be69a320bbb 100644 --- a/.github/workflows/skip.yml +++ b/.github/workflows/skip.yml @@ -28,6 +28,6 @@ jobs: strategy: matrix: os: [Ubuntu, macOS, Windows] - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11-dev"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - run: exit 0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 27563811d61..c0d132dd9a5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ ci: repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -32,35 +32,34 @@ repos: hooks: - id: yesqa additional_dependencies: &flake8_deps - - flake8-annotations==2.9.0 - - flake8-broken-line==0.5.0 - - flake8-bugbear==22.7.1 - - flake8-comprehensions==3.10.0 - - flake8-eradicate==1.3.0 - - flake8-quotes==3.3.1 + - flake8-annotations==3.0.0 + - flake8-bugbear==23.1.20 + - flake8-comprehensions==3.10.1 + - flake8-eradicate==1.4.0 + - flake8-pie==0.16.0 + - flake8-quotes==3.3.2 - flake8-simplify==0.19.3 - flake8-tidy-imports==4.8.0 - - flake8-type-checking==2.1.2 - - flake8-typing-imports==1.12.0 + - flake8-type-checking==2.3.0 + - flake8-typing-imports==1.14.0 - flake8-use-fstring==1.4 - - pep8-naming==0.13.1 - - flake8-pie==0.16.0 + - pep8-naming==0.13.3 - repo: https://github.com/asottile/pyupgrade - rev: v3.1.0 + rev: v3.3.1 hooks: - id: pyupgrade args: [--py37-plus] exclude: ^(install|get)-poetry.py$ - repo: https://github.com/hadialqattan/pycln - rev: v2.1.1 + rev: v2.1.2 hooks: - id: pycln args: [--all] - repo: https://github.com/pycqa/isort - rev: 5.10.1 + rev: 5.12.0 hooks: - id: isort name: "isort (python)" @@ -77,17 +76,18 @@ repos: args: [--lines-after-imports, "-1"] - repo: https://github.com/psf/black - rev: 22.10.0 + rev: 22.12.0 hooks: - id: black - repo: https://github.com/pycqa/flake8 - rev: 5.0.4 + # freeze to commit rev to prevent automatic updates, since newer versions of flake8 are not compatible with plugins + rev: 6027577d325b0dd8bf1e465ebd29b71b5f0d005b hooks: - id: flake8 additional_dependencies: *flake8_deps - repo: https://github.com/pre-commit/pre-commit - rev: v2.20.0 + rev: v2.21.0 hooks: - id: validate_manifest diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index d2662b8a420..08f733c18e8 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -3,16 +3,14 @@ description: run poetry check to validate config entry: poetry check language: python - language_version: python3 pass_filenames: false - files: ^pyproject.toml$ + files: ^(.*/)?pyproject.toml$ - id: poetry-lock name: poetry-lock description: run poetry lock to update lock file entry: poetry lock language: python - language_version: python3 pass_filenames: false - id: poetry-export @@ -20,7 +18,6 @@ description: run poetry export to sync lock file with requirements.txt entry: poetry export language: python - language_version: python3 pass_filenames: false files: ^poetry.lock$ args: ["-f", "requirements.txt", "-o", "requirements.txt"] diff --git a/CHANGELOG.md b/CHANGELOG.md index 3635a702289..8cc748df8fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,192 @@ # Change Log + +## [1.4.2] - 2023-04-02 + +### Changed + +- When trying to install wheels with invalid `RECORD` files, Poetry does not fail anymore but only prints a warning. + This mitigates an unintended change introduced in Poetry 1.4.1 ([#7694](https://github.com/python-poetry/poetry/pull/7694)). + +### Fixed + +- Fix an issue where relative git submodule urls were not parsed correctly ([#7017](https://github.com/python-poetry/poetry/pull/7017)). +- Fix an issue where Poetry could freeze when building a project with a build script if it generated enough output to fill the OS pipe buffer ([#7699](https://github.com/python-poetry/poetry/pull/7699)). + + +## [1.4.1] - 2023-03-19 + +### Fixed + +- Fix an issue where `poetry install` did not respect the requirements for building editable dependencies ([#7579](https://github.com/python-poetry/poetry/pull/7579)). +- Fix an issue where `poetry init` crashed due to bad input when adding packages interactively ([#7569](https://github.com/python-poetry/poetry/pull/7569)). +- Fix an issue where `poetry install` ignored the `subdirectory` argument of git dependencies ([#7580](https://github.com/python-poetry/poetry/pull/7580)). +- Fix an issue where installing packages with `no-binary` could result in a false hash mismatch ([#7594](https://github.com/python-poetry/poetry/pull/7594)). +- Fix an issue where the hash of sdists was neither validated nor written to the `direct_url.json` during installation ([#7594](https://github.com/python-poetry/poetry/pull/7594)). +- Fix an issue where `poetry install --sync` attempted to remove itself ([#7626](https://github.com/python-poetry/poetry/pull/7626)). +- Fix an issue where wheels with non-normalized `dist-info` directory names could not be installed ([#7671](https://github.com/python-poetry/poetry/pull/7671)). +- Fix an issue where `poetry install --compile` compiled with optimization level 1 ([#7666](https://github.com/python-poetry/poetry/pull/7666)). + +### Docs + +- Clarify the behavior of the `--extras` option ([#7563](https://github.com/python-poetry/poetry/pull/7563)). +- Expand the FAQ on reasons for slow dependency resolution ([#7620](https://github.com/python-poetry/poetry/pull/7620)). + + +### poetry-core ([`1.5.2`](https://github.com/python-poetry/poetry-core/releases/tag/1.5.2)) + +- Fix an issue where wheels built on Windows could contain duplicate entries in the RECORD file ([#555](https://github.com/python-poetry/poetry-core/pull/555)). + + +## [1.4.0] - 2023-02-27 + +### Added + +- **Add a modern installer (`installer.modern-installation`) for faster installation of packages and independence from pip** ([#6205](https://github.com/python-poetry/poetry/pull/6205)). +- Add support for `Private ::` trove classifiers ([#7271](https://github.com/python-poetry/poetry/pull/7271)). +- Add the version of poetry in the `@generated` comment at the beginning of the lock file ([#7339](https://github.com/python-poetry/poetry/pull/7339)). +- Add support for `virtualenvs.prefer-active-python` when running `poetry new` and `poetry init` ([#7100](https://github.com/python-poetry/poetry/pull/7100)). + +### Changed + +- **Deprecate the old installer, i.e. setting `experimental.new-installer` to `false`** ([#7358](https://github.com/python-poetry/poetry/pull/7358)). +- Remove unused `platform` field from cached package info and bump the cache version ([#7304](https://github.com/python-poetry/poetry/pull/7304)). +- Extra dependencies of the root project are now sorted in the lock file ([#7375](https://github.com/python-poetry/poetry/pull/7375)). +- Remove upper boundary for `importlib-metadata` dependency ([#7434](https://github.com/python-poetry/poetry/pull/7434)). +- Validate path dependencies during use instead of during construction ([#6844](https://github.com/python-poetry/poetry/pull/6844)). +- Remove the deprecated `repository` modules ([#7468](https://github.com/python-poetry/poetry/pull/7468)). + +### Fixed + +- Fix an issue where an unconditional dependency of an extra was not installed in specific environments ([#7175](https://github.com/python-poetry/poetry/pull/7175)). +- Fix an issue where a pre-release of a dependency was chosen even if a stable release fulfilled the constraint ([#7225](https://github.com/python-poetry/poetry/pull/7225), [#7236](https://github.com/python-poetry/poetry/pull/7236)). +- Fix an issue where HTTP redirects were not handled correctly during publishing ([#7160](https://github.com/python-poetry/poetry/pull/7160)). +- Fix an issue where `poetry check` did not handle the `-C, --directory` option correctly ([#7241](https://github.com/python-poetry/poetry/pull/7241)). +- Fix an issue where the subdirectory information of a git dependency was not written to the lock file ([#7367](https://github.com/python-poetry/poetry/pull/7367)). +- Fix an issue where the wrong Python version was selected when creating an virtual environment ([#7221](https://github.com/python-poetry/poetry/pull/7221)). +- Fix an issue where packages that should be kept were uninstalled when calling `poetry install --sync` ([#7389](https://github.com/python-poetry/poetry/pull/7389)). +- Fix an issue where an incorrect value was set for `sys.argv[0]` when running installed scripts ([#6737](https://github.com/python-poetry/poetry/pull/6737)). +- Fix an issue where hashes in `direct_url.json` files were not written according to the specification ([#7475](https://github.com/python-poetry/poetry/pull/7475)). +- Fix an issue where poetry commands failed due to special characters in the path of the project or virtual environment ([#7471](https://github.com/python-poetry/poetry/pull/7471)). +- Fix an issue where poetry crashed with a `JSONDecodeError` when running a Python script that produced certain warnings ([#6665](https://github.com/python-poetry/poetry/pull/6665)). + +### Docs + +- Add advice on how to maintain a poetry plugin ([#6977](https://github.com/python-poetry/poetry/pull/6977)). +- Update tox examples to comply with the latest tox release ([#7341](https://github.com/python-poetry/poetry/pull/7341)). +- Mention that the `poetry export` can export `constraints.txt` files ([#7383](https://github.com/python-poetry/poetry/pull/7383)). +- Add clarifications for moving configuration files ([#6864](https://github.com/python-poetry/poetry/pull/6864)). +- Mention the different types of exact version specifications ([#7503](https://github.com/python-poetry/poetry/pull/7503)). + +### poetry-core ([`1.5.1`](https://github.com/python-poetry/poetry-core/releases/tag/1.5.1)) + +- Improve marker handling ([#528](https://github.com/python-poetry/poetry-core/pull/528), +[#534](https://github.com/python-poetry/poetry-core/pull/534), +[#530](https://github.com/python-poetry/poetry-core/pull/530), +[#546](https://github.com/python-poetry/poetry-core/pull/546), +[#547](https://github.com/python-poetry/poetry-core/pull/547)). +- Validate whether dependencies referenced in `extras` are defined in the main dependency group ([#542](https://github.com/python-poetry/poetry-core/pull/542)). +- Poetry no longer generates a `setup.py` file in sdists by default ([#318](https://github.com/python-poetry/poetry-core/pull/318)). +- Fix an issue where trailing newlines were allowed in `tool.poetry.description` ([#505](https://github.com/python-poetry/poetry-core/pull/505)). +- Fix an issue where the name of the data folder in wheels was not normalized ([#532](https://github.com/python-poetry/poetry-core/pull/532)). +- Fix an issue where the order of entries in the RECORD file was not deterministic ([#545](https://github.com/python-poetry/poetry-core/pull/545)). +- Fix an issue where zero padding was not correctly handled in version comparisons ([#540](https://github.com/python-poetry/poetry-core/pull/540)). +- Fix an issue where sdist builds did not support multiple READMEs ([#486](https://github.com/python-poetry/poetry-core/pull/486)). + +### poetry-plugin-export ([`^1.3.0`](https://github.com/python-poetry/poetry-plugin-export/releases/tag/1.3.0)) + +- Fix an issue where the export failed if there was a circular dependency on the root package ([#118](https://github.com/python-poetry/poetry-plugin-export/pull/118)). + + +## [1.3.2] - 2023-01-10 + +### Fixed + +- Fix a performance regression when locking dependencies from PyPI ([#7232](https://github.com/python-poetry/poetry/pull/7232)). +- Fix an issue where passing a relative path via `-C, --directory` fails ([#7266](https://github.com/python-poetry/poetry/pull/7266)). + +### Docs + +- Update docs to reflect the removal of the deprecated `get-poetry.py` installer from the repository ([#7288](https://github.com/python-poetry/poetry/pull/7288)). +- Add clarifications for `virtualenvs.path` settings ([#7286](https://github.com/python-poetry/poetry/pull/7286)). + + +## [1.3.1] - 2022-12-12 + +### Fixed + +- Fix an issue where an explicit dependency on `lockfile` was missing, resulting in a broken Poetry in rare circumstances ([7169](https://github.com/python-poetry/poetry/pull/7169)). + + +## [1.3.0] - 2022-12-09 + +### Added + +- Mark the lock file with an `@generated` comment as used by common tooling ([#2773](https://github.com/python-poetry/poetry/pull/2773)). +- `poetry check` validates trove classifiers and warns for deprecations ([#2881](https://github.com/python-poetry/poetry/pull/2881)). +- Introduce a top level `-C, --directory` option to set the working path ([#6810](https://github.com/python-poetry/poetry/pull/6810)). + +### Changed + +- **New lock file format (version 2.0)** ([#6393](https://github.com/python-poetry/poetry/pull/6393)). +- Path dependency metadata is unconditionally re-locked ([#6843](https://github.com/python-poetry/poetry/pull/6843)). +- URL dependency hashes are locked ([#7121](https://github.com/python-poetry/poetry/pull/7121)). +- `poetry update` and `poetry lock` should now resolve dependencies more similarly ([#6477](https://github.com/python-poetry/poetry/pull/6477)). +- `poetry publish` will report more useful errors when a file does not exist ([#4417](https://github.com/python-poetry/poetry/pull/4417)). +- `poetry add` will check for duplicate entries using canonical names ([#6832](https://github.com/python-poetry/poetry/pull/6832)). +- Wheels are preferred to source distributions when gathering metadata ([#6547](https://github.com/python-poetry/poetry/pull/6547)). +- Git dependencies of extras are only fetched if the extra is requested ([#6615](https://github.com/python-poetry/poetry/pull/6615)). +- Invoke `pip` with `--no-input` to prevent hanging without feedback ([#6724](https://github.com/python-poetry/poetry/pull/6724), [#6966](https://github.com/python-poetry/poetry/pull/6966)). +- Invoke `pip` with `--isolated` to prevent the influence of user configuration ([#6531](https://github.com/python-poetry/poetry/pull/6531)). +- Interrogate environments with Python in isolated (`-I`) mode ([#6628](https://github.com/python-poetry/poetry/pull/6628)). +- Raise an informative error when multiple version constraints overlap and are incompatible ([#7098](https://github.com/python-poetry/poetry/pull/7098)). + +### Fixed + +- **Fix an issue where concurrent instances of Poetry would corrupt the artifact cache** ([#6186](https://github.com/python-poetry/poetry/pull/6186)). +- **Fix an issue where Poetry can hang after being interrupted due to stale locking in cache** ([#6471](https://github.com/python-poetry/poetry/pull/6471)). +- Fix an issue where the output of commands executed with `--dry-run` contained duplicate entries ([#4660](https://github.com/python-poetry/poetry/pull/4660)). +- Fix an issue where `requests`'s pool size did not match the number of installer workers ([#6805](https://github.com/python-poetry/poetry/pull/6805)). +- Fix an issue where `poetry show --outdated` failed with a runtime error related to direct origin dependencies ([#6016](https://github.com/python-poetry/poetry/pull/6016)). +- Fix an issue where only the last command of an `ApplicationPlugin` is registered ([#6304](https://github.com/python-poetry/poetry/pull/6304)). +- Fix an issue where git dependencies were fetched unnecessarily when running `poetry lock --no-update` ([#6131](https://github.com/python-poetry/poetry/pull/6131)). +- Fix an issue where stdout was polluted with messages that should go to stderr ([#6429](https://github.com/python-poetry/poetry/pull/6429)). +- Fix an issue with `poetry shell` activation and zsh ([#5795](https://github.com/python-poetry/poetry/pull/5795)). +- Fix an issue where a url dependencies were shown as outdated ([#6396](https://github.com/python-poetry/poetry/pull/6396)). +- Fix an issue where the `source` field of a dependency with extras was ignored ([#6472](https://github.com/python-poetry/poetry/pull/6472)). +- Fix an issue where a package from the wrong source was installed for a multiple-constraints dependency with different sources ([#6747](https://github.com/python-poetry/poetry/pull/6747)). +- Fix an issue where dependencies from different sources where merged during dependency resolution ([#6679](https://github.com/python-poetry/poetry/pull/6679)). +- Fix an issue where `experimental.system-git-client` could not be used via environment variable ([#6783](https://github.com/python-poetry/poetry/pull/6783)). +- Fix an issue where Poetry fails with an `AssertionError` due to `distribution.files` being `None` ([#6788](https://github.com/python-poetry/poetry/pull/6788)). +- Fix an issue where `poetry env info` did not respect `virtualenvs.prefer-active-python` ([#6986](https://github.com/python-poetry/poetry/pull/6986)). +- Fix an issue where `poetry env list` does not list the in-project environment ([#6979](https://github.com/python-poetry/poetry/pull/6979)). +- Fix an issue where `poetry env remove` removed the wrong environment ([#6195](https://github.com/python-poetry/poetry/pull/6195)). +- Fix an issue where the return code of a script was not relayed as exit code ([#6824](https://github.com/python-poetry/poetry/pull/6824)). +- Fix an issue where the solver could silently swallow `ValueError` ([#6790](https://github.com/python-poetry/poetry/pull/6790)). + +### Docs + +- Improve documentation of package sources ([#5605](https://github.com/python-poetry/poetry/pull/5605)). +- Correct the default cache path on Windows ([#7012](https://github.com/python-poetry/poetry/pull/7012)). + +### poetry-core ([`1.4.0`](https://github.com/python-poetry/poetry-core/releases/tag/1.4.0)) + +- The PEP 517 `metadata_directory` is now respected as an input to the `build_wheel` hook ([#487](https://github.com/python-poetry/poetry-core/pull/487)). +- `ParseConstraintError` is now raised on version and constraint parsing errors, and includes information on the package that caused the error ([#514](https://github.com/python-poetry/poetry-core/pull/514)). +- Fix an issue where invalid PEP 508 requirements were generated due to a missing space before semicolons ([#510](https://github.com/python-poetry/poetry-core/pull/510)). +- Fix an issue where relative paths were encoded into package requirements, instead of a file:// URL as required by PEP 508 ([#512](https://github.com/python-poetry/poetry-core/pull/512)). + +### poetry-plugin-export ([`^1.2.0`](https://github.com/python-poetry/poetry-plugin-export/releases/tag/1.2.0)) + +- Ensure compatibility with Poetry 1.3.0. No functional changes. + +### cleo ([`^2.0.0`](https://github.com/python-poetry/poetry-core/releases/tag/2.0.0)) + +- Fix an issue where shell completions had syntax errors ([#247](https://github.com/python-poetry/cleo/pull/247)). +- Fix an issue where not reading all the output of a command resulted in a "Broken pipe" error ([#165](https://github.com/python-poetry/cleo/pull/165)). +- Fix an issue where errors were not shown in non-verbose mode ([#166](https://github.com/python-poetry/cleo/pull/166)). + + ## [1.2.2] - 2022-10-10 ### Added @@ -83,25 +270,25 @@ ### Docs - Added note about how to add a git dependency with a subdirectory ([#6218](https://github.com/python-poetry/poetry/pull/6218)) -- Fixed several style issues in the docs ([#6255](https://github.com/python-poetry/poetry/pull/6255)) -- Fixed outdated info about `--only` parameter ([#6264](https://github.com/python-poetry/poetry/pull/6264)) +- Fixed several style issues in the docs ([#6254](https://github.com/python-poetry/poetry/pull/6254)) +- Fixed outdated info about `--only` parameter ([#6263](https://github.com/python-poetry/poetry/pull/6263)) ## [1.2.0rc2] - 2022-08-26 ### Fixed -- Fixed an issue where virtual environments were created unnecessarily when running `poetry self` commands ([#6226](https://github.com/python-poetry/poetry/pull/6226)) -- Ensure that packages' `pretty_name` are written to the lock file ([#6243](https://github.com/python-poetry/poetry/pull/6243)) +- Fixed an issue where virtual environments were created unnecessarily when running `poetry self` commands ([#6225](https://github.com/python-poetry/poetry/pull/6225)) +- Ensure that packages' `pretty_name` are written to the lock file ([#6237](https://github.com/python-poetry/poetry/pull/6237)) ### Improvements -- Improved the consistency of `Pool().remove_repository()` to make it easier to write poetry plugins ([#6231](https://github.com/python-poetry/poetry/pull/6231)) +- Improved the consistency of `Pool().remove_repository()` to make it easier to write poetry plugins ([#6214](https://github.com/python-poetry/poetry/pull/6214)) ### Docs -- Removed mentions of Python 2.7 from docs ([#6235](https://github.com/python-poetry/poetry/pull/6235)) -- Added note about the difference between groups and extras ([#6232](https://github.com/python-poetry/poetry/pull/6232)) +- Removed mentions of Python 2.7 from docs ([#6234](https://github.com/python-poetry/poetry/pull/6234)) +- Added note about the difference between groups and extras ([#6230](https://github.com/python-poetry/poetry/pull/6230)) ## [1.2.0rc1] - 2022-08-22 @@ -1612,7 +1799,13 @@ Initial release -[Unreleased]: https://github.com/python-poetry/poetry/compare/1.2.2...master +[Unreleased]: https://github.com/python-poetry/poetry/compare/1.4.2...master +[1.4.2]: https://github.com/python-poetry/poetry/releases/tag/1.4.2 +[1.4.1]: https://github.com/python-poetry/poetry/releases/tag/1.4.1 +[1.4.0]: https://github.com/python-poetry/poetry/releases/tag/1.4.0 +[1.3.2]: https://github.com/python-poetry/poetry/releases/tag/1.3.2 +[1.3.1]: https://github.com/python-poetry/poetry/releases/tag/1.3.1 +[1.3.0]: https://github.com/python-poetry/poetry/releases/tag/1.3.0 [1.2.2]: https://github.com/python-poetry/poetry/releases/tag/1.2.2 [1.2.1]: https://github.com/python-poetry/poetry/releases/tag/1.2.1 [1.2.0]: https://github.com/python-poetry/poetry/releases/tag/1.2.0 diff --git a/LICENSE b/LICENSE index 44cf2b30e68..81a8e1e4739 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2018 Sébastien Eustace +Copyright (c) 2018-present Sébastien Eustace Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/docs/_index.md b/docs/_index.md index c383fdac017..ebe15c88b62 100644 --- a/docs/_index.md +++ b/docs/_index.md @@ -46,9 +46,13 @@ The script can be executed directly (i.e. 'curl python') or downloaded and then (e.g. in a CI environment). {{% warning %}} -The previous `get-poetry.py` and `install-poetry.py` installers are deprecated. Any installs performed -using `get-poetry.py` should be uninstalled and reinstalled using `install.python-poetry.org` to ensure -in-place upgrades are possible. +The `get-poetry.py` installer has been deprecated and removed. If you installed +Poetry using `get-poetry.py`, please uninstall (as documented in the relevant +step below), and then follow these installation instructions. + +The previous `install-poetry.py` script as included in the Poetry repository +is deprecated and frozen. Please migrate to the standalone version described +above, as the in-tree version is [scheduled to be removed](https://github.com/python-poetry/poetry/issues/6676). {{% /warning %}} **Linux, macOS, Windows (WSL)** @@ -182,11 +186,13 @@ curl -sSL https://install.python-poetry.org | POETRY_UNINSTALL=1 python3 - ``` {{% warning %}} -If you installed using the deprecated `get-poetry.py` script, you should use it to uninstall instead: +If you installed using the deprecated `get-poetry.py` script, you should remove the path it uses manually, e.g. ```bash -curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python3 - --uninstall +rm -rf "${POETRY_HOME:-~/.poetry}" ``` + +Also remove ~/.poetry/bin from your `$PATH` in your shell configuration, if it is present. {{% /warning %}} {{< /step >}} @@ -233,11 +239,11 @@ poetry@preview --version Finally, `pipx` can install any valid [pip requirement spec](https://pip.pypa.io/en/stable/cli/pip_install/), which allows for installations of the development version from `git`, or even for local testing of pull requests: -``` +```bash pipx install --suffix @master git+https://github.com/python-poetry/poetry.git@master pipx install --suffix @pr1234 git+https://github.com/python-poetry/poetry.git@refs/pull/1234/head - ``` + {{< /step >}} {{< step >}} **Update Poetry** diff --git a/docs/cli.md b/docs/cli.md index dee328483da..e57b1548e2e 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -29,6 +29,7 @@ then `--help` combined with any of those can give you more information. * `--no-interaction (-n)`: Do not ask any interactive question. * `--no-plugins`: Disables plugins. * `--no-cache`: Disables Poetry source caches. +* `--directory=DIRECTORY (-C)`: The working directory for the Poetry command (defaults to the current working directory). ## new @@ -224,6 +225,30 @@ If you want to skip this installation, use the `--no-root` option. poetry install --no-root ``` +Similar to `--no-root` you can use `--no-directory` to skip directory path dependencies: + +```bash +poetry install --no-directory +``` + +This is mainly useful for caching in CI or when building Docker images. See the [FAQ entry]({{< relref "faq#poetry-busts-my-docker-cache-because-it-requires-me-to-copy-my-source-files-in-before-installing-3rd-party-dependencies" >}}) for more information on this option. + +By default `poetry` does not compile Python source files to bytecode during installation. +This speeds up the installation process, but the first execution may take a little more +time because Python then compiles source files to bytecode automatically. +If you want to compile source files to bytecode during installation, +you can use the `--compile` option: + +```bash +poetry install --compile +``` + +{{% note %}} +The `--compile` option has no effect if `installer.modern-installation` +is set to `false` because the old installer always compiles source files to bytecode. +{{% /note %}} + + ### Options * `--without`: The dependency groups to ignore. @@ -232,9 +257,11 @@ poetry install --no-root * `--only-root`: Install only the root project, exclude all dependencies. * `--sync`: Synchronize the environment with the locked packages and the specified groups. * `--no-root`: Do not install the root package (your project). +* `--no-directory`: Skip all directory path dependencies (including transitive ones). * `--dry-run`: Output the operations but do not execute anything (implicitly enables --verbose). * `--extras (-E)`: Features to install (multiple values allowed). * `--all-extras`: Install all extra features (conflicts with --extras). +* `--compile`: Compile Python source files to bytecode. * `--no-dev`: Do not install dev dependencies. (**Deprecated**, use `--without dev` or `--only main` instead) * `--remove-untracked`: Remove dependencies not presented in the lock file. (**Deprecated**, use `--sync` instead) @@ -690,7 +717,7 @@ group defined in `tool.poetry.dependencies` when used without specifying any opt ### Options * `--format (-f)`: The format to export to (default: `requirements.txt`). - Currently, only `requirements.txt` is supported. + Currently, only `constraints.txt` and `requirements.txt` are supported. * `--output (-o)`: The name of the output file. If omitted, print to standard output. * `--dev`: Include development dependencies. (**Deprecated**, use `--with dev` instead) @@ -757,11 +784,12 @@ You cannot use the name `pypi` as it is reserved for use by the default PyPI sou #### 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. +* `--default`: Set this source as the [default]({{< relref "repositories#default-package-source" >}}) (disable PyPI). Deprecated in favor of `--priority`. +* `--secondary`: Set this source as a [secondary]({{< relref "repositories#secondary-package-sources" >}}) source. Deprecated in favor of `--priority`. +* `--priority`: Set the priority of this source. Accepted values are: [`default`]({{< relref "repositories#default-package-source" >}}), [`secondary`]({{< relref "repositories#secondary-package-sources" >}}), and [`explicit`]({{< relref "repositories#explicit-package-sources" >}}). Refer to the dedicated sections in [Repositories]({{< relref "repositories" >}}) for more information. {{% note %}} -You cannot set a source as both `default` and `secondary`. +At most one of the options above can be provided. See [package sources]({{< relref "repositories#package-sources" >}}) for more information. {{% /note %}} ### source show diff --git a/docs/configuration.md b/docs/configuration.md index d8c354bed87..4277cb1a095 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -12,7 +12,7 @@ menu: # Configuration 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. +or directly in the `config.toml` file that will be automatically created when you first run that command. This file can typically be found in one of the following directories: - macOS: `~/Library/Preferences/pypoetry` @@ -138,7 +138,7 @@ You can override the Data directory by setting the `POETRY_DATA_DIR` or `POETRY_ ### Cache Directory - Linux: `$XDG_CACHE_HOME/pypoetry` or `~/.cache/pypoetry` -- Windows: `%APPDATA%\pypoetry\Cache` +- Windows: `%LOCALAPPDATA%\pypoetry` - MacOS: `~/Library/Caches/pypoetry` You can override the Cache directory by setting the `POETRY_CACHE_DIR` environment variable. @@ -196,6 +196,19 @@ the number of maximum workers is still limited at `number_of_cores + 4`. This configuration is ignored when `installer.parallel` is set to `false`. {{% /note %}} +### `installer.modern-installation` + +**Type**: `boolean` + +**Default**: `true` + +*Introduced in 1.4.0* + +Use a more modern and faster method for package installation. + +If this causes issues, you can disable it by setting it to `false` and report the problems +you encounter on the [issue tracker](https://github.com/python-poetry/poetry/issues). + ### `installer.no-binary` **Type**: `string | boolean` @@ -380,6 +393,10 @@ Applies on virtualenv creation. Directory where virtual environments will be created. +{{% note %}} +This setting controls the global virtual environment storage path. It most likely will not be useful at the local level. To store virtual environments in the project root, see `virtualenvs.in-project`. +{{% /note %}} + ### `virtualenvs.prefer-active-python` (experimental) **Type**: `boolean` diff --git a/docs/dependency-specification.md b/docs/dependency-specification.md index b841d68bc10..1d0d0b51a53 100644 --- a/docs/dependency-specification.md +++ b/docs/dependency-specification.md @@ -79,11 +79,15 @@ Multiple version requirements can also be separated with a comma, e.g. `>= 1.2, You can specify the exact version of a package. -`==1.2.3` is an example of an exact version specification. +`1.2.3` is an example of an exact version specification. This will tell Poetry to install this version and this version only. If other dependencies require a different version, the solver will ultimately fail and abort any install or update procedures. +Exact versions can also be specified with `==` according to [PEP 440](https://peps.python.org/pep-0440/). + +`==1.2.3` is an example of this. + ### Using the `@` operator When adding dependencies via `poetry add`, you can use the `@` operator. @@ -247,14 +251,14 @@ for extras in your project refer to [`extras`]({{< relref "pyproject#extras" >}} ## `source` dependencies -To depend on a package from an [alternate repository]({{< relref "repositories/#install-dependencies-from-a-private-repository" >}}), +To depend on a package from an [alternate repository]({{< relref "repositories#installing-from-private-package-sources" >}}), you can use the `source` property: ```toml [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" -secondary = true +priority = "secondary" [tool.poetry.dependencies] my-cool-package = { version = "*", source = "foo" } @@ -267,7 +271,7 @@ poetry add my-cool-package --source foo ``` {{% note %}} -In this example, we expect `foo` to be configured correctly. See [using a private repository](repositories.md#using-a-private-repository) +In this example, we expect `foo` to be configured correctly. See [using a private repository]({{< relref "repositories#installing-from-private-package-sources" >}}) for further information. {{% /note %}} diff --git a/docs/faq.md b/docs/faq.md index 75c08cb6473..71ac22c1c2d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -22,28 +22,65 @@ and, as such, they are not available via the PyPI JSON API. At this point, Poetr but to download the packages and inspect them to get the necessary information. This is an expensive operation, both in bandwidth and time, which is why it seems this is a long process. -At the moment there is no way around it. +At the moment there is no way around it. However, if you notice that Poetry +is downloading many versions of a single package, you can lessen the workload +by constraining that one package in your pyproject.toml more narrowly. That way +Poetry does not have to sift through so many versions of it, which may speed up +the locking process considerably in some cases. {{% note %}} -Once Poetry has cached the releases' information, the dependency resolution process +Once Poetry has cached the releases' information on your machine, the dependency resolution process will be much faster. {{% /note %}} -### Why are unbound version constraints a bad idea? +### What kind of versioning scheme does Poetry use for itself? + +Poetry uses "major.minor.micro" version identifiers as mentioned in +[PEP 440](https://peps.python.org/pep-0440/#final-releases). + +Version bumps are done similar to Python's versioning: +* A major version bump (incrementing the first number) is only done for breaking changes + if a deprecation cycle is not possible and many users have to perform some manual steps + to migrate from one version to the next one. +* A minor version bump (incrementing the second number) may include new features as well + as new deprecations and drop features deprecated in an earlier minor release. +* A micro version bump (incrementing the third number) usually only includes bug fixes. + Deprecated features will not be dropped in a micro release. + +### Why does Poetry not adhere to semantic versioning? + +Because of its large user base, even small changes not considered relevant by most users +can turn out to be a breaking change for some users in hindsight. +Sticking to strict [semantic versioning](https://semver.org) and (almost) always bumping +the major version instead of the minor version does not seem desirable +since the minor version will not carry any meaning anymore. + +### 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. +(Users can still work around the broken dependency by restricting it by themselves.) -The only good alternative is to define an upper bound on your constraints, +To avoid such issues you can define an upper bound on your constraints, which you can increase in a new release after testing that your package is compatible with the new major version of your dependency. -For example instead of using `>=3.4` you should use `^3.4` which allows all versions `<4.0`. +For example instead of using `>=3.4` you can use `^3.4` which allows all versions `<4.0`. The `^` operator works very well with libraries following [semantic versioning](https://semver.org). +However, when defining an upper bound, users of your package are not able to update +a dependency beyond the upper bound even if it does not break anything +and is fully compatible with your package. +You have to release a new version of your package with an increased upper bound first. + +If your package will be used as a library in other packages, it might be better to avoid +upper bounds and thus unnecessary dependency conflicts (unless you already know for sure +that the next release of the dependency will break your package). +If your package will be used as an application, it might be worth to define an upper bound. + ### Is tox supported? **Yes**. By using the [isolated builds](https://tox.readthedocs.io/en/latest/config.html#conf-isolated_build) `tox` provides, @@ -81,7 +118,7 @@ Thus, dependencies are resolved by `pip`. isolated_build = true [testenv] -whitelist_externals = poetry +allowlist_externals = poetry commands_pre = poetry install --no-root --sync commands = @@ -99,7 +136,7 @@ isolated_build = true [testenv] skip_install = true -whitelist_externals = poetry +allowlist_externals = poetry commands_pre = poetry install commands = @@ -152,3 +189,37 @@ This is done so to be compliant with the broader Python ecosystem. For example, if Poetry builds a distribution for a project that uses a version that is not valid according to [PEP 440](https://peps.python.org/pep-0440), third party tools will be unable to parse the version correctly. + + +### Poetry busts my Docker cache because it requires me to COPY my source files in before installing 3rd party dependencies + +By default running `poetry install ...` requires you to have your source files present (both the "root" package and any directory path dependencies you might have). +This interacts poorly with Docker's caching mechanisms because any change to a source file will make any layers (subsequent commands in your Dockerfile) re-run. +For example, you might have a Dockerfile that looks something like this: + +```text +FROM python +COPY pyproject.toml poetry.lock . +COPY src/ ./src +RUN pip install poetry && poetry install --no-dev +``` + +As soon as *any* source file changes, the cache for the `RUN` layer will be invalidated, which forces all 3rd party dependencies (likely the slowest step out of these) to be installed again if you changed any files in `src/`. + +To avoid this cache busting you can split this into two steps: + +1. Install 3rd party dependencies. +2. Copy over your source code and install just the source code. + +This might look something like this: + +```text +FROM python +COPY pyproject.toml poetry.lock . +RUN pip install poetry && poetry install --no-root --no-directory +COPY src/ ./src +RUN poetry install --no-dev +``` + +The two key options we are using here are `--no-root` (skips installing the project source) and `--no-directory` (skips installing any local directory path dependencies, you can omit this if you don't have any). +[More information on the options available for `poetry install`]({{< relref "cli#install" >}}). diff --git a/docs/libraries.md b/docs/libraries.md index ab56979540b..5ebe373cc94 100644 --- a/docs/libraries.md +++ b/docs/libraries.md @@ -19,10 +19,11 @@ This chapter will tell you how to make your library installable through Poetry. Poetry requires [PEP 440](https://peps.python.org/pep-0440)-compliant versions for all projects. -While Poetry does not enforce any release convention, it does encourage the use of +While Poetry does not enforce any release convention, it used to encourage the use of [semantic versioning](https://semver.org/) within the scope of -[PEP 440](https://peps.python.org/pep-0440/#semantic-versioning). This has many advantages for the end users -and allows them to set appropriate [version constraints]({{< relref "dependency-specification#version-constraints" >}}). +[PEP 440](https://peps.python.org/pep-0440/#semantic-versioning) and supports +[version constraints]({{< relref "dependency-specification/#caret-requirements" >}}) +that are especially suitable for semver. {{% note %}} diff --git a/docs/managing-dependencies.md b/docs/managing-dependencies.md index c1fa129674c..14932d43df9 100644 --- a/docs/managing-dependencies.md +++ b/docs/managing-dependencies.md @@ -59,19 +59,24 @@ use [extras]({{< relref "pyproject#extras" >}}) instead. Extras can be installed {{% /note %}} {{% note %}} -**A note about the `dev-dependencies` section** +**A note about defining a `dev` dependencies group** -Any dependency declared in the `dev-dependencies` section will automatically be added to a `dev` group. -So the two following notations are equivalent: +The proper way to define a `dev` dependencies group since Poetry 1.2.0 is the following: ```toml -[tool.poetry.dev-dependencies] +[tool.poetry.group.dev.dependencies] pytest = "^6.0.0" pytest-mock = "*" ``` +This group notation is preferred since Poetry 1.2.0 and not usable in earlier versions. +For backwards compatibility with older versions of Poetry, +any dependency declared in the `dev-dependencies` section will automatically be added to the `dev` group. +So the above and following notations are equivalent: + ```toml -[tool.poetry.group.dev.dependencies] +# Poetry pre-1.2.x style, understood by Poetry 1.0–1.2 +[tool.poetry.dev-dependencies] pytest = "^6.0.0" pytest-mock = "*" ``` @@ -158,7 +163,7 @@ poetry install --only docs ``` {{% note %}} -If you only want to install the project's runtime dependencies, you can do so with the +If you only want to install the project's runtime dependencies, you can do so with the `--only main` notation: ```bash diff --git a/docs/plugins.md b/docs/plugins.md index 1db8c065f0f..da1bb5ad55e 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -249,3 +249,18 @@ If you want to uninstall a plugin, you can run: ```shell $POETRY_HOME/bin/pip uninstall poetry-plugin ``` + + +## Maintaining a plugin + +When writing a plugin, you will probably access internals of Poetry, since there is no +stable public API. Although we try our best to deprecate methods first, before +removing them, sometimes the signature of an internal method has to be changed. + +As the author of a plugin, you are probably testing your plugin +against the latest release of Poetry. +Additionally, you should consider testing against the latest release branch and the +master branch of Poetry and schedule a CI job that runs regularly even if you did not +make any changes to your plugin. +This way, you will notice internal changes that break your plugin immediately +and can prepare for the next Poetry release. diff --git a/docs/pre-commit-hooks.md b/docs/pre-commit-hooks.md index 7fa16e8e896..ab833dab3f3 100644 --- a/docs/pre-commit-hooks.md +++ b/docs/pre-commit-hooks.md @@ -34,6 +34,9 @@ to make sure the poetry configuration does not get committed in a broken state. The hook takes the same arguments as the poetry command. For more information see the [check command]({{< relref "cli#check" >}}). +{{% note %}} +If the `pyproject.toml` file is not in the root directory, you can specify `args: ["-C", "./subdirectory"]`. +{{% /note %}} ## poetry-lock @@ -106,7 +109,7 @@ repos: `pre-commit autoupdate` updates the `rev` for each repository defined in your `.pre-commit-config.yaml` to the latest available tag in the default branch. -Poetry follows a branching strategy, where the default branch is the active developement branch +Poetry follows a branching strategy, where the default branch is the active development branch and fixes gets back ported to stable branches. New tags are assigned in these stable branches. `pre-commit` does not support such a branching strategy and has decided to not implement diff --git a/docs/pyproject.md b/docs/pyproject.md index 955c64d4c6a..a344ffc0db5 100644 --- a/docs/pyproject.md +++ b/docs/pyproject.md @@ -208,7 +208,7 @@ packages = [ ] ``` -If you want to restrict a package to a specific [build](#build) format you can specify +If you want to restrict a package to a specific build format you can specify it by using `format`: ```toml @@ -395,6 +395,14 @@ You can install all extras with the `--all-extras` option: poetry install --all-extras ``` +{{% note %}} +Note that `install --extras` and the variations mentioned above (`--all-extras`, `--extras foo`, etc.) only work on dependencies defined in the current project. If you want to install extras defined by dependencies, you'll have to express that in the dependency itself: +```toml +[tool.poetry.group.dev.dependencies] +fastapi = {version="^0.92.0", extras=["all"]} +``` +{{% /note %}} + 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). diff --git a/docs/repositories.md b/docs/repositories.md index 952ae6dbc76..38f993892f4 100644 --- a/docs/repositories.md +++ b/docs/repositories.md @@ -33,7 +33,7 @@ First, [configure](#project-configuration) the [package source](#package-source) project. ```bash -poetry source add --secondary foo https://pypi.example.org/simple/ +poetry source add --priority=secondary foo https://pypi.example.org/simple/ ``` Then, assuming the repository requires authentication, configure credentials for it. @@ -120,12 +120,20 @@ This will generate the following configuration snippet in your [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" -default = false -secondary = false +priority = "primary" ``` -Any package source not marked as `secondary` will take precedence over [PyPI](https://pypi.org). +If `priority` is undefined, the source is considered a primary source that takes precedence over PyPI, secondary and explicit sources. +Package sources are considered in the following order: +1. [default source](#default-package-source), +2. primary sources, +3. PyPI (unless disabled by another default source), +4. [secondary sources](#secondary-package-sources), + +[Explicit sources](#explicit-package-sources) are considered only for packages that explicitly [indicate their source](#package-source-constraint). + +Within each priority class, package sources are considered in order of appearance in `pyproject.toml`. {{% note %}} @@ -148,10 +156,10 @@ you must declare **all** package sources to be [secondary](#secondary-package-so By default, Poetry configures [PyPI](https://pypi.org) as the default package source for your project. You can alter this behaviour and exclusively look up packages only from the configured -package sources by adding a **single** source with `default = true`. +package sources by adding a **single** source with `priority = "default"`. ```bash -poetry source add --default foo https://foo.bar/simple/ +poetry source add --priority=default foo https://foo.bar/simple/ ``` {{% warning %}} @@ -164,30 +172,45 @@ as a package source for your project. #### Secondary Package Sources If package sources are configured as secondary, all it means is that these will be given a lower -priority when selecting compatible package distribution that also exists in your default package -source. +priority when selecting compatible package distribution that also exists in your default and primary package sources. -You can configure a package source as a secondary source with `secondary = true` in your package +You can configure a package source as a secondary source with `priority = "secondary"` in your package source configuration. ```bash -poetry source add --secondary foo https://foo.bar/simple/ +poetry source add --priority=secondary https://foo.bar/simple/ ``` There can be more than one secondary package source. -{{% note %}} +#### Explicit Package Sources + +*Introduced in 1.5.0* + +If package sources are configured as explicit, these sources are only searched when a package configuration [explicitly indicates](#package-source-constraint) that it should be found on this package source. + +You can configure a package source as an explicit source with `priority = "explicit` in your package source configuration. + +```bash +poetry source add --priority=explicit foo https://foo.bar/simple/ +``` + +There can be more than one explicit package source. + +#### Package Source Constraint All package sources (including secondary sources) will be searched during the package lookup process. These network requests will occur for all sources, regardless of if the package is found at one or more sources. -In order to limit the search for a specific package to a particular package repository, you can specify the source explicitly. This is strongly suggested for all private packages to avoid dependency confusion attacks. +In order to limit the search for a specific package to a particular package repository, you can specify the source explicitly. ```bash poetry add --source internal-pypi httpx ``` +This results in the following configuration in `pyproject.toml`: + ```toml [tool.poetry.dependencies] ... @@ -195,10 +218,48 @@ httpx = { version = "^0.22", source = "internal-pypi" } [[tool.poetry.source]] name = "internal-pypi" -url = "https://foo.bar/simple/" -secondary = true +url = ... +priority = ... +``` + +{{% note %}} + +A repository that is configured to be the only source for retrieving a certain package can itself have any priority. +In particular, it does not need to have priority `"explicit"`. +If a repository is configured to be the source of a package, it will be the only source that is considered for that package +and the repository priority will have no effect on the resolution. + +{{% /note %}} + +{{% note %}} + +Package `source` keys are not inherited by their dependencies. +In particular, if `package-A` is configured to be found in `source = internal-pypi`, +and `package-A` depends on `package-B` that is also to be found on `internal-pypi`, +then `package-B` needs to be configured as such in `pyproject.toml`. +The easiest way to achieve this is to add `package-B` with a wildcard constraint: + +```bash +poetry add --source internal-pypi package-B@* ``` +This will ensure that `package-B` is searched only in the `internal-pypi` package source. +The version constraints on `package-B` are derived from `package-A` (and other client packages), as usual. + +If you want to avoid additional main dependencies, +you can add `package-B` to a dedicated [dependency group]({{< relref "managing-dependencies#dependency-groups" >}}): + +```bash +poetry add --group explicit --source internal-pypi package-B@* +``` + +{{% /note %}} + +{{% note %}} + +Package source constraints are strongly suggested for all packages that are expected +to be provided only by one specific source to avoid dependency confusion attacks. + {{% /note %}} ### Supported Package Sources @@ -231,7 +292,7 @@ httpx = {version = "^0.22.0", source = "pypi"} {{% warning %}} -If any source within a project is configured with `default = true`, The implicit `pypi` source will +If any source within a project is configured with `priority = "default"`, The implicit `pypi` source will be disabled and not used for any packages. {{% /warning %}} @@ -249,7 +310,7 @@ manifest as long dependency resolution times when adding packages from this sour {{% /warning %}} -These package sources maybe configured via the following command in your project. +These package sources may be configured via the following command in your project. ```bash poetry source add testpypi https://test.pypi.org/simple/ @@ -286,7 +347,7 @@ inspecting it locally is the only remaining option. Some projects choose to release their binary distributions via a single page link source that partially follows the structure of a package page in [PEP 503](https://peps.python.org/pep-0503/). -These package sources maybe configured via the following command in your project. +These package sources may be configured via the following command in your project. ```bash poetry source add jax https://storage.googleapis.com/jax-releases/jax_releases.html @@ -342,7 +403,7 @@ 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 +poetry config pypi-token.pypi ``` If you still want to use your username and password, you can do so with the following diff --git a/get-poetry.py b/get-poetry.py deleted file mode 100644 index 7c3788b156a..00000000000 --- a/get-poetry.py +++ /dev/null @@ -1,1161 +0,0 @@ -""" -This script will install Poetry and its dependencies -in isolation from the rest of the system. - -It does, in order: - - - Downloads the latest stable (or pre-release) version of poetry. - - Downloads all its dependencies in the poetry/_vendor directory. - - Copies it and all extra files in $POETRY_HOME. - - Updates the PATH in a system-specific way. - -There will be a `poetry` script that will be installed in $POETRY_HOME/bin -which will act as the poetry command but is slightly different in the sense -that it will use the current Python installation. - -What this means is that one Poetry installation can serve for multiple -Python versions. -""" -import argparse -import hashlib -import json -import os -import platform -import re -import shutil -import stat -import subprocess -import sys -import tarfile -import tempfile - -from contextlib import closing -from contextlib import contextmanager -from functools import cmp_to_key -from gzip import GzipFile -from io import UnsupportedOperation -from io import open - - -try: - from urllib.error import HTTPError - from urllib.request import Request - from urllib.request import urlopen -except ImportError: - from urllib2 import HTTPError - from urllib2 import Request - from urllib2 import urlopen - -try: - input = raw_input -except NameError: - pass - - -try: - try: - import winreg - except ImportError: - import _winreg as winreg -except ImportError: - winreg = None - -try: - u = unicode -except NameError: - u = str - -SHELL = os.getenv("SHELL", "") -WINDOWS = sys.platform.startswith("win") or (sys.platform == "cli" and os.name == "nt") - - -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("green", None, None), - "comment": style("yellow", None, None), - "error": style("red", None, None), - "warning": style("yellow", None, None), - "deprecation": style("magenta", None, None), -} - - -def is_decorated(): - if platform.system().lower() == "windows": - return ( - os.getenv("ANSICON") is not None - or os.getenv("ConEmuANSI") == "ON" - or os.getenv("Term") == "xterm" - ) - - 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) - - -@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 - - -def string_to_bool(value): - value = value.lower() - - return value in {"true", "1", "y", "yes"} - - -def expanduser(path): - """ - Expand ~ and ~user constructions. - - Includes a workaround for http://bugs.python.org/issue14768 - """ - expanded = os.path.expanduser(path) - if path.startswith("~/") and expanded.startswith("//"): - expanded = expanded[1:] - - return expanded - - -HOME = expanduser("~") -POETRY_HOME = os.environ.get("POETRY_HOME") or os.path.join(HOME, ".poetry") -POETRY_BIN = os.path.join(POETRY_HOME, "bin") -POETRY_ENV = os.path.join(POETRY_HOME, "env") -POETRY_LIB = os.path.join(POETRY_HOME, "lib") -POETRY_LIB_BACKUP = os.path.join(POETRY_HOME, "lib-backup") - - -BIN = """# -*- 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() -""" - -BAT = u('@echo off\r\n{python_executable} "{poetry_bin}" %*\r\n') - - -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} - -{platform_msg} - -You can uninstall at any time by executing this script with the --uninstall option, -and these changes will be reverted. -""" - -PRE_UNINSTALL_MESSAGE = """# We are sorry to see you go! - -This will uninstall {poetry}. - -It will remove the `poetry` command from {poetry}'s bin directory, located at: - -{poetry_home_bin} - -This will also remove {poetry} from your system's PATH. -""" - - -PRE_MESSAGE_UNIX = """This path will then be added to your `PATH` environment variable by -modifying the profile file{plural} located at: - -{rcfiles}""" - - -PRE_MESSAGE_FISH = """This path will then be added to your `PATH` environment variable by -modifying the `fish_user_paths` universal variable.""" - -PRE_MESSAGE_WINDOWS = """This path will then be added to your `PATH` environment variable by -modifying the `HKEY_CURRENT_USER/Environment/PATH` registry key.""" - -PRE_MESSAGE_NO_MODIFY_PATH = """This path needs to be in your `PATH` environment variable, -but will not be added automatically.""" - -POST_MESSAGE_UNIX = """{poetry} ({version}) is installed now. Great! - -To get started you need {poetry}'s bin directory ({poetry_home_bin}) in your `PATH` -environment variable. Next time you log in this will be done -automatically. - -To configure your current shell run `source {poetry_home_env}` -""" - -POST_MESSAGE_FISH = """{poetry} ({version}) is installed now. Great! - -{poetry}'s bin directory ({poetry_home_bin}) has been added to your `PATH` -environment variable by modifying the `fish_user_paths` universal variable. -""" - -POST_MESSAGE_WINDOWS = """{poetry} ({version}) is installed now. Great! - -To get started you need Poetry's bin directory ({poetry_home_bin}) in your `PATH` -environment variable. Future applications will automatically have the -correct environment, but you may need to restart your current shell. -""" - -POST_MESSAGE_UNIX_NO_MODIFY_PATH = """{poetry} ({version}) is installed now. Great! - -To get started you need {poetry}'s bin directory ({poetry_home_bin}) in your `PATH` -environment variable. - -To configure your current shell run `source {poetry_home_env}` -""" - -POST_MESSAGE_FISH_NO_MODIFY_PATH = """{poetry} ({version}) is installed now. Great! - -To get started you need {poetry}'s bin directory ({poetry_home_bin}) -in your `PATH` environment variable, which you can add by running -the following command: - - set -U fish_user_paths {poetry_home_bin} $fish_user_paths -""" - -POST_MESSAGE_WINDOWS_NO_MODIFY_PATH = """{poetry} ({version}) is installed now. Great! - -To get started you need Poetry's bin directory ({poetry_home_bin}) in your `PATH` -environment variable. This has not been done automatically. -""" - - -class Installer: - CURRENT_PYTHON = sys.executable - CURRENT_PYTHON_VERSION = sys.version_info[:2] - 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]+)?" - ) - - REPOSITORY_URL = "https://github.com/python-poetry/poetry" - BASE_URL = REPOSITORY_URL + "/releases/download/" - FALLBACK_BASE_URL = "https://github.com/sdispater/poetry/releases/download/" - - def __init__( - self, - version=None, - preview=False, - force=False, - modify_path=True, - accept_all=False, - file=None, - base_url=BASE_URL, - ): - self._version = version - self._preview = preview - self._force = force - self._modify_path = modify_path - self._accept_all = accept_all - self._offline_file = file - self._base_url = base_url - - def allows_prereleases(self): - return self._preview - - def run(self): - version, current_version = self.get_version() - - if version is None: - return 1 - - self.customize_install() - self.display_pre_message() - self.ensure_home() - - try: - self.install( - version, upgrade=current_version is not None, file=self._offline_file - ) - except subprocess.CalledProcessError as e: - print(colorize("error", "An error has occurred: {}".format(str(e)))) - print(e.output.decode()) - - return e.returncode - - self.display_post_message(version) - - return 0 - - def uninstall(self): - self.display_pre_uninstall_message() - - if not self.customize_uninstall(): - return - - self.remove_home() - self.remove_from_path() - - def get_version(self): - current_version = None - if os.path.exists(POETRY_LIB): - with open( - os.path.join(POETRY_LIB, "poetry", "__version__.py"), encoding="utf-8" - ) as f: - version_content = f.read() - - current_version_re = re.match( - '(?ms).*__version__ = "(.+)".*', version_content - ) - if not current_version_re: - print( - colorize( - "warning", - "Unable to get the current Poetry version. Assuming None", - ) - ) - else: - current_version = current_version_re.group(1) - - # Skip retrieving online release versions if install file is specified - if self._offline_file is not None: - if current_version is not None and not self._force: - print("There is a version of Poetry already installed.") - return None, current_version - - return "from an offline file", current_version - - print(colorize("info", "Retrieving Poetry metadata")) - - 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 - - print("") - releases = sorted( - metadata["releases"].keys(), key=cmp_to_key(_compare_versions) - ) - - if self._version and self._version not in releases: - print(colorize("error", "Version {} does not exist.".format(self._version))) - - return None, None - - 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) - - print( - colorize( - "deprecation", - "This installer is deprecated, and scheduled for removal from the" - " Poetry repository on or after January 1, 2023.\nSee" - " https://github.com/python-poetry/poetry/issues/6377 for" - " details.\n\nYou should migrate to https://install.python-poetry.org" - " instead, which supports all versions of Poetry, and allows for `self" - " update` of versions 1.1.7 or newer.\nInstructions are available at" - " https://python-poetry.org/docs/#installation.\n", - ) - ) - - if not os.environ.get("GET_POETRY_IGNORE_DEPRECATION") and not self._version: - print( - colorize( - "deprecation", - "Without an explicit version, this installer will attempt to" - " install the latest version of Poetry.\nThis installer cannot" - " install Poetry 1.2.0a1 or newer (and installs will be unable to" - " `self update` to 1.2.0a1 or newer).\n\nTo continue to use this" - " deprecated installer, you must specify an explicit version with" - " --version or POETRY_VERSION=.\nAlternatively," - " if you wish to force this deprecated installer to use the latest" - " installable release, set GET_POETRY_IGNORE_DEPRECATION=1.\n", - ) - ) - - 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 - - if not os.environ.get("GET_POETRY_IGNORE_DEPRECATION"): - version = release - break - else: - if _is_supported(release): - print( - colorize( - "warning", - "Version {release} will be installed as it is the" - " latest version supported by this installer.\n\nThis" - " is not the latest version of Poetry! If you wish to" - " install the latest version, you must move to the new" - " installer as described above!\n".format( - release=release - ), - ) - ) - version = release - break - else: - print( - colorize( - "warning", - "Version {release} is available but is not supported by" - " this installer!".format(release=release), - ) - ) - - if not _is_supported(version): - print( - colorize( - "error", - "Version {version} is not supported by this installer! Please" - " specify a version prior to 1.2.0a1 to continue!".format( - version=version - ), - ) - ) - - return None, None - - current_version = None - if os.path.exists(POETRY_LIB): - with open( - os.path.join(POETRY_LIB, "poetry", "__version__.py"), encoding="utf-8" - ) as f: - version_content = f.read() - - current_version_re = re.match( - '(?ms).*__version__ = "(.+)".*', version_content - ) - if not current_version_re: - print( - colorize( - "warning", - "Unable to get the current Poetry version. Assuming None", - ) - ) - else: - current_version = current_version_re.group(1) - - if current_version == version and not self._force: - print("Latest version already installed.") - return None, current_version - - return version, current_version - - def customize_install(self): - if not self._accept_all: - print("Before we start, please answer the following questions.") - print("You may simply press the Enter key to leave unchanged.") - - modify_path = input("Modify PATH variable? ([y]/n) ") or "y" - if modify_path.lower() in {"n", "no"}: - self._modify_path = False - - print("") - - def customize_uninstall(self): - if not self._accept_all: - print() - - uninstall = ( - input("Are you sure you want to uninstall Poetry? (y/[n]) ") or "n" - ) - if uninstall.lower() not in {"y", "yes"}: - return False - - print("") - - return True - - def ensure_home(self): - """ - Ensures that $POETRY_HOME exists or create it. - """ - if not os.path.exists(POETRY_HOME): - os.mkdir(POETRY_HOME, 0o755) - - def remove_home(self): - """ - Removes $POETRY_HOME. - """ - if not os.path.exists(POETRY_HOME): - return - - shutil.rmtree(POETRY_HOME) - - def install(self, version, upgrade=False, file=None): - """ - Installs Poetry in $POETRY_HOME. - """ - if file is not None: - print("Attempting to install from file: " + colorize("info", file)) - else: - print("Installing version: " + colorize("info", version)) - - self.make_lib(version) - self.make_bin() - self.make_env() - self.update_path() - - return 0 - - def make_lib(self, version): - """ - Packs everything into a single lib/ directory. - """ - if os.path.exists(POETRY_LIB_BACKUP): - shutil.rmtree(POETRY_LIB_BACKUP) - - # Backup the current installation - if os.path.exists(POETRY_LIB): - shutil.copytree(POETRY_LIB, POETRY_LIB_BACKUP) - shutil.rmtree(POETRY_LIB) - - try: - self._make_lib(version) - except Exception: - if not os.path.exists(POETRY_LIB_BACKUP): - raise - - shutil.copytree(POETRY_LIB_BACKUP, POETRY_LIB) - shutil.rmtree(POETRY_LIB_BACKUP) - - raise - finally: - if os.path.exists(POETRY_LIB_BACKUP): - shutil.rmtree(POETRY_LIB_BACKUP) - - def _make_lib(self, version): - # Check if an offline installer file has been specified - if self._offline_file is not None: - try: - self.extract_lib(self._offline_file) - return - except Exception: - raise RuntimeError("Could not install from offline file.") - - # We get the payload from the remote host - platform = sys.platform - if platform == "linux2": - platform = "linux" - - url = self._base_url + "{}/".format(version) - name = "poetry-{}-{}.tar.gz".format(version, platform) - checksum = "poetry-{}-{}.sha256sum".format(version, platform) - - try: - r = urlopen(url + "{}".format(checksum)) - except HTTPError as e: - if e.code == 404: - raise RuntimeError("Could not find {} file".format(checksum)) - - raise - - checksum = r.read().decode() - - try: - r = urlopen(url + "{}".format(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 - - print( - " - Downloading {} ({:.2f}MB)".format( - colorize("comment", name), size / 1024 / 1024 - ) - ) - - sha = hashlib.sha256() - with temporary_directory(prefix="poetry-installer-") 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) - - # Checking hashes - if checksum != sha.hexdigest(): - raise RuntimeError( - "Hashes for {} do not match: {} != {}".format( - name, checksum, sha.hexdigest() - ) - ) - - self.extract_lib(tar) - - def extract_lib(self, filename): - gz = GzipFile(filename, mode="rb") - try: - with tarfile.TarFile(filename, fileobj=gz, format=tarfile.PAX_FORMAT) as f: - f.extractall(POETRY_LIB) - finally: - gz.close() - - def _which_python(self): - """Decides which python executable we'll embed in the launcher script.""" - allowed_executables = ["python3", "python"] - 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: - return executable - - if fallback is None: - # keep this one as the fallback; it was the first valid executable we - # found. - fallback = executable - - if fallback is None: - raise RuntimeError( - "No python executable found in shell environment. Tried: " - + str(allowed_executables) - ) - - return fallback - - def make_bin(self): - if not os.path.exists(POETRY_BIN): - os.mkdir(POETRY_BIN, 0o755) - - python_executable = self._which_python() - - if WINDOWS: - with open(os.path.join(POETRY_BIN, "poetry.bat"), "w") as f: - f.write( - u( - BAT.format( - python_executable=python_executable, - poetry_bin=os.path.join(POETRY_BIN, "poetry").replace( - os.environ["USERPROFILE"], "%USERPROFILE%" - ), - ) - ) - ) - - with open(os.path.join(POETRY_BIN, "poetry"), "w", encoding="utf-8") as f: - if WINDOWS: - python_executable = "python" - - f.write(u("#!/usr/bin/env {}\n".format(python_executable))) - f.write(u(BIN)) - - if not WINDOWS: - # Making the file executable - st = os.stat(os.path.join(POETRY_BIN, "poetry")) - os.chmod(os.path.join(POETRY_BIN, "poetry"), st.st_mode | stat.S_IEXEC) - - def make_env(self): - if WINDOWS: - return - - with open(os.path.join(POETRY_HOME, "env"), "w") as f: - f.write(u(self.get_export_string())) - - def update_path(self): - """ - Tries to update the $PATH automatically. - """ - if not self._modify_path: - return - - if "fish" in SHELL: - return self.add_to_fish_path() - - if WINDOWS: - return self.add_to_windows_path() - - # Updating any profile we can on UNIX systems - export_string = self.get_export_string() - - addition = "\n{}\n".format(export_string) - - profiles = self.get_unix_profiles() - for profile in profiles: - if not os.path.exists(profile): - continue - - with open(profile, "r") as f: - content = f.read() - - if addition not in content: - with open(profile, "a") as f: - f.write(u(addition)) - - def add_to_fish_path(self): - """ - Ensure POETRY_BIN directory is on Fish shell $PATH - """ - current_path = os.environ.get("PATH", None) - if current_path is None: - print( - colorize( - "warning", - "\nUnable to get the PATH value. It will not be updated" - " automatically.", - ) - ) - self._modify_path = False - - return - - if POETRY_BIN not in current_path: - fish_user_paths = subprocess.check_output( - ["fish", "-c", "echo $fish_user_paths"] - ).decode("utf-8") - if POETRY_BIN not in fish_user_paths: - cmd = "set -U fish_user_paths {} $fish_user_paths".format(POETRY_BIN) - set_fish_user_path = ["fish", "-c", "{}".format(cmd)] - subprocess.check_output(set_fish_user_path) - else: - print( - colorize( - "warning", - "\nPATH already contains {} and thus was not modified.".format( - POETRY_BIN - ), - ) - ) - - def add_to_windows_path(self): - try: - old_path = self.get_windows_path_var() - except WindowsError: - old_path = None - - if old_path is None: - print( - colorize( - "warning", - "Unable to get the PATH value. It will not be updated" - " automatically", - ) - ) - self._modify_path = False - - return - - new_path = POETRY_BIN - if POETRY_BIN in old_path: - old_path = old_path.replace(POETRY_BIN + ";", "") - - if old_path: - new_path += ";" - new_path += old_path - - self.set_windows_path_var(new_path) - - def get_windows_path_var(self): - 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 set_windows_path_var(self, value): - import ctypes - - with winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) as root: - with winreg.OpenKey(root, "Environment", 0, winreg.KEY_ALL_ACCESS) as key: - winreg.SetValueEx(key, "PATH", 0, winreg.REG_EXPAND_SZ, value) - - # Tell other processes to update their environment - HWND_BROADCAST = 0xFFFF - WM_SETTINGCHANGE = 0x1A - - SMTO_ABORTIFHUNG = 0x0002 - - result = ctypes.c_long() - SendMessageTimeoutW = ctypes.windll.user32.SendMessageTimeoutW - SendMessageTimeoutW( - HWND_BROADCAST, - WM_SETTINGCHANGE, - 0, - "Environment", - SMTO_ABORTIFHUNG, - 5000, - ctypes.byref(result), - ) - - def remove_from_path(self): - if "fish" in SHELL: - return self.remove_from_fish_path() - - elif WINDOWS: - return self.remove_from_windows_path() - - return self.remove_from_unix_path() - - def remove_from_fish_path(self): - fish_user_paths = subprocess.check_output( - ["fish", "-c", "echo $fish_user_paths"] - ).decode("utf-8") - if POETRY_BIN in fish_user_paths: - cmd = "set -U fish_user_paths (string match -v {} $fish_user_paths)".format( - POETRY_BIN - ) - set_fish_user_path = ["fish", "-c", "{}".format(cmd)] - subprocess.check_output(set_fish_user_path) - - def remove_from_windows_path(self): - path = self.get_windows_path_var() - - poetry_path = POETRY_BIN - if poetry_path in path: - path = path.replace(POETRY_BIN + ";", "") - - if poetry_path in path: - path = path.replace(POETRY_BIN, "") - - self.set_windows_path_var(path) - - def remove_from_unix_path(self): - # Updating any profile we can on UNIX systems - export_string = self.get_export_string() - - addition = "{}\n".format(export_string) - - profiles = self.get_unix_profiles() - for profile in profiles: - if not os.path.exists(profile): - continue - - with open(profile, "r") as f: - content = f.readlines() - - if addition not in content: - continue - - new_content = [] - for line in content: - if line == addition: - if new_content and not new_content[-1].strip(): - new_content = new_content[:-1] - - continue - - new_content.append(line) - - with open(profile, "w") as f: - f.writelines(new_content) - - def get_export_string(self): - path = POETRY_BIN.replace(os.getenv("HOME", ""), "$HOME") - export_string = 'export PATH="{}:$PATH"'.format(path) - - return export_string - - def get_unix_profiles(self): - profiles = [os.path.join(HOME, ".profile")] - - if "zsh" in SHELL: - zdotdir = os.getenv("ZDOTDIR", HOME) - profiles.append(os.path.join(zdotdir, ".zshrc")) - - bash_profile = os.path.join(HOME, ".bash_profile") - if os.path.exists(bash_profile): - profiles.append(bash_profile) - - return profiles - - def display_pre_message(self): - if WINDOWS: - home = POETRY_BIN.replace(os.getenv("USERPROFILE", ""), "%USERPROFILE%") - else: - home = POETRY_BIN.replace(os.getenv("HOME", ""), "$HOME") - - kwargs = { - "poetry": colorize("info", "Poetry"), - "poetry_home_bin": colorize("comment", home), - } - - if not self._modify_path: - kwargs["platform_msg"] = PRE_MESSAGE_NO_MODIFY_PATH - else: - if "fish" in SHELL: - kwargs["platform_msg"] = PRE_MESSAGE_FISH - elif WINDOWS: - kwargs["platform_msg"] = PRE_MESSAGE_WINDOWS - else: - profiles = [ - colorize("comment", p.replace(os.getenv("HOME", ""), "$HOME")) - for p in self.get_unix_profiles() - ] - kwargs["platform_msg"] = PRE_MESSAGE_UNIX.format( - rcfiles="\n".join(profiles), plural="s" if len(profiles) > 1 else "" - ) - - print(PRE_MESSAGE.format(**kwargs)) - - def display_pre_uninstall_message(self): - home_bin = POETRY_BIN - if WINDOWS: - home_bin = home_bin.replace(os.getenv("USERPROFILE", ""), "%USERPROFILE%") - else: - home_bin = home_bin.replace(os.getenv("HOME", ""), "$HOME") - - kwargs = { - "poetry": colorize("info", "Poetry"), - "poetry_home_bin": colorize("comment", home_bin), - } - - print(PRE_UNINSTALL_MESSAGE.format(**kwargs)) - - def display_post_message(self, version): - print("") - - kwargs = { - "poetry": colorize("info", "Poetry"), - "version": colorize("comment", version), - } - - if WINDOWS: - message = POST_MESSAGE_WINDOWS - if not self._modify_path: - message = POST_MESSAGE_WINDOWS_NO_MODIFY_PATH - - poetry_home_bin = POETRY_BIN.replace( - os.getenv("USERPROFILE", ""), "%USERPROFILE%" - ) - elif "fish" in SHELL: - message = POST_MESSAGE_FISH - if not self._modify_path: - message = POST_MESSAGE_FISH_NO_MODIFY_PATH - - poetry_home_bin = POETRY_BIN.replace(os.getenv("HOME", ""), "$HOME") - else: - message = POST_MESSAGE_UNIX - if not self._modify_path: - message = POST_MESSAGE_UNIX_NO_MODIFY_PATH - - poetry_home_bin = POETRY_BIN.replace(os.getenv("HOME", ""), "$HOME") - kwargs["poetry_home_env"] = colorize( - "comment", POETRY_ENV.replace(os.getenv("HOME", ""), "$HOME") - ) - - kwargs["poetry_home_bin"] = colorize("comment", poetry_home_bin) - - print(message.format(**kwargs)) - - def call(self, *args): - return subprocess.check_output(args, stderr=subprocess.STDOUT) - - 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( - "--no-modify-path", - help="do not modify $PATH", - dest="no_modify_path", - 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( - "--file", - dest="file", - action="store", - help=( - "Install from a local file instead of fetching the latest version " - "of Poetry available online." - ), - ) - - args = parser.parse_args() - - base_url = Installer.BASE_URL - - if args.file is None: - try: - urlopen(Installer.REPOSITORY_URL) - except HTTPError as e: - if e.code == 404: - base_url = Installer.FALLBACK_BASE_URL - else: - raise - - installer = Installer( - version=args.version or os.getenv("POETRY_VERSION"), - preview=args.preview or string_to_bool(os.getenv("POETRY_PREVIEW", "0")), - force=args.force, - modify_path=not args.no_modify_path, - accept_all=args.accept_all - or string_to_bool(os.getenv("POETRY_ACCEPT", "0")) - or not is_interactive(), - file=args.file, - base_url=base_url, - ) - - 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/install-poetry.py b/install-poetry.py index a5e56fd1243..2fbabbc16c3 100644 --- a/install-poetry.py +++ b/install-poetry.py @@ -493,9 +493,11 @@ def _is_self_upgrade_supported(x): self._write( colorize( "warning", - f"You are installing {version}. When using the current installer," - " this version does not support updating using the 'self update'" - " command. Please use 1.1.7 or later.", + ( + f"You are installing {version}. When using the current" + " installer, this version does not support updating using the" + " 'self update' command. Please use 1.1.7 or later." + ), ) ) if not self._accept_all: @@ -900,9 +902,11 @@ def main(): sys.stdout.write( colorize( "warning", - "The canonical source for Poetry's installation script is now" - " https://install.python-poetry.org. Please update your usage to reflect" - " this.\n", + ( + "The canonical source for Poetry's installation script is now" + " https://install.python-poetry.org. Please update your usage to" + " reflect this.\n" + ), ) ) sys.exit(main()) diff --git a/poetry.lock b/poetry.lock index 3a4cb997d61..36bd49a456a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,32 +1,72 @@ +# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. + [[package]] name = "attrs" -version = "22.1.0" +version = "22.2.0" description = "Classes Without Boilerplate" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" +files = [ + {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, + {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, +] [package.extras] -dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] -docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] -tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] +tests = ["attrs[tests-no-zope]", "zope.interface"] +tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] [[package]] -name = "backports.cached-property" +name = "backports-cached-property" version = "1.0.2" description = "cached_property() - computed once per instance, cached as attribute" category = "main" optional = false python-versions = ">=3.6.0" +files = [ + {file = "backports.cached-property-1.0.2.tar.gz", hash = "sha256:9306f9eed6ec55fd156ace6bc1094e2c86fae5fb2bf07b6a9c00745c656e75dd"}, + {file = "backports.cached_property-1.0.2-py3-none-any.whl", hash = "sha256:baeb28e1cd619a3c9ab8941431fe34e8490861fb998c6c4590693d50171db0cc"}, +] + +[[package]] +name = "build" +version = "0.10.0" +description = "A simple, correct Python build frontend" +category = "main" +optional = false +python-versions = ">= 3.7" +files = [ + {file = "build-0.10.0-py3-none-any.whl", hash = "sha256:af266720050a66c893a6096a2f410989eeac74ff9a68ba194b3f6473e8e26171"}, + {file = "build-0.10.0.tar.gz", hash = "sha256:d5b71264afdb5951d6704482aac78de887c80691c52b88a9ad195983ca2c9269"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "os_name == \"nt\""} +importlib-metadata = {version = ">=0.22", markers = "python_version < \"3.8\""} +packaging = ">=19.0" +pyproject_hooks = "*" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["furo (>=2021.08.31)", "sphinx (>=4.0,<5.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)"] +test = ["filelock (>=3)", "pytest (>=6.2.4)", "pytest-cov (>=2.12)", "pytest-mock (>=2)", "pytest-rerunfailures (>=9.1)", "pytest-xdist (>=1.34)", "setuptools (>=42.0.0)", "setuptools (>=56.0.0)", "toml (>=0.10.0)", "wheel (>=0.36.0)"] +typing = ["importlib-metadata (>=5.1)", "mypy (==0.991)", "tomli", "typing-extensions (>=3.7.4.3)"] +virtualenv = ["virtualenv (>=20.0.35)"] [[package]] -name = "CacheControl" +name = "cachecontrol" version = "0.12.11" description = "httplib2 caching for requests" category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "CacheControl-0.12.11-py2.py3-none-any.whl", hash = "sha256:2c75d6a8938cb1933c75c50184549ad42728a27e9f6b92fd677c3151aa72555b"}, + {file = "CacheControl-0.12.11.tar.gz", hash = "sha256:a5b9fcc986b184db101aa280b42ecdcdfc524892596f606858e0b7a8b4d9e144"}, +] [package.dependencies] lockfile = {version = ">=0.9", optional = true, markers = "extra == \"filecache\""} @@ -44,6 +84,10 @@ description = "Cachy provides a simple yet effective caching library." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "cachy-0.3.0-py2.py3-none-any.whl", hash = "sha256:338ca09c8860e76b275aff52374330efedc4d5a5e45dc1c5b539c1ead0786fe7"}, + {file = "cachy-0.3.0.tar.gz", hash = "sha256:186581f4ceb42a0bbe040c407da73c14092379b1e4c0e327fdb72ae4a9b269b1"}, +] [package.extras] memcached = ["python-memcached (>=1.59,<2.0)"] @@ -52,11 +96,15 @@ redis = ["redis (>=3.3.6,<4.0.0)"] [[package]] name = "certifi" -version = "2022.9.24" +version = "2022.12.7" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] [[package]] name = "cffi" @@ -65,6 +113,72 @@ description = "Foreign Function Interface for Python calling C code." category = "main" optional = false python-versions = "*" +files = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] [package.dependencies] pycparser = "*" @@ -76,45 +190,197 @@ description = "Validate configuration and produce human readable error messages. category = "dev" optional = false python-versions = ">=3.6.1" +files = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] [[package]] name = "charset-normalizer" -version = "2.1.1" +version = "3.0.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false -python-versions = ">=3.6.0" - -[package.extras] -unicode_backport = ["unicodedata2"] +python-versions = "*" +files = [ + {file = "charset-normalizer-3.0.1.tar.gz", hash = "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-win32.whl", hash = "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-win32.whl", hash = "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-win32.whl", hash = "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-win32.whl", hash = "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-win32.whl", hash = "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59"}, + {file = "charset_normalizer-3.0.1-py3-none-any.whl", hash = "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24"}, +] [[package]] name = "cleo" -version = "1.0.0a5" +version = "2.0.1" description = "Cleo allows you to create beautiful and testable command-line interfaces." category = "main" optional = false python-versions = ">=3.7,<4.0" +files = [ + {file = "cleo-2.0.1-py3-none-any.whl", hash = "sha256:6eb133670a3ed1f3b052d53789017b6e50fca66d1287e6e6696285f4cb8ea448"}, + {file = "cleo-2.0.1.tar.gz", hash = "sha256:eb4b2e1f3063c11085cebe489a6e9124163c226575a3c3be69b2e51af4a15ec5"}, +] [package.dependencies] -crashtest = ">=0.3.1,<0.4.0" -pylev = ">=1.3.0,<2.0.0" +crashtest = ">=0.4.1,<0.5.0" +rapidfuzz = ">=2.2.0,<3.0.0" [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." -category = "dev" +category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] [[package]] name = "coverage" -version = "6.5.0" +version = "7.1.0" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "coverage-7.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3b946bbcd5a8231383450b195cfb58cb01cbe7f8949f5758566b881df4b33baf"}, + {file = "coverage-7.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec8e767f13be637d056f7e07e61d089e555f719b387a7070154ad80a0ff31801"}, + {file = "coverage-7.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a5a5879a939cb84959d86869132b00176197ca561c664fc21478c1eee60d75"}, + {file = "coverage-7.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b643cb30821e7570c0aaf54feaf0bfb630b79059f85741843e9dc23f33aaca2c"}, + {file = "coverage-7.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32df215215f3af2c1617a55dbdfb403b772d463d54d219985ac7cd3bf124cada"}, + {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:33d1ae9d4079e05ac4cc1ef9e20c648f5afabf1a92adfaf2ccf509c50b85717f"}, + {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:29571503c37f2ef2138a306d23e7270687c0efb9cab4bd8038d609b5c2393a3a"}, + {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:63ffd21aa133ff48c4dff7adcc46b7ec8b565491bfc371212122dd999812ea1c"}, + {file = "coverage-7.1.0-cp310-cp310-win32.whl", hash = "sha256:4b14d5e09c656de5038a3f9bfe5228f53439282abcab87317c9f7f1acb280352"}, + {file = "coverage-7.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:8361be1c2c073919500b6601220a6f2f98ea0b6d2fec5014c1d9cfa23dd07038"}, + {file = "coverage-7.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:da9b41d4539eefd408c46725fb76ecba3a50a3367cafb7dea5f250d0653c1040"}, + {file = "coverage-7.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5b15ed7644ae4bee0ecf74fee95808dcc34ba6ace87e8dfbf5cb0dc20eab45a"}, + {file = "coverage-7.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d12d076582507ea460ea2a89a8c85cb558f83406c8a41dd641d7be9a32e1274f"}, + {file = "coverage-7.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2617759031dae1bf183c16cef8fcfb3de7617f394c813fa5e8e46e9b82d4222"}, + {file = "coverage-7.1.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4e4881fa9e9667afcc742f0c244d9364d197490fbc91d12ac3b5de0bf2df146"}, + {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9d58885215094ab4a86a6aef044e42994a2bd76a446dc59b352622655ba6621b"}, + {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ffeeb38ee4a80a30a6877c5c4c359e5498eec095878f1581453202bfacc8fbc2"}, + {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3baf5f126f30781b5e93dbefcc8271cb2491647f8283f20ac54d12161dff080e"}, + {file = "coverage-7.1.0-cp311-cp311-win32.whl", hash = "sha256:ded59300d6330be27bc6cf0b74b89ada58069ced87c48eaf9344e5e84b0072f7"}, + {file = "coverage-7.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:6a43c7823cd7427b4ed763aa7fb63901ca8288591323b58c9cd6ec31ad910f3c"}, + {file = "coverage-7.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a726d742816cb3a8973c8c9a97539c734b3a309345236cd533c4883dda05b8d"}, + {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc7c85a150501286f8b56bd8ed3aa4093f4b88fb68c0843d21ff9656f0009d6a"}, + {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5b4198d85a3755d27e64c52f8c95d6333119e49fd001ae5798dac872c95e0f8"}, + {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb726cb861c3117a553f940372a495fe1078249ff5f8a5478c0576c7be12050"}, + {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:51b236e764840a6df0661b67e50697aaa0e7d4124ca95e5058fa3d7cbc240b7c"}, + {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7ee5c9bb51695f80878faaa5598040dd6c9e172ddcf490382e8aedb8ec3fec8d"}, + {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c31b75ae466c053a98bf26843563b3b3517b8f37da4d47b1c582fdc703112bc3"}, + {file = "coverage-7.1.0-cp37-cp37m-win32.whl", hash = "sha256:3b155caf3760408d1cb903b21e6a97ad4e2bdad43cbc265e3ce0afb8e0057e73"}, + {file = "coverage-7.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2a60d6513781e87047c3e630b33b4d1e89f39836dac6e069ffee28c4786715f5"}, + {file = "coverage-7.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f2cba5c6db29ce991029b5e4ac51eb36774458f0a3b8d3137241b32d1bb91f06"}, + {file = "coverage-7.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beeb129cacea34490ffd4d6153af70509aa3cda20fdda2ea1a2be870dfec8d52"}, + {file = "coverage-7.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c45948f613d5d18c9ec5eaa203ce06a653334cf1bd47c783a12d0dd4fd9c851"}, + {file = "coverage-7.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef382417db92ba23dfb5864a3fc9be27ea4894e86620d342a116b243ade5d35d"}, + {file = "coverage-7.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c7c0d0827e853315c9bbd43c1162c006dd808dbbe297db7ae66cd17b07830f0"}, + {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e5cdbb5cafcedea04924568d990e20ce7f1945a1dd54b560f879ee2d57226912"}, + {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9817733f0d3ea91bea80de0f79ef971ae94f81ca52f9b66500c6a2fea8e4b4f8"}, + {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:218fe982371ac7387304153ecd51205f14e9d731b34fb0568181abaf7b443ba0"}, + {file = "coverage-7.1.0-cp38-cp38-win32.whl", hash = "sha256:04481245ef966fbd24ae9b9e537ce899ae584d521dfbe78f89cad003c38ca2ab"}, + {file = "coverage-7.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8ae125d1134bf236acba8b83e74c603d1b30e207266121e76484562bc816344c"}, + {file = "coverage-7.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2bf1d5f2084c3932b56b962a683074a3692bce7cabd3aa023c987a2a8e7612f6"}, + {file = "coverage-7.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:98b85dd86514d889a2e3dd22ab3c18c9d0019e696478391d86708b805f4ea0fa"}, + {file = "coverage-7.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38da2db80cc505a611938d8624801158e409928b136c8916cd2e203970dde4dc"}, + {file = "coverage-7.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3164d31078fa9efe406e198aecd2a02d32a62fecbdef74f76dad6a46c7e48311"}, + {file = "coverage-7.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db61a79c07331e88b9a9974815c075fbd812bc9dbc4dc44b366b5368a2936063"}, + {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ccb092c9ede70b2517a57382a601619d20981f56f440eae7e4d7eaafd1d1d09"}, + {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:33ff26d0f6cc3ca8de13d14fde1ff8efe1456b53e3f0273e63cc8b3c84a063d8"}, + {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d47dd659a4ee952e90dc56c97d78132573dc5c7b09d61b416a9deef4ebe01a0c"}, + {file = "coverage-7.1.0-cp39-cp39-win32.whl", hash = "sha256:d248cd4a92065a4d4543b8331660121b31c4148dd00a691bfb7a5cdc7483cfa4"}, + {file = "coverage-7.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7ed681b0f8e8bcbbffa58ba26fcf5dbc8f79e7997595bf071ed5430d8c08d6f3"}, + {file = "coverage-7.1.0-pp37.pp38.pp39-none-any.whl", hash = "sha256:755e89e32376c850f826c425ece2c35a4fc266c081490eb0a841e7c1cb0d3bda"}, + {file = "coverage-7.1.0.tar.gz", hash = "sha256:10188fe543560ec4874f974b5305cd1a8bdcfa885ee00ea3a03733464c4ca265"}, +] [package.dependencies] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} @@ -124,44 +390,98 @@ toml = ["tomli"] [[package]] name = "crashtest" -version = "0.3.1" +version = "0.4.1" description = "Manage Python errors with ease" category = "main" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.7,<4.0" +files = [ + {file = "crashtest-0.4.1-py3-none-any.whl", hash = "sha256:8d23eac5fa660409f57472e3851dab7ac18aba459a8d19cbbba86d3d5aecd2a5"}, + {file = "crashtest-0.4.1.tar.gz", hash = "sha256:80d7b1f316ebfbd429f648076d6275c877ba30ba48979de4191714a75266f0ce"}, +] [[package]] name = "cryptography" -version = "38.0.1" +version = "39.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "cryptography-39.0.1-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:6687ef6d0a6497e2b58e7c5b852b53f62142cfa7cd1555795758934da363a965"}, + {file = "cryptography-39.0.1-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:706843b48f9a3f9b9911979761c91541e3d90db1ca905fd63fee540a217698bc"}, + {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:5d2d8b87a490bfcd407ed9d49093793d0f75198a35e6eb1a923ce1ee86c62b41"}, + {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e17b26de248c33f3acffb922748151d71827d6021d98c70e6c1a25ddd78505"}, + {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e124352fd3db36a9d4a21c1aa27fd5d051e621845cb87fb851c08f4f75ce8be6"}, + {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:5aa67414fcdfa22cf052e640cb5ddc461924a045cacf325cd164e65312d99502"}, + {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:35f7c7d015d474f4011e859e93e789c87d21f6f4880ebdc29896a60403328f1f"}, + {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f24077a3b5298a5a06a8e0536e3ea9ec60e4c7ac486755e5fb6e6ea9b3500106"}, + {file = "cryptography-39.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f0c64d1bd842ca2633e74a1a28033d139368ad959872533b1bab8c80e8240a0c"}, + {file = "cryptography-39.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0f8da300b5c8af9f98111ffd512910bc792b4c77392a9523624680f7956a99d4"}, + {file = "cryptography-39.0.1-cp36-abi3-win32.whl", hash = "sha256:fe913f20024eb2cb2f323e42a64bdf2911bb9738a15dba7d3cce48151034e3a8"}, + {file = "cryptography-39.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ced4e447ae29ca194449a3f1ce132ded8fcab06971ef5f618605aacaa612beac"}, + {file = "cryptography-39.0.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:807ce09d4434881ca3a7594733669bd834f5b2c6d5c7e36f8c00f691887042ad"}, + {file = "cryptography-39.0.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5caeb8188c24888c90b5108a441c106f7faa4c4c075a2bcae438c6e8ca73cef"}, + {file = "cryptography-39.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4789d1e3e257965e960232345002262ede4d094d1a19f4d3b52e48d4d8f3b885"}, + {file = "cryptography-39.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:96f1157a7c08b5b189b16b47bc9db2332269d6680a196341bf30046330d15388"}, + {file = "cryptography-39.0.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e422abdec8b5fa8462aa016786680720d78bdce7a30c652b7fadf83a4ba35336"}, + {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:b0afd054cd42f3d213bf82c629efb1ee5f22eba35bf0eec88ea9ea7304f511a2"}, + {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:6f8ba7f0328b79f08bdacc3e4e66fb4d7aab0c3584e0bd41328dce5262e26b2e"}, + {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ef8b72fa70b348724ff1218267e7f7375b8de4e8194d1636ee60510aae104cd0"}, + {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:aec5a6c9864be7df2240c382740fcf3b96928c46604eaa7f3091f58b878c0bb6"}, + {file = "cryptography-39.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdd188c8a6ef8769f148f88f859884507b954cc64db6b52f66ef199bb9ad660a"}, + {file = "cryptography-39.0.1.tar.gz", hash = "sha256:d1f6198ee6d9148405e49887803907fe8962a23e6c6f83ea7d98f1c0de375695"}, +] [package.dependencies] cffi = ">=1.12" [package.extras] -docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] -pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +pep8test = ["black", "check-manifest", "mypy", "ruff", "types-pytz", "types-requests"] sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] +test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist", "pytz"] +test-randomorder = ["pytest-randomly"] +tox = ["tox"] [[package]] name = "deepdiff" -version = "5.8.1" -description = "Deep Difference and Search of any Python object/data." +version = "6.2.2" +description = "Deep Difference and Search of any Python object/data. Recreate objects by adding adding deltas to each other." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +files = [ + {file = "deepdiff-6.2.2-py3-none-any.whl", hash = "sha256:dea62316741f86c1d8e946f47c4c21386788457c898a495a5e6b0ccdcd76d9b6"}, + {file = "deepdiff-6.2.2.tar.gz", hash = "sha256:d04d997a68bf8bea01f8a97395877314ef5c2131d8f57bba2295f3adda725282"}, +] + +[package.dependencies] +ordered-set = ">=4.0.2,<4.2.0" + +[package.extras] +cli = ["click (==8.1.3)", "pyyaml (==6.0)"] + +[[package]] +name = "deepdiff" +version = "6.2.3" +description = "Deep Difference and Search of any Python object/data. Recreate objects by adding adding deltas to each other." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "deepdiff-6.2.3-py3-none-any.whl", hash = "sha256:d83b06e043447d6770860a635abecb46e849b0494c43ced2ecafda7628c7ce72"}, + {file = "deepdiff-6.2.3.tar.gz", hash = "sha256:a02aaa8171351eba675cff5f795ec7a90987f86ad5449553308d4e18df57dc3d"}, +] [package.dependencies] -ordered-set = ">=4.1.0,<4.2.0" +ordered-set = ">=4.0.2,<4.2.0" +orjson = "*" [package.extras] -cli = ["clevercsv (==0.7.1)", "click (==8.0.3)", "pyyaml (==5.4.1)", "toml (==0.10.2)"] +cli = ["click (==8.1.3)", "pyyaml (==6.0)"] [[package]] name = "distlib" @@ -170,16 +490,79 @@ description = "Distribution utilities" category = "main" optional = false python-versions = "*" +files = [ + {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, + {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, +] [[package]] name = "dulwich" -version = "0.20.46" +version = "0.21.3" description = "Python Git Library" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +files = [ + {file = "dulwich-0.21.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25376efc6ea2ee9daa868a120d4f9c905dcb7774f68931be921fba41a657f58a"}, + {file = "dulwich-0.21.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9213a114dd19cfca19715088f12f143e918c5e1b4e26f7acf1a823d7da9e1413"}, + {file = "dulwich-0.21.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:baf5b3b901272837bee2311ecbd28fdbe960d288a070dc72bdfdf48cfcbb8090"}, + {file = "dulwich-0.21.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7b8cb38a93de87b980f882f0dcd19f2e3ad43216f34e06916315cb3a03e6964"}, + {file = "dulwich-0.21.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4f8ff776ca38ce272d9c164a7f77db8a54a8cad6d9468124317adf8732be07d"}, + {file = "dulwich-0.21.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:03ed9448f2944166e28aa8d3f4c8feeceb5c6880e9ffe5ab274869d45abd9589"}, + {file = "dulwich-0.21.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6618e35268d116bffddd6dbec360a40c54b3164f8af0513d95d8698f36e2eacc"}, + {file = "dulwich-0.21.3-cp310-cp310-win32.whl", hash = "sha256:d7ad871d044a96f794170f2434e832c6b42804d0b53721377d03f865245cd273"}, + {file = "dulwich-0.21.3-cp310-cp310-win_amd64.whl", hash = "sha256:ba3d42cd83d7f89b9c1b2f76df971e8ab58815f8060da4dc67b9ae9dba1b34cc"}, + {file = "dulwich-0.21.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:092829f27a2c87cdf6b6523216822859ecf01d281ddfae0e58cad1f44adafff6"}, + {file = "dulwich-0.21.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bb54fe45deb55e4caae4ea2c1dba93ee79fb5c377287b14056d4c30fb156920e"}, + {file = "dulwich-0.21.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d0ac29adf468a838884e1507d81e872096238c76fe7da7f3325507e4390b6867"}, + {file = "dulwich-0.21.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8d1837c3d2d8e56aacc13a91ec7540b3baadc1b254fbdf225a2d15b72b654c3"}, + {file = "dulwich-0.21.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cf246530b8d574b33a9614da76881b96c190c0fe78f76ab016c88082c0da051"}, + {file = "dulwich-0.21.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:be0801ae3f9017c6437bcd23a4bf2b2aa88e465f7efeed4b079944d07e3df994"}, + {file = "dulwich-0.21.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5a1137177b62eec949c0f1564eef73920f842af5ebfc260c20d9cd47e8ecd519"}, + {file = "dulwich-0.21.3-cp311-cp311-win32.whl", hash = "sha256:b09b6166876d2cba8f331a548932b09e11c9386db0525c9ca15c399b666746fc"}, + {file = "dulwich-0.21.3-cp311-cp311-win_amd64.whl", hash = "sha256:250ec581682af846cb85844f8032b7642dd278006b1c3abd5e8e718eba0b1b00"}, + {file = "dulwich-0.21.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ad7de37c9ff817bc5d26f89100f87b7f1a5cc25e5eaaa54f11dc66cca9652e4"}, + {file = "dulwich-0.21.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b541bd58426a30753ab12cc024ba29b6699d197d9d0d9f130b9768ab20e0e6a"}, + {file = "dulwich-0.21.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f8d45f5fcdb52c60c902a951f549faad9979314e7e069f4fa3d14eb409b16a0"}, + {file = "dulwich-0.21.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a98989ff1ed20825728495ffb859cd700a120850074184d2e1ec08a0b1ab8ab3"}, + {file = "dulwich-0.21.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:33f73e8f902c6397cc73a727db1f6e75add8ce894bfbb1a15daa2f7a4138a744"}, + {file = "dulwich-0.21.3-cp37-cp37m-win32.whl", hash = "sha256:a2e6270923bf5ec0e9f720d689579a904f401c62193222d000d8cb8e880684e9"}, + {file = "dulwich-0.21.3-cp37-cp37m-win_amd64.whl", hash = "sha256:1799c04bd53ec404ebd2c82c1d66197a31e5f0549c95348bb7d3f57a28c94241"}, + {file = "dulwich-0.21.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c80ade5cdb0ea447e7f43b32abc2f4a628dcdfa64dc8ee5ab4262987e5e0814f"}, + {file = "dulwich-0.21.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:40f8f461eba87ef2e8ce0005ca2c12f1b4fdbbafd3a717b8570060d7cd35ee0c"}, + {file = "dulwich-0.21.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af7a417e19068b1abeb9addd3c045a2d6e40d15365af6aa3cbe2d47305b5bb11"}, + {file = "dulwich-0.21.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:026427b5ef0f1fe138ed22078e49b00175b58b11e5c18e2be00f06ee0782603b"}, + {file = "dulwich-0.21.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae38c6d24d7aff003a241c8f1dd268eb1c6f7625d91e3435836ff5a5eed05ce5"}, + {file = "dulwich-0.21.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:512bb4b04e403a38860f7eb22abeeaefba3c4a9c08bc7beec8885494c5828034"}, + {file = "dulwich-0.21.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3b048f84c94c3284f29bf228f1094ccc48763d76ede5c35632153bd7f697b846"}, + {file = "dulwich-0.21.3-cp38-cp38-win32.whl", hash = "sha256:a275b3a579dfd923d6330f6e5c2886dbdb5da4e004c5abecb107eb347d301412"}, + {file = "dulwich-0.21.3-cp38-cp38-win_amd64.whl", hash = "sha256:208d01a9cda1bae16c92e8c54e806701a16969346aba44b8d6921c6c227277a9"}, + {file = "dulwich-0.21.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:73f9feba3da1ae66f0b521d7c2727db7f5025a83facdc73f4f39abe2b6d4f00d"}, + {file = "dulwich-0.21.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cf7af6458cf6343a2a0632ae2fc5f04821b2ffefc7b8a27f4eacb726ef89c682"}, + {file = "dulwich-0.21.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aaf5c4528e83e3176e7dbb01dcec34fb41c93279a8f8527cf33e5df88bfb910"}, + {file = "dulwich-0.21.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:075c8e9d2694ff16fc6e8a5ec0c771b7c33be12e4ebecc346fd74315d3d84605"}, + {file = "dulwich-0.21.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b686b49adeb7fc45791dfae96ffcffeba1038e8b7603f369d6661f59e479fc"}, + {file = "dulwich-0.21.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:21ee962211839bb6e52d41f363ce9dbb0638d341a1c02263e163d69012f58b25"}, + {file = "dulwich-0.21.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2bf2be68fddfc0adfe43be99ab31f6b0f16b9ef1e40464679ba831ff615ad4a3"}, + {file = "dulwich-0.21.3-cp39-cp39-win32.whl", hash = "sha256:ddb790f2fdc22984fba643866b21d04733c5cf7c3ace2a1e99e0c1c1d2336aab"}, + {file = "dulwich-0.21.3-cp39-cp39-win_amd64.whl", hash = "sha256:c97561c22fc05d0f6ba370d9bd67f86c313c38f31a1793e0ee9acb78ee28e4b8"}, + {file = "dulwich-0.21.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b9fc609a3d4009ee31212f435f5a75720ef24280f6d23edfd53f77b562a79c5b"}, + {file = "dulwich-0.21.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f2cb11fe789b72feeae7cdf6e27375c33ed6915f8ca5ea7ce81b5e234c75a9e"}, + {file = "dulwich-0.21.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c349431f5c8aa99b8744550d0bb4615f63e73450584202ac5db0e5d7da4d82ff"}, + {file = "dulwich-0.21.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:9f08e5cc10143d3da2a2cf735d8b932ef4e4e1d74b0c74ce66c52eab02068be8"}, + {file = "dulwich-0.21.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:67dbf4dd7586b2d437f539d5dc930ebceaf74a4150720644d6ea7e5ffc1cb2ff"}, + {file = "dulwich-0.21.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89af4ee347f361338bad5c27b023f9d19e7aed17aa75cb519f28e6cf1658a0ba"}, + {file = "dulwich-0.21.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cd83f84e58aa59fb9d85cf15e74be83a5be876ac5876d5030f60fcce7ab36f1"}, + {file = "dulwich-0.21.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8ba1fe3fb415fd34cae5ca090fb82030b6e8423d6eb2c4c9c4fbf50b15c7664c"}, + {file = "dulwich-0.21.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:058aaba18aefe18fcd84b216fd34d032ad453967dcf3dee263278951cd43e2d4"}, + {file = "dulwich-0.21.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08ee426b609dab552839b5c7394ae9af2112c164bb727b7f85a69980eced9251"}, + {file = "dulwich-0.21.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf1f6edc968619a4355481c29d5571726723bc12924e2b25bd3348919f9bc992"}, + {file = "dulwich-0.21.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c69c95d5242171d07396761f759a8a4d566e9a01bf99612f9b9e309e70a80fc"}, + {file = "dulwich-0.21.3.tar.gz", hash = "sha256:7ca3b453d767eb83b3ec58f0cfcdc934875a341cdfdb0dc55c1431c96608cf83"}, +] [package.dependencies] +typing-extensions = {version = "*", markers = "python_version <= \"3.7\""} urllib3 = ">=1.25" [package.extras] @@ -188,6 +571,21 @@ https = ["urllib3 (>=1.24.1)"] paramiko = ["paramiko"] pgp = ["gpg"] +[[package]] +name = "exceptiongroup" +version = "1.1.0" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, + {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, +] + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "execnet" version = "1.9.0" @@ -195,29 +593,29 @@ description = "execnet: rapid multi-Python deployment" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, + {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, +] [package.extras] testing = ["pre-commit"] [[package]] name = "filelock" -version = "3.8.0" +version = "3.9.0" description = "A platform independent file lock." category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"}, + {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"}, +] [package.extras] -docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] -testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] - -[[package]] -name = "flatdict" -version = "4.0.1" -description = "Python module for interacting with nested dicts as a single level dict with delimited keys." -category = "dev" -optional = false -python-versions = "*" +docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] [[package]] name = "html5lib" @@ -226,6 +624,10 @@ description = "HTML parser based on the WHATWG HTML specification" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"}, + {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"}, +] [package.dependencies] six = ">=1.9" @@ -244,14 +646,21 @@ description = "HTTP client mock for Python" category = "dev" optional = false python-versions = ">=3" +files = [ + {file = "httpretty-1.1.4.tar.gz", hash = "sha256:20de0e5dd5a18292d36d928cc3d6e52f8b2ac73daec40d41eb62dee154933b68"}, +] [[package]] name = "identify" -version = "2.5.6" +version = "2.5.18" description = "File identification library for Python" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "identify-2.5.18-py2.py3-none-any.whl", hash = "sha256:93aac7ecf2f6abf879b8f29a8002d3c6de7086b8c28d88e1ad15045a15ab63f9"}, + {file = "identify-2.5.18.tar.gz", hash = "sha256:89e144fa560cc4cffb6ef2ab5e9fb18ed9f9b3cb054384bab4b95c12f6c309fe"}, +] [package.extras] license = ["ukkonen"] @@ -263,54 +672,86 @@ description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] [[package]] name = "importlib-metadata" -version = "4.13.0" +version = "6.0.0" description = "Read metadata from Python packages" category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"}, + {file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"}, +] [package.dependencies] typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] [[package]] name = "importlib-resources" -version = "5.10.0" +version = "5.12.0" description = "Read resources from Python packages" category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"}, + {file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"}, +] [package.dependencies] zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [[package]] name = "iniconfig" -version = "1.1.1" -description = "iniconfig: brain-dead simple config-ini parsing" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "installer" +version = "0.7.0" +description = "A library for installing Python wheels." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "installer-0.7.0-py3-none-any.whl", hash = "sha256:05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53"}, + {file = "installer-0.7.0.tar.gz", hash = "sha256:a26d3e3116289bb08216e0d0f7d925fcef0b0194eedfa0c944bcaaa106c4b631"}, +] [[package]] -name = "jaraco.classes" +name = "jaraco-classes" version = "3.2.3" description = "Utility functions for Python class constructs" category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "jaraco.classes-3.2.3-py3-none-any.whl", hash = "sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158"}, + {file = "jaraco.classes-3.2.3.tar.gz", hash = "sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a"}, +] [package.dependencies] more-itertools = "*" @@ -326,6 +767,10 @@ description = "Low-level, pure Python DBus protocol wrapper." category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, + {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, +] [package.extras] test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] @@ -333,11 +778,15 @@ trio = ["async_generator", "trio"] [[package]] name = "jsonschema" -version = "4.16.0" +version = "4.17.3" description = "An implementation of JSON Schema validation for Python" category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "jsonschema-4.17.3-py3-none-any.whl", hash = "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6"}, + {file = "jsonschema-4.17.3.tar.gz", hash = "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d"}, +] [package.dependencies] attrs = ">=17.4.0" @@ -353,21 +802,27 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- [[package]] name = "keyring" -version = "23.9.3" +version = "23.13.1" description = "Store and access your passwords safely." category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "keyring-23.13.1-py3-none-any.whl", hash = "sha256:771ed2a91909389ed6148631de678f82ddc73737d85a927f382a8a1b157898cd"}, + {file = "keyring-23.13.1.tar.gz", hash = "sha256:ba2e15a9b35e21908d0aaf4e0a47acc52d6ae33444df0da2b49d41a46ef6d678"}, +] [package.dependencies] -importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} +importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} +importlib-resources = {version = "*", markers = "python_version < \"3.9\""} "jaraco.classes" = "*" 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\""} +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} [package.extras] -docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] +completion = ["shtab"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [[package]] @@ -377,14 +832,22 @@ description = "Platform-independent file locking module" category = "main" optional = false python-versions = "*" +files = [ + {file = "lockfile-0.12.2-py2.py3-none-any.whl", hash = "sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa"}, + {file = "lockfile-0.12.2.tar.gz", hash = "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799"}, +] [[package]] name = "more-itertools" -version = "8.14.0" +version = "9.0.0" description = "More routines for operating on iterables, beyond itertools" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" +files = [ + {file = "more-itertools-9.0.0.tar.gz", hash = "sha256:5a6257e40878ef0520b1803990e3e22303a41b5714006c32a3fd8304b26ea1ab"}, + {file = "more_itertools-9.0.0-py3-none-any.whl", hash = "sha256:250e83d7e81d0c87ca6bd942e6aeab8cc9daa6096d12c5308f3f92fa5e5c1f41"}, +] [[package]] name = "msgpack" @@ -393,14 +856,96 @@ description = "MessagePack serializer" category = "main" optional = false python-versions = "*" +files = [ + {file = "msgpack-1.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4ab251d229d10498e9a2f3b1e68ef64cb393394ec477e3370c457f9430ce9250"}, + {file = "msgpack-1.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:112b0f93202d7c0fef0b7810d465fde23c746a2d482e1e2de2aafd2ce1492c88"}, + {file = "msgpack-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:002b5c72b6cd9b4bafd790f364b8480e859b4712e91f43014fe01e4f957b8467"}, + {file = "msgpack-1.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35bc0faa494b0f1d851fd29129b2575b2e26d41d177caacd4206d81502d4c6a6"}, + {file = "msgpack-1.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4733359808c56d5d7756628736061c432ded018e7a1dff2d35a02439043321aa"}, + {file = "msgpack-1.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb514ad14edf07a1dbe63761fd30f89ae79b42625731e1ccf5e1f1092950eaa6"}, + {file = "msgpack-1.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c23080fdeec4716aede32b4e0ef7e213c7b1093eede9ee010949f2a418ced6ba"}, + {file = "msgpack-1.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:49565b0e3d7896d9ea71d9095df15b7f75a035c49be733051c34762ca95bbf7e"}, + {file = "msgpack-1.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:aca0f1644d6b5a73eb3e74d4d64d5d8c6c3d577e753a04c9e9c87d07692c58db"}, + {file = "msgpack-1.0.4-cp310-cp310-win32.whl", hash = "sha256:0dfe3947db5fb9ce52aaea6ca28112a170db9eae75adf9339a1aec434dc954ef"}, + {file = "msgpack-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dea20515f660aa6b7e964433b1808d098dcfcabbebeaaad240d11f909298075"}, + {file = "msgpack-1.0.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e83f80a7fec1a62cf4e6c9a660e39c7f878f603737a0cdac8c13131d11d97f52"}, + {file = "msgpack-1.0.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c11a48cf5e59026ad7cb0dc29e29a01b5a66a3e333dc11c04f7e991fc5510a9"}, + {file = "msgpack-1.0.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1276e8f34e139aeff1c77a3cefb295598b504ac5314d32c8c3d54d24fadb94c9"}, + {file = "msgpack-1.0.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c9566f2c39ccced0a38d37c26cc3570983b97833c365a6044edef3574a00c08"}, + {file = "msgpack-1.0.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:fcb8a47f43acc113e24e910399376f7277cf8508b27e5b88499f053de6b115a8"}, + {file = "msgpack-1.0.4-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:76ee788122de3a68a02ed6f3a16bbcd97bc7c2e39bd4d94be2f1821e7c4a64e6"}, + {file = "msgpack-1.0.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0a68d3ac0104e2d3510de90a1091720157c319ceeb90d74f7b5295a6bee51bae"}, + {file = "msgpack-1.0.4-cp36-cp36m-win32.whl", hash = "sha256:85f279d88d8e833ec015650fd15ae5eddce0791e1e8a59165318f371158efec6"}, + {file = "msgpack-1.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:c1683841cd4fa45ac427c18854c3ec3cd9b681694caf5bff04edb9387602d661"}, + {file = "msgpack-1.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a75dfb03f8b06f4ab093dafe3ddcc2d633259e6c3f74bb1b01996f5d8aa5868c"}, + {file = "msgpack-1.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9667bdfdf523c40d2511f0e98a6c9d3603be6b371ae9a238b7ef2dc4e7a427b0"}, + {file = "msgpack-1.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11184bc7e56fd74c00ead4f9cc9a3091d62ecb96e97653add7a879a14b003227"}, + {file = "msgpack-1.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac5bd7901487c4a1dd51a8c58f2632b15d838d07ceedaa5e4c080f7190925bff"}, + {file = "msgpack-1.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1e91d641d2bfe91ba4c52039adc5bccf27c335356055825c7f88742c8bb900dd"}, + {file = "msgpack-1.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2a2df1b55a78eb5f5b7d2a4bb221cd8363913830145fad05374a80bf0877cb1e"}, + {file = "msgpack-1.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:545e3cf0cf74f3e48b470f68ed19551ae6f9722814ea969305794645da091236"}, + {file = "msgpack-1.0.4-cp37-cp37m-win32.whl", hash = "sha256:2cc5ca2712ac0003bcb625c96368fd08a0f86bbc1a5578802512d87bc592fe44"}, + {file = "msgpack-1.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:eba96145051ccec0ec86611fe9cf693ce55f2a3ce89c06ed307de0e085730ec1"}, + {file = "msgpack-1.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7760f85956c415578c17edb39eed99f9181a48375b0d4a94076d84148cf67b2d"}, + {file = "msgpack-1.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:449e57cc1ff18d3b444eb554e44613cffcccb32805d16726a5494038c3b93dab"}, + {file = "msgpack-1.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d603de2b8d2ea3f3bcb2efe286849aa7a81531abc52d8454da12f46235092bcb"}, + {file = "msgpack-1.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f5d88c99f64c456413d74a975bd605a9b0526293218a3b77220a2c15458ba9"}, + {file = "msgpack-1.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916c78f33602ecf0509cc40379271ba0f9ab572b066bd4bdafd7434dee4bc6e"}, + {file = "msgpack-1.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81fc7ba725464651190b196f3cd848e8553d4d510114a954681fd0b9c479d7e1"}, + {file = "msgpack-1.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d5b5b962221fa2c5d3a7f8133f9abffc114fe218eb4365e40f17732ade576c8e"}, + {file = "msgpack-1.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:77ccd2af37f3db0ea59fb280fa2165bf1b096510ba9fe0cc2bf8fa92a22fdb43"}, + {file = "msgpack-1.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b17be2478b622939e39b816e0aa8242611cc8d3583d1cd8ec31b249f04623243"}, + {file = "msgpack-1.0.4-cp38-cp38-win32.whl", hash = "sha256:2bb8cdf50dd623392fa75525cce44a65a12a00c98e1e37bf0fb08ddce2ff60d2"}, + {file = "msgpack-1.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:26b8feaca40a90cbe031b03d82b2898bf560027160d3eae1423f4a67654ec5d6"}, + {file = "msgpack-1.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:462497af5fd4e0edbb1559c352ad84f6c577ffbbb708566a0abaaa84acd9f3ae"}, + {file = "msgpack-1.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2999623886c5c02deefe156e8f869c3b0aaeba14bfc50aa2486a0415178fce55"}, + {file = "msgpack-1.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f0029245c51fd9473dc1aede1160b0a29f4a912e6b1dd353fa6d317085b219da"}, + {file = "msgpack-1.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed6f7b854a823ea44cf94919ba3f727e230da29feb4a99711433f25800cf747f"}, + {file = "msgpack-1.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df96d6eaf45ceca04b3f3b4b111b86b33785683d682c655063ef8057d61fd92"}, + {file = "msgpack-1.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a4192b1ab40f8dca3f2877b70e63799d95c62c068c84dc028b40a6cb03ccd0f"}, + {file = "msgpack-1.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e3590f9fb9f7fbc36df366267870e77269c03172d086fa76bb4eba8b2b46624"}, + {file = "msgpack-1.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1576bd97527a93c44fa856770197dec00d223b0b9f36ef03f65bac60197cedf8"}, + {file = "msgpack-1.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:63e29d6e8c9ca22b21846234913c3466b7e4ee6e422f205a2988083de3b08cae"}, + {file = "msgpack-1.0.4-cp39-cp39-win32.whl", hash = "sha256:fb62ea4b62bfcb0b380d5680f9a4b3f9a2d166d9394e9bbd9666c0ee09a3645c"}, + {file = "msgpack-1.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:4d5834a2a48965a349da1c5a79760d94a1a0172fbb5ab6b5b33cbf8447e109ce"}, + {file = "msgpack-1.0.4.tar.gz", hash = "sha256:f5d869c18f030202eb412f08b28d2afeea553d6613aee89e200d7aca7ef01f5f"}, +] [[package]] name = "mypy" -version = "0.982" +version = "1.0.1" description = "Optional static typing for Python" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "mypy-1.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:71a808334d3f41ef011faa5a5cd8153606df5fc0b56de5b2e89566c8093a0c9a"}, + {file = "mypy-1.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:920169f0184215eef19294fa86ea49ffd4635dedfdea2b57e45cb4ee85d5ccaf"}, + {file = "mypy-1.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27a0f74a298769d9fdc8498fcb4f2beb86f0564bcdb1a37b58cbbe78e55cf8c0"}, + {file = "mypy-1.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:65b122a993d9c81ea0bfde7689b3365318a88bde952e4dfa1b3a8b4ac05d168b"}, + {file = "mypy-1.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:5deb252fd42a77add936b463033a59b8e48eb2eaec2976d76b6878d031933fe4"}, + {file = "mypy-1.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2013226d17f20468f34feddd6aae4635a55f79626549099354ce641bc7d40262"}, + {file = "mypy-1.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:48525aec92b47baed9b3380371ab8ab6e63a5aab317347dfe9e55e02aaad22e8"}, + {file = "mypy-1.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c96b8a0c019fe29040d520d9257d8c8f122a7343a8307bf8d6d4a43f5c5bfcc8"}, + {file = "mypy-1.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:448de661536d270ce04f2d7dddaa49b2fdba6e3bd8a83212164d4174ff43aa65"}, + {file = "mypy-1.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:d42a98e76070a365a1d1c220fcac8aa4ada12ae0db679cb4d910fabefc88b994"}, + {file = "mypy-1.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e64f48c6176e243ad015e995de05af7f22bbe370dbb5b32bd6988438ec873919"}, + {file = "mypy-1.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fdd63e4f50e3538617887e9aee91855368d9fc1dea30da743837b0df7373bc4"}, + {file = "mypy-1.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dbeb24514c4acbc78d205f85dd0e800f34062efcc1f4a4857c57e4b4b8712bff"}, + {file = "mypy-1.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a2948c40a7dd46c1c33765718936669dc1f628f134013b02ff5ac6c7ef6942bf"}, + {file = "mypy-1.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5bc8d6bd3b274dd3846597855d96d38d947aedba18776aa998a8d46fabdaed76"}, + {file = "mypy-1.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:17455cda53eeee0a4adb6371a21dd3dbf465897de82843751cf822605d152c8c"}, + {file = "mypy-1.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e831662208055b006eef68392a768ff83596035ffd6d846786578ba1714ba8f6"}, + {file = "mypy-1.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e60d0b09f62ae97a94605c3f73fd952395286cf3e3b9e7b97f60b01ddfbbda88"}, + {file = "mypy-1.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:0af4f0e20706aadf4e6f8f8dc5ab739089146b83fd53cb4a7e0e850ef3de0bb6"}, + {file = "mypy-1.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:24189f23dc66f83b839bd1cce2dfc356020dfc9a8bae03978477b15be61b062e"}, + {file = "mypy-1.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93a85495fb13dc484251b4c1fd7a5ac370cd0d812bbfc3b39c1bafefe95275d5"}, + {file = "mypy-1.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f546ac34093c6ce33f6278f7c88f0f147a4849386d3bf3ae193702f4fe31407"}, + {file = "mypy-1.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c6c2ccb7af7154673c591189c3687b013122c5a891bb5651eca3db8e6c6c55bd"}, + {file = "mypy-1.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:15b5a824b58c7c822c51bc66308e759243c32631896743f030daf449fe3677f3"}, + {file = "mypy-1.0.1-py3-none-any.whl", hash = "sha256:eda5c8b9949ed411ff752b9a01adda31afe7eae1e53e946dbdf9db23865e66c4"}, + {file = "mypy-1.0.1.tar.gz", hash = "sha256:28cea5a6392bb43d266782983b5a4216c25544cd7d80be681a155ddcdafd152d"}, +] [package.dependencies] mypy-extensions = ">=0.4.3" @@ -410,16 +955,21 @@ typing-extensions = ">=3.10" [package.extras] dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] python2 = ["typed-ast (>=1.4.0,<2)"] reports = ["lxml"] [[package]] name = "mypy-extensions" -version = "0.4.3" -description = "Experimental type system extensions for programs checked with the mypy typechecker." +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] [[package]] name = "nodeenv" @@ -428,6 +978,10 @@ description = "Node.js virtual environment builder" category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, + {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, +] [package.dependencies] setuptools = "*" @@ -439,20 +993,79 @@ description = "An OrderedSet is a custom MutableSet that remembers its order, so category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8"}, + {file = "ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562"}, +] [package.extras] dev = ["black", "mypy", "pytest"] +[[package]] +name = "orjson" +version = "3.8.6" +description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "orjson-3.8.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:062a9a74c10c439acc35cf67f31ac88d9464a11025700bab421e6cdf54a54a35"}, + {file = "orjson-3.8.6-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:692c255109867cc8211267f4416d2915845273bf4f403bbca5419f5b15ac9175"}, + {file = "orjson-3.8.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a20905c7a5ebc280343704c4dd19343ef966c9dea5a38ade6e0461a6deb8eda"}, + {file = "orjson-3.8.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34ce4a8b8f0fea483bce6985c015953f475540b7d756efd48a571b1803c318ee"}, + {file = "orjson-3.8.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57ecad7616ec842d8c382ed42a778cdcdadc67cfb46b804b43079f937b63b31"}, + {file = "orjson-3.8.6-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:323065cf14fdd4096dbf93ea1634e7e030044af8c1000803bcdc132fbfd395f5"}, + {file = "orjson-3.8.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4cb4f37fca8cf8309de421634447437f229bc03b240cec8ad4ac241fd4b1bcf4"}, + {file = "orjson-3.8.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:32353b14c5e0b55b6a8759e993482a2d8c44d492489840718b74658de67671e2"}, + {file = "orjson-3.8.6-cp310-none-win_amd64.whl", hash = "sha256:3e44f78db3a15902b5e8386119979691ed3dd61d1ded10bad2c7106fd50641ef"}, + {file = "orjson-3.8.6-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:c59ec129d523abd4f2d65c0733d0e57af7dd09c69142f1aa564b04358f04ace3"}, + {file = "orjson-3.8.6-cp311-cp311-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:d44d89314a66e98e690ce64c8771d963eb64ae6cea662d0a1d077ed024627228"}, + {file = "orjson-3.8.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:865ef341c4d310ac2689bf811dbc0930b2f13272f8eade1511dc40b186f6d562"}, + {file = "orjson-3.8.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:52809a37a0daa6992835ee0625aca22b4c0693dba3cb465948e6c9796de927b0"}, + {file = "orjson-3.8.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d7402121d06d11fafcaed7d06f9d68b11bbe39868e0e1bc19239ee5b6b98b2b"}, + {file = "orjson-3.8.6-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:583338b7dabb509ca4c3b4f160f58a5228bf6c6e0f8a2981663f683791f39d45"}, + {file = "orjson-3.8.6-cp311-none-win_amd64.whl", hash = "sha256:4a6c0a0ef2f535ba7a5d01f014b53d05eeb372d43556edb25c75a4d52690a123"}, + {file = "orjson-3.8.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:9d35573e7f5817a26d8ce1134c3463d31bc3b39aad3ad7ae06bb67d6078fa9c0"}, + {file = "orjson-3.8.6-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:94d8fdc12adc0450994931d722cb38be5e4caa273219881abb96c15a9e9f151f"}, + {file = "orjson-3.8.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8fc43bfb73d394b9bf12062cd6dab72abf728ac7869f972e4bb7327fd3330b8"}, + {file = "orjson-3.8.6-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a38387387139695a7e52b9f568e39c1632b22eb34939afc5efed265fa8277b84"}, + {file = "orjson-3.8.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e048c6df7453c3da4de10fa5c44f6c655b157b712628888ce880cd5bbf30013"}, + {file = "orjson-3.8.6-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:d3b0950d792b25c0aa52505faf09237fd98136d09616a0837f7cdb0fde9e2730"}, + {file = "orjson-3.8.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:38bc8a388080d8fd297388bcff4939e350ffafe4a006567e0dd81cdb8c7b86fa"}, + {file = "orjson-3.8.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5b3251ab7113f2400d76f2b4a2d6592e7d5a5cf45fa948c894340553671ef8f1"}, + {file = "orjson-3.8.6-cp37-none-win_amd64.whl", hash = "sha256:2c83a33cf389fd286bd9ef0befc406307444b9553d2e9ba14b90b9332524cfa6"}, + {file = "orjson-3.8.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:53f51c23398cfe818d9bb09079d31a60c6cd77e7eee1d555cfcc735460db4190"}, + {file = "orjson-3.8.6-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:6190e23a2fb9fc78228b289b3ec295094671ca0299319c8c72727aa9e7dbe06f"}, + {file = "orjson-3.8.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61fff8a8b4cd4e489b291fe5105b6138b1831490f1a0dc726d5e17ebe811d595"}, + {file = "orjson-3.8.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c192813f527f886bd85abc5a9e8d9dde16ffa06d7305de526a7c4657730dbf4e"}, + {file = "orjson-3.8.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aae1487fba9d955b2679f0a697665ed8fc32563b3252acc240e097184c184e29"}, + {file = "orjson-3.8.6-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cd2bd48e9a14f2130790a3c2dcb897bd93c2e5c244919799430a6d9b8212cb50"}, + {file = "orjson-3.8.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:006178fd654a0a4f14f5912b8320ba9a26ab9c0ae7ce1c7eeb4b5249d6cada29"}, + {file = "orjson-3.8.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9d5ad2fddccc89ab64b6333823b250ce8430fc51f014954e5a2d4c933f5deb9f"}, + {file = "orjson-3.8.6-cp38-none-win_amd64.whl", hash = "sha256:aef3d558f5bd809733ebf2cbce7e1338ce62812db317478427236b97036aba0f"}, + {file = "orjson-3.8.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:7d216a5f3d23eac2c7c654e7bd30280c27cdf5edc32325e6ad8e880d36c265b7"}, + {file = "orjson-3.8.6-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:004122c95e08db7201b80224de3a8f2ad79b9717040e6884c6015f27b010127d"}, + {file = "orjson-3.8.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:006c492577ad046cb7e50237a8d8935131a35f7e7f8320fbc3514da6fbc0b436"}, + {file = "orjson-3.8.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:67554103b415349b6ee2db82d2422da1c8f4c2d280d20772217f6d1d227410b6"}, + {file = "orjson-3.8.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:caa5053f19584816f063c887d94385db481fc01d995d6a717ce4fbb929653ec2"}, + {file = "orjson-3.8.6-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2bdd64566870a8a0bdcf8c7df2f4452391dd55070f5cd98cc581914e8c263d85"}, + {file = "orjson-3.8.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:550a4dec128d1adfd0262ef9ad7878d62d1cc0bddaaa05e41d8ca28414dc86bc"}, + {file = "orjson-3.8.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3f5ad9442e8a99fb436279a8614a00aca272ea8dabb692cadee70a4874d6e03"}, + {file = "orjson-3.8.6-cp39-none-win_amd64.whl", hash = "sha256:aa7b112e3273d1744f7bc983ffd3dd0d004062c69dfa68e119515a7e115c46c8"}, + {file = "orjson-3.8.6.tar.gz", hash = "sha256:91ef8a554d33fbc5bb61c3972f3e8baa994f72c4967671e64e7dac1cc06f50e1"}, +] + [[package]] name = "packaging" -version = "21.3" +version = "23.0" description = "Core utilities for Python packages" category = "main" optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" +python-versions = ">=3.7" +files = [ + {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, + {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, +] [[package]] name = "pexpect" @@ -461,40 +1074,59 @@ description = "Pexpect allows easy control of interactive console applications." category = "main" optional = false python-versions = "*" +files = [ + {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, + {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, +] [package.dependencies] ptyprocess = ">=0.5" [[package]] name = "pkginfo" -version = "1.8.3" -description = "Query metadatdata from sdists / bdists / installed packages." +version = "1.9.6" +description = "Query metadata from sdists / bdists / installed packages." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.6" +files = [ + {file = "pkginfo-1.9.6-py3-none-any.whl", hash = "sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546"}, + {file = "pkginfo-1.9.6.tar.gz", hash = "sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046"}, +] [package.extras] -testing = ["coverage", "nose"] +testing = ["pytest", "pytest-cov"] [[package]] -name = "pkgutil_resolve_name" +name = "pkgutil-resolve-name" version = "1.3.10" description = "Resolve a name to an object." category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, + {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, +] [[package]] name = "platformdirs" -version = "2.5.2" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "2.6.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, + {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.4", markers = "python_version < \"3.8\""} [package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" @@ -503,6 +1135,10 @@ description = "plugin and hook calling mechanisms for python" category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] [package.dependencies] importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} @@ -513,34 +1149,46 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "poetry-core" -version = "1.3.2" +version = "1.5.2" description = "Poetry PEP 517 Build Backend" category = "main" optional = false python-versions = ">=3.7,<4.0" +files = [ + {file = "poetry_core-1.5.2-py3-none-any.whl", hash = "sha256:832d40a1ea5fd10c0f648d0575cadddc8b79f06f91d83a1f1a73a7e1dfacfbd7"}, + {file = "poetry_core-1.5.2.tar.gz", hash = "sha256:c6556c3b1ec5b8668e6ef5a4494726bc41d31907339425e194e78a6178436c14"}, +] [package.dependencies] importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""} [[package]] name = "poetry-plugin-export" -version = "1.1.2" +version = "1.3.0" description = "Poetry plugin to export the dependencies to various formats" category = "main" optional = false python-versions = ">=3.7,<4.0" +files = [ + {file = "poetry_plugin_export-1.3.0-py3-none-any.whl", hash = "sha256:6e5919bf84afcb08cdd419a03f909f490d8671f00633a3c6df8ba09b0820dc2f"}, + {file = "poetry_plugin_export-1.3.0.tar.gz", hash = "sha256:61ae5ec1db233aba947a48e1ce54c6ff66afd0e1c87195d6bce64c73a5ae658c"}, +] [package.dependencies] -poetry = ">=1.2.0,<2.0.0" -poetry-core = ">=1.1.0,<2.0.0" +poetry = ">=1.3.0,<2.0.0" +poetry-core = ">=1.3.0,<2.0.0" [[package]] name = "pre-commit" -version = "2.20.0" +version = "2.21.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, + {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, +] [package.dependencies] cfgv = ">=2.0.0" @@ -548,16 +1196,31 @@ identify = ">=1.0.0" importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} nodeenv = ">=0.11.1" pyyaml = ">=5.1" -toml = "*" -virtualenv = ">=20.0.8" +virtualenv = ">=20.10.0" [[package]] name = "psutil" -version = "5.9.2" +version = "5.9.4" description = "Cross-platform lib for process and system monitoring in Python." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "psutil-5.9.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8"}, + {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe"}, + {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549"}, + {file = "psutil-5.9.4-cp27-cp27m-win32.whl", hash = "sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad"}, + {file = "psutil-5.9.4-cp27-cp27m-win_amd64.whl", hash = "sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94"}, + {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24"}, + {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7"}, + {file = "psutil-5.9.4-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7"}, + {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1"}, + {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08"}, + {file = "psutil-5.9.4-cp36-abi3-win32.whl", hash = "sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff"}, + {file = "psutil-5.9.4-cp36-abi3-win_amd64.whl", hash = "sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4"}, + {file = "psutil-5.9.4-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e"}, + {file = "psutil-5.9.4.tar.gz", hash = "sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62"}, +] [package.extras] test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] @@ -569,14 +1232,10 @@ description = "Run a subprocess in a pseudo terminal" category = "main" optional = false python-versions = "*" - -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] [[package]] name = "pycparser" @@ -585,51 +1244,84 @@ description = "C parser in Python" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] [[package]] -name = "pylev" -version = "1.4.0" -description = "A pure Python Levenshtein implementation that's not freaking GPL'd." -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" +name = "pyproject-hooks" +version = "1.0.0" +description = "Wrappers to call pyproject.toml-based build backend hooks." category = "main" optional = false -python-versions = ">=3.6.8" +python-versions = ">=3.7" +files = [ + {file = "pyproject_hooks-1.0.0-py3-none-any.whl", hash = "sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8"}, + {file = "pyproject_hooks-1.0.0.tar.gz", hash = "sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5"}, +] -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] +[package.dependencies] +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [[package]] name = "pyrsistent" -version = "0.18.1" +version = "0.19.3" description = "Persistent/Functional/Immutable data structures" category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "pyrsistent-0.19.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a"}, + {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64"}, + {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf"}, + {file = "pyrsistent-0.19.3-cp310-cp310-win32.whl", hash = "sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a"}, + {file = "pyrsistent-0.19.3-cp310-cp310-win_amd64.whl", hash = "sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da"}, + {file = "pyrsistent-0.19.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9"}, + {file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393"}, + {file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19"}, + {file = "pyrsistent-0.19.3-cp311-cp311-win32.whl", hash = "sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3"}, + {file = "pyrsistent-0.19.3-cp311-cp311-win_amd64.whl", hash = "sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8"}, + {file = "pyrsistent-0.19.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28"}, + {file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf"}, + {file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9"}, + {file = "pyrsistent-0.19.3-cp37-cp37m-win32.whl", hash = "sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1"}, + {file = "pyrsistent-0.19.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b"}, + {file = "pyrsistent-0.19.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8"}, + {file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a"}, + {file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c"}, + {file = "pyrsistent-0.19.3-cp38-cp38-win32.whl", hash = "sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c"}, + {file = "pyrsistent-0.19.3-cp38-cp38-win_amd64.whl", hash = "sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7"}, + {file = "pyrsistent-0.19.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc"}, + {file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2"}, + {file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3"}, + {file = "pyrsistent-0.19.3-cp39-cp39-win32.whl", hash = "sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2"}, + {file = "pyrsistent-0.19.3-cp39-cp39-win_amd64.whl", hash = "sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98"}, + {file = "pyrsistent-0.19.3-py3-none-any.whl", hash = "sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64"}, + {file = "pyrsistent-0.19.3.tar.gz", hash = "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440"}, +] [[package]] name = "pytest" -version = "7.1.3" +version = "7.2.1" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"}, + {file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"}, +] [package.dependencies] attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] @@ -641,6 +1333,10 @@ description = "Pytest plugin for measuring coverage." category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, + {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, +] [package.dependencies] coverage = {version = ">=5.2.1", extras = ["toml"]} @@ -649,25 +1345,17 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] -[[package]] -name = "pytest-forked" -version = "1.4.0" -description = "run tests in isolated forked subprocesses" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -py = "*" -pytest = ">=3.10" - [[package]] name = "pytest-github-actions-annotate-failures" -version = "0.1.7" +version = "0.1.8" description = "pytest plugin to annotate failed tests with a workflow command for GitHub Actions" category = "dev" optional = false -python-versions = "*" +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" +files = [ + {file = "pytest-github-actions-annotate-failures-0.1.8.tar.gz", hash = "sha256:2d6e6cb5f8d0aae4a27a20cc4e20fabd3199a121c57f44bc48fe28e372e0be23"}, + {file = "pytest_github_actions_annotate_failures-0.1.8-py2.py3-none-any.whl", hash = "sha256:6a882ff21672fa79deae8d917eb965a6bde2b25191e7632e1adfc23ffac008ab"}, +] [package.dependencies] pytest = ">=4.0.0" @@ -679,6 +1367,10 @@ description = "Thin-wrapper around the mock package for easier use with pytest" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "pytest-mock-3.10.0.tar.gz", hash = "sha256:fbbdb085ef7c252a326fd8cdcac0aa3b1333d8811f131bdcc701002e1be7ed4f"}, + {file = "pytest_mock-3.10.0-py3-none-any.whl", hash = "sha256:f4c973eeae0282963eb293eb173ce91b091a79c1334455acfac9ddee8a1c784b"}, +] [package.dependencies] pytest = ">=5.0" @@ -693,6 +1385,10 @@ description = "Pytest plugin to randomly order tests and control random.seed." category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "pytest-randomly-3.12.0.tar.gz", hash = "sha256:d60c2db71ac319aee0fc6c4110a7597d611a8b94a5590918bfa8583f00caccb2"}, + {file = "pytest_randomly-3.12.0-py3-none-any.whl", hash = "sha256:f4f2e803daf5d1ba036cc22bf4fe9dbbf99389ec56b00e5cba732fb5c1d07fdd"}, +] [package.dependencies] importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} @@ -700,17 +1396,20 @@ pytest = "*" [[package]] name = "pytest-xdist" -version = "2.5.0" -description = "pytest xdist plugin for distributed testing and loop-on-failing modes" +version = "3.2.0" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +files = [ + {file = "pytest-xdist-3.2.0.tar.gz", hash = "sha256:fa10f95a2564cd91652f2d132725183c3b590d9fdcdec09d3677386ecf4c1ce9"}, + {file = "pytest_xdist-3.2.0-py3-none-any.whl", hash = "sha256:336098e3bbd8193276867cc87db8b22903c3927665dff9d1ac8684c02f597b68"}, +] [package.dependencies] execnet = ">=1.1" psutil = {version = ">=3.0", optional = true, markers = "extra == \"psutil\""} pytest = ">=6.2.0" -pytest-forked = "*" [package.extras] psutil = ["psutil (>=3.0)"] @@ -724,51 +1423,211 @@ description = "" category = "main" optional = false python-versions = "*" +files = [ + {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, + {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, +] [[package]] -name = "PyYAML" +name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] + +[[package]] +name = "rapidfuzz" +version = "2.13.7" +description = "rapid fuzzy string matching" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "rapidfuzz-2.13.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b75dd0928ce8e216f88660ab3d5c5ffe990f4dd682fd1709dba29d5dafdde6de"}, + {file = "rapidfuzz-2.13.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:24d3fea10680d085fd0a4d76e581bfb2b1074e66e78fd5964d4559e1fcd2a2d4"}, + {file = "rapidfuzz-2.13.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8109e0324d21993d5b2d111742bf5958f3516bf8c59f297c5d1cc25a2342eb66"}, + {file = "rapidfuzz-2.13.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f705652360d520c2de52bee11100c92f59b3e3daca308ebb150cbc58aecdad"}, + {file = "rapidfuzz-2.13.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7496e8779905b02abc0ab4ba2a848e802ab99a6e20756ffc967a0de4900bd3da"}, + {file = "rapidfuzz-2.13.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24eb6b843492bdc63c79ee4b2f104059b7a2201fef17f25177f585d3be03405a"}, + {file = "rapidfuzz-2.13.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:467c1505362823a5af12b10234cb1c4771ccf124c00e3fc9a43696512bd52293"}, + {file = "rapidfuzz-2.13.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53dcae85956853b787c27c1cb06f18bb450e22cf57a4ad3444cf03b8ff31724a"}, + {file = "rapidfuzz-2.13.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46b9b8aa09998bc48dd800854e8d9b74bc534d7922c1d6e1bbf783e7fa6ac29c"}, + {file = "rapidfuzz-2.13.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1fbad8fb28d98980f5bff33c7842efef0315d42f0cd59082108482a7e6b61410"}, + {file = "rapidfuzz-2.13.7-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:43fb8cb030f888c3f076d40d428ed5eb4331f5dd6cf1796cfa39c67bf0f0fc1e"}, + {file = "rapidfuzz-2.13.7-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:b6bad92de071cbffa2acd4239c1779f66851b60ffbbda0e4f4e8a2e9b17e7eef"}, + {file = "rapidfuzz-2.13.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d00df2e4a81ffa56a6b1ec4d2bc29afdcb7f565e0b8cd3092fece2290c4c7a79"}, + {file = "rapidfuzz-2.13.7-cp310-cp310-win32.whl", hash = "sha256:2c836f0f2d33d4614c3fbaf9a1eb5407c0fe23f8876f47fd15b90f78daa64c34"}, + {file = "rapidfuzz-2.13.7-cp310-cp310-win_amd64.whl", hash = "sha256:c36fd260084bb636b9400bb92016c6bd81fd80e59ed47f2466f85eda1fc9f782"}, + {file = "rapidfuzz-2.13.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b34e8c0e492949ecdd5da46a1cfc856a342e2f0389b379b1a45a3cdcd3176a6e"}, + {file = "rapidfuzz-2.13.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:875d51b3497439a72e2d76183e1cb5468f3f979ab2ddfc1d1f7dde3b1ecfb42f"}, + {file = "rapidfuzz-2.13.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ae33a72336059213996fe4baca4e0e4860913905c2efb7c991eab33b95a98a0a"}, + {file = "rapidfuzz-2.13.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5585189b3d90d81ccd62d4f18530d5ac8972021f0aaaa1ffc6af387ff1dce75"}, + {file = "rapidfuzz-2.13.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42085d4b154a8232767de8296ac39c8af5bccee6b823b0507de35f51c9cbc2d7"}, + {file = "rapidfuzz-2.13.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:585206112c294e335d84de5d5f179c0f932837752d7420e3de21db7fdc476278"}, + {file = "rapidfuzz-2.13.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f891b98f8bc6c9d521785816085e9657212621e93f223917fb8e32f318b2957e"}, + {file = "rapidfuzz-2.13.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08590905a95ccfa43f4df353dcc5d28c15d70664299c64abcad8721d89adce4f"}, + {file = "rapidfuzz-2.13.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b5dd713a1734574c2850c566ac4286594bacbc2d60b9170b795bee4b68656625"}, + {file = "rapidfuzz-2.13.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:988f8f6abfba7ee79449f8b50687c174733b079521c3cc121d65ad2d38831846"}, + {file = "rapidfuzz-2.13.7-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b3210869161a864f3831635bb13d24f4708c0aa7208ef5baac1ac4d46e9b4208"}, + {file = "rapidfuzz-2.13.7-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f6fe570e20e293eb50491ae14ddeef71a6a7e5f59d7e791393ffa99b13f1f8c2"}, + {file = "rapidfuzz-2.13.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6120f2995f5154057454c5de99d86b4ef3b38397899b5da1265467e8980b2f60"}, + {file = "rapidfuzz-2.13.7-cp311-cp311-win32.whl", hash = "sha256:b20141fa6cee041917801de0bab503447196d372d4c7ee9a03721b0a8edf5337"}, + {file = "rapidfuzz-2.13.7-cp311-cp311-win_amd64.whl", hash = "sha256:ec55a81ac2b0f41b8d6fb29aad16e55417036c7563bad5568686931aa4ff08f7"}, + {file = "rapidfuzz-2.13.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7d005e058d86f2a968a8d28ca6f2052fab1f124a39035aa0523261d6baf21e1f"}, + {file = "rapidfuzz-2.13.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe59a0c21a032024edb0c8e43f5dee5623fef0b65a1e3c1281836d9ce199af3b"}, + {file = "rapidfuzz-2.13.7-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdfc04f7647c29fb48da7a04082c34cdb16f878d3c6d098d62d5715c0ad3000c"}, + {file = "rapidfuzz-2.13.7-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:68a89bb06d5a331511961f4d3fa7606f8e21237467ba9997cae6f67a1c2c2b9e"}, + {file = "rapidfuzz-2.13.7-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:effe182767d102cb65dfbbf74192237dbd22d4191928d59415aa7d7c861d8c88"}, + {file = "rapidfuzz-2.13.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25b4cedf2aa19fb7212894ce5f5219010cce611b60350e9a0a4d492122e7b351"}, + {file = "rapidfuzz-2.13.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3a9bd02e1679c0fd2ecf69b72d0652dbe2a9844eaf04a36ddf4adfbd70010e95"}, + {file = "rapidfuzz-2.13.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5e2b3d020219baa75f82a4e24b7c8adcb598c62f0e54e763c39361a9e5bad510"}, + {file = "rapidfuzz-2.13.7-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:cf62dacb3f9234f3fddd74e178e6d25c68f2067fde765f1d95f87b1381248f58"}, + {file = "rapidfuzz-2.13.7-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:fa263135b892686e11d5b84f6a1892523123a00b7e5882eff4fbdabb38667347"}, + {file = "rapidfuzz-2.13.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:fa4c598ed77f74ec973247ca776341200b0f93ec3883e34c222907ce72cb92a4"}, + {file = "rapidfuzz-2.13.7-cp37-cp37m-win32.whl", hash = "sha256:c2523f8180ebd9796c18d809e9a19075a1060b1a170fde3799e83db940c1b6d5"}, + {file = "rapidfuzz-2.13.7-cp37-cp37m-win_amd64.whl", hash = "sha256:5ada0a14c67452358c1ee52ad14b80517a87b944897aaec3e875279371a9cb96"}, + {file = "rapidfuzz-2.13.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ca8a23097c1f50e0fdb4de9e427537ca122a18df2eead06ed39c3a0bef6d9d3a"}, + {file = "rapidfuzz-2.13.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9be02162af0376d64b840f2fc8ee3366794fc149f1e06d095a6a1d42447d97c5"}, + {file = "rapidfuzz-2.13.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af4f7c3c904ca709493eb66ca9080b44190c38e9ecb3b48b96d38825d5672559"}, + {file = "rapidfuzz-2.13.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f50d1227e6e2a0e3ae1fb1c9a2e1c59577d3051af72c7cab2bcc430cb5e18da"}, + {file = "rapidfuzz-2.13.7-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c71d9d512b76f05fa00282227c2ae884abb60e09f08b5ca3132b7e7431ac7f0d"}, + {file = "rapidfuzz-2.13.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b52ac2626945cd21a2487aeefed794c14ee31514c8ae69b7599170418211e6f6"}, + {file = "rapidfuzz-2.13.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca00fafd2756bc9649bf80f1cf72c647dce38635f0695d7ce804bc0f759aa756"}, + {file = "rapidfuzz-2.13.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d248a109699ce9992304e79c1f8735c82cc4c1386cd8e27027329c0549f248a2"}, + {file = "rapidfuzz-2.13.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c88adbcb933f6b8612f6c593384bf824e562bb35fc8a0f55fac690ab5b3486e5"}, + {file = "rapidfuzz-2.13.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c8601a66fbfc0052bb7860d2eacd303fcde3c14e87fdde409eceff516d659e77"}, + {file = "rapidfuzz-2.13.7-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:27be9c63215d302ede7d654142a2e21f0d34ea6acba512a4ae4cfd52bbaa5b59"}, + {file = "rapidfuzz-2.13.7-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3dcffe1f3cbda0dc32133a2ae2255526561ca594f15f9644384549037b355245"}, + {file = "rapidfuzz-2.13.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8450d15f7765482e86ef9be2ad1a05683cd826f59ad236ef7b9fb606464a56aa"}, + {file = "rapidfuzz-2.13.7-cp38-cp38-win32.whl", hash = "sha256:460853983ab88f873173e27cc601c5276d469388e6ad6e08c4fd57b2a86f1064"}, + {file = "rapidfuzz-2.13.7-cp38-cp38-win_amd64.whl", hash = "sha256:424f82c35dbe4f83bdc3b490d7d696a1dc6423b3d911460f5493b7ffae999fd2"}, + {file = "rapidfuzz-2.13.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c3fbe449d869ea4d0909fc9d862007fb39a584fb0b73349a6aab336f0d90eaed"}, + {file = "rapidfuzz-2.13.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:16080c05a63d6042643ae9b6cfec1aefd3e61cef53d0abe0df3069b9d4b72077"}, + {file = "rapidfuzz-2.13.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dbcf5371ea704759fcce772c66a07647751d1f5dbdec7818331c9b31ae996c77"}, + {file = "rapidfuzz-2.13.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:114810491efb25464016fd554fdf1e20d390309cecef62587494fc474d4b926f"}, + {file = "rapidfuzz-2.13.7-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99a84ab9ac9a823e7e93b4414f86344052a5f3e23b23aa365cda01393ad895bd"}, + {file = "rapidfuzz-2.13.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:81642a24798851b118f82884205fc1bd9ff70b655c04018c467824b6ecc1fabc"}, + {file = "rapidfuzz-2.13.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3741cb0bf9794783028e8b0cf23dab917fa5e37a6093b94c4c2f805f8e36b9f"}, + {file = "rapidfuzz-2.13.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:759a3361711586a29bc753d3d1bdb862983bd9b9f37fbd7f6216c24f7c972554"}, + {file = "rapidfuzz-2.13.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1333fb3d603d6b1040e365dca4892ba72c7e896df77a54eae27dc07db90906e3"}, + {file = "rapidfuzz-2.13.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:916bc2e6cf492c77ad6deb7bcd088f0ce9c607aaeabc543edeb703e1fbc43e31"}, + {file = "rapidfuzz-2.13.7-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:23524635840500ce6f4d25005c9529a97621689c85d2f727c52eed1782839a6a"}, + {file = "rapidfuzz-2.13.7-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ebe303cd9839af69dd1f7942acaa80b1ba90bacef2e7ded9347fbed4f1654672"}, + {file = "rapidfuzz-2.13.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fe56659ccadbee97908132135de4b875543353351e0c92e736b7c57aee298b5a"}, + {file = "rapidfuzz-2.13.7-cp39-cp39-win32.whl", hash = "sha256:3f11a7eff7bc6301cd6a5d43f309e22a815af07e1f08eeb2182892fca04c86cb"}, + {file = "rapidfuzz-2.13.7-cp39-cp39-win_amd64.whl", hash = "sha256:e8914dad106dacb0775718e54bf15e528055c4e92fb2677842996f2d52da5069"}, + {file = "rapidfuzz-2.13.7-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f7930adf84301797c3f09c94b9c5a9ed90a9e8b8ed19b41d2384937e0f9f5bd"}, + {file = "rapidfuzz-2.13.7-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c31022d9970177f6affc6d5dd757ed22e44a10890212032fabab903fdee3bfe7"}, + {file = "rapidfuzz-2.13.7-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f42b82f268689f429def9ecfb86fa65ceea0eaf3fed408b570fe113311bf5ce7"}, + {file = "rapidfuzz-2.13.7-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b477b43ced896301665183a5e0faec0f5aea2373005648da8bdcb3c4b73f280"}, + {file = "rapidfuzz-2.13.7-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d63def9bbc6b35aef4d76dc740301a4185867e8870cbb8719ec9de672212fca8"}, + {file = "rapidfuzz-2.13.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c66546e30addb04a16cd864f10f5821272a1bfe6462ee5605613b4f1cb6f7b48"}, + {file = "rapidfuzz-2.13.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f799d1d6c33d81e983d3682571cc7d993ae7ff772c19b3aabb767039c33f6d1e"}, + {file = "rapidfuzz-2.13.7-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d82f20c0060ffdaadaf642b88ab0aa52365b56dffae812e188e5bdb998043588"}, + {file = "rapidfuzz-2.13.7-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:042644133244bfa7b20de635d500eb9f46af7097f3d90b1724f94866f17cb55e"}, + {file = "rapidfuzz-2.13.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:75c45dcd595f8178412367e302fd022860ea025dc4a78b197b35428081ed33d5"}, + {file = "rapidfuzz-2.13.7-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3d8b081988d0a49c486e4e845a547565fee7c6e7ad8be57ff29c3d7c14c6894c"}, + {file = "rapidfuzz-2.13.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16ffad751f43ab61001187b3fb4a9447ec2d1aedeff7c5bac86d3b95f9980cc3"}, + {file = "rapidfuzz-2.13.7-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:020858dd89b60ce38811cd6e37875c4c3c8d7fcd8bc20a0ad2ed1f464b34dc4e"}, + {file = "rapidfuzz-2.13.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cda1e2f66bb4ba7261a0f4c2d052d5d909798fca557cbff68f8a79a87d66a18f"}, + {file = "rapidfuzz-2.13.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b6389c50d8d214c9cd11a77f6d501529cb23279a9c9cafe519a3a4b503b5f72a"}, + {file = "rapidfuzz-2.13.7.tar.gz", hash = "sha256:8d3e252d4127c79b4d7c2ae47271636cbaca905c8bb46d80c7930ab906cf4b5c"}, +] + +[package.extras] +full = ["numpy"] [[package]] name = "requests" -version = "2.28.1" +version = "2.28.2" description = "Python HTTP for Humans." category = "main" optional = false python-versions = ">=3.7, <4" +files = [ + {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, + {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, +] [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<3" +charset-normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<1.27" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-toolbelt" -version = "0.9.1" +version = "0.10.1" description = "A utility belt for advanced users of python-requests" category = "main" optional = false -python-versions = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "requests-toolbelt-0.10.1.tar.gz", hash = "sha256:62e09f7ff5ccbda92772a29f394a49c3ad6cb181d568b1337626b2abb628a63d"}, + {file = "requests_toolbelt-0.10.1-py2.py3-none-any.whl", hash = "sha256:18565aa58116d9951ac39baa288d3adb5b3ff975c4f25eee78555d89e8f247f7"}, +] [package.dependencies] requests = ">=2.0.1,<3.0.0" [[package]] -name = "SecretStorage" +name = "secretstorage" version = "3.3.3" description = "Python bindings to FreeDesktop.org Secret Service API" category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, + {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, +] [package.dependencies] cryptography = ">=2.0" @@ -776,24 +1635,32 @@ jeepney = ">=0.6" [[package]] name = "setuptools" -version = "65.4.1" +version = "67.3.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "setuptools-67.3.2-py3-none-any.whl", hash = "sha256:bb6d8e508de562768f2027902929f8523932fcd1fb784e6d573d2cafac995a48"}, + {file = "setuptools-67.3.2.tar.gz", hash = "sha256:95f00380ef2ffa41d9bba85d95b27689d923c93dfbafed4aecd7cf988a25e012"}, +] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "shellingham" -version = "1.5.0" +version = "1.5.0.post1" description = "Tool to Detect Surrounding Shell" category = "main" optional = false -python-versions = ">=3.4" +python-versions = ">=3.7" +files = [ + {file = "shellingham-1.5.0.post1-py2.py3-none-any.whl", hash = "sha256:368bf8c00754fd4f55afb7bbb86e272df77e4dc76ac29dbcbb81a59e9fc15744"}, + {file = "shellingham-1.5.0.post1.tar.gz", hash = "sha256:823bc5fb5c34d60f285b624e7264f4dda254bc803a3774a147bf99c0e3004a28"}, +] [[package]] name = "six" @@ -802,38 +1669,46 @@ description = "Python 2 and 3 compatibility utilities" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] [[package]] name = "tomlkit" -version = "0.11.5" +version = "0.11.6" description = "Style preserving TOML library" category = "main" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.6" +files = [ + {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, + {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, +] [[package]] name = "trove-classifiers" -version = "2022.9.26" +version = "2023.2.8" description = "Canonical source for classifiers on PyPI (pypi.org)." category = "main" optional = false python-versions = "*" +files = [ + {file = "trove-classifiers-2023.2.8.tar.gz", hash = "sha256:3b6960fb96c1d4cc9988bdc1b90bcd65fcf5d9843d884dfc86bd674ff81a4dea"}, + {file = "trove_classifiers-2023.2.8-py3-none-any.whl", hash = "sha256:3aff899dd9792c4d095740980a5967eb98094ff881faf0b29afc775b75aaaac6"}, +] [[package]] name = "typed-ast" @@ -842,57 +1717,107 @@ description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, + {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, + {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, + {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, + {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, + {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, + {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, + {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, + {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, + {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, +] [[package]] name = "types-html5lib" -version = "1.1.11" +version = "1.1.11.11" description = "Typing stubs for html5lib" category = "dev" optional = false python-versions = "*" +files = [ + {file = "types-html5lib-1.1.11.11.tar.gz", hash = "sha256:8cf7fb57dcaf3e612806e9bc25ae366ff7ca71a3418ae829f5b1a9c52cbb4960"}, + {file = "types_html5lib-1.1.11.11-py3-none-any.whl", hash = "sha256:7456a07a4d162bb8c42c2a088c60cca7c63d06cf2c409a8de39536a6cdbbccc2"}, +] [[package]] name = "types-jsonschema" -version = "4.16.1" +version = "4.17.0.5" description = "Typing stubs for jsonschema" category = "dev" optional = false python-versions = "*" +files = [ + {file = "types-jsonschema-4.17.0.5.tar.gz", hash = "sha256:7adc7bfca4afe291de0c93eca9367aa72a4fbe8ce87fe15642c600ad97d45dd6"}, + {file = "types_jsonschema-4.17.0.5-py3-none-any.whl", hash = "sha256:79ac8a7763fe728947af90a24168b91621edf7e8425bf3670abd4ea0d4758fba"}, +] [[package]] name = "types-requests" -version = "2.28.11.2" +version = "2.28.11.13" description = "Typing stubs for requests" category = "dev" optional = false python-versions = "*" +files = [ + {file = "types-requests-2.28.11.13.tar.gz", hash = "sha256:3fd332842e8759ea5f7eb7789df8aa772ba155216ccf10ef4aa3b0e5b42e1b46"}, + {file = "types_requests-2.28.11.13-py3-none-any.whl", hash = "sha256:94896f6f8e9f3db11e422c6e3e4abbc5d7ccace853eac74b23bdd65eeee3cdee"}, +] [package.dependencies] types-urllib3 = "<1.27" [[package]] name = "types-urllib3" -version = "1.26.25" +version = "1.26.25.6" description = "Typing stubs for urllib3" category = "dev" optional = false python-versions = "*" +files = [ + {file = "types-urllib3-1.26.25.6.tar.gz", hash = "sha256:35586727cbd7751acccf2c0f34a88baffc092f435ab62458f10776466590f2d5"}, + {file = "types_urllib3-1.26.25.6-py3-none-any.whl", hash = "sha256:a6c23c41bd03e542eaee5423a018f833077b51c4bf9ceb5aa544e12b812d5604"}, +] [[package]] name = "typing-extensions" -version = "4.4.0" +version = "4.5.0" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, + {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, +] [[package]] name = "urllib3" -version = "1.26.12" +version = "1.26.14" 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.*, !=3.5.*, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, + {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, +] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] @@ -906,17 +1831,42 @@ description = "Virtual Python Environment builder" category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "virtualenv-20.16.5-py3-none-any.whl", hash = "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27"}, + {file = "virtualenv-20.16.5.tar.gz", hash = "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da"}, +] [package.dependencies] distlib = ">=0.3.5,<1" filelock = ">=3.4.1,<4" -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""} platformdirs = ">=2.4,<3" [package.extras] docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"] testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] +[[package]] +name = "virtualenv" +version = "20.19.0" +description = "Virtual Python Environment builder" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.19.0-py3-none-any.whl", hash = "sha256:54eb59e7352b573aa04d53f80fc9736ed0ad5143af445a1e539aada6eb947dd1"}, + {file = "virtualenv-20.19.0.tar.gz", hash = "sha256:37a640ba82ed40b226599c522d411e4be5edb339a0c0de030c0dc7b646d61590"}, +] + +[package.dependencies] +distlib = ">=0.3.6,<1" +filelock = ">=3.4.1,<4" +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""} +platformdirs = ">=2.4,<4" + +[package.extras] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] +test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"] + [[package]] name = "webencodings" version = "0.5.1" @@ -924,760 +1874,113 @@ description = "Character encoding aliases for legacy web content" category = "main" optional = false python-versions = "*" +files = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] [[package]] name = "xattr" -version = "0.9.9" +version = "0.10.1" description = "Python wrapper for extended filesystem attributes" category = "main" optional = false python-versions = "*" +files = [ + {file = "xattr-0.10.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:16a660a883e703b311d1bbbcafc74fa877585ec081cd96e8dd9302c028408ab1"}, + {file = "xattr-0.10.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1e2973e72faa87ca29d61c23b58c3c89fe102d1b68e091848b0e21a104123503"}, + {file = "xattr-0.10.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:13279fe8f7982e3cdb0e088d5cb340ce9cbe5ef92504b1fd80a0d3591d662f68"}, + {file = "xattr-0.10.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1dc9b9f580ef4b8ac5e2c04c16b4d5086a611889ac14ecb2e7e87170623a0b75"}, + {file = "xattr-0.10.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:485539262c2b1f5acd6b6ea56e0da2bc281a51f74335c351ea609c23d82c9a79"}, + {file = "xattr-0.10.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:295b3ab335fcd06ca0a9114439b34120968732e3f5e9d16f456d5ec4fa47a0a2"}, + {file = "xattr-0.10.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:a126eb38e14a2f273d584a692fe36cff760395bf7fc061ef059224efdb4eb62c"}, + {file = "xattr-0.10.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:b0e919c24f5b74428afa91507b15e7d2ef63aba98e704ad13d33bed1288dca81"}, + {file = "xattr-0.10.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:e31d062cfe1aaeab6ba3db6bd255f012d105271018e647645941d6609376af18"}, + {file = "xattr-0.10.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:209fb84c09b41c2e4cf16dd2f481bb4a6e2e81f659a47a60091b9bcb2e388840"}, + {file = "xattr-0.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c4120090dac33eddffc27e487f9c8f16b29ff3f3f8bcb2251b2c6c3f974ca1e1"}, + {file = "xattr-0.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3e739d624491267ec5bb740f4eada93491de429d38d2fcdfb97b25efe1288eca"}, + {file = "xattr-0.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2677d40b95636f3482bdaf64ed9138fb4d8376fb7933f434614744780e46e42d"}, + {file = "xattr-0.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40039f1532c4456fd0f4c54e9d4e01eb8201248c321c6c6856262d87e9a99593"}, + {file = "xattr-0.10.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:148466e5bb168aba98f80850cf976e931469a3c6eb11e9880d9f6f8b1e66bd06"}, + {file = "xattr-0.10.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0aedf55b116beb6427e6f7958ccd80a8cbc80e82f87a4cd975ccb61a8d27b2ee"}, + {file = "xattr-0.10.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c3024a9ff157247c8190dd0eb54db4a64277f21361b2f756319d9d3cf20e475f"}, + {file = "xattr-0.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f1be6e733e9698f645dbb98565bb8df9b75e80e15a21eb52787d7d96800e823b"}, + {file = "xattr-0.10.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7880c8a54c18bc091a4ce0adc5c6d81da1c748aec2fe7ac586d204d6ec7eca5b"}, + {file = "xattr-0.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:89c93b42c3ba8aedbc29da759f152731196c2492a2154371c0aae3ef8ba8301b"}, + {file = "xattr-0.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6b905e808df61b677eb972f915f8a751960284358b520d0601c8cbc476ba2df6"}, + {file = "xattr-0.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ef954d0655f93a34d07d0cc7e02765ec779ff0b59dc898ee08c6326ad614d5"}, + {file = "xattr-0.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:199b20301b6acc9022661412346714ce764d322068ef387c4de38062474db76c"}, + {file = "xattr-0.10.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec0956a8ab0f0d3f9011ba480f1e1271b703d11542375ef73eb8695a6bd4b78b"}, + {file = "xattr-0.10.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffcb57ca1be338d69edad93cf59aac7c6bb4dbb92fd7bf8d456c69ea42f7e6d2"}, + {file = "xattr-0.10.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f0563196ee54756fe2047627d316977dc77d11acd7a07970336e1a711e934db"}, + {file = "xattr-0.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc354f086f926a1c7f04886f97880fed1a26d20e3bc338d0d965fd161dbdb8ab"}, + {file = "xattr-0.10.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c0cd2d02ef2fb45ecf2b0da066a58472d54682c6d4f0452dfe7ae2f3a76a42ea"}, + {file = "xattr-0.10.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49626096ddd72dcc1654aadd84b103577d8424f26524a48d199847b5d55612d0"}, + {file = "xattr-0.10.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ceaa26bef8fcb17eb59d92a7481c2d15d20211e217772fb43c08c859b01afc6a"}, + {file = "xattr-0.10.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8c014c371391f28f8cd27d73ea59f42b30772cd640b5a2538ad4f440fd9190b"}, + {file = "xattr-0.10.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:46c32cd605673606b9388a313b0050ee7877a0640d7561eea243ace4fa2cc5a6"}, + {file = "xattr-0.10.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:772b22c4ff791fe5816a7c2a1c9fcba83f9ab9bea138eb44d4d70f34676232b4"}, + {file = "xattr-0.10.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:183ad611a2d70b5a3f5f7aadef0fcef604ea33dcf508228765fd4ddac2c7321d"}, + {file = "xattr-0.10.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8068df3ebdfa9411e58d5ae4a05d807ec5994645bb01af66ec9f6da718b65c5b"}, + {file = "xattr-0.10.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bc40570155beb85e963ae45300a530223d9822edfdf09991b880e69625ba38a"}, + {file = "xattr-0.10.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:436e1aaf23c07e15bed63115f1712d2097e207214fc6bcde147c1efede37e2c5"}, + {file = "xattr-0.10.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7298455ccf3a922d403339781b10299b858bb5ec76435445f2da46fb768e31a5"}, + {file = "xattr-0.10.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:986c2305c6c1a08f78611eb38ef9f1f47682774ce954efb5a4f3715e8da00d5f"}, + {file = "xattr-0.10.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5dc6099e76e33fa3082a905fe59df766b196534c705cf7a2e3ad9bed2b8a180e"}, + {file = "xattr-0.10.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:042ad818cda6013162c0bfd3816f6b74b7700e73c908cde6768da824686885f8"}, + {file = "xattr-0.10.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9d4c306828a45b41b76ca17adc26ac3dc00a80e01a5ba85d71df2a3e948828f2"}, + {file = "xattr-0.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a606280b0c9071ef52572434ecd3648407b20df3d27af02c6592e84486b05894"}, + {file = "xattr-0.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5b49d591cf34cda2079fd7a5cb2a7a1519f54dc2e62abe3e0720036f6ed41a85"}, + {file = "xattr-0.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8705ac6791426559c1a5c2b88bb2f0e83dc5616a09b4500899bfff6a929302"}, + {file = "xattr-0.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5ea974930e876bc5c146f54ac0f85bb39b7b5de2b6fc63f90364712ae368ebe"}, + {file = "xattr-0.10.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f55a2dd73a12a1ae5113c5d9cd4b4ab6bf7950f4d76d0a1a0c0c4264d50da61d"}, + {file = "xattr-0.10.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:475c38da0d3614cc5564467c4efece1e38bd0705a4dbecf8deeb0564a86fb010"}, + {file = "xattr-0.10.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:925284a4a28e369459b2b7481ea22840eed3e0573a4a4c06b6b0614ecd27d0a7"}, + {file = "xattr-0.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa32f1b45fed9122bed911de0fcc654da349e1f04fa4a9c8ef9b53e1cc98b91e"}, + {file = "xattr-0.10.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c5d3d0e728bace64b74c475eb4da6148cd172b2d23021a1dcd055d92f17619ac"}, + {file = "xattr-0.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8faaacf311e2b5cc67c030c999167a78a9906073e6abf08eaa8cf05b0416515c"}, + {file = "xattr-0.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cc6b8d5ca452674e1a96e246a3d2db5f477aecbc7c945c73f890f56323e75203"}, + {file = "xattr-0.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3725746a6502f40f72ef27e0c7bfc31052a239503ff3eefa807d6b02a249be22"}, + {file = "xattr-0.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:789bd406d1aad6735e97b20c6d6a1701e1c0661136be9be862e6a04564da771f"}, + {file = "xattr-0.10.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9a7a807ab538210ff8532220d8fc5e2d51c212681f63dbd4e7ede32543b070f"}, + {file = "xattr-0.10.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3e5825b5fc99ecdd493b0cc09ec35391e7a451394fdf623a88b24726011c950d"}, + {file = "xattr-0.10.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:80638d1ce7189dc52f26c234cee3522f060fadab6a8bc3562fe0ddcbe11ba5a4"}, + {file = "xattr-0.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3ff0dbe4a6ce2ce065c6de08f415bcb270ecfd7bf1655a633ddeac695ce8b250"}, + {file = "xattr-0.10.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5267e5f9435c840d2674194150b511bef929fa7d3bc942a4a75b9eddef18d8d8"}, + {file = "xattr-0.10.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b27dfc13b193cb290d5d9e62f806bb9a99b00cd73bb6370d556116ad7bb5dc12"}, + {file = "xattr-0.10.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:636ebdde0277bce4d12d2ef2550885804834418fee0eb456b69be928e604ecc4"}, + {file = "xattr-0.10.1-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d60c27922ec80310b45574351f71e0dd3a139c5295e8f8b19d19c0010196544f"}, + {file = "xattr-0.10.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b34df5aad035d0343bd740a95ca30db99b776e2630dca9cc1ba8e682c9cc25ea"}, + {file = "xattr-0.10.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f24a7c04ff666d0fe905dfee0a84bc899d624aeb6dccd1ea86b5c347f15c20c1"}, + {file = "xattr-0.10.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3878e1aff8eca64badad8f6d896cb98c52984b1e9cd9668a3ab70294d1ef92d"}, + {file = "xattr-0.10.1-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4abef557028c551d59cf2fb3bf63f2a0c89f00d77e54c1c15282ecdd56943496"}, + {file = "xattr-0.10.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0e14bd5965d3db173d6983abdc1241c22219385c22df8b0eb8f1846c15ce1fee"}, + {file = "xattr-0.10.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f9be588a4b6043b03777d50654c6079af3da60cc37527dbb80d36ec98842b1e"}, + {file = "xattr-0.10.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bc4ae264aa679aacf964abf3ea88e147eb4a22aea6af8c6d03ebdebd64cfd6"}, + {file = "xattr-0.10.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:827b5a97673b9997067fde383a7f7dc67342403093b94ea3c24ae0f4f1fec649"}, + {file = "xattr-0.10.1.tar.gz", hash = "sha256:c12e7d81ffaa0605b3ac8c22c2994a8e18a9cf1c59287a1b7722a2289c952ec5"}, +] [package.dependencies] cffi = ">=1.0" [[package]] name = "zipp" -version = "3.9.0" +version = "3.14.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "zipp-3.14.0-py3-none-any.whl", hash = "sha256:188834565033387710d046e3fe96acfc9b5e86cbca7f39ff69cf21a4128198b7"}, + {file = "zipp-3.14.0.tar.gz", hash = "sha256:9e5421e176ef5ab4c0ad896624e87a7b2f07aca746c9b2aa305952800cb8eecb"}, +] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [metadata] -lock-version = "1.1" +lock-version = "2.0" python-versions = "^3.7" -content-hash = "7db62de4ee4f2831829fc08b6a3d190f177c62d5af7676e20fb228bb58274850" - -[metadata.files] -attrs = [ - {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, - {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, -] -"backports.cached-property" = [ - {file = "backports.cached-property-1.0.2.tar.gz", hash = "sha256:9306f9eed6ec55fd156ace6bc1094e2c86fae5fb2bf07b6a9c00745c656e75dd"}, - {file = "backports.cached_property-1.0.2-py3-none-any.whl", hash = "sha256:baeb28e1cd619a3c9ab8941431fe34e8490861fb998c6c4590693d50171db0cc"}, -] -CacheControl = [ - {file = "CacheControl-0.12.11-py2.py3-none-any.whl", hash = "sha256:2c75d6a8938cb1933c75c50184549ad42728a27e9f6b92fd677c3151aa72555b"}, - {file = "CacheControl-0.12.11.tar.gz", hash = "sha256:a5b9fcc986b184db101aa280b42ecdcdfc524892596f606858e0b7a8b4d9e144"}, -] -cachy = [ - {file = "cachy-0.3.0-py2.py3-none-any.whl", hash = "sha256:338ca09c8860e76b275aff52374330efedc4d5a5e45dc1c5b539c1ead0786fe7"}, - {file = "cachy-0.3.0.tar.gz", hash = "sha256:186581f4ceb42a0bbe040c407da73c14092379b1e4c0e327fdb72ae4a9b269b1"}, -] -certifi = [ - {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, - {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, -] -cffi = [ - {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, - {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, - {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, - {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, - {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, - {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, - {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, - {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, - {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, - {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, - {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, - {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, - {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, - {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, - {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, - {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, - {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, - {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, - {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, -] -cfgv = [ - {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, - {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, -] -charset-normalizer = [ - {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, - {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, -] -cleo = [ - {file = "cleo-1.0.0a5-py3-none-any.whl", hash = "sha256:ff53056589300976e960f75afb792dfbfc9c78dcbb5a448e207a17b643826360"}, - {file = "cleo-1.0.0a5.tar.gz", hash = "sha256:097c9d0e0332fd53cc89fc11eb0a6ba0309e6a3933c08f7b38558555486925d3"}, -] -colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, -] -coverage = [ - {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, - {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, - {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, - {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, - {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, - {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, - {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, - {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, - {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, - {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, - {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, - {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, - {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, - {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, - {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, - {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, - {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, - {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, - {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, - {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, - {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, - {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, - {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, - {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, - {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, - {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, - {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, - {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, - {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, - {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, - {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, - {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, - {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, - {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, - {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, - {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, - {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, - {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, - {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, - {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, - {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, - {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, - {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, - {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, - {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, - {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, - {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, - {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, - {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, - {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, -] -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-38.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f"}, - {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6"}, - {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a"}, - {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294"}, - {file = "cryptography-38.0.1-cp36-abi3-win32.whl", hash = "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0"}, - {file = "cryptography-38.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a"}, - {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d"}, - {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9"}, - {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b"}, - {file = "cryptography-38.0.1.tar.gz", hash = "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7"}, -] -deepdiff = [ - {file = "deepdiff-5.8.1-py3-none-any.whl", hash = "sha256:e9aea49733f34fab9a0897038d8f26f9d94a97db1790f1b814cced89e9e0d2b7"}, - {file = "deepdiff-5.8.1.tar.gz", hash = "sha256:8d4eb2c4e6cbc80b811266419cb71dd95a157094a3947ccf937a94d44943c7b8"}, -] -distlib = [ - {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, - {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, -] -dulwich = [ - {file = "dulwich-0.20.46-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:6676196e9cf377cde62aa2f5d741e93207437343e0c62368bd0d784c322a3c49"}, - {file = "dulwich-0.20.46-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a1ca555a3eafe7388d6cb81bb08f34608a1592500f0bd4c26734c91d208a546"}, - {file = "dulwich-0.20.46-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:769442c9657b10fc35ac625beeaf440540c9288c96fcfaba3e58adf745c5cafd"}, - {file = "dulwich-0.20.46-cp310-cp310-win32.whl", hash = "sha256:de22a54f68c6c4e97f9b924abd46da4618536d7934b9849066be9fc5cd31205d"}, - {file = "dulwich-0.20.46-cp310-cp310-win_amd64.whl", hash = "sha256:42fa5a68908556eb6c40f231a67caf6a4660588aad707a9d6b334fa1d8f04bf7"}, - {file = "dulwich-0.20.46-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:3e16376031466848e44aabf3489fafb054482143744b21167dbd168731041c74"}, - {file = "dulwich-0.20.46-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:153c7512587384a290c60fef330f1ab397a59559e19e8b02a0169ff21b4c69fb"}, - {file = "dulwich-0.20.46-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5b68bd815cd2769c75e5a78708eb0440612df19b370a977aa9e01a056baa9ed"}, - {file = "dulwich-0.20.46-cp311-cp311-win32.whl", hash = "sha256:b1339bca70764eb8e780d80c72e7c1cb4651201dc9e43ec5d616bf51eb3bb3a6"}, - {file = "dulwich-0.20.46-cp311-cp311-win_amd64.whl", hash = "sha256:1162fdafb2abdfe66649617061f3853cb26384fade1f6884f6fe6e9c570a7552"}, - {file = "dulwich-0.20.46-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:6826512f778eaa47e2e8c0a46cdc555958f9f5286771490b8642b4b508ea5d25"}, - {file = "dulwich-0.20.46-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:100d39bc18196a07c521fd5f60f78f397493303daa0b8690216864bbc621cd5d"}, - {file = "dulwich-0.20.46-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4cd2cd7baa81246bdc8c5272d4e9224e2255da7a0618a220aab5e07b9888e9b"}, - {file = "dulwich-0.20.46-cp36-cp36m-win32.whl", hash = "sha256:6eed5a3194d64112605fc0f638f4fa91771495e8674fa3e6d6b33bf150d297d5"}, - {file = "dulwich-0.20.46-cp36-cp36m-win_amd64.whl", hash = "sha256:9ca4d73987f5b0e2e843497876f9bb39a47384a2e50597a85542285f5c890293"}, - {file = "dulwich-0.20.46-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:b9f49de83911eed7adbe83136229837ef9d102e42dbe6aacb1a18be45c997ace"}, - {file = "dulwich-0.20.46-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38be7d3a78d608ecab3348f7920d6b9002e7972dd245206dc8075cfdb91621d"}, - {file = "dulwich-0.20.46-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4b7a7feb966a4669c254b18385fe0b3c639f3b1f5ddef0d9e083364cc762847"}, - {file = "dulwich-0.20.46-cp37-cp37m-win32.whl", hash = "sha256:f9552ac246bceab1c5cdd1ec3cfe9446fe76b9853eaf59d3244df03eb27fd3fe"}, - {file = "dulwich-0.20.46-cp37-cp37m-win_amd64.whl", hash = "sha256:90a075aeb0fdbad7e18b9db3af161e3d635e2b7697b7a4b467e6844a13b0b210"}, - {file = "dulwich-0.20.46-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:8d6fee82cedb2362942d9ef94061901f7e07d7d8674e4c7b6fceeef7822ae275"}, - {file = "dulwich-0.20.46-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:669c6b3d82996518a7fec4604771bd285e23f0860f41f565fef5987265d431d9"}, - {file = "dulwich-0.20.46-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd3eac228117487a959ac8f49ea2787eac34acc69999fe7adae70b23e3c3571c"}, - {file = "dulwich-0.20.46-cp38-cp38-win32.whl", hash = "sha256:92024f572d32680e021219f77015c8b443c38022e502b7f51ad7cf51a6285a36"}, - {file = "dulwich-0.20.46-cp38-cp38-win_amd64.whl", hash = "sha256:d928de1eba0326a2a8a52ed94c9bf7c315ff4db606a1aa3ae688d39574f93267"}, - {file = "dulwich-0.20.46-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:a5d1b7a3a7d84a5dedbb90092e00097357106b9642ac08a96c2ae89ccd8afd9a"}, - {file = "dulwich-0.20.46-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b739d759c10e2af7c964dcc97fd4e5dc49e8567d080eed8906fc422c79b7fdcf"}, - {file = "dulwich-0.20.46-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fc7a4f633f5468453d5dd84a753cd99d4433f0397437229a0a8b10347935591"}, - {file = "dulwich-0.20.46-cp39-cp39-win32.whl", hash = "sha256:525115c4d1fbf60a5fe98f340b4ca597ba47b2c75d9c5ec750dd0e9115ef8ec6"}, - {file = "dulwich-0.20.46-cp39-cp39-win_amd64.whl", hash = "sha256:73e2585a9fcf1f8cdad8597a0c384c0b365b2e8346463130c96d9ea1478587ae"}, - {file = "dulwich-0.20.46.tar.gz", hash = "sha256:4f0e88ffff5db1523d93d92f1525fe5fa161318ffbaad502c1b9b3be7a067172"}, -] -execnet = [ - {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, - {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, -] -filelock = [ - {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, - {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, -] -flatdict = [ - {file = "flatdict-4.0.1.tar.gz", hash = "sha256:cd32f08fd31ed21eb09ebc76f06b6bd12046a24f77beb1fd0281917e47f26742"}, -] -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-1.1.4.tar.gz", hash = "sha256:20de0e5dd5a18292d36d928cc3d6e52f8b2ac73daec40d41eb62dee154933b68"}, -] -identify = [ - {file = "identify-2.5.6-py2.py3-none-any.whl", hash = "sha256:b276db7ec52d7e89f5bc4653380e33054ddc803d25875952ad90b0f012cbcdaa"}, - {file = "identify-2.5.6.tar.gz", hash = "sha256:6c32dbd747aa4ceee1df33f25fed0b0f6e0d65721b15bd151307ff7056d50245"}, -] -idna = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] -importlib-metadata = [ - {file = "importlib_metadata-4.13.0-py3-none-any.whl", hash = "sha256:8a8a81bcf996e74fee46f0d16bd3eaa382a7eb20fd82445c3ad11f4090334116"}, - {file = "importlib_metadata-4.13.0.tar.gz", hash = "sha256:dd0173e8f150d6815e098fd354f6414b0f079af4644ddfe90c71e2fc6174346d"}, -] -importlib-resources = [ - {file = "importlib_resources-5.10.0-py3-none-any.whl", hash = "sha256:ee17ec648f85480d523596ce49eae8ead87d5631ae1551f913c0100b5edd3437"}, - {file = "importlib_resources-5.10.0.tar.gz", hash = "sha256:c01b1b94210d9849f286b86bb51bcea7cd56dde0600d8db721d7b81330711668"}, -] -iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, -] -"jaraco.classes" = [ - {file = "jaraco.classes-3.2.3-py3-none-any.whl", hash = "sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158"}, - {file = "jaraco.classes-3.2.3.tar.gz", hash = "sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a"}, -] -jeepney = [ - {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, - {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, -] -jsonschema = [ - {file = "jsonschema-4.16.0-py3-none-any.whl", hash = "sha256:9e74b8f9738d6a946d70705dc692b74b5429cd0960d58e79ffecfc43b2221eb9"}, - {file = "jsonschema-4.16.0.tar.gz", hash = "sha256:165059f076eff6971bae5b742fc029a7b4ef3f9bcf04c14e4776a7605de14b23"}, -] -keyring = [ - {file = "keyring-23.9.3-py3-none-any.whl", hash = "sha256:69732a15cb1433bdfbc3b980a8a36a04878a6cfd7cb99f497b573f31618001c0"}, - {file = "keyring-23.9.3.tar.gz", hash = "sha256:69b01dd83c42f590250fe7a1f503fc229b14de83857314b1933a3ddbf595c4a5"}, -] -lockfile = [ - {file = "lockfile-0.12.2-py2.py3-none-any.whl", hash = "sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa"}, - {file = "lockfile-0.12.2.tar.gz", hash = "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799"}, -] -more-itertools = [ - {file = "more-itertools-8.14.0.tar.gz", hash = "sha256:c09443cd3d5438b8dafccd867a6bc1cb0894389e90cb53d227456b0b0bccb750"}, - {file = "more_itertools-8.14.0-py3-none-any.whl", hash = "sha256:1bc4f91ee5b1b31ac7ceacc17c09befe6a40a503907baf9c839c229b5095cfd2"}, -] -msgpack = [ - {file = "msgpack-1.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4ab251d229d10498e9a2f3b1e68ef64cb393394ec477e3370c457f9430ce9250"}, - {file = "msgpack-1.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:112b0f93202d7c0fef0b7810d465fde23c746a2d482e1e2de2aafd2ce1492c88"}, - {file = "msgpack-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:002b5c72b6cd9b4bafd790f364b8480e859b4712e91f43014fe01e4f957b8467"}, - {file = "msgpack-1.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35bc0faa494b0f1d851fd29129b2575b2e26d41d177caacd4206d81502d4c6a6"}, - {file = "msgpack-1.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4733359808c56d5d7756628736061c432ded018e7a1dff2d35a02439043321aa"}, - {file = "msgpack-1.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb514ad14edf07a1dbe63761fd30f89ae79b42625731e1ccf5e1f1092950eaa6"}, - {file = "msgpack-1.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c23080fdeec4716aede32b4e0ef7e213c7b1093eede9ee010949f2a418ced6ba"}, - {file = "msgpack-1.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:49565b0e3d7896d9ea71d9095df15b7f75a035c49be733051c34762ca95bbf7e"}, - {file = "msgpack-1.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:aca0f1644d6b5a73eb3e74d4d64d5d8c6c3d577e753a04c9e9c87d07692c58db"}, - {file = "msgpack-1.0.4-cp310-cp310-win32.whl", hash = "sha256:0dfe3947db5fb9ce52aaea6ca28112a170db9eae75adf9339a1aec434dc954ef"}, - {file = "msgpack-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dea20515f660aa6b7e964433b1808d098dcfcabbebeaaad240d11f909298075"}, - {file = "msgpack-1.0.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e83f80a7fec1a62cf4e6c9a660e39c7f878f603737a0cdac8c13131d11d97f52"}, - {file = "msgpack-1.0.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c11a48cf5e59026ad7cb0dc29e29a01b5a66a3e333dc11c04f7e991fc5510a9"}, - {file = "msgpack-1.0.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1276e8f34e139aeff1c77a3cefb295598b504ac5314d32c8c3d54d24fadb94c9"}, - {file = "msgpack-1.0.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c9566f2c39ccced0a38d37c26cc3570983b97833c365a6044edef3574a00c08"}, - {file = "msgpack-1.0.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:fcb8a47f43acc113e24e910399376f7277cf8508b27e5b88499f053de6b115a8"}, - {file = "msgpack-1.0.4-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:76ee788122de3a68a02ed6f3a16bbcd97bc7c2e39bd4d94be2f1821e7c4a64e6"}, - {file = "msgpack-1.0.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0a68d3ac0104e2d3510de90a1091720157c319ceeb90d74f7b5295a6bee51bae"}, - {file = "msgpack-1.0.4-cp36-cp36m-win32.whl", hash = "sha256:85f279d88d8e833ec015650fd15ae5eddce0791e1e8a59165318f371158efec6"}, - {file = "msgpack-1.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:c1683841cd4fa45ac427c18854c3ec3cd9b681694caf5bff04edb9387602d661"}, - {file = "msgpack-1.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a75dfb03f8b06f4ab093dafe3ddcc2d633259e6c3f74bb1b01996f5d8aa5868c"}, - {file = "msgpack-1.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9667bdfdf523c40d2511f0e98a6c9d3603be6b371ae9a238b7ef2dc4e7a427b0"}, - {file = "msgpack-1.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11184bc7e56fd74c00ead4f9cc9a3091d62ecb96e97653add7a879a14b003227"}, - {file = "msgpack-1.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac5bd7901487c4a1dd51a8c58f2632b15d838d07ceedaa5e4c080f7190925bff"}, - {file = "msgpack-1.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1e91d641d2bfe91ba4c52039adc5bccf27c335356055825c7f88742c8bb900dd"}, - {file = "msgpack-1.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2a2df1b55a78eb5f5b7d2a4bb221cd8363913830145fad05374a80bf0877cb1e"}, - {file = "msgpack-1.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:545e3cf0cf74f3e48b470f68ed19551ae6f9722814ea969305794645da091236"}, - {file = "msgpack-1.0.4-cp37-cp37m-win32.whl", hash = "sha256:2cc5ca2712ac0003bcb625c96368fd08a0f86bbc1a5578802512d87bc592fe44"}, - {file = "msgpack-1.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:eba96145051ccec0ec86611fe9cf693ce55f2a3ce89c06ed307de0e085730ec1"}, - {file = "msgpack-1.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7760f85956c415578c17edb39eed99f9181a48375b0d4a94076d84148cf67b2d"}, - {file = "msgpack-1.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:449e57cc1ff18d3b444eb554e44613cffcccb32805d16726a5494038c3b93dab"}, - {file = "msgpack-1.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d603de2b8d2ea3f3bcb2efe286849aa7a81531abc52d8454da12f46235092bcb"}, - {file = "msgpack-1.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f5d88c99f64c456413d74a975bd605a9b0526293218a3b77220a2c15458ba9"}, - {file = "msgpack-1.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916c78f33602ecf0509cc40379271ba0f9ab572b066bd4bdafd7434dee4bc6e"}, - {file = "msgpack-1.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81fc7ba725464651190b196f3cd848e8553d4d510114a954681fd0b9c479d7e1"}, - {file = "msgpack-1.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d5b5b962221fa2c5d3a7f8133f9abffc114fe218eb4365e40f17732ade576c8e"}, - {file = "msgpack-1.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:77ccd2af37f3db0ea59fb280fa2165bf1b096510ba9fe0cc2bf8fa92a22fdb43"}, - {file = "msgpack-1.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b17be2478b622939e39b816e0aa8242611cc8d3583d1cd8ec31b249f04623243"}, - {file = "msgpack-1.0.4-cp38-cp38-win32.whl", hash = "sha256:2bb8cdf50dd623392fa75525cce44a65a12a00c98e1e37bf0fb08ddce2ff60d2"}, - {file = "msgpack-1.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:26b8feaca40a90cbe031b03d82b2898bf560027160d3eae1423f4a67654ec5d6"}, - {file = "msgpack-1.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:462497af5fd4e0edbb1559c352ad84f6c577ffbbb708566a0abaaa84acd9f3ae"}, - {file = "msgpack-1.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2999623886c5c02deefe156e8f869c3b0aaeba14bfc50aa2486a0415178fce55"}, - {file = "msgpack-1.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f0029245c51fd9473dc1aede1160b0a29f4a912e6b1dd353fa6d317085b219da"}, - {file = "msgpack-1.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed6f7b854a823ea44cf94919ba3f727e230da29feb4a99711433f25800cf747f"}, - {file = "msgpack-1.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df96d6eaf45ceca04b3f3b4b111b86b33785683d682c655063ef8057d61fd92"}, - {file = "msgpack-1.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a4192b1ab40f8dca3f2877b70e63799d95c62c068c84dc028b40a6cb03ccd0f"}, - {file = "msgpack-1.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e3590f9fb9f7fbc36df366267870e77269c03172d086fa76bb4eba8b2b46624"}, - {file = "msgpack-1.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1576bd97527a93c44fa856770197dec00d223b0b9f36ef03f65bac60197cedf8"}, - {file = "msgpack-1.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:63e29d6e8c9ca22b21846234913c3466b7e4ee6e422f205a2988083de3b08cae"}, - {file = "msgpack-1.0.4-cp39-cp39-win32.whl", hash = "sha256:fb62ea4b62bfcb0b380d5680f9a4b3f9a2d166d9394e9bbd9666c0ee09a3645c"}, - {file = "msgpack-1.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:4d5834a2a48965a349da1c5a79760d94a1a0172fbb5ab6b5b33cbf8447e109ce"}, - {file = "msgpack-1.0.4.tar.gz", hash = "sha256:f5d869c18f030202eb412f08b28d2afeea553d6613aee89e200d7aca7ef01f5f"}, -] -mypy = [ - {file = "mypy-0.982-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5085e6f442003fa915aeb0a46d4da58128da69325d8213b4b35cc7054090aed5"}, - {file = "mypy-0.982-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:41fd1cf9bc0e1c19b9af13a6580ccb66c381a5ee2cf63ee5ebab747a4badeba3"}, - {file = "mypy-0.982-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f793e3dd95e166b66d50e7b63e69e58e88643d80a3dcc3bcd81368e0478b089c"}, - {file = "mypy-0.982-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86ebe67adf4d021b28c3f547da6aa2cce660b57f0432617af2cca932d4d378a6"}, - {file = "mypy-0.982-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:175f292f649a3af7082fe36620369ffc4661a71005aa9f8297ea473df5772046"}, - {file = "mypy-0.982-cp310-cp310-win_amd64.whl", hash = "sha256:8ee8c2472e96beb1045e9081de8e92f295b89ac10c4109afdf3a23ad6e644f3e"}, - {file = "mypy-0.982-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58f27ebafe726a8e5ccb58d896451dd9a662a511a3188ff6a8a6a919142ecc20"}, - {file = "mypy-0.982-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6af646bd46f10d53834a8e8983e130e47d8ab2d4b7a97363e35b24e1d588947"}, - {file = "mypy-0.982-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7aeaa763c7ab86d5b66ff27f68493d672e44c8099af636d433a7f3fa5596d40"}, - {file = "mypy-0.982-cp37-cp37m-win_amd64.whl", hash = "sha256:724d36be56444f569c20a629d1d4ee0cb0ad666078d59bb84f8f887952511ca1"}, - {file = "mypy-0.982-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14d53cdd4cf93765aa747a7399f0961a365bcddf7855d9cef6306fa41de01c24"}, - {file = "mypy-0.982-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:26ae64555d480ad4b32a267d10cab7aec92ff44de35a7cd95b2b7cb8e64ebe3e"}, - {file = "mypy-0.982-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6389af3e204975d6658de4fb8ac16f58c14e1bacc6142fee86d1b5b26aa52bda"}, - {file = "mypy-0.982-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b35ce03a289480d6544aac85fa3674f493f323d80ea7226410ed065cd46f206"}, - {file = "mypy-0.982-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c6e564f035d25c99fd2b863e13049744d96bd1947e3d3d2f16f5828864506763"}, - {file = "mypy-0.982-cp38-cp38-win_amd64.whl", hash = "sha256:cebca7fd333f90b61b3ef7f217ff75ce2e287482206ef4a8b18f32b49927b1a2"}, - {file = "mypy-0.982-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a705a93670c8b74769496280d2fe6cd59961506c64f329bb179970ff1d24f9f8"}, - {file = "mypy-0.982-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75838c649290d83a2b83a88288c1eb60fe7a05b36d46cbea9d22efc790002146"}, - {file = "mypy-0.982-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:91781eff1f3f2607519c8b0e8518aad8498af1419e8442d5d0afb108059881fc"}, - {file = "mypy-0.982-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaa97b9ddd1dd9901a22a879491dbb951b5dec75c3b90032e2baa7336777363b"}, - {file = "mypy-0.982-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a692a8e7d07abe5f4b2dd32d731812a0175626a90a223d4b58f10f458747dd8a"}, - {file = "mypy-0.982-cp39-cp39-win_amd64.whl", hash = "sha256:eb7a068e503be3543c4bd329c994103874fa543c1727ba5288393c21d912d795"}, - {file = "mypy-0.982-py3-none-any.whl", hash = "sha256:1021c241e8b6e1ca5a47e4d52601274ac078a89845cfde66c6d5f769819ffa1d"}, - {file = "mypy-0.982.tar.gz", hash = "sha256:85f7a343542dc8b1ed0a888cdd34dca56462654ef23aa673907305b260b3d746"}, -] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -nodeenv = [ - {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, - {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, -] -ordered-set = [ - {file = "ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8"}, - {file = "ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562"}, -] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] -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.8.3-py2.py3-none-any.whl", hash = "sha256:848865108ec99d4901b2f7e84058b6e7660aae8ae10164e015a6dcf5b242a594"}, - {file = "pkginfo-1.8.3.tar.gz", hash = "sha256:a84da4318dd86f870a9447a8c98340aa06216bfc6f2b7bdc4b8766984ae1867c"}, -] -pkgutil_resolve_name = [ - {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, - {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, -] -platformdirs = [ - {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, - {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, -] -pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] -poetry-core = [ - {file = "poetry-core-1.3.2.tar.gz", hash = "sha256:0ab006a40cb38d6a38b97264f6835da2f08a96912f2728ce668e9ac6a34f686f"}, - {file = "poetry_core-1.3.2-py3-none-any.whl", hash = "sha256:ea0f5a90b339cde132b4e43cff78a1b440cd928db864bb67cfc97fdfcefe7218"}, -] -poetry-plugin-export = [ - {file = "poetry-plugin-export-1.1.2.tar.gz", hash = "sha256:5e92525dd63f38ce74a51ed68ea91d753523f21ce5f9ef8d3b793e2a4b2222ef"}, - {file = "poetry_plugin_export-1.1.2-py3-none-any.whl", hash = "sha256:946e3313b3d00c18fb9a50522e9d5e6a7e111beaba8d6ae33297662fc2070ac1"}, -] -pre-commit = [ - {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"}, - {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"}, -] -psutil = [ - {file = "psutil-5.9.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:8f024fbb26c8daf5d70287bb3edfafa22283c255287cf523c5d81721e8e5d82c"}, - {file = "psutil-5.9.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:b2f248ffc346f4f4f0d747ee1947963613216b06688be0be2e393986fe20dbbb"}, - {file = "psutil-5.9.2-cp27-cp27m-win32.whl", hash = "sha256:b1928b9bf478d31fdffdb57101d18f9b70ed4e9b0e41af751851813547b2a9ab"}, - {file = "psutil-5.9.2-cp27-cp27m-win_amd64.whl", hash = "sha256:404f4816c16a2fcc4eaa36d7eb49a66df2d083e829d3e39ee8759a411dbc9ecf"}, - {file = "psutil-5.9.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:94e621c6a4ddb2573d4d30cba074f6d1aa0186645917df42c811c473dd22b339"}, - {file = "psutil-5.9.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:256098b4f6ffea6441eb54ab3eb64db9ecef18f6a80d7ba91549195d55420f84"}, - {file = "psutil-5.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:614337922702e9be37a39954d67fdb9e855981624d8011a9927b8f2d3c9625d9"}, - {file = "psutil-5.9.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39ec06dc6c934fb53df10c1672e299145ce609ff0611b569e75a88f313634969"}, - {file = "psutil-5.9.2-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3ac2c0375ef498e74b9b4ec56df3c88be43fe56cac465627572dbfb21c4be34"}, - {file = "psutil-5.9.2-cp310-cp310-win32.whl", hash = "sha256:e4c4a7636ffc47b7141864f1c5e7d649f42c54e49da2dd3cceb1c5f5d29bfc85"}, - {file = "psutil-5.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:f4cb67215c10d4657e320037109939b1c1d2fd70ca3d76301992f89fe2edb1f1"}, - {file = "psutil-5.9.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:dc9bda7d5ced744622f157cc8d8bdd51735dafcecff807e928ff26bdb0ff097d"}, - {file = "psutil-5.9.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75291912b945a7351d45df682f9644540d564d62115d4a20d45fa17dc2d48f8"}, - {file = "psutil-5.9.2-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4018d5f9b6651f9896c7a7c2c9f4652e4eea53f10751c4e7d08a9093ab587ec"}, - {file = "psutil-5.9.2-cp36-cp36m-win32.whl", hash = "sha256:f40ba362fefc11d6bea4403f070078d60053ed422255bd838cd86a40674364c9"}, - {file = "psutil-5.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9770c1d25aee91417eba7869139d629d6328a9422ce1cdd112bd56377ca98444"}, - {file = "psutil-5.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:42638876b7f5ef43cef8dcf640d3401b27a51ee3fa137cb2aa2e72e188414c32"}, - {file = "psutil-5.9.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91aa0dac0c64688667b4285fa29354acfb3e834e1fd98b535b9986c883c2ce1d"}, - {file = "psutil-5.9.2-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fb54941aac044a61db9d8eb56fc5bee207db3bc58645d657249030e15ba3727"}, - {file = "psutil-5.9.2-cp37-cp37m-win32.whl", hash = "sha256:7cbb795dcd8ed8fd238bc9e9f64ab188f3f4096d2e811b5a82da53d164b84c3f"}, - {file = "psutil-5.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:5d39e3a2d5c40efa977c9a8dd4f679763c43c6c255b1340a56489955dbca767c"}, - {file = "psutil-5.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd331866628d18223a4265371fd255774affd86244fc307ef66eaf00de0633d5"}, - {file = "psutil-5.9.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b315febaebae813326296872fdb4be92ad3ce10d1d742a6b0c49fb619481ed0b"}, - {file = "psutil-5.9.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7929a516125f62399d6e8e026129c8835f6c5a3aab88c3fff1a05ee8feb840d"}, - {file = "psutil-5.9.2-cp38-cp38-win32.whl", hash = "sha256:561dec454853846d1dd0247b44c2e66a0a0c490f937086930ec4b8f83bf44f06"}, - {file = "psutil-5.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:67b33f27fc0427483b61563a16c90d9f3b547eeb7af0ef1b9fe024cdc9b3a6ea"}, - {file = "psutil-5.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b3591616fa07b15050b2f87e1cdefd06a554382e72866fcc0ab2be9d116486c8"}, - {file = "psutil-5.9.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b29f581b5edab1f133563272a6011925401804d52d603c5c606936b49c8b97"}, - {file = "psutil-5.9.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4642fd93785a29353d6917a23e2ac6177308ef5e8be5cc17008d885cb9f70f12"}, - {file = "psutil-5.9.2-cp39-cp39-win32.whl", hash = "sha256:ed29ea0b9a372c5188cdb2ad39f937900a10fb5478dc077283bf86eeac678ef1"}, - {file = "psutil-5.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:68b35cbff92d1f7103d8f1db77c977e72f49fcefae3d3d2b91c76b0e7aef48b8"}, - {file = "psutil-5.9.2.tar.gz", hash = "sha256:feb861a10b6c3bb00701063b37e4afc754f8217f0f09c42280586bd6ac712b5c"}, -] -ptyprocess = [ - {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.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] -pycparser = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] -pylev = [ - {file = "pylev-1.4.0-py2.py3-none-any.whl", hash = "sha256:7b2e2aa7b00e05bb3f7650eb506fc89f474f70493271a35c242d9a92188ad3dd"}, - {file = "pylev-1.4.0.tar.gz", hash = "sha256:9e77e941042ad3a4cc305dcdf2b2dec1aec2fbe3dd9015d2698ad02b173006d1"}, -] -pyparsing = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] -pyrsistent = [ - {file = "pyrsistent-0.18.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:df46c854f490f81210870e509818b729db4488e1f30f2a1ce1698b2295a878d1"}, - {file = "pyrsistent-0.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d45866ececf4a5fff8742c25722da6d4c9e180daa7b405dc0a2a2790d668c26"}, - {file = "pyrsistent-0.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ed6784ceac462a7d6fcb7e9b663e93b9a6fb373b7f43594f9ff68875788e01e"}, - {file = "pyrsistent-0.18.1-cp310-cp310-win32.whl", hash = "sha256:e4f3149fd5eb9b285d6bfb54d2e5173f6a116fe19172686797c056672689daf6"}, - {file = "pyrsistent-0.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:636ce2dc235046ccd3d8c56a7ad54e99d5c1cd0ef07d9ae847306c91d11b5fec"}, - {file = "pyrsistent-0.18.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e92a52c166426efbe0d1ec1332ee9119b6d32fc1f0bbfd55d5c1088070e7fc1b"}, - {file = "pyrsistent-0.18.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7a096646eab884bf8bed965bad63ea327e0d0c38989fc83c5ea7b8a87037bfc"}, - {file = "pyrsistent-0.18.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdfd2c361b8a8e5d9499b9082b501c452ade8bbf42aef97ea04854f4a3f43b22"}, - {file = "pyrsistent-0.18.1-cp37-cp37m-win32.whl", hash = "sha256:7ec335fc998faa4febe75cc5268a9eac0478b3f681602c1f27befaf2a1abe1d8"}, - {file = "pyrsistent-0.18.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6455fc599df93d1f60e1c5c4fe471499f08d190d57eca040c0ea182301321286"}, - {file = "pyrsistent-0.18.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fd8da6d0124efa2f67d86fa70c851022f87c98e205f0594e1fae044e7119a5a6"}, - {file = "pyrsistent-0.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bfe2388663fd18bd8ce7db2c91c7400bf3e1a9e8bd7d63bf7e77d39051b85ec"}, - {file = "pyrsistent-0.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e3e1fcc45199df76053026a51cc59ab2ea3fc7c094c6627e93b7b44cdae2c8c"}, - {file = "pyrsistent-0.18.1-cp38-cp38-win32.whl", hash = "sha256:b568f35ad53a7b07ed9b1b2bae09eb15cdd671a5ba5d2c66caee40dbf91c68ca"}, - {file = "pyrsistent-0.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1b96547410f76078eaf66d282ddca2e4baae8964364abb4f4dcdde855cd123a"}, - {file = "pyrsistent-0.18.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f87cc2863ef33c709e237d4b5f4502a62a00fab450c9e020892e8e2ede5847f5"}, - {file = "pyrsistent-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc66318fb7ee012071b2792024564973ecc80e9522842eb4e17743604b5e045"}, - {file = "pyrsistent-0.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:914474c9f1d93080338ace89cb2acee74f4f666fb0424896fcfb8d86058bf17c"}, - {file = "pyrsistent-0.18.1-cp39-cp39-win32.whl", hash = "sha256:1b34eedd6812bf4d33814fca1b66005805d3640ce53140ab8bbb1e2651b0d9bc"}, - {file = "pyrsistent-0.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:e24a828f57e0c337c8d8bb9f6b12f09dfdf0273da25fda9e314f0b684b415a07"}, - {file = "pyrsistent-0.18.1.tar.gz", hash = "sha256:d4d61f8b993a7255ba714df3aca52700f8125289f84f704cf80916517c46eb96"}, -] -pytest = [ - {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, - {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, -] -pytest-cov = [ - {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, - {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, -] -pytest-forked = [ - {file = "pytest-forked-1.4.0.tar.gz", hash = "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e"}, - {file = "pytest_forked-1.4.0-py3-none-any.whl", hash = "sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8"}, -] -pytest-github-actions-annotate-failures = [ - {file = "pytest-github-actions-annotate-failures-0.1.7.tar.gz", hash = "sha256:c6af8f9d13f1f09ef4c104a30875a4975db131ddbba979c8e48fdc456c8dde1f"}, - {file = "pytest_github_actions_annotate_failures-0.1.7-py2.py3-none-any.whl", hash = "sha256:c4a7346d1d95f731a6b53e9a45f10ca56593978149266dd7526876cce403ea38"}, -] -pytest-mock = [ - {file = "pytest-mock-3.10.0.tar.gz", hash = "sha256:fbbdb085ef7c252a326fd8cdcac0aa3b1333d8811f131bdcc701002e1be7ed4f"}, - {file = "pytest_mock-3.10.0-py3-none-any.whl", hash = "sha256:f4c973eeae0282963eb293eb173ce91b091a79c1334455acfac9ddee8a1c784b"}, -] -pytest-randomly = [ - {file = "pytest-randomly-3.12.0.tar.gz", hash = "sha256:d60c2db71ac319aee0fc6c4110a7597d611a8b94a5590918bfa8583f00caccb2"}, - {file = "pytest_randomly-3.12.0-py3-none-any.whl", hash = "sha256:f4f2e803daf5d1ba036cc22bf4fe9dbbf99389ec56b00e5cba732fb5c1d07fdd"}, -] -pytest-xdist = [ - {file = "pytest-xdist-2.5.0.tar.gz", hash = "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf"}, - {file = "pytest_xdist-2.5.0-py3-none-any.whl", hash = "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65"}, -] -pywin32-ctypes = [ - {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, - {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, -] -PyYAML = [ - {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, - {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, - {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, - {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, - {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, - {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, - {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, - {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, - {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, - {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, - {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, - {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, - {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, - {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, - {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, - {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, - {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, -] -requests = [ - {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, - {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, -] -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"}, -] -SecretStorage = [ - {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, - {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, -] -setuptools = [ - {file = "setuptools-65.4.1-py3-none-any.whl", hash = "sha256:1b6bdc6161661409c5f21508763dc63ab20a9ac2f8ba20029aaaa7fdb9118012"}, - {file = "setuptools-65.4.1.tar.gz", hash = "sha256:3050e338e5871e70c72983072fe34f6032ae1cdeeeb67338199c2f74e083a80e"}, -] -shellingham = [ - {file = "shellingham-1.5.0-py2.py3-none-any.whl", hash = "sha256:a8f02ba61b69baaa13facdba62908ca8690a94b8119b69f5ec5873ea85f7391b"}, - {file = "shellingham-1.5.0.tar.gz", hash = "sha256:72fb7f5c63103ca2cb91b23dee0c71fe8ad6fbfd46418ef17dbe40db51592dad"}, -] -six = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] -toml = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] -tomlkit = [ - {file = "tomlkit-0.11.5-py3-none-any.whl", hash = "sha256:f2ef9da9cef846ee027947dc99a45d6b68a63b0ebc21944649505bf2e8bc5fe7"}, - {file = "tomlkit-0.11.5.tar.gz", hash = "sha256:571854ebbb5eac89abcb4a2e47d7ea27b89bf29e09c35395da6f03dd4ae23d1c"}, -] -trove-classifiers = [ - {file = "trove-classifiers-2022.9.26.tar.gz", hash = "sha256:4fda647dabbc8e2ae3e0188c91823f57a7040fa436ebdefa8c9b41180bea243e"}, - {file = "trove_classifiers-2022.9.26-py3-none-any.whl", hash = "sha256:9b552c49617946de9df75687cfd556bce346aead331477a3a41a26ab66d6a1fe"}, -] -typed-ast = [ - {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, - {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, - {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, - {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, - {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, - {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, - {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, - {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, - {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, - {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, -] -types-html5lib = [ - {file = "types-html5lib-1.1.11.tar.gz", hash = "sha256:2b67bbaf3125b840720dc5890f243c3661583a503f0ed33166acf31c67e53717"}, - {file = "types_html5lib-1.1.11-py3-none-any.whl", hash = "sha256:dda54159be6ef58a67bf10bdd6fe5b4559e55e1df6bb18c47915281a8be0e5fd"}, -] -types-jsonschema = [ - {file = "types-jsonschema-4.16.1.tar.gz", hash = "sha256:95e31d2b90da218faf3d8fa34fa33ae55fc52c79b2cb7308755cc2d7d71b1096"}, - {file = "types_jsonschema-4.16.1-py3-none-any.whl", hash = "sha256:21ca9a227185b83655c71755b5834c36d66ca43f9de77c018d61c4f917f851ab"}, -] -types-requests = [ - {file = "types-requests-2.28.11.2.tar.gz", hash = "sha256:fdcd7bd148139fb8eef72cf4a41ac7273872cad9e6ada14b11ff5dfdeee60ed3"}, - {file = "types_requests-2.28.11.2-py3-none-any.whl", hash = "sha256:14941f8023a80b16441b3b46caffcbfce5265fd14555844d6029697824b5a2ef"}, -] -types-urllib3 = [ - {file = "types-urllib3-1.26.25.tar.gz", hash = "sha256:5aef0e663724eef924afa8b320b62ffef2c1736c1fa6caecfc9bc6c8ae2c3def"}, - {file = "types_urllib3-1.26.25-py3-none-any.whl", hash = "sha256:c1d78cef7bd581e162e46c20a57b2e1aa6ebecdcf01fd0713bb90978ff3e3427"}, -] -typing-extensions = [ - {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, - {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, -] -urllib3 = [ - {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, - {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, -] -virtualenv = [ - {file = "virtualenv-20.16.5-py3-none-any.whl", hash = "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27"}, - {file = "virtualenv-20.16.5.tar.gz", hash = "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da"}, -] -webencodings = [ - {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, - {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, -] -xattr = [ - {file = "xattr-0.9.9-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:58a9fb4fd19b467e88f4b75b5243706caa57e312d3aee757b53b57c7fd0f4ba9"}, - {file = "xattr-0.9.9-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e71efca59705c7abde5b7f76323ebe00ed2977f10cba4204b9421dada036b5ca"}, - {file = "xattr-0.9.9-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:1aad96b6603961c3d1ca1aaa8369b1a8d684a7b37357b2428087c286bf0e561c"}, - {file = "xattr-0.9.9-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:46cb74f98d31d9d70f975ec3e6554360a9bdcbb4b9fb50a69fabe54f9f928c97"}, - {file = "xattr-0.9.9-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:80c2db56058a687d7439be041f916cbeb2943fbe2623e53d5da721a4552d8991"}, - {file = "xattr-0.9.9-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:c360d1cc42e885b64d84f64de3c501dd7bce576248327ef583b4625ee63aa023"}, - {file = "xattr-0.9.9-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:debd87afe6bdf88c3689bde52eecf2b166388b13ef7388259d23223374db417d"}, - {file = "xattr-0.9.9-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:4280c9f33a8678828f1bbc3d3dc8b823b5e4a113ee5ecb0fb98bff60cc2b9ad1"}, - {file = "xattr-0.9.9-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:e0916ec1656d2071cd3139d1f52426825985d8ed076f981ef7f0bc13dfa8e96c"}, - {file = "xattr-0.9.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a517916fbf2f58a3222bb2048fe1eeff4e23e07a4ce6228a27de004c80bf53ab"}, - {file = "xattr-0.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e886c882b3b28c7a684c3e3daf46347da5428a46b88bc6d62c4867d574b90c54"}, - {file = "xattr-0.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:373e3d1fd9258438fc38d1438142d3659f36743f374a20457346ef26741ed441"}, - {file = "xattr-0.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a7beeb54ca140273b2f6320bb98b701ec30628af2ebe4eb30f7051419eb4ef3"}, - {file = "xattr-0.9.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef3ca29cdaae9c47c625d84bb6c9046f7275cccde0ea805caa23ca58d3671f3f"}, - {file = "xattr-0.9.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c381d890931cd18b137ce3fb5c5f08b672c3c61e2e47b1a7442ee46e827abfe"}, - {file = "xattr-0.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:59c5783ccf57cf2700ce57d51a92134900ed26f6ab20d209f383fb898903fea6"}, - {file = "xattr-0.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:966b885b69d95362e2a12d39f84889cf857090e57263b5ac33409498aa00c160"}, - {file = "xattr-0.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efaaf0cb1ea8e9febb7baad301ae8cc9ad7a96fdfc5c6399d165e7a19e3e61ce"}, - {file = "xattr-0.9.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f19fa75ed1e9db86354efab29869cb2be6976d456bd7c89e67b118d5384a1d98"}, - {file = "xattr-0.9.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ca28ad06828244b315214ee35388f57e81e90aac2ceac3f32e42ae394e31b9c"}, - {file = "xattr-0.9.9-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:532c7f1656dd2fe937116b9e210229f716d7fc7ac142f9cdace7da92266d32e8"}, - {file = "xattr-0.9.9-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11c28033c17e98c67e0def9d6ebd415ad3c006a7bc3fee6bad79c5e52d0dff49"}, - {file = "xattr-0.9.9-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:473cabb30e544ea08c8c01c1ef18053147cdc8552d443ac97815e46fbb13c7d4"}, - {file = "xattr-0.9.9-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c4a308522b444d090fbd66a385c9519b6b977818226921b0d2fc403667c93564"}, - {file = "xattr-0.9.9-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:82493434488aca72d88b5129dac8f212e7b8bdca7ceffe7bb977c850f2452e4e"}, - {file = "xattr-0.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e41d289706c7e8940f4d08e865da6a8ae988123e40a44f9a97ddc09e67795d7d"}, - {file = "xattr-0.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef08698e360cf43688dca3db3421b156b29948a714d5d089348073f463c11646"}, - {file = "xattr-0.9.9-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4eb10ac16ca8d534c0395425d52121e0c1981f808e1b3f577f6a5ec33d3853e4"}, - {file = "xattr-0.9.9-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5605fec07b0e964bd980cc70ec335b9eb1b7ac7c6f314c7c2d8f54b09104fe4c"}, - {file = "xattr-0.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:974e7d577ddb15e4552fb0ec10a4cfe09bdf6267365aa2b8394bb04637785aad"}, - {file = "xattr-0.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ad6777de922c638bfa87a0d7faebc5722ddef04a1210b2a8909289b58b769af0"}, - {file = "xattr-0.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3887e70873ebf0efbde32f9929ec1c7e45ec0013561743e2cc0406a91e51113b"}, - {file = "xattr-0.9.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:83caa8e93a45a0f25f91b92d9b45f490c87bff74f02555df6312efeba0dacc31"}, - {file = "xattr-0.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e33ec0a1d913d946d1ab7509f37ee37306c45af735347f13b963df34ffe6e029"}, - {file = "xattr-0.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:263c58dca83372260c5c195e0b59959e38e1f107f0b7350de82e3db38479036c"}, - {file = "xattr-0.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:125dfb9905428162349d3b8b825d9a18280893f0cb0db2a2467d5ef253fa6ce2"}, - {file = "xattr-0.9.9-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e243524e0dde16d7a2e1b52512ad2c6964df2143dd1c79b820dcb4c6c0822c20"}, - {file = "xattr-0.9.9-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01ec07d24a14406bdc6a123041c63a88e1c4a3f820e4a7d30f7609d57311b499"}, - {file = "xattr-0.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:85c1df5f1d209345ea96de137419e886a27bb55076b3ae01faacf35aafcf3a61"}, - {file = "xattr-0.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ca74d3eff92d6dc16e271fbad9cbab547fb9a0c983189c4031c3ff3d150dd871"}, - {file = "xattr-0.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7d17505e49ac70c0e71939c5aac96417a863583fb30a2d6304d5ac881230548f"}, - {file = "xattr-0.9.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1ae47a6398d3c04623fa386a4aa2f66e5cd3cdb1a7e69d1bfaeb8c73983bf271"}, - {file = "xattr-0.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:809e2537d0aff9fca97dacf3245cbbaf711bbced5d1b0235a8d1906b04e26114"}, - {file = "xattr-0.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de3af84364f06d67b3662ccf7c1a73e1d389d8d274394e952651e7bf1bbd2718"}, - {file = "xattr-0.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b62cdad232d2d2dedd39b543701db8e3883444ec0d57ce3fab8f75e5f8b0301"}, - {file = "xattr-0.9.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b11d2eda397d47f7075743409683c233519ca52aa1dac109b413a4d8c15b740"}, - {file = "xattr-0.9.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:661c0a939aefdf071887121f534bb10588d69c7b2dfca5c486af2fc81a0786e8"}, - {file = "xattr-0.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5db7c2db320a8d5264d437d71f1eb7270a7e4a6545296e7766161d17752590b7"}, - {file = "xattr-0.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:83203e60cbaca9536d297e5039b285a600ff84e6e9e8536fe2d521825eeeb437"}, - {file = "xattr-0.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:42bfb4e4da06477e739770ac6942edbdc71e9fc3b497b67db5fba712fa8109c2"}, - {file = "xattr-0.9.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:67047d04d1c56ad4f0f5886085e91b0077238ab3faaec6492c3c21920c6566eb"}, - {file = "xattr-0.9.9-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:885782bc82ded1a3f684d54a1af259ae9fcc347fa54b5a05b8aad82b8a42044c"}, - {file = "xattr-0.9.9-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bc84ccec618b5aa089e7cee8b07fcc92d4069aac4053da604c8143a0d6b1381"}, - {file = "xattr-0.9.9-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baeff3e5dda8ea7e9424cfaee51829f46afe3836c30d02f343f9049c685681ca"}, - {file = "xattr-0.9.9.tar.gz", hash = "sha256:09cb7e1efb3aa1b4991d6be4eb25b73dc518b4fe894f0915f5b0dcede972f346"}, -] -zipp = [ - {file = "zipp-3.9.0-py3-none-any.whl", hash = "sha256:972cfa31bc2fedd3fa838a51e9bc7e64b7fb725a8c00e7431554311f180e9980"}, - {file = "zipp-3.9.0.tar.gz", hash = "sha256:3a7af91c3db40ec72dd9d154ae18e008c69efe8ca88dde4f9a731bb82fe2f9eb"}, -] +content-hash = "fb909b5c273da18b6715b134312d9a97edfa8dbfc2c7807fde3ace3d179c21ff" diff --git a/pyproject.toml b/pyproject.toml index ecf47ae886d..63e65101815 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "poetry" -version = "1.3.0.dev0" +version = "1.5.0.dev0" description = "Python dependency management and packaging made easy." authors = [ "Sébastien Eustace ", @@ -47,32 +47,42 @@ generate-setup-file = false [tool.poetry.dependencies] python = "^3.7" -poetry-core = "^1.3.2" -poetry-plugin-export = "^1.1.2" +poetry-core = "1.5.2" +poetry-plugin-export = "^1.3.0" "backports.cached-property" = { version = "^1.0.2", python = "<3.8" } +build = "^0.10.0" cachecontrol = { version = "^0.12.9", extras = ["filecache"] } -cleo = "^1.0.0a5" -crashtest = "^0.3.0" -dulwich = "^0.20.46" +cleo = "^2.0.0" +crashtest = "^0.4.1" +dulwich = "^0.21.2" filelock = "^3.8.0" html5lib = "^1.0" -importlib-metadata = { version = "^4.4", python = "<3.10" } +importlib-metadata = { version = ">=4.4", python = "<3.10" } +installer = "^0.7.0" jsonschema = "^4.10.0" keyring = "^23.9.0" +lockfile = "^0.12.2" # packaging uses calver, so version is unclamped packaging = ">=20.4" pexpect = "^4.7.0" -pkginfo = "^1.5" +pkginfo = "^1.9.4" platformdirs = "^2.5.2" +pyproject-hooks = "^1.0.0" requests = "^2.18" -requests-toolbelt = "^0.9.1" +requests-toolbelt = ">=0.9.1,<0.11.0" shellingham = "^1.5" +tomli = { version = "^2.0.1", python = "<3.11" } # exclude 0.11.2 and 0.11.3 due to https://github.com/sdispater/tomlkit/issues/225 tomlkit = ">=0.11.1,<1.0.0,!=0.11.2,!=0.11.3" -trove-classifiers = "^2022.5.19" +# trove-classifiers uses calver, so version is unclamped +trove-classifiers = ">=2022.5.19" # exclude 20.4.5 - 20.4.6 due to https://github.com/pypa/pip/issues/9953 -virtualenv = "^20.4.3,!=20.4.5,!=20.4.6" -xattr = { version = "^0.9.7", markers = "sys_platform == 'darwin'" } +virtualenv = [ + { version = "^20.4.3,!=20.4.5,!=20.4.6" }, + # see https://github.com/python-poetry/poetry/pull/6950 for details + { version = "<20.16.6", markers = "sys_platform == 'win32' and python_version == '3.9'" }, +] +xattr = { version = "^0.10.0", markers = "sys_platform == 'darwin'" } urllib3 = "^1.26.0" [tool.poetry.group.dev.dependencies] @@ -81,18 +91,22 @@ pre-commit = "^2.6" [tool.poetry.group.test.dependencies] # Cachy frozen to test backwards compatibility for `poetry.utils.cache`. cachy = "0.3.0" -deepdiff = "^5.0" -flatdict = "^4.0.1" +deepdiff = [ + { version = "^6.2" }, + # avoid orjson, which is required by deepdiff 6.2.3, on FreeBSD + # because it requires a rust compiler + { version = "<6.2.3", markers = "platform_system == 'FreeBSD'"}, +] httpretty = "^1.0" pytest = "^7.1" pytest-cov = "^4.0" pytest-mock = "^3.9" pytest-randomly = "^3.12" -pytest-xdist = { version = "^2.5", extras = ["psutil"] } +pytest-xdist = { version = "^3.1", extras = ["psutil"] } zipp = { version = "^3.4", python = "<3.8" } [tool.poetry.group.typing.dependencies] -mypy = ">=0.971" +mypy = ">=1.0" types-html5lib = ">=1.1.9" types-jsonschema = ">=4.9.0" types-requests = ">=2.28.8" @@ -146,6 +160,11 @@ enable_error_code = [ "redundant-expr", "truthy-bool", ] +exclude = [ + "tests/fixtures", + "tests/masonry/builders/fixtures", + "tests/utils/fixtures" +] # use of importlib-metadata backport at python3.7 makes it impossible to # satisfy mypy without some ignores: but we get a different set of ignores at @@ -155,28 +174,25 @@ enable_error_code = [ # warning. [[tool.mypy.overrides]] module = [ - 'poetry.console.commands.self.show.plugins', - 'poetry.installation.executor', - 'poetry.mixology.version_solver', - 'poetry.plugins.plugin_manager', - 'poetry.repositories.installed_repository', - 'poetry.utils.env', + 'poetry.plugins.plugin_manager', + 'poetry.repositories.installed_repository', + 'poetry.utils.env', ] warn_unused_ignores = false [[tool.mypy.overrides]] module = [ - 'cachecontrol.*', - 'cachy.*', - 'cleo.*', - 'crashtest.*', - 'lockfile.*', - 'pexpect.*', - 'pkginfo.*', - 'requests_toolbelt.*', - 'shellingham.*', - 'virtualenv.*', - 'xattr.*', + 'cachecontrol.*', + 'cachy.*', + 'deepdiff.*', + 'httpretty.*', + 'keyring.*', + 'lockfile.*', + 'pexpect.*', + 'requests_toolbelt.*', + 'shellingham.*', + 'virtualenv.*', + 'xattr.*', ] ignore_missing_imports = true diff --git a/src/poetry/_vendor/.gitignore b/src/poetry/_vendor/.gitignore deleted file mode 100644 index d6b7ef32c84..00000000000 --- a/src/poetry/_vendor/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/src/poetry/config/config.py b/src/poetry/config/config.py index 7cbcba3fc10..65aa3100cf3 100644 --- a/src/poetry/config/config.py +++ b/src/poetry/config/config.py @@ -11,12 +11,12 @@ from typing import Any from packaging.utils import canonicalize_name -from poetry.core.toml import TOMLFile from poetry.config.dict_config_source import DictConfigSource from poetry.config.file_config_source import FileConfigSource from poetry.locations import CONFIG_DIR from poetry.locations import DEFAULT_CACHE_DIR +from poetry.toml import TOMLFile if TYPE_CHECKING: @@ -100,7 +100,6 @@ def validator(cls, policy: str) -> bool: logger = logging.getLogger(__name__) - _default_config: Config | None = None @@ -124,8 +123,16 @@ class Config: "prefer-active-python": False, "prompt": "{project_name}-py{python_version}", }, - "experimental": {"new-installer": True, "system-git-client": False}, - "installer": {"parallel": True, "max-workers": None, "no-binary": None}, + "experimental": { + "new-installer": True, + "system-git-client": False, + }, + "installer": { + "modern-installation": True, + "parallel": True, + "max-workers": None, + "no-binary": None, + }, } def __init__( @@ -192,7 +199,7 @@ def _get_environment_repositories() -> dict[str, dict[str, str]]: repositories = {} pattern = re.compile(r"POETRY_REPOSITORIES_(?P[A-Z_]+)_URL") - for env_key in os.environ.keys(): + for env_key in os.environ: match = pattern.match(env_key) if match: repositories[match.group("name").lower().replace("_", "-")] = { @@ -203,7 +210,11 @@ def _get_environment_repositories() -> dict[str, dict[str, str]]: @property def repository_cache_directory(self) -> Path: - return Path(self.get("cache-dir")) / "cache" / "repositories" + return Path(self.get("cache-dir")).expanduser() / "cache" / "repositories" + + @property + def artifacts_cache_directory(self) -> Path: + return Path(self.get("cache-dir")).expanduser() / "artifacts" @property def virtualenvs_path(self) -> Path: @@ -267,6 +278,7 @@ def _get_normalizer(name: str) -> Callable[[str], Any]: "virtualenvs.options.prefer-active-python", "experimental.new-installer", "experimental.system-git-client", + "installer.modern-installation", "installer.parallel", }: return boolean_normalizer diff --git a/src/poetry/config/file_config_source.py b/src/poetry/config/file_config_source.py index 7119fa19911..b3ccbb2f925 100644 --- a/src/poetry/config/file_config_source.py +++ b/src/poetry/config/file_config_source.py @@ -13,9 +13,10 @@ if TYPE_CHECKING: from collections.abc import Iterator - from poetry.core.toml.file import TOMLFile from tomlkit.toml_document import TOMLDocument + from poetry.toml.file import TOMLFile + class FileConfigSource(ConfigSource): def __init__(self, file: TOMLFile, auth_config: bool = False) -> None: diff --git a/src/poetry/config/source.py b/src/poetry/config/source.py index f3af0c589e2..aa0f9499b08 100644 --- a/src/poetry/config/source.py +++ b/src/poetry/config/source.py @@ -1,14 +1,43 @@ from __future__ import annotations import dataclasses +import warnings + +from poetry.repositories.repository_pool import Priority @dataclasses.dataclass(order=True, eq=True) class Source: name: str url: str - default: bool = dataclasses.field(default=False) - secondary: bool = dataclasses.field(default=False) + default: dataclasses.InitVar[bool] = False + secondary: dataclasses.InitVar[bool] = False + priority: Priority = ( + Priority.PRIMARY + ) # cheating in annotation: str will be converted to Priority in __post_init__ + + def __post_init__(self, default: bool, secondary: bool) -> None: + if isinstance(self.priority, str): + self.priority = Priority[self.priority.upper()] + if default or secondary: + warnings.warn( + ( + "Parameters 'default' and 'secondary' to" + " 'Source' are deprecated. Please provide" + " 'priority' instead." + ), + DeprecationWarning, + stacklevel=2, + ) + if default: + self.priority = Priority.DEFAULT + elif secondary: + self.priority = Priority.SECONDARY def to_dict(self) -> dict[str, str | bool]: - return dataclasses.asdict(self) + return dataclasses.asdict( + self, + dict_factory=lambda x: { + k: v if not isinstance(v, Priority) else v.name.lower() for (k, v) in x + }, + ) diff --git a/src/poetry/console/application.py b/src/poetry/console/application.py index e54c679be9c..8c246b369bf 100644 --- a/src/poetry/console/application.py +++ b/src/poetry/console/application.py @@ -6,13 +6,13 @@ from contextlib import suppress from importlib import import_module from typing import TYPE_CHECKING -from typing import Any 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.exceptions import CleoError from cleo.formatters.style import Style from cleo.io.null_io import NullIO @@ -24,7 +24,7 @@ if TYPE_CHECKING: from collections.abc import Callable - from cleo.events.console_command_event import ConsoleCommandEvent + from cleo.events.event import Event from cleo.io.inputs.argv_input import ArgvInput from cleo.io.inputs.definition import Definition from cleo.io.inputs.input import Input @@ -93,7 +93,7 @@ def _load() -> Command: ] -class Application(BaseApplication): # type: ignore[misc] +class Application(BaseApplication): def __init__(self) -> None: super().__init__("poetry", __version__) @@ -121,8 +121,13 @@ def poetry(self) -> Poetry: if self._poetry is not None: return self._poetry + project_path = Path.cwd() + + if self._io and self._io.input.option("directory"): + project_path = self._io.input.option("directory") + self._poetry = Factory().create_poetry( - Path.cwd(), + cwd=project_path, io=self._io, disable_plugins=self._disable_plugins, disable_cache=self._disable_cache, @@ -132,8 +137,8 @@ def poetry(self) -> Poetry: @property def command_loader(self) -> CommandLoader: - command_loader: CommandLoader | None = self._command_loader - assert command_loader is not None + command_loader = self._command_loader + assert isinstance(command_loader, CommandLoader) return command_loader def reset_poetry(self) -> None: @@ -189,7 +194,7 @@ def _configure_io(self, io: IO) -> None: # We need to check if the command being run # is the "run" command. definition = self.definition - with suppress(CleoException): + with suppress(CleoError): io.input.bind(definition) name = io.input.first_argument @@ -210,7 +215,7 @@ def _configure_io(self, io: IO) -> None: for shortcut in shortcuts: run_input.add_parameter_option("-" + shortcut.lstrip("-")) - with suppress(CleoException): + with suppress(CleoError): run_input.bind(definition) for option_name, value in input.options.items(): @@ -222,12 +227,13 @@ def _configure_io(self, io: IO) -> None: super()._configure_io(io) def register_command_loggers( - self, event: ConsoleCommandEvent, event_name: str, _: Any + self, event: Event, event_name: str, _: EventDispatcher ) -> None: from poetry.console.logging.filters import POETRY_FILTER from poetry.console.logging.io_formatter import IOFormatter from poetry.console.logging.io_handler import IOHandler + assert isinstance(event, ConsoleCommandEvent) command = event.command if not isinstance(command, Command): return @@ -272,12 +278,11 @@ def register_command_loggers( logger.setLevel(_level) - def configure_env( - self, event: ConsoleCommandEvent, event_name: str, _: Any - ) -> None: + def configure_env(self, event: Event, event_name: str, _: EventDispatcher) -> None: from poetry.console.commands.env_command import EnvCommand from poetry.console.commands.self.self_command import SelfCommand + assert isinstance(event, ConsoleCommandEvent) command = event.command if not isinstance(command, EnvCommand) or isinstance(command, SelfCommand): return @@ -290,8 +295,8 @@ def configure_env( io = event.io poetry = command.poetry - env_manager = EnvManager(poetry) - env = env_manager.create_venv(io) + env_manager = EnvManager(poetry, io=io) + env = env_manager.create_venv() if env.is_venv() and io.is_verbose(): io.write_line(f"Using virtualenv: {env.path}") @@ -300,10 +305,11 @@ def configure_env( @classmethod def configure_installer_for_event( - cls, event: ConsoleCommandEvent, event_name: str, _: Any + cls, event: Event, event_name: str, _: EventDispatcher ) -> None: from poetry.console.commands.installer_command import InstallerCommand + assert isinstance(event, ConsoleCommandEvent) command = event.command if not isinstance(command, InstallerCommand): return @@ -329,7 +335,10 @@ def configure_installer_for_command(command: InstallerCommand, io: IO) -> None: poetry.config, disable_cache=poetry.disable_cache, ) - installer.use_executor(poetry.config.get("experimental.new-installer", False)) + use_executor = poetry.config.get("experimental.new-installer", False) + if not use_executor: + # only set if false because the method is deprecated + installer.use_executor(False) command.set_installer(installer) def _load_plugins(self, io: IO | None = None) -> None: @@ -367,6 +376,18 @@ def _default_definition(self) -> Definition: ) ) + definition.add_option( + Option( + "--directory", + "-C", + flag=False, + description=( + "The working directory for the Poetry command (defaults to the" + " current working directory)." + ), + ) + ) + return definition def _get_solution_provider_repository(self) -> SolutionProviderRepository: diff --git a/src/poetry/console/command_loader.py b/src/poetry/console/command_loader.py index a590552078b..74ae8bcd080 100644 --- a/src/poetry/console/command_loader.py +++ b/src/poetry/console/command_loader.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING -from cleo.exceptions import LogicException +from cleo.exceptions import CleoLogicError from cleo.loaders.factory_command_loader import FactoryCommandLoader @@ -12,11 +12,11 @@ from cleo.commands.command import Command -class CommandLoader(FactoryCommandLoader): # type: ignore[misc] +class CommandLoader(FactoryCommandLoader): def register_factory( self, command_name: str, factory: Callable[[], Command] ) -> None: if command_name in self._factories: - raise LogicException(f'The command "{command_name}" already exists.') + raise CleoLogicError(f'The command "{command_name}" already exists.') self._factories[command_name] = factory diff --git a/src/poetry/console/commands/add.py b/src/poetry/console/commands/add.py index 6a0c38f9f23..451965cd431 100644 --- a/src/poetry/console/commands/add.py +++ b/src/poetry/console/commands/add.py @@ -30,7 +30,10 @@ class AddCommand(InstallerCommand, InitCommand): option( "dev", "D", - "Add as a development dependency. (Deprecated)", + ( + "Add as a development dependency. (Deprecated) Use" + " --group=dev instead." + ), ), option("editable", "e", "Add vcs/path dependencies as editable."), option( @@ -63,8 +66,10 @@ class AddCommand(InstallerCommand, InitCommand): option( "dry-run", None, - "Output the operations but do not execute anything (implicitly enables" - " --verbose).", + ( + "Output the operations but do not execute anything (implicitly enables" + " --verbose)." + ), ), option("lock", None, "Do not perform operations (only update the lockfile)."), ] @@ -215,7 +220,15 @@ def handle(self) -> int: constraint_name = _constraint["name"] assert isinstance(constraint_name, str) - section[constraint_name] = constraint + + canonical_constraint_name = canonicalize_name(constraint_name) + + for key in section: + if canonicalize_name(key) == canonical_constraint_name: + section[key] = constraint + break + else: + section[constraint_name] = constraint with contextlib.suppress(ValueError): self.poetry.package.dependency_group(group).remove_dependency( @@ -233,7 +246,7 @@ def handle(self) -> int: # Refresh the locker self.poetry.set_locker( - self.poetry.locker.__class__(self.poetry.locker.lock.path, poetry_content) + self.poetry.locker.__class__(self.poetry.locker.lock, poetry_content) ) self.installer.set_locker(self.poetry.locker) diff --git a/src/poetry/console/commands/check.py b/src/poetry/console/commands/check.py index 30812883523..3ca831e7213 100644 --- a/src/poetry/console/commands/check.py +++ b/src/poetry/console/commands/check.py @@ -1,7 +1,5 @@ from __future__ import annotations -from pathlib import Path - from poetry.console.commands.command import Command @@ -33,6 +31,10 @@ def validate_classifiers( unrecognized = sorted( project_classifiers - set(classifiers) - set(deprecated_classifiers) ) + # Allow "Private ::" classifiers as recommended on PyPI and the packaging guide + # to allow users to avoid accidentally publishing private packages to PyPI. + # https://pypi.org/classifiers/ + unrecognized = [u for u in unrecognized if not u.startswith("Private ::")] if unrecognized: errors.append(f"Unrecognized classifiers: {unrecognized!r}.") @@ -56,12 +58,11 @@ def validate_classifiers( return errors, warnings def handle(self) -> int: - from poetry.core.pyproject.toml import PyProjectTOML - from poetry.factory import Factory + from poetry.pyproject.toml import PyProjectTOML # Load poetry config and display errors, if any - poetry_file = Factory.locate(Path.cwd()) + poetry_file = self.poetry.file.path config = PyProjectTOML(poetry_file).poetry_config check_result = Factory.validate(config, strict=True) diff --git a/src/poetry/console/commands/command.py b/src/poetry/console/commands/command.py index 4bc26ad567b..2aba4e6be24 100644 --- a/src/poetry/console/commands/command.py +++ b/src/poetry/console/commands/command.py @@ -4,7 +4,7 @@ from typing import Any from cleo.commands.command import Command as BaseCommand -from cleo.exceptions import ValueException +from cleo.exceptions import CleoValueError if TYPE_CHECKING: @@ -12,7 +12,7 @@ from poetry.poetry import Poetry -class Command(BaseCommand): # type: ignore[misc] +class Command(BaseCommand): loggers: list[str] = [] _poetry: Poetry | None = None @@ -28,7 +28,10 @@ def set_poetry(self, poetry: Poetry) -> None: self._poetry = poetry def get_application(self) -> Application: - application: Application = self.application + from poetry.console.application import Application + + application = self.application + assert isinstance(application, Application) return application def reset_poetry(self) -> None: @@ -37,5 +40,5 @@ def reset_poetry(self) -> None: def option(self, name: str, default: Any = None) -> Any: try: return super().option(name) - except ValueException: + except CleoValueError: return default diff --git a/src/poetry/console/commands/config.py b/src/poetry/console/commands/config.py index 03773a5ef0d..a35a8b5d263 100644 --- a/src/poetry/console/commands/config.py +++ b/src/poetry/console/commands/config.py @@ -16,7 +16,6 @@ from poetry.config.config import boolean_validator from poetry.config.config import int_normalizer from poetry.console.commands.command import Command -from poetry.locations import DEFAULT_CACHE_DIR if TYPE_CHECKING: @@ -52,74 +51,32 @@ class ConfigCommand(Command): LIST_PROHIBITED_SETTINGS = {"http-basic", "pypi-token"} @property - def unique_config_values(self) -> dict[str, tuple[Any, Any, Any]]: + def unique_config_values(self) -> dict[str, tuple[Any, Any]]: unique_config_values = { - "cache-dir": ( - str, - lambda val: str(Path(val)), - str(DEFAULT_CACHE_DIR / "virtualenvs"), - ), - "virtualenvs.create": (boolean_validator, boolean_normalizer, True), - "virtualenvs.in-project": (boolean_validator, boolean_normalizer, False), - "virtualenvs.options.always-copy": ( - boolean_validator, - boolean_normalizer, - False, - ), + "cache-dir": (str, lambda val: str(Path(val))), + "virtualenvs.create": (boolean_validator, boolean_normalizer), + "virtualenvs.in-project": (boolean_validator, boolean_normalizer), + "virtualenvs.options.always-copy": (boolean_validator, boolean_normalizer), "virtualenvs.options.system-site-packages": ( boolean_validator, boolean_normalizer, - False, - ), - "virtualenvs.options.no-pip": ( - boolean_validator, - boolean_normalizer, - False, ), + "virtualenvs.options.no-pip": (boolean_validator, boolean_normalizer), "virtualenvs.options.no-setuptools": ( boolean_validator, boolean_normalizer, - False, - ), - "virtualenvs.path": ( - str, - lambda val: str(Path(val)), - str(DEFAULT_CACHE_DIR / "virtualenvs"), - ), - "virtualenvs.prefer-active-python": ( - boolean_validator, - boolean_normalizer, - False, - ), - "experimental.new-installer": ( - boolean_validator, - boolean_normalizer, - True, - ), - "experimental.system-git-client": ( - boolean_validator, - boolean_normalizer, - False, - ), - "installer.parallel": ( - boolean_validator, - boolean_normalizer, - True, - ), - "installer.max-workers": ( - lambda val: int(val) > 0, - int_normalizer, - None, - ), - "virtualenvs.prompt": ( - str, - lambda val: str(val), - "{project_name}-py{python_version}", ), + "virtualenvs.path": (str, lambda val: str(Path(val))), + "virtualenvs.prefer-active-python": (boolean_validator, boolean_normalizer), + "experimental.new-installer": (boolean_validator, boolean_normalizer), + "experimental.system-git-client": (boolean_validator, boolean_normalizer), + "installer.modern-installation": (boolean_validator, boolean_normalizer), + "installer.parallel": (boolean_validator, boolean_normalizer), + "installer.max-workers": (lambda val: int(val) > 0, int_normalizer), + "virtualenvs.prompt": (str, lambda val: str(val)), "installer.no-binary": ( PackageFilterPolicy.validator, PackageFilterPolicy.normalize, - None, ), } @@ -129,11 +86,11 @@ def handle(self) -> int: from pathlib import Path from poetry.core.pyproject.exceptions import PyProjectException - from poetry.core.toml.file import TOMLFile from poetry.config.config import Config from poetry.config.file_config_source import FileConfigSource from poetry.locations import CONFIG_DIR + from poetry.toml.file import TOMLFile config = Config.create() config_file = TOMLFile(CONFIG_DIR / "config.toml") @@ -196,8 +153,7 @@ def handle(self) -> int: values: list[str] = self.argument("value") - unique_config_values = self.unique_config_values - if setting_key in unique_config_values: + if setting_key in self.unique_config_values: if self.option("unset"): config.config_source.remove_property(setting_key) return 0 @@ -205,7 +161,7 @@ def handle(self) -> int: return self._handle_single_value( config.config_source, setting_key, - unique_config_values[setting_key], + self.unique_config_values[setting_key], values, ) @@ -255,6 +211,7 @@ def handle(self) -> int: username = values[0] # Only username, so we prompt for password password = self.secret("Password:") + assert isinstance(password, str) elif len(values) != 2: raise ValueError( "Expected one or two arguments " @@ -310,10 +267,10 @@ def _handle_single_value( self, source: ConfigSource, key: str, - callbacks: tuple[Any, Any, Any], + callbacks: tuple[Any, Any], values: list[Any], ) -> int: - validator, normalizer, _ = callbacks + validator, normalizer = callbacks if len(values) > 1: raise RuntimeError("You can only pass one value.") diff --git a/src/poetry/console/commands/debug/info.py b/src/poetry/console/commands/debug/info.py index f90d8e794d3..d76c808cee9 100644 --- a/src/poetry/console/commands/debug/info.py +++ b/src/poetry/console/commands/debug/info.py @@ -22,7 +22,7 @@ def handle(self) -> int: ] ) ) - command = self.application.get("env info") + command = self.get_application().get("env info") exit_code: int = command.run(self.io) return exit_code diff --git a/src/poetry/console/commands/debug/resolve.py b/src/poetry/console/commands/debug/resolve.py index 61b87352cc0..05cf4b15736 100644 --- a/src/poetry/console/commands/debug/resolve.py +++ b/src/poetry/console/commands/debug/resolve.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from cleo.helpers import argument from cleo.helpers import option from cleo.io.outputs.output import Verbosity @@ -8,6 +10,10 @@ from poetry.console.commands.show import ShowCommand +if TYPE_CHECKING: + from cleo.ui.table import Rows + + class DebugResolveCommand(InitCommand): name = "debug resolve" description = "Debugs dependency resolution." @@ -36,8 +42,8 @@ def handle(self) -> int: from poetry.factory import Factory from poetry.puzzle.solver import Solver - from poetry.repositories.pool import Pool from poetry.repositories.repository import Repository + from poetry.repositories.repository_pool import RepositoryPool from poetry.utils.env import EnvManager packages = self.argument("package") @@ -86,7 +92,7 @@ def handle(self) -> int: self.line("") if self.option("tree"): - show_command = self.application.find("show") + show_command = self.get_application().find("show") assert isinstance(show_command, ShowCommand) show_command.init_styles(self.io) @@ -103,11 +109,11 @@ def handle(self) -> int: table = self.table(style="compact") table.style.set_vertical_border_chars("", " ") - rows = [] + rows: Rows = [] if self.option("install"): env = EnvManager(self.poetry).get() - pool = Pool() + pool = RepositoryPool() locked_repository = Repository("poetry-locked") for op in ops: locked_repository.add_package(op.package) diff --git a/src/poetry/console/commands/env/remove.py b/src/poetry/console/commands/env/remove.py index d23fafe5526..d7029b96ac3 100644 --- a/src/poetry/console/commands/env/remove.py +++ b/src/poetry/console/commands/env/remove.py @@ -13,8 +13,10 @@ class EnvRemoveCommand(Command): arguments = [ argument( "python", - "The python executables associated with, or names of the virtual" - " environments which are to be removed.", + ( + "The python executables associated with, or names of the virtual" + " environments which are to be removed." + ), optional=True, multiple=True, ) diff --git a/src/poetry/console/commands/env/use.py b/src/poetry/console/commands/env/use.py index cdfc8cbe554..c48312d9e4c 100644 --- a/src/poetry/console/commands/env/use.py +++ b/src/poetry/console/commands/env/use.py @@ -14,14 +14,14 @@ class EnvUseCommand(Command): def handle(self) -> int: from poetry.utils.env import EnvManager - manager = EnvManager(self.poetry) + manager = EnvManager(self.poetry, io=self.io) if self.argument("python") == "system": - manager.deactivate(self.io) + manager.deactivate() return 0 - env = manager.activate(self.argument("python"), self.io) + env = manager.activate(self.argument("python")) self.line(f"Using virtualenv: {env.path}") diff --git a/src/poetry/console/commands/group_command.py b/src/poetry/console/commands/group_command.py index 27438bf0c07..88b9a1ce39a 100644 --- a/src/poetry/console/commands/group_command.py +++ b/src/poetry/console/commands/group_command.py @@ -1,11 +1,13 @@ from __future__ import annotations +from collections import defaultdict from typing import TYPE_CHECKING from cleo.helpers import option from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.console.commands.command import Command +from poetry.console.exceptions import GroupNotFound if TYPE_CHECKING: @@ -78,6 +80,7 @@ def activated_groups(self) -> set[str]: for groups in self.option(key, "") for group in groups.split(",") } + self._validate_group_options(groups) for opt, new, group in [ ("no-dev", "only", MAIN_GROUP), @@ -107,3 +110,22 @@ def project_with_activated_groups_only(self) -> ProjectPackage: return self.poetry.package.with_dependency_groups( list(self.activated_groups), only=True ) + + def _validate_group_options(self, group_options: dict[str, set[str]]) -> None: + """ + Raises en error if it detects that a group is not part of pyproject.toml + """ + invalid_options = defaultdict(set) + for opt, groups in group_options.items(): + for group in groups: + if not self.poetry.package.has_dependency_group(group): + invalid_options[group].add(opt) + if invalid_options: + message_parts = [] + for group in sorted(invalid_options): + opts = ", ".join( + f"--{opt}" + for opt in sorted(invalid_options[group]) + ) + message_parts.append(f"{group} (via {opts})") + raise GroupNotFound(f"Group(s) not found: {', '.join(message_parts)}") diff --git a/src/poetry/console/commands/init.py b/src/poetry/console/commands/init.py index e2ebf9cf9b7..9204c487f38 100644 --- a/src/poetry/console/commands/init.py +++ b/src/poetry/console/commands/init.py @@ -1,7 +1,5 @@ from __future__ import annotations -import sys - from pathlib import Path from typing import TYPE_CHECKING from typing import Any @@ -23,7 +21,7 @@ from poetry.core.packages.package import Package from tomlkit.items import InlineTable - from poetry.repositories import Pool + from poetry.repositories import RepositoryPool Requirements = Dict[str, Union[str, Mapping[str, Any]]] @@ -42,16 +40,20 @@ class InitCommand(Command): option( "dependency", None, - "Package to require, with an optional version constraint, " - "e.g. requests:^2.10.0 or requests=2.11.1.", + ( + "Package to require, with an optional version constraint, " + "e.g. requests:^2.10.0 or requests=2.11.1." + ), flag=False, multiple=True, ), option( "dev-dependency", None, - "Package to require for development, with an optional version constraint, " - "e.g. requests:^2.10.0 or requests=2.11.1.", + ( + "Package to require for development, with an optional version" + " constraint, e.g. requests:^2.10.0 or requests=2.11.1." + ), flag=False, multiple=True, ), @@ -66,18 +68,29 @@ class InitCommand(Command): def __init__(self) -> None: super().__init__() - self._pool: Pool | None = None + self._pool: RepositoryPool | None = None def handle(self) -> int: from pathlib import Path - from poetry.core.pyproject.toml import PyProjectTOML from poetry.core.vcs.git import GitConfig + from poetry.config.config import Config from poetry.layouts import layout - from poetry.utils.env import SystemEnv + from poetry.pyproject.toml import PyProjectTOML + from poetry.utils.env import EnvManager + + project_path = Path.cwd() + + if self.io.input.option("directory"): + project_path = Path(self.io.input.option("directory")) + if not project_path.exists() or not project_path.is_dir(): + self.line_error( + "The --directory path is not a directory." + ) + return 1 - pyproject = PyProjectTOML(Path.cwd() / "pyproject.toml") + pyproject = PyProjectTOML(project_path / "pyproject.toml") if pyproject.file.exists(): if pyproject.is_poetry_project(): @@ -136,10 +149,7 @@ def handle(self) -> int: question.set_validator(lambda v: self._validate_author(v, author)) author = self.ask(question) - if not author: - authors = [] - else: - authors = [author] + authors = [author] if author else [] license = self.option("license") if not license: @@ -147,10 +157,16 @@ def handle(self) -> int: python = self.option("python") if not python: - current_env = SystemEnv(Path(sys.executable)) - default_python = "^" + ".".join( - str(v) for v in current_env.version_info[:2] + config = Config.create() + default_python = ( + "^" + + EnvManager.get_python_version( + precision=2, + prefer_active_python=config.get("virtualenvs.prefer-active-python"), + io=self.io, + ).to_string() ) + question = self.create_question( f"Compatible Python versions [{default_python}]: ", default=default_python, @@ -166,7 +182,7 @@ def handle(self) -> int: self._determine_requirements(self.option("dependency")) ) - question = "Would you like to define your main dependencies interactively?" + question_text = "Would you like to define your main dependencies interactively?" help_message = """\ You can specify a package in the following forms: - A single name (requests): this will search for matches on PyPI @@ -180,7 +196,7 @@ def handle(self) -> int: """ help_displayed = False - if self.confirm(question, True): + if self.confirm(question_text, True): if self.io.is_interactive(): self.line(help_message) help_displayed = True @@ -196,10 +212,10 @@ def handle(self) -> int: self._determine_requirements(self.option("dev-dependency")) ) - question = ( + question_text = ( "Would you like to define your development dependencies interactively?" ) - if self.confirm(question, True): + if self.confirm(question_text, True): if self.io.is_interactive() and not help_displayed: self.line(help_message) @@ -221,8 +237,9 @@ def handle(self) -> int: ) content = layout_.generate_poetry_content() - for section in content: - pyproject.data.append(section, content[section]) + for section, item in content.items(): + pyproject.data.append(section, item) + if self.io.is_interactive(): self.line("Generated file") self.line("") @@ -272,6 +289,11 @@ def _determine_requirements( ) question.set_validator(self._validate_package) + follow_up_question = self.create_question( + "\nAdd a package (leave blank to skip):" + ) + follow_up_question.set_validator(self._validate_package) + package = self.ask(question) while package: constraint = self._parse_requirements([package])[0] @@ -283,7 +305,7 @@ def _determine_requirements( ): self.line(f"Adding {package}") result.append(constraint) - package = self.ask("\nAdd a package (leave blank to skip):") + package = self.ask(follow_up_question) continue canonicalized_name = canonicalize_name(constraint["name"]) @@ -308,8 +330,10 @@ def _determine_requirements( choices.append("") package = self.choice( - "\nEnter package # to add, or the complete package name if it" - " is not listed", + ( + "\nEnter package # to add, or the complete package name if" + " it is not listed" + ), choices, attempts=3, default=len(choices) - 1, @@ -328,8 +352,8 @@ def _determine_requirements( "Enter the version constraint to require " "(or leave blank to use the latest version):" ) - question.attempts = 3 - question.validator = lambda x: (x or "").strip() or False + question.set_max_attempts(3) + question.set_validator(lambda x: (x or "").strip() or None) package_constraint = self.ask(question) @@ -349,7 +373,7 @@ def _determine_requirements( result.append(constraint) if self.io.is_interactive(): - package = self.ask("\nAdd a package (leave blank to skip):") + package = self.ask(follow_up_question) return result @@ -403,7 +427,7 @@ def _find_best_version_for_package( # TODO: find similar raise ValueError(f"Could not find a matching version of package {name}") - return package.pretty_name, selector.find_recommended_require_version(package) + return package.pretty_name, f"^{package.version.to_string()}" def _parse_requirements(self, requirements: list[str]) -> list[dict[str, Any]]: from poetry.core.pyproject.exceptions import PyProjectException @@ -462,15 +486,15 @@ def _validate_package(package: str | None) -> str | None: return package - def _get_pool(self) -> Pool: - from poetry.repositories import Pool + def _get_pool(self) -> RepositoryPool: + from poetry.repositories import RepositoryPool from poetry.repositories.pypi_repository import PyPiRepository if isinstance(self, EnvCommand): return self.poetry.pool if self._pool is None: - self._pool = Pool() + self._pool = RepositoryPool() self._pool.add_repository(PyPiRepository()) return self._pool diff --git a/src/poetry/console/commands/install.py b/src/poetry/console/commands/install.py index 5503e917d01..90003d5793e 100644 --- a/src/poetry/console/commands/install.py +++ b/src/poetry/console/commands/install.py @@ -14,29 +14,47 @@ class InstallCommand(InstallerCommand): option( "no-dev", None, - "Do not install the development dependencies." - " (Deprecated)", + ( + "Do not install the development dependencies." + " (Deprecated)" + ), ), option( "sync", None, - "Synchronize the environment with the locked packages and the specified" - " groups.", + ( + "Synchronize the environment with the locked packages and the specified" + " groups." + ), ), option( "no-root", None, "Do not install the root package (the current project)." ), + option( + "no-directory", + None, + ( + "Do not install any directory path dependencies; useful to install" + " dependencies without source code, e.g. for caching of Docker layers)" + ), + flag=True, + multiple=False, + ), option( "dry-run", None, - "Output the operations but do not execute anything " - "(implicitly enables --verbose).", + ( + "Output the operations but do not execute anything " + "(implicitly enables --verbose)." + ), ), option( "remove-untracked", None, - "Removes packages not present in the lock file." - " (Deprecated)", + ( + "Removes packages not present in the lock file." + " (Deprecated)" + ), ), option( "extras", @@ -46,12 +64,15 @@ class InstallCommand(InstallerCommand): multiple=True, ), option("all-extras", None, "Install all extra dependencies."), + option("only-root", None, "Exclude all dependencies."), option( - "only-root", + "compile", None, - "Exclude all dependencies.", - flag=True, - multiple=False, + ( + "Compile Python source files to bytecode." + " (This option has no effect if modern-installation is disabled" + " because the old installer always compiles.)" + ), ), ] @@ -84,9 +105,10 @@ def handle(self) -> int: from poetry.masonry.builders.editable import EditableBuilder - self.installer.use_executor( - self.poetry.config.get("experimental.new-installer", False) - ) + use_executor = self.poetry.config.get("experimental.new-installer", False) + if not use_executor: + # only set if false because the method is deprecated + self.installer.use_executor(False) if self.option("extras") and self.option("all-extras"): self.line_error( @@ -136,8 +158,10 @@ def handle(self) -> int: with_synchronization = True self.installer.only_groups(self.activated_groups) + self.installer.skip_directory(self.option("no-directory")) self.installer.dry_run(self.option("dry-run")) self.installer.requires_synchronization(with_synchronization) + self.installer.executor.enable_bytecode_compilation(self.option("compile")) self.installer.verbose(self.io.is_verbose()) return_code = self.installer.run() diff --git a/src/poetry/console/commands/lock.py b/src/poetry/console/commands/lock.py index 57c9ed74f77..2d74c109fba 100644 --- a/src/poetry/console/commands/lock.py +++ b/src/poetry/console/commands/lock.py @@ -16,8 +16,10 @@ class LockCommand(InstallerCommand): option( "check", None, - "Check that the poetry.lock file corresponds to the current" - " version of pyproject.toml.", + ( + "Check that the poetry.lock file corresponds to the current" + " version of pyproject.toml." + ), ), ] @@ -33,9 +35,10 @@ class LockCommand(InstallerCommand): loggers = ["poetry.repositories.pypi_repository"] def handle(self) -> int: - self.installer.use_executor( - self.poetry.config.get("experimental.new-installer", False) - ) + use_executor = self.poetry.config.get("experimental.new-installer", False) + if not use_executor: + # only set if false because the method is deprecated + self.installer.use_executor(False) if self.option("check"): if self.poetry.locker.is_locked() and self.poetry.locker.is_fresh(): diff --git a/src/poetry/console/commands/new.py b/src/poetry/console/commands/new.py index cde571aa202..5f896cf80e8 100644 --- a/src/poetry/console/commands/new.py +++ b/src/poetry/console/commands/new.py @@ -1,7 +1,5 @@ from __future__ import annotations -import sys - from contextlib import suppress from cleo.helpers import argument @@ -31,13 +29,17 @@ def handle(self) -> int: from poetry.core.vcs.git import GitConfig + from poetry.config.config import Config from poetry.layouts import layout - from poetry.utils.env import SystemEnv + from poetry.utils.env import EnvManager + + if self.io.input.option("directory"): + self.line_error( + "--directory only makes sense with existing projects, and will" + " be ignored. You should consider the option --path instead." + ) - if self.option("src"): - layout_cls = layout("src") - else: - layout_cls = layout("standard") + layout_cls = layout("src") if self.option("src") else layout("standard") path = Path(self.argument("path")) if not path.is_absolute(): @@ -65,8 +67,17 @@ def handle(self) -> int: if author_email: author += f" <{author_email}>" - current_env = SystemEnv(Path(sys.executable)) - default_python = "^" + ".".join(str(v) for v in current_env.version_info[:2]) + poetry_config = Config.create() + default_python = ( + "^" + + EnvManager.get_python_version( + precision=2, + prefer_active_python=poetry_config.get( + "virtualenvs.prefer-active-python" + ), + io=self.io, + ).to_string() + ) layout_ = layout_cls( name, diff --git a/src/poetry/console/commands/publish.py b/src/poetry/console/commands/publish.py index b98c58bfc73..a53df8208dd 100644 --- a/src/poetry/console/commands/publish.py +++ b/src/poetry/console/commands/publish.py @@ -44,7 +44,7 @@ class PublishCommand(Command): the config command. """ - loggers = ["poetry.masonry.publishing.publisher"] + loggers = ["poetry.publishing.publisher"] def handle(self) -> int: from poetry.publishing.publisher import Publisher diff --git a/src/poetry/console/commands/remove.py b/src/poetry/console/commands/remove.py index 452d8f60a9a..0eba3907b38 100644 --- a/src/poetry/console/commands/remove.py +++ b/src/poetry/console/commands/remove.py @@ -21,14 +21,19 @@ class RemoveCommand(InstallerCommand): option( "dev", "D", - "Remove a package from the development dependencies." - " (Deprecated)", + ( + "Remove a package from the development dependencies." + " (Deprecated)" + " Use --group=dev instead." + ), ), option( "dry-run", None, - "Output the operations but do not execute anything " - "(implicitly enables --verbose).", + ( + "Output the operations but do not execute anything " + "(implicitly enables --verbose)." + ), ), ] @@ -103,7 +108,7 @@ def handle(self) -> int: # Refresh the locker self.poetry.set_locker( - self.poetry.locker.__class__(self.poetry.locker.lock.path, poetry_content) + self.poetry.locker.__class__(self.poetry.locker.lock, poetry_content) ) self.installer.set_locker(self.poetry.locker) diff --git a/src/poetry/console/commands/run.py b/src/poetry/console/commands/run.py index 6496b45af24..5b008a2fc53 100644 --- a/src/poetry/console/commands/run.py +++ b/src/poetry/console/commands/run.py @@ -5,6 +5,7 @@ from cleo.helpers import argument from poetry.console.commands.env_command import EnvCommand +from poetry.utils._compat import WINDOWS if TYPE_CHECKING: @@ -44,7 +45,28 @@ def _module(self) -> Module: return module - def run_script(self, script: str | dict[str, str], args: str) -> int: + def run_script(self, script: str | dict[str, str], args: list[str]) -> int: + """Runs an entry point script defined in the section ``[tool.poetry.scripts]``. + + When a script exists in the venv bin folder, i.e. after ``poetry install``, + then ``sys.argv[0]`` must be set to the full path of the executable, so + ``poetry run foo`` and ``poetry shell``, ``foo`` have the same ``sys.argv[0]`` + that points to the full path. + + Otherwise (when an entry point script does not exist), ``sys.argv[0]`` is the + script name only, i.e. ``poetry run foo`` has ``sys.argv == ['foo']``. + """ + for script_dir in self.env.script_dirs: + script_path = script_dir / args[0] + if WINDOWS: + script_path = script_path.with_suffix(".cmd") + if script_path.exists(): + args = [str(script_path), *args[1:]] + break + else: + # If we reach this point, the script is not installed + self._warning_not_installed_script(args[0]) + if isinstance(script, dict): script = script["callable"] @@ -58,7 +80,18 @@ def run_script(self, script: str | dict[str, str], args: str) -> int: "import sys; " "from importlib import import_module; " f"sys.argv = {args!r}; {src_in_sys_path}" - f"import_module('{module}').{callable_}()" + f"sys.exit(import_module('{module}').{callable_}())" ] return self.env.execute(*cmd) + + def _warning_not_installed_script(self, script: str) -> None: + message = f"""\ +Warning: '{script}' is an entry point defined in pyproject.toml, but it's not \ +installed as a script. You may get improper `sys.argv[0]`. + +The support to run uninstalled scripts will be removed in a future release. + +Run `poetry install` to resolve and get rid of this message. +""" + self.line_error(message, style="warning") diff --git a/src/poetry/console/commands/self/self_command.py b/src/poetry/console/commands/self/self_command.py index 0c1a4ec3f79..db626f1ed4e 100644 --- a/src/poetry/console/commands/self/self_command.py +++ b/src/poetry/console/commands/self/self_command.py @@ -5,11 +5,11 @@ from poetry.core.packages.dependency import Dependency from poetry.core.packages.project_package import ProjectPackage -from poetry.core.pyproject.toml import PyProjectTOML from poetry.__version__ import __version__ from poetry.console.commands.installer_command import InstallerCommand from poetry.factory import Factory +from poetry.pyproject.toml import PyProjectTOML from poetry.utils.env import EnvManager from poetry.utils.env import SystemEnv from poetry.utils.helpers import directory @@ -77,7 +77,8 @@ def generate_system_pyproject(self) -> None: for key in preserved: content["tool"]["poetry"][key] = preserved[key] # type: ignore[index] - self.system_pyproject.write_text(content.as_string(), encoding="utf-8") + pyproject = PyProjectTOML(self.system_pyproject) + pyproject.file.write(content) def reset_poetry(self) -> None: with directory(self.system_pyproject.parent): diff --git a/src/poetry/console/commands/self/show/plugins.py b/src/poetry/console/commands/self/show/plugins.py index fa2653433ed..15c98548a9e 100644 --- a/src/poetry/console/commands/self/show/plugins.py +++ b/src/poetry/console/commands/self/show/plugins.py @@ -25,14 +25,14 @@ def append(self, entry_point: metadata.EntryPoint) -> None: from poetry.plugins.application_plugin import ApplicationPlugin from poetry.plugins.plugin import Plugin - group = entry_point.group # type: ignore[attr-defined] + group = entry_point.group if group == ApplicationPlugin.group: self.application_plugins.append(entry_point) elif group == Plugin.group: self.plugins.append(entry_point) else: - name = entry_point.name # type: ignore[attr-defined] + name = entry_point.name raise ValueError(f"Unknown plugin group ({group}) for {name}") diff --git a/src/poetry/console/commands/self/update.py b/src/poetry/console/commands/self/update.py index f42a3d477d8..6cd94d71402 100644 --- a/src/poetry/console/commands/self/update.py +++ b/src/poetry/console/commands/self/update.py @@ -23,8 +23,10 @@ class SelfUpdateCommand(SelfCommand): option( "dry-run", None, - "Output the operations but do not execute anything " - "(implicitly enables --verbose).", + ( + "Output the operations but do not execute anything " + "(implicitly enables --verbose)." + ), ), ] help = """\ diff --git a/src/poetry/console/commands/show.py b/src/poetry/console/commands/show.py index 7b0c55b3e24..a97c0dc34bc 100644 --- a/src/poetry/console/commands/show.py +++ b/src/poetry/console/commands/show.py @@ -12,6 +12,7 @@ if TYPE_CHECKING: from cleo.io.io import IO + from cleo.ui.table import Rows from packaging.utils import NormalizedName from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package @@ -47,8 +48,10 @@ class ShowCommand(GroupCommand, EnvCommand): option( "why", None, - "When showing the full list, or a --tree for a single package," - " also display why it's included.", + ( + "When showing the full list, or a --tree for a single" + " package, also display why it's included." + ), ), option("latest", "l", "Show the latest version."), option( @@ -139,10 +142,7 @@ def _display_single_package_information( packages = [pkg] if required_by: packages = [ - p - for p in locked_packages - for r in required_by.keys() - if p.name == r + p for p in locked_packages for r in required_by if p.name == r ] else: # if no rev-deps exist we'll make this clear as it can otherwise @@ -160,7 +160,7 @@ def _display_single_package_information( return 0 - rows = [ + rows: Rows = [ ["name", f" : {pkg.pretty_name}"], ["version", f" : {pkg.pretty_version}"], ["description", f" : {pkg.description}"], @@ -194,11 +194,11 @@ def _display_packages_information( from poetry.puzzle.solver import Solver from poetry.repositories.installed_repository import InstalledRepository - from poetry.repositories.pool import Pool + from poetry.repositories.repository_pool import RepositoryPool from poetry.utils.helpers import get_package_version_display_string locked_packages = locked_repository.packages - pool = Pool(ignore_repository_names=True) + pool = RepositoryPool(ignore_repository_names=True) pool.add_repository(locked_repository) solver = Solver( root, @@ -519,18 +519,25 @@ def find_latest_package( from poetry.version.version_selector import VersionSelector # find the latest version allowed in this pool + requires = root.all_requires if package.is_direct_origin(): - requires = root.all_requires - for dep in requires: if dep.name == package.name and dep.source_type == package.source_type: provider = Provider(root, self.poetry.pool, NullIO()) return provider.search_for_direct_origin_dependency(dep) + allow_prereleases = False + for dep in requires: + if dep.name == package.name: + allow_prereleases = dep.allows_prereleases() + break + name = package.name selector = VersionSelector(self.poetry.pool) - return selector.find_best_candidate(name, f">={package.pretty_version}") + return selector.find_best_candidate( + name, f">={package.pretty_version}", allow_prereleases + ) def get_update_status(self, latest: Package, package: Package) -> str: from poetry.core.constraints.version import parse_constraint diff --git a/src/poetry/console/commands/source/add.py b/src/poetry/console/commands/source/add.py index 3011980c529..6875d444be8 100644 --- a/src/poetry/console/commands/source/add.py +++ b/src/poetry/console/commands/source/add.py @@ -7,6 +7,7 @@ from poetry.config.source import Source from poetry.console.commands.command import Command +from poetry.repositories.repository_pool import Priority class SourceAddCommand(Command): @@ -25,37 +26,77 @@ class SourceAddCommand(Command): 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.", + ( + "Set this source as the default (disable PyPI). A " + "default source will also be the fallback source if " + "you add other sources. (Deprecated, use --priority)" + ), + ), + option( + "secondary", + "s", + ( + "Set this source as secondary. (Deprecated, use" + " --priority)" + ), + ), + option( + "priority", + "p", + ( + "Set the priority of this source. One of:" + f" {', '.join(p.name.lower() for p in Priority)}. Defaults to" + f" {Priority.PRIMARY.name.lower()}." + ), + flag=False, ), - option("secondary", "s", "Set this source as secondary."), ] def handle(self) -> int: from poetry.factory import Factory - from poetry.repositories import Pool from poetry.utils.source import source_to_table - name = self.argument("name") - url = self.argument("url") - is_default = self.option("default") - is_secondary = self.option("secondary") + name: str = self.argument("name") + url: str = self.argument("url") + is_default: bool = self.option("default", False) + is_secondary: bool = self.option("secondary", False) + priority: Priority | None = self.option("priority", None) if is_default and is_secondary: self.line_error( - "Cannot configure a source as both default and" - " secondary." + "Cannot configure a source as both default and" + " secondary." ) return 1 - new_source: Source | None = Source( - name=name, url=url, default=is_default, secondary=is_secondary - ) + if is_default or is_secondary: + if priority is not None: + self.line_error( + "Priority was passed through both --priority and a" + " deprecated flag (--default or --secondary). Please only provide" + " one of these." + ) + return 1 + else: + self.line_error( + "Warning: Priority was set through a deprecated flag" + " (--default or --secondary). Consider using --priority next" + " time." + ) + + if is_default: + priority = Priority.DEFAULT + elif is_secondary: + priority = Priority.SECONDARY + elif priority is None: + priority = Priority.PRIMARY + + new_source = Source(name=name, url=url, priority=priority) existing_sources = self.poetry.get_sources() sources = AoT([]) + is_new_source = True for source in existing_sources: if source == new_source: self.line( @@ -63,7 +104,10 @@ def handle(self) -> int: " addition." ) return 0 - elif source.default and is_default: + elif ( + source.priority is Priority.DEFAULT + and new_source.priority is Priority.DEFAULT + ): self.line_error( f"Source with name {source.name} is already set to" " default. Only one default source can be configured at a" @@ -71,24 +115,22 @@ def handle(self) -> int: ) return 1 - if new_source and source.name == name: - self.line(f"Source with name {name} already exists. Updating.") + if source.name == name: source = new_source - new_source = None + is_new_source = False sources.append(source_to_table(source)) - if new_source is not None: + if is_new_source: self.line(f"Adding source with name {name}.") sources.append(source_to_table(new_source)) + else: + self.line(f"Source with name {name} already exists. Updating.") # 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) + pool = Factory.create_pool(self.poetry.config, sources, NullIO()) + pool.repository(name) except ValueError as e: self.line_error( f"Failed to validate addition of {name}: {e}" diff --git a/src/poetry/console/commands/source/show.py b/src/poetry/console/commands/source/show.py index 9643118c5e0..5014708d391 100644 --- a/src/poetry/console/commands/source/show.py +++ b/src/poetry/console/commands/source/show.py @@ -1,10 +1,16 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from cleo.helpers import argument from poetry.console.commands.command import Command +if TYPE_CHECKING: + from cleo.ui.table import Rows + + class SourceShowCommand(Command): name = "source show" description = "Show information about sources configured for the project." @@ -27,29 +33,23 @@ def handle(self) -> int: return 0 if names and not any(s.name in names for s in sources): - self.line_error(f"No source found with name(s): {', '.join(names)}") + self.line_error( + f"No source found with name(s): {', '.join(names)}", + style="error", + ) 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 = [ + rows: Rows = [ ["name", f" : {source.name}"], ["url", f" : {source.url}"], [ - "default", - f" : {bool_string.get(source.default, False)}", - ], - [ - "secondary", - f" : {bool_string.get(source.secondary, False)}", + "priority", + f" : {source.priority.name.lower()}", ], ] table.add_rows(rows) diff --git a/src/poetry/console/commands/update.py b/src/poetry/console/commands/update.py index b880f5a89db..955438a5474 100644 --- a/src/poetry/console/commands/update.py +++ b/src/poetry/console/commands/update.py @@ -20,14 +20,18 @@ class UpdateCommand(InstallerCommand): option( "no-dev", None, - "Do not update the development dependencies." - " (Deprecated)", + ( + "Do not update the development dependencies." + " (Deprecated)" + ), ), option( "dry-run", None, - "Output the operations but do not execute anything " - "(implicitly enables --verbose).", + ( + "Output the operations but do not execute anything " + "(implicitly enables --verbose)." + ), ), option("lock", None, "Do not perform operations (only update the lockfile)."), ] @@ -37,9 +41,10 @@ class UpdateCommand(InstallerCommand): def handle(self) -> int: packages = self.argument("packages") - self.installer.use_executor( - self.poetry.config.get("experimental.new-installer", False) - ) + use_executor = self.poetry.config.get("experimental.new-installer", False) + if not use_executor: + # only set if false because the method is deprecated + self.installer.use_executor(False) if packages: self.installer.whitelist({name: "*" for name in packages}) diff --git a/src/poetry/console/commands/version.py b/src/poetry/console/commands/version.py index c494879c4df..b6953c99c87 100644 --- a/src/poetry/console/commands/version.py +++ b/src/poetry/console/commands/version.py @@ -5,6 +5,7 @@ from cleo.helpers import argument from cleo.helpers import option +from poetry.core.version.exceptions import InvalidVersion from tomlkit.toml_document import TOMLDocument from poetry.console.commands.command import Command @@ -95,7 +96,7 @@ def increment_version(self, version: str, rule: str) -> Version: try: parsed = Version.parse(version) - except ValueError: + except InvalidVersion: raise ValueError("The project's version doesn't seem to follow semver") if rule in {"major", "premajor"}: diff --git a/src/poetry/console/exceptions.py b/src/poetry/console/exceptions.py index 09fa60ad81e..2cc359ddb75 100644 --- a/src/poetry/console/exceptions.py +++ b/src/poetry/console/exceptions.py @@ -1,7 +1,11 @@ from __future__ import annotations -from cleo.exceptions import CleoSimpleException +from cleo.exceptions import CleoError -class PoetrySimpleConsoleException(CleoSimpleException): # type: ignore[misc] +class PoetryConsoleError(CleoError): + pass + + +class GroupNotFound(PoetryConsoleError): pass diff --git a/src/poetry/console/io/inputs/run_argv_input.py b/src/poetry/console/io/inputs/run_argv_input.py index b27f19cab37..36735202118 100644 --- a/src/poetry/console/io/inputs/run_argv_input.py +++ b/src/poetry/console/io/inputs/run_argv_input.py @@ -9,7 +9,7 @@ from cleo.io.inputs.definition import Definition -class RunArgvInput(ArgvInput): # type: ignore[misc] +class RunArgvInput(ArgvInput): def __init__( self, argv: list[str] | None = None, @@ -43,12 +43,9 @@ def has_parameter_option( # 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 + leading = value + "=" if value.startswith("--") else value - if token == value or leading != "" and token.find(leading) == 0: + if token == value or leading != "" and token.startswith(leading): return True return False diff --git a/src/poetry/factory.py b/src/poetry/factory.py index 46856736354..d1a46a654aa 100644 --- a/src/poetry/factory.py +++ b/src/poetry/factory.py @@ -12,7 +12,6 @@ from poetry.core.factory import Factory as BaseFactory from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.core.packages.project_package import ProjectPackage -from poetry.core.toml.file import TOMLFile from poetry.config.config import Config from poetry.json import validate_object @@ -20,19 +19,21 @@ from poetry.plugins.plugin import Plugin from poetry.plugins.plugin_manager import PluginManager from poetry.poetry import Poetry +from poetry.toml.file import TOMLFile if TYPE_CHECKING: + from collections.abc import Iterable from pathlib import Path from cleo.io.io import IO from poetry.core.packages.package import Package from tomlkit.toml_document import TOMLDocument + from poetry.repositories import RepositoryPool from poetry.repositories.legacy_repository import LegacyRepository from poetry.utils.dependency_specification import DependencySpec - logger = logging.getLogger(__name__) @@ -54,15 +55,19 @@ def create_poetry( base_poetry = super().create_poetry(cwd=cwd, with_groups=with_groups) - locker = Locker( - base_poetry.file.parent / "poetry.lock", base_poetry.local_config + # TODO: backward compatibility, can be simplified if poetry-core with + # https://github.com/python-poetry/poetry-core/pull/483 is available + poetry_file: Path = ( + getattr(base_poetry, "pyproject_path", None) or base_poetry.file.path ) + locker = Locker(poetry_file.parent / "poetry.lock", base_poetry.local_config) + # Loading global configuration config = Config.create() # Loading local configuration - local_config_file = TOMLFile(base_poetry.file.parent / "poetry.toml") + local_config_file = TOMLFile(poetry_file.parent / "poetry.toml") if local_config_file.exists(): if io.is_debug(): io.write_line(f"Loading configuration file {local_config_file.path}") @@ -81,7 +86,7 @@ def create_poetry( config.merge({"repositories": repositories}) poetry = Poetry( - base_poetry.file.path, + poetry_file, base_poetry.local_config, base_poetry.package, locker, @@ -89,13 +94,13 @@ def create_poetry( disable_cache, ) - # Configuring sources - self.configure_sources( - poetry, - poetry.local_config.get("source", []), - config, - io, - disable_cache=disable_cache, + poetry.set_pool( + self.create_pool( + config, + poetry.local_config.get("source", []), + io, + disable_cache=disable_cache, + ) ) plugin_manager = PluginManager(Plugin.group, disable_plugins=disable_plugins) @@ -107,51 +112,75 @@ def create_poetry( @classmethod def get_package(cls, name: str, version: str) -> ProjectPackage: - return ProjectPackage(name, version, version) + return ProjectPackage(name, version) @classmethod - def configure_sources( + def create_pool( cls, - poetry: Poetry, - sources: list[dict[str, str]], - config: Config, - io: IO, + auth_config: Config, + sources: Iterable[dict[str, Any]] = (), + io: IO | None = None, disable_cache: bool = False, - ) -> None: + ) -> RepositoryPool: + from poetry.repositories import RepositoryPool + from poetry.repositories.repository_pool import Priority + + if io is None: + io = NullIO() + if disable_cache: logger.debug("Disabling source caches") + pool = RepositoryPool() + for source in sources: repository = cls.create_package_source( - source, config, disable_cache=disable_cache + source, auth_config, disable_cache=disable_cache ) - is_default = bool(source.get("default", False)) - is_secondary = bool(source.get("secondary", False)) + priority = Priority[source.get("priority", Priority.PRIMARY.name).upper()] + if "default" in source or "secondary" in source: + warning = ( + "Found deprecated key 'default' or 'secondary' in" + " pyproject.toml configuration for source" + f" {source.get('name')}. Please provide the key 'priority'" + " instead. Accepted values are:" + f" {', '.join(repr(p.name.lower()) for p in Priority)}." + ) + io.write_error_line(f"Warning: {warning}") + if source.get("default"): + priority = Priority.DEFAULT + elif source.get("secondary"): + priority = Priority.SECONDARY + if io.is_debug(): message = f"Adding repository {repository.name} ({repository.url})" - if is_default: + if priority is Priority.DEFAULT: message += " and setting it as the default one" - elif is_secondary: - message += " and setting it as secondary" + else: + message += f" and setting it as {priority.name.lower()}" io.write_line(message) - poetry.pool.add_repository(repository, is_default, secondary=is_secondary) + pool.add_repository(repository, priority=priority) - # 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(): + # Only add PyPI if no default repository is configured + if pool.has_default(): if io.is_debug(): io.write_line("Deactivating the PyPI repository") else: from poetry.repositories.pypi_repository import PyPiRepository - default = not poetry.pool.has_primary_repositories() - poetry.pool.add_repository( - PyPiRepository(disable_cache=disable_cache), default, not default + if pool.has_primary_repositories(): + pypi_priority = Priority.SECONDARY + else: + pypi_priority = Priority.DEFAULT + + pool.add_repository( + PyPiRepository(disable_cache=disable_cache), priority=pypi_priority ) + return pool + @classmethod def create_package_source( cls, source: dict[str, str], auth_config: Config, disable_cache: bool = False @@ -181,9 +210,7 @@ def create_package_source( ) @classmethod - def create_pyproject_from_package( - cls, package: Package, path: Path | None = None - ) -> TOMLDocument: + def create_pyproject_from_package(cls, package: Package) -> TOMLDocument: import tomlkit from poetry.utils.dependency_specification import dependency_to_specification @@ -257,7 +284,8 @@ def create_pyproject_from_package( constraint["optional"] = True if len(constraint) == 1 and "version" in constraint: - constraint = cast(str, constraint["version"]) + assert isinstance(constraint["version"], str) + constraint = constraint["version"] elif not constraint: constraint = "*" @@ -280,12 +308,6 @@ def create_pyproject_from_package( content["extras"] = extras_section pyproject = cast("TOMLDocument", pyproject) - pyproject.add(tomlkit.nl()) - - if path: - path.joinpath("pyproject.toml").write_text( - pyproject.as_string(), encoding="utf-8" - ) return pyproject diff --git a/src/poetry/inspection/info.py b/src/poetry/inspection/info.py index 665fd641c84..10bd7d04ac9 100644 --- a/src/poetry/inspection/info.py +++ b/src/poetry/inspection/info.py @@ -17,11 +17,12 @@ from poetry.core.factory import Factory from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package -from poetry.core.pyproject.toml import PyProjectTOML 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.core.version.requirements import InvalidRequirement +from poetry.pyproject.toml import PyProjectTOML from poetry.utils.env import EnvCommandError from poetry.utils.env import ephemeral_environment from poetry.utils.setup_reader import SetupReader @@ -40,7 +41,7 @@ PEP517_META_BUILD = """\ import build import build.env -import pep517 +import pyproject_hooks source = '{source}' dest = '{dest}' @@ -50,18 +51,18 @@ srcdir=source, scripts_dir=env.scripts_dir, python_executable=env.executable, - runner=pep517.quiet_subprocess_runner, + runner=pyproject_hooks.quiet_subprocess_runner, ) env.install(builder.build_system_requires) env.install(builder.get_requires_for_build('wheel')) builder.metadata_path(dest) """ -PEP517_META_BUILD_DEPS = ["build===0.7.0", "pep517==0.12.0"] +PEP517_META_BUILD_DEPS = ["build==0.10.0", "pyproject_hooks==1.0.0"] class PackageInfoError(ValueError): - def __init__(self, path: Path | str, *reasons: BaseException | str) -> None: + def __init__(self, path: Path, *reasons: BaseException | str) -> None: reasons = (f"Unable to determine package info for path: {path!s}",) + reasons super().__init__("\n\n".join(str(msg).strip() for msg in reasons if msg)) @@ -73,7 +74,6 @@ def __init__( name: str | None = None, version: str | None = None, summary: str | None = None, - platform: str | None = None, requires_dist: list[str] | None = None, requires_python: str | None = None, files: list[dict[str, str]] | None = None, @@ -83,7 +83,6 @@ def __init__( self.name = name self.version = version self.summary = summary - self.platform = platform self.requires_dist = requires_dist self.requires_python = requires_python self.files = files or [] @@ -101,7 +100,6 @@ 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 - self.platform = other.platform or self.platform self.requires_dist = other.requires_dist or self.requires_dist self.requires_python = other.requires_python or self.requires_python self.files = other.files or self.files @@ -116,7 +114,6 @@ def asdict(self) -> dict[str, Any]: "name": self.name, "version": self.version, "summary": self.summary, - "platform": self.platform, "requires_dist": self.requires_dist, "requires_python": self.requires_python, "files": self.files, @@ -201,12 +198,18 @@ def to_package( dependency = Dependency.create_from_pep_508(req, relative_to=root_dir) except InvalidMarker: # Invalid marker, We strip the markers hoping for the best + logger.warning( + "Stripping invalid marker (%s) found in %s-%s dependencies", + req, + package.name, + package.version, + ) req = req.split(";")[0] dependency = Dependency.create_from_pep_508(req, relative_to=root_dir) - except ValueError: - # Likely unable to parse constraint so we skip it + except InvalidRequirement: + # Unable to parse requirement so we skip it logger.warning( - "Invalid constraint (%s) found in %s-%s dependencies, skipping", + "Invalid requirement (%s) found in %s-%s dependencies, skipping", req, package.name, package.version, @@ -255,7 +258,6 @@ def _from_distribution( name=dist.name, version=dist.version, summary=dist.summary, - platform=dist.supported_platforms, requires_dist=requirements, requires_python=dist.requires_python, ) @@ -416,6 +418,7 @@ def from_metadata(cls, path: Path) -> PackageInfo | None: else: directories = list(cls._find_dist_info(path=path)) + dist: pkginfo.BDist | pkginfo.SDist | pkginfo.Wheel for directory in directories: try: if directory.suffix == ".egg-info": @@ -453,7 +456,6 @@ def from_package(cls, package: Package) -> PackageInfo: name=package.name, version=str(package.version), summary=package.description, - platform=package.platform, requires_dist=list(requires), requires_python=package.python_versions, files=package.files, @@ -593,6 +595,7 @@ def get_pep517_metadata(path: Path) -> PackageInfo: "install", "--disable-pip-version-check", "--ignore-installed", + "--no-input", *PEP517_META_BUILD_DEPS, ) venv.run( @@ -614,16 +617,16 @@ def get_pep517_metadata(path: Path) -> PackageInfo: ) cwd = Path.cwd() - os.chdir(path.as_posix()) + os.chdir(path) try: venv.run("python", "setup.py", "egg_info") info = PackageInfo.from_metadata(path) except EnvCommandError as fbe: raise PackageInfoError( - path, "Fallback egg_info generation failed.", fbe + path, e, "Fallback egg_info generation failed.", fbe ) finally: - os.chdir(cwd.as_posix()) + os.chdir(cwd) if info: logger.debug("Falling back to parsed setup.py file for %s", path) diff --git a/src/poetry/installation/chef.py b/src/poetry/installation/chef.py index fa3eb267f00..d9114441191 100644 --- a/src/poetry/installation/chef.py +++ b/src/poetry/installation/chef.py @@ -1,88 +1,195 @@ from __future__ import annotations -import hashlib -import json +import tarfile +import tempfile +import zipfile +from contextlib import redirect_stdout +from io import StringIO from pathlib import Path from typing import TYPE_CHECKING -from poetry.installation.chooser import InvalidWheelName -from poetry.installation.chooser import Wheel +from build import BuildBackendException +from build import ProjectBuilder +from build.env import IsolatedEnv as BaseIsolatedEnv +from poetry.core.utils.helpers import temporary_directory +from pyproject_hooks import quiet_subprocess_runner # type: ignore[import] + +from poetry.utils._compat import decode +from poetry.utils.env import ephemeral_environment if TYPE_CHECKING: - from poetry.core.packages.utils.link import Link + from collections.abc import Callable + from collections.abc import Collection + from contextlib import AbstractContextManager - from poetry.config.config import Config + from poetry.repositories import RepositoryPool + from poetry.utils.cache import ArtifactCache from poetry.utils.env import Env -class Chef: - def __init__(self, config: Config, env: Env) -> None: - self._env = env - self._cache_dir = ( - Path(config.get("cache-dir")).expanduser().joinpath("artifacts") - ) - - def get_cached_archive_for_link(self, link: Link) -> Path | None: - archives = self.get_cached_archives_for_link(link) - if not archives: - return None - - candidates: list[tuple[float | None, Path]] = [] - for archive in archives: - if archive.suffix != ".whl": - candidates.append((float("inf"), archive)) - continue - - try: - wheel = Wheel(archive.name) - except InvalidWheelName: - continue - - if not wheel.is_supported_by_environment(self._env): - continue - - candidates.append( - (wheel.get_minimum_supported_index(self._env.supported_tags), archive), - ) - - if not candidates: - return None - - return min(candidates)[1] - - def get_cached_archives_for_link(self, link: Link) -> list[Path]: - cache_dir = self.get_cache_directory_for_link(link) +class ChefError(Exception): + ... - archive_types = ["whl", "tar.gz", "tar.bz2", "bz2", "zip"] - paths = [] - for archive_type in archive_types: - for archive in cache_dir.glob(f"*.{archive_type}"): - paths.append(Path(archive)) - return paths +class ChefBuildError(ChefError): + ... - 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: - key_parts[link.hash_name] = link.hash +class IsolatedEnv(BaseIsolatedEnv): + def __init__(self, env: Env, pool: RepositoryPool) -> None: + self._env = env + self._pool = pool + + @property + def executable(self) -> str: + return str(self._env.python) + + @property + def scripts_dir(self) -> str: + return str(self._env._bin_dir) + + def install(self, requirements: Collection[str]) -> None: + from cleo.io.null_io import NullIO + from poetry.core.packages.dependency import Dependency + from poetry.core.packages.project_package import ProjectPackage + + from poetry.config.config import Config + from poetry.installation.installer import Installer + from poetry.packages.locker import Locker + from poetry.repositories.installed_repository import InstalledRepository + + # We build Poetry dependencies from the requirements + package = ProjectPackage("__root__", "0.0.0") + package.python_versions = ".".join(str(v) for v in self._env.version_info[:3]) + for requirement in requirements: + dependency = Dependency.create_from_pep_508(requirement) + package.add_dependency(dependency) + + installer = Installer( + NullIO(), + self._env, + package, + Locker(self._env.path.joinpath("poetry.lock"), {}), + self._pool, + Config.create(), + InstalledRepository.load(self._env), + ) + installer.update(True) + installer.run() - if link.subdirectory_fragment: - key_parts["subdirectory"] = link.subdirectory_fragment - key_parts["interpreter_name"] = self._env.marker_env["interpreter_name"] - key_parts["interpreter_version"] = "".join( - self._env.marker_env["interpreter_version"].split(".")[:2] - ) +class Chef: + def __init__( + self, artifact_cache: ArtifactCache, env: Env, pool: RepositoryPool + ) -> None: + self._env = env + self._pool = pool + self._artifact_cache = artifact_cache + + def prepare( + self, archive: Path, output_dir: Path | None = None, *, editable: bool = False + ) -> Path: + if not self._should_prepare(archive): + return archive + + if archive.is_dir(): + destination = output_dir or Path(tempfile.mkdtemp(prefix="poetry-chef-")) + return self._prepare(archive, destination=destination, editable=editable) + + return self._prepare_sdist(archive, destination=output_dir) + + def _prepare( + self, directory: Path, destination: Path, *, editable: bool = False + ) -> Path: + from subprocess import CalledProcessError + + with ephemeral_environment(self._env.python) as venv: + env = IsolatedEnv(venv, self._pool) + builder = ProjectBuilder( + directory, + python_executable=env.executable, + scripts_dir=env.scripts_dir, + runner=quiet_subprocess_runner, + ) + env.install(builder.build_system_requires) - key = hashlib.sha256( - json.dumps( - key_parts, sort_keys=True, separators=(",", ":"), ensure_ascii=True - ).encode("ascii") - ).hexdigest() + stdout = StringIO() + error: Exception | None = None + try: + with redirect_stdout(stdout): + dist_format = "wheel" if not editable else "editable" + env.install( + builder.build_system_requires + | builder.get_requires_for_build(dist_format) + ) + path = Path( + builder.build( + dist_format, + destination.as_posix(), + ) + ) + except BuildBackendException as e: + message_parts = [str(e)] + if isinstance(e.exception, CalledProcessError) and ( + e.exception.stdout is not None or e.exception.stderr is not None + ): + message_parts.append( + decode(e.exception.stderr) + if e.exception.stderr is not None + else decode(e.exception.stdout) + ) + + error = ChefBuildError("\n\n".join(message_parts)) + + if error is not None: + raise error from None + + return path + + def _prepare_sdist(self, archive: Path, destination: Path | None = None) -> Path: + from poetry.core.packages.utils.link import Link + + suffix = archive.suffix + context: Callable[ + [str], AbstractContextManager[zipfile.ZipFile | tarfile.TarFile] + ] + if suffix == ".zip": + context = zipfile.ZipFile + else: + context = tarfile.open + + with temporary_directory() as tmp_dir: + with context(archive.as_posix()) as archive_archive: + archive_archive.extractall(tmp_dir) + + archive_dir = Path(tmp_dir) + + elements = list(archive_dir.glob("*")) + + if len(elements) == 1 and elements[0].is_dir(): + sdist_dir = elements[0] + else: + sdist_dir = archive_dir / archive.name.rstrip(suffix) + if not sdist_dir.is_dir(): + sdist_dir = archive_dir + + if destination is None: + destination = self._artifact_cache.get_cache_directory_for_link( + Link(archive.as_uri()) + ) + + destination.mkdir(parents=True, exist_ok=True) + + return self._prepare( + sdist_dir, + destination, + ) - split_key = [key[:2], key[2:4], key[4:6], key[6:]] + def _should_prepare(self, archive: Path) -> bool: + return archive.is_dir() or not self._is_wheel(archive) - return self._cache_dir.joinpath(*split_key) + @classmethod + def _is_wheel(cls, archive: Path) -> bool: + return archive.suffix == ".whl" diff --git a/src/poetry/installation/chooser.py b/src/poetry/installation/chooser.py index d5ee2c6d57f..b484504ed9a 100644 --- a/src/poetry/installation/chooser.py +++ b/src/poetry/installation/chooser.py @@ -6,11 +6,9 @@ from typing import TYPE_CHECKING from typing import Any -from packaging.tags import Tag - from poetry.config.config import Config from poetry.config.config import PackageFilterPolicy -from poetry.utils.patterns import wheel_file_re +from poetry.utils.wheel import Wheel if TYPE_CHECKING: @@ -18,50 +16,21 @@ from poetry.core.packages.package import Package from poetry.core.packages.utils.link import Link - from poetry.repositories.pool import Pool + from poetry.repositories.repository_pool import RepositoryPool from poetry.utils.env import Env logger = logging.getLogger(__name__) -class InvalidWheelName(Exception): - pass - - -class Wheel: - def __init__(self, filename: str) -> None: - wheel_info = wheel_file_re.match(filename) - if not wheel_info: - raise InvalidWheelName(f"{filename} is not a valid wheel filename.") - - self.filename = filename - self.name = wheel_info.group("name").replace("_", "-") - self.version = wheel_info.group("ver").replace("_", "-") - self.build_tag = wheel_info.group("build") - self.pyversions = wheel_info.group("pyver").split(".") - self.abis = wheel_info.group("abi").split(".") - self.plats = wheel_info.group("plat").split(".") - - self.tags = { - Tag(x, y, z) for x in self.pyversions for y in self.abis for z in self.plats - } - - def get_minimum_supported_index(self, tags: list[Tag]) -> int | None: - 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: Env) -> bool: - return bool(set(env.supported_tags).intersection(self.tags)) - - class Chooser: """ A Chooser chooses an appropriate release archive for packages. """ - def __init__(self, pool: Pool, env: Env, config: Config | None = None) -> None: + def __init__( + self, pool: RepositoryPool, env: Env, config: Config | None = None + ) -> None: self._pool = pool self._env = env self._config = config or Config.create() @@ -78,8 +47,10 @@ def choose_for(self, package: Package) -> Link: if link.is_wheel: if not self._no_binary_policy.allows(package.name): logger.debug( - "Skipping wheel for %s as requested in no binary policy for" - " package (%s)", + ( + "Skipping wheel for %s as requested in no binary policy for" + " package (%s)" + ), link.filename, package.name, ) @@ -87,8 +58,10 @@ def choose_for(self, package: Package) -> Link: if not Wheel(link.filename).is_supported_by_environment(self._env): logger.debug( - "Skipping wheel %s as this is not supported by the current" - " environment", + ( + "Skipping wheel %s as this is not supported by the current" + " environment" + ), link.filename, ) continue @@ -118,7 +91,7 @@ def _get_links(self, package: Package) -> list[Link]: repository = self._pool.repository("pypi") links = repository.find_links_for_package(package) - hashes = [f["hash"] for f in package.files] + hashes = {f["hash"] for f in package.files} if not hashes: return links diff --git a/src/poetry/installation/executor.py b/src/poetry/installation/executor.py index 2dc53d53016..d8b658461f0 100644 --- a/src/poetry/installation/executor.py +++ b/src/poetry/installation/executor.py @@ -13,22 +13,24 @@ from subprocess import CalledProcessError from typing import TYPE_CHECKING from typing import Any -from typing import cast 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.installation.chef import Chef +from poetry.installation.chef import ChefBuildError from poetry.installation.chooser import Chooser from poetry.installation.operations import Install from poetry.installation.operations import Uninstall from poetry.installation.operations import Update +from poetry.installation.wheel_installer import WheelInstaller +from poetry.puzzle.exceptions import SolverProblemError from poetry.utils._compat import decode from poetry.utils.authenticator import Authenticator +from poetry.utils.cache import ArtifactCache from poetry.utils.env import EnvCommandError from poetry.utils.helpers import atomic_open +from poetry.utils.helpers import get_file_hash from poetry.utils.helpers import pluralize from poetry.utils.helpers import remove_directory from poetry.utils.pip import pip_install @@ -42,7 +44,7 @@ from poetry.config.config import Config from poetry.installation.operations.operation import Operation - from poetry.repositories import Pool + from poetry.repositories import RepositoryPool from poetry.utils.env import Env @@ -50,7 +52,7 @@ class Executor: def __init__( self, env: Env, - pool: Pool, + pool: RepositoryPool, config: Config, io: IO, parallel: bool | None = None, @@ -61,11 +63,10 @@ def __init__( self._dry_run = False self._enabled = True self._verbose = False - self._authenticator = Authenticator( - config, self._io, disable_cache=disable_cache + self._wheel_installer = WheelInstaller(self._env) + self._use_modern_installation = config.get( + "installer.modern-installation", True ) - self._chef = Chef(config, self._env) - self._chooser = Chooser(pool, self._env, config) if parallel is None: parallel = config.get("installer.parallel", True) @@ -77,6 +78,13 @@ def __init__( else: self._max_workers = 1 + self._artifact_cache = ArtifactCache(cache_dir=config.artifacts_cache_directory) + self._authenticator = Authenticator( + config, self._io, disable_cache=disable_cache, pool_size=self._max_workers + ) + self._chef = Chef(self._artifact_cache, self._env, pool) + self._chooser = Chooser(pool, self._env, config) + self._executor = ThreadPoolExecutor(max_workers=self._max_workers) self._total_operations = 0 self._executed_operations = 0 @@ -118,6 +126,9 @@ def verbose(self, verbose: bool = True) -> Executor: return self + def enable_bytecode_compilation(self, enable: bool = True) -> None: + self._wheel_installer.enable_bytecode_compilation(enable) + def pip_install( self, req: Path, upgrade: bool = False, editable: bool = False ) -> int: @@ -196,6 +207,14 @@ def execute(self, operations: list[Operation]) -> int: for warning in self._yanked_warnings: self._io.write_error_line(f"Warning: {warning}") + for path, issues in self._wheel_installer.invalid_wheels.items(): + formatted_issues = "\n".join(issues) + warning = ( + f"Validation of the RECORD file of {path.name} failed." + " Please report to the maintainers of that package so they can fix" + f" their build process. Details:\n{formatted_issues}\n" + ) + self._io.write_error_line(f"Warning: {warning}") return 1 if self._shutdown else 0 @@ -291,6 +310,35 @@ def _execute_operation(self, operation: Operation) -> None: with self._lock: trace = ExceptionTrace(e) trace.render(io) + if isinstance(e, ChefBuildError): + pkg = operation.package + pip_command = "pip wheel --use-pep517" + if pkg.develop: + requirement = pkg.source_url + pip_command += " --editable" + else: + requirement = ( + pkg.to_dependency().to_pep_508().split(";")[0].strip() + ) + io.write_line("") + io.write_line( + "" + "Note: This error originates from the build backend," + " and is likely not a problem with poetry" + f" but with {pkg.pretty_name} ({pkg.full_pretty_version})" + " not supporting PEP 517 builds. You can verify this by" + f" running '{pip_command} \"{requirement}\"'." + "" + ) + elif isinstance(e, SolverProblemError): + pkg = operation.package + io.write_line("") + io.write_line( + "" + "Cannot resolve build-system.requires" + f" for {pkg.pretty_name}." + "" + ) io.write_line("") finally: with self._lock: @@ -318,10 +366,12 @@ def _do_execute_operation(self, operation: Operation) -> int: if self.supports_fancy_output(): self._write( operation, - f" • {operation_message}: " - "Skipped " - "for the following reason: " - f"{operation.skip_reason}", + ( + f" • {operation_message}: " + "Skipped " + "for the following reason: " + f"{operation.skip_reason}" + ), ) self._skipped[operation.job_type] += 1 @@ -469,18 +519,22 @@ def _execute_uninstall(self, operation: Uninstall) -> int: message = f" • {op_msg}: Removing..." self._write(operation, message) - return self._remove(operation) + return self._remove(operation.package) def _install(self, operation: Install | Update) -> int: package = operation.package - if package.source_type == "directory": - return self._install_directory(operation) + if package.source_type == "directory" and not self._use_modern_installation: + return self._install_directory_without_wheel_installer(operation) + cleanup_archive: bool = False if package.source_type == "git": - return self._install_git(operation) - - if package.source_type == "file": - archive = self._prepare_file(operation) + archive = self._prepare_git_archive(operation) + cleanup_archive = operation.package.develop + elif package.source_type == "file": + archive = self._prepare_archive(operation) + elif package.source_type == "directory": + archive = self._prepare_archive(operation) + cleanup_archive = True elif package.source_type == "url": assert package.source_url is not None archive = self._download_link(operation, Link(package.source_url)) @@ -493,14 +547,29 @@ def _install(self, operation: Install | Update) -> int: " Installing..." ) self._write(operation, message) - return self.pip_install(archive, upgrade=operation.job_type == "update") + + if not self._use_modern_installation: + return self.pip_install(archive, upgrade=operation.job_type == "update") + + try: + if operation.job_type == "update": + # Uninstall first + # TODO: Make an uninstaller and find a way to rollback in case + # the new package can't be installed + assert isinstance(operation, Update) + self._remove(operation.initial_package) + + self._wheel_installer.install(archive) + finally: + if cleanup_archive: + archive.unlink() + + return 0 def _update(self, operation: Install | Update) -> int: return self._install(operation) - def _remove(self, operation: Uninstall) -> int: - package = operation.package - + def _remove(self, package: Package) -> int: # If we have a VCS package, remove its source directory if package.source_type == "git": src_dir = self._env.path / "src" / package.name @@ -515,7 +584,9 @@ def _remove(self, operation: Uninstall) -> int: raise - def _prepare_file(self, operation: Install | Update) -> Path: + def _prepare_archive( + self, operation: Install | Update, *, output_dir: Path | None = None + ) -> Path: package = operation.package operation_message = self.get_operation_message(operation) @@ -527,13 +598,76 @@ def _prepare_file(self, operation: Install | Update) -> Path: assert package.source_url is not None archive = Path(package.source_url) + if package.source_subdirectory: + archive = archive / package.source_subdirectory if not Path(package.source_url).is_absolute() and package.root_dir: archive = package.root_dir / archive + self._populate_hashes_dict(archive, package) + + return self._chef.prepare( + archive, editable=package.develop, output_dir=output_dir + ) + + def _prepare_git_archive(self, operation: Install | Update) -> Path: + from poetry.vcs.git import Git + + package = operation.package + assert package.source_url is not None + + if package.source_resolved_reference and not package.develop: + # Only cache git archives when we know precise reference hash, + # otherwise we might get stale archives + cached_archive = self._artifact_cache.get_cached_archive_for_git( + package.source_url, + package.source_resolved_reference, + package.source_subdirectory, + env=self._env, + ) + if cached_archive is not None: + return cached_archive + + operation_message = self.get_operation_message(operation) + + message = ( + f" • {operation_message}: Cloning..." + ) + self._write(operation, message) + + source = Git.clone( + url=package.source_url, + source_root=self._env.path / "src", + revision=package.source_resolved_reference or package.source_reference, + ) + + # Now we just need to install from the source directory + original_url = package.source_url + package._source_url = str(source.path) + + output_dir = None + if package.source_resolved_reference and not package.develop: + output_dir = self._artifact_cache.get_cache_directory_for_git( + original_url, + package.source_resolved_reference, + package.source_subdirectory, + ) + + archive = self._prepare_archive(operation, output_dir=output_dir) + if not package.develop: + package._source_url = original_url + + if output_dir is not None and output_dir.is_dir(): + # Mark directories with cached git packages, to distinguish from + # "normal" cache + (output_dir / ".created_from_git_dependency").touch() + return archive - def _install_directory(self, operation: Install | Update) -> int: + def _install_directory_without_wheel_installer( + self, operation: Install | Update + ) -> int: from poetry.factory import Factory + from poetry.pyproject.toml import PyProjectTOML package = operation.package operation_message = self.get_operation_message(operation) @@ -553,7 +687,7 @@ def _install_directory(self, operation: Install | Update) -> int: if package.source_subdirectory: req /= package.source_subdirectory - pyproject = PyProjectTOML(os.path.join(req, "pyproject.toml")) + pyproject = PyProjectTOML(req / "pyproject.toml") package_poetry = None if pyproject.is_poetry_project(): @@ -594,34 +728,6 @@ def _install_directory(self, operation: Install | Update) -> int: return self.pip_install(req, upgrade=True, editable=package.develop) - def _install_git(self, operation: Install | Update) -> int: - from poetry.vcs.git import Git - - package = operation.package - operation_message = self.get_operation_message(operation) - - message = ( - f" • {operation_message}: Cloning..." - ) - self._write(operation, message) - - assert package.source_url is not None - source = Git.clone( - url=package.source_url, - source_root=self._env.path / "src", - revision=package.source_resolved_reference or package.source_reference, - ) - - # Now we just need to install from the source directory - original_url = package.source_url - package._source_url = str(source.path) - - status_code = self._install_directory(operation) - - package._source_url = original_url - - return status_code - def _download(self, operation: Install | Update) -> Path: link = self._chooser.choose_for(operation.package) @@ -642,13 +748,19 @@ def _download(self, operation: Install | Update) -> Path: def _download_link(self, operation: Install | Update, link: Link) -> Path: package = operation.package - archive = self._chef.get_cached_archive_for_link(link) - if archive is None: - # No cached distributions was found, so we download and prepare it + output_dir = self._artifact_cache.get_cache_directory_for_link(link) + # Try to get cached original package for the link provided + original_archive = self._artifact_cache.get_cached_archive_for_link( + link, strict=True + ) + if original_archive is None: + # No cached original distributions was found, so we download and prepare it try: - archive = self._download_archive(operation, link) + original_archive = self._download_archive(operation, link) except BaseException: - cache_directory = self._chef.get_cache_directory_for_link(link) + cache_directory = self._artifact_cache.get_cache_directory_for_link( + link + ) cached_file = cache_directory.joinpath(link.filename) # We can't use unlink(missing_ok=True) because it's not available # prior to Python 3.8 @@ -657,18 +769,40 @@ def _download_link(self, operation: Install | Update, link: Link) -> Path: raise - if package.files: - archive_hash = self._validate_archive_hash(archive, package) + # Get potential higher prioritized cached archive, otherwise it will fall back + # to the original archive. + archive = self._artifact_cache.get_cached_archive_for_link( + link, + strict=False, + env=self._env, + ) + # 'archive' can at this point never be None. Since we previously downloaded + # an archive, we now should have something cached that we can use here + assert archive is not None - self._hashes[package.name] = archive_hash + if archive.suffix != ".whl": + message = ( + f" • {self.get_operation_message(operation)}:" + " Preparing..." + ) + self._write(operation, message) + + archive = self._chef.prepare(archive, output_dir=output_dir) + + # Use the original archive to provide the correct hash. + self._populate_hashes_dict(original_archive, package) return archive + def _populate_hashes_dict(self, archive: Path, package: Package) -> None: + if package.files and archive.name in {f["file"] for f in package.files}: + archive_hash = self._validate_archive_hash(archive, package) + self._hashes[package.name] = archive_hash + @staticmethod def _validate_archive_hash(archive: Path, package: Package) -> str: - file_dep = FileDependency(package.name, archive) - archive_hash: str = "sha256:" + file_dep.hash() - known_hashes = {f["hash"] for f in package.files} + archive_hash: str = "sha256:" + get_file_hash(archive) + known_hashes = {f["hash"] for f in package.files if f["file"] == archive.name} if archive_hash not in known_hashes: raise RuntimeError( @@ -705,7 +839,9 @@ def _download_archive(self, operation: Install | Update, link: Link) -> Path: progress.start() done = 0 - archive = self._chef.get_cache_directory_for_link(link) / link.filename + archive = ( + self._artifact_cache.get_cache_directory_for_link(link) / link.filename + ) archive.parent.mkdir(parents=True, exist_ok=True) with atomic_open(archive) as f: for chunk in response.iter_content(chunk_size=4096): @@ -758,12 +894,12 @@ def _save_url_reference(self, operation: Operation) -> None: url_reference: dict[str, Any] | None = None - if package.source_type == "git": + if package.source_type == "git" and not package.develop: url_reference = self._create_git_url_reference(package) + elif package.source_type in ("directory", "git"): + url_reference = self._create_directory_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) @@ -771,7 +907,8 @@ def _save_url_reference(self, operation: Operation) -> None: for dist in self._env.site_packages.distributions( name=package.name, writable_only=True ): - dist_path = cast(Path, dist._path) # type: ignore[attr-defined] + dist_path = dist._path # type: ignore[attr-defined] + assert isinstance(dist_path, Path) url = dist_path / "direct_url.json" url.write_text(json.dumps(url_reference), encoding="utf-8") @@ -797,20 +934,12 @@ def _create_git_url_reference(self, package: Package) -> dict[str, Any]: return reference def _create_url_url_reference(self, package: Package) -> dict[str, Any]: - archive_info = {} - - if package.name in self._hashes: - archive_info["hash"] = self._hashes[package.name] + archive_info = self._get_archive_info(package) - reference = {"url": package.source_url, "archive_info": archive_info} - - return reference + return {"url": package.source_url, "archive_info": archive_info} def _create_file_url_reference(self, package: Package) -> dict[str, Any]: - archive_info = {} - - if package.name in self._hashes: - archive_info["hash"] = self._hashes[package.name] + archive_info = self._get_archive_info(package) assert package.source_url is not None return { @@ -829,3 +958,20 @@ def _create_directory_url_reference(self, package: Package) -> dict[str, Any]: "url": Path(package.source_url).as_uri(), "dir_info": dir_info, } + + def _get_archive_info(self, package: Package) -> dict[str, Any]: + """ + Create dictionary `archive_info` for file `direct_url.json`. + + Specification: https://packaging.python.org/en/latest/specifications/direct-url + (it supersedes PEP 610) + + :param package: This must be a poetry package instance. + """ + archive_info = {} + + if package.name in self._hashes: + algorithm, value = self._hashes[package.name].split(":") + archive_info["hashes"] = {algorithm: value} + + return archive_info diff --git a/src/poetry/installation/installer.py b/src/poetry/installation/installer.py index 83082020d21..c049f0da500 100644 --- a/src/poetry/installation/installer.py +++ b/src/poetry/installation/installer.py @@ -1,5 +1,7 @@ from __future__ import annotations +import warnings + from typing import TYPE_CHECKING from cleo.io.null_io import NullIO @@ -10,8 +12,8 @@ from poetry.installation.operations import Uninstall from poetry.installation.operations import Update from poetry.installation.pip_installer import PipInstaller -from poetry.repositories import Pool from poetry.repositories import Repository +from poetry.repositories import RepositoryPool from poetry.repositories.installed_repository import InstalledRepository from poetry.repositories.lockfile_repository import LockfileRepository from poetry.utils.extras import get_extra_package_names @@ -39,7 +41,7 @@ def __init__( env: Env, package: ProjectPackage, locker: Locker, - pool: Pool, + pool: RepositoryPool, config: Config, installed: Repository | None = None, executor: Executor | None = None, @@ -57,6 +59,7 @@ def __init__( self._verbose = False self._write_lock = True self._groups: Iterable[str] | None = None + self._skip_directory = False self._execute_operations = True self._lock = False @@ -71,7 +74,7 @@ def __init__( ) self._executor = executor - self._use_executor = False + self._use_executor = True self._installer = self._get_installer() if installed is None: @@ -148,6 +151,11 @@ def update(self, update: bool = True) -> Installer: return self + def skip_directory(self, skip_directory: bool = False) -> Installer: + self._skip_directory = skip_directory + + return self + def lock(self, update: bool = True) -> Installer: """ Prepare the installer for locking only. @@ -180,6 +188,14 @@ def extras(self, extras: list[str]) -> Installer: return self def use_executor(self, use_executor: bool = True) -> Installer: + warnings.warn( + ( + "Calling use_executor() is deprecated since it's true by default now" + " and deactivating it will be removed in a future release." + ), + DeprecationWarning, + stacklevel=2, + ) self._use_executor = use_executor return self @@ -201,10 +217,16 @@ def _do_refresh(self) -> int: self._io, ) + # Always re-solve directory dependencies, otherwise we can't determine + # if anything has changed (and the lock file contains an invalid version). + use_latest = [ + p.name for p in locked_repository.packages if p.source_type == "directory" + ] + with solver.provider.use_source_root( source_root=self._env.path.joinpath("src") ): - ops = solver.solve(use_latest=[]).calculate_operations() + ops = solver.solve(use_latest=use_latest).calculate_operations() lockfile_repo = LockfileRepository() self._populate_lockfile_repo(lockfile_repo, ops) @@ -275,12 +297,10 @@ def _do_install(self) -> int: lockfile_repo = LockfileRepository() self._populate_lockfile_repo(lockfile_repo, ops) - if self._update: + if self._lock and self._update: + # If we are only in lock mode, no need to go any further self._write_lock_file(lockfile_repo) - - if self._lock: - # If we are only in lock mode, no need to go any further - return 0 + return 0 if self._groups is not None: root = self._package.with_dependency_groups(list(self._groups), only=True) @@ -294,7 +314,7 @@ def _do_install(self) -> int: ) # We resolve again by only using the lock file - pool = Pool(ignore_repository_names=True) + pool = RepositoryPool(ignore_repository_names=True) # Making a new repo containing the packages # newly resolved and the ones from the current lock file @@ -320,6 +340,7 @@ def _do_install(self) -> int: ops = solver.solve(use_latest=self._whitelist).calculate_operations( with_uninstalls=self._requires_synchronization, synchronize=self._requires_synchronization, + skip_directory=self._skip_directory, ) if not self._requires_synchronization: @@ -346,7 +367,13 @@ def _do_install(self) -> int: self._filter_operations(ops, lockfile_repo) # Execute operations - return self._execute(ops) + status = self._execute(ops) + + if status == 0 and self._update: + # Only write lock file when installation is success + self._write_lock_file(lockfile_repo) + + return status def _write_lock_file(self, repo: LockfileRepository, force: bool = False) -> None: if self._write_lock and (force or self._update): @@ -360,6 +387,14 @@ def _execute(self, operations: list[Operation]) -> int: if self._use_executor: return self._executor.execute(operations) + self._io.write_error( + "" + "Setting `experimental.new-installer` to false is deprecated and" + " slated for removal in an upcoming minor release.\n" + "(Despite of the setting's name the new installer is not experimental!)" + "" + ) + if not operations and (self._execute_operations or self._dry_run): self._io.write_line("No dependencies to install or update") @@ -519,10 +554,7 @@ def _get_operations_from_lock( def _filter_operations(self, ops: Iterable[Operation], repo: Repository) -> None: extra_packages = self._get_extra_packages(repo) for op in ops: - if isinstance(op, Update): - package = op.target_package - else: - package = op.package + package = op.target_package if isinstance(op, Update) else op.package if op.job_type == "uninstall": continue diff --git a/src/poetry/installation/pip_installer.py b/src/poetry/installation/pip_installer.py index 7ee24915f62..65c91bd15e3 100644 --- a/src/poetry/installation/pip_installer.py +++ b/src/poetry/installation/pip_installer.py @@ -11,10 +11,10 @@ from typing import Any from poetry.core.constraints.version import Version -from poetry.core.pyproject.toml import PyProjectTOML from poetry.installation.base_installer import BaseInstaller -from poetry.repositories.http import HTTPRepository +from poetry.pyproject.toml import PyProjectTOML +from poetry.repositories.http_repository import HTTPRepository from poetry.utils._compat import encode from poetry.utils.helpers import remove_directory from poetry.utils.pip import pip_install @@ -25,12 +25,12 @@ from poetry.core.masonry.builders.builder import Builder from poetry.core.packages.package import Package - from poetry.repositories.pool import Pool + from poetry.repositories.repository_pool import RepositoryPool from poetry.utils.env import Env class PipInstaller(BaseInstaller): - def __init__(self, env: Env, io: IO, pool: Pool) -> None: + def __init__(self, env: Env, io: IO, pool: RepositoryPool) -> None: self._env = env self._io = io self._pool = pool @@ -46,7 +46,7 @@ def install(self, package: Package, update: bool = False) -> None: return - args = ["install", "--no-deps"] + args = ["install", "--no-deps", "--no-input"] if not package.is_direct_origin() and package.source_url: assert package.source_reference is not None @@ -221,7 +221,7 @@ def install_directory(self, package: Package) -> str | int: if package.source_subdirectory: req /= package.source_subdirectory - pyproject = PyProjectTOML(os.path.join(req, "pyproject.toml")) + pyproject = PyProjectTOML(req / "pyproject.toml") package_poetry = None if pyproject.is_poetry_project(): diff --git a/src/poetry/installation/wheel_installer.py b/src/poetry/installation/wheel_installer.py new file mode 100644 index 00000000000..18e42e9cbd9 --- /dev/null +++ b/src/poetry/installation/wheel_installer.py @@ -0,0 +1,114 @@ +from __future__ import annotations + +import platform +import sys + +from pathlib import Path +from typing import TYPE_CHECKING + +from installer import install +from installer.destinations import SchemeDictionaryDestination +from installer.sources import WheelFile +from installer.sources import _WheelFileValidationError + +from poetry.__version__ import __version__ +from poetry.utils._compat import WINDOWS + + +if TYPE_CHECKING: + from typing import BinaryIO + + from installer.records import RecordEntry + from installer.scripts import LauncherKind + from installer.utils import Scheme + + from poetry.utils.env import Env + + +class WheelDestination(SchemeDictionaryDestination): + """ """ + + def write_to_fs( + self, + scheme: Scheme, + path: str, + stream: BinaryIO, + is_executable: bool, + ) -> RecordEntry: + from installer.records import Hash + from installer.records import RecordEntry + from installer.utils import copyfileobj_with_hashing + from installer.utils import make_file_executable + + target_path = Path(self.scheme_dict[scheme]) / path + if target_path.exists(): + # Contrary to the base library we don't raise an error + # here since it can break namespace packages (like Poetry's) + pass + + parent_folder = target_path.parent + if not parent_folder.exists(): + # Due to the parallel installation it can happen + # that two threads try to create the directory. + parent_folder.mkdir(parents=True, exist_ok=True) + + with target_path.open("wb") as f: + hash_, size = copyfileobj_with_hashing(stream, f, self.hash_algorithm) + + if is_executable: + make_file_executable(target_path) + + return RecordEntry(path, Hash(self.hash_algorithm, hash_), size) + + def for_source(self, source: WheelFile) -> WheelDestination: + scheme_dict = self.scheme_dict.copy() + + scheme_dict["headers"] = str(Path(scheme_dict["headers"]) / source.distribution) + + return self.__class__( + scheme_dict, + interpreter=self.interpreter, + script_kind=self.script_kind, + bytecode_optimization_levels=self.bytecode_optimization_levels, + ) + + +class WheelInstaller: + def __init__(self, env: Env) -> None: + self._env = env + + script_kind: LauncherKind + if not WINDOWS: + script_kind = "posix" + else: + if platform.uname()[4].startswith("arm"): + script_kind = "win-arm64" if sys.maxsize > 2**32 else "win-arm" + else: + script_kind = "win-amd64" if sys.maxsize > 2**32 else "win-ia32" + + schemes = self._env.paths + schemes["headers"] = schemes["include"] + + self._destination = WheelDestination( + schemes, interpreter=str(self._env.python), script_kind=script_kind + ) + + self.invalid_wheels: dict[Path, list[str]] = {} + + def enable_bytecode_compilation(self, enable: bool = True) -> None: + self._destination.bytecode_optimization_levels = (-1,) if enable else () + + def install(self, wheel: Path) -> None: + with WheelFile.open(wheel) as source: + try: + source.validate_record() + except _WheelFileValidationError as e: + self.invalid_wheels[wheel] = e.issues + install( + source=source, + destination=self._destination.for_source(source), + # Additional metadata that is generated by the installation tool. + additional_metadata={ + "INSTALLER": f"Poetry {__version__}".encode(), + }, + ) diff --git a/src/poetry/json/__init__.py b/src/poetry/json/__init__.py index 1e237794380..3cabdcf69e4 100644 --- a/src/poetry/json/__init__.py +++ b/src/poetry/json/__init__.py @@ -1,7 +1,6 @@ from __future__ import annotations import json -import os from pathlib import Path from typing import Any @@ -11,7 +10,7 @@ from poetry.core.json import SCHEMA_DIR as CORE_SCHEMA_DIR -SCHEMA_DIR = os.path.join(os.path.dirname(__file__), "schemas") +SCHEMA_DIR = Path(__file__).parent / "schemas" class ValidationError(ValueError): @@ -39,16 +38,12 @@ def validate_object(obj: dict[str, Any]) -> list[str]: errors.append(message) core_schema = json.loads( - Path(CORE_SCHEMA_DIR, "poetry-schema.json").read_text(encoding="utf-8") + (CORE_SCHEMA_DIR / "poetry-schema.json").read_text(encoding="utf-8") ) - if core_schema["additionalProperties"]: - # TODO: make this un-conditional once core update to >1.1.0b2 - properties = {*schema["properties"].keys(), *core_schema["properties"].keys()} - additional_properties = set(obj.keys()) - properties - for key in additional_properties: - errors.append( - f"Additional properties are not allowed ('{key}' was unexpected)" - ) + properties = {*schema["properties"].keys(), *core_schema["properties"].keys()} + additional_properties = set(obj.keys()) - properties + for key in additional_properties: + errors.append(f"Additional properties are not allowed ('{key}' was unexpected)") return errors diff --git a/src/poetry/json/schemas/poetry.json b/src/poetry/json/schemas/poetry.json index 7532fd836b4..c9191b03d23 100644 --- a/src/poetry/json/schemas/poetry.json +++ b/src/poetry/json/schemas/poetry.json @@ -26,20 +26,29 @@ "properties": { "name": { "type": "string", - "description": "The name of the repository" + "description": "The name of the repository." }, "url": { "type": "string", - "description": "The url of the repository", + "description": "The url of the repository.", "format": "uri" }, "default": { "type": "boolean", - "description": "Make this repository the default (disable PyPI)" + "description": "Make this repository the default (disable PyPI). (deprecated, see priority)" }, "secondary": { "type": "boolean", - "description": "Declare this repository as secondary, i.e. it will only be looked up last for packages." + "description": "Declare this repository as secondary, i.e. default repositories take precedence. (deprecated, see priority)" + }, + "priority": { + "enum": [ + "primary", + "default", + "secondary", + "explicit" + ], + "description": "Declare the priority of this repository." }, "links": { "type": "boolean", @@ -49,6 +58,22 @@ "type": "boolean", "description": "For PEP 503 simple API repositories, pre-fetch and index the available packages. (experimental)" } + }, + "not": { + "anyOf": [ + { + "required": [ + "priority", + "default" + ] + }, + { + "required": [ + "priority", + "secondary" + ] + } + ] } } } diff --git a/src/poetry/layouts/layout.py b/src/poetry/layouts/layout.py index 66b5ea946ca..45fcb28b6a5 100644 --- a/src/poetry/layouts/layout.py +++ b/src/poetry/layouts/layout.py @@ -5,16 +5,17 @@ from typing import Any from packaging.utils import canonicalize_name -from poetry.core.pyproject.toml import PyProjectTOML from poetry.core.utils.helpers import module_name from tomlkit import inline_table from tomlkit import loads from tomlkit import table from tomlkit.toml_document import TOMLDocument +from poetry.pyproject.toml import PyProjectTOML + if TYPE_CHECKING: - from typing import Mapping + from collections.abc import Mapping from tomlkit.items import InlineTable @@ -48,8 +49,8 @@ def __init__( author: str | None = None, license: str | None = None, python: str = "*", - dependencies: dict[str, str | Mapping[str, Any]] | None = None, - dev_dependencies: dict[str, str | Mapping[str, Any]] | None = None, + dependencies: Mapping[str, str | Mapping[str, Any]] | None = None, + dev_dependencies: Mapping[str, str | Mapping[str, Any]] | None = None, ) -> None: self._project = canonicalize_name(project) self._package_path_relative = Path( @@ -194,6 +195,6 @@ def _create_tests(path: Path) -> None: def _write_poetry(self, path: Path) -> None: pyproject = PyProjectTOML(path / "pyproject.toml") content = self.generate_poetry_content() - for section in content: - pyproject.data.append(section, content[section]) + for section, item in content.items(): + pyproject.data.append(section, item) pyproject.save() diff --git a/src/poetry/locations.py b/src/poetry/locations.py index 0e3b884eb5b..7d10f7d11d8 100644 --- a/src/poetry/locations.py +++ b/src/poetry/locations.py @@ -33,9 +33,14 @@ if any(file.exists() for file in (auth_toml, config_toml)): logger.warning( - "Configuration file exists at %s, reusing this directory.\n\nConsider" - " moving configuration to %s, as support for the legacy directory will be" - " removed in an upcoming release.", + ( + ( + "Configuration file exists at %s, reusing this" + " directory.\n\nConsider moving TOML configuration files to %s, as" + " support for the legacy directory will be removed in an upcoming" + " release." + ), + ), _LEGACY_CONFIG_DIR, CONFIG_DIR, ) diff --git a/src/poetry/masonry/builders/editable.py b/src/poetry/masonry/builders/editable.py index 1b38c2bee03..81d577b1445 100644 --- a/src/poetry/masonry/builders/editable.py +++ b/src/poetry/masonry/builders/editable.py @@ -4,7 +4,6 @@ import hashlib import json import os -import shutil from base64 import urlsafe_b64encode from pathlib import Path @@ -44,6 +43,7 @@ class EditableBuilder(Builder): def __init__(self, poetry: Poetry, env: Env, io: IO) -> None: + self._poetry: Poetry super().__init__(poetry) self._env = env @@ -105,19 +105,15 @@ def _setup_build(self) -> None: pip_install(self._path, self._env, upgrade=True, editable=True) else: # Temporarily rename pyproject.toml - shutil.move( - str(self._poetry.file), str(self._poetry.file.with_suffix(".tmp")) - ) + renamed_pyproject = self._poetry.file.with_suffix(".tmp") + self._poetry.file.path.rename(renamed_pyproject) try: pip_install(self._path, self._env, upgrade=True, editable=True) finally: - shutil.move( - str(self._poetry.file.with_suffix(".tmp")), - str(self._poetry.file), - ) + renamed_pyproject.rename(self._poetry.file.path) finally: if not has_setup: - os.remove(str(setup)) + os.remove(setup) def _add_pth(self) -> list[Path]: paths = { @@ -249,7 +245,7 @@ def _add_dist_info(self, added_files: list[Path]) -> None: json.dumps( { "dir_info": {"editable": True}, - "url": self._poetry.file.path.parent.as_uri(), + "url": self._poetry.file.path.parent.absolute().as_uri(), } ) ) diff --git a/src/poetry/mixology/failure.py b/src/poetry/mixology/failure.py index f4629aa56c7..6d13ee2ca56 100644 --- a/src/poetry/mixology/failure.py +++ b/src/poetry/mixology/failure.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING -from typing import cast from poetry.core.constraints.version import parse_constraint @@ -114,7 +113,8 @@ def _visit( conjunction = "So," if conclusion or incompatibility == self._root else "And" incompatibility_string = str(incompatibility) - cause: ConflictCause = cast(ConflictCause, incompatibility.cause) + cause = incompatibility.cause + assert isinstance(cause, ConflictCause) if isinstance(cause.conflict.cause, ConflictCause) and isinstance( cause.other.cause, ConflictCause @@ -144,8 +144,10 @@ def _visit( self._visit(without_line) self._write( incompatibility, - f"{conjunction} because {with_line!s} ({line})," - f" {incompatibility_string}.", + ( + f"{conjunction} because {with_line!s} ({line})," + f" {incompatibility_string}." + ), numbered=numbered, ) else: @@ -170,9 +172,11 @@ def _visit( self._write( incompatibility, - f"{conjunction} because" - f" {cause.conflict!s} ({self._line_numbers[cause.conflict]})," - f" {incompatibility_string}", + ( + f"{conjunction} because {cause.conflict!s}" + f" ({self._line_numbers[cause.conflict]})," + f" {incompatibility_string}" + ), numbered=numbered, ) elif isinstance(cause.conflict.cause, ConflictCause) or isinstance( @@ -198,7 +202,8 @@ def _visit( numbered=numbered, ) elif self._is_collapsible(derived): - derived_cause: ConflictCause = cast(ConflictCause, derived.cause) + derived_cause = derived.cause + assert isinstance(derived_cause, ConflictCause) if isinstance(derived_cause.conflict.cause, ConflictCause): collapsed_derived = derived_cause.conflict collapsed_ext = derived_cause.other @@ -233,7 +238,8 @@ def _is_collapsible(self, incompatibility: Incompatibility) -> bool: if self._derivations[incompatibility] > 1: return False - cause: ConflictCause = cast(ConflictCause, incompatibility.cause) + cause = incompatibility.cause + assert isinstance(cause, ConflictCause) if isinstance(cause.conflict.cause, ConflictCause) and isinstance( cause.other.cause, ConflictCause ): diff --git a/src/poetry/mixology/incompatibility.py b/src/poetry/mixology/incompatibility.py index b629ea7a699..8ac85731a17 100644 --- a/src/poetry/mixology/incompatibility.py +++ b/src/poetry/mixology/incompatibility.py @@ -5,7 +5,6 @@ from poetry.mixology.incompatibility_cause import ConflictCause from poetry.mixology.incompatibility_cause import DependencyCause from poetry.mixology.incompatibility_cause import NoVersionsCause -from poetry.mixology.incompatibility_cause import PackageNotFoundCause from poetry.mixology.incompatibility_cause import PlatformCause from poetry.mixology.incompatibility_cause import PythonCause from poetry.mixology.incompatibility_cause import RootCause @@ -146,11 +145,6 @@ def __str__(self) -> str: f"no versions of {self._terms[0].dependency.name} match" f" {self._terms[0].constraint}" ) - elif isinstance(self._cause, PackageNotFoundCause): - assert len(self._terms) == 1 - assert self._terms[0].is_positive() - - return f"{self._terms[0].dependency.name} doesn't exist" elif isinstance(self._cause, RootCause): assert len(self._terms) == 1 assert not self._terms[0].is_positive() @@ -420,8 +414,6 @@ def _try_requires_forbidden( 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): - buffer.append("which doesn't exist") else: buffer.append("which is forbidden") diff --git a/src/poetry/mixology/incompatibility_cause.py b/src/poetry/mixology/incompatibility_cause.py index 87eee3eef16..1536d1b22b2 100644 --- a/src/poetry/mixology/incompatibility_cause.py +++ b/src/poetry/mixology/incompatibility_cause.py @@ -79,17 +79,3 @@ def __init__(self, platform: str) -> None: @property def platform(self) -> str: return self._platform - - -class PackageNotFoundCause(IncompatibilityCause): - """ - The incompatibility represents a package that couldn't be found by its - source. - """ - - def __init__(self, error: Exception) -> None: - self._error = error - - @property - def error(self) -> Exception: - return self._error diff --git a/src/poetry/mixology/solutions/providers/python_requirement_solution_provider.py b/src/poetry/mixology/solutions/providers/python_requirement_solution_provider.py index dba0d58480e..ef278abb57b 100644 --- a/src/poetry/mixology/solutions/providers/python_requirement_solution_provider.py +++ b/src/poetry/mixology/solutions/providers/python_requirement_solution_provider.py @@ -6,31 +6,32 @@ from crashtest.contracts.has_solutions_for_exception import HasSolutionsForException +from poetry.puzzle.exceptions import SolverProblemError + if TYPE_CHECKING: from crashtest.contracts.solution import Solution - from poetry.puzzle.exceptions import SolverProblemError - -class PythonRequirementSolutionProvider(HasSolutionsForException): # type: ignore[misc] +class PythonRequirementSolutionProvider(HasSolutionsForException): def can_solve(self, exception: Exception) -> bool: - from poetry.puzzle.exceptions import SolverProblemError - if not isinstance(exception, SolverProblemError): return False m = re.match( - "^The current project's Python requirement (.+) is not compatible " - "with some of the required packages Python requirement", + ( + "^The current project's Python requirement (.+) is not compatible " + "with some of the required packages Python requirement" + ), str(exception), ) return bool(m) - def get_solutions(self, exception: SolverProblemError) -> list[Solution]: + def get_solutions(self, exception: Exception) -> list[Solution]: from poetry.mixology.solutions.solutions.python_requirement_solution import ( PythonRequirementSolution, ) + assert isinstance(exception, SolverProblemError) return [PythonRequirementSolution(exception)] diff --git a/src/poetry/mixology/solutions/solutions/python_requirement_solution.py b/src/poetry/mixology/solutions/solutions/python_requirement_solution.py index 54e6c819107..b625e124610 100644 --- a/src/poetry/mixology/solutions/solutions/python_requirement_solution.py +++ b/src/poetry/mixology/solutions/solutions/python_requirement_solution.py @@ -10,7 +10,7 @@ from poetry.puzzle.exceptions import SolverProblemError -class PythonRequirementSolution(Solution): # type: ignore[misc] +class PythonRequirementSolution(Solution): def __init__(self, exception: SolverProblemError) -> None: from poetry.core.constraints.version import parse_constraint diff --git a/src/poetry/mixology/version_solver.py b/src/poetry/mixology/version_solver.py index 06ae39443e0..6671c684606 100644 --- a/src/poetry/mixology/version_solver.py +++ b/src/poetry/mixology/version_solver.py @@ -3,7 +3,6 @@ import functools import time -from contextlib import suppress from typing import TYPE_CHECKING from poetry.core.packages.dependency import Dependency @@ -12,7 +11,6 @@ from poetry.mixology.incompatibility import Incompatibility from poetry.mixology.incompatibility_cause import ConflictCause from poetry.mixology.incompatibility_cause import NoVersionsCause -from poetry.mixology.incompatibility_cause import PackageNotFoundCause from poetry.mixology.incompatibility_cause import RootCause from poetry.mixology.partial_solution import PartialSolution from poetry.mixology.result import SolverResult @@ -322,8 +320,8 @@ def _resolve_conflict(self, incompatibility: Incompatibility) -> Incompatibility # The most_recent_satisfier may not satisfy most_recent_term on its own # if there are a collection of constraints on most_recent_term that # only satisfy it together. For example, if most_recent_term is - # `foo ^1.0.0` and _solution contains `[foo >=1.0.0, - # foo <2.0.0]`, then most_recent_satisfier will be `foo <2.0.0` even + # `foo ^1.0.0` and _solution contains `[foo >=1.0.0, + # foo <2.0.0]`, then most_recent_satisfier will be `foo <2.0.0` even # though it doesn't totally satisfy `foo ^1.0.0`. # # In this case, we add `not (most_recent_satisfier \ most_recent_term)` to @@ -333,7 +331,9 @@ def _resolve_conflict(self, incompatibility: Incompatibility) -> Incompatibility # .. _algorithm documentation: # https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution # noqa: E501 if difference is not None: - new_terms.append(difference.inverse) + inverse = difference.inverse + if inverse.dependency != most_recent_satisfier.dependency: + new_terms.append(inverse) incompatibility = Incompatibility( new_terms, ConflictCause(incompatibility, most_recent_satisfier.cause) @@ -395,10 +395,8 @@ def _get_min(dependency: Dependency) -> tuple[bool, int, int]: if locked: return is_specific_marker, Preference.LOCKED, 1 - try: - num_packages = len(self._dependency_cache.search_for(dependency)) - except ValueError: - num_packages = 0 + num_packages = len(self._dependency_cache.search_for(dependency)) + if num_packages < 2: preference = Preference.NO_CHOICE elif use_latest: @@ -414,28 +412,8 @@ def _get_min(dependency: Dependency) -> tuple[bool, int, int]: locked = self._provider.get_locked(dependency) if locked is None: - try: - packages = self._dependency_cache.search_for(dependency) - except ValueError as e: - self._add_incompatibility( - Incompatibility([Term(dependency, True)], PackageNotFoundCause(e)) - ) - complete_name: str = dependency.complete_name - return complete_name - - package = None - if locked is not None: - package = next( - ( - p - for p in packages - if p.package.version == locked.package.version - ), - None, - ) - if package is None: - with suppress(IndexError): - package = packages[0] + packages = self._dependency_cache.search_for(dependency) + package = next(iter(packages), None) if package is None: # If there are no versions that satisfy the constraint, diff --git a/src/poetry/packages/locker.py b/src/poetry/packages/locker.py index b9df3e15456..987a1acd609 100644 --- a/src/poetry/packages/locker.py +++ b/src/poetry/packages/locker.py @@ -16,7 +16,6 @@ from poetry.core.constraints.version import parse_constraint from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package -from poetry.core.toml.file import TOMLFile from poetry.core.version.markers import parse_marker from poetry.core.version.requirements import InvalidRequirement from tomlkit import array @@ -24,7 +23,10 @@ from tomlkit import document from tomlkit import inline_table from tomlkit import table -from tomlkit.exceptions import TOMLKitError + +from poetry.__version__ import __version__ +from poetry.toml.file import TOMLFile +from poetry.utils._compat import tomllib if TYPE_CHECKING: @@ -32,7 +34,6 @@ 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 tomlkit.items import Table from tomlkit.toml_document import TOMLDocument from poetry.repositories.lockfile_repository import LockfileRepository @@ -40,8 +41,8 @@ logger = logging.getLogger(__name__) _GENERATED_IDENTIFIER = "@" + "generated" GENERATED_COMMENT = ( - f"This file is automatically {_GENERATED_IDENTIFIER} by Poetry and should not be" - " changed by hand." + f"This file is automatically {_GENERATED_IDENTIFIER} by Poetry" + f" {__version__} and should not be changed by hand." ) @@ -52,18 +53,18 @@ class Locker: _legacy_keys = ["dependencies", "source", "extras", "dev-dependencies"] _relevant_keys = [*_legacy_keys, "group"] - def __init__(self, lock: str | Path, local_config: dict[str, Any]) -> None: - self._lock = TOMLFile(lock) + def __init__(self, lock: Path, local_config: dict[str, Any]) -> None: + self._lock = lock self._local_config = local_config - self._lock_data: TOMLDocument | None = None + self._lock_data: dict[str, Any] | None = None self._content_hash = self._get_content_hash() @property - def lock(self) -> TOMLFile: + def lock(self) -> Path: return self._lock @property - def lock_data(self) -> TOMLDocument: + def lock_data(self) -> dict[str, Any]: if self._lock_data is None: self._lock_data = self._get_lock_data() @@ -79,7 +80,8 @@ def is_fresh(self) -> bool: """ Checks whether the lock file is still up to date with the current hash. """ - lock = self._lock.read() + with self.lock.open("rb") as f: + lock = tomllib.load(f) metadata = lock.get("metadata", {}) if "content-hash" in metadata: @@ -111,13 +113,12 @@ def locked_repository(self) -> LockfileRepository: source_type = source.get("type") url = source.get("url") if source_type in ["directory", "file"]: - url = self._lock.path.parent.joinpath(url).resolve().as_posix() + url = self.lock.parent.joinpath(url).resolve().as_posix() name = info["name"] package = Package( name, info["version"], - info["version"], source_type=source_type, source_url=url, source_reference=source.get("reference"), @@ -196,7 +197,7 @@ def locked_repository(self) -> LockfileRepository: package.marker = parse_marker(split_dep[1].strip()) for dep_name, constraint in info.get("dependencies", {}).items(): - root_dir = self._lock.path.parent + root_dir = self.lock.parent if package.source_type == "directory": # root dir should be the source of the package relative to the lock # path @@ -223,6 +224,18 @@ def locked_repository(self) -> LockfileRepository: return repository def set_lock_data(self, root: Package, packages: list[Package]) -> bool: + """Store lock data and eventually persist to the lock file""" + lock = self._compute_lock_data(root, packages) + + if self._should_write(lock): + self._write_lock_data(lock) + return True + + return False + + def _compute_lock_data( + self, root: Package, packages: list[Package] + ) -> TOMLDocument: package_specs = self._lock_packages(packages) # Retrieving hashes for package in package_specs: @@ -243,7 +256,7 @@ def set_lock_data(self, root: Package, packages: list[Package]) -> bool: if root.extras: lock["extras"] = { - extra: [dep.pretty_name for dep in deps] + extra: sorted(dep.pretty_name for dep in deps) for extra, deps in sorted(root.extras.items()) } @@ -253,6 +266,10 @@ def set_lock_data(self, root: Package, packages: list[Package]) -> bool: "content-hash": self._content_hash, } + return lock + + def _should_write(self, lock: TOMLDocument) -> bool: + # if lock file exists: compare with existing lock data do_write = True if self.is_locked(): try: @@ -262,16 +279,11 @@ def set_lock_data(self, root: Package, packages: list[Package]) -> bool: pass else: do_write = lock != lock_data - if do_write: - self._write_lock_data(lock) return do_write def _write_lock_data(self, data: TOMLDocument) -> None: - self.lock.write(data) - - # Checking lock file data consistency - if data != self.lock.read(): - raise RuntimeError("Inconsistent lock file data.") + lockfile = TOMLFile(self.lock) + lockfile.write(data) self._lock_data = None @@ -292,16 +304,17 @@ def _get_content_hash(self) -> str: return sha256(json.dumps(relevant_content, sort_keys=True).encode()).hexdigest() - def _get_lock_data(self) -> TOMLDocument: - if not self._lock.exists(): + def _get_lock_data(self) -> dict[str, Any]: + if not self.lock.exists(): raise RuntimeError("No lockfile found. Unable to read locked packages") - try: - lock_data: TOMLDocument = self._lock.read() - except TOMLKitError as e: - raise RuntimeError(f"Unable to read the lock file ({e}).") + with self.lock.open("rb") as f: + try: + lock_data = tomllib.load(f) + except tomllib.TOMLDecodeError as e: + raise RuntimeError(f"Unable to read the lock file ({e}).") - metadata = cast("Table", lock_data["metadata"]) + metadata = lock_data["metadata"] lock_version = Version.parse(metadata.get("lock-version", "1.0")) current_version = Version.parse(self._VERSION) accepted_versions = parse_constraint(self._READ_VERSION_RANGE) @@ -379,6 +392,10 @@ def _dump_package(self, package: Package) -> dict[str, Any]: constraint["tag"] = dependency.tag elif dependency.rev: constraint["rev"] = dependency.rev + + if dependency.directory: + constraint["subdirectory"] = dependency.directory + else: constraint["version"] = str(dependency.pretty_constraint) @@ -441,7 +458,7 @@ def _dump_package(self, package: Package) -> dict[str, Any]: url = Path( os.path.relpath( Path(url).resolve(), - Path(self._lock.path.parent).resolve(), + Path(self.lock.parent).resolve(), ) ).as_posix() @@ -465,8 +482,3 @@ def _dump_package(self, package: Package) -> dict[str, Any]: data["develop"] = package.develop return data - - -class NullLocker(Locker): - def set_lock_data(self, root: Package, packages: list[Package]) -> bool: - pass diff --git a/src/poetry/plugins/plugin_manager.py b/src/poetry/plugins/plugin_manager.py index 1a01dc36335..309facb1c39 100644 --- a/src/poetry/plugins/plugin_manager.py +++ b/src/poetry/plugins/plugin_manager.py @@ -71,7 +71,7 @@ def activate(self, *args: Any, **kwargs: Any) -> None: plugin.activate(*args, **kwargs) def _load_plugin_entry_point(self, ep: metadata.EntryPoint) -> None: - logger.debug("Loading the %s plugin", ep.name) # type: ignore[attr-defined] + logger.debug("Loading the %s plugin", ep.name) plugin = ep.load() # type: ignore[no-untyped-call] diff --git a/src/poetry/poetry.py b/src/poetry/poetry.py index 42ceff2e47e..3cb227eaaaa 100644 --- a/src/poetry/poetry.py +++ b/src/poetry/poetry.py @@ -2,11 +2,13 @@ from typing import TYPE_CHECKING from typing import Any +from typing import cast from poetry.core.poetry import Poetry as BasePoetry from poetry.__version__ import __version__ from poetry.config.source import Source +from poetry.pyproject.toml import PyProjectTOML if TYPE_CHECKING: @@ -17,7 +19,8 @@ from poetry.config.config import Config from poetry.packages.locker import Locker from poetry.plugins.plugin_manager import PluginManager - from poetry.repositories.pool import Pool + from poetry.repositories.repository_pool import RepositoryPool + from poetry.toml import TOMLFile class Poetry(BasePoetry): @@ -32,22 +35,39 @@ def __init__( config: Config, disable_cache: bool = False, ) -> None: - from poetry.repositories.pool import Pool - - super().__init__(file, local_config, package) + from poetry.repositories.repository_pool import RepositoryPool + + try: + super().__init__( # type: ignore[call-arg] + file, local_config, package, pyproject_type=PyProjectTOML + ) + except TypeError: + # TODO: backward compatibility, can be simplified if poetry-core with + # https://github.com/python-poetry/poetry-core/pull/483 is available + super().__init__(file, local_config, package) + self._pyproject = PyProjectTOML(file) self._locker = locker self._config = config - self._pool = Pool() + self._pool = RepositoryPool() self._plugin_manager: PluginManager | None = None self._disable_cache = disable_cache + @property + def pyproject(self) -> PyProjectTOML: + pyproject = super().pyproject + return cast("PyProjectTOML", pyproject) + + @property + def file(self) -> TOMLFile: # type: ignore[override] + return self.pyproject.file + @property def locker(self) -> Locker: return self._locker @property - def pool(self) -> Pool: + def pool(self) -> RepositoryPool: return self._pool @property @@ -63,7 +83,7 @@ def set_locker(self, locker: Locker) -> Poetry: return self - def set_pool(self, pool: Pool) -> Poetry: + def set_pool(self, pool: RepositoryPool) -> Poetry: self._pool = pool return self diff --git a/src/poetry/publishing/uploader.py b/src/poetry/publishing/uploader.py index 7b1e8e19048..256fd235259 100644 --- a/src/poetry/publishing/uploader.py +++ b/src/poetry/publishing/uploader.py @@ -21,6 +21,7 @@ from poetry.__version__ import __version__ from poetry.utils.constants import REQUESTS_TIMEOUT +from poetry.utils.constants import STATUS_FORCELIST from poetry.utils.patterns import wheel_file_re @@ -68,7 +69,8 @@ def adapter(self) -> adapters.HTTPAdapter: connect=5, total=10, allowed_methods=["GET"], - status_forcelist=[500, 501, 502, 503], + respect_retry_after_header=True, + status_forcelist=STATUS_FORCELIST, ) return adapters.HTTPAdapter(max_retries=retry) @@ -266,7 +268,7 @@ def _upload_file( f" - Uploading {file.name} %percent%%" ) bar.finish() - elif resp.status_code == 301: + elif 300 <= resp.status_code < 400: if self._io.output.is_decorated(): self._io.overwrite( f" - Uploading {file.name} FAILED" diff --git a/src/poetry/puzzle/provider.py b/src/poetry/puzzle/provider.py index 1e634995dd7..28558ab4607 100644 --- a/src/poetry/puzzle/provider.py +++ b/src/poetry/puzzle/provider.py @@ -12,7 +12,6 @@ from contextlib import contextmanager from pathlib import Path from typing import TYPE_CHECKING -from typing import Collection from typing import cast from cleo.ui.progress_indicator import ProgressIndicator @@ -20,6 +19,7 @@ from poetry.core.constraints.version import Version from poetry.core.packages.utils.utils import get_python_constraint_from_marker from poetry.core.version.markers import AnyMarker +from poetry.core.version.markers import EmptyMarker from poetry.core.version.markers import MarkerUnion from poetry.inspection.info import PackageInfo @@ -33,11 +33,13 @@ from poetry.puzzle.exceptions import OverrideNeeded from poetry.repositories.exceptions import PackageNotFound from poetry.utils.helpers import download_file +from poetry.utils.helpers import get_file_hash from poetry.vcs.git import Git if TYPE_CHECKING: from collections.abc import Callable + from collections.abc import Collection from collections.abc import Iterable from collections.abc import Iterator @@ -48,18 +50,31 @@ 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.path_dependency import PathDependency from poetry.core.packages.url_dependency import URLDependency from poetry.core.packages.vcs_dependency import VCSDependency from poetry.core.version.markers import BaseMarker - from poetry.repositories import Pool + from poetry.repositories import RepositoryPool from poetry.utils.env import Env logger = logging.getLogger(__name__) -class Indicator(ProgressIndicator): # type: ignore[misc] +class IncompatibleConstraintsError(Exception): + """ + Exception when there are duplicate dependencies with incompatible constraints. + """ + + def __init__(self, package: Package, *dependencies: Dependency) -> None: + constraints = "\n".join(dep.to_pep_508() for dep in dependencies) + super().__init__( + f"Incompatible constraints in requirements of {package}:\n{constraints}" + ) + + +class Indicator(ProgressIndicator): CONTEXT: str | None = None @staticmethod @@ -124,7 +139,7 @@ class Provider: def __init__( self, package: Package, - pool: Pool, + pool: RepositoryPool, io: IO, *, installed: list[Package] | None = None, @@ -156,7 +171,7 @@ def __init__( ) @property - def pool(self) -> Pool: + def pool(self) -> RepositoryPool: return self._pool @property @@ -376,6 +391,7 @@ def get_package_from_vcs( ) def _search_for_file(self, dependency: FileDependency) -> Package: + dependency.validate(raise_error=True) package = self.get_package_from_file(dependency.full_path) self.validate_package_for_dependency(dependency=dependency, package=package) @@ -384,7 +400,10 @@ def _search_for_file(self, dependency: FileDependency) -> Package: package.root_dir = dependency.base package.files = [ - {"file": dependency.path.name, "hash": "sha256:" + dependency.hash()} + { + "file": dependency.path.name, + "hash": "sha256:" + get_file_hash(dependency.full_path), + } ] return package @@ -403,6 +422,7 @@ def get_package_from_file(cls, file_path: Path) -> Package: return package def _search_for_directory(self, dependency: DirectoryDependency) -> Package: + dependency.validate(raise_error=True) package = self.get_package_from_directory(dependency.full_path) self.validate_package_for_dependency(dependency=dependency, package=package) @@ -441,6 +461,10 @@ def get_package_from_url(cls, url: str) -> Package: download_file(url, dest) package = cls.get_package_from_file(dest) + package.files = [ + {"file": file_name, "hash": "sha256:" + get_file_hash(dest)} + ] + package._source_type = "url" package._source_url = url @@ -631,6 +655,11 @@ def complete_package( if locked is not None and locked.package.is_same_package_as(dep): continue self.search_for_direct_origin_dependency(dep) + else: + for dep in _dependencies: + if dep.is_file() or dep.is_directory(): + dep = cast("PathDependency", dep) + dep.validate(raise_error=True) dependencies = self._get_dependencies_with_overrides( _dependencies, dependency_package @@ -740,55 +769,7 @@ def fmt_warning(d: Dependency) -> str: f"Different requirements found for {warnings}." ) - # We need to check if one of the duplicate dependencies - # has no markers. If there is one, we need to change its - # environment markers to the inverse of the union of the - # other dependencies markers. - # For instance, if we have the following dependencies: - # - ipython - # - ipython (1.2.4) ; implementation_name == "pypy" - # - # the marker for `ipython` will become `implementation_name != "pypy"`. - # - # Further, we have to merge the constraints of the requirements - # without markers into the constraints of the requirements with markers. - # for instance, if we have the following dependencies: - # - foo (>= 1.2) - # - foo (!= 1.2.1) ; python == 3.10 - # - # the constraint for the second entry will become (!= 1.2.1, >= 1.2) - any_markers_dependencies = [d for d in deps if d.marker.is_any()] - other_markers_dependencies = [d for d in deps if not d.marker.is_any()] - - marker = other_markers_dependencies[0].marker - for other_dep in other_markers_dependencies[1:]: - marker = marker.union(other_dep.marker) - inverted_marker = marker.invert() - - if any_markers_dependencies: - for dep_any in any_markers_dependencies: - dep_any.marker = inverted_marker - for dep_other in other_markers_dependencies: - dep_other.constraint = dep_other.constraint.intersect( - dep_any.constraint - ) - elif not inverted_marker.is_empty() and self._python_constraint.allows_any( - get_python_constraint_from_marker(inverted_marker) - ): - # if there is no any marker dependency - # and the inverted marker is not empty, - # a dependency with the inverted union of all markers is required - # in order to not miss other dependencies later, for instance: - # - foo (1.0) ; python == 3.7 - # - foo (2.0) ; python == 3.8 - # - bar (2.0) ; python == 3.8 - # - bar (3.0) ; python == 3.9 - # - # the last dependency would be missed without this, - # because the intersection with both foo dependencies is empty - inverted_marker_dep = deps[0].with_constraint(EmptyConstraint()) - inverted_marker_dep.marker = inverted_marker - deps.append(inverted_marker_dep) + deps = self._handle_any_marker_dependencies(package, deps) overrides = [] overrides_marker_intersection: BaseMarker = AnyMarker() @@ -853,17 +834,7 @@ def get_locked(self, dependency: Dependency) -> DependencyPackage | None: locked = self._locked.get(dependency.name, []) for dependency_package in locked: package = dependency_package.package - if ( - # Locked dependencies are always without features. - # Thus, we can't use is_same_package_as() here because it compares - # the complete_name (including features). - dependency.name == package.name - and ( - dependency.source_type is None - or dependency.is_same_source_as(package) - ) - and dependency.constraint.allows(package.version) - ): + if package.satisfies(dependency): return DependencyPackage(dependency, package) return None @@ -984,20 +955,13 @@ def _merge_dependencies_by_constraint( for dep in dependencies: by_constraint[dep.constraint].append(dep) for constraint, _deps in by_constraint.items(): - new_markers = [] - for dep in _deps: - marker = dep.marker.without_extras() - if marker.is_any(): - # No marker or only extras - continue - - new_markers.append(marker) - - if not new_markers: - continue + new_markers = [dep.marker for dep in _deps] dep = _deps[0] - dep.marker = dep.marker.union(MarkerUnion(*new_markers)) + + # Union with EmptyMarker is to make sure we get the benefit of marker + # simplifications. + dep.marker = MarkerUnion(*new_markers).union(EmptyMarker()) by_constraint[constraint] = [dep] return [value[0] for value in by_constraint.values()] @@ -1031,3 +995,73 @@ def _merge_dependencies_by_marker( ) deps.append(_deps[0].with_constraint(new_constraint)) return deps + + def _handle_any_marker_dependencies( + self, package: Package, dependencies: list[Dependency] + ) -> list[Dependency]: + """ + We need to check if one of the duplicate dependencies + has no markers. If there is one, we need to change its + environment markers to the inverse of the union of the + other dependencies markers. + For instance, if we have the following dependencies: + - ipython + - ipython (1.2.4) ; implementation_name == "pypy" + + the marker for `ipython` will become `implementation_name != "pypy"`. + + Further, we have to merge the constraints of the requirements + without markers into the constraints of the requirements with markers. + for instance, if we have the following dependencies: + - foo (>= 1.2) + - foo (!= 1.2.1) ; python == 3.10 + + the constraint for the second entry will become (!= 1.2.1, >= 1.2). + """ + any_markers_dependencies = [d for d in dependencies if d.marker.is_any()] + other_markers_dependencies = [d for d in dependencies if not d.marker.is_any()] + + if any_markers_dependencies: + for dep_other in other_markers_dependencies: + new_constraint = dep_other.constraint + for dep_any in any_markers_dependencies: + new_constraint = new_constraint.intersect(dep_any.constraint) + if new_constraint.is_empty(): + raise IncompatibleConstraintsError( + package, dep_other, *any_markers_dependencies + ) + dep_other.constraint = new_constraint + + marker = other_markers_dependencies[0].marker + for other_dep in other_markers_dependencies[1:]: + marker = marker.union(other_dep.marker) + inverted_marker = marker.invert() + + if ( + not inverted_marker.is_empty() + and self._python_constraint.allows_any( + get_python_constraint_from_marker(inverted_marker) + ) + and (not self._env or inverted_marker.validate(self._env.marker_env)) + ): + if any_markers_dependencies: + for dep_any in any_markers_dependencies: + dep_any.marker = inverted_marker + else: + # If there is no any marker dependency + # and the inverted marker is not empty, + # a dependency with the inverted union of all markers is required + # in order to not miss other dependencies later, for instance: + # - foo (1.0) ; python == 3.7 + # - foo (2.0) ; python == 3.8 + # - bar (2.0) ; python == 3.8 + # - bar (3.0) ; python == 3.9 + # + # the last dependency would be missed without this, + # because the intersection with both foo dependencies is empty. + inverted_marker_dep = dependencies[0].with_constraint(EmptyConstraint()) + inverted_marker_dep.marker = inverted_marker + dependencies.append(inverted_marker_dep) + else: + dependencies = other_markers_dependencies + return dependencies diff --git a/src/poetry/puzzle/solver.py b/src/poetry/puzzle/solver.py index 3a9f9b89d22..682033a04ae 100644 --- a/src/poetry/puzzle/solver.py +++ b/src/poetry/puzzle/solver.py @@ -5,7 +5,6 @@ from collections import defaultdict from contextlib import contextmanager from typing import TYPE_CHECKING -from typing import Collection from typing import FrozenSet from typing import Tuple from typing import TypeVar @@ -21,6 +20,7 @@ if TYPE_CHECKING: + from collections.abc import Collection from collections.abc import Iterator from cleo.io.io import IO @@ -31,7 +31,7 @@ from poetry.packages import DependencyPackage from poetry.puzzle.transaction import Transaction - from poetry.repositories import Pool + from poetry.repositories import RepositoryPool from poetry.utils.env import Env @@ -39,7 +39,7 @@ class Solver: def __init__( self, package: ProjectPackage, - pool: Pool, + pool: RepositoryPool, installed: list[Package], locked: list[Package], io: IO, @@ -181,8 +181,16 @@ def _solve(self) -> tuple[list[Package], list[int]]: if _package.name == dep.name: continue - if dep not in _package.requires: + try: + index = _package.requires.index(dep) + except ValueError: _package.add_dependency(dep) + else: + _dep = _package.requires[index] + if _dep.marker != dep.marker: + # marker of feature package is more accurate + # because it includes relevant extras + _dep.marker = dep.marker else: final_packages.append(package) depths.append(results[package]) diff --git a/src/poetry/puzzle/transaction.py b/src/poetry/puzzle/transaction.py index 5c5dbd061c9..665093416cb 100644 --- a/src/poetry/puzzle/transaction.py +++ b/src/poetry/puzzle/transaction.py @@ -27,7 +27,11 @@ def __init__( self._root_package = root_package def calculate_operations( - self, with_uninstalls: bool = True, synchronize: bool = False + self, + with_uninstalls: bool = True, + synchronize: bool = False, + *, + skip_directory: bool = False, ) -> list[Operation]: from poetry.installation.operations import Install from poetry.installation.operations import Uninstall @@ -70,10 +74,14 @@ def calculate_operations( break - if not installed: + if not ( + installed + or (skip_directory and result_package.source_type == "directory") + ): operations.append(Install(result_package, priority=priority)) if with_uninstalls: + uninstalls: set[str] = set() for current_package in self._current_packages: found = any( current_package.name == result_package.name @@ -83,11 +91,12 @@ def calculate_operations( if not found: for installed_package in self._installed_packages: if installed_package.name == current_package.name: + uninstalls.add(installed_package.name) operations.append(Uninstall(current_package)) if synchronize: - current_package_names = { - current_package.name for current_package in self._current_packages + result_package_names = { + result_package.name for result_package, _ in self._result_packages } # We preserve pip/setuptools/wheel when not managed by poetry, this is # done to avoid externally managed virtual environments causing @@ -96,9 +105,12 @@ def calculate_operations( "pip", "setuptools", "wheel", - } - current_package_names + } - result_package_names for installed_package in self._installed_packages: + if installed_package.name in uninstalls: + continue + if ( self._root_package and installed_package.name == self._root_package.name @@ -108,7 +120,8 @@ def calculate_operations( if installed_package.name in preserved_package_names: continue - if installed_package.name not in current_package_names: + if installed_package.name not in result_package_names: + uninstalls.add(installed_package.name) operations.append(Uninstall(installed_package)) return sorted( diff --git a/src/poetry/pyproject/__init__.py b/src/poetry/pyproject/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/poetry/pyproject/toml.py b/src/poetry/pyproject/toml.py new file mode 100644 index 00000000000..3e9010b4c85 --- /dev/null +++ b/src/poetry/pyproject/toml.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from poetry.core.pyproject.toml import PyProjectTOML as BasePyProjectTOML +from tomlkit.api import table +from tomlkit.items import Table +from tomlkit.toml_document import TOMLDocument + +from poetry.toml import TOMLFile + + +if TYPE_CHECKING: + from pathlib import Path + + +class PyProjectTOML(BasePyProjectTOML): + """ + Enhanced version of poetry-core's PyProjectTOML + which is capable of writing pyproject.toml + + The poetry-core class uses tomli to read the file, + here we use tomlkit to preserve comments and formatting when writing. + """ + + def __init__(self, path: Path) -> None: + super().__init__(path) + self._toml_file = TOMLFile(path=path) + self._toml_document: TOMLDocument | None = None + + @property + def file(self) -> TOMLFile: # type: ignore[override] + return self._toml_file + + @property + def data(self) -> TOMLDocument: + if self._toml_document is None: + if not self.file.exists(): + self._toml_document = TOMLDocument() + else: + self._toml_document = self.file.read() + + return self._toml_document + + def save(self) -> None: + data = self.data + + if self._build_system is not None: + if "build-system" not in data: + data["build-system"] = table() + + build_system = data["build-system"] + assert isinstance(build_system, Table) + + build_system["requires"] = self._build_system.requires + build_system["build-backend"] = self._build_system.build_backend + + self.file.write(data=data) + + def reload(self) -> None: + self._toml_document = None + self._build_system = None diff --git a/src/poetry/repositories/__init__.py b/src/poetry/repositories/__init__.py index ca887e51aa5..d923a9691aa 100644 --- a/src/poetry/repositories/__init__.py +++ b/src/poetry/repositories/__init__.py @@ -1,7 +1,7 @@ from __future__ import annotations -from poetry.repositories.pool import Pool from poetry.repositories.repository import Repository +from poetry.repositories.repository_pool import RepositoryPool -__all__ = ["Pool", "Repository"] +__all__ = ["Repository", "RepositoryPool"] diff --git a/src/poetry/repositories/cached.py b/src/poetry/repositories/cached_repository.py similarity index 98% rename from src/poetry/repositories/cached.py rename to src/poetry/repositories/cached_repository.py index b593feb44a2..a04fb4d56f0 100644 --- a/src/poetry/repositories/cached.py +++ b/src/poetry/repositories/cached_repository.py @@ -22,7 +22,7 @@ class CachedRepository(Repository, ABC): - CACHE_VERSION = parse_constraint("1.1.0") + CACHE_VERSION = parse_constraint("2.0.0") def __init__( self, name: str, disable_cache: bool = False, config: Config | None = None diff --git a/src/poetry/repositories/http.py b/src/poetry/repositories/http_repository.py similarity index 96% rename from src/poetry/repositories/http.py rename to src/poetry/repositories/http_repository.py index 07315736d5a..298fc1e815b 100644 --- a/src/poetry/repositories/http.py +++ b/src/poetry/repositories/http_repository.py @@ -1,5 +1,6 @@ from __future__ import annotations +import functools import hashlib import os import urllib @@ -18,7 +19,7 @@ from poetry.core.utils.helpers import temporary_directory from poetry.core.version.markers import parse_marker -from poetry.repositories.cached import CachedRepository +from poetry.repositories.cached_repository import CachedRepository from poetry.repositories.exceptions import PackageNotFound from poetry.repositories.exceptions import RepositoryError from poetry.repositories.link_sources.html import HTMLPage @@ -29,8 +30,11 @@ if TYPE_CHECKING: + from packaging.utils import NormalizedName + from poetry.config.config import Config from poetry.inspection.info import PackageInfo + from poetry.repositories.link_sources.base import LinkSource from poetry.utils.authenticator import RepositoryCertificateConfig @@ -50,6 +54,7 @@ def __init__( disable_cache=disable_cache, ) self._authenticator.add_repository(name, url) + self.get_page = functools.lru_cache(maxsize=None)(self._get_page) @property def session(self) -> Authenticator: @@ -291,8 +296,8 @@ def _get_response(self, endpoint: str) -> requests.Response | None: ) return response - def _get_page(self, endpoint: str) -> HTMLPage | None: - response = self._get_response(endpoint) + def _get_page(self, name: NormalizedName) -> LinkSource: + response = self._get_response(f"/{name}/") if not response: - return None + raise PackageNotFound(f"Package [{name}] not found.") return HTMLPage(response.url, response.text) diff --git a/src/poetry/repositories/installed_repository.py b/src/poetry/repositories/installed_repository.py index 228df3533f7..d8ff78c8455 100644 --- a/src/poetry/repositories/installed_repository.py +++ b/src/poetry/repositories/installed_repository.py @@ -20,15 +20,6 @@ from poetry.utils.env import Env -_VENDORS = Path(__file__).parent.parent.joinpath("_vendor") - - -try: - FileNotFoundError -except NameError: - FileNotFoundError = OSError - - logger = logging.getLogger(__name__) @@ -266,26 +257,22 @@ def load(cls, env: Env, with_dependencies: bool = False) -> InstalledRepository: if path in skipped: continue - try: - name = canonicalize_name(distribution.metadata["name"]) - except TypeError: + name = distribution.metadata.get("name") # type: ignore[attr-defined] + if name is None: logger.warning( - "Project environment contains an invalid distribution" - " (%s). Consider removing it manually or recreate the" - " environment.", + ( + "Project environment contains an invalid distribution" + " (%s). Consider removing it manually or recreate" + " the environment." + ), path, ) skipped.add(path) continue - if name in seen: - continue + name = canonicalize_name(name) - try: - path.relative_to(_VENDORS) - except ValueError: - pass - else: + if name in seen: continue package = cls.create_package_from_distribution(distribution, env) diff --git a/src/poetry/repositories/legacy_repository.py b/src/poetry/repositories/legacy_repository.py index 9eaf286928b..2c23a9e281d 100644 --- a/src/poetry/repositories/legacy_repository.py +++ b/src/poetry/repositories/legacy_repository.py @@ -7,7 +7,7 @@ from poetry.inspection.info import PackageInfo from poetry.repositories.exceptions import PackageNotFound -from poetry.repositories.http import HTTPRepository +from poetry.repositories.http_repository import HTTPRepository from poetry.repositories.link_sources.html import SimpleRepositoryPage @@ -72,8 +72,9 @@ def package( return package def find_links_for_package(self, package: Package) -> list[Link]: - page = self._get_page(f"/{package.name}/") - if page is None: + try: + page = self.get_page(package.name) + except PackageNotFound: return [] return list(page.links_for_version(package.name, package.version)) @@ -84,18 +85,10 @@ def _find_packages( """ Find packages on the remote server. """ - versions: list[tuple[Version, str | bool]] - - key: str = name - if not constraint.is_any(): - key = f"{key}:{constraint!s}" - - page = self._get_page(f"/{name}/") - if page is None: - self._log( - f"No packages found for {name}", - level="debug", - ) + try: + page = self.get_page(name) + except PackageNotFound: + self._log(f"No packages found for {name}", level="debug") return [] versions = [ @@ -119,9 +112,7 @@ def _find_packages( def _get_release_info( self, name: NormalizedName, version: Version ) -> dict[str, Any]: - page = self._get_page(f"/{name}/") - if page is None: - raise PackageNotFound(f'No package named "{name}"') + page = self.get_page(name) links = list(page.links_for_version(name, version)) yanked = page.yanked(name, version) @@ -132,7 +123,6 @@ def _get_release_info( name=name, version=version.text, summary="", - platform=None, requires_dist=[], requires_python=None, files=[], @@ -141,8 +131,8 @@ def _get_release_info( ), ) - def _get_page(self, endpoint: str) -> SimpleRepositoryPage | None: - response = self._get_response(endpoint) + def _get_page(self, name: NormalizedName) -> SimpleRepositoryPage: + response = self._get_response(f"/{name}/") if not response: - return None + raise PackageNotFound(f"Package [{name}] not found.") return SimpleRepositoryPage(response.url, response.text) diff --git a/src/poetry/repositories/link_sources/base.py b/src/poetry/repositories/link_sources/base.py index 5f9a26c3048..975553eec40 100644 --- a/src/poetry/repositories/link_sources/base.py +++ b/src/poetry/repositories/link_sources/base.py @@ -9,6 +9,7 @@ from poetry.core.constraints.version import Version from poetry.core.packages.package import Package +from poetry.core.version.exceptions import InvalidVersion from poetry.utils._compat import cached_property from poetry.utils.patterns import sdist_file_re @@ -84,7 +85,7 @@ def link_package_data(cls, link: Link) -> Package | None: if version_string: try: version = Version.parse(version_string) - except ValueError: + except InvalidVersion: logger.debug( "Skipping url (%s) due to invalid version (%s)", link.url, version ) diff --git a/src/poetry/repositories/pypi_repository.py b/src/poetry/repositories/pypi_repository.py index 8846b417739..2863382383c 100644 --- a/src/poetry/repositories/pypi_repository.py +++ b/src/poetry/repositories/pypi_repository.py @@ -15,9 +15,9 @@ from poetry.core.version.exceptions import InvalidVersion from poetry.repositories.exceptions import PackageNotFound -from poetry.repositories.http import HTTPRepository +from poetry.repositories.http_repository import HTTPRepository from poetry.repositories.link_sources.json import SimpleJsonPage -from poetry.utils._compat import to_str +from poetry.utils._compat import decode from poetry.utils.constants import REQUESTS_TIMEOUT @@ -82,12 +82,14 @@ def search(self, query: str) -> list[Package]: try: package = Package(name, version) - package.description = to_str(description.strip()) + package.description = decode(description.strip()) results.append(package) except InvalidVersion: self._log( - f'Unable to parse version "{version}" for the {name} package,' - " skipping", + ( + f'Unable to parse version "{version}" for the {name} package,' + " skipping" + ), level="debug", ) @@ -109,32 +111,18 @@ def _find_packages( Find packages on the remote server. """ try: - json_page = self.get_json_page(name) + json_page = self.get_page(name) except PackageNotFound: - self._log( - f"No packages found for {name}", - level="debug", - ) + self._log(f"No packages found for {name}", level="debug") return [] - versions: list[tuple[Version, str | bool]] - - key: str = name - if not constraint.is_any(): - key = f"{key}:{constraint!s}" - versions = [ (version, json_page.yanked(name, version)) for version in json_page.versions(name) if constraint.allows(version) ] - pretty_name = json_page.content["name"] - packages = [ - Package(pretty_name, version, yanked=yanked) for version, yanked in versions - ] - - return packages + return [Package(name, version, yanked=yanked) for version, yanked in versions] def _get_package_info(self, name: str) -> dict[str, Any]: headers = {"Accept": "application/vnd.pypi.simple.v1+json"} @@ -174,7 +162,6 @@ def _get_release_info( name=info["name"], version=info["version"], summary=info["summary"], - platform=info["platform"], requires_dist=info["requires_dist"], requires_python=info["requires_python"], files=info.get("files", []), @@ -224,7 +211,7 @@ def _get_release_info( return data.asdict() - def get_json_page(self, name: NormalizedName) -> SimpleJsonPage: + def _get_page(self, name: NormalizedName) -> SimpleJsonPage: source = self._base_url + f"simple/{name}/" info = self.get_package_info(name) return SimpleJsonPage(source, info) diff --git a/src/poetry/repositories/repository.py b/src/poetry/repositories/repository.py index 46ecaac7475..c9dd4b85969 100644 --- a/src/poetry/repositories/repository.py +++ b/src/poetry/repositories/repository.py @@ -6,7 +6,6 @@ from packaging.utils import canonicalize_name from poetry.core.constraints.version import Version -from poetry.core.constraints.version import VersionRange from poetry.repositories.abstract_repository import AbstractRepository from poetry.repositories.exceptions import PackageNotFound @@ -34,11 +33,10 @@ def packages(self) -> list[Package]: def find_packages(self, dependency: Dependency) -> list[Package]: packages = [] - constraint, allow_prereleases = self._get_constraints_from_dependency( - dependency - ) ignored_pre_release_packages = [] + constraint = dependency.constraint + allow_prereleases = dependency.allows_prereleases() for package in self._find_packages(dependency.name, constraint): if package.yanked and not isinstance(constraint, Version): # PEP 592: yanked files are always ignored, unless they are the only @@ -50,9 +48,7 @@ def find_packages(self, dependency: Dependency) -> list[Package]: and not allow_prereleases and not package.is_direct_origin() ): - if constraint.is_any(): - # we need this when all versions of the package are pre-releases - ignored_pre_release_packages.append(package) + ignored_pre_release_packages.append(package) continue packages.append(package) @@ -94,23 +90,6 @@ def search(self, query: str) -> list[Package]: return results - @staticmethod - def _get_constraints_from_dependency( - dependency: Dependency, - ) -> tuple[VersionConstraint, bool]: - constraint = dependency.constraint - - allow_prereleases = dependency.allows_prereleases() - if isinstance(constraint, VersionRange) and ( - constraint.max is not None - and constraint.max.is_unstable() - or constraint.min is not None - and constraint.min.is_unstable() - ): - allow_prereleases = True - - return constraint, allow_prereleases - def _find_packages( self, name: NormalizedName, constraint: VersionConstraint ) -> list[Package]: diff --git a/src/poetry/repositories/pool.py b/src/poetry/repositories/repository_pool.py similarity index 63% rename from src/poetry/repositories/pool.py rename to src/poetry/repositories/repository_pool.py index 8a345b16cfc..304f7e9ed33 100644 --- a/src/poetry/repositories/pool.py +++ b/src/poetry/repositories/repository_pool.py @@ -1,6 +1,7 @@ from __future__ import annotations import enum +import warnings from collections import OrderedDict from dataclasses import dataclass @@ -25,6 +26,7 @@ class Priority(IntEnum): DEFAULT = enum.auto() PRIMARY = enum.auto() SECONDARY = enum.auto() + EXPLICIT = enum.auto() @dataclass(frozen=True) @@ -33,13 +35,13 @@ class PrioritizedRepository: priority: Priority -class Pool(AbstractRepository): +class RepositoryPool(AbstractRepository): def __init__( self, repositories: list[Repository] | None = None, ignore_repository_names: bool = False, ) -> None: - super().__init__("poetry-pool") + super().__init__("poetry-repository-pool") self._repositories: OrderedDict[str, PrioritizedRepository] = OrderedDict() self._ignore_repository_names = ignore_repository_names @@ -50,11 +52,30 @@ def __init__( @property def repositories(self) -> list[Repository]: - unsorted_repositories = self._repositories.values() - sorted_repositories = sorted( - unsorted_repositories, key=lambda prio_repo: prio_repo.priority + """ + Returns the repositories in the pool, + in the order they will be searched for packages. + + ATTENTION: For backwards compatibility and practical reasons, + repositories with priority EXPLICIT are NOT included, + because they will not be searched. + """ + sorted_repositories = self._sorted_repositories + return [ + prio_repo.repository + for prio_repo in sorted_repositories + if prio_repo.priority is not Priority.EXPLICIT + ] + + @property + def all_repositories(self) -> list[Repository]: + return [prio_repo.repository for prio_repo in self._sorted_repositories] + + @property + def _sorted_repositories(self) -> list[PrioritizedRepository]: + return sorted( + self._repositories.values(), key=lambda prio_repo: prio_repo.priority ) - return [prio_repo.repository for prio_repo in sorted_repositories] def has_default(self) -> bool: return self._contains_priority(Priority.DEFAULT) @@ -71,14 +92,25 @@ def has_repository(self, name: str) -> bool: return name.lower() in self._repositories def repository(self, name: str) -> Repository: + return self._get_prioritized_repository(name).repository + + def get_priority(self, name: str) -> Priority: + return self._get_prioritized_repository(name).priority + + def _get_prioritized_repository(self, name: str) -> PrioritizedRepository: name = name.lower() if self.has_repository(name): - return self._repositories[name].repository + return self._repositories[name] raise IndexError(f'Repository "{name}" does not exist.') def add_repository( - self, repository: Repository, default: bool = False, secondary: bool = False - ) -> Pool: + self, + repository: Repository, + default: bool = False, + secondary: bool = False, + *, + priority: Priority = Priority.PRIMARY, + ) -> RepositoryPool: """ Adds a repository to the pool. """ @@ -88,22 +120,34 @@ def add_repository( f"A repository with name {repository_name} was already added." ) - if default and self.has_default(): + if default or secondary: + warnings.warn( + ( + "Parameters 'default' and 'secondary' to" + " 'RepositoryPool.add_repository' are deprecated. Please provide" + " the keyword-argument 'priority' instead." + ), + DeprecationWarning, + stacklevel=2, + ) + if default: + priority = Priority.DEFAULT + else: + priority = Priority.SECONDARY + + if priority is Priority.DEFAULT and self.has_default(): raise ValueError("Only one repository can be the default.") - priority = Priority.PRIMARY - if default: - priority = Priority.DEFAULT - elif secondary: - priority = Priority.SECONDARY self._repositories[repository_name] = PrioritizedRepository( repository, priority ) return self - def remove_repository(self, name: str) -> Pool: + def remove_repository(self, name: str) -> RepositoryPool: if not self.has_repository(name): - raise IndexError(f"Pool can not remove unknown repository '{name}'.") + raise IndexError( + f"RepositoryPool can not remove unknown repository '{name}'." + ) del self._repositories[name.lower()] return self diff --git a/src/poetry/repositories/single_page_repository.py b/src/poetry/repositories/single_page_repository.py index e8de0b141f8..7bdc469bbf0 100644 --- a/src/poetry/repositories/single_page_repository.py +++ b/src/poetry/repositories/single_page_repository.py @@ -1,15 +1,22 @@ from __future__ import annotations +from typing import TYPE_CHECKING + +from poetry.repositories.exceptions import PackageNotFound from poetry.repositories.legacy_repository import LegacyRepository from poetry.repositories.link_sources.html import SimpleRepositoryPage +if TYPE_CHECKING: + from packaging.utils import NormalizedName + + class SinglePageRepository(LegacyRepository): - def _get_page(self, endpoint: str | None = None) -> SimpleRepositoryPage | None: + def _get_page(self, name: NormalizedName) -> SimpleRepositoryPage: """ Single page repositories only have one page irrespective of endpoint. """ response = self._get_response("") if not response: - return None + raise PackageNotFound(f"Package [{name}] not found.") return SimpleRepositoryPage(response.url, response.text) diff --git a/src/poetry/toml/__init__.py b/src/poetry/toml/__init__.py new file mode 100644 index 00000000000..32aee9a2f04 --- /dev/null +++ b/src/poetry/toml/__init__.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from poetry.toml.exceptions import TOMLError +from poetry.toml.file import TOMLFile + + +__all__ = ["TOMLError", "TOMLFile"] diff --git a/src/poetry/toml/exceptions.py b/src/poetry/toml/exceptions.py new file mode 100644 index 00000000000..66fcec0063b --- /dev/null +++ b/src/poetry/toml/exceptions.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from poetry.core.exceptions import PoetryCoreException +from tomlkit.exceptions import TOMLKitError + + +class TOMLError(TOMLKitError, PoetryCoreException): + pass diff --git a/src/poetry/toml/file.py b/src/poetry/toml/file.py new file mode 100644 index 00000000000..79c2e018419 --- /dev/null +++ b/src/poetry/toml/file.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from typing import Any + +from tomlkit.toml_file import TOMLFile as BaseTOMLFile + + +if TYPE_CHECKING: + from pathlib import Path + + from tomlkit.toml_document import TOMLDocument + + +class TOMLFile(BaseTOMLFile): + def __init__(self, path: Path) -> None: + super().__init__(path) + self.__path = path + + @property + def path(self) -> Path: + return self.__path + + def exists(self) -> bool: + return self.__path.exists() + + def read(self) -> TOMLDocument: + from tomlkit.exceptions import TOMLKitError + + from poetry.toml import TOMLError + + try: + return super().read() + except (ValueError, TOMLKitError) as e: + raise TOMLError(f"Invalid TOML file {self.path.as_posix()}: {e}") + + def __getattr__(self, item: str) -> Any: + return getattr(self.__path, item) + + def __str__(self) -> str: + return self.__path.as_posix() diff --git a/src/poetry/utils/_compat.py b/src/poetry/utils/_compat.py index 9ee8875f061..cf316b58c26 100644 --- a/src/poetry/utils/_compat.py +++ b/src/poetry/utils/_compat.py @@ -7,6 +7,14 @@ # TODO: use try/except ImportError when # https://github.com/python/mypy/issues/1393 is fixed + +if sys.version_info < (3, 11): + # compatibility for python <3.11 + import tomli as tomllib +else: + import tomllib # nopycln: import + + if sys.version_info < (3, 10): # compatibility for python <3.10 import importlib_metadata as metadata @@ -48,23 +56,11 @@ def encode(string: str, encodings: list[str] | None = None) -> bytes: return string.encode(encodings[0], errors="ignore") -def to_str(string: str) -> str: - return decode(string) - - -def list_to_shell_command(cmd: list[str]) -> str: - return " ".join( - f'"{token}"' if " " in token and token[0] not in {"'", '"'} else token - for token in cmd - ) - - __all__ = [ "WINDOWS", "cached_property", "decode", "encode", - "list_to_shell_command", "metadata", - "to_str", + "tomllib", ] diff --git a/src/poetry/utils/authenticator.py b/src/poetry/utils/authenticator.py index 4da767919a1..0fb238fb56c 100644 --- a/src/poetry/utils/authenticator.py +++ b/src/poetry/utils/authenticator.py @@ -17,13 +17,15 @@ import requests.auth import requests.exceptions -from cachecontrol import CacheControl +from cachecontrol import CacheControlAdapter from cachecontrol.caches import FileCache from filelock import FileLock from poetry.config.config import Config from poetry.exceptions import PoetryException from poetry.utils.constants import REQUESTS_TIMEOUT +from poetry.utils.constants import RETRY_AFTER_HEADER +from poetry.utils.constants import STATUS_FORCELIST from poetry.utils.password_manager import HTTPAuthCredential from poetry.utils.password_manager import PasswordManager @@ -128,6 +130,7 @@ def __init__( io: IO | None = None, cache_id: str | None = None, disable_cache: bool = False, + pool_size: int = 10, ) -> None: self._config = config or Config.create() self._io = io @@ -153,6 +156,7 @@ def __init__( self.get_repository_config_for_url = functools.lru_cache(maxsize=None)( self._get_repository_config_for_url ) + self._pool_size = pool_size def create_session(self) -> requests.Session: session = requests.Session() @@ -160,7 +164,13 @@ def create_session(self) -> requests.Session: if self._cache_control is None: return session - session = CacheControl(sess=session, cache=self._cache_control) + adapter = CacheControlAdapter( + cache=self._cache_control, + pool_maxsize=self._pool_size, + ) + session.mount("http://", adapter) + session.mount("https://", adapter) + return session def get_session(self, url: str | None = None) -> requests.Session: @@ -242,6 +252,7 @@ def request( send_kwargs.update(settings) attempt = 0 + resp = None while True: is_last_attempt = attempt >= 5 @@ -251,14 +262,14 @@ def request( if is_last_attempt: raise e else: - if resp.status_code not in [502, 503, 504] or is_last_attempt: + if resp.status_code not in STATUS_FORCELIST or is_last_attempt: if raise_for_status: resp.raise_for_status() return resp if not is_last_attempt: attempt += 1 - delay = 0.5 * attempt + delay = self._get_backoff(resp, attempt) logger.debug("Retrying HTTP request in %s seconds.", delay) time.sleep(delay) continue @@ -266,6 +277,14 @@ def request( # this should never really be hit under any sane circumstance raise PoetryException("Failed HTTP {} request", method.upper()) + def _get_backoff(self, response: requests.Response | None, attempt: int) -> float: + if response is not None: + retry_after = response.headers.get(RETRY_AFTER_HEADER, "") + if retry_after: + return float(retry_after) + + return 0.5 * attempt + def get(self, url: str, **kwargs: Any) -> requests.Response: return self.request("get", url, **kwargs) diff --git a/src/poetry/utils/cache.py b/src/poetry/utils/cache.py index ba88a077055..3d449c356f6 100644 --- a/src/poetry/utils/cache.py +++ b/src/poetry/utils/cache.py @@ -1,58 +1,37 @@ from __future__ import annotations -import contextlib import dataclasses import hashlib import json +import logging import shutil import time from pathlib import Path +from typing import TYPE_CHECKING from typing import Any -from typing import Callable from typing import Generic from typing import TypeVar +from poetry.utils._compat import decode +from poetry.utils._compat import encode +from poetry.utils.wheel import InvalidWheelName +from poetry.utils.wheel import Wheel -# Used by Cachy for items that do not expire. -MAX_DATE = 9999999999 -T = TypeVar("T") - - -def decode(string: bytes, encodings: list[str] | None = None) -> str: - """ - Compatiblity decode function pulled from cachy. - :param string: The byte string to decode. - :param encodings: List of encodings to apply - :return: Decoded string - """ - if encodings is None: - encodings = ["utf-8", "latin1", "ascii"] - - for encoding in encodings: - with contextlib.suppress(UnicodeDecodeError): - return string.decode(encoding) - - return string.decode(encodings[0], errors="ignore") +if TYPE_CHECKING: + from collections.abc import Callable + from poetry.core.packages.utils.link import Link -def encode(string: str, encodings: list[str] | None = None) -> bytes: - """ - Compatibility encode function from cachy. + from poetry.utils.env import Env - :param string: The string to encode. - :param encodings: List of encodings to apply - :return: Encoded byte string - """ - if encodings is None: - encodings = ["utf-8", "latin1", "ascii"] - for encoding in encodings: - with contextlib.suppress(UnicodeDecodeError): - return string.encode(encoding) +# Used by Cachy for items that do not expire. +MAX_DATE = 9999999999 +T = TypeVar("T") - return string.encode(encodings[0], errors="ignore") +logger = logging.getLogger(__name__) def _expiration(minutes: int) -> int: @@ -130,7 +109,7 @@ def put(self, key: str, value: Any, minutes: int | None = None) -> None: ) path = self._path(key) path.parent.mkdir(parents=True, exist_ok=True) - with open(path, "wb") as f: + with path.open("wb") as f: f.write(self._serialize(payload)) def forget(self, key: str) -> None: @@ -171,8 +150,15 @@ def _get_payload(self, key: str) -> T | None: if not path.exists(): return None - with open(path, "rb") as f: - payload = self._deserialize(f.read()) + with path.open("rb") as f: + file_content = f.read() + + try: + payload = self._deserialize(file_content) + except (json.JSONDecodeError, ValueError): + self.forget(key) + logger.warning("Corrupt cache file was detected and cleaned up.") + return None if payload.expired: self.forget(key) @@ -196,3 +182,115 @@ def _deserialize(self, data_raw: bytes) -> CacheItem[T]: data = json.loads(data_str[10:]) expires = int(data_str[:10]) return CacheItem(data, expires) + + +class ArtifactCache: + def __init__(self, *, cache_dir: Path) -> None: + self._cache_dir = cache_dir + + 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: + key_parts[link.hash_name] = link.hash + + if link.subdirectory_fragment: + key_parts["subdirectory"] = link.subdirectory_fragment + + return self._get_directory_from_hash(key_parts) + + def _get_directory_from_hash(self, key_parts: object) -> Path: + key = hashlib.sha256( + json.dumps( + key_parts, sort_keys=True, separators=(",", ":"), ensure_ascii=True + ).encode("ascii") + ).hexdigest() + + split_key = [key[:2], key[2:4], key[4:6], key[6:]] + return self._cache_dir.joinpath(*split_key) + + def get_cache_directory_for_git( + self, url: str, ref: str, subdirectory: str | None + ) -> Path: + key_parts = {"url": url, "ref": ref} + if subdirectory: + key_parts["subdirectory"] = subdirectory + + return self._get_directory_from_hash(key_parts) + + def get_cached_archive_for_link( + self, + link: Link, + *, + strict: bool, + env: Env | None = None, + ) -> Path | None: + cache_dir = self.get_cache_directory_for_link(link) + + return self._get_cached_archive( + cache_dir, strict=strict, filename=link.filename, env=env + ) + + def get_cached_archive_for_git( + self, url: str, reference: str, subdirectory: str | None, env: Env + ) -> Path | None: + cache_dir = self.get_cache_directory_for_git(url, reference, subdirectory) + + return self._get_cached_archive(cache_dir, strict=False, env=env) + + def _get_cached_archive( + self, + cache_dir: Path, + *, + strict: bool, + filename: str | None = None, + env: Env | None = None, + ) -> Path | None: + assert strict or env is not None + # implication "strict -> filename should not be None" + assert not strict or filename is not None + + archives = self._get_cached_archives(cache_dir) + if not archives: + return None + + candidates: list[tuple[float | None, Path]] = [] + + for archive in archives: + if strict: + # in strict mode return the original cached archive instead of the + # prioritized archive type. + if filename == archive.name: + return archive + continue + + assert env is not None + + if archive.suffix != ".whl": + candidates.append((float("inf"), archive)) + continue + + try: + wheel = Wheel(archive.name) + except InvalidWheelName: + continue + + if not wheel.is_supported_by_environment(env): + continue + + candidates.append( + (wheel.get_minimum_supported_index(env.supported_tags), archive), + ) + + if not candidates: + return None + + return min(candidates)[1] + + def _get_cached_archives(self, cache_dir: Path) -> list[Path]: + archive_types = ["whl", "tar.gz", "tar.bz2", "bz2", "zip"] + paths: list[Path] = [] + for archive_type in archive_types: + paths += cache_dir.glob(f"*.{archive_type}") + + return paths diff --git a/src/poetry/utils/constants.py b/src/poetry/utils/constants.py index 0f799b16d7d..56bec540ae2 100644 --- a/src/poetry/utils/constants.py +++ b/src/poetry/utils/constants.py @@ -3,3 +3,8 @@ # Timeout for HTTP requests using the requests library. REQUESTS_TIMEOUT = 15 + +RETRY_AFTER_HEADER = "retry-after" + +# Server response codes to retry requests on. +STATUS_FORCELIST = [429, 500, 501, 502, 503, 504] diff --git a/src/poetry/utils/dependency_specification.py b/src/poetry/utils/dependency_specification.py index e80a2b97019..adcfd3d55ab 100644 --- a/src/poetry/utils/dependency_specification.py +++ b/src/poetry/utils/dependency_specification.py @@ -114,7 +114,7 @@ def _parse_dependency_specification_simple( require: DependencySpec = {} if " " in pair: - name, version = pair.split(" ", 2) + name, version = pair.split(" ", 1) extras_m = re.search(r"\[([\w\d,-_]+)\]$", name) if extras_m: extras = [e.strip() for e in extras_m.group(1).split(",")] diff --git a/src/poetry/utils/env.py b/src/poetry/utils/env.py index d76972ca50d..47d5ac62a3f 100644 --- a/src/poetry/utils/env.py +++ b/src/poetry/utils/env.py @@ -9,6 +9,7 @@ import platform import plistlib import re +import shutil import subprocess import sys import sysconfig @@ -25,6 +26,7 @@ import tomlkit import virtualenv +from cleo.io.null_io import NullIO from cleo.io.outputs.output import Verbosity from packaging.tags import Tag from packaging.tags import interpreter_name @@ -32,14 +34,13 @@ from packaging.tags import sys_tags from poetry.core.constraints.version import Version from poetry.core.constraints.version import parse_constraint -from poetry.core.toml.file import TOMLFile from poetry.core.utils.helpers import temporary_directory from virtualenv.seed.wheels.embed import get_embed_wheel +from poetry.toml.file import TOMLFile from poetry.utils._compat import WINDOWS 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 metadata from poetry.utils.helpers import get_real_windows_path from poetry.utils.helpers import is_dir_writable @@ -58,7 +59,6 @@ from poetry.poetry import Poetry - GET_SYS_TAGS = f""" import importlib.util import json @@ -83,7 +83,6 @@ ) """ - GET_ENVIRONMENT_INFO = """\ import json import os @@ -154,7 +153,6 @@ def _version_nodot(version): print(json.dumps(env)) """ - GET_BASE_PREFIX = """\ import sys @@ -173,9 +171,9 @@ def _version_nodot(version): """ GET_PYTHON_VERSION_ONELINER = ( - "\"import sys; print('.'.join([str(s) for s in sys.version_info[:3]]))\"" + "import sys; print('.'.join([str(s) for s in sys.version_info[:3]]))" ) -GET_ENV_PATH_ONELINER = '"import sys; print(sys.prefix)"' +GET_ENV_PATH_ONELINER = "import sys; print(sys.prefix)" GET_SYS_PATH = """\ import json @@ -328,8 +326,8 @@ def find_distribution_files_with_suffix( for distribution in self.distributions( name=distribution_name, writable_only=writable_only ): - assert distribution.files is not None - for file in distribution.files: + files = [] if distribution.files is None else distribution.files + for file in files: if file.name.endswith(suffix): yield Path( distribution.locate_file(file), # type: ignore[no-untyped-call] @@ -341,8 +339,8 @@ def find_distribution_files_with_name( for distribution in self.distributions( name=distribution_name, writable_only=writable_only ): - assert distribution.files is not None - for file in distribution.files: + files = [] if distribution.files is None else distribution.files + for file in files: if file.name == name: yield Path( distribution.locate_file(file), # type: ignore[no-untyped-call] @@ -372,8 +370,8 @@ def remove_distribution_files(self, distribution_name: str) -> list[Path]: for distribution in self.distributions( name=distribution_name, writable_only=True ): - assert distribution.files is not None - for file in distribution.files: + files = [] if distribution.files is None else distribution.files + for file in files: path = Path( distribution.locate_file(file), # type: ignore[no-untyped-call] ) @@ -383,7 +381,7 @@ def remove_distribution_files(self, distribution_name: str) -> list[Path]: distribution_path: Path = distribution._path # type: ignore[attr-defined] if distribution_path.exists(): - remove_directory(str(distribution_path), force=True) + remove_directory(distribution_path, force=True) paths.append(distribution_path) @@ -391,16 +389,13 @@ def remove_distribution_files(self, distribution_name: str) -> list[Path]: def _path_method_wrapper( self, - path: str | Path, + path: Path, method: str, *args: Any, return_first: bool = True, writable_only: bool = False, **kwargs: Any, ) -> 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 ) @@ -422,17 +417,17 @@ def _path_method_wrapper( raise OSError(f"Unable to access any of {paths_csv(candidates)}") - def write_text(self, path: str | Path, *args: Any, **kwargs: Any) -> Path: + def write_text(self, path: Path, *args: Any, **kwargs: Any) -> Path: paths = self._path_method_wrapper(path, "write_text", *args, **kwargs) assert isinstance(paths, tuple) return paths[0] - def mkdir(self, path: str | Path, *args: Any, **kwargs: Any) -> Path: + def mkdir(self, path: Path, *args: Any, **kwargs: Any) -> Path: paths = self._path_method_wrapper(path, "mkdir", *args, **kwargs) assert isinstance(paths, tuple) return paths[0] - def exists(self, path: str | Path) -> bool: + def exists(self, path: Path) -> bool: return any( value[-1] for value in self._path_method_wrapper(path, "exists", return_first=False) @@ -440,7 +435,7 @@ def exists(self, path: str | Path) -> bool: def find( self, - path: str | Path, + path: Path, writable_only: bool = False, ) -> list[Path]: return [ @@ -451,12 +446,6 @@ def find( 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 @@ -472,13 +461,21 @@ class EnvCommandError(EnvError): def __init__(self, e: CalledProcessError, input: str | None = None) -> None: self.e = e - message = ( - f"Command {e.cmd} errored with the following return code {e.returncode}," - f" and output: \n{decode(e.output)}" - ) + message_parts = [ + f"Command {e.cmd} errored with the following return code {e.returncode}" + ] + if e.output: + message_parts.append(f"Output:\n{decode(e.output)}") + if e.stderr: + message_parts.append(f"Error output:\n{decode(e.stderr)}") if input: - message += f"input was : {input}" - super().__init__(message) + message_parts.append(f"Input:\n{input}") + super().__init__("\n\n".join(message_parts)) + + +class PythonVersionNotFound(EnvError): + def __init__(self, expected: str) -> None: + super().__init__(f"Could not find the python executable {expected}") class NoCompatiblePythonVersionFound(EnvError): @@ -521,44 +518,77 @@ class EnvManager: ENVS_FILE = "envs.toml" - def __init__(self, poetry: Poetry) -> None: + def __init__(self, poetry: Poetry, io: None | IO = None) -> None: self._poetry = poetry + self._io = io or NullIO() + + @staticmethod + def _full_python_path(python: str) -> Path | None: + # eg first find pythonXY.bat on windows. + path_python = shutil.which(python) + if path_python is None: + return None - def _full_python_path(self, python: str) -> str: try: executable = decode( subprocess.check_output( - list_to_shell_command( - [python, "-c", '"import sys; print(sys.executable)"'] - ), - shell=True, + [path_python, "-c", "import sys; print(sys.executable)"], ).strip() ) - except CalledProcessError as e: - raise EnvCommandError(e) + return Path(executable) - return executable + except CalledProcessError: + return None - def _detect_active_python(self, io: IO) -> str | None: - executable = None + @staticmethod + def _detect_active_python(io: None | IO = None) -> Path | None: + io = io or NullIO() + io.write_error_line( + ( + "Trying to detect current active python executable as specified in" + " the config." + ), + verbosity=Verbosity.VERBOSE, + ) - try: - io.write_error_line( - "Trying to detect current active python executable as specified in the" - " config.", - verbosity=Verbosity.VERBOSE, - ) - executable = self._full_python_path("python") + executable = EnvManager._full_python_path("python") + + if executable is not None: io.write_error_line(f"Found: {executable}", verbosity=Verbosity.VERBOSE) - except CalledProcessError: + else: io.write_error_line( - "Unable to detect the current active python executable. Falling back to" - " default.", + ( + "Unable to detect the current active python executable. Falling" + " back to default." + ), verbosity=Verbosity.VERBOSE, ) + return executable - def activate(self, python: str, io: IO) -> Env: + @staticmethod + def get_python_version( + precision: int = 3, + prefer_active_python: bool = False, + io: None | IO = None, + ) -> Version: + version = ".".join(str(v) for v in sys.version_info[:precision]) + + if prefer_active_python: + executable = EnvManager._detect_active_python(io) + + if executable: + python_patch = decode( + subprocess.check_output( + [executable, "-c", GET_PYTHON_VERSION_ONELINER], + ).strip() + ) + + version = ".".join(str(v) for v in python_patch.split(".")[:precision]) + + return Version.parse(version) + + def activate(self, python: str) -> Env: venv_path = self._poetry.config.virtualenvs_path cwd = self._poetry.file.parent @@ -573,13 +603,14 @@ def activate(self, python: str, io: IO) -> Env: # Executable in PATH or full executable path pass - python = self._full_python_path(python) + python_path = self._full_python_path(python) + if python_path is None: + raise PythonVersionNotFound(python) try: python_version_string = decode( subprocess.check_output( - list_to_shell_command([python, "-c", GET_PYTHON_VERSION_ONELINER]), - shell=True, + [python_path, "-c", GET_PYTHON_VERSION_ONELINER], ) ) except CalledProcessError as e: @@ -604,7 +635,7 @@ def activate(self, python: str, io: IO) -> Env: if patch != current_patch: create = True - self.create_venv(io, executable=python, force=create) + self.create_venv(executable=python_path, force=create) return self.get(reload=True) @@ -638,7 +669,7 @@ def activate(self, python: str, io: IO) -> Env: if patch != current_patch: create = True - self.create_venv(io, executable=python, force=create) + self.create_venv(executable=python_path, force=create) # Activate envs[base_env_name] = {"minor": minor, "patch": patch} @@ -646,7 +677,7 @@ def activate(self, python: str, io: IO) -> Env: return self.get(reload=True) - def deactivate(self, io: IO) -> None: + def deactivate(self) -> None: venv_path = self._poetry.config.virtualenvs_path name = self.generate_env_name( self._poetry.package.name, str(self._poetry.file.parent) @@ -658,7 +689,7 @@ def deactivate(self, io: IO) -> None: env = envs.get(name) if env is not None: venv = venv_path / f"{name}-py{env['minor']}" - io.write_error_line( + self._io.write_error_line( f"Deactivating virtualenv: {venv}" ) del envs[name] @@ -669,7 +700,12 @@ def get(self, reload: bool = False) -> Env: if self._env is not None and not reload: return self._env - python_minor = ".".join([str(v) for v in sys.version_info[:2]]) + prefer_active_python = self._poetry.config.get( + "virtualenvs.prefer-active-python" + ) + python_minor = self.get_python_version( + precision=2, prefer_active_python=prefer_active_python, io=self._io + ).to_string() venv_path = self._poetry.config.virtualenvs_path @@ -734,13 +770,11 @@ def list(self, name: str | None = None) -> list[VirtualEnv]: venv_name = self.generate_env_name(name, str(self._poetry.file.parent)) venv_path = self._poetry.config.virtualenvs_path - env_list = [ - VirtualEnv(Path(p)) for p in sorted(venv_path.glob(f"{venv_name}-py*")) - ] + env_list = [VirtualEnv(p) for p in sorted(venv_path.glob(f"{venv_name}-py*"))] venv = self._poetry.file.parent / ".venv" if ( - self._poetry.config.get("virtualenvs.in-project") + self._poetry.config.get("virtualenvs.in-project") is not False and venv.exists() and venv.is_dir() ): @@ -769,8 +803,7 @@ def remove(self, python: str) -> Env: try: env_dir = decode( subprocess.check_output( - list_to_shell_command([python, "-c", GET_ENV_PATH_ONELINER]), - shell=True, + [python, "-c", GET_ENV_PATH_ONELINER], ) ).strip("\n") env_name = Path(env_dir).name @@ -813,7 +846,7 @@ def remove(self, python: str) -> Env: else: venv_path = self._poetry.config.virtualenvs_path # Get all the poetry envs, even for other projects - env_names = [Path(p).name for p in sorted(venv_path.glob("*-*-py*"))] + env_names = [p.name for p in sorted(venv_path.glob("*-*-py*"))] if python in env_names: raise IncorrectEnvError(python) @@ -829,8 +862,7 @@ def remove(self, python: str) -> Env: try: python_version_string = decode( subprocess.check_output( - list_to_shell_command([python, "-c", GET_PYTHON_VERSION_ONELINER]), - shell=True, + [python, "-c", GET_PYTHON_VERSION_ONELINER], ) ) except CalledProcessError as e: @@ -861,9 +893,8 @@ def remove(self, python: str) -> Env: def create_venv( self, - io: IO, name: str | None = None, - executable: str | None = None, + executable: Path | None = None, force: bool = False, ) -> Env: if self._env is not None and not force: @@ -894,9 +925,11 @@ def create_venv( venv_prompt = self._poetry.config.get("virtualenvs.prompt") if not executable and prefer_active_python: - executable = self._detect_active_python(io) + executable = self._detect_active_python() - venv_path = cwd / ".venv" if root_venv else self._poetry.config.virtualenvs_path + venv_path: Path = ( + cwd / ".venv" if root_venv else self._poetry.config.virtualenvs_path + ) if not name: name = self._poetry.package.name assert name is not None @@ -906,10 +939,7 @@ def create_venv( if executable: python_patch = decode( subprocess.check_output( - list_to_shell_command( - [executable, "-c", GET_PYTHON_VERSION_ONELINER] - ), - shell=True, + [executable, "-c", GET_PYTHON_VERSION_ONELINER], ).strip() ) python_minor = ".".join(python_patch.split(".")[:2]) @@ -927,50 +957,47 @@ def create_venv( self._poetry.package.python_versions, python_patch ) - io.write_error_line( + self._io.write_error_line( f"The currently activated Python version {python_patch} is not" f" supported by the project ({self._poetry.package.python_versions}).\n" "Trying to find and use a compatible version. " ) - for python_to_try in sorted( + for suffix in sorted( self._poetry.package.AVAILABLE_PYTHONS, key=lambda v: (v.startswith("3"), -len(v), v), reverse=True, ): - if len(python_to_try) == 1: - if not parse_constraint(f"^{python_to_try}.0").allows_any( + if len(suffix) == 1: + if not parse_constraint(f"^{suffix}.0").allows_any( supported_python ): continue - elif not supported_python.allows_any( - parse_constraint(python_to_try + ".*") - ): + elif not supported_python.allows_any(parse_constraint(suffix + ".*")): continue - python = "python" + python_to_try + python_name = f"python{suffix}" + if self._io.is_debug(): + self._io.write_error_line(f"Trying {python_name}") - if io.is_debug(): - io.write_error_line(f"Trying {python}") + python = self._full_python_path(python_name) + if python is None: + continue try: python_patch = decode( subprocess.check_output( - list_to_shell_command( - [python, "-c", GET_PYTHON_VERSION_ONELINER] - ), + [python, "-c", GET_PYTHON_VERSION_ONELINER], stderr=subprocess.STDOUT, - shell=True, ).strip() ) except CalledProcessError: continue - if not python_patch: - continue - if supported_python.allows(Version.parse(python_patch)): - io.write_error_line(f"Using {python} ({python_patch})") + self._io.write_error_line( + f"Using {python_name} ({python_patch})" + ) executable = python python_minor = ".".join(python_patch.split(".")[:2]) break @@ -995,7 +1022,7 @@ def create_venv( if not venv.exists(): if create_venv is False: - io.write_error_line( + self._io.write_error_line( "" "Skipping virtualenv creation, " "as specified in config file." @@ -1004,7 +1031,7 @@ def create_venv( return self.get_system_env() - io.write_error_line( + self._io.write_error_line( f"Creating virtualenv {name} in" f" {venv_path if not WINDOWS else get_real_windows_path(venv_path)!s}" ) @@ -1012,15 +1039,17 @@ def create_venv( create_venv = False if force: if not env.is_sane(): - io.write_error_line( + self._io.write_error_line( f"The virtual environment found in {env.path} seems to" " be broken." ) - io.write_error_line(f"Recreating virtualenv {name} in {venv!s}") + self._io.write_error_line( + f"Recreating virtualenv {name} in {venv!s}" + ) self.remove_venv(venv) create_venv = True - elif io.is_very_verbose(): - io.write_error_line(f"Virtualenv {name} already exists.") + elif self._io.is_very_verbose(): + self._io.write_error_line(f"Virtualenv {name} already exists.") if create_venv: self.build_venv( @@ -1051,8 +1080,8 @@ def create_venv( @classmethod def build_venv( cls, - path: Path | str, - executable: str | Path | None = None, + path: Path, + executable: Path | None = None, flags: dict[str, bool] | None = None, with_pip: bool | None = None, with_wheel: bool | None = None, @@ -1083,14 +1112,13 @@ def build_venv( else flags.pop("no-wheel", flags["no-pip"]) ) - if isinstance(executable, Path): - executable = executable.resolve().as_posix() + executable_str = None if executable is None else executable.resolve().as_posix() args = [ "--no-download", "--no-periodic-update", "--python", - executable or sys.executable, + executable_str or sys.executable, ] if prompt is not None: @@ -1118,9 +1146,7 @@ def build_venv( return cli_result @classmethod - def remove_venv(cls, path: Path | str) -> None: - if isinstance(path, str): - path = Path(path) + def remove_venv(cls, path: Path) -> None: assert path.is_dir() try: remove_directory(path) @@ -1208,10 +1234,7 @@ def __init__(self, path: Path, base: Path | None = None) -> None: path = get_real_windows_path(path) base = get_real_windows_path(base) if base else None - if not self._is_windows or self._is_mingw: - bin_dir = "bin" - else: - bin_dir = "Scripts" + bin_dir = "bin" if not self._is_windows or self._is_mingw else "Scripts" self._path = path self._bin_dir = self._path / bin_dir @@ -1231,7 +1254,7 @@ def __init__(self, path: Path, base: Path | None = None) -> None: self._platlib: Path | None = None self._script_dirs: list[Path] | None = None - self._embedded_pip_path: str | None = None + self._embedded_pip_path: Path | None = None @property def path(self) -> Path: @@ -1242,8 +1265,9 @@ def base(self) -> Path: return self._base @property - def version_info(self) -> tuple[Any, ...]: - return tuple(self.marker_env["version_info"]) + def version_info(self) -> tuple[int, int, int, str, int]: + version_info: tuple[int, int, int, str, int] = self.marker_env["version_info"] + return version_info @property def python_implementation(self) -> str: @@ -1251,7 +1275,7 @@ def python_implementation(self) -> str: return implementation @property - def python(self) -> str: + def python(self) -> Path: """ Path to current python executable """ @@ -1311,21 +1335,21 @@ def get_embedded_wheel(self, distribution: str) -> Path: return path @property - def pip_embedded(self) -> str: + def pip_embedded(self) -> Path: if self._embedded_pip_path is None: - self._embedded_pip_path = str(self.get_embedded_wheel("pip") / "pip") + self._embedded_pip_path = self.get_embedded_wheel("pip") / "pip" return self._embedded_pip_path @property - def pip(self) -> str: + def pip(self) -> Path: """ Path to current pip executable """ # we do not use as_posix() here due to issues with windows pathlib2 # implementation path = self._bin(self._pip_executable) - if not Path(path).exists(): - return str(self.pip_embedded) + if not path.exists(): + return self.pip_embedded return path @property @@ -1402,6 +1426,16 @@ def paths(self) -> dict[str, str]: if self._paths is None: self._paths = self.get_paths() + if self.is_venv(): + # We copy pip's logic here for the `include` path + self._paths["include"] = str( + self.path.joinpath( + "include", + "site", + f"python{self.version_info[0]}.{self.version_info[1]}", + ) + ) + return self._paths @property @@ -1433,10 +1467,10 @@ def get_marker_env(self) -> dict[str, Any]: raise NotImplementedError() def get_pip_command(self, embedded: bool = False) -> list[str]: - if embedded or not Path(self._bin(self._pip_executable)).exists(): - return [self.python, self.pip_embedded] + if embedded or not self._bin(self._pip_executable).exists(): + return [str(self.python), str(self.pip_embedded)] # run as module so that pip can update itself on Windows - return [self.python, "-m", "pip"] + return [str(self.python), "-m", "pip"] def get_supported_tags(self) -> list[Tag]: raise NotImplementedError() @@ -1463,7 +1497,7 @@ def get_command_from_bin(self, bin: str) -> list[str]: # embedded pip when pip is not available in the environment return self.get_pip_command() - return [self._bin(bin)] + return [str(self._bin(bin))] def run(self, bin: str, *args: str, **kwargs: Any) -> str | int: cmd = self.get_command_from_bin(bin) + list(args) @@ -1476,7 +1510,14 @@ def run_pip(self, *args: str, **kwargs: Any) -> int | str: def run_python_script(self, content: str, **kwargs: Any) -> int | str: return self.run( - self._executable, "-I", "-W", "ignore", "-", input_=content, **kwargs + self._executable, + "-I", + "-W", + "ignore", + "-", + input_=content, + stderr=subprocess.PIPE, + **kwargs, ) def _run(self, cmd: list[str], **kwargs: Any) -> int | str: @@ -1486,34 +1527,24 @@ def _run(self, cmd: list[str], **kwargs: Any) -> int | str: call = kwargs.pop("call", False) input_ = kwargs.pop("input_", None) env = kwargs.pop("env", dict(os.environ)) + stderr = kwargs.pop("stderr", subprocess.STDOUT) try: - if self._is_windows: - kwargs["shell"] = True - - command: str | list[str] - if kwargs.get("shell", False): - command = list_to_shell_command(cmd) - else: - command = cmd - if input_: output = subprocess.run( - command, + cmd, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, + stderr=stderr, input=encode(input_), check=True, + env=env, **kwargs, ).stdout elif call: - return subprocess.call( - command, stderr=subprocess.STDOUT, env=env, **kwargs - ) + assert stderr != subprocess.PIPE + return subprocess.call(cmd, stderr=stderr, env=env, **kwargs) else: - output = subprocess.check_output( - command, stderr=subprocess.STDOUT, env=env, **kwargs - ) + output = subprocess.check_output(cmd, stderr=stderr, env=env, **kwargs) except CalledProcessError as e: raise EnvCommandError(e, input=input_) @@ -1527,7 +1558,7 @@ def execute(self, bin: str, *args: str, **kwargs: Any) -> int: return os.execvpe(command[0], command, env=env) kwargs["shell"] = True - exe = subprocess.Popen([command[0]] + command[1:], env=env, **kwargs) + exe = subprocess.Popen(command, env=env, **kwargs) exe.communicate() return exe.returncode @@ -1545,7 +1576,7 @@ def script_dirs(self) -> list[Path]: self._script_dirs.append(self.userbase / self._script_dirs[0].name) return self._script_dirs - def _bin(self, bin: str) -> str: + def _bin(self, bin: str) -> Path: """ Return path to the given executable. """ @@ -1566,11 +1597,11 @@ def _bin(self, bin: str) -> str: bin_path = self._path / bin if bin_path.exists(): - return str(bin_path) + return bin_path - return bin + return Path(bin) - return str(bin_path) + return bin_path def __eq__(self, other: object) -> bool: if not isinstance(other, Env): @@ -1588,8 +1619,8 @@ class SystemEnv(Env): """ @property - def python(self) -> str: - return sys.executable + def python(self) -> Path: + return Path(sys.executable) @property def sys_path(self) -> list[str]: @@ -1866,7 +1897,7 @@ def execute(self, bin: str, *args: str, **kwargs: Any) -> int: if not self._is_windows: return os.execvpe(command[0], command, env=env) - exe = subprocess.Popen([command[0]] + command[1:], env=env, **kwargs) + exe = subprocess.Popen(command, env=env, **kwargs) exe.communicate() return exe.returncode @@ -1890,6 +1921,17 @@ def __init__( self._execute = execute self.executed: list[list[str]] = [] + @property + def paths(self) -> dict[str, str]: + if self._paths is None: + self._paths = self.get_paths() + self._paths["platlib"] = str(self._path / "platlib") + self._paths["purelib"] = str(self._path / "purelib") + self._paths["scripts"] = str(self._path / "scripts") + self._paths["data"] = str(self._path / "data") + + return self._paths + def _run(self, cmd: list[str], **kwargs: Any) -> int | str: self.executed.append(cmd) @@ -1904,20 +1946,20 @@ def execute(self, bin: str, *args: str, **kwargs: Any) -> int: return super().execute(bin, *args, **kwargs) return 0 - def _bin(self, bin: str) -> str: - return bin + def _bin(self, bin: str) -> Path: + return Path(bin) @contextmanager def ephemeral_environment( - executable: str | Path | None = None, + executable: Path | None = None, flags: dict[str, bool] | None = None, ) -> Iterator[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(), + path=venv_dir, executable=executable, flags=flags, ) @@ -1959,6 +2001,7 @@ def build_environment( "install", "--disable-pip-version-check", "--ignore-installed", + "--no-input", *poetry.pyproject.build_system.requires, ) diff --git a/src/poetry/utils/extras.py b/src/poetry/utils/extras.py index f6b93df82cc..5c2d593196d 100644 --- a/src/poetry/utils/extras.py +++ b/src/poetry/utils/extras.py @@ -6,7 +6,7 @@ if TYPE_CHECKING: from collections.abc import Collection from collections.abc import Iterable - from typing import Mapping + from collections.abc import Mapping from packaging.utils import NormalizedName from poetry.core.packages.package import Package diff --git a/src/poetry/utils/helpers.py b/src/poetry/utils/helpers.py index b50d9e9acfc..b39e8230e10 100644 --- a/src/poetry/utils/helpers.py +++ b/src/poetry/utils/helpers.py @@ -1,24 +1,25 @@ from __future__ import annotations +import hashlib +import io import os import shutil import stat import sys import tempfile +from collections.abc import Mapping from contextlib import contextmanager from pathlib import Path from typing import TYPE_CHECKING from typing import Any -from typing import Iterator -from typing import Mapping -from typing import cast from poetry.utils.constants import REQUESTS_TIMEOUT if TYPE_CHECKING: from collections.abc import Callable + from collections.abc import Iterator from io import BufferedWriter from poetry.core.packages.package import Package @@ -64,7 +65,7 @@ def _on_rm_error(func: Callable[[str], None], path: str, exc_info: Exception) -> def remove_directory( - path: Path | str, *args: Any, force: bool = False, **kwargs: Any + path: Path, *args: Any, force: bool = False, **kwargs: Any ) -> None: """ Helper function handle safe removal, and optionally forces stubborn file removal. @@ -73,15 +74,15 @@ def remove_directory( Internally, all arguments are passed to `shutil.rmtree`. """ - if Path(path).is_symlink(): - return os.unlink(str(path)) + if path.is_symlink(): + return os.unlink(path) kwargs["onerror"] = kwargs.pop("onerror", _on_rm_error if force else None) shutil.rmtree(path, *args, **kwargs) def merge_dicts(d1: dict[str, Any], d2: dict[str, Any]) -> None: - for k in d2.keys(): + for k in d2: if k in d1 and isinstance(d1[k], dict) and isinstance(d2[k], Mapping): merge_dicts(d1[k], d2[k]) else: @@ -120,7 +121,7 @@ def download_file( # but skip the updating set_indicator = total_size > 1024 * 1024 - with open(dest, "wb") as f: + with dest.open("wb") as f: for chunk in response.iter_content(chunk_size=chunk_size): if chunk: f.write(chunk) @@ -189,7 +190,8 @@ def _get_win_folder_from_registry(csidl_name: str) -> str: ) dir, type = _winreg.QueryValueEx(key, shell_folder_name) - return cast(str, dir) + assert isinstance(dir, str) + return dir def _get_win_folder_with_ctypes(csidl_name: str) -> str: @@ -237,7 +239,7 @@ def get_win_folder(csidl_name: str) -> Path: raise RuntimeError("Method can only be called on Windows.") -def get_real_windows_path(path: str | Path) -> Path: +def get_real_windows_path(path: Path) -> Path: program_files = get_win_folder("CSIDL_PROGRAM_FILES") local_appdata = get_win_folder("CSIDL_LOCAL_APPDATA") @@ -252,3 +254,12 @@ def get_real_windows_path(path: str | Path) -> Path: path = path.resolve() return path + + +def get_file_hash(path: Path, hash_name: str = "sha256") -> str: + h = hashlib.new(hash_name) + with path.open("rb") as fp: + for content in iter(lambda: fp.read(io.DEFAULT_BUFFER_SIZE), b""): + h.update(content) + + return h.hexdigest() diff --git a/src/poetry/utils/pip.py b/src/poetry/utils/pip.py index 58b3504fba7..eb38bbf0b3b 100644 --- a/src/poetry/utils/pip.py +++ b/src/poetry/utils/pip.py @@ -55,4 +55,4 @@ def pip_install( try: return environment.run_pip(*args) except EnvCommandError as e: - raise PoetryException(f"Failed to install {path.as_posix()}") from e + raise PoetryException(f"Failed to install {path}") from e diff --git a/src/poetry/utils/setup_reader.py b/src/poetry/utils/setup_reader.py index f0ecdde8220..f9cecc4335a 100644 --- a/src/poetry/utils/setup_reader.py +++ b/src/poetry/utils/setup_reader.py @@ -3,12 +3,16 @@ import ast from configparser import ConfigParser -from pathlib import Path +from typing import TYPE_CHECKING from typing import Any from poetry.core.constraints.version import Version +if TYPE_CHECKING: + from pathlib import Path + + class SetupReader: """ Class that reads a setup.py file without executing it. @@ -25,10 +29,7 @@ class SetupReader: FILES = ["setup.py", "setup.cfg"] @classmethod - def read_from_directory(cls, directory: str | Path) -> dict[str, Any]: - if isinstance(directory, str): - directory = Path(directory) - + def read_from_directory(cls, directory: Path) -> dict[str, Any]: result = cls.DEFAULT.copy() for filename in cls.FILES: filepath = directory / filename @@ -38,16 +39,13 @@ def read_from_directory(cls, directory: str | Path) -> dict[str, Any]: read_file_func = getattr(cls(), "read_" + filename.replace(".", "_")) new_result = read_file_func(filepath) - for key in result.keys(): + for key in result: if new_result[key]: result[key] = new_result[key] return result - def read_setup_py(self, filepath: str | Path) -> dict[str, Any]: - if isinstance(filepath, str): - filepath = Path(filepath) - + def read_setup_py(self, filepath: Path) -> dict[str, Any]: with filepath.open(encoding="utf-8") as f: content = f.read() @@ -71,7 +69,7 @@ def read_setup_py(self, filepath: str | Path) -> dict[str, Any]: return result - def read_setup_cfg(self, filepath: str | Path) -> dict[str, Any]: + def read_setup_cfg(self, filepath: Path) -> dict[str, Any]: parser = ConfigParser() parser.read(str(filepath)) diff --git a/src/poetry/utils/wheel.py b/src/poetry/utils/wheel.py new file mode 100644 index 00000000000..f45c50b3b35 --- /dev/null +++ b/src/poetry/utils/wheel.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import logging + +from typing import TYPE_CHECKING + +from packaging.tags import Tag + +from poetry.utils.patterns import wheel_file_re + + +if TYPE_CHECKING: + from poetry.utils.env import Env + + +logger = logging.getLogger(__name__) + + +class InvalidWheelName(Exception): + pass + + +class Wheel: + def __init__(self, filename: str) -> None: + wheel_info = wheel_file_re.match(filename) + if not wheel_info: + raise InvalidWheelName(f"{filename} is not a valid wheel filename.") + + self.filename = filename + self.name = wheel_info.group("name").replace("_", "-") + self.version = wheel_info.group("ver").replace("_", "-") + self.build_tag = wheel_info.group("build") + self.pyversions = wheel_info.group("pyver").split(".") + self.abis = wheel_info.group("abi").split(".") + self.plats = wheel_info.group("plat").split(".") + + self.tags = { + Tag(x, y, z) for x in self.pyversions for y in self.abis for z in self.plats + } + + def get_minimum_supported_index(self, tags: list[Tag]) -> int | None: + 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: Env) -> bool: + return bool(set(env.supported_tags).intersection(self.tags)) diff --git a/src/poetry/vcs/git/backend.py b/src/poetry/vcs/git/backend.py index 7b37d259690..3d58850f468 100644 --- a/src/poetry/vcs/git/backend.py +++ b/src/poetry/vcs/git/backend.py @@ -7,6 +7,7 @@ from pathlib import Path from subprocess import CalledProcessError from typing import TYPE_CHECKING +from urllib.parse import urljoin from dulwich import porcelain from dulwich.client import HTTPUnauthorized @@ -17,7 +18,7 @@ from dulwich.refs import ANNOTATED_TAG_SUFFIX from dulwich.repo import Repo -from poetry.console.exceptions import PoetrySimpleConsoleException +from poetry.console.exceptions import PoetryConsoleError from poetry.utils.authenticator import get_default_authenticator from poetry.utils.helpers import remove_directory @@ -136,11 +137,11 @@ def is_sha_short(self) -> bool: @dataclasses.dataclass class GitRepoLocalInfo: - repo: dataclasses.InitVar[Repo | Path | str] + repo: dataclasses.InitVar[Repo | Path] origin: str = dataclasses.field(init=False) revision: str = dataclasses.field(init=False) - def __post_init__(self, repo: Repo | Path | str) -> None: + def __post_init__(self, repo: Repo | Path) -> None: repo = Git.as_repo(repo=repo) if not isinstance(repo, Repo) else repo self.origin = Git.get_remote_url(repo=repo, remote="origin") self.revision = Git.get_revision(repo=repo) @@ -148,7 +149,7 @@ def __post_init__(self, repo: Repo | Path | str) -> None: class Git: @staticmethod - def as_repo(repo: Path | str) -> Repo: + def as_repo(repo: Path) -> Repo: return Repo(str(repo)) @staticmethod @@ -170,7 +171,7 @@ def get_revision(repo: Repo) -> str: return repo.head().decode("utf-8") @classmethod - def info(cls, repo: Repo | Path | str) -> GitRepoLocalInfo: + def info(cls, repo: Repo | Path) -> GitRepoLocalInfo: return GitRepoLocalInfo(repo=repo) @staticmethod @@ -223,7 +224,7 @@ def _clone_legacy(url: str, refspec: GitRefSpec, target: Path) -> Repo: try: SystemGit.clone(url, target) except CalledProcessError: - raise PoetrySimpleConsoleException( + raise PoetryConsoleError( f"Failed to clone {url}, check your git configuration and permissions" " for this repository." ) @@ -235,9 +236,7 @@ def _clone_legacy(url: str, refspec: GitRefSpec, target: Path) -> Repo: try: SystemGit.checkout(revision, target) except CalledProcessError: - raise PoetrySimpleConsoleException( - f"Failed to checkout {url} at '{revision}'" - ) + raise PoetryConsoleError(f"Failed to checkout {url} at '{revision}'") repo = Repo(str(target)) return repo @@ -264,7 +263,7 @@ def _clone(cls, url: str, refspec: GitRefSpec, target: Path) -> Repo: try: refspec.resolve(remote_refs=remote_refs) except KeyError: # branch / ref does not exist - raise PoetrySimpleConsoleException( + raise PoetryConsoleError( f"Failed to clone {url} at '{refspec.key}', verify ref exists on" " remote." ) @@ -297,23 +296,28 @@ def _clone(cls, url: str, refspec: GitRefSpec, target: Path) -> Repo: if isinstance(e, KeyError): # the local copy is at a bad state, lets remove it logger.debug( - "Removing local clone (%s) of repository as it is in a" - " broken state.", + ( + "Removing local clone (%s) of repository as it is in a" + " broken state." + ), local.path, ) - remove_directory(local.path, force=True) + remove_directory(Path(local.path), force=True) if isinstance(e, AssertionError) and "Invalid object name" not in str(e): raise logger.debug( - "\nRequested ref (%s) was not fetched to local copy and cannot" - " be used. The following error was raised:\n\n\t%s", + ( + "\nRequested ref (%s) was not fetched to local copy and" + " cannot be used. The following error was" + " raised:\n\n\t%s" + ), refspec.key, e, ) - raise PoetrySimpleConsoleException( + raise PoetryConsoleError( f"Failed to clone {url} at '{refspec.key}', verify ref exists on" " remote." ) @@ -328,16 +332,24 @@ def _clone_submodules(cls, repo: Repo) -> None: repo_root = Path(repo.path) modules_config = repo_root.joinpath(".gitmodules") + # A relative URL by definition starts with ../ or ./ + relative_submodule_regex = re.compile(r"^\.{1,2}/") + if modules_config.exists(): config = ConfigFile.from_path(str(modules_config)) url: bytes path: bytes submodules = parse_submodules(config) + for path, url, name in submodules: path_relative = Path(path.decode("utf-8")) path_absolute = repo_root.joinpath(path_relative) + url_string = url.decode("utf-8") + if relative_submodule_regex.search(url_string): + url_string = urljoin(f"{Git.get_remote_url(repo)}/", url_string) + source_root = path_absolute.parent source_root.mkdir(parents=True, exist_ok=True) @@ -354,7 +366,7 @@ def _clone_submodules(cls, repo: Repo) -> None: continue cls.clone( - url=url.decode("utf-8"), + url=url_string, source_root=source_root, name=path_relative.name, revision=revision, @@ -435,8 +447,10 @@ def clone( # without additional configuration or changes for existing projects that # use http basic auth credentials. logger.debug( - "Unable to fetch from private repository '%s', falling back to" - " system git", + ( + "Unable to fetch from private repository '%s', falling back to" + " system git" + ), url, ) diff --git a/src/poetry/version/version_selector.py b/src/poetry/version/version_selector.py index 476e3635444..195e83bd1a8 100644 --- a/src/poetry/version/version_selector.py +++ b/src/poetry/version/version_selector.py @@ -2,17 +2,15 @@ from typing import TYPE_CHECKING -from poetry.core.constraints.version import Version - if TYPE_CHECKING: from poetry.core.packages.package import Package - from poetry.repositories import Pool + from poetry.repositories import RepositoryPool class VersionSelector: - def __init__(self, pool: Pool) -> None: + def __init__(self, pool: RepositoryPool) -> None: self._pool = pool def find_best_candidate( @@ -56,14 +54,3 @@ def find_best_candidate( package = candidate return 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: str, pretty_version: str) -> str: - try: - return f"^{Version.parse(version).to_string()}" - except ValueError: - return pretty_version diff --git a/tests/compat.py b/tests/compat.py index bacac6652c3..a5808d9288b 100644 --- a/tests/compat.py +++ b/tests/compat.py @@ -8,6 +8,8 @@ from typing_extensions import Protocol # nopycln: import else: - import zipfile # noqa: F401 + import zipfile - from typing import Protocol # noqa: F401 + from typing import Protocol + +__all__ = ["zipfile", "Protocol"] diff --git a/tests/config/test_config.py b/tests/config/test_config.py index ce691470585..0194e7799d5 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -5,14 +5,14 @@ from pathlib import Path from typing import TYPE_CHECKING +from typing import Any import pytest -from flatdict import FlatDict - from poetry.config.config import Config from poetry.config.config import boolean_normalizer from poetry.config.config import int_normalizer +from tests.helpers import flatten_dict if TYPE_CHECKING: @@ -20,8 +20,8 @@ from collections.abc import Iterator -def get_options_based_on_normalizer(normalizer: Callable) -> str: - flattened_config = FlatDict(Config.default_config, delimiter=".") +def get_options_based_on_normalizer(normalizer: Callable[[str], Any]) -> str: + flattened_config = flatten_dict(obj=Config.default_config, delimiter=".") for k in flattened_config: if Config._get_normalizer(k) == normalizer: @@ -31,13 +31,13 @@ def get_options_based_on_normalizer(normalizer: Callable) -> str: @pytest.mark.parametrize( ("name", "value"), [("installer.parallel", True), ("virtualenvs.create", True)] ) -def test_config_get_default_value(config: Config, name: str, value: bool): +def test_config_get_default_value(config: Config, name: str, value: bool) -> None: assert config.get(name) is value def test_config_get_processes_depended_on_values( config: Config, config_cache_dir: Path -): +) -> None: assert str(config_cache_dir / "virtualenvs") == config.get("virtualenvs.path") @@ -63,7 +63,7 @@ def test_config_get_from_environment_variable( env_var: str, env_value: str, value: bool, -): +) -> None: os.environ[env_var] = env_value assert config.get(name) is value @@ -74,6 +74,6 @@ def test_config_get_from_environment_variable( ) def test_config_expands_tilde_for_virtualenvs_path( config: Config, path_config: str, expected: Path -): +) -> None: config.merge({"virtualenvs": {"path": path_config}}) assert config.virtualenvs_path == expected diff --git a/tests/conftest.py b/tests/conftest.py index 5de0afdd013..65abc45743a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,13 +5,11 @@ import re import shutil import sys -import tempfile from contextlib import suppress from pathlib import Path from typing import TYPE_CHECKING from typing import Any -from typing import TextIO import httpretty import pytest @@ -24,12 +22,11 @@ from poetry.inspection.info import PackageInfo from poetry.inspection.info import PackageInfoError from poetry.layouts import layout -from poetry.repositories import Pool from poetry.repositories import Repository +from poetry.repositories import RepositoryPool from poetry.utils.env import EnvManager from poetry.utils.env import SystemEnv from poetry.utils.env import VirtualEnv -from poetry.utils.helpers import remove_directory from tests.helpers import MOCK_DEFAULT_GIT_REVISION from tests.helpers import TestLocker from tests.helpers import TestRepository @@ -41,6 +38,7 @@ if TYPE_CHECKING: from collections.abc import Iterator + from collections.abc import Mapping from _pytest.config import Config as PyTestConfig from _pytest.config.argparsing import Parser @@ -70,6 +68,9 @@ def pytest_configure(config: PyTestConfig) -> None: class Config(BaseConfig): + _config_source: DictConfigSource + _auth_config_source: DictConfigSource + def get(self, setting_name: str, default: Any = None) -> Any: self.merge(self._config_source.config) self.merge(self._auth_config_source.config) @@ -91,7 +92,7 @@ def all(self) -> dict[str, Any]: class DummyBackend(KeyringBackend): def __init__(self) -> None: - self._passwords = {} + self._passwords: dict[str, dict[str | None, str | None]] = {} @classmethod def priority(cls) -> int: @@ -166,8 +167,8 @@ def with_chained_null_keyring(mocker: MockerFixture) -> None: @pytest.fixture -def config_cache_dir(tmp_dir: str) -> Path: - path = Path(tmp_dir) / ".cache" / "pypoetry" +def config_cache_dir(tmp_path: Path) -> Path: + path = tmp_path / ".cache" / "pypoetry" path.mkdir(parents=True) return path @@ -216,8 +217,10 @@ def config( @pytest.fixture() -def config_dir(tmp_dir: str) -> Path: - return Path(tempfile.mkdtemp(prefix="poetry_config_", dir=tmp_dir)) +def config_dir(tmp_path: Path) -> Path: + path = tmp_path / "config" + path.mkdir() + return path @pytest.fixture(autouse=True) @@ -231,7 +234,7 @@ def download_mock(mocker: MockerFixture) -> None: # Patch download to not download anything but to just copy from fixtures mocker.patch("poetry.utils.helpers.download_file", new=mock_download) mocker.patch("poetry.puzzle.provider.download_file", new=mock_download) - mocker.patch("poetry.repositories.http.download_file", new=mock_download) + mocker.patch("poetry.repositories.http_repository.download_file", new=mock_download) @pytest.fixture(autouse=True) @@ -280,11 +283,16 @@ def http() -> Iterator[type[httpretty.httpretty]]: @pytest.fixture +def project_root() -> Path: + return Path(__file__).parent.parent + + +@pytest.fixture(scope="session") def fixture_base() -> Path: return Path(__file__).parent / "fixtures" -@pytest.fixture +@pytest.fixture(scope="session") def fixture_dir(fixture_base: Path) -> FixtureDirGetter: def _fixture_dir(name: str) -> Path: return fixture_base / name @@ -293,39 +301,15 @@ def _fixture_dir(name: str) -> Path: @pytest.fixture -def tmp_dir() -> Iterator[str]: - dir_ = tempfile.mkdtemp(prefix="poetry_") - - yield Path(dir_).resolve().as_posix() - - remove_directory(dir_, force=True) - - -@pytest.fixture -def mocked_open_files(mocker: MockerFixture) -> list: - files = [] - original = Path.open - - def mocked_open(self: Path, *args: Any, **kwargs: Any) -> TextIO: - if self.name in {"pyproject.toml"}: - return mocker.MagicMock() - return original(self, *args, **kwargs) - - mocker.patch("pathlib.Path.open", mocked_open) - - return files - - -@pytest.fixture -def tmp_venv(tmp_dir: str) -> Iterator[VirtualEnv]: - venv_path = Path(tmp_dir) / "venv" +def tmp_venv(tmp_path: Path) -> Iterator[VirtualEnv]: + venv_path = tmp_path / "venv" - EnvManager.build_venv(str(venv_path)) + EnvManager.build_venv(venv_path) venv = VirtualEnv(venv_path) yield venv - shutil.rmtree(str(venv.path)) + shutil.rmtree(venv.path) @pytest.fixture @@ -359,24 +343,25 @@ def repo(http: type[httpretty.httpretty]) -> TestRepository: @pytest.fixture def project_factory( - tmp_dir: str, + tmp_path: Path, config: Config, repo: TestRepository, installed: Repository, default_python: str, load_required_fixtures: None, ) -> ProjectFactory: - workspace = Path(tmp_dir) + workspace = tmp_path def _factory( name: str | None = None, - dependencies: dict[str, str] | None = None, - dev_dependencies: dict[str, str] | None = None, + dependencies: Mapping[str, str] | None = None, + dev_dependencies: Mapping[str, str] | None = None, pyproject_content: str | None = None, poetry_lock_content: str | None = None, install_deps: bool = True, source: Path | None = None, locker_config: dict[str, Any] | None = None, + use_test_locker: bool = True, ) -> Poetry: project_dir = workspace / f"poetry-fixture-{name}" dependencies = dependencies or {} @@ -390,11 +375,10 @@ def _factory( project_dir.mkdir(parents=True, exist_ok=True) if pyproject_content: - with project_dir.joinpath("pyproject.toml").open( - "w", encoding="utf-8" - ) as f: + with (project_dir / "pyproject.toml").open("w", encoding="utf-8") as f: f.write(pyproject_content) else: + assert name is not None layout("src")( name, "0.1.0", @@ -411,15 +395,17 @@ def _factory( poetry = Factory().create_poetry(project_dir) - locker = TestLocker( - poetry.locker.lock.path, locker_config or poetry.locker._local_config - ) - locker.write() + if use_test_locker: + locker = TestLocker( + poetry.locker.lock, locker_config or poetry.locker._local_config + ) + locker.write() + + poetry.set_locker(locker) - poetry.set_locker(locker) poetry.set_config(config) - pool = Pool() + pool = RepositoryPool() pool.add_repository(repo) poetry.set_pool(pool) @@ -436,11 +422,6 @@ def _factory( return _factory -@pytest.fixture -def project_root() -> Path: - return Path(__file__).parent.parent - - @pytest.fixture(autouse=True) def set_simple_log_formatter() -> None: """ @@ -453,10 +434,10 @@ def set_simple_log_formatter() -> None: @pytest.fixture -def fixture_copier(fixture_base: Path, tmp_dir: str) -> FixtureCopier: +def fixture_copier(fixture_base: Path, tmp_path: Path) -> FixtureCopier: def _copy(relative_path: str, target: Path | None = None) -> Path: - path = fixture_base.joinpath(relative_path) - target = target or Path(tmp_dir, relative_path) + path = fixture_base / relative_path + target = target or (tmp_path / relative_path) target.parent.mkdir(parents=True, exist_ok=True) if target.exists(): diff --git a/tests/console/commands/cache/test_list.py b/tests/console/commands/cache/test_list.py index 22db34cb0de..2def8564831 100644 --- a/tests/console/commands/cache/test_list.py +++ b/tests/console/commands/cache/test_list.py @@ -20,7 +20,7 @@ def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: def test_cache_list( tester: CommandTester, mock_caches: None, repository_one: str, repository_two: str -): +) -> None: tester.execute() expected = f"""\ @@ -31,7 +31,7 @@ def test_cache_list( assert tester.io.fetch_output() == expected -def test_cache_list_empty(tester: CommandTester, repository_cache_dir: Path): +def test_cache_list_empty(tester: CommandTester, repository_cache_dir: Path) -> None: tester.execute() expected = """\ diff --git a/tests/console/commands/debug/test_resolve.py b/tests/console/commands/debug/test_resolve.py index ddb45d4d1c8..1d733e3612f 100644 --- a/tests/console/commands/debug/test_resolve.py +++ b/tests/console/commands/debug/test_resolve.py @@ -33,7 +33,7 @@ def __add_packages(repo: TestRepository) -> None: repo.add_package(get_package("cleo", "0.6.5")) -def test_debug_resolve_gives_resolution_results(tester: CommandTester): +def test_debug_resolve_gives_resolution_results(tester: CommandTester) -> None: tester.execute("cachy") expected = """\ @@ -48,7 +48,9 @@ def test_debug_resolve_gives_resolution_results(tester: CommandTester): assert tester.io.fetch_output() == expected -def test_debug_resolve_tree_option_gives_the_dependency_tree(tester: CommandTester): +def test_debug_resolve_tree_option_gives_the_dependency_tree( + tester: CommandTester, +) -> None: tester.execute("cachy --tree") expected = """\ @@ -63,7 +65,7 @@ def test_debug_resolve_tree_option_gives_the_dependency_tree(tester: CommandTest assert tester.io.fetch_output() == expected -def test_debug_resolve_git_dependency(tester: CommandTester): +def test_debug_resolve_git_dependency(tester: CommandTester) -> None: tester.execute("git+https://github.com/demo/demo.git") expected = """\ diff --git a/tests/console/commands/env/conftest.py b/tests/console/commands/env/conftest.py index 32827c33e7a..4f613248cc0 100644 --- a/tests/console/commands/env/conftest.py +++ b/tests/console/commands/env/conftest.py @@ -2,7 +2,6 @@ import os -from pathlib import Path from typing import TYPE_CHECKING import pytest @@ -12,6 +11,7 @@ if TYPE_CHECKING: from collections.abc import Iterator + from pathlib import Path from tests.helpers import PoetryTestApplication @@ -25,8 +25,8 @@ def venv_name(app: PoetryTestApplication) -> str: @pytest.fixture -def venv_cache(tmp_dir: str) -> Path: - return Path(tmp_dir) +def venv_cache(tmp_path: Path) -> Path: + return tmp_path @pytest.fixture(scope="module") @@ -49,7 +49,7 @@ def venvs_in_cache_dirs( ) -> list[str]: directories = [] for version in python_versions: - directory = venv_cache.joinpath(f"{venv_name}-py{version}") + directory = venv_cache / f"{venv_name}-py{version}" directory.mkdir(parents=True, exist_ok=True) directories.append(directory.name) return directories @@ -66,3 +66,29 @@ def venvs_in_project_dir(app: PoetryTestApplication) -> Iterator[Path]: yield venv_dir finally: venv_dir.rmdir() + + +@pytest.fixture +def venvs_in_project_dir_none(app: PoetryTestApplication) -> Iterator[Path]: + os.environ.pop("VIRTUAL_ENV", None) + venv_dir = app.poetry.file.parent.joinpath(".venv") + venv_dir.mkdir(exist_ok=True) + app.poetry.config.merge({"virtualenvs": {"in-project": None}}) + + try: + yield venv_dir + finally: + venv_dir.rmdir() + + +@pytest.fixture +def venvs_in_project_dir_false(app: PoetryTestApplication) -> Iterator[Path]: + os.environ.pop("VIRTUAL_ENV", None) + venv_dir = app.poetry.file.parent.joinpath(".venv") + venv_dir.mkdir(exist_ok=True) + app.poetry.config.merge({"virtualenvs": {"in-project": False}}) + + try: + yield venv_dir + finally: + venv_dir.rmdir() diff --git a/tests/console/commands/env/helpers.py b/tests/console/commands/env/helpers.py index 0bf94128154..942c27243d4 100644 --- a/tests/console/commands/env/helpers.py +++ b/tests/console/commands/env/helpers.py @@ -1,5 +1,7 @@ from __future__ import annotations +import os + from pathlib import Path from typing import TYPE_CHECKING from typing import Any @@ -10,8 +12,6 @@ if TYPE_CHECKING: from collections.abc import Callable - from poetry.core.version.pep440.version import PEP440Version - VERSION_3_7_1 = Version.parse("3.7.1") @@ -20,16 +20,21 @@ def build_venv(path: Path | str, **_: Any) -> None: def check_output_wrapper( - version: PEP440Version = VERSION_3_7_1, -) -> Callable[[str, Any, Any], str]: - def check_output(cmd: str, *_: Any, **__: Any) -> str: - if "sys.version_info[:3]" in cmd: + version: Version = VERSION_3_7_1, +) -> Callable[[list[str], Any, Any], str]: + def check_output(cmd: list[str], *args: Any, **kwargs: Any) -> str: + # cmd is a list, like ["python", "-c", "do stuff"] + python_cmd = cmd[2] + if "sys.version_info[:3]" in python_cmd: return version.text - elif "sys.version_info[:2]" in cmd: + elif "sys.version_info[:2]" in python_cmd: return f"{version.major}.{version.minor}" - elif '-c "import sys; print(sys.executable)"' in cmd: - return f"/usr/bin/{cmd.split()[0]}" + elif "import sys; print(sys.executable)" in python_cmd: + executable = cmd[0] + basename = os.path.basename(executable) + return f"/usr/bin/{basename}" else: - return str(Path("/prefix")) + assert "import sys; print(sys.prefix)" in python_cmd + return "/prefix" return check_output diff --git a/tests/console/commands/env/test_list.py b/tests/console/commands/env/test_list.py index 2f284470bbd..b617d3e39c2 100644 --- a/tests/console/commands/env/test_list.py +++ b/tests/console/commands/env/test_list.py @@ -5,7 +5,7 @@ import pytest import tomlkit -from poetry.core.toml.file import TOMLFile +from poetry.toml.file import TOMLFile if TYPE_CHECKING: @@ -36,10 +36,10 @@ def test_none_activated( venvs_in_cache_dirs: list[str], mocker: MockerFixture, env: MockEnv, -): +) -> None: mocker.patch("poetry.utils.env.EnvManager.get", return_value=env) tester.execute() - expected = "\n".join(venvs_in_cache_dirs).strip() + expected = "\n".join(venvs_in_cache_dirs) assert tester.io.fetch_output().strip() == expected @@ -48,15 +48,31 @@ def test_activated( venvs_in_cache_dirs: list[str], venv_cache: Path, venv_activate_37: None, -): +) -> None: tester.execute() - expected = ( - "\n".join(venvs_in_cache_dirs).strip().replace("py3.7", "py3.7 (Activated)") - ) + expected = "\n".join(venvs_in_cache_dirs).replace("py3.7", "py3.7 (Activated)") assert tester.io.fetch_output().strip() == expected -def test_in_project_venv(tester: CommandTester, venvs_in_project_dir: list[str]): +def test_in_project_venv( + tester: CommandTester, venvs_in_project_dir: list[str] +) -> None: tester.execute() expected = ".venv (Activated)\n" assert tester.io.fetch_output() == expected + + +def test_in_project_venv_no_explicit_config( + tester: CommandTester, venvs_in_project_dir_none: list[str] +) -> None: + tester.execute() + expected = ".venv (Activated)\n" + assert tester.io.fetch_output() == expected + + +def test_in_project_venv_is_false( + tester: CommandTester, venvs_in_project_dir_false: list[str] +) -> None: + tester.execute() + expected = "" + assert tester.io.fetch_output() == expected diff --git a/tests/console/commands/env/test_remove.py b/tests/console/commands/env/test_remove.py index 428d4f13dfd..9586be90ea1 100644 --- a/tests/console/commands/env/test_remove.py +++ b/tests/console/commands/env/test_remove.py @@ -29,7 +29,7 @@ def test_remove_by_python_version( venvs_in_cache_dirs: list[str], venv_name: str, venv_cache: Path, -): +) -> None: check_output = mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.6")), @@ -49,7 +49,7 @@ def test_remove_by_name( venvs_in_cache_dirs: list[str], venv_name: str, venv_cache: Path, -): +) -> None: expected = "" for name in venvs_in_cache_dirs: @@ -67,7 +67,7 @@ def test_remove_all( venvs_in_cache_dirs: list[str], venv_name: str, venv_cache: Path, -): +) -> None: expected = {""} tester.execute("--all") for name in venvs_in_cache_dirs: @@ -81,7 +81,7 @@ def test_remove_all_and_version( venvs_in_cache_dirs: list[str], venv_name: str, venv_cache: Path, -): +) -> None: expected = {""} tester.execute(f"--all {venvs_in_cache_dirs[0]}") for name in venvs_in_cache_dirs: @@ -95,7 +95,7 @@ def test_remove_multiple( venvs_in_cache_dirs: list[str], venv_name: str, venv_cache: Path, -): +) -> None: expected = {""} removed_envs = venvs_in_cache_dirs[0:2] remaining_envs = venvs_in_cache_dirs[2:] diff --git a/tests/console/commands/env/test_use.py b/tests/console/commands/env/test_use.py index ad05e1e73b1..d0abd38f8c4 100644 --- a/tests/console/commands/env/test_use.py +++ b/tests/console/commands/env/test_use.py @@ -9,8 +9,8 @@ import tomlkit from poetry.core.constraints.version import Version -from poetry.core.toml.file import TOMLFile +from poetry.toml.file import TOMLFile 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 @@ -55,7 +55,8 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file( venv_cache: Path, venv_name: str, venvs_in_cache_config: None, -): +) -> None: + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(), @@ -70,7 +71,7 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file( venv_py37 = venv_cache / f"{venv_name}-py3.7" mock_build_env.assert_called_with( venv_py37, - executable="/usr/bin/python3.7", + executable=Path("/usr/bin/python3.7"), flags={ "always-copy": False, "system-site-packages": False, @@ -94,12 +95,13 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file( def test_get_prefers_explicitly_activated_virtualenvs_over_env_var( + mocker: MockerFixture, tester: CommandTester, current_python: tuple[int, int, int], venv_cache: Path, venv_name: str, venvs_in_cache_config: None, -): +) -> None: os.environ["VIRTUAL_ENV"] = "/environment/prefix" python_minor = ".".join(str(v) for v in current_python[:2]) @@ -112,6 +114,8 @@ def test_get_prefers_explicitly_activated_virtualenvs_over_env_var( doc[venv_name] = {"minor": python_minor, "patch": python_patch} envs_file.write(doc) + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") + tester.execute(python_minor) expected = f"""\ @@ -128,12 +132,13 @@ def test_get_prefers_explicitly_activated_non_existing_virtualenvs_over_env_var( venv_cache: Path, venv_name: str, venvs_in_cache_config: None, -): +) -> None: os.environ["VIRTUAL_ENV"] = "/environment/prefix" python_minor = ".".join(str(v) for v in current_python[:2]) venv_dir = venv_cache / f"{venv_name}-py{python_minor}" + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") mocker.patch( "poetry.utils.env.EnvManager._env", new_callable=mocker.PropertyMock, diff --git a/tests/console/commands/self/conftest.py b/tests/console/commands/self/conftest.py index 6a175b5fdf0..381e22e2f90 100644 --- a/tests/console/commands/self/conftest.py +++ b/tests/console/commands/self/conftest.py @@ -1,21 +1,28 @@ from __future__ import annotations from typing import TYPE_CHECKING +from typing import Any +from typing import Callable import pytest from poetry.core.packages.package import Package from poetry.__version__ import __version__ -from poetry.repositories import Pool +from poetry.factory import Factory +from poetry.repositories import RepositoryPool from poetry.utils.env import EnvManager if TYPE_CHECKING: + from collections.abc import Iterable + import httpretty + from cleo.io.io import IO from pytest_mock import MockerFixture + from poetry.config.config import Config from poetry.repositories.repository import Repository from poetry.utils.env import VirtualEnv from tests.helpers import TestRepository @@ -34,8 +41,25 @@ def save_environ(environ: None) -> Repository: @pytest.fixture() -def pool(repo: TestRepository) -> Pool: - return Pool([repo]) +def pool(repo: TestRepository) -> RepositoryPool: + return RepositoryPool([repo]) + + +def create_pool_factory( + repo: Repository, +) -> Callable[[Config, Iterable[dict[str, Any]], IO, bool], RepositoryPool]: + def _create_pool( + config: Config, + sources: Iterable[dict[str, Any]] = (), + io: IO | None = None, + disable_cache: bool = False, + ) -> RepositoryPool: + pool = RepositoryPool() + pool.add_repository(repo) + + return pool + + return _create_pool @pytest.fixture(autouse=True) @@ -43,14 +67,21 @@ def setup_mocks( mocker: MockerFixture, tmp_venv: VirtualEnv, installed: Repository, - pool: Pool, + pool: RepositoryPool, http: type[httpretty.httpretty], + repo: Repository, ) -> None: mocker.patch.object(EnvManager, "get_system_env", return_value=tmp_venv) - mocker.patch("poetry.repositories.pool.Pool.find_packages", pool.find_packages) - mocker.patch("poetry.repositories.pool.Pool.package", pool.package) + mocker.patch( + "poetry.repositories.repository_pool.RepositoryPool.find_packages", + pool.find_packages, + ) + mocker.patch( + "poetry.repositories.repository_pool.RepositoryPool.package", pool.package + ) mocker.patch("poetry.installation.executor.pip_install") mocker.patch( "poetry.installation.installer.Installer._get_installed", return_value=installed, ) + mocker.patch.object(Factory, "create_pool", side_effect=create_pool_factory(repo)) diff --git a/tests/console/commands/self/test_add_plugins.py b/tests/console/commands/self/test_add_plugins.py index e8447a32b13..981a3fe8db8 100644 --- a/tests/console/commands/self/test_add_plugins.py +++ b/tests/console/commands/self/test_add_plugins.py @@ -38,7 +38,7 @@ def assert_plugin_add_result( def test_add_no_constraint( tester: CommandTester, repo: TestRepository, -): +) -> None: repo.add_package(Package("poetry-plugin", "0.1.0")) tester.execute("poetry-plugin") @@ -49,11 +49,11 @@ def test_add_no_constraint( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing poetry-plugin (0.1.0) + +Writing lock file """ assert_plugin_add_result(tester, expected, "^0.1.0") @@ -61,7 +61,7 @@ def test_add_no_constraint( def test_add_with_constraint( tester: CommandTester, repo: TestRepository, -): +) -> None: repo.add_package(Package("poetry-plugin", "0.1.0")) repo.add_package(Package("poetry-plugin", "0.2.0")) @@ -71,11 +71,11 @@ def test_add_with_constraint( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing poetry-plugin (0.2.0) + +Writing lock file """ assert_plugin_add_result(tester, expected, "^0.2.0") @@ -84,7 +84,7 @@ def test_add_with_constraint( def test_add_with_git_constraint( tester: CommandTester, repo: TestRepository, -): +) -> None: repo.add_package(Package("pendulum", "2.0.5")) tester.execute("git+https://github.com/demo/poetry-plugin.git") @@ -93,12 +93,12 @@ def test_add_with_git_constraint( 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) + +Writing lock file """ assert_plugin_add_result( @@ -109,7 +109,7 @@ def test_add_with_git_constraint( def test_add_with_git_constraint_with_extras( tester: CommandTester, repo: TestRepository, -): +) -> None: repo.add_package(Package("pendulum", "2.0.5")) repo.add_package(Package("tomlkit", "0.7.0")) @@ -119,13 +119,13 @@ def test_add_with_git_constraint_with_extras( 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) + +Writing lock file """ assert_plugin_add_result( @@ -153,7 +153,7 @@ def test_add_with_git_constraint_with_subdirectory( rev: str | None, tester: CommandTester, repo: TestRepository, -): +) -> None: repo.add_package(Package("pendulum", "2.0.5")) tester.execute(url) @@ -162,12 +162,12 @@ def test_add_with_git_constraint_with_subdirectory( 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) + +Writing lock file """ constraint = { @@ -189,9 +189,11 @@ def test_add_existing_plugin_warns_about_no_operation( tester: CommandTester, repo: TestRepository, installed: TestRepository, -): - SelfCommand.get_default_system_pyproject_file().write_text( - f"""\ +) -> None: + pyproject = SelfCommand.get_default_system_pyproject_file() + with open(pyproject, "w", encoding="utf-8", newline="") as f: + f.write( + f"""\ [tool.poetry] name = "poetry-instance" version = "1.2.0" @@ -203,9 +205,8 @@ def test_add_existing_plugin_warns_about_no_operation( [tool.poetry.group.{SelfCommand.ADDITIONAL_PACKAGE_GROUP}.dependencies] poetry-plugin = "^1.2.3" -""", - encoding="utf-8", - ) +""" + ) installed.add_package(Package("poetry-plugin", "1.2.3")) @@ -229,9 +230,11 @@ def test_add_existing_plugin_updates_if_requested( tester: CommandTester, repo: TestRepository, installed: TestRepository, -): - SelfCommand.get_default_system_pyproject_file().write_text( - f"""\ +) -> None: + pyproject = SelfCommand.get_default_system_pyproject_file() + with open(pyproject, "w", encoding="utf-8", newline="") as f: + f.write( + f"""\ [tool.poetry] name = "poetry-instance" version = "1.2.0" @@ -243,9 +246,8 @@ def test_add_existing_plugin_updates_if_requested( [tool.poetry.group.{SelfCommand.ADDITIONAL_PACKAGE_GROUP}.dependencies] poetry-plugin = "^1.2.3" -""", - encoding="utf-8", - ) +""" + ) installed.add_package(Package("poetry-plugin", "1.2.3")) @@ -260,11 +262,11 @@ def test_add_existing_plugin_updates_if_requested( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 0 installs, 1 update, 0 removals • Updating poetry-plugin (1.2.3 -> 2.3.4) + +Writing lock file """ assert_plugin_add_result(tester, expected, "^2.3.4") @@ -274,7 +276,7 @@ def test_adding_a_plugin_can_update_poetry_dependencies_if_needed( tester: CommandTester, repo: TestRepository, installed: TestRepository, -): +) -> None: poetry_package = Package("poetry", "1.2.0") poetry_package.add_dependency(Factory.create_dependency("tomlkit", "^0.7.0")) @@ -296,12 +298,12 @@ def test_adding_a_plugin_can_update_poetry_dependencies_if_needed( 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) + +Writing lock file """ assert_plugin_add_result(tester, expected, "^1.2.3") diff --git a/tests/console/commands/self/test_remove_plugins.py b/tests/console/commands/self/test_remove_plugins.py index 2b988443469..9d1dfbaeae3 100644 --- a/tests/console/commands/self/test_remove_plugins.py +++ b/tests/console/commands/self/test_remove_plugins.py @@ -37,7 +37,8 @@ def install_plugin(installed: Repository) -> None: ) content = Factory.create_pyproject_from_package(package) system_pyproject_file = SelfCommand.get_default_system_pyproject_file() - system_pyproject_file.write_text(content.as_string(), encoding="utf-8") + with open(system_pyproject_file, "w", encoding="utf-8", newline="") as f: + f.write(content.as_string()) lock_content = { "package": [ @@ -65,18 +66,18 @@ def install_plugin(installed: Repository) -> None: installed.add_package(plugin) -def test_remove_installed_package(tester: CommandTester): +def test_remove_installed_package(tester: CommandTester) -> None: 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) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -86,7 +87,7 @@ def test_remove_installed_package(tester: CommandTester): assert not dependencies -def test_remove_installed_package_dry_run(tester: CommandTester): +def test_remove_installed_package_dry_run(tester: CommandTester) -> None: tester.execute("poetry-plugin --dry-run") expected = f"""\ diff --git a/tests/console/commands/self/test_self_command.py b/tests/console/commands/self/test_self_command.py new file mode 100644 index 00000000000..c07be3059d5 --- /dev/null +++ b/tests/console/commands/self/test_self_command.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import pytest + +from poetry.core.packages.dependency import Dependency +from poetry.core.packages.package import Package +from poetry.core.packages.project_package import ProjectPackage + +from poetry.__version__ import __version__ +from poetry.console.commands.self.self_command import SelfCommand +from poetry.factory import Factory + + +@pytest.fixture +def example_system_pyproject(): + package = ProjectPackage("poetry-instance", __version__) + plugin = Package("poetry-plugin", "1.2.3") + + package.add_dependency( + Dependency(plugin.name, "^1.2.3", groups=[SelfCommand.ADDITIONAL_PACKAGE_GROUP]) + ) + content = Factory.create_pyproject_from_package(package) + return content.as_string().rstrip("\n") + + +@pytest.mark.parametrize("existing_newlines", [0, 2]) +def test_generate_system_pyproject_trailing_newline( + existing_newlines: int, + example_system_pyproject: str, +): + cmd = SelfCommand() + cmd.system_pyproject.write_text(example_system_pyproject + "\n" * existing_newlines) + cmd.generate_system_pyproject() + generated = cmd.system_pyproject.read_text() + + assert len(generated) - len(generated.rstrip("\n")) == existing_newlines + + +def test_generate_system_pyproject_carriage_returns( + example_system_pyproject: str, +): + cmd = SelfCommand() + cmd.system_pyproject.write_text(example_system_pyproject + "\n") + cmd.generate_system_pyproject() + + with open(cmd.system_pyproject, newline="") as f: # do not translate newlines + generated = f.read() + + assert "\r\r" not in generated diff --git a/tests/console/commands/self/test_show_plugins.py b/tests/console/commands/self/test_show_plugins.py index 3ce27786760..410473f9133 100644 --- a/tests/console/commands/self/test_show_plugins.py +++ b/tests/console/commands/self/test_show_plugins.py @@ -1,6 +1,5 @@ from __future__ import annotations -from pathlib import Path from typing import TYPE_CHECKING from typing import Any from typing import Callable @@ -17,6 +16,7 @@ if TYPE_CHECKING: from os import PathLike + from pathlib import Path from cleo.io.io import IO from cleo.testers.command_tester import CommandTester @@ -64,7 +64,7 @@ def plugin_package(plugin_package_requires_dist: list[str]) -> Package: @pytest.fixture() -def plugin_distro(plugin_package: Package, tmp_dir: str) -> metadata.Distribution: +def plugin_distro(plugin_package: Package, tmp_path: Path) -> metadata.Distribution: class MockDistribution(metadata.Distribution): def read_text(self, filename: str) -> str | None: if filename == "METADATA": @@ -81,7 +81,7 @@ def read_text(self, filename: str) -> str | None: return None def locate_file(self, path: PathLike[str]) -> PathLike[str]: - return Path(tmp_dir, path) + return tmp_path / path return MockDistribution() @@ -153,7 +153,7 @@ def mock_metadata_entry_points( def test_show_displays_installed_plugins( app: PoetryTestApplication, tester: CommandTester, -): +) -> None: tester.execute("") expected = """ @@ -179,7 +179,7 @@ def test_show_displays_installed_plugins( def test_show_displays_installed_plugins_with_multiple_plugins( app: PoetryTestApplication, tester: CommandTester, -): +) -> None: tester.execute("") expected = """ @@ -205,7 +205,7 @@ def test_show_displays_installed_plugins_with_multiple_plugins( def test_show_displays_installed_plugins_with_dependencies( app: PoetryTestApplication, tester: CommandTester, -): +) -> None: tester.execute("") expected = """ diff --git a/tests/console/commands/self/test_update.py b/tests/console/commands/self/test_update.py index 9cc7e523d23..d2c6ef0c933 100644 --- a/tests/console/commands/self/test_update.py +++ b/tests/console/commands/self/test_update.py @@ -1,6 +1,5 @@ from __future__ import annotations -from pathlib import Path from typing import TYPE_CHECKING import pytest @@ -10,15 +9,30 @@ from poetry.__version__ import __version__ from poetry.factory import Factory +from poetry.installation.executor import Executor +from poetry.installation.wheel_installer import WheelInstaller if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester + from pytest_mock import MockerFixture from tests.helpers import TestRepository from tests.types import CommandTesterFactory + from tests.types import FixtureDirGetter -FIXTURES = Path(__file__).parent.joinpath("fixtures") + +@pytest.fixture +def setup(mocker: MockerFixture, fixture_dir: FixtureDirGetter) -> None: + mocker.patch.object( + Executor, + "_download", + return_value=fixture_dir("distributions").joinpath( + "demo-0.1.2-py2.py3-none-any.whl" + ), + ) + + mocker.patch.object(WheelInstaller, "install") @pytest.fixture() @@ -30,7 +44,7 @@ def test_self_update_can_update_from_recommended_installation( tester: CommandTester, repo: TestRepository, installed: TestRepository, -): +) -> None: new_version = Version.parse(__version__).next_minor().text old_poetry = Package("poetry", __version__) @@ -55,12 +69,12 @@ def test_self_update_can_update_from_recommended_installation( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 0 installs, 2 updates, 0 removals • Updating cleo (0.8.2 -> 1.0.0) • Updating poetry ({__version__} -> {new_version}) + +Writing lock file """ assert tester.io.fetch_output() == expected_output diff --git a/tests/console/commands/source/conftest.py b/tests/console/commands/source/conftest.py index 254b3454297..5ec79df2c9e 100644 --- a/tests/console/commands/source/conftest.py +++ b/tests/console/commands/source/conftest.py @@ -5,6 +5,7 @@ import pytest from poetry.config.source import Source +from poetry.repositories.repository_pool import Priority if TYPE_CHECKING: @@ -24,15 +25,39 @@ def source_two() -> Source: @pytest.fixture -def source_default() -> Source: +def source_default_deprecated() -> Source: return Source(name="default", url="https://default.com", default=True) @pytest.fixture -def source_secondary() -> Source: +def source_secondary_deprecated() -> Source: return Source(name="secondary", url="https://secondary.com", secondary=True) +@pytest.fixture +def source_primary() -> Source: + return Source(name="primary", url="https://primary.com", priority=Priority.PRIMARY) + + +@pytest.fixture +def source_default() -> Source: + return Source(name="default", url="https://default.com", priority=Priority.DEFAULT) + + +@pytest.fixture +def source_secondary() -> Source: + return Source( + name="secondary", url="https://secondary.com", priority=Priority.SECONDARY + ) + + +@pytest.fixture +def source_explicit() -> Source: + return Source( + name="explicit", url="https://explicit.com", priority=Priority.EXPLICIT + ) + + _existing_source = Source(name="existing", url="https://existing.com") @@ -41,7 +66,7 @@ def source_existing() -> Source: return _existing_source -PYPROJECT_WITH_SOURCES = f""" +PYPROJECT_WITHOUT_SOURCES = """ [tool.poetry] name = "source-command-test" version = "0.1.0" @@ -52,6 +77,10 @@ def source_existing() -> Source: python = "^3.9" [tool.poetry.dev-dependencies] +""" + + +PYPROJECT_WITH_SOURCES = f"""{PYPROJECT_WITHOUT_SOURCES} [[tool.poetry.source]] name = "{_existing_source.name}" @@ -59,6 +88,11 @@ def source_existing() -> Source: """ +@pytest.fixture +def poetry_without_source(project_factory: ProjectFactory) -> Poetry: + return project_factory(pyproject_content=PYPROJECT_WITHOUT_SOURCES) + + @pytest.fixture def poetry_with_source(project_factory: ProjectFactory) -> Poetry: return project_factory(pyproject_content=PYPROJECT_WITH_SOURCES) @@ -74,3 +108,22 @@ def add_multiple_sources( add = command_tester_factory("source add", poetry=poetry_with_source) for source in [source_one, source_two]: add.execute(f"{source.name} {source.url}") + + +@pytest.fixture +def add_all_source_types( + command_tester_factory: CommandTesterFactory, + poetry_with_source: Poetry, + source_primary: Source, + source_default: Source, + source_secondary: Source, + source_explicit: Source, +) -> None: + add = command_tester_factory("source add", poetry=poetry_with_source) + for source in [ + source_primary, + source_default, + source_secondary, + source_explicit, + ]: + add.execute(f"{source.name} {source.url} --priority={source.name}") diff --git a/tests/console/commands/source/test_add.py b/tests/console/commands/source/test_add.py index 7e43b9e2014..468e9dec271 100644 --- a/tests/console/commands/source/test_add.py +++ b/tests/console/commands/source/test_add.py @@ -1,16 +1,16 @@ from __future__ import annotations -import dataclasses - from typing import TYPE_CHECKING import pytest +from poetry.config.source import Source +from poetry.repositories.repository_pool import Priority + if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester - from poetry.config.source import Source from poetry.poetry import Poetry from tests.types import CommandTesterFactory @@ -22,6 +22,28 @@ def tester( return command_tester_factory("source add", poetry=poetry_with_source) +def assert_source_added_legacy( + tester: CommandTester, + poetry: Poetry, + source_existing: Source, + source_added: Source, +) -> None: + assert ( + tester.io.fetch_error().strip() + == "Warning: Priority was set through a deprecated flag" + " (--default or --secondary). Consider using --priority next" + " time." + ) + 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 assert_source_added( tester: CommandTester, poetry: Poetry, @@ -43,32 +65,88 @@ def test_source_add_simple( source_existing: Source, source_one: Source, poetry_with_source: Poetry, -): +) -> None: 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_legacy( + tester: CommandTester, + source_existing: Source, + source_default: Source, + poetry_with_source: Poetry, +) -> None: + tester.execute(f"--default {source_default.name} {source_default.url}") + assert_source_added_legacy( + tester, poetry_with_source, source_existing, source_default + ) + + +def test_source_add_secondary_legacy( + tester: CommandTester, + source_existing: Source, + source_secondary: Source, + poetry_with_source: Poetry, +): + tester.execute(f"--secondary {source_secondary.name} {source_secondary.url}") + assert_source_added_legacy( + tester, poetry_with_source, source_existing, source_secondary + ) + + def test_source_add_default( tester: CommandTester, source_existing: Source, source_default: Source, poetry_with_source: Poetry, ): - tester.execute(f"--default {source_default.name} {source_default.url}") + tester.execute(f"--priority=default {source_default.name} {source_default.url}") assert_source_added(tester, poetry_with_source, source_existing, source_default) +def test_source_add_second_default_fails( + tester: CommandTester, + source_existing: Source, + source_default: Source, + poetry_with_source: Poetry, +): + tester.execute(f"--priority=default {source_default.name} {source_default.url}") + assert_source_added(tester, poetry_with_source, source_existing, source_default) + poetry_with_source.pyproject.reload() + + tester.execute(f"--priority=default {source_default.name}1 {source_default.url}") + assert ( + tester.io.fetch_error().strip() + == f"Source with name {source_default.name} is already set to" + " default. Only one default source can be configured at a" + " time." + ) + assert tester.status_code == 1 + + def test_source_add_secondary( tester: CommandTester, source_existing: Source, source_secondary: Source, poetry_with_source: Poetry, -): - tester.execute(f"--secondary {source_secondary.name} {source_secondary.url}") +) -> None: + tester.execute( + f"--priority=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: CommandTester): +def test_source_add_explicit( + tester: CommandTester, + source_existing: Source, + source_explicit: Source, + poetry_with_source: Poetry, +) -> None: + tester.execute(f"--priority=explicit {source_explicit.name} {source_explicit.url}") + assert_source_added(tester, poetry_with_source, source_existing, source_explicit) + + +def test_source_add_error_default_and_secondary_legacy(tester: CommandTester) -> None: tester.execute("--default --secondary error https://error.com") assert ( tester.io.fetch_error().strip() @@ -77,7 +155,18 @@ def test_source_add_error_default_and_secondary(tester: CommandTester): assert tester.status_code == 1 -def test_source_add_error_pypi(tester: CommandTester): +def test_source_add_error_priority_and_deprecated_legacy(tester: CommandTester): + tester.execute("--priority secondary --secondary error https://error.com") + assert ( + tester.io.fetch_error().strip() + == "Priority was passed through both --priority and a" + " deprecated flag (--default or --secondary). Please only provide" + " one of these." + ) + assert tester.status_code == 1 + + +def test_source_add_error_pypi(tester: CommandTester) -> None: tester.execute("pypi https://test.pypi.org/simple/") assert ( tester.io.fetch_error().strip() @@ -87,10 +176,52 @@ def test_source_add_error_pypi(tester: CommandTester): assert tester.status_code == 1 -def test_source_add_existing( +def test_source_add_existing_legacy( tester: CommandTester, source_existing: Source, poetry_with_source: Poetry -): +) -> None: tester.execute(f"--default {source_existing.name} {source_existing.url}") + assert ( + tester.io.fetch_error().strip() + == "Warning: Priority was set through a deprecated flag" + " (--default or --secondary). Consider using --priority next" + " time." + ) + assert ( + tester.io.fetch_output().strip() + == f"Source with name {source_existing.name} already exists. Updating." + ) + + poetry_with_source.pyproject.reload() + sources = poetry_with_source.get_sources() + + assert len(sources) == 1 + assert sources[0] != source_existing + expected_source = Source( + name=source_existing.name, url=source_existing.url, priority=Priority.DEFAULT + ) + assert sources[0] == expected_source + + +def test_source_add_existing_no_change( + tester: CommandTester, source_existing: Source, poetry_with_source: Poetry +): + tester.execute(f"--priority=primary {source_existing.name} {source_existing.url}") + assert ( + tester.io.fetch_output().strip() + == f"Source with name {source_existing.name} already exists. Skipping addition." + ) + + poetry_with_source.pyproject.reload() + sources = poetry_with_source.get_sources() + + assert len(sources) == 1 + assert sources[0] == source_existing + + +def test_source_add_existing_updating( + tester: CommandTester, source_existing: Source, poetry_with_source: Poetry +): + tester.execute(f"--priority=default {source_existing.name} {source_existing.url}") assert ( tester.io.fetch_output().strip() == f"Source with name {source_existing.name} already exists. Updating." @@ -101,4 +232,28 @@ def test_source_add_existing( assert len(sources) == 1 assert sources[0] != source_existing - assert sources[0] == dataclasses.replace(source_existing, default=True) + expected_source = Source( + name=source_existing.name, url=source_existing.url, priority=Priority.DEFAULT + ) + assert sources[0] == expected_source + + +def test_source_add_existing_fails_due_to_other_default( + tester: CommandTester, + source_existing: Source, + source_default: Source, + poetry_with_source: Poetry, +): + tester.execute(f"--priority=default {source_default.name} {source_default.url}") + tester.io.fetch_output() + + tester.execute(f"--priority=default {source_existing.name} {source_existing.url}") + + assert ( + tester.io.fetch_error().strip() + == f"Source with name {source_default.name} is already set to" + " default. Only one default source can be configured at a" + " time." + ) + assert tester.io.fetch_output().strip() == "" + assert tester.status_code == 1 diff --git a/tests/console/commands/source/test_remove.py b/tests/console/commands/source/test_remove.py index 914eb3e03b3..49d881328a8 100644 --- a/tests/console/commands/source/test_remove.py +++ b/tests/console/commands/source/test_remove.py @@ -28,7 +28,7 @@ def test_source_remove_simple( source_existing: Source, source_one: Source, source_two: Source, -): +) -> None: tester.execute(f"{source_existing.name}") assert ( tester.io.fetch_output().strip() @@ -42,7 +42,7 @@ def test_source_remove_simple( assert tester.status_code == 0 -def test_source_remove_error(tester: CommandTester): +def test_source_remove_error(tester: CommandTester) -> None: 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 index 8b975cf4b92..d3c94682650 100644 --- a/tests/console/commands/source/test_show.py +++ b/tests/console/commands/source/test_show.py @@ -22,24 +22,38 @@ def tester( return command_tester_factory("source show", poetry=poetry_with_source) -def test_source_show_simple(tester: CommandTester): +@pytest.fixture +def tester_no_sources( + command_tester_factory: CommandTesterFactory, + poetry_without_source: Poetry, +) -> CommandTester: + return command_tester_factory("source show", poetry=poetry_without_source) + + +@pytest.fixture +def tester_all_types( + command_tester_factory: CommandTesterFactory, + poetry_with_source: Poetry, + add_all_source_types: None, +) -> CommandTester: + return command_tester_factory("source show", poetry=poetry_with_source) + + +def test_source_show_simple(tester: CommandTester) -> None: 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 +name : existing +url : https://existing.com +priority : primary + +name : one +url : https://one.com +priority : primary + +name : two +url : https://two.com +priority : primary """.splitlines() assert [ line.strip() for line in tester.io.fetch_output().strip().splitlines() @@ -47,14 +61,13 @@ def test_source_show_simple(tester: CommandTester): assert tester.status_code == 0 -def test_source_show_one(tester: CommandTester, source_one: Source): +def test_source_show_one(tester: CommandTester, source_one: Source) -> None: tester.execute(f"{source_one.name}") expected = """\ -name : one -url : https://one.com -default : no -secondary : no +name : one +url : https://one.com +priority : primary """.splitlines() assert [ line.strip() for line in tester.io.fetch_output().strip().splitlines() @@ -62,19 +75,19 @@ def test_source_show_one(tester: CommandTester, source_one: Source): assert tester.status_code == 0 -def test_source_show_two(tester: CommandTester, source_one: Source, source_two: Source): +def test_source_show_two( + tester: CommandTester, source_one: Source, source_two: Source +) -> None: 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 +name : one +url : https://one.com +priority : primary + +name : two +url : https://two.com +priority : primary """.splitlines() assert [ line.strip() for line in tester.io.fetch_output().strip().splitlines() @@ -82,7 +95,42 @@ def test_source_show_two(tester: CommandTester, source_one: Source, source_two: assert tester.status_code == 0 -def test_source_show_error(tester: CommandTester): +@pytest.mark.parametrize( + "source_str", + ( + "source_primary", + "source_default", + "source_secondary", + "source_explicit", + ), +) +def test_source_show_given_priority( + tester_all_types: CommandTester, source_str: Source, request: pytest.FixtureRequest +) -> None: + source = request.getfixturevalue(source_str) + tester_all_types.execute(f"{source.name}") + + expected = f"""\ +name : {source.name} +url : {source.url} +priority : {source.name} +""".splitlines() + assert [ + line.strip() for line in tester_all_types.io.fetch_output().strip().splitlines() + ] == expected + assert tester_all_types.status_code == 0 + + +def test_source_show_no_sources(tester_no_sources: CommandTester) -> None: + tester_no_sources.execute("error") + assert ( + tester_no_sources.io.fetch_output().strip() + == "No sources configured for this project." + ) + assert tester_no_sources.status_code == 0 + + +def test_source_show_error(tester: CommandTester) -> None: 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_about.py b/tests/console/commands/test_about.py index 8f6902c35b3..4368acfd094 100644 --- a/tests/console/commands/test_about.py +++ b/tests/console/commands/test_about.py @@ -16,7 +16,7 @@ def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("about") -def test_about(tester: CommandTester): +def test_about(tester: CommandTester) -> None: from poetry.utils._compat import metadata tester.execute() diff --git a/tests/console/commands/test_add.py b/tests/console/commands/test_add.py index ff34c4f8b20..8daf01ea9d9 100644 --- a/tests/console/commands/test_add.py +++ b/tests/console/commands/test_add.py @@ -10,12 +10,15 @@ from poetry.core.constraints.version import Version from poetry.core.packages.package import Package +from poetry.puzzle.exceptions import SolverProblemError from poetry.repositories.legacy_repository import LegacyRepository from tests.helpers import get_dependency from tests.helpers import get_package if TYPE_CHECKING: + from typing import Any + from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture @@ -43,6 +46,20 @@ def poetry_with_up_to_date_lockfile( ) +@pytest.fixture +def poetry_with_path_dependency( + project_factory: ProjectFactory, fixture_dir: FixtureDirGetter +) -> Poetry: + source = fixture_dir("with_path_dependency") + + poetry = project_factory( + name="foobar", + source=source, + use_test_locker=False, + ) + return poetry + + @pytest.fixture() def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("add") @@ -50,14 +67,14 @@ def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: @pytest.fixture() def old_tester(tester: CommandTester) -> CommandTester: - tester.command.installer.use_executor(False) + tester.command.installer._use_executor = False return tester def test_add_no_constraint( app: PoetryTestApplication, repo: TestRepository, tester: CommandTester -): +) -> None: repo.add_package(get_package("cachy", "0.1.0")) repo.add_package(get_package("cachy", "0.2.0")) @@ -69,11 +86,11 @@ def test_add_no_constraint( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing cachy (0.2.0) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -87,7 +104,7 @@ def test_add_no_constraint( def test_add_replace_by_constraint( app: PoetryTestApplication, repo: TestRepository, tester: CommandTester -): +) -> None: repo.add_package(get_package("cachy", "0.1.0")) repo.add_package(get_package("cachy", "0.2.0")) @@ -99,11 +116,11 @@ def test_add_replace_by_constraint( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing cachy (0.2.0) + +Writing lock file """ assert tester.io.fetch_output() == expected assert tester.command.installer.executor.installations_count == 1 @@ -118,11 +135,11 @@ def test_add_replace_by_constraint( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing cachy (0.1.0) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -134,7 +151,7 @@ def test_add_replace_by_constraint( def test_add_no_constraint_editable_error( app: PoetryTestApplication, repo: TestRepository, tester: CommandTester -): +) -> None: content = app.poetry.file.read()["tool"]["poetry"] repo.add_package(get_package("cachy", "0.2.0")) @@ -155,7 +172,7 @@ def test_add_no_constraint_editable_error( def test_add_equal_constraint( app: PoetryTestApplication, repo: TestRepository, tester: CommandTester -): +) -> None: repo.add_package(get_package("cachy", "0.1.0")) repo.add_package(get_package("cachy", "0.2.0")) @@ -166,11 +183,11 @@ def test_add_equal_constraint( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing cachy (0.1.0) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -179,7 +196,7 @@ def test_add_equal_constraint( def test_add_greater_constraint( app: PoetryTestApplication, repo: TestRepository, tester: CommandTester -): +) -> None: repo.add_package(get_package("cachy", "0.1.0")) repo.add_package(get_package("cachy", "0.2.0")) @@ -190,11 +207,11 @@ def test_add_greater_constraint( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing cachy (0.2.0) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -207,7 +224,7 @@ def test_add_constraint_with_extras( repo: TestRepository, tester: CommandTester, extra_name: str, -): +) -> None: cachy1 = get_package("cachy", "0.1.0") cachy1.extras = {"msgpack": [get_dependency("msgpack-python")]} msgpack_dep = get_dependency("msgpack-python", ">=0.5 <0.6", optional=True) @@ -224,12 +241,12 @@ def test_add_constraint_with_extras( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing msgpack-python (0.5.3) • Installing cachy (0.1.0) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -238,7 +255,7 @@ def test_add_constraint_with_extras( def test_add_constraint_dependencies( app: PoetryTestApplication, repo: TestRepository, tester: CommandTester -): +) -> None: cachy2 = get_package("cachy", "0.2.0") msgpack_dep = get_dependency("msgpack-python", ">=0.5 <0.6") cachy2.add_dependency(msgpack_dep) @@ -254,12 +271,12 @@ def test_add_constraint_dependencies( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing msgpack-python (0.5.3) • Installing cachy (0.2.0) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -271,7 +288,7 @@ def test_add_git_constraint( repo: TestRepository, tester: CommandTester, tmp_venv: VirtualEnv, -): +) -> None: tester.command.set_env(tmp_venv) repo.add_package(get_package("pendulum", "1.4.4")) @@ -284,12 +301,12 @@ def test_add_git_constraint( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing pendulum (1.4.4) • Installing demo (0.1.2 9cf87a2) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -308,7 +325,7 @@ def test_add_git_constraint_with_poetry( repo: TestRepository, tester: CommandTester, tmp_venv: VirtualEnv, -): +) -> None: tester.command.set_env(tmp_venv) repo.add_package(get_package("pendulum", "1.4.4")) @@ -320,12 +337,12 @@ def test_add_git_constraint_with_poetry( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing pendulum (1.4.4) • Installing demo (0.1.2 9cf87a2) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -339,7 +356,7 @@ def test_add_git_constraint_with_extras( tester: CommandTester, tmp_venv: VirtualEnv, extra_name: str, -): +) -> None: tester.command.set_env(tmp_venv) repo.add_package(get_package("pendulum", "1.4.4")) @@ -353,14 +370,14 @@ def test_add_git_constraint_with_extras( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 4 installs, 0 updates, 0 removals • Installing cleo (0.6.5) • Installing pendulum (1.4.4) • Installing tomlkit (0.5.5) • Installing demo (0.1.2 9cf87a2) + +Writing lock file """ assert tester.io.fetch_output().strip() == expected.strip() @@ -392,18 +409,18 @@ def test_add_git_constraint_with_subdirectory( repo: TestRepository, tester: CommandTester, env: MockEnv, -): +) -> None: tester.execute(url) expected = """\ Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing two (2.0.0 9cf87a2) + +Writing lock file """ assert tester.io.fetch_output().strip() == expected.strip() assert tester.command.installer.executor.installations_count == 1 @@ -429,7 +446,7 @@ def test_add_git_ssh_constraint( repo: TestRepository, tester: CommandTester, tmp_venv: VirtualEnv, -): +) -> None: tester.command.set_env(tmp_venv) repo.add_package(get_package("pendulum", "1.4.4")) @@ -443,12 +460,12 @@ def test_add_git_ssh_constraint( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing pendulum (1.4.4) • Installing demo (0.1.2 9cf87a2) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -478,7 +495,7 @@ def test_add_directory_constraint( app: PoetryTestApplication, repo: TestRepository, tester: CommandTester, -): +) -> None: repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cleo", "0.6.5")) @@ -490,12 +507,12 @@ def test_add_directory_constraint( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing pendulum (1.4.4) • Installing demo (0.1.2 {app.poetry.file.parent.joinpath(path).resolve().as_posix()}) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -520,7 +537,7 @@ def test_add_directory_with_poetry( app: PoetryTestApplication, repo: TestRepository, tester: CommandTester, -): +) -> None: repo.add_package(get_package("pendulum", "1.4.4")) path = "../git/github.com/demo/pyproject-demo" @@ -531,12 +548,12 @@ def test_add_directory_with_poetry( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing pendulum (1.4.4) • Installing demo (0.1.2 {app.poetry.file.parent.joinpath(path).resolve().as_posix()}) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -552,7 +569,7 @@ def test_add_file_constraint_wheel( repo: TestRepository, tester: CommandTester, poetry: Poetry, -): +) -> None: repo.add_package(get_package("pendulum", "1.4.4")) path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" @@ -563,12 +580,12 @@ def test_add_file_constraint_wheel( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing pendulum (1.4.4) • Installing demo (0.1.0 {app.poetry.file.parent.joinpath(path).resolve().as_posix()}) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -588,7 +605,7 @@ def test_add_file_constraint_sdist( app: PoetryTestApplication, repo: TestRepository, tester: CommandTester, -): +) -> None: repo.add_package(get_package("pendulum", "1.4.4")) path = "../distributions/demo-0.1.0.tar.gz" @@ -599,12 +616,12 @@ def test_add_file_constraint_sdist( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing pendulum (1.4.4) • Installing demo (0.1.0 {app.poetry.file.parent.joinpath(path).resolve().as_posix()}) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -622,7 +639,7 @@ def test_add_constraint_with_extras_option( repo: TestRepository, tester: CommandTester, extra_name: str, -): +) -> None: cachy2 = get_package("cachy", "0.2.0") cachy2.extras = {"msgpack": [get_dependency("msgpack-python")]} msgpack_dep = get_dependency("msgpack-python", ">=0.5 <0.6", optional=True) @@ -639,12 +656,12 @@ def test_add_constraint_with_extras_option( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing msgpack-python (0.5.3) • Installing cachy (0.2.0) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -664,7 +681,7 @@ def test_add_url_constraint_wheel( repo: TestRepository, tester: CommandTester, mocker: MockerFixture, -): +) -> None: p = mocker.patch("pathlib.Path.cwd") p.return_value = Path(__file__) / ".." @@ -679,13 +696,13 @@ def test_add_url_constraint_wheel( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing pendulum (1.4.4) • Installing demo\ (0.1.0 https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -706,7 +723,7 @@ def test_add_url_constraint_wheel_with_extras( tester: CommandTester, extra_name: str, mocker: MockerFixture, -): +) -> None: repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cleo", "0.6.5")) repo.add_package(get_package("tomlkit", "0.5.5")) @@ -721,8 +738,6 @@ def test_add_url_constraint_wheel_with_extras( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 4 installs, 0 updates, 0 removals • Installing cleo (0.6.5) @@ -730,6 +745,8 @@ def test_add_url_constraint_wheel_with_extras( • Installing tomlkit (0.5.5) • Installing demo\ (0.1.0 https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl) + +Writing lock file """ # Order might be different, split into lines and compare the overall output. expected = set(expected.splitlines()) @@ -750,7 +767,7 @@ def test_add_url_constraint_wheel_with_extras( def test_add_constraint_with_python( app: PoetryTestApplication, repo: TestRepository, tester: CommandTester -): +) -> None: cachy2 = get_package("cachy", "0.2.0") repo.add_package(get_package("cachy", "0.1.0")) @@ -763,11 +780,11 @@ def test_add_constraint_with_python( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing cachy (0.2.0) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -784,9 +801,10 @@ def test_add_constraint_with_platform( repo: TestRepository, tester: CommandTester, env: MockEnv, -): +) -> None: platform = sys.platform env._platform = platform + env._marker_env = None cachy2 = get_package("cachy", "0.2.0") @@ -800,11 +818,11 @@ def test_add_constraint_with_platform( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing cachy (0.2.0) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -824,7 +842,7 @@ def test_add_constraint_with_source( poetry: Poetry, tester: CommandTester, mocker: MockerFixture, -): +) -> None: repo = LegacyRepository(name="my-index", url="https://my-index.fake") repo.add_package(get_package("cachy", "0.2.0")) mocker.patch.object( @@ -851,11 +869,11 @@ def test_add_constraint_with_source( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing cachy (0.2.0) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -872,7 +890,7 @@ def test_add_constraint_with_source( def test_add_constraint_with_source_that_does_not_exist( app: PoetryTestApplication, tester: CommandTester -): +) -> None: with pytest.raises(IndexError) as e: tester.execute("foo --source i-dont-exist") @@ -884,7 +902,7 @@ def test_add_constraint_not_found_with_source( poetry: Poetry, mocker: MockerFixture, tester: CommandTester, -): +) -> None: repo = LegacyRepository(name="my-index", url="https://my-index.fake") mocker.patch.object(repo, "find_packages", return_value=[]) @@ -901,7 +919,7 @@ def test_add_constraint_not_found_with_source( def test_add_to_section_that_does_not_exist_yet( app: PoetryTestApplication, repo: TestRepository, tester: CommandTester -): +) -> None: repo.add_package(get_package("cachy", "0.1.0")) repo.add_package(get_package("cachy", "0.2.0")) @@ -913,11 +931,11 @@ def test_add_to_section_that_does_not_exist_yet( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing cachy (0.2.0) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -944,7 +962,7 @@ def test_add_to_section_that_does_not_exist_yet( def test_add_to_dev_section_deprecated( app: PoetryTestApplication, repo: TestRepository, tester: CommandTester -): +) -> None: repo.add_package(get_package("cachy", "0.1.0")) repo.add_package(get_package("cachy", "0.2.0")) @@ -960,11 +978,11 @@ def test_add_to_dev_section_deprecated( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing cachy (0.2.0) + +Writing lock file """ assert tester.io.fetch_error() == warning @@ -979,7 +997,7 @@ def test_add_to_dev_section_deprecated( def test_add_should_not_select_prereleases( app: PoetryTestApplication, repo: TestRepository, tester: CommandTester -): +) -> None: repo.add_package(get_package("pyyaml", "3.13")) repo.add_package(get_package("pyyaml", "4.2b2")) @@ -991,11 +1009,11 @@ def test_add_should_not_select_prereleases( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing pyyaml (3.13) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -1009,7 +1027,7 @@ def test_add_should_not_select_prereleases( def test_add_should_skip_when_adding_existing_package_with_no_constraint( app: PoetryTestApplication, repo: TestRepository, tester: CommandTester -): +) -> None: content = app.poetry.file.read() content["tool"]["poetry"]["dependencies"]["foo"] = "^1.0" app.poetry.file.write(content) @@ -1033,7 +1051,7 @@ def test_add_should_skip_when_adding_existing_package_with_no_constraint( def test_add_should_skip_when_adding_canonicalized_existing_package_with_no_constraint( app: PoetryTestApplication, repo: TestRepository, tester: CommandTester -): +) -> None: content = app.poetry.file.read() content["tool"]["poetry"]["dependencies"]["foo-bar"] = "^1.0" app.poetry.file.write(content) @@ -1055,9 +1073,47 @@ def test_add_should_skip_when_adding_canonicalized_existing_package_with_no_cons assert expected in tester.io.fetch_output() +def test_add_latest_should_not_create_duplicate_keys( + project_factory: ProjectFactory, + repo: TestRepository, + command_tester_factory: CommandTesterFactory, +) -> None: + pyproject_content = """\ + [tool.poetry] + name = "simple-project" + version = "1.2.3" + description = "Some description." + authors = [ + "Python Poetry " + ] + license = "MIT" + readme = "README.md" + + [tool.poetry.dependencies] + python = "^3.6" + Foo = "^0.6" + """ + + poetry = project_factory(name="simple-project", pyproject_content=pyproject_content) + content = poetry.file.read() + + assert "Foo" in content["tool"]["poetry"]["dependencies"] + assert content["tool"]["poetry"]["dependencies"]["Foo"] == "^0.6" + assert "foo" not in content["tool"]["poetry"]["dependencies"] + + tester = command_tester_factory("add", poetry=poetry) + repo.add_package(get_package("foo", "1.1.2")) + tester.execute("foo@latest") + + updated_content = poetry.file.read() + assert "Foo" in updated_content["tool"]["poetry"]["dependencies"] + assert updated_content["tool"]["poetry"]["dependencies"]["Foo"] == "^1.1.2" + assert "foo" not in updated_content["tool"]["poetry"]["dependencies"] + + def test_add_should_work_when_adding_existing_package_with_latest_constraint( app: PoetryTestApplication, repo: TestRepository, tester: CommandTester -): +) -> None: content = app.poetry.file.read() content["tool"]["poetry"]["dependencies"]["foo"] = "^1.0" app.poetry.file.write(content) @@ -1072,11 +1128,11 @@ def test_add_should_work_when_adding_existing_package_with_latest_constraint( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing foo (1.1.2) + +Writing lock file """ assert expected in tester.io.fetch_output() @@ -1089,7 +1145,7 @@ def test_add_should_work_when_adding_existing_package_with_latest_constraint( def test_add_chooses_prerelease_if_only_prereleases_are_available( app: PoetryTestApplication, repo: TestRepository, tester: CommandTester -): +) -> None: repo.add_package(get_package("foo", "1.2.3b0")) repo.add_package(get_package("foo", "1.2.3b1")) @@ -1101,18 +1157,18 @@ def test_add_chooses_prerelease_if_only_prereleases_are_available( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing foo (1.2.3b1) + +Writing lock file """ assert expected in tester.io.fetch_output() def test_add_prefers_stable_releases( app: PoetryTestApplication, repo: TestRepository, tester: CommandTester -): +) -> None: repo.add_package(get_package("foo", "1.2.3")) repo.add_package(get_package("foo", "1.2.4b1")) @@ -1124,11 +1180,11 @@ def test_add_prefers_stable_releases( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing foo (1.2.3) + +Writing lock file """ assert expected in tester.io.fetch_output() @@ -1136,7 +1192,7 @@ def test_add_prefers_stable_releases( def test_add_with_lock( app: PoetryTestApplication, repo: TestRepository, tester: CommandTester -): +) -> None: content_hash = app.poetry.locker._get_content_hash() repo.add_package(get_package("cachy", "0.2.0")) @@ -1160,7 +1216,7 @@ def test_add_no_constraint_old_installer( repo: TestRepository, installer: NoopInstaller, old_tester: CommandTester, -): +) -> None: repo.add_package(get_package("cachy", "0.1.0")) repo.add_package(get_package("cachy", "0.2.0")) @@ -1172,11 +1228,11 @@ def test_add_no_constraint_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.2.0) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1194,7 +1250,7 @@ def test_add_equal_constraint_old_installer( repo: TestRepository, installer: NoopInstaller, old_tester: CommandTester, -): +) -> None: repo.add_package(get_package("cachy", "0.1.0")) repo.add_package(get_package("cachy", "0.2.0")) @@ -1205,11 +1261,11 @@ def test_add_equal_constraint_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.1.0) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1222,7 +1278,7 @@ def test_add_greater_constraint_old_installer( repo: TestRepository, installer: NoopInstaller, old_tester: CommandTester, -): +) -> None: repo.add_package(get_package("cachy", "0.1.0")) repo.add_package(get_package("cachy", "0.2.0")) @@ -1233,11 +1289,11 @@ def test_add_greater_constraint_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.2.0) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1252,7 +1308,7 @@ def test_add_constraint_with_extras_old_installer( installer: NoopInstaller, old_tester: CommandTester, extra_name: str, -): +) -> None: cachy1 = get_package("cachy", "0.1.0") cachy1.extras = {"msgpack": [get_dependency("msgpack-python")]} msgpack_dep = get_dependency("msgpack-python", ">=0.5 <0.6", optional=True) @@ -1269,12 +1325,12 @@ def test_add_constraint_with_extras_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing msgpack-python (0.5.3) - Installing cachy (0.1.0) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1287,7 +1343,7 @@ def test_add_constraint_dependencies_old_installer( repo: TestRepository, installer: NoopInstaller, old_tester: CommandTester, -): +) -> None: cachy2 = get_package("cachy", "0.2.0") msgpack_dep = get_dependency("msgpack-python", ">=0.5 <0.6") cachy2.add_dependency(msgpack_dep) @@ -1303,12 +1359,12 @@ def test_add_constraint_dependencies_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing msgpack-python (0.5.3) - Installing cachy (0.2.0) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1321,7 +1377,7 @@ def test_add_git_constraint_old_installer( repo: TestRepository, installer: NoopInstaller, old_tester: CommandTester, -): +) -> None: repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cleo", "0.6.5")) @@ -1332,12 +1388,12 @@ def test_add_git_constraint_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.2 9cf87a2) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1357,7 +1413,7 @@ def test_add_git_constraint_with_poetry_old_installer( repo: TestRepository, installer: NoopInstaller, old_tester: CommandTester, -): +) -> None: repo.add_package(get_package("pendulum", "1.4.4")) old_tester.execute("git+https://github.com/demo/pyproject-demo.git") @@ -1367,12 +1423,12 @@ def test_add_git_constraint_with_poetry_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.2 9cf87a2) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1387,7 +1443,7 @@ def test_add_git_constraint_with_extras_old_installer( installer: NoopInstaller, old_tester: CommandTester, extra_name: str, -): +) -> None: repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cleo", "0.6.5")) repo.add_package(get_package("tomlkit", "0.5.5")) @@ -1399,14 +1455,14 @@ def test_add_git_constraint_with_extras_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 4 installs, 0 updates, 0 removals - Installing cleo (0.6.5) - Installing pendulum (1.4.4) - Installing tomlkit (0.5.5) - Installing demo (0.1.2 9cf87a2) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1427,7 +1483,7 @@ def test_add_git_ssh_constraint_old_installer( repo: TestRepository, installer: NoopInstaller, old_tester: CommandTester, -): +) -> None: repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cleo", "0.6.5")) @@ -1438,12 +1494,12 @@ def test_add_git_ssh_constraint_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.2 9cf87a2) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1468,7 +1524,7 @@ def test_add_directory_constraint_old_installer( repo: TestRepository, installer: NoopInstaller, old_tester: CommandTester, -): +) -> None: repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cleo", "0.6.5")) @@ -1480,12 +1536,12 @@ def test_add_directory_constraint_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.2 {app.poetry.file.parent.joinpath(path).resolve().as_posix()}) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1507,7 +1563,7 @@ def test_add_directory_with_poetry_old_installer( repo: TestRepository, installer: NoopInstaller, old_tester: CommandTester, -): +) -> None: repo.add_package(get_package("pendulum", "1.4.4")) path = "../git/github.com/demo/pyproject-demo" @@ -1518,12 +1574,12 @@ def test_add_directory_with_poetry_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.2 {app.poetry.file.parent.joinpath(path).resolve().as_posix()}) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1540,7 +1596,7 @@ def test_add_file_constraint_wheel_old_installer( repo: TestRepository, installer: NoopInstaller, old_tester: CommandTester, -): +) -> None: repo.add_package(get_package("pendulum", "1.4.4")) path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" @@ -1551,12 +1607,12 @@ def test_add_file_constraint_wheel_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.0 {app.poetry.file.parent.joinpath(path).resolve().as_posix()}) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1578,7 +1634,7 @@ def test_add_file_constraint_sdist_old_installer( repo: TestRepository, installer: NoopInstaller, old_tester: CommandTester, -): +) -> None: repo.add_package(get_package("pendulum", "1.4.4")) path = "../distributions/demo-0.1.0.tar.gz" @@ -1589,12 +1645,12 @@ def test_add_file_constraint_sdist_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.0 {app.poetry.file.parent.joinpath(path).resolve().as_posix()}) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1614,7 +1670,7 @@ def test_add_constraint_with_extras_option_old_installer( installer: NoopInstaller, old_tester: CommandTester, extra_name: str, -): +) -> None: cachy2 = get_package("cachy", "0.2.0") cachy2.extras = {"msgpack": [get_dependency("msgpack-python")]} msgpack_dep = get_dependency("msgpack-python", ">=0.5 <0.6", optional=True) @@ -1631,12 +1687,12 @@ def test_add_constraint_with_extras_option_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing msgpack-python (0.5.3) - Installing cachy (0.2.0) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1658,7 +1714,7 @@ def test_add_url_constraint_wheel_old_installer( installer: NoopInstaller, mocker: MockerFixture, old_tester: CommandTester, -): +) -> None: p = mocker.patch("pathlib.Path.cwd") p.return_value = Path(__file__) / ".." @@ -1673,13 +1729,13 @@ def test_add_url_constraint_wheel_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo\ (0.1.0 https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1701,7 +1757,7 @@ def test_add_url_constraint_wheel_with_extras_old_installer( installer: NoopInstaller, old_tester: CommandTester, extra_name: str, -): +) -> None: repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cleo", "0.6.5")) repo.add_package(get_package("tomlkit", "0.5.5")) @@ -1716,8 +1772,6 @@ def test_add_url_constraint_wheel_with_extras_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 4 installs, 0 updates, 0 removals - Installing cleo (0.6.5) @@ -1725,6 +1779,8 @@ def test_add_url_constraint_wheel_with_extras_old_installer( - Installing tomlkit (0.5.5) - Installing demo\ (0.1.0 https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1747,7 +1803,7 @@ def test_add_constraint_with_python_old_installer( repo: TestRepository, installer: NoopInstaller, old_tester: CommandTester, -): +) -> None: cachy2 = get_package("cachy", "0.2.0") repo.add_package(get_package("cachy", "0.1.0")) @@ -1760,11 +1816,11 @@ def test_add_constraint_with_python_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.2.0) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1783,9 +1839,10 @@ def test_add_constraint_with_platform_old_installer( installer: NoopInstaller, env: MockEnv, old_tester: CommandTester, -): +) -> None: platform = sys.platform env._platform = platform + env._marker_env = None cachy2 = get_package("cachy", "0.2.0") @@ -1799,11 +1856,11 @@ def test_add_constraint_with_platform_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.2.0) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1825,7 +1882,7 @@ def test_add_constraint_with_source_old_installer( installer: NoopInstaller, old_tester: CommandTester, mocker: MockerFixture, -): +) -> None: repo = LegacyRepository(name="my-index", url="https://my-index.fake") repo.add_package(get_package("cachy", "0.2.0")) mocker.patch.object( @@ -1852,11 +1909,11 @@ def test_add_constraint_with_source_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.2.0) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1874,7 +1931,7 @@ def test_add_constraint_with_source_old_installer( def test_add_constraint_with_source_that_does_not_exist_old_installer( app: PoetryTestApplication, old_tester: CommandTester -): +) -> None: with pytest.raises(IndexError) as e: old_tester.execute("foo --source i-dont-exist") @@ -1886,7 +1943,7 @@ def test_add_constraint_not_found_with_source_old_installer( poetry: Poetry, mocker: MockerFixture, old_tester: CommandTester, -): +) -> None: repo = LegacyRepository(name="my-index", url="https://my-index.fake") mocker.patch.object(repo, "find_packages", return_value=[]) @@ -1906,7 +1963,7 @@ def test_add_to_section_that_does_no_exist_yet_old_installer( repo: TestRepository, installer: NoopInstaller, old_tester: CommandTester, -): +) -> None: repo.add_package(get_package("cachy", "0.1.0")) repo.add_package(get_package("cachy", "0.2.0")) @@ -1918,11 +1975,11 @@ def test_add_to_section_that_does_no_exist_yet_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.2.0) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1940,7 +1997,7 @@ def test_add_should_not_select_prereleases_old_installer( repo: TestRepository, installer: NoopInstaller, old_tester: CommandTester, -): +) -> None: repo.add_package(get_package("pyyaml", "3.13")) repo.add_package(get_package("pyyaml", "4.2b2")) @@ -1952,11 +2009,11 @@ def test_add_should_not_select_prereleases_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing pyyaml (3.13) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1974,7 +2031,7 @@ def test_add_should_skip_when_adding_existing_package_with_no_constraint_old_ins repo: TestRepository, installer: NoopInstaller, old_tester: CommandTester, -): +) -> None: content = app.poetry.file.read() content["tool"]["poetry"]["dependencies"]["foo"] = "^1.0" app.poetry.file.write(content) @@ -2002,7 +2059,7 @@ def test_add_should_work_when_adding_existing_package_with_latest_constraint_old repo: TestRepository, installer: NoopInstaller, old_tester: CommandTester, -): +) -> None: content = app.poetry.file.read() content["tool"]["poetry"]["dependencies"]["foo"] = "^1.0" app.poetry.file.write(content) @@ -2017,11 +2074,11 @@ def test_add_should_work_when_adding_existing_package_with_latest_constraint_old Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing foo (1.1.2) + +Writing lock file """ assert expected in old_tester.io.fetch_output() @@ -2037,7 +2094,7 @@ def test_add_chooses_prerelease_if_only_prereleases_are_available_old_installer( repo: TestRepository, installer: NoopInstaller, old_tester: CommandTester, -): +) -> None: repo.add_package(get_package("foo", "1.2.3b0")) repo.add_package(get_package("foo", "1.2.3b1")) @@ -2049,11 +2106,11 @@ def test_add_chooses_prerelease_if_only_prereleases_are_available_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing foo (1.2.3b1) + +Writing lock file """ assert expected in old_tester.io.fetch_output() @@ -2063,7 +2120,7 @@ def test_add_preferes_stable_releases_old_installer( repo: TestRepository, installer: NoopInstaller, old_tester: CommandTester, -): +) -> None: repo.add_package(get_package("foo", "1.2.3")) repo.add_package(get_package("foo", "1.2.4b1")) @@ -2075,11 +2132,11 @@ def test_add_preferes_stable_releases_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing foo (1.2.3) + +Writing lock file """ assert expected in old_tester.io.fetch_output() @@ -2090,7 +2147,7 @@ def test_add_with_lock_old_installer( repo: TestRepository, installer: NoopInstaller, old_tester: CommandTester, -): +) -> None: repo.add_package(get_package("cachy", "0.2.0")) old_tester.execute("cachy --lock") @@ -2112,11 +2169,12 @@ def test_add_keyboard_interrupt_restore_content( repo: TestRepository, command_tester_factory: CommandTesterFactory, mocker: MockerFixture, -): +) -> None: tester = command_tester_factory("add", poetry=poetry_with_up_to_date_lockfile) mocker.patch( - "poetry.installation.installer.Installer.run", side_effect=KeyboardInterrupt() + "poetry.installation.installer.Installer._execute", + side_effect=KeyboardInterrupt(), ) original_pyproject_content = poetry_with_up_to_date_lockfile.file.read() original_lockfile_content = poetry_with_up_to_date_lockfile._locker.lock_data @@ -2144,7 +2202,7 @@ def test_add_with_dry_run_keep_files_intact( poetry_with_up_to_date_lockfile: Poetry, repo: TestRepository, command_tester_factory: CommandTesterFactory, -): +) -> None: tester = command_tester_factory("add", poetry=poetry_with_up_to_date_lockfile) original_pyproject_content = poetry_with_up_to_date_lockfile.file.read() @@ -2159,3 +2217,57 @@ def test_add_with_dry_run_keep_files_intact( assert ( poetry_with_up_to_date_lockfile._locker.lock_data == original_lockfile_content ) + + +def test_add_should_not_change_lock_file_when_dependency_installation_fail( + poetry_with_up_to_date_lockfile: Poetry, + repo: TestRepository, + command_tester_factory: CommandTesterFactory, + mocker: MockerFixture, +) -> None: + tester = command_tester_factory("add", poetry=poetry_with_up_to_date_lockfile) + + repo.add_package(get_package("docker", "4.3.1")) + repo.add_package(get_package("cachy", "0.2.0")) + + original_pyproject_content = poetry_with_up_to_date_lockfile.file.read() + original_lockfile_content = poetry_with_up_to_date_lockfile.locker.lock_data + + def error(_: Any) -> int: + tester.io.write("\n BuildError\n\n") + return 1 + + mocker.patch("poetry.installation.installer.Installer._execute", side_effect=error) + tester.execute("cachy") + + expected = """\ +Using version ^0.2.0 for cachy + +Updating dependencies +Resolving dependencies... + + BuildError + +""" + + assert poetry_with_up_to_date_lockfile.file.read() == original_pyproject_content + assert poetry_with_up_to_date_lockfile.locker.lock_data == original_lockfile_content + assert tester.io.fetch_output() == expected + + +def test_add_with_path_dependency_no_loopiness( + poetry_with_path_dependency: Poetry, + repo: TestRepository, + command_tester_factory: CommandTesterFactory, +) -> None: + """https://github.com/python-poetry/poetry/issues/7398""" + tester = command_tester_factory("add", poetry=poetry_with_path_dependency) + + requests_old = get_package("requests", "2.25.1") + requests_new = get_package("requests", "2.28.2") + + repo.add_package(requests_old) + repo.add_package(requests_new) + + with pytest.raises(SolverProblemError): + tester.execute("requests") diff --git a/tests/console/commands/test_build.py b/tests/console/commands/test_build.py new file mode 100644 index 00000000000..ed9f3a3c5fa --- /dev/null +++ b/tests/console/commands/test_build.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import shutil +import tarfile + +from typing import TYPE_CHECKING + +from poetry.factory import Factory + + +if TYPE_CHECKING: + from pathlib import Path + + from poetry.utils.env import VirtualEnv + from tests.types import CommandTesterFactory + from tests.types import FixtureDirGetter + + +def test_build_with_multiple_readme_files( + fixture_dir: FixtureDirGetter, + tmp_path: Path, + tmp_venv: VirtualEnv, + command_tester_factory: CommandTesterFactory, +) -> None: + source_dir = fixture_dir("with_multiple_readme_files") + target_dir = tmp_path / "project" + shutil.copytree(str(source_dir), str(target_dir)) + + poetry = Factory().create_poetry(target_dir) + tester = command_tester_factory("build", poetry, environment=tmp_venv) + + tester.execute() + + build_dir = target_dir / "dist" + assert build_dir.exists() + + sdist_file = build_dir / "my_package-0.1.tar.gz" + assert sdist_file.exists() + assert sdist_file.stat().st_size > 0 + + (wheel_file,) = build_dir.glob("my_package-0.1-*.whl") + assert wheel_file.exists() + assert wheel_file.stat().st_size > 0 + + with tarfile.open(sdist_file) as tf: + sdist_content = tf.getnames() + + assert "my_package-0.1/README-1.rst" in sdist_content + assert "my_package-0.1/README-2.rst" in sdist_content diff --git a/tests/console/commands/test_check.py b/tests/console/commands/test_check.py index 1c21a80f3e5..771f95813dc 100644 --- a/tests/console/commands/test_check.py +++ b/tests/console/commands/test_check.py @@ -1,6 +1,5 @@ from __future__ import annotations -from pathlib import Path from typing import TYPE_CHECKING import pytest @@ -11,6 +10,7 @@ from pytest_mock import MockerFixture from tests.types import CommandTesterFactory + from tests.types import FixtureDirGetter @pytest.fixture() @@ -18,7 +18,7 @@ def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("check") -def test_check_valid(tester: CommandTester): +def test_check_valid(tester: CommandTester) -> None: tester.execute() expected = """\ @@ -28,13 +28,15 @@ def test_check_valid(tester: CommandTester): assert tester.io.fetch_output() == expected -def test_check_invalid(mocker: MockerFixture, tester: CommandTester): +def test_check_invalid( + mocker: MockerFixture, tester: CommandTester, fixture_dir: FixtureDirGetter +) -> None: + from poetry.toml import TOMLFile + mocker.patch( - "poetry.factory.Factory.locate", - return_value=Path(__file__).parent.parent.parent - / "fixtures" - / "invalid_pyproject" - / "pyproject.toml", + "poetry.poetry.Poetry.file", + return_value=TOMLFile(fixture_dir("invalid_pyproject") / "pyproject.toml"), + new_callable=mocker.PropertyMock, ) tester.execute() @@ -54,3 +56,20 @@ def test_check_invalid(mocker: MockerFixture, tester: CommandTester): """ assert tester.io.fetch_error() == expected + + +def test_check_private( + mocker: MockerFixture, tester: CommandTester, fixture_dir: FixtureDirGetter +) -> None: + mocker.patch( + "poetry.factory.Factory.locate", + return_value=fixture_dir("private_pyproject") / "pyproject.toml", + ) + + tester.execute() + + expected = """\ +All set! +""" + + assert tester.io.fetch_output() == expected diff --git a/tests/console/commands/test_config.py b/tests/console/commands/test_config.py index 052b726c2fd..063200edbf8 100644 --- a/tests/console/commands/test_config.py +++ b/tests/console/commands/test_config.py @@ -24,6 +24,7 @@ from poetry.config.dict_config_source import DictConfigSource from tests.types import CommandTesterFactory from tests.types import FixtureDirGetter + from tests.types import ProjectFactory @pytest.fixture() @@ -33,7 +34,7 @@ def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: def test_show_config_with_local_config_file_empty( tester: CommandTester, mocker: MockerFixture -): +) -> None: mocker.patch( "poetry.factory.Factory.create_poetry", side_effect=PyProjectException("[tool.poetry] section not found"), @@ -45,7 +46,7 @@ def test_show_config_with_local_config_file_empty( def test_list_displays_default_value_if_not_set( tester: CommandTester, config: Config, config_cache_dir: Path -): +) -> None: tester.execute("--list") cache_dir = json.dumps(str(config_cache_dir)) @@ -54,6 +55,7 @@ def test_list_displays_default_value_if_not_set( experimental.new-installer = true experimental.system-git-client = false installer.max-workers = null +installer.modern-installation = true installer.no-binary = null installer.parallel = true virtualenvs.create = true @@ -72,7 +74,7 @@ def test_list_displays_default_value_if_not_set( def test_list_displays_set_get_setting( tester: CommandTester, config: Config, config_cache_dir: Path -): +) -> None: tester.execute("virtualenvs.create false") tester.execute("--list") @@ -83,6 +85,7 @@ def test_list_displays_set_get_setting( experimental.new-installer = true experimental.system-git-client = false installer.max-workers = null +installer.modern-installation = true installer.no-binary = null installer.parallel = true virtualenvs.create = false @@ -100,7 +103,7 @@ def test_list_displays_set_get_setting( assert tester.io.fetch_output() == expected -def test_display_single_setting(tester: CommandTester, config: Config): +def test_display_single_setting(tester: CommandTester, config: Config) -> None: tester.execute("virtualenvs.create") expected = """true @@ -111,7 +114,7 @@ def test_display_single_setting(tester: CommandTester, config: Config): def test_display_single_local_setting( command_tester_factory: CommandTesterFactory, fixture_dir: FixtureDirGetter -): +) -> None: tester = command_tester_factory( "config", poetry=Factory().create_poetry(fixture_dir("with_local_config")) ) @@ -125,7 +128,7 @@ def test_display_single_local_setting( def test_list_displays_set_get_local_setting( tester: CommandTester, config: Config, config_cache_dir: Path -): +) -> None: tester.execute("virtualenvs.create false --local") tester.execute("--list") @@ -136,6 +139,7 @@ def test_list_displays_set_get_local_setting( experimental.new-installer = true experimental.system-git-client = false installer.max-workers = null +installer.modern-installation = true installer.no-binary = null installer.parallel = true virtualenvs.create = false @@ -153,7 +157,47 @@ def test_list_displays_set_get_local_setting( assert tester.io.fetch_output() == expected -def test_set_pypi_token(tester: CommandTester, auth_config_source: DictConfigSource): +def test_list_must_not_display_sources_from_pyproject_toml( + project_factory: ProjectFactory, + fixture_dir: FixtureDirGetter, + command_tester_factory: CommandTesterFactory, + config: Config, + config_cache_dir: Path, +) -> None: + source = fixture_dir("with_non_default_source_implicit") + pyproject_content = (source / "pyproject.toml").read_text(encoding="utf-8") + poetry = project_factory("foo", pyproject_content=pyproject_content) + tester = command_tester_factory("config", poetry=poetry) + + tester.execute("--list") + + cache_dir = json.dumps(str(config_cache_dir)) + venv_path = json.dumps(os.path.join("{cache-dir}", "virtualenvs")) + expected = f"""cache-dir = {cache_dir} +experimental.new-installer = true +experimental.system-git-client = false +installer.max-workers = null +installer.modern-installation = true +installer.no-binary = null +installer.parallel = true +repositories.foo.url = "https://foo.bar/simple/" +virtualenvs.create = true +virtualenvs.in-project = null +virtualenvs.options.always-copy = false +virtualenvs.options.no-pip = false +virtualenvs.options.no-setuptools = false +virtualenvs.options.system-site-packages = false +virtualenvs.path = {venv_path} # {config_cache_dir / 'virtualenvs'} +virtualenvs.prefer-active-python = false +virtualenvs.prompt = "{{project_name}}-py{{python_version}}" +""" + + assert tester.io.fetch_output() == expected + + +def test_set_pypi_token( + tester: CommandTester, auth_config_source: DictConfigSource +) -> None: tester.execute("pypi-token.pypi mytoken") tester.execute("--list") @@ -164,7 +208,7 @@ def test_set_client_cert( tester: CommandTester, auth_config_source: DictConfigSource, mocker: MockerFixture, -): +) -> None: mocker.spy(ConfigSource, "__init__") tester.execute("certificates.foo.client-cert path/to/cert.pem") @@ -189,7 +233,7 @@ def test_set_cert( mocker: MockerFixture, value: str, result: str | bool, -): +) -> None: mocker.spy(ConfigSource, "__init__") tester.execute(f"certificates.foo.cert {value}") @@ -199,7 +243,7 @@ def test_set_cert( def test_config_installer_parallel( tester: CommandTester, command_tester_factory: CommandTesterFactory -): +) -> None: tester.execute("--local installer.parallel") assert tester.io.fetch_output().strip() == "true" diff --git a/tests/console/commands/test_init.py b/tests/console/commands/test_init.py index 5f176ba0eb9..b4bed706b99 100644 --- a/tests/console/commands/test_init.py +++ b/tests/console/commands/test_init.py @@ -2,10 +2,12 @@ import os import shutil +import subprocess import sys from pathlib import Path from typing import TYPE_CHECKING +from typing import Any import pytest @@ -13,8 +15,7 @@ from packaging.utils import canonicalize_name from poetry.console.commands.init import InitCommand -from poetry.repositories import Pool -from poetry.utils._compat import decode +from poetry.repositories import RepositoryPool from tests.helpers import PoetryTestApplication from tests.helpers import get_package @@ -26,6 +27,7 @@ from poetry.core.packages.package import Package from pytest_mock import MockerFixture + from poetry.config.config import Config from poetry.poetry import Poetry from tests.helpers import TestRepository from tests.types import FixtureDirGetter @@ -33,11 +35,10 @@ @pytest.fixture def source_dir(tmp_path: Path) -> Iterator[Path]: - cwd = os.getcwd() - + cwd = Path.cwd() try: - os.chdir(str(tmp_path)) - yield Path(tmp_path.as_posix()) + os.chdir(tmp_path) + yield tmp_path finally: os.chdir(cwd) @@ -46,7 +47,8 @@ def source_dir(tmp_path: Path) -> Iterator[Path]: def patches(mocker: MockerFixture, source_dir: Path, repo: TestRepository) -> None: mocker.patch("pathlib.Path.cwd", return_value=source_dir) mocker.patch( - "poetry.console.commands.init.InitCommand._get_pool", return_value=Pool([repo]) + "poetry.console.commands.init.InitCommand._get_pool", + return_value=RepositoryPool([repo]), ) @@ -93,7 +95,7 @@ def init_basic_toml() -> str: def test_basic_interactive( tester: CommandTester, init_basic_inputs: str, init_basic_toml: str -): +) -> None: tester.execute(inputs=init_basic_inputs) assert init_basic_toml in tester.io.fetch_output() @@ -104,7 +106,7 @@ def test_noninteractive( poetry: Poetry, repo: TestRepository, tmp_path: Path, -): +) -> None: command = app.find("init") command._pool = poetry.pool @@ -126,7 +128,9 @@ def test_noninteractive( assert 'pytest = "^3.6.0"' in toml_content -def test_interactive_with_dependencies(tester: CommandTester, repo: TestRepository): +def test_interactive_with_dependencies( + tester: CommandTester, repo: TestRepository +) -> None: repo.add_package(get_package("django-pendulum", "0.1.6-pre4")) repo.add_package(get_package("pendulum", "2.0.0")) repo.add_package(get_package("pytest", "3.6.0")) @@ -181,7 +185,7 @@ def test_interactive_with_dependencies(tester: CommandTester, repo: TestReposito # Regression test for https://github.com/python-poetry/poetry/issues/2355 def test_interactive_with_dependencies_and_no_selection( tester: CommandTester, repo: TestRepository -): +) -> None: repo.add_package(get_package("django-pendulum", "0.1.6-pre4")) repo.add_package(get_package("pendulum", "2.0.0")) repo.add_package(get_package("pytest", "3.6.0")) @@ -222,7 +226,7 @@ def test_interactive_with_dependencies_and_no_selection( assert expected in tester.io.fetch_output() -def test_empty_license(tester: CommandTester): +def test_empty_license(tester: CommandTester) -> None: inputs = [ "my-package", # Package name "1.2.3", # Version @@ -252,7 +256,9 @@ def test_empty_license(tester: CommandTester): assert expected in tester.io.fetch_output() -def test_interactive_with_git_dependencies(tester: CommandTester, repo: TestRepository): +def test_interactive_with_git_dependencies( + tester: CommandTester, repo: TestRepository +) -> None: repo.add_package(get_package("pendulum", "2.0.0")) repo.add_package(get_package("pytest", "3.6.0")) @@ -330,7 +336,7 @@ def test_generate_choice_list( tester: CommandTester, package_name: str, _generate_choice_list_packages: list[Package], -): +) -> None: init_command = tester.command packages = _generate_choice_list_packages @@ -343,7 +349,7 @@ def test_generate_choice_list( def test_interactive_with_git_dependencies_with_reference( tester: CommandTester, repo: TestRepository -): +) -> None: repo.add_package(get_package("pendulum", "2.0.0")) repo.add_package(get_package("pytest", "3.6.0")) @@ -389,7 +395,7 @@ def test_interactive_with_git_dependencies_with_reference( def test_interactive_with_git_dependencies_and_other_name( tester: CommandTester, repo: TestRepository -): +) -> None: repo.add_package(get_package("pendulum", "2.0.0")) repo.add_package(get_package("pytest", "3.6.0")) @@ -438,7 +444,7 @@ def test_interactive_with_directory_dependency( repo: TestRepository, source_dir: Path, fixture_dir: FixtureDirGetter, -): +) -> None: repo.add_package(get_package("pendulum", "2.0.0")) repo.add_package(get_package("pytest", "3.6.0")) @@ -489,7 +495,7 @@ def test_interactive_with_directory_dependency_and_other_name( repo: TestRepository, source_dir: Path, fixture_dir: FixtureDirGetter, -): +) -> None: repo.add_package(get_package("pendulum", "2.0.0")) repo.add_package(get_package("pytest", "3.6.0")) @@ -541,7 +547,7 @@ def test_interactive_with_file_dependency( repo: TestRepository, source_dir: Path, fixture_dir: FixtureDirGetter, -): +) -> None: repo.add_package(get_package("pendulum", "2.0.0")) repo.add_package(get_package("pytest", "3.6.0")) @@ -590,10 +596,7 @@ def test_interactive_with_file_dependency( def test_interactive_with_wrong_dependency_inputs( tester: CommandTester, repo: TestRepository -): - repo.add_package(get_package("pendulum", "2.0.0")) - repo.add_package(get_package("pytest", "3.6.0")) - +) -> None: inputs = [ "my-package", # Package name "1.2.3", # Version @@ -602,10 +605,8 @@ def test_interactive_with_wrong_dependency_inputs( "MIT", # License "^3.8", # Python "", # Interactive packages + "foo 1.19.2", "pendulum 2.0.0 foo", # Package name and constraint (invalid) - "pendulum 2.0.0", # Package name and constraint (invalid) - "pendulum 2.0.0", # Package name and constraint (invalid) - "pendulum 2.0.0", # Package name and constraint (invalid) "pendulum@^2.0.0", # Package name and constraint (valid) "", # End package selection "", # Interactive dev packages @@ -629,6 +630,7 @@ def test_interactive_with_wrong_dependency_inputs( [tool.poetry.dependencies] python = "^3.8" +foo = "1.19.2" pendulum = "^2.0.0" [tool.poetry.group.dev.dependencies] @@ -638,7 +640,7 @@ def test_interactive_with_wrong_dependency_inputs( assert expected in tester.io.fetch_output() -def test_python_option(tester: CommandTester): +def test_python_option(tester: CommandTester) -> None: inputs = [ "my-package", # Package name "1.2.3", # Version @@ -668,7 +670,7 @@ def test_python_option(tester: CommandTester): assert expected in tester.io.fetch_output() -def test_predefined_dependency(tester: CommandTester, repo: TestRepository): +def test_predefined_dependency(tester: CommandTester, repo: TestRepository) -> None: repo.add_package(get_package("pendulum", "2.0.0")) inputs = [ @@ -704,7 +706,7 @@ def test_predefined_dependency(tester: CommandTester, repo: TestRepository): def test_predefined_and_interactive_dependencies( tester: CommandTester, repo: TestRepository -): +) -> None: repo.add_package(get_package("pendulum", "2.0.0")) repo.add_package(get_package("pyramid", "1.10")) @@ -745,7 +747,7 @@ def test_predefined_and_interactive_dependencies( assert 'pyramid = "^1.10"' in output -def test_predefined_dev_dependency(tester: CommandTester, repo: TestRepository): +def test_predefined_dev_dependency(tester: CommandTester, repo: TestRepository) -> None: repo.add_package(get_package("pytest", "3.6.0")) inputs = [ @@ -784,7 +786,7 @@ def test_predefined_dev_dependency(tester: CommandTester, repo: TestRepository): def test_predefined_and_interactive_dev_dependencies( tester: CommandTester, repo: TestRepository -): +) -> None: repo.add_package(get_package("pytest", "3.6.0")) repo.add_package(get_package("pytest-requests", "0.2.0")) @@ -830,7 +832,7 @@ def test_predefined_and_interactive_dev_dependencies( assert 'pytest = "^3.6.0"' in output -def test_predefined_all_options(tester: CommandTester, repo: TestRepository): +def test_predefined_all_options(tester: CommandTester, repo: TestRepository) -> None: repo.add_package(get_package("pendulum", "2.0.0")) repo.add_package(get_package("pytest", "3.6.0")) @@ -843,13 +845,15 @@ def test_predefined_all_options(tester: CommandTester, repo: TestRepository): ] tester.execute( - "--name my-package " - "--description 'This is a description' " - "--author 'Foo Bar ' " - "--python '^3.8' " - "--license MIT " - "--dependency pendulum " - "--dev-dependency pytest", + ( + "--name my-package " + "--description 'This is a description' " + "--author 'Foo Bar ' " + "--python '^3.8' " + "--license MIT " + "--dependency pendulum " + "--dev-dependency pytest" + ), inputs="\n".join(inputs), ) @@ -875,7 +879,7 @@ def test_predefined_all_options(tester: CommandTester, repo: TestRepository): assert expected in output -def test_add_package_with_extras_and_whitespace(tester: CommandTester): +def test_add_package_with_extras_and_whitespace(tester: CommandTester) -> None: result = tester.command._parse_requirements(["databases[postgresql, sqlite]"]) assert result[0]["name"] == "databases" @@ -889,13 +893,13 @@ def test_init_existing_pyproject_simple( source_dir: Path, init_basic_inputs: str, init_basic_toml: str, -): +) -> None: pyproject_file = source_dir / "pyproject.toml" existing_section = """ [tool.black] line-length = 88 """ - pyproject_file.write_text(decode(existing_section)) + pyproject_file.write_text(existing_section) tester.execute(inputs=init_basic_inputs) assert f"{existing_section}\n{init_basic_toml}" in pyproject_file.read_text() @@ -907,7 +911,7 @@ def test_init_existing_pyproject_consistent_linesep( init_basic_inputs: str, init_basic_toml: str, linesep: str, -): +) -> None: pyproject_file = source_dir / "pyproject.toml" existing_section = """ [tool.black] @@ -929,21 +933,23 @@ def test_init_non_interactive_existing_pyproject_add_dependency( source_dir: Path, init_basic_inputs: str, repo: TestRepository, -): +) -> None: pyproject_file = source_dir / "pyproject.toml" existing_section = """ [tool.black] line-length = 88 """ - pyproject_file.write_text(decode(existing_section)) + pyproject_file.write_text(existing_section) repo.add_package(get_package("foo", "1.19.2")) tester.execute( - "--author 'Your Name ' " - "--name 'my-package' " - "--python '^3.6' " - "--dependency foo", + ( + "--author 'Your Name ' " + "--name 'my-package' " + "--python '^3.6' " + "--dependency foo" + ), interactive=False, ) @@ -965,14 +971,14 @@ def test_init_non_interactive_existing_pyproject_add_dependency( def test_init_existing_pyproject_with_build_system_fails( tester: CommandTester, source_dir: Path, init_basic_inputs: str -): +) -> None: pyproject_file = source_dir / "pyproject.toml" existing_section = """ [build-system] requires = ["setuptools >= 40.6.0", "wheel"] build-backend = "setuptools.build_meta" """ - pyproject_file.write_text(decode(existing_section)) + pyproject_file.write_text(existing_section) tester.execute(inputs=init_basic_inputs) assert ( tester.io.fetch_error().strip() @@ -995,14 +1001,14 @@ def test_init_existing_pyproject_with_build_system_fails( " foo 2.0 ", ], ) -def test__validate_package_valid(name: str | None): +def test_validate_package_valid(name: str | None) -> None: assert InitCommand._validate_package(name) == name @pytest.mark.parametrize( "name", ["foo bar 2.0", " foo bar 2.0 ", "foo bar foobar 2.0"] ) -def test__validate_package_invalid(name: str): +def test_validate_package_invalid(name: str) -> None: with pytest.raises(ValueError): assert InitCommand._validate_package(name) @@ -1021,7 +1027,7 @@ def test_package_include( tester: CommandTester, package_name: str, include: str | None, -): +) -> None: tester.execute( inputs="\n".join( ( @@ -1056,3 +1062,46 @@ def test_package_include( f'python = "^3.10"\n' ) assert expected in tester.io.fetch_output() + + +@pytest.mark.parametrize( + ["prefer_active", "python"], + [ + (True, "1.1"), + (False, f"{sys.version_info[0]}.{sys.version_info[1]}"), + ], +) +def test_respect_prefer_active_on_init( + prefer_active: bool, + python: str, + config: Config, + mocker: MockerFixture, + tester: CommandTester, + source_dir: Path, +) -> None: + from poetry.utils.env import GET_PYTHON_VERSION_ONELINER + + orig_check_output = subprocess.check_output + + def mock_check_output(cmd: str, *_: Any, **__: Any) -> str: + if GET_PYTHON_VERSION_ONELINER in cmd: + return "1.1.1" + + return orig_check_output(cmd, *_, **__) + + mocker.patch("subprocess.check_output", side_effect=mock_check_output) + + config.config["virtualenvs"]["prefer-active-python"] = prefer_active + pyproject_file = source_dir / "pyproject.toml" + + tester.execute( + "--author 'Your Name ' --name 'my-package'", + interactive=False, + ) + + expected = f"""\ +[tool.poetry.dependencies] +python = "^{python}" +""" + + assert expected in pyproject_file.read_text() diff --git a/tests/console/commands/test_install.py b/tests/console/commands/test_install.py index 64f786f0daf..6f4f407dd63 100644 --- a/tests/console/commands/test_install.py +++ b/tests/console/commands/test_install.py @@ -1,5 +1,7 @@ from __future__ import annotations +import re + from typing import TYPE_CHECKING import pytest @@ -7,6 +9,8 @@ from poetry.core.masonry.utils.module import ModuleOrPackageNotFound from poetry.core.packages.dependency_group import MAIN_GROUP +from poetry.console.exceptions import GroupNotFound + if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester @@ -14,6 +18,7 @@ from poetry.poetry import Poetry from tests.types import CommandTesterFactory + from tests.types import FixtureDirGetter from tests.types import ProjectFactory @@ -69,6 +74,22 @@ def tester( return command_tester_factory("install") +def _project_factory( + fixture_name: str, + project_factory: ProjectFactory, + fixture_dir: FixtureDirGetter, +) -> Poetry: + 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, + source=source, + ) + + @pytest.mark.parametrize( ("options", "groups"), [ @@ -98,7 +119,7 @@ def test_group_options_are_passed_to_the_installer( with_root: bool, tester: CommandTester, mocker: MockerFixture, -): +) -> None: """ Group options are passed properly to the installer. """ @@ -134,7 +155,7 @@ def test_group_options_are_passed_to_the_installer( def test_sync_option_is_passed_to_the_installer( tester: CommandTester, mocker: MockerFixture -): +) -> None: """ The --sync option is passed properly to the installer. """ @@ -145,9 +166,45 @@ def test_sync_option_is_passed_to_the_installer( assert tester.command.installer._requires_synchronization +@pytest.mark.parametrize("compile", [False, True]) +def test_compile_option_is_passed_to_the_installer( + tester: CommandTester, mocker: MockerFixture, compile: bool +) -> None: + """ + The --compile option is passed properly to the installer. + """ + mocker.patch.object(tester.command.installer, "run", return_value=1) + enable_bytecode_compilation_mock = mocker.patch.object( + tester.command.installer.executor._wheel_installer, + "enable_bytecode_compilation", + ) + + tester.execute("--compile" if compile else "") + + enable_bytecode_compilation_mock.assert_called_once_with(compile) + + +@pytest.mark.parametrize("skip_directory_cli_value", [True, False]) +def test_no_directory_is_passed_to_installer( + tester: CommandTester, mocker: MockerFixture, skip_directory_cli_value: bool +) -> None: + """ + The --no-directory option is passed to the installer. + """ + + mocker.patch.object(tester.command.installer, "run", return_value=1) + + if skip_directory_cli_value is True: + tester.execute("--no-directory") + else: + tester.execute() + + assert tester.command.installer._skip_directory is skip_directory_cli_value + + def test_no_all_extras_doesnt_populate_installer( tester: CommandTester, mocker: MockerFixture -): +) -> None: """ Not passing --all-extras means the installer doesn't see any extras. """ @@ -158,7 +215,9 @@ def test_no_all_extras_doesnt_populate_installer( assert not tester.command.installer._extras -def test_all_extras_populates_installer(tester: CommandTester, mocker: MockerFixture): +def test_all_extras_populates_installer( + tester: CommandTester, mocker: MockerFixture +) -> None: """ The --all-extras option results in extras passed to the installer. """ @@ -169,7 +228,20 @@ def test_all_extras_populates_installer(tester: CommandTester, mocker: MockerFix assert tester.command.installer._extras == ["extras-a", "extras-b"] -def test_extras_conflicts_all_extras(tester: CommandTester, mocker: MockerFixture): +def test_extras_are_parsed_and_populate_installer( + tester: CommandTester, + mocker: MockerFixture, +) -> None: + mocker.patch.object(tester.command.installer, "run", return_value=0) + + tester.execute('--extras "first second third"') + + assert tester.command.installer._extras == ["first", "second", "third"] + + +def test_extras_conflicts_all_extras( + tester: CommandTester, mocker: MockerFixture +) -> None: """ The --extras doesn't make sense with --all-extras. """ @@ -185,7 +257,93 @@ def test_extras_conflicts_all_extras(tester: CommandTester, mocker: MockerFixtur ) -def test_dry_run_populates_installer(tester: CommandTester, mocker: MockerFixture): +@pytest.mark.parametrize( + "options", + [ + "--with foo", + "--without foo", + "--with foo,bar --without baz", + "--only foo", + ], +) +def test_only_root_conflicts_with_without_only( + options: str, + tester: CommandTester, + mocker: MockerFixture, +) -> None: + mocker.patch.object(tester.command.installer, "run", return_value=0) + + tester.execute(f"{options} --only-root") + + assert tester.status_code == 1 + assert ( + tester.io.fetch_error() + == "The `--with`, `--without` and `--only` options cannot be used with" + " the `--only-root` option.\n" + ) + + +@pytest.mark.parametrize( + ("options", "valid_groups", "should_raise"), + [ + ({"--with": MAIN_GROUP}, {MAIN_GROUP}, False), + ({"--with": "spam"}, set(), True), + ({"--with": "spam,foo"}, {"foo"}, True), + ({"--without": "spam"}, set(), True), + ({"--without": "spam,bar"}, {"bar"}, True), + ({"--with": "eggs,ham", "--without": "spam"}, set(), True), + ({"--with": "eggs,ham", "--without": "spam,baz"}, {"baz"}, True), + ({"--only": "spam"}, set(), True), + ({"--only": "bim"}, {"bim"}, False), + ({"--only": MAIN_GROUP}, {MAIN_GROUP}, False), + ], +) +def test_invalid_groups_with_without_only( + tester: CommandTester, + mocker: MockerFixture, + options: dict[str, str], + valid_groups: set[str], + should_raise: bool, +) -> None: + mocker.patch.object(tester.command.installer, "run", return_value=0) + + cmd_args = " ".join(f"{flag} {groups}" for (flag, groups) in options.items()) + + if not should_raise: + tester.execute(cmd_args) + assert tester.status_code == 0 + else: + with pytest.raises(GroupNotFound, match=r"^Group\(s\) not found:") as e: + tester.execute(cmd_args) + assert tester.status_code is None + for opt, groups in options.items(): + group_list = groups.split(",") + invalid_groups = sorted(set(group_list) - valid_groups) + for group in invalid_groups: + assert ( + re.search(rf"{group} \(via .*{opt}.*\)", str(e.value)) is not None + ) + + +def test_remove_untracked_outputs_deprecation_warning( + tester: CommandTester, + mocker: MockerFixture, +) -> None: + mocker.patch.object(tester.command.installer, "run", return_value=0) + + tester.execute("--remove-untracked") + + assert tester.status_code == 0 + assert ( + tester.io.fetch_error() + == "The `--remove-untracked` option is deprecated, use the `--sync` option" + " instead.\n" + ) + + +def test_dry_run_populates_installer( + tester: CommandTester, mocker: MockerFixture +) -> None: """ The --dry-run option results in extras passed to the installer. """ @@ -195,3 +353,69 @@ def test_dry_run_populates_installer(tester: CommandTester, mocker: MockerFixtur tester.execute("--dry-run") assert tester.command.installer._dry_run is True + + +def test_dry_run_does_not_build(tester: CommandTester, mocker: MockerFixture) -> None: + mocker.patch.object(tester.command.installer, "run", return_value=0) + mocked_editable_builder = mocker.patch( + "poetry.masonry.builders.editable.EditableBuilder" + ) + + tester.execute("--dry-run") + + assert mocked_editable_builder.return_value.build.call_count == 0 + + +def test_install_logs_output(tester: CommandTester, mocker: MockerFixture) -> None: + mocker.patch.object(tester.command.installer, "run", return_value=0) + mocker.patch("poetry.masonry.builders.editable.EditableBuilder") + + tester.execute() + + assert tester.status_code == 0 + assert ( + tester.io.fetch_output() + == "\nInstalling the current project: simple-project (1.2.3)\n" + ) + + +def test_install_logs_output_decorated( + tester: CommandTester, mocker: MockerFixture +) -> None: + mocker.patch.object(tester.command.installer, "run", return_value=0) + mocker.patch("poetry.masonry.builders.editable.EditableBuilder") + + tester.execute(decorated=True) + + expected = ( + "\n" + "\x1b[39;1mInstalling\x1b[39;22m the current project: " + "\x1b[36msimple-project\x1b[39m (\x1b[39;1m1.2.3\x1b[39;22m)" + "\x1b[1G\x1b[2K" + "\x1b[39;1mInstalling\x1b[39;22m the current project: " + "\x1b[36msimple-project\x1b[39m (\x1b[32m1.2.3\x1b[39m)" + "\n" + ) + assert tester.status_code == 0 + assert tester.io.fetch_output() == expected + + +@pytest.mark.parametrize("options", ["", "--without dev"]) +@pytest.mark.parametrize( + "project", ["missing_directory_dependency", "missing_file_dependency"] +) +def test_install_path_dependency_does_not_exist( + command_tester_factory: CommandTesterFactory, + project_factory: ProjectFactory, + fixture_dir: FixtureDirGetter, + project: str, + options: str, +) -> None: + poetry = _project_factory(project, project_factory, fixture_dir) + poetry.locker.locked(True) + tester = command_tester_factory("install", poetry=poetry) + if options: + tester.execute(options) + else: + with pytest.raises(ValueError, match="does not exist"): + tester.execute(options) diff --git a/tests/console/commands/test_lock.py b/tests/console/commands/test_lock.py index 69de642e117..7a910caccf7 100644 --- a/tests/console/commands/test_lock.py +++ b/tests/console/commands/test_lock.py @@ -43,6 +43,7 @@ def _project_factory( name="foobar", pyproject_content=pyproject_content, poetry_lock_content=poetry_lock_content, + source=source, ) @@ -67,6 +68,13 @@ def poetry_with_old_lockfile( return _project_factory("old_lock", project_factory, fixture_dir) +@pytest.fixture +def poetry_with_nested_path_deps_old_lockfile( + project_factory: ProjectFactory, fixture_dir: FixtureDirGetter +) -> Poetry: + return _project_factory("old_lock_path_dependency", project_factory, fixture_dir) + + @pytest.fixture def poetry_with_incompatible_lockfile( project_factory: ProjectFactory, fixture_dir: FixtureDirGetter @@ -85,7 +93,7 @@ def test_lock_check_outdated( command_tester_factory: CommandTesterFactory, poetry_with_outdated_lockfile: Poetry, http: type[httpretty.httpretty], -): +) -> None: http.disable() locker = Locker( @@ -111,7 +119,7 @@ def test_lock_check_up_to_date( command_tester_factory: CommandTesterFactory, poetry_with_up_to_date_lockfile: Poetry, http: type[httpretty.httpretty], -): +) -> None: http.disable() locker = Locker( @@ -133,7 +141,7 @@ def test_lock_no_update( command_tester_factory: CommandTesterFactory, poetry_with_old_lockfile: Poetry, repo: TestRepository, -): +) -> None: repo.add_package(get_package("sampleproject", "1.3.1")) repo.add_package(get_package("sampleproject", "2.0.0")) @@ -166,6 +174,93 @@ def test_lock_no_update( assert locked_repository.find_packages(package.to_dependency()) +def test_lock_no_update_path_dependencies( + command_tester_factory: CommandTesterFactory, + poetry_with_nested_path_deps_old_lockfile: Poetry, + repo: TestRepository, +) -> None: + """ + The lock file contains a variant of the directory dependency "quix" that does + not depend on "sampleproject". Although the version of "quix" has not been changed, + it should be re-solved because there is always only one valid version + of a directory dependency at any time. + """ + repo.add_package(get_package("sampleproject", "1.3.1")) + + locker = Locker( + lock=poetry_with_nested_path_deps_old_lockfile.pyproject.file.path.parent + / "poetry.lock", + local_config=poetry_with_nested_path_deps_old_lockfile.locker._local_config, + ) + poetry_with_nested_path_deps_old_lockfile.set_locker(locker) + + tester = command_tester_factory( + "lock", poetry=poetry_with_nested_path_deps_old_lockfile + ) + tester.execute("--no-update") + + packages = locker.locked_repository().packages + + assert {p.name for p in packages} == {"quix", "sampleproject"} + + +@pytest.mark.parametrize("update", [True, False]) +@pytest.mark.parametrize( + "project", ["missing_directory_dependency", "missing_file_dependency"] +) +def test_lock_path_dependency_does_not_exist( + command_tester_factory: CommandTesterFactory, + project_factory: ProjectFactory, + fixture_dir: FixtureDirGetter, + project: str, + update: bool, +) -> None: + poetry = _project_factory(project, project_factory, fixture_dir) + locker = Locker( + lock=poetry.pyproject.file.path.parent / "poetry.lock", + local_config=poetry.locker._local_config, + ) + poetry.set_locker(locker) + options = "" if update else "--no-update" + + tester = command_tester_factory("lock", poetry=poetry) + if update or "directory" in project: + # directory dependencies are always updated + with pytest.raises(ValueError, match="does not exist"): + tester.execute(options) + else: + tester.execute(options) + + +@pytest.mark.parametrize("update", [True, False]) +@pytest.mark.parametrize( + "project", ["deleted_directory_dependency", "deleted_file_dependency"] +) +def test_lock_path_dependency_deleted_from_pyproject( + command_tester_factory: CommandTesterFactory, + project_factory: ProjectFactory, + fixture_dir: FixtureDirGetter, + project: str, + update: bool, +) -> None: + poetry = _project_factory(project, project_factory, fixture_dir) + locker = Locker( + lock=poetry.pyproject.file.path.parent / "poetry.lock", + local_config=poetry.locker._local_config, + ) + poetry.set_locker(locker) + + tester = command_tester_factory("lock", poetry=poetry) + if update: + tester.execute("") + else: + tester.execute("--no-update") + + packages = locker.locked_repository().packages + + assert {p.name for p in packages} == set() + + @pytest.mark.parametrize("is_no_update", [False, True]) def test_lock_with_incompatible_lockfile( command_tester_factory: CommandTesterFactory, diff --git a/tests/console/commands/test_new.py b/tests/console/commands/test_new.py index 79f09e20f27..7dd76f7cea7 100644 --- a/tests/console/commands/test_new.py +++ b/tests/console/commands/test_new.py @@ -1,7 +1,11 @@ from __future__ import annotations +import subprocess +import sys + from pathlib import Path from typing import TYPE_CHECKING +from typing import Any import pytest @@ -10,7 +14,9 @@ if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester + from pytest_mock import MockerFixture + from poetry.config.config import Config from poetry.poetry import Poetry from tests.types import CommandTesterFactory @@ -149,18 +155,20 @@ def test_command_new( package_path: str, include_from: str | None, tester: CommandTester, - tmp_dir: str, -): - path = Path(tmp_dir) / directory - options.append(path.as_posix()) + tmp_path: Path, +) -> None: + path = tmp_path / directory + options.append(str(path)) tester.execute(" ".join(options)) verify_project_directory(path, package_name, package_path, include_from) @pytest.mark.parametrize(("fmt",), [(None,), ("md",), ("rst",), ("adoc",), ("creole",)]) -def test_command_new_with_readme(fmt: str | None, tester: CommandTester, tmp_dir: str): +def test_command_new_with_readme( + fmt: str | None, tester: CommandTester, tmp_path: Path +) -> None: package = "package" - path = Path(tmp_dir) / package + path = tmp_path / package options = [path.as_posix()] if fmt: @@ -170,3 +178,47 @@ def test_command_new_with_readme(fmt: str | None, tester: CommandTester, tmp_dir poetry = verify_project_directory(path, package, package, None) assert poetry.local_config.get("readme") == f"README.{fmt or 'md'}" + + +@pytest.mark.parametrize( + ["prefer_active", "python"], + [ + (True, "1.1"), + (False, f"{sys.version_info[0]}.{sys.version_info[1]}"), + ], +) +def test_respect_prefer_active_on_new( + prefer_active: bool, + python: str, + config: Config, + mocker: MockerFixture, + tester: CommandTester, + tmp_path: Path, +) -> None: + from poetry.utils.env import GET_PYTHON_VERSION_ONELINER + + orig_check_output = subprocess.check_output + + def mock_check_output(cmd: str, *_: Any, **__: Any) -> str: + if GET_PYTHON_VERSION_ONELINER in cmd: + return "1.1.1" + + return orig_check_output(cmd, *_, **__) + + mocker.patch("subprocess.check_output", side_effect=mock_check_output) + + config.config["virtualenvs"]["prefer-active-python"] = prefer_active + + package = "package" + path = tmp_path / package + options = [str(path)] + tester.execute(" ".join(options)) + + pyproject_file = path / "pyproject.toml" + + expected = f"""\ +[tool.poetry.dependencies] +python = "^{python}" +""" + + assert expected in pyproject_file.read_text() diff --git a/tests/console/commands/test_publish.py b/tests/console/commands/test_publish.py index f4241a1a870..5f230c01ff3 100644 --- a/tests/console/commands/test_publish.py +++ b/tests/console/commands/test_publish.py @@ -23,7 +23,7 @@ def test_publish_returns_non_zero_code_for_upload_errors( app: PoetryTestApplication, app_tester: ApplicationTester, http: type[httpretty.httpretty], -): +) -> None: http.register_uri( http.POST, "https://upload.pypi.org/legacy/", status=400, body="Bad Request" ) @@ -48,7 +48,7 @@ def test_publish_returns_non_zero_code_for_connection_errors( app: PoetryTestApplication, app_tester: ApplicationTester, http: type[httpretty.httpretty], -): +) -> None: def request_callback(*_: Any, **__: Any) -> None: raise requests.ConnectionError() @@ -65,7 +65,9 @@ def request_callback(*_: Any, **__: Any) -> None: assert expected in app_tester.io.fetch_error() -def test_publish_with_cert(app_tester: ApplicationTester, mocker: MockerFixture): +def test_publish_with_cert( + app_tester: ApplicationTester, mocker: MockerFixture +) -> None: publisher_publish = mocker.patch("poetry.publishing.Publisher.publish") app_tester.execute("publish --cert path/to/ca.pem") @@ -75,7 +77,9 @@ def test_publish_with_cert(app_tester: ApplicationTester, mocker: MockerFixture) ] == publisher_publish.call_args -def test_publish_with_client_cert(app_tester: ApplicationTester, mocker: MockerFixture): +def test_publish_with_client_cert( + app_tester: ApplicationTester, mocker: MockerFixture +) -> None: publisher_publish = mocker.patch("poetry.publishing.Publisher.publish") app_tester.execute("publish --client-cert path/to/client.pem") @@ -94,7 +98,7 @@ def test_publish_with_client_cert(app_tester: ApplicationTester, mocker: MockerF ) def test_publish_dry_run_skip_existing( app_tester: ApplicationTester, http: type[httpretty.httpretty], options: str -): +) -> None: http.register_uri( http.POST, "https://upload.pypi.org/legacy/", status=409, body="Conflict" ) @@ -113,7 +117,7 @@ def test_publish_dry_run_skip_existing( def test_skip_existing_output( app_tester: ApplicationTester, http: type[httpretty.httpretty] -): +) -> None: http.register_uri( http.POST, "https://upload.pypi.org/legacy/", status=409, body="Conflict" ) diff --git a/tests/console/commands/test_remove.py b/tests/console/commands/test_remove.py index 0a2fa1b6c00..68c16edf065 100644 --- a/tests/console/commands/test_remove.py +++ b/tests/console/commands/test_remove.py @@ -48,7 +48,7 @@ def test_remove_without_specific_group_removes_from_all_groups( repo: TestRepository, command_tester_factory: CommandTesterFactory, installed: Repository, -): +) -> None: """ Removing without specifying a group removes packages from all groups. """ @@ -105,7 +105,7 @@ def test_remove_without_specific_group_removes_from_specific_groups( repo: TestRepository, command_tester_factory: CommandTesterFactory, installed: Repository, -): +) -> None: """ Removing with a specific group given removes packages only from this group. """ @@ -162,7 +162,7 @@ def test_remove_does_not_live_empty_groups( repo: TestRepository, command_tester_factory: CommandTesterFactory, installed: Repository, -): +) -> None: """ Empty groups are automatically discarded after package removal. """ @@ -208,7 +208,7 @@ def test_remove_canonicalized_named_removes_dependency_correctly( repo: TestRepository, command_tester_factory: CommandTesterFactory, installed: Repository, -): +) -> None: """ Removing a dependency using a canonicalized named removes the dependency. """ @@ -267,7 +267,7 @@ def test_remove_command_should_not_write_changes_upon_installer_errors( repo: TestRepository, command_tester_factory: CommandTesterFactory, mocker: MockerFixture, -): +) -> None: repo.add_package(Package("foo", "2.0.0")) command_tester_factory("add").execute("foo") @@ -285,7 +285,7 @@ def test_remove_with_dry_run_keep_files_intact( poetry_with_up_to_date_lockfile: Poetry, repo: TestRepository, command_tester_factory: CommandTesterFactory, -): +) -> None: tester = command_tester_factory("remove", poetry=poetry_with_up_to_date_lockfile) original_pyproject_content = poetry_with_up_to_date_lockfile.file.read() diff --git a/tests/console/commands/test_run.py b/tests/console/commands/test_run.py index 6465c7f55ef..53c5dbbb3d3 100644 --- a/tests/console/commands/test_run.py +++ b/tests/console/commands/test_run.py @@ -1,5 +1,7 @@ from __future__ import annotations +import subprocess + from typing import TYPE_CHECKING import pytest @@ -12,9 +14,12 @@ from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture + from poetry.poetry import Poetry from poetry.utils.env import MockEnv from poetry.utils.env import VirtualEnv from tests.types import CommandTesterFactory + from tests.types import FixtureDirGetter + from tests.types import ProjectFactory @pytest.fixture @@ -27,14 +32,27 @@ def patches(mocker: MockerFixture, env: MockEnv) -> None: mocker.patch("poetry.utils.env.EnvManager.get", return_value=env) -def test_run_passes_all_args(app_tester: ApplicationTester, env: MockEnv): +@pytest.fixture +def poetry_with_scripts( + project_factory: ProjectFactory, fixture_dir: FixtureDirGetter +) -> Poetry: + source = fixture_dir("scripts") + + return project_factory( + name="scripts", + pyproject_content=(source / "pyproject.toml").read_text(encoding="utf-8"), + source=source, + ) + + +def test_run_passes_all_args(app_tester: ApplicationTester, env: MockEnv) -> None: app_tester.execute("run python -V") assert [["python", "-V"]] == env.executed def test_run_keeps_options_passed_before_command( app_tester: ApplicationTester, env: MockEnv -): +) -> None: app_tester.execute("-V --no-ansi run python", decorated=True) assert not app_tester.io.is_decorated() @@ -46,27 +64,24 @@ def test_run_keeps_options_passed_before_command( def test_run_has_helpful_error_when_command_not_found( app_tester: ApplicationTester, env: MockEnv, capfd: pytest.CaptureFixture[str] -): +) -> None: + nonexistent_command = "nonexistent-command" env._execute = True - app_tester.execute("run nonexistent-command") + app_tester.execute(f"run {nonexistent_command}") - assert env.executed == [["nonexistent-command"]] + assert env.executed == [[nonexistent_command]] assert app_tester.status_code == 1 if WINDOWS: # On Windows we use a shell to run commands which provides its own error # message when a command is not found that is not captured by the # ApplicationTester but is captured by pytest, and we can access it via capfd. - # The expected string in this assertion assumes Command Prompt (cmd.exe) is the - # shell used. - assert capfd.readouterr().err.splitlines() == [ - ( - "'nonexistent-command' is not recognized as an internal or external" - " command," - ), - "operable program or batch file.", - ] + # The exact error message depends on the system language. Thus, we check only + # for the name of the command. + assert nonexistent_command in capfd.readouterr().err else: - assert app_tester.io.fetch_error() == "Command not found: nonexistent-command\n" + assert ( + app_tester.io.fetch_error() == f"Command not found: {nonexistent_command}\n" + ) @pytest.mark.skipif( @@ -79,7 +94,7 @@ def test_run_has_helpful_error_when_command_not_found( def test_run_console_scripts_of_editable_dependencies_on_windows( tmp_venv: VirtualEnv, command_tester_factory: CommandTesterFactory, -): +) -> None: """ On Windows, Poetry installs console scripts of editable dependencies by creating in the environment's `Scripts/` directory both: @@ -105,3 +120,77 @@ def test_run_console_scripts_of_editable_dependencies_on_windows( # We prove that the CMD script executed successfully by verifying the exit code # matches what we wrote in the script assert tester.execute("quix") == 123 + + +def test_run_script_exit_code( + poetry_with_scripts: Poetry, + command_tester_factory: CommandTesterFactory, + tmp_venv: VirtualEnv, + mocker: MockerFixture, +) -> None: + mocker.patch( + "os.execvpe", + lambda file, args, env: subprocess.call([file] + args[1:], env=env), + ) + install_tester = command_tester_factory( + "install", + poetry=poetry_with_scripts, + environment=tmp_venv, + ) + assert install_tester.execute() == 0 + tester = command_tester_factory( + "run", poetry=poetry_with_scripts, environment=tmp_venv + ) + assert tester.execute("exit-code") == 42 + assert tester.execute("return-code") == 42 + + +@pytest.mark.parametrize( + "installed_script", [False, True], ids=["not installed", "installed"] +) +def test_run_script_sys_argv0( + installed_script: bool, + poetry_with_scripts: Poetry, + command_tester_factory: CommandTesterFactory, + tmp_venv: VirtualEnv, + mocker: MockerFixture, +) -> None: + """ + If RunCommand calls an installed script defined in pyproject.toml, + sys.argv[0] must be set to the full path of the script. + """ + mocker.patch("poetry.utils.env.EnvManager.get", return_value=tmp_venv) + mocker.patch( + "os.execvpe", + lambda file, args, env: subprocess.call([file] + args[1:], env=env), + ) + + install_tester = command_tester_factory( + "install", + poetry=poetry_with_scripts, + environment=tmp_venv, + ) + assert install_tester.execute() == 0 + if not installed_script: + for path in tmp_venv.script_dirs[0].glob("check-argv0*"): + path.unlink() + + tester = command_tester_factory( + "run", poetry=poetry_with_scripts, environment=tmp_venv + ) + argv1 = "absolute" if installed_script else "relative" + assert tester.execute(f"check-argv0 {argv1}") == 0 + + if installed_script: + expected_message = "" + else: + expected_message = """\ +Warning: 'check-argv0' is an entry point defined in pyproject.toml, but it's not \ +installed as a script. You may get improper `sys.argv[0]`. + +The support to run uninstalled scripts will be removed in a future release. + +Run `poetry install` to resolve and get rid of this message. + +""" + assert tester.io.fetch_error() == expected_message diff --git a/tests/console/commands/test_shell.py b/tests/console/commands/test_shell.py index abdaec46789..a8d0b8407db 100644 --- a/tests/console/commands/test_shell.py +++ b/tests/console/commands/test_shell.py @@ -20,7 +20,7 @@ def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("shell") -def test_shell(tester: CommandTester, mocker: MockerFixture): +def test_shell(tester: CommandTester, mocker: MockerFixture) -> None: shell_activate = mocker.patch("poetry.utils.shell.Shell.activate") tester.execute() @@ -32,7 +32,7 @@ def test_shell(tester: CommandTester, mocker: MockerFixture): assert tester.status_code == 0 -def test_shell_already_active(tester: CommandTester, mocker: MockerFixture): +def test_shell_already_active(tester: CommandTester, mocker: MockerFixture) -> None: os.environ["POETRY_ACTIVE"] = "1" shell_activate = mocker.patch("poetry.utils.shell.Shell.activate") @@ -71,7 +71,7 @@ def test__is_venv_activated( real_prefix: str | None, prefix: str, expected: bool, -): +) -> None: mocker.patch.object(tester.command.env, "_path", Path("foobar")) mocker.patch("sys.prefix", prefix) diff --git a/tests/console/commands/test_show.py b/tests/console/commands/test_show.py index bd6d8382ea9..7fb6c60c7eb 100644 --- a/tests/console/commands/test_show.py +++ b/tests/console/commands/test_show.py @@ -8,6 +8,7 @@ from poetry.core.packages.dependency_group import DependencyGroup from poetry.factory import Factory +from poetry.utils._compat import tomllib from tests.helpers import MOCK_DEFAULT_GIT_REVISION from tests.helpers import get_package @@ -28,7 +29,7 @@ def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: def test_show_basic_with_installed_packages( tester: CommandTester, poetry: Poetry, installed: Repository -): +) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0")) poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0")) poetry.package.add_dependency( @@ -257,7 +258,7 @@ def test_show_basic_with_group_options( tester: CommandTester, poetry: Poetry, installed: Repository, -): +) -> None: _configure_project_with_groups(poetry, installed) tester.execute(options) @@ -267,7 +268,7 @@ def test_show_basic_with_group_options( def test_show_basic_with_installed_packages_single( tester: CommandTester, poetry: Poetry, installed: Repository -): +) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0")) cachy_010 = get_package("cachy", "0.1.0") @@ -309,7 +310,7 @@ def test_show_basic_with_installed_packages_single( def test_show_basic_with_installed_packages_single_canonicalized( tester: CommandTester, poetry: Poetry, installed: Repository -): +) -> None: poetry.package.add_dependency(Factory.create_dependency("foo-bar", "^0.1.0")) foo_bar = get_package("foo-bar", "0.1.0") @@ -351,7 +352,7 @@ def test_show_basic_with_installed_packages_single_canonicalized( def test_show_basic_with_not_installed_packages_non_decorated( tester: CommandTester, poetry: Poetry, installed: Repository -): +) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0")) poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0")) @@ -407,7 +408,7 @@ def test_show_basic_with_not_installed_packages_non_decorated( def test_show_basic_with_not_installed_packages_decorated( tester: CommandTester, poetry: Poetry, installed: Repository -): +) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0")) poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0")) @@ -466,7 +467,7 @@ def test_show_latest_non_decorated( poetry: Poetry, installed: Repository, repo: TestRepository, -): +) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0")) poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0")) @@ -536,7 +537,7 @@ def test_show_latest_decorated( poetry: Poetry, installed: Repository, repo: TestRepository, -): +) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0")) poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0")) @@ -608,7 +609,7 @@ def test_show_outdated( poetry: Poetry, installed: Repository, repo: TestRepository, -): +) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0")) poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0")) @@ -674,7 +675,7 @@ def test_show_outdated_with_only_up_to_date_packages( poetry: Poetry, installed: Repository, repo: TestRepository, -): +) -> None: cachy_020 = get_package("cachy", "0.2.0") cachy_020.description = "Cachy package" @@ -716,7 +717,7 @@ def test_show_outdated_has_prerelease_but_not_allowed( poetry: Poetry, installed: Repository, repo: TestRepository, -): +) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0")) poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0")) @@ -787,7 +788,7 @@ def test_show_outdated_has_prerelease_and_allowed( poetry: Poetry, installed: Repository, repo: TestRepository, -): +) -> None: poetry.package.add_dependency( Factory.create_dependency( "cachy", {"version": ">=0.0.1", "allow-prereleases": True} @@ -862,7 +863,7 @@ def test_show_outdated_formatting( poetry: Poetry, installed: Repository, repo: TestRepository, -): +) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0")) poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0")) @@ -941,7 +942,7 @@ def test_show_outdated_local_dependencies( poetry: Poetry, installed: Repository, repo: TestRepository, -): +) -> None: cachy_010 = get_package("cachy", "0.1.0") cachy_010.description = "Cachy package" cachy_020 = get_package("cachy", "0.2.0") @@ -1057,7 +1058,7 @@ def test_show_outdated_git_dev_dependency( poetry: Poetry, installed: Repository, repo: TestRepository, -): +) -> None: cachy_010 = get_package("cachy", "0.1.0") cachy_010.description = "Cachy package" cachy_020 = get_package("cachy", "0.2.0") @@ -1157,7 +1158,7 @@ def test_show_outdated_no_dev_git_dev_dependency( poetry: Poetry, installed: Repository, repo: TestRepository, -): +) -> None: cachy_010 = get_package("cachy", "0.1.0") cachy_010.description = "Cachy package" cachy_020 = get_package("cachy", "0.2.0") @@ -1254,7 +1255,7 @@ def test_show_hides_incompatible_package( poetry: Poetry, installed: Repository, repo: TestRepository, -): +) -> None: poetry.package.add_dependency( Factory.create_dependency("cachy", {"version": "^0.1.0", "python": "< 2.0"}) ) @@ -1315,7 +1316,7 @@ def test_show_all_shows_incompatible_package( poetry: Poetry, installed: Repository, repo: TestRepository, -): +) -> None: cachy_010 = get_package("cachy", "0.1.0") cachy_010.description = "Cachy package" @@ -1368,9 +1369,106 @@ def test_show_all_shows_incompatible_package( assert tester.io.fetch_output() == expected +def test_show_hides_incompatible_package_with_duplicate( + tester: CommandTester, + poetry: Poetry, + installed: Repository, + repo: TestRepository, +) -> None: + poetry.package.add_dependency( + Factory.create_dependency("cachy", {"version": "0.1.0", "platform": "linux"}) + ) + poetry.package.add_dependency( + Factory.create_dependency("cachy", {"version": "0.1.1", "platform": "darwin"}) + ) + + poetry.locker.mock_lock_data( + { + "package": [ + { + "name": "cachy", + "version": "0.1.0", + "description": "Cachy package", + "optional": False, + "platform": "*", + "python-versions": "*", + "files": [], + }, + { + "name": "cachy", + "version": "0.1.1", + "description": "Cachy package", + "optional": False, + "platform": "*", + "python-versions": "*", + "files": [], + }, + ], + "metadata": {"content-hash": "123456789"}, + } + ) + + tester.execute() + + expected = """\ +cachy (!) 0.1.1 Cachy package +""" + + assert tester.io.fetch_output() == expected + + +def test_show_all_shows_all_duplicates( + tester: CommandTester, + poetry: Poetry, + installed: Repository, + repo: TestRepository, +) -> None: + poetry.package.add_dependency( + Factory.create_dependency("cachy", {"version": "0.1.0", "platform": "linux"}) + ) + poetry.package.add_dependency( + Factory.create_dependency("cachy", {"version": "0.1.1", "platform": "darwin"}) + ) + + poetry.locker.mock_lock_data( + { + "package": [ + { + "name": "cachy", + "version": "0.1.0", + "description": "Cachy package", + "optional": False, + "platform": "*", + "python-versions": "*", + "files": [], + }, + { + "name": "cachy", + "version": "0.1.1", + "description": "Cachy package", + "optional": False, + "platform": "*", + "python-versions": "*", + "files": [], + }, + ], + "metadata": {"content-hash": "123456789"}, + } + ) + + tester.execute("--all") + + expected = """\ +cachy 0.1.0 Cachy package +cachy (!) 0.1.1 Cachy package +""" + + assert tester.io.fetch_output() == expected + + def test_show_non_dev_with_basic_installed_packages( tester: CommandTester, poetry: Poetry, installed: Repository -): +) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0")) poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0")) poetry.package.add_dependency( @@ -1446,7 +1544,7 @@ def test_show_non_dev_with_basic_installed_packages( def test_show_with_group_only( tester: CommandTester, poetry: Poetry, installed: Repository -): +) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0")) poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0")) poetry.package.add_dependency( @@ -1521,7 +1619,7 @@ def test_show_with_group_only( def test_show_with_optional_group( tester: CommandTester, poetry: Poetry, installed: Repository -): +) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0")) poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0")) group = DependencyGroup("dev", optional=True) @@ -1605,7 +1703,9 @@ def test_show_with_optional_group( assert tester.io.fetch_output() == expected -def test_show_tree(tester: CommandTester, poetry: Poetry, installed: Repository): +def test_show_tree( + tester: CommandTester, poetry: Poetry, installed: Repository +) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.2.0")) cachy2 = get_package("cachy", "0.2.0") @@ -1657,7 +1757,9 @@ def test_show_tree(tester: CommandTester, poetry: Poetry, installed: Repository) assert tester.io.fetch_output() == expected -def test_show_tree_no_dev(tester: CommandTester, poetry: Poetry, installed: Repository): +def test_show_tree_no_dev( + tester: CommandTester, poetry: Poetry, installed: Repository +) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.2.0")) poetry.package.add_dependency( Factory.create_dependency("pytest", "^6.1.0", groups=["dev"]) @@ -1726,7 +1828,7 @@ def test_show_tree_no_dev(tester: CommandTester, poetry: Poetry, installed: Repo def test_show_tree_why_package( tester: CommandTester, poetry: Poetry, installed: Repository -): +) -> None: poetry.package.add_dependency(Factory.create_dependency("a", "=0.0.1")) a = get_package("a", "0.0.1") @@ -1783,7 +1885,9 @@ def test_show_tree_why_package( assert tester.io.fetch_output() == expected -def test_show_tree_why(tester: CommandTester, poetry: Poetry, installed: Repository): +def test_show_tree_why( + tester: CommandTester, poetry: Poetry, installed: Repository +) -> None: poetry.package.add_dependency(Factory.create_dependency("a", "=0.0.1")) a = get_package("a", "0.0.1") @@ -1841,7 +1945,7 @@ def test_show_tree_why(tester: CommandTester, poetry: Poetry, installed: Reposit def test_show_required_by_deps( tester: CommandTester, poetry: Poetry, installed: Repository -): +) -> None: poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.2.0")) poetry.package.add_dependency(Factory.create_dependency("pendulum", "2.0.0")) @@ -1916,7 +2020,7 @@ def test_show_required_by_deps( assert actual == expected -def test_show_errors_without_lock_file(tester: CommandTester, poetry: Poetry): +def test_show_errors_without_lock_file(tester: CommandTester, poetry: Poetry) -> None: assert not poetry.locker.lock.exists() tester.execute() @@ -1931,7 +2035,7 @@ def test_show_dependency_installed_from_git_in_dev( poetry: Poetry, installed: Repository, repo: TestRepository, -): +) -> None: # Add a regular dependency for a package in main, and a git dependency for the same # package in dev. poetry.package.add_dependency(Factory.create_dependency("demo", "^0.1.1")) @@ -1999,7 +2103,7 @@ def test_url_dependency_is_not_outdated_by_repository_package( poetry: Poetry, installed: Repository, repo: TestRepository, -): +) -> None: demo_url = "https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl" poetry.package.add_dependency( Factory.create_dependency( @@ -2043,3 +2147,33 @@ def test_url_dependency_is_not_outdated_by_repository_package( # version in the repository. tester.execute("--outdated") assert tester.io.fetch_output() == "" + + +@pytest.mark.parametrize( + ("project_directory", "required_fixtures"), + [ + ( + "deleted_directory_dependency", + [], + ), + ], +) +def test_show_outdated_missing_directory_dependency( + tester: CommandTester, + poetry: Poetry, + installed: Repository, + repo: TestRepository, +) -> None: + with (poetry.pyproject.file.path.parent / "poetry.lock").open(mode="rb") as f: + data = tomllib.load(f) + poetry.locker.mock_lock_data(data) + + poetry.package.add_dependency( + Factory.create_dependency( + "missing", + {"path": data["package"][0]["source"]["url"]}, + ) + ) + + with pytest.raises(ValueError, match="does not exist"): + tester.execute("") diff --git a/tests/console/commands/test_update.py b/tests/console/commands/test_update.py index dd3306cf2ba..cb0f0052ac1 100644 --- a/tests/console/commands/test_update.py +++ b/tests/console/commands/test_update.py @@ -40,7 +40,7 @@ def test_update_with_dry_run_keep_files_intact( poetry_with_up_to_date_lockfile: Poetry, repo: TestRepository, command_tester_factory: CommandTesterFactory, -): +) -> None: tester = command_tester_factory("update", poetry=poetry_with_up_to_date_lockfile) original_pyproject_content = poetry_with_up_to_date_lockfile.file.read() diff --git a/tests/console/commands/test_version.py b/tests/console/commands/test_version.py index d578b7fd918..33d5cb3824f 100644 --- a/tests/console/commands/test_version.py +++ b/tests/console/commands/test_version.py @@ -51,31 +51,31 @@ def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: ) def test_increment_version( version: str, rule: str, expected: str, command: VersionCommand -): +) -> None: assert command.increment_version(version, rule).text == expected -def test_version_show(tester: CommandTester): +def test_version_show(tester: CommandTester) -> None: tester.execute() assert tester.io.fetch_output() == "simple-project 1.2.3\n" -def test_short_version_show(tester: CommandTester): +def test_short_version_show(tester: CommandTester) -> None: tester.execute("--short") assert tester.io.fetch_output() == "1.2.3\n" -def test_version_update(tester: CommandTester): +def test_version_update(tester: CommandTester) -> None: tester.execute("2.0.0") assert tester.io.fetch_output() == "Bumping version from 1.2.3 to 2.0.0\n" -def test_short_version_update(tester: CommandTester): +def test_short_version_update(tester: CommandTester) -> None: tester.execute("--short 2.0.0") assert tester.io.fetch_output() == "2.0.0\n" -def test_dry_run(tester: CommandTester): +def test_dry_run(tester: CommandTester) -> None: old_pyproject = tester.command.poetry.file.path.read_text() tester.execute("--dry-run major") diff --git a/tests/console/conftest.py b/tests/console/conftest.py index a78ceaa89f9..8d04adf2c42 100644 --- a/tests/console/conftest.py +++ b/tests/console/conftest.py @@ -2,7 +2,6 @@ import os -from pathlib import Path from typing import TYPE_CHECKING import pytest @@ -22,6 +21,7 @@ if TYPE_CHECKING: from collections.abc import Iterator + from pathlib import Path from pytest_mock import MockerFixture @@ -31,6 +31,7 @@ from poetry.utils.env import Env from tests.conftest import Config from tests.types import CommandTesterFactory + from tests.types import FixtureDirGetter from tests.types import ProjectFactory @@ -40,8 +41,8 @@ def installer() -> NoopInstaller: @pytest.fixture -def env(tmp_dir: str) -> MockEnv: - path = Path(tmp_dir) / ".venv" +def env(tmp_path: Path) -> MockEnv: + path = tmp_path / ".venv" path.mkdir(parents=True) return MockEnv(path=path, is_venv=True) @@ -96,11 +97,12 @@ def project_directory() -> str: @pytest.fixture -def poetry(project_directory: str, project_factory: ProjectFactory) -> Poetry: - return project_factory( - name="simple", - source=Path(__file__).parent.parent / "fixtures" / project_directory, - ) +def poetry( + project_directory: str, + project_factory: ProjectFactory, + fixture_dir: FixtureDirGetter, +) -> Poetry: + return project_factory(name="simple", source=fixture_dir(project_directory)) @pytest.fixture @@ -166,7 +168,6 @@ def _tester( executor=executor or TestExecutor(env, poetry.pool, poetry.config, tester.io), ) - installer.use_executor(True) command.set_installer(installer) return tester diff --git a/tests/console/test_application.py b/tests/console/test_application.py index 9ed2e2aeb54..7bca231d04a 100644 --- a/tests/console/test_application.py +++ b/tests/console/test_application.py @@ -39,7 +39,7 @@ def with_add_command_plugin(mocker: MockerFixture) -> None: mock_metadata_entry_points(mocker, AddCommandPlugin) -def test_application_with_plugins(with_add_command_plugin: None): +def test_application_with_plugins(with_add_command_plugin: None) -> None: app = Application() tester = ApplicationTester(app) @@ -49,7 +49,7 @@ def test_application_with_plugins(with_add_command_plugin: None): assert tester.status_code == 0 -def test_application_with_plugins_disabled(with_add_command_plugin: None): +def test_application_with_plugins_disabled(with_add_command_plugin: None) -> None: app = Application() tester = ApplicationTester(app) @@ -59,7 +59,7 @@ def test_application_with_plugins_disabled(with_add_command_plugin: None): assert tester.status_code == 0 -def test_application_execute_plugin_command(with_add_command_plugin: None): +def test_application_execute_plugin_command(with_add_command_plugin: None) -> None: app = Application() tester = ApplicationTester(app) @@ -71,7 +71,7 @@ def test_application_execute_plugin_command(with_add_command_plugin: None): def test_application_execute_plugin_command_with_plugins_disabled( with_add_command_plugin: None, -): +) -> None: app = Application() tester = ApplicationTester(app) @@ -83,7 +83,7 @@ def test_application_execute_plugin_command_with_plugins_disabled( @pytest.mark.parametrize("disable_cache", [True, False]) -def test_application_verify_source_cache_flag(disable_cache: bool): +def test_application_verify_source_cache_flag(disable_cache: bool) -> None: app = Application() tester = ApplicationTester(app) diff --git a/tests/fixtures/build_system_requires_not_available/README.rst b/tests/fixtures/build_system_requires_not_available/README.rst new file mode 100644 index 00000000000..f7fe15470f9 --- /dev/null +++ b/tests/fixtures/build_system_requires_not_available/README.rst @@ -0,0 +1,2 @@ +My Package +========== diff --git a/tests/fixtures/build_system_requires_not_available/pyproject.toml b/tests/fixtures/build_system_requires_not_available/pyproject.toml new file mode 100644 index 00000000000..bfe752e82ba --- /dev/null +++ b/tests/fixtures/build_system_requires_not_available/pyproject.toml @@ -0,0 +1,29 @@ +[tool.poetry] +name = "simple-project" +version = "1.2.3" +description = "Some description." +authors = [ + "Sébastien Eustace " +] +license = "MIT" + +readme = ["README.rst"] + +homepage = "https://python-poetry.org" +repository = "https://github.com/python-poetry/poetry" +documentation = "https://python-poetry.org/docs" + +keywords = ["packaging", "dependency", "poetry"] + +classifiers = [ + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Python Modules" +] + +# Requirements +[tool.poetry.dependencies] +python = "^3.7" + +[build-system] +requires = ["poetry-core==0.999"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/build_system_requires_not_available/simple_project/__init__.py b/tests/fixtures/build_system_requires_not_available/simple_project/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/deleted_directory_dependency/poetry.lock b/tests/fixtures/deleted_directory_dependency/poetry.lock new file mode 100644 index 00000000000..de0370101b8 --- /dev/null +++ b/tests/fixtures/deleted_directory_dependency/poetry.lock @@ -0,0 +1,20 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "missing" +version = "1.2.3" +description = "" +category = "main" +optional = false +python-versions = "*" +files = [] +develop = false + +[package.source] +type = "directory" +url = "missing" + +[metadata] +lock-version = "2.0" +python-versions = "*" +content-hash = "bec78476925e4cda6b22e91551ce4337264bdc3394c4f8297ad238f67a436d0e" diff --git a/tests/fixtures/deleted_directory_dependency/pyproject.toml b/tests/fixtures/deleted_directory_dependency/pyproject.toml new file mode 100644 index 00000000000..74151f00969 --- /dev/null +++ b/tests/fixtures/deleted_directory_dependency/pyproject.toml @@ -0,0 +1,10 @@ +[tool.poetry] +name = "project-with-missing-directory-dependency" +version = "1.2.3" +description = "This is a description" +authors = ["Your Name "] +license = "MIT" +packages = [] + +[tool.poetry.dependencies] +python = "*" diff --git a/tests/fixtures/deleted_file_dependency/poetry.lock b/tests/fixtures/deleted_file_dependency/poetry.lock new file mode 100644 index 00000000000..d5bb9f16541 --- /dev/null +++ b/tests/fixtures/deleted_file_dependency/poetry.lock @@ -0,0 +1,20 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "missing" +version = "1.2.3" +description = "" +category = "main" +optional = false +python-versions = "*" +files = [] +develop = false + +[package.source] +type = "file" +url = "missing-0.1.0-py2.py3-none-any.whl" + +[metadata] +lock-version = "2.0" +python-versions = "*" +content-hash = "bec78476925e4cda6b22e91551ce4337264bdc3394c4f8297ad238f67a436d0e" diff --git a/tests/fixtures/deleted_file_dependency/pyproject.toml b/tests/fixtures/deleted_file_dependency/pyproject.toml new file mode 100644 index 00000000000..74151f00969 --- /dev/null +++ b/tests/fixtures/deleted_file_dependency/pyproject.toml @@ -0,0 +1,10 @@ +[tool.poetry] +name = "project-with-missing-directory-dependency" +version = "1.2.3" +description = "This is a description" +authors = ["Your Name "] +license = "MIT" +packages = [] + +[tool.poetry.dependencies] +python = "*" diff --git a/tests/fixtures/distributions/demo-0.1.0.tar.gz b/tests/fixtures/distributions/demo-0.1.0.tar.gz index 133b64421f8..37349b770e5 100644 Binary files a/tests/fixtures/distributions/demo-0.1.0.tar.gz and b/tests/fixtures/distributions/demo-0.1.0.tar.gz differ diff --git a/tests/fixtures/distributions/demo-0.1.2-py2.py3-none-any.whl b/tests/fixtures/distributions/demo-0.1.2-py2.py3-none-any.whl new file mode 100644 index 00000000000..9f1ce9264e8 Binary files /dev/null and b/tests/fixtures/distributions/demo-0.1.2-py2.py3-none-any.whl differ diff --git a/tests/fixtures/distributions/demo_invalid_record-0.1.0-py2.py3-none-any.whl b/tests/fixtures/distributions/demo_invalid_record-0.1.0-py2.py3-none-any.whl new file mode 100644 index 00000000000..184475aa276 Binary files /dev/null and b/tests/fixtures/distributions/demo_invalid_record-0.1.0-py2.py3-none-any.whl differ diff --git a/tests/fixtures/distributions/demo_invalid_record2-0.1.0-py2.py3-none-any.whl b/tests/fixtures/distributions/demo_invalid_record2-0.1.0-py2.py3-none-any.whl new file mode 100644 index 00000000000..d171e08c415 Binary files /dev/null and b/tests/fixtures/distributions/demo_invalid_record2-0.1.0-py2.py3-none-any.whl differ diff --git a/tests/fixtures/extended_with_no_setup/README.md b/tests/fixtures/extended_with_no_setup/README.md new file mode 100644 index 00000000000..a7508bd515e --- /dev/null +++ b/tests/fixtures/extended_with_no_setup/README.md @@ -0,0 +1,2 @@ +Module 1 +======== diff --git a/tests/fixtures/extended_with_no_setup/build.py b/tests/fixtures/extended_with_no_setup/build.py new file mode 100644 index 00000000000..3ef8cfae13a --- /dev/null +++ b/tests/fixtures/extended_with_no_setup/build.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +import os +import shutil + +from distutils.command.build_ext import build_ext +from distutils.core import Distribution +from distutils.core import Extension + + +extensions = [Extension("extended.extended", ["extended/extended.c"])] + + +def build(): + distribution = Distribution({"name": "extended", "ext_modules": extensions}) + distribution.package_dir = "extended" + + cmd = build_ext(distribution) + cmd.ensure_finalized() + cmd.run() + + # Copy built extensions back to the project + for output in cmd.get_outputs(): + relative_extension = os.path.relpath(output, cmd.build_lib) + shutil.copyfile(output, relative_extension) + mode = os.stat(relative_extension).st_mode + mode |= (mode & 0o444) >> 2 + os.chmod(relative_extension, mode) + + +if __name__ == "__main__": + build() diff --git a/tests/fixtures/extended_with_no_setup/extended/__init__.py b/tests/fixtures/extended_with_no_setup/extended/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/extended_with_no_setup/extended/extended.c b/tests/fixtures/extended_with_no_setup/extended/extended.c new file mode 100644 index 00000000000..25a028eb11e --- /dev/null +++ b/tests/fixtures/extended_with_no_setup/extended/extended.c @@ -0,0 +1,58 @@ +#include + + +static PyObject *hello(PyObject *self) { + return PyUnicode_FromString("Hello"); +} + + +static PyMethodDef module_methods[] = { + { + "hello", + (PyCFunction) hello, + NULL, + PyDoc_STR("Say hello.") + }, + {NULL} +}; + +#if PY_MAJOR_VERSION >= 3 +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "extended", + NULL, + -1, + module_methods, + NULL, + NULL, + NULL, + NULL, +}; +#endif + +PyMODINIT_FUNC +#if PY_MAJOR_VERSION >= 3 +PyInit_extended(void) +#else +init_extended(void) +#endif +{ + PyObject *module; + +#if PY_MAJOR_VERSION >= 3 + module = PyModule_Create(&moduledef); +#else + module = Py_InitModule3("extended", module_methods, NULL); +#endif + + if (module == NULL) +#if PY_MAJOR_VERSION >= 3 + return NULL; +#else + return; +#endif + +#if PY_MAJOR_VERSION >= 3 + return module; +#endif +} diff --git a/tests/fixtures/extended_with_no_setup/pyproject.toml b/tests/fixtures/extended_with_no_setup/pyproject.toml new file mode 100644 index 00000000000..779fb1bd9dc --- /dev/null +++ b/tests/fixtures/extended_with_no_setup/pyproject.toml @@ -0,0 +1,26 @@ +[tool.poetry] +name = "extended" +version = "0.1" +description = "Some description." +authors = [ + "Sébastien Eustace " +] +license = "MIT" + +readme = "README.md" + +homepage = "https://python-poetry.org/" + +include = [ + # C extensions must be included in the wheel distributions + {path = "extended/*.so", format = "wheel"}, + {path = "extended/*.pyd", format = "wheel"}, +] + +[tool.poetry.build] +script = "build.py" +generate-setup-file = false + +[build-system] +requires = ["poetry-core>=1.5.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/missing_directory_dependency/poetry.lock b/tests/fixtures/missing_directory_dependency/poetry.lock new file mode 100644 index 00000000000..671a7060742 --- /dev/null +++ b/tests/fixtures/missing_directory_dependency/poetry.lock @@ -0,0 +1,20 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "missing" +version = "1.2.3" +description = "" +category = "dev" +optional = false +python-versions = "*" +files = [] +develop = false + +[package.source] +type = "directory" +url = "missing" + +[metadata] +lock-version = "2.0" +python-versions = "*" +content-hash = "bec78476925e4cda6b22e91551ce4337264bdc3394c4f8297ad238f67a436d0e" diff --git a/tests/fixtures/missing_directory_dependency/pyproject.toml b/tests/fixtures/missing_directory_dependency/pyproject.toml new file mode 100644 index 00000000000..570ca5debc3 --- /dev/null +++ b/tests/fixtures/missing_directory_dependency/pyproject.toml @@ -0,0 +1,13 @@ +[tool.poetry] +name = "project-with-missing-directory-dependency" +version = "1.2.3" +description = "This is a description" +authors = ["Your Name "] +license = "MIT" +packages = [] + +[tool.poetry.dependencies] +python = "*" + +[tool.poetry.dev-dependencies] +missing = { path = "./missing" } diff --git a/tests/fixtures/missing_file_dependency/poetry.lock b/tests/fixtures/missing_file_dependency/poetry.lock new file mode 100644 index 00000000000..76c3027db45 --- /dev/null +++ b/tests/fixtures/missing_file_dependency/poetry.lock @@ -0,0 +1,20 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "missing" +version = "1.2.3" +description = "" +category = "dev" +optional = false +python-versions = "*" +files = [] +develop = false + +[package.source] +type = "file" +url = "missing-0.1.0-py2.py3-none-any.whl" + +[metadata] +lock-version = "2.0" +python-versions = "*" +content-hash = "bec78476925e4cda6b22e91551ce4337264bdc3394c4f8297ad238f67a436d0e" diff --git a/tests/fixtures/missing_file_dependency/pyproject.toml b/tests/fixtures/missing_file_dependency/pyproject.toml new file mode 100644 index 00000000000..0be727e2104 --- /dev/null +++ b/tests/fixtures/missing_file_dependency/pyproject.toml @@ -0,0 +1,13 @@ +[tool.poetry] +name = "project-with-missing-directory-dependency" +version = "1.2.3" +description = "This is a description" +authors = ["Your Name "] +license = "MIT" +packages = [] + +[tool.poetry.dependencies] +python = "*" + +[tool.poetry.dev-dependencies] +missing = { file = "missing-0.1.0-py2.py3-none-any.whl" } diff --git a/tests/fixtures/old_lock_path_dependency/poetry.lock b/tests/fixtures/old_lock_path_dependency/poetry.lock new file mode 100644 index 00000000000..45c6e792d7f --- /dev/null +++ b/tests/fixtures/old_lock_path_dependency/poetry.lock @@ -0,0 +1,20 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "quix" +version = "1.2.3" +description = "Some description." +category = "main" +optional = false +python-versions = "~2.7 || ^3.4" +files = [] +develop = true + +[package.source] +type = "directory" +url = "quix" + +[metadata] +lock-version = "2.0" +python-versions = "^3.8" +content-hash = "d2e1cf4390093213432fb8b58f90774a5247bfe860dedc2b023b27accc14cfad" diff --git a/tests/fixtures/old_lock_path_dependency/pyproject.toml b/tests/fixtures/old_lock_path_dependency/pyproject.toml new file mode 100644 index 00000000000..00547cfee26 --- /dev/null +++ b/tests/fixtures/old_lock_path_dependency/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "foobar" +version = "0.1.0" +description = "" +authors = ["Poetry Developer "] + +[tool.poetry.dependencies] +python = "^3.8" +quix = { path = "./quix", develop = true} + +[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_path_dependency/quix/pyproject.toml b/tests/fixtures/old_lock_path_dependency/quix/pyproject.toml new file mode 100644 index 00000000000..1bec75cf124 --- /dev/null +++ b/tests/fixtures/old_lock_path_dependency/quix/pyproject.toml @@ -0,0 +1,11 @@ +[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" +sampleproject = ">=1.3.1" diff --git a/tests/fixtures/private_pyproject/pyproject.toml b/tests/fixtures/private_pyproject/pyproject.toml new file mode 100644 index 00000000000..a572e83c8ff --- /dev/null +++ b/tests/fixtures/private_pyproject/pyproject.toml @@ -0,0 +1,17 @@ +[tool.poetry] +name = "private" +version = "0.1.0" +description = "" +authors = ["Your Name "] +readme = "README.md" +classifiers = [ + "Private :: Do Not Upload", +] + + +[tool.poetry.dependencies] +python = "^3.7" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/scripts/README.md b/tests/fixtures/scripts/README.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/scripts/pyproject.toml b/tests/fixtures/scripts/pyproject.toml new file mode 100644 index 00000000000..06880366cc9 --- /dev/null +++ b/tests/fixtures/scripts/pyproject.toml @@ -0,0 +1,18 @@ +[tool.poetry] +name = "scripts" +version = "0.1.0" +description = "" +authors = ["Your Name "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.7" + +[tool.poetry.scripts] +check-argv0 = "scripts.check_argv0:main" +exit-code = "scripts.exit_code:main" +return-code = "scripts.return_code:main" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/scripts/scripts/__init__.py b/tests/fixtures/scripts/scripts/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/scripts/scripts/check_argv0.py b/tests/fixtures/scripts/scripts/check_argv0.py new file mode 100644 index 00000000000..e1dbc79d343 --- /dev/null +++ b/tests/fixtures/scripts/scripts/check_argv0.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +import sys + +from pathlib import Path + + +def main() -> int: + path = Path(sys.argv[0]) + if sys.argv[1] == "absolute": + if not path.is_absolute(): + raise RuntimeError(f"sys.argv[0] is not an absolute path: {path}") + if not path.exists(): + raise RuntimeError(f"sys.argv[0] does not exist: {path}") + else: + if path.is_absolute(): + raise RuntimeError(f"sys.argv[0] is an absolute path: {path}") + + return 0 + + +if __name__ == "__main__": + raise sys.exit(main()) diff --git a/tests/fixtures/scripts/scripts/exit_code.py b/tests/fixtures/scripts/scripts/exit_code.py new file mode 100644 index 00000000000..862e3956cbe --- /dev/null +++ b/tests/fixtures/scripts/scripts/exit_code.py @@ -0,0 +1,9 @@ +from __future__ import annotations + + +def main() -> None: + raise SystemExit(42) + + +if __name__ == "__main__": + main() diff --git a/tests/fixtures/scripts/scripts/return_code.py b/tests/fixtures/scripts/scripts/return_code.py new file mode 100644 index 00000000000..f645790ee37 --- /dev/null +++ b/tests/fixtures/scripts/scripts/return_code.py @@ -0,0 +1,9 @@ +from __future__ import annotations + + +def main() -> int: + return 42 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/fixtures/with_default_source/pyproject.toml b/tests/fixtures/with_default_source/pyproject.toml index 0d639ec25d7..7a274d6201d 100644 --- a/tests/fixtures/with_default_source/pyproject.toml +++ b/tests/fixtures/with_default_source/pyproject.toml @@ -58,4 +58,4 @@ my-script = "my_package:main" [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" -default = true +priority = "default" diff --git a/tests/fixtures/with_default_source_legacy/README.rst b/tests/fixtures/with_default_source_legacy/README.rst new file mode 100644 index 00000000000..f7fe15470f9 --- /dev/null +++ b/tests/fixtures/with_default_source_legacy/README.rst @@ -0,0 +1,2 @@ +My Package +========== diff --git a/tests/fixtures/with_default_source_legacy/pyproject.toml b/tests/fixtures/with_default_source_legacy/pyproject.toml new file mode 100644 index 00000000000..0d639ec25d7 --- /dev/null +++ b/tests/fixtures/with_default_source_legacy/pyproject.toml @@ -0,0 +1,61 @@ +[tool.poetry] +name = "my-package" +version = "1.2.3" +description = "Some description." +authors = [ + "Sébastien Eustace " +] +license = "MIT" + +readme = "README.rst" + +homepage = "https://python-poetry.org" +repository = "https://github.com/python-poetry/poetry" +documentation = "https://python-poetry.org/docs" + +keywords = ["packaging", "dependency", "poetry"] + +classifiers = [ + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Python Modules" +] + +# Requirements +[tool.poetry.dependencies] +python = "~2.7 || ^3.6" +cleo = "^0.6" +pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" } +requests = { version = "^2.18", optional = true, extras=[ "security" ] } +pathlib2 = { version = "^2.2", python = "~2.7" } + +orator = { version = "^0.9", optional = true } + +# File dependency +demo = { path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" } + +# Dir dependency with setup.py +my-package = { path = "../project_with_setup/" } + +# Dir dependency with pyproject.toml +simple-project = { path = "../simple_project/" } + + +[tool.poetry.extras] +db = [ "orator" ] + +[tool.poetry.dev-dependencies] +pytest = "~3.4" + + +[tool.poetry.scripts] +my-script = "my_package:main" + + +[tool.poetry.plugins."blogtool.parsers"] +".rst" = "some_module::SomeClass" + + +[[tool.poetry.source]] +name = "foo" +url = "https://foo.bar/simple/" +default = true diff --git a/tests/fixtures/with_explicit_source/pyproject.toml b/tests/fixtures/with_explicit_source/pyproject.toml new file mode 100644 index 00000000000..e19ad99bad4 --- /dev/null +++ b/tests/fixtures/with_explicit_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 = "explicit" +url = "https://explicit.com/simple/" +priority = "explicit" diff --git a/tests/fixtures/with_multiple_readme_files/README-1.rst b/tests/fixtures/with_multiple_readme_files/README-1.rst new file mode 100644 index 00000000000..265d70d6a73 --- /dev/null +++ b/tests/fixtures/with_multiple_readme_files/README-1.rst @@ -0,0 +1,2 @@ +Single Python +============= diff --git a/tests/fixtures/with_multiple_readme_files/README-2.rst b/tests/fixtures/with_multiple_readme_files/README-2.rst new file mode 100644 index 00000000000..a5693d97336 --- /dev/null +++ b/tests/fixtures/with_multiple_readme_files/README-2.rst @@ -0,0 +1,2 @@ +Changelog +========= diff --git a/tests/fixtures/with_multiple_readme_files/my_package/__init__.py b/tests/fixtures/with_multiple_readme_files/my_package/__init__.py new file mode 100644 index 00000000000..ceb22ae175a --- /dev/null +++ b/tests/fixtures/with_multiple_readme_files/my_package/__init__.py @@ -0,0 +1,6 @@ +"""Example module""" + +from __future__ import annotations + + +__version__ = "0.1" diff --git a/tests/fixtures/with_multiple_readme_files/pyproject.toml b/tests/fixtures/with_multiple_readme_files/pyproject.toml new file mode 100644 index 00000000000..7178a33c579 --- /dev/null +++ b/tests/fixtures/with_multiple_readme_files/pyproject.toml @@ -0,0 +1,19 @@ +[tool.poetry] +name = "my-package" +version = "0.1" +description = "Some description." +authors = [ + "Your Name " +] +license = "MIT" + +readme = [ + "README-1.rst", + "README-2.rst" +] + +homepage = "https://python-poetry.org" + + +[tool.poetry.dependencies] +python = "^2.7" diff --git a/tests/fixtures/with_non_default_multiple_secondary_sources/pyproject.toml b/tests/fixtures/with_non_default_multiple_secondary_sources/pyproject.toml index 933bee96912..d5db76c6094 100644 --- a/tests/fixtures/with_non_default_multiple_secondary_sources/pyproject.toml +++ b/tests/fixtures/with_non_default_multiple_secondary_sources/pyproject.toml @@ -16,9 +16,9 @@ python = "~2.7 || ^3.6" [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" -secondary = true +priority = "secondary" [[tool.poetry.source]] name = "bar" url = "https://bar.baz/simple/" -secondary = true +priority = "secondary" diff --git a/tests/fixtures/with_non_default_multiple_secondary_sources_legacy/pyproject.toml b/tests/fixtures/with_non_default_multiple_secondary_sources_legacy/pyproject.toml new file mode 100644 index 00000000000..933bee96912 --- /dev/null +++ b/tests/fixtures/with_non_default_multiple_secondary_sources_legacy/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 index 6cacb602e8b..e40dd03e66e 100644 --- a/tests/fixtures/with_non_default_multiple_sources/pyproject.toml +++ b/tests/fixtures/with_non_default_multiple_sources/pyproject.toml @@ -16,7 +16,7 @@ python = "~2.7 || ^3.6" [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" -secondary = true +priority = "secondary" [[tool.poetry.source]] name = "bar" diff --git a/tests/fixtures/with_non_default_multiple_sources_legacy/pyproject.toml b/tests/fixtures/with_non_default_multiple_sources_legacy/pyproject.toml new file mode 100644 index 00000000000..6cacb602e8b --- /dev/null +++ b/tests/fixtures/with_non_default_multiple_sources_legacy/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 index 453e3f9747f..5cce04b0591 100644 --- a/tests/fixtures/with_non_default_secondary_source/pyproject.toml +++ b/tests/fixtures/with_non_default_secondary_source/pyproject.toml @@ -16,4 +16,4 @@ python = "~2.7 || ^3.6" [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" -secondary = true +priority = "secondary" diff --git a/tests/fixtures/with_non_default_secondary_source_legacy/pyproject.toml b/tests/fixtures/with_non_default_secondary_source_legacy/pyproject.toml new file mode 100644 index 00000000000..453e3f9747f --- /dev/null +++ b/tests/fixtures/with_non_default_secondary_source_legacy/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_explicit/pyproject.toml b/tests/fixtures/with_non_default_source_explicit/pyproject.toml new file mode 100644 index 00000000000..23e7733cf34 --- /dev/null +++ b/tests/fixtures/with_non_default_source_explicit/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/" +priority = "primary" diff --git a/tests/fixtures/with_non_default_source/pyproject.toml b/tests/fixtures/with_non_default_source_implicit/pyproject.toml similarity index 100% rename from tests/fixtures/with_non_default_source/pyproject.toml rename to tests/fixtures/with_non_default_source_implicit/pyproject.toml diff --git a/tests/fixtures/with_path_dependency/bazz/setup.py b/tests/fixtures/with_path_dependency/bazz/setup.py new file mode 100644 index 00000000000..97223196678 --- /dev/null +++ b/tests/fixtures/with_path_dependency/bazz/setup.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from distutils.core import setup + + +setup( + name="bazz", + version="1", + py_modules=["demo"], + package_dir={"src": "src"}, + install_requires=["requests~=2.25.1"], +) diff --git a/tests/fixtures/with_path_dependency/poetry.lock b/tests/fixtures/with_path_dependency/poetry.lock new file mode 100644 index 00000000000..3c2e1b9b7c6 --- /dev/null +++ b/tests/fixtures/with_path_dependency/poetry.lock @@ -0,0 +1,98 @@ +# This file is automatically @generated by Poetry 1.4.0.dev0 and should not be changed by hand. + +[[package]] +name = "bazz" +version = "1" +description = "" +category = "main" +optional = false +python-versions = "*" +files = [] +develop = true + +[package.dependencies] +requests = ">=2.25.1,<2.26.0" + +[package.source] +type = "directory" +url = "bazz" + +[[package]] +name = "certifi" +version = "2022.12.7" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] + +[[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.*" +files = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] + +[[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.*" +files = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] + +[[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.*" +files = [ + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<5" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +security = ["cryptography (>=1.3.4)", "pyOpenSSL (>=0.14)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] + +[[package]] +name = "urllib3" +version = "1.26.14" +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.*, !=3.5.*" +files = [ + {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, + {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "81853eb3be9c7faf1950f23273d0fb3dae75833f6e4aa21b6c3038da25ba26f6" diff --git a/tests/fixtures/with_path_dependency/pyproject.toml b/tests/fixtures/with_path_dependency/pyproject.toml new file mode 100644 index 00000000000..705ba4fe229 --- /dev/null +++ b/tests/fixtures/with_path_dependency/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "foobar" +version = "0.1.0" +description = "" +authors = [] +readme = "README.md" +packages = [{include = "foobar"}] + +[tool.poetry.dependencies] +python = "^3.9" +bazz = { path = "./bazz", develop = true } + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/with_two_default_sources/pyproject.toml b/tests/fixtures/with_two_default_sources/pyproject.toml index 0d0a8a5446b..fa84b0a40a7 100644 --- a/tests/fixtures/with_two_default_sources/pyproject.toml +++ b/tests/fixtures/with_two_default_sources/pyproject.toml @@ -58,9 +58,9 @@ my-script = "my_package:main" [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" -default = true +priority = "default" [[tool.poetry.source]] name = "bar" url = "https://bar.foo/simple/" -default = true +priority = "default" diff --git a/tests/fixtures/with_two_default_sources_legacy/README.rst b/tests/fixtures/with_two_default_sources_legacy/README.rst new file mode 100644 index 00000000000..f7fe15470f9 --- /dev/null +++ b/tests/fixtures/with_two_default_sources_legacy/README.rst @@ -0,0 +1,2 @@ +My Package +========== diff --git a/tests/fixtures/with_two_default_sources_legacy/pyproject.toml b/tests/fixtures/with_two_default_sources_legacy/pyproject.toml new file mode 100644 index 00000000000..0d0a8a5446b --- /dev/null +++ b/tests/fixtures/with_two_default_sources_legacy/pyproject.toml @@ -0,0 +1,66 @@ +[tool.poetry] +name = "my-package" +version = "1.2.3" +description = "Some description." +authors = [ + "Sébastien Eustace " +] +license = "MIT" + +readme = "README.rst" + +homepage = "https://python-poetry.org" +repository = "https://github.com/python-poetry/poetry" +documentation = "https://python-poetry.org/docs" + +keywords = ["packaging", "dependency", "poetry"] + +classifiers = [ + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Python Modules" +] + +# Requirements +[tool.poetry.dependencies] +python = "~2.7 || ^3.6" +cleo = "^0.6" +pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" } +requests = { version = "^2.18", optional = true, extras=[ "security" ] } +pathlib2 = { version = "^2.2", python = "~2.7" } + +orator = { version = "^0.9", optional = true } + +# File dependency +demo = { path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" } + +# Dir dependency with setup.py +my-package = { path = "../project_with_setup/" } + +# Dir dependency with pyproject.toml +simple-project = { path = "../simple_project/" } + + +[tool.poetry.extras] +db = [ "orator" ] + +[tool.poetry.dev-dependencies] +pytest = "~3.4" + + +[tool.poetry.scripts] +my-script = "my_package:main" + + +[tool.poetry.plugins."blogtool.parsers"] +".rst" = "some_module::SomeClass" + + +[[tool.poetry.source]] +name = "foo" +url = "https://foo.bar/simple/" +default = true + +[[tool.poetry.source]] +name = "bar" +url = "https://bar.foo/simple/" +default = true diff --git a/tests/helpers.py b/tests/helpers.py index 2e57f1b2356..b21de6a551d 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -9,11 +9,9 @@ from pathlib import Path from typing import TYPE_CHECKING -from typing import Any 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.config.config import Config @@ -28,6 +26,8 @@ if TYPE_CHECKING: from collections.abc import Iterator + from typing import Any + from typing import Mapping from poetry.core.constraints.version import Version from poetry.core.packages.dependency import Dependency @@ -68,13 +68,6 @@ def get_dependency( return Factory.create_dependency(name, constraint or "*", groups=groups) -def fixture(path: str | None = None) -> Path: - if path: - return FIXTURE_PATH / path - else: - return FIXTURE_PATH - - def copy_or_symlink(source: Path, dest: Path) -> None: if dest.is_symlink() or dest.is_file(): dest.unlink() # missing_ok is only available in Python >= 3.8 @@ -113,7 +106,7 @@ def mock_clone( parsed = ParsedUrl.parse(url) path = re.sub(r"(.git)?$", "", parsed.pathname.lstrip("/")) - folder = Path(__file__).parent / "fixtures" / "git" / parsed.resource / path + folder = FIXTURE_PATH / "git" / parsed.resource / path if not source_root: source_root = Path(Config.create().get("cache-dir")) / "src" @@ -128,8 +121,7 @@ def mock_clone( def mock_download(url: str, dest: Path) -> None: parts = urllib.parse.urlparse(url) - fixtures = Path(__file__).parent / "fixtures" - fixture = fixtures / parts.path.lstrip("/") + fixture = FIXTURE_PATH / parts.path.lstrip("/") copy_or_symlink(fixture, dest) @@ -181,13 +173,13 @@ def reset_poetry(self) -> None: self._poetry.set_pool(poetry.pool) self._poetry.set_config(poetry.config) self._poetry.set_locker( - TestLocker(poetry.locker.lock.path, self._poetry.local_config) + TestLocker(poetry.locker.lock, self._poetry.local_config) ) class TestLocker(Locker): - def __init__(self, lock: str | Path, local_config: dict) -> None: - self._lock = TOMLFile(lock) + def __init__(self, lock: Path, local_config: dict) -> None: + self._lock = lock self._local_config = local_config self._lock_data = None self._content_hash = self._get_content_hash() @@ -284,3 +276,33 @@ def mock_metadata_entry_points( "entry_points", return_value=[make_entry_point_from_plugin(name, cls, dist)], ) + + +def flatten_dict(obj: Mapping[str, Any], delimiter: str = ".") -> Mapping[str, Any]: + """ + Flatten a nested dict. + + A flatdict replacement. + + :param obj: A nested dict to be flattened + :delimiter str: A delimiter used in the key path + :return: Flattened dict + """ + + def recurse_keys(obj: Mapping[str, Any]) -> Iterator[tuple[list[str], Any]]: + """ + A recursive generator to yield key paths and their values + + :param obj: A nested dict to be flattened + :return: dict + """ + if isinstance(obj, dict): + for key in obj: + for leaf in recurse_keys(obj[key]): + leaf_path, leaf_value = leaf + leaf_path.insert(0, key) + yield (leaf_path, leaf_value) + else: + yield ([], obj) + + return {delimiter.join(path): value for path, value in recurse_keys(obj)} diff --git a/tests/inspection/test_info.py b/tests/inspection/test_info.py index 389f2a739bf..4b1e0335944 100644 --- a/tests/inspection/test_info.py +++ b/tests/inspection/test_info.py @@ -1,6 +1,5 @@ from __future__ import annotations -from pathlib import Path from subprocess import CalledProcessError from typing import TYPE_CHECKING @@ -8,48 +7,48 @@ from poetry.inspection.info import PackageInfo from poetry.inspection.info import PackageInfoError -from poetry.utils._compat import decode from poetry.utils.env import EnvCommandError from poetry.utils.env import VirtualEnv if TYPE_CHECKING: + from pathlib import Path + from pytest_mock import MockerFixture -FIXTURE_DIR_BASE = Path(__file__).parent.parent / "fixtures" -FIXTURE_DIR_INSPECTIONS = FIXTURE_DIR_BASE / "inspection" + from tests.types import FixtureDirGetter @pytest.fixture(autouse=True) -def pep517_metadata_mock(): +def pep517_metadata_mock() -> None: pass @pytest.fixture -def demo_sdist() -> Path: - return FIXTURE_DIR_BASE / "distributions" / "demo-0.1.0.tar.gz" +def demo_sdist(fixture_dir: FixtureDirGetter) -> Path: + return fixture_dir("distributions") / "demo-0.1.0.tar.gz" @pytest.fixture -def demo_wheel() -> Path: - return FIXTURE_DIR_BASE / "distributions" / "demo-0.1.0-py2.py3-none-any.whl" +def demo_wheel(fixture_dir: FixtureDirGetter) -> Path: + return fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl" @pytest.fixture def source_dir(tmp_path: Path) -> Path: - return Path(tmp_path.as_posix()) + path = tmp_path / "source" + path.mkdir() + return path @pytest.fixture def demo_setup(source_dir: Path) -> Path: setup_py = source_dir / "setup.py" setup_py.write_text( - decode( - "from setuptools import setup; " - 'setup(name="demo", ' - 'version="0.1.0", ' - 'install_requires=["package"])' - ) + "from setuptools import setup; " + 'setup(name="demo", ' + 'version="0.1.0", ' + 'install_requires=["package"])' ) return source_dir @@ -58,16 +57,14 @@ def demo_setup(source_dir: Path) -> Path: def demo_setup_cfg(source_dir: Path) -> Path: setup_cfg = source_dir / "setup.cfg" setup_cfg.write_text( - decode( - "\n".join( - [ - "[metadata]", - "name = demo", - "version = 0.1.0", - "[options]", - "install_requires = package", - ] - ) + "\n".join( + [ + "[metadata]", + "name = demo", + "version = 0.1.0", + "[options]", + "install_requires = package", + ] ) ) return source_dir @@ -77,12 +74,10 @@ def demo_setup_cfg(source_dir: Path) -> Path: def demo_setup_complex(source_dir: Path) -> Path: setup_py = source_dir / "setup.py" setup_py.write_text( - decode( - "from setuptools import setup; " - 'setup(name="demo", ' - 'version="0.1.0", ' - 'install_requires=[i for i in ["package"]])' - ) + "from setuptools import setup; " + 'setup(name="demo", ' + 'version="0.1.0", ' + 'install_requires=[i for i in ["package"]])' ) return source_dir @@ -90,50 +85,59 @@ def demo_setup_complex(source_dir: Path) -> Path: @pytest.fixture 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]\nrequires = ["setuptools", "wheel"]') - ) + pyproject_toml.write_text('[build-system]\nrequires = ["setuptools", "wheel"]') return demo_setup_complex -def demo_check_info(info: PackageInfo, requires_dist: set[str] = None) -> None: +def demo_check_info(info: PackageInfo, requires_dist: set[str] | None = None) -> None: assert info.name == "demo" assert info.version == "0.1.0" assert info.requires_dist - requires_dist = requires_dist or { - 'cleo; extra == "foo"', - "pendulum (>=1.4.4)", - 'tomlkit; extra == "bar"', - } - assert set(info.requires_dist) == requires_dist + if requires_dist: + assert set(info.requires_dist) == requires_dist + else: + assert set(info.requires_dist) in ( + # before https://github.com/python-poetry/poetry-core/pull/510 + { + 'cleo; extra == "foo"', + "pendulum (>=1.4.4)", + 'tomlkit; extra == "bar"', + }, + # after https://github.com/python-poetry/poetry-core/pull/510 + { + 'cleo ; extra == "foo"', + "pendulum (>=1.4.4)", + 'tomlkit ; extra == "bar"', + }, + ) -def test_info_from_sdist(demo_sdist: Path): +def test_info_from_sdist(demo_sdist: Path) -> None: info = PackageInfo.from_sdist(demo_sdist) demo_check_info(info) -def test_info_from_wheel(demo_wheel: Path): +def test_info_from_wheel(demo_wheel: Path) -> None: info = PackageInfo.from_wheel(demo_wheel) demo_check_info(info) -def test_info_from_bdist(demo_wheel: Path): +def test_info_from_bdist(demo_wheel: Path) -> None: info = PackageInfo.from_bdist(demo_wheel) demo_check_info(info) -def test_info_from_poetry_directory(): +def test_info_from_poetry_directory(fixture_dir: FixtureDirGetter) -> None: info = PackageInfo.from_directory( - FIXTURE_DIR_INSPECTIONS / "demo", disable_build=True + fixture_dir("inspection") / "demo", disable_build=True ) demo_check_info(info) def test_info_from_poetry_directory_fallback_on_poetry_create_error( - mocker: MockerFixture, -): + mocker: MockerFixture, fixture_dir: FixtureDirGetter +) -> None: mock_create_poetry = mocker.patch( "poetry.inspection.info.Factory.create_poetry", side_effect=RuntimeError ) @@ -142,33 +146,34 @@ def test_info_from_poetry_directory_fallback_on_poetry_create_error( "poetry.inspection.info.get_pep517_metadata" ) - PackageInfo.from_directory(FIXTURE_DIR_INSPECTIONS / "demo_poetry_package") + PackageInfo.from_directory(fixture_dir("inspection") / "demo_poetry_package") assert mock_create_poetry.call_count == 1 assert mock_get_poetry_package.call_count == 1 assert mock_get_pep517_metadata.call_count == 1 -def test_info_from_requires_txt(): +def test_info_from_requires_txt(fixture_dir: FixtureDirGetter) -> None: info = PackageInfo.from_metadata( - FIXTURE_DIR_INSPECTIONS / "demo_only_requires_txt.egg-info" + fixture_dir("inspection") / "demo_only_requires_txt.egg-info" ) + assert info is not None demo_check_info(info) -def test_info_from_setup_py(demo_setup: Path): +def test_info_from_setup_py(demo_setup: Path) -> None: info = PackageInfo.from_setup_files(demo_setup) demo_check_info(info, requires_dist={"package"}) -def test_info_from_setup_cfg(demo_setup_cfg: Path): +def test_info_from_setup_cfg(demo_setup_cfg: Path) -> None: info = PackageInfo.from_setup_files(demo_setup_cfg) demo_check_info(info, requires_dist={"package"}) -def test_info_no_setup_pkg_info_no_deps(): +def test_info_no_setup_pkg_info_no_deps(fixture_dir: FixtureDirGetter) -> None: info = PackageInfo.from_directory( - FIXTURE_DIR_INSPECTIONS / "demo_no_setup_pkg_info_no_deps", + fixture_dir("inspection") / "demo_no_setup_pkg_info_no_deps", disable_build=True, ) assert info.name == "demo" @@ -176,28 +181,28 @@ def test_info_no_setup_pkg_info_no_deps(): assert info.requires_dist is None -def test_info_setup_simple(mocker: MockerFixture, demo_setup: Path): +def test_info_setup_simple(mocker: MockerFixture, demo_setup: Path) -> None: spy = mocker.spy(VirtualEnv, "run") info = PackageInfo.from_directory(demo_setup) assert spy.call_count == 0 demo_check_info(info, requires_dist={"package"}) -def test_info_setup_cfg(mocker: MockerFixture, demo_setup_cfg: Path): +def test_info_setup_cfg(mocker: MockerFixture, demo_setup_cfg: Path) -> None: spy = mocker.spy(VirtualEnv, "run") info = PackageInfo.from_directory(demo_setup_cfg) assert spy.call_count == 0 demo_check_info(info, requires_dist={"package"}) -def test_info_setup_complex(demo_setup_complex: Path): +def test_info_setup_complex(demo_setup_complex: Path) -> None: info = PackageInfo.from_directory(demo_setup_complex) demo_check_info(info, requires_dist={"package"}) def test_info_setup_complex_pep517_error( mocker: MockerFixture, demo_setup_complex: Path -): +) -> None: mocker.patch( "poetry.utils.env.VirtualEnv.run", autospec=True, @@ -208,14 +213,16 @@ def test_info_setup_complex_pep517_error( PackageInfo.from_directory(demo_setup_complex) -def test_info_setup_complex_pep517_legacy(demo_setup_complex_pep517_legacy: Path): +def test_info_setup_complex_pep517_legacy( + demo_setup_complex_pep517_legacy: Path, +) -> None: info = PackageInfo.from_directory(demo_setup_complex_pep517_legacy) demo_check_info(info, requires_dist={"package"}) def test_info_setup_complex_disable_build( mocker: MockerFixture, demo_setup_complex: Path -): +) -> None: spy = mocker.spy(VirtualEnv, "run") info = PackageInfo.from_directory(demo_setup_complex, disable_build=True) assert spy.call_count == 0 @@ -227,7 +234,7 @@ def test_info_setup_complex_disable_build( @pytest.mark.parametrize("missing", ["version", "name", "install_requires"]) def test_info_setup_missing_mandatory_should_trigger_pep517( mocker: MockerFixture, source_dir: Path, missing: str -): +) -> None: setup = "from setuptools import setup; " setup += "setup(" setup += 'name="demo", ' if missing != "name" else "" @@ -236,19 +243,15 @@ def test_info_setup_missing_mandatory_should_trigger_pep517( setup += ")" setup_py = source_dir / "setup.py" - setup_py.write_text(decode(setup)) + setup_py.write_text(setup) spy = mocker.spy(VirtualEnv, "run") - try: - PackageInfo.from_directory(source_dir) - except PackageInfoError: - assert spy.call_count == 3 - else: - assert spy.call_count == 1 + _ = PackageInfo.from_directory(source_dir) + assert spy.call_count == 1 -def test_info_prefer_poetry_config_over_egg_info(): +def test_info_prefer_poetry_config_over_egg_info(fixture_dir: FixtureDirGetter) -> None: info = PackageInfo.from_directory( - FIXTURE_DIR_INSPECTIONS / "demo_with_obsolete_egg_info" + fixture_dir("inspection") / "demo_with_obsolete_egg_info" ) demo_check_info(info) diff --git a/tests/installation/fixtures/with-pypi-repository.test b/tests/installation/fixtures/with-pypi-repository.test index 03444764ee9..de60c4ffce2 100644 --- a/tests/installation/fixtures/with-pypi-repository.test +++ b/tests/installation/fixtures/with-pypi-repository.test @@ -1,3 +1,5 @@ +# This file is automatically @generated by Poetry 1.5.0.dev0 and should not be changed by hand. + [[package]] name = "attrs" version = "17.4.0" @@ -5,14 +7,10 @@ description = "Classes Without Boilerplate" category = "dev" optional = false python-versions = "*" - -[[package.files]] -file = "attrs-17.4.0-py2.py3-none-any.whl" -hash = "sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450" - -[[package.files]] -file = "attrs-17.4.0.tar.gz" -hash = "sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9" +files = [ + {file = "attrs-17.4.0-py2.py3-none-any.whl", hash = "sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450"}, + {file = "attrs-17.4.0.tar.gz", hash = "sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9"}, +] [package.extras] dev = ["coverage", "hypothesis", "pympler", "pytest", "six", "sphinx", "zope.interface", "zope.interface"] @@ -26,30 +24,10 @@ description = "Cross-platform colored terminal text." category = "dev" optional = false python-versions = "*" - -[[package.files]] -file = "colorama-0.3.9-py2.py3-none-any.whl" -hash = "sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda" - -[[package.files]] -file = "colorama-0.3.9.tar.gz" -hash = "sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1" - -[[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.files]] -file = "funcsigs-1.0.2-py2.py3-none-any.whl" -hash = "sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca" - -[[package.files]] -file = "funcsigs-1.0.2.tar.gz" -hash = "sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50" +files = [ + {file = "colorama-0.3.9-py2.py3-none-any.whl", hash = "sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda"}, + {file = "colorama-0.3.9.tar.gz", hash = "sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"}, +] [[package]] name = "more-itertools" @@ -58,18 +36,11 @@ description = "More routines for operating on iterables, beyond itertools" category = "dev" optional = false python-versions = "*" - -[[package.files]] -file = "more-itertools-4.1.0.tar.gz" -hash = "sha256:c9ce7eccdcb901a2c75d326ea134e0886abfbea5f93e91cc95de9507c0816c44" - -[[package.files]] -file = "more_itertools-4.1.0-py2-none-any.whl" -hash = "sha256:11a625025954c20145b37ff6309cd54e39ca94f72f6bb9576d1195db6fa2442e" - -[[package.files]] -file = "more_itertools-4.1.0-py3-none-any.whl" -hash = "sha256:0dd8f72eeab0d2c3bd489025bb2f6a1b8342f9b198f6fc37b52d15cfa4531fea" +files = [ + {file = "more-itertools-4.1.0.tar.gz", hash = "sha256:c9ce7eccdcb901a2c75d326ea134e0886abfbea5f93e91cc95de9507c0816c44"}, + {file = "more_itertools-4.1.0-py2-none-any.whl", hash = "sha256:11a625025954c20145b37ff6309cd54e39ca94f72f6bb9576d1195db6fa2442e"}, + {file = "more_itertools-4.1.0-py3-none-any.whl", hash = "sha256:0dd8f72eeab0d2c3bd489025bb2f6a1b8342f9b198f6fc37b52d15cfa4531fea"}, +] [package.dependencies] six = ">=1.0.0,<2.0.0" @@ -81,10 +52,9 @@ description = "plugin and hook calling mechanisms for python" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package.files]] -file = "pluggy-0.6.0.tar.gz" -hash = "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff" +files = [ + {file = "pluggy-0.6.0.tar.gz", hash = "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff"}, +] [[package]] name = "py" @@ -93,14 +63,10 @@ description = "library with cross-python path, ini-parsing, io, code, log facili category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package.files]] -file = "py-1.5.3-py2.py3-none-any.whl" -hash = "sha256:983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a" - -[[package.files]] -file = "py-1.5.3.tar.gz" -hash = "sha256:29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881" +files = [ + {file = "py-1.5.3-py2.py3-none-any.whl", hash = "sha256:983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a"}, + {file = "py-1.5.3.tar.gz", hash = "sha256:29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881"}, +] [[package]] name = "pytest" @@ -109,44 +75,36 @@ description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package.files]] -file = "pytest-3.5.0-py2.py3-none-any.whl" -hash = "sha256:6266f87ab64692112e5477eba395cfedda53b1933ccd29478e671e73b420c19c" - -[[package.files]] -file = "pytest-3.5.0.tar.gz" -hash = "sha256:fae491d1874f199537fd5872b5e1f0e74a009b979df9d53d1553fd03da1703e1" +files = [ + {file = "pytest-3.5.0-py2.py3-none-any.whl", hash = "sha256:6266f87ab64692112e5477eba395cfedda53b1933ccd29478e671e73b420c19c"}, + {file = "pytest-3.5.0.tar.gz", hash = "sha256:fae491d1874f199537fd5872b5e1f0e74a009b979df9d53d1553fd03da1703e1"}, +] [package.dependencies] -py = ">=1.5.0" -six = ">=1.10.0" attrs = ">=17.4.0" -setuptools = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} more-itertools = ">=4.0.0" pluggy = ">=0.5,<0.7" -funcsigs = {"version" = "*", "markers" = "python_version < \"3.0\""} -colorama = {"version" = "*", "markers" = "sys_platform == \"win32\""} +py = ">=1.5.0" +setuptools = "*" +six = ">=1.10.0" [[package]] name = "setuptools" -version = "39.2.0" +version = "67.6.1" 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.files]] -file = "setuptools-39.2.0-py2.py3-none-any.whl" -hash = "sha256:8fca9275c89964f13da985c3656cb00ba029d7f3916b37990927ffdf264e7926" - -[[package.files]] -file = "setuptools-39.2.0.zip" -hash = "sha256:f7cddbb5f5c640311eb00eab6e849f7701fa70bf6a183fc8a2c33dd1d1672fb2" +python-versions = ">=3.7" +files = [ + {file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"}, + {file = "setuptools-67.6.1.tar.gz", hash = "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a"}, +] [package.extras] -certs = ["certifi (==2016.9.26)"] -ssl = ["wincertstore (==0.2)"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" @@ -155,16 +113,12 @@ description = "Python 2 and 3 compatibility utilities" category = "dev" optional = false python-versions = "*" - -[[package.files]] -file = "six-1.11.0-py2.py3-none-any.whl" -hash = "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" - -[[package.files]] -file = "six-1.11.0.tar.gz" -hash = "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9" +files = [ + {file = "six-1.11.0-py2.py3-none-any.whl", hash = "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"}, + {file = "six-1.11.0.tar.gz", hash = "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9"}, +] [metadata] -python-versions = "*" lock-version = "2.0" +python-versions = ">=3.7" content-hash = "123456789" diff --git a/tests/installation/fixtures/with-same-version-url-dependencies.test b/tests/installation/fixtures/with-same-version-url-dependencies.test index e80c72a3d03..52a3690e887 100644 --- a/tests/installation/fixtures/with-same-version-url-dependencies.test +++ b/tests/installation/fixtures/with-same-version-url-dependencies.test @@ -5,7 +5,9 @@ description = "" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [] +files = [ + {file = "demo-0.1.0-py2.py3-none-any.whl", hash = "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a"} +] [package.source] type = "url" @@ -25,8 +27,9 @@ description = "" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [] - +files = [ + {file = "demo-0.1.0.tar.gz", hash = "sha256:9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad"} +] [package.source] type = "url" url = "https://python-poetry.org/distributions/demo-0.1.0.tar.gz" diff --git a/tests/installation/fixtures/with-url-dependency.test b/tests/installation/fixtures/with-url-dependency.test index 13cab35bc46..e6878546942 100644 --- a/tests/installation/fixtures/with-url-dependency.test +++ b/tests/installation/fixtures/with-url-dependency.test @@ -5,7 +5,9 @@ description = "" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [] +files = [ + {file = "demo-0.1.0-py2.py3-none-any.whl", hash = "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a"} +] [package.source] type = "url" diff --git a/tests/installation/test_chef.py b/tests/installation/test_chef.py index 6af8f682eea..ca00ba01e73 100644 --- a/tests/installation/test_chef.py +++ b/tests/installation/test_chef.py @@ -1,105 +1,125 @@ from __future__ import annotations +import os +import tempfile + from pathlib import Path from typing import TYPE_CHECKING +from zipfile import ZipFile import pytest -from packaging.tags import Tag from poetry.core.packages.utils.link import Link +from poetry.factory import Factory from poetry.installation.chef import Chef -from poetry.utils.env import MockEnv +from poetry.repositories import RepositoryPool +from poetry.utils.cache import ArtifactCache +from poetry.utils.env import EnvManager +from tests.repositories.test_pypi_repository import MockRepository if TYPE_CHECKING: from pytest_mock import MockerFixture from tests.conftest import Config + from tests.types import FixtureDirGetter -@pytest.mark.parametrize( - ("link", "cached"), - [ - ( - "https://files.python-poetry.org/demo-0.1.0.tar.gz", - "/cache/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", - ), - ( - "https://example.com/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", - "/cache/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", - ), - ], -) -def test_get_cached_archive_for_link( - config: Config, mocker: MockerFixture, link: str, cached: str -): - chef = Chef( - config, - MockEnv( - version_info=(3, 8, 3), - marker_env={"interpreter_name": "cpython", "interpreter_version": "3.8.3"}, - supported_tags=[ - Tag("cp38", "cp38", "macosx_10_15_x86_64"), - Tag("py3", "none", "any"), - ], - ), - ) +@pytest.fixture() +def pool() -> RepositoryPool: + pool = RepositoryPool() + + pool.add_repository(MockRepository()) + + return pool - mocker.patch.object( - chef, - "get_cached_archives_for_link", - return_value=[ - Path("/cache/demo-0.1.0-py2.py3-none-any"), - Path("/cache/demo-0.1.0.tar.gz"), - Path("/cache/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl"), - Path("/cache/demo-0.1.0-cp37-cp37-macosx_10_15_x86_64.whl"), - ], - ) - archive = chef.get_cached_archive_for_link(Link(link)) +@pytest.fixture(autouse=True) +def setup(mocker: MockerFixture, pool: RepositoryPool) -> None: + mocker.patch.object(Factory, "create_pool", return_value=pool) - assert Path(cached) == archive +@pytest.fixture +def artifact_cache(config: Config) -> ArtifactCache: + return ArtifactCache(cache_dir=config.artifacts_cache_directory) -def test_get_cached_archives_for_link(config: Config, mocker: MockerFixture): + +def test_prepare_sdist( + config: Config, + config_cache_dir: Path, + artifact_cache: ArtifactCache, + fixture_dir: FixtureDirGetter, +) -> None: chef = Chef( - config, - MockEnv( - marker_env={"interpreter_name": "cpython", "interpreter_version": "3.8.3"} - ), + artifact_cache, EnvManager.get_system_env(), Factory.create_pool(config) ) + archive = (fixture_dir("distributions") / "demo-0.1.0.tar.gz").resolve() + destination = artifact_cache.get_cache_directory_for_link(Link(archive.as_uri())) - distributions = Path(__file__).parent.parent.joinpath("fixtures/distributions") - mocker.patch.object( - chef, - "get_cache_directory_for_link", - return_value=distributions, - ) + wheel = chef.prepare(archive) + + assert wheel.parent == destination + assert wheel.name == "demo-0.1.0-py3-none-any.whl" - archives = chef.get_cached_archives_for_link( - Link("https://files.python-poetry.org/demo-0.1.0.tar.gz") + +def test_prepare_directory( + config: Config, + config_cache_dir: Path, + artifact_cache: ArtifactCache, + fixture_dir: FixtureDirGetter, +) -> None: + chef = Chef( + artifact_cache, EnvManager.get_system_env(), Factory.create_pool(config) ) + archive = fixture_dir("simple_project").resolve() - assert archives - assert set(archives) == {Path(path) for path in distributions.glob("demo-0.1.0*")} + wheel = chef.prepare(archive) + assert wheel.name == "simple_project-1.2.3-py2.py3-none-any.whl" -def test_get_cache_directory_for_link(config: Config, config_cache_dir: Path): + assert wheel.parent.parent == Path(tempfile.gettempdir()) + # cleanup generated tmp dir artifact + os.unlink(wheel) + + +def test_prepare_directory_with_extensions( + config: Config, + config_cache_dir: Path, + artifact_cache: ArtifactCache, + fixture_dir: FixtureDirGetter, +) -> None: + env = EnvManager.get_system_env() + chef = Chef(artifact_cache, env, Factory.create_pool(config)) + archive = fixture_dir("extended_with_no_setup").resolve() + + wheel = chef.prepare(archive) + + assert wheel.parent.parent == Path(tempfile.gettempdir()) + assert wheel.name == f"extended-0.1-{env.supported_tags[0]}.whl" + + # cleanup generated tmp dir artifact + os.unlink(wheel) + + +def test_prepare_directory_editable( + config: Config, + config_cache_dir: Path, + artifact_cache: ArtifactCache, + fixture_dir: FixtureDirGetter, +) -> None: chef = Chef( - config, - MockEnv( - marker_env={"interpreter_name": "cpython", "interpreter_version": "3.8.3"} - ), + artifact_cache, EnvManager.get_system_env(), Factory.create_pool(config) ) + archive = fixture_dir("simple_project").resolve() - directory = chef.get_cache_directory_for_link( - Link("https://files.python-poetry.org/poetry-1.1.0.tar.gz") - ) + wheel = chef.prepare(archive, editable=True) - expected = Path( - f"{config_cache_dir.as_posix()}/artifacts/ba/63/13/" - "283a3b3b7f95f05e9e6f84182d276f7bb0951d5b0cc24422b33f7a4648" - ) + assert wheel.parent.parent == Path(tempfile.gettempdir()) + assert wheel.name == "simple_project-1.2.3-py2.py3-none-any.whl" + + with ZipFile(wheel) as z: + assert "simple_project.pth" in z.namelist() - assert directory == expected + # cleanup generated tmp dir artifact + os.unlink(wheel) diff --git a/tests/installation/test_chooser.py b/tests/installation/test_chooser.py index b4a8d36bc26..fbd3861ad05 100644 --- a/tests/installation/test_chooser.py +++ b/tests/installation/test_chooser.py @@ -13,8 +13,8 @@ from poetry.installation.chooser import Chooser from poetry.repositories.legacy_repository import LegacyRepository -from poetry.repositories.pool import Pool from poetry.repositories.pypi_repository import PyPiRepository +from poetry.repositories.repository_pool import RepositoryPool from poetry.utils.env import MockEnv @@ -58,7 +58,7 @@ def callback( fixture = JSON_FIXTURES / (name + ".json") if not fixture.exists(): - return + return None with fixture.open(encoding="utf-8") as f: return [200, headers, f.read()] @@ -98,7 +98,7 @@ def callback( parts = uri.rsplit("/") name = parts[-2] - fixture = LEGACY_FIXTURES / (name + "_partial_yank" + ".html") + fixture = LEGACY_FIXTURES / (name + "-partial-yank" + ".html") with fixture.open(encoding="utf-8") as f: return [200, headers, f.read()] @@ -111,8 +111,8 @@ def callback( @pytest.fixture() -def pool() -> Pool: - pool = Pool() +def pool() -> RepositoryPool: + pool = RepositoryPool() pool.add_repository(PyPiRepository(disable_cache=True)) pool.add_repository( @@ -127,8 +127,12 @@ def pool() -> Pool: @pytest.mark.parametrize("source_type", ["", "legacy"]) def test_chooser_chooses_universal_wheel_link_if_available( - env: MockEnv, mock_pypi: None, mock_legacy: None, source_type: str, pool: Pool -): + env: MockEnv, + mock_pypi: None, + mock_legacy: None, + source_type: str, + pool: RepositoryPool, +) -> None: chooser = Chooser(pool, env) package = Package("pytest", "3.5.0") @@ -162,11 +166,11 @@ def test_chooser_no_binary_policy( mock_pypi: None, mock_legacy: None, source_type: str, - pool: Pool, + pool: RepositoryPool, policy: str, filename: str, config: Config, -): +) -> None: config.merge({"installer": {"no-binary": policy.split(",")}}) chooser = Chooser(pool, env, config) @@ -188,8 +192,12 @@ def test_chooser_no_binary_policy( @pytest.mark.parametrize("source_type", ["", "legacy"]) def test_chooser_chooses_specific_python_universal_wheel_link_if_available( - env: MockEnv, mock_pypi: None, mock_legacy: None, source_type: str, pool: Pool -): + env: MockEnv, + mock_pypi: None, + mock_legacy: None, + source_type: str, + pool: RepositoryPool, +) -> None: chooser = Chooser(pool, env) package = Package("isort", "4.3.4") @@ -209,8 +217,8 @@ def test_chooser_chooses_specific_python_universal_wheel_link_if_available( @pytest.mark.parametrize("source_type", ["", "legacy"]) def test_chooser_chooses_system_specific_wheel_link_if_available( - mock_pypi: None, mock_legacy: None, source_type: str, pool: Pool -): + mock_pypi: None, mock_legacy: None, source_type: str, pool: RepositoryPool +) -> None: env = MockEnv( supported_tags=[Tag("cp37", "cp37m", "win32"), Tag("py3", "none", "any")] ) @@ -237,8 +245,8 @@ def test_chooser_chooses_sdist_if_no_compatible_wheel_link_is_available( mock_pypi: None, mock_legacy: None, source_type: str, - pool: Pool, -): + pool: RepositoryPool, +) -> None: chooser = Chooser(pool, env) package = Package("pyyaml", "3.13.0") @@ -262,8 +270,8 @@ def test_chooser_chooses_distributions_that_match_the_package_hashes( mock_pypi: None, mock_legacy: None, source_type: str, - pool: Pool, -): + pool: RepositoryPool, +) -> None: chooser = Chooser(pool, env) package = Package("isort", "4.3.4") @@ -295,7 +303,7 @@ def test_chooser_chooses_yanked_if_no_others( mock_pypi: None, mock_legacy: None, source_type: str, - pool: Pool, + pool: RepositoryPool, ) -> None: chooser = Chooser(pool, env) @@ -326,7 +334,7 @@ def test_chooser_chooses_yanked_if_no_others( def test_chooser_does_not_choose_yanked_if_others( mock_legacy: None, mock_legacy_partial_yank: None, - pool: Pool, + pool: RepositoryPool, ) -> None: chooser = Chooser(pool, MockEnv(supported_tags=[Tag("py2", "none", "any")])) @@ -372,8 +380,8 @@ def test_chooser_throws_an_error_if_package_hashes_do_not_match( mock_pypi: None, mock_legacy: None, source_type: None, - pool: Pool, -): + pool: RepositoryPool, +) -> None: chooser = Chooser(pool, env) package = Package("isort", "4.3.4") diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py index a5aa06b110e..18d5cd1b60c 100644 --- a/tests/installation/test_executor.py +++ b/tests/installation/test_executor.py @@ -4,24 +4,37 @@ import json import re import shutil +import tempfile from pathlib import Path +from subprocess import CalledProcessError from typing import TYPE_CHECKING from typing import Any +from typing import Callable +from urllib.parse import urlparse import pytest +from build import BuildBackendException +from build import ProjectBuilder from cleo.formatters.style import Style from cleo.io.buffered_io import BufferedIO +from cleo.io.outputs.output import Verbosity from poetry.core.packages.package import Package from poetry.core.packages.utils.link import Link +from poetry.core.packages.utils.utils import path_to_url +from poetry.factory import Factory +from poetry.installation.chef import Chef as BaseChef 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.installation.wheel_installer import WheelInstaller +from poetry.repositories.repository_pool import RepositoryPool +from poetry.utils.cache import ArtifactCache from poetry.utils.env import MockEnv +from poetry.vcs.git.backend import Git from tests.repositories.test_pypi_repository import MockRepository @@ -37,15 +50,55 @@ from tests.types import FixtureDirGetter +class Chef(BaseChef): + _directory_wheels: list[Path] | None = None + _sdist_wheels: list[Path] | None = None + + def set_directory_wheel(self, wheels: Path | list[Path]) -> None: + if not isinstance(wheels, list): + wheels = [wheels] + + self._directory_wheels = wheels + + def set_sdist_wheel(self, wheels: Path | list[Path]) -> None: + if not isinstance(wheels, list): + wheels = [wheels] + + self._sdist_wheels = wheels + + def _prepare_sdist(self, archive: Path, destination: Path | None = None) -> Path: + if self._sdist_wheels is not None: + wheel = self._sdist_wheels.pop(0) + self._sdist_wheels.append(wheel) + + return wheel + + return super()._prepare_sdist(archive) + + def _prepare( + self, directory: Path, destination: Path, *, editable: bool = False + ) -> Path: + if self._directory_wheels is not None: + wheel = self._directory_wheels.pop(0) + self._directory_wheels.append(wheel) + + destination.mkdir(parents=True, exist_ok=True) + dst_wheel = destination / wheel.name + shutil.copyfile(wheel, dst_wheel) + return dst_wheel + + return super()._prepare(directory, destination, editable=editable) + + @pytest.fixture -def env(tmp_dir: str) -> MockEnv: - path = Path(tmp_dir) / ".venv" +def env(tmp_path: Path) -> MockEnv: + path = tmp_path / ".venv" path.mkdir(parents=True) return MockEnv(path=path, is_venv=True) -@pytest.fixture() +@pytest.fixture def io() -> BufferedIO: io = BufferedIO() io.output.formatter.set_style("c1_dark", Style("cyan", options=["dark"])) @@ -56,7 +109,7 @@ def io() -> BufferedIO: return io -@pytest.fixture() +@pytest.fixture def io_decorated() -> BufferedIO: io = BufferedIO(decorated=True) io.output.formatter.set_style("c1", Style("cyan")) @@ -65,32 +118,48 @@ def io_decorated() -> BufferedIO: return io -@pytest.fixture() +@pytest.fixture def io_not_decorated() -> BufferedIO: io = BufferedIO(decorated=False) return io -@pytest.fixture() -def pool() -> Pool: - pool = Pool() +@pytest.fixture +def pool() -> RepositoryPool: + pool = RepositoryPool() pool.add_repository(MockRepository()) return pool -@pytest.fixture() -def mock_file_downloads(http: type[httpretty.httpretty]) -> None: +@pytest.fixture +def artifact_cache(config: Config) -> ArtifactCache: + return ArtifactCache(cache_dir=config.artifacts_cache_directory) + + +@pytest.fixture +def mock_file_downloads( + http: type[httpretty.httpretty], fixture_dir: FixtureDirGetter +) -> None: def callback( request: HTTPrettyRequest, uri: str, headers: dict[str, Any] ) -> list[int | dict[str, Any] | str]: + name = Path(urlparse(uri).path).name + fixture = Path(__file__).parent.parent.joinpath( - "fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl" + "repositories/fixtures/pypi.org/dists/" + name ) - with fixture.open("rb") as f: - return [200, headers, f.read()] + if not fixture.exists(): + fixture = fixture_dir("distributions") / name + + if not fixture.exists(): + fixture = ( + fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl" + ) + + return [200, headers, fixture.read_bytes()] http.register_uri( http.GET, @@ -99,29 +168,62 @@ def callback( ) +@pytest.fixture +def copy_wheel(tmp_path: Path, fixture_dir: FixtureDirGetter) -> Callable[[], Path]: + def _copy_wheel() -> Path: + tmp_name = tempfile.mktemp() + (tmp_path / tmp_name).mkdir() + + shutil.copyfile( + fixture_dir("distributions") / "demo-0.1.2-py2.py3-none-any.whl", + tmp_path / tmp_name / "demo-0.1.2-py2.py3-none-any.whl", + ) + return tmp_path / tmp_name / "demo-0.1.2-py2.py3-none-any.whl" + + return _copy_wheel + + +@pytest.fixture +def wheel(copy_wheel: Callable[[], Path]) -> Path: + archive = copy_wheel() + + yield archive + + if archive.exists(): + archive.unlink() + + def test_execute_executes_a_batch_of_operations( mocker: MockerFixture, config: Config, - pool: Pool, + pool: RepositoryPool, io: BufferedIO, - tmp_dir: str, + tmp_path: Path, mock_file_downloads: None, env: MockEnv, -): - pip_install = mocker.patch("poetry.installation.executor.pip_install") + copy_wheel: Callable[[], Path], + fixture_dir: FixtureDirGetter, +) -> None: + wheel_install = mocker.patch.object(WheelInstaller, "install") - config.merge({"cache-dir": tmp_dir}) + config.merge({"cache-dir": str(tmp_path)}) + artifact_cache = ArtifactCache(cache_dir=config.artifacts_cache_directory) + + prepare_spy = mocker.spy(Chef, "_prepare") + chef = Chef(artifact_cache, env, Factory.create_pool(config)) + chef.set_directory_wheel([copy_wheel(), copy_wheel()]) + chef.set_sdist_wheel(copy_wheel()) + + io.set_verbosity(Verbosity.VERY_VERBOSE) executor = Executor(env, pool, config, io) + executor._chef = chef file_package = Package( "demo", "0.1.0", source_type="file", - source_url=Path(__file__) - .parent.parent.joinpath( - "fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl" - ) + source_url=(fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl") .resolve() .as_posix(), ) @@ -130,10 +232,7 @@ def test_execute_executes_a_batch_of_operations( "simple-project", "1.2.3", source_type="directory", - source_url=Path(__file__) - .parent.parent.joinpath("fixtures/simple_project") - .resolve() - .as_posix(), + source_url=fixture_dir("simple_project").resolve().as_posix(), ) git_package = Package( @@ -171,11 +270,16 @@ def test_execute_executes_a_batch_of_operations( expected = set(expected.splitlines()) output = set(io.fetch_output().splitlines()) assert output == expected - assert len(env.executed) == 1 + assert wheel_install.call_count == 5 + # Two pip uninstalls: one for the remove operation one for the update operation + assert len(env.executed) == 2 assert return_code == 0 - assert pip_install.call_count == 5 - assert pip_install.call_args.kwargs.get("upgrade", False) - assert pip_install.call_args.kwargs.get("editable", False) + + assert prepare_spy.call_count == 2 + assert prepare_spy.call_args_list == [ + mocker.call(chef, mocker.ANY, destination=mocker.ANY, editable=False), + mocker.call(chef, mocker.ANY, destination=mocker.ANY, editable=True), + ] @pytest.mark.parametrize( @@ -203,15 +307,15 @@ def test_execute_executes_a_batch_of_operations( ) def test_execute_prints_warning_for_yanked_package( config: Config, - pool: Pool, + pool: RepositoryPool, io: BufferedIO, - tmp_dir: str, + tmp_path: Path, mock_file_downloads: None, env: MockEnv, operations: list[Operation], has_warning: bool, -): - config.merge({"cache-dir": tmp_dir}) +) -> None: + config.merge({"cache-dir": str(tmp_path)}) executor = Executor(env, pool, config, io) @@ -222,8 +326,9 @@ def test_execute_prints_warning_for_yanked_package( "(black-21.11b0-py3-none-any.whl) is yanked. Reason for being yanked: " "Broken regex dependency. Use 21.11b1 instead." ) + output = io.fetch_output() error = io.fetch_error() - assert return_code == 0 + assert return_code == 0, f"\noutput: {output}\nerror: {error}\n" assert "pytest" not in error if has_warning: assert expected in error @@ -233,9 +338,73 @@ def test_execute_prints_warning_for_yanked_package( assert error.count("yanked") == 0 +def test_execute_prints_warning_for_invalid_wheels( + config: Config, + pool: RepositoryPool, + io: BufferedIO, + tmp_path: Path, + mock_file_downloads: None, + env: MockEnv, +) -> None: + config.merge({"cache-dir": str(tmp_path)}) + + executor = Executor(env, pool, config, io) + + base_url = "https://files.pythonhosted.org/" + wheel1 = "demo_invalid_record-0.1.0-py2.py3-none-any.whl" + wheel2 = "demo_invalid_record2-0.1.0-py2.py3-none-any.whl" + return_code = executor.execute( + [ + Install( + Package( + "demo-invalid-record", + "0.1.0", + source_type="url", + source_url=f"{base_url}/{wheel1}", + ) + ), + Install( + Package( + "demo-invalid-record2", + "0.1.0", + source_type="url", + source_url=f"{base_url}/{wheel2}", + ) + ), + ] + ) + + warning1 = f"""\ +Warning: Validation of the RECORD file of {wheel1} failed.\ + Please report to the maintainers of that package so they can fix their build process.\ + Details: +In .*?{wheel1}, demo/__init__.py is not mentioned in RECORD +In .*?{wheel1}, demo_invalid_record-0.1.0.dist-info/WHEEL is not mentioned in RECORD +""" + + warning2 = f"""\ +Warning: Validation of the RECORD file of {wheel2} failed.\ + Please report to the maintainers of that package so they can fix their build process.\ + Details: +In .*?{wheel2}, hash / size of demo_invalid_record2-0.1.0.dist-info/METADATA didn't\ + match RECORD +""" + + output = io.fetch_output() + error = io.fetch_error() + assert return_code == 0, f"\noutput: {output}\nerror: {error}\n" + assert re.match(f"{warning1}\n{warning2}", error) or re.match( + f"{warning2}\n{warning1}", error + ), error + + def test_execute_shows_skipped_operations_if_verbose( - config: Config, pool: Pool, io: BufferedIO, config_cache_dir: Path, env: MockEnv -): + config: Config, + pool: RepositoryPool, + io: BufferedIO, + config_cache_dir: Path, + env: MockEnv, +) -> None: config.merge({"cache-dir": config_cache_dir.as_posix()}) executor = Executor(env, pool, config, io) @@ -258,8 +427,12 @@ def test_execute_shows_skipped_operations_if_verbose( def test_execute_should_show_errors( - config: Config, pool: Pool, mocker: MockerFixture, io: BufferedIO, env: MockEnv -): + config: Config, + pool: RepositoryPool, + mocker: MockerFixture, + io: BufferedIO, + env: MockEnv, +) -> None: executor = Executor(env, pool, config, io) executor.verbose() @@ -281,36 +454,30 @@ def test_execute_should_show_errors( def test_execute_works_with_ansi_output( - mocker: MockerFixture, config: Config, - pool: Pool, + pool: RepositoryPool, io_decorated: BufferedIO, - tmp_dir: str, + tmp_path: Path, mock_file_downloads: None, env: MockEnv, -): - config.merge({"cache-dir": tmp_dir}) +) -> None: + config.merge({"cache-dir": str(tmp_path)}) 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.1")), + Install(Package("cleo", "1.0.0a5")), ] ) - env._run.assert_called_once() # fmt: off expected = [ "\x1b[39;1mPackage operations\x1b[39;22m: \x1b[34m1\x1b[39m install, \x1b[34m0\x1b[39m updates, \x1b[34m0\x1b[39m removals", # noqa: E501 - "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m3.5.1\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mPending...\x1b[39m", # noqa: E501 - "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m3.5.1\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mDownloading...\x1b[39m", # noqa: E501 - "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m3.5.1\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mInstalling...\x1b[39m", # noqa: E501 - "\x1b[32;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[32m3.5.1\x1b[39m\x1b[39m)\x1b[39m", # finished # noqa: E501 + "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mcleo\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m1.0.0a5\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mPending...\x1b[39m", # noqa: E501 + "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mcleo\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m1.0.0a5\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mDownloading...\x1b[39m", # noqa: E501 + "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mcleo\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m1.0.0a5\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mInstalling...\x1b[39m", # noqa: E501 + "\x1b[32;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mcleo\x1b[39m\x1b[39m (\x1b[39m\x1b[32m1.0.0a5\x1b[39m\x1b[39m)\x1b[39m", # finished # noqa: E501 ] # fmt: on @@ -325,31 +492,26 @@ def test_execute_works_with_ansi_output( def test_execute_works_with_no_ansi_output( mocker: MockerFixture, config: Config, - pool: Pool, + pool: RepositoryPool, io_not_decorated: BufferedIO, - tmp_dir: str, + tmp_path: Path, mock_file_downloads: None, env: MockEnv, -): - config.merge({"cache-dir": tmp_dir}) +) -> None: + config.merge({"cache-dir": str(tmp_path)}) 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.1")), + Install(Package("cleo", "1.0.0a5")), ] ) - env._run.assert_called_once() expected = """ Package operations: 1 install, 0 updates, 0 removals - • Installing pytest (3.5.1) + • Installing cleo (1.0.0a5) """ expected = set(expected.splitlines()) output = set(io_not_decorated.fetch_output().splitlines()) @@ -358,8 +520,12 @@ def test_execute_works_with_no_ansi_output( def test_execute_should_show_operation_as_cancelled_on_subprocess_keyboard_interrupt( - config: Config, pool: Pool, mocker: MockerFixture, io: BufferedIO, env: MockEnv -): + config: Config, + pool: RepositoryPool, + mocker: MockerFixture, + io: BufferedIO, + env: MockEnv, +) -> None: executor = Executor(env, pool, config, io) executor.verbose() @@ -379,8 +545,12 @@ def test_execute_should_show_operation_as_cancelled_on_subprocess_keyboard_inter def test_execute_should_gracefully_handle_io_error( - config: Config, pool: Pool, mocker: MockerFixture, io: BufferedIO, env: MockEnv -): + config: Config, + pool: RepositoryPool, + mocker: MockerFixture, + io: BufferedIO, + env: MockEnv, +) -> None: executor = Executor(env, pool, config, io) executor.verbose() @@ -408,31 +578,30 @@ def write_line(string: str, **kwargs: Any) -> None: def test_executor_should_delete_incomplete_downloads( config: Config, io: BufferedIO, - tmp_dir: str, + tmp_path: Path, mocker: MockerFixture, - pool: Pool, + pool: RepositoryPool, mock_file_downloads: None, env: MockEnv, -): - fixture = Path(__file__).parent.parent.joinpath( - "fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl" - ) - destination_fixture = Path(tmp_dir) / "tomlkit-0.5.3-py2.py3-none-any.whl" + fixture_dir: FixtureDirGetter, +) -> None: + fixture = fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl" + destination_fixture = tmp_path / "tomlkit-0.5.3-py2.py3-none-any.whl" shutil.copyfile(str(fixture), str(destination_fixture)) mocker.patch( "poetry.installation.executor.Executor._download_archive", side_effect=Exception("Download error"), ) mocker.patch( - "poetry.installation.chef.Chef.get_cached_archive_for_link", - side_effect=lambda link: None, + "poetry.installation.executor.ArtifactCache.get_cached_archive_for_link", + return_value=None, ) mocker.patch( - "poetry.installation.chef.Chef.get_cache_directory_for_link", - return_value=Path(tmp_dir), + "poetry.installation.executor.ArtifactCache.get_cache_directory_for_link", + return_value=tmp_path, ) - config.merge({"cache-dir": tmp_dir}) + config.merge({"cache-dir": str(tmp_path)}) executor = Executor(env, pool, config, io) @@ -444,7 +613,7 @@ def test_executor_should_delete_incomplete_downloads( def verify_installed_distribution( venv: VirtualEnv, package: Package, url_reference: dict[str, Any] | None = None -): +) -> None: distributions = list(venv.site_packages.distributions(name=package.name)) assert len(distributions) == 1 @@ -488,11 +657,17 @@ def test_executor_should_not_write_pep610_url_references_for_cached_package( mocker: MockerFixture, fixture_dir: FixtureDirGetter, tmp_venv: VirtualEnv, - pool: Pool, + pool: RepositoryPool, config: Config, io: BufferedIO, -): +) -> None: link_cached = fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl" + package.files = [ + { + "file": "demo-0.1.0-py2.py3-none-any.whl", + "hash": "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a", # noqa: E501 + } + ] mocker.patch( "poetry.installation.executor.Executor._download", return_value=link_cached @@ -501,99 +676,325 @@ def test_executor_should_not_write_pep610_url_references_for_cached_package( executor = Executor(tmp_venv, pool, config, io) executor.execute([Install(package)]) verify_installed_distribution(tmp_venv, package) + assert link_cached.exists(), "cached file should not be deleted" -def test_executor_should_write_pep610_url_references_for_files( - tmp_venv: VirtualEnv, pool: Pool, config: Config, io: BufferedIO -): - url = ( - Path(__file__) - .parent.parent.joinpath( - "fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl" - ) - .resolve() - ) +def test_executor_should_write_pep610_url_references_for_wheel_files( + tmp_venv: VirtualEnv, + pool: RepositoryPool, + config: Config, + io: BufferedIO, + fixture_dir: FixtureDirGetter, +) -> None: + url = (fixture_dir("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()) + # Set package.files so the executor will attempt to hash the package + package.files = [ + { + "file": "demo-0.1.0-py2.py3-none-any.whl", + "hash": "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a", # noqa: E501 + } + ] executor = Executor(tmp_venv, pool, config, io) executor.execute([Install(package)]) - verify_installed_distribution( - tmp_venv, package, {"archive_info": {}, "url": url.as_uri()} - ) + expected_url_reference = { + "archive_info": { + "hashes": { + "sha256": ( + "70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a" + ) + }, + }, + "url": url.as_uri(), + } + verify_installed_distribution(tmp_venv, package, expected_url_reference) + assert url.exists(), "source file should not be deleted" + + +def test_executor_should_write_pep610_url_references_for_non_wheel_files( + tmp_venv: VirtualEnv, + pool: RepositoryPool, + config: Config, + io: BufferedIO, + fixture_dir: FixtureDirGetter, +) -> None: + url = (fixture_dir("distributions") / "demo-0.1.0.tar.gz").resolve() + package = Package("demo", "0.1.0", source_type="file", source_url=url.as_posix()) + # Set package.files so the executor will attempt to hash the package + package.files = [ + { + "file": "demo-0.1.0.tar.gz", + "hash": "sha256:9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad", # noqa: E501 + } + ] + + executor = Executor(tmp_venv, pool, config, io) + executor.execute([Install(package)]) + expected_url_reference = { + "archive_info": { + "hashes": { + "sha256": ( + "9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad" + ) + }, + }, + "url": url.as_uri(), + } + verify_installed_distribution(tmp_venv, package, expected_url_reference) + assert url.exists(), "source file should not be deleted" def test_executor_should_write_pep610_url_references_for_directories( - tmp_venv: VirtualEnv, pool: Pool, config: Config, io: BufferedIO -): - url = Path(__file__).parent.parent.joinpath("fixtures/simple_project").resolve() + tmp_venv: VirtualEnv, + pool: RepositoryPool, + config: Config, + artifact_cache: ArtifactCache, + io: BufferedIO, + wheel: Path, + fixture_dir: FixtureDirGetter, + mocker: MockerFixture, +) -> None: + url = (fixture_dir("git") / "github.com" / "demo" / "demo").resolve() package = Package( - "simple-project", "1.2.3", source_type="directory", source_url=url.as_posix() + "demo", "0.1.2", source_type="directory", source_url=url.as_posix() ) + chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) + chef.set_directory_wheel(wheel) + prepare_spy = mocker.spy(chef, "prepare") + executor = Executor(tmp_venv, pool, config, io) + executor._chef = chef executor.execute([Install(package)]) verify_installed_distribution( tmp_venv, package, {"dir_info": {}, "url": url.as_uri()} ) + assert not prepare_spy.spy_return.exists(), "archive not cleaned up" def test_executor_should_write_pep610_url_references_for_editable_directories( - tmp_venv: VirtualEnv, pool: Pool, config: Config, io: BufferedIO -): - url = Path(__file__).parent.parent.joinpath("fixtures/simple_project").resolve() + tmp_venv: VirtualEnv, + pool: RepositoryPool, + config: Config, + artifact_cache: ArtifactCache, + io: BufferedIO, + wheel: Path, + fixture_dir: FixtureDirGetter, + mocker: MockerFixture, +) -> None: + url = (fixture_dir("git") / "github.com" / "demo" / "demo").resolve() package = Package( - "simple-project", - "1.2.3", + "demo", + "0.1.2", source_type="directory", source_url=url.as_posix(), develop=True, ) + chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) + chef.set_directory_wheel(wheel) + prepare_spy = mocker.spy(chef, "prepare") + executor = Executor(tmp_venv, pool, config, io) + executor._chef = chef executor.execute([Install(package)]) verify_installed_distribution( tmp_venv, package, {"dir_info": {"editable": True}, "url": url.as_uri()} ) + assert not prepare_spy.spy_return.exists(), "archive not cleaned up" -def test_executor_should_write_pep610_url_references_for_urls( +@pytest.mark.parametrize("is_artifact_cached", [False, True]) +def test_executor_should_write_pep610_url_references_for_wheel_urls( tmp_venv: VirtualEnv, - pool: Pool, + pool: RepositoryPool, config: Config, io: BufferedIO, mock_file_downloads: None, -): + mocker: MockerFixture, + fixture_dir: FixtureDirGetter, + is_artifact_cached: bool, +) -> None: + if is_artifact_cached: + link_cached = fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl" + mocker.patch( + "poetry.installation.executor.ArtifactCache.get_cached_archive_for_link", + return_value=link_cached, + ) + download_spy = mocker.spy(Executor, "_download_archive") + package = Package( "demo", "0.1.0", source_type="url", source_url="https://files.pythonhosted.org/demo-0.1.0-py2.py3-none-any.whl", ) + # Set package.files so the executor will attempt to hash the package + package.files = [ + { + "file": "demo-0.1.0-py2.py3-none-any.whl", + "hash": "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a", # noqa: E501 + } + ] executor = Executor(tmp_venv, pool, config, io) - executor.execute([Install(package)]) - verify_installed_distribution( - tmp_venv, package, {"archive_info": {}, "url": package.source_url} + operation = Install(package) + executor.execute([operation]) + expected_url_reference = { + "archive_info": { + "hashes": { + "sha256": ( + "70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a" + ) + }, + }, + "url": package.source_url, + } + verify_installed_distribution(tmp_venv, package, expected_url_reference) + if is_artifact_cached: + download_spy.assert_not_called() + else: + download_spy.assert_called_once_with( + mocker.ANY, operation, Link(package.source_url) + ) + assert download_spy.spy_return.exists(), "cached file should not be deleted" + + +@pytest.mark.parametrize( + ( + "is_sdist_cached", + "is_wheel_cached", + "expect_artifact_building", + "expect_artifact_download", + ), + [ + (True, False, True, False), + (True, True, False, False), + (False, False, True, True), + (False, True, False, True), + ], +) +def test_executor_should_write_pep610_url_references_for_non_wheel_urls( + tmp_venv: VirtualEnv, + pool: RepositoryPool, + config: Config, + io: BufferedIO, + mock_file_downloads: None, + mocker: MockerFixture, + fixture_dir: FixtureDirGetter, + is_sdist_cached: bool, + is_wheel_cached: bool, + expect_artifact_building: bool, + expect_artifact_download: bool, +) -> None: + built_wheel = fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl" + mock_prepare = mocker.patch( + "poetry.installation.chef.Chef._prepare", + return_value=built_wheel, ) + download_spy = mocker.spy(Executor, "_download_archive") + + if is_sdist_cached | is_wheel_cached: + cached_sdist = fixture_dir("distributions") / "demo-0.1.0.tar.gz" + cached_wheel = fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl" + + def mock_get_cached_archive_for_link_func( + _: Link, *, strict: bool, **__: Any + ) -> None: + if is_wheel_cached and not strict: + return cached_wheel + if is_sdist_cached: + return cached_sdist + return None + + mocker.patch( + "poetry.installation.executor.ArtifactCache.get_cached_archive_for_link", + side_effect=mock_get_cached_archive_for_link_func, + ) + + package = Package( + "demo", + "0.1.0", + source_type="url", + source_url="https://files.pythonhosted.org/demo-0.1.0.tar.gz", + ) + # Set package.files so the executor will attempt to hash the package + package.files = [ + { + "file": "demo-0.1.0.tar.gz", + "hash": "sha256:9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad", # noqa: E501 + } + ] + + executor = Executor(tmp_venv, pool, config, io) + operation = Install(package) + executor.execute([operation]) + expected_url_reference = { + "archive_info": { + "hashes": { + "sha256": ( + "9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad" + ) + }, + }, + "url": package.source_url, + } + verify_installed_distribution(tmp_venv, package, expected_url_reference) + + if expect_artifact_building: + mock_prepare.assert_called_once() + else: + mock_prepare.assert_not_called() + + if expect_artifact_download: + download_spy.assert_called_once_with( + mocker.ANY, operation, Link(package.source_url) + ) + assert download_spy.spy_return.exists(), "cached file should not be deleted" + else: + download_spy.assert_not_called() +@pytest.mark.parametrize("is_artifact_cached", [False, True]) def test_executor_should_write_pep610_url_references_for_git( tmp_venv: VirtualEnv, - pool: Pool, + pool: RepositoryPool, config: Config, + artifact_cache: ArtifactCache, io: BufferedIO, mock_file_downloads: None, -): + wheel: Path, + mocker: MockerFixture, + fixture_dir: FixtureDirGetter, + is_artifact_cached: bool, +) -> None: + if is_artifact_cached: + link_cached = fixture_dir("distributions") / "demo-0.1.2-py2.py3-none-any.whl" + mocker.patch( + "poetry.installation.executor.ArtifactCache.get_cached_archive_for_git", + return_value=link_cached, + ) + clone_spy = mocker.spy(Git, "clone") + + source_resolved_reference = "123456" + source_url = "https://github.com/demo/demo.git" + package = Package( "demo", "0.1.2", source_type="git", source_reference="master", - source_resolved_reference="123456", - source_url="https://github.com/demo/demo.git", + source_resolved_reference=source_resolved_reference, + source_url=source_url, ) + chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) + chef.set_directory_wheel(wheel) + prepare_spy = mocker.spy(chef, "prepare") + executor = Executor(tmp_venv, pool, config, io) + executor._chef = chef executor.execute([Install(package)]) verify_installed_distribution( tmp_venv, @@ -608,71 +1009,135 @@ def test_executor_should_write_pep610_url_references_for_git( }, ) + if is_artifact_cached: + clone_spy.assert_not_called() + prepare_spy.assert_not_called() + else: + clone_spy.assert_called_once_with( + url=source_url, source_root=mocker.ANY, revision=source_resolved_reference + ) + prepare_spy.assert_called_once() + assert prepare_spy.spy_return.exists(), "cached file should not be deleted" + assert (prepare_spy.spy_return.parent / ".created_from_git_dependency").exists() -def test_executor_should_write_pep610_url_references_for_git_with_subdirectories( + +def test_executor_should_write_pep610_url_references_for_editable_git( tmp_venv: VirtualEnv, - pool: Pool, + pool: RepositoryPool, config: Config, + artifact_cache: ArtifactCache, io: BufferedIO, mock_file_downloads: None, -): + wheel: Path, + mocker: MockerFixture, + fixture_dir: FixtureDirGetter, +) -> None: + source_resolved_reference = "123456" + source_url = "https://github.com/demo/demo.git" + package = Package( - "two", - "2.0.0", + "demo", + "0.1.2", source_type="git", source_reference="master", - source_resolved_reference="123456", - source_url="https://github.com/demo/subdirectories.git", - source_subdirectory="two", + source_resolved_reference=source_resolved_reference, + source_url=source_url, + develop=True, ) + chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) + chef.set_directory_wheel(wheel) + prepare_spy = mocker.spy(chef, "prepare") + cache_spy = mocker.spy(artifact_cache, "get_cached_archive_for_git") + executor = Executor(tmp_venv, pool, config, io) + executor._chef = chef executor.execute([Install(package)]) verify_installed_distribution( tmp_venv, package, { - "vcs_info": { - "vcs": "git", - "requested_revision": "master", - "commit_id": "123456", - }, - "url": package.source_url, - "subdirectory": package.source_subdirectory, + "dir_info": {"editable": True}, + "url": Path(package.source_url).as_uri(), }, ) + cache_spy.assert_not_called() + prepare_spy.assert_called_once() + assert not prepare_spy.spy_return.exists(), "editable git should not be cached" + assert not (prepare_spy.spy_return.parent / ".created_from_git_dependency").exists() -def test_executor_should_use_cached_link_and_hash( + +def test_executor_should_append_subdirectory_for_git( + mocker: MockerFixture, tmp_venv: VirtualEnv, - pool: Pool, + pool: RepositoryPool, config: Config, + artifact_cache: ArtifactCache, io: BufferedIO, - mocker: MockerFixture, - fixture_dir: FixtureDirGetter, -): - link_cached = fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl" + mock_file_downloads: None, + wheel: Path, +) -> None: + package = Package( + "demo", + "0.1.2", + source_type="git", + source_reference="master", + source_resolved_reference="123456", + source_url="https://github.com/demo/subdirectories.git", + source_subdirectory="two", + ) - mocker.patch( - "poetry.installation.chef.Chef.get_cached_archive_for_link", - return_value=link_cached, + chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) + chef.set_directory_wheel(wheel) + spy = mocker.spy(chef, "prepare") + + executor = Executor(tmp_venv, pool, config, io) + executor._chef = chef + executor.execute([Install(package)]) + + archive_arg = spy.call_args[0][0] + assert archive_arg == tmp_venv.path / "src/demo/subdirectories/two" + + +def test_executor_should_write_pep610_url_references_for_git_with_subdirectories( + tmp_venv: VirtualEnv, + pool: RepositoryPool, + config: Config, + artifact_cache: ArtifactCache, + io: BufferedIO, + mock_file_downloads: None, + wheel: Path, +) -> None: + package = Package( + "demo", + "0.1.2", + source_type="git", + source_reference="master", + source_resolved_reference="123456", + source_url="https://github.com/demo/subdirectories.git", + source_subdirectory="two", ) - package = Package("demo", "0.1.0") - # Set package.files so the executor will attempt to hash the package - package.files = [ - { - "file": "demo-0.1.0-py2.py3-none-any.whl", - "hash": "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a", # noqa: E501 - } - ] + chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) + chef.set_directory_wheel(wheel) executor = Executor(tmp_venv, pool, config, io) - archive = executor._download_link( - Install(package), - Link("https://example.com/demo-0.1.0-py2.py3-none-any.whl"), + executor._chef = chef + executor.execute([Install(package)]) + verify_installed_distribution( + tmp_venv, + package, + { + "vcs_info": { + "vcs": "git", + "requested_revision": "master", + "commit_id": "123456", + }, + "url": package.source_url, + "subdirectory": package.source_subdirectory, + }, ) - assert archive == link_cached @pytest.mark.parametrize( @@ -688,7 +1153,7 @@ def test_executor_should_use_cached_link_and_hash( ) def test_executor_should_be_initialized_with_correct_workers( tmp_venv: VirtualEnv, - pool: Pool, + pool: RepositoryPool, config: Config, io: BufferedIO, mocker: MockerFixture, @@ -696,7 +1161,7 @@ def test_executor_should_be_initialized_with_correct_workers( cpu_count: int | None, side_effect: Exception | None, expected_workers: int, -): +) -> None: config.merge({"installer": {"max-workers": max_workers}}) mocker.patch("os.cpu_count", return_value=cpu_count, side_effect=side_effect) @@ -706,15 +1171,16 @@ def test_executor_should_be_initialized_with_correct_workers( assert executor._max_workers == expected_workers -def test_executer_fallback_on_poetry_create_error( +def test_executor_fallback_on_poetry_create_error_without_wheel_installer( mocker: MockerFixture, config: Config, - pool: Pool, + pool: RepositoryPool, io: BufferedIO, - tmp_dir: str, + tmp_path: Path, mock_file_downloads: None, env: MockEnv, -): + fixture_dir: FixtureDirGetter, +) -> None: mock_pip_install = mocker.patch("poetry.installation.executor.pip_install") mock_sdist_builder = mocker.patch("poetry.core.masonry.builders.sdist.SdistBuilder") mock_editable_builder = mocker.patch( @@ -724,7 +1190,12 @@ def test_executer_fallback_on_poetry_create_error( "poetry.factory.Factory.create_poetry", side_effect=RuntimeError ) - config.merge({"cache-dir": tmp_dir}) + config.merge( + { + "cache-dir": str(tmp_path), + "installer": {"modern-installation": False}, + } + ) executor = Executor(env, pool, config, io) @@ -732,10 +1203,7 @@ def test_executer_fallback_on_poetry_create_error( "simple-project", "1.2.3", source_type="directory", - source_url=Path(__file__) - .parent.parent.joinpath("fixtures/simple_project") - .resolve() - .as_posix(), + source_url=fixture_dir("simple_project").resolve().as_posix(), ) return_code = executor.execute( @@ -760,3 +1228,163 @@ def test_executer_fallback_on_poetry_create_error( assert mock_pip_install.call_count == 1 assert mock_pip_install.call_args[1].get("upgrade") is True assert mock_pip_install.call_args[1].get("editable") is False + + +@pytest.mark.parametrize("failing_method", ["build", "get_requires_for_build"]) +@pytest.mark.parametrize("editable", [False, True]) +def test_build_backend_errors_are_reported_correctly_if_caused_by_subprocess( + failing_method: str, + editable: bool, + mocker: MockerFixture, + config: Config, + pool: RepositoryPool, + io: BufferedIO, + mock_file_downloads: None, + env: MockEnv, + fixture_dir: FixtureDirGetter, +) -> None: + error = BuildBackendException( + CalledProcessError(1, ["pip"], output=b"Error on stdout") + ) + mocker.patch.object(ProjectBuilder, failing_method, side_effect=error) + io.set_verbosity(Verbosity.NORMAL) + + executor = Executor(env, pool, config, io) + + package_name = "simple-project" + package_version = "1.2.3" + directory_package = Package( + package_name, + package_version, + source_type="directory", + source_url=fixture_dir("simple_project").resolve().as_posix(), + develop=editable, + ) + # must not be included in the error message + directory_package.python_versions = ">=3.7" + + return_code = executor.execute([Install(directory_package)]) + + assert return_code == 1 + + package_url = directory_package.source_url + expected_start = f""" +Package operations: 1 install, 0 updates, 0 removals + + • Installing {package_name} ({package_version} {package_url}) + + ChefBuildError + + Backend operation failed: CalledProcessError(1, ['pip']) + \ + + Error on stdout +""" + + if editable: + pip_command = "pip wheel --use-pep517 --editable" + requirement = directory_package.source_url + assert Path(requirement).exists() + else: + pip_command = "pip wheel --use-pep517" + requirement = f"{package_name} @ {path_to_url(directory_package.source_url)}" + expected_end = f""" +Note: This error originates from the build backend, and is likely not a problem with \ +poetry but with {package_name} ({package_version} {package_url}) not supporting \ +PEP 517 builds. You can verify this by running '{pip_command} "{requirement}"'. + +""" + + output = io.fetch_output() + assert output.startswith(expected_start) + assert output.endswith(expected_end) + + +@pytest.mark.parametrize("encoding", ["utf-8", "latin-1"]) +@pytest.mark.parametrize("stderr", [None, "Errör on stderr"]) +def test_build_backend_errors_are_reported_correctly_if_caused_by_subprocess_encoding( + encoding: str, + stderr: str | None, + mocker: MockerFixture, + config: Config, + pool: RepositoryPool, + io: BufferedIO, + mock_file_downloads: None, + env: MockEnv, + fixture_dir: FixtureDirGetter, +) -> None: + """Test that the output of the subprocess is decoded correctly.""" + stdout = "Errör on stdout" + error = BuildBackendException( + CalledProcessError( + 1, + ["pip"], + output=stdout.encode(encoding), + stderr=stderr.encode(encoding) if stderr else None, + ) + ) + mocker.patch.object(ProjectBuilder, "get_requires_for_build", side_effect=error) + io.set_verbosity(Verbosity.NORMAL) + + executor = Executor(env, pool, config, io) + + directory_package = Package( + "simple-project", + "1.2.3", + source_type="directory", + source_url=fixture_dir("simple_project").resolve().as_posix(), + ) + + return_code = executor.execute([Install(directory_package)]) + + assert return_code == 1 + assert (stderr or stdout) in io.fetch_output() + + +def test_build_system_requires_not_available( + config: Config, + pool: RepositoryPool, + io: BufferedIO, + mock_file_downloads: None, + env: MockEnv, + fixture_dir: FixtureDirGetter, +) -> None: + io.set_verbosity(Verbosity.NORMAL) + + executor = Executor(env, pool, config, io) + + package_name = "simple-project" + package_version = "1.2.3" + directory_package = Package( + package_name, + package_version, + source_type="directory", + source_url=fixture_dir("build_system_requires_not_available") + .resolve() + .as_posix(), + ) + + return_code = executor.execute( + [ + Install(directory_package), + ] + ) + + assert return_code == 1 + + package_url = directory_package.source_url + expected_start = f"""\ +Package operations: 1 install, 0 updates, 0 removals + + • Installing {package_name} ({package_version} {package_url}) + + SolveFailure + + Because -root- depends on poetry-core (0.999) which doesn't match any versions,\ + version solving failed. +""" + expected_end = "Cannot resolve build-system.requires for simple-project." + + output = io.fetch_output().strip() + assert output.startswith(expected_start) + assert output.endswith(expected_end) diff --git a/tests/installation/test_installer.py b/tests/installation/test_installer.py index f7fd6213c58..39e07fb5b06 100644 --- a/tests/installation/test_installer.py +++ b/tests/installation/test_installer.py @@ -18,16 +18,16 @@ from poetry.core.packages.dependency_group import DependencyGroup 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 from poetry.installation.executor import Executor as BaseExecutor from poetry.installation.noop_installer import NoopInstaller from poetry.packages import Locker as BaseLocker -from poetry.repositories import Pool from poetry.repositories import Repository +from poetry.repositories import RepositoryPool from poetry.repositories.installed_repository import InstalledRepository +from poetry.toml.file import TOMLFile from poetry.utils.env import MockEnv from poetry.utils.env import NullEnv from tests.helpers import MOCK_DEFAULT_GIT_REVISION @@ -60,12 +60,12 @@ class Executor(BaseExecutor): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self._installs: list[DependencyPackage] = [] + self._installs: list[Package] = [] self._updates: list[DependencyPackage] = [] self._uninstalls: list[DependencyPackage] = [] @property - def installations(self) -> list[DependencyPackage]: + def installations(self) -> list[Package]: return self._installs @property @@ -76,12 +76,14 @@ def updates(self) -> list[DependencyPackage]: def removals(self) -> list[DependencyPackage]: return self._uninstalls - def _do_execute_operation(self, operation: Operation) -> None: - super()._do_execute_operation(operation) + def _do_execute_operation(self, operation: Operation) -> int: + ret_val = super()._do_execute_operation(operation) if not operation.skipped: getattr(self, f"_{operation.job_type}s").append(operation.package) + return ret_val + def _execute_install(self, operation: Operation) -> int: return 0 @@ -101,18 +103,19 @@ def load( class Locker(BaseLocker): - def __init__(self, lock_path: str | Path) -> None: - self._lock = TOMLFile(Path(lock_path).joinpath("poetry.lock")) + def __init__(self, lock_path: Path) -> None: + self._lock = lock_path / "poetry.lock" self._written_data = None self._locked = False + self._lock_data = None self._content_hash = self._get_content_hash() @property - def written_data(self) -> dict | None: + def written_data(self) -> dict[str, Any] | None: return self._written_data - def set_lock_path(self, lock: str | Path) -> Locker: - self._lock = TOMLFile(Path(lock).joinpath("poetry.lock")) + def set_lock_path(self, lock: Path) -> Locker: + self._lock = lock / "poetry.lock" return self @@ -121,7 +124,7 @@ def locked(self, is_locked: bool = True) -> Locker: return self - def mock_lock_data(self, data: dict) -> None: + def mock_lock_data(self, data: dict[str, Any]) -> None: self._lock_data = data def is_locked(self) -> bool: @@ -133,7 +136,7 @@ def is_fresh(self) -> bool: def _get_content_hash(self) -> str: return "123456789" - def _write_lock_data(self, data: dict) -> None: + def _write_lock_data(self, data: dict[str, Any]) -> None: for package in data["package"]: python_versions = str(package["python-versions"]) package["python-versions"] = python_versions @@ -156,8 +159,8 @@ def repo() -> Repository: @pytest.fixture() -def pool(repo: Repository) -> Pool: - pool = Pool() +def pool(repo: Repository) -> RepositoryPool: + pool = RepositoryPool() pool.add_repository(repo) return pool @@ -174,14 +177,14 @@ def locker(project_root: Path) -> Locker: @pytest.fixture() -def env() -> NullEnv: - return NullEnv() +def env(tmp_path: Path) -> NullEnv: + return NullEnv(path=tmp_path) @pytest.fixture() def installer( package: ProjectPackage, - pool: Pool, + pool: RepositoryPool, locker: Locker, env: NullEnv, installed: CustomInstalledRepository, @@ -197,27 +200,27 @@ def installer( installed=installed, executor=Executor(env, pool, config, NullIO()), ) - installer.use_executor(True) - return installer -def fixture(name: str) -> dict: +def fixture(name: str) -> dict[str, Any]: file = TOMLFile(Path(__file__).parent / "fixtures" / f"{name}.test") + content: dict[str, Any] = file.read() - return json.loads(json.dumps(file.read())) + return content -def test_run_no_dependencies(installer: Installer, locker: Locker): - installer.run() - expected = fixture("no-dependencies") +def test_run_no_dependencies(installer: Installer, locker: Locker) -> None: + result = installer.run() + assert result == 0 + expected = fixture("no-dependencies") assert locker.written_data == expected def test_run_with_dependencies( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage -): +) -> None: package_a = get_package("A", "1.0") package_b = get_package("B", "1.1") repo.add_package(package_a) @@ -226,9 +229,10 @@ def test_run_with_dependencies( package.add_dependency(Factory.create_dependency("A", "~1.0")) package.add_dependency(Factory.create_dependency("B", "^1.0")) - installer.run() - expected = fixture("with-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-dependencies") assert locker.written_data == expected @@ -238,7 +242,7 @@ def test_run_update_after_removing_dependencies( repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, -): +) -> None: locker.locked(True) locker.mock_lock_data( { @@ -294,9 +298,10 @@ def test_run_update_after_removing_dependencies( package.add_dependency(Factory.create_dependency("B", "~1.1")) installer.update(True) - installer.run() - expected = fixture("with-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-dependencies") assert locker.written_data == expected assert installer.executor.installations_count == 0 @@ -401,7 +406,7 @@ def test_run_install_with_dependency_groups( repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, -): +) -> None: _configure_run_install_dev( locker, repo, @@ -415,7 +420,8 @@ def test_run_install_with_dependency_groups( installer.only_groups(groups) installer.requires_synchronization(True) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == installs assert installer.executor.updates_count == updates @@ -428,7 +434,7 @@ def test_run_install_does_not_remove_locked_packages_if_installed_but_not_requir repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, -): +) -> None: package_a = get_package("a", "1.0") package_b = get_package("b", "1.1") package_c = get_package("c", "1.2") @@ -485,7 +491,8 @@ def test_run_install_does_not_remove_locked_packages_if_installed_but_not_requir } ) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 0 assert installer.executor.updates_count == 0 @@ -498,7 +505,7 @@ def test_run_install_removes_locked_packages_if_installed_and_synchronization_is repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, -): +) -> None: package_a = get_package("a", "1.0") package_b = get_package("b", "1.1") package_c = get_package("c", "1.2") @@ -569,7 +576,7 @@ def test_run_install_removes_no_longer_locked_packages_if_installed( repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, -): +) -> None: package_a = get_package("a", "1.0") package_b = get_package("b", "1.1") package_c = get_package("c", "1.2") @@ -627,7 +634,8 @@ def test_run_install_removes_no_longer_locked_packages_if_installed( ) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 0 assert installer.executor.updates_count == 0 @@ -650,7 +658,7 @@ def test_run_install_with_synchronization( repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, -): +) -> None: package_a = get_package("a", "1.0") package_b = get_package("b", "1.1") package_c = get_package("c", "1.2") @@ -705,7 +713,8 @@ def test_run_install_with_synchronization( ) installer.requires_synchronization(True) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 0 assert installer.executor.updates_count == 0 @@ -722,7 +731,7 @@ def test_run_install_with_synchronization( def test_run_whitelist_add( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage -): +) -> None: locker.locked(True) locker.mock_lock_data( { @@ -758,9 +767,10 @@ def test_run_whitelist_add( installer.update(True) installer.whitelist(["B"]) - installer.run() - expected = fixture("with-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-dependencies") assert locker.written_data == expected @@ -770,7 +780,7 @@ def test_run_whitelist_remove( repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, -): +) -> None: locker.locked(True) locker.mock_lock_data( { @@ -813,9 +823,10 @@ def test_run_whitelist_remove( installer.update(True) installer.whitelist(["B"]) - installer.run() - expected = fixture("remove") + result = installer.run() + assert result == 0 + expected = fixture("remove") assert locker.written_data == expected assert installer.executor.installations_count == 1 assert installer.executor.updates_count == 0 @@ -824,7 +835,7 @@ def test_run_whitelist_remove( def test_add_with_sub_dependencies( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage -): +) -> None: package_a = get_package("A", "1.0") package_b = get_package("B", "1.1") package_c = get_package("C", "1.2") @@ -840,15 +851,16 @@ def test_add_with_sub_dependencies( package_a.add_dependency(Factory.create_dependency("D", "^1.0")) package_b.add_dependency(Factory.create_dependency("C", "~1.2")) - installer.run() - expected = fixture("with-sub-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-sub-dependencies") assert locker.written_data == expected def test_run_with_python_versions( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage -): +) -> None: package.python_versions = "~2.7 || ^3.4" package_a = get_package("A", "1.0") @@ -867,15 +879,16 @@ def test_run_with_python_versions( package.add_dependency(Factory.create_dependency("B", "^1.0")) package.add_dependency(Factory.create_dependency("C", "^1.0")) - installer.run() - expected = fixture("with-python-versions") + result = installer.run() + assert result == 0 + expected = fixture("with-python-versions") assert locker.written_data == expected def test_run_with_optional_and_python_restricted_dependencies( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage -): +) -> None: package.python_versions = "~2.7 || ^3.4" package_a = get_package("A", "1.0") @@ -902,9 +915,10 @@ def test_run_with_optional_and_python_restricted_dependencies( Factory.create_dependency("C", {"version": "^1.0", "python": "~2.7 || ^3.4"}) ) - installer.run() - expected = fixture("with-optional-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-optional-dependencies") assert locker.written_data == expected # We should only have 2 installs: @@ -921,7 +935,7 @@ def test_run_with_optional_and_platform_restricted_dependencies( repo: Repository, package: ProjectPackage, mocker: MockerFixture, -): +) -> None: mocker.patch("sys.platform", "darwin") package_a = get_package("A", "1.0") @@ -948,9 +962,10 @@ def test_run_with_optional_and_platform_restricted_dependencies( Factory.create_dependency("C", {"version": "^1.0", "platform": "darwin"}) ) - installer.run() - expected = fixture("with-platform-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-platform-dependencies") assert locker.written_data == expected # We should only have 2 installs: @@ -963,7 +978,7 @@ def test_run_with_optional_and_platform_restricted_dependencies( def test_run_with_dependencies_extras( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage -): +) -> None: package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_c = get_package("C", "1.0") @@ -982,15 +997,16 @@ def test_run_with_dependencies_extras( Factory.create_dependency("B", {"version": "^1.0", "extras": ["foo"]}) ) - installer.run() - expected = fixture("with-dependencies-extras") + result = installer.run() + assert result == 0 + expected = fixture("with-dependencies-extras") assert locker.written_data == expected def test_run_with_dependencies_nested_extras( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage -): +) -> None: package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") package_c = get_package("C", "1.0") @@ -1013,15 +1029,16 @@ def test_run_with_dependencies_nested_extras( package.add_dependency(dependency_a) - installer.run() - expected = fixture("with-dependencies-nested-extras") + result = installer.run() + assert result == 0 + expected = fixture("with-dependencies-nested-extras") assert locker.written_data == expected def test_run_does_not_install_extras_if_not_requested( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage -): +) -> None: package.extras["foo"] = [get_dependency("D")] package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") @@ -1040,9 +1057,10 @@ def test_run_does_not_install_extras_if_not_requested( Factory.create_dependency("D", {"version": "^1.0", "optional": True}) ) - installer.run() - expected = fixture("extras") + result = installer.run() + assert result == 0 + expected = fixture("extras") # Extras are pinned in lock assert locker.written_data == expected @@ -1052,7 +1070,7 @@ def test_run_does_not_install_extras_if_not_requested( def test_run_installs_extras_if_requested( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage -): +) -> None: package.extras["foo"] = [get_dependency("D")] package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") @@ -1072,10 +1090,11 @@ def test_run_installs_extras_if_requested( ) installer.extras(["foo"]) - installer.run() - expected = fixture("extras") + result = installer.run() + assert result == 0 # Extras are pinned in lock + expected = fixture("extras") assert locker.written_data == expected # But should not be installed @@ -1084,7 +1103,7 @@ def test_run_installs_extras_if_requested( def test_run_installs_extras_with_deps_if_requested( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage -): +) -> None: package.extras["foo"] = [get_dependency("C")] package_a = get_package("A", "1.0") package_b = get_package("B", "1.0") @@ -1105,7 +1124,9 @@ def test_run_installs_extras_with_deps_if_requested( package_c.add_dependency(Factory.create_dependency("D", "^1.0")) installer.extras(["foo"]) - installer.run() + result = installer.run() + assert result == 0 + expected = fixture("extras-with-dependencies") # Extras are pinned in lock @@ -1117,7 +1138,7 @@ def test_run_installs_extras_with_deps_if_requested( def test_run_installs_extras_with_deps_if_requested_locked( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage -): +) -> None: locker.locked(True) locker.mock_lock_data(fixture("extras-with-dependencies")) package.extras["foo"] = [get_dependency("C")] @@ -1140,9 +1161,9 @@ def test_run_installs_extras_with_deps_if_requested_locked( package_c.add_dependency(Factory.create_dependency("D", "^1.0")) installer.extras(["foo"]) - installer.run() + result = installer.run() + assert result == 0 - # But should not be installed assert installer.executor.installations_count == 4 # A, B, C, D @@ -1151,16 +1172,19 @@ def test_installer_with_pypi_repository( locker: Locker, installed: CustomInstalledRepository, config: Config, -): - pool = Pool() + env: NullEnv, +) -> None: + pool = RepositoryPool() pool.add_repository(MockRepository()) installer = Installer( - NullIO(), NullEnv(), package, locker, pool, config, installed=installed + NullIO(), env, package, locker, pool, config, installed=installed ) + package.python_versions = ">=3.7" package.add_dependency(Factory.create_dependency("pytest", "^3.5", groups=["dev"])) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-pypi-repository") @@ -1173,7 +1197,7 @@ def test_run_installs_with_local_file( repo: Repository, package: ProjectPackage, fixture_dir: FixtureDirGetter, -): +) -> None: root_dir = Path(__file__).parent.parent.parent package.root_dir = root_dir locker.set_lock_path(root_dir) @@ -1186,7 +1210,8 @@ def test_run_installs_with_local_file( repo.add_package(get_package("pendulum", "1.4.4")) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-file-dependency") @@ -1200,7 +1225,7 @@ def test_run_installs_wheel_with_no_requires_dist( repo: Repository, package: ProjectPackage, fixture_dir: FixtureDirGetter, -): +) -> None: root_dir = Path(__file__).parent.parent.parent package.root_dir = root_dir locker.set_lock_path(root_dir) @@ -1213,7 +1238,8 @@ def test_run_installs_wheel_with_no_requires_dist( ) ) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-wheel-dependency-no-requires-dist") @@ -1229,7 +1255,7 @@ def test_run_installs_with_local_poetry_directory_and_extras( package: ProjectPackage, tmpdir: Path, fixture_dir: FixtureDirGetter, -): +) -> None: root_dir = Path(__file__).parent.parent.parent package.root_dir = root_dir locker.set_lock_path(root_dir) @@ -1244,7 +1270,8 @@ def test_run_installs_with_local_poetry_directory_and_extras( repo.add_package(get_package("pendulum", "1.4.4")) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-directory-dependency-poetry") assert locker.written_data == expected @@ -1252,14 +1279,18 @@ def test_run_installs_with_local_poetry_directory_and_extras( assert installer.executor.installations_count == 2 -def test_run_installs_with_local_poetry_directory_transitive( +@pytest.mark.parametrize("skip_directory", [True, False]) +def test_run_installs_with_local_poetry_directory_and_skip_directory_flag( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, - tmpdir: Path, fixture_dir: FixtureDirGetter, -): + skip_directory: bool, +) -> None: + """When we set Installer.skip_directory(True) no path dependencies should + be installed (including transitive dependencies). + """ root_dir = fixture_dir("directory") package.root_dir = root_dir locker.set_lock_path(root_dir) @@ -1275,13 +1306,27 @@ def test_run_installs_with_local_poetry_directory_transitive( repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cachy", "0.2.0")) - installer.run() + installer.skip_directory(skip_directory) + + result = installer.run() + assert result == 0 + + executor: Executor = installer.executor # type: ignore expected = fixture("with-directory-dependency-poetry-transitive") assert locker.written_data == expected - assert installer.executor.installations_count == 6 + directory_installs = [ + p.name for p in executor.installations if p.source_type == "directory" + ] + + if skip_directory: + assert not directory_installs, directory_installs + assert installer.executor.installations_count == 2 + else: + assert directory_installs, directory_installs + assert installer.executor.installations_count == 6 def test_run_installs_with_local_poetry_file_transitive( @@ -1291,7 +1336,7 @@ def test_run_installs_with_local_poetry_file_transitive( package: ProjectPackage, tmpdir: str, fixture_dir: FixtureDirGetter, -): +) -> None: root_dir = fixture_dir("directory") package.root_dir = root_dir locker.set_lock_path(root_dir) @@ -1309,7 +1354,8 @@ def test_run_installs_with_local_poetry_file_transitive( repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cachy", "0.2.0")) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-file-dependency-transitive") @@ -1325,7 +1371,7 @@ def test_run_installs_with_local_setuptools_directory( package: ProjectPackage, tmpdir: Path, fixture_dir: FixtureDirGetter, -): +) -> None: root_dir = Path(__file__).parent.parent.parent package.root_dir = root_dir locker.set_lock_path(root_dir) @@ -1341,7 +1387,8 @@ def test_run_installs_with_local_setuptools_directory( repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cachy", "0.2.0")) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-directory-dependency-setuptools") @@ -1351,7 +1398,7 @@ def test_run_installs_with_local_setuptools_directory( def test_run_with_prereleases( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage -): +) -> None: locker.locked(True) locker.mock_lock_data( { @@ -1387,9 +1434,10 @@ def test_run_with_prereleases( installer.update(True) installer.whitelist({"B": "^1.1"}) - installer.run() - expected = fixture("with-prereleases") + result = installer.run() + assert result == 0 + expected = fixture("with-prereleases") assert locker.written_data == expected @@ -1442,7 +1490,7 @@ def test_run_changes_category_if_needed( def test_run_update_all_with_lock( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage -): +) -> None: locker.locked(True) locker.mock_lock_data( { @@ -1473,15 +1521,16 @@ def test_run_update_all_with_lock( installer.update(True) - installer.run() - expected = fixture("update-with-lock") + result = installer.run() + assert result == 0 + expected = fixture("update-with-lock") assert locker.written_data == expected def test_run_update_with_locked_extras( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage -): +) -> None: locker.locked(True) locker.mock_lock_data( { @@ -1546,15 +1595,16 @@ def test_run_update_with_locked_extras( installer.update(True) installer.whitelist("D") - installer.run() - expected = fixture("update-with-locked-extras") + result = installer.run() + assert result == 0 + expected = fixture("update-with-locked-extras") assert locker.written_data == expected def test_run_install_duplicate_dependencies_different_constraints( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") @@ -1579,7 +1629,8 @@ def test_run_install_duplicate_dependencies_different_constraints( repo.add_package(package_c12) repo.add_package(package_c15) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-duplicate-dependencies") @@ -1597,7 +1648,7 @@ def test_run_install_duplicate_dependencies_different_constraints( def test_run_install_duplicate_dependencies_different_constraints_with_lock( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage -): +) -> None: locker.locked(True) locker.mock_lock_data( { @@ -1691,7 +1742,8 @@ def test_run_install_duplicate_dependencies_different_constraints_with_lock( repo.add_package(package_c15) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-duplicate-dependencies") @@ -1708,7 +1760,7 @@ def test_run_update_uninstalls_after_removal_transient_dependency( repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, -): +) -> None: locker.locked(True) locker.mock_lock_data( { @@ -1757,7 +1809,8 @@ def test_run_update_uninstalls_after_removal_transient_dependency( installed.add_package(get_package("B", "1.0")) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 0 assert installer.executor.updates_count == 0 @@ -1770,7 +1823,7 @@ def test_run_install_duplicate_dependencies_different_constraints_with_lock_upda repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, -): +) -> None: locker.locked(True) locker.mock_lock_data( { @@ -1862,7 +1915,8 @@ def test_run_install_duplicate_dependencies_different_constraints_with_lock_upda installer.update(True) installer.whitelist(["A"]) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-duplicate-dependencies-update") @@ -1882,7 +1936,7 @@ def test_installer_test_solver_finds_compatible_package_for_dependency_python_no repo: Repository, package: ProjectPackage, installed: CustomInstalledRepository, -): +) -> None: package.python_versions = "~2.7 || ^3.4" package.add_dependency( Factory.create_dependency("A", {"version": "^1.0", "python": "^3.5"}) @@ -1897,7 +1951,8 @@ def test_installer_test_solver_finds_compatible_package_for_dependency_python_no repo.add_package(package_a100) repo.add_package(package_a101) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-conditional-dependency") assert locker.written_data == expected @@ -1911,9 +1966,9 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de package: ProjectPackage, installed: CustomInstalledRepository, env: NullEnv, - pool: Pool, + pool: RepositoryPool, config: Config, -): +) -> None: package.add_dependency(Factory.create_dependency("A", {"version": "^1.0"})) package_a = get_package("A", "1.0.0") @@ -1936,7 +1991,8 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de repo.add_package(package_d) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 3 assert installer.executor.updates_count == 0 @@ -1960,11 +2016,10 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de installed=installed, executor=Executor(env, pool, config, NullIO()), ) - installer.use_executor() - installer.update(True) installer.whitelist(["D"]) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 1 assert installer.executor.updates_count == 0 @@ -1979,10 +2034,10 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de env: NullEnv, mocker: MockerFixture, config: Config, -): +) -> None: mocker.patch("sys.platform", "darwin") - pool = Pool() + pool = RepositoryPool() pool.add_repository(MockRepository()) installer = Installer( @@ -1995,12 +2050,12 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de installed=installed, executor=Executor(env, pool, config, NullIO()), ) - installer.use_executor() package.add_dependency(Factory.create_dependency("poetry", {"version": "^0.12.0"})) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 3 assert installer.executor.updates_count == 0 @@ -2024,11 +2079,10 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de installed=installed, executor=Executor(env, pool, config, NullIO()), ) - installer.use_executor() - installer.update(True) installer.whitelist(["pytest"]) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 7 assert installer.executor.updates_count == 0 @@ -2042,8 +2096,8 @@ def test_installer_required_extras_should_be_installed( installed: CustomInstalledRepository, env: NullEnv, config: Config, -): - pool = Pool() +) -> None: + pool = RepositoryPool() pool.add_repository(MockRepository()) installer = Installer( @@ -2056,8 +2110,6 @@ def test_installer_required_extras_should_be_installed( installed=installed, executor=Executor(env, pool, config, NullIO()), ) - installer.use_executor() - package.add_dependency( Factory.create_dependency( "cachecontrol", {"version": "^0.12.5", "extras": ["filecache"]} @@ -2065,7 +2117,8 @@ def test_installer_required_extras_should_be_installed( ) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 2 assert installer.executor.updates_count == 0 @@ -2084,10 +2137,9 @@ def test_installer_required_extras_should_be_installed( installed=installed, executor=Executor(env, pool, config, NullIO()), ) - installer.use_executor() - installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 2 assert installer.executor.updates_count == 0 @@ -2096,7 +2148,7 @@ def test_installer_required_extras_should_be_installed( def test_update_multiple_times_with_split_dependencies_is_idempotent( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage -): +) -> None: locker.locked(True) locker.mock_lock_data( { @@ -2154,21 +2206,24 @@ def test_update_multiple_times_with_split_dependencies_is_idempotent( expected = fixture("with-multiple-updates") installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert locker.written_data == expected locker.mock_lock_data(locker.written_data) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert locker.written_data == expected locker.mock_lock_data(locker.written_data) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert locker.written_data == expected @@ -2179,13 +2234,13 @@ def test_installer_can_install_dependencies_from_forced_source( installed: CustomInstalledRepository, env: NullEnv, config: Config, -): +) -> None: package.python_versions = "^3.7" package.add_dependency( Factory.create_dependency("tomlkit", {"version": "^0.5", "source": "legacy"}) ) - pool = Pool() + pool = RepositoryPool() pool.add_repository(MockLegacyRepository()) pool.add_repository(MockRepository()) @@ -2199,10 +2254,9 @@ def test_installer_can_install_dependencies_from_forced_source( installed=installed, executor=Executor(env, pool, config, NullIO()), ) - installer.use_executor() - installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 1 assert installer.executor.updates_count == 0 @@ -2211,13 +2265,14 @@ def test_installer_can_install_dependencies_from_forced_source( def test_run_installs_with_url_file( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage -): +) -> None: url = "https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl" package.add_dependency(Factory.create_dependency("demo", {"url": url})) repo.add_package(get_package("pendulum", "1.4.4")) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-url-dependency") @@ -2228,7 +2283,7 @@ def test_run_installs_with_url_file( @pytest.mark.parametrize("env_platform", ["linux", "win32"]) def test_run_installs_with_same_version_url_files( - pool: Pool, + pool: RepositoryPool, locker: Locker, installed: CustomInstalledRepository, config: Config, @@ -2266,8 +2321,8 @@ def test_run_installs_with_same_version_url_files( NullIO(), ), ) - installer.use_executor(True) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-same-version-url-dependencies") assert locker.written_data == expected @@ -2278,7 +2333,7 @@ def test_run_installs_with_same_version_url_files( def test_installer_uses_prereleases_if_they_are_compatible( installer: Installer, locker: Locker, package: ProjectPackage, repo: Repository -): +) -> None: package.python_versions = "~2.7 || ^3.4" package.add_dependency( Factory.create_dependency( @@ -2291,7 +2346,8 @@ def test_installer_uses_prereleases_if_they_are_compatible( repo.add_package(package_b) - installer.run() + result = installer.run() + assert result == 0 del installer.installer.installs[:] locker.locked(True) @@ -2301,7 +2357,8 @@ def test_installer_uses_prereleases_if_they_are_compatible( installer.whitelist(["b"]) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 2 @@ -2312,8 +2369,8 @@ def test_installer_can_handle_old_lock_files( repo: Repository, installed: CustomInstalledRepository, config: Config, -): - pool = Pool() +) -> None: + pool = RepositoryPool() pool.add_repository(MockRepository()) package.add_dependency(Factory.create_dependency("pytest", "^3.5", groups=["dev"])) @@ -2331,9 +2388,8 @@ def test_installer_can_handle_old_lock_files( installed=installed, executor=Executor(MockEnv(), pool, config, NullIO()), ) - installer.use_executor() - - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 6 @@ -2352,9 +2408,8 @@ def test_installer_can_handle_old_lock_files( NullIO(), ), ) - installer.use_executor() - - installer.run() + result = installer.run() + assert result == 0 # funcsigs will be added assert installer.executor.installations_count == 7 @@ -2374,14 +2429,36 @@ def test_installer_can_handle_old_lock_files( NullIO(), ), ) - installer.use_executor() - - installer.run() + result = installer.run() + assert result == 0 # colorama will be added assert installer.executor.installations_count == 8 +def test_installer_does_not_write_lock_file_when_installation_fails( + installer: Installer, + locker: Locker, + repo: Repository, + package: ProjectPackage, + mocker: MockerFixture, +) -> None: + repo.add_package(get_package("A", "1.0")) + package.add_dependency(Factory.create_dependency("A", "~1.0")) + + locker.locked(False) + + mocker.patch("poetry.installation.installer.Installer._execute", return_value=1) + result = installer.run() + assert result == 1 # error + + assert locker._lock_data is None + + assert installer.executor.installations_count == 0 + assert installer.executor.updates_count == 0 + assert installer.executor.removals_count == 0 + + @pytest.mark.parametrize("quiet", [True, False]) def test_run_with_dependencies_quiet( installer: Installer, @@ -2389,7 +2466,7 @@ def test_run_with_dependencies_quiet( repo: Repository, package: ProjectPackage, quiet: bool, -): +) -> None: package_a = get_package("A", "1.0") package_b = get_package("B", "1.1") repo.add_package(package_a) @@ -2401,9 +2478,10 @@ def test_run_with_dependencies_quiet( package.add_dependency(Factory.create_dependency("A", "~1.0")) package.add_dependency(Factory.create_dependency("B", "^1.0")) - installer.run() - expected = fixture("with-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-dependencies") assert locker.written_data == expected installer._io.output._buffer.seek(0) @@ -2415,7 +2493,7 @@ def test_run_with_dependencies_quiet( def test_installer_should_use_the_locked_version_of_git_dependencies( installer: Installer, locker: Locker, package: ProjectPackage, repo: Repository -): +) -> None: locker.locked(True) locker.mock_lock_data( { @@ -2464,7 +2542,8 @@ def test_installer_should_use_the_locked_version_of_git_dependencies( repo.add_package(get_package("pendulum", "1.4.4")) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations[-1] == Package( "demo", @@ -2483,7 +2562,7 @@ def test_installer_should_use_the_locked_version_of_git_dependencies_with_extras package: ProjectPackage, repo: Repository, is_locked: bool, -): +) -> None: if is_locked: locker.locked(True) locker.mock_lock_data(fixture("with-vcs-dependency-with-extras")) @@ -2505,7 +2584,8 @@ def test_installer_should_use_the_locked_version_of_git_dependencies_with_extras repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cleo", "1.0.0")) - installer.run() + result = installer.run() + assert result == 0 assert len(installer.executor.installations) == 3 assert installer.executor.installations[-1] == Package( @@ -2525,7 +2605,7 @@ def test_installer_should_use_the_locked_version_of_git_dependencies_without_ref package: ProjectPackage, repo: Repository, is_locked: bool, -): +) -> None: """ If there is no explicit reference (branch or tag or rev) in pyproject.toml, HEAD is used. @@ -2543,7 +2623,8 @@ def test_installer_should_use_the_locked_version_of_git_dependencies_without_ref repo.add_package(get_package("pendulum", "1.4.4")) - installer.run() + result = installer.run() + assert result == 0 assert len(installer.executor.installations) == 2 assert installer.executor.installations[-1] == Package( @@ -2554,3 +2635,107 @@ def test_installer_should_use_the_locked_version_of_git_dependencies_without_ref source_reference="HEAD", source_resolved_reference=expected_reference, ) + + +# https://github.com/python-poetry/poetry/issues/6710 +@pytest.mark.parametrize("env_platform", ["darwin", "linux"]) +def test_installer_distinguishes_locked_packages_by_source( + pool: RepositoryPool, + locker: Locker, + installed: CustomInstalledRepository, + config: Config, + repo: Repository, + package: ProjectPackage, + env_platform: str, +) -> None: + # Require 1.11.0+cpu from pytorch for most platforms, but specify 1.11.0 and pypi on + # darwin. + package.add_dependency( + Factory.create_dependency( + "torch", + { + "version": "1.11.0+cpu", + "markers": "sys_platform != 'darwin'", + "source": "pytorch", + }, + ) + ) + package.add_dependency( + Factory.create_dependency( + "torch", + { + "version": "1.11.0", + "markers": "sys_platform == 'darwin'", + "source": "pypi", + }, + ) + ) + + # Locking finds both the pypi and the pytorch packages. + locker.locked(True) + locker.mock_lock_data( + { + "package": [ + { + "name": "torch", + "version": "1.11.0", + "category": "main", + "optional": False, + "files": [], + "python-versions": "*", + }, + { + "name": "torch", + "version": "1.11.0+cpu", + "category": "main", + "optional": False, + "files": [], + "python-versions": "*", + "source": { + "type": "legacy", + "url": "https://download.pytorch.org/whl", + "reference": "pytorch", + }, + }, + ], + "metadata": { + "python-versions": "*", + "platform": "*", + "content-hash": "123456789", + }, + } + ) + installer = Installer( + NullIO(), + MockEnv(platform=env_platform), + package, + locker, + pool, + config, + installed=installed, + executor=Executor( + MockEnv(platform=env_platform), + pool, + config, + NullIO(), + ), + ) + result = installer.run() + assert result == 0 + + # Results of installation are consistent with the platform requirements. + version = "1.11.0" if env_platform == "darwin" else "1.11.0+cpu" + source_type = None if env_platform == "darwin" else "legacy" + source_url = ( + None if env_platform == "darwin" else "https://download.pytorch.org/whl" + ) + source_reference = None if env_platform == "darwin" else "pytorch" + + assert len(installer.executor.installations) == 1 + assert installer.executor.installations[0] == Package( + "torch", + version, + source_type=source_type, + source_url=source_url, + source_reference=source_reference, + ) diff --git a/tests/installation/test_installer_old.py b/tests/installation/test_installer_old.py index 3c7fe7f2e6f..ced7a6fc934 100644 --- a/tests/installation/test_installer_old.py +++ b/tests/installation/test_installer_old.py @@ -4,20 +4,21 @@ from pathlib import Path from typing import TYPE_CHECKING +from typing import Any import pytest from cleo.io.null_io import NullIO 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 from poetry.installation.noop_installer import NoopInstaller from poetry.packages import Locker as BaseLocker -from poetry.repositories import Pool from poetry.repositories import Repository +from poetry.repositories import RepositoryPool from poetry.repositories.installed_repository import InstalledRepository +from poetry.toml.file import TOMLFile from poetry.utils.env import MockEnv from poetry.utils.env import NullEnv from tests.helpers import get_dependency @@ -40,6 +41,10 @@ class Installer(BaseInstaller): + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self._use_executor = False + def _get_installer(self) -> NoopInstaller: return NoopInstaller() @@ -53,18 +58,19 @@ def load( class Locker(BaseLocker): - def __init__(self, lock_path: str | Path) -> None: - self._lock = TOMLFile(Path(lock_path).joinpath("poetry.lock")) + def __init__(self, lock_path: Path) -> None: + self._lock = lock_path / "poetry.lock" self._written_data = None self._locked = False + self._lock_data = None self._content_hash = self._get_content_hash() @property def written_data(self) -> dict | None: return self._written_data - def set_lock_path(self, lock: str | Path) -> Locker: - self._lock = TOMLFile(Path(lock).joinpath("poetry.lock")) + def set_lock_path(self, lock: Path) -> Locker: + self._lock = lock / "poetry.lock" return self @@ -108,8 +114,8 @@ def repo() -> Repository: @pytest.fixture() -def pool(repo: Repository) -> Pool: - pool = Pool() +def pool(repo: Repository) -> RepositoryPool: + pool = RepositoryPool() pool.add_repository(repo) return pool @@ -126,14 +132,14 @@ def locker(project_root: Path) -> Locker: @pytest.fixture() -def env() -> NullEnv: - return NullEnv() +def env(tmp_path: Path) -> NullEnv: + return NullEnv(path=tmp_path) @pytest.fixture() def installer( package: ProjectPackage, - pool: Pool, + pool: RepositoryPool, locker: Locker, env: NullEnv, installed: CustomInstalledRepository, @@ -149,9 +155,10 @@ def fixture(name: str) -> str: def test_run_no_dependencies(installer: Installer, locker: Locker): - installer.run() - expected = fixture("no-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("no-dependencies") assert locker.written_data == expected @@ -166,9 +173,10 @@ def test_run_with_dependencies( package.add_dependency(Factory.create_dependency("A", "~1.0")) package.add_dependency(Factory.create_dependency("B", "^1.0")) - installer.run() - expected = fixture("with-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-dependencies") assert locker.written_data == expected @@ -234,9 +242,10 @@ def test_run_update_after_removing_dependencies( package.add_dependency(Factory.create_dependency("B", "~1.1")) installer.update(True) - installer.run() - expected = fixture("with-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-dependencies") assert locker.written_data == expected installs = installer.installer.installs @@ -312,7 +321,8 @@ def test_run_install_no_group( package.add_dependency(Factory.create_dependency("C", "~1.2", groups=["dev"])) installer.only_groups([]) - installer.run() + result = installer.run() + assert result == 0 installs = installer.installer.installs assert len(installs) == 0 @@ -395,7 +405,8 @@ def test_run_install_with_synchronization( ) installer.requires_synchronization(True) - installer.run() + result = installer.run() + assert result == 0 installs = installer.installer.installs assert len(installs) == 0 @@ -451,9 +462,10 @@ def test_run_whitelist_add( installer.update(True) installer.whitelist(["B"]) - installer.run() - expected = fixture("with-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-dependencies") assert locker.written_data == expected @@ -506,9 +518,10 @@ def test_run_whitelist_remove( installer.update(True) installer.whitelist(["B"]) - installer.run() - expected = fixture("remove") + result = installer.run() + assert result == 0 + expected = fixture("remove") assert locker.written_data == expected assert len(installer.installer.installs) == 1 assert len(installer.installer.updates) == 0 @@ -533,9 +546,10 @@ def test_add_with_sub_dependencies( package_a.add_dependency(Factory.create_dependency("D", "^1.0")) package_b.add_dependency(Factory.create_dependency("C", "~1.2")) - installer.run() - expected = fixture("with-sub-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-sub-dependencies") assert locker.written_data == expected @@ -560,9 +574,10 @@ def test_run_with_python_versions( package.add_dependency(Factory.create_dependency("B", "^1.0")) package.add_dependency(Factory.create_dependency("C", "^1.0")) - installer.run() - expected = fixture("with-python-versions") + result = installer.run() + assert result == 0 + expected = fixture("with-python-versions") assert locker.written_data == expected @@ -595,9 +610,10 @@ def test_run_with_optional_and_python_restricted_dependencies( Factory.create_dependency("C", {"version": "^1.0", "python": "~2.7 || ^3.4"}) ) - installer.run() - expected = fixture("with-optional-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-optional-dependencies") assert locker.written_data == expected installer = installer.installer @@ -642,9 +658,10 @@ def test_run_with_optional_and_platform_restricted_dependencies( Factory.create_dependency("C", {"version": "^1.0", "platform": "darwin"}) ) - installer.run() - expected = fixture("with-platform-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-platform-dependencies") assert locker.written_data == expected installer = installer.installer @@ -677,9 +694,10 @@ def test_run_with_dependencies_extras( Factory.create_dependency("B", {"version": "^1.0", "extras": ["foo"]}) ) - installer.run() - expected = fixture("with-dependencies-extras") + result = installer.run() + assert result == 0 + expected = fixture("with-dependencies-extras") assert locker.written_data == expected @@ -704,10 +722,11 @@ def test_run_does_not_install_extras_if_not_requested( Factory.create_dependency("D", {"version": "^1.0", "optional": True}) ) - installer.run() - expected = fixture("extras") + result = installer.run() + assert result == 0 # Extras are pinned in lock + expected = fixture("extras") assert locker.written_data == expected # But should not be installed @@ -737,10 +756,11 @@ def test_run_installs_extras_if_requested( ) installer.extras(["foo"]) - installer.run() - expected = fixture("extras") + result = installer.run() + assert result == 0 # Extras are pinned in lock + expected = fixture("extras") assert locker.written_data == expected # But should not be installed @@ -771,10 +791,11 @@ def test_run_installs_extras_with_deps_if_requested( package_c.add_dependency(Factory.create_dependency("D", "^1.0")) installer.extras(["foo"]) - installer.run() - expected = fixture("extras-with-dependencies") + result = installer.run() + assert result == 0 # Extras are pinned in lock + expected = fixture("extras-with-dependencies") assert locker.written_data == expected # But should not be installed @@ -807,7 +828,8 @@ def test_run_installs_extras_with_deps_if_requested_locked( package_c.add_dependency(Factory.create_dependency("D", "^1.0")) installer.extras(["foo"]) - installer.run() + result = installer.run() + assert result == 0 # But should not be installed installer = installer.installer @@ -819,19 +841,21 @@ def test_installer_with_pypi_repository( locker: Locker, installed: CustomInstalledRepository, config: Config, + env: NullEnv, ): - pool = Pool() + pool = RepositoryPool() pool.add_repository(MockRepository()) installer = Installer( - NullIO(), NullEnv(), package, locker, pool, config, installed=installed + NullIO(), env, package, locker, pool, config, installed=installed ) + package.python_versions = ">=3.7" package.add_dependency(Factory.create_dependency("pytest", "^3.5", groups=["dev"])) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-pypi-repository") - assert expected == locker.written_data @@ -847,7 +871,8 @@ def test_run_installs_with_local_file( repo.add_package(get_package("pendulum", "1.4.4")) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-file-dependency") @@ -868,7 +893,8 @@ def test_run_installs_wheel_with_no_requires_dist( ) package.add_dependency(Factory.create_dependency("demo", {"file": str(file_path)})) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-wheel-dependency-no-requires-dist") @@ -894,7 +920,8 @@ def test_run_installs_with_local_poetry_directory_and_extras( repo.add_package(get_package("pendulum", "1.4.4")) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-directory-dependency-poetry") @@ -926,7 +953,8 @@ def test_run_installs_with_local_poetry_directory_transitive( repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cachy", "0.2.0")) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-directory-dependency-poetry-transitive") @@ -958,7 +986,8 @@ def test_run_installs_with_local_poetry_file_transitive( repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cachy", "0.2.0")) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-file-dependency-transitive") @@ -983,7 +1012,8 @@ def test_run_installs_with_local_setuptools_directory( repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cachy", "0.2.0")) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-directory-dependency-setuptools") @@ -1030,9 +1060,10 @@ def test_run_with_prereleases( installer.update(True) installer.whitelist({"B": "^1.1"}) - installer.run() - expected = fixture("with-prereleases") + result = installer.run() + assert result == 0 + expected = fixture("with-prereleases") assert locker.written_data == expected @@ -1116,9 +1147,10 @@ def test_run_update_all_with_lock( installer.update(True) - installer.run() - expected = fixture("update-with-lock") + result = installer.run() + assert result == 0 + expected = fixture("update-with-lock") assert locker.written_data == expected @@ -1189,9 +1221,10 @@ def test_run_update_with_locked_extras( installer.update(True) installer.whitelist("D") - installer.run() - expected = fixture("update-with-locked-extras") + result = installer.run() + assert result == 0 + expected = fixture("update-with-locked-extras") assert locker.written_data == expected @@ -1222,7 +1255,8 @@ def test_run_install_duplicate_dependencies_different_constraints( repo.add_package(package_c12) repo.add_package(package_c15) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-duplicate-dependencies") @@ -1336,7 +1370,8 @@ def test_run_install_duplicate_dependencies_different_constraints_with_lock( repo.add_package(package_c15) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-duplicate-dependencies") @@ -1405,7 +1440,8 @@ def test_run_update_uninstalls_after_removal_transient_dependency( installed.add_package(get_package("B", "1.0")) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 installs = installer.installer.installs assert len(installs) == 0 @@ -1513,7 +1549,8 @@ def test_run_install_duplicate_dependencies_different_constraints_with_lock_upda installer.update(True) installer.whitelist(["A"]) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-duplicate-dependencies-update") @@ -1551,7 +1588,8 @@ def test_installer_test_solver_finds_compatible_package_for_dependency_python_no repo.add_package(package_a100) repo.add_package(package_a101) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-conditional-dependency") assert locker.written_data == expected @@ -1567,7 +1605,7 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de package: ProjectPackage, installed: CustomInstalledRepository, env: NullEnv, - pool: Pool, + pool: RepositoryPool, config: Config, ): package.add_dependency(Factory.create_dependency("A", {"version": "^1.0"})) @@ -1592,7 +1630,8 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de repo.add_package(package_d) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert len(installer.installer.installs) == 3 assert len(installer.installer.updates) == 0 @@ -1612,7 +1651,8 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de installer.update(True) installer.whitelist(["D"]) - installer.run() + result = installer.run() + assert result == 0 assert len(installer.installer.installs) == 1 assert len(installer.installer.updates) == 0 @@ -1630,7 +1670,7 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de ): mocker.patch("sys.platform", "darwin") - pool = Pool() + pool = RepositoryPool() pool.add_repository(MockRepository()) installer = Installer( @@ -1640,7 +1680,8 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de package.add_dependency(Factory.create_dependency("poetry", {"version": "^0.12.0"})) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert len(installer.installer.installs) == 3 assert len(installer.installer.updates) == 0 @@ -1660,7 +1701,8 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de installer.update(True) installer.whitelist(["pytest"]) - installer.run() + result = installer.run() + assert result == 0 assert len(installer.installer.installs) == 7 assert len(installer.installer.updates) == 0 @@ -1675,7 +1717,7 @@ def test_installer_required_extras_should_be_installed( env: NullEnv, config: Config, ): - pool = Pool() + pool = RepositoryPool() pool.add_repository(MockRepository()) installer = Installer( @@ -1689,7 +1731,8 @@ def test_installer_required_extras_should_be_installed( ) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert len(installer.installer.installs) == 2 assert len(installer.installer.updates) == 0 @@ -1703,7 +1746,8 @@ def test_installer_required_extras_should_be_installed( ) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert len(installer.installer.installs) == 2 assert len(installer.installer.updates) == 0 @@ -1770,21 +1814,24 @@ def test_update_multiple_times_with_split_dependencies_is_idempotent( expected = fixture("with-multiple-updates") installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert locker.written_data == expected locker.mock_lock_data(locker.written_data) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert locker.written_data == expected locker.mock_lock_data(locker.written_data) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert locker.written_data == expected @@ -1801,7 +1848,7 @@ def test_installer_can_install_dependencies_from_forced_source( Factory.create_dependency("tomlkit", {"version": "^0.5", "source": "legacy"}) ) - pool = Pool() + pool = RepositoryPool() pool.add_repository(MockLegacyRepository()) pool.add_repository(MockRepository()) @@ -1810,7 +1857,8 @@ def test_installer_can_install_dependencies_from_forced_source( ) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert len(installer.installer.installs) == 1 assert len(installer.installer.updates) == 0 @@ -1825,7 +1873,8 @@ def test_run_installs_with_url_file( repo.add_package(get_package("pendulum", "1.4.4")) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-url-dependency") @@ -1849,7 +1898,8 @@ def test_installer_uses_prereleases_if_they_are_compatible( repo.add_package(package_b) - installer.run() + result = installer.run() + assert result == 0 del installer.installer.installs[:] locker.locked(True) @@ -1859,7 +1909,8 @@ def test_installer_uses_prereleases_if_they_are_compatible( installer.whitelist(["b"]) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert len(installer.installer.installs) == 2 @@ -1871,7 +1922,7 @@ def test_installer_can_handle_old_lock_files( installed: CustomInstalledRepository, config: Config, ): - pool = Pool() + pool = RepositoryPool() pool.add_repository(MockRepository()) package.add_dependency(Factory.create_dependency("pytest", "^3.5", groups=["dev"])) @@ -1883,7 +1934,8 @@ def test_installer_can_handle_old_lock_files( NullIO(), MockEnv(), package, locker, pool, config, installed=installed ) - installer.run() + result = installer.run() + assert result == 0 assert len(installer.installer.installs) == 6 @@ -1897,7 +1949,8 @@ def test_installer_can_handle_old_lock_files( installed=installed, ) - installer.run() + result = installer.run() + assert result == 0 # funcsigs will be added assert len(installer.installer.installs) == 7 @@ -1912,7 +1965,8 @@ def test_installer_can_handle_old_lock_files( installed=installed, ) - installer.run() + result = installer.run() + assert result == 0 # colorama will be added assert len(installer.installer.installs) == 8 diff --git a/tests/installation/test_pip_installer.py b/tests/installation/test_pip_installer.py index eab8303cba3..2a07da92f73 100644 --- a/tests/installation/test_pip_installer.py +++ b/tests/installation/test_pip_installer.py @@ -13,7 +13,8 @@ from poetry.installation.pip_installer import PipInstaller from poetry.repositories.legacy_repository import LegacyRepository -from poetry.repositories.pool import Pool +from poetry.repositories.repository_pool import Priority +from poetry.repositories.repository_pool import RepositoryPool from poetry.utils.authenticator import RepositoryCertificateConfig from poetry.utils.env import NullEnv @@ -23,6 +24,7 @@ from poetry.utils.env import VirtualEnv from tests.conftest import Config + from tests.types import FixtureDirGetter @pytest.fixture @@ -53,16 +55,21 @@ def package_git_with_subdirectory() -> Package: @pytest.fixture -def pool() -> Pool: - return Pool() +def pool() -> RepositoryPool: + return RepositoryPool() + + +@pytest.fixture() +def env(tmp_path: Path) -> NullEnv: + return NullEnv(path=tmp_path) @pytest.fixture -def installer(pool: Pool) -> PipInstaller: - return PipInstaller(NullEnv(), NullIO(), pool) +def installer(pool: RepositoryPool, env: NullEnv) -> PipInstaller: + return PipInstaller(env, NullIO(), pool) -def test_requirement(installer: PipInstaller): +def test_requirement(installer: PipInstaller) -> None: package = Package("ipython", "7.5.0") package.files = [ {"file": "foo-0.1.0.tar.gz", "hash": "md5:dbdc53e3918f28fa335a173432402a00"}, @@ -83,8 +90,8 @@ def test_requirement(installer: PipInstaller): assert result == expected -def test_requirement_source_type_url(): - installer = PipInstaller(NullEnv(), NullIO(), Pool()) +def test_requirement_source_type_url(env: NullEnv) -> None: + installer = PipInstaller(env, NullIO(), RepositoryPool()) foo = Package( "foo", @@ -100,10 +107,9 @@ def test_requirement_source_type_url(): def test_requirement_git_subdirectory( - pool: Pool, package_git_with_subdirectory: Package + pool: RepositoryPool, package_git_with_subdirectory: Package, env: NullEnv ) -> None: - null_env = NullEnv() - installer = PipInstaller(null_env, NullIO(), pool) + installer = PipInstaller(env, NullIO(), pool) result = installer.requirement(package_git_with_subdirectory) expected = ( "git+https://github.com/demo/subdirectories.git" @@ -112,12 +118,14 @@ def test_requirement_git_subdirectory( assert result == expected installer.install(package_git_with_subdirectory) - assert len(null_env.executed) == 1 - cmd = null_env.executed[0] + assert len(env.executed) == 1 + cmd = env.executed[0] assert Path(cmd[-1]).parts[-3:] == ("demo", "subdirectories", "two") -def test_requirement_git_develop_false(installer: PipInstaller, package_git: Package): +def test_requirement_git_develop_false( + installer: PipInstaller, package_git: Package +) -> None: package_git.develop = False result = installer.requirement(package_git) expected = "git+git@github.com:demo/demo.git@master#egg=demo" @@ -125,11 +133,13 @@ def test_requirement_git_develop_false(installer: PipInstaller, package_git: Pac assert result == expected -def test_install_with_non_pypi_default_repository(pool: Pool, installer: PipInstaller): +def test_install_with_non_pypi_default_repository( + pool: RepositoryPool, installer: PipInstaller +) -> None: default = LegacyRepository("default", "https://default.com") another = LegacyRepository("another", "https://another.com") - pool.add_repository(default, default=True) + pool.add_repository(default, priority=Priority.DEFAULT) pool.add_repository(another) foo = Package( @@ -158,7 +168,9 @@ def test_install_with_non_pypi_default_repository(pool: Pool, installer: PipInst ("cert", "cert"), ], ) -def test_install_with_certs(mocker: MockerFixture, key: str, option: str): +def test_install_with_certs( + mocker: MockerFixture, key: str, option: str, env: NullEnv +) -> None: client_path = "path/to/client.pem" mocker.patch( "poetry.utils.authenticator.Authenticator.get_certs_for_url", @@ -166,12 +178,10 @@ def test_install_with_certs(mocker: MockerFixture, key: str, option: str): ) default = LegacyRepository("default", "https://foo.bar") - pool = Pool() - pool.add_repository(default, default=True) + pool = RepositoryPool() + pool.add_repository(default, priority=Priority.DEFAULT) - null_env = NullEnv() - - installer = PipInstaller(null_env, NullIO(), pool) + installer = PipInstaller(env, NullIO(), pool) foo = Package( "foo", @@ -183,15 +193,17 @@ def test_install_with_certs(mocker: MockerFixture, key: str, option: str): installer.install(foo) - assert len(null_env.executed) == 1 - cmd = null_env.executed[0] + assert len(env.executed) == 1 + cmd = env.executed[0] assert f"--{option}" in cmd cert_index = cmd.index(f"--{option}") # Need to do the str(Path()) bit because Windows paths get modified by Path assert cmd[cert_index + 1] == str(Path(client_path)) -def test_requirement_git_develop_true(installer: PipInstaller, package_git: Package): +def test_requirement_git_develop_true( + installer: PipInstaller, package_git: Package +) -> None: package_git.develop = True result = installer.requirement(package_git) expected = ["-e", "git+git@github.com:demo/demo.git@master#egg=demo"] @@ -200,8 +212,8 @@ def test_requirement_git_develop_true(installer: PipInstaller, package_git: Pack def test_uninstall_git_package_nspkg_pth_cleanup( - mocker: MockerFixture, tmp_venv: VirtualEnv, pool: Pool -): + mocker: MockerFixture, tmp_venv: VirtualEnv, pool: RepositoryPool +) -> None: # this test scenario requires a real installation using the pip installer installer = PipInstaller(tmp_venv, NullIO(), pool) @@ -232,7 +244,7 @@ def copy_only(source: Path, dest: Path) -> None: installer.install(package) installer.remove(package) - pth_file = f"{package.name}-nspkg.pth" + pth_file = Path(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 @@ -240,16 +252,14 @@ def copy_only(source: Path, dest: Path) -> None: assert not re.match(rf"Error processing line 1 of .*{pth_file}", output) -def test_install_with_trusted_host(config: Config): +def test_install_with_trusted_host(config: Config, env: NullEnv) -> None: config.merge({"certificates": {"default": {"cert": False}}}) default = LegacyRepository("default", "https://foo.bar") - pool = Pool() - pool.add_repository(default, default=True) + pool = RepositoryPool() + pool.add_repository(default, priority=Priority.DEFAULT) - null_env = NullEnv() - - installer = PipInstaller(null_env, NullIO(), pool) + installer = PipInstaller(env, NullIO(), pool) foo = Package( "foo", @@ -261,16 +271,19 @@ def test_install_with_trusted_host(config: Config): installer.install(foo) - assert len(null_env.executed) == 1 - cmd = null_env.executed[0] + assert len(env.executed) == 1 + cmd = env.executed[0] assert "--trusted-host" in cmd cert_index = cmd.index("--trusted-host") assert cmd[cert_index + 1] == "foo.bar" def test_install_directory_fallback_on_poetry_create_error( - mocker: MockerFixture, tmp_venv: VirtualEnv, pool: Pool -): + mocker: MockerFixture, + tmp_venv: VirtualEnv, + pool: RepositoryPool, + fixture_dir: FixtureDirGetter, +) -> None: mock_create_poetry = mocker.patch( "poetry.factory.Factory.create_poetry", side_effect=RuntimeError ) @@ -284,9 +297,7 @@ def test_install_directory_fallback_on_poetry_create_error( "demo", "1.0.0", source_type="directory", - source_url=str( - Path(__file__).parent.parent / "fixtures/inspection/demo_poetry_package" - ), + source_url=str(fixture_dir("inspection") / "demo_poetry_package"), ) installer = PipInstaller(tmp_venv, NullIO(), pool) diff --git a/tests/installation/test_wheel_installer.py b/tests/installation/test_wheel_installer.py new file mode 100644 index 00000000000..b7b3d7c7c93 --- /dev/null +++ b/tests/installation/test_wheel_installer.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import re + +from pathlib import Path +from typing import TYPE_CHECKING + +import pytest + +from poetry.core.constraints.version import parse_constraint + +from poetry.installation.wheel_installer import WheelInstaller +from poetry.utils.env import MockEnv + + +if TYPE_CHECKING: + from _pytest.tmpdir import TempPathFactory + + from tests.types import FixtureDirGetter + + +@pytest.fixture +def env(tmp_path: Path) -> MockEnv: + return MockEnv(path=tmp_path) + + +@pytest.fixture(scope="module") +def demo_wheel(fixture_dir: FixtureDirGetter) -> Path: + return fixture_dir("distributions/demo-0.1.0-py2.py3-none-any.whl") + + +@pytest.fixture(scope="module") +def default_installation(tmp_path_factory: TempPathFactory, demo_wheel: Path) -> Path: + env = MockEnv(path=tmp_path_factory.mktemp("default_install")) + installer = WheelInstaller(env) + installer.install(demo_wheel) + return Path(env.paths["purelib"]) + + +def test_default_installation_source_dir_content(default_installation: Path) -> None: + source_dir = default_installation / "demo" + assert source_dir.exists() + assert (source_dir / "__init__.py").exists() + + +def test_default_installation_dist_info_dir_content(default_installation: Path) -> None: + dist_info_dir = default_installation / "demo-0.1.0.dist-info" + assert dist_info_dir.exists() + assert (dist_info_dir / "INSTALLER").exists() + assert (dist_info_dir / "METADATA").exists() + assert (dist_info_dir / "RECORD").exists() + assert (dist_info_dir / "WHEEL").exists() + + +def test_installer_file_contains_valid_version(default_installation: Path) -> None: + installer_file = default_installation / "demo-0.1.0.dist-info" / "INSTALLER" + with open(installer_file) as f: + installer_content = f.read() + match = re.match(r"Poetry (?P.*)", installer_content) + assert match + parse_constraint(match.group("version")) # must not raise an error + + +def test_default_installation_no_bytecode(default_installation: Path) -> None: + cache_dir = default_installation / "demo" / "__pycache__" + assert not cache_dir.exists() + + +@pytest.mark.parametrize("compile", [True, False]) +def test_enable_bytecode_compilation( + env: MockEnv, demo_wheel: Path, compile: bool +) -> None: + installer = WheelInstaller(env) + installer.enable_bytecode_compilation(compile) + installer.install(demo_wheel) + cache_dir = Path(env.paths["purelib"]) / "demo" / "__pycache__" + if compile: + assert cache_dir.exists() + assert list(cache_dir.glob("*.pyc")) + assert not list(cache_dir.glob("*.opt-1.pyc")) + assert not list(cache_dir.glob("*.opt-2.pyc")) + else: + assert not cache_dir.exists() diff --git a/tests/integration/test_utils_vcs_git.py b/tests/integration/test_utils_vcs_git.py index 1ca7ea0c843..afbc256bd6b 100644 --- a/tests/integration/test_utils_vcs_git.py +++ b/tests/integration/test_utils_vcs_git.py @@ -14,16 +14,16 @@ from dulwich.client import get_transport_and_path from dulwich.config import ConfigFile from dulwich.repo import Repo -from poetry.core.pyproject.toml import PyProjectTOML -from poetry.console.exceptions import PoetrySimpleConsoleException +from poetry.console.exceptions import PoetryConsoleError +from poetry.pyproject.toml import PyProjectTOML from poetry.utils.authenticator import Authenticator from poetry.vcs.git import Git from poetry.vcs.git.backend import GitRefSpec if TYPE_CHECKING: - from _pytest.tmpdir import TempdirFactory + from _pytest.tmpdir import TempPathFactory from dulwich.client import FetchPackResult from dulwich.client import GitClient from pytest_mock import MockerFixture @@ -79,9 +79,9 @@ def source_directory_name(source_url: str) -> str: @pytest.fixture(scope="module") -def local_repo(tmpdir_factory: TempdirFactory, source_directory_name: str) -> Repo: +def local_repo(tmp_path_factory: TempPathFactory, source_directory_name: str) -> Repo: with Repo.init( - tmpdir_factory.mktemp("src") / source_directory_name, mkdir=True + tmp_path_factory.mktemp("src") / source_directory_name, mkdir=True ) as repo: yield repo @@ -112,7 +112,7 @@ def remote_default_branch(remote_default_ref: bytes) -> str: # Regression test for https://github.com/python-poetry/poetry/issues/6722 -def test_use_system_git_client_from_environment_variables(): +def test_use_system_git_client_from_environment_variables() -> None: os.environ["POETRY_EXPERIMENTAL_SYSTEM_GIT_CLIENT"] = "true" assert Git.is_using_legacy_client() @@ -132,7 +132,7 @@ def test_git_clone_default_branch_head( remote_refs: FetchPackResult, remote_default_ref: bytes, mocker: MockerFixture, -): +) -> None: spy = mocker.spy(Git, "_clone") spy_legacy = mocker.spy(Git, "_clone_legacy") @@ -143,19 +143,19 @@ def test_git_clone_default_branch_head( spy.assert_called() -def test_git_clone_fails_for_non_existent_branch(source_url: str): +def test_git_clone_fails_for_non_existent_branch(source_url: str) -> None: branch = uuid.uuid4().hex - with pytest.raises(PoetrySimpleConsoleException) as e: + with pytest.raises(PoetryConsoleError) as e: Git.clone(url=source_url, branch=branch) assert f"Failed to clone {source_url} at '{branch}'" in str(e.value) -def test_git_clone_fails_for_non_existent_revision(source_url: str): +def test_git_clone_fails_for_non_existent_revision(source_url: str) -> None: revision = sha1(uuid.uuid4().bytes).hexdigest() - with pytest.raises(PoetrySimpleConsoleException) as e: + with pytest.raises(PoetryConsoleError) as e: Git.clone(url=source_url, revision=revision) assert f"Failed to clone {source_url} at '{revision}'" in str(e.value) @@ -240,6 +240,30 @@ def test_git_clone_clones_submodules(source_url: str) -> None: assert len(list(submodule_package_directory.glob("*"))) > 1 +def test_git_clone_clones_submodules_with_relative_urls(source_url: str) -> None: + with Git.clone(url=source_url, branch="relative_submodule") as repo: + submodule_package_directory = ( + Path(repo.path) / "submodules" / "relative-url-submodule" + ) + + assert submodule_package_directory.exists() + assert submodule_package_directory.joinpath("README.md").exists() + assert len(list(submodule_package_directory.glob("*"))) > 1 + + +def test_git_clone_clones_submodules_with_relative_urls_and_explicit_base( + source_url: str, +) -> None: + with Git.clone(url=source_url, branch="relative_submodule") as repo: + submodule_package_directory = ( + Path(repo.path) / "submodules" / "relative-url-submodule-with-base" + ) + + assert submodule_package_directory.exists() + assert submodule_package_directory.joinpath("README.md").exists() + assert len(list(submodule_package_directory.glob("*"))) > 1 + + def test_system_git_fallback_on_http_401( mocker: MockerFixture, source_url: str, diff --git a/tests/json/fixtures/source/complete_invalid_priority.toml b/tests/json/fixtures/source/complete_invalid_priority.toml new file mode 100644 index 00000000000..ec27c209a2a --- /dev/null +++ b/tests/json/fixtures/source/complete_invalid_priority.toml @@ -0,0 +1,17 @@ +[tool.poetry] +name = "foobar" +version = "0.1.0" +description = "" +authors = ["Your Name "] + +[tool.poetry.dependencies] +python = "^3.10" + +[[tool.poetry.source]] +name = "pypi-simple" +url = "https://pypi.org/simple/" +priority = "arbitrary" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/json/fixtures/source/complete_invalid_priority_legacy_and_new.toml b/tests/json/fixtures/source/complete_invalid_priority_legacy_and_new.toml new file mode 100644 index 00000000000..4e2789b49d8 --- /dev/null +++ b/tests/json/fixtures/source/complete_invalid_priority_legacy_and_new.toml @@ -0,0 +1,18 @@ +[tool.poetry] +name = "foobar" +version = "0.1.0" +description = "" +authors = ["Your Name "] + +[tool.poetry.dependencies] +python = "^3.10" + +[[tool.poetry.source]] +name = "pypi-simple" +url = "https://pypi.org/simple/" +default = false +priority = "primary" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/json/fixtures/source/complete_invalid_url.toml b/tests/json/fixtures/source/complete_invalid_url.toml new file mode 100644 index 00000000000..6c61ba5df9b --- /dev/null +++ b/tests/json/fixtures/source/complete_invalid_url.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "foobar" +version = "0.1.0" +description = "" +authors = ["Your Name "] + +[tool.poetry.dependencies] +python = "^3.10" + +[[tool.poetry.source]] +name = "pypi-simple" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/json/fixtures/source/complete_valid.toml b/tests/json/fixtures/source/complete_valid.toml index d0b4565ffa4..fedab7bed39 100644 --- a/tests/json/fixtures/source/complete_valid.toml +++ b/tests/json/fixtures/source/complete_valid.toml @@ -10,8 +10,7 @@ python = "^3.10" [[tool.poetry.source]] name = "pypi-simple" url = "https://pypi.org/simple/" -default = false -secondary = false +priority = "explicit" [build-system] requires = ["poetry-core"] diff --git a/tests/json/fixtures/source/complete_invalid.toml b/tests/json/fixtures/source/complete_valid_legacy.toml similarity index 90% rename from tests/json/fixtures/source/complete_invalid.toml rename to tests/json/fixtures/source/complete_valid_legacy.toml index bd70bc6a833..d0b4565ffa4 100644 --- a/tests/json/fixtures/source/complete_invalid.toml +++ b/tests/json/fixtures/source/complete_valid_legacy.toml @@ -9,6 +9,7 @@ python = "^3.10" [[tool.poetry.source]] name = "pypi-simple" +url = "https://pypi.org/simple/" default = false secondary = false diff --git a/tests/json/test_schema_sources.py b/tests/json/test_schema_sources.py index 4f20a0b3884..78e446bc6b3 100644 --- a/tests/json/test_schema_sources.py +++ b/tests/json/test_schema_sources.py @@ -2,24 +2,58 @@ from pathlib import Path -from poetry.core.toml import TOMLFile - from poetry.factory import Factory +from poetry.toml import TOMLFile FIXTURE_DIR = Path(__file__).parent / "fixtures" / "source" +def test_pyproject_toml_valid_legacy() -> None: + toml = TOMLFile(FIXTURE_DIR / "complete_valid_legacy.toml").read() + content = toml["tool"]["poetry"] + assert Factory.validate(content) == {"errors": [], "warnings": []} + + def test_pyproject_toml_valid() -> None: toml = TOMLFile(FIXTURE_DIR / "complete_valid.toml").read() content = toml["tool"]["poetry"] assert Factory.validate(content) == {"errors": [], "warnings": []} -def test_pyproject_toml_invalid() -> None: - toml = TOMLFile(FIXTURE_DIR / "complete_invalid.toml").read() +def test_pyproject_toml_invalid_url() -> None: + toml = TOMLFile(FIXTURE_DIR / "complete_invalid_url.toml").read() content = toml["tool"]["poetry"] assert Factory.validate(content) == { "errors": ["[source.0] 'url' is a required property"], "warnings": [], } + + +def test_pyproject_toml_invalid_priority() -> None: + toml = TOMLFile(FIXTURE_DIR / "complete_invalid_priority.toml").read() + content = toml["tool"]["poetry"] + assert Factory.validate(content) == { + "errors": [ + "[source.0.priority] 'arbitrary' is not one of ['primary', 'default'," + " 'secondary', 'explicit']" + ], + "warnings": [], + } + + +def test_pyproject_toml_invalid_priority_legacy_and_new() -> None: + toml = TOMLFile( + FIXTURE_DIR / "complete_invalid_priority_legacy_and_new.toml" + ).read() + content = toml["tool"]["poetry"] + assert Factory.validate(content) == { + "errors": [ + "[source.0] {'name': 'pypi-simple', 'url': " + "'https://pypi.org/simple/', 'default': False, 'priority': " + "'primary'} should not be valid under {'anyOf': [{'required': " + "['priority', 'default']}, {'required': ['priority', " + "'secondary']}]}" + ], + "warnings": [], + } diff --git a/tests/masonry/builders/test_editable_builder.py b/tests/masonry/builders/test_editable_builder.py index effefb4c816..28b6628a972 100644 --- a/tests/masonry/builders/test_editable_builder.py +++ b/tests/masonry/builders/test_editable_builder.py @@ -28,42 +28,40 @@ from pytest_mock import MockerFixture from poetry.poetry import Poetry + from tests.types import FixtureDirGetter @pytest.fixture() -def simple_poetry() -> Poetry: - poetry = Factory().create_poetry( - Path(__file__).parent.parent.parent / "fixtures" / "simple_project" - ) +def simple_poetry(fixture_dir: FixtureDirGetter) -> Poetry: + poetry = Factory().create_poetry(fixture_dir("simple_project")) return poetry @pytest.fixture() -def project_with_include() -> Poetry: - poetry = Factory().create_poetry( - Path(__file__).parent.parent.parent / "fixtures" / "with-include" - ) +def project_with_include(fixture_dir: FixtureDirGetter) -> Poetry: + poetry = Factory().create_poetry(fixture_dir("with-include")) return poetry @pytest.fixture() -def extended_poetry() -> Poetry: - poetry = Factory().create_poetry( - Path(__file__).parent.parent.parent / "fixtures" / "extended_project" - ) +def extended_poetry(fixture_dir: FixtureDirGetter) -> Poetry: + poetry = Factory().create_poetry(fixture_dir("extended_project")) return poetry @pytest.fixture() -def extended_without_setup_poetry() -> Poetry: - poetry = Factory().create_poetry( - Path(__file__).parent.parent.parent - / "fixtures" - / "extended_project_without_setup" - ) +def extended_without_setup_poetry(fixture_dir: FixtureDirGetter) -> Poetry: + poetry = Factory().create_poetry(fixture_dir("extended_project_without_setup")) + + return poetry + + +@pytest.fixture +def with_multiple_readme_files(fixture_dir: FixtureDirGetter) -> Poetry: + poetry = Factory().create_poetry(fixture_dir("with_multiple_readme_files")) return poetry @@ -74,10 +72,10 @@ def env_manager(simple_poetry: Poetry) -> EnvManager: @pytest.fixture -def tmp_venv(tmp_dir: str, env_manager: EnvManager) -> VirtualEnv: - venv_path = Path(tmp_dir) / "venv" +def tmp_venv(tmp_path: Path, env_manager: EnvManager) -> VirtualEnv: + venv_path = tmp_path / "venv" - env_manager.build_venv(str(venv_path)) + env_manager.build_venv(venv_path) venv = VirtualEnv(venv_path) yield venv @@ -87,20 +85,20 @@ def tmp_venv(tmp_dir: str, env_manager: EnvManager) -> VirtualEnv: def test_builder_installs_proper_files_for_standard_packages( simple_poetry: Poetry, tmp_venv: VirtualEnv -): +) -> None: builder = EditableBuilder(simple_poetry, tmp_venv, NullIO()) builder.build() assert tmp_venv._bin_dir.joinpath("foo").exists() - pth_file = "simple_project.pth" + pth_file = Path("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 = "simple_project-1.2.3.dist-info" + dist_info = Path("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] @@ -167,7 +165,7 @@ def test_builder_installs_proper_files_for_standard_packages( assert all(len(row) == 3 for row in records) record_entries = {row[0] for row in records} - pth_file = "simple_project.pth" + pth_file = Path("simple_project.pth") assert tmp_venv.site_packages.exists(pth_file) assert str(tmp_venv.site_packages.find(pth_file)[0]) in record_entries assert str(tmp_venv._bin_dir.joinpath("foo")) in record_entries @@ -213,10 +211,10 @@ def test_builder_installs_proper_files_for_standard_packages( def test_builder_falls_back_on_setup_and_pip_for_packages_with_build_scripts( - mocker: MockerFixture, extended_poetry: Poetry, tmp_dir: str -): + mocker: MockerFixture, extended_poetry: Poetry, tmp_path: Path +) -> None: pip_install = mocker.patch("poetry.masonry.builders.editable.pip_install") - env = MockEnv(path=Path(tmp_dir) / "foo") + env = MockEnv(path=tmp_path / "foo") builder = EditableBuilder(extended_poetry, env, NullIO()) builder.build() @@ -226,10 +224,12 @@ def test_builder_falls_back_on_setup_and_pip_for_packages_with_build_scripts( assert [] == env.executed -def test_builder_setup_generation_runs_with_pip_editable(tmp_dir: str) -> None: +def test_builder_setup_generation_runs_with_pip_editable( + fixture_dir: FixtureDirGetter, tmp_path: Path +) -> None: # create an isolated copy of the project - fixture = Path(__file__).parent.parent.parent / "fixtures" / "extended_project" - extended_project = Path(tmp_dir) / "extended_project" + fixture = fixture_dir("extended_project") + extended_project = tmp_path / "extended_project" shutil.copytree(fixture, extended_project) assert extended_project.exists() @@ -264,11 +264,11 @@ def test_builder_setup_generation_runs_with_pip_editable(tmp_dir: str) -> None: def test_builder_installs_proper_files_when_packages_configured( project_with_include: Poetry, tmp_venv: VirtualEnv -): +) -> None: builder = EditableBuilder(project_with_include, tmp_venv, NullIO()) builder.build() - pth_file = "with_include.pth" + pth_file = Path("with_include.pth") assert tmp_venv.site_packages.exists(pth_file) pth_file = tmp_venv.site_packages.find(pth_file)[0] @@ -287,10 +287,48 @@ def test_builder_installs_proper_files_when_packages_configured( assert len(paths) == len(expected) +def test_builder_generates_proper_metadata_when_multiple_readme_files( + with_multiple_readme_files: Poetry, tmp_venv: VirtualEnv +) -> None: + builder = EditableBuilder(with_multiple_readme_files, tmp_venv, NullIO()) + + builder.build() + + dist_info = Path("my_package-0.1.dist-info") + assert tmp_venv.site_packages.exists(dist_info) + + dist_info = tmp_venv.site_packages.find(dist_info)[0] + assert dist_info.joinpath("METADATA").exists() + + metadata = """\ +Metadata-Version: 2.1 +Name: my-package +Version: 0.1 +Summary: Some description. +Home-page: https://python-poetry.org +License: MIT +Author: Your Name +Author-email: you@example.com +Requires-Python: >=2.7,<3.0 +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Description-Content-Type: text/x-rst + +Single Python +============= + +Changelog +========= + +""" + assert dist_info.joinpath("METADATA").read_text(encoding="utf-8") == metadata + + def test_builder_should_execute_build_scripts( - mocker: MockerFixture, extended_without_setup_poetry: Poetry, tmp_dir: str -): - env = MockEnv(path=Path(tmp_dir) / "foo") + mocker: MockerFixture, extended_without_setup_poetry: Poetry, tmp_path: Path +) -> None: + env = MockEnv(path=tmp_path / "foo") mocker.patch( "poetry.masonry.builders.editable.build_environment" ).return_value.__enter__.return_value = env diff --git a/tests/mixology/helpers.py b/tests/mixology/helpers.py index 500d2d95c07..c2c28e19853 100644 --- a/tests/mixology/helpers.py +++ b/tests/mixology/helpers.py @@ -10,11 +10,13 @@ if TYPE_CHECKING: + from collections.abc import Mapping + from packaging.utils import NormalizedName from poetry.core.factory import DependencyConstraint from poetry.core.packages.project_package import ProjectPackage - from poetry.mixology import SolverResult + from poetry.mixology.result import SolverResult from poetry.repositories import Repository from tests.mixology.version_solver.conftest import Provider @@ -23,7 +25,7 @@ def add_to_repo( repository: Repository, name: str, version: str, - deps: dict[str, DependencyConstraint] | None = None, + deps: Mapping[str, DependencyConstraint] | None = None, python: str | None = None, yanked: bool = False, ) -> None: @@ -62,7 +64,7 @@ def check_solver_result( except AssertionError as e: if error: assert str(e) == error - return + return None raise packages = {} diff --git a/tests/mixology/version_solver/conftest.py b/tests/mixology/version_solver/conftest.py index 453cc004399..33bb6a3a724 100644 --- a/tests/mixology/version_solver/conftest.py +++ b/tests/mixology/version_solver/conftest.py @@ -8,8 +8,8 @@ from poetry.core.packages.project_package import ProjectPackage from poetry.puzzle.provider import Provider as BaseProvider -from poetry.repositories import Pool from poetry.repositories import Repository +from poetry.repositories import RepositoryPool if TYPE_CHECKING: @@ -28,8 +28,8 @@ def repo() -> Repository: @pytest.fixture -def pool(repo: TestRepository) -> Pool: - pool = Pool() +def pool(repo: TestRepository) -> RepositoryPool: + pool = RepositoryPool() pool.add_repository(repo) return pool @@ -41,5 +41,5 @@ def root() -> ProjectPackage: @pytest.fixture -def provider(pool: Pool, root: ProjectPackage) -> Provider: +def provider(pool: RepositoryPool, root: ProjectPackage) -> Provider: return Provider(root, pool, NullIO()) diff --git a/tests/mixology/version_solver/test_backtracking.py b/tests/mixology/version_solver/test_backtracking.py index 6354d4c6a48..68e8f638746 100644 --- a/tests/mixology/version_solver/test_backtracking.py +++ b/tests/mixology/version_solver/test_backtracking.py @@ -16,7 +16,7 @@ def test_circular_dependency_on_older_version( root: ProjectPackage, provider: Provider, repo: Repository -): +) -> None: root.add_dependency(Factory.create_dependency("a", ">=1.0.0")) add_to_repo(repo, "a", "1.0.0") @@ -28,7 +28,7 @@ def test_circular_dependency_on_older_version( def test_diamond_dependency_graph( root: ProjectPackage, provider: Provider, repo: Repository -): +) -> None: root.add_dependency(Factory.create_dependency("a", "*")) root.add_dependency(Factory.create_dependency("b", "*")) @@ -47,7 +47,7 @@ def test_diamond_dependency_graph( def test_backjumps_after_partial_satisfier( root: ProjectPackage, provider: Provider, repo: Repository -): +) -> None: # c 2.0.0 is incompatible with y 2.0.0 because it requires x 1.0.0, but that # requirement only exists because of both a and b. The solver should be able # to deduce c 2.0.0's incompatibility and select c 1.0.0 instead. @@ -72,7 +72,7 @@ def test_backjumps_after_partial_satisfier( def test_rolls_back_leaf_versions_first( root: ProjectPackage, provider: Provider, repo: Repository -): +) -> None: # The latest versions of a and b disagree on c. An older version of either # will resolve the problem. This test validates that b, which is farther # in the dependency graph from myapp is downgraded first. @@ -88,7 +88,9 @@ def test_rolls_back_leaf_versions_first( check_solver_result(root, provider, {"a": "2.0.0", "b": "1.0.0", "c": "2.0.0"}) -def test_simple_transitive(root: ProjectPackage, provider: Provider, repo: Repository): +def test_simple_transitive( + root: ProjectPackage, provider: Provider, repo: Repository +) -> None: # Only one version of baz, so foo and bar will have to downgrade # until they reach it root.add_dependency(Factory.create_dependency("foo", "*")) @@ -110,7 +112,7 @@ def test_simple_transitive(root: ProjectPackage, provider: Provider, repo: Repos def test_backjump_to_nearer_unsatisfied_package( root: ProjectPackage, provider: Provider, repo: Repository -): +) -> None: # This ensures it doesn't exhaustively search all versions of b when it's # a-2.0.0 whose dependency on c-2.0.0-nonexistent led to the problem. We # make sure b has more versions than a so that the solver tries a first @@ -132,7 +134,7 @@ def test_backjump_to_nearer_unsatisfied_package( def test_traverse_into_package_with_fewer_versions_first( root: ProjectPackage, provider: Provider, repo: Repository -): +) -> None: # Dependencies are ordered so that packages with fewer versions are tried # first. Here, there are two valid solutions (either a or b must be # downgraded once). The chosen one depends on which dep is traversed first. @@ -158,7 +160,7 @@ def test_traverse_into_package_with_fewer_versions_first( def test_backjump_past_failed_package_on_disjoint_constraint( root: ProjectPackage, provider: Provider, repo: Repository -): +) -> None: root.add_dependency(Factory.create_dependency("a", "*")) root.add_dependency(Factory.create_dependency("foo", ">2.0.0")) diff --git a/tests/mixology/version_solver/test_basic_graph.py b/tests/mixology/version_solver/test_basic_graph.py index 210abc371e4..8146f5ad1be 100644 --- a/tests/mixology/version_solver/test_basic_graph.py +++ b/tests/mixology/version_solver/test_basic_graph.py @@ -18,7 +18,7 @@ def test_simple_dependencies( root: ProjectPackage, provider: Provider, repo: Repository -): +) -> None: root.add_dependency(Factory.create_dependency("a", "1.0.0")) root.add_dependency(Factory.create_dependency("b", "1.0.0")) @@ -45,7 +45,7 @@ def test_simple_dependencies( def test_shared_dependencies_with_overlapping_constraints( root: ProjectPackage, provider: Provider, repo: Repository -): +) -> None: root.add_dependency(Factory.create_dependency("a", "1.0.0")) root.add_dependency(Factory.create_dependency("b", "1.0.0")) @@ -62,7 +62,7 @@ def test_shared_dependencies_with_overlapping_constraints( def test_shared_dependency_where_dependent_version_affects_other_dependencies( root: ProjectPackage, provider: Provider, repo: Repository -): +) -> None: root.add_dependency(Factory.create_dependency("foo", "<=1.0.2")) root.add_dependency(Factory.create_dependency("bar", "1.0.0")) @@ -82,7 +82,7 @@ def test_shared_dependency_where_dependent_version_affects_other_dependencies( def test_circular_dependency( root: ProjectPackage, provider: Provider, repo: Repository -): +) -> None: root.add_dependency(Factory.create_dependency("foo", "1.0.0")) add_to_repo(repo, "foo", "1.0.0", deps={"bar": "1.0.0"}) diff --git a/tests/mixology/version_solver/test_dependency_cache.py b/tests/mixology/version_solver/test_dependency_cache.py index 4606911e7e1..dffa7e535be 100644 --- a/tests/mixology/version_solver/test_dependency_cache.py +++ b/tests/mixology/version_solver/test_dependency_cache.py @@ -18,7 +18,7 @@ def test_solver_dependency_cache_respects_source_type( root: ProjectPackage, provider: Provider, repo: Repository -): +) -> None: dependency_pypi = Factory.create_dependency("demo", ">=0.1.0") dependency_git = Factory.create_dependency( "demo", {"git": "https://github.com/demo/demo.git"}, groups=["dev"] @@ -62,7 +62,7 @@ def test_solver_dependency_cache_respects_source_type( def test_solver_dependency_cache_respects_subdirectories( root: ProjectPackage, provider: Provider, repo: Repository -): +) -> None: dependency_one = Factory.create_dependency( "one", { diff --git a/tests/mixology/version_solver/test_python_constraint.py b/tests/mixology/version_solver/test_python_constraint.py index 52bbdd7bdab..d54a918bbd6 100644 --- a/tests/mixology/version_solver/test_python_constraint.py +++ b/tests/mixology/version_solver/test_python_constraint.py @@ -16,7 +16,7 @@ def test_dependency_does_not_match_root_python_constraint( root: ProjectPackage, provider: Provider, repo: Repository -): +) -> None: provider.set_package_python_versions("^3.6") root.add_dependency(Factory.create_dependency("foo", "*")) diff --git a/tests/mixology/version_solver/test_unsolvable.py b/tests/mixology/version_solver/test_unsolvable.py index 75ff37da9cb..92cc5a98515 100644 --- a/tests/mixology/version_solver/test_unsolvable.py +++ b/tests/mixology/version_solver/test_unsolvable.py @@ -1,6 +1,5 @@ from __future__ import annotations -from pathlib import Path from typing import TYPE_CHECKING from poetry.factory import Factory @@ -13,11 +12,12 @@ from poetry.repositories import Repository from tests.mixology.version_solver.conftest import Provider + from tests.types import FixtureDirGetter def test_no_version_matching_constraint( root: ProjectPackage, provider: Provider, repo: Repository -): +) -> None: root.add_dependency(Factory.create_dependency("foo", "^1.0")) add_to_repo(repo, "foo", "2.0.0") @@ -35,7 +35,7 @@ def test_no_version_matching_constraint( def test_no_version_that_matches_combined_constraints( root: ProjectPackage, provider: Provider, repo: Repository -): +) -> None: root.add_dependency(Factory.create_dependency("foo", "1.0.0")) root.add_dependency(Factory.create_dependency("bar", "1.0.0")) @@ -58,7 +58,7 @@ def test_no_version_that_matches_combined_constraints( def test_disjoint_constraints( root: ProjectPackage, provider: Provider, repo: Repository -): +) -> None: root.add_dependency(Factory.create_dependency("foo", "1.0.0")) root.add_dependency(Factory.create_dependency("bar", "1.0.0")) @@ -80,7 +80,7 @@ def test_disjoint_constraints( def test_disjoint_root_constraints( root: ProjectPackage, provider: Provider, repo: Repository -): +) -> None: root.add_dependency(Factory.create_dependency("foo", "1.0.0")) root.add_dependency(Factory.create_dependency("foo", "2.0.0")) @@ -94,11 +94,13 @@ def test_disjoint_root_constraints( def test_disjoint_root_constraints_path_dependencies( - root: ProjectPackage, provider: Provider, repo: Repository -): + root: ProjectPackage, + provider: Provider, + repo: Repository, + fixture_dir: FixtureDirGetter, +) -> None: provider.set_package_python_versions("^3.7") - fixtures = Path(__file__).parent.parent.parent / "fixtures" - project_dir = fixtures.joinpath("with_conditional_path_deps") + project_dir = fixture_dir("with_conditional_path_deps") dependency1 = Factory.create_dependency("demo", {"path": project_dir / "demo_one"}) root.add_dependency(dependency1) dependency2 = Factory.create_dependency("demo", {"path": project_dir / "demo_two"}) @@ -112,7 +114,9 @@ def test_disjoint_root_constraints_path_dependencies( check_solver_result(root, provider, error=error) -def test_no_valid_solution(root: ProjectPackage, provider: Provider, repo: Repository): +def test_no_valid_solution( + root: ProjectPackage, provider: Provider, repo: Repository +) -> None: root.add_dependency(Factory.create_dependency("a", "*")) root.add_dependency(Factory.create_dependency("b", "*")) @@ -135,7 +139,7 @@ def test_no_valid_solution(root: ProjectPackage, provider: Provider, repo: Repos def test_package_with_the_same_name_gives_clear_error_message( root: ProjectPackage, provider: Provider, repo: Repository -): +) -> None: pkg_name = "a" root.add_dependency(Factory.create_dependency(pkg_name, "*")) add_to_repo(repo, pkg_name, "1.0.0", deps={pkg_name: "1.0.0"}) diff --git a/tests/mixology/version_solver/test_with_lock.py b/tests/mixology/version_solver/test_with_lock.py index caa7790f856..75351c842f8 100644 --- a/tests/mixology/version_solver/test_with_lock.py +++ b/tests/mixology/version_solver/test_with_lock.py @@ -16,13 +16,13 @@ if TYPE_CHECKING: from poetry.core.packages.project_package import ProjectPackage - from poetry.repositories import Pool from poetry.repositories import Repository + from poetry.repositories import RepositoryPool def test_with_compatible_locked_dependencies( - root: ProjectPackage, repo: Repository, pool: Pool -): + root: ProjectPackage, repo: Repository, pool: RepositoryPool +) -> None: root.add_dependency(Factory.create_dependency("foo", "*")) add_to_repo(repo, "foo", "1.0.0", deps={"bar": "1.0.0"}) @@ -43,8 +43,8 @@ def test_with_compatible_locked_dependencies( def test_with_incompatible_locked_dependencies( - root: ProjectPackage, repo: Repository, pool: Pool -): + root: ProjectPackage, repo: Repository, pool: RepositoryPool +) -> None: root.add_dependency(Factory.create_dependency("foo", ">1.0.1")) add_to_repo(repo, "foo", "1.0.0", deps={"bar": "1.0.0"}) @@ -65,8 +65,8 @@ def test_with_incompatible_locked_dependencies( def test_with_unrelated_locked_dependencies( - root: ProjectPackage, repo: Repository, pool: Pool -): + root: ProjectPackage, repo: Repository, pool: RepositoryPool +) -> None: root.add_dependency(Factory.create_dependency("foo", "*")) add_to_repo(repo, "foo", "1.0.0", deps={"bar": "1.0.0"}) @@ -88,8 +88,8 @@ def test_with_unrelated_locked_dependencies( def test_unlocks_dependencies_if_necessary_to_ensure_that_a_new_dependency_is_satisfied( - root: ProjectPackage, repo: Repository, pool: Pool -): + root: ProjectPackage, repo: Repository, pool: RepositoryPool +) -> None: root.add_dependency(Factory.create_dependency("foo", "*")) root.add_dependency(Factory.create_dependency("newdep", "2.0.0")) @@ -125,8 +125,8 @@ def test_unlocks_dependencies_if_necessary_to_ensure_that_a_new_dependency_is_sa def test_with_compatible_locked_dependencies_use_latest( - root: ProjectPackage, repo: Repository, pool: Pool -): + root: ProjectPackage, repo: Repository, pool: RepositoryPool +) -> None: root.add_dependency(Factory.create_dependency("foo", "*")) root.add_dependency(Factory.create_dependency("baz", "*")) @@ -155,8 +155,8 @@ def test_with_compatible_locked_dependencies_use_latest( def test_with_compatible_locked_dependencies_with_extras( - root: ProjectPackage, repo: Repository, pool: Pool -): + root: ProjectPackage, repo: Repository, pool: RepositoryPool +) -> None: root.add_dependency(Factory.create_dependency("foo", "^1.0")) package_foo_0 = get_package("foo", "1.0.0") @@ -189,8 +189,8 @@ def test_with_compatible_locked_dependencies_with_extras( def test_with_yanked_package_in_lock( - root: ProjectPackage, repo: Repository, pool: Pool -): + root: ProjectPackage, repo: Repository, pool: RepositoryPool +) -> None: root.add_dependency(Factory.create_dependency("foo", "*")) add_to_repo(repo, "foo", "1") @@ -205,6 +205,7 @@ def test_with_yanked_package_in_lock( provider, result={"foo": "2"}, ) + assert result is not None foo = result.packages[0] assert foo.yanked @@ -218,8 +219,8 @@ def test_with_yanked_package_in_lock( def test_no_update_is_respected_for_legacy_repository( - root: ProjectPackage, repo: Repository, pool: Pool -): + root: ProjectPackage, repo: Repository, pool: RepositoryPool +) -> None: root.add_dependency(Factory.create_dependency("foo", "^1.0")) foo_100 = Package( diff --git a/tests/packages/test_locker.py b/tests/packages/test_locker.py index 9da6c422f7c..bdd80e4ec86 100644 --- a/tests/packages/test_locker.py +++ b/tests/packages/test_locker.py @@ -12,12 +12,12 @@ from typing import TYPE_CHECKING import pytest -import tomlkit from poetry.core.constraints.version import Version from poetry.core.packages.package import Package from poetry.core.packages.project_package import ProjectPackage +from poetry.__version__ import __version__ from poetry.factory import Factory from poetry.packages.locker import GENERATED_COMMENT from poetry.packages.locker import Locker @@ -34,7 +34,7 @@ def locker() -> Locker: with tempfile.NamedTemporaryFile() as f: f.close() - locker = Locker(f.name, {}) + locker = Locker(Path(f.name), {}) return locker @@ -44,7 +44,7 @@ def root() -> ProjectPackage: return ProjectPackage("root", "1.2.3") -def test_lock_file_data_is_ordered(locker: Locker, root: ProjectPackage): +def test_lock_file_data_is_ordered(locker: Locker, root: ProjectPackage) -> None: package_a = get_package("A", "1.0.0") package_a.add_dependency(Factory.create_dependency("B", "^1.0")) package_a.files = [{"file": "foo", "hash": "456"}, {"file": "bar", "hash": "123"}] @@ -200,7 +200,7 @@ def test_lock_file_data_is_ordered(locker: Locker, root: ProjectPackage): assert content == expected -def test_locker_properly_loads_extras(locker: Locker): +def test_locker_properly_loads_extras(locker: Locker) -> None: content = f"""\ # {GENERATED_COMMENT} @@ -231,7 +231,8 @@ def test_locker_properly_loads_extras(locker: Locker): content-hash = "c3d07fca33fba542ef2b2a4d75bf5b48d892d21a830e2ad9c952ba5123a52f77" """ # noqa: E800 - locker.lock.write(tomlkit.parse(content)) + with open(locker.lock, "w", encoding="utf-8") as f: + f.write(content) packages = locker.locked_repository().packages @@ -245,7 +246,7 @@ def test_locker_properly_loads_extras(locker: Locker): assert lockfile_dep.name == "lockfile" -def test_locker_properly_loads_nested_extras(locker: Locker): +def test_locker_properly_loads_nested_extras(locker: Locker) -> None: content = f"""\ # {GENERATED_COMMENT} @@ -294,7 +295,8 @@ def test_locker_properly_loads_nested_extras(locker: Locker): content-hash = "123456789" """ # noqa: E800 - locker.lock.write(tomlkit.parse(content)) + with open(locker.lock, "w", encoding="utf-8") as f: + f.write(content) repository = locker.locked_repository() assert len(repository.packages) == 3 @@ -325,7 +327,7 @@ def test_locker_properly_loads_nested_extras(locker: Locker): assert len(packages) == 1 -def test_locker_properly_loads_extras_legacy(locker: Locker): +def test_locker_properly_loads_extras_legacy(locker: Locker) -> None: content = f"""\ # {GENERATED_COMMENT} @@ -359,7 +361,8 @@ def test_locker_properly_loads_extras_legacy(locker: Locker): content-hash = "123456789" """ # noqa: E800 - locker.lock.write(tomlkit.parse(content)) + with open(locker.lock, "w", encoding="utf-8") as f: + f.write(content) repository = locker.locked_repository() assert len(repository.packages) == 2 @@ -399,7 +402,8 @@ def test_locker_properly_loads_subdir(locker: Locker) -> None: python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ - locker.lock.write(tomlkit.parse(content)) + with open(locker.lock, "w", encoding="utf-8") as f: + f.write(content) repository = locker.locked_repository() assert len(repository.packages) == 1 @@ -495,7 +499,8 @@ def test_locker_properly_assigns_metadata_files(locker: Locker) -> None: {file = "demo-1.0-py3-none-any.whl", hash = "sha256"}, ] """ - locker.lock.write(tomlkit.parse(content)) + with open(locker.lock, "w", encoding="utf-8") as f: + f.write(content) repository = locker.locked_repository() assert len(repository.packages) == 5 @@ -525,7 +530,9 @@ def test_locker_properly_assigns_metadata_files(locker: Locker) -> None: package.files = [] -def test_lock_packages_with_null_description(locker: Locker, root: ProjectPackage): +def test_lock_packages_with_null_description( + locker: Locker, root: ProjectPackage +) -> None: package_a = get_package("A", "1.0.0") package_a.description = None @@ -555,7 +562,9 @@ def test_lock_packages_with_null_description(locker: Locker, root: ProjectPackag assert content == expected -def test_lock_file_should_not_have_mixed_types(locker: Locker, root: ProjectPackage): +def test_lock_file_should_not_have_mixed_types( + locker: Locker, root: ProjectPackage +) -> None: package_a = get_package("A", "1.0.0") package_a.add_dependency(Factory.create_dependency("B", "^1.0.0")) package_a.add_dependency( @@ -599,7 +608,9 @@ def test_lock_file_should_not_have_mixed_types(locker: Locker, root: ProjectPack assert content == expected -def test_reading_lock_file_should_raise_an_error_on_invalid_data(locker: Locker): +def test_reading_lock_file_should_raise_an_error_on_invalid_data( + locker: Locker, +) -> None: content = f"""\ # {GENERATED_COMMENT} @@ -634,7 +645,7 @@ def test_reading_lock_file_should_raise_an_error_on_invalid_data(locker: Locker) def test_locking_legacy_repository_package_should_include_source_section( root: ProjectPackage, locker: Locker -): +) -> None: package_a = Package( "A", "1.0.0", @@ -677,7 +688,7 @@ def test_locking_legacy_repository_package_should_include_source_section( def test_locker_should_emit_warnings_if_lock_version_is_newer_but_allowed( locker: Locker, caplog: LogCaptureFixture -): +) -> None: version = ".".join(Version.parse(Locker._VERSION).next_minor().text.split(".")[:2]) content = f"""\ [metadata] @@ -687,7 +698,8 @@ def test_locker_should_emit_warnings_if_lock_version_is_newer_but_allowed( """ caplog.set_level(logging.WARNING, logger="poetry.packages.locker") - locker.lock.write(tomlkit.parse(content)) + with open(locker.lock, "w", encoding="utf-8") as f: + f.write(content) _ = locker.lock_data @@ -706,7 +718,7 @@ def test_locker_should_emit_warnings_if_lock_version_is_newer_but_allowed( def test_locker_should_raise_an_error_if_lock_version_is_newer_and_not_allowed( locker: Locker, caplog: LogCaptureFixture -): +) -> None: content = f"""\ # {GENERATED_COMMENT} @@ -717,13 +729,50 @@ def test_locker_should_raise_an_error_if_lock_version_is_newer_and_not_allowed( """ # noqa: E800 caplog.set_level(logging.WARNING, logger="poetry.packages.locker") - locker.lock.write(tomlkit.parse(content)) + with open(locker.lock, "w", encoding="utf-8") as f: + f.write(content) with pytest.raises(RuntimeError, match="^The lock file is not compatible"): _ = locker.lock_data -def test_extras_dependencies_are_ordered(locker: Locker, root: ProjectPackage): +def test_root_extras_dependencies_are_ordered( + locker: Locker, root: ProjectPackage, fixture_base: Path +) -> None: + Factory.create_dependency("B", "1.0.0", root_dir=fixture_base) + Factory.create_dependency("C", "1.0.0", root_dir=fixture_base) + package_first = Factory.create_dependency("first", "1.0.0", root_dir=fixture_base) + package_second = Factory.create_dependency("second", "1.0.0", root_dir=fixture_base) + package_third = Factory.create_dependency("third", "1.0.0", root_dir=fixture_base) + + root.extras = { + "C": [package_third, package_second, package_first], + "B": [package_first, package_second, package_third], + } + locker.set_lock_data(root, []) + + expected = f"""\ +# {GENERATED_COMMENT} +package = [] + +[extras] +B = ["first", "second", "third"] +C = ["first", "second", "third"] + +[metadata] +lock-version = "2.0" +python-versions = "*" +content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" +""" # noqa: E800 + + with locker.lock.open(encoding="utf-8") as f: + content = f.read() + + print(content) + assert content == expected + + +def test_extras_dependencies_are_ordered(locker: Locker, root: ProjectPackage) -> None: package_a = get_package("A", "1.0.0") package_a.add_dependency( Factory.create_dependency( @@ -763,7 +812,7 @@ def test_extras_dependencies_are_ordered(locker: Locker, root: ProjectPackage): def test_locker_should_neither_emit_warnings_nor_raise_error_for_lower_compatible_versions( # noqa: E501 locker: Locker, caplog: LogCaptureFixture -): +) -> None: older_version = "1.1" content = f"""\ [metadata] @@ -775,7 +824,8 @@ def test_locker_should_neither_emit_warnings_nor_raise_error_for_lower_compatibl """ caplog.set_level(logging.WARNING, logger="poetry.packages.locker") - locker.lock.write(tomlkit.parse(content)) + with open(locker.lock, "w", encoding="utf-8") as f: + f.write(content) _ = locker.lock_data @@ -783,25 +833,24 @@ def test_locker_should_neither_emit_warnings_nor_raise_error_for_lower_compatibl def test_locker_dumps_dependency_information_correctly( - locker: Locker, root: ProjectPackage -): - root_dir = Path(__file__).parent.parent.joinpath("fixtures") + locker: Locker, root: ProjectPackage, fixture_base: Path +) -> None: 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 + "B", {"path": "project_with_extras", "develop": True}, root_dir=fixture_base ) ) package_a.add_dependency( Factory.create_dependency( "C", {"path": "directory/project_with_transitive_directory_dependencies"}, - root_dir=root_dir, + root_dir=fixture_base, ) ) package_a.add_dependency( Factory.create_dependency( - "D", {"path": "distributions/demo-0.1.0.tar.gz"}, root_dir=root_dir + "D", {"path": "distributions/demo-0.1.0.tar.gz"}, root_dir=fixture_base ) ) package_a.add_dependency( @@ -814,6 +863,25 @@ def test_locker_dumps_dependency_information_correctly( "F", {"git": "https://github.com/python-poetry/poetry.git", "branch": "foo"} ) ) + package_a.add_dependency( + Factory.create_dependency( + "G", + { + "git": "https://github.com/python-poetry/poetry.git", + "subdirectory": "bar", + }, + ) + ) + package_a.add_dependency( + Factory.create_dependency( + "H", {"git": "https://github.com/python-poetry/poetry.git", "tag": "baz"} + ) + ) + package_a.add_dependency( + Factory.create_dependency( + "I", {"git": "https://github.com/python-poetry/poetry.git", "rev": "spam"} + ) + ) packages = [package_a] @@ -840,6 +908,9 @@ def test_locker_dumps_dependency_information_correctly( 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"}} +G = {{git = "https://github.com/python-poetry/poetry.git", subdirectory = "bar"}} +H = {{git = "https://github.com/python-poetry/poetry.git", tag = "baz"}} +I = {{git = "https://github.com/python-poetry/poetry.git", rev = "spam"}} [metadata] lock-version = "2.0" @@ -896,15 +967,14 @@ def test_locker_dumps_subdir(locker: Locker, root: ProjectPackage) -> None: def test_locker_dumps_dependency_extras_in_correct_order( - locker: Locker, root: ProjectPackage -): - root_dir = Path(__file__).parent.parent.joinpath("fixtures") + locker: Locker, root: ProjectPackage, fixture_base: Path +) -> None: package_a = get_package("A", "1.0.0") - Factory.create_dependency("B", "1.0.0", root_dir=root_dir) - Factory.create_dependency("C", "1.0.0", root_dir=root_dir) - package_first = Factory.create_dependency("first", "1.0.0", root_dir=root_dir) - package_second = Factory.create_dependency("second", "1.0.0", root_dir=root_dir) - package_third = Factory.create_dependency("third", "1.0.0", root_dir=root_dir) + Factory.create_dependency("B", "1.0.0", root_dir=fixture_base) + Factory.create_dependency("C", "1.0.0", root_dir=fixture_base) + package_first = Factory.create_dependency("first", "1.0.0", root_dir=fixture_base) + package_second = Factory.create_dependency("second", "1.0.0", root_dir=fixture_base) + package_third = Factory.create_dependency("third", "1.0.0", root_dir=fixture_base) package_a.extras = { "C": [package_third, package_second, package_first], @@ -943,7 +1013,7 @@ def test_locker_dumps_dependency_extras_in_correct_order( def test_locked_repository_uses_root_dir_of_package( locker: Locker, mocker: MockerFixture -): +) -> None: content = f"""\ # {GENERATED_COMMENT} @@ -970,7 +1040,9 @@ def test_locked_repository_uses_root_dir_of_package( content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ # noqa: E800 - locker.lock.write(tomlkit.parse(content)) + with open(locker.lock, "w", encoding="utf-8") as f: + f.write(content) + create_dependency_patch = mocker.patch( "poetry.factory.Factory.create_dependency", autospec=True ) @@ -983,7 +1055,7 @@ def test_locked_repository_uses_root_dir_of_package( root_dir = call_kwargs["root_dir"] assert root_dir.match("*/lib/libA") # relative_to raises an exception if not relative - is_relative_to comes in py3.9 - assert root_dir.relative_to(locker.lock.path.parent.resolve()) is not None + assert root_dir.relative_to(locker.lock.parent.resolve()) is not None @pytest.mark.parametrize( @@ -1017,7 +1089,7 @@ def test_content_hash_with_legacy_is_compatible( relevant_content[key] = local_config.get(key) locker = locker.__class__( - lock=locker.lock.path, + lock=locker.lock, local_config=local_config, ) @@ -1029,7 +1101,7 @@ def test_content_hash_with_legacy_is_compatible( assert (content_hash == old_content_hash) or fresh -def test_lock_file_resolves_file_url_symlinks(root: ProjectPackage): +def test_lock_file_resolves_file_url_symlinks(root: ProjectPackage) -> None: """ Create directories and file structure as follows: @@ -1064,7 +1136,7 @@ def test_lock_file_resolves_file_url_symlinks(root: ProjectPackage): # Test is not possible in that case. return raise - locker = Locker(str(symlink_path) + os.sep + Path(lock_file.name).name, {}) + locker = Locker(symlink_path / lock_file.name, {}) package_local = Package( "local-package", @@ -1115,3 +1187,29 @@ def test_lock_file_resolves_file_url_symlinks(root: ProjectPackage): """ # noqa: E800 assert content == expected + + +def test_lockfile_is_not_rewritten_if_only_poetry_version_changed( + locker: Locker, root: ProjectPackage +) -> None: + generated_comment_old_version = GENERATED_COMMENT.replace(__version__, "1.3.2") + assert generated_comment_old_version != GENERATED_COMMENT + old_content = f"""\ +# {generated_comment_old_version} +package = [] + +[metadata] +lock-version = "2.0" +python-versions = "*" +content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" +""" # noqa: E800 + + with open(locker.lock, "w", encoding="utf-8") as f: + f.write(old_content) + + assert not locker.set_lock_data(root, []) + + with locker.lock.open(encoding="utf-8") as f: + content = f.read() + + assert content == old_content diff --git a/tests/plugins/test_plugin_manager.py b/tests/plugins/test_plugin_manager.py index 868d61f26f0..05ebd7aa5b8 100644 --- a/tests/plugins/test_plugin_manager.py +++ b/tests/plugins/test_plugin_manager.py @@ -18,11 +18,11 @@ if TYPE_CHECKING: + from cleo.io.io import IO from pytest_mock import MockerFixture from tests.conftest import Config - -CWD = Path(__file__).parent.parent / "fixtures" / "simple_project" + from tests.types import FixtureDirGetter class ManagerFactory(Protocol): @@ -31,9 +31,9 @@ def __call__(self, group: str = Plugin.group) -> PluginManager: class MyPlugin(Plugin): - def activate(self, poetry: Poetry, io: BufferedIO) -> None: + def activate(self, poetry: Poetry, io: IO) -> None: io.write_line("Setting readmes") - poetry.package.readmes = ("README.md",) + poetry.package.readmes = (Path("README.md"),) class MyCommandPlugin(ApplicationPlugin): @@ -41,18 +41,19 @@ class MyCommandPlugin(ApplicationPlugin): class InvalidPlugin: - def activate(self, poetry: Poetry, io: BufferedIO) -> None: + def activate(self, poetry: Poetry, io: IO) -> None: io.write_line("Updating version") poetry.package.version = "9.9.9" -@pytest.fixture() -def poetry(tmp_dir: str, config: Config) -> Poetry: +@pytest.fixture +def poetry(fixture_dir: FixtureDirGetter, config: Config) -> Poetry: + project_path = fixture_dir("simple_project") poetry = Poetry( - CWD / "pyproject.toml", + project_path / "pyproject.toml", {}, ProjectPackage("simple-project", "1.2.3"), - Locker(CWD / "poetry.lock", {}), + Locker(project_path / "poetry.lock", {}), config, ) @@ -60,7 +61,7 @@ def poetry(tmp_dir: str, config: Config) -> Poetry: @pytest.fixture() -def io() -> BufferedIO: +def io() -> IO: return BufferedIO() @@ -82,12 +83,12 @@ def test_load_plugins_and_activate( poetry: Poetry, io: BufferedIO, with_my_plugin: None, -): +) -> None: manager = manager_factory() manager.load_plugins() manager.activate(poetry, io) - assert poetry.package.readmes == ("README.md",) + assert poetry.package.readmes == (Path("README.md"),) assert io.fetch_output() == "Setting readmes\n" @@ -106,7 +107,7 @@ def test_load_plugins_with_invalid_plugin( poetry: Poetry, io: BufferedIO, with_invalid_plugin: None, -): +) -> None: manager = manager_factory() with pytest.raises(ValueError): @@ -118,7 +119,7 @@ def test_load_plugins_with_plugins_disabled( poetry: Poetry, io: BufferedIO, with_my_plugin: None, -): +) -> None: no_plugin_manager.load_plugins() assert poetry.package.version.text == "1.2.3" diff --git a/tests/publishing/test_publisher.py b/tests/publishing/test_publisher.py index afc992f828a..5233785a854 100644 --- a/tests/publishing/test_publisher.py +++ b/tests/publishing/test_publisher.py @@ -23,7 +23,7 @@ def test_publish_publishes_to_pypi_by_default( fixture_dir: FixtureDirGetter, mocker: MockerFixture, config: Config -): +) -> None: 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")) @@ -48,7 +48,7 @@ def test_publish_can_publish_to_given_repository( mocker: MockerFixture, config: Config, fixture_name: str, -): +) -> None: uploader_auth = mocker.patch("poetry.publishing.uploader.Uploader.auth") uploader_upload = mocker.patch("poetry.publishing.uploader.Uploader.upload") @@ -77,7 +77,7 @@ def test_publish_can_publish_to_given_repository( def test_publish_raises_error_for_undefined_repository( fixture_dir: FixtureDirGetter, config: Config -): +) -> None: poetry = Factory().create_poetry(fixture_dir("sample_project")) poetry._config = config poetry.config.merge( @@ -91,7 +91,7 @@ def test_publish_raises_error_for_undefined_repository( def test_publish_uses_token_if_it_exists( fixture_dir: FixtureDirGetter, mocker: MockerFixture, config: Config -): +) -> None: 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")) @@ -110,7 +110,7 @@ def test_publish_uses_token_if_it_exists( def test_publish_uses_cert( fixture_dir: FixtureDirGetter, mocker: MockerFixture, config: Config -): +) -> None: cert = "path/to/ca.pem" uploader_auth = mocker.patch("poetry.publishing.uploader.Uploader.auth") uploader_upload = mocker.patch("poetry.publishing.uploader.Uploader.upload") @@ -141,7 +141,7 @@ def test_publish_uses_cert( def test_publish_uses_client_cert( fixture_dir: FixtureDirGetter, mocker: MockerFixture, config: Config -): +) -> None: client_cert = "path/to/client.pem" uploader_upload = mocker.patch("poetry.publishing.uploader.Uploader.upload") poetry = Factory().create_poetry(fixture_dir("sample_project")) @@ -172,7 +172,7 @@ def test_publish_read_from_environment_variable( environ: None, mocker: MockerFixture, config: Config, -): +) -> None: os.environ["POETRY_REPOSITORIES_FOO_URL"] = "https://foo.bar" os.environ["POETRY_HTTP_BASIC_FOO_USERNAME"] = "bar" os.environ["POETRY_HTTP_BASIC_FOO_PASSWORD"] = "baz" diff --git a/tests/publishing/test_uploader.py b/tests/publishing/test_uploader.py index be6256052a8..9443d09fabb 100644 --- a/tests/publishing/test_uploader.py +++ b/tests/publishing/test_uploader.py @@ -26,7 +26,7 @@ def uploader(fixture_dir: FixtureDirGetter) -> Uploader: def test_uploader_properly_handles_400_errors( http: type[httpretty.httpretty], uploader: Uploader -): +) -> None: http.register_uri(http.POST, "https://foo.com", status=400, body="Bad request") with pytest.raises(UploadError) as e: @@ -37,7 +37,7 @@ def test_uploader_properly_handles_400_errors( def test_uploader_properly_handles_403_errors( http: type[httpretty.httpretty], uploader: Uploader -): +) -> None: http.register_uri(http.POST, "https://foo.com", status=403, body="Unauthorized") with pytest.raises(UploadError) as e: @@ -48,7 +48,7 @@ def test_uploader_properly_handles_403_errors( def test_uploader_properly_handles_nonstandard_errors( http: type[httpretty.httpretty], uploader: Uploader -): +) -> None: # content based off a true story. # Message changed to protect the ~~innocent~~ guilty. content = ( @@ -62,12 +62,38 @@ def test_uploader_properly_handles_nonstandard_errors( with pytest.raises(UploadError) as e: uploader.upload("https://foo.com") - assert str(e.value) == f"HTTP Error 400: Bad Request | {content}" + assert str(e.value) == f"HTTP Error 400: Bad Request | {content!r}" + + +@pytest.mark.parametrize( + "status, body", + [ + (308, "Permanent Redirect"), + (307, "Temporary Redirect"), + (304, "Not Modified"), + (303, "See Other"), + (302, "Found"), + (301, "Moved Permanently"), + (300, "Multiple Choices"), + ], +) +def test_uploader_properly_handles_redirects( + http: type[httpretty.httpretty], uploader: Uploader, status: int, body: str +) -> None: + http.register_uri(http.POST, "https://foo.com", status=status, body=body) + + with pytest.raises(UploadError) as e: + uploader.upload("https://foo.com") + + assert ( + str(e.value) + == "Redirects are not supported. Is the URL missing a trailing slash?" + ) def test_uploader_properly_handles_301_redirects( http: type[httpretty.httpretty], uploader: Uploader -): +) -> None: http.register_uri(http.POST, "https://foo.com", status=301, body="Redirect") with pytest.raises(UploadError) as e: @@ -81,7 +107,7 @@ def test_uploader_properly_handles_301_redirects( def test_uploader_registers_for_appropriate_400_errors( mocker: MockerFixture, http: type[httpretty.httpretty], uploader: Uploader -): +) -> None: register = mocker.patch("poetry.publishing.uploader.Uploader._register") http.register_uri( http.POST, "https://foo.com", status=400, body="No package was ever registered" @@ -105,7 +131,7 @@ def test_uploader_registers_for_appropriate_400_errors( ) def test_uploader_skips_existing( http: type[httpretty.httpretty], uploader: Uploader, status: int, body: str -): +) -> None: http.register_uri(http.POST, "https://foo.com", status=status, body=body) # should not raise @@ -114,7 +140,7 @@ def test_uploader_skips_existing( def test_uploader_skip_existing_bubbles_unskippable_errors( http: type[httpretty.httpretty], uploader: Uploader -): +) -> None: http.register_uri(http.POST, "https://foo.com", status=403, body="Unauthorized") with pytest.raises(UploadError): @@ -123,7 +149,7 @@ def test_uploader_skip_existing_bubbles_unskippable_errors( def test_uploader_properly_handles_file_not_existing( mocker: MockerFixture, http: type[httpretty.httpretty], uploader: Uploader -): +) -> None: mocker.patch("pathlib.Path.is_file", return_value=False) with pytest.raises(UploadError) as e: diff --git a/tests/puzzle/test_provider.py b/tests/puzzle/test_provider.py index 753a0fcbcb4..1264321b9d0 100644 --- a/tests/puzzle/test_provider.py +++ b/tests/puzzle/test_provider.py @@ -1,8 +1,8 @@ from __future__ import annotations -from pathlib import Path from subprocess import CalledProcessError from typing import TYPE_CHECKING +from typing import Any import pytest @@ -19,8 +19,8 @@ from poetry.inspection.info import PackageInfo from poetry.packages import DependencyPackage from poetry.puzzle.provider import Provider -from poetry.repositories.pool import Pool from poetry.repositories.repository import Repository +from poetry.repositories.repository_pool import RepositoryPool from poetry.utils.env import EnvCommandError from poetry.utils.env import MockEnv as BaseMockEnv from tests.helpers import get_dependency @@ -29,12 +29,14 @@ if TYPE_CHECKING: from pytest_mock import MockerFixture + from tests.types import FixtureDirGetter + SOME_URL = "https://example.com/path.tar.gz" class MockEnv(BaseMockEnv): - def run(self, bin: str, *args: str) -> None: + def run(self, bin: str, *args: str, **kwargs: Any) -> str | int: raise EnvCommandError(CalledProcessError(1, "python", output="")) @@ -49,15 +51,15 @@ def repository() -> Repository: @pytest.fixture -def pool(repository: Repository) -> Pool: - pool = Pool() +def pool(repository: Repository) -> RepositoryPool: + pool = RepositoryPool() pool.add_repository(repository) return pool @pytest.fixture -def provider(root: ProjectPackage, pool: Pool) -> Provider: +def provider(root: ProjectPackage, pool: RepositoryPool) -> Provider: return Provider(root, pool, NullIO()) @@ -67,15 +69,7 @@ def provider(root: ProjectPackage, pool: Pool) -> Provider: (Dependency("foo", "<2"), [Package("foo", "1")]), (Dependency("foo", "<2", extras=["bar"]), [Package("foo", "1")]), (Dependency("foo", ">=1"), [Package("foo", "2"), Package("foo", "1")]), - ( - Dependency("foo", ">=1a"), - [ - Package("foo", "3a"), - Package("foo", "2"), - Package("foo", "2a"), - Package("foo", "1"), - ], - ), + (Dependency("foo", ">=1a"), [Package("foo", "2"), Package("foo", "1")]), ( Dependency("foo", ">=1", allows_prereleases=True), [ @@ -101,6 +95,7 @@ def test_search_for( repository.add_package(foo2a) repository.add_package(foo2) repository.add_package(foo3a) + assert provider.search_for(dependency) == expected @@ -164,7 +159,7 @@ def test_search_for_direct_origin_and_extras( @pytest.mark.parametrize("value", [True, False]) -def test_search_for_vcs_retains_develop_flag(provider: Provider, value: bool): +def test_search_for_vcs_retains_develop_flag(provider: Provider, value: bool) -> None: dependency = VCSDependency( "demo", "git", "https://github.com/demo/demo.git", develop=value ) @@ -172,7 +167,7 @@ def test_search_for_vcs_retains_develop_flag(provider: Provider, value: bool): assert package.develop == value -def test_search_for_vcs_setup_egg_info(provider: Provider): +def test_search_for_vcs_setup_egg_info(provider: Provider) -> None: dependency = VCSDependency("demo", "git", "https://github.com/demo/demo.git") package = provider.search_for_direct_origin_dependency(dependency) @@ -190,7 +185,7 @@ def test_search_for_vcs_setup_egg_info(provider: Provider): } -def test_search_for_vcs_setup_egg_info_with_extras(provider: Provider): +def test_search_for_vcs_setup_egg_info_with_extras(provider: Provider) -> None: dependency = VCSDependency( "demo", "git", "https://github.com/demo/demo.git", extras=["foo"] ) @@ -210,7 +205,7 @@ def test_search_for_vcs_setup_egg_info_with_extras(provider: Provider): } -def test_search_for_vcs_read_setup(provider: Provider, mocker: MockerFixture): +def test_search_for_vcs_read_setup(provider: Provider, mocker: MockerFixture) -> None: mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv()) dependency = VCSDependency("demo", "git", "https://github.com/demo/demo.git") @@ -232,7 +227,7 @@ def test_search_for_vcs_read_setup(provider: Provider, mocker: MockerFixture): def test_search_for_vcs_read_setup_with_extras( provider: Provider, mocker: MockerFixture -): +) -> None: mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv()) dependency = VCSDependency( @@ -252,7 +247,7 @@ def test_search_for_vcs_read_setup_with_extras( def test_search_for_vcs_read_setup_raises_error_if_no_version( provider: Provider, mocker: MockerFixture -): +) -> None: mocker.patch( "poetry.inspection.info.get_pep517_metadata", return_value=PackageInfo(name="demo", version=None), @@ -265,15 +260,12 @@ def test_search_for_vcs_read_setup_raises_error_if_no_version( @pytest.mark.parametrize("directory", ["demo", "non-canonical-name"]) -def test_search_for_directory_setup_egg_info(provider: Provider, directory: str): +def test_search_for_directory_setup_egg_info( + provider: Provider, directory: str, fixture_dir: FixtureDirGetter +) -> None: dependency = DirectoryDependency( "demo", - Path(__file__).parent.parent - / "fixtures" - / "git" - / "github.com" - / "demo" - / directory, + fixture_dir("git") / "github.com" / "demo" / directory, ) package = provider.search_for_direct_origin_dependency(dependency) @@ -291,15 +283,12 @@ def test_search_for_directory_setup_egg_info(provider: Provider, directory: str) } -def test_search_for_directory_setup_egg_info_with_extras(provider: Provider): +def test_search_for_directory_setup_egg_info_with_extras( + provider: Provider, fixture_dir: FixtureDirGetter +) -> None: dependency = DirectoryDependency( "demo", - Path(__file__).parent.parent - / "fixtures" - / "git" - / "github.com" - / "demo" - / "demo", + fixture_dir("git") / "github.com" / "demo" / "demo", extras=["foo"], ) @@ -319,21 +308,13 @@ def test_search_for_directory_setup_egg_info_with_extras(provider: Provider): @pytest.mark.parametrize("directory", ["demo", "non-canonical-name"]) -def test_search_for_directory_setup_with_base(provider: Provider, directory: str): +def test_search_for_directory_setup_with_base( + provider: Provider, directory: str, fixture_dir: FixtureDirGetter +) -> None: dependency = DirectoryDependency( "demo", - Path(__file__).parent.parent - / "fixtures" - / "git" - / "github.com" - / "demo" - / directory, - base=Path(__file__).parent.parent - / "fixtures" - / "git" - / "github.com" - / "demo" - / directory, + fixture_dir("git") / "github.com" / "demo" / directory, + base=fixture_dir("git") / "github.com" / "demo" / directory, ) package = provider.search_for_direct_origin_dependency(dependency) @@ -349,29 +330,17 @@ def test_search_for_directory_setup_with_base(provider: Provider, directory: str "foo": [get_dependency("cleo")], "bar": [get_dependency("tomlkit")], } - assert package.root_dir == ( - Path(__file__).parent.parent - / "fixtures" - / "git" - / "github.com" - / "demo" - / directory - ) + assert package.root_dir == (fixture_dir("git") / "github.com" / "demo" / directory) def test_search_for_directory_setup_read_setup( - provider: Provider, mocker: MockerFixture -): + provider: Provider, mocker: MockerFixture, fixture_dir: FixtureDirGetter +) -> None: mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv()) dependency = DirectoryDependency( "demo", - Path(__file__).parent.parent - / "fixtures" - / "git" - / "github.com" - / "demo" - / "demo", + fixture_dir("git") / "github.com" / "demo" / "demo", ) package = provider.search_for_direct_origin_dependency(dependency) @@ -390,18 +359,13 @@ def test_search_for_directory_setup_read_setup( def test_search_for_directory_setup_read_setup_with_extras( - provider: Provider, mocker: MockerFixture -): + provider: Provider, mocker: MockerFixture, fixture_dir: FixtureDirGetter +) -> None: mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv()) dependency = DirectoryDependency( "demo", - Path(__file__).parent.parent - / "fixtures" - / "git" - / "github.com" - / "demo" - / "demo", + fixture_dir("git") / "github.com" / "demo" / "demo", extras=["foo"], ) @@ -420,15 +384,12 @@ def test_search_for_directory_setup_read_setup_with_extras( } -def test_search_for_directory_setup_read_setup_with_no_dependencies(provider: Provider): +def test_search_for_directory_setup_read_setup_with_no_dependencies( + provider: Provider, fixture_dir: FixtureDirGetter +) -> None: dependency = DirectoryDependency( "demo", - Path(__file__).parent.parent - / "fixtures" - / "git" - / "github.com" - / "demo" - / "no-dependencies", + fixture_dir("git") / "github.com" / "demo" / "no-dependencies", ) package = provider.search_for_direct_origin_dependency(dependency) @@ -439,10 +400,12 @@ def test_search_for_directory_setup_read_setup_with_no_dependencies(provider: Pr assert package.extras == {} -def test_search_for_directory_poetry(provider: Provider): +def test_search_for_directory_poetry( + provider: Provider, fixture_dir: FixtureDirGetter +) -> None: dependency = DirectoryDependency( "project-with-extras", - Path(__file__).parent.parent / "fixtures" / "project_with_extras", + fixture_dir("project_with_extras"), ) package = provider.search_for_direct_origin_dependency(dependency) @@ -467,10 +430,12 @@ def test_search_for_directory_poetry(provider: Provider): } -def test_search_for_directory_poetry_with_extras(provider: Provider): +def test_search_for_directory_poetry_with_extras( + provider: Provider, fixture_dir: FixtureDirGetter +) -> None: dependency = DirectoryDependency( "project-with-extras", - Path(__file__).parent.parent / "fixtures" / "project_with_extras", + fixture_dir("project_with_extras"), extras=["extras_a"], ) @@ -496,13 +461,12 @@ def test_search_for_directory_poetry_with_extras(provider: Provider): } -def test_search_for_file_sdist(provider: Provider): +def test_search_for_file_sdist( + provider: Provider, fixture_dir: FixtureDirGetter +) -> None: dependency = FileDependency( "demo", - Path(__file__).parent.parent - / "fixtures" - / "distributions" - / "demo-0.1.0.tar.gz", + fixture_dir("distributions") / "demo-0.1.0.tar.gz", ) package = provider.search_for_direct_origin_dependency(dependency) @@ -527,13 +491,12 @@ def test_search_for_file_sdist(provider: Provider): } -def test_search_for_file_sdist_with_extras(provider: Provider): +def test_search_for_file_sdist_with_extras( + provider: Provider, fixture_dir: FixtureDirGetter +) -> None: dependency = FileDependency( "demo", - Path(__file__).parent.parent - / "fixtures" - / "distributions" - / "demo-0.1.0.tar.gz", + fixture_dir("distributions") / "demo-0.1.0.tar.gz", extras=["foo"], ) @@ -559,13 +522,12 @@ def test_search_for_file_sdist_with_extras(provider: Provider): } -def test_search_for_file_wheel(provider: Provider): +def test_search_for_file_wheel( + provider: Provider, fixture_dir: FixtureDirGetter +) -> None: dependency = FileDependency( "demo", - Path(__file__).parent.parent - / "fixtures" - / "distributions" - / "demo-0.1.0-py2.py3-none-any.whl", + fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl", ) package = provider.search_for_direct_origin_dependency(dependency) @@ -590,13 +552,12 @@ def test_search_for_file_wheel(provider: Provider): } -def test_search_for_file_wheel_with_extras(provider: Provider): +def test_search_for_file_wheel_with_extras( + provider: Provider, fixture_dir: FixtureDirGetter +) -> None: dependency = FileDependency( "demo", - Path(__file__).parent.parent - / "fixtures" - / "distributions" - / "demo-0.1.0-py2.py3-none-any.whl", + fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl", extras=["foo"], ) @@ -646,10 +607,9 @@ def test_complete_package_does_not_merge_different_source_names( def test_complete_package_preserves_source_type( - provider: Provider, root: ProjectPackage + provider: Provider, root: ProjectPackage, fixture_dir: FixtureDirGetter ) -> None: - fixtures = Path(__file__).parent.parent / "fixtures" - project_dir = fixtures.joinpath("with_conditional_path_deps") + project_dir = fixture_dir("with_conditional_path_deps") for folder in ["demo_one", "demo_two"]: path = (project_dir / folder).as_posix() root.add_dependency(Factory.create_dependency("demo", {"path": path})) @@ -744,7 +704,7 @@ def test_complete_package_with_extras_preserves_source_name( @pytest.mark.parametrize("with_extra", [False, True]) def test_complete_package_fetches_optional_vcs_dependency_only_if_requested( provider: Provider, repository: Repository, mocker: MockerFixture, with_extra: bool -): +) -> None: optional_vcs_dependency = Factory.create_dependency( "demo", {"git": "https://github.com/demo/demo.git", "optional": True} ) diff --git a/tests/puzzle/test_solver.py b/tests/puzzle/test_solver.py index 601151eab16..9b80652fd68 100644 --- a/tests/puzzle/test_solver.py +++ b/tests/puzzle/test_solver.py @@ -1,6 +1,7 @@ from __future__ import annotations -from pathlib import Path +import re + from typing import TYPE_CHECKING from typing import Any @@ -19,8 +20,10 @@ from poetry.packages import DependencyPackage from poetry.puzzle import Solver from poetry.puzzle.exceptions import SolverProblemError -from poetry.repositories.pool import Pool +from poetry.puzzle.provider import IncompatibleConstraintsError from poetry.repositories.repository import Repository +from poetry.repositories.repository_pool import Priority +from poetry.repositories.repository_pool import RepositoryPool from poetry.utils.env import MockEnv from tests.helpers import MOCK_DEFAULT_GIT_REVISION from tests.helpers import get_dependency @@ -39,6 +42,7 @@ from poetry.installation.operations.operation import Operation from poetry.puzzle.provider import Provider from poetry.puzzle.transaction import Transaction + from tests.types import FixtureDirGetter DEFAULT_SOURCE_REF = ( VCSDependency("poetry", "git", "git@github.com:python-poetry/poetry.git").branch @@ -67,12 +71,12 @@ def repo() -> Repository: @pytest.fixture() -def pool(repo: Repository) -> Pool: - return Pool([repo]) +def pool(repo: Repository) -> RepositoryPool: + return RepositoryPool([repo]) @pytest.fixture() -def solver(package: ProjectPackage, pool: Pool, io: NullIO) -> Solver: +def solver(package: ProjectPackage, pool: RepositoryPool, io: NullIO) -> Solver: return Solver(package, pool, [], [], io) @@ -111,7 +115,7 @@ def check_solver_result( def test_solver_install_single( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") @@ -123,8 +127,8 @@ def test_solver_install_single( def test_solver_remove_if_no_longer_locked( - package: ProjectPackage, pool: Pool, io: NullIO -): + package: ProjectPackage, pool: RepositoryPool, io: NullIO +) -> None: package_a = get_package("A", "1.0") solver = Solver(package, pool, [package_a], [package_a], io) @@ -134,8 +138,8 @@ def test_solver_remove_if_no_longer_locked( def test_remove_non_installed( - package: ProjectPackage, repo: Repository, pool: Pool, io: NullIO -): + package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO +) -> None: package_a = get_package("A", "1.0") repo.add_package(package_a) @@ -147,7 +151,7 @@ def test_remove_non_installed( def test_install_non_existing_package_fail( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency(Factory.create_dependency("B", "1")) package_a = get_package("A", "1.0") @@ -158,8 +162,8 @@ def test_install_non_existing_package_fail( def test_install_unpublished_package_does_not_fail( - package: ProjectPackage, repo: Repository, pool: Pool, io: NullIO -): + package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO +) -> None: package.add_dependency(Factory.create_dependency("B", "1")) package_a = get_package("A", "1.0") @@ -184,7 +188,9 @@ def test_install_unpublished_package_does_not_fail( ) -def test_solver_with_deps(solver: Solver, repo: Repository, package: ProjectPackage): +def test_solver_with_deps( + solver: Solver, repo: Repository, package: ProjectPackage +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") @@ -210,7 +216,7 @@ def test_solver_with_deps(solver: Solver, repo: Repository, package: ProjectPack def test_install_honours_not_equal( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") @@ -240,7 +246,7 @@ def test_install_honours_not_equal( def test_install_with_deps_in_order( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "*")) package.add_dependency(Factory.create_dependency("C", "*")) @@ -270,8 +276,8 @@ def test_install_with_deps_in_order( def test_install_installed( - package: ProjectPackage, repo: Repository, pool: Pool, io: NullIO -): + package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") @@ -286,8 +292,8 @@ def test_install_installed( def test_update_installed( - package: ProjectPackage, repo: Repository, pool: Pool, io: NullIO -): + package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") @@ -304,8 +310,8 @@ def test_update_installed( def test_update_with_use_latest( - package: ProjectPackage, repo: Repository, pool: Pool, io: NullIO -): + package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "*")) @@ -333,7 +339,9 @@ def test_update_with_use_latest( ) -def test_solver_sets_groups(solver: Solver, repo: Repository, package: ProjectPackage): +def test_solver_sets_groups( + solver: Solver, repo: Repository, package: ProjectPackage +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "*", groups=["dev"])) @@ -364,7 +372,7 @@ def test_solver_sets_groups(solver: Solver, repo: Repository, package: ProjectPa def test_solver_respects_root_package_python_versions( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: set_package_python_versions(solver.provider, "~3.4") package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "*")) @@ -397,7 +405,7 @@ def test_solver_respects_root_package_python_versions( def test_solver_fails_if_mismatch_root_python_versions( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: set_package_python_versions(solver.provider, "^3.4") package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "*")) @@ -419,7 +427,7 @@ def test_solver_fails_if_mismatch_root_python_versions( def test_solver_ignores_python_restricted_if_mismatch_root_package_python_versions( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: set_package_python_versions(solver.provider, "~3.8") package.add_dependency( Factory.create_dependency("A", {"version": "1.0", "python": "<3.8"}) @@ -443,7 +451,7 @@ def test_solver_ignores_python_restricted_if_mismatch_root_package_python_versio def test_solver_solves_optional_and_compatible_packages( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: set_package_python_versions(solver.provider, "~3.4") package.extras["foo"] = [get_dependency("B")] package.add_dependency( @@ -478,7 +486,7 @@ def test_solver_solves_optional_and_compatible_packages( def test_solver_does_not_return_extras_if_not_requested( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "*")) @@ -505,7 +513,7 @@ def test_solver_does_not_return_extras_if_not_requested( def test_solver_returns_extras_if_requested( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency( Factory.create_dependency("B", {"version": "*", "extras": ["foo"]}) @@ -545,7 +553,7 @@ def test_solver_returns_extras_only_requested( repo: Repository, package: ProjectPackage, enabled_extra: bool | None, -): +) -> None: extras = [enabled_extra] if enabled_extra is not None else [] package.add_dependency(Factory.create_dependency("A", "*")) @@ -607,7 +615,7 @@ def test_solver_returns_extras_when_multiple_extras_use_same_dependency( repo: Repository, package: ProjectPackage, enabled_extra: bool | None, -): +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") @@ -656,7 +664,7 @@ def test_solver_returns_extras_only_requested_nested( repo: Repository, package: ProjectPackage, enabled_extra: bool | None, -): +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") @@ -711,7 +719,7 @@ def test_solver_returns_extras_only_requested_nested( def test_solver_finds_extras_next_to_non_extras( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: # Root depends on A[foo] package.add_dependency( Factory.create_dependency("A", {"version": "*", "extras": ["foo"]}) @@ -757,8 +765,8 @@ def test_solver_finds_extras_next_to_non_extras( def test_solver_merge_extras_into_base_package_multiple_repos_fixes_5727( - solver: Solver, repo: Repository, pool: Pool, package: ProjectPackage -): + solver: Solver, repo: Repository, pool: RepositoryPool, package: ProjectPackage +) -> None: package.add_dependency( Factory.create_dependency("A", {"version": "*", "source": "legacy"}) ) @@ -788,9 +796,48 @@ def test_solver_merge_extras_into_base_package_multiple_repos_fixes_5727( assert len(ops[0].package.requires) == 0, "a should not require itself" +def test_solver_returns_extras_if_excluded_by_markers_without_extras( + solver: Solver, repo: Repository, package: ProjectPackage +) -> None: + package.add_dependency( + Factory.create_dependency("A", {"version": "*", "extras": ["foo"]}) + ) + + package_a = get_package("A", "1.0") + package_b = get_package("B", "1.0") + + # mandatory dependency with marker + dep = get_dependency("B", "^1.0") + dep.marker = parse_marker("sys_platform != 'linux'") + package_a.add_dependency(dep) + + # optional dependency with same constraint and no marker except for extra + dep = get_dependency("B", "^1.0", optional=True) + dep.marker = parse_marker("extra == 'foo'") + package_a.extras = {"foo": [dep]} + package_a.add_dependency(dep) + + repo.add_package(package_a) + repo.add_package(package_b) + + transaction = solver.solve() + + ops = check_solver_result( + transaction, + [ + {"job": "install", "package": package_b}, + {"job": "install", "package": package_a}, + ], + ) + assert ( + str(ops[1].package.requires[0].marker) + == 'sys_platform != "linux" or extra == "foo"' + ) + + def test_solver_returns_prereleases_if_requested( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "*")) package.add_dependency( @@ -821,7 +868,7 @@ def test_solver_returns_prereleases_if_requested( def test_solver_does_not_return_prereleases_if_not_requested( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "*")) package.add_dependency(Factory.create_dependency("C", "*")) @@ -850,7 +897,7 @@ def test_solver_does_not_return_prereleases_if_not_requested( def test_solver_sub_dependencies_with_requirements( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "*")) @@ -888,7 +935,7 @@ def test_solver_sub_dependencies_with_requirements( def test_solver_sub_dependencies_with_requirements_complex( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency( Factory.create_dependency("A", {"version": "^1.0", "python": "<5.0"}) ) @@ -947,7 +994,7 @@ def test_solver_sub_dependencies_with_requirements_complex( def test_solver_sub_dependencies_with_not_supported_python_version( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: set_package_python_versions(solver.provider, "^3.5") package.add_dependency(Factory.create_dependency("A", "*")) @@ -969,7 +1016,7 @@ def test_solver_sub_dependencies_with_not_supported_python_version( def test_solver_sub_dependencies_with_not_supported_python_version_transitive( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: set_package_python_versions(solver.provider, "^3.4") package.add_dependency( @@ -1013,7 +1060,7 @@ def test_solver_sub_dependencies_with_not_supported_python_version_transitive( def test_solver_with_dependency_in_both_main_and_dev_dependencies( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: set_package_python_versions(solver.provider, "^3.5") package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency( @@ -1066,7 +1113,7 @@ def test_solver_with_dependency_in_both_main_and_dev_dependencies( def test_solver_with_dependency_in_both_main_and_dev_dependencies_with_one_more_dependent( # noqa: E501 solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("E", "*")) package.add_dependency( @@ -1126,7 +1173,7 @@ def test_solver_with_dependency_in_both_main_and_dev_dependencies_with_one_more_ def test_solver_with_dependency_and_prerelease_sub_dependencies( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") @@ -1153,7 +1200,7 @@ def test_solver_with_dependency_and_prerelease_sub_dependencies( def test_solver_circular_dependency( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") @@ -1185,7 +1232,7 @@ def test_solver_circular_dependency( def test_solver_circular_dependency_chain( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") @@ -1222,7 +1269,7 @@ def test_solver_circular_dependency_chain( def test_solver_dense_dependencies( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: # The root package depends on packages A0...An-1, # And package Ai depends on packages A0...Ai-1 # This graph is a transitive tournament @@ -1245,7 +1292,7 @@ def test_solver_dense_dependencies( def test_solver_duplicate_dependencies_same_constraint( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") @@ -1274,7 +1321,7 @@ def test_solver_duplicate_dependencies_same_constraint( def test_solver_duplicate_dependencies_different_constraints( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") @@ -1306,7 +1353,7 @@ def test_solver_duplicate_dependencies_different_constraints( def test_solver_duplicate_dependencies_different_constraints_same_requirements( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") @@ -1333,7 +1380,7 @@ def test_solver_duplicate_dependencies_different_constraints_same_requirements( def test_solver_duplicate_dependencies_different_constraints_merge_by_marker( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") @@ -1371,7 +1418,7 @@ def test_solver_duplicate_dependencies_different_constraints_merge_by_marker( @pytest.mark.parametrize("git_first", [False, True]) def test_solver_duplicate_dependencies_different_sources_types_are_preserved( solver: Solver, repo: Repository, package: ProjectPackage, git_first: bool -): +) -> None: pendulum = get_package("pendulum", "2.0.3") repo.add_package(pendulum) repo.add_package(get_package("cleo", "1.0.0")) @@ -1439,7 +1486,7 @@ def test_solver_duplicate_dependencies_different_sources_types_are_preserved( def test_solver_duplicate_dependencies_different_constraints_merge_no_markers( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "1.0")) @@ -1480,9 +1527,167 @@ def test_solver_duplicate_dependencies_different_constraints_merge_no_markers( ) +def test_solver_duplicate_dependencies_different_constraints_conflict( + solver: Solver, repo: Repository, package: ProjectPackage +) -> None: + package.add_dependency(Factory.create_dependency("A", ">=1.1")) + package.add_dependency( + Factory.create_dependency("A", {"version": "<1.1", "python": "3.10"}) + ) + + repo.add_package(get_package("A", "1.0")) + repo.add_package(get_package("A", "1.1")) + repo.add_package(get_package("A", "1.2")) + + expectation = ( + "Incompatible constraints in requirements of root (1.0):\n" + 'A (<1.1) ; python_version == "3.10"\n' + "A (>=1.1)" + ) + with pytest.raises(IncompatibleConstraintsError, match=re.escape(expectation)): + solver.solve() + + +def test_solver_duplicate_dependencies_different_constraints_discard_no_markers1( + solver: Solver, repo: Repository, package: ProjectPackage +) -> None: + """ + Initial dependencies: + A (>=1.0) + A (<1.2) ; python >= 3.10 + A (<1.1) ; python < 3.10 + + Merged dependencies: + A (>=1.0) ; + A (>=1.0,<1.2) ; python >= 3.10 + A (>=1.0,<1.1) ; python < 3.10 + + The dependency with an empty marker has to be ignored. + """ + package.add_dependency(Factory.create_dependency("A", ">=1.0")) + package.add_dependency( + Factory.create_dependency("A", {"version": "<1.2", "python": ">=3.10"}) + ) + package.add_dependency( + Factory.create_dependency("A", {"version": "<1.1", "python": "<3.10"}) + ) + package.add_dependency(Factory.create_dependency("B", "*")) + + package_a10 = get_package("A", "1.0") + package_a11 = get_package("A", "1.1") + package_a12 = get_package("A", "1.2") + package_b = get_package("B", "1.0") + package_b.add_dependency(Factory.create_dependency("A", "*")) + + repo.add_package(package_a10) + repo.add_package(package_a11) + repo.add_package(package_a12) + repo.add_package(package_b) + + transaction = solver.solve() + + check_solver_result( + transaction, + [ + # only a10 and a11, not a12 + {"job": "install", "package": package_a10}, + {"job": "install", "package": package_a11}, + {"job": "install", "package": package_b}, + ], + ) + + +def test_solver_duplicate_dependencies_different_constraints_discard_no_markers2( + solver: Solver, repo: Repository, package: ProjectPackage +) -> None: + """ + Initial dependencies: + A (>=1.0) + A (<1.2) ; python == 3.10 + + Merged dependencies: + A (>=1.0) ; python != 3.10 + A (>=1.0,<1.2) ; python == 3.10 + + The first dependency has to be ignored + because it is not compatible with the project's python constraint. + """ + set_package_python_versions(solver.provider, "~3.10") + package.add_dependency(Factory.create_dependency("A", ">=1.0")) + package.add_dependency( + Factory.create_dependency("A", {"version": "<1.2", "python": "3.10"}) + ) + package.add_dependency(Factory.create_dependency("B", "*")) + + package_a10 = get_package("A", "1.0") + package_a11 = get_package("A", "1.1") + package_a12 = get_package("A", "1.2") + package_b = get_package("B", "1.0") + package_b.add_dependency(Factory.create_dependency("A", "*")) + + repo.add_package(package_a10) + repo.add_package(package_a11) + repo.add_package(package_a12) + repo.add_package(package_b) + + transaction = solver.solve() + + check_solver_result( + transaction, + [ + {"job": "install", "package": package_a11}, # only a11, not a12 + {"job": "install", "package": package_b}, + ], + ) + + +def test_solver_duplicate_dependencies_different_constraints_discard_no_markers3( + solver: Solver, repo: Repository, package: ProjectPackage +) -> None: + """ + Initial dependencies: + A (>=1.0) + A (<1.2) ; python == 3.10 + + Merged dependencies: + A (>=1.0) ; python != 3.10 + A (>=1.0,<1.2) ; python == 3.10 + + The first dependency has to be ignored + because it is not compatible with the current environment. + """ + package.add_dependency(Factory.create_dependency("A", ">=1.0")) + package.add_dependency( + Factory.create_dependency("A", {"version": "<1.2", "python": "3.10"}) + ) + package.add_dependency(Factory.create_dependency("B", "*")) + + package_a10 = get_package("A", "1.0") + package_a11 = get_package("A", "1.1") + package_a12 = get_package("A", "1.2") + package_b = get_package("B", "1.0") + package_b.add_dependency(Factory.create_dependency("A", "*")) + + repo.add_package(package_a10) + repo.add_package(package_a11) + repo.add_package(package_a12) + repo.add_package(package_b) + + with solver.use_environment(MockEnv((3, 10, 0))): + transaction = solver.solve() + + check_solver_result( + transaction, + [ + {"job": "install", "package": package_a11}, # only a11, not a12 + {"job": "install", "package": package_b}, + ], + ) + + def test_solver_duplicate_dependencies_ignore_overrides_with_empty_marker_intersection( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: """ Distinct requirements per marker: * Python 2.7: A (which requires B) and B @@ -1553,7 +1758,7 @@ def test_solver_duplicate_dependencies_ignore_overrides_with_empty_marker_inters def test_solver_duplicate_dependencies_ignore_overrides_with_empty_marker_intersection2( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: """ Empty intersection between top level dependency and transient dependency. """ @@ -1596,7 +1801,7 @@ def test_solver_duplicate_dependencies_ignore_overrides_with_empty_marker_inters def test_solver_duplicate_dependencies_sub_dependencies( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package_a = get_package("A", "1.0") @@ -1635,10 +1840,11 @@ def test_solver_duplicate_dependencies_sub_dependencies( ) -def test_duplicate_path_dependencies(solver: Solver, package: ProjectPackage) -> None: +def test_duplicate_path_dependencies( + solver: Solver, package: ProjectPackage, fixture_dir: FixtureDirGetter +) -> None: set_package_python_versions(solver.provider, "^3.7") - fixtures = Path(__file__).parent.parent / "fixtures" - project_dir = fixtures / "with_conditional_path_deps" + project_dir = fixture_dir("with_conditional_path_deps") path1 = (project_dir / "demo_one").as_posix() demo1 = Package("demo", "1.2.3", source_type="directory", source_url=path1) @@ -1668,11 +1874,10 @@ def test_duplicate_path_dependencies(solver: Solver, package: ProjectPackage) -> def test_duplicate_path_dependencies_same_path( - solver: Solver, package: ProjectPackage + solver: Solver, package: ProjectPackage, fixture_dir: FixtureDirGetter ) -> None: set_package_python_versions(solver.provider, "^3.7") - fixtures = Path(__file__).parent.parent / "fixtures" - project_dir = fixtures / "with_conditional_path_deps" + project_dir = fixture_dir("with_conditional_path_deps") path1 = (project_dir / "demo_one").as_posix() demo1 = Package("demo", "1.2.3", source_type="directory", source_url=path1) @@ -1694,7 +1899,7 @@ def test_duplicate_path_dependencies_same_path( def test_solver_fails_if_dependency_name_does_not_match_package( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency( Factory.create_dependency( "my-demo", {"git": "https://github.com/demo/demo.git"} @@ -1707,7 +1912,7 @@ def test_solver_fails_if_dependency_name_does_not_match_package( def test_solver_does_not_get_stuck_in_recursion_on_circular_dependency( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package_a = get_package("A", "1.0") package_a.add_dependency(Factory.create_dependency("B", "^1.0")) package_b = get_package("B", "1.0") @@ -1735,7 +1940,7 @@ def test_solver_does_not_get_stuck_in_recursion_on_circular_dependency( def test_solver_can_resolve_git_dependencies( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: pendulum = get_package("pendulum", "2.0.3") cleo = get_package("cleo", "1.0.0") repo.add_package(pendulum) @@ -1770,7 +1975,7 @@ def test_solver_can_resolve_git_dependencies( def test_solver_can_resolve_git_dependencies_with_extras( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: pendulum = get_package("pendulum", "2.0.3") cleo = get_package("cleo", "1.0.0") repo.add_package(pendulum) @@ -1810,7 +2015,7 @@ def test_solver_can_resolve_git_dependencies_with_extras( ) def test_solver_can_resolve_git_dependencies_with_ref( solver: Solver, repo: Repository, package: ProjectPackage, ref: dict[str, str] -): +) -> None: pendulum = get_package("pendulum", "2.0.3") cleo = get_package("cleo", "1.0.0") repo.add_package(pendulum) @@ -1845,7 +2050,7 @@ def test_solver_can_resolve_git_dependencies_with_ref( def test_solver_does_not_trigger_conflict_for_python_constraint_if_python_requirement_is_compatible( # noqa: E501 solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: set_package_python_versions(solver.provider, "~2.7 || ^3.4") package.add_dependency( Factory.create_dependency("A", {"version": "^1.0", "python": "^3.6"}) @@ -1863,7 +2068,7 @@ def test_solver_does_not_trigger_conflict_for_python_constraint_if_python_requir def test_solver_does_not_trigger_conflict_for_python_constraint_if_python_requirement_is_compatible_multiple( # noqa: E501 solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: set_package_python_versions(solver.provider, "~2.7 || ^3.4") package.add_dependency( Factory.create_dependency("A", {"version": "^1.0", "python": "^3.6"}) @@ -1895,7 +2100,7 @@ def test_solver_does_not_trigger_conflict_for_python_constraint_if_python_requir def test_solver_triggers_conflict_for_dependency_python_not_fully_compatible_with_package_python( # noqa: E501 solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: set_package_python_versions(solver.provider, "~2.7 || ^3.4") package.add_dependency( Factory.create_dependency("A", {"version": "^1.0", "python": "^3.5"}) @@ -1912,7 +2117,7 @@ def test_solver_triggers_conflict_for_dependency_python_not_fully_compatible_wit def test_solver_finds_compatible_package_for_dependency_python_not_fully_compatible_with_package_python( # noqa: E501 solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: set_package_python_versions(solver.provider, "~2.7 || ^3.4") package.add_dependency( Factory.create_dependency("A", {"version": "^1.0", "python": "^3.5"}) @@ -1934,7 +2139,7 @@ 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( # noqa: E501 solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: dep1 = Dependency.create_from_pep_508('B (>=1.0); extra == "foo"') dep1.activate() dep2 = Dependency.create_from_pep_508('B (>=2.0); extra == "bar"') @@ -1972,7 +2177,7 @@ def test_solver_does_not_trigger_new_resolution_on_duplicate_dependencies_if_onl def test_solver_does_not_raise_conflict_for_locked_conditional_dependencies( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: set_package_python_versions(solver.provider, "~2.7 || ^3.4") package.add_dependency( Factory.create_dependency("A", {"version": "^1.0", "python": "^3.6"}) @@ -2004,7 +2209,7 @@ def test_solver_does_not_raise_conflict_for_locked_conditional_dependencies( def test_solver_returns_extras_if_requested_in_dependencies_and_not_in_root_package( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency(Factory.create_dependency("A", "*")) package.add_dependency(Factory.create_dependency("B", "*")) package.add_dependency(Factory.create_dependency("C", "*")) @@ -2043,7 +2248,7 @@ def test_solver_returns_extras_if_requested_in_dependencies_and_not_in_root_pack def test_solver_should_not_resolve_prerelease_version_if_not_requested( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency(Factory.create_dependency("A", "~1.8.0")) package.add_dependency(Factory.create_dependency("B", "^0.5.0")) @@ -2062,7 +2267,7 @@ def test_solver_should_not_resolve_prerelease_version_if_not_requested( def test_solver_ignores_dependencies_with_incompatible_python_full_version_marker( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: set_package_python_versions(solver.provider, "^3.6") package.add_dependency(Factory.create_dependency("A", "^1.0")) package.add_dependency(Factory.create_dependency("B", "^2.0")) @@ -2094,8 +2299,8 @@ def test_solver_ignores_dependencies_with_incompatible_python_full_version_marke def test_solver_git_dependencies_update( - package: ProjectPackage, repo: Repository, pool: Pool, io: NullIO -): + package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO +) -> None: pendulum = get_package("pendulum", "2.0.3") cleo = get_package("cleo", "1.0.0") repo.add_package(pendulum) @@ -2143,8 +2348,8 @@ def test_solver_git_dependencies_update( def test_solver_git_dependencies_update_skipped( - package: ProjectPackage, repo: Repository, pool: Pool, io: NullIO -): + package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO +) -> None: pendulum = get_package("pendulum", "2.0.3") cleo = get_package("cleo", "1.0.0") repo.add_package(pendulum) @@ -2176,8 +2381,8 @@ def test_solver_git_dependencies_update_skipped( def test_solver_git_dependencies_short_hash_update_skipped( - package: ProjectPackage, repo: Repository, pool: Pool, io: NullIO -): + package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO +) -> None: pendulum = get_package("pendulum", "2.0.3") cleo = get_package("cleo", "1.0.0") repo.add_package(pendulum) @@ -2222,19 +2427,15 @@ def test_solver_git_dependencies_short_hash_update_skipped( def test_solver_can_resolve_directory_dependencies( - solver: Solver, repo: Repository, package: ProjectPackage -): + solver: Solver, + repo: Repository, + package: ProjectPackage, + fixture_dir: FixtureDirGetter, +) -> None: pendulum = get_package("pendulum", "2.0.3") repo.add_package(pendulum) - path = ( - Path(__file__).parent.parent - / "fixtures" - / "git" - / "github.com" - / "demo" - / "demo" - ).as_posix() + path = (fixture_dir("git") / "github.com" / "demo" / "demo").as_posix() package.add_dependency(Factory.create_dependency("demo", {"path": path})) @@ -2257,10 +2458,11 @@ def test_solver_can_resolve_directory_dependencies( def test_solver_can_resolve_directory_dependencies_nested_editable( repo: Repository, - pool: Pool, + pool: RepositoryPool, io: NullIO, -): - base = Path(__file__).parent.parent / "fixtures" / "project_with_nested_local" + fixture_dir: FixtureDirGetter, +) -> None: + base = fixture_dir("project_with_nested_local") poetry = Factory().create_poetry(cwd=base) package = poetry.package @@ -2310,21 +2512,17 @@ def test_solver_can_resolve_directory_dependencies_nested_editable( def test_solver_can_resolve_directory_dependencies_with_extras( - solver: Solver, repo: Repository, package: ProjectPackage -): + solver: Solver, + repo: Repository, + package: ProjectPackage, + fixture_dir: FixtureDirGetter, +) -> None: pendulum = get_package("pendulum", "2.0.3") cleo = get_package("cleo", "1.0.0") repo.add_package(pendulum) repo.add_package(cleo) - path = ( - Path(__file__).parent.parent - / "fixtures" - / "git" - / "github.com" - / "demo" - / "demo" - ).as_posix() + path = (fixture_dir("git") / "github.com" / "demo" / "demo").as_posix() package.add_dependency( Factory.create_dependency("demo", {"path": path, "extras": ["foo"]}) @@ -2352,17 +2550,15 @@ def test_solver_can_resolve_directory_dependencies_with_extras( def test_solver_can_resolve_sdist_dependencies( - solver: Solver, repo: Repository, package: ProjectPackage -): + solver: Solver, + repo: Repository, + package: ProjectPackage, + fixture_dir: FixtureDirGetter, +) -> None: pendulum = get_package("pendulum", "2.0.3") repo.add_package(pendulum) - path = ( - Path(__file__).parent.parent - / "fixtures" - / "distributions" - / "demo-0.1.0.tar.gz" - ).as_posix() + path = (fixture_dir("distributions") / "demo-0.1.0.tar.gz").as_posix() package.add_dependency(Factory.create_dependency("demo", {"path": path})) @@ -2384,19 +2580,17 @@ def test_solver_can_resolve_sdist_dependencies( def test_solver_can_resolve_sdist_dependencies_with_extras( - solver: Solver, repo: Repository, package: ProjectPackage -): + solver: Solver, + repo: Repository, + package: ProjectPackage, + fixture_dir: FixtureDirGetter, +) -> None: pendulum = get_package("pendulum", "2.0.3") cleo = get_package("cleo", "1.0.0") repo.add_package(pendulum) repo.add_package(cleo) - path = ( - Path(__file__).parent.parent - / "fixtures" - / "distributions" - / "demo-0.1.0.tar.gz" - ).as_posix() + path = (fixture_dir("distributions") / "demo-0.1.0.tar.gz").as_posix() package.add_dependency( Factory.create_dependency("demo", {"path": path, "extras": ["foo"]}) @@ -2424,17 +2618,15 @@ def test_solver_can_resolve_sdist_dependencies_with_extras( def test_solver_can_resolve_wheel_dependencies( - solver: Solver, repo: Repository, package: ProjectPackage -): + solver: Solver, + repo: Repository, + package: ProjectPackage, + fixture_dir: FixtureDirGetter, +) -> None: pendulum = get_package("pendulum", "2.0.3") repo.add_package(pendulum) - path = ( - Path(__file__).parent.parent - / "fixtures" - / "distributions" - / "demo-0.1.0-py2.py3-none-any.whl" - ).as_posix() + path = (fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl").as_posix() package.add_dependency(Factory.create_dependency("demo", {"path": path})) @@ -2456,19 +2648,17 @@ def test_solver_can_resolve_wheel_dependencies( def test_solver_can_resolve_wheel_dependencies_with_extras( - solver: Solver, repo: Repository, package: ProjectPackage -): + solver: Solver, + repo: Repository, + package: ProjectPackage, + fixture_dir: FixtureDirGetter, +) -> None: pendulum = get_package("pendulum", "2.0.3") cleo = get_package("cleo", "1.0.0") repo.add_package(pendulum) repo.add_package(cleo) - path = ( - Path(__file__).parent.parent - / "fixtures" - / "distributions" - / "demo-0.1.0-py2.py3-none-any.whl" - ).as_posix() + path = (fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl").as_posix() package.add_dependency( Factory.create_dependency("demo", {"path": path, "extras": ["foo"]}) @@ -2497,9 +2687,9 @@ def test_solver_can_resolve_wheel_dependencies_with_extras( def test_solver_can_solve_with_legacy_repository_using_proper_dists( package: ProjectPackage, io: NullIO -): +) -> None: repo = MockLegacyRepository() - pool = Pool([repo]) + pool = RepositoryPool([repo]) solver = Solver(package, pool, [], [], io) @@ -2540,11 +2730,11 @@ def test_solver_can_solve_with_legacy_repository_using_proper_dists( def test_solver_can_solve_with_legacy_repository_using_proper_python_compatible_dists( package: ProjectPackage, io: NullIO, -): +) -> None: package.python_versions = "^3.7" repo = MockLegacyRepository() - pool = Pool([repo]) + pool = RepositoryPool([repo]) solver = Solver(package, pool, [], [], io) @@ -2569,11 +2759,11 @@ def test_solver_can_solve_with_legacy_repository_using_proper_python_compatible_ ) -def test_solver_skips_invalid_versions(package: ProjectPackage, io: NullIO): +def test_solver_skips_invalid_versions(package: ProjectPackage, io: NullIO) -> None: package.python_versions = "^3.7" repo = MockPyPIRepository() - pool = Pool([repo]) + pool = RepositoryPool([repo]) solver = Solver(package, pool, [], [], io) @@ -2588,7 +2778,7 @@ def test_solver_skips_invalid_versions(package: ProjectPackage, io: NullIO): def test_multiple_constraints_on_root( package: ProjectPackage, solver: Solver, repo: Repository -): +) -> None: package.add_dependency( Factory.create_dependency("foo", {"version": "^1.0", "python": "^2.7"}) ) @@ -2612,12 +2802,12 @@ def test_multiple_constraints_on_root( def test_solver_chooses_most_recent_version_amongst_repositories( package: ProjectPackage, io: NullIO -): +) -> None: package.python_versions = "^3.7" package.add_dependency(Factory.create_dependency("tomlkit", {"version": "^0.5"})) repo = MockLegacyRepository() - pool = Pool([repo, MockPyPIRepository()]) + pool = RepositoryPool([repo, MockPyPIRepository()]) solver = Solver(package, pool, [], [], io) @@ -2633,14 +2823,14 @@ def test_solver_chooses_most_recent_version_amongst_repositories( def test_solver_chooses_from_correct_repository_if_forced( package: ProjectPackage, io: NullIO -): +) -> None: package.python_versions = "^3.7" package.add_dependency( Factory.create_dependency("tomlkit", {"version": "^0.5", "source": "legacy"}) ) repo = MockLegacyRepository() - pool = Pool([repo, MockPyPIRepository()]) + pool = RepositoryPool([repo, MockPyPIRepository()]) solver = Solver(package, pool, [], [], io) @@ -2668,7 +2858,7 @@ def test_solver_chooses_from_correct_repository_if_forced( def test_solver_chooses_from_correct_repository_if_forced_and_transitive_dependency( package: ProjectPackage, io: NullIO, -): +) -> None: package.python_versions = "^3.7" package.add_dependency(Factory.create_dependency("foo", "^1.0")) package.add_dependency( @@ -2679,7 +2869,7 @@ def test_solver_chooses_from_correct_repository_if_forced_and_transitive_depende foo = get_package("foo", "1.0.0") foo.add_dependency(Factory.create_dependency("tomlkit", "^0.5.0")) repo.add_package(foo) - pool = Pool([MockLegacyRepository(), repo, MockPyPIRepository()]) + pool = RepositoryPool([MockLegacyRepository(), repo, MockPyPIRepository()]) solver = Solver(package, pool, [], [], io) @@ -2710,12 +2900,12 @@ def test_solver_chooses_from_correct_repository_if_forced_and_transitive_depende def test_solver_does_not_choose_from_secondary_repository_by_default( package: ProjectPackage, io: NullIO -): +) -> None: package.python_versions = "^3.7" package.add_dependency(Factory.create_dependency("clikit", {"version": "^0.2.0"})) - pool = Pool() - pool.add_repository(MockPyPIRepository(), secondary=True) + pool = RepositoryPool() + pool.add_repository(MockPyPIRepository(), priority=Priority.SECONDARY) pool.add_repository(MockLegacyRepository()) solver = Solver(package, pool, [], [], io) @@ -2758,14 +2948,14 @@ def test_solver_does_not_choose_from_secondary_repository_by_default( def test_solver_chooses_from_secondary_if_explicit( package: ProjectPackage, io: NullIO, -): +) -> None: package.python_versions = "^3.7" package.add_dependency( Factory.create_dependency("clikit", {"version": "^0.2.0", "source": "PyPI"}) ) - pool = Pool() - pool.add_repository(MockPyPIRepository(), secondary=True) + pool = RepositoryPool() + pool.add_repository(MockPyPIRepository(), priority=Priority.SECONDARY) pool.add_repository(MockLegacyRepository()) solver = Solver(package, pool, [], [], io) @@ -2797,12 +2987,77 @@ def test_solver_chooses_from_secondary_if_explicit( assert ops[2].package.source_url is None +def test_solver_does_not_choose_from_explicit_repository( + package: ProjectPackage, io: NullIO +) -> None: + package.python_versions = "^3.7" + package.add_dependency(Factory.create_dependency("attrs", {"version": "^17.4.0"})) + + pool = RepositoryPool() + pool.add_repository(MockPyPIRepository(), priority=Priority.EXPLICIT) + pool.add_repository(MockLegacyRepository()) + + solver = Solver(package, pool, [], [], io) + + with pytest.raises(SolverProblemError): + solver.solve() + + +def test_solver_chooses_direct_dependency_from_explicit_if_explicit( + package: ProjectPackage, + io: NullIO, +) -> None: + package.python_versions = "^3.7" + package.add_dependency( + Factory.create_dependency("pylev", {"version": "^1.2.0", "source": "PyPI"}) + ) + + pool = RepositoryPool() + pool.add_repository(MockPyPIRepository(), priority=Priority.EXPLICIT) + pool.add_repository(MockLegacyRepository()) + + solver = Solver(package, pool, [], [], io) + + transaction = solver.solve() + + ops = check_solver_result( + transaction, + [ + {"job": "install", "package": get_package("pylev", "1.3.0")}, + ], + ) + + assert ops[0].package.source_type is None + assert ops[0].package.source_url is None + + +def test_solver_ignores_explicit_repo_for_transient_dependencies( + package: ProjectPackage, + io: NullIO, +) -> None: + # clikit depends on pylev, which is in MockPyPIRepository (explicit) but not in + # MockLegacyRepository + package.python_versions = "^3.7" + package.add_dependency( + Factory.create_dependency("clikit", {"version": "^0.2.0", "source": "PyPI"}) + ) + + pool = RepositoryPool() + pool.add_repository(MockPyPIRepository(), priority=Priority.EXPLICIT) + pool.add_repository(MockLegacyRepository()) + + solver = Solver(package, pool, [], [], io) + + with pytest.raises(SolverProblemError): + solver.solve() + + def test_solver_discards_packages_with_empty_markers( package: ProjectPackage, repo: Repository, - pool: Pool, + pool: RepositoryPool, io: NullIO, -): +) -> None: package.python_versions = "~2.7 || ^3.4" package.add_dependency( Factory.create_dependency( @@ -2838,7 +3093,7 @@ def test_solver_discards_packages_with_empty_markers( def test_solver_does_not_raise_conflict_for_conditional_dev_dependencies( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: set_package_python_versions(solver.provider, "~2.7 || ^3.5") package.add_dependency( Factory.create_dependency( @@ -2870,7 +3125,7 @@ def test_solver_does_not_raise_conflict_for_conditional_dev_dependencies( def test_solver_does_not_loop_indefinitely_on_duplicate_constraints_with_extras( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: set_package_python_versions(solver.provider, "~2.7 || ^3.5") package.add_dependency( Factory.create_dependency( @@ -2902,9 +3157,9 @@ def test_solver_does_not_loop_indefinitely_on_duplicate_constraints_with_extras( def test_solver_does_not_fail_with_locked_git_and_non_git_dependencies( package: ProjectPackage, repo: Repository, - pool: Pool, + pool: RepositoryPool, io: NullIO, -): +) -> None: package.add_dependency( Factory.create_dependency("demo", {"git": "https://github.com/demo/demo.git"}) ) @@ -2939,7 +3194,7 @@ def test_solver_does_not_fail_with_locked_git_and_non_git_dependencies( def test_ignore_python_constraint_no_overlap_dependencies( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: pytest = get_package("demo", "1.0.0") pytest.add_dependency( Factory.create_dependency( @@ -2964,7 +3219,7 @@ def test_ignore_python_constraint_no_overlap_dependencies( def test_solver_should_not_go_into_an_infinite_loop_on_duplicate_dependencies( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: set_package_python_versions(solver.provider, "~2.7 || ^3.5") package.add_dependency(Factory.create_dependency("A", "^1.0")) @@ -2995,7 +3250,9 @@ def test_solver_should_not_go_into_an_infinite_loop_on_duplicate_dependencies( ) -def test_solver_synchronize_single(package: ProjectPackage, pool: Pool, io: NullIO): +def test_solver_synchronize_single( + package: ProjectPackage, pool: RepositoryPool, io: NullIO +) -> None: package_a = get_package("a", "1.0") solver = Solver(package, pool, [package_a], [], io) @@ -3009,9 +3266,9 @@ def test_solver_synchronize_single(package: ProjectPackage, pool: Pool, io: Null @pytest.mark.skip(reason="Poetry no longer has critical package requirements") def test_solver_with_synchronization_keeps_critical_package( package: ProjectPackage, - pool: Pool, + pool: RepositoryPool, io: NullIO, -): +) -> None: package_pip = get_package("setuptools", "1.0") solver = Solver(package, pool, [package_pip], [], io) @@ -3021,8 +3278,11 @@ def test_solver_with_synchronization_keeps_critical_package( def test_solver_cannot_choose_another_version_for_directory_dependencies( - solver: Solver, repo: Repository, package: ProjectPackage -): + solver: Solver, + repo: Repository, + package: ProjectPackage, + fixture_dir: FixtureDirGetter, +) -> None: pendulum = get_package("pendulum", "2.0.3") demo = get_package("demo", "0.1.0") foo = get_package("foo", "1.2.3") @@ -3031,14 +3291,7 @@ def test_solver_cannot_choose_another_version_for_directory_dependencies( repo.add_package(demo) repo.add_package(pendulum) - path = ( - Path(__file__).parent.parent - / "fixtures" - / "git" - / "github.com" - / "demo" - / "demo" - ).as_posix() + path = (fixture_dir("git") / "github.com" / "demo" / "demo").as_posix() package.add_dependency(Factory.create_dependency("demo", {"path": path})) package.add_dependency(Factory.create_dependency("foo", "^1.2.3")) @@ -3050,8 +3303,11 @@ def test_solver_cannot_choose_another_version_for_directory_dependencies( def test_solver_cannot_choose_another_version_for_file_dependencies( - solver: Solver, repo: Repository, package: ProjectPackage -): + solver: Solver, + repo: Repository, + package: ProjectPackage, + fixture_dir: FixtureDirGetter, +) -> None: pendulum = get_package("pendulum", "2.0.3") demo = get_package("demo", "0.0.8") foo = get_package("foo", "1.2.3") @@ -3060,12 +3316,7 @@ def test_solver_cannot_choose_another_version_for_file_dependencies( repo.add_package(demo) repo.add_package(pendulum) - path = ( - Path(__file__).parent.parent - / "fixtures" - / "distributions" - / "demo-0.1.0-py2.py3-none-any.whl" - ).as_posix() + path = (fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl").as_posix() package.add_dependency(Factory.create_dependency("demo", {"path": path})) package.add_dependency(Factory.create_dependency("foo", "^1.2.3")) @@ -3078,7 +3329,7 @@ def test_solver_cannot_choose_another_version_for_file_dependencies( def test_solver_cannot_choose_another_version_for_git_dependencies( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: pendulum = get_package("pendulum", "2.0.3") demo = get_package("demo", "0.0.8") foo = get_package("foo", "1.2.3") @@ -3103,13 +3354,9 @@ def test_solver_cannot_choose_another_version_for_url_dependencies( repo: Repository, package: ProjectPackage, http: type[httpretty.httpretty], -): - path = ( - Path(__file__).parent.parent - / "fixtures" - / "distributions" - / "demo-0.1.0-py2.py3-none-any.whl" - ) + fixture_dir: FixtureDirGetter, +) -> None: + path = fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl" http.register_uri( "GET", @@ -3140,8 +3387,8 @@ def test_solver_cannot_choose_another_version_for_url_dependencies( def test_solver_should_not_update_same_version_packages_if_installed_has_no_source_type( - package: ProjectPackage, repo: Repository, pool: Pool, io: NullIO -): + package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO +) -> None: package.add_dependency(Factory.create_dependency("foo", "1.0.0")) foo = Package( @@ -3163,7 +3410,7 @@ def test_solver_should_not_update_same_version_packages_if_installed_has_no_sour def test_solver_should_use_the_python_constraint_from_the_environment_if_available( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: set_package_python_versions(solver.provider, "~2.7 || ^3.5") package.add_dependency(Factory.create_dependency("A", "^1.0")) @@ -3190,7 +3437,7 @@ def test_solver_should_use_the_python_constraint_from_the_environment_if_availab def test_solver_should_resolve_all_versions_for_multiple_duplicate_dependencies( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.python_versions = "~2.7 || ^3.5" package.add_dependency( Factory.create_dependency( @@ -3238,7 +3485,7 @@ def test_solver_should_resolve_all_versions_for_multiple_duplicate_dependencies( def test_solver_should_not_raise_errors_for_irrelevant_python_constraints( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.python_versions = "^3.6" set_package_python_versions(solver.provider, "^3.6") package.add_dependency( @@ -3256,7 +3503,7 @@ def test_solver_should_not_raise_errors_for_irrelevant_python_constraints( def test_solver_can_resolve_transitive_extras( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency(Factory.create_dependency("requests", "^2.24.0")) package.add_dependency(Factory.create_dependency("PyOTA", "^2.1.0")) @@ -3294,7 +3541,7 @@ def test_solver_can_resolve_transitive_extras( def test_solver_can_resolve_for_packages_with_missing_extras( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.add_dependency( Factory.create_dependency( "django-anymail", {"version": "^6.0", "extras": ["postmark"]} @@ -3330,8 +3577,8 @@ def test_solver_can_resolve_for_packages_with_missing_extras( def test_solver_can_resolve_python_restricted_package_dependencies( - package: ProjectPackage, repo: Repository, pool: Pool, io: NullIO -): + package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO +) -> None: package.add_dependency( Factory.create_dependency("futures", {"version": "^3.3.0", "python": "~2.7"}) ) @@ -3362,7 +3609,7 @@ def test_solver_can_resolve_python_restricted_package_dependencies( def test_solver_should_not_raise_errors_for_irrelevant_transitive_python_constraints( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: package.python_versions = "~2.7 || ^3.5" set_package_python_versions(solver.provider, "~2.7 || ^3.5") package.add_dependency(Factory.create_dependency("virtualenv", "^20.4.3")) @@ -3414,10 +3661,10 @@ def test_solver_should_not_raise_errors_for_irrelevant_transitive_python_constra def test_solver_keeps_multiple_locked_dependencies_for_same_package( package: ProjectPackage, repo: Repository, - pool: Pool, + pool: RepositoryPool, io: NullIO, is_locked: bool, -): +) -> None: package.add_dependency( Factory.create_dependency("A", {"version": "~1.1", "python": "<3.7"}) ) @@ -3468,10 +3715,10 @@ def test_solver_keeps_multiple_locked_dependencies_for_same_package( def test_solver_does_not_update_ref_of_locked_vcs_package( package: ProjectPackage, repo: Repository, - pool: Pool, + pool: RepositoryPool, io: NullIO, is_locked: bool, -): +) -> None: locked_ref = "123456" latest_ref = "9cf87a285a2d3fbb0b9fa621997b3acc3631ed24" demo_locked = Package( @@ -3524,10 +3771,10 @@ def test_solver_does_not_update_ref_of_locked_vcs_package( def test_solver_does_not_fetch_locked_vcs_package_with_ref( package: ProjectPackage, repo: Repository, - pool: Pool, + pool: RepositoryPool, io: NullIO, mocker: MockerFixture, -): +) -> None: locked_ref = "123456" demo_locked = Package( "demo", @@ -3556,8 +3803,11 @@ def test_solver_does_not_fetch_locked_vcs_package_with_ref( def test_solver_direct_origin_dependency_with_extras_requested_by_other_package( - solver: Solver, repo: Repository, package: ProjectPackage -): + solver: Solver, + repo: Repository, + package: ProjectPackage, + fixture_dir: FixtureDirGetter, +) -> None: """ Another package requires the same dependency with extras that is required by the project as direct origin dependency without any extras. @@ -3572,14 +3822,7 @@ def test_solver_direct_origin_dependency_with_extras_requested_by_other_package( repo.add_package(pendulum) repo.add_package(cleo) - path = ( - Path(__file__).parent.parent - / "fixtures" - / "git" - / "github.com" - / "demo" - / "demo" - ).as_posix() + path = (fixture_dir("git") / "github.com" / "demo" / "demo").as_posix() # project requires path dependency of demo while demo-foo requires demo[foo] package.add_dependency(Factory.create_dependency("demo", {"path": path})) @@ -3609,7 +3852,7 @@ def test_solver_direct_origin_dependency_with_extras_requested_by_other_package( def test_solver_incompatible_dependency_with_and_without_extras( solver: Solver, repo: Repository, package: ProjectPackage -): +) -> None: """ The solver first encounters a requirement for google-auth and then later an incompatible requirement for google-auth[aiohttp]. @@ -3660,8 +3903,8 @@ def test_solver_incompatible_dependency_with_and_without_extras( def test_update_with_prerelease_and_no_solution( - package: ProjectPackage, repo: Repository, pool: Pool, io: NullIO -): + package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO +) -> None: # Locked and installed: cleo which depends on an old version of crashtest. cleo = get_package("cleo", "1.0.0a5") crashtest = get_package("crashtest", "0.3.0") @@ -3689,7 +3932,7 @@ def test_update_with_prerelease_and_no_solution( def test_solver_yanked_warning( package: ProjectPackage, - pool: Pool, + pool: RepositoryPool, repo: Repository, ) -> None: package.add_dependency(Factory.create_dependency("foo", "==1")) @@ -3728,8 +3971,12 @@ def test_solver_yanked_warning( @pytest.mark.parametrize("is_locked", [False, True]) def test_update_with_use_latest_vs_lock( - package: ProjectPackage, repo: Repository, pool: Pool, io: NullIO, is_locked: bool -): + package: ProjectPackage, + repo: Repository, + pool: RepositoryPool, + io: NullIO, + is_locked: bool, +) -> None: """ A1 depends on B2, A2 and A3 depend on B1. Same for C. B1 depends on A2/C2, B2 depends on A1/C1. diff --git a/tests/puzzle/test_transaction.py b/tests/puzzle/test_transaction.py index ae4093f5b12..67d3499183a 100644 --- a/tests/puzzle/test_transaction.py +++ b/tests/puzzle/test_transaction.py @@ -121,6 +121,31 @@ def test_it_should_remove_installed_packages_if_required(): ) +def test_it_should_not_remove_installed_packages_that_are_in_result(): + transaction = Transaction( + [], + [ + (Package("a", "1.0.0"), 1), + (Package("b", "2.0.0"), 2), + (Package("c", "3.0.0"), 0), + ], + installed_packages=[ + Package("a", "1.0.0"), + Package("b", "2.0.0"), + Package("c", "3.0.0"), + ], + ) + + check_operations( + transaction.calculate_operations(synchronize=True), + [ + {"job": "install", "package": Package("a", "1.0.0"), "skipped": True}, + {"job": "install", "package": Package("b", "2.0.0"), "skipped": True}, + {"job": "install", "package": Package("c", "3.0.0"), "skipped": True}, + ], + ) + + def test_it_should_update_installed_packages_if_sources_are_different(): transaction = Transaction( [Package("a", "1.0.0")], diff --git a/tests/pyproject/__init__.py b/tests/pyproject/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/pyproject/conftest.py b/tests/pyproject/conftest.py new file mode 100644 index 00000000000..82ff2198389 --- /dev/null +++ b/tests/pyproject/conftest.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + + +if TYPE_CHECKING: + from pathlib import Path + + +@pytest.fixture +def pyproject_toml(tmp_path: Path) -> Path: + path = tmp_path / "pyproject.toml" + with path.open(mode="w"): + pass + return path + + +@pytest.fixture +def build_system_section(pyproject_toml: Path) -> str: + content = """ +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" +""" + with pyproject_toml.open(mode="a") as f: + f.write(content) + return content + + +@pytest.fixture +def poetry_section(pyproject_toml: Path) -> str: + content = """ +[tool.poetry] +name = "poetry" + +[tool.poetry.dependencies] +python = "^3.5" +""" + with pyproject_toml.open(mode="a") as f: + f.write(content) + return content diff --git a/tests/pyproject/test_pyproject_toml.py b/tests/pyproject/test_pyproject_toml.py new file mode 100644 index 00000000000..4f85d91c18f --- /dev/null +++ b/tests/pyproject/test_pyproject_toml.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import uuid + +from typing import TYPE_CHECKING + +from poetry.pyproject.toml import PyProjectTOML + + +if TYPE_CHECKING: + from pathlib import Path + + +def test_pyproject_toml_reload(pyproject_toml: Path, poetry_section: str) -> None: + pyproject = PyProjectTOML(pyproject_toml) + name_original = pyproject.poetry_config["name"] + name_new = str(uuid.uuid4()) + + pyproject.poetry_config["name"] = name_new + assert isinstance(pyproject.poetry_config["name"], str) + assert pyproject.poetry_config["name"] == name_new + + pyproject.reload() + assert pyproject.poetry_config["name"] == name_original + + +def test_pyproject_toml_save( + pyproject_toml: Path, poetry_section: str, build_system_section: str +) -> None: + pyproject = PyProjectTOML(pyproject_toml) + + name = str(uuid.uuid4()) + build_backend = str(uuid.uuid4()) + build_requires = str(uuid.uuid4()) + + pyproject.poetry_config["name"] = name + pyproject.build_system.build_backend = build_backend + pyproject.build_system.requires.append(build_requires) + + pyproject.save() + + pyproject = PyProjectTOML(pyproject_toml) + + assert isinstance(pyproject.poetry_config["name"], str) + assert pyproject.poetry_config["name"] == name + assert pyproject.build_system.build_backend == build_backend + assert build_requires in pyproject.build_system.requires diff --git a/tests/pyproject/test_pyproject_toml_file.py b/tests/pyproject/test_pyproject_toml_file.py new file mode 100644 index 00000000000..1c7c02a1439 --- /dev/null +++ b/tests/pyproject/test_pyproject_toml_file.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from poetry.core.exceptions import PoetryCoreException + +from poetry.toml import TOMLFile + + +if TYPE_CHECKING: + from pathlib import Path + + +def test_pyproject_toml_file_invalid(pyproject_toml: Path) -> None: + with pyproject_toml.open(mode="a") as f: + f.write("<<<<<<<<<<<") + + with pytest.raises(PoetryCoreException) as excval: + _ = TOMLFile(pyproject_toml).read() + + assert f"Invalid TOML file {pyproject_toml.as_posix()}" in str(excval.value) + + +def test_pyproject_toml_file_getattr(tmp_path: Path, pyproject_toml: Path) -> None: + file = TOMLFile(pyproject_toml) + assert file.parent == tmp_path diff --git a/tests/repositories/fixtures/legacy/futures_partial_yank.html b/tests/repositories/fixtures/legacy/futures-partial-yank.html similarity index 100% rename from tests/repositories/fixtures/legacy/futures_partial_yank.html rename to tests/repositories/fixtures/legacy/futures-partial-yank.html diff --git a/tests/repositories/fixtures/pypi.org/dists/black-21.11b0-py3-none-any.whl b/tests/repositories/fixtures/pypi.org/dists/black-21.11b0-py3-none-any.whl index f0e3956e20f..50daa412ff7 100644 Binary files a/tests/repositories/fixtures/pypi.org/dists/black-21.11b0-py3-none-any.whl and b/tests/repositories/fixtures/pypi.org/dists/black-21.11b0-py3-none-any.whl differ diff --git a/tests/repositories/fixtures/pypi.org/dists/cleo-1.0.0a5-py3-none-any.whl b/tests/repositories/fixtures/pypi.org/dists/cleo-1.0.0a5-py3-none-any.whl new file mode 100644 index 00000000000..42ec0d1f0d1 Binary files /dev/null and b/tests/repositories/fixtures/pypi.org/dists/cleo-1.0.0a5-py3-none-any.whl differ diff --git a/tests/repositories/fixtures/pypi.org/dists/importlib_metadata-1.7.0-py2.py3-none-any.whl b/tests/repositories/fixtures/pypi.org/dists/importlib_metadata-1.7.0-py2.py3-none-any.whl new file mode 100644 index 00000000000..cfcb2db4d4a Binary files /dev/null and b/tests/repositories/fixtures/pypi.org/dists/importlib_metadata-1.7.0-py2.py3-none-any.whl differ diff --git a/tests/repositories/fixtures/pypi.org/dists/poetry_core-1.5.0-py3-none-any.whl b/tests/repositories/fixtures/pypi.org/dists/poetry_core-1.5.0-py3-none-any.whl new file mode 100644 index 00000000000..abb480016b6 Binary files /dev/null and b/tests/repositories/fixtures/pypi.org/dists/poetry_core-1.5.0-py3-none-any.whl differ diff --git a/tests/repositories/fixtures/pypi.org/dists/pytest-3.5.0-py2.py3-none-any.whl b/tests/repositories/fixtures/pypi.org/dists/pytest-3.5.0-py2.py3-none-any.whl new file mode 100644 index 00000000000..911f902125d Binary files /dev/null and b/tests/repositories/fixtures/pypi.org/dists/pytest-3.5.0-py2.py3-none-any.whl differ diff --git a/tests/repositories/fixtures/pypi.org/dists/pytest-3.5.1-py2.py3-none-any.whl b/tests/repositories/fixtures/pypi.org/dists/pytest-3.5.1-py2.py3-none-any.whl new file mode 100644 index 00000000000..4eb8ce84187 Binary files /dev/null and b/tests/repositories/fixtures/pypi.org/dists/pytest-3.5.1-py2.py3-none-any.whl differ diff --git a/tests/repositories/fixtures/pypi.org/dists/zipp-3.5.0-py3-none-any.whl b/tests/repositories/fixtures/pypi.org/dists/zipp-3.5.0-py3-none-any.whl new file mode 100644 index 00000000000..9b606195459 Binary files /dev/null and b/tests/repositories/fixtures/pypi.org/dists/zipp-3.5.0-py3-none-any.whl differ diff --git a/tests/repositories/fixtures/pypi.org/json/cleo/1.0.0a5.json b/tests/repositories/fixtures/pypi.org/json/cleo/1.0.0a5.json new file mode 100644 index 00000000000..570ed2f108b --- /dev/null +++ b/tests/repositories/fixtures/pypi.org/json/cleo/1.0.0a5.json @@ -0,0 +1,90 @@ +{ + "info": { + "author": "Sébastien Eustace", + "author_email": "sebastien@eustace.io", + "bugtrack_url": null, + "classifiers": [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9" + ], + "description": "", + "description_content_type": "text/markdown", + "docs_url": null, + "download_url": "", + "downloads": { + "last_day": -1, + "last_month": -1, + "last_week": -1 + }, + "home_page": "https://github.com/python-poetry/cleo", + "keywords": "cli,commands", + "license": "MIT", + "maintainer": "", + "maintainer_email": "", + "name": "cleo", + "package_url": "https://pypi.org/project/cleo/", + "platform": null, + "project_url": "https://pypi.org/project/cleo/", + "project_urls": { + "Homepage": "https://github.com/python-poetry/cleo" + }, + "release_url": "https://pypi.org/project/cleo/1.0.0a5/", + "requires_dist": [ + "crashtest (>=0.3.1,<0.4.0)", + "pylev (>=1.3.0,<2.0.0)" + ], + "requires_python": ">=3.7,<4.0", + "summary": "Cleo allows you to create beautiful and testable command-line interfaces.", + "version": "1.0.0a5", + "yanked": false, + "yanked_reason": null + }, + "last_serial": 14027784, + "urls": [ + { + "comment_text": "", + "digests": { + "md5": "4c78006d13e68c0c0796b954b1df0a3f", + "sha256": "ff53056589300976e960f75afb792dfbfc9c78dcbb5a448e207a17b643826360" + }, + "downloads": -1, + "filename": "cleo-1.0.0a5-py3-none-any.whl", + "has_sig": false, + "md5_digest": "4c78006d13e68c0c0796b954b1df0a3f", + "packagetype": "bdist_wheel", + "python_version": "py3", + "requires_python": ">=3.7,<4.0", + "size": 78701, + "upload_time": "2022-06-03T20:16:19", + "upload_time_iso_8601": "2022-06-03T20:16:19.386916Z", + "url": "https://files.pythonhosted.org/packages/45/0c/3825603bf62f360829b1eea29a43dadce30829067e288170b3bf738aafd0/cleo-1.0.0a5-py3-none-any.whl", + "yanked": false, + "yanked_reason": null + }, + { + "comment_text": "", + "digests": { + "md5": "90e60b2ad117d3534f92a4ce37f9f462", + "sha256": "097c9d0e0332fd53cc89fc11eb0a6ba0309e6a3933c08f7b38558555486925d3" + }, + "downloads": -1, + "filename": "cleo-1.0.0a5.tar.gz", + "has_sig": false, + "md5_digest": "90e60b2ad117d3534f92a4ce37f9f462", + "packagetype": "sdist", + "python_version": "source", + "requires_python": ">=3.7,<4.0", + "size": 61431, + "upload_time": "2022-06-03T20:16:21", + "upload_time_iso_8601": "2022-06-03T20:16:21.133890Z", + "url": "https://files.pythonhosted.org/packages/2f/16/1c1902b225756745f9860451a44a2e2a3c26ee91c72295e83c63df605ed1/cleo-1.0.0a5.tar.gz", + "yanked": false, + "yanked_reason": null + } + ], + "vulnerabilities": [] +} diff --git a/tests/repositories/fixtures/pypi.org/json/importlib-metadata.json b/tests/repositories/fixtures/pypi.org/json/importlib-metadata.json new file mode 100644 index 00000000000..2796a50aadb --- /dev/null +++ b/tests/repositories/fixtures/pypi.org/json/importlib-metadata.json @@ -0,0 +1,27 @@ +{ + "name": "importlib-metadata", + "files": [ + { + "hashes": { + "md5": "8ae1f31228e29443c08e07501a99d1b8", + "sha256": "dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070" + }, + "filename": "importlib_metadata-1.7.0-py2.py3-none-any.whl", + "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7", + "url": "https://files.pythonhosted.org/packages/8e/58/cdea07eb51fc2b906db0968a94700866fc46249bdc75cac23f9d13168929/importlib_metadata-1.7.0-py2.py3-none-any.whl" + }, + { + "hashes": { + "md5": "4505ea85600cca1e693a4f8f5dd27ba8", + "sha256": "90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83" + }, + "filename": "importlib_metadata-1.7.0.tar.gz", + "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7", + "url": "https://files.pythonhosted.org/packages/e2/ae/0b037584024c1557e537d25482c306cf6327b5a09b6c4b893579292c1c38/importlib_metadata-1.7.0.tar.gz" + } + ], + "meta": { + "api-version": "1.0", + "_last-serial": 3879671 + } +} diff --git a/tests/repositories/fixtures/pypi.org/json/importlib-metadata/1.7.0.json b/tests/repositories/fixtures/pypi.org/json/importlib-metadata/1.7.0.json new file mode 100644 index 00000000000..595674750f8 --- /dev/null +++ b/tests/repositories/fixtures/pypi.org/json/importlib-metadata/1.7.0.json @@ -0,0 +1,140 @@ +{ + "info": { + "author": "Barry Warsaw", + "author_email": "barry@python.org", + "bugtrack_url": null, + "classifiers": [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 3", + "Topic :: Software Development :: Libraries" + ], + "description": "", + "description_content_type": "", + "docs_url": null, + "download_url": "", + "downloads": { + "last_day": -1, + "last_month": -1, + "last_week": -1 + }, + "home_page": "http://importlib-metadata.readthedocs.io/", + "keywords": "", + "license": "Apache Software License", + "maintainer": "", + "maintainer_email": "", + "name": "importlib-metadata", + "package_url": "https://pypi.org/project/importlib-metadata/", + "platform": "", + "project_url": "https://pypi.org/project/importlib-metadata/", + "project_urls": { + "Homepage": "http://importlib-metadata.readthedocs.io/" + }, + "release_url": "https://pypi.org/project/importlib-metadata/1.7.0/", + "requires_dist": [ + "zipp (>=0.5)", + "pathlib2 ; python_version < \"3\"", + "contextlib2 ; python_version < \"3\"", + "configparser (>=3.5) ; python_version < \"3\"", + "sphinx ; extra == 'docs'", + "rst.linker ; extra == 'docs'", + "packaging ; extra == 'testing'", + "pep517 ; extra == 'testing'", + "importlib-resources (>=1.3) ; (python_version < \"3.9\") and extra == 'testing'" + ], + "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7", + "summary": "Read metadata from Python packages", + "version": "1.7.0", + "yanked": false, + "yanked_reason": null + }, + "last_serial": 10821863, + "releases": { + "1.7.0": [ + { + "comment_text": "", + "digests": { + "md5": "8ae1f31228e29443c08e07501a99d1b8", + "sha256": "dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070" + }, + "downloads": -1, + "filename": "importlib_metadata-1.7.0-py2.py3-none-any.whl", + "has_sig": false, + "md5_digest": "8ae1f31228e29443c08e07501a99d1b8", + "packagetype": "bdist_wheel", + "python_version": "py2.py3", + "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7", + "size": 31809, + "upload_time": "2020-06-26T21:38:16", + "upload_time_iso_8601": "2020-06-26T21:38:16.079439Z", + "url": "https://files.pythonhosted.org/packages/8e/58/cdea07eb51fc2b906db0968a94700866fc46249bdc75cac23f9d13168929/importlib_metadata-1.7.0-py2.py3-none-any.whl", + "yanked": false, + "yanked_reason": null + }, + { + "comment_text": "", + "digests": { + "md5": "4505ea85600cca1e693a4f8f5dd27ba8", + "sha256": "90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83" + }, + "downloads": -1, + "filename": "importlib_metadata-1.7.0.tar.gz", + "has_sig": false, + "md5_digest": "4505ea85600cca1e693a4f8f5dd27ba8", + "packagetype": "sdist", + "python_version": "source", + "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7", + "size": 29233, + "upload_time": "2020-06-26T21:38:17", + "upload_time_iso_8601": "2020-06-26T21:38:17.338581Z", + "url": "https://files.pythonhosted.org/packages/e2/ae/0b037584024c1557e537d25482c306cf6327b5a09b6c4b893579292c1c38/importlib_metadata-1.7.0.tar.gz", + "yanked": false, + "yanked_reason": null + } + ] + }, + "urls": [ + { + "comment_text": "", + "digests": { + "md5": "8ae1f31228e29443c08e07501a99d1b8", + "sha256": "dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070" + }, + "downloads": -1, + "filename": "importlib_metadata-1.7.0-py2.py3-none-any.whl", + "has_sig": false, + "md5_digest": "8ae1f31228e29443c08e07501a99d1b8", + "packagetype": "bdist_wheel", + "python_version": "py2.py3", + "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7", + "size": 31809, + "upload_time": "2020-06-26T21:38:16", + "upload_time_iso_8601": "2020-06-26T21:38:16.079439Z", + "url": "https://files.pythonhosted.org/packages/8e/58/cdea07eb51fc2b906db0968a94700866fc46249bdc75cac23f9d13168929/importlib_metadata-1.7.0-py2.py3-none-any.whl", + "yanked": false, + "yanked_reason": null + }, + { + "comment_text": "", + "digests": { + "md5": "4505ea85600cca1e693a4f8f5dd27ba8", + "sha256": "90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83" + }, + "downloads": -1, + "filename": "importlib_metadata-1.7.0.tar.gz", + "has_sig": false, + "md5_digest": "4505ea85600cca1e693a4f8f5dd27ba8", + "packagetype": "sdist", + "python_version": "source", + "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7", + "size": 29233, + "upload_time": "2020-06-26T21:38:17", + "upload_time_iso_8601": "2020-06-26T21:38:17.338581Z", + "url": "https://files.pythonhosted.org/packages/e2/ae/0b037584024c1557e537d25482c306cf6327b5a09b6c4b893579292c1c38/importlib_metadata-1.7.0.tar.gz", + "yanked": false, + "yanked_reason": null + } + ] +} diff --git a/tests/repositories/fixtures/pypi.org/json/poetry-core.json b/tests/repositories/fixtures/pypi.org/json/poetry-core.json new file mode 100644 index 00000000000..38b248a3c1d --- /dev/null +++ b/tests/repositories/fixtures/pypi.org/json/poetry-core.json @@ -0,0 +1,27 @@ +{ + "files": [ + { + "filename": "poetry_core-1.5.0-py3-none-any.whl", + "hashes": { + "sha256": "e216b70f013c47b82a72540d34347632c5bfe59fd54f5fe5d51f6a68b19aaf84" + }, + "requires-python": ">=3.7,<4.0", + "url": "https://files.pythonhosted.org/packages/2d/99/6b0c5fe90e247b2b7b96a27cdf39ee59a02aab3c01d7243fc0c63cd7fb73/poetry_core-1.5.0-py3-none-any.whl", + "yanked": false + }, + { + "filename": "poetry_core-1.5.0.tar.gz", + "hashes": { + "sha256": "253521bb7104e1df81f64d7b49ea1825057c91fa156d7d0bd752fefdad6f8c7a" + }, + "requires-python": ">=3.7,<4.0", + "url": "https://files.pythonhosted.org/packages/57/bb/2435fef60bb01f6c0891d9482c7053b50e90639f0f74d7658e99bdd4da69/poetry_core-1.5.0.tar.gz", + "yanked": false + } + ], + "meta": { + "_last-serial": 16600250, + "api-version": "1.0" + }, + "name": "poetry-core" +} diff --git a/tests/repositories/fixtures/pypi.org/json/poetry-core/1.5.0.json b/tests/repositories/fixtures/pypi.org/json/poetry-core/1.5.0.json new file mode 100644 index 00000000000..9cfc450c625 --- /dev/null +++ b/tests/repositories/fixtures/pypi.org/json/poetry-core/1.5.0.json @@ -0,0 +1,96 @@ +{ + "info": { + "author": "Sébastien Eustace", + "author_email": "sebastien@eustace.io", + "bugtrack_url": null, + "classifiers": [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Python Modules" + ], + "description": "", + "description_content_type": "text/markdown", + "docs_url": null, + "download_url": "", + "downloads": { + "last_day": -1, + "last_month": -1, + "last_week": -1 + }, + "home_page": "https://github.com/python-poetry/poetry-core", + "keywords": "packaging,dependency,poetry", + "license": "MIT", + "maintainer": "", + "maintainer_email": "", + "name": "poetry-core", + "package_url": "https://pypi.org/project/poetry-core/", + "platform": null, + "project_url": "https://pypi.org/project/poetry-core/", + "project_urls": { + "Bug Tracker": "https://github.com/python-poetry/poetry/issues", + "Homepage": "https://github.com/python-poetry/poetry-core", + "Repository": "https://github.com/python-poetry/poetry-core" + }, + "release_url": "https://pypi.org/project/poetry-core/1.5.0/", + "requires_dist": [ + "importlib-metadata (>=1.7.0) ; python_version < \"3.8\"" + ], + "requires_python": ">=3.7,<4.0", + "summary": "Poetry PEP 517 Build Backend", + "version": "1.5.0", + "yanked": false, + "yanked_reason": null + }, + "last_serial": 16600250, + "urls": [ + { + "comment_text": "", + "digests": { + "blake2b_256": "2d996b0c5fe90e247b2b7b96a27cdf39ee59a02aab3c01d7243fc0c63cd7fb73", + "md5": "be7589b4902793e66d7d979bd8581591", + "sha256": "e216b70f013c47b82a72540d34347632c5bfe59fd54f5fe5d51f6a68b19aaf84" + }, + "downloads": -1, + "filename": "poetry_core-1.5.0-py3-none-any.whl", + "has_sig": false, + "md5_digest": "be7589b4902793e66d7d979bd8581591", + "packagetype": "bdist_wheel", + "python_version": "py3", + "requires_python": ">=3.7,<4.0", + "size": 464992, + "upload_time": "2023-01-28T10:52:52", + "upload_time_iso_8601": "2023-01-28T10:52:52.445537Z", + "url": "https://files.pythonhosted.org/packages/2d/99/6b0c5fe90e247b2b7b96a27cdf39ee59a02aab3c01d7243fc0c63cd7fb73/poetry_core-1.5.0-py3-none-any.whl", + "yanked": false, + "yanked_reason": null + }, + { + "comment_text": "", + "digests": { + "blake2b_256": "57bb2435fef60bb01f6c0891d9482c7053b50e90639f0f74d7658e99bdd4da69", + "md5": "481671a4895af7cdda4944eab67f3843", + "sha256": "253521bb7104e1df81f64d7b49ea1825057c91fa156d7d0bd752fefdad6f8c7a" + }, + "downloads": -1, + "filename": "poetry_core-1.5.0.tar.gz", + "has_sig": false, + "md5_digest": "481671a4895af7cdda4944eab67f3843", + "packagetype": "sdist", + "python_version": "source", + "requires_python": ">=3.7,<4.0", + "size": 448812, + "upload_time": "2023-01-28T10:52:53", + "upload_time_iso_8601": "2023-01-28T10:52:53.916268Z", + "url": "https://files.pythonhosted.org/packages/57/bb/2435fef60bb01f6c0891d9482c7053b50e90639f0f74d7658e99bdd4da69/poetry_core-1.5.0.tar.gz", + "yanked": false, + "yanked_reason": null + } + ], + "vulnerabilities": [] +} diff --git a/tests/repositories/fixtures/pypi.org/json/setuptools.json b/tests/repositories/fixtures/pypi.org/json/setuptools.json index 6703a5efc9b..24858bd11b8 100644 --- a/tests/repositories/fixtures/pypi.org/json/setuptools.json +++ b/tests/repositories/fixtures/pypi.org/json/setuptools.json @@ -16,10 +16,36 @@ "md5": "dd4e3fa83a21bf7bf9c51026dc8a4e59", "sha256": "f7cddbb5f5c640311eb00eab6e849f7701fa70bf6a183fc8a2c33dd1d1672fb2" } + }, + { + "filename": "setuptools-67.6.1-py3-none-any.whl", + "hashes": { + "sha256": "e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078" + }, + "requires-python": ">=3.7", + "size": 1089263, + "upload-time": "2023-03-28T13:45:43.525946Z", + "url": "https://files.pythonhosted.org/packages/0b/fc/8781442def77b0aa22f63f266d4dadd486ebc0c5371d6290caf4320da4b7/setuptools-67.6.1-py3-none-any.whl", + "yanked": false + }, + { + "filename": "setuptools-67.6.1.tar.gz", + "hashes": { + "sha256": "257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a" + }, + "requires-python": ">=3.7", + "size": 2486256, + "upload-time": "2023-03-28T13:45:45.967259Z", + "url": "https://files.pythonhosted.org/packages/cb/46/22ec35f286a77e6b94adf81b4f0d59f402ed981d4251df0ba7b992299146/setuptools-67.6.1.tar.gz", + "yanked": false } ], "meta": { "api-version": "1.0", "_last-serial": 3879671 - } + }, + "versions": [ + "39.2.0", + "67.6.1" + ] } diff --git a/tests/repositories/fixtures/pypi.org/json/setuptools/67.6.1.json b/tests/repositories/fixtures/pypi.org/json/setuptools/67.6.1.json new file mode 100644 index 00000000000..7a2c4192756 --- /dev/null +++ b/tests/repositories/fixtures/pypi.org/json/setuptools/67.6.1.json @@ -0,0 +1,140 @@ +{ + "info": { + "author": "Python Packaging Authority", + "author_email": "distutils-sig@python.org", + "bugtrack_url": null, + "classifiers": [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: System :: Archiving :: Packaging", + "Topic :: System :: Systems Administration", + "Topic :: Utilities" + ], + "description": ".. image:: https://img.shields.io/pypi/v/setuptools.svg\n :target: https://pypi.org/project/setuptools\n\n.. image:: https://img.shields.io/pypi/pyversions/setuptools.svg\n\n.. image:: https://github.com/pypa/setuptools/workflows/tests/badge.svg\n :target: https://github.com/pypa/setuptools/actions?query=workflow%3A%22tests%22\n :alt: tests\n\n.. image:: https://img.shields.io/badge/code%20style-black-000000.svg\n :target: https://github.com/psf/black\n :alt: Code style: Black\n\n.. image:: https://img.shields.io/readthedocs/setuptools/latest.svg\n :target: https://setuptools.pypa.io\n\n.. image:: https://img.shields.io/badge/skeleton-2023-informational\n :target: https://blog.jaraco.com/skeleton\n\n.. image:: https://img.shields.io/codecov/c/github/pypa/setuptools/master.svg?logo=codecov&logoColor=white\n :target: https://codecov.io/gh/pypa/setuptools\n\n.. image:: https://tidelift.com/badges/github/pypa/setuptools?style=flat\n :target: https://tidelift.com/subscription/pkg/pypi-setuptools?utm_source=pypi-setuptools&utm_medium=readme\n\n.. image:: https://img.shields.io/discord/803025117553754132\n :target: https://discord.com/channels/803025117553754132/815945031150993468\n :alt: Discord\n\nSee the `Installation Instructions\n`_ in the Python Packaging\nUser's Guide for instructions on installing, upgrading, and uninstalling\nSetuptools.\n\nQuestions and comments should be directed to `GitHub Discussions\n`_.\nBug reports and especially tested patches may be\nsubmitted directly to the `bug tracker\n`_.\n\n\nCode of Conduct\n===============\n\nEveryone interacting in the setuptools project's codebases, issue trackers,\nchat rooms, and fora is expected to follow the\n`PSF Code of Conduct `_.\n\n\nFor Enterprise\n==============\n\nAvailable as part of the Tidelift Subscription.\n\nSetuptools and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use.\n\n`Learn more `_.\n\n\nSecurity Contact\n================\n\nTo report a security vulnerability, please use the\n`Tidelift security contact `_.\nTidelift will coordinate the fix and disclosure.\n", + "description_content_type": "", + "docs_url": null, + "download_url": "", + "downloads": { + "last_day": -1, + "last_month": -1, + "last_week": -1 + }, + "home_page": "https://github.com/pypa/setuptools", + "keywords": "CPAN PyPI distutils eggs package management", + "license": "", + "maintainer": "", + "maintainer_email": "", + "name": "setuptools", + "package_url": "https://pypi.org/project/setuptools/", + "platform": null, + "project_url": "https://pypi.org/project/setuptools/", + "project_urls": { + "Changelog": "https://setuptools.pypa.io/en/stable/history.html", + "Documentation": "https://setuptools.pypa.io/", + "Homepage": "https://github.com/pypa/setuptools" + }, + "release_url": "https://pypi.org/project/setuptools/67.6.1/", + "requires_dist": [ + "sphinx (>=3.5) ; extra == 'docs'", + "jaraco.packaging (>=9) ; extra == 'docs'", + "rst.linker (>=1.9) ; extra == 'docs'", + "furo ; extra == 'docs'", + "sphinx-lint ; extra == 'docs'", + "jaraco.tidelift (>=1.4) ; extra == 'docs'", + "pygments-github-lexers (==0.0.5) ; extra == 'docs'", + "sphinx-favicon ; extra == 'docs'", + "sphinx-inline-tabs ; extra == 'docs'", + "sphinx-reredirects ; extra == 'docs'", + "sphinxcontrib-towncrier ; extra == 'docs'", + "sphinx-notfound-page (==0.8.3) ; extra == 'docs'", + "sphinx-hoverxref (<2) ; extra == 'docs'", + "pytest (>=6) ; extra == 'testing'", + "pytest-checkdocs (>=2.4) ; extra == 'testing'", + "flake8 (<5) ; extra == 'testing'", + "pytest-enabler (>=1.3) ; extra == 'testing'", + "pytest-perf ; extra == 'testing'", + "flake8-2020 ; extra == 'testing'", + "virtualenv (>=13.0.0) ; extra == 'testing'", + "wheel ; extra == 'testing'", + "pip (>=19.1) ; extra == 'testing'", + "jaraco.envs (>=2.2) ; extra == 'testing'", + "pytest-xdist ; extra == 'testing'", + "jaraco.path (>=3.2.0) ; extra == 'testing'", + "build[virtualenv] ; extra == 'testing'", + "filelock (>=3.4.0) ; extra == 'testing'", + "pip-run (>=8.8) ; extra == 'testing'", + "ini2toml[lite] (>=0.9) ; extra == 'testing'", + "tomli-w (>=1.0.0) ; extra == 'testing'", + "pytest-timeout ; extra == 'testing'", + "pytest ; extra == 'testing-integration'", + "pytest-xdist ; extra == 'testing-integration'", + "pytest-enabler ; extra == 'testing-integration'", + "virtualenv (>=13.0.0) ; extra == 'testing-integration'", + "tomli ; extra == 'testing-integration'", + "wheel ; extra == 'testing-integration'", + "jaraco.path (>=3.2.0) ; extra == 'testing-integration'", + "jaraco.envs (>=2.2) ; extra == 'testing-integration'", + "build[virtualenv] ; extra == 'testing-integration'", + "filelock (>=3.4.0) ; extra == 'testing-integration'", + "pytest-black (>=0.3.7) ; (platform_python_implementation != \"PyPy\") and extra == 'testing'", + "pytest-cov ; (platform_python_implementation != \"PyPy\") and extra == 'testing'", + "pytest-mypy (>=0.9.1) ; (platform_python_implementation != \"PyPy\") and extra == 'testing'", + "pytest-flake8 ; (python_version < \"3.12\") and extra == 'testing'" + ], + "requires_python": ">=3.7", + "summary": "Easily download, build, install, upgrade, and uninstall Python packages", + "version": "67.6.1", + "yanked": false, + "yanked_reason": null + }, + "last_serial": 17478645, + "urls": [ + { + "comment_text": "", + "digests": { + "blake2b_256": "0bfc8781442def77b0aa22f63f266d4dadd486ebc0c5371d6290caf4320da4b7", + "md5": "3b5b846e000da033d54eeaaf7915126e", + "sha256": "e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078" + }, + "downloads": -1, + "filename": "setuptools-67.6.1-py3-none-any.whl", + "has_sig": false, + "md5_digest": "3b5b846e000da033d54eeaaf7915126e", + "packagetype": "bdist_wheel", + "python_version": "py3", + "requires_python": ">=3.7", + "size": 1089263, + "upload_time": "2023-03-28T13:45:43", + "upload_time_iso_8601": "2023-03-28T13:45:43.525946Z", + "url": "https://files.pythonhosted.org/packages/0b/fc/8781442def77b0aa22f63f266d4dadd486ebc0c5371d6290caf4320da4b7/setuptools-67.6.1-py3-none-any.whl", + "yanked": false, + "yanked_reason": null + }, + { + "comment_text": "", + "digests": { + "blake2b_256": "cb4622ec35f286a77e6b94adf81b4f0d59f402ed981d4251df0ba7b992299146", + "md5": "a661b7cdf4cf1e914f866506c1022dee", + "sha256": "257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a" + }, + "downloads": -1, + "filename": "setuptools-67.6.1.tar.gz", + "has_sig": false, + "md5_digest": "a661b7cdf4cf1e914f866506c1022dee", + "packagetype": "sdist", + "python_version": "source", + "requires_python": ">=3.7", + "size": 2486256, + "upload_time": "2023-03-28T13:45:45", + "upload_time_iso_8601": "2023-03-28T13:45:45.967259Z", + "url": "https://files.pythonhosted.org/packages/cb/46/22ec35f286a77e6b94adf81b4f0d59f402ed981d4251df0ba7b992299146/setuptools-67.6.1.tar.gz", + "yanked": false, + "yanked_reason": null + } + ], + "vulnerabilities": [] +} diff --git a/tests/repositories/fixtures/pypi.org/json/wheel.json b/tests/repositories/fixtures/pypi.org/json/wheel.json new file mode 100644 index 00000000000..6a6d6b8df7b --- /dev/null +++ b/tests/repositories/fixtures/pypi.org/json/wheel.json @@ -0,0 +1,34 @@ +{ + "files": [ + { + "filename": "wheel-0.40.0-py3-none-any.whl", + "hashes": { + "sha256": "d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247" + }, + "requires-python": ">=3.7", + "size": 64545, + "upload-time": "2023-03-14T15:10:00.828550Z", + "url": "https://files.pythonhosted.org/packages/61/86/cc8d1ff2ca31a312a25a708c891cf9facbad4eae493b3872638db6785eb5/wheel-0.40.0-py3-none-any.whl", + "yanked": false + }, + { + "filename": "wheel-0.40.0.tar.gz", + "hashes": { + "sha256": "cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873" + }, + "requires-python": ">=3.7", + "size": 96226, + "upload-time": "2023-03-14T15:10:02.873691Z", + "url": "https://files.pythonhosted.org/packages/fc/ef/0335f7217dd1e8096a9e8383e1d472aa14717878ffe07c4772e68b6e8735/wheel-0.40.0.tar.gz", + "yanked": false + } + ], + "meta": { + "_last-serial": 17289142, + "api-version": "1.1" + }, + "name": "wheel", + "versions": [ + "0.40.0" + ] +} diff --git a/tests/repositories/fixtures/pypi.org/json/wheel/0.40.0.json b/tests/repositories/fixtures/pypi.org/json/wheel/0.40.0.json new file mode 100644 index 00000000000..6043ad6cd68 --- /dev/null +++ b/tests/repositories/fixtures/pypi.org/json/wheel/0.40.0.json @@ -0,0 +1,98 @@ +{ + "info": { + "author": "", + "author_email": "Daniel Holth ", + "bugtrack_url": null, + "classifiers": [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Topic :: System :: Archiving :: Packaging" + ], + "description": "wheel\n=====\n\nThis library is the reference implementation of the Python wheel packaging\nstandard, as defined in `PEP 427`_.\n\nIt has two different roles:\n\n#. A setuptools_ extension for building wheels that provides the\n ``bdist_wheel`` setuptools command\n#. A command line tool for working with wheel files\n\nIt should be noted that wheel is **not** intended to be used as a library, and\nas such there is no stable, public API.\n\n.. _PEP 427: https://www.python.org/dev/peps/pep-0427/\n.. _setuptools: https://pypi.org/project/setuptools/\n\nDocumentation\n-------------\n\nThe documentation_ can be found on Read The Docs.\n\n.. _documentation: https://wheel.readthedocs.io/\n\nCode of Conduct\n---------------\n\nEveryone interacting in the wheel project's codebases, issue trackers, chat\nrooms, and mailing lists is expected to follow the `PSF Code of Conduct`_.\n\n.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md\n\n", + "description_content_type": "text/x-rst", + "docs_url": null, + "download_url": "", + "downloads": { + "last_day": -1, + "last_month": -1, + "last_week": -1 + }, + "home_page": "", + "keywords": "wheel,packaging", + "license": "", + "maintainer": "", + "maintainer_email": "Alex Grönholm ", + "name": "wheel", + "package_url": "https://pypi.org/project/wheel/", + "platform": null, + "project_url": "https://pypi.org/project/wheel/", + "project_urls": { + "Changelog": "https://wheel.readthedocs.io/en/stable/news.html", + "Documentation": "https://wheel.readthedocs.io/", + "Issue Tracker": "https://github.com/pypa/wheel/issues" + }, + "release_url": "https://pypi.org/project/wheel/0.40.0/", + "requires_dist": [ + "pytest >= 6.0.0 ; extra == \"test\"" + ], + "requires_python": ">=3.7", + "summary": "A built-package format for Python", + "version": "0.40.0", + "yanked": false, + "yanked_reason": null + }, + "last_serial": 17289142, + "urls": [ + { + "comment_text": "", + "digests": { + "blake2b_256": "6186cc8d1ff2ca31a312a25a708c891cf9facbad4eae493b3872638db6785eb5", + "md5": "517d39f133bd7b1ff17caf09784b7543", + "sha256": "d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247" + }, + "downloads": -1, + "filename": "wheel-0.40.0-py3-none-any.whl", + "has_sig": false, + "md5_digest": "517d39f133bd7b1ff17caf09784b7543", + "packagetype": "bdist_wheel", + "python_version": "py3", + "requires_python": ">=3.7", + "size": 64545, + "upload_time": "2023-03-14T15:10:00", + "upload_time_iso_8601": "2023-03-14T15:10:00.828550Z", + "url": "https://files.pythonhosted.org/packages/61/86/cc8d1ff2ca31a312a25a708c891cf9facbad4eae493b3872638db6785eb5/wheel-0.40.0-py3-none-any.whl", + "yanked": false, + "yanked_reason": null + }, + { + "comment_text": "", + "digests": { + "blake2b_256": "fcef0335f7217dd1e8096a9e8383e1d472aa14717878ffe07c4772e68b6e8735", + "md5": "ec5004c46d1905da98bb5bc1a10ddd21", + "sha256": "cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873" + }, + "downloads": -1, + "filename": "wheel-0.40.0.tar.gz", + "has_sig": false, + "md5_digest": "ec5004c46d1905da98bb5bc1a10ddd21", + "packagetype": "sdist", + "python_version": "source", + "requires_python": ">=3.7", + "size": 96226, + "upload_time": "2023-03-14T15:10:02", + "upload_time_iso_8601": "2023-03-14T15:10:02.873691Z", + "url": "https://files.pythonhosted.org/packages/fc/ef/0335f7217dd1e8096a9e8383e1d472aa14717878ffe07c4772e68b6e8735/wheel-0.40.0.tar.gz", + "yanked": false, + "yanked_reason": null + } + ], + "vulnerabilities": [] +} diff --git a/tests/repositories/fixtures/pypi.org/json/zipp.json b/tests/repositories/fixtures/pypi.org/json/zipp.json new file mode 100644 index 00000000000..085679a0c6e --- /dev/null +++ b/tests/repositories/fixtures/pypi.org/json/zipp.json @@ -0,0 +1,27 @@ +{ + "name": "zipp", + "files": [ + { + "hashes": { + "md5": "0ec47fbf522751f6c5fa904cb33f1f59", + "sha256": "957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3" + }, + "filename": "zipp-3.5.0-py3-none-any.whl", + "requires_python": ">=3.6", + "url": "https://files.pythonhosted.org/packages/92/d9/89f433969fb8dc5b9cbdd4b4deb587720ec1aeb59a020cf15002b9593eef/zipp-3.5.0-py3-none-any.whl" + }, + { + "hashes": { + "md5": "617efbf3edb707c57008ec00f408972f", + "sha256": "f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4" + }, + "filename": "zipp-3.5.0.tar.gz", + "requires_python": ">=3.6", + "url": "https://files.pythonhosted.org/packages/3a/9f/1d4b62cbe8d222539a84089eeab603d8e45ee1f897803a0ae0860400d6e7/zipp-3.5.0.tar.gz" + } + ], + "meta": { + "api-version": "1.0", + "_last-serial": 3879671 + } +} diff --git a/tests/repositories/fixtures/pypi.org/json/zipp/3.5.0.json b/tests/repositories/fixtures/pypi.org/json/zipp/3.5.0.json new file mode 100644 index 00000000000..356bd2b3483 --- /dev/null +++ b/tests/repositories/fixtures/pypi.org/json/zipp/3.5.0.json @@ -0,0 +1,142 @@ +{ + "info": { + "author": "Jason R. Coombs", + "author_email": "jaraco@jaraco.com", + "bugtrack_url": null, + "classifiers": [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only" + ], + "description": "", + "description_content_type": "", + "docs_url": null, + "download_url": "", + "downloads": { + "last_day": -1, + "last_month": -1, + "last_week": -1 + }, + "home_page": "https://github.com/jaraco/zipp", + "keywords": "", + "license": "", + "maintainer": "", + "maintainer_email": "", + "name": "zipp", + "package_url": "https://pypi.org/project/zipp/", + "platform": "", + "project_url": "https://pypi.org/project/zipp/", + "project_urls": { + "Homepage": "https://github.com/jaraco/zipp" + }, + "release_url": "https://pypi.org/project/zipp/3.5.0/", + "requires_dist": [ + "sphinx ; extra == 'docs'", + "jaraco.packaging (>=8.2) ; extra == 'docs'", + "rst.linker (>=1.9) ; extra == 'docs'", + "pytest (>=4.6) ; extra == 'testing'", + "pytest-checkdocs (>=2.4) ; extra == 'testing'", + "pytest-flake8 ; extra == 'testing'", + "pytest-cov ; extra == 'testing'", + "pytest-enabler (>=1.0.1) ; extra == 'testing'", + "jaraco.itertools ; extra == 'testing'", + "func-timeout ; extra == 'testing'", + "pytest-black (>=0.3.7) ; (platform_python_implementation != \"PyPy\" and python_version < \"3.10\") and extra == 'testing'", + "pytest-mypy ; (platform_python_implementation != \"PyPy\" and python_version < \"3.10\") and extra == 'testing'" + ], + "requires_python": ">=3.6", + "summary": "Backport of pathlib-compatible object wrapper for zip files", + "version": "3.5.0", + "yanked": false, + "yanked_reason": null + }, + "last_serial": 10811847, + "releases": { + "3.5.0": [ + { + "comment_text": "", + "digests": { + "md5": "0ec47fbf522751f6c5fa904cb33f1f59", + "sha256": "957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3" + }, + "downloads": -1, + "filename": "zipp-3.5.0-py3-none-any.whl", + "has_sig": false, + "md5_digest": "0ec47fbf522751f6c5fa904cb33f1f59", + "packagetype": "bdist_wheel", + "python_version": "py3", + "requires_python": ">=3.6", + "size": 5700, + "upload_time": "2021-07-02T23:51:45", + "upload_time_iso_8601": "2021-07-02T23:51:45.759726Z", + "url": "https://files.pythonhosted.org/packages/92/d9/89f433969fb8dc5b9cbdd4b4deb587720ec1aeb59a020cf15002b9593eef/zipp-3.5.0-py3-none-any.whl", + "yanked": false, + "yanked_reason": null + }, + { + "comment_text": "", + "digests": { + "md5": "617efbf3edb707c57008ec00f408972f", + "sha256": "f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4" + }, + "downloads": -1, + "filename": "zipp-3.5.0.tar.gz", + "has_sig": false, + "md5_digest": "617efbf3edb707c57008ec00f408972f", + "packagetype": "sdist", + "python_version": "source", + "requires_python": ">=3.6", + "size": 13270, + "upload_time": "2021-07-02T23:51:47", + "upload_time_iso_8601": "2021-07-02T23:51:47.004396Z", + "url": "https://files.pythonhosted.org/packages/3a/9f/1d4b62cbe8d222539a84089eeab603d8e45ee1f897803a0ae0860400d6e7/zipp-3.5.0.tar.gz", + "yanked": false, + "yanked_reason": null + } + ] + }, + "urls": [ + { + "comment_text": "", + "digests": { + "md5": "0ec47fbf522751f6c5fa904cb33f1f59", + "sha256": "957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3" + }, + "downloads": -1, + "filename": "zipp-3.5.0-py3-none-any.whl", + "has_sig": false, + "md5_digest": "0ec47fbf522751f6c5fa904cb33f1f59", + "packagetype": "bdist_wheel", + "python_version": "py3", + "requires_python": ">=3.6", + "size": 5700, + "upload_time": "2021-07-02T23:51:45", + "upload_time_iso_8601": "2021-07-02T23:51:45.759726Z", + "url": "https://files.pythonhosted.org/packages/92/d9/89f433969fb8dc5b9cbdd4b4deb587720ec1aeb59a020cf15002b9593eef/zipp-3.5.0-py3-none-any.whl", + "yanked": false, + "yanked_reason": null + }, + { + "comment_text": "", + "digests": { + "md5": "617efbf3edb707c57008ec00f408972f", + "sha256": "f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4" + }, + "downloads": -1, + "filename": "zipp-3.5.0.tar.gz", + "has_sig": false, + "md5_digest": "617efbf3edb707c57008ec00f408972f", + "packagetype": "sdist", + "python_version": "source", + "requires_python": ">=3.6", + "size": 13270, + "upload_time": "2021-07-02T23:51:47", + "upload_time_iso_8601": "2021-07-02T23:51:47.004396Z", + "url": "https://files.pythonhosted.org/packages/3a/9f/1d4b62cbe8d222539a84089eeab603d8e45ee1f897803a0ae0860400d6e7/zipp-3.5.0.tar.gz", + "yanked": false, + "yanked_reason": null + } + ] +} diff --git a/tests/repositories/test_installed_repository.py b/tests/repositories/test_installed_repository.py index 87dfe183c8a..628852708a1 100644 --- a/tests/repositories/test_installed_repository.py +++ b/tests/repositories/test_installed_repository.py @@ -22,14 +22,12 @@ SITE_PURELIB = ENV_DIR / "lib" / "python3.7" / "site-packages" SITE_PLATLIB = ENV_DIR / "lib64" / "python3.7" / "site-packages" SRC = ENV_DIR / "src" -VENDOR_DIR = ENV_DIR / "vendor" / "py3.7" INSTALLED_RESULTS = [ metadata.PathDistribution(SITE_PURELIB / "cleo-0.7.6.dist-info"), metadata.PathDistribution(SRC / "pendulum" / "pendulum.egg-info"), metadata.PathDistribution( zipfile.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"), @@ -53,10 +51,10 @@ class MockEnv(BaseMockEnv): @property - def paths(self) -> dict[str, Path]: + def paths(self) -> dict[str, str]: return { - "purelib": SITE_PURELIB, - "platlib": SITE_PLATLIB, + "purelib": SITE_PURELIB.as_posix(), + "platlib": SITE_PLATLIB.as_posix(), } @property @@ -80,11 +78,6 @@ def mock_git_info(mocker: MockerFixture) -> None: ) -@pytest.fixture(autouse=True) -def mock_installed_repository_vendors(mocker: MockerFixture) -> None: - mocker.patch("poetry.repositories.installed_repository._VENDORS", str(VENDOR_DIR)) - - @pytest.fixture def repository(mocker: MockerFixture, env: MockEnv) -> InstalledRepository: mocker.patch( @@ -103,14 +96,14 @@ def get_package_from_repository( return None -def test_load_successful(repository: InstalledRepository): - assert len(repository.packages) == len(INSTALLED_RESULTS) - 1 +def test_load_successful(repository: InstalledRepository) -> None: + assert len(repository.packages) == len(INSTALLED_RESULTS) def test_load_successful_with_invalid_distribution( - caplog: LogCaptureFixture, mocker: MockerFixture, env: MockEnv, tmp_dir: str + caplog: LogCaptureFixture, mocker: MockerFixture, env: MockEnv, tmp_path: Path ) -> None: - invalid_dist_info = Path(tmp_dir) / "site-packages" / "invalid-0.1.0.dist-info" + invalid_dist_info = tmp_path / "site-packages" / "invalid-0.1.0.dist-info" invalid_dist_info.mkdir(parents=True) mocker.patch( "poetry.utils._compat.metadata.Distribution.discover", @@ -118,9 +111,7 @@ def test_load_successful_with_invalid_distribution( ) repository_with_invalid_distribution = InstalledRepository.load(env) - assert ( - len(repository_with_invalid_distribution.packages) == len(INSTALLED_RESULTS) - 1 - ) + assert len(repository_with_invalid_distribution.packages) == len(INSTALLED_RESULTS) assert len(caplog.messages) == 1 message = caplog.messages[0] @@ -128,12 +119,12 @@ def test_load_successful_with_invalid_distribution( assert str(invalid_dist_info) in message -def test_load_ensure_isolation(repository: InstalledRepository): +def test_load_ensure_isolation(repository: InstalledRepository) -> None: package = get_package_from_repository("attrs", repository) assert package is None -def test_load_standard_package(repository: InstalledRepository): +def test_load_standard_package(repository: InstalledRepository) -> None: cleo = get_package_from_repository("cleo", repository) assert cleo is not None assert cleo.name == "cleo" @@ -148,7 +139,7 @@ def test_load_standard_package(repository: InstalledRepository): assert foo.version.text == "0.1.0" -def test_load_git_package(repository: InstalledRepository): +def test_load_git_package(repository: InstalledRepository) -> None: pendulum = get_package_from_repository("pendulum", repository) assert pendulum is not None assert pendulum.name == "pendulum" @@ -162,7 +153,7 @@ def test_load_git_package(repository: InstalledRepository): assert pendulum.source_reference == "bb058f6b78b2d28ef5d9a5e759cfa179a1a713d6" -def test_load_git_package_pth(repository: InstalledRepository): +def test_load_git_package_pth(repository: InstalledRepository) -> None: bender = get_package_from_repository("bender", repository) assert bender is not None assert bender.name == "bender" @@ -170,14 +161,14 @@ def test_load_git_package_pth(repository: InstalledRepository): assert bender.source_type == "git" -def test_load_platlib_package(repository: InstalledRepository): +def test_load_platlib_package(repository: InstalledRepository) -> None: lib64 = get_package_from_repository("lib64", repository) assert lib64 is not None assert lib64.name == "lib64" assert lib64.version.text == "2.3.4" -def test_load_editable_package(repository: InstalledRepository): +def test_load_editable_package(repository: InstalledRepository) -> None: # test editable package with text .pth file editable = get_package_from_repository("editable", repository) assert editable is not None @@ -190,7 +181,7 @@ def test_load_editable_package(repository: InstalledRepository): ) -def test_load_editable_with_import_package(repository: InstalledRepository): +def test_load_editable_with_import_package(repository: InstalledRepository) -> None: # test editable package with executable .pth file editable = get_package_from_repository("editable-with-import", repository) assert editable is not None @@ -200,7 +191,7 @@ def test_load_editable_with_import_package(repository: InstalledRepository): assert editable.source_url is None -def test_load_standard_package_with_pth_file(repository: InstalledRepository): +def test_load_standard_package_with_pth_file(repository: InstalledRepository) -> None: # test standard packages with .pth file is not treated as editable standard = get_package_from_repository("standard", repository) assert standard is not None @@ -210,7 +201,7 @@ def test_load_standard_package_with_pth_file(repository: InstalledRepository): assert standard.source_url is None -def test_load_pep_610_compliant_git_packages(repository: InstalledRepository): +def test_load_pep_610_compliant_git_packages(repository: InstalledRepository) -> None: package = get_package_from_repository("git-pep-610", repository) assert package is not None @@ -224,7 +215,7 @@ def test_load_pep_610_compliant_git_packages(repository: InstalledRepository): def test_load_pep_610_compliant_git_packages_no_requested_version( repository: InstalledRepository, -): +) -> None: package = get_package_from_repository( "git-pep-610-no-requested-version", repository ) @@ -243,7 +234,7 @@ def test_load_pep_610_compliant_git_packages_no_requested_version( def test_load_pep_610_compliant_git_packages_with_subdirectory( repository: InstalledRepository, -): +) -> None: package = get_package_from_repository("git-pep-610-subdirectory", repository) assert package is not None assert package.name == "git-pep-610-subdirectory" @@ -255,7 +246,7 @@ def test_load_pep_610_compliant_git_packages_with_subdirectory( assert package.source_subdirectory == "subdir" -def test_load_pep_610_compliant_url_packages(repository: InstalledRepository): +def test_load_pep_610_compliant_url_packages(repository: InstalledRepository) -> None: package = get_package_from_repository("url-pep-610", repository) assert package is not None @@ -268,7 +259,7 @@ def test_load_pep_610_compliant_url_packages(repository: InstalledRepository): ) -def test_load_pep_610_compliant_file_packages(repository: InstalledRepository): +def test_load_pep_610_compliant_file_packages(repository: InstalledRepository) -> None: package = get_package_from_repository("file-pep-610", repository) assert package is not None @@ -278,7 +269,9 @@ def test_load_pep_610_compliant_file_packages(repository: InstalledRepository): assert package.source_url == "/path/to/distributions/file-pep-610-1.2.3.tar.gz" -def test_load_pep_610_compliant_directory_packages(repository: InstalledRepository): +def test_load_pep_610_compliant_directory_packages( + repository: InstalledRepository, +) -> None: package = get_package_from_repository("directory-pep-610", repository) assert package is not None @@ -291,7 +284,7 @@ def test_load_pep_610_compliant_directory_packages(repository: InstalledReposito def test_load_pep_610_compliant_editable_directory_packages( repository: InstalledRepository, -): +) -> None: package = get_package_from_repository("editable-directory-pep-610", repository) assert package is not None diff --git a/tests/repositories/test_legacy_repository.py b/tests/repositories/test_legacy_repository.py index ab8f7022654..298d3f860f4 100644 --- a/tests/repositories/test_legacy_repository.py +++ b/tests/repositories/test_legacy_repository.py @@ -3,6 +3,7 @@ import base64 import re import shutil +import urllib.parse as urlparse from pathlib import Path from typing import TYPE_CHECKING @@ -21,15 +22,11 @@ from poetry.repositories.link_sources.html import SimpleRepositoryPage -try: - import urllib.parse as urlparse -except ImportError: - import urlparse - if TYPE_CHECKING: import httpretty from _pytest.monkeypatch import MonkeyPatch + from packaging.utils import NormalizedName from poetry.config.config import Config @@ -45,16 +42,13 @@ class MockRepository(LegacyRepository): def __init__(self) -> None: super().__init__("legacy", url="http://legacy.foo.bar", disable_cache=True) - def _get_page(self, endpoint: str) -> SimpleRepositoryPage | None: - parts = endpoint.split("/") - name = parts[1] - + def _get_page(self, name: NormalizedName) -> SimpleRepositoryPage: fixture = self.FIXTURES / (name + ".html") if not fixture.exists(): - return None + raise PackageNotFound(f"Package [{name}] not found.") with fixture.open(encoding="utf-8") as f: - return SimpleRepositoryPage(self._url + endpoint, f.read()) + return SimpleRepositoryPage(self._url + f"/{name}/", f.read()) def _download(self, url: str, dest: Path) -> None: filename = urlparse.urlparse(url).path.rsplit("/")[-1] @@ -73,7 +67,7 @@ def test_packages_property_returns_empty_list() -> None: def test_page_relative_links_path_are_correct() -> None: repo = MockRepository() - page = repo._get_page("/relative") + page = repo.get_page("relative") assert page is not None for link in page.links: @@ -84,7 +78,7 @@ def test_page_relative_links_path_are_correct() -> None: def test_page_absolute_links_path_are_correct() -> None: repo = MockRepository() - page = repo._get_page("/absolute") + page = repo.get_page("absolute") assert page is not None for link in page.links: @@ -95,7 +89,7 @@ def test_page_absolute_links_path_are_correct() -> None: def test_page_clean_link() -> None: repo = MockRepository() - page = repo._get_page("/relative") + page = repo.get_page("relative") assert page is not None cleaned = page.clean_link('https://legacy.foo.bar/test /the"/cleaning\0') @@ -105,7 +99,7 @@ def test_page_clean_link() -> None: def test_page_invalid_version_link() -> None: repo = MockRepository() - page = repo._get_page("/invalid-version") + page = repo.get_page("invalid-version") assert page is not None links = list(page.links) @@ -123,7 +117,7 @@ def test_page_invalid_version_link() -> None: def test_sdist_format_support() -> None: repo = MockRepository() - page = repo._get_page("/relative") + page = repo.get_page("relative") assert page is not None bz2_links = list(filter(lambda link: link.ext == ".tar.bz2", page.links)) assert len(bz2_links) == 1 @@ -213,7 +207,7 @@ def test_find_packages_no_prereleases() -> None: @pytest.mark.parametrize( - ["constraint", "count"], [("*", 1), (">=1", 0), (">=19.0.0a0", 1)] + ["constraint", "count"], [("*", 1), (">=1", 1), ("<=18", 0), (">=19.0.0a0", 1)] ) def test_find_packages_only_prereleases(constraint: str, count: int) -> None: repo = MockRepository() @@ -228,13 +222,6 @@ def test_find_packages_only_prereleases(constraint: str, count: int) -> None: assert package.source_url == repo.url -def test_find_packages_only_prereleases_empty_when_not_any() -> None: - repo = MockRepository() - packages = repo.find_packages(Factory.create_dependency("black", ">=1")) - - assert len(packages) == 0 - - @pytest.mark.parametrize( ["constraint", "expected"], [ @@ -441,8 +428,8 @@ def test_package_yanked( def test_package_partial_yank(): class SpecialMockRepository(MockRepository): - def _get_page(self, endpoint: str) -> SimpleRepositoryPage | None: - return super()._get_page(f"/{endpoint.strip('/')}_partial_yank/") + def _get_page(self, name: NormalizedName) -> SimpleRepositoryPage: + return super()._get_page(canonicalize_name(f"{name}-partial-yank")) repo = MockRepository() package = repo.package("futures", Version.parse("3.2.0")) @@ -488,31 +475,32 @@ def __init__( def test_get_200_returns_page(http: type[httpretty.httpretty]) -> None: - repo = MockHttpRepository({"/foo": 200}, http) + repo = MockHttpRepository({"/foo/": 200}, http) - assert repo._get_page("/foo") + assert repo.get_page("foo") @pytest.mark.parametrize("status_code", [401, 403, 404]) def test_get_40x_and_returns_none( http: type[httpretty.httpretty], status_code: int ) -> None: - repo = MockHttpRepository({"/foo": status_code}, http) + repo = MockHttpRepository({"/foo/": status_code}, http) - assert repo._get_page("/foo") is None + with pytest.raises(PackageNotFound): + repo.get_page("foo") def test_get_5xx_raises(http: type[httpretty.httpretty]) -> None: - repo = MockHttpRepository({"/foo": 500}, http) + repo = MockHttpRepository({"/foo/": 500}, http) with pytest.raises(RepositoryError): - repo._get_page("/foo") + repo.get_page("foo") def test_get_redirected_response_url( http: type[httpretty.httpretty], monkeypatch: MonkeyPatch ) -> None: - repo = MockHttpRepository({"/foo": 200}, http) + repo = MockHttpRepository({"/foo/": 200}, http) redirect_url = "http://legacy.redirect.bar" def get_mock( @@ -524,7 +512,7 @@ def get_mock( return response monkeypatch.setattr(repo.session, "get", get_mock) - page = repo._get_page("/foo") + page = repo.get_page("foo") assert page is not None assert page._url == "http://legacy.redirect.bar/foo/" @@ -560,7 +548,7 @@ def test_authenticator_with_implicit_repository_configuration( ) repo = LegacyRepository(name="source", url="https://foo.bar/simple", config=config) - repo._get_page("/foo") + repo.get_page("/foo") request = http.last_request() diff --git a/tests/repositories/test_pypi_repository.py b/tests/repositories/test_pypi_repository.py index 7396324af5a..3c2c1a6b532 100644 --- a/tests/repositories/test_pypi_repository.py +++ b/tests/repositories/test_pypi_repository.py @@ -16,11 +16,13 @@ from requests.models import Response from poetry.factory import Factory +from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.link_sources.json import SimpleJsonPage from poetry.repositories.pypi_repository import PyPiRepository -from poetry.utils._compat import encode if TYPE_CHECKING: + from packaging.utils import NormalizedName from pytest_mock import MockerFixture @@ -36,6 +38,14 @@ class MockRepository(PyPiRepository): def __init__(self, fallback: bool = False) -> None: super().__init__(url="http://foo.bar", disable_cache=True, fallback=fallback) + def get_json_page(self, name: NormalizedName) -> SimpleJsonPage: + fixture = self.JSON_FIXTURES / (name + ".json") + + if not fixture.exists(): + raise PackageNotFound(f"Package [{name}] not found.") + + return SimpleJsonPage("", json.loads(fixture.read_text())) + def _get( self, url: str, headers: dict[str, str] | None = None ) -> dict[str, Any] | None: @@ -87,7 +97,7 @@ def test_find_packages_does_not_select_prereleases_if_not_allowed() -> None: @pytest.mark.parametrize( - ["constraint", "count"], [("*", 1), (">=1", 0), (">=19.0.0a0", 1)] + ["constraint", "count"], [("*", 1), (">=1", 1), ("<=18", 0), (">=19.0.0a0", 1)] ) def test_find_packages_only_prereleases(constraint: str, count: int) -> None: repo = MockRepository() @@ -139,11 +149,18 @@ def test_package() -> None: win_inet = package.extras["socks"][0] assert win_inet.name == "win-inet-pton" assert win_inet.python_versions == "~2.7 || ~2.6" - assert ( - str(win_inet.marker) - == 'sys_platform == "win32" and (python_version == "2.7"' - ' or python_version == "2.6") and extra == "socks"' + + # Different versions of poetry-core simplify the following marker differently, + # either is fine. + marker1 = ( + 'sys_platform == "win32" and (python_version == "2.7" or python_version ==' + ' "2.6") and extra == "socks"' + ) + marker2 = ( + 'sys_platform == "win32" and python_version == "2.7" and extra == "socks" or' + ' sys_platform == "win32" and python_version == "2.6" and extra == "socks"' ) + assert str(win_inet.marker) in {marker1, marker2} @pytest.mark.parametrize( @@ -284,10 +301,8 @@ def test_pypi_repository_supports_reading_bz2_files() -> None: ] } - for name in expected_extras.keys(): - assert ( - sorted(package.extras[name], key=lambda r: r.name) == expected_extras[name] - ) + for name, expected_extra in expected_extras.items(): + assert sorted(package.extras[name], key=lambda r: r.name) == expected_extra def test_invalid_versions_ignored() -> None: @@ -307,7 +322,7 @@ def test_get_should_invalid_cache_on_too_many_redirects_error( response = Response() response.status_code = 200 response.encoding = "utf-8" - response.raw = BytesIO(encode('{"foo": "bar"}')) + response.raw = BytesIO(b'{"foo": "bar"}') mocker.patch( "poetry.utils.authenticator.Authenticator.get", side_effect=[TooManyRedirects(), response], @@ -325,14 +340,6 @@ def test_urls() -> None: assert repository.authenticated_url == "https://pypi.org/simple/" -def test_use_pypi_pretty_name() -> None: - repo = MockRepository(fallback=True) - - package = repo.find_packages(Factory.create_dependency("twisted", "*")) - assert len(package) == 1 - assert package[0].pretty_name == "Twisted" - - def test_find_links_for_package_of_supported_types(): repo = MockRepository() package = repo.find_packages(Factory.create_dependency("hbmqtt", "0.9.6")) diff --git a/tests/repositories/test_pool.py b/tests/repositories/test_repository_pool.py similarity index 71% rename from tests/repositories/test_pool.py rename to tests/repositories/test_repository_pool.py index dc68bf0015b..21c5941d73d 100644 --- a/tests/repositories/test_pool.py +++ b/tests/repositories/test_repository_pool.py @@ -4,16 +4,17 @@ from poetry.core.constraints.version import Version -from poetry.repositories import Pool from poetry.repositories import Repository +from poetry.repositories import RepositoryPool from poetry.repositories.exceptions import PackageNotFound from poetry.repositories.legacy_repository import LegacyRepository +from poetry.repositories.repository_pool import Priority from tests.helpers import get_dependency from tests.helpers import get_package def test_pool() -> None: - pool = Pool() + pool = RepositoryPool() assert len(pool.repositories) == 0 assert not pool.has_default() @@ -22,15 +23,16 @@ def test_pool() -> None: def test_pool_with_initial_repositories() -> None: repo = Repository("repo") - pool = Pool([repo]) + pool = RepositoryPool([repo]) assert len(pool.repositories) == 1 assert not pool.has_default() assert pool.has_primary_repositories() + assert pool.get_priority("repo") == Priority.PRIMARY def test_repository_no_repository() -> None: - pool = Pool() + pool = RepositoryPool() with pytest.raises(IndexError): pool.repository("foo") @@ -41,50 +43,83 @@ def test_adding_repositories_with_same_name_twice_raises_value_error() -> None: repo2 = Repository("repo") with pytest.raises(ValueError): - Pool([repo1, repo2]) + RepositoryPool([repo1, repo2]) with pytest.raises(ValueError): - Pool([repo1]).add_repository(repo2) + RepositoryPool([repo1]).add_repository(repo2) -def test_repository_from_normal_pool() -> None: +@pytest.mark.parametrize("priority", (p for p in Priority)) +def test_repository_from_single_repo_pool(priority: Priority) -> None: repo = LegacyRepository("foo", "https://foo.bar") - pool = Pool() - pool.add_repository(repo) - - assert pool.repository("foo") is repo + pool = RepositoryPool() + pool.add_repository(repo, priority=priority) -def test_repository_from_secondary_pool() -> None: + assert pool.repository("foo") is repo + assert pool.get_priority("foo") == priority + + +@pytest.mark.parametrize( + ("default", "secondary", "expected_priority"), + [ + (False, True, Priority.SECONDARY), + (True, False, Priority.DEFAULT), + (True, True, Priority.DEFAULT), + ], +) +def test_repository_from_single_repo_pool_legacy( + default: bool, secondary: bool, expected_priority: Priority +) -> None: repo = LegacyRepository("foo", "https://foo.bar") - pool = Pool() - pool.add_repository(repo, secondary=True) + pool = RepositoryPool() + + with pytest.warns(DeprecationWarning): + pool.add_repository(repo, default=default, secondary=secondary) assert pool.repository("foo") is repo + assert pool.get_priority("foo") == expected_priority -def test_repository_with_normal_default_and_secondary_repositories() -> None: +def test_repository_with_normal_default_secondary_and_explicit_repositories(): secondary = LegacyRepository("secondary", "https://secondary.com") default = LegacyRepository("default", "https://default.com") repo1 = LegacyRepository("foo", "https://foo.bar") repo2 = LegacyRepository("bar", "https://bar.baz") + explicit = LegacyRepository("explicit", "https://bar.baz") - pool = Pool() + pool = RepositoryPool() pool.add_repository(repo1) - pool.add_repository(secondary, secondary=True) + pool.add_repository(secondary, priority=Priority.SECONDARY) pool.add_repository(repo2) - pool.add_repository(default, default=True) + pool.add_repository(explicit, priority=Priority.EXPLICIT) + pool.add_repository(default, priority=Priority.DEFAULT) assert pool.repository("secondary") is secondary assert pool.repository("default") is default assert pool.repository("foo") is repo1 assert pool.repository("bar") is repo2 + assert pool.repository("explicit") is explicit assert pool.has_default() assert pool.has_primary_repositories() +def test_repository_explicit_repositories_do_not_show() -> None: + explicit = LegacyRepository("explicit", "https://explicit.com") + default = LegacyRepository("default", "https://default.com") + + pool = RepositoryPool() + pool.add_repository(explicit, priority=Priority.EXPLICIT) + pool.add_repository(default, priority=Priority.DEFAULT) + + assert pool.repository("explicit") is explicit + assert pool.repository("default") is default + assert pool.repositories == [default] + assert pool.all_repositories == [default, explicit] + + def test_remove_non_existing_repository_raises_indexerror() -> None: - pool = Pool() + pool = RepositoryPool() with pytest.raises(IndexError): pool.remove_repository("foo") @@ -95,7 +130,7 @@ def test_remove_existing_repository_successful() -> None: repo2 = LegacyRepository("bar", "https://bar.baz") repo3 = LegacyRepository("baz", "https://baz.quux") - pool = Pool() + pool = RepositoryPool() pool.add_repository(repo1) pool.add_repository(repo2) pool.add_repository(repo3) @@ -112,22 +147,22 @@ def test_remove_default_repository() -> None: repo2 = LegacyRepository("bar", "https://bar.baz") new_default = LegacyRepository("new_default", "https://new.default.com") - pool = Pool() + pool = RepositoryPool() pool.add_repository(repo1) pool.add_repository(repo2) - pool.add_repository(default, default=True) + pool.add_repository(default, priority=Priority.DEFAULT) assert pool.has_default() pool.remove_repository("default") + assert not pool.has_repository("default") assert not pool.has_default() - pool.add_repository(new_default, default=True) + pool.add_repository(new_default, priority=Priority.DEFAULT) + assert pool.get_priority("new_default") is Priority.DEFAULT assert pool.has_default() - assert pool.repositories[0] is new_default - assert not pool.has_repository("default") def test_repository_ordering() -> None: @@ -140,22 +175,22 @@ def test_repository_ordering() -> None: secondary2 = LegacyRepository("secondary2", "https://secondary2.com") secondary3 = LegacyRepository("secondary3", "https://secondary3.com") - pool = Pool() - pool.add_repository(secondary1, secondary=True) + pool = RepositoryPool() + pool.add_repository(secondary1, priority=Priority.SECONDARY) pool.add_repository(primary1) - pool.add_repository(default1, default=True) + pool.add_repository(default1, priority=Priority.DEFAULT) pool.add_repository(primary2) - pool.add_repository(secondary2, secondary=True) + pool.add_repository(secondary2, priority=Priority.SECONDARY) pool.remove_repository("primary2") pool.remove_repository("secondary2") pool.add_repository(primary3) - pool.add_repository(secondary3, secondary=True) + pool.add_repository(secondary3, priority=Priority.SECONDARY) assert pool.repositories == [default1, primary1, primary3, secondary1, secondary3] with pytest.raises(ValueError): - pool.add_repository(default2, default=True) + pool.add_repository(default2, priority=Priority.DEFAULT) def test_pool_get_package_in_any_repository() -> None: @@ -163,7 +198,7 @@ def test_pool_get_package_in_any_repository() -> None: repo1 = Repository("repo1", [package1]) package2 = get_package("bar", "1.0.0") repo2 = Repository("repo2", [package1, package2]) - pool = Pool([repo1, repo2]) + pool = RepositoryPool([repo1, repo2]) returned_package1 = pool.package("foo", Version.parse("1.0.0")) returned_package2 = pool.package("bar", Version.parse("1.0.0")) @@ -176,7 +211,7 @@ def test_pool_get_package_in_specified_repository() -> None: package = get_package("foo", "1.0.0") repo1 = Repository("repo1") repo2 = Repository("repo2", [package]) - pool = Pool([repo1, repo2]) + pool = RepositoryPool([repo1, repo2]) returned_package = pool.package( "foo", Version.parse("1.0.0"), repository_name="repo2" @@ -186,7 +221,7 @@ def test_pool_get_package_in_specified_repository() -> None: def test_pool_no_package_from_any_repository_raises_package_not_found() -> None: - pool = Pool() + pool = RepositoryPool() pool.add_repository(Repository("repo")) with pytest.raises(PackageNotFound): @@ -197,7 +232,7 @@ def test_pool_no_package_from_specified_repository_raises_package_not_found() -> package = get_package("foo", "1.0.0") repo1 = Repository("repo1") repo2 = Repository("repo2", [package]) - pool = Pool([repo1, repo2]) + pool = RepositoryPool([repo1, repo2]) with pytest.raises(PackageNotFound): pool.package("foo", Version.parse("1.0.0"), repository_name="repo1") @@ -210,7 +245,7 @@ def test_pool_find_packages_in_any_repository() -> None: package4 = get_package("bar", "1.2.3") repo1 = Repository("repo1", [package1, package3]) repo2 = Repository("repo2", [package1, package2, package4]) - pool = Pool([repo1, repo2]) + pool = RepositoryPool([repo1, repo2]) available_dependency = get_dependency("foo", "^1.0.0") returned_packages_available = pool.find_packages(available_dependency) @@ -228,7 +263,7 @@ def test_pool_find_packages_in_specified_repository() -> None: package_bar = get_package("bar", "1.2.3") repo1 = Repository("repo1", [package_foo1, package_foo3]) repo2 = Repository("repo2", [package_foo1, package_foo2, package_bar]) - pool = Pool([repo1, repo2]) + pool = RepositoryPool([repo1, repo2]) available_dependency = get_dependency("foo", "^1.0.0") available_dependency.source_name = "repo2" @@ -247,7 +282,7 @@ def test_search_no_legacy_repositories() -> None: package_foobar = get_package("foobar", "1.0.0") repo1 = Repository("repo1", [package_foo1, package_foo2]) repo2 = Repository("repo2", [package_foo1, package_foobar]) - pool = Pool([repo1, repo2]) + pool = RepositoryPool([repo1, repo2]) assert pool.search("foo") == [ package_foo1, @@ -263,6 +298,6 @@ def test_search_legacy_repositories_are_skipped() -> None: package = get_package("foo", "1.0.0") repo1 = Repository("repo1", [package]) repo2 = LegacyRepository("repo2", "https://fake.repo/") - pool = Pool([repo1, repo2]) + pool = RepositoryPool([repo1, repo2]) assert pool.search("foo") == [package] diff --git a/tests/repositories/test_single_page_repository.py b/tests/repositories/test_single_page_repository.py index b90bb29ab5b..804fa3d0125 100644 --- a/tests/repositories/test_single_page_repository.py +++ b/tests/repositories/test_single_page_repository.py @@ -3,13 +3,19 @@ import re from pathlib import Path +from typing import TYPE_CHECKING from poetry.core.packages.dependency import Dependency +from poetry.repositories.exceptions import PackageNotFound from poetry.repositories.link_sources.html import SimpleRepositoryPage from poetry.repositories.single_page_repository import SinglePageRepository +if TYPE_CHECKING: + from packaging.utils import NormalizedName + + class MockSinglePageRepository(SinglePageRepository): FIXTURES = Path(__file__).parent / "fixtures" / "single-page" @@ -20,10 +26,10 @@ def __init__(self, page: str) -> None: disable_cache=True, ) - def _get_page(self, endpoint: str = None) -> SimpleRepositoryPage | None: + def _get_page(self, name: NormalizedName) -> SimpleRepositoryPage: fixture = self.FIXTURES / self.url.rsplit("/", 1)[-1] if not fixture.exists(): - return + raise PackageNotFound(f"Package [{name}] not found.") with fixture.open(encoding="utf-8") as f: return SimpleRepositoryPage(self._url, f.read()) @@ -35,7 +41,7 @@ def _download(self, url: str, dest: Path) -> None: def test_single_page_repository_get_page(): repo = MockSinglePageRepository("jax_releases") - page = repo._get_page("/ignored") + page = repo.get_page("/ignored") links = list(page.links) assert len(links) == 21 diff --git a/tests/test_factory.py b/tests/test_factory.py index 7eafc85d210..d3a39620e86 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -1,19 +1,20 @@ from __future__ import annotations -from pathlib import Path from typing import TYPE_CHECKING import pytest +from cleo.io.buffered_io import BufferedIO from deepdiff import DeepDiff from packaging.utils import canonicalize_name from poetry.core.constraints.version import parse_constraint -from poetry.core.toml.file import TOMLFile from poetry.factory import Factory from poetry.plugins.plugin import Plugin from poetry.repositories.legacy_repository import LegacyRepository from poetry.repositories.pypi_repository import PyPiRepository +from poetry.repositories.repository_pool import Priority +from poetry.toml.file import TOMLFile from tests.helpers import mock_metadata_entry_points @@ -24,8 +25,6 @@ from poetry.poetry import Poetry from tests.types import FixtureDirGetter -fixtures_dir = Path(__file__).parent / "fixtures" - class MyPlugin(Plugin): def activate(self, poetry: Poetry, io: IO) -> None: @@ -33,8 +32,8 @@ def activate(self, poetry: Poetry, io: IO) -> None: poetry.package.readmes = ("README.md",) -def test_create_poetry(): - poetry = Factory().create_poetry(fixtures_dir / "sample_project") +def test_create_poetry(fixture_dir: FixtureDirGetter) -> None: + poetry = Factory().create_poetry(fixture_dir("sample_project")) package = poetry.package @@ -46,7 +45,7 @@ def test_create_poetry(): for readme in package.readmes: assert ( - readme.relative_to(fixtures_dir).as_posix() == "sample_project/README.rst" + readme.relative_to(fixture_dir("sample_project")).as_posix() == "README.rst" ) assert package.homepage == "https://python-poetry.org" @@ -145,8 +144,10 @@ def test_create_poetry(): ("project_with_extras",), ], ) -def test_create_pyproject_from_package(project: str): - poetry = Factory().create_poetry(fixtures_dir / project) +def test_create_pyproject_from_package( + project: str, fixture_dir: FixtureDirGetter +) -> None: + poetry = Factory().create_poetry(fixture_dir(project)) package = poetry.package pyproject = Factory.create_pyproject_from_package(package) @@ -175,8 +176,10 @@ def test_create_pyproject_from_package(project: str): assert not DeepDiff(expected, result) -def test_create_poetry_with_packages_and_includes(): - poetry = Factory().create_poetry(fixtures_dir / "with-include") +def test_create_poetry_with_packages_and_includes( + fixture_dir: FixtureDirGetter, +) -> None: + poetry = Factory().create_poetry(fixture_dir("with-include")) package = poetry.package @@ -196,9 +199,11 @@ def test_create_poetry_with_packages_and_includes(): ] -def test_create_poetry_with_multi_constraints_dependency(): +def test_create_poetry_with_multi_constraints_dependency( + fixture_dir: FixtureDirGetter, +) -> None: poetry = Factory().create_poetry( - fixtures_dir / "project_with_multi_constraints_dependency" + fixture_dir("project_with_multi_constraints_dependency") ) package = poetry.package @@ -206,111 +211,193 @@ def test_create_poetry_with_multi_constraints_dependency(): assert len(package.requires) == 2 -def test_poetry_with_default_source(with_simple_keyring: None): - poetry = Factory().create_poetry(fixtures_dir / "with_default_source") +def test_poetry_with_default_source_legacy( + fixture_dir: FixtureDirGetter, with_simple_keyring: None +) -> None: + io = BufferedIO() + poetry = Factory().create_poetry(fixture_dir("with_default_source_legacy"), io=io) assert len(poetry.pool.repositories) == 1 + assert "Found deprecated key" in io.fetch_error() -def test_poetry_with_non_default_source(with_simple_keyring: None): - poetry = Factory().create_poetry(fixtures_dir / "with_non_default_source") - - assert len(poetry.pool.repositories) == 2 +def test_poetry_with_default_source( + fixture_dir: FixtureDirGetter, with_simple_keyring: None +) -> None: + io = BufferedIO() + poetry = Factory().create_poetry(fixture_dir("with_default_source"), io=io) - assert not poetry.pool.has_default() - - assert poetry.pool.repositories[0].name == "foo" - assert isinstance(poetry.pool.repositories[0], LegacyRepository) + assert len(poetry.pool.repositories) == 1 + assert io.fetch_error() == "" - assert poetry.pool.repositories[1].name == "PyPI" - assert isinstance(poetry.pool.repositories[1], PyPiRepository) +@pytest.mark.parametrize( + "project", + ("with_non_default_source_implicit", "with_non_default_source_explicit"), +) +def test_poetry_with_non_default_source( + project: str, fixture_dir: FixtureDirGetter, with_simple_keyring: None +) -> None: + poetry = Factory().create_poetry(fixture_dir(project)) -def test_poetry_with_non_default_secondary_source(with_simple_keyring: None): - poetry = Factory().create_poetry(fixtures_dir / "with_non_default_secondary_source") + assert not poetry.pool.has_default() + assert poetry.pool.has_repository("PyPI") + assert poetry.pool.get_priority("PyPI") is Priority.SECONDARY + assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) + assert poetry.pool.has_repository("foo") + assert poetry.pool.get_priority("foo") is Priority.PRIMARY + assert isinstance(poetry.pool.repository("foo"), LegacyRepository) + assert {repo.name for repo in poetry.pool.repositories} == {"PyPI", "foo"} + + +def test_poetry_with_non_default_secondary_source_legacy( + fixture_dir: FixtureDirGetter, with_simple_keyring: None +) -> None: + poetry = Factory().create_poetry( + fixture_dir("with_non_default_secondary_source_legacy") + ) - assert len(poetry.pool.repositories) == 2 + assert poetry.pool.has_repository("PyPI") + assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) + assert poetry.pool.get_priority("PyPI") is Priority.DEFAULT + assert poetry.pool.has_repository("foo") + assert isinstance(poetry.pool.repository("foo"), LegacyRepository) + assert [repo.name for repo in poetry.pool.repositories] == ["PyPI", "foo"] - assert poetry.pool.has_default() - repository = poetry.pool.repositories[0] - assert repository.name == "PyPI" - assert isinstance(repository, PyPiRepository) +def test_poetry_with_non_default_secondary_source( + fixture_dir: FixtureDirGetter, with_simple_keyring: None +) -> None: + poetry = Factory().create_poetry(fixture_dir("with_non_default_secondary_source")) - repository = poetry.pool.repositories[1] - assert repository.name == "foo" - assert isinstance(repository, LegacyRepository) + assert poetry.pool.has_repository("PyPI") + assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) + assert poetry.pool.get_priority("PyPI") is Priority.DEFAULT + assert poetry.pool.has_repository("foo") + assert isinstance(poetry.pool.repository("foo"), LegacyRepository) + assert [repo.name for repo in poetry.pool.repositories] == ["PyPI", "foo"] -def test_poetry_with_non_default_multiple_secondary_sources(with_simple_keyring: None): +def test_poetry_with_non_default_multiple_secondary_sources_legacy( + fixture_dir: FixtureDirGetter, + with_simple_keyring: None, +) -> None: poetry = Factory().create_poetry( - fixtures_dir / "with_non_default_multiple_secondary_sources" + fixture_dir("with_non_default_multiple_secondary_sources_legacy") ) - assert len(poetry.pool.repositories) == 3 + assert poetry.pool.has_repository("PyPI") + assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) + assert poetry.pool.get_priority("PyPI") is Priority.DEFAULT + assert poetry.pool.has_repository("foo") + assert isinstance(poetry.pool.repository("foo"), LegacyRepository) + assert poetry.pool.has_repository("bar") + assert isinstance(poetry.pool.repository("bar"), LegacyRepository) + assert {repo.name for repo in poetry.pool.repositories} == {"PyPI", "foo", "bar"} - assert poetry.pool.has_default() - repository = poetry.pool.repositories[0] - assert repository.name == "PyPI" - assert isinstance(repository, PyPiRepository) +def test_poetry_with_non_default_multiple_secondary_sources( + fixture_dir: FixtureDirGetter, with_simple_keyring: None +) -> None: + poetry = Factory().create_poetry( + fixture_dir("with_non_default_multiple_secondary_sources") + ) + + assert poetry.pool.has_repository("PyPI") + assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) + assert poetry.pool.get_priority("PyPI") is Priority.DEFAULT + assert poetry.pool.has_repository("foo") + assert isinstance(poetry.pool.repository("foo"), LegacyRepository) + assert poetry.pool.has_repository("bar") + assert isinstance(poetry.pool.repository("bar"), LegacyRepository) + assert {repo.name for repo in poetry.pool.repositories} == {"PyPI", "foo", "bar"} - 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_legacy( + fixture_dir: FixtureDirGetter, with_simple_keyring: None +) -> None: + poetry = Factory().create_poetry( + fixture_dir("with_non_default_multiple_sources_legacy") + ) + assert not poetry.pool.has_default() + assert poetry.pool.has_repository("bar") + assert isinstance(poetry.pool.repository("bar"), LegacyRepository) + assert poetry.pool.has_repository("PyPI") + assert poetry.pool.get_priority("PyPI") is Priority.SECONDARY + assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) + assert poetry.pool.has_repository("foo") + assert isinstance(poetry.pool.repository("foo"), LegacyRepository) + assert {repo.name for repo in poetry.pool.repositories} == {"bar", "PyPI", "foo"} -def test_poetry_with_non_default_multiple_sources(with_simple_keyring: None): - poetry = Factory().create_poetry(fixtures_dir / "with_non_default_multiple_sources") - assert len(poetry.pool.repositories) == 3 +def test_poetry_with_non_default_multiple_sources( + fixture_dir: FixtureDirGetter, with_simple_keyring: None +) -> None: + poetry = Factory().create_poetry(fixture_dir("with_non_default_multiple_sources")) assert not poetry.pool.has_default() + assert poetry.pool.has_repository("PyPI") + assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) + assert poetry.pool.get_priority("PyPI") is Priority.SECONDARY + assert poetry.pool.has_repository("bar") + assert isinstance(poetry.pool.repository("bar"), LegacyRepository) + assert poetry.pool.has_repository("foo") + assert isinstance(poetry.pool.repository("foo"), LegacyRepository) + assert {repo.name for repo in poetry.pool.repositories} == {"PyPI", "bar", "foo"} - 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) +def test_poetry_with_no_default_source(fixture_dir: FixtureDirGetter) -> None: + poetry = Factory().create_poetry(fixture_dir("sample_project")) - repository = poetry.pool.repositories[2] - assert repository.name == "PyPI" - assert isinstance(repository, PyPiRepository) + assert poetry.pool.has_repository("PyPI") + assert poetry.pool.get_priority("PyPI") is Priority.DEFAULT + assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) + assert {repo.name for repo in poetry.pool.repositories} == {"PyPI"} -def test_poetry_with_no_default_source(): - poetry = Factory().create_poetry(fixtures_dir / "sample_project") +def test_poetry_with_explicit_source( + fixture_dir: FixtureDirGetter, with_simple_keyring: None +) -> None: + poetry = Factory().create_poetry(fixture_dir("with_explicit_source")) assert len(poetry.pool.repositories) == 1 + assert len(poetry.pool.all_repositories) == 2 + assert poetry.pool.has_repository("PyPI") + assert poetry.pool.get_priority("PyPI") is Priority.DEFAULT + assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) + assert poetry.pool.has_repository("explicit") + assert isinstance(poetry.pool.repository("explicit"), LegacyRepository) + assert [repo.name for repo in poetry.pool.repositories] == ["PyPI"] + + +def test_poetry_with_two_default_sources_legacy( + fixture_dir: FixtureDirGetter, with_simple_keyring: None +) -> None: + with pytest.raises(ValueError) as e: + Factory().create_poetry(fixture_dir("with_two_default_sources_legacy")) - assert poetry.pool.has_default() - - assert poetry.pool.repositories[0].name == "PyPI" - assert isinstance(poetry.pool.repositories[0], PyPiRepository) + assert str(e.value) == "Only one repository can be the default." -def test_poetry_with_two_default_sources(with_simple_keyring: None): +def test_poetry_with_two_default_sources( + fixture_dir: FixtureDirGetter, with_simple_keyring: None +) -> None: with pytest.raises(ValueError) as e: - Factory().create_poetry(fixtures_dir / "with_two_default_sources") + Factory().create_poetry(fixture_dir("with_two_default_sources")) assert str(e.value) == "Only one repository can be the default." -def test_validate(): - complete = TOMLFile(fixtures_dir / "complete.toml") +def test_validate(fixture_dir: FixtureDirGetter) -> None: + complete = TOMLFile(fixture_dir("complete.toml")) content = complete.read()["tool"]["poetry"] assert Factory.validate(content) == {"errors": [], "warnings": []} -def test_validate_fails(): - complete = TOMLFile(fixtures_dir / "complete.toml") +def test_validate_fails(fixture_dir: FixtureDirGetter) -> None: + complete = TOMLFile(fixture_dir("complete.toml")) content = complete.read()["tool"]["poetry"] content["this key is not in the schema"] = "" @@ -322,11 +409,11 @@ def test_validate_fails(): assert Factory.validate(content) == {"errors": [expected], "warnings": []} -def test_create_poetry_fails_on_invalid_configuration(): +def test_create_poetry_fails_on_invalid_configuration( + fixture_dir: FixtureDirGetter, +) -> None: with pytest.raises(RuntimeError) as e: - Factory().create_poetry( - Path(__file__).parent / "fixtures" / "invalid_pyproject" / "pyproject.toml" - ) + Factory().create_poetry(fixture_dir("invalid_pyproject") / "pyproject.toml") expected = """\ The Poetry configuration is invalid: @@ -335,7 +422,7 @@ def test_create_poetry_fails_on_invalid_configuration(): assert str(e.value) == expected -def test_create_poetry_with_local_config(fixture_dir: FixtureDirGetter): +def test_create_poetry_with_local_config(fixture_dir: FixtureDirGetter) -> None: poetry = Factory().create_poetry(fixture_dir("with_local_config")) assert not poetry.config.get("virtualenvs.in-project") @@ -346,9 +433,11 @@ def test_create_poetry_with_local_config(fixture_dir: FixtureDirGetter): assert not poetry.config.get("virtualenvs.options.system-site-packages") -def test_create_poetry_with_plugins(mocker: MockerFixture): +def test_create_poetry_with_plugins( + mocker: MockerFixture, fixture_dir: FixtureDirGetter +) -> None: mock_metadata_entry_points(mocker, MyPlugin) - poetry = Factory().create_poetry(fixtures_dir / "sample_project") + poetry = Factory().create_poetry(fixture_dir("sample_project")) assert poetry.package.readmes == ("README.md",) diff --git a/tests/test_helpers.py b/tests/test_helpers.py new file mode 100644 index 00000000000..7db5580b460 --- /dev/null +++ b/tests/test_helpers.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from tests.helpers import flatten_dict + + +def test_flatten_dict() -> None: + orig_dict = { + "a": 1, + "b": 2, + "c": { + "x": 8, + "y": 9, + }, + } + + flattened_dict = { + "a": 1, + "b": 2, + "c:x": 8, + "c:y": 9, + } + + assert flattened_dict == flatten_dict(orig_dict, delimiter=":") diff --git a/tests/types.py b/tests/types.py index 95ce4cbc940..5cc9ea5bf24 100644 --- a/tests/types.py +++ b/tests/types.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import TYPE_CHECKING +from typing import Any from tests.compat import Protocol @@ -46,6 +47,8 @@ def __call__( poetry_lock_content: str | None = None, install_deps: bool = True, source: Path | None = None, + locker_config: dict[str, Any] | None = None, + use_test_locker: bool = True, ) -> Poetry: ... diff --git a/tests/utils/test_authenticator.py b/tests/utils/test_authenticator.py index 91e6a574bc8..2a0d1bee564 100644 --- a/tests/utils/test_authenticator.py +++ b/tests/utils/test_authenticator.py @@ -42,12 +42,12 @@ def mock_remote(http: type[httpretty.httpretty]) -> None: @pytest.fixture() -def repo(): +def repo() -> dict[str, dict[str, str]]: return {"foo": {"url": "https://foo.bar/simple/"}} @pytest.fixture -def mock_config(config: Config, repo: dict[str, dict[str, str]]): +def mock_config(config: Config, repo: dict[str, dict[str, str]]) -> Config: config.merge( { "repositories": repo, @@ -60,7 +60,7 @@ def mock_config(config: Config, repo: dict[str, dict[str, str]]): def test_authenticator_uses_url_provided_credentials( mock_config: Config, mock_remote: None, http: type[httpretty.httpretty] -): +) -> None: authenticator = Authenticator(mock_config, NullIO()) authenticator.request("get", "https://foo001:bar002@foo.bar/files/foo-0.1.0.tar.gz") @@ -71,7 +71,7 @@ def test_authenticator_uses_url_provided_credentials( def test_authenticator_uses_credentials_from_config_if_not_provided( mock_config: Config, mock_remote: None, http: type[httpretty.httpretty] -): +) -> None: authenticator = Authenticator(mock_config, NullIO()) authenticator.request("get", "https://foo.bar/files/foo-0.1.0.tar.gz") @@ -85,7 +85,7 @@ def test_authenticator_uses_username_only_credentials( mock_remote: None, http: type[httpretty.httpretty], with_simple_keyring: None, -): +) -> None: authenticator = Authenticator(mock_config, NullIO()) authenticator.request("get", "https://foo001@foo.bar/files/foo-0.1.0.tar.gz") @@ -96,7 +96,7 @@ def test_authenticator_uses_username_only_credentials( def test_authenticator_uses_password_only_credentials( mock_config: Config, mock_remote: None, http: type[httpretty.httpretty] -): +) -> None: authenticator = Authenticator(mock_config, NullIO()) authenticator.request("get", "https://:bar002@foo.bar/files/foo-0.1.0.tar.gz") @@ -111,7 +111,7 @@ def test_authenticator_uses_empty_strings_as_default_password( repo: dict[str, dict[str, str]], http: type[httpretty.httpretty], with_simple_keyring: None, -): +) -> None: config.merge( { "repositories": repo, @@ -132,7 +132,7 @@ def test_authenticator_uses_empty_strings_as_default_username( mock_remote: None, repo: dict[str, dict[str, str]], http: type[httpretty.httpretty], -): +) -> None: config.merge( { "repositories": repo, @@ -155,7 +155,7 @@ def test_authenticator_falls_back_to_keyring_url( http: type[httpretty.httpretty], with_simple_keyring: None, dummy_keyring: DummyBackend, -): +) -> None: config.merge( { "repositories": repo, @@ -163,7 +163,7 @@ def test_authenticator_falls_back_to_keyring_url( ) dummy_keyring.set_password( - "https://foo.bar/simple/", None, SimpleCredential(None, "bar") + "https://foo.bar/simple/", None, SimpleCredential("foo", "bar") ) authenticator = Authenticator(config, NullIO()) @@ -171,7 +171,7 @@ def test_authenticator_falls_back_to_keyring_url( request = http.last_request() - assert request.headers["Authorization"] == "Basic OmJhcg==" + assert request.headers["Authorization"] == "Basic Zm9vOmJhcg==" def test_authenticator_falls_back_to_keyring_netloc( @@ -181,35 +181,35 @@ def test_authenticator_falls_back_to_keyring_netloc( http: type[httpretty.httpretty], with_simple_keyring: None, dummy_keyring: DummyBackend, -): +) -> None: config.merge( { "repositories": repo, } ) - dummy_keyring.set_password("foo.bar", None, SimpleCredential(None, "bar")) + dummy_keyring.set_password("foo.bar", None, SimpleCredential("foo", "bar")) authenticator = Authenticator(config, NullIO()) authenticator.request("get", "https://foo.bar/files/foo-0.1.0.tar.gz") request = http.last_request() - assert request.headers["Authorization"] == "Basic OmJhcg==" + assert request.headers["Authorization"] == "Basic Zm9vOmJhcg==" @pytest.mark.filterwarnings("ignore::pytest.PytestUnhandledThreadExceptionWarning") def test_authenticator_request_retries_on_exception( mocker: MockerFixture, config: Config, http: type[httpretty.httpretty] -): +) -> None: sleep = mocker.patch("time.sleep") sdist_uri = f"https://foo.bar/files/{uuid.uuid4()!s}/foo-0.1.0.tar.gz" content = str(uuid.uuid4()) - seen = [] + seen: list[str] = [] def callback( - request: requests.Request, uri: str, response_headers: dict - ) -> list[int | dict | str]: + request: requests.Request, uri: str, response_headers: dict[str, str] + ) -> list[int | dict[str, str] | str]: if seen.count(uri) < 2: seen.append(uri) raise requests.exceptions.ConnectionError("Disconnected") @@ -226,7 +226,7 @@ def callback( @pytest.mark.filterwarnings("ignore::pytest.PytestUnhandledThreadExceptionWarning") def test_authenticator_request_raises_exception_when_attempts_exhausted( mocker: MockerFixture, config: Config, http: type[httpretty.httpretty] -): +) -> None: sleep = mocker.patch("time.sleep") sdist_uri = f"https://foo.bar/files/{uuid.uuid4()!s}/foo-0.1.0.tar.gz" @@ -242,6 +242,33 @@ def callback(*_: Any, **___: Any) -> None: assert sleep.call_count == 5 +def test_authenticator_request_respects_retry_header( + mocker: MockerFixture, + config: Config, + http: type[httpretty.httpretty], +) -> None: + sleep = mocker.patch("time.sleep") + sdist_uri = f"https://foo.bar/files/{uuid.uuid4()!s}/foo-0.1.0.tar.gz" + content = str(uuid.uuid4()) + seen: list[str] = [] + + def callback( + request: requests.Request, uri: str, response_headers: dict[str, str] + ) -> list[int | dict[str, str] | str]: + if not seen.count(uri): + seen.append(uri) + return [429, {"Retry-After": "42"}, "Retry later"] + + return [200, response_headers, content] + + http.register_uri(httpretty.GET, sdist_uri, body=callback) + authenticator = Authenticator(config, NullIO()) + + response = authenticator.request("get", sdist_uri) + assert sleep.call_args[0] == (42.0,) + assert response.text == content + + @pytest.mark.parametrize( ["status", "attempts"], [ @@ -249,7 +276,9 @@ def callback(*_: Any, **___: Any) -> None: (401, 0), (403, 0), (404, 0), - (500, 0), + (429, 5), + (500, 5), + (501, 5), (502, 5), (503, 5), (504, 5), @@ -261,14 +290,14 @@ def test_authenticator_request_retries_on_status_code( http: type[httpretty.httpretty], status: int, attempts: int, -): +) -> None: sleep = mocker.patch("time.sleep") sdist_uri = f"https://foo.bar/files/{uuid.uuid4()!s}/foo-0.1.0.tar.gz" content = str(uuid.uuid4()) def callback( - request: requests.Request, uri: str, response_headers: dict - ) -> list[int | dict | str]: + request: requests.Request, uri: str, response_headers: dict[str, str] + ) -> list[int | dict[str, str] | str]: return [status, response_headers, content] http.register_uri(httpretty.GET, sdist_uri, body=callback) @@ -290,7 +319,7 @@ def test_authenticator_uses_env_provided_credentials( mock_remote: type[httpretty.httpretty], http: type[httpretty.httpretty], monkeypatch: MonkeyPatch, -): +) -> None: monkeypatch.setenv("POETRY_HTTP_BASIC_FOO_USERNAME", "bar") monkeypatch.setenv("POETRY_HTTP_BASIC_FOO_PASSWORD", "baz") @@ -321,7 +350,7 @@ def test_authenticator_uses_certs_from_config_if_not_provided( mocker: MockerFixture, cert: str | None, client_cert: str | None, -): +) -> None: configured_cert = "/path/to/cert" configured_client_cert = "/path/to/client-cert" @@ -351,7 +380,7 @@ def test_authenticator_uses_certs_from_config_if_not_provided( def test_authenticator_uses_credentials_from_config_matched_by_url_path( config: Config, mock_remote: None, http: type[httpretty.httpretty] -): +) -> None: config.merge( { "repositories": { @@ -384,7 +413,7 @@ def test_authenticator_uses_credentials_from_config_matched_by_url_path( def test_authenticator_uses_credentials_from_config_with_at_sign_in_path( config: Config, mock_remote: None, http: type[httpretty.httpretty] -): +) -> None: config.merge( { "repositories": { @@ -410,7 +439,7 @@ def test_authenticator_falls_back_to_keyring_url_matched_by_path( http: type[httpretty.httpretty], with_simple_keyring: None, dummy_keyring: DummyBackend, -): +) -> None: config.merge( { "repositories": { @@ -421,10 +450,10 @@ def test_authenticator_falls_back_to_keyring_url_matched_by_path( ) dummy_keyring.set_password( - "https://foo.bar/alpha/files/simple/", None, SimpleCredential(None, "bar") + "https://foo.bar/alpha/files/simple/", None, SimpleCredential("foo", "bar") ) dummy_keyring.set_password( - "https://foo.bar/beta/files/simple/", None, SimpleCredential(None, "baz") + "https://foo.bar/beta/files/simple/", None, SimpleCredential("foo", "baz") ) authenticator = Authenticator(config, NullIO()) @@ -432,13 +461,13 @@ def test_authenticator_falls_back_to_keyring_url_matched_by_path( authenticator.request("get", "https://foo.bar/alpha/files/simple/foo-0.1.0.tar.gz") request = http.last_request() - basic_auth = base64.b64encode(b":bar").decode() + basic_auth = base64.b64encode(b"foo:bar").decode() assert request.headers["Authorization"] == f"Basic {basic_auth}" authenticator.request("get", "https://foo.bar/beta/files/simple/foo-0.1.0.tar.gz") request = http.last_request() - basic_auth = base64.b64encode(b":baz").decode() + basic_auth = base64.b64encode(b"foo:baz").decode() assert request.headers["Authorization"] == f"Basic {basic_auth}" @@ -448,7 +477,7 @@ def test_authenticator_uses_env_provided_credentials_matched_by_url_path( mock_remote: type[httpretty.httpretty], http: type[httpretty.httpretty], monkeypatch: MonkeyPatch, -): +) -> None: monkeypatch.setenv("POETRY_HTTP_BASIC_FOO_ALPHA_USERNAME", "bar") monkeypatch.setenv("POETRY_HTTP_BASIC_FOO_ALPHA_PASSWORD", "alpha") monkeypatch.setenv("POETRY_HTTP_BASIC_FOO_BETA_USERNAME", "baz") @@ -484,7 +513,7 @@ def test_authenticator_azure_feed_guid_credentials( http: type[httpretty.httpretty], with_simple_keyring: None, dummy_keyring: DummyBackend, -): +) -> None: config.merge( { "repositories": { @@ -529,7 +558,7 @@ def test_authenticator_add_repository( http: type[httpretty.httpretty], with_simple_keyring: None, dummy_keyring: DummyBackend, -): +) -> None: config.merge( { "http-basic": { @@ -565,7 +594,7 @@ def test_authenticator_git_repositories( http: type[httpretty.httpretty], with_simple_keyring: None, dummy_keyring: DummyBackend, -): +) -> None: config.merge( { "repositories": { diff --git a/tests/utils/test_cache.py b/tests/utils/test_cache.py index c1bbae5071a..3475ab25cb2 100644 --- a/tests/utils/test_cache.py +++ b/tests/utils/test_cache.py @@ -1,5 +1,8 @@ from __future__ import annotations +import shutil + +from pathlib import Path from typing import TYPE_CHECKING from typing import Any from typing import TypeVar @@ -9,18 +12,21 @@ import pytest from cachy import CacheManager +from packaging.tags import Tag +from poetry.core.packages.utils.link import Link +from poetry.utils.cache import ArtifactCache from poetry.utils.cache import FileCache +from poetry.utils.env import MockEnv if TYPE_CHECKING: - from pathlib import Path - from _pytest.monkeypatch import MonkeyPatch from pytest import FixtureRequest from pytest_mock import MockerFixture from tests.conftest import Config + from tests.types import FixtureDirGetter FILE_CACHE = Union[FileCache, CacheManager] @@ -192,3 +198,216 @@ def test_cachy_compatibility( assert cachy_file_cache.get("key3") == test_str assert cachy_file_cache.get("key4") == test_obj + + +def test_missing_cache_file(poetry_file_cache: FileCache) -> None: + poetry_file_cache.put("key1", "value") + + key1_path = ( + poetry_file_cache.path + / "81/74/09/96/87/a2/66/21/8174099687a26621f4e2cdd7cc03b3dacedb3fb962255b1aafd033cabe831530" # noqa: E501 + ) + assert key1_path.exists() + key1_path.unlink() # corrupt cache by removing a key file + + assert poetry_file_cache.get("key1") is None + + +def test_missing_cache_path(poetry_file_cache: FileCache) -> None: + poetry_file_cache.put("key1", "value") + + key1_partial_path = poetry_file_cache.path / "81/74/09/96/87/a2/" + assert key1_partial_path.exists() + shutil.rmtree( + key1_partial_path + ) # corrupt cache by removing a subdirectory containing a key file + + assert poetry_file_cache.get("key1") is None + + +@pytest.mark.parametrize( + "corrupt_payload", + [ + "", # empty file + b"\x00", # null + "99999999", # truncated file + '999999a999"value"', # corrupt lifetime + b'9999999999"va\xd8\x00"', # invalid unicode + "fil3systemFa!led", # garbage file + ], +) +def test_detect_corrupted_cache_key_file( + corrupt_payload: str | bytes, poetry_file_cache: FileCache +) -> None: + poetry_file_cache.put("key1", "value") + + key1_path = ( + poetry_file_cache.path + / "81/74/09/96/87/a2/66/21/8174099687a26621f4e2cdd7cc03b3dacedb3fb962255b1aafd033cabe831530" # noqa: E501 + ) + assert key1_path.exists() + + # original content: 9999999999"value" + + write_modes = {str: "w", bytes: "wb"} + with open(key1_path, write_modes[type(corrupt_payload)]) as f: + f.write(corrupt_payload) # write corrupt data + + assert poetry_file_cache.get("key1") is None + + +def test_get_cache_directory_for_link(tmp_path: Path) -> None: + cache = ArtifactCache(cache_dir=tmp_path) + directory = cache.get_cache_directory_for_link( + Link("https://files.python-poetry.org/poetry-1.1.0.tar.gz") + ) + + expected = Path( + f"{tmp_path.as_posix()}/11/4f/a8/" + "1c89d75547e4967082d30a28360401c82c83b964ddacee292201bf85f2" + ) + + assert directory == expected + + +@pytest.mark.parametrize("subdirectory", [None, "subdir"]) +def test_get_cache_directory_for_git(tmp_path: Path, subdirectory: str | None) -> None: + cache = ArtifactCache(cache_dir=tmp_path) + directory = cache.get_cache_directory_for_git( + url="https://github.com/demo/demo.git", ref="123456", subdirectory=subdirectory + ) + + if subdirectory: + expected = Path( + f"{tmp_path.as_posix()}/53/08/33/" + "7851e5806669aa15ab0c555b13bd5523978057323c6a23a9cee18ec51c" + ) + else: + expected = Path( + f"{tmp_path.as_posix()}/61/14/30/" + "7c57f8fd71e4eee40b18893b9b586cba45177f15e300f4fb8b14ccc933" + ) + + assert directory == expected + + +def test_get_cached_archives(fixture_dir: FixtureDirGetter) -> None: + distributions = fixture_dir("distributions") + cache = ArtifactCache(cache_dir=Path()) + + archives = cache._get_cached_archives(distributions) + + assert archives + assert set(archives) == set(distributions.glob("*.whl")) | set( + distributions.glob("*.tar.gz") + ) + + +@pytest.mark.parametrize( + ("link", "strict", "available_packages"), + [ + ( + "https://files.python-poetry.org/demo-0.1.0.tar.gz", + True, + [ + Path("/cache/demo-0.1.0-py2.py3-none-any"), + Path("/cache/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl"), + Path("/cache/demo-0.1.0-cp37-cp37-macosx_10_15_x86_64.whl"), + ], + ), + ( + "https://example.com/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", + False, + [], + ), + ], +) +def test_get_not_found_cached_archive_for_link( + mocker: MockerFixture, + link: str, + strict: bool, + available_packages: list[Path], +) -> None: + env = MockEnv( + version_info=(3, 8, 3), + marker_env={"interpreter_name": "cpython", "interpreter_version": "3.8.3"}, + supported_tags=[ + Tag("cp38", "cp38", "macosx_10_15_x86_64"), + Tag("py3", "none", "any"), + ], + ) + cache = ArtifactCache(cache_dir=Path()) + + mocker.patch.object( + cache, + "_get_cached_archives", + return_value=available_packages, + ) + + archive = cache.get_cached_archive_for_link(Link(link), strict=strict, env=env) + + assert archive is None + + +@pytest.mark.parametrize( + ("link", "cached", "strict"), + [ + ( + "https://files.python-poetry.org/demo-0.1.0.tar.gz", + "/cache/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", + False, + ), + ( + "https://example.com/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", + "/cache/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", + False, + ), + ( + "https://files.python-poetry.org/demo-0.1.0.tar.gz", + "/cache/demo-0.1.0.tar.gz", + True, + ), + ( + "https://example.com/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", + "/cache/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", + True, + ), + ], +) +def test_get_found_cached_archive_for_link( + mocker: MockerFixture, + link: str, + cached: str, + strict: bool, +) -> None: + env = MockEnv( + version_info=(3, 8, 3), + marker_env={"interpreter_name": "cpython", "interpreter_version": "3.8.3"}, + supported_tags=[ + Tag("cp38", "cp38", "macosx_10_15_x86_64"), + Tag("py3", "none", "any"), + ], + ) + cache = ArtifactCache(cache_dir=Path()) + + mocker.patch.object( + cache, + "_get_cached_archives", + return_value=[ + Path("/cache/demo-0.1.0-py2.py3-none-any"), + Path("/cache/demo-0.1.0.tar.gz"), + Path("/cache/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl"), + Path("/cache/demo-0.1.0-cp37-cp37-macosx_10_15_x86_64.whl"), + ], + ) + + archive = cache.get_cached_archive_for_link(Link(link), strict=strict, env=env) + + assert Path(cached) == archive + + +def test_get_cached_archive_for_git() -> None: + """Smoke test that checks that no assertion is raised.""" + cache = ArtifactCache(cache_dir=Path()) + archive = cache.get_cached_archive_for_git("url", "ref", "subdirectory", MockEnv()) + assert archive is None diff --git a/tests/utils/test_env.py b/tests/utils/test_env.py index 52f1352dd5b..a5f8957424f 100644 --- a/tests/utils/test_env.py +++ b/tests/utils/test_env.py @@ -5,20 +5,21 @@ import sys from pathlib import Path +from threading import Thread from typing import TYPE_CHECKING from typing import Any import pytest import tomlkit -from cleo.io.null_io import NullIO from poetry.core.constraints.version import Version -from poetry.core.toml.file import TOMLFile from poetry.factory import Factory from poetry.repositories.installed_repository import InstalledRepository +from poetry.toml.file import TOMLFile from poetry.utils._compat import WINDOWS from poetry.utils.env import GET_BASE_PREFIX +from poetry.utils.env import GET_PYTHON_VERSION_ONELINER from poetry.utils.env import EnvCommandError from poetry.utils.env import EnvManager from poetry.utils.env import GenericEnv @@ -26,6 +27,7 @@ from poetry.utils.env import InvalidCurrentPythonVersionError from poetry.utils.env import MockEnv from poetry.utils.env import NoCompatiblePythonVersionFound +from poetry.utils.env import PythonVersionNotFound from poetry.utils.env import SystemEnv from poetry.utils.env import VirtualEnv from poetry.utils.env import build_environment @@ -40,6 +42,7 @@ from poetry.poetry import Poetry from tests.conftest import Config + from tests.types import FixtureDirGetter from tests.types import ProjectFactory MINIMAL_SCRIPT = """\ @@ -67,7 +70,7 @@ def __init__( self._sys_path = sys_path @property - def sys_path(self) -> list[str] | None: + def sys_path(self) -> list[str]: if self._sys_path is not None: return self._sys_path @@ -75,9 +78,8 @@ def sys_path(self) -> list[str] | None: @pytest.fixture() -def poetry(project_factory: ProjectFactory) -> Poetry: - fixture = Path(__file__).parent.parent / "fixtures" / "simple_project" - return project_factory("simple", source=fixture) +def poetry(project_factory: ProjectFactory, fixture_dir: FixtureDirGetter) -> Poetry: + return project_factory("simple", source=fixture_dir("simple_project")) @pytest.fixture() @@ -86,24 +88,24 @@ def manager(poetry: Poetry) -> EnvManager: def test_virtualenvs_with_spaces_in_their_path_work_as_expected( - tmp_dir: str, manager: EnvManager -): - venv_path = Path(tmp_dir) / "Virtual Env" + tmp_path: Path, manager: EnvManager +) -> None: + venv_path = tmp_path / "Virtual Env" - manager.build_venv(str(venv_path)) + manager.build_venv(venv_path) venv = VirtualEnv(venv_path) - assert venv.run("python", "-V", shell=True).startswith("Python") + assert venv.run("python", "-V").startswith("Python") @pytest.mark.skipif(sys.platform != "darwin", reason="requires darwin") -def test_venv_backup_exclusion(tmp_dir: str, manager: EnvManager): +def test_venv_backup_exclusion(tmp_path: Path, manager: EnvManager) -> None: import xattr - venv_path = Path(tmp_dir) / "Virtual Env" + venv_path = tmp_path / "Virtual Env" - manager.build_venv(str(venv_path)) + manager.build_venv(venv_path) value = ( b"bplist00_\x10\x11com.apple.backupd" @@ -119,34 +121,32 @@ def test_venv_backup_exclusion(tmp_dir: str, manager: EnvManager): def test_env_commands_with_spaces_in_their_arg_work_as_expected( - tmp_dir: str, manager: EnvManager -): - venv_path = Path(tmp_dir) / "Virtual Env" - manager.build_venv(str(venv_path)) + tmp_path: Path, manager: EnvManager +) -> None: + venv_path = tmp_path / "Virtual Env" + manager.build_venv(venv_path) venv = VirtualEnv(venv_path) - assert venv.run("python", venv.pip, "--version", shell=True).startswith( + assert venv.run("python", str(venv.pip), "--version").startswith( f"pip {venv.pip_version} from " ) def test_env_shell_commands_with_stdinput_in_their_arg_work_as_expected( - tmp_dir: str, manager: EnvManager -): - venv_path = Path(tmp_dir) / "Virtual Env" - manager.build_venv(str(venv_path)) + tmp_path: Path, manager: EnvManager +) -> None: + venv_path = tmp_path / "Virtual Env" + manager.build_venv(venv_path) venv = VirtualEnv(venv_path) - run_output_path = Path( - venv.run("python", "-", input_=GET_BASE_PREFIX, shell=True).strip() - ) + run_output_path = Path(venv.run("python", "-", input_=GET_BASE_PREFIX).strip()) venv_base_prefix_path = Path(str(venv.get_base_prefix())) assert run_output_path.resolve() == venv_base_prefix_path.resolve() def test_env_get_supported_tags_matches_inside_virtualenv( - tmp_dir: str, manager: EnvManager -): - venv_path = Path(tmp_dir) / "Virtual Env" - manager.build_venv(str(venv_path)) + tmp_path: Path, manager: EnvManager +) -> None: + venv_path = tmp_path / "Virtual Env" + manager.build_venv(venv_path) venv = VirtualEnv(venv_path) import packaging.tags @@ -171,7 +171,7 @@ def test_env_get_venv_with_venv_folder_present( poetry: Poetry, in_project_venv_dir: Path, in_project: bool | None, -): +) -> None: poetry.config.config["virtualenvs"]["in-project"] = in_project venv = manager.get() if in_project is False: @@ -189,33 +189,39 @@ def build_venv(path: Path | str, **__: Any) -> None: def check_output_wrapper( version: Version = VERSION_3_7_1, -) -> Callable[[str, Any, Any], str]: - def check_output(cmd: str, *args: Any, **kwargs: Any) -> str: - if "sys.version_info[:3]" in cmd: +) -> Callable[[list[str], Any, Any], str]: + def check_output(cmd: list[str], *args: Any, **kwargs: Any) -> str: + # cmd is a list, like ["python", "-c", "do stuff"] + python_cmd = cmd[2] + if "sys.version_info[:3]" in python_cmd: return version.text - elif "sys.version_info[:2]" in cmd: + elif "sys.version_info[:2]" in python_cmd: return f"{version.major}.{version.minor}" - elif '-c "import sys; print(sys.executable)"' in cmd: - return f"/usr/bin/{cmd.split()[0]}" + elif "import sys; print(sys.executable)" in python_cmd: + executable = cmd[0] + basename = os.path.basename(executable) + return f"/usr/bin/{basename}" else: - return str(Path("/prefix")) + assert "import sys; print(sys.prefix)" in python_cmd + return "/prefix" return check_output def test_activate_activates_non_existing_virtualenv_no_envs_file( - tmp_dir: str, + tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, -): +) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] - config.merge({"virtualenvs": {"path": str(tmp_dir)}}) + config.merge({"virtualenvs": {"path": str(tmp_path)}}) + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(), @@ -226,11 +232,11 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file( ) m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv) - env = manager.activate("python3.7", NullIO()) + env = manager.activate("python3.7") m.assert_called_with( - Path(tmp_dir) / f"{venv_name}-py3.7", - executable="/usr/bin/python3.7", + tmp_path / f"{venv_name}-py3.7", + executable=Path("/usr/bin/python3.7"), flags={ "always-copy": False, "system-site-packages": False, @@ -240,31 +246,56 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file( prompt="simple-project-py3.7", ) - envs_file = TOMLFile(Path(tmp_dir) / "envs.toml") + envs_file = TOMLFile(tmp_path / "envs.toml") assert envs_file.exists() envs = envs_file.read() assert envs[venv_name]["minor"] == "3.7" assert envs[venv_name]["patch"] == "3.7.1" - assert env.path == Path(tmp_dir) / f"{venv_name}-py3.7" + assert env.path == tmp_path / f"{venv_name}-py3.7" assert env.base == Path("/prefix") +def test_activate_fails_when_python_cannot_be_found( + tmp_path: Path, + manager: EnvManager, + poetry: Poetry, + config: Config, + mocker: MockerFixture, + venv_name: str, +) -> None: + if "VIRTUAL_ENV" in os.environ: + del os.environ["VIRTUAL_ENV"] + + os.mkdir(tmp_path / f"{venv_name}-py3.7") + + config.merge({"virtualenvs": {"path": str(tmp_path)}}) + + mocker.patch("shutil.which", return_value=None) + + with pytest.raises(PythonVersionNotFound) as e: + manager.activate("python3.7") + + expected_message = "Could not find the python executable python3.7" + assert str(e.value) == expected_message + + def test_activate_activates_existing_virtualenv_no_envs_file( - tmp_dir: str, + tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, -): +) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] - os.mkdir(os.path.join(tmp_dir, f"{venv_name}-py3.7")) + os.mkdir(tmp_path / f"{venv_name}-py3.7") - config.merge({"virtualenvs": {"path": str(tmp_dir)}}) + config.merge({"virtualenvs": {"path": str(tmp_path)}}) + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(), @@ -275,40 +306,41 @@ def test_activate_activates_existing_virtualenv_no_envs_file( ) m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv) - env = manager.activate("python3.7", NullIO()) + env = manager.activate("python3.7") m.assert_not_called() - envs_file = TOMLFile(Path(tmp_dir) / "envs.toml") + envs_file = TOMLFile(tmp_path / "envs.toml") assert envs_file.exists() envs = envs_file.read() assert envs[venv_name]["minor"] == "3.7" assert envs[venv_name]["patch"] == "3.7.1" - assert env.path == Path(tmp_dir) / f"{venv_name}-py3.7" + assert env.path == tmp_path / f"{venv_name}-py3.7" assert env.base == Path("/prefix") def test_activate_activates_same_virtualenv_with_envs_file( - tmp_dir: str, + tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, -): +) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] - envs_file = TOMLFile(Path(tmp_dir) / "envs.toml") + envs_file = TOMLFile(tmp_path / "envs.toml") doc = tomlkit.document() doc[venv_name] = {"minor": "3.7", "patch": "3.7.1"} envs_file.write(doc) - os.mkdir(os.path.join(tmp_dir, f"{venv_name}-py3.7")) + os.mkdir(tmp_path / f"{venv_name}-py3.7") - config.merge({"virtualenvs": {"path": str(tmp_dir)}}) + config.merge({"virtualenvs": {"path": str(tmp_path)}}) + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(), @@ -319,7 +351,7 @@ def test_activate_activates_same_virtualenv_with_envs_file( ) m = mocker.patch("poetry.utils.env.EnvManager.create_venv") - env = manager.activate("python3.7", NullIO()) + env = manager.activate("python3.7") m.assert_not_called() @@ -328,30 +360,31 @@ def test_activate_activates_same_virtualenv_with_envs_file( assert envs[venv_name]["minor"] == "3.7" assert envs[venv_name]["patch"] == "3.7.1" - assert env.path == Path(tmp_dir) / f"{venv_name}-py3.7" + assert env.path == tmp_path / f"{venv_name}-py3.7" assert env.base == Path("/prefix") def test_activate_activates_different_virtualenv_with_envs_file( - tmp_dir: str, + tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, -): +) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] - envs_file = TOMLFile(Path(tmp_dir) / "envs.toml") + envs_file = TOMLFile(tmp_path / "envs.toml") doc = tomlkit.document() doc[venv_name] = {"minor": "3.7", "patch": "3.7.1"} envs_file.write(doc) - os.mkdir(os.path.join(tmp_dir, f"{venv_name}-py3.7")) + os.mkdir(tmp_path / f"{venv_name}-py3.7") - config.merge({"virtualenvs": {"path": str(tmp_dir)}}) + config.merge({"virtualenvs": {"path": str(tmp_path)}}) + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.6")), @@ -362,11 +395,11 @@ def test_activate_activates_different_virtualenv_with_envs_file( ) m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv) - env = manager.activate("python3.6", NullIO()) + env = manager.activate("python3.6") m.assert_called_with( - Path(tmp_dir) / f"{venv_name}-py3.6", - executable="/usr/bin/python3.6", + tmp_path / f"{venv_name}-py3.6", + executable=Path("/usr/bin/python3.6"), flags={ "always-copy": False, "system-site-packages": False, @@ -381,30 +414,31 @@ def test_activate_activates_different_virtualenv_with_envs_file( assert envs[venv_name]["minor"] == "3.6" assert envs[venv_name]["patch"] == "3.6.6" - assert env.path == Path(tmp_dir) / f"{venv_name}-py3.6" + assert env.path == tmp_path / f"{venv_name}-py3.6" assert env.base == Path("/prefix") def test_activate_activates_recreates_for_different_patch( - tmp_dir: str, + tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, -): +) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] - envs_file = TOMLFile(Path(tmp_dir) / "envs.toml") + envs_file = TOMLFile(tmp_path / "envs.toml") doc = tomlkit.document() doc[venv_name] = {"minor": "3.7", "patch": "3.7.0"} envs_file.write(doc) - os.mkdir(os.path.join(tmp_dir, f"{venv_name}-py3.7")) + os.mkdir(tmp_path / f"{venv_name}-py3.7") - config.merge({"virtualenvs": {"path": str(tmp_dir)}}) + config.merge({"virtualenvs": {"path": str(tmp_path)}}) + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(), @@ -426,11 +460,11 @@ def test_activate_activates_recreates_for_different_patch( "poetry.utils.env.EnvManager.remove_venv", side_effect=EnvManager.remove_venv ) - env = manager.activate("python3.7", NullIO()) + env = manager.activate("python3.7") build_venv_m.assert_called_with( - Path(tmp_dir) / f"{venv_name}-py3.7", - executable="/usr/bin/python3.7", + tmp_path / f"{venv_name}-py3.7", + executable=Path("/usr/bin/python3.7"), flags={ "always-copy": False, "system-site-packages": False, @@ -439,39 +473,40 @@ def test_activate_activates_recreates_for_different_patch( }, prompt="simple-project-py3.7", ) - remove_venv_m.assert_called_with(Path(tmp_dir) / f"{venv_name}-py3.7") + remove_venv_m.assert_called_with(tmp_path / f"{venv_name}-py3.7") assert envs_file.exists() envs = envs_file.read() assert envs[venv_name]["minor"] == "3.7" assert envs[venv_name]["patch"] == "3.7.1" - assert env.path == Path(tmp_dir) / f"{venv_name}-py3.7" + assert env.path == tmp_path / f"{venv_name}-py3.7" assert env.base == Path("/prefix") - assert (Path(tmp_dir) / f"{venv_name}-py3.7").exists() + assert (tmp_path / f"{venv_name}-py3.7").exists() def test_activate_does_not_recreate_when_switching_minor( - tmp_dir: str, + tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, -): +) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] - envs_file = TOMLFile(Path(tmp_dir) / "envs.toml") + envs_file = TOMLFile(tmp_path / "envs.toml") doc = tomlkit.document() doc[venv_name] = {"minor": "3.7", "patch": "3.7.0"} envs_file.write(doc) - os.mkdir(os.path.join(tmp_dir, f"{venv_name}-py3.7")) - os.mkdir(os.path.join(tmp_dir, f"{venv_name}-py3.6")) + os.mkdir(tmp_path / f"{venv_name}-py3.7") + os.mkdir(tmp_path / f"{venv_name}-py3.6") - config.merge({"virtualenvs": {"path": str(tmp_dir)}}) + config.merge({"virtualenvs": {"path": str(tmp_path)}}) + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.6")), @@ -487,7 +522,7 @@ def test_activate_does_not_recreate_when_switching_minor( "poetry.utils.env.EnvManager.remove_venv", side_effect=EnvManager.remove_venv ) - env = manager.activate("python3.6", NullIO()) + env = manager.activate("python3.6") build_venv_m.assert_not_called() remove_venv_m.assert_not_called() @@ -497,58 +532,55 @@ def test_activate_does_not_recreate_when_switching_minor( assert envs[venv_name]["minor"] == "3.6" assert envs[venv_name]["patch"] == "3.6.6" - assert env.path == Path(tmp_dir) / f"{venv_name}-py3.6" + assert env.path == tmp_path / f"{venv_name}-py3.6" assert env.base == Path("/prefix") - assert (Path(tmp_dir) / f"{venv_name}-py3.6").exists() + assert (tmp_path / f"{venv_name}-py3.6").exists() def test_deactivate_non_activated_but_existing( - tmp_dir: str, + tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, -): +) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] python = ".".join(str(c) for c in sys.version_info[:2]) - (Path(tmp_dir) / f"{venv_name}-py{python}").mkdir() + (tmp_path / f"{venv_name}-py{python}").mkdir() - config.merge({"virtualenvs": {"path": str(tmp_dir)}}) + config.merge({"virtualenvs": {"path": str(tmp_path)}}) mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(), ) - manager.deactivate(NullIO()) + manager.deactivate() env = manager.get() - assert env.path == Path(tmp_dir) / f"{venv_name}-py{python}" - assert Path("/prefix") + assert env.path == tmp_path / f"{venv_name}-py{python}" def test_deactivate_activated( - tmp_dir: str, + tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, -): +) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] version = Version.from_parts(*sys.version_info[:3]) other_version = Version.parse("3.4") if version.major == 2 else version.next_minor() - (Path(tmp_dir) / f"{venv_name}-py{version.major}.{version.minor}").mkdir() - ( - Path(tmp_dir) / f"{venv_name}-py{other_version.major}.{other_version.minor}" - ).mkdir() + (tmp_path / f"{venv_name}-py{version.major}.{version.minor}").mkdir() + (tmp_path / f"{venv_name}-py{other_version.major}.{other_version.minor}").mkdir() - envs_file = TOMLFile(Path(tmp_dir) / "envs.toml") + envs_file = TOMLFile(tmp_path / "envs.toml") doc = tomlkit.document() doc[venv_name] = { "minor": f"{other_version.major}.{other_version.minor}", @@ -556,37 +588,36 @@ def test_deactivate_activated( } envs_file.write(doc) - config.merge({"virtualenvs": {"path": str(tmp_dir)}}) + config.merge({"virtualenvs": {"path": str(tmp_path)}}) mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(), ) - manager.deactivate(NullIO()) + manager.deactivate() env = manager.get() - assert env.path == Path(tmp_dir) / f"{venv_name}-py{version.major}.{version.minor}" - assert Path("/prefix") + assert env.path == tmp_path / f"{venv_name}-py{version.major}.{version.minor}" envs = envs_file.read() assert len(envs) == 0 def test_get_prefers_explicitly_activated_virtualenvs_over_env_var( - tmp_dir: str, + tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, -): +) -> None: os.environ["VIRTUAL_ENV"] = "/environment/prefix" - config.merge({"virtualenvs": {"path": str(tmp_dir)}}) - (Path(tmp_dir) / f"{venv_name}-py3.7").mkdir() + config.merge({"virtualenvs": {"path": str(tmp_path)}}) + (tmp_path / f"{venv_name}-py3.7").mkdir() - envs_file = TOMLFile(Path(tmp_dir) / "envs.toml") + envs_file = TOMLFile(tmp_path / "envs.toml") doc = tomlkit.document() doc[venv_name] = {"minor": "3.7", "patch": "3.7.0"} envs_file.write(doc) @@ -602,41 +633,41 @@ def test_get_prefers_explicitly_activated_virtualenvs_over_env_var( env = manager.get() - assert env.path == Path(tmp_dir) / f"{venv_name}-py3.7" + assert env.path == tmp_path / f"{venv_name}-py3.7" assert env.base == Path("/prefix") def test_list( - tmp_dir: str, + tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, venv_name: str, -): - config.merge({"virtualenvs": {"path": str(tmp_dir)}}) +) -> None: + config.merge({"virtualenvs": {"path": str(tmp_path)}}) - (Path(tmp_dir) / f"{venv_name}-py3.7").mkdir() - (Path(tmp_dir) / f"{venv_name}-py3.6").mkdir() + (tmp_path / f"{venv_name}-py3.7").mkdir() + (tmp_path / f"{venv_name}-py3.6").mkdir() venvs = manager.list() assert len(venvs) == 2 - assert venvs[0].path == (Path(tmp_dir) / f"{venv_name}-py3.6") - assert venvs[1].path == (Path(tmp_dir) / f"{venv_name}-py3.7") + assert venvs[0].path == tmp_path / f"{venv_name}-py3.6" + assert venvs[1].path == tmp_path / f"{venv_name}-py3.7" def test_remove_by_python_version( - tmp_dir: str, + tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, -): - config.merge({"virtualenvs": {"path": str(tmp_dir)}}) +) -> None: + config.merge({"virtualenvs": {"path": str(tmp_path)}}) - (Path(tmp_dir) / f"{venv_name}-py3.7").mkdir() - (Path(tmp_dir) / f"{venv_name}-py3.6").mkdir() + (tmp_path / f"{venv_name}-py3.7").mkdir() + (tmp_path / f"{venv_name}-py3.6").mkdir() mocker.patch( "subprocess.check_output", @@ -645,23 +676,23 @@ def test_remove_by_python_version( venv = manager.remove("3.6") - expected_venv_path = Path(tmp_dir) / f"{venv_name}-py3.6" + expected_venv_path = tmp_path / f"{venv_name}-py3.6" assert venv.path == expected_venv_path assert not expected_venv_path.exists() def test_remove_by_name( - tmp_dir: str, + tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, -): - config.merge({"virtualenvs": {"path": str(tmp_dir)}}) +) -> None: + config.merge({"virtualenvs": {"path": str(tmp_path)}}) - (Path(tmp_dir) / f"{venv_name}-py3.7").mkdir() - (Path(tmp_dir) / f"{venv_name}-py3.6").mkdir() + (tmp_path / f"{venv_name}-py3.7").mkdir() + (tmp_path / f"{venv_name}-py3.6").mkdir() mocker.patch( "subprocess.check_output", @@ -670,23 +701,23 @@ def test_remove_by_name( venv = manager.remove(f"{venv_name}-py3.6") - expected_venv_path = Path(tmp_dir) / f"{venv_name}-py3.6" + expected_venv_path = tmp_path / f"{venv_name}-py3.6" assert venv.path == expected_venv_path assert not expected_venv_path.exists() def test_remove_by_string_with_python_and_version( - tmp_dir: str, + tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, -): - config.merge({"virtualenvs": {"path": str(tmp_dir)}}) +) -> None: + config.merge({"virtualenvs": {"path": str(tmp_path)}}) - (Path(tmp_dir) / f"{venv_name}-py3.7").mkdir() - (Path(tmp_dir) / f"{venv_name}-py3.6").mkdir() + (tmp_path / f"{venv_name}-py3.7").mkdir() + (tmp_path / f"{venv_name}-py3.6").mkdir() mocker.patch( "subprocess.check_output", @@ -695,30 +726,30 @@ def test_remove_by_string_with_python_and_version( venv = manager.remove("python3.6") - expected_venv_path = Path(tmp_dir) / f"{venv_name}-py3.6" + expected_venv_path = tmp_path / f"{venv_name}-py3.6" assert venv.path == expected_venv_path assert not expected_venv_path.exists() def test_remove_by_full_path_to_python( - tmp_dir: str, + tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, -): - config.merge({"virtualenvs": {"path": str(tmp_dir)}}) +) -> None: + config.merge({"virtualenvs": {"path": str(tmp_path)}}) - (Path(tmp_dir) / f"{venv_name}-py3.7").mkdir() - (Path(tmp_dir) / f"{venv_name}-py3.6").mkdir() + (tmp_path / f"{venv_name}-py3.7").mkdir() + (tmp_path / f"{venv_name}-py3.6").mkdir() mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.6")), ) - expected_venv_path = Path(tmp_dir) / f"{venv_name}-py3.6" + expected_venv_path = tmp_path / f"{venv_name}-py3.6" python_path = expected_venv_path / "bin" / "python" venv = manager.remove(str(python_path)) @@ -728,16 +759,16 @@ def test_remove_by_full_path_to_python( def test_raises_if_acting_on_different_project_by_full_path( - tmp_dir: str, + tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, -): - config.merge({"virtualenvs": {"path": str(tmp_dir)}}) +) -> None: + config.merge({"virtualenvs": {"path": str(tmp_path)}}) different_venv_name = "different-project" - different_venv_path = Path(tmp_dir) / f"{different_venv_name}-py3.6" + different_venv_path = tmp_path / f"{different_venv_name}-py3.6" different_venv_bin_path = different_venv_path / "bin" different_venv_bin_path.mkdir(parents=True) @@ -755,12 +786,12 @@ def test_raises_if_acting_on_different_project_by_full_path( def test_raises_if_acting_on_different_project_by_name( - tmp_dir: str, + tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, -): - config.merge({"virtualenvs": {"path": str(tmp_dir)}}) +) -> None: + config.merge({"virtualenvs": {"path": str(tmp_path)}}) different_venv_name = ( EnvManager.generate_env_name( @@ -769,7 +800,7 @@ def test_raises_if_acting_on_different_project_by_name( ) + "-py3.6" ) - different_venv_path = Path(tmp_dir) / different_venv_name + different_venv_path = tmp_path / different_venv_name different_venv_bin_path = different_venv_path / "bin" different_venv_bin_path.mkdir(parents=True) @@ -781,27 +812,27 @@ def test_raises_if_acting_on_different_project_by_name( def test_raises_when_passing_old_env_after_dir_rename( - tmp_dir: str, + tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, venv_name: str, -): +) -> None: # Make sure that poetry raises when trying to remove old venv after you've renamed # root directory of the project, which will create another venv with new name. # This is not ideal as you still "can't" remove it by name, but it at least doesn't # cause any unwanted side effects - config.merge({"virtualenvs": {"path": str(tmp_dir)}}) + config.merge({"virtualenvs": {"path": str(tmp_path)}}) previous_venv_name = EnvManager.generate_env_name( poetry.package.name, "previous_dir_name", ) - venv_path = Path(tmp_dir) / f"{venv_name}-py3.6" + venv_path = tmp_path / f"{venv_name}-py3.6" venv_path.mkdir() previous_venv_name = f"{previous_venv_name}-py3.6" - previous_venv_path = Path(tmp_dir) / previous_venv_name + previous_venv_path = tmp_path / previous_venv_name previous_venv_path.mkdir() with pytest.raises(IncorrectEnvError): @@ -809,31 +840,31 @@ def test_raises_when_passing_old_env_after_dir_rename( def test_remove_also_deactivates( - tmp_dir: str, + tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, -): - config.merge({"virtualenvs": {"path": str(tmp_dir)}}) +) -> None: + config.merge({"virtualenvs": {"path": str(tmp_path)}}) - (Path(tmp_dir) / f"{venv_name}-py3.7").mkdir() - (Path(tmp_dir) / f"{venv_name}-py3.6").mkdir() + (tmp_path / f"{venv_name}-py3.7").mkdir() + (tmp_path / f"{venv_name}-py3.6").mkdir() mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.6")), ) - envs_file = TOMLFile(Path(tmp_dir) / "envs.toml") + envs_file = TOMLFile(tmp_path / "envs.toml") doc = tomlkit.document() doc[venv_name] = {"minor": "3.6", "patch": "3.6.6"} envs_file.write(doc) venv = manager.remove("python3.6") - expected_venv_path = Path(tmp_dir) / f"{venv_name}-py3.6" + expected_venv_path = tmp_path / f"{venv_name}-py3.6" assert venv.path == expected_venv_path assert not expected_venv_path.exists() @@ -842,18 +873,18 @@ def test_remove_also_deactivates( def test_remove_keeps_dir_if_not_deleteable( - tmp_dir: str, + tmp_path: Path, manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture, venv_name: str, -): +) -> None: # Ensure we empty rather than delete folder if its is an active mount point. # See https://github.com/python-poetry/poetry/pull/2064 - config.merge({"virtualenvs": {"path": str(tmp_dir)}}) + config.merge({"virtualenvs": {"path": str(tmp_path)}}) - venv_path = Path(tmp_dir) / f"{venv_name}-py3.6" + venv_path = tmp_path / f"{venv_name}-py3.6" venv_path.mkdir() folder1_path = venv_path / "folder1" @@ -895,17 +926,17 @@ def err_on_rm_venv_only(path: Path | str, *args: Any, **kwargs: Any) -> None: @pytest.mark.skipif(os.name == "nt", reason="Symlinks are not support for Windows") -def test_env_has_symlinks_on_nix(tmp_dir: str, tmp_venv: VirtualEnv): +def test_env_has_symlinks_on_nix(tmp_path: Path, tmp_venv: VirtualEnv) -> None: assert os.path.islink(tmp_venv.python) -def test_run_with_input(tmp_dir: str, tmp_venv: VirtualEnv): +def test_run_with_input(tmp_path: Path, tmp_venv: VirtualEnv) -> None: result = tmp_venv.run("python", "-", input_=MINIMAL_SCRIPT) assert result == "Minimal Output" + os.linesep -def test_run_with_input_non_zero_return(tmp_dir: str, tmp_venv: VirtualEnv): +def test_run_with_input_non_zero_return(tmp_path: Path, tmp_venv: VirtualEnv) -> None: with pytest.raises(EnvCommandError) as process_error: # Test command that will return non-zero returncode. tmp_venv.run("python", "-", input_=ERRORING_SCRIPT) @@ -914,8 +945,8 @@ def test_run_with_input_non_zero_return(tmp_dir: str, tmp_venv: VirtualEnv): def test_run_with_keyboard_interrupt( - tmp_dir: str, tmp_venv: VirtualEnv, mocker: MockerFixture -): + tmp_path: Path, tmp_venv: VirtualEnv, mocker: MockerFixture +) -> None: mocker.patch("subprocess.run", side_effect=KeyboardInterrupt()) with pytest.raises(KeyboardInterrupt): tmp_venv.run("python", "-", input_=MINIMAL_SCRIPT) @@ -923,8 +954,8 @@ def test_run_with_keyboard_interrupt( def test_call_with_input_and_keyboard_interrupt( - tmp_dir: str, tmp_venv: VirtualEnv, mocker: MockerFixture -): + tmp_path: Path, tmp_venv: VirtualEnv, mocker: MockerFixture +) -> None: mocker.patch("subprocess.run", side_effect=KeyboardInterrupt()) kwargs = {"call": True} with pytest.raises(KeyboardInterrupt): @@ -933,8 +964,8 @@ def test_call_with_input_and_keyboard_interrupt( def test_call_no_input_with_keyboard_interrupt( - tmp_dir: str, tmp_venv: VirtualEnv, mocker: MockerFixture -): + tmp_path: Path, tmp_venv: VirtualEnv, mocker: MockerFixture +) -> None: mocker.patch("subprocess.call", side_effect=KeyboardInterrupt()) kwargs = {"call": True} with pytest.raises(KeyboardInterrupt): @@ -943,38 +974,117 @@ def test_call_no_input_with_keyboard_interrupt( def test_run_with_called_process_error( - tmp_dir: str, tmp_venv: VirtualEnv, mocker: MockerFixture -): + tmp_path: Path, tmp_venv: VirtualEnv, mocker: MockerFixture +) -> None: mocker.patch( - "subprocess.run", side_effect=subprocess.CalledProcessError(42, "some_command") + "subprocess.run", + side_effect=subprocess.CalledProcessError( + 42, "some_command", "some output", "some error" + ), ) - with pytest.raises(EnvCommandError): + with pytest.raises(EnvCommandError) as error: tmp_venv.run("python", "-", input_=MINIMAL_SCRIPT) subprocess.run.assert_called_once() + assert "some output" in str(error.value) + assert "some error" in str(error.value) def test_call_with_input_and_called_process_error( - tmp_dir: str, tmp_venv: VirtualEnv, mocker: MockerFixture -): + tmp_path: Path, tmp_venv: VirtualEnv, mocker: MockerFixture +) -> None: mocker.patch( - "subprocess.run", side_effect=subprocess.CalledProcessError(42, "some_command") + "subprocess.run", + side_effect=subprocess.CalledProcessError( + 42, "some_command", "some output", "some error" + ), ) kwargs = {"call": True} - with pytest.raises(EnvCommandError): + with pytest.raises(EnvCommandError) as error: tmp_venv.run("python", "-", input_=MINIMAL_SCRIPT, **kwargs) subprocess.run.assert_called_once() + assert "some output" in str(error.value) + assert "some error" in str(error.value) def test_call_no_input_with_called_process_error( - tmp_dir: str, tmp_venv: VirtualEnv, mocker: MockerFixture -): + tmp_path: Path, tmp_venv: VirtualEnv, mocker: MockerFixture +) -> None: mocker.patch( - "subprocess.call", side_effect=subprocess.CalledProcessError(42, "some_command") + "subprocess.call", + side_effect=subprocess.CalledProcessError( + 42, "some_command", "some output", "some error" + ), ) kwargs = {"call": True} - with pytest.raises(EnvCommandError): + with pytest.raises(EnvCommandError) as error: tmp_venv.run("python", "-", **kwargs) subprocess.call.assert_called_once() + assert "some output" in str(error.value) + assert "some error" in str(error.value) + + +def test_check_output_with_called_process_error( + tmp_path: Path, tmp_venv: VirtualEnv, mocker: MockerFixture +) -> None: + mocker.patch( + "subprocess.check_output", + side_effect=subprocess.CalledProcessError( + 42, "some_command", "some output", "some error" + ), + ) + with pytest.raises(EnvCommandError) as error: + tmp_venv.run("python", "-") + subprocess.check_output.assert_called_once() + assert "some output" in str(error.value) + assert "some error" in str(error.value) + + +@pytest.mark.parametrize("out", ["sys.stdout", "sys.stderr"]) +def test_call_does_not_block_on_full_pipe( + tmp_path: Path, tmp_venv: VirtualEnv, out: str +) -> None: + """see https://github.com/python-poetry/poetry/issues/7698""" + script = tmp_path / "script.py" + script.write_text( + f"""\ +import sys +for i in range(10000): + print('just print a lot of text to fill the buffer', file={out}) +""" + ) + + def target(result: list[int]) -> None: + result.append(tmp_venv.run("python", str(script), call=True)) + + results = [] + # use a separate thread, so that the test does not block in case of error + thread = Thread(target=target, args=(results,)) + thread.start() + thread.join(1) # must not block + assert results and results[0] == 0 + + +def test_run_python_script_called_process_error( + tmp_path: Path, tmp_venv: VirtualEnv, mocker: MockerFixture +) -> None: + mocker.patch( + "subprocess.run", + side_effect=subprocess.CalledProcessError( + 42, "some_command", "some output", "some error" + ), + ) + with pytest.raises(EnvCommandError) as error: + tmp_venv.run_python_script(MINIMAL_SCRIPT) + assert "some output" in str(error.value) + assert "some error" in str(error.value) + + +def test_run_python_script_only_stdout(tmp_path: Path, tmp_venv: VirtualEnv) -> None: + output = tmp_venv.run_python_script( + "import sys; print('some warning', file=sys.stderr); print('some output')" + ) + assert "some output" in output + assert "some warning" not in output def test_create_venv_tries_to_find_a_compatible_python_executable_using_generic_ones_first( # noqa: E501 @@ -984,13 +1094,14 @@ def test_create_venv_tries_to_find_a_compatible_python_executable_using_generic_ mocker: MockerFixture, config_virtualenvs_path: Path, venv_name: str, -): +) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] poetry.package.python_versions = "^3.6" mocker.patch("sys.version_info", (2, 7, 16)) + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.7.5")), @@ -999,11 +1110,11 @@ def test_create_venv_tries_to_find_a_compatible_python_executable_using_generic_ "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) - manager.create_venv(NullIO()) + manager.create_venv() m.assert_called_with( config_virtualenvs_path / f"{venv_name}-py3.7", - executable="python3", + executable=Path("/usr/bin/python3"), flags={ "always-copy": False, "system-site-packages": False, @@ -1014,6 +1125,34 @@ def test_create_venv_tries_to_find_a_compatible_python_executable_using_generic_ ) +def test_create_venv_finds_no_python_executable( + manager: EnvManager, + poetry: Poetry, + config: Config, + mocker: MockerFixture, + config_virtualenvs_path: Path, + venv_name: str, +) -> None: + if "VIRTUAL_ENV" in os.environ: + del os.environ["VIRTUAL_ENV"] + + poetry.package.python_versions = "^3.6" + + mocker.patch("sys.version_info", (2, 7, 16)) + mocker.patch("shutil.which", return_value=None) + + with pytest.raises(NoCompatiblePythonVersionFound) as e: + manager.create_venv() + + expected_message = ( + "Poetry was unable to find a compatible version. " + "If you have one, you can explicitly use it " + 'via the "env use" command.' + ) + + assert str(e.value) == expected_message + + def test_create_venv_tries_to_find_a_compatible_python_executable_using_specific_ones( manager: EnvManager, poetry: Poetry, @@ -1021,23 +1160,27 @@ def test_create_venv_tries_to_find_a_compatible_python_executable_using_specific mocker: MockerFixture, config_virtualenvs_path: Path, venv_name: str, -): +) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] poetry.package.python_versions = "^3.6" mocker.patch("sys.version_info", (2, 7, 16)) - mocker.patch("subprocess.check_output", side_effect=["3.5.3", "3.9.0"]) + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") + mocker.patch( + "subprocess.check_output", + side_effect=["/usr/bin/python3", "3.5.3", "/usr/bin/python3.9", "3.9.0"], + ) m = mocker.patch( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) - manager.create_venv(NullIO()) + manager.create_venv() m.assert_called_with( config_virtualenvs_path / f"{venv_name}-py3.9", - executable="python3.9", + executable=Path("/usr/bin/python3.9"), flags={ "always-copy": False, "system-site-packages": False, @@ -1050,7 +1193,7 @@ def test_create_venv_tries_to_find_a_compatible_python_executable_using_specific def test_create_venv_fails_if_no_compatible_python_version_could_be_found( manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture -): +) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] @@ -1062,7 +1205,7 @@ def test_create_venv_fails_if_no_compatible_python_version_could_be_found( ) with pytest.raises(NoCompatiblePythonVersionFound) as e: - manager.create_venv(NullIO()) + manager.create_venv() expected_message = ( "Poetry was unable to find a compatible version. " @@ -1076,7 +1219,7 @@ def test_create_venv_fails_if_no_compatible_python_version_could_be_found( def test_create_venv_does_not_try_to_find_compatible_versions_with_executable( manager: EnvManager, poetry: Poetry, config: Config, mocker: MockerFixture -): +) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] @@ -1088,7 +1231,7 @@ def test_create_venv_does_not_try_to_find_compatible_versions_with_executable( ) with pytest.raises(NoCompatiblePythonVersionFound) as e: - manager.create_venv(NullIO(), executable="3.8") + manager.create_venv(executable=Path("python3.8")) expected_message = ( "The specified Python version (3.8.0) is not supported by the project (^4.8).\n" @@ -1107,7 +1250,7 @@ def test_create_venv_uses_patch_version_to_detect_compatibility( mocker: MockerFixture, config_virtualenvs_path: Path, venv_name: str, -): +) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] @@ -1125,7 +1268,7 @@ def test_create_venv_uses_patch_version_to_detect_compatibility( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) - manager.create_venv(NullIO()) + manager.create_venv() assert not check_output.called m.assert_called_with( @@ -1148,12 +1291,13 @@ def test_create_venv_uses_patch_version_to_detect_compatibility_with_executable( mocker: MockerFixture, config_virtualenvs_path: Path, venv_name: str, -): +) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] version = Version.from_parts(*sys.version_info[:3]) - poetry.package.python_versions = f"~{version.major}.{version.minor-1}.0" + poetry.package.python_versions = f"~{version.major}.{version.minor - 1}.0" + venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent)) check_output = mocker.patch( "subprocess.check_output", @@ -1165,14 +1309,12 @@ def test_create_venv_uses_patch_version_to_detect_compatibility_with_executable( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) - manager.create_venv( - NullIO(), executable=f"python{version.major}.{version.minor - 1}" - ) + manager.create_venv(executable=Path(f"python{version.major}.{version.minor - 1}")) assert check_output.called m.assert_called_with( config_virtualenvs_path / f"{venv_name}-py{version.major}.{version.minor - 1}", - executable=f"python{version.major}.{version.minor - 1}", + executable=Path(f"python{version.major}.{version.minor - 1}"), flags={ "always-copy": False, "system-site-packages": False, @@ -1185,11 +1327,11 @@ def test_create_venv_uses_patch_version_to_detect_compatibility_with_executable( def test_create_venv_fails_if_current_python_version_is_not_supported( manager: EnvManager, poetry: Poetry -): +) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] - manager.create_venv(NullIO()) + manager.create_venv() current_version = Version.parse(".".join(str(c) for c in sys.version_info[:3])) next_version = ".".join( @@ -1199,7 +1341,7 @@ def test_create_venv_fails_if_current_python_version_is_not_supported( poetry.package.python_versions = package_version with pytest.raises(InvalidCurrentPythonVersionError) as e: - manager.create_venv(NullIO()) + manager.create_venv() expected_message = ( f"Current Python version ({current_version}) is not allowed by the project" @@ -1214,21 +1356,22 @@ def test_activate_with_in_project_setting_does_not_fail_if_no_venvs_dir( manager: EnvManager, poetry: Poetry, config: Config, - tmp_dir: str, + tmp_path: Path, mocker: MockerFixture, -): +) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] config.merge( { "virtualenvs": { - "path": str(Path(tmp_dir) / "virtualenvs"), + "path": str(tmp_path / "virtualenvs"), "in-project": True, } } ) + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(), @@ -1239,11 +1382,11 @@ def test_activate_with_in_project_setting_does_not_fail_if_no_venvs_dir( ) m = mocker.patch("poetry.utils.env.EnvManager.build_venv") - manager.activate("python3.7", NullIO()) + manager.activate("python3.7") m.assert_called_with( poetry.file.parent / ".venv", - executable="/usr/bin/python3.7", + executable=Path("/usr/bin/python3.7"), flags={ "always-copy": False, "system-site-packages": False, @@ -1253,11 +1396,11 @@ def test_activate_with_in_project_setting_does_not_fail_if_no_venvs_dir( prompt="simple-project-py3.7", ) - envs_file = TOMLFile(Path(tmp_dir) / "virtualenvs" / "envs.toml") + envs_file = TOMLFile(tmp_path / "virtualenvs" / "envs.toml") assert not envs_file.exists() -def test_system_env_has_correct_paths(): +def test_system_env_has_correct_paths() -> None: env = SystemEnv(Path(sys.prefix)) paths = env.paths @@ -1266,13 +1409,14 @@ def test_system_env_has_correct_paths(): assert paths.get("platlib") is not None assert paths.get("scripts") is not None assert env.site_packages.path == Path(paths["purelib"]) + assert paths["include"] is not None @pytest.mark.parametrize( "enabled", [True, False], ) -def test_system_env_usersite(mocker: MockerFixture, enabled: bool): +def test_system_env_usersite(mocker: MockerFixture, enabled: bool) -> None: mocker.patch("site.check_enableusersite", return_value=enabled) env = SystemEnv(Path(sys.prefix)) assert (enabled and env.usersite is not None) or ( @@ -1280,16 +1424,21 @@ def test_system_env_usersite(mocker: MockerFixture, enabled: bool): ) -def test_venv_has_correct_paths(tmp_venv: VirtualEnv): +def test_venv_has_correct_paths(tmp_venv: VirtualEnv) -> None: paths = tmp_venv.paths 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 == Path(paths["purelib"]) + assert paths["include"] == str( + tmp_venv.path.joinpath( + f"include/site/python{tmp_venv.version_info[0]}.{tmp_venv.version_info[1]}" + ) + ) -def test_env_system_packages(tmp_path: Path, poetry: Poetry): +def test_env_system_packages(tmp_path: Path, poetry: Poetry) -> None: venv_path = tmp_path / "venv" pyvenv_cfg = venv_path / "pyvenv.cfg" @@ -1313,7 +1462,7 @@ def test_env_system_packages(tmp_path: Path, poetry: Poetry): ) def test_env_no_pip( tmp_path: Path, poetry: Poetry, flags: dict[str, bool], packages: set[str] -): +) -> None: venv_path = tmp_path / "venv" EnvManager(poetry).build_venv(path=venv_path, flags=flags) env = VirtualEnv(venv_path) @@ -1328,9 +1477,9 @@ def test_env_no_pip( assert installed_packages == packages -def test_env_finds_the_correct_executables(tmp_dir: str, manager: EnvManager): - venv_path = Path(tmp_dir) / "Virtual Env" - manager.build_venv(str(venv_path), with_pip=True) +def test_env_finds_the_correct_executables(tmp_path: Path, manager: EnvManager) -> None: + venv_path = tmp_path / "Virtual Env" + manager.build_venv(venv_path, with_pip=True) venv = VirtualEnv(venv_path) default_executable = expected_executable = f"python{'.exe' if WINDOWS else ''}" @@ -1359,15 +1508,13 @@ def test_env_finds_the_correct_executables(tmp_dir: str, manager: EnvManager): def test_env_finds_the_correct_executables_for_generic_env( - tmp_dir: str, manager: EnvManager -): - venv_path = Path(tmp_dir) / "Virtual Env" - child_venv_path = Path(tmp_dir) / "Child Virtual Env" - manager.build_venv(str(venv_path), with_pip=True) + tmp_path: Path, manager: EnvManager +) -> None: + venv_path = tmp_path / "Virtual Env" + child_venv_path = tmp_path / "Child Virtual Env" + manager.build_venv(venv_path, with_pip=True) parent_venv = VirtualEnv(venv_path) - manager.build_venv( - str(child_venv_path), executable=parent_venv.python, with_pip=True - ) + manager.build_venv(child_venv_path, executable=parent_venv.python, with_pip=True) venv = GenericEnv(parent_venv.path, child_env=VirtualEnv(child_venv_path)) expected_executable = ( @@ -1386,15 +1533,13 @@ def test_env_finds_the_correct_executables_for_generic_env( def test_env_finds_fallback_executables_for_generic_env( - tmp_dir: str, manager: EnvManager -): - venv_path = Path(tmp_dir) / "Virtual Env" - child_venv_path = Path(tmp_dir) / "Child Virtual Env" - manager.build_venv(str(venv_path), with_pip=True) + tmp_path: Path, manager: EnvManager +) -> None: + venv_path = tmp_path / "Virtual Env" + child_venv_path = tmp_path / "Child Virtual Env" + manager.build_venv(venv_path, with_pip=True) parent_venv = VirtualEnv(venv_path) - manager.build_venv( - str(child_venv_path), executable=parent_venv.python, with_pip=True - ) + manager.build_venv(child_venv_path, executable=parent_venv.python, with_pip=True) venv = GenericEnv(parent_venv.path, child_env=VirtualEnv(child_venv_path)) default_executable = f"python{'.exe' if WINDOWS else ''}" @@ -1456,28 +1601,37 @@ def test_create_venv_accepts_fallback_version_w_nonzero_patchlevel( mocker: MockerFixture, config_virtualenvs_path: Path, venv_name: str, -): +) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] poetry.package.python_versions = "~3.5.1" + def mock_check_output(cmd: str, *args: Any, **kwargs: Any) -> str: + if GET_PYTHON_VERSION_ONELINER in cmd: + executable = cmd[0] + if "python3.5" in str(executable): + return "3.5.12" + else: + return "3.7.1" + else: + return "/usr/bin/python3.5" + + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") check_output = mocker.patch( "subprocess.check_output", - side_effect=lambda cmd, *args, **kwargs: str( - "3.5.12" if "python3.5" in cmd else "3.7.1" - ), + side_effect=mock_check_output, ) m = mocker.patch( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) - manager.create_venv(NullIO()) + manager.create_venv() assert check_output.called m.assert_called_with( config_virtualenvs_path / f"{venv_name}-py3.5", - executable="python3.5", + executable=Path("/usr/bin/python3.5"), flags={ "always-copy": False, "system-site-packages": False, @@ -1490,8 +1644,8 @@ def test_create_venv_accepts_fallback_version_w_nonzero_patchlevel( def test_generate_env_name_ignores_case_for_case_insensitive_fs( poetry: Poetry, - tmp_dir: str, -): + tmp_path: Path, +) -> None: venv_name1 = EnvManager.generate_env_name(poetry.package.name, "MyDiR") venv_name2 = EnvManager.generate_env_name(poetry.package.name, "mYdIr") if sys.platform == "win32": @@ -1500,7 +1654,9 @@ def test_generate_env_name_ignores_case_for_case_insensitive_fs( assert venv_name1 != venv_name2 -def test_generate_env_name_uses_real_path(tmp_dir: str, mocker: MockerFixture): +def test_generate_env_name_uses_real_path( + tmp_path: Path, mocker: MockerFixture +) -> None: mocker.patch("os.path.realpath", return_value="the_real_dir") venv_name1 = EnvManager.generate_env_name("simple-project", "the_real_dir") venv_name2 = EnvManager.generate_env_name("simple-project", "linked_dir") @@ -1508,19 +1664,17 @@ def test_generate_env_name_uses_real_path(tmp_dir: str, mocker: MockerFixture): @pytest.fixture() -def extended_without_setup_poetry() -> Poetry: - poetry = Factory().create_poetry( - Path(__file__).parent.parent / "fixtures" / "extended_project_without_setup" - ) +def extended_without_setup_poetry(fixture_dir: FixtureDirGetter) -> Poetry: + poetry = Factory().create_poetry(fixture_dir("extended_project_without_setup")) return poetry def test_build_environment_called_build_script_specified( - mocker: MockerFixture, extended_without_setup_poetry: Poetry, tmp_dir: str -): - project_env = MockEnv(path=Path(tmp_dir) / "project") - ephemeral_env = MockEnv(path=Path(tmp_dir) / "ephemeral") + mocker: MockerFixture, extended_without_setup_poetry: Poetry, tmp_path: Path +) -> None: + project_env = MockEnv(path=tmp_path / "project") + ephemeral_env = MockEnv(path=tmp_path / "ephemeral") mocker.patch( "poetry.utils.env.ephemeral_environment" @@ -1530,21 +1684,22 @@ def test_build_environment_called_build_script_specified( assert env == ephemeral_env assert env.executed == [ [ - sys.executable, - env.pip_embedded, + str(sys.executable), + str(env.pip_embedded), "install", "--disable-pip-version-check", "--ignore-installed", + "--no-input", *extended_without_setup_poetry.pyproject.build_system.requires, ] ] def test_build_environment_not_called_without_build_script_specified( - mocker: MockerFixture, poetry: Poetry, tmp_dir: str -): - project_env = MockEnv(path=Path(tmp_dir) / "project") - ephemeral_env = MockEnv(path=Path(tmp_dir) / "ephemeral") + mocker: MockerFixture, poetry: Poetry, tmp_path: Path +) -> None: + project_env = MockEnv(path=tmp_path / "project") + ephemeral_env = MockEnv(path=tmp_path / "ephemeral") mocker.patch( "poetry.utils.env.ephemeral_environment" @@ -1556,22 +1711,23 @@ def test_build_environment_not_called_without_build_script_specified( def test_create_venv_project_name_empty_sets_correct_prompt( + fixture_dir: FixtureDirGetter, project_factory: ProjectFactory, config: Config, mocker: MockerFixture, config_virtualenvs_path: Path, -): +) -> None: if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] - fixture = Path(__file__).parent.parent / "fixtures" / "no_name_project" - poetry = project_factory("no", source=fixture) + poetry = project_factory("no", source=fixture_dir("no_name_project")) manager = EnvManager(poetry) poetry.package.python_versions = "^3.7" venv_name = manager.generate_env_name("", str(poetry.file.parent)) mocker.patch("sys.version_info", (2, 7, 16)) + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.7.5")), @@ -1580,11 +1736,11 @@ def test_create_venv_project_name_empty_sets_correct_prompt( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) - manager.create_venv(NullIO()) + manager.create_venv() m.assert_called_with( config_virtualenvs_path / f"{venv_name}-py3.7", - executable="python3", + executable=Path("/usr/bin/python3"), flags={ "always-copy": False, "system-site-packages": False, @@ -1593,3 +1749,31 @@ def test_create_venv_project_name_empty_sets_correct_prompt( }, prompt="virtualenv-py3.7", ) + + +def test_fallback_on_detect_active_python( + poetry: Poetry, mocker: MockerFixture +) -> None: + m = mocker.patch( + "subprocess.check_output", + side_effect=subprocess.CalledProcessError(1, "some command"), + ) + env_manager = EnvManager(poetry) + active_python = env_manager._detect_active_python() + + assert active_python is None + assert m.call_count == 1 + + +@pytest.mark.skipif(sys.platform != "win32", reason="Windows only") +def test_detect_active_python_with_bat(poetry: Poetry, tmp_path: Path) -> None: + """On Windows pyenv uses batch files for python management.""" + python_wrapper = tmp_path / "python.bat" + wrapped_python = Path(r"C:\SpecialPython\python.exe") + with python_wrapper.open("w") as f: + f.write(f"@echo {wrapped_python}") + os.environ["PATH"] = str(python_wrapper.parent) + os.pathsep + os.environ["PATH"] + + active_python = EnvManager(poetry)._detect_active_python() + + assert active_python == wrapped_python diff --git a/tests/utils/test_env_site.py b/tests/utils/test_env_site.py index f3fdc9bb6d3..d4882b46530 100644 --- a/tests/utils/test_env_site.py +++ b/tests/utils/test_env_site.py @@ -5,7 +5,6 @@ from pathlib import Path from typing import TYPE_CHECKING -from poetry.utils._compat import decode from poetry.utils.env import SitePackages @@ -13,17 +12,17 @@ from pytest_mock import MockerFixture -def test_env_site_simple(tmp_dir: str, mocker: MockerFixture): +def test_env_site_simple(tmp_path: Path, mocker: MockerFixture): # emulate permission error when creating directory mocker.patch("pathlib.Path.mkdir", side_effect=OSError()) - site_packages = SitePackages(Path("/non-existent"), fallbacks=[Path(tmp_dir)]) + site_packages = SitePackages(Path("/non-existent"), fallbacks=[tmp_path]) candidates = site_packages.make_candidates(Path("hello.txt"), writable_only=True) - hello = Path(tmp_dir) / "hello.txt" + hello = tmp_path / "hello.txt" assert len(candidates) == 1 assert candidates[0].as_posix() == hello.as_posix() - content = decode(str(uuid.uuid4())) + content = str(uuid.uuid4()) site_packages.write_text(Path("hello.txt"), content, encoding="utf-8") assert hello.read_text(encoding="utf-8") == content @@ -31,18 +30,17 @@ def test_env_site_simple(tmp_dir: str, mocker: MockerFixture): assert not (site_packages.path / "hello.txt").exists() -def test_env_site_select_first(tmp_dir: str): - path = Path(tmp_dir) - fallback = path / "fallback" +def test_env_site_select_first(tmp_path: Path): + fallback = tmp_path / "fallback" fallback.mkdir(parents=True) - site_packages = SitePackages(path, fallbacks=[fallback]) + site_packages = SitePackages(tmp_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())) + content = str(uuid.uuid4()) site_packages.write_text(Path("hello.txt"), content, encoding="utf-8") assert (site_packages.path / "hello.txt").exists() diff --git a/tests/utils/test_helpers.py b/tests/utils/test_helpers.py index b5ab3cfebdc..55314a7d743 100644 --- a/tests/utils/test_helpers.py +++ b/tests/utils/test_helpers.py @@ -1,9 +1,19 @@ from __future__ import annotations +from typing import TYPE_CHECKING + +import pytest + from poetry.core.utils.helpers import parse_requires +from poetry.utils.helpers import get_file_hash + -def test_parse_requires(): +if TYPE_CHECKING: + from tests.types import FixtureDirGetter + + +def test_parse_requires() -> None: requires = """\ jsonschema>=2.6.0.0,<3.0.0.0 lockfile>=0.12.0.0,<0.13.0.0 @@ -57,3 +67,65 @@ def test_parse_requires(): ] # fmt: on assert result == expected + + +def test_default_hash(fixture_dir: FixtureDirGetter) -> None: + sha_256 = "9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad" + assert get_file_hash(fixture_dir("distributions") / "demo-0.1.0.tar.gz") == sha_256 + + +try: + from hashlib import algorithms_guaranteed +except ImportError: + algorithms_guaranteed = {"md5", "sha1", "sha224", "sha256", "sha384", "sha512"} + + +@pytest.mark.parametrize( + "hash_name,expected", + [ + (hash_name, value) + for hash_name, value in [ + ("sha224", "d26bd24163fe91c16b4b0162e773514beab77b76114d9faf6a31e350"), + ( + "sha3_512", + "196f4af9099185054ed72ca1d4c57707da5d724df0af7c3dfcc0fd018b0e0533908e790a291600c7d196fe4411b4f5f6db45213fe6e5cd5512bf18b2e9eff728", # noqa: E501 + ), + ( + "blake2s", + "6dd9007d36c106defcf362cc637abeca41e8e93999928c8fcfaba515ed33bc93", + ), + ( + "sha3_384", + "787264d7885a0c305d2ee4daecfff435d11818399ef96cacef7e7c6bb638ce475f630d39fdd2800ca187dcd0071dc410", # noqa: E501 + ), + ( + "blake2b", + "077a34e8252c8f6776bddd0d34f321cc52762cb4c11a1c7aa9b6168023f1722caf53c9f029074a6eb990a8de341d415dd986293bc2a2fccddad428be5605696e", # noqa: E501 + ), + ( + "sha256", + "9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad", + ), + ( + "sha512", + "766ecf369b6bdf801f6f7bbfe23923cc9793d633a55619472cd3d5763f9154711fbf57c8b6ca74e4a82fa9bd8380af831e7b8668e68e362669fc60b1d81d79ad", # noqa: E501 + ), + ( + "sha384", + "c638f32460f318035e4600284ba64fb531630740aebd33885946e527002d742787ff09eb65fd81bc34ce5ff5ef11cfe8", # noqa: E501 + ), + ("sha3_224", "72980fc7bdf8c4d34268dc469442b09e1ccd2a8ff390954fc4d55a5a"), + ("sha1", "91b585bd38f72d7ceedb07d03f94911b772fdc4c"), + ( + "sha3_256", + "7da5c08b416e6bcb339d6bedc0fe077c6e69af00607251ef4424c356ea061fcb", + ), + ] + if hash_name in algorithms_guaranteed + ], +) +def test_guaranteed_hash( + hash_name: str, expected: str, fixture_dir: FixtureDirGetter +) -> None: + file_path = fixture_dir("distributions") / "demo-0.1.0.tar.gz" + assert get_file_hash(file_path, hash_name) == expected diff --git a/tests/utils/test_password_manager.py b/tests/utils/test_password_manager.py index 468ef14f689..d09f88b1fdf 100644 --- a/tests/utils/test_password_manager.py +++ b/tests/utils/test_password_manager.py @@ -20,7 +20,7 @@ def test_set_http_password( config: Config, with_simple_keyring: None, dummy_keyring: DummyBackend -): +) -> None: manager = PasswordManager(config) assert manager.keyring.is_available() @@ -35,13 +35,14 @@ def test_set_http_password( def test_get_http_auth( config: Config, with_simple_keyring: None, dummy_keyring: DummyBackend -): +) -> None: dummy_keyring.set_password("poetry-repository-foo", "bar", "baz") config.auth_config_source.add_property("http-basic.foo", {"username": "bar"}) manager = PasswordManager(config) assert manager.keyring.is_available() auth = manager.get_http_auth("foo") + assert auth is not None assert auth["username"] == "bar" assert auth["password"] == "baz" @@ -49,7 +50,7 @@ def test_get_http_auth( def test_delete_http_password( config: Config, with_simple_keyring: None, dummy_keyring: DummyBackend -): +) -> None: dummy_keyring.set_password("poetry-repository-foo", "bar", "baz") config.auth_config_source.add_property("http-basic.foo", {"username": "bar"}) manager = PasswordManager(config) @@ -63,7 +64,7 @@ def test_delete_http_password( def test_set_pypi_token( config: Config, with_simple_keyring: None, dummy_keyring: DummyBackend -): +) -> None: manager = PasswordManager(config) assert manager.keyring.is_available() @@ -76,7 +77,7 @@ def test_set_pypi_token( def test_get_pypi_token( config: Config, with_simple_keyring: None, dummy_keyring: DummyBackend -): +) -> None: dummy_keyring.set_password("poetry-repository-foo", "__token__", "baz") manager = PasswordManager(config) @@ -86,7 +87,7 @@ def test_get_pypi_token( def test_delete_pypi_token( config: Config, with_simple_keyring: None, dummy_keyring: DummyBackend -): +) -> None: dummy_keyring.set_password("poetry-repository-foo", "__token__", "baz") manager = PasswordManager(config) @@ -98,7 +99,7 @@ def test_delete_pypi_token( def test_set_http_password_with_unavailable_backend( config: Config, with_fail_keyring: None -): +) -> None: manager = PasswordManager(config) assert not manager.keyring.is_available() @@ -111,7 +112,7 @@ def test_set_http_password_with_unavailable_backend( def test_get_http_auth_with_unavailable_backend( config: Config, with_fail_keyring: None -): +) -> None: config.auth_config_source.add_property( "http-basic.foo", {"username": "bar", "password": "baz"} ) @@ -119,6 +120,7 @@ def test_get_http_auth_with_unavailable_backend( assert not manager.keyring.is_available() auth = manager.get_http_auth("foo") + assert auth is not None assert auth["username"] == "bar" assert auth["password"] == "baz" @@ -126,7 +128,7 @@ def test_get_http_auth_with_unavailable_backend( def test_delete_http_password_with_unavailable_backend( config: Config, with_fail_keyring: None -): +) -> None: config.auth_config_source.add_property( "http-basic.foo", {"username": "bar", "password": "baz"} ) @@ -140,7 +142,7 @@ def test_delete_http_password_with_unavailable_backend( def test_set_pypi_token_with_unavailable_backend( config: Config, with_fail_keyring: None -): +) -> None: manager = PasswordManager(config) assert not manager.keyring.is_available() @@ -151,7 +153,7 @@ def test_set_pypi_token_with_unavailable_backend( def test_get_pypi_token_with_unavailable_backend( config: Config, with_fail_keyring: None -): +) -> None: config.auth_config_source.add_property("pypi-token.foo", "baz") manager = PasswordManager(config) @@ -161,7 +163,7 @@ def test_get_pypi_token_with_unavailable_backend( def test_delete_pypi_token_with_unavailable_backend( config: Config, with_fail_keyring: None -): +) -> None: config.auth_config_source.add_property("pypi-token.foo", "baz") manager = PasswordManager(config) @@ -173,7 +175,7 @@ def test_delete_pypi_token_with_unavailable_backend( def test_keyring_raises_errors_on_keyring_errors( mocker: MockerFixture, with_fail_keyring: None -): +) -> None: mocker.patch("poetry.utils.password_manager.PoetryKeyring._check") key_ring = PoetryKeyring("poetry") @@ -189,7 +191,7 @@ def test_keyring_raises_errors_on_keyring_errors( def test_keyring_with_chainer_backend_and_fail_keyring_should_be_unavailable( with_chained_fail_keyring: None, -): +) -> None: key_ring = PoetryKeyring("poetry") assert not key_ring.is_available() @@ -197,7 +199,7 @@ def test_keyring_with_chainer_backend_and_fail_keyring_should_be_unavailable( def test_keyring_with_chainer_backend_and_null_keyring_should_be_unavailable( with_chained_null_keyring: None, -): +) -> None: key_ring = PoetryKeyring("poetry") assert not key_ring.is_available() @@ -205,7 +207,7 @@ def test_keyring_with_chainer_backend_and_null_keyring_should_be_unavailable( def test_null_keyring_should_be_unavailable( with_null_keyring: None, -): +) -> None: key_ring = PoetryKeyring("poetry") assert not key_ring.is_available() @@ -213,7 +215,7 @@ def test_null_keyring_should_be_unavailable( def test_fail_keyring_should_be_unavailable( with_fail_keyring: None, -): +) -> None: key_ring = PoetryKeyring("poetry") assert not key_ring.is_available() @@ -221,13 +223,14 @@ def test_fail_keyring_should_be_unavailable( def test_get_http_auth_from_environment_variables( environ: None, config: Config, with_simple_keyring: None -): +) -> None: os.environ["POETRY_HTTP_BASIC_FOO_USERNAME"] = "bar" os.environ["POETRY_HTTP_BASIC_FOO_PASSWORD"] = "baz" manager = PasswordManager(config) auth = manager.get_http_auth("foo") + assert auth is not None assert auth["username"] == "bar" assert auth["password"] == "baz" @@ -238,7 +241,7 @@ def test_get_pypi_token_with_env_var_positive( config: Config, with_simple_keyring: None, dummy_keyring: DummyBackend, -): +) -> None: sample_token = "sampletoken-1234" repo_name = "foo" manager = PasswordManager(config) @@ -252,7 +255,7 @@ def test_get_pypi_token_with_env_var_positive( def test_get_pypi_token_with_env_var_not_available( config: Config, with_simple_keyring: None, dummy_keyring: DummyBackend -): +) -> None: repo_name = "foo" manager = PasswordManager(config) diff --git a/tests/utils/test_pip.py b/tests/utils/test_pip.py index c6bf8422538..6f5c4851dab 100644 --- a/tests/utils/test_pip.py +++ b/tests/utils/test_pip.py @@ -10,6 +10,8 @@ if TYPE_CHECKING: + from pathlib import Path + from pytest_mock import MockerFixture from poetry.utils.env import VirtualEnv @@ -17,7 +19,7 @@ def test_pip_install_successful( - tmp_dir: str, tmp_venv: VirtualEnv, fixture_dir: FixtureDirGetter + tmp_path: Path, tmp_venv: VirtualEnv, fixture_dir: FixtureDirGetter ): file_path = fixture_dir("distributions/demo-0.1.0-py2.py3-none-any.whl") result = pip_install(file_path, tmp_venv) @@ -26,7 +28,7 @@ def test_pip_install_successful( def test_pip_install_with_keyboard_interrupt( - tmp_dir: str, + tmp_path: Path, tmp_venv: VirtualEnv, fixture_dir: FixtureDirGetter, mocker: MockerFixture, diff --git a/tests/utils/test_setup_reader.py b/tests/utils/test_setup_reader.py index d72e5386275..551ad707b34 100644 --- a/tests/utils/test_setup_reader.py +++ b/tests/utils/test_setup_reader.py @@ -1,7 +1,6 @@ from __future__ import annotations -import os - +from pathlib import Path from typing import TYPE_CHECKING import pytest @@ -16,16 +15,16 @@ @pytest.fixture() -def setup() -> Callable[[str], str]: - def _setup(name: str) -> str: - return os.path.join(os.path.dirname(__file__), "fixtures", "setups", name) +def setup() -> Callable[[str], Path]: + def _setup(name: str) -> Path: + return Path(__file__).parent / "fixtures" / "setups" / name return _setup def test_setup_reader_read_first_level_setup_call_with_direct_types( - setup: Callable[[str], str] -): + setup: Callable[[str], Path] +) -> None: result = SetupReader.read_from_directory(setup("flask")) expected_name = "Flask" @@ -58,8 +57,8 @@ def test_setup_reader_read_first_level_setup_call_with_direct_types( def test_setup_reader_read_first_level_setup_call_with_variables( - setup: Callable[[str], str] -): + setup: Callable[[str], Path] +) -> None: result = SetupReader.read_from_directory(setup("requests")) expected_name = None @@ -85,8 +84,8 @@ def test_setup_reader_read_first_level_setup_call_with_variables( def test_setup_reader_read_sub_level_setup_call_with_direct_types( - setup: Callable[[str], str] -): + setup: Callable[[str], Path] +) -> None: result = SetupReader.read_from_directory(setup("sqlalchemy")) expected_name = "SQLAlchemy" @@ -110,7 +109,7 @@ def test_setup_reader_read_sub_level_setup_call_with_direct_types( assert result["python_requires"] is None -def test_setup_reader_read_setup_cfg(setup: Callable[[str], str]): +def test_setup_reader_read_setup_cfg(setup: Callable[[str], Path]) -> None: result = SetupReader.read_from_directory(setup("with-setup-cfg")) expected_name = "with-setup-cfg" @@ -129,12 +128,12 @@ def test_setup_reader_read_setup_cfg(setup: Callable[[str], str]): assert result["python_requires"] == expected_python_requires -def test_setup_reader_read_setup_cfg_with_attr(setup: Callable[[str], str]): +def test_setup_reader_read_setup_cfg_with_attr(setup: Callable[[str], Path]) -> None: with pytest.raises(InvalidVersion): SetupReader.read_from_directory(setup("with-setup-cfg-attr")) -def test_setup_reader_read_setup_kwargs(setup: Callable[[str], str]): +def test_setup_reader_read_setup_kwargs(setup: Callable[[str], Path]) -> None: result = SetupReader.read_from_directory(setup("pendulum")) expected_name = "pendulum" @@ -150,7 +149,7 @@ def test_setup_reader_read_setup_kwargs(setup: Callable[[str], str]): assert result["python_requires"] == expected_python_requires -def test_setup_reader_read_setup_call_in_main(setup: Callable[[str], str]): +def test_setup_reader_read_setup_call_in_main(setup: Callable[[str], Path]) -> None: result = SetupReader.read_from_directory(setup("pyyaml")) expected_name = "PyYAML" @@ -166,7 +165,9 @@ def test_setup_reader_read_setup_call_in_main(setup: Callable[[str], str]): assert result["python_requires"] == expected_python_requires -def test_setup_reader_read_extras_require_with_variables(setup: Callable[[str], str]): +def test_setup_reader_read_extras_require_with_variables( + setup: Callable[[str], Path] +) -> None: result = SetupReader.read_from_directory(setup("extras_require_with_vars")) expected_name = "extras_require_with_vars" @@ -182,7 +183,7 @@ def test_setup_reader_read_extras_require_with_variables(setup: Callable[[str], assert result["python_requires"] == expected_python_requires -def test_setup_reader_setuptools(setup: Callable[[str], str]): +def test_setup_reader_setuptools(setup: Callable[[str], Path]) -> None: result = SetupReader.read_from_directory(setup("setuptools_setup")) expected_name = "my_package" diff --git a/tests/utils/test_source.py b/tests/utils/test_source.py index a970b7262ca..4908a6e0978 100644 --- a/tests/utils/test_source.py +++ b/tests/utils/test_source.py @@ -7,6 +7,7 @@ from tomlkit.items import Trivia from poetry.config.source import Source +from poetry.repositories.repository_pool import Priority from poetry.utils.source import source_to_table @@ -16,25 +17,58 @@ ( Source("foo", "https://example.com"), { - "default": False, "name": "foo", - "secondary": False, + "priority": "primary", "url": "https://example.com", }, ), ( - Source("bar", "https://example.com/bar", True, True), + Source("bar", "https://example.com/bar", priority=Priority.EXPLICIT), { - "default": True, "name": "bar", - "secondary": True, + "priority": "explicit", "url": "https://example.com/bar", }, ), ], ) -def test_source_to_table(source: Source, table_body: dict[str, str | bool]): +def test_source_to_table(source: Source, table_body: dict[str, str | bool]) -> None: table = Table(Container(), Trivia(), False) table._value = table_body assert source_to_table(source) == table + + +def test_source_default_is_primary() -> None: + source = Source("foo", "https://example.com") + assert source.priority == Priority.PRIMARY + + +@pytest.mark.parametrize( + ("default", "secondary", "expected_priority"), + [ + (False, True, Priority.SECONDARY), + (True, False, Priority.DEFAULT), + (True, True, Priority.DEFAULT), + ], +) +def test_source_legacy_handling( + default: bool, secondary: bool, expected_priority: Priority +) -> None: + with pytest.warns(DeprecationWarning): + source = Source( + "foo", "https://example.com", default=default, secondary=secondary + ) + assert source.priority == expected_priority + + +@pytest.mark.parametrize( + ("priority", "expected_priority"), + [ + ("secondary", Priority.SECONDARY), + ("SECONDARY", Priority.SECONDARY), + ], +) +def test_source_priority_as_string(priority: str, expected_priority: Priority) -> None: + source = Source("foo", "https://example.com", priority=priority) + assert source.priority == Priority.SECONDARY