diff --git a/.github/actions/bootstrap-poetry/action.yaml b/.github/actions/bootstrap-poetry/action.yaml index cec135d8991..328e0417681 100644 --- a/.github/actions/bootstrap-poetry/action.yaml +++ b/.github/actions/bootstrap-poetry/action.yaml @@ -8,9 +8,6 @@ inputs: python-latest: description: Use an uncached Python if a newer match is available default: 'false' - python-prereleases: - description: Allow usage of pre-release Python versions - default: 'false' poetry-spec: description: pip-compatible installation specification to use for Poetry default: 'poetry' @@ -32,7 +29,7 @@ runs: with: python-version: ${{ inputs.python-version }} check-latest: ${{ inputs.python-latest == 'true' }} - allow-prereleases: ${{ inputs.python-prereleases == 'true' }} + allow-prereleases: true update-environment: false - run: > diff --git a/.github/workflows/.tests-matrix.yaml b/.github/workflows/.tests-matrix.yaml index 9a9bae65d01..605f8e6f2bd 100644 --- a/.github/workflows/.tests-matrix.yaml +++ b/.github/workflows/.tests-matrix.yaml @@ -78,25 +78,47 @@ jobs: if: inputs.run-pytest-export steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - - - uses: ./.github/actions/bootstrap-poetry with: - python-version: ${{ inputs.python-version }} + path: poetry - - uses: ./.github/actions/poetry-install + - uses: ./poetry/.github/actions/bootstrap-poetry with: - args: --with github-actions + python-version: ${{ inputs.python-version }} - - run: poetry run pip list --format json | jq -r '.[] | "\(.name)=\(.version)"' >> $GITHUB_OUTPUT - id: package-versions + - name: Get poetry-plugin-export version + run: | + PLUGIN_VERSION=$(curl -s https://pypi.org/pypi/poetry-plugin-export/json | jq -r ".info.version") + echo "Found version ${PLUGIN_VERSION}" + echo version=${PLUGIN_VERSION} >> $GITHUB_OUTPUT + id: poetry-plugin-export-version - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - name: Check out poetry-plugin-export + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 with: path: poetry-plugin-export repository: python-poetry/poetry-plugin-export - ref: refs/tags/${{ steps.package-versions.outputs.poetry-plugin-export }} - - - run: poetry run -C .. pytest -v - working-directory: ./poetry-plugin-export - - - run: git -C poetry-plugin-export diff --exit-code --stat HEAD + ref: refs/tags/${{ steps.poetry-plugin-export-version.outputs.version }} + + - name: Use local poetry + working-directory: poetry-plugin-export + run: poetry add --lock --group dev ../poetry + + # This step can be removed after having released a poetry-plugin-export version + # that has cffi>=1.17.0 in its lock file. + - name: Force more recent cffi (workaround for Python 3.13) + working-directory: poetry-plugin-export + run: poetry update --lock cffi + + - name: Install + working-directory: poetry-plugin-export + run: poetry install + + - name: Run tests + working-directory: poetry-plugin-export + run: poetry run pytest -v + + - name: Check for clean working tree + working-directory: poetry-plugin-export + run: | + git checkout -- pyproject.toml poetry.lock + git diff --exit-code --stat HEAD diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index d50675a3330..b3aaa8b2c2f 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -31,10 +31,20 @@ jobs: with: repository: python-poetry/website + # use .github from pull request target instead of pull_request.head + # for pull_request_target trigger to avoid arbitrary code execution - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 with: - path: poetry + path: poetry-github + sparse-checkout: .github + + # only checkout docs from pull_request.head to not use something else by accident + # for pull_request_target trigger (security) + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + with: + path: poetry-docs ref: ${{ github.event.pull_request.head.sha }} + sparse-checkout: docs - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: @@ -44,16 +54,16 @@ jobs: with: hugo-version: '0.83.1' - - uses: ./poetry/.github/actions/bootstrap-poetry + - uses: ./poetry-github/.github/actions/bootstrap-poetry - - uses: ./poetry/.github/actions/poetry-install + - uses: ./poetry-github/.github/actions/poetry-install with: args: --no-root --only main - name: website-build run: | # Rebuild the docs files from the PR checkout. - poetry run python bin/website build --local ./poetry + poetry run python bin/website build --local ./poetry-docs # Build website assets (CSS/JS). npm ci && npm run prod # Build the static website. diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 72aecab2a4b..51a4434c25e 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -123,7 +123,13 @@ jobs: image: windows-2022 - name: macOS aarch64 image: macos-14 - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: + - "3.8" + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" fail-fast: false status: diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 6d060fa1cf7..bdacdbdf659 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -14,15 +14,6 @@ pass_filenames: false files: ^(.*/)?(poetry\.lock|pyproject\.toml)$ -- id: poetry-export - name: poetry-export - description: run poetry export to sync lock file with requirements.txt - entry: poetry export - language: python - pass_filenames: false - files: ^(.*/)?poetry\.lock$ - args: ["-f", "requirements.txt", "-o", "requirements.txt"] - - id: poetry-install name: poetry-install description: run poetry install to install dependencies from the lock file diff --git a/docs/basic-usage.md b/docs/basic-usage.md index 425742e9fc2..c7fa444bc66 100644 --- a/docs/basic-usage.md +++ b/docs/basic-usage.md @@ -38,16 +38,18 @@ The `pyproject.toml` file is what is the most important here. This will orchestr your project and its dependencies. For now, it looks like this: ```toml -[tool.poetry] +[project] name = "poetry-demo" version = "0.1.0" description = "" -authors = ["Sébastien Eustace "] +authors = [ + {name = "Sébastien Eustace", email = "sebastien@eustace.io"} +] readme = "README.md" -packages = [{include = "poetry_demo"}] +requires-python = ">=3.8" -[tool.poetry.dependencies] -python = "^3.7" +[tool.poetry] +packages = [{include = "poetry_demo"}] [build-system] @@ -122,7 +124,20 @@ In the [pyproject section]({{< relref "pyproject" >}}) you can see which fields ### Specifying dependencies -If you want to add dependencies to your project, you can specify them in the `tool.poetry.dependencies` section. +If you want to add dependencies to your project, you can specify them in the +`project` or `tool.poetry.dependencies` section. +See the [Dependency specification]({{< relref "dependency-specification" >}}) +for more information. + +```toml +[project] +# ... +dependencies = [ + "pendulum (>=2.1,<3.0)" +] +``` + +or ```toml [tool.poetry.dependencies] diff --git a/docs/cli.md b/docs/cli.md index 6ff3f48664a..fb94f416ce1 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -158,16 +158,18 @@ the `--without` option. poetry install --without test,docs ``` -{{% note %}} -The `--no-dev` option is now deprecated. You should use the `--only main` or `--without dev` notation instead. -{{% /note %}} - You can also select optional dependency groups with the `--with` option. ```bash poetry install --with test,docs ``` +To install all dependency groups including the optional groups, use the ``--all-groups`` flag. + +```bash +poetry install --all-groups +``` + It's also possible to only install specific dependency groups by using the `only` option. ```bash @@ -258,11 +260,11 @@ poetry install --compile * `--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). +* `--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). +* `--all-extras`: Install all extra features (conflicts with `--extras`). +* `--all-groups`: Install dependencies from all groups (conflicts with `--only`, `--with`, and `--without`). * `--compile`: Compile Python source files to bytecode. -* `--no-dev`: Do not install dev dependencies. (**Deprecated**, use `--only main` or `--without dev` instead) * `--remove-untracked`: Remove dependencies not presented in the lock file. (**Deprecated**, use `--sync` instead) {{% note %}} @@ -300,8 +302,7 @@ You can do this using the `add` command. * `--without`: The dependency groups to ignore. * `--with`: The optional dependency groups to include. * `--only`: The only dependency groups to include. -* `--dry-run` : Outputs the operations but will not execute anything (implicitly enables --verbose). -* `--no-dev` : Do not update the development dependencies. (**Deprecated**, use `--only main` or `--without dev` instead) +* `--dry-run` : Outputs the operations but will not execute anything (implicitly enables `--verbose`). * `--lock` : Do not perform install (only update the lockfile). * `--sync`: Synchronize the environment with the locked packages and the specified groups. @@ -321,8 +322,9 @@ poetry add requests pendulum ``` {{% note %}} -A package is looked up, by default, only from the [Default Package Source]({{< relref "repositories/#default-package-source" >}}). -You can modify the default source (PyPI); or add and use [Supplemental Package Sources]({{< relref "repositories/#supplemental-package-sources" >}}) +A package is looked up, by default, only from [PyPI](https://pypi.org). +You can modify the default source (PyPI); +or add and use [Supplemental Package Sources]({{< relref "repositories/#supplemental-package-sources" >}}) or [Explicit Package Sources]({{< relref "repositories/#explicit-package-sources" >}}). For more information, refer to the [Package Sources]({{< relref "repositories/#package-sources" >}}) documentation. @@ -453,15 +455,15 @@ about dependency groups. ### Options * `--group (-G)`: The group to add the dependency to. -* `--dev (-D)`: Add package as development dependency. (**Deprecated**, use `-G dev` instead) +* `--dev (-D)`: Add package as development dependency. (shortcut for `-G dev`) * `--editable (-e)`: Add vcs/path dependencies as editable. * `--extras (-E)`: Extras to activate for the dependency. (multiple values allowed) -* `--optional`: Add as an optional dependency. +* `--optional`: Add as an optional dependency to an extra. * `--python`: Python version for which the dependency must be installed. * `--platform`: Platforms for which the dependency must be installed. * `--source`: Name of the source to use to install the package. * `--allow-prereleases`: Accept prereleases. -* `--dry-run`: Output the operations but do not execute anything (implicitly enables --verbose). +* `--dry-run`: Output the operations but do not execute anything (implicitly enables `--verbose`). * `--lock`: Do not perform install (only update the lockfile). @@ -486,8 +488,8 @@ about dependency groups. ### Options * `--group (-G)`: The group to remove the dependency from. -* `--dev (-D)`: Removes a package from the development dependencies. (**Deprecated**, use `-G dev` instead) -* `--dry-run` : Outputs the operations but will not execute anything (implicitly enables --verbose). +* `--dev (-D)`: Removes a package from the development dependencies. (shortcut for `-G dev`) +* `--dry-run` : Outputs the operations but will not execute anything (implicitly enables `--verbose`). * `--lock`: Do not perform operations (only update the lockfile). @@ -514,7 +516,7 @@ dependencies - pytzdata >=2017.2.2 required by - - calendar >=1.4.0 + - calendar requires >=1.4.0 ``` ### Options @@ -523,7 +525,6 @@ required by * `--why`: When showing the full list, or a `--tree` for a single package, display whether they are a direct dependency or required by other packages. * `--with`: The optional dependency groups to include. * `--only`: The only dependency groups to include. -* `--no-dev`: Do not list the dev dependencies. (**Deprecated**, use `--only main` or `--without dev` instead) * `--tree`: List the dependencies as a tree. * `--latest (-l)`: Show the latest version. * `--outdated (-o)`: Show the latest version but only for packages that are outdated. @@ -708,7 +709,9 @@ poetry search requests pendulum This command locks (without installing) the dependencies specified in `pyproject.toml`. {{% note %}} -By default, this will lock all dependencies to the latest available compatible versions. To only refresh the lock file, use the `--no-update` option. +By default, packages that have already been added to the lock file before will not be updated. +To update all dependencies to the latest available compatible versions, use `poetry update --lock` +or `poetry lock --regenerate`, which normally produce the same result. This command is also available as a pre-commit hook. See [pre-commit hooks]({{< relref "pre-commit-hooks#poetry-lock">}}) for more information. {{% /note %}} @@ -719,7 +722,7 @@ poetry lock ### Options * `--check`: Verify that `poetry.lock` is consistent with `pyproject.toml`. (**Deprecated**) Use `poetry check --lock` instead. -* `--no-update`: Do not update locked versions, only refresh lock file. +* `--regenerate`: Ignore existing lock file and overwrite it with a new lock file created from scratch. ## version @@ -768,22 +771,28 @@ The option `--next-phase` allows the increment of prerelease phase versions. ## export +{{% warning %}} +This command is provided by the [Export Poetry Plugin](https://github.com/python-poetry/poetry-plugin-export). +The plugin is no longer installed by default with Poetry 2.0. + +See [Using plugins]({{< relref "plugins#using-plugins" >}}) for information on how to install a plugin. +As described in [Project plugins]({{< relref "plugins#project-plugins" >}}), +you can also define in your `pyproject.toml` that the plugin is required for the development of your project: + +```toml +[tool.poetry.requires-plugins] +poetry-plugin-export = ">1.8" +``` +{{% /warning %}} + This command exports the lock file to other formats. ```bash poetry export -f requirements.txt --output requirements.txt ``` -{{% warning %}} -This command is provided by the [Export Poetry Plugin](https://github.com/python-poetry/poetry-plugin-export). -In a future version of Poetry this plugin will not be installed by default anymore. -In order to avoid a breaking change and make your automation forward-compatible, -please install poetry-plugin-export explicitly. -See [Using plugins]({{< relref "plugins#using-plugins" >}}) for details on how to install a plugin. -{{% /warning %}} - {{% note %}} -This command is also available as a pre-commit hook. +The `export` command is also available as a pre-commit hook. See [pre-commit hooks]({{< relref "pre-commit-hooks#poetry-export" >}}) for more information. {{% /note %}} @@ -798,7 +807,6 @@ group defined in `tool.poetry.dependencies` when used without specifying any opt 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) * `--extras (-E)`: Extra sets of dependencies to include. * `--without`: The dependency groups to ignore. * `--with`: The optional dependency groups to include. @@ -830,10 +838,10 @@ poetry cache list The `cache clear` command removes packages from a cached repository. -For example, to clear the whole cache of packages from the `pypi` repository, run: +For example, to clear the whole cache of packages from the `PyPI` repository, run: ```bash -poetry cache clear pypi --all +poetry cache clear PyPI --all ``` To only remove a specific package from a cache, you have to specify the cache entry in the following form `cache:package:version`: @@ -865,13 +873,7 @@ poetry source add --priority=explicit pypi #### Options -* `--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" >}}), [`supplemental`]({{< relref "repositories#supplemental-package-sources" >}}), and [`explicit`]({{< relref "repositories#explicit-package-sources" >}}). Refer to the dedicated sections in [Repositories]({{< relref "repositories" >}}) for more information. - -{{% note %}} -At most one of the options above can be provided. See [package sources]({{< relref "repositories#package-sources" >}}) for more information. -{{% /note %}} +* `--priority`: Set the priority of this source. Accepted values are: [`supplemental`]({{< relref "repositories#supplemental-package-sources" >}}), and [`explicit`]({{< relref "repositories#explicit-package-sources" >}}). Refer to the dedicated sections in [Repositories]({{< relref "repositories" >}}) for more information. ### source show @@ -996,7 +998,7 @@ poetry self add artifacts-keyring * `--extras (-E)`: Extras to activate for the dependency. (multiple values allowed) * `--allow-prereleases`: Accept prereleases. * `--source`: Name of the source to use to install the package. -* `--dry-run`: Output the operations but do not execute anything (implicitly enables --verbose). +* `--dry-run`: Output the operations but do not execute anything (implicitly enables `--verbose`). ### self update @@ -1014,7 +1016,7 @@ poetry self update #### Options * `--preview`: Allow the installation of pre-release versions. -* `--dry-run`: Output the operations but do not execute anything (implicitly enables --verbose). +* `--dry-run`: Output the operations but do not execute anything (implicitly enables `--verbose`). ### self lock @@ -1068,7 +1070,7 @@ poetry self remove poetry-plugin-export #### Options -* `--dry-run`: Outputs the operations but will not execute anything (implicitly enables --verbose). +* `--dry-run`: Outputs the operations but will not execute anything (implicitly enables `--verbose`). ### self install @@ -1087,4 +1089,4 @@ poetry self install --sync #### Options * `--sync`: Synchronize the environment with the locked packages and the specified groups. -* `--dry-run`: Output the operations but do not execute anything (implicitly enables --verbose). +* `--dry-run`: Output the operations but do not execute anything (implicitly enables `--verbose`). diff --git a/docs/configuration.md b/docs/configuration.md index ebbd085d149..7c64129d1ab 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -56,7 +56,6 @@ virtualenvs.create = true virtualenvs.in-project = null virtualenvs.options.always-copy = true virtualenvs.options.no-pip = false -virtualenvs.options.no-setuptools = false virtualenvs.options.system-site-packages = false virtualenvs.path = "{cache-dir}/virtualenvs" # /path/to/cache/directory/virtualenvs virtualenvs.prefer-active-python = false @@ -389,35 +388,10 @@ Poetry, for its internal operations, uses the `pip` wheel embedded in the `virtu in Poetry's runtime environment. If a user runs `poetry run pip` when this option is set to `true`, the `pip` the embedded instance of `pip` is used. -You can safely set this, along with `no-setuptools`, to `true`, if you desire a virtual environment with no additional -packages. This is desirable for production environments. +You can safely set this to `true`, if you desire a virtual environment with no additional packages. +This is desirable for production environments. {{% /note %}} -### `virtualenvs.options.no-setuptools` - -**Type**: `boolean` - -**Default**: `false` - -**Environment Variable**: `POETRY_VIRTUALENVS_OPTIONS_NO_SETUPTOOLS` - -*Introduced in 1.2.0* - -If set to `true` the `--no-setuptools` parameter is passed to `virtualenv` on creation of the virtual environment. This -means when a new virtual environment is created, `setuptools` will not be installed in the environment. Poetry, for its -internal operations, does not require `setuptools` and this can safely be set to `true`. - -For environments using python 3.12 or later, `virtualenv` defaults to not -installing `setuptools` when creating a virtual environment. -In such environments this poetry configuration option therefore has no effect: -`setuptools` is not installed either way. -If your project relies on `setuptools`, you should declare it as a dependency. - -{{% warning %}} -Some development tools like IDEs, make an assumption that `setuptools` (and other) packages are always present and -available within a virtual environment. This can cause some features in these tools to not work as expected. -{{% /warning %}} - ### `virtualenvs.options.system-site-packages` **Type**: `boolean` diff --git a/docs/contributing.md b/docs/contributing.md index 3d6d430b1fa..3252e88bab3 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -33,6 +33,8 @@ related reports. * **Check that your issue does not already exist** in the [issue tracker]. * **Make sure your issue is really a bug, and is not a support request or question** better suited for [Discussions] or [Discord]. +* **Try running your commands with the** `--no-cache` **flag**. +* **Try clearing your cache with** `poetry cache clear --all PyPI` **and rerunning your commands**. {{% note %}} If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and @@ -152,6 +154,14 @@ poetry install poetry run pytest ``` +{{% note %}} +If you want to see the coverage stats after the tests are complete, use: + +```bash +poetry run pytest --cov=src/poetry --cov-report term +``` +{{% /note %}} + When you contribute to Poetry, automated tools will be run to make sure your code is suitable to be merged. Besides pytest, you will need to make sure your code typechecks properly using [mypy](http://mypy-lang.org/): diff --git a/docs/dependency-specification.md b/docs/dependency-specification.md index f8aaf57a866..efb9a93af31 100644 --- a/docs/dependency-specification.md +++ b/docs/dependency-specification.md @@ -14,10 +14,87 @@ menu: Dependencies for a project can be specified in various forms, which depend on the type of the dependency and on the optional constraints that might be needed for it to be installed. +## `project.dependencies` and `tool.poetry.dependencies` + +Prior Poetry 2.0, dependencies had to be declared in the `tool.poetry.dependencies` +section of the `pyproject.toml` file. + +```toml +[tool.poetry.dependencies] +requests = "^2.13.0" +``` + +With Poetry 2.0, you should consider using the `project.dependencies` section instead. + +```toml +[project] +# ... +dependencies = [ + "requests (>=2.23.0,<3.0.0)" +] +``` + +While dependencies in `tool.poetry.dependencies` are specified using toml tables, +dependencies in `project.dependencies` are specified as strings according +to [PEP 508](https://peps.python.org/pep-0508/). + +In many cases, `tool.poetry.dependencies` can be replaced with `project.dependencies`. +However, there are some cases where you might still need to use `tool.poetry.dependencies`. +For example, if you want to define additional information that is not required for building +but only for locking (for example an explicit source), you can enrich dependency +information in the `tool.poetry` section. + +```toml +[project] +# ... +dependencies = [ + "requests>=2.13.0", +] + +[tool.poetry.dependencies] +requests = { source = "private-source" } +``` + +When both are specified, `project.dependencies` are used for metadata when building the project, +`tool.poetry.dependencies` is only used to enrich `project.dependencies` for locking. + +Alternatively, you can add `dependencies` to `dynamic` and define your dependencies +completely in the `tool.poetry` section. Using only the `tool.poetry` section might +make sense in non-package mode when you will not build an sdist or a wheel. + +```toml +[project] +# ... +dynamic = [ "dependencies" ] + +[tool.poetry.dependencies] +requests = { version = ">=2.13.0", source = "private-source" } +``` + +{{% note %}} +Another use case for `tool.poetry.dependencies` are relative path dependencies +since `project.dependencies` only support absolute paths. +{{% /note %}} + +{{% note %}} +Only main dependencies can be specified in the `project` section. +Other [Dependency groups]({{< relref "managing-dependencies#dependency-groups" >}}) +must still be specified in the `tool.poetry` section. +{{% /note %}} + ## Version constraints +{{% warning %}} +Some of the following constraints can only be used in `tool.poetry.dependencies` and not in `project.dependencies`. +When using `poetry add` such constraints are automatically converted into an equivalent constraint. +{{% /warning %}} + ### Caret requirements +{{% warning %}} +Not supported in `project.dependencies`. +{{% /warning %}} + **Caret requirements** allow [SemVer](https://semver.org/) compatible updates to a specified version. An update is allowed if the new version number does not modify the left-most non-zero digit in the major, minor, patch grouping. For instance, if we previously ran `poetry add requests@^2.13.0` and wanted to update the library and ran `poetry update requests`, poetry would update us to version `2.14.0` if it was available, but would not update us to `3.0.0`. If instead we had specified the version string as `^0.1.13`, poetry would update to `0.1.14` but not `0.2.0`. `0.0.x` is not considered compatible with any other version. Here are some more examples of caret requirements and the versions that would be allowed with them: @@ -34,6 +111,10 @@ Here are some more examples of caret requirements and the versions that would be ### Tilde requirements +{{% warning %}} +Not supported in `project.dependencies`. +{{% /warning %}} + **Tilde requirements** specify a minimal version with some ability to update. If you specify a major, minor, and patch version or only a major and minor version, only patch-level changes are allowed. If you only specify a major version, then minor- and patch-level changes are allowed. @@ -131,6 +212,16 @@ the minimum information you need to specify is the location of the repository wi requests = { git = "https://github.com/requests/requests.git" } ``` +or in the `project` section: + +```toml +[project] +# ... +dependencies = [ + "requests @ git+https://github.com/requests/requests.git" +] +``` + Since we haven’t specified any other information, Poetry assumes that we intend to use the latest commit on the `main` branch to build our project. @@ -149,6 +240,18 @@ flask = { git = "https://github.com/pallets/flask.git", rev = "38eb5d3b" } numpy = { git = "https://github.com/numpy/numpy.git", tag = "v0.13.2" } ``` +or in the `project` section: + +```toml +[project] +# ... +dependencies = [ + "requests @ git+https://github.com/requests/requests.git@next", + "flask @ git+https://github.com/pallets/flask.git@38eb5d3b", + "numpy @ git+https://github.com/numpy/numpy.git@v0.13.2", +] +``` + In cases where the package you want to install is located in a subdirectory of the VCS repository, you can use the `subdirectory` option, similarly to what [pip](https://pip.pypa.io/en/stable/topics/vcs-support/#url-fragments) provides: ```toml @@ -212,6 +315,16 @@ my-package = { path = "../my-package/", develop = false } my-package = { path = "../my-package/dist/my-package-0.1.0.tar.gz" } ``` +In the `project` section, you can only use absolute paths: + +```toml +[project] +# ... +dependencies = [ + "my-package @ file:///absolute/path/to/my-package/dist/my-package-0.1.0.tar.gz" +] +``` + {{% note %}} Before poetry 1.1 directory path dependencies were installed in editable mode by default. You should set the `develop` attribute explicitly, to make sure the behavior is the same for all poetry versions. @@ -228,6 +341,16 @@ you can use the `url` property: my-package = { url = "https://example.com/my-package-0.1.0.tar.gz" } ``` +or in the `project` section: + +```toml +[project] +# ... +dependencies = [ + "my-package @ https://example.com/my-package-0.1.0.tar.gz" +] +``` + with the corresponding `add` call: ```bash @@ -244,6 +367,16 @@ for a dependency as shown here. gunicorn = { version = "^20.1", extras = ["gevent"] } ``` +or in the `project` section: + +```toml +[project] +# ... +dependencies = [ + "gunicorn[gevent] (>=20.1,<21.0)" +] +``` + {{% note %}} These activate extra defined for the dependency, to configure an optional dependency for extras in your project refer to [`extras`]({{< relref "pyproject#extras" >}}). @@ -275,6 +408,10 @@ In this example, we expect `foo` to be configured correctly. See [using a privat for further information. {{% /note %}} +{{% note %}} +It is not possible to define source dependencies in the `project` section. +{{% /note %}} + ## Python restricted dependencies You can also specify that a dependency should be installed only for specific Python versions: @@ -286,7 +423,18 @@ tomli = { version = "^2.0.1", python = "<3.11" } ```toml [tool.poetry.dependencies] -pathlib2 = { version = "^2.2", python = "^3.2" } +pathlib2 = { version = "^2.2", python = "^3.9" } +``` + +or in the `project` section: + +```toml +[project] +# ... +dependencies = [ + "tomli (>=2.0.1,<3.11) ; python_version < '3.11'", + "pathlib2 (>=2.2,<3.0) ; python_version >= '3.9' and python_version < '4.0'" +] ``` ## Using environment markers @@ -300,6 +448,16 @@ via the `markers` property: pathlib2 = { version = "^2.2", markers = "python_version <= '3.4' or sys_platform == 'win32'" } ``` +or in the `project` section: + +```toml +[project] +# ... +dependencies = [ + "pathlib2 (>=2.2,<3.0) ; python_version <= '3.4' or sys_platform == 'win32'" +] +``` + ## Multiple constraints dependencies Sometimes, one of your dependency may have different version ranges depending @@ -317,6 +475,17 @@ foo = [ ] ``` +or in the `project` section: + +```toml +[project] +# ... +dependencies = [ + "foo (<=1.9) ; python_version >= '3.6' and python_version < '3.8'", + "foo (>=2.0,<3.0) ; python_version >= '3.8'" +] +``` + {{% note %}} The constraints **must** have different requirements (like `python`) otherwise it will cause an error when resolving dependencies. diff --git a/docs/faq.md b/docs/faq.md index 57843f71ffa..c35a92326a6 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -211,6 +211,18 @@ Usually you will want to match the supported Python range of your project with t Alternatively you can tell Poetry to install this dependency [only for a specific range of Python versions]({{< relref "dependency-specification#multiple-constraints-dependencies" >}}), if you know that it's not needed in all versions. +If you do not want to set an upper bound in the metadata when building your project, +you can omit it in the `project` section and only set it in `tool.poetry.dependencies`: + +```toml +[project] +# ... +requires-python = ">=3.7" # used for metadata when building the project + +[tool.poetry.dependencies] +python = ">=3.7,<3.11" # used for locking dependencies +``` + ### Why does Poetry enforce PEP 440 versions? diff --git a/docs/managing-dependencies.md b/docs/managing-dependencies.md index 14932d43df9..67a5bb42de1 100644 --- a/docs/managing-dependencies.md +++ b/docs/managing-dependencies.md @@ -11,6 +11,14 @@ type: docs # Managing dependencies +{{% note %}} +Since Poetry 2.0, main dependencies can be specified in `project.dependencies` +instead of `tool.poetry.dependencies`. +See [Dependency specification]({{< relref "dependency-specification" >}}) for more information. +Only main dependencies can be specified in the `project` section. +Other groups must still be specified in the `tool.poetry` section. +{{% /note %}} + ## Dependency groups Poetry provides a way to **organize** your dependencies by **groups**. For instance, you might have @@ -37,7 +45,22 @@ the dependencies logically. {{% /note %}} {{% note %}} -The dependencies declared in `tool.poetry.dependencies` are part of an implicit `main` group. +The dependencies declared in `project.dependencies` respectively `tool.poetry.dependencies` +are part of an implicit `main` group. +{{% /note %}} + +```toml +[project] +# ... +dependencies = [ # main dependency group + "httpx", + "pendulum", +] + +[tool.poetry.group.test.dependencies] +pytest = "^6.0.0" +pytest-mock = "*" +``` ```toml [tool.poetry.dependencies] # main dependency group @@ -48,7 +71,6 @@ pendulum = "*" pytest = "^6.0.0" pytest-mock = "*" ``` -{{% /note %}} {{% note %}} Dependency groups, other than the implicit `main` group, must only contain dependencies you need in your development diff --git a/docs/plugins.md b/docs/plugins.md index 9f133b32024..68a028acfe2 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -32,16 +32,16 @@ The plugin package must depend on Poetry and declare a proper [plugin]({{< relref "pyproject#plugins" >}}) in the `pyproject.toml` file. ```toml -[tool.poetry] +[project] name = "my-poetry-plugin" version = "1.0.0" - # ... -[tool.poetry.dependencies] -python = "^3.7" -poetry = "^1.2" +requires-python = ">=3.7" +dependencies = [ + "poetry (>=1.2,<2.0)", +] -[tool.poetry.plugins."poetry.plugin"] +[project.entry-points."poetry.plugin"] demo = "poetry_demo_plugin.plugin:MyPlugin" ``` @@ -255,6 +255,28 @@ You can also list all currently installed plugins by running: poetry self show plugins ``` +### Project plugins + +You can also specify that a plugin is required for your project +in the `tool.poetry.requires-plugins` section of the pyproject.toml file: + +```toml +[tool.poetry.requires-plugins] +my-application-plugin = ">1.0" +``` + +If the plugin is not installed in Poetry's own environment when running `poetry install`, +it will be installed only for the current project under `.poetry/plugins` +in the project's directory. + +The syntax to specify `plugins` is the same as for [dependencies]({{< relref "managing-dependencies" >}}). + +{{% warning %}} +You can even overwrite a plugin in Poetry's own environment with another version. +However, if a plugin's dependencies are not compatible with packages in Poetry's own +environment, installation will fail. +{{% /warning %}} + ## Maintaining a plugin diff --git a/docs/pre-commit-hooks.md b/docs/pre-commit-hooks.md index 1b6938b41be..7824e4d52be 100644 --- a/docs/pre-commit-hooks.md +++ b/docs/pre-commit-hooks.md @@ -152,3 +152,30 @@ Thus, `pre-commit autoupdate` is not usable for the hooks described here. You can avoid changing the `rev` to an unexpected value by using the `--repo` parameter (may be specified multiple times), to explicitly list repositories that should be updated. An option to explicitly exclude repositories [will not be implemented](https://github.com/pre-commit/pre-commit/issues/1959) into `pre-commit`. + +### Are there any alternatives to `pre-commit autoupdate`? + +You may use [pre-commit-update](https://pypi.org/project/pre-commit-update/) as an alternative to +`pre-commit autoupdate`. + +Since `pre-commit-update` can be used as a pre-commit hook itself, the easiest way +to make use of it would be to include it inside `.pre-commit-config.yaml`: + +```yaml +repos: +- repo: https://gitlab.com/vojko.pribudic.foss/pre-commit-update + rev: v0.5.1post1 + hooks: + - id: pre-commit-update +- repo: https://github.com/python-poetry/poetry + rev: 1.8.3 + hooks: + - id: poetry-check + - id: poetry-lock + - id: poetry-export + - id: poetry-install +``` + +Your `.pre-commit-config.yaml` repos will be checked and updated every time pre-commit hooks run. + +For more advanced configuration, please check the `pre-commit-update` documentation. diff --git a/docs/pyproject.md b/docs/pyproject.md index 75ad139623c..39b7b192253 100644 --- a/docs/pyproject.md +++ b/docs/pyproject.md @@ -11,11 +11,330 @@ menu: # The `pyproject.toml` file +In package mode, the only required fields are `name` and `version` +(either in the `project` section or in the `tool.poetry` section). +Other fields are optional. +In non-package mode, all fields are optional. + +{{% note %}} +Run `poetry check` to print warnings about deprecated fields. +{{% /note %}} + + +## The `project` section + +The `project` section of the `pyproject.toml` file according to the +[specification of the PyPA](https://packaging.python.org/en/latest/specifications/pyproject-toml/#declaring-project-metadata-the-project-table). + +### name + +The name of the package. **Required in package mode** + +This should be a valid name as defined by [PEP 508](https://peps.python.org/pep-0508/#names). + + +```toml +name = "my-package" +``` + +### version + +The version of the package. **Required in package mode** + +This should be a valid [PEP 440](https://peps.python.org/pep-0440/) string. + +```toml +version = "0.1.0" +``` + +If you want to set the version dynamically via `poetry build --local-version` +or you are using a plugin, which sets the version dynamically, you should add `version` +to dynamic and define the base version in the `tool.poetry` section, for example: + +```toml +[project] +name = "my-package" +dynamic = [ "version" ] + +[tool.poetry] +version = "1.0" # base version +``` + +### description + +A short description of the package. + +```toml +description = "A short description of the package." +``` + +### license + +The license of the package. + +The recommended notation for the most common licenses is (alphabetical): + +* Apache-2.0 +* BSD-2-Clause +* BSD-3-Clause +* BSD-4-Clause +* GPL-2.0-only +* GPL-2.0-or-later +* GPL-3.0-only +* GPL-3.0-or-later +* LGPL-2.1-only +* LGPL-2.1-or-later +* LGPL-3.0-only +* LGPL-3.0-or-later +* MIT + +Optional, but it is highly recommended to supply this. +More identifiers are listed at the [SPDX Open Source License Registry](https://spdx.org/licenses/). + +```toml +license = { text = "MIT" } +``` +{{% note %}} +If your project is proprietary and does not use a specific licence, you can set this value as `Proprietary`. +{{% /note %}} + +You can also specify a license file. However, when doing this the complete license text +will be added to the metadata and the License classifier cannot be determined +automatically so that you have to add it manually. + +```toml +license = { file = "LICENSE" } +``` + +### readme + +A path to the README file or the content. + +```toml +[tool.poetry] +# ... +readme = "README.md" +``` + +{{% note %}} +If you want to define multiple README files, you have to add `readme` to `dynamic` +and define them in the `tool.poetry` section. +{{% /note %}} + +```toml +[project] +# ... +dynamic = [ "readme" ] + +[tool.poetry] +# ... +readme = ["docs/README1.md", "docs/README2.md"] +``` + +### requires-python + +The Python version requirements of the project. + +```toml +requires-python = ">=3.8" +``` + +{{% note %}} +If you need an upper bound for locking, but do not want to define an upper bound +in your package metadata, you can omit the upper bound in the `requires-python` field +and add it in the `tool.poetry.dependencies` section. +{{% /note %}} + +```toml +[project] +# ... +requires-python = ">=3.8" + +[tool.poetry.dependencies] +python = ">=3.8,<4.0" +``` + +### authors + +The authors of the package. + +This is a list of authors and should contain at least one author. + +```toml +authors = [ + { name = "Sébastien Eustace", email = "sebastien@eustace.io" }, +] +``` + +### maintainers + +The maintainers of the package. + +This is a list of maintainers and should be distinct from authors. + +```toml +maintainers = [ + { name = "John Smith", email = "johnsmith@example.org" }, + { name = "Jane Smith", email = "janesmith@example.org" }, +] +``` + +### keywords + +A list of keywords that the package is related to. + +```toml +keywords = [ "packaging", "poetry" ] +``` + +### classifiers + +A list of PyPI [trove classifiers](https://pypi.org/classifiers/) that describe the project. + +```toml +classifiers = [ + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Python Modules" +] +``` + +{{% warning %}} +Note that suitable classifiers based on your `python` requirement and `license` +are **not** automatically added for you if you define classifiers statically +in the `project` section. + +If you want to enrich classifiers automatically, you should add `classifiers` to `dynamic` +and use the `tool.poetry` section instead. +{{% /warning %}} + +```toml +[project] +# ... +dynamic = [ "classifiers" ] + +[tool.poetry] +# ... +classifiers = [ + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Python Modules" +] +``` + +### urls + +The URLs of the project. + +```toml +[tool.poetry.urls] +homepage = "https://python-poetry.org/" +repository = "https://github.com/python-poetry/poetry" +documentation = "https://python-poetry.org/docs/" +"Bug Tracker" = "https://github.com/python-poetry/poetry/issues" +``` + +If you publish your package on PyPI, they will appear in the `Project Links` section. + +### scripts + +This section describes the console scripts that will be installed when installing the package. + +```toml +[project.scripts] +my_package_cli = 'my_package.console:run' +``` + +Here, we will have the `my_package_cli` script installed which will execute the `run` function in the `console` module in the `my_package` package. + +{{% note %}} +When a script is added or updated, run `poetry install` to make them available in the project's virtualenv. +{{% /note %}} + +### gui-scripts + +This section describes the GUI scripts that will be installed when installing the package. + +```toml +[project.scripts] +my_package_gui = 'my_package.gui:run' +``` + +Here, we will have the `my_package_gui` script installed which will execute the `run` function in the `gui` module in the `my_package` package. + +{{% note %}} +When a script is added or updated, run `poetry install` to make them available in the project's virtualenv. +{{% /note %}} + +### entry-points + +Entry points can be used to define plugins for your package. + +Poetry supports arbitrary plugins, which are exposed as the ecosystem-standard +[entry points](https://packaging.python.org/en/latest/specifications/entry-points/) +and discoverable using `importlib.metadata`. +This is similar to (and compatible with) the entry points feature of `setuptools`. +The syntax for registering a plugin is: + +```toml +[project.entry-points] # Optional super table + +[project.entry-points."A"] +B = "C:D" +``` +Which are: + +- `A` - type of the plugin, for example `poetry.plugin` or `flake8.extension` +- `B` - name of the plugin +- `C` - python module import path +- `D` - the entry point of the plugin (a function or class) + +Example (from [`poetry-plugin-export`](http://github.com/python-poetry/poetry-plugin-export)): + +```toml +[project.entry-points."poetry.application.plugin"] +export = "poetry_plugin_export.plugins:ExportApplicationPlugin" +``` + +### dependencies + +The `dependencies` of the project. + +```toml +dependencies = [ + "requests>=2.13.0", +] +``` + +These are the dependencies that will be declared when building an sdist or a wheel. + +See [Dependency specification]({{< relref "dependency-specification" >}}) for more information +about the relation between `project.dependencies` and `tool.poetry.dependencies`. + +### optional-dependencies + +The optional dependencies of the project (also known as extras). + +```toml +[project.optional-dependencies] +mysql = [ "mysqlclient>=1.3,<2.0" ] +pgsql = [ "psycopg2>=2.9,<3.0" ] +databases = [ "mysqlclient>=1.3,<2.0", "psycopg2>=2.9,<3.0" ] +``` + +{{% note %}} + +You can enrich optional dependencies for locking in the `tool.poetry` section +analogous to `dependencies`. + +{{% /note %}} + + +## The `tool.poetry` section + The `tool.poetry` section of the `pyproject.toml` file is composed of multiple sections. -## package-mode +### package-mode -Whether Poetry operates in package mode (default) or not. **Optional** +Whether Poetry operates in package mode (default) or not. See [basic usage]({{< relref "basic-usage#operating-modes" >}}) for more information. @@ -23,9 +342,11 @@ See [basic usage]({{< relref "basic-usage#operating-modes" >}}) for more informa package-mode = false ``` -## name +### name -The name of the package. **Required in package mode** +**Deprecated**: Use `project.name` instead. + +The name of the package. **Required in package mode if not defined in the project section** This should be a valid name as defined by [PEP 508](https://peps.python.org/pep-0508/#names). @@ -34,9 +355,15 @@ This should be a valid name as defined by [PEP 508](https://peps.python.org/pep- name = "my-package" ``` -## version +### version -The version of the package. **Required in package mode** +{{% note %}} +If you do not want to set the version dynamically via `poetry build --local-version` +and you are not using a plugin, which sets the version dynamically, +prefer `project.version` over this setting. +{{% /note %}} + +The version of the package. **Required in package mode if not defined in the project section** This should be a valid [PEP 440](https://peps.python.org/pep-0440/) string. @@ -51,15 +378,19 @@ If you would like to use semantic versioning for your project, please see {{% /note %}} -## description +### description -A short description of the package. **Required in package mode** +**Deprecated**: Use `project.description` instead. + +A short description of the package. ```toml description = "A short description of the package." ``` -## license +### license + +**Deprecated**: Use `project.license` instead. The license of the package. @@ -89,9 +420,11 @@ license = "MIT" If your project is proprietary and does not use a specific licence, you can set this value as `Proprietary`. {{% /note %}} -## authors +### authors -The authors of the package. **Required in package mode** +**Deprecated**: Use `project.authors` instead. + +The authors of the package. This is a list of authors and should contain at least one author. Authors must be in the form `name `. @@ -101,9 +434,11 @@ authors = [ ] ``` -## maintainers +### maintainers + +**Deprecated**: Use `project.maintainers` instead. -The maintainers of the package. **Optional** +The maintainers of the package. This is a list of maintainers and should be distinct from authors. Maintainers may contain an email and be in the form `name `. @@ -114,10 +449,13 @@ maintainers = [ ] ``` -## readme +### readme + +{{% note %}} +If you do not want to set multiple README files, prefer `project.readme` over this setting. +{{% /note %}} A path, or list of paths corresponding to the README file(s) of the package. -**Optional** The file(s) can be of any format, but if you intend to publish to PyPI keep the [recommendations for a PyPI-friendly README]( @@ -147,41 +485,49 @@ readme = "README.md" readme = ["docs/README1.md", "docs/README2.md"] ``` -## homepage +### homepage + +**Deprecated**: Use `project.urls` instead. -An URL to the website of the project. **Optional** +An URL to the website of the project. ```toml homepage = "https://python-poetry.org/" ``` -## repository +### repository -An URL to the repository of the project. **Optional** +**Deprecated**: Use `project.urls` instead. + +An URL to the repository of the project. ```toml repository = "https://github.com/python-poetry/poetry" ``` -## documentation +### documentation + +**Deprecated**: Use `project.urls` instead. -An URL to the documentation of the project. **Optional** +An URL to the documentation of the project. ```toml documentation = "https://python-poetry.org/docs/" ``` -## keywords +### keywords -A list of keywords that the package is related to. **Optional** +**Deprecated**: Use `project.keywords` instead. + +A list of keywords that the package is related to. ```toml keywords = ["packaging", "poetry"] ``` -## classifiers +### classifiers -A list of PyPI [trove classifiers](https://pypi.org/classifiers/) that describe the project. **Optional** +A list of PyPI [trove classifiers](https://pypi.org/classifiers/) that describe the project. ```toml [tool.poetry] @@ -193,12 +539,17 @@ classifiers = [ ``` {{% note %}} -Note that Python classifiers are still automatically added for you and are determined by your `python` requirement. +Note that Python classifiers are automatically added for you +and are determined by your `python` requirement. The `license` property will also set the License classifier automatically. + +If you do not want Poetry to automatically add suitable classifiers +based on the `python` requirement and `license` property, +use `project.classifiers` instead of this setting. {{% /note %}} -## packages +### packages A list of packages and modules to include in the final distribution. @@ -271,7 +622,7 @@ Poetry is clever enough to detect Python subpackages. Thus, you only have to specify the directory where your root package resides. {{% /note %}} -## include and exclude +### include and exclude A list of patterns that will be included in the final package. @@ -309,7 +660,7 @@ In contrast, `exclude` defaults to both `sdist` and `wheel`. exclude = ["my_package/excluded.py"] ``` -## dependencies and dependency groups +### dependencies and dependency groups Poetry is configured to look for dependencies on [PyPI](https://pypi.org) by default. Only the name and a version string are required in this case. @@ -360,7 +711,9 @@ See [Dependency groups]({{< relref "managing-dependencies#dependency-groups" >}} at how to manage dependency groups and [Dependency specification]({{< relref "dependency-specification" >}}) for more information on other keys and specifying version ranges. -## `scripts` +### scripts + +**Deprecated**: Use `project.scripts` instead. This section describes the scripts or executables that will be installed when installing the package @@ -375,7 +728,9 @@ Here, we will have the `my_package_cli` script installed which will execute the When a script is added or updated, run `poetry install` to make them available in the project's virtualenv. {{% /note %}} -## `extras` +### extras + +**Deprecated**: Use `project.optional-dependencies` instead. Poetry supports extras to allow expression of: @@ -447,7 +802,9 @@ Dependencies listed in [dependency groups]({{< relref "managing-dependencies#dep {{% /note %}} -## `plugins` +### plugins + +**Deprecated**: Use `project.entry-points` instead. Poetry supports arbitrary plugins, which are exposed as the ecosystem-standard [entry points](https://packaging.python.org/en/latest/specifications/entry-points/) and discoverable using `importlib.metadata`. This is similar to (and compatible with) the entry points feature of `setuptools`. The syntax for registering a plugin is: @@ -472,7 +829,9 @@ Example (from [`poetry-plugin-export`](http://github.com/python-poetry/poetry-pl export = "poetry_plugin_export.plugins:ExportApplicationPlugin" ``` -## `urls` +### urls + +**Deprecated**: Use `project.urls` instead. In addition to the basic urls (`homepage`, `repository` and `documentation`), you can specify any custom url in the `urls` section. @@ -484,6 +843,29 @@ any custom url in the `urls` section. If you publish your package on PyPI, they will appear in the `Project Links` section. +## `requires-poetry` + +A constraint for the Poetry version that is required for this project. +If you are using a Poetry version that is not allowed by this constraint, +an error will be raised. + +```toml +[tool.poetry] +requires-poetry = ">=2.0" +``` + +## `requires-plugins` + +In this section, you can specify that certain plugins are required for your project: + +```toml +[tool.poetry.requires-plugins] +my-application-plugin = ">=1.0" +my-plugin = ">=1.0,<2.0" +``` + +See [Project plugins]({{< relref "plugins#project-plugins" >}}) for more information. + ## Poetry and PEP-517 [PEP-517](https://www.python.org/dev/peps/pep-0517/) introduces a standard way diff --git a/docs/repositories.md b/docs/repositories.md index 133d338efa1..37e4ece74f2 100644 --- a/docs/repositories.md +++ b/docs/repositories.md @@ -104,12 +104,15 @@ poetry publish --build --repository foo-pub ## Package Sources -By default, Poetry is configured to use the Python ecosystem's canonical package index +By default, if you have not configured any primary source, +Poetry is configured to use the Python ecosystem's canonical package index [PyPI](https://pypi.org). +You can alter this behaviour and exclusively look up packages only from the configured +package sources by adding at least one primary source. {{% note %}} -With the exception of the implicitly configured source for [PyPI](https://pypi.org) named `pypi`, +Except for the implicitly configured source for [PyPI](https://pypi.org) named `PyPI`, package sources are local to a project and must be configured within the project's [`pyproject.toml`]({{< relref "pyproject" >}}) file. This is **not** the same configuration used when publishing a package. @@ -142,56 +145,17 @@ url = "https://foo.bar/simple/" priority = "primary" ``` -If `priority` is undefined, the source is considered a primary source that takes precedence over PyPI, secondary, supplemental and explicit sources. +If `priority` is undefined, the source is considered a primary source, +which disables the implicit PyPI source and takes precedence over supplemental sources. Package sources are considered in the following order: -1. [default source](#default-package-source-deprecated) (DEPRECATED), -2. [primary sources](#primary-package-sources), -3. implicit PyPI (unless disabled by another [primary source](#primary-package-sources), [default source](#default-package-source-deprecated) or configured explicitly), -4. [secondary sources](#secondary-package-sources-deprecated) (DEPRECATED), -5. [supplemental sources](#supplemental-package-sources). +1. [primary sources](#primary-package-sources) or implicit PyPI (if there are no primary sources), +2. [supplemental sources](#supplemental-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 %}} - -If you want to change the priority of [PyPI](https://pypi.org), you can set it explicitly, e.g. - -```bash -poetry source add --priority=primary PyPI -``` - -If you prefer to disable PyPI completely, -just add a [primary source](#primary-package-sources) -or configure PyPI as [explicit source](#explicit-package-sources). - -{{% /note %}} - - -#### Default Package Source (DEPRECATED) - -*Deprecated in 1.8.0* - -{{% warning %}} - -Configuring a default package source is deprecated because it is the same -as the topmost [primary source](#primary-package-sources). -Just configure a primary package source and put it first in the list of package sources. - -{{% /warning %}} - -By default, if you have not configured any primary source, -Poetry will configure [PyPI](https://pypi.org) as the package source for your project. -You can alter this behaviour and exclusively look up packages only from the configured -package sources by adding at least one primary source (recommended) -or a **single** source with `priority = "default"` (deprecated). - -```bash -poetry source add --priority=default foo https://foo.bar/simple/ -``` - #### Primary Package Sources @@ -234,27 +198,6 @@ with Poetry, the PyPI repository cannot be configured with a given URL. Remember {{% /warning %}} -#### Secondary Package Sources (DEPRECATED) - -*Deprecated in 1.5.0* - -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 and primary package sources. If the package source should instead be searched only if higher-priority repositories did not return results, please consider a [supplemental source](#supplemental-package-sources) instead. - -You can configure a package source as a secondary source with `priority = "secondary"` in your package -source configuration. - -```bash -poetry source add --priority=secondary foo https://foo.bar/simple/ -``` - -There can be more than one secondary package source. - -{{% warning %}} - -Secondary package sources are deprecated in favor of supplemental package sources. - -{{% /warning %}} #### Supplemental Package Sources @@ -305,9 +248,10 @@ poetry add --source pytorch-gpu-src torch torchvision torchaudio #### Package Source Constraint -All package sources (including secondary and possibly supplemental 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. +All package sources (including possibly supplemental sources) will be searched +during the package lookup process. +These network requests will occur for all primary sources, regardless of if the package is +found at one or more sources, and all supplemental sources until the package is found. In order to limit the search for a specific package to a particular package repository, you can specify the source explicitly. @@ -398,8 +342,8 @@ httpx = {version = "^0.22.0", source = "pypi"} {{% warning %}} -If any source within a project is configured with `priority = "default"`, The implicit `pypi` source will -be disabled and not used for any packages. +The implicit `PyPI` source will be disabled and not used for any packages +if at least one [primary source](#primary-package-sources) is configured. {{% /warning %}} diff --git a/poetry.lock b/poetry.lock index 14b6ec951e8..c89e75fbf0e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -833,6 +833,7 @@ files = [ {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, + {file = "msgpack-1.0.8-py3-none-any.whl", hash = "sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca"}, {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] diff --git a/src/poetry/config/config.py b/src/poetry/config/config.py index f2a8129f231..e16ead0a912 100644 --- a/src/poetry/config/config.py +++ b/src/poetry/config/config.py @@ -114,12 +114,7 @@ class Config: "options": { "always-copy": False, "system-site-packages": False, - # we default to False here in order to prevent development environment - # breakages for IDEs etc. as when working in these environments - # assumptions are often made about virtual environments having pip and - # setuptools. "no-pip": False, - "no-setuptools": False, }, "prefer-active-python": False, "prompt": "{project_name}-py{python_version}", @@ -139,9 +134,6 @@ class Config: "solver": { "lazy-wheel": True, }, - "warnings": { - "export": True, - }, "keyring": { "enabled": True, }, @@ -305,13 +297,11 @@ def _get_normalizer(name: str) -> Callable[[str], Any]: "virtualenvs.in-project", "virtualenvs.options.always-copy", "virtualenvs.options.no-pip", - "virtualenvs.options.no-setuptools", "virtualenvs.options.system-site-packages", "virtualenvs.options.prefer-active-python", "experimental.system-git-client", "installer.parallel", "solver.lazy-wheel", - "warnings.export", "keyring.enabled", }: return boolean_normalizer diff --git a/src/poetry/config/config_source.py b/src/poetry/config/config_source.py index ed97fa9176a..ae9da9da050 100644 --- a/src/poetry/config/config_source.py +++ b/src/poetry/config/config_source.py @@ -1,11 +1,13 @@ from __future__ import annotations +from abc import ABC +from abc import abstractmethod from typing import Any -class ConfigSource: - def add_property(self, key: str, value: Any) -> None: - raise NotImplementedError() +class ConfigSource(ABC): + @abstractmethod + def add_property(self, key: str, value: Any) -> None: ... - def remove_property(self, key: str) -> None: - raise NotImplementedError() + @abstractmethod + def remove_property(self, key: str) -> None: ... diff --git a/src/poetry/config/source.py b/src/poetry/config/source.py index f2ff82c2ece..aa7d9a25743 100644 --- a/src/poetry/config/source.py +++ b/src/poetry/config/source.py @@ -1,7 +1,6 @@ from __future__ import annotations import dataclasses -import warnings from typing import TYPE_CHECKING @@ -16,27 +15,13 @@ class Source: name: str url: str = "" - 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: + def __post_init__(self) -> 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( diff --git a/src/poetry/console/application.py b/src/poetry/console/application.py index d144c427d4f..a10b41a309e 100644 --- a/src/poetry/console/application.py +++ b/src/poetry/console/application.py @@ -4,7 +4,9 @@ import re from contextlib import suppress +from functools import cached_property from importlib import import_module +from pathlib import Path from typing import TYPE_CHECKING from typing import cast @@ -111,20 +113,13 @@ def __init__(self) -> None: @property def poetry(self) -> Poetry: - from pathlib import Path - from poetry.factory import Factory if self._poetry is not None: return self._poetry - project_path = Path.cwd() - - if self._io and self._io.input.option("directory"): - project_path = Path(self._io.input.option("directory")).absolute() - self._poetry = Factory().create_poetry( - cwd=project_path, + cwd=self._directory, io=self._io, disable_plugins=self._disable_plugins, disable_cache=self._disable_cache, @@ -340,16 +335,11 @@ def _load_plugins(self, io: IO | None = None) -> None: from poetry.plugins.application_plugin import ApplicationPlugin from poetry.plugins.plugin_manager import PluginManager + PluginManager.add_project_plugin_path(self._directory) manager = PluginManager(ApplicationPlugin.group) manager.load_plugins() manager.activate(self) - # We have to override the command from poetry-plugin-export - # with the wrapper. - if self.command_loader.has("export"): - del self.command_loader._factories["export"] - self.command_loader._factories["export"] = load_command("export") - self._plugins_loaded = True @property @@ -382,6 +372,12 @@ def _default_definition(self) -> Definition: return definition + @cached_property + def _directory(self) -> Path: + if self._io and self._io.input.option("directory"): + return Path(self._io.input.option("directory")).absolute() + return Path.cwd() + def main() -> int: exit_code: int = Application().run() diff --git a/src/poetry/console/commands/add.py b/src/poetry/console/commands/add.py index 330b29c951c..cfd7bc009a1 100644 --- a/src/poetry/console/commands/add.py +++ b/src/poetry/console/commands/add.py @@ -9,6 +9,7 @@ from cleo.helpers import argument from cleo.helpers import option from packaging.utils import canonicalize_name +from poetry.core.packages.dependency import Dependency from poetry.core.packages.dependency_group import MAIN_GROUP from tomlkit.toml_document import TOMLDocument @@ -17,8 +18,11 @@ if TYPE_CHECKING: + from collections.abc import Collection + from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option + from packaging.utils import NormalizedName class AddCommand(InstallerCommand, InitCommand): @@ -39,8 +43,7 @@ class AddCommand(InstallerCommand, InitCommand): option( "dev", "D", - "Add as a development dependency. (Deprecated) Use" - " --group=dev instead.", + "Add as a development dependency. (shortcut for '-G dev')", ), option("editable", "e", "Add vcs/path dependencies as editable."), option( @@ -50,7 +53,12 @@ class AddCommand(InstallerCommand, InitCommand): flag=False, multiple=True, ), - option("optional", None, "Add as an optional dependency."), + option( + "optional", + None, + "Add as an optional dependency to an extra.", + flag=False, + ), option( "python", None, @@ -111,6 +119,7 @@ class AddCommand(InstallerCommand, InitCommand): def handle(self) -> int: from poetry.core.constraints.version import parse_constraint + from tomlkit import array from tomlkit import inline_table from tomlkit import nl from tomlkit import table @@ -119,10 +128,6 @@ def handle(self) -> int: packages = self.argument("name") if self.option("dev"): - self.line_error( - "The --dev option is deprecated, " - "use the `--group dev` notation instead." - ) group = "dev" else: group = self.option("group", self.default_group or MAIN_GROUP) @@ -132,19 +137,42 @@ def handle(self) -> int: "You can only specify one package when using the --extras option" ) + optional = self.option("optional") + if optional and group != MAIN_GROUP: + raise ValueError("You can only add optional dependencies to the main group") + # tomlkit types are awkward to work with, treat content as a mostly untyped # dictionary. content: dict[str, Any] = self.poetry.file.read() - poetry_content = content["tool"]["poetry"] + project_content = content.get("project", table()) + poetry_content = content.get("tool", {}).get("poetry", table()) project_name = ( - canonicalize_name(name) if (name := poetry_content.get("name")) else None + canonicalize_name(name) + if (name := project_content.get("name", poetry_content.get("name"))) + else None ) + use_project_section = False + project_dependency_names = [] if group == MAIN_GROUP: - if "dependencies" not in poetry_content: - poetry_content["dependencies"] = table() + if ( + "dependencies" in project_content + or "optional-dependencies" in project_content + ): + use_project_section = True + if optional: + project_section = project_content.get( + "optional-dependencies", {} + ).get(optional, array()) + else: + project_section = project_content.get("dependencies", array()) + project_dependency_names = [ + Dependency.create_from_pep_508(dep).name for dep in project_section + ] + else: + project_section = array() - section = poetry_content["dependencies"] + poetry_section = poetry_content.get("dependencies", table()) else: if "group" not in poetry_content: poetry_content["group"] = table(is_super_table=True) @@ -160,9 +188,12 @@ def handle(self) -> int: if "dependencies" not in this_group: this_group["dependencies"] = table() - section = this_group["dependencies"] + poetry_section = this_group["dependencies"] + project_section = [] - existing_packages = self.get_existing_packages_from_input(packages, section) + existing_packages = self.get_existing_packages_from_input( + packages, poetry_section, project_dependency_names + ) if existing_packages: self.notify_about_existing_packages(existing_packages) @@ -173,6 +204,13 @@ def handle(self) -> int: self.line("Nothing to add.") return 0 + if optional and not use_project_section: + self.line_error( + "Optional dependencies will not be added to extras" + " in legacy mode. Consider converting your project to use the [project]" + " section." + ) + requirements = self._determine_requirements( packages, allow_prereleases=self.option("allow-prereleases"), @@ -187,13 +225,13 @@ def handle(self) -> int: parse_constraint(version) constraint: dict[str, Any] = inline_table() - for name, value in _constraint.items(): - if name == "name": + for key, value in _constraint.items(): + if key == "name": continue - constraint[name] = value + constraint[key] = value - if self.option("optional"): + if optional: constraint["optional"] = True if self.option("allow-prereleases"): @@ -244,28 +282,67 @@ def handle(self) -> int: self.line_error("\nNo changes were applied.") return 1 - 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( constraint_name ) - self.poetry.package.add_dependency( - Factory.create_dependency( - constraint_name, - constraint, - groups=[group], - root_dir=self.poetry.file.path.parent, - ) + dependency = Factory.create_dependency( + constraint_name, + constraint, + groups=[group], + root_dir=self.poetry.file.path.parent, ) + self.poetry.package.add_dependency(dependency) + + if use_project_section: + try: + index = project_dependency_names.index(canonical_constraint_name) + except ValueError: + project_section.append(dependency.to_pep_508()) + else: + project_section[index] = dependency.to_pep_508() + + # create a second constraint for tool.poetry.dependencies with keys + # that cannot be stored in the project section + poetry_constraint: dict[str, Any] = inline_table() + if not isinstance(constraint, str): + for key in ["allow-prereleases", "develop", "source"]: + if value := constraint.get(key): + poetry_constraint[key] = value + if poetry_constraint: + # add marker related keys to avoid ambiguity + for key in ["python", "platform"]: + if value := constraint.get(key): + poetry_constraint[key] = value + else: + poetry_constraint = constraint + + if poetry_constraint: + for key in poetry_section: + if canonicalize_name(key) == canonical_constraint_name: + poetry_section[key] = poetry_constraint + break + else: + poetry_section[constraint_name] = poetry_constraint # Refresh the locker + if project_section: + assert group == MAIN_GROUP + if optional: + if "optional-dependencies" not in project_content: + project_content["optional-dependencies"] = table() + if optional not in project_content["optional-dependencies"]: + project_content["optional-dependencies"][optional] = project_section + elif "dependencies" not in project_content: + project_content["dependencies"] = project_section + if poetry_section: + if "tool" not in content: + content["tool"] = table() + if "poetry" not in content["tool"]: + content["tool"]["poetry"] = poetry_content + if group == MAIN_GROUP and "dependencies" not in poetry_content: + poetry_content["dependencies"] = poetry_section self.poetry.locker.set_pyproject_data(content) self.installer.set_locker(self.poetry.locker) @@ -289,13 +366,20 @@ def handle(self) -> int: return status def get_existing_packages_from_input( - self, packages: list[str], section: dict[str, Any] + self, + packages: list[str], + section: dict[str, Any], + project_dependencies: Collection[NormalizedName], ) -> list[str]: existing_packages = [] for name in packages: + normalized_name = canonicalize_name(name) + if normalized_name in project_dependencies: + existing_packages.append(name) + continue for key in section: - if canonicalize_name(key) == canonicalize_name(name): + if normalized_name == canonicalize_name(key): existing_packages.append(name) return existing_packages diff --git a/src/poetry/console/commands/check.py b/src/poetry/console/commands/check.py index ba533f992ee..bd672409a81 100644 --- a/src/poetry/console/commands/check.py +++ b/src/poetry/console/commands/check.py @@ -130,21 +130,27 @@ def handle(self) -> int: # Load poetry config and display errors, if any poetry_file = self.poetry.file.path - config = PyProjectTOML(poetry_file).poetry_config - check_result = Factory.validate(config, strict=True) + toml_data = PyProjectTOML(poetry_file).data + check_result = Factory.validate(toml_data, strict=True) + + project = toml_data.get("project", {}) + poetry_config = toml_data["tool"]["poetry"] # Validate trove classifiers - project_classifiers = set(config.get("classifiers", [])) + project_classifiers = set( + project.get("classifiers") or poetry_config.get("classifiers", []) + ) errors, warnings = self._validate_classifiers(project_classifiers) check_result["errors"].extend(errors) check_result["warnings"].extend(warnings) # Validate readme (files must exist) - if "readme" in config: - errors = self._validate_readme(config["readme"], poetry_file) + # TODO: consider [project.readme] as well + if "readme" in poetry_config: + errors = self._validate_readme(poetry_config["readme"], poetry_file) check_result["errors"].extend(errors) - check_result["errors"] += self._validate_dependencies_source(config) + check_result["errors"] += self._validate_dependencies_source(poetry_config) # Verify that lock file is consistent if self.option("lock") and not self.poetry.locker.is_locked(): diff --git a/src/poetry/console/commands/config.py b/src/poetry/console/commands/config.py index 25fec8ce72f..2495ed0aa4a 100644 --- a/src/poetry/console/commands/config.py +++ b/src/poetry/console/commands/config.py @@ -66,10 +66,6 @@ def unique_config_values(self) -> dict[str, tuple[Any, Any]]: boolean_normalizer, ), "virtualenvs.options.no-pip": (boolean_validator, boolean_normalizer), - "virtualenvs.options.no-setuptools": ( - boolean_validator, - boolean_normalizer, - ), "virtualenvs.path": (str, lambda val: str(Path(val))), "virtualenvs.prefer-active-python": (boolean_validator, boolean_normalizer), "virtualenvs.prompt": (str, str), @@ -86,7 +82,6 @@ def unique_config_values(self) -> dict[str, tuple[Any, Any]]: PackageFilterPolicy.normalize, ), "solver.lazy-wheel": (boolean_validator, boolean_normalizer), - "warnings.export": (boolean_validator, boolean_normalizer), "keyring.enabled": (boolean_validator, boolean_normalizer), } @@ -95,7 +90,7 @@ def unique_config_values(self) -> dict[str, tuple[Any, Any]]: def handle(self) -> int: from pathlib import Path - from poetry.core.pyproject.exceptions import PyProjectException + from poetry.core.pyproject.exceptions import PyProjectError from poetry.config.config import Config from poetry.config.file_config_source import FileConfigSource @@ -109,7 +104,7 @@ def handle(self) -> int: local_config_file = TOMLFile(self.poetry.file.path.parent / "poetry.toml") if local_config_file.exists(): config.merge(local_config_file.read()) - except (RuntimeError, PyProjectException): + except (RuntimeError, PyProjectError): local_config_file = TOMLFile(Path.cwd() / "poetry.toml") if self.option("local"): diff --git a/src/poetry/console/commands/export.py b/src/poetry/console/commands/export.py deleted file mode 100644 index cf18e279deb..00000000000 --- a/src/poetry/console/commands/export.py +++ /dev/null @@ -1,21 +0,0 @@ -from __future__ import annotations - -from poetry_plugin_export.command import ( # type: ignore[import-untyped] - ExportCommand as BaseExportCommand, -) - - -class ExportCommand(BaseExportCommand): # type: ignore[misc] - def handle(self) -> int: - if self.poetry.config.get("warnings.export"): - self.line_error( - "Warning: poetry-plugin-export will not be installed by default in a" - " future version of Poetry.\n" - "In order to avoid a breaking change and make your automation" - " forward-compatible, please install poetry-plugin-export explicitly." - " See https://python-poetry.org/docs/plugins/#using-plugins for details" - " on how to install a plugin.\n" - "To disable this warning run 'poetry config warnings.export false'.", - style="warning", - ) - return super().handle() # type: ignore[no-any-return] diff --git a/src/poetry/console/commands/group_command.py b/src/poetry/console/commands/group_command.py index c080dc1e5e1..06dea92cc78 100644 --- a/src/poetry/console/commands/group_command.py +++ b/src/poetry/console/commands/group_command.py @@ -4,10 +4,9 @@ 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 +from poetry.console.exceptions import GroupNotFoundError if TYPE_CHECKING: @@ -80,19 +79,13 @@ 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), - ("dev", "with", "dev"), - ]: - if self.io.input.has_option(opt) and self.option(opt): - self.line_error( - f"The `--{opt}` option is" - f" deprecated, use the `--{new} {group}`" - " notation instead." - ) - groups[new].add(group) + if self.option("all-groups"): + groups["with"] = self.poetry.package.dependency_group_names( + include_optional=True + ) + + self._validate_group_options(groups) if groups["only"] and (groups["with"] or groups["without"]): self.line_error( @@ -128,4 +121,4 @@ def _validate_group_options(self, group_options: dict[str, set[str]]) -> None: for opt in sorted(invalid_options[group]) ) message_parts.append(f"{group} (via {opts})") - raise GroupNotFound(f"Group(s) not found: {', '.join(message_parts)}") + raise GroupNotFoundError(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 6725008daaf..e7fbf13109f 100644 --- a/src/poetry/console/commands/init.py +++ b/src/poetry/console/commands/init.py @@ -16,6 +16,7 @@ from poetry.console.commands.command import Command from poetry.console.commands.env_command import EnvCommand from poetry.utils.dependency_specification import RequirementsParser +from poetry.utils.env.python_manager import Python if TYPE_CHECKING: @@ -96,7 +97,6 @@ def _init_pyproject( from poetry.config.config import Config from poetry.layouts import layout from poetry.pyproject.toml import PyProjectTOML - from poetry.utils.env import EnvManager is_interactive = self.io.is_interactive() and allow_interactive @@ -105,8 +105,8 @@ def _init_pyproject( if pyproject.file.exists(): if pyproject.is_poetry_project(): self.line_error( - "A pyproject.toml file with a poetry section already" - " exists." + "A pyproject.toml file with a project and/or" + " a poetry section already exists." ) return 1 @@ -174,11 +174,7 @@ def _init_pyproject( config = Config.create() python = ( ">=" - + EnvManager.get_python_version( - precision=2, - prefer_active_python=config.get("virtualenvs.prefer-active-python"), - io=self.io, - ).to_string() + + Python.get_preferred_python(config, self.io).minor_version.to_string() ) if is_interactive: @@ -255,7 +251,7 @@ def _init_pyproject( if create_layout: layout_.create(project_path, with_pyproject=False) - content = layout_.generate_poetry_content() + content = layout_.generate_project_content() for section, item in content.items(): pyproject.data.append(section, item) @@ -463,12 +459,12 @@ def _find_best_version_for_package( return package.pretty_name, f"^{version.to_string()}" def _parse_requirements(self, requirements: list[str]) -> list[dict[str, Any]]: - from poetry.core.pyproject.exceptions import PyProjectException + from poetry.core.pyproject.exceptions import PyProjectError try: cwd = self.poetry.file.path.parent artifact_cache = self.poetry.pool.artifact_cache - except (PyProjectException, RuntimeError): + except (PyProjectError, RuntimeError): cwd = Path.cwd() artifact_cache = self._get_pool().artifact_cache diff --git a/src/poetry/console/commands/install.py b/src/poetry/console/commands/install.py index 63f4f93cb48..972d04d8f77 100644 --- a/src/poetry/console/commands/install.py +++ b/src/poetry/console/commands/install.py @@ -6,6 +6,7 @@ from cleo.helpers import option from poetry.console.commands.installer_command import InstallerCommand +from poetry.plugins.plugin_manager import PluginManager if TYPE_CHECKING: @@ -18,12 +19,6 @@ class InstallCommand(InstallerCommand): options: ClassVar[list[Option]] = [ *InstallerCommand._group_dependency_options(), - option( - "no-dev", - None, - "Do not install the development dependencies." - " (Deprecated)", - ), option( "sync", None, @@ -47,12 +42,6 @@ class InstallCommand(InstallerCommand): "Output the operations but do not execute anything " "(implicitly enables --verbose).", ), - option( - "remove-untracked", - None, - "Removes packages not present in the lock file." - " (Deprecated)", - ), option( "extras", "E", @@ -61,6 +50,7 @@ class InstallCommand(InstallerCommand): multiple=True, ), option("all-extras", None, "Install all extra dependencies."), + option("all-groups", None, "Install dependencies from all groups."), option("only-root", None, "Exclude all dependencies."), option( "compile", @@ -100,10 +90,12 @@ def activated_groups(self) -> set[str]: return super().activated_groups def handle(self) -> int: - from poetry.core.masonry.utils.module import ModuleOrPackageNotFound + from poetry.core.masonry.utils.module import ModuleOrPackageNotFoundError from poetry.masonry.builders.editable import EditableBuilder + PluginManager.ensure_project_plugins(self.poetry, self.io) + if self.option("extras") and self.option("all-extras"): self.line_error( "You cannot specify explicit" @@ -113,12 +105,14 @@ def handle(self) -> int: return 1 if self.option("only-root") and any( - self.option(key) for key in {"with", "without", "only"} + self.option(key) for key in {"with", "without", "only", "all-groups"} ): self.line_error( "The `--with`," - " `--without` and" - " `--only` options cannot be used with" + " `--without`," + " `--only` and" + " `--all-groups`" + " options cannot be used with" " the `--only-root`" " option." ) @@ -131,6 +125,17 @@ def handle(self) -> int: ) return 1 + if ( + self.option("only") or self.option("with") or self.option("without") + ) and self.option("all-groups"): + self.line_error( + "You cannot specify `--with`," + " `--without`, or" + " `--only` when using" + " `--all-groups`." + ) + return 1 + extras: list[str] if self.option("all-extras"): extras = list(self.poetry.package.extras.keys()) @@ -142,14 +147,6 @@ def handle(self) -> int: self.installer.extras(extras) with_synchronization = self.option("sync") - if self.option("remove-untracked"): - self.line_error( - "The `--remove-untracked` option is" - " deprecated, use the `--sync` option" - " instead." - ) - - with_synchronization = True self.installer.only_groups(self.activated_groups) self.installer.skip_directory(self.option("no-directory")) @@ -189,7 +186,7 @@ def handle(self) -> int: try: builder = EditableBuilder(self.poetry, self.env, self.io) builder.build() - except (ModuleOrPackageNotFound, FileNotFoundError) as e: + except (ModuleOrPackageNotFoundError, FileNotFoundError) as e: # This is likely due to the fact that the project is an application # not following the structure expected by Poetry. # No need for an editable install in this case. @@ -201,10 +198,11 @@ def handle(self) -> int: "If you want to use Poetry only for dependency management" " but not for packaging, you can disable package mode by setting" " package-mode = false in your pyproject.toml file.\n" - "In a future version of Poetry this warning will become an error!", - style="warning", + "If you did intend to install the current project, you may need" + " to set `packages` in your pyproject.toml file.\n", + style="error", ) - return 0 + return 1 if overwrite: self.overwrite(log_install.format(tag="success")) diff --git a/src/poetry/console/commands/lock.py b/src/poetry/console/commands/lock.py index c64f1dfd2ad..a7edc8b467d 100644 --- a/src/poetry/console/commands/lock.py +++ b/src/poetry/console/commands/lock.py @@ -18,14 +18,10 @@ class LockCommand(InstallerCommand): options: ClassVar[list[Option]] = [ option( - "no-update", None, "Do not update locked versions, only refresh lock file." - ), - option( - "check", + "regenerate", None, - "Check that the poetry.lock file corresponds to the current" - " version of pyproject.toml. (Deprecated) Use" - " poetry check --lock instead.", + "Ignore existing lock file" + " and overwrite it with a new lock file created from scratch.", ), ] @@ -34,6 +30,8 @@ class LockCommand(InstallerCommand): current directory, processes it, and locks the dependencies in the\ poetry.lock file. +By default, packages that have already been added to the lock file before +will not be updated. poetry lock """ @@ -41,22 +39,6 @@ class LockCommand(InstallerCommand): loggers: ClassVar[list[str]] = ["poetry.repositories.pypi_repository"] def handle(self) -> int: - if self.option("check"): - self.line_error( - "poetry lock --check is deprecated, use `poetry" - " check --lock` instead." - ) - if self.poetry.locker.is_locked() and self.poetry.locker.is_fresh(): - self.line("poetry.lock is consistent with pyproject.toml.") - return 0 - self.line_error( - "" - "Error: pyproject.toml changed significantly since poetry.lock was last generated. " - "Run `poetry lock [--no-update]` to fix the lock file." - "" - ) - return 1 - - self.installer.lock(update=not self.option("no-update")) + self.installer.lock(update=self.option("regenerate")) return self.installer.run() diff --git a/src/poetry/console/commands/remove.py b/src/poetry/console/commands/remove.py index 718a7d59903..a4ce37fc44a 100644 --- a/src/poetry/console/commands/remove.py +++ b/src/poetry/console/commands/remove.py @@ -7,6 +7,7 @@ from cleo.helpers import argument from cleo.helpers import option from packaging.utils import canonicalize_name +from poetry.core.packages.dependency import Dependency from poetry.core.packages.dependency_group import MAIN_GROUP from tomlkit.toml_document import TOMLDocument @@ -31,8 +32,7 @@ class RemoveCommand(InstallerCommand): "dev", "D", "Remove a package from the development dependencies." - " (Deprecated)" - " Use --group=dev instead.", + " (shortcut for '-G dev')", ), option( "dry-run", @@ -57,48 +57,50 @@ def handle(self) -> int: packages = self.argument("packages") if self.option("dev"): - self.line_error( - "The --dev option is deprecated, " - "use the `--group dev` notation instead." - ) group = "dev" else: group = self.option("group", self.default_group) content: dict[str, Any] = self.poetry.file.read() - poetry_content = content["tool"]["poetry"] + project_content = content.get("project", {}) + poetry_content = content.get("tool", {}).get("poetry", {}) if group is None: - removed = [] + # remove from all groups + removed = set() group_sections = [ - (group_name, group_section.get("dependencies", {})) - for group_name, group_section in poetry_content.get("group", {}).items() + ( + MAIN_GROUP, + project_content.get("dependencies", []), + poetry_content.get("dependencies", {}), + ) ] + group_sections.extend( + (group_name, [], group_section.get("dependencies", {})) + for group_name, group_section in poetry_content.get("group", {}).items() + ) - for group_name, section in [ - (MAIN_GROUP, poetry_content["dependencies"]), - *group_sections, - ]: - removed += self._remove_packages(packages, section, group_name) - if group_name != MAIN_GROUP: - if not section: - del poetry_content["group"][group_name] - else: - poetry_content["group"][group_name]["dependencies"] = section + for group_name, project_section, poetry_section in group_sections: + removed |= self._remove_packages( + packages, project_section, poetry_section, group_name + ) + if group_name != MAIN_GROUP and not poetry_section: + del poetry_content["group"][group_name] elif group == "dev" and "dev-dependencies" in poetry_content: # We need to account for the old `dev-dependencies` section removed = self._remove_packages( - packages, poetry_content["dev-dependencies"], "dev" + packages, [], poetry_content["dev-dependencies"], "dev" ) if not poetry_content["dev-dependencies"]: del poetry_content["dev-dependencies"] else: - removed = [] + removed = set() if "group" in poetry_content: if group in poetry_content["group"]: removed = self._remove_packages( packages, + [], poetry_content["group"][group].get("dependencies", {}), group, ) @@ -109,15 +111,13 @@ def handle(self) -> int: if "group" in poetry_content and not poetry_content["group"]: del poetry_content["group"] - removed_set = set(removed) - not_found = set(packages).difference(removed_set) + not_found = set(packages).difference(removed) if not_found: raise ValueError( "The following packages were not found: " + ", ".join(sorted(not_found)) ) # Refresh the locker - content["tool"]["poetry"] = poetry_content self.poetry.locker.set_pyproject_data(content) self.installer.set_locker(self.poetry.locker) self.installer.set_package(self.poetry.package) @@ -125,7 +125,7 @@ def handle(self) -> int: self.installer.verbose(self.io.is_verbose()) self.installer.update(True) self.installer.execute_operations(not self.option("lock")) - self.installer.whitelist(removed_set) + self.installer.whitelist(removed) status = self.installer.run() @@ -136,17 +136,27 @@ def handle(self) -> int: return status def _remove_packages( - self, packages: list[str], section: dict[str, Any], group_name: str - ) -> list[str]: - removed = [] + self, + packages: list[str], + project_section: list[str], + poetry_section: dict[str, Any], + group_name: str, + ) -> set[str]: + removed = set() group = self.poetry.package.dependency_group(group_name) - section_keys = list(section.keys()) for package in packages: - for existing_package in section_keys: - if canonicalize_name(existing_package) == canonicalize_name(package): - del section[existing_package] - removed.append(package) - group.remove_dependency(package) + normalized_name = canonicalize_name(package) + for requirement in project_section.copy(): + if Dependency.create_from_pep_508(requirement).name == normalized_name: + project_section.remove(requirement) + removed.add(package) + for existing_package in list(poetry_section): + if canonicalize_name(existing_package) == normalized_name: + del poetry_section[existing_package] + removed.add(package) + + for package in removed: + group.remove_dependency(package) return removed diff --git a/src/poetry/console/commands/self/show/plugins.py b/src/poetry/console/commands/self/show/plugins.py index 9f8299a35fa..3c59a28a265 100644 --- a/src/poetry/console/commands/self/show/plugins.py +++ b/src/poetry/console/commands/self/show/plugins.py @@ -70,9 +70,7 @@ def _system_project_handle(self) -> int: } for group in [ApplicationPlugin.group, Plugin.group]: - for entry_point in PluginManager(group).get_plugin_entry_points( - env=system_env - ): + for entry_point in PluginManager(group).get_plugin_entry_points(): assert entry_point.dist is not None package = packages_by_name[canonicalize_name(entry_point.dist.name)] diff --git a/src/poetry/console/commands/show.py b/src/poetry/console/commands/show.py index 6bcd7eca7fb..b47b4574666 100644 --- a/src/poetry/console/commands/show.py +++ b/src/poetry/console/commands/show.py @@ -44,11 +44,6 @@ class ShowCommand(GroupCommand, EnvCommand): ] options: ClassVar[list[Option]] = [ *GroupCommand._group_dependency_options(), - option( - "no-dev", - None, - "Do not list the development dependencies. (Deprecated)", - ), option("tree", "t", "List the dependencies as a tree."), option( "why", @@ -200,7 +195,7 @@ def _display_single_package_information( self.line("") self.line("required by") for parent, requires_version in required_by.items(): - self.line(f" - {parent} {requires_version}") + self.line(f" - {parent} requires {requires_version}") return 0 diff --git a/src/poetry/console/commands/source/add.py b/src/poetry/console/commands/source/add.py index 669ac2f3c01..541593cac1a 100644 --- a/src/poetry/console/commands/source/add.py +++ b/src/poetry/console/commands/source/add.py @@ -36,19 +36,6 @@ class SourceAddCommand(Command): ] options: ClassVar[list[Option]] = [ - 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. (Deprecated, use --priority)", - ), - option( - "secondary", - "s", - "Set this source as secondary. (Deprecated, use" - " --priority)", - ), option( "priority", "p", @@ -65,8 +52,6 @@ def handle(self) -> int: name: str = self.argument("name") lower_name = name.lower() url: str = self.argument("url") - is_default: bool = self.option("default", False) - is_secondary: bool = self.option("secondary", False) priority_str: str | None = self.option("priority", None) if lower_name == "pypi": @@ -82,66 +67,16 @@ def handle(self) -> int: ) return 1 - if is_default and is_secondary: - self.line_error( - "Cannot configure a source as both default and" - " secondary." - ) - return 1 - - if is_default or is_secondary: - if priority_str 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_str is None: + if priority_str is None: priority = Priority.PRIMARY else: priority = Priority[priority_str.upper()] - if priority is Priority.SECONDARY: - allowed_prios = ( - p for p in Priority if p not in {Priority.DEFAULT, Priority.SECONDARY} - ) - self.line_error( - "Warning: Priority 'secondary' is deprecated. Consider" - " changing the priority to one of the non-deprecated values:" - f" {', '.join(repr(p.name.lower()) for p in allowed_prios)}." - ) - if priority is Priority.DEFAULT: - self.line_error( - "Warning: Priority 'default' is deprecated. You can achieve" - " the same effect by changing the priority to 'primary' and putting" - " the source first." - ) - sources = AoT([]) new_source = Source(name=name, url=url, priority=priority) is_new_source = True for source in self.poetry.get_sources(): - if source.priority is Priority.DEFAULT and 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" - " time." - ) - return 1 - if source.name.lower() == lower_name: source = new_source is_new_source = False diff --git a/src/poetry/console/commands/update.py b/src/poetry/console/commands/update.py index dc300231d27..a9eed5aa724 100644 --- a/src/poetry/console/commands/update.py +++ b/src/poetry/console/commands/update.py @@ -25,12 +25,6 @@ class UpdateCommand(InstallerCommand): ] options: ClassVar[list[Option]] = [ *InstallerCommand._group_dependency_options(), - option( - "no-dev", - None, - "Do not update the development dependencies." - " (Deprecated)", - ), option( "sync", None, diff --git a/src/poetry/console/commands/version.py b/src/poetry/console/commands/version.py index 3968557a8aa..8bf7d61b428 100644 --- a/src/poetry/console/commands/version.py +++ b/src/poetry/console/commands/version.py @@ -6,7 +6,7 @@ from cleo.helpers import argument from cleo.helpers import option -from poetry.core.version.exceptions import InvalidVersion +from poetry.core.version.exceptions import InvalidVersionError from tomlkit.toml_document import TOMLDocument from poetry.console.commands.command import Command @@ -69,8 +69,12 @@ def handle(self) -> int: if not self.option("dry-run"): content: dict[str, Any] = self.poetry.file.read() - poetry_content = content["tool"]["poetry"] - poetry_content["version"] = version.text + project_content = content.get("project", {}) + if "version" in project_content: + project_content["version"] = version.text + poetry_content = content.get("tool", {}).get("poetry", {}) + if "version" in poetry_content: + poetry_content["version"] = version.text assert isinstance(content, TOMLDocument) self.poetry.file.write(content) @@ -92,7 +96,7 @@ def increment_version( try: parsed = Version.parse(version) - except InvalidVersion: + except InvalidVersionError: 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 2cc359ddb75..d45c38dd3df 100644 --- a/src/poetry/console/exceptions.py +++ b/src/poetry/console/exceptions.py @@ -7,5 +7,5 @@ class PoetryConsoleError(CleoError): pass -class GroupNotFound(PoetryConsoleError): +class GroupNotFoundError(PoetryConsoleError): pass diff --git a/src/poetry/console/logging/formatters/formatter.py b/src/poetry/console/logging/formatters/formatter.py index d18002be4b7..4d0f1302bab 100644 --- a/src/poetry/console/logging/formatters/formatter.py +++ b/src/poetry/console/logging/formatters/formatter.py @@ -1,6 +1,9 @@ from __future__ import annotations +from abc import ABC +from abc import abstractmethod -class Formatter: - def format(self, msg: str) -> str: - raise NotImplementedError() + +class Formatter(ABC): + @abstractmethod + def format(self, msg: str) -> str: ... diff --git a/src/poetry/exceptions.py b/src/poetry/exceptions.py index 68154f8d87d..170d1028edf 100644 --- a/src/poetry/exceptions.py +++ b/src/poetry/exceptions.py @@ -1,5 +1,5 @@ from __future__ import annotations -class PoetryException(Exception): +class PoetryError(Exception): pass diff --git a/src/poetry/factory.py b/src/poetry/factory.py index 4cfcd91d060..18e7cdc1df5 100644 --- a/src/poetry/factory.py +++ b/src/poetry/factory.py @@ -10,11 +10,14 @@ from cleo.io.null_io import NullIO from packaging.utils import canonicalize_name +from poetry.core.constraints.version import Version +from poetry.core.constraints.version import parse_constraint from poetry.core.factory import Factory as BaseFactory from poetry.core.packages.dependency_group import MAIN_GROUP +from poetry.__version__ import __version__ from poetry.config.config import Config -from poetry.exceptions import PoetryException +from poetry.exceptions import PoetryError from poetry.json import validate_object from poetry.packages.locker import Locker from poetry.plugins.plugin import Plugin @@ -56,6 +59,15 @@ def create_poetry( base_poetry = super().create_poetry(cwd=cwd, with_groups=with_groups) + if version_str := base_poetry.local_config.get("requires-poetry"): + version_constraint = parse_constraint(version_str) + version = Version.parse(__version__) + if not version_constraint.allows(version): + raise PoetryError( + f"This project requires Poetry {version_constraint}," + f" but you are using Poetry {version}" + ) + poetry_file = base_poetry.pyproject_path locker = Locker(poetry_file.parent / "poetry.lock", base_poetry.pyproject.data) @@ -73,7 +85,7 @@ def create_poetry( # Load local sources repositories = {} existing_repositories = config.get("repositories", {}) - for source in base_poetry.pyproject.poetry_config.get("source", []): + for source in base_poetry.local_config.get("source", []): name = source.get("name") url = source.get("url") if name and url and name not in existing_repositories: @@ -99,9 +111,10 @@ def create_poetry( ) ) - plugin_manager = PluginManager(Plugin.group, disable_plugins=disable_plugins) - plugin_manager.load_plugins() - plugin_manager.activate(poetry, io) + if not disable_plugins: + plugin_manager = PluginManager(Plugin.group) + plugin_manager.load_plugins() + plugin_manager.activate(poetry, io) return poetry @@ -130,46 +143,12 @@ def create_pool( source, config, disable_cache=disable_cache ) 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 priority is Priority.SECONDARY: - allowed_prios = (p for p in Priority if p is not Priority.SECONDARY) - warning = ( - "Found deprecated priority 'secondary' for source" - f" '{source.get('name')}' in pyproject.toml. Consider changing the" - " priority to one of the non-deprecated values:" - f" {', '.join(repr(p.name.lower()) for p in allowed_prios)}." - ) - io.write_error_line(f"Warning: {warning}") - elif priority is Priority.DEFAULT: - warning = ( - "Found deprecated priority 'default' for source" - f" '{source.get('name')}' in pyproject.toml. You can achieve" - " the same effect by changing the priority to 'primary' and putting" - " the source first." - ) - io.write_error_line(f"Warning: {warning}") if io.is_debug(): - message = f"Adding repository {repository.name} ({repository.url})" - if priority is Priority.DEFAULT: - message += " and setting it as the default one" - else: - message += f" and setting it as {priority.name.lower()}" - - io.write_line(message) + io.write_line( + f"Adding repository {repository.name} ({repository.url})" + f" and setting it as {priority.name.lower()}" + ) pool.add_repository(repository, priority=priority) if repository.name.lower() == "pypi": @@ -177,7 +156,7 @@ def create_pool( # Only add PyPI if no default repository is configured if not explicit_pypi: - if pool.has_default() or pool.has_primary_repositories(): + if pool.has_primary_repositories(): if io.is_debug(): io.write_line("Deactivating the PyPI repository") else: @@ -189,7 +168,7 @@ def create_pool( ) if not pool.repositories: - raise PoetryException( + raise PoetryError( "At least one source must not be configured as 'explicit'." ) @@ -340,22 +319,26 @@ def create_pyproject_from_package(cls, package: Package) -> TOMLDocument: @classmethod def validate( - cls, config: dict[str, Any], strict: bool = False + cls, toml_data: dict[str, Any], strict: bool = False ) -> dict[str, list[str]]: - results = super().validate(config, strict) + results = super().validate(toml_data, strict) + poetry_config = toml_data["tool"]["poetry"] - results["errors"].extend(validate_object(config)) + results["errors"].extend(validate_object(poetry_config)) # A project should not depend on itself. - dependencies = set(config.get("dependencies", {}).keys()) - dependencies.update(config.get("dev-dependencies", {}).keys()) - groups = config.get("group", {}).values() + # TODO: consider [project.dependencies] and [project.optional-dependencies] + dependencies = set(poetry_config.get("dependencies", {}).keys()) + dependencies.update(poetry_config.get("dev-dependencies", {}).keys()) + groups = poetry_config.get("group", {}).values() for group in groups: dependencies.update(group.get("dependencies", {}).keys()) dependencies = {canonicalize_name(d) for d in dependencies} - project_name = config.get("name") + project_name = toml_data.get("project", {}).get("name") or poetry_config.get( + "name" + ) if project_name is not None and canonicalize_name(project_name) in dependencies: results["errors"].append( f"Project name ({project_name}) is same as one of its dependencies" diff --git a/src/poetry/inspection/info.py b/src/poetry/inspection/info.py index 457fc97555f..e260818164d 100644 --- a/src/poetry/inspection/info.py +++ b/src/poetry/inspection/info.py @@ -22,8 +22,8 @@ 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.core.version.markers import InvalidMarkerError +from poetry.core.version.requirements import InvalidRequirementError from poetry.utils.helpers import extractall from poetry.utils.isolated_build import isolated_builder @@ -178,7 +178,7 @@ def to_package( try: # Attempt to parse the PEP-508 requirement string dependency = Dependency.create_from_pep_508(req, relative_to=root_dir) - except InvalidMarker: + except InvalidMarkerError: # Invalid marker, We strip the markers hoping for the best logger.warning( "Stripping invalid marker (%s) found in %s-%s dependencies", @@ -188,7 +188,7 @@ def to_package( ) req = req.split(";")[0] dependency = Dependency.create_from_pep_508(req, relative_to=root_dir) - except InvalidRequirement: + except InvalidRequirementError: # Unable to parse requirement so we skip it logger.warning( "Invalid requirement (%s) found in %s-%s dependencies, skipping", diff --git a/src/poetry/inspection/lazy_wheel.py b/src/poetry/inspection/lazy_wheel.py index 66c6dfccd88..8722a04ef5c 100644 --- a/src/poetry/inspection/lazy_wheel.py +++ b/src/poetry/inspection/lazy_wheel.py @@ -14,7 +14,6 @@ from typing import Any from typing import BinaryIO from typing import ClassVar -from typing import TypeVar from typing import cast from urllib.parse import urlparse from zipfile import BadZipFile @@ -34,6 +33,7 @@ from packaging.metadata import RawMetadata from requests import Session + from typing_extensions import Self from poetry.utils.authenticator import Authenticator @@ -45,20 +45,20 @@ class LazyWheelUnsupportedError(Exception): """Raised when a lazy wheel is unsupported.""" -class HTTPRangeRequestUnsupported(LazyWheelUnsupportedError): +class HTTPRangeRequestUnsupportedError(LazyWheelUnsupportedError): """Raised when the remote server appears unable to support byte ranges.""" -class HTTPRangeRequestNotRespected(LazyWheelUnsupportedError): +class HTTPRangeRequestNotRespectedError(LazyWheelUnsupportedError): """Raised when the remote server tells us that it supports byte ranges but does not respect a respective request.""" -class UnsupportedWheel(LazyWheelUnsupportedError): +class UnsupportedWheelError(LazyWheelUnsupportedError): """Unsupported wheel.""" -class InvalidWheel(LazyWheelUnsupportedError): +class InvalidWheelError(LazyWheelUnsupportedError): """Invalid (e.g. corrupt) wheel.""" def __init__(self, location: str, name: str) -> None: @@ -77,8 +77,8 @@ def metadata_from_wheel_url( This uses HTTP range requests to only fetch the portion of the wheel containing metadata, just enough for the object to be constructed. - :raises HTTPRangeRequestUnsupported: if range requests are unsupported for ``url``. - :raises InvalidWheel: if the zip file contents could not be parsed. + :raises HTTPRangeRequestUnsupportedError: if range requests are unsupported for ``url``. + :raises InvalidWheelError: if the zip file contents could not be parsed. """ try: # After context manager exit, wheel.name will point to a deleted file path. @@ -89,11 +89,11 @@ def metadata_from_wheel_url( metadata, _ = parse_email(metadata_bytes) return metadata - except (BadZipFile, UnsupportedWheel): + except (BadZipFile, UnsupportedWheelError): # We assume that these errors have occurred because the wheel contents # themselves are invalid, not because we've messed up our bookkeeping # and produced an invalid file. - raise InvalidWheel(url, name) + raise InvalidWheelError(url, name) except Exception as e: if isinstance(e, LazyWheelUnsupportedError): # this is expected when the code handles issues with lazy wheel metadata retrieval correctly @@ -168,9 +168,6 @@ def minimal_intervals_covering( yield from self._merge(start, end, left, right) -T = TypeVar("T", bound="ReadOnlyIOWrapper") - - class ReadOnlyIOWrapper(BinaryIO): """Implement read-side ``BinaryIO`` methods wrapping an inner ``BinaryIO``. @@ -181,7 +178,7 @@ class ReadOnlyIOWrapper(BinaryIO): def __init__(self, inner: BinaryIO) -> None: self._file = inner - def __enter__(self: T) -> T: + def __enter__(self) -> Self: self._file.__enter__() return self @@ -286,15 +283,12 @@ def writelines(self, lines: Iterable[Any]) -> None: raise NotImplementedError -U = TypeVar("U", bound="LazyFileOverHTTP") - - class LazyFileOverHTTP(ReadOnlyIOWrapper): """File-like object representing a fixed-length file over HTTP. This uses HTTP range requests to lazily fetch the file's content into a temporary file. If such requests are not supported by the server, raises - ``HTTPRangeRequestUnsupported`` in the ``__enter__`` method.""" + ``HTTPRangeRequestUnsupportedError`` in the ``__enter__`` method.""" def __init__( self, @@ -311,7 +305,7 @@ def __init__( self._session = session self._url = url - def __enter__(self: U) -> U: + def __enter__(self) -> Self: super().__enter__() self._setup_content() return self @@ -407,7 +401,7 @@ def _reset_content(self) -> None: def _content_length_from_head(self) -> int: """Performs a HEAD request to extract the Content-Length. - :raises HTTPRangeRequestUnsupported: if the response fails to indicate support + :raises HTTPRangeRequestUnsupportedError: if the response fails to indicate support for "bytes" ranges.""" self._request_count += 1 head = self._session.head( @@ -417,7 +411,7 @@ def _content_length_from_head(self) -> int: assert head.status_code == codes.ok accepted_range = head.headers.get("Accept-Ranges", None) if accepted_range != "bytes": - raise HTTPRangeRequestUnsupported( + raise HTTPRangeRequestUnsupportedError( f"server does not support byte ranges: header was '{accepted_range}'" ) return int(head.headers["Content-Length"]) @@ -437,7 +431,7 @@ def _stream_response(self, start: int, end: int) -> Response: response = self._session.get(self._url, headers=headers, stream=True) response.raise_for_status() if int(response.headers["Content-Length"]) != (end - start + 1): - raise HTTPRangeRequestNotRespected( + raise HTTPRangeRequestNotRespectedError( f"server did not respect byte range request: " f"requested {end - start + 1} bytes, got " f"{response.headers['Content-Length']} bytes" @@ -590,7 +584,9 @@ def _parse_full_length_from_content_range(arg: str) -> int: """ m = re.match(r"bytes [^/]+/([0-9]+)", arg) if m is None: - raise HTTPRangeRequestUnsupported(f"could not parse Content-Range: '{arg}'") + raise HTTPRangeRequestUnsupportedError( + f"could not parse Content-Range: '{arg}'" + ) return int(m.group(1)) def _try_initial_chunk_request( @@ -620,7 +616,7 @@ def _try_initial_chunk_request( if accept_ranges == "bytes" and content_length <= initial_chunk_size: return content_length, tail - raise HTTPRangeRequestUnsupported( + raise HTTPRangeRequestUnsupportedError( f"did not receive partial content: got code {code}" ) @@ -722,7 +718,7 @@ def _prefetch_metadata(self, name: str) -> str: end = info.header_offset break if start is None: - raise UnsupportedWheel( + raise UnsupportedWheelError( f"no {self._metadata_regex!r} found for {name} in {self.name}" ) # If it is the last entry of the zip, then give us everything diff --git a/src/poetry/installation/chef.py b/src/poetry/installation/chef.py index 35586e42036..7341678a9df 100644 --- a/src/poetry/installation/chef.py +++ b/src/poetry/installation/chef.py @@ -50,7 +50,7 @@ def _prepare( ) -> Path: from subprocess import CalledProcessError - distribution: DistributionType = "editable" if editable else "wheel" # type: ignore[assignment] + distribution: DistributionType = "editable" if editable else "wheel" error: Exception | None = None try: diff --git a/src/poetry/installation/executor.py b/src/poetry/installation/executor.py index 196cdd5acfd..2f5f550a619 100644 --- a/src/poetry/installation/executor.py +++ b/src/poetry/installation/executor.py @@ -6,6 +6,7 @@ import json import threading +from collections import defaultdict from concurrent.futures import ThreadPoolExecutor from concurrent.futures import wait from pathlib import Path @@ -32,6 +33,7 @@ from poetry.utils.helpers import remove_directory from poetry.utils.isolated_build import IsolatedBuildError from poetry.utils.isolated_build import IsolatedBuildInstallError +from poetry.vcs.git import Git if TYPE_CHECKING: @@ -45,6 +47,12 @@ from poetry.utils.env import Env +def _package_get_name(package: Package) -> str | None: + if url := package.repository_url: + return Git.get_name_from_source_url(url) + return None + + class Executor: def __init__( self, @@ -144,6 +152,7 @@ def execute(self, operations: list[Operation]) -> int: for _, group in groups: tasks = [] serial_operations = [] + serial_git_operations = defaultdict(list) for operation in group: if self._shutdown: break @@ -158,17 +167,43 @@ def execute(self, operations: list[Operation]) -> int: operation.package.develop and operation.package.source_type in {"directory", "git"} ) - if not operation.skipped and is_parallel_unsafe: + # Skipped operations are safe to execute in parallel + if operation.skipped: + is_parallel_unsafe = False + + if is_parallel_unsafe: serial_operations.append(operation) - continue + elif operation.package.source_type == "git": + # Serially execute git operations that get cloned to the same directory, + # to prevent multiple parallel git operations in the same repo. + serial_git_operations[_package_get_name(operation.package)].append( + operation + ) + else: + tasks.append( + self._executor.submit(self._execute_operation, operation) + ) - tasks.append(self._executor.submit(self._execute_operation, operation)) + def _serialize( + repository_serial_operations: list[Operation], + ) -> None: + for operation in repository_serial_operations: + self._execute_operation(operation) + + # For each git repository, execute all operations serially + for repository_git_operations in serial_git_operations.values(): + tasks.append( + self._executor.submit( + _serialize, + repository_serial_operations=repository_git_operations, + ) + ) try: wait(tasks) for operation in serial_operations: - wait([self._executor.submit(self._execute_operation, operation)]) + self._execute_operation(operation) except KeyboardInterrupt: self._shutdown = True @@ -577,8 +612,6 @@ def _prepare_archive( ) 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 diff --git a/src/poetry/installation/operations/operation.py b/src/poetry/installation/operations/operation.py index e6d7ce582f1..824da8b852e 100644 --- a/src/poetry/installation/operations/operation.py +++ b/src/poetry/installation/operations/operation.py @@ -1,16 +1,16 @@ from __future__ import annotations +from abc import ABC +from abc import abstractmethod from typing import TYPE_CHECKING -from typing import TypeVar if TYPE_CHECKING: from poetry.core.packages.package import Package + from typing_extensions import Self -T = TypeVar("T", bound="Operation") - -class Operation: +class Operation(ABC): def __init__(self, reason: str | None = None, priority: float = 0) -> None: self._reason = reason @@ -19,8 +19,8 @@ def __init__(self, reason: str | None = None, priority: float = 0) -> None: self._priority = priority @property - def job_type(self) -> str: - raise NotImplementedError + @abstractmethod + def job_type(self) -> str: ... @property def reason(self) -> str | None: @@ -39,14 +39,14 @@ def priority(self) -> float: return self._priority @property - def package(self) -> Package: - raise NotImplementedError() + @abstractmethod + def package(self) -> Package: ... def format_version(self, package: Package) -> str: version: str = package.full_pretty_version return version - def skip(self: T, reason: str) -> T: + def skip(self, reason: str) -> Self: self._skipped = True self._skip_reason = reason diff --git a/src/poetry/json/__init__.py b/src/poetry/json/__init__.py index c9872731a37..0ff29680144 100644 --- a/src/poetry/json/__init__.py +++ b/src/poetry/json/__init__.py @@ -30,8 +30,8 @@ def validate_object(obj: dict[str, Any]) -> list[str]: (CORE_SCHEMA_DIR / "poetry-schema.json").read_text(encoding="utf-8") ) - properties = {*schema["properties"].keys(), *core_schema["properties"].keys()} - additional_properties = set(obj.keys()) - properties + properties = schema["properties"].keys() | core_schema["properties"].keys() + additional_properties = obj.keys() - properties for key in additional_properties: errors.append(f"Additional properties are not allowed ('{key}' was unexpected)") diff --git a/src/poetry/json/schemas/poetry.json b/src/poetry/json/schemas/poetry.json index 93a822d28b9..ca48f5a9e3c 100644 --- a/src/poetry/json/schemas/poetry.json +++ b/src/poetry/json/schemas/poetry.json @@ -4,6 +4,17 @@ "type": "object", "required": [], "properties": { + "requires-poetry": { + "type": "string", + "description": "The version constraint for Poetry itself.", + "$ref": "#/definitions/dependency" + }, + "requires-plugins": { + "type": "object", + "description": "Poetry plugins that are required for this project.", + "$ref": "#/definitions/dependencies", + "additionalProperties": false + }, "source": { "type": "array", "description": "A set of additional repositories where packages can be found.", @@ -32,19 +43,9 @@ "description": "The url of the repository.", "format": "uri" }, - "default": { - "type": "boolean", - "description": "Make this repository the default (disable PyPI). (deprecated, see priority)" - }, - "secondary": { - "type": "boolean", - "description": "Declare this repository as secondary, i.e. default repositories take precedence. (deprecated, see priority)" - }, "priority": { "enum": [ "primary", - "default", - "secondary", "supplemental", "explicit" ], @@ -58,20 +59,336 @@ "type": "boolean", "description": "For PEP 503 simple API repositories, pre-fetch and index the available packages. (experimental)" } - }, - "not": { - "anyOf": [ + } + }, + "dependencies": { + "type": "object", + "patternProperties": { + "^[a-zA-Z-_.0-9]+$": { + "oneOf": [ + { + "$ref": "#/definitions/dependency" + }, + { + "$ref": "#/definitions/long-dependency" + }, + { + "$ref": "#/definitions/git-dependency" + }, + { + "$ref": "#/definitions/file-dependency" + }, + { + "$ref": "#/definitions/path-dependency" + }, + { + "$ref": "#/definitions/url-dependency" + }, + { + "$ref": "#/definitions/multiple-constraints-dependency" + }, + { + "$ref": "#/definitions/dependency-options" + } + ] + } + } + }, + "dependency": { + "type": "string", + "description": "The constraint of the dependency." + }, + "long-dependency": { + "type": "object", + "required": [ + "version" + ], + "additionalProperties": false, + "properties": { + "version": { + "type": "string", + "description": "The constraint of the dependency." + }, + "python": { + "type": "string", + "description": "The python versions for which the dependency should be installed." + }, + "platform": { + "type": "string", + "description": "The platform(s) for which the dependency should be installed." + }, + "markers": { + "type": "string", + "description": "The PEP 508 compliant environment markers for which the dependency should be installed." + }, + "allow-prereleases": { + "type": "boolean", + "description": "Whether the dependency allows prereleases or not." + }, + "allows-prereleases": { + "type": "boolean", + "description": "Whether the dependency allows prereleases or not." + }, + "optional": { + "type": "boolean", + "description": "Whether the dependency is optional or not." + }, + "extras": { + "type": "array", + "description": "The required extras for this dependency.", + "items": { + "type": "string" + } + }, + "source": { + "type": "string", + "description": "The exclusive source used to search for this dependency." + } + } + }, + "git-dependency": { + "type": "object", + "required": [ + "git" + ], + "additionalProperties": false, + "properties": { + "git": { + "type": "string", + "description": "The url of the git repository." + }, + "branch": { + "type": "string", + "description": "The branch to checkout." + }, + "tag": { + "type": "string", + "description": "The tag to checkout." + }, + "rev": { + "type": "string", + "description": "The revision to checkout." + }, + "subdirectory": { + "type": "string", + "description": "The relative path to the directory where the package is located." + }, + "python": { + "type": "string", + "description": "The python versions for which the dependency should be installed." + }, + "platform": { + "type": "string", + "description": "The platform(s) for which the dependency should be installed." + }, + "markers": { + "type": "string", + "description": "The PEP 508 compliant environment markers for which the dependency should be installed." + }, + "allow-prereleases": { + "type": "boolean", + "description": "Whether the dependency allows prereleases or not." + }, + "allows-prereleases": { + "type": "boolean", + "description": "Whether the dependency allows prereleases or not." + }, + "optional": { + "type": "boolean", + "description": "Whether the dependency is optional or not." + }, + "extras": { + "type": "array", + "description": "The required extras for this dependency.", + "items": { + "type": "string" + } + }, + "develop": { + "type": "boolean", + "description": "Whether to install the dependency in development mode." + } + } + }, + "file-dependency": { + "type": "object", + "required": [ + "file" + ], + "additionalProperties": false, + "properties": { + "file": { + "type": "string", + "description": "The path to the file." + }, + "subdirectory": { + "type": "string", + "description": "The relative path to the directory where the package is located." + }, + "python": { + "type": "string", + "description": "The python versions for which the dependency should be installed." + }, + "platform": { + "type": "string", + "description": "The platform(s) for which the dependency should be installed." + }, + "markers": { + "type": "string", + "description": "The PEP 508 compliant environment markers for which the dependency should be installed." + }, + "optional": { + "type": "boolean", + "description": "Whether the dependency is optional or not." + }, + "extras": { + "type": "array", + "description": "The required extras for this dependency.", + "items": { + "type": "string" + } + } + } + }, + "path-dependency": { + "type": "object", + "required": [ + "path" + ], + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "description": "The path to the dependency." + }, + "subdirectory": { + "type": "string", + "description": "The relative path to the directory where the package is located." + }, + "python": { + "type": "string", + "description": "The python versions for which the dependency should be installed." + }, + "platform": { + "type": "string", + "description": "The platform(s) for which the dependency should be installed." + }, + "markers": { + "type": "string", + "description": "The PEP 508 compliant environment markers for which the dependency should be installed." + }, + "optional": { + "type": "boolean", + "description": "Whether the dependency is optional or not." + }, + "extras": { + "type": "array", + "description": "The required extras for this dependency.", + "items": { + "type": "string" + } + }, + "develop": { + "type": "boolean", + "description": "Whether to install the dependency in development mode." + } + } + }, + "url-dependency": { + "type": "object", + "required": [ + "url" + ], + "additionalProperties": false, + "properties": { + "url": { + "type": "string", + "description": "The url to the file." + }, + "subdirectory": { + "type": "string", + "description": "The relative path to the directory where the package is located." + }, + "python": { + "type": "string", + "description": "The python versions for which the dependency should be installed." + }, + "platform": { + "type": "string", + "description": "The platform(s) for which the dependency should be installed." + }, + "markers": { + "type": "string", + "description": "The PEP 508 compliant environment markers for which the dependency should be installed." + }, + "optional": { + "type": "boolean", + "description": "Whether the dependency is optional or not." + }, + "extras": { + "type": "array", + "description": "The required extras for this dependency.", + "items": { + "type": "string" + } + } + } + }, + "dependency-options": { + "type": "object", + "additionalProperties": false, + "properties": { + "python": { + "type": "string", + "description": "The python versions for which the dependency should be installed." + }, + "platform": { + "type": "string", + "description": "The platform(s) for which the dependency should be installed." + }, + "markers": { + "type": "string", + "description": "The PEP 508 compliant environment markers for which the dependency should be installed." + }, + "allow-prereleases": { + "type": "boolean", + "description": "Whether the dependency allows prereleases or not." + }, + "source": { + "type": "string", + "description": "The exclusive source used to search for this dependency." + }, + "develop": { + "type": "boolean", + "description": "Whether to install the dependency in development mode." + } + } + }, + "multiple-constraints-dependency": { + "type": "array", + "minItems": 1, + "items": { + "oneOf": [ + { + "$ref": "#/definitions/dependency" + }, + { + "$ref": "#/definitions/long-dependency" + }, + { + "$ref": "#/definitions/git-dependency" + }, + { + "$ref": "#/definitions/file-dependency" + }, + { + "$ref": "#/definitions/path-dependency" + }, { - "required": [ - "priority", - "default" - ] + "$ref": "#/definitions/url-dependency" }, { - "required": [ - "priority", - "secondary" - ] + "$ref": "#/definitions/dependency-options" } ] } diff --git a/src/poetry/layouts/layout.py b/src/poetry/layouts/layout.py index f5174ba3951..1fbd897f4a8 100644 --- a/src/poetry/layouts/layout.py +++ b/src/poetry/layouts/layout.py @@ -5,12 +5,14 @@ from typing import Any from packaging.utils import canonicalize_name +from poetry.core.packages.package import AUTHOR_REGEX 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.factory import Factory from poetry.pyproject.toml import PyProjectTOML @@ -21,16 +23,20 @@ POETRY_DEFAULT = """\ -[tool.poetry] +[project] name = "" version = "" description = "" -authors = [] -license = "" +authors = [ +] +license = {} readme = "" -packages = [] +requires-python = "" +dependencies = [ +] -[tool.poetry.dependencies] +[tool.poetry] +packages = [] [tool.poetry.group.dev.dependencies] """ @@ -48,7 +54,7 @@ def __init__( readme_format: str = "md", author: str | None = None, license: str | None = None, - python: str = "*", + python: str | None = None, dependencies: Mapping[str, str | Mapping[str, Any]] | None = None, dev_dependencies: Mapping[str, str | Mapping[str, Any]] | None = None, ) -> None: @@ -117,34 +123,49 @@ def create( if with_pyproject: self._write_poetry(path) - def generate_poetry_content(self) -> TOMLDocument: + def generate_project_content(self) -> TOMLDocument: template = POETRY_DEFAULT content: dict[str, Any] = loads(template) - poetry_content = content["tool"]["poetry"] - poetry_content["name"] = self._project - poetry_content["version"] = self._version - poetry_content["description"] = self._description - poetry_content["authors"].append(self._author) + project_content = content["project"] + project_content["name"] = self._project + project_content["version"] = self._version + project_content["description"] = self._description + m = AUTHOR_REGEX.match(self._author) + if m is None: + # This should not happen because author has been validated before. + raise ValueError(f"Invalid author: {self._author}") + else: + author = {"name": m.group("name")} + if email := m.group("email"): + author["email"] = email + project_content["authors"].append(author) if self._license: - poetry_content["license"] = self._license + project_content["license"]["text"] = self._license + else: + project_content.remove("license") + + project_content["readme"] = f"README.{self._readme_format}" + + if self._python: + project_content["requires-python"] = self._python else: - poetry_content.remove("license") + project_content.remove("requires-python") + + for dep_name, dep_constraint in self._dependencies.items(): + dependency = Factory.create_dependency(dep_name, dep_constraint) + project_content["dependencies"].append(dependency.to_pep_508()) + + poetry_content = content["tool"]["poetry"] - poetry_content["readme"] = f"README.{self._readme_format}" packages = self.get_package_include() if packages: poetry_content["packages"].append(packages) else: poetry_content.remove("packages") - poetry_content["dependencies"]["python"] = self._python - - for dep_name, dep_constraint in self._dependencies.items(): - poetry_content["dependencies"][dep_name] = dep_constraint - if self._dev_dependencies: for dep_name, dep_constraint in self._dev_dependencies.items(): poetry_content["group"]["dev"]["dependencies"][dep_name] = ( @@ -153,6 +174,9 @@ def generate_poetry_content(self) -> TOMLDocument: else: del poetry_content["group"] + if not poetry_content: + del content["tool"]["poetry"] + # Add build system build_system = table() build_system_version = "" @@ -194,7 +218,7 @@ def _create_tests(path: Path) -> None: def _write_poetry(self, path: Path) -> None: pyproject = PyProjectTOML(path / "pyproject.toml") - content = self.generate_poetry_content() + content = self.generate_project_content() for section, item in content.items(): pyproject.data.append(section, item) pyproject.save() diff --git a/src/poetry/mixology/failure.py b/src/poetry/mixology/failure.py index 84a7fb0642d..230cbdbd85d 100644 --- a/src/poetry/mixology/failure.py +++ b/src/poetry/mixology/failure.py @@ -4,15 +4,15 @@ from poetry.core.constraints.version import parse_constraint -from poetry.mixology.incompatibility_cause import ConflictCause -from poetry.mixology.incompatibility_cause import PythonCause +from poetry.mixology.incompatibility_cause import ConflictCauseError +from poetry.mixology.incompatibility_cause import PythonCauseError if TYPE_CHECKING: from poetry.mixology.incompatibility import Incompatibility -class SolveFailure(Exception): +class SolveFailureError(Exception): def __init__(self, incompatibility: Incompatibility) -> None: self._incompatibility = incompatibility @@ -38,7 +38,7 @@ def write(self) -> str: version_solutions = [] required_python_version_notification = False for incompatibility in self._root.external_incompatibilities: - if isinstance(incompatibility.cause, PythonCause): + if isinstance(incompatibility.cause, PythonCauseError): root_constraint = parse_constraint( incompatibility.cause.root_python_version ) @@ -73,7 +73,7 @@ def write(self) -> str: if required_python_version_notification: buffer.append("") - if isinstance(self._root.cause, ConflictCause): + if isinstance(self._root.cause, ConflictCauseError): self._visit(self._root) else: self._write(self._root, f"Because {self._root}, version solving failed.") @@ -150,10 +150,10 @@ def _visit( incompatibility_string = str(incompatibility) cause = incompatibility.cause - assert isinstance(cause, ConflictCause) + assert isinstance(cause, ConflictCauseError) - if isinstance(cause.conflict.cause, ConflictCause) and isinstance( - cause.other.cause, ConflictCause + if isinstance(cause.conflict.cause, ConflictCauseError) and isinstance( + cause.other.cause, ConflictCauseError ): conflict_line = self._line_numbers.get(cause.conflict) other_line = self._line_numbers.get(cause.other) @@ -211,17 +211,17 @@ def _visit( f" {incompatibility_string}", numbered=numbered, ) - elif isinstance(cause.conflict.cause, ConflictCause) or isinstance( - cause.other.cause, ConflictCause + elif isinstance(cause.conflict.cause, ConflictCauseError) or isinstance( + cause.other.cause, ConflictCauseError ): derived = ( cause.conflict - if isinstance(cause.conflict.cause, ConflictCause) + if isinstance(cause.conflict.cause, ConflictCauseError) else cause.other ) ext = ( cause.other - if isinstance(cause.conflict.cause, ConflictCause) + if isinstance(cause.conflict.cause, ConflictCauseError) else cause.conflict ) @@ -235,8 +235,8 @@ def _visit( ) elif self._is_collapsible(derived): derived_cause = derived.cause - assert isinstance(derived_cause, ConflictCause) - if isinstance(derived_cause.conflict.cause, ConflictCause): + assert isinstance(derived_cause, ConflictCauseError) + if isinstance(derived_cause.conflict.cause, ConflictCauseError): collapsed_derived = derived_cause.conflict collapsed_ext = derived_cause.other else: @@ -271,29 +271,29 @@ def _is_collapsible(self, incompatibility: Incompatibility) -> bool: return False cause = incompatibility.cause - assert isinstance(cause, ConflictCause) - if isinstance(cause.conflict.cause, ConflictCause) and isinstance( - cause.other.cause, ConflictCause + assert isinstance(cause, ConflictCauseError) + if isinstance(cause.conflict.cause, ConflictCauseError) and isinstance( + cause.other.cause, ConflictCauseError ): return False - if not isinstance(cause.conflict.cause, ConflictCause) and not isinstance( - cause.other.cause, ConflictCause + if not isinstance(cause.conflict.cause, ConflictCauseError) and not isinstance( + cause.other.cause, ConflictCauseError ): return False complex = ( cause.conflict - if isinstance(cause.conflict.cause, ConflictCause) + if isinstance(cause.conflict.cause, ConflictCauseError) else cause.other ) return complex not in self._line_numbers - def _is_single_line(self, cause: ConflictCause) -> bool: - return not isinstance(cause.conflict.cause, ConflictCause) and not isinstance( - cause.other.cause, ConflictCause - ) + def _is_single_line(self, cause: ConflictCauseError) -> bool: + return not isinstance( + cause.conflict.cause, ConflictCauseError + ) and not isinstance(cause.other.cause, ConflictCauseError) def _count_derivations(self, incompatibility: Incompatibility) -> None: if incompatibility in self._derivations: @@ -301,6 +301,6 @@ def _count_derivations(self, incompatibility: Incompatibility) -> None: else: self._derivations[incompatibility] = 1 cause = incompatibility.cause - if isinstance(cause, ConflictCause): + if isinstance(cause, ConflictCauseError): self._count_derivations(cause.conflict) self._count_derivations(cause.other) diff --git a/src/poetry/mixology/incompatibility.py b/src/poetry/mixology/incompatibility.py index 9a2cc96294c..32ceec37863 100644 --- a/src/poetry/mixology/incompatibility.py +++ b/src/poetry/mixology/incompatibility.py @@ -2,30 +2,30 @@ from typing import TYPE_CHECKING -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 PlatformCause -from poetry.mixology.incompatibility_cause import PythonCause -from poetry.mixology.incompatibility_cause import RootCause +from poetry.mixology.incompatibility_cause import ConflictCauseError +from poetry.mixology.incompatibility_cause import DependencyCauseError +from poetry.mixology.incompatibility_cause import NoVersionsCauseError +from poetry.mixology.incompatibility_cause import PlatformCauseError +from poetry.mixology.incompatibility_cause import PythonCauseError +from poetry.mixology.incompatibility_cause import RootCauseError if TYPE_CHECKING: from collections.abc import Callable from collections.abc import Iterator - from poetry.mixology.incompatibility_cause import IncompatibilityCause + from poetry.mixology.incompatibility_cause import IncompatibilityCauseError from poetry.mixology.term import Term class Incompatibility: - def __init__(self, terms: list[Term], cause: IncompatibilityCause) -> None: + def __init__(self, terms: list[Term], cause: IncompatibilityCauseError) -> None: # Remove the root package from generated incompatibilities, since it will # always be satisfied. This makes error reporting clearer, and may also # make solving more efficient. if ( len(terms) != 1 - and isinstance(cause, ConflictCause) + and isinstance(cause, ConflictCauseError) and any(term.is_positive() and term.dependency.is_root for term in terms) ): terms = [ @@ -81,7 +81,7 @@ def terms(self) -> list[Term]: return self._terms @property - def cause(self) -> IncompatibilityCause: + def cause(self) -> IncompatibilityCauseError: return self._cause @property @@ -92,8 +92,8 @@ def external_incompatibilities( Returns all external incompatibilities in this incompatibility's derivation graph. """ - if isinstance(self._cause, ConflictCause): - cause: ConflictCause = self._cause + if isinstance(self._cause, ConflictCauseError): + cause: ConflictCauseError = self._cause yield from cause.conflict.external_incompatibilities yield from cause.other.external_incompatibilities @@ -106,7 +106,7 @@ def is_failure(self) -> bool: ) def __str__(self) -> str: - if isinstance(self._cause, DependencyCause): + if isinstance(self._cause, DependencyCauseError): assert len(self._terms) == 2 depender = self._terms[0] @@ -118,7 +118,7 @@ def __str__(self) -> str: f"{self._terse(depender, allow_every=True)} depends on" f" {self._terse(dependee)}" ) - elif isinstance(self._cause, PythonCause): + elif isinstance(self._cause, PythonCauseError): assert len(self._terms) == 1 assert self._terms[0].is_positive() @@ -126,7 +126,7 @@ def __str__(self) -> str: text += f"Python {self._cause.python_version}" return text - elif isinstance(self._cause, PlatformCause): + elif isinstance(self._cause, PlatformCauseError): assert len(self._terms) == 1 assert self._terms[0].is_positive() @@ -134,7 +134,7 @@ def __str__(self) -> str: text += f"platform {self._cause.platform}" return text - elif isinstance(self._cause, NoVersionsCause): + elif isinstance(self._cause, NoVersionsCauseError): assert len(self._terms) == 1 assert self._terms[0].is_positive() @@ -142,7 +142,7 @@ def __str__(self) -> str: f"no versions of {self._terms[0].dependency.name} match" f" {self._terms[0].constraint}" ) - elif isinstance(self._cause, RootCause): + elif isinstance(self._cause, RootCauseError): assert len(self._terms) == 1 assert not self._terms[0].is_positive() assert self._terms[0].dependency.is_root @@ -261,8 +261,8 @@ def _try_requires_both( ) buffer = [self._terse(this_positive, allow_every=True) + " "] - is_dependency = isinstance(self.cause, DependencyCause) and isinstance( - other.cause, DependencyCause + is_dependency = isinstance(self.cause, DependencyCauseError) and isinstance( + other.cause, DependencyCauseError ) if is_dependency: @@ -331,7 +331,7 @@ def _try_requires_through( prior_string = " or ".join([self._terse(term) for term in prior_positives]) buffer.append(f"if {prior_string} then ") else: - if isinstance(prior.cause, DependencyCause): + if isinstance(prior.cause, DependencyCauseError): verb = "depends on" else: verb = "requires" @@ -346,7 +346,7 @@ def _try_requires_through( buffer.append(" which ") - if isinstance(latter.cause, DependencyCause): + if isinstance(latter.cause, DependencyCauseError): buffer.append("depends on ") else: buffer.append("requires ") @@ -397,7 +397,7 @@ def _try_requires_forbidden( buffer.append(f"if {prior_string} then ") else: buffer.append(self._terse(positives[0], allow_every=True)) - if isinstance(prior.cause, DependencyCause): + if isinstance(prior.cause, DependencyCauseError): buffer.append(" depends on ") else: buffer.append(" requires ") @@ -406,10 +406,10 @@ def _try_requires_forbidden( if prior_line is not None: buffer.append(f"({prior_line}) ") - if isinstance(latter.cause, PythonCause): - cause: PythonCause = latter.cause + if isinstance(latter.cause, PythonCauseError): + cause: PythonCauseError = latter.cause buffer.append(f"which requires Python {cause.python_version}") - elif isinstance(latter.cause, NoVersionsCause): + elif isinstance(latter.cause, NoVersionsCauseError): buffer.append("which doesn't match any versions") else: buffer.append("which is forbidden") diff --git a/src/poetry/mixology/incompatibility_cause.py b/src/poetry/mixology/incompatibility_cause.py index 1536d1b22b2..aaabd570eda 100644 --- a/src/poetry/mixology/incompatibility_cause.py +++ b/src/poetry/mixology/incompatibility_cause.py @@ -7,25 +7,25 @@ from poetry.mixology.incompatibility import Incompatibility -class IncompatibilityCause(Exception): +class IncompatibilityCauseError(Exception): """ The reason and Incompatibility's terms are incompatible. """ -class RootCause(IncompatibilityCause): +class RootCauseError(IncompatibilityCauseError): pass -class NoVersionsCause(IncompatibilityCause): +class NoVersionsCauseError(IncompatibilityCauseError): pass -class DependencyCause(IncompatibilityCause): +class DependencyCauseError(IncompatibilityCauseError): pass -class ConflictCause(IncompatibilityCause): +class ConflictCauseError(IncompatibilityCauseError): """ The incompatibility was derived from two existing incompatibilities during conflict resolution. @@ -47,7 +47,7 @@ def __str__(self) -> str: return str(self._conflict) -class PythonCause(IncompatibilityCause): +class PythonCauseError(IncompatibilityCauseError): """ The incompatibility represents a package's python constraint (Python versions) being incompatible @@ -67,7 +67,7 @@ def root_python_version(self) -> str: return self._root_python_version -class PlatformCause(IncompatibilityCause): +class PlatformCauseError(IncompatibilityCauseError): """ The incompatibility represents a package's platform constraint (OS most likely) being incompatible with the current platform. diff --git a/src/poetry/mixology/version_solver.py b/src/poetry/mixology/version_solver.py index 5bb02d9418a..30b87087f60 100644 --- a/src/poetry/mixology/version_solver.py +++ b/src/poetry/mixology/version_solver.py @@ -10,11 +10,11 @@ from poetry.core.packages.dependency import Dependency -from poetry.mixology.failure import SolveFailure +from poetry.mixology.failure import SolveFailureError 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 RootCause +from poetry.mixology.incompatibility_cause import ConflictCauseError +from poetry.mixology.incompatibility_cause import NoVersionsCauseError +from poetry.mixology.incompatibility_cause import RootCauseError from poetry.mixology.partial_solution import PartialSolution from poetry.mixology.result import SolverResult from poetry.mixology.set_relation import SetRelation @@ -165,7 +165,7 @@ def solve(self) -> SolverResult: root_dependency.is_root = True self._add_incompatibility( - Incompatibility([Term(root_dependency, False)], RootCause()) + Incompatibility([Term(root_dependency, False)], RootCauseError()) ) try: @@ -412,7 +412,8 @@ def _resolve_conflict(self, incompatibility: Incompatibility) -> Incompatibility new_terms.append(inverse) incompatibility = Incompatibility( - new_terms, ConflictCause(incompatibility, most_recent_satisfier.cause) + new_terms, + ConflictCauseError(incompatibility, most_recent_satisfier.cause), ) new_incompatibility = True @@ -424,7 +425,7 @@ def _resolve_conflict(self, incompatibility: Incompatibility) -> Incompatibility self._log(f'! which is caused by "{most_recent_satisfier.cause}"') self._log(f"! thus: {incompatibility}") - raise SolveFailure(incompatibility) + raise SolveFailureError(incompatibility) def _choose_package_version(self) -> str | None: """ @@ -503,7 +504,7 @@ def _get_min(dependency: Dependency) -> tuple[bool, int, int]: # If there are no versions that satisfy the constraint, # add an incompatibility that indicates that. self._add_incompatibility( - Incompatibility([Term(dependency, True)], NoVersionsCause()) + Incompatibility([Term(dependency, True)], NoVersionsCauseError()) ) complete_name = dependency.complete_name diff --git a/src/poetry/packages/locker.py b/src/poetry/packages/locker.py index 80177f16dd3..245a89e97cd 100644 --- a/src/poetry/packages/locker.py +++ b/src/poetry/packages/locker.py @@ -18,7 +18,7 @@ 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.version.requirements import InvalidRequirement +from poetry.core.version.requirements import InvalidRequirementError from tomlkit import array from tomlkit import comment from tomlkit import document @@ -59,6 +59,11 @@ class Locker: "dev-dependencies", ] _relevant_keys: ClassVar[list[str]] = [*_legacy_keys, "group"] + _relevant_project_keys: ClassVar[list[str]] = [ + "requires-python", + "dependencies", + "optional-dependencies", + ] def __init__(self, lock: Path, pyproject_data: dict[str, Any]) -> None: self._lock = lock @@ -189,7 +194,7 @@ def locked_repository(self) -> LockfileRepository: for dep in deps: try: dependency = Dependency.create_from_pep_508(dep) - except InvalidRequirement: + except InvalidRequirementError: # handle lock files with invalid PEP 508 m = re.match(r"^(.+?)(?:\[(.+?)])?(?:\s+\((.+)\))?$", dep) if not m: @@ -324,16 +329,37 @@ def _get_content_hash(self) -> str: """ Returns the sha256 hash of the sorted content of the pyproject file. """ - content = self._pyproject_data.get("tool", {}).get("poetry", {}) + project_content = self._pyproject_data.get("project", {}) + tool_poetry_content = self._pyproject_data.get("tool", {}).get("poetry", {}) - relevant_content = {} + relevant_project_content = {} + for key in self._relevant_project_keys: + data = project_content.get(key) + if data is not None: + relevant_project_content[key] = data + + relevant_poetry_content = {} for key in self._relevant_keys: - data = content.get(key) + data = tool_poetry_content.get(key) - if data is None and key not in self._legacy_keys: + if data is None and ( + # Special handling for legacy keys is just for backwards compatibility, + # and thereby not required if there is relevant content in [project]. + key not in self._legacy_keys or relevant_project_content + ): continue - relevant_content[key] = data + relevant_poetry_content[key] = data + + if relevant_project_content: + relevant_content = { + "project": relevant_project_content, + "tool": {"poetry": relevant_poetry_content}, + } + else: + # For backwards compatibility, we have to put the relevant content + # of the [tool.poetry] section at top level! + relevant_content = relevant_poetry_content return sha256(json.dumps(relevant_content, sort_keys=True).encode()).hexdigest() diff --git a/src/poetry/plugins/base_plugin.py b/src/poetry/plugins/base_plugin.py index 07146060746..4df1c984621 100644 --- a/src/poetry/plugins/base_plugin.py +++ b/src/poetry/plugins/base_plugin.py @@ -1,9 +1,10 @@ from __future__ import annotations +from abc import ABC from abc import abstractmethod -class BasePlugin: +class BasePlugin(ABC): """ Base class for all plugin types @@ -18,4 +19,3 @@ def group(self) -> str: """ Name of entrypoint group the plugin belongs to. """ - raise NotImplementedError() diff --git a/src/poetry/plugins/plugin.py b/src/poetry/plugins/plugin.py index ea72662c3c2..0e00bbba3c5 100644 --- a/src/poetry/plugins/plugin.py +++ b/src/poetry/plugins/plugin.py @@ -20,5 +20,4 @@ class Plugin(BasePlugin): group = "poetry.plugin" @abstractmethod - def activate(self, poetry: Poetry, io: IO) -> None: - raise NotImplementedError() + def activate(self, poetry: Poetry, io: IO) -> None: ... diff --git a/src/poetry/plugins/plugin_manager.py b/src/poetry/plugins/plugin_manager.py index 4ab286c36f7..95ac8b0341d 100644 --- a/src/poetry/plugins/plugin_manager.py +++ b/src/poetry/plugins/plugin_manager.py @@ -1,18 +1,42 @@ from __future__ import annotations +import hashlib +import json import logging +import shutil +import sys +from functools import cached_property +from pathlib import Path from typing import TYPE_CHECKING +from typing import Sequence +import tomlkit + +from poetry.core.packages.project_package import ProjectPackage + +from poetry.__version__ import __version__ +from poetry.installation import Installer +from poetry.packages import Locker from poetry.plugins.application_plugin import ApplicationPlugin from poetry.plugins.plugin import Plugin +from poetry.repositories import Repository +from poetry.repositories.installed_repository import InstalledRepository +from poetry.toml import TOMLFile from poetry.utils._compat import metadata +from poetry.utils._compat import tomllib +from poetry.utils.env import Env +from poetry.utils.env import EnvManager if TYPE_CHECKING: from typing import Any - from poetry.utils.env import Env + from cleo.io.io import IO + from poetry.core.packages.dependency import Dependency + from poetry.core.packages.package import Package + + from poetry.poetry import Poetry logger = logging.getLogger(__name__) @@ -23,42 +47,42 @@ class PluginManager: This class registers and activates plugins. """ - def __init__(self, group: str, disable_plugins: bool = False) -> None: + def __init__(self, group: str) -> None: self._group = group - self._disable_plugins = disable_plugins self._plugins: list[Plugin] = [] - def load_plugins(self, env: Env | None = None) -> None: - if self._disable_plugins: + @staticmethod + def add_project_plugin_path(directory: Path) -> None: + from poetry.factory import Factory + + try: + pyproject_toml = Factory.locate(directory) + except RuntimeError: + # no pyproject.toml -> no project plugins return - plugin_entrypoints = self.get_plugin_entry_points(env=env) + plugin_path = pyproject_toml.parent / ProjectPluginCache.PATH + if plugin_path.exists(): + EnvManager.get_system_env(naive=True).sys_path.insert(0, str(plugin_path)) + + @classmethod + def ensure_project_plugins(cls, poetry: Poetry, io: IO) -> None: + ProjectPluginCache(poetry, io).ensure_plugins() + + def load_plugins(self) -> None: + plugin_entrypoints = self.get_plugin_entry_points() for ep in plugin_entrypoints: self._load_plugin_entry_point(ep) - @staticmethod - def _is_plugin_candidate(ep: metadata.EntryPoint, env: Env | None = None) -> bool: - """ - Helper method to check if given entry point is a valid as a plugin candidate. - When an environment is specified, the entry point's associated distribution - should be installed, and discoverable in the given environment. - """ - return env is None or ( - ep.dist is not None - and env.site_packages.find_distribution(ep.dist.name) is not None - ) + def get_plugin_entry_points(self) -> list[metadata.EntryPoint]: + return list(metadata.entry_points(group=self._group)) - def get_plugin_entry_points( - self, env: Env | None = None - ) -> list[metadata.EntryPoint]: - return [ - ep - for ep in metadata.entry_points(group=self._group) - if self._is_plugin_candidate(ep, env) - ] + def activate(self, *args: Any, **kwargs: Any) -> None: + for plugin in self._plugins: + plugin.activate(*args, **kwargs) - def add_plugin(self, plugin: Plugin) -> None: + def _add_plugin(self, plugin: Plugin) -> None: if not isinstance(plugin, (Plugin, ApplicationPlugin)): raise ValueError( "The Poetry plugin must be an instance of Plugin or ApplicationPlugin" @@ -66,10 +90,6 @@ def add_plugin(self, plugin: Plugin) -> None: self._plugins.append(plugin) - def activate(self, *args: Any, **kwargs: Any) -> None: - for plugin in self._plugins: - plugin.activate(*args, **kwargs) - def _load_plugin_entry_point(self, ep: metadata.EntryPoint) -> None: logger.debug("Loading the %s plugin", ep.name) @@ -80,4 +100,215 @@ def _load_plugin_entry_point(self, ep: metadata.EntryPoint) -> None: "The Poetry plugin must be an instance of Plugin or ApplicationPlugin" ) - self.add_plugin(plugin()) + self._add_plugin(plugin()) + + +class ProjectPluginCache: + PATH = Path(".poetry") / "plugins" + + def __init__(self, poetry: Poetry, io: IO) -> None: + self._poetry = poetry + self._io = io + self._path = poetry.pyproject_path.parent / self.PATH + self._config_file = self._path / "config.toml" + self._gitignore_file = self._path.parent / ".gitignore" + + @property + def _plugin_section(self) -> dict[str, Any]: + plugins = self._poetry.local_config.get("requires-plugins", {}) + assert isinstance(plugins, dict) + return plugins + + @cached_property + def _config(self) -> dict[str, Any]: + return { + "python": sys.version, + "poetry": __version__, + "plugins-hash": hashlib.sha256( + json.dumps(self._plugin_section, sort_keys=True).encode() + ).hexdigest(), + } + + def ensure_plugins(self) -> None: + from poetry.factory import Factory + + # parse project plugins + plugins = [] + for name, constraints in self._plugin_section.items(): + _constraints = ( + constraints if isinstance(constraints, list) else [constraints] + ) + for _constraint in _constraints: + plugins.append(Factory.create_dependency(name, _constraint)) + + if not plugins: + if self._path.exists(): + self._io.write_line( + "No project plugins defined." + " Removing the project's plugin cache" + ) + self._io.write_line("") + shutil.rmtree(self._path) + return + + if self._is_fresh(): + if self._io.is_debug(): + self._io.write_line("The project's plugin cache is up to date.") + self._io.write_line("") + return + elif self._path.exists(): + self._io.write_line( + "Removing the project's plugin cache because it is outdated" + ) + # Just remove the cache for two reasons: + # 1. Since the path of the cache has already been added to sys.path + # at this point, we had to distinguish between packages installed + # directly into Poetry's env and packages installed in the project cache. + # 2. Updating packages in the cache does not work out of the box, + # probably, because we use pip to uninstall and pip does not know + # about the cache so that we end up with just overwriting installed + # packages and multiple dist-info folders per package. + # In sum, we keep it simple by always starting from an empty cache + # if something has changed. + shutil.rmtree(self._path) + + # determine plugins relevant for Poetry's environment + poetry_env = EnvManager.get_system_env(naive=True) + relevant_plugins = { + plugin.name: plugin + for plugin in plugins + if plugin.marker.validate(poetry_env.marker_env) + } + if not relevant_plugins: + if self._io.is_debug(): + self._io.write_line( + "No relevant project plugins for Poetry's environment defined." + ) + self._io.write_line("") + self._write_config() + return + + self._io.write_line( + "Ensuring that the Poetry plugins required" + " by the project are available..." + ) + + # check if required plugins are already available + missing_plugin_count = len(relevant_plugins) + satisfied_plugins = set() + insufficient_plugins = [] + installed_packages = [] + installed_repo = InstalledRepository.load(poetry_env) + for package in installed_repo.packages: + if required_plugin := relevant_plugins.get(package.name): + if package.satisfies(required_plugin): + satisfied_plugins.add(package.name) + installed_packages.append(package) + else: + insufficient_plugins.append((package, required_plugin)) + # Do not add the package to installed_packages so that + # the solver does not consider it. + missing_plugin_count -= 1 + if missing_plugin_count == 0: + break + else: + installed_packages.append(package) + + if missing_plugin_count == 0 and not insufficient_plugins: + # all required plugins are installed and satisfy the requirements + self._write_config() + self._io.write_line( + "All required plugins have already been installed" + " in Poetry's environment." + ) + self._io.write_line("") + return + + if insufficient_plugins and self._io.is_debug(): + plugins_str = "\n".join( + f" - {req}\n installed: {p}" for p, req in insufficient_plugins + ) + self._io.write_line( + "The following Poetry plugins are required by the project" + f" but are not satisfied by the installed versions:\n{plugins_str}" + ) + + # install missing plugins + missing_plugins = [ + plugin + for name, plugin in relevant_plugins.items() + if name not in satisfied_plugins + ] + plugins_str = "\n".join(f" - {p}" for p in missing_plugins) + self._io.write_line( + "The following Poetry plugins are required by the project" + f" but are not installed in Poetry's environment:\n{plugins_str}\n" + f"Installing Poetry plugins only for the current project..." + ) + self._install(missing_plugins, poetry_env, installed_packages) + self._io.write_line("") + self._write_config() + + def _is_fresh(self) -> bool: + if not self._config_file.exists(): + return False + + with self._config_file.open("rb") as f: + stored_config = tomllib.load(f) + + return stored_config == self._config + + def _install( + self, + plugins: Sequence[Dependency], + poetry_env: Env, + locked_packages: Sequence[Package], + ) -> None: + project = ProjectPackage(name="poetry-project-instance", version="0") + project.python_versions = ".".join(str(v) for v in poetry_env.version_info[:3]) + # consider all packages in Poetry's environment pinned + for package in locked_packages: + project.add_dependency(package.to_dependency()) + # add missing plugin dependencies + for dependency in plugins: + project.add_dependency(dependency) + + # force new package to be installed in the project cache instead of Poetry's env + poetry_env.paths["platlib"] = str(self._path) + poetry_env.paths["purelib"] = str(self._path) + + self._ensure_cache_directory() + + installer = Installer( + self._io, + poetry_env, + project, + Locker(self._path / "poetry.lock", {}), + self._poetry.pool, + self._poetry.config, + # Build installed repository from locked packages so that plugins + # that may be overwritten are not included. + Repository("poetry-repo", locked_packages), + ) + installer.update(True) + + if installer.run() != 0: + raise RuntimeError("Failed to install required Poetry plugins") + + def _write_config(self) -> None: + self._ensure_cache_directory() + + document = tomlkit.document() + + for key, value in self._config.items(): + document[key] = value + + TOMLFile(self._config_file).write(data=document) + + def _ensure_cache_directory(self) -> None: + if self._path.exists(): + return + + self._path.mkdir(parents=True, exist_ok=True) + # only write .gitignore if path did not exist before + self._gitignore_file.write_text("*", encoding="utf-8") diff --git a/src/poetry/publishing/uploader.py b/src/poetry/publishing/uploader.py index 068e5d4522b..1c8e5b07ff2 100644 --- a/src/poetry/publishing/uploader.py +++ b/src/poetry/publishing/uploader.py @@ -8,8 +8,6 @@ from poetry.core.masonry.metadata import Metadata from poetry.core.masonry.utils.helpers import distribution_name -from requests.exceptions import ConnectionError -from requests.exceptions import HTTPError from requests_toolbelt import user_agent from requests_toolbelt.multipart import MultipartEncoder from requests_toolbelt.multipart import MultipartEncoderMonitor @@ -28,23 +26,7 @@ class UploadError(Exception): - def __init__(self, error: ConnectionError | HTTPError | str) -> None: - if isinstance(error, HTTPError): - if error.response is None: - message = "HTTP Error connecting to the repository" - else: - message = ( - f"HTTP Error {error.response.status_code}: " - f"{error.response.reason} | {error.response.content!r}" - ) - elif isinstance(error, ConnectionError): - message = ( - "Connection Error: We were unable to connect to the repository, " - "ensure the url is correct and can be reached." - ) - else: - message = error - super().__init__(message) + pass class Uploader: @@ -272,12 +254,22 @@ def _upload_file( bar.display() else: resp.raise_for_status() - except (requests.ConnectionError, requests.HTTPError) as e: + + except requests.RequestException as e: if self._io.output.is_decorated(): self._io.overwrite( f" - Uploading {file.name} FAILED" ) - raise UploadError(e) + + if e.response is not None: + message = ( + f"HTTP Error {e.response.status_code}: " + f"{e.response.reason} | {e.response.content!r}" + ) + raise UploadError(message) from e + + raise UploadError("Error connecting to repository") from e + finally: self._io.write_line("") diff --git a/src/poetry/puzzle/exceptions.py b/src/poetry/puzzle/exceptions.py index 6bb7c0027bc..c33ab4f2f93 100644 --- a/src/poetry/puzzle/exceptions.py +++ b/src/poetry/puzzle/exceptions.py @@ -7,21 +7,21 @@ from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package - from poetry.mixology.failure import SolveFailure + from poetry.mixology.failure import SolveFailureError class SolverProblemError(Exception): - def __init__(self, error: SolveFailure) -> None: + def __init__(self, error: SolveFailureError) -> None: self._error = error super().__init__(str(error)) @property - def error(self) -> SolveFailure: + def error(self) -> SolveFailureError: return self._error -class OverrideNeeded(Exception): +class OverrideNeededError(Exception): def __init__(self, *overrides: dict[Package, dict[str, Dependency]]) -> None: self._overrides = overrides diff --git a/src/poetry/puzzle/provider.py b/src/poetry/puzzle/provider.py index 754795b845c..590afc8c7b1 100644 --- a/src/poetry/puzzle/provider.py +++ b/src/poetry/puzzle/provider.py @@ -20,14 +20,14 @@ from poetry.core.version.markers import union as marker_union from poetry.mixology.incompatibility import Incompatibility -from poetry.mixology.incompatibility_cause import DependencyCause -from poetry.mixology.incompatibility_cause import PythonCause +from poetry.mixology.incompatibility_cause import DependencyCauseError +from poetry.mixology.incompatibility_cause import PythonCauseError from poetry.mixology.term import Term from poetry.packages import DependencyPackage from poetry.packages.direct_origin import DirectOrigin from poetry.packages.package_collection import PackageCollection -from poetry.puzzle.exceptions import OverrideNeeded -from poetry.repositories.exceptions import PackageNotFound +from poetry.puzzle.exceptions import OverrideNeededError +from poetry.repositories.exceptions import PackageNotFoundError from poetry.utils.helpers import get_file_hash @@ -446,7 +446,7 @@ def incompatibilities_for( return [ Incompatibility( [Term(package.to_dependency(), True)], - PythonCause( + PythonCauseError( package.python_versions, str(self._python_constraint) ), ) @@ -464,7 +464,7 @@ def incompatibilities_for( return [ Incompatibility( [Term(package.to_dependency(), True), Term(dep, False)], - DependencyCause(), + DependencyCauseError(), ) for dep in dependencies ] @@ -493,7 +493,7 @@ def complete_package( repository_name=dependency.source_name, ), ) - except PackageNotFound as e: + except PackageNotFoundError as e: try: dependency_package = next( DependencyPackage(dependency, pkg) @@ -650,7 +650,7 @@ def fmt_warning(d: Dependency) -> str: overrides.append(current_overrides) if overrides: - raise OverrideNeeded(*overrides) + raise OverrideNeededError(*overrides) # Modifying dependencies as needed clean_dependencies = [] diff --git a/src/poetry/puzzle/solver.py b/src/poetry/puzzle/solver.py index 9675bc1f6f0..0b6742217d8 100644 --- a/src/poetry/puzzle/solver.py +++ b/src/poetry/puzzle/solver.py @@ -7,11 +7,10 @@ from typing import TYPE_CHECKING from typing import FrozenSet from typing import Tuple -from typing import TypeVar from poetry.mixology import resolve_version -from poetry.mixology.failure import SolveFailure -from poetry.puzzle.exceptions import OverrideNeeded +from poetry.mixology.failure import SolveFailureError +from poetry.puzzle.exceptions import OverrideNeededError from poetry.puzzle.exceptions import SolverProblemError from poetry.puzzle.provider import Indicator from poetry.puzzle.provider import Provider @@ -27,6 +26,7 @@ from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package from poetry.core.packages.project_package import ProjectPackage + from typing_extensions import Self from poetry.puzzle.transaction import Transaction from poetry.repositories import RepositoryPool @@ -155,9 +155,9 @@ def _solve(self) -> tuple[list[Package], list[int]]: result = resolve_version(self._package, self._provider) packages = result.packages - except OverrideNeeded as e: + except OverrideNeededError as e: return self._solve_in_compatibility_mode(e.overrides) - except SolveFailure as e: + except SolveFailureError as e: raise SolverProblemError(e) combined_nodes = depth_first_search(PackageNode(self._package, packages)) @@ -179,16 +179,14 @@ def _solve(self) -> tuple[list[Package], list[int]]: if _package.name == dep.name: continue - 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 + # Avoid duplication. + if any( + _dep == dep and _dep.marker == dep.marker + for _dep in _package.requires + ): + continue + + _package.add_dependency(dep) else: final_packages.append(package) depths.append(results[package]) @@ -199,8 +197,6 @@ def _solve(self) -> tuple[list[Package], list[int]]: DFSNodeID = Tuple[str, FrozenSet[str], bool] -T = TypeVar("T", bound="DFSNode") - class DFSNode: def __init__(self, id: DFSNodeID, name: str, base_name: str) -> None: @@ -208,7 +204,7 @@ def __init__(self, id: DFSNodeID, name: str, base_name: str) -> None: self.name = name self.base_name = base_name - def reachable(self: T) -> Sequence[T]: + def reachable(self) -> Sequence[Self]: return [] def visit(self, parents: list[PackageNode]) -> None: diff --git a/src/poetry/repositories/exceptions.py b/src/poetry/repositories/exceptions.py index c742f268a42..1e5be368749 100644 --- a/src/poetry/repositories/exceptions.py +++ b/src/poetry/repositories/exceptions.py @@ -5,7 +5,7 @@ class RepositoryError(Exception): pass -class PackageNotFound(Exception): +class PackageNotFoundError(Exception): pass diff --git a/src/poetry/repositories/http_repository.py b/src/poetry/repositories/http_repository.py index 7d0249b897d..581bd7eebb7 100644 --- a/src/poetry/repositories/http_repository.py +++ b/src/poetry/repositories/http_repository.py @@ -24,12 +24,12 @@ from poetry.inspection.lazy_wheel import LazyWheelUnsupportedError from poetry.inspection.lazy_wheel import metadata_from_wheel_url from poetry.repositories.cached_repository import CachedRepository -from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.exceptions import RepositoryError from poetry.repositories.link_sources.html import HTMLPage from poetry.utils.authenticator import Authenticator from poetry.utils.constants import REQUESTS_TIMEOUT -from poetry.utils.helpers import HTTPRangeRequestSupported +from poetry.utils.helpers import HTTPRangeRequestSupportedError from poetry.utils.helpers import download_file from poetry.utils.helpers import get_highest_priority_hash_type from poetry.utils.patterns import wheel_file_re @@ -146,7 +146,7 @@ def _get_info_from_wheel(self, link: Link) -> PackageInfo: link, raise_accepts_ranges=raise_accepts_ranges ) as filepath: return PackageInfo.from_wheel(filepath) - except HTTPRangeRequestSupported: + except HTTPRangeRequestSupportedError: # The domain did not support range requests for the first URL(s) we tried, # but supports it for some URLs (especially the current URL), # so we abort the download, update _supports_range_requests to try @@ -336,7 +336,7 @@ def _get_info_from_links( def _links_to_data(self, links: list[Link], data: PackageInfo) -> dict[str, Any]: if not links: - raise PackageNotFound( + raise PackageNotFoundError( f'No valid distribution links found for package: "{data.name}" version:' f' "{data.version}"' ) @@ -429,5 +429,5 @@ def _get_response(self, endpoint: str) -> requests.Response | None: def _get_page(self, name: NormalizedName) -> LinkSource: response = self._get_response(f"/{name}/") if not response: - raise PackageNotFound(f"Package [{name}] not found.") + raise PackageNotFoundError(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 07a92156a19..891e8f6b79e 100644 --- a/src/poetry/repositories/installed_repository.py +++ b/src/poetry/repositories/installed_repository.py @@ -9,6 +9,7 @@ from packaging.utils import canonicalize_name from poetry.core.packages.package import Package +from poetry.core.packages.utils.utils import is_python_project from poetry.core.packages.utils.utils import url_to_path from poetry.core.utils.helpers import module_name @@ -141,7 +142,10 @@ def create_package_from_distribution( # TODO: handle multiple source directories? if is_editable_package: source_type = "directory" - source_url = paths.pop().as_posix() + path = paths.pop() + if path.name == "src": + path = path.parent + source_url = path.as_posix() elif cls.is_vcs_package(path, env): ( source_type, @@ -150,8 +154,7 @@ def create_package_from_distribution( ) = cls.get_package_vcs_properties_from_path( env.path / "src" / canonicalize_name(distribution.metadata["name"]) ) - else: - # If not, it's a path dependency + elif is_python_project(path.parent): source_type = "directory" source_url = str(path.parent) @@ -239,7 +242,7 @@ def load(cls, env: Env, with_dependencies: bool = False) -> InstalledRepository: seen = set() skipped = set() - for entry in reversed(env.sys_path): + for entry in env.sys_path: if not entry.strip(): logger.debug( "Project environment contains an empty path in sys_path," diff --git a/src/poetry/repositories/legacy_repository.py b/src/poetry/repositories/legacy_repository.py index 8b1bcea8236..5cdcd7cc35f 100644 --- a/src/poetry/repositories/legacy_repository.py +++ b/src/poetry/repositories/legacy_repository.py @@ -10,7 +10,7 @@ from poetry.core.packages.package import Package from poetry.inspection.info import PackageInfo -from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.http_repository import HTTPRepository from poetry.repositories.link_sources.html import HTMLPage from poetry.repositories.link_sources.html import SimpleRepositoryRootPage @@ -68,7 +68,7 @@ def package( def find_links_for_package(self, package: Package) -> list[Link]: try: page = self.get_page(package.name) - except PackageNotFound: + except PackageNotFoundError: return [] return list(page.links_for_version(package.name, package.version)) @@ -81,7 +81,7 @@ def _find_packages( """ try: page = self.get_page(name) - except PackageNotFound: + except PackageNotFoundError: self._log(f"No packages found for {name}", level="debug") return [] @@ -127,7 +127,7 @@ def _get_release_info( def _get_page(self, name: NormalizedName) -> HTMLPage: if not (response := self._get_response(f"/{name}/")): - raise PackageNotFound(f"Package [{name}] not found.") + raise PackageNotFoundError(f"Package [{name}] not found.") return HTMLPage(response.url, response.text) @cached_property @@ -145,7 +145,7 @@ def search(self, query: str) -> list[Package]: results: list[Package] = [] for candidate in self.root_page.search(query): - with suppress(PackageNotFound): + with suppress(PackageNotFoundError): page = self.get_page(candidate) for package in page.packages: diff --git a/src/poetry/repositories/link_sources/base.py b/src/poetry/repositories/link_sources/base.py index 9948ec8cd35..53bf073c523 100644 --- a/src/poetry/repositories/link_sources/base.py +++ b/src/poetry/repositories/link_sources/base.py @@ -11,7 +11,7 @@ from poetry.core.constraints.version import Version from poetry.core.packages.package import Package -from poetry.core.version.exceptions import InvalidVersion +from poetry.core.version.exceptions import InvalidVersionError from poetry.utils.patterns import sdist_file_re from poetry.utils.patterns import wheel_file_re @@ -86,7 +86,7 @@ def link_package_data(cls, link: Link) -> Package | None: if version_string: try: version = Version.parse(version_string) - except InvalidVersion: + except InvalidVersionError: 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 43d6c512324..5e47047b1c3 100644 --- a/src/poetry/repositories/pypi_repository.py +++ b/src/poetry/repositories/pypi_repository.py @@ -11,9 +11,9 @@ from cachecontrol.controller import logger as cache_control_logger from poetry.core.packages.package import Package from poetry.core.packages.utils.link import Link -from poetry.core.version.exceptions import InvalidVersion +from poetry.core.version.exceptions import InvalidVersionError -from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.http_repository import HTTPRepository from poetry.repositories.link_sources.json import SimpleJsonPage from poetry.repositories.parsers.pypi_search_parser import SearchResultParser @@ -64,7 +64,7 @@ def search(self, query: str) -> list[Package]: package = Package(result.name, result.version) package.description = result.description.strip() results.append(package) - except InvalidVersion: + except InvalidVersionError: self._log( f'Unable to parse version "{result.version}" for the' f" {result.name} package, skipping", @@ -90,7 +90,7 @@ def _find_packages( """ try: json_page = self.get_page(name) - except PackageNotFound: + except PackageNotFoundError: self._log(f"No packages found for {name}", level="debug") return [] @@ -106,7 +106,7 @@ def _get_package_info(self, name: NormalizedName) -> dict[str, Any]: headers = {"Accept": "application/vnd.pypi.simple.v1+json"} info = self._get(f"simple/{name}/", headers=headers) if info is None: - raise PackageNotFound(f"Package [{name}] not found.") + raise PackageNotFoundError(f"Package [{name}] not found.") return info @@ -132,7 +132,7 @@ def _get_release_info( json_data = self._get(f"pypi/{name}/{version}/json") if json_data is None: - raise PackageNotFound(f"Package [{name}] not found.") + raise PackageNotFoundError(f"Package [{name}] not found.") info = json_data["info"] diff --git a/src/poetry/repositories/repository.py b/src/poetry/repositories/repository.py index 1f6aef2ff7c..f1586090b14 100644 --- a/src/poetry/repositories/repository.py +++ b/src/poetry/repositories/repository.py @@ -8,10 +8,12 @@ from poetry.core.constraints.version import Version from poetry.repositories.abstract_repository import AbstractRepository -from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.exceptions import PackageNotFoundError if TYPE_CHECKING: + from collections.abc import Sequence + from packaging.utils import NormalizedName from poetry.core.constraints.version import VersionConstraint from poetry.core.packages.dependency import Dependency @@ -20,7 +22,7 @@ class Repository(AbstractRepository): - def __init__(self, name: str, packages: list[Package] | None = None) -> None: + def __init__(self, name: str, packages: Sequence[Package] | None = None) -> None: super().__init__(name) self._packages: list[Package] = [] @@ -105,4 +107,4 @@ def package( if canonicalized_name == package.name and package.version == version: return package - raise PackageNotFound(f"Package {name} ({version}) not found.") + raise PackageNotFoundError(f"Package {name} ({version}) not found.") diff --git a/src/poetry/repositories/repository_pool.py b/src/poetry/repositories/repository_pool.py index 9d6338f4ff2..6b55c20c0b7 100644 --- a/src/poetry/repositories/repository_pool.py +++ b/src/poetry/repositories/repository_pool.py @@ -1,7 +1,6 @@ from __future__ import annotations import enum -import warnings from collections import OrderedDict from dataclasses import dataclass @@ -10,7 +9,7 @@ from poetry.config.config import Config from poetry.repositories.abstract_repository import AbstractRepository -from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.repository import Repository from poetry.utils.cache import ArtifactCache @@ -20,15 +19,11 @@ from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package -_SENTINEL = object() - class Priority(IntEnum): # The order of the members below dictates the actual priority. The first member has # top priority. - DEFAULT = enum.auto() PRIMARY = enum.auto() - SECONDARY = enum.auto() SUPPLEMENTAL = enum.auto() EXPLICIT = enum.auto() @@ -43,7 +38,6 @@ class RepositoryPool(AbstractRepository): def __init__( self, repositories: list[Repository] | None = None, - ignore_repository_names: object = _SENTINEL, *, config: Config | None = None, ) -> None: @@ -59,15 +53,6 @@ def __init__( cache_dir=(config or Config.create()).artifacts_cache_directory ) - if ignore_repository_names is not _SENTINEL: - warnings.warn( - "The 'ignore_repository_names' argument to 'RepositoryPool.__init__' is" - " deprecated. It has no effect anymore and will be removed in a future" - " version.", - DeprecationWarning, - stacklevel=2, - ) - @staticmethod def from_packages(packages: list[Package], config: Config | None) -> RepositoryPool: pool = RepositoryPool(config=config) @@ -118,9 +103,6 @@ def _sorted_repositories(self) -> list[PrioritizedRepository]: def artifact_cache(self) -> ArtifactCache: return self._artifact_cache - def has_default(self) -> bool: - return self._contains_priority(Priority.DEFAULT) - def has_primary_repositories(self) -> bool: return self._contains_priority(Priority.PRIMARY) @@ -145,12 +127,7 @@ def _get_prioritized_repository(self, name: str) -> PrioritizedRepository: raise IndexError(f'Repository "{name}" does not exist.') def add_repository( - self, - repository: Repository, - default: bool = False, - secondary: bool = False, - *, - priority: Priority = Priority.PRIMARY, + self, repository: Repository, *, priority: Priority = Priority.PRIMARY ) -> RepositoryPool: """ Adds a repository to the pool. @@ -161,19 +138,6 @@ def add_repository( f"A repository with name {repository_name} was already added." ) - 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, - ) - priority = Priority.DEFAULT if default else Priority.SECONDARY - - if priority is Priority.DEFAULT and self.has_default(): - raise ValueError("Only one repository can be the default.") - self._repositories[repository_name] = PrioritizedRepository( repository, priority ) @@ -202,9 +166,9 @@ def package( for repo in self.repositories: try: return repo.package(name, version, extras=extras) - except PackageNotFound: + except PackageNotFoundError: continue - raise PackageNotFound(f"Package {name} ({version}) not found.") + raise PackageNotFoundError(f"Package {name} ({version}) not found.") def find_packages(self, dependency: Dependency) -> list[Package]: repository_name = dependency.source_name diff --git a/src/poetry/repositories/single_page_repository.py b/src/poetry/repositories/single_page_repository.py index 446957f12db..dc318fd91f6 100644 --- a/src/poetry/repositories/single_page_repository.py +++ b/src/poetry/repositories/single_page_repository.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING -from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.legacy_repository import LegacyRepository from poetry.repositories.link_sources.html import HTMLPage @@ -18,5 +18,5 @@ def _get_page(self, name: NormalizedName) -> HTMLPage: """ response = self._get_response("") if not response: - raise PackageNotFound(f"Package [{name}] not found.") + raise PackageNotFoundError(f"Package [{name}] not found.") return HTMLPage(response.url, response.text) diff --git a/src/poetry/toml/exceptions.py b/src/poetry/toml/exceptions.py index 66fcec0063b..176dbb95263 100644 --- a/src/poetry/toml/exceptions.py +++ b/src/poetry/toml/exceptions.py @@ -1,8 +1,8 @@ from __future__ import annotations -from poetry.core.exceptions import PoetryCoreException +from poetry.core.exceptions import PoetryCoreError from tomlkit.exceptions import TOMLKitError -class TOMLError(TOMLKitError, PoetryCoreException): +class TOMLError(TOMLKitError, PoetryCoreError): pass diff --git a/src/poetry/toml/file.py b/src/poetry/toml/file.py index 6733de7f037..8eef0198d73 100644 --- a/src/poetry/toml/file.py +++ b/src/poetry/toml/file.py @@ -1,9 +1,6 @@ from __future__ import annotations -import warnings - from typing import TYPE_CHECKING -from typing import Any from tomlkit.toml_file import TOMLFile as BaseTOMLFile @@ -36,15 +33,5 @@ def read(self) -> TOMLDocument: except (ValueError, TOMLKitError) as e: raise TOMLError(f"Invalid TOML file {self.path.as_posix()}: {e}") - def __getattr__(self, item: str) -> Any: - warnings.warn( - "`__getattr__` will be removed from the `TOMLFile` in a future release." - "\n\nInstead of accessing properties of the underlying `Path` as " - "`tomlfile.whatever`, prefer `tomlfile.path.whatever`.", - DeprecationWarning, - stacklevel=2, - ) - return getattr(self.__path, item) - def __str__(self) -> str: return self.__path.as_posix() diff --git a/src/poetry/utils/authenticator.py b/src/poetry/utils/authenticator.py index 73ccba6c814..4d9388ab37c 100644 --- a/src/poetry/utils/authenticator.py +++ b/src/poetry/utils/authenticator.py @@ -21,7 +21,7 @@ from poetry.__version__ import __version__ from poetry.config.config import Config -from poetry.exceptions import PoetryException +from poetry.exceptions import PoetryError from poetry.utils.constants import REQUESTS_TIMEOUT from poetry.utils.constants import RETRY_AFTER_HEADER from poetry.utils.constants import STATUS_FORCELIST @@ -78,7 +78,7 @@ def certs(self, config: Config) -> RepositoryCertificateConfig: return RepositoryCertificateConfig.create(self.name, config) def get_http_credentials( - self, password_manager: PasswordManager, username: str | None = None + self, password_manager: PasswordManager ) -> HTTPAuthCredential: # try with the repository name via the password manager credential = HTTPAuthCredential( @@ -247,7 +247,7 @@ def request( continue # this should never really be hit under any sane circumstance - raise PoetryException("Failed HTTP {} request", method.upper()) + raise PoetryError("Failed HTTP {} request", method.upper()) def _get_backoff(self, response: requests.Response | None, attempt: int) -> float: if response is not None: @@ -268,15 +268,15 @@ def post(self, url: str, **kwargs: Any) -> requests.Response: return self.request("post", url, **kwargs) def _get_credentials_for_repository( - self, repository: AuthenticatorRepositoryConfig, username: str | None = None + self, repository: AuthenticatorRepositoryConfig ) -> HTTPAuthCredential: # cache repository credentials by repository url to avoid multiple keyring # backend queries when packages are being downloaded from the same source - key = f"{repository.url}#username={username or ''}" + key = repository.url if key not in self._credentials: self._credentials[key] = repository.get_http_credentials( - password_manager=self._password_manager, username=username + password_manager=self._password_manager ) return self._credentials[key] @@ -346,9 +346,7 @@ def get_credentials_for_url(self, url: str) -> HTTPAuthCredential: def get_pypi_token(self, name: str) -> str | None: return self._password_manager.get_pypi_token(name) - def get_http_auth( - self, name: str, username: str | None = None - ) -> HTTPAuthCredential | None: + def get_http_auth(self, name: str) -> HTTPAuthCredential | None: if name == "pypi": repository = AuthenticatorRepositoryConfig( name, "https://upload.pypi.org/legacy/" @@ -358,9 +356,7 @@ def get_http_auth( return None repository = self.configured_repositories[name] - return self._get_credentials_for_repository( - repository=repository, username=username - ) + return self._get_credentials_for_repository(repository=repository) def get_certs_for_repository(self, name: str) -> RepositoryCertificateConfig: if name.lower() == "pypi" or name not in self.configured_repositories: diff --git a/src/poetry/utils/cache.py b/src/poetry/utils/cache.py index 1f0f18ed5e5..90dd1a46f52 100644 --- a/src/poetry/utils/cache.py +++ b/src/poetry/utils/cache.py @@ -19,7 +19,7 @@ from poetry.utils._compat import decode from poetry.utils._compat import encode from poetry.utils.helpers import get_highest_priority_hash_type -from poetry.utils.wheel import InvalidWheelName +from poetry.utils.wheel import InvalidWheelNameError from poetry.utils.wheel import Wheel @@ -316,7 +316,7 @@ def _get_cached_archive( try: wheel = Wheel(archive.name) - except InvalidWheelName: + except InvalidWheelNameError: continue if not wheel.is_supported_by_environment(env): diff --git a/src/poetry/utils/env/__init__.py b/src/poetry/utils/env/__init__.py index 89882ddfd05..c3834f4fbaf 100644 --- a/src/poetry/utils/env/__init__.py +++ b/src/poetry/utils/env/__init__.py @@ -12,8 +12,8 @@ from poetry.utils.env.exceptions import EnvError from poetry.utils.env.exceptions import IncorrectEnvError from poetry.utils.env.exceptions import InvalidCurrentPythonVersionError -from poetry.utils.env.exceptions import NoCompatiblePythonVersionFound -from poetry.utils.env.exceptions import PythonVersionNotFound +from poetry.utils.env.exceptions import NoCompatiblePythonVersionFoundError +from poetry.utils.env.exceptions import PythonVersionNotFoundError from poetry.utils.env.generic_env import GenericEnv from poetry.utils.env.mock_env import MockEnv from poetry.utils.env.null_env import NullEnv @@ -67,7 +67,7 @@ def build_environment( if not env or poetry.package.build_script: with ephemeral_environment( executable=env.python if env else None, - flags={"no-pip": True, "no-setuptools": True, "no-wheel": True}, + flags={"no-pip": True}, ) as venv: if io: requires = [ @@ -102,8 +102,8 @@ def build_environment( "EnvCommandError", "IncorrectEnvError", "InvalidCurrentPythonVersionError", - "NoCompatiblePythonVersionFound", - "PythonVersionNotFound", + "NoCompatiblePythonVersionFoundError", + "PythonVersionNotFoundError", "Env", "EnvManager", "GenericEnv", diff --git a/src/poetry/utils/env/base_env.py b/src/poetry/utils/env/base_env.py index 5a8f2fc6146..7c5574c19e3 100644 --- a/src/poetry/utils/env/base_env.py +++ b/src/poetry/utils/env/base_env.py @@ -7,6 +7,8 @@ import sys import sysconfig +from abc import ABC +from abc import abstractmethod from functools import cached_property from pathlib import Path from subprocess import CalledProcessError @@ -32,7 +34,7 @@ PythonVersion = Tuple[int, int, int, str, int] -class Env: +class Env(ABC): """ An abstract Python environment. """ @@ -58,7 +60,6 @@ def __init__(self, path: Path, base: Path | None = None) -> None: self._base = base or path self._site_packages: SitePackages | None = None - self._paths: dict[str, str] | None = None self._supported_tags: list[Tag] | None = None self._purelib: Path | None = None self._platlib: Path | None = None @@ -223,25 +224,23 @@ def is_path_relative_to_lib(self, path: Path) -> bool: return False @property - def sys_path(self) -> list[str]: - raise NotImplementedError() + @abstractmethod + def sys_path(self) -> list[str]: ... - @property + @cached_property 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]}", - ) + paths = self.get_paths() + + if self.is_venv(): + # We copy pip's logic here for the `include` path + paths["include"] = str( + self.path.joinpath( + "include", + "site", + f"python{self.version_info[0]}.{self.version_info[1]}", ) - - return self._paths + ) + return paths @property def supported_tags(self) -> list[Tag]: @@ -262,8 +261,8 @@ def get_base_prefix(cls) -> Path: return Path(sys.prefix) - def get_marker_env(self) -> dict[str, Any]: - raise NotImplementedError() + @abstractmethod + def get_marker_env(self) -> dict[str, Any]: ... def get_pip_command(self, embedded: bool = False) -> list[str]: if embedded or not Path(self._bin(self._pip_executable)).exists(): @@ -271,11 +270,11 @@ def get_pip_command(self, embedded: bool = False) -> list[str]: # run as module so that pip can update itself on Windows return [str(self.python), "-m", "pip"] - def get_supported_tags(self) -> list[Tag]: - raise NotImplementedError() + @abstractmethod + def get_supported_tags(self) -> list[Tag]: ... - def get_paths(self) -> dict[str, str]: - raise NotImplementedError() + @abstractmethod + def get_paths(self) -> dict[str, str]: ... def is_valid_for_marker(self, marker: BaseMarker) -> bool: valid: bool = marker.validate(self.marker_env) @@ -351,8 +350,8 @@ def execute(self, bin: str, *args: str, **kwargs: Any) -> int: exe.communicate() return exe.returncode - def is_venv(self) -> bool: - raise NotImplementedError() + @abstractmethod + def is_venv(self) -> bool: ... @property def script_dirs(self) -> list[Path]: diff --git a/src/poetry/utils/env/env_manager.py b/src/poetry/utils/env/env_manager.py index 60f49e99889..8d0835b0692 100644 --- a/src/poetry/utils/env/env_manager.py +++ b/src/poetry/utils/env/env_manager.py @@ -5,7 +5,6 @@ import os import plistlib import re -import shutil import subprocess import sys @@ -18,9 +17,7 @@ import virtualenv from cleo.io.null_io import NullIO -from cleo.io.outputs.output import Verbosity from poetry.core.constraints.version import Version -from poetry.core.constraints.version import parse_constraint from poetry.toml.file import TOMLFile from poetry.utils._compat import WINDOWS @@ -28,9 +25,10 @@ from poetry.utils.env.exceptions import EnvCommandError from poetry.utils.env.exceptions import IncorrectEnvError from poetry.utils.env.exceptions import InvalidCurrentPythonVersionError -from poetry.utils.env.exceptions import NoCompatiblePythonVersionFound -from poetry.utils.env.exceptions import PythonVersionNotFound +from poetry.utils.env.exceptions import NoCompatiblePythonVersionFoundError +from poetry.utils.env.exceptions import PythonVersionNotFoundError from poetry.utils.env.generic_env import GenericEnv +from poetry.utils.env.python_manager import Python from poetry.utils.env.script_strings import GET_ENV_PATH_ONELINER from poetry.utils.env.script_strings import GET_PYTHON_VERSION_ONELINER from poetry.utils.env.system_env import SystemEnv @@ -97,70 +95,6 @@ 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 - - try: - encoding = "locale" if sys.version_info >= (3, 10) else None - executable = subprocess.check_output( - [path_python, "-c", "import sys; print(sys.executable)"], - text=True, - encoding=encoding, - ).strip() - return Path(executable) - - except CalledProcessError: - return 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, - ) - - executable = EnvManager._full_python_path("python") - - if executable is not None: - io.write_error_line(f"Found: {executable}", verbosity=Verbosity.VERBOSE) - else: - io.write_error_line( - "Unable to detect the current active python executable. Falling" - " back to default.", - verbosity=Verbosity.VERBOSE, - ) - - return executable - - @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: - encoding = "locale" if sys.version_info >= (3, 10) else None - python_patch = subprocess.check_output( - [executable, "-c", GET_PYTHON_VERSION_ONELINER], - text=True, - encoding=encoding, - ).strip() - - version = ".".join(str(v) for v in python_patch.split(".")[:precision]) - - return Version.parse(version) - @property def in_project_venv(self) -> Path: venv: Path = self._poetry.file.path.parent / ".venv" @@ -189,23 +123,9 @@ def activate(self, python: str) -> Env: # Executable in PATH or full executable path pass - python_path = self._full_python_path(python) - if python_path is None: - raise PythonVersionNotFound(python) - - try: - encoding = "locale" if sys.version_info >= (3, 10) else None - python_version_string = subprocess.check_output( - [python_path, "-c", GET_PYTHON_VERSION_ONELINER], - text=True, - encoding=encoding, - ) - except CalledProcessError as e: - raise EnvCommandError(e) - - python_version = Version.parse(python_version_string.strip()) - minor = f"{python_version.major}.{python_version.minor}" - patch = python_version.text + python_ = Python.get_by_name(python) + if python_ is None: + raise PythonVersionNotFoundError(python) create = False # If we are required to create the virtual environment in the project directory, @@ -218,10 +138,10 @@ def activate(self, python: str) -> Env: _venv = VirtualEnv(venv) current_patch = ".".join(str(v) for v in _venv.version_info[:3]) - if patch != current_patch: + if python_.patch_version.to_string() != current_patch: create = True - self.create_venv(executable=python_path, force=create) + self.create_venv(executable=python_.executable, force=create) return self.get(reload=True) @@ -233,11 +153,14 @@ def activate(self, python: str) -> Env: current_minor = current_env["minor"] current_patch = current_env["patch"] - if current_minor == minor and current_patch != patch: + if ( + current_minor == python_.minor_version.to_string() + and current_patch != python_.patch_version.to_string() + ): # We need to recreate create = True - name = f"{self.base_env_name}-py{minor}" + name = f"{self.base_env_name}-py{python_.minor_version.to_string()}" venv = venv_path / name # Create if needed @@ -251,13 +174,16 @@ def activate(self, python: str) -> Env: _venv = VirtualEnv(venv) current_patch = ".".join(str(v) for v in _venv.version_info[:3]) - if patch != current_patch: + if python_.patch_version.to_string() != current_patch: create = True - self.create_venv(executable=python_path, force=create) + self.create_venv(executable=python_.executable, force=create) # Activate - envs[self.base_env_name] = {"minor": minor, "patch": patch} + envs[self.base_env_name] = { + "minor": python_.minor_version.to_string(), + "patch": python_.patch_version.to_string(), + } self.envs_file.write(envs) return self.get(reload=True) @@ -277,12 +203,8 @@ def get(self, reload: bool = False) -> Env: if self._env is not None and not reload: return self._env - 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() + python = Python.get_preferred_python(config=self._poetry.config, io=self._io) + python_minor = python.minor_version.to_string() env = None envs = None @@ -480,8 +402,11 @@ def create_venv( ) venv_prompt = self._poetry.config.get("virtualenvs.prompt") - if not executable and prefer_active_python: - executable = self._detect_active_python() + python = ( + Python(executable) + if executable + else Python.get_preferred_python(config=self._poetry.config, io=self._io) + ) venv_path = ( self.in_project_venv @@ -491,19 +416,8 @@ def create_venv( if not name: name = self._poetry.package.name - python_patch = ".".join([str(v) for v in sys.version_info[:3]]) - python_minor = ".".join([str(v) for v in sys.version_info[:2]]) - if executable: - encoding = "locale" if sys.version_info >= (3, 10) else None - python_patch = subprocess.check_output( - [executable, "-c", GET_PYTHON_VERSION_ONELINER], - text=True, - encoding=encoding, - ).strip() - python_minor = ".".join(python_patch.split(".")[:2]) - supported_python = self._poetry.package.python_constraint - if not supported_python.allows(Version.parse(python_patch)): + if not supported_python.allows(python.patch_version): # The currently activated or chosen Python version # is not compatible with the Python constraint specified # for the project. @@ -511,72 +425,30 @@ def create_venv( # and notify the user of the incompatibility. # Otherwise, we try to find a compatible Python version. if executable and not prefer_active_python: - raise NoCompatiblePythonVersionFound( - self._poetry.package.python_versions, python_patch + raise NoCompatiblePythonVersionFoundError( + self._poetry.package.python_versions, + python.patch_version.to_string(), ) self._io.write_error_line( - f"The currently activated Python version {python_patch} is not" + f"The currently activated Python version {python.patch_version.to_string()} is not" f" supported by the project ({self._poetry.package.python_versions}).\n" "Trying to find and use a compatible version. " ) - for suffix in sorted( - self._poetry.package.AVAILABLE_PYTHONS, - key=lambda v: (v.startswith("3"), -len(v), v), - reverse=True, - ): - if len(suffix) == 1: - if not parse_constraint(f"^{suffix}.0").allows_any( - supported_python - ): - continue - elif not supported_python.allows_any(parse_constraint(suffix + ".*")): - continue - - python_name = f"python{suffix}" - if self._io.is_debug(): - self._io.write_error_line(f"Trying {python_name}") - - python = self._full_python_path(python_name) - if python is None: - continue - - try: - encoding = "locale" if sys.version_info >= (3, 10) else None - python_patch = subprocess.check_output( - [python, "-c", GET_PYTHON_VERSION_ONELINER], - stderr=subprocess.STDOUT, - text=True, - encoding=encoding, - ).strip() - except CalledProcessError: - continue - - if supported_python.allows(Version.parse(python_patch)): - self._io.write_error_line( - f"Using {python_name} ({python_patch})" - ) - executable = python - python_minor = ".".join(python_patch.split(".")[:2]) - break - - if not executable: - raise NoCompatiblePythonVersionFound( - self._poetry.package.python_versions - ) + python = Python.get_compatible_python(poetry=self._poetry, io=self._io) if in_project_venv: venv = venv_path else: name = self.generate_env_name(name, str(cwd)) - name = f"{name}-py{python_minor.strip()}" + name = f"{name}-py{python.minor_version.to_string()}" venv = venv_path / name if venv_prompt is not None: venv_prompt = venv_prompt.format( project_name=self._poetry.package.name or "virtualenv", - python_version=python_minor, + python_version=python.minor_version.to_string(), ) if not venv.exists(): @@ -613,7 +485,7 @@ def create_venv( if create_venv: self.build_venv( venv, - executable=executable, + executable=python.executable, flags=self._poetry.config.get("virtualenvs.options"), prompt=venv_prompt, ) @@ -643,8 +515,6 @@ def build_venv( executable: Path | None = None, flags: dict[str, str | bool] | None = None, with_pip: bool | None = None, - with_wheel: bool | None = None, - with_setuptools: bool | None = None, prompt: str | None = None, ) -> virtualenv.run.session.Session: flags = flags or {} @@ -652,25 +522,9 @@ def build_venv( if with_pip is not None: flags["no-pip"] = not with_pip - if with_wheel is not None: - wheel_flags: dict[str, str | bool] = ( - {"wheel": "bundle"} if with_wheel else {"no-wheel": True} - ) - flags.update(wheel_flags) - - if with_setuptools is not None: - setuptools_flags: dict[str, str | bool] = ( - {"setuptools": "bundle"} if with_setuptools else {"no-setuptools": True} - ) - flags.update(setuptools_flags) - flags.setdefault("no-pip", True) - - if "setuptools" not in flags and "no-setuptools" not in flags: - flags["no-setuptools"] = True - - if "wheel" not in flags and "no-wheel" not in flags: - flags["no-wheel"] = True + flags.setdefault("no-setuptools", True) + flags.setdefault("no-wheel", True) if WINDOWS: path = get_real_windows_path(path) diff --git a/src/poetry/utils/env/exceptions.py b/src/poetry/utils/env/exceptions.py index ece3b3924a0..78082d45932 100644 --- a/src/poetry/utils/env/exceptions.py +++ b/src/poetry/utils/env/exceptions.py @@ -33,12 +33,12 @@ def __init__(self, e: CalledProcessError) -> None: super().__init__("\n\n".join(message_parts)) -class PythonVersionNotFound(EnvError): +class PythonVersionNotFoundError(EnvError): def __init__(self, expected: str) -> None: super().__init__(f"Could not find the python executable {expected}") -class NoCompatiblePythonVersionFound(EnvError): +class NoCompatiblePythonVersionFoundError(EnvError): def __init__(self, expected: str, given: str | None = None) -> None: if given: message = ( diff --git a/src/poetry/utils/env/null_env.py b/src/poetry/utils/env/null_env.py index 7bd0a9e1c79..95de157862d 100644 --- a/src/poetry/utils/env/null_env.py +++ b/src/poetry/utils/env/null_env.py @@ -2,6 +2,7 @@ import sys +from functools import cached_property from pathlib import Path from typing import Any @@ -20,16 +21,14 @@ def __init__( self._execute = execute self.executed: list[list[str]] = [] - @property + @cached_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 + paths = self.get_paths() + paths["platlib"] = str(self._path / "platlib") + paths["purelib"] = str(self._path / "purelib") + paths["scripts"] = str(self._path / "scripts") + paths["data"] = str(self._path / "data") + return paths def _run(self, cmd: list[str], **kwargs: Any) -> str: self.executed.append(cmd) diff --git a/src/poetry/utils/env/python_manager.py b/src/poetry/utils/env/python_manager.py new file mode 100644 index 00000000000..ce1d8103978 --- /dev/null +++ b/src/poetry/utils/env/python_manager.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +import shutil +import subprocess +import sys + +from functools import cached_property +from pathlib import Path +from typing import TYPE_CHECKING + +from cleo.io.null_io import NullIO +from cleo.io.outputs.output import Verbosity +from poetry.core.constraints.version import Version +from poetry.core.constraints.version import parse_constraint + +from poetry.utils._compat import decode +from poetry.utils.env.exceptions import NoCompatiblePythonVersionFoundError +from poetry.utils.env.script_strings import GET_PYTHON_VERSION_ONELINER + + +if TYPE_CHECKING: + from cleo.io.io import IO + + from poetry.config.config import Config + from poetry.poetry import Poetry + + +class Python: + def __init__(self, executable: str | Path, version: Version | None = None) -> None: + self.executable = Path(executable) + self._version = version + + @property + def version(self) -> Version: + if not self._version: + if self.executable == Path(sys.executable): + python_version = ".".join(str(v) for v in sys.version_info[:3]) + else: + encoding = "locale" if sys.version_info >= (3, 10) else None + python_version = decode( + subprocess.check_output( + [str(self.executable), "-c", GET_PYTHON_VERSION_ONELINER], + text=True, + encoding=encoding, + ).strip() + ) + self._version = Version.parse(python_version) + + return self._version + + @cached_property + def patch_version(self) -> Version: + return Version.from_parts( + major=self.version.major, + minor=self.version.minor, + patch=self.version.patch, + ) + + @cached_property + def minor_version(self) -> Version: + return Version.from_parts(major=self.version.major, minor=self.version.minor) + + @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 + + try: + encoding = "locale" if sys.version_info >= (3, 10) else None + executable = subprocess.check_output( + [path_python, "-c", "import sys; print(sys.executable)"], + text=True, + encoding=encoding, + ).strip() + return Path(executable) + + except subprocess.CalledProcessError: + return None + + @staticmethod + def _detect_active_python(io: IO) -> Path | None: + io.write_error_line( + "Trying to detect current active python executable as specified in" + " the config.", + verbosity=Verbosity.VERBOSE, + ) + + executable = Python._full_python_path("python") + + if executable is not None: + io.write_error_line(f"Found: {executable}", verbosity=Verbosity.VERBOSE) + else: + io.write_error_line( + "Unable to detect the current active python executable. Falling" + " back to default.", + verbosity=Verbosity.VERBOSE, + ) + + return executable + + @classmethod + def get_system_python(cls) -> Python: + return cls(executable=sys.executable) + + @classmethod + def get_by_name(cls, python_name: str) -> Python | None: + executable = cls._full_python_path(python_name) + if not executable: + return None + + return cls(executable=executable) + + @classmethod + def get_preferred_python(cls, config: Config, io: IO | None = None) -> Python: + io = io or NullIO() + + if config.get("virtualenvs.prefer-active-python") and ( + active_python := Python._detect_active_python(io) + ): + return cls(executable=active_python) + + return cls.get_system_python() + + @classmethod + def get_compatible_python(cls, poetry: Poetry, io: IO | None = None) -> Python: + io = io or NullIO() + supported_python = poetry.package.python_constraint + python = None + + for suffix in [ + *sorted( + poetry.package.AVAILABLE_PYTHONS, + key=lambda v: (v.startswith("3"), -len(v), v), + reverse=True, + ), + "", + ]: + if len(suffix) == 1: + if not parse_constraint(f"^{suffix}.0").allows_any(supported_python): + continue + elif suffix and not supported_python.allows_any( + parse_constraint(suffix + ".*") + ): + continue + + python_name = f"python{suffix}" + if io.is_debug(): + io.write_error_line(f"Trying {python_name}") + + executable = cls._full_python_path(python_name) + if executable is None: + continue + + candidate = cls(executable) + if supported_python.allows(candidate.patch_version): + python = candidate + io.write_error_line( + f"Using {python_name} ({python.patch_version})" + ) + break + + if not python: + raise NoCompatiblePythonVersionFoundError(poetry.package.python_versions) + + return python diff --git a/src/poetry/utils/helpers.py b/src/poetry/utils/helpers.py index 07e0ce41687..22608f8c3eb 100644 --- a/src/poetry/utils/helpers.py +++ b/src/poetry/utils/helpers.py @@ -125,7 +125,7 @@ def merge_dicts(d1: dict[str, Any], d2: dict[str, Any]) -> None: d1[k] = d2[k] -class HTTPRangeRequestSupported(Exception): +class HTTPRangeRequestSupportedError(Exception): """Raised when server unexpectedly supports byte ranges.""" @@ -143,7 +143,7 @@ def download_file( downloader = Downloader(url, dest, session, max_retries=max_retries) if raise_accepts_ranges and downloader.accepts_ranges: - raise HTTPRangeRequestSupported(f"URL {url} supports range requests.") + raise HTTPRangeRequestSupportedError(f"URL {url} supports range requests.") set_indicator = False with Indicator.context() as update_context: diff --git a/src/poetry/utils/isolated_build.py b/src/poetry/utils/isolated_build.py index e78361f8480..5879676c691 100644 --- a/src/poetry/utils/isolated_build.py +++ b/src/poetry/utils/isolated_build.py @@ -136,7 +136,7 @@ def isolated_builder( with ephemeral_environment( executable=python_executable, - flags={"no-pip": True, "no-setuptools": True, "no-wheel": True}, + flags={"no-pip": True}, ) as venv: env = IsolatedEnv(venv, pool) stdout = StringIO() diff --git a/src/poetry/utils/pip.py b/src/poetry/utils/pip.py index b74294795ed..458e897855e 100644 --- a/src/poetry/utils/pip.py +++ b/src/poetry/utils/pip.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING -from poetry.exceptions import PoetryException +from poetry.exceptions import PoetryError from poetry.utils.env import EnvCommandError @@ -45,7 +45,7 @@ def pip_install( if editable: if not path.is_dir(): - raise PoetryException( + raise PoetryError( "Cannot install non directory dependencies in editable mode" ) args.append("-e") @@ -55,4 +55,4 @@ def pip_install( try: return environment.run_pip(*args) except EnvCommandError as e: - raise PoetryException(f"Failed to install {path}") from e + raise PoetryError(f"Failed to install {path}") from e diff --git a/src/poetry/utils/wheel.py b/src/poetry/utils/wheel.py index f45c50b3b35..bb4a70b7907 100644 --- a/src/poetry/utils/wheel.py +++ b/src/poetry/utils/wheel.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) -class InvalidWheelName(Exception): +class InvalidWheelNameError(Exception): pass @@ -24,7 +24,7 @@ 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.") + raise InvalidWheelNameError(f"{filename} is not a valid wheel filename.") self.filename = filename self.name = wheel_info.group("name").replace("_", "-") diff --git a/src/poetry/vcs/git/backend.py b/src/poetry/vcs/git/backend.py index bd0ed643695..7d77bf653af 100644 --- a/src/poetry/vcs/git/backend.py +++ b/src/poetry/vcs/git/backend.py @@ -219,7 +219,7 @@ def _fetch_remote_refs(cls, url: str, local: Repo) -> FetchPackResult: if wassima.RUSTLS_LOADED else None, ) - kwargs["pool_manager"] = pool_manager + kwargs["pool_manager"] = pool_manager # type: ignore[assignment] client, path = get_transport_and_path(url, config=config, **kwargs) diff --git a/tests/config/test_source.py b/tests/config/test_source.py index 2331dcb4339..e639b81c91f 100644 --- a/tests/config/test_source.py +++ b/tests/config/test_source.py @@ -43,29 +43,11 @@ def test_source_default_is_primary() -> None: 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), + ("supplemental", Priority.SUPPLEMENTAL), + ("SUPPLEMENTAL", Priority.SUPPLEMENTAL), ], ) def test_source_priority_as_string(priority: str, expected_priority: Priority) -> None: @@ -74,4 +56,4 @@ def test_source_priority_as_string(priority: str, expected_priority: Priority) - "https://example.com", priority=priority, # type: ignore[arg-type] ) - assert source.priority == Priority.SECONDARY + assert source.priority == Priority.SUPPLEMENTAL diff --git a/tests/conftest.py b/tests/conftest.py index 1c443dbee30..e8d289a1931 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -49,10 +49,10 @@ from collections.abc import Iterator from collections.abc import Mapping - from _pytest.config import Config as PyTestConfig - from _pytest.config.argparsing import Parser - from _pytest.tmpdir import TempPathFactory from keyring.credentials import Credential + from pytest import Config as PyTestConfig + from pytest import Parser + from pytest import TempPathFactory from pytest_mock import MockerFixture from poetry.poetry import Poetry @@ -138,7 +138,7 @@ def get_credential( if password is None: return None - return SimpleCredential(username, password) # type: ignore[no-untyped-call] + return SimpleCredential(username, password) def delete_password(self, service: str, username: str) -> None: if service in self._passwords and username in self._passwords[service]: @@ -534,7 +534,6 @@ def venv_flags_default() -> dict[str, bool]: "always-copy": False, "system-site-packages": False, "no-pip": False, - "no-setuptools": False, } diff --git a/tests/console/commands/cache/conftest.py b/tests/console/commands/cache/conftest.py index 186b71995fd..a5e0fe7fe6b 100644 --- a/tests/console/commands/cache/conftest.py +++ b/tests/console/commands/cache/conftest.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: from pathlib import Path - from _pytest.monkeypatch import MonkeyPatch + from pytest import MonkeyPatch from tests.conftest import Config diff --git a/tests/console/commands/conftest.py b/tests/console/commands/conftest.py index 8c095a6bb81..b96f5a07e8b 100644 --- a/tests/console/commands/conftest.py +++ b/tests/console/commands/conftest.py @@ -12,7 +12,7 @@ def init_basic_inputs() -> str: "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "n", # Interactive packages "n", # Interactive dev packages "\n", # Generate @@ -23,14 +23,14 @@ def init_basic_inputs() -> str: @pytest.fixture() def init_basic_toml() -> str: return """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" - -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" +requires-python = ">=3.6" """ diff --git a/tests/console/commands/env/test_use.py b/tests/console/commands/env/test_use.py index 662e3434939..478ef414ef8 100644 --- a/tests/console/commands/env/test_use.py +++ b/tests/console/commands/env/test_use.py @@ -77,7 +77,6 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file( "always-copy": False, "system-site-packages": False, "no-pip": False, - "no-setuptools": False, }, prompt="simple-project-py3.7", ) diff --git a/tests/console/commands/source/conftest.py b/tests/console/commands/source/conftest.py index 7e44e280069..264ee640e6f 100644 --- a/tests/console/commands/source/conftest.py +++ b/tests/console/commands/source/conftest.py @@ -24,33 +24,11 @@ def source_two() -> Source: return Source(name="two", url="https://two.com") -@pytest.fixture -def source_default_deprecated() -> Source: - return Source(name="default", url="https://default.com", default=True) - - -@pytest.fixture -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_supplemental() -> Source: return Source( @@ -158,16 +136,12 @@ def add_all_source_types( command_tester_factory: CommandTesterFactory, poetry_with_source: Poetry, source_primary: Source, - source_default: Source, - source_secondary: Source, source_supplemental: 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_supplemental, source_explicit, ]: diff --git a/tests/console/commands/source/test_add.py b/tests/console/commands/source/test_add.py index 53ae876133a..91a2a8cb127 100644 --- a/tests/console/commands/source/test_add.py +++ b/tests/console/commands/source/test_add.py @@ -22,51 +22,13 @@ def tester( return command_tester_factory("source add", poetry=poetry_with_source) -def _get_source_warning(priority: Priority) -> str: - if priority is Priority.SECONDARY: - return ( - "Warning: Priority 'secondary' is deprecated. Consider changing the" - " priority to one of the non-deprecated values: 'primary'," - " 'supplemental', 'explicit'." - ) - elif priority is Priority.DEFAULT: - return ( - "Warning: Priority 'default' is deprecated. You can achieve" - " the same effect by changing the priority to 'primary' and putting" - " the source first." - ) - return "" - - -def assert_source_added_legacy( - tester: CommandTester, - poetry: Poetry, - source_existing: Source, - source_added: Source, -) -> None: - warning = ( - "Warning: Priority was set through a deprecated flag (--default or" - " --secondary). Consider using --priority next time.\n" - + _get_source_warning(source_added.priority) - ) - assert tester.io.fetch_error().strip() == warning - 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, source_existing: Source, source_added: Source, ) -> None: - assert tester.io.fetch_error().strip() == _get_source_warning(source_added.priority) + assert tester.io.fetch_error().strip() == "" assert ( tester.io.fetch_output().strip() == f"Adding source with name {source_added.name}." @@ -87,72 +49,6 @@ def test_source_add_simple( 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, -) -> None: - 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, -) -> None: - 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, -) -> None: - 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"{_get_source_warning(source_default.priority)}\n" - 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, -) -> 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_supplemental( tester: CommandTester, source_existing: Source, @@ -177,26 +73,6 @@ def test_source_add_explicit( 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() - == "Cannot configure a source as both default and secondary." - ) - assert tester.status_code == 1 - - -def test_source_add_error_priority_and_deprecated_legacy(tester: CommandTester) -> None: - 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_no_url(tester: CommandTester) -> None: tester.execute("foo") assert ( @@ -238,32 +114,6 @@ def test_source_add_pypi_explicit( ) -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" - f" time.\n{_get_source_warning(Priority.DEFAULT)}" - ) - 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 - - @pytest.mark.parametrize("modifier", ["lower", "upper"]) def test_source_add_existing_no_change_except_case_of_name( modifier: str, @@ -295,7 +145,7 @@ def test_source_add_existing_updating( poetry_with_source: Poetry, ) -> None: name = getattr(source_existing.name, modifier)() - tester.execute(f"--priority=default {name} {source_existing.url}") + tester.execute(f"--priority=supplemental {name} {source_existing.url}") assert ( tester.io.fetch_output().strip() == f"Source with name {name} already exists. Updating." @@ -307,31 +157,6 @@ def test_source_add_existing_updating( assert len(sources) == 1 assert sources[0] != source_existing expected_source = Source( - name=name, url=source_existing.url, priority=Priority.DEFAULT + name=name, url=source_existing.url, priority=Priority.SUPPLEMENTAL ) assert sources[0] == expected_source - - -@pytest.mark.parametrize("modifier", ["lower", "upper"]) -def test_source_add_existing_fails_due_to_other_default( - modifier: str, - tester: CommandTester, - source_existing: Source, - source_default: Source, - poetry_with_source: Poetry, -) -> None: - tester.execute(f"--priority=default {source_default.name} {source_default.url}") - tester.io.fetch_error() - tester.io.fetch_output() - - name = getattr(source_existing.name, modifier)() - tester.execute(f"--priority=default {name} {source_existing.url}") - - assert ( - tester.io.fetch_error().strip() - == f"{_get_source_warning(source_default.priority)}\n" - 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_show.py b/tests/console/commands/source/test_show.py index 91993a62557..bee08603d5c 100644 --- a/tests/console/commands/source/test_show.py +++ b/tests/console/commands/source/test_show.py @@ -119,8 +119,6 @@ def test_source_show_two( "source_str", ( "source_primary", - "source_default", - "source_secondary", "source_supplemental", "source_explicit", ), diff --git a/tests/console/commands/test_add.py b/tests/console/commands/test_add.py index 73ba0241018..9c7509d7c52 100644 --- a/tests/console/commands/test_add.py +++ b/tests/console/commands/test_add.py @@ -27,6 +27,7 @@ from pytest_mock import MockerFixture from tomlkit import TOMLDocument + from poetry.config.config import Config from poetry.poetry import Poetry from poetry.utils.env import MockEnv from poetry.utils.env import VirtualEnv @@ -37,6 +38,13 @@ from tests.types import ProjectFactory +@pytest.fixture(autouse=True) +def config(config: Config) -> Config: + # Disable parallel installs to get reproducible output. + config.merge({"installer": {"parallel": False}}) + return config + + @pytest.fixture def poetry_with_up_to_date_lockfile( project_factory: ProjectFactory, fixture_dir: FixtureDirGetter @@ -795,10 +803,40 @@ def test_add_url_constraint_wheel_with_extras( } +@pytest.mark.parametrize("project_dependencies", [True, False]) +@pytest.mark.parametrize( + ("existing_extras", "expected_extras"), + [ + (None, {"my-extra": ["cachy (==0.2.0)"]}), + ( + {"other": ["foo>2"]}, + {"other": ["foo>2"], "my-extra": ["cachy (==0.2.0)"]}, + ), + ({"my-extra": ["foo>2"]}, {"my-extra": ["foo>2", "cachy (==0.2.0)"]}), + ( + {"my-extra": ["foo>2", "cachy (==0.1.0)", "bar>1"]}, + {"my-extra": ["foo>2", "cachy (==0.2.0)", "bar>1"]}, + ), + ], +) def test_add_constraint_with_optional( - app: PoetryTestApplication, tester: CommandTester + app: PoetryTestApplication, + tester: CommandTester, + project_dependencies: bool, + existing_extras: dict[str, list[str]] | None, + expected_extras: dict[str, list[str]], ) -> None: - tester.execute("cachy=0.2.0 --optional") + pyproject: dict[str, Any] = app.poetry.file.read() + if project_dependencies: + pyproject["project"]["dependencies"] = ["foo>1"] + if existing_extras: + pyproject["project"]["optional-dependencies"] = existing_extras + else: + pyproject["tool"]["poetry"]["dependencies"]["foo"] = "^1.0" + pyproject = cast("TOMLDocument", pyproject) + app.poetry.file.write(pyproject) + + tester.execute("cachy=0.2.0 --optional my-extra") expected = """\ Updating dependencies @@ -813,14 +851,37 @@ def test_add_constraint_with_optional( assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 0 - pyproject: dict[str, Any] = app.poetry.file.read() - content = pyproject["tool"]["poetry"] + pyproject2: dict[str, Any] = app.poetry.file.read() + project_content = pyproject2["project"] + poetry_content = pyproject2["tool"]["poetry"] + + if project_dependencies: + assert "cachy" not in poetry_content["dependencies"] + assert "cachy" not in project_content["dependencies"] + assert "my-extra" in project_content["optional-dependencies"] + assert project_content["optional-dependencies"] == expected_extras + assert not tester.io.fetch_error() + else: + assert "dependencies" not in project_content + assert "optional-dependencies" not in project_content + assert "cachy" in poetry_content["dependencies"] + assert poetry_content["dependencies"]["cachy"] == { + "version": "0.2.0", + "optional": True, + } + assert ( + "Optional dependencies will not be added to extras in legacy mode." + in tester.io.fetch_error() + ) - assert "cachy" in content["dependencies"] - assert content["dependencies"]["cachy"] == { - "version": "0.2.0", - "optional": True, - } + +def test_add_constraint_with_optional_not_main_group( + app: PoetryTestApplication, tester: CommandTester +) -> None: + with pytest.raises(ValueError) as e: + tester.execute("cachy=0.2.0 --group dev --optional my-extra") + + assert str(e.value) == "You can only add optional dependencies to the main group" def test_add_constraint_with_python( @@ -1010,14 +1071,36 @@ def test_add_to_section_that_does_not_exist_yet( assert expected in string_content -def test_add_to_dev_section_deprecated( - app: PoetryTestApplication, tester: CommandTester +def test_add_creating_poetry_section_does_not_remove_existing_tools( + repo: TestRepository, + project_factory: ProjectFactory, + command_tester_factory: CommandTesterFactory, ) -> None: - tester.execute("cachy --dev") + repo.add_package(get_package("cachy", "0.2.0")) - warning = """\ -The --dev option is deprecated, use the `--group dev` notation instead. -""" + poetry = project_factory( + name="foobar", + pyproject_content=( + '[project]\nname = "foobar"\nversion="0"\n' '[tool.foo]\nkey = "value"\n' + ), + ) + tester = command_tester_factory("add", poetry=poetry) + tester.execute("--group dev cachy") + + assert isinstance(tester.command, InstallerCommand) + assert tester.command.installer.executor.installations_count == 2 + + pyproject: dict[str, Any] = poetry.file.read() + content = pyproject["tool"]["poetry"] + + assert "cachy" in content["group"]["dev"]["dependencies"] + assert content["group"]["dev"]["dependencies"]["cachy"] == "^0.2.0" + assert "foo" in pyproject["tool"] + assert pyproject["tool"]["foo"]["key"] == "value" + + +def test_add_to_dev_section(app: PoetryTestApplication, tester: CommandTester) -> None: + tester.execute("cachy --dev") expected = """\ Using version ^0.2.0 for cachy @@ -1033,7 +1116,7 @@ def test_add_to_dev_section_deprecated( Writing lock file """ - assert tester.io.fetch_error() == warning + assert tester.io.fetch_error() == "" assert tester.io.fetch_output() == expected assert isinstance(tester.command, InstallerCommand) assert tester.command.installer.executor.installations_count == 2 @@ -1074,11 +1157,18 @@ def test_add_should_not_select_prereleases( assert content["dependencies"]["pyyaml"] == "^3.13" +@pytest.mark.parametrize("project_dependencies", [True, False]) def test_add_should_skip_when_adding_existing_package_with_no_constraint( - app: PoetryTestApplication, repo: TestRepository, tester: CommandTester + app: PoetryTestApplication, + repo: TestRepository, + tester: CommandTester, + project_dependencies: bool, ) -> None: pyproject: dict[str, Any] = app.poetry.file.read() - pyproject["tool"]["poetry"]["dependencies"]["foo"] = "^1.0" + if project_dependencies: + pyproject["project"]["dependencies"] = ["foo>1"] + else: + pyproject["tool"]["poetry"]["dependencies"]["foo"] = "^1.0" pyproject = cast("TOMLDocument", pyproject) app.poetry.file.write(pyproject) @@ -1099,11 +1189,18 @@ def test_add_should_skip_when_adding_existing_package_with_no_constraint( assert expected in tester.io.fetch_output() +@pytest.mark.parametrize("project_dependencies", [True, False]) def test_add_should_skip_when_adding_canonicalized_existing_package_with_no_constraint( - app: PoetryTestApplication, repo: TestRepository, tester: CommandTester + app: PoetryTestApplication, + repo: TestRepository, + tester: CommandTester, + project_dependencies: bool, ) -> None: pyproject: dict[str, Any] = app.poetry.file.read() - pyproject["tool"]["poetry"]["dependencies"]["foo-bar"] = "^1.0" + if project_dependencies: + pyproject["project"]["dependencies"] = ["foo-bar>1"] + else: + pyproject["tool"]["poetry"]["dependencies"]["foo-bar"] = "^1.0" pyproject = cast("TOMLDocument", pyproject) app.poetry.file.write(pyproject) @@ -1136,49 +1233,72 @@ def test_add_should_fail_circular_dependency( assert expected in tester.io.fetch_error() +@pytest.mark.parametrize("project_dependencies", [True, False]) def test_add_latest_should_not_create_duplicate_keys( project_factory: ProjectFactory, repo: TestRepository, command_tester_factory: CommandTesterFactory, + project_dependencies: bool, ) -> 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" - """ + if project_dependencies: + pyproject_content = """\ + [project] + name = "simple-project" + version = "1.2.3" + dependencies = [ + "Foo >= 0.6,<0.7", + ] + """ + else: + pyproject_content = """\ + [tool.poetry] + name = "simple-project" + version = "1.2.3" + + [tool.poetry.dependencies] + python = "^3.6" + Foo = "^0.6" + """ poetry = project_factory(name="simple-project", pyproject_content=pyproject_content) pyproject: dict[str, Any] = poetry.file.read() - assert "Foo" in pyproject["tool"]["poetry"]["dependencies"] - assert pyproject["tool"]["poetry"]["dependencies"]["Foo"] == "^0.6" - assert "foo" not in pyproject["tool"]["poetry"]["dependencies"] + if project_dependencies: + assert "tool" not in pyproject + assert pyproject["project"]["dependencies"] == ["Foo >= 0.6,<0.7"] + else: + assert "project" not in pyproject + assert "Foo" in pyproject["tool"]["poetry"]["dependencies"] + assert pyproject["tool"]["poetry"]["dependencies"]["Foo"] == "^0.6" + assert "foo" not in pyproject["tool"]["poetry"]["dependencies"] tester = command_tester_factory("add", poetry=poetry) repo.add_package(get_package("foo", "1.1.2")) tester.execute("foo@latest") updated_pyproject: dict[str, Any] = poetry.file.read() - assert "Foo" in updated_pyproject["tool"]["poetry"]["dependencies"] - assert updated_pyproject["tool"]["poetry"]["dependencies"]["Foo"] == "^1.1.2" - assert "foo" not in updated_pyproject["tool"]["poetry"]["dependencies"] + if project_dependencies: + assert "tool" not in updated_pyproject + assert updated_pyproject["project"]["dependencies"] == ["foo (>=1.1.2,<2.0.0)"] + else: + assert "project" not in updated_pyproject + assert "Foo" in updated_pyproject["tool"]["poetry"]["dependencies"] + assert updated_pyproject["tool"]["poetry"]["dependencies"]["Foo"] == "^1.1.2" + assert "foo" not in updated_pyproject["tool"]["poetry"]["dependencies"] +@pytest.mark.parametrize("project_dependencies", [True, False]) def test_add_should_work_when_adding_existing_package_with_latest_constraint( - app: PoetryTestApplication, repo: TestRepository, tester: CommandTester + app: PoetryTestApplication, + repo: TestRepository, + tester: CommandTester, + project_dependencies: bool, ) -> None: pyproject: dict[str, Any] = app.poetry.file.read() - pyproject["tool"]["poetry"]["dependencies"]["foo"] = "^1.0" + if project_dependencies: + pyproject["project"]["dependencies"] = ["foo>1"] + else: + pyproject["tool"]["poetry"]["dependencies"]["foo"] = "^1.0" pyproject = cast("TOMLDocument", pyproject) app.poetry.file.write(pyproject) @@ -1202,10 +1322,16 @@ def test_add_should_work_when_adding_existing_package_with_latest_constraint( assert expected in tester.io.fetch_output() pyproject2: dict[str, Any] = app.poetry.file.read() - content = pyproject2["tool"]["poetry"] + project_content = pyproject2["project"] + poetry_content = pyproject2["tool"]["poetry"] - assert "foo" in content["dependencies"] - assert content["dependencies"]["foo"] == "^1.1.2" + if project_dependencies: + assert "foo" not in poetry_content["dependencies"] + assert project_content["dependencies"] == ["foo (>=1.1.2,<2.0.0)"] + else: + assert "dependencies" not in project_content + assert "foo" in poetry_content["dependencies"] + assert poetry_content["dependencies"]["foo"] == "^1.1.2" def test_add_chooses_prerelease_if_only_prereleases_are_available( @@ -1466,3 +1592,134 @@ def test_add_does_not_update_locked_dependencies( p for p in lock_data["package"] if p["name"] == "docker" ) assert docker_locked_after_command["version"] == expected_docker + + +def test_add_creates_dependencies_array_if_necessary( + project_factory: ProjectFactory, + repo: TestRepository, + command_tester_factory: CommandTesterFactory, +) -> None: + pyproject_content = """\ + [project] + name = "simple-project" + version = "1.2.3" + + [project.optional-dependencies] + test = ["foo"] + """ + + poetry = project_factory(name="simple-project", pyproject_content=pyproject_content) + + repo.add_package(get_package("foo", "2.0")) + repo.add_package(get_package("bar", "2.0")) + + tester = command_tester_factory("add", poetry=poetry) + tester.execute("bar>=1.0") + + updated_pyproject: dict[str, Any] = poetry.file.read() + assert updated_pyproject["project"]["dependencies"] == ["bar (>=1.0)"] + + +@pytest.mark.parametrize("has_poetry_section", [True, False]) +def test_add_does_not_add_poetry_dependencies_if_not_necessary( + project_factory: ProjectFactory, + repo: TestRepository, + command_tester_factory: CommandTesterFactory, + has_poetry_section: bool, +) -> None: + pyproject_content = """\ + [project] + name = "simple-project" + version = "1.2.3" + dependencies = [ + "foo >= 1.0", + ] + """ + if has_poetry_section: + pyproject_content += """\ + [tool.poetry] + packages = [ { include = "simple" } ] + """ + + poetry = project_factory(name="simple-project", pyproject_content=pyproject_content) + pyproject: dict[str, Any] = poetry.file.read() + + if has_poetry_section: + assert "dependencies" not in pyproject["tool"]["poetry"] + else: + assert "tool" not in pyproject + + repo.add_package(get_package("foo", "2.0")) + repo.add_package(get_package("bar", "2.0")) + + tester = command_tester_factory("add", poetry=poetry) + tester.execute("bar>=1.0 --platform linux") + + updated_pyproject: dict[str, Any] = poetry.file.read() + if has_poetry_section: + assert "dependencies" not in pyproject["tool"]["poetry"] + else: + assert "tool" not in pyproject + assert updated_pyproject["project"]["dependencies"] == [ + "foo >= 1.0", + 'bar (>=1.0) ; sys_platform == "linux"', + ] + + +@pytest.mark.parametrize("has_poetry_section", [True, False]) +def test_add_poetry_dependencies_if_necessary( + project_factory: ProjectFactory, + repo: TestRepository, + command_tester_factory: CommandTesterFactory, + mocker: MockerFixture, + has_poetry_section: bool, +) -> None: + pyproject_content = """\ + [project] + name = "simple-project" + version = "1.2.3" + dependencies = [ + "foo >= 1.0", + ] + """ + if has_poetry_section: + pyproject_content += """\ + [tool.poetry] + packages = [ { include = "simple" } ] + """ + + poetry = project_factory(name="simple-project", pyproject_content=pyproject_content) + pyproject: dict[str, Any] = poetry.file.read() + + if has_poetry_section: + assert "dependencies" not in pyproject["tool"]["poetry"] + else: + assert "tool" not in pyproject + + repo.add_package(get_package("foo", "2.0")) + other_repo = LegacyRepository(name="my-index", url="https://my-index.fake") + poetry.pool.add_repository(other_repo) + package = get_package("bar", "2.0") + mocker.patch.object(other_repo, "package", return_value=package) + mocker.patch.object(other_repo, "_find_packages", wraps=lambda _, name: [package]) + repo.add_package(package) + + tester = command_tester_factory("add", poetry=poetry) + tester.execute("bar>=1.0 --platform linux --allow-prereleases --source my-index") + + updated_pyproject: dict[str, Any] = poetry.file.read() + if has_poetry_section: + assert "dependencies" not in pyproject["tool"]["poetry"] + else: + assert "tool" not in pyproject + assert updated_pyproject["project"]["dependencies"] == [ + "foo >= 1.0", + 'bar (>=1.0) ; sys_platform == "linux"', + ] + assert updated_pyproject["tool"]["poetry"]["dependencies"] == { + "bar": { + "platform": "linux", + "source": "my-index", + "allow-prereleases": True, + } + } diff --git a/tests/console/commands/test_check.py b/tests/console/commands/test_check.py index 67cfef8410b..1496ebcd544 100644 --- a/tests/console/commands/test_check.py +++ b/tests/console/commands/test_check.py @@ -22,8 +22,8 @@ @pytest.fixture -def poetry_sample_project(set_project_context: SetProjectContext) -> Iterator[Poetry]: - with set_project_context("sample_project", in_place=False) as cwd: +def poetry_simple_project(set_project_context: SetProjectContext) -> Iterator[Poetry]: + with set_project_context("simple_project", in_place=False) as cwd: yield Factory().create_poetry(cwd) @@ -45,9 +45,9 @@ def poetry_with_up_to_date_lockfile( @pytest.fixture() def tester( - command_tester_factory: CommandTesterFactory, poetry_sample_project: Poetry + command_tester_factory: CommandTesterFactory, poetry_simple_project: Poetry ) -> CommandTester: - return command_tester_factory("check", poetry=poetry_sample_project) + return command_tester_factory("check", poetry=poetry_simple_project) def test_check_valid(tester: CommandTester) -> None: @@ -60,6 +60,57 @@ def test_check_valid(tester: CommandTester) -> None: assert tester.io.fetch_output() == expected +def test_check_valid_legacy( + mocker: MockerFixture, tester: CommandTester, fixture_dir: FixtureDirGetter +) -> None: + mocker.patch( + "poetry.poetry.Poetry.file", + return_value=TOMLFile(fixture_dir("simple_project_legacy") / "pyproject.toml"), + new_callable=mocker.PropertyMock, + ) + tester.execute() + + expected = ( + "Warning: [tool.poetry.name] is deprecated. Use [project.name] instead.\n" + "Warning: [tool.poetry.version] is set but 'version' is not in " + "[project.dynamic]. If it is static use [project.version]. If it is dynamic, " + "add 'version' to [project.dynamic].\n" + "If you want to set the version dynamically via `poetry build " + "--local-version` or you are using a plugin, which sets the version " + "dynamically, you should define the version in [tool.poetry] and add " + "'version' to [project.dynamic].\n" + "Warning: [tool.poetry.description] is deprecated. Use [project.description] " + "instead.\n" + "Warning: [tool.poetry.readme] is set but 'readme' is not in " + "[project.dynamic]. If it is static use [project.readme]. If it is dynamic, " + "add 'readme' to [project.dynamic].\n" + "If you want to define multiple readmes, you should define them in " + "[tool.poetry] and add 'readme' to [project.dynamic].\n" + "Warning: [tool.poetry.license] is deprecated. Use [project.license] instead.\n" + "Warning: [tool.poetry.authors] is deprecated. Use [project.authors] instead.\n" + "Warning: [tool.poetry.keywords] is deprecated. Use [project.keywords] " + "instead.\n" + "Warning: [tool.poetry.classifiers] is set but 'classifiers' is not in " + "[project.dynamic]. If it is static use [project.classifiers]. If it is " + "dynamic, add 'classifiers' to [project.dynamic].\n" + "ATTENTION: Per default Poetry determines classifiers for supported Python " + "versions and license automatically. If you define classifiers in [project], " + "you disable the automatic enrichment. In other words, you have to define all " + "classifiers manually. If you want to use Poetry's automatic enrichment of " + "classifiers, you should define them in [tool.poetry] and add 'classifiers' " + "to [project.dynamic].\n" + "Warning: [tool.poetry.homepage] is deprecated. Use [project.urls] instead.\n" + "Warning: [tool.poetry.repository] is deprecated. Use [project.urls] instead.\n" + "Warning: [tool.poetry.documentation] is deprecated. Use [project.urls] " + "instead.\n" + "Warning: Defining console scripts in [tool.poetry.scripts] is deprecated. " + "Use [project.scripts] instead. ([tool.poetry.scripts] should only be used " + "for scripts of type 'file').\n" + ) + + assert tester.io.fetch_error() == expected + + def test_check_invalid( mocker: MockerFixture, tester: CommandTester, fixture_dir: FixtureDirGetter ) -> None: @@ -71,10 +122,7 @@ def test_check_invalid( tester.execute("--lock") - fastjsonschema_error = "data must contain ['description'] properties" - custom_error = "The fields ['description'] are required in package mode." - expected_template = """\ -Error: {schema_error} + expected = """\ Error: Project name (invalid) is same as one of its dependencies Error: Unrecognized classifiers: ['Intended Audience :: Clowns']. Error: Declared README file does not exist: never/exists.md @@ -91,12 +139,8 @@ def test_check_invalid( 'Topic :: Communications :: Chat :: AOL Instant Messenger'.\ Must be removed. """ - expected = { - expected_template.format(schema_error=schema_error) - for schema_error in (fastjsonschema_error, custom_error) - } - assert tester.io.fetch_error() in expected + assert tester.io.fetch_error() == expected def test_check_private( diff --git a/tests/console/commands/test_config.py b/tests/console/commands/test_config.py index ec88be4510d..55d42daaba6 100644 --- a/tests/console/commands/test_config.py +++ b/tests/console/commands/test_config.py @@ -8,7 +8,7 @@ import pytest from deepdiff import DeepDiff -from poetry.core.pyproject.exceptions import PyProjectException +from poetry.core.pyproject.exceptions import PyProjectError from poetry.config.config_source import ConfigSource from poetry.console.commands.install import InstallCommand @@ -39,7 +39,7 @@ def test_show_config_with_local_config_file_empty( ) -> None: mocker.patch( "poetry.factory.Factory.create_poetry", - side_effect=PyProjectException("[tool.poetry] section not found"), + side_effect=PyProjectError("[tool.poetry] section not found"), ) tester.execute() @@ -66,12 +66,10 @@ def test_list_displays_default_value_if_not_set( 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}}" -warnings.export = true """ assert tester.io.fetch_output() == expected @@ -99,12 +97,10 @@ def test_list_displays_set_get_setting( 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}}" -warnings.export = true """ assert config.set_config_source.call_count == 0 # type: ignore[attr-defined] @@ -153,12 +149,10 @@ def test_unset_setting( 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}}" -warnings.export = true """ assert config.set_config_source.call_count == 0 # type: ignore[attr-defined] assert tester.io.fetch_output() == expected @@ -185,12 +179,10 @@ def test_unset_repo_setting( 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}}" -warnings.export = true """ assert config.set_config_source.call_count == 0 # type: ignore[attr-defined] assert tester.io.fetch_output() == expected @@ -315,12 +307,10 @@ def test_list_displays_set_get_local_setting( 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}}" -warnings.export = true """ assert config.set_config_source.call_count == 1 # type: ignore[attr-defined] @@ -333,7 +323,7 @@ def test_list_must_not_display_sources_from_pyproject_toml( command_tester_factory: CommandTesterFactory, config_cache_dir: Path, ) -> None: - source = fixture_dir("with_non_default_source_implicit") + source = fixture_dir("with_primary_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) @@ -356,12 +346,10 @@ def test_list_must_not_display_sources_from_pyproject_toml( 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}}" -warnings.export = true """ assert tester.io.fetch_output() == expected diff --git a/tests/console/commands/test_export.py b/tests/console/commands/test_export.py deleted file mode 100644 index 962c2c8c151..00000000000 --- a/tests/console/commands/test_export.py +++ /dev/null @@ -1,31 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -import pytest - - -if TYPE_CHECKING: - from cleo.testers.command_tester import CommandTester - - from tests.conftest import Config - from tests.types import CommandTesterFactory - - -@pytest.fixture -def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: - return command_tester_factory("export") - - -def test_export_prints_warning(tester: CommandTester) -> None: - tester.execute("") - assert ( - "Warning: poetry-plugin-export will not be installed by default" - in tester.io.fetch_error() - ) - - -def test_disable_export_warning(tester: CommandTester, config: Config) -> None: - config.config["warnings"]["export"] = False - tester.execute("") - assert "poetry-plugin-export" not in tester.io.fetch_error() diff --git a/tests/console/commands/test_init.py b/tests/console/commands/test_init.py index 80aee46e048..2393c425f88 100644 --- a/tests/console/commands/test_init.py +++ b/tests/console/commands/test_init.py @@ -24,8 +24,8 @@ if TYPE_CHECKING: from collections.abc import Iterator - from _pytest.fixtures import FixtureRequest from poetry.core.packages.package import Package + from pytest import FixtureRequest from pytest_mock import MockerFixture from poetry.config.config import Config @@ -93,7 +93,7 @@ def test_noninteractive( toml_content = (tmp_path / "pyproject.toml").read_text(encoding="utf-8") assert 'name = "my-package"' in toml_content - assert 'pytest = "^3.6.0"' in toml_content + assert '"pytest (>=3.6.0,<4.0.0)"' in toml_content def test_interactive_with_dependencies( @@ -110,7 +110,7 @@ def test_interactive_with_dependencies( "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "", # Interactive packages "pendulu", # Search for package "1", # Second option is pendulum @@ -129,18 +129,22 @@ def test_interactive_with_dependencies( tester.execute(inputs="\n".join(inputs)) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" +requires-python = ">=3.6" +dependencies = [ + "pendulum (>=2.0.0,<3.0.0)", + "flask (>=2.0.0,<3.0.0)" +] -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" -pendulum = "^2.0.0" -flask = "^2.0.0" +[tool.poetry] [tool.poetry.group.dev.dependencies] pytest = "^3.6.0" @@ -163,7 +167,7 @@ def test_interactive_with_dependencies_and_no_selection( "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "", # Interactive packages "pendulu", # Search for package "", # Do not select an option @@ -177,16 +181,16 @@ def test_interactive_with_dependencies_and_no_selection( ] tester.execute(inputs="\n".join(inputs)) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" - -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" +requires-python = ">=3.6" """ assert expected in tester.io.fetch_output() @@ -208,15 +212,15 @@ def test_empty_license(tester: CommandTester) -> None: python = ".".join(str(c) for c in sys.version_info[:2]) expected = f"""\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "" -authors = ["Your Name "] +authors = [ + {{name = "Your Name",email = "you@example.com"}} +] readme = "README.md" - -[tool.poetry.dependencies] -python = ">={python}" +requires-python = ">={python}" """ assert expected in tester.io.fetch_output() @@ -233,7 +237,7 @@ def test_interactive_with_git_dependencies( "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "", # Interactive packages "git+https://github.com/demo/demo.git", # Search for package "", # Stop searching for packages @@ -247,17 +251,21 @@ def test_interactive_with_git_dependencies( tester.execute(inputs="\n".join(inputs)) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" +requires-python = ">=3.6" +dependencies = [ + "demo @ git+https://github.com/demo/demo.git" +] -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" -demo = {git = "https://github.com/demo/demo.git"} +[tool.poetry] [tool.poetry.group.dev.dependencies] pytest = "^3.6.0" @@ -325,7 +333,7 @@ def test_interactive_with_git_dependencies_with_reference( "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "", # Interactive packages "git+https://github.com/demo/demo.git@develop", # Search for package "", # Stop searching for packages @@ -339,17 +347,21 @@ def test_interactive_with_git_dependencies_with_reference( tester.execute(inputs="\n".join(inputs)) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" +requires-python = ">=3.6" +dependencies = [ + "demo @ git+https://github.com/demo/demo.git@develop" +] -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" -demo = {git = "https://github.com/demo/demo.git", rev = "develop"} +[tool.poetry] [tool.poetry.group.dev.dependencies] pytest = "^3.6.0" @@ -370,7 +382,7 @@ def test_interactive_with_git_dependencies_and_other_name( "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "", # Interactive packages "git+https://github.com/demo/pyproject-demo.git", # Search for package "", # Stop searching for packages @@ -384,17 +396,21 @@ def test_interactive_with_git_dependencies_and_other_name( tester.execute(inputs="\n".join(inputs)) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" +requires-python = ">=3.6" +dependencies = [ + "demo @ git+https://github.com/demo/pyproject-demo.git" +] -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" -demo = {git = "https://github.com/demo/pyproject-demo.git"} +[tool.poetry] [tool.poetry.group.dev.dependencies] pytest = "^3.6.0" @@ -421,7 +437,7 @@ def test_interactive_with_directory_dependency( "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "", # Interactive packages "./demo", # Search for package "", # Stop searching for packages @@ -434,18 +450,23 @@ def test_interactive_with_directory_dependency( ] tester.execute(inputs="\n".join(inputs)) - expected = """\ -[tool.poetry] + demo_uri = (Path.cwd() / "demo").as_uri() + expected = f"""\ +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {{name = "Your Name",email = "you@example.com"}} +] +license = {{text = "MIT"}} readme = "README.md" +requires-python = ">=3.6" +dependencies = [ + "demo @ {demo_uri}" +] -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" -demo = {path = "demo"} +[tool.poetry] [tool.poetry.group.dev.dependencies] pytest = "^3.6.0" @@ -471,7 +492,7 @@ def test_interactive_with_directory_dependency_and_other_name( "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "", # Interactive packages "./pyproject-demo", # Search for package "", # Stop searching for packages @@ -484,18 +505,23 @@ def test_interactive_with_directory_dependency_and_other_name( ] tester.execute(inputs="\n".join(inputs)) - expected = """\ -[tool.poetry] + demo_uri = (Path.cwd() / "pyproject-demo").as_uri() + expected = f"""\ +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {{name = "Your Name",email = "you@example.com"}} +] +license = {{text = "MIT"}} readme = "README.md" +requires-python = ">=3.6" +dependencies = [ + "demo @ {demo_uri}" +] -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" -demo = {path = "pyproject-demo"} +[tool.poetry] [tool.poetry.group.dev.dependencies] pytest = "^3.6.0" @@ -522,7 +548,7 @@ def test_interactive_with_file_dependency( "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "", # Interactive packages "./demo-0.1.0-py2.py3-none-any.whl", # Search for package "", # Stop searching for packages @@ -535,18 +561,23 @@ def test_interactive_with_file_dependency( ] tester.execute(inputs="\n".join(inputs)) - expected = """\ -[tool.poetry] + demo_uri = (Path.cwd() / "demo-0.1.0-py2.py3-none-any.whl").as_uri() + expected = f"""\ +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {{name = "Your Name",email = "you@example.com"}} +] +license = {{text = "MIT"}} readme = "README.md" +requires-python = ">=3.6" +dependencies = [ + "demo @ {demo_uri}" +] -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" -demo = {path = "demo-0.1.0-py2.py3-none-any.whl"} +[tool.poetry] [tool.poetry.group.dev.dependencies] pytest = "^3.6.0" @@ -564,7 +595,7 @@ def test_interactive_with_wrong_dependency_inputs( "This is a description", # Description "n", # Author "MIT", # License - "^3.8", # Python + ">=3.8", # Python "", # Interactive packages "foo 1.19.2", "pendulum 2.0.0 foo", # Package name and constraint (invalid) @@ -580,18 +611,22 @@ def test_interactive_with_wrong_dependency_inputs( tester.execute(inputs="\n".join(inputs)) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" +requires-python = ">=3.8" +dependencies = [ + "foo (==1.19.2)", + "pendulum (>=2.0.0,<3.0.0)" +] -[tool.poetry.dependencies] -python = "^3.8" -foo = "1.19.2" -pendulum = "^2.0.0" +[tool.poetry] [tool.poetry.group.dev.dependencies] pytest = "3.6.0" @@ -611,19 +646,19 @@ def test_python_option(tester: CommandTester) -> None: "n", # Interactive dev packages "\n", # Generate ] - tester.execute("--python '~2.7 || ^3.6'", inputs="\n".join(inputs)) + tester.execute("--python '>=3.6'", inputs="\n".join(inputs)) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" - -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" +requires-python = ">=3.6" """ assert expected in tester.io.fetch_output() @@ -638,7 +673,7 @@ def test_predefined_dependency(tester: CommandTester, repo: TestRepository) -> N "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "n", # Interactive packages "n", # Interactive dev packages "\n", # Generate @@ -646,17 +681,19 @@ def test_predefined_dependency(tester: CommandTester, repo: TestRepository) -> N tester.execute("--dependency pendulum", inputs="\n".join(inputs)) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" - -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" -pendulum = "^2.0.0" +requires-python = ">=3.6" +dependencies = [ + "pendulum (>=2.0.0,<3.0.0)" +] """ assert expected in tester.io.fetch_output() @@ -674,7 +711,7 @@ def test_predefined_and_interactive_dependencies( "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "", # Interactive packages "pyramid", # Search for package "0", # First option @@ -687,21 +724,22 @@ def test_predefined_and_interactive_dependencies( tester.execute("--dependency pendulum", inputs="\n".join(inputs)) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" - -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" +requires-python = ">=3.6" +dependencies = [ + "pendulum (>=2.0.0,<3.0.0)", + "pyramid (>=1.10,<2.0)" +] """ - output = tester.io.fetch_output() - assert expected in output - assert 'pendulum = "^2.0.0"' in output - assert 'pyramid = "^1.10"' in output + assert expected in tester.io.fetch_output() def test_predefined_dev_dependency(tester: CommandTester, repo: TestRepository) -> None: @@ -713,7 +751,7 @@ def test_predefined_dev_dependency(tester: CommandTester, repo: TestRepository) "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "n", # Interactive packages "n", # Interactive dev packages "\n", # Generate @@ -722,16 +760,20 @@ def test_predefined_dev_dependency(tester: CommandTester, repo: TestRepository) tester.execute("--dev-dependency pytest", inputs="\n".join(inputs)) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" +requires-python = ">=3.6" +dependencies = [ +] -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" +[tool.poetry] [tool.poetry.group.dev.dependencies] pytest = "^3.6.0" @@ -752,7 +794,7 @@ def test_predefined_and_interactive_dev_dependencies( "This is a description", # Description "n", # Author "MIT", # License - "~2.7 || ^3.6", # Python + ">=3.6", # Python "n", # Interactive packages "", # Interactive dev packages "pytest-requests", # Search for package @@ -765,16 +807,20 @@ def test_predefined_and_interactive_dev_dependencies( tester.execute("--dev-dependency pytest", inputs="\n".join(inputs)) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Your Name "] -license = "MIT" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +license = {text = "MIT"} readme = "README.md" +requires-python = ">=3.6" +dependencies = [ +] -[tool.poetry.dependencies] -python = "~2.7 || ^3.6" +[tool.poetry] [tool.poetry.group.dev.dependencies] pytest = "^3.6.0" @@ -803,7 +849,7 @@ def test_predefined_all_options(tester: CommandTester, repo: TestRepository) -> "--name my-package " "--description 'This is a description' " "--author 'Foo Bar ' " - "--python '^3.8' " + "--python '>=3.8' " "--license MIT " "--dependency pendulum " "--dev-dependency pytest", @@ -811,17 +857,21 @@ def test_predefined_all_options(tester: CommandTester, repo: TestRepository) -> ) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "1.2.3" description = "This is a description" -authors = ["Foo Bar "] -license = "MIT" +authors = [ + {name = "Foo Bar",email = "foo@example.com"} +] +license = {text = "MIT"} readme = "README.md" +requires-python = ">=3.8" +dependencies = [ + "pendulum (>=2.0.0,<3.0.0)" +] -[tool.poetry.dependencies] -python = "^3.8" -pendulum = "^2.0.0" +[tool.poetry] [tool.poetry.group.dev.dependencies] pytest = "^3.6.0" @@ -900,22 +950,24 @@ def test_init_non_interactive_existing_pyproject_add_dependency( tester.execute( "--author 'Your Name ' " "--name 'my-package' " - "--python '^3.6' " + "--python '>=3.6' " "--dependency foo", interactive=False, ) expected = """\ -[tool.poetry] +[project] name = "my-package" version = "0.1.0" description = "" -authors = ["Your Name "] +authors = [ + {name = "Your Name",email = "you@example.com"} +] readme = "README.md" - -[tool.poetry.dependencies] -python = "^3.6" -foo = "^1.19.2" +requires-python = ">=3.6" +dependencies = [ + "foo (>=1.19.2,<2.0.0)" +] """ assert f"{existing_section}\n{expected}" in pyproject_file.read_text( encoding="utf-8" @@ -1005,7 +1057,7 @@ def test_package_include( "", # Description "poetry", # Author "", # License - "^3.10", # Python + ">=3.10", # Python "n", # Interactive packages "n", # Interactive dev packages "\n", # Generate @@ -1015,19 +1067,21 @@ def test_package_include( packages = "" if include and module_name(package_name) != include: - packages = f'packages = [{{include = "{include}"}}]\n' + packages = f'\n[tool.poetry]\npackages = [{{include = "{include}"}}]\n' expected = ( - f"[tool.poetry]\n" + "[project]\n" f'name = "{package_name.replace(".", "-")}"\n' # canonicalized - f'version = "0.1.0"\n' - f'description = ""\n' - f'authors = ["poetry"]\n' - f'readme = "README.md"\n' - f"{packages}" # This line is optional. Thus no newline here. - f"\n" - f"[tool.poetry.dependencies]\n" - f'python = "^3.10"\n' + 'version = "0.1.0"\n' + 'description = ""\n' + 'authors = [\n' + ' {name = "poetry"}\n' + ']\n' + 'readme = "README.md"\n' + 'requires-python = ">=3.10"\n' + 'dependencies = [\n' + ']\n' + f"{packages}" # This line is optional. Thus, no newline here. ) assert expected in tester.io.fetch_output() @@ -1059,7 +1113,10 @@ def mock_check_output(cmd: str, *_: Any, **__: Any) -> str: return result mocker.patch("subprocess.check_output", side_effect=mock_check_output) - + mocker.patch( + "poetry.utils.env.python_manager.Python._full_python_path", + return_value=Path(f"/usr/bin/python{python}"), + ) config.config["virtualenvs"]["prefer-active-python"] = prefer_active pyproject_file = source_dir / "pyproject.toml" @@ -1069,8 +1126,7 @@ def mock_check_output(cmd: str, *_: Any, **__: Any) -> str: ) expected = f"""\ -[tool.poetry.dependencies] -python = ">={python}" +requires-python = ">={python}" """ assert expected in pyproject_file.read_text(encoding="utf-8") diff --git a/tests/console/commands/test_install.py b/tests/console/commands/test_install.py index 2692fa59f88..ae163df3ec2 100644 --- a/tests/console/commands/test_install.py +++ b/tests/console/commands/test_install.py @@ -6,11 +6,11 @@ import pytest -from poetry.core.masonry.utils.module import ModuleOrPackageNotFound +from poetry.core.masonry.utils.module import ModuleOrPackageNotFoundError from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.console.commands.installer_command import InstallerCommand -from poetry.console.exceptions import GroupNotFound +from poetry.console.exceptions import GroupNotFoundError from tests.helpers import TestLocker @@ -104,13 +104,12 @@ def _project_factory( ("--without foo,bar", {MAIN_GROUP, "baz", "bim"}), (f"--without {MAIN_GROUP}", {"foo", "bar", "baz", "bim"}), ("--with foo,bar --without baz --without bim --only bam", {"bam"}), + ("--all-groups", {MAIN_GROUP, "foo", "bar", "baz", "bim", "bam"}), # net result zero options ("--with foo", {MAIN_GROUP, "foo", "bar", "baz", "bim"}), ("--without bam", {MAIN_GROUP, "foo", "bar", "baz", "bim"}), ("--with bam --without bam", {MAIN_GROUP, "foo", "bar", "baz", "bim"}), ("--with foo --without foo", {MAIN_GROUP, "bar", "baz", "bim"}), - # deprecated options - ("--no-dev", {MAIN_GROUP}), ], ) @pytest.mark.parametrize("with_root", [True, False]) @@ -128,7 +127,7 @@ def test_group_options_are_passed_to_the_installer( mocker.patch.object(tester.command.installer, "run", return_value=0) editable_builder_mock = mocker.patch( "poetry.masonry.builders.editable.EditableBuilder", - side_effect=ModuleOrPackageNotFound(), + side_effect=ModuleOrPackageNotFoundError(), ) if not with_root: @@ -136,7 +135,7 @@ def test_group_options_are_passed_to_the_installer( status_code = tester.execute(options) - if options == "--no-root --only-root": + if options == "--no-root --only-root" or with_root: assert status_code == 1 return else: @@ -247,6 +246,20 @@ def test_extras_are_parsed_and_populate_installer( assert tester.command.installer._extras == ["first", "second", "third"] +def test_install_ensures_project_plugins( + tester: CommandTester, mocker: MockerFixture +) -> None: + assert isinstance(tester.command, InstallerCommand) + mocker.patch.object(tester.command.installer, "run", return_value=1) + ensure_project_plugins = mocker.patch( + "poetry.plugins.plugin_manager.PluginManager.ensure_project_plugins" + ) + + tester.execute("") + + ensure_project_plugins.assert_called_once() + + def test_extras_conflicts_all_extras( tester: CommandTester, mocker: MockerFixture ) -> None: @@ -273,9 +286,10 @@ def test_extras_conflicts_all_extras( "--without foo", "--with foo,bar --without baz", "--only foo", + "--all-groups", ], ) -def test_only_root_conflicts_with_without_only( +def test_only_root_conflicts_with_without_only_all_groups( options: str, tester: CommandTester, mocker: MockerFixture, @@ -288,11 +302,37 @@ def test_only_root_conflicts_with_without_only( assert tester.status_code == 1 assert ( tester.io.fetch_error() - == "The `--with`, `--without` and `--only` options cannot be used with" + == "The `--with`, `--without`, `--only` and `--all-groups` options cannot be used with" " the `--only-root` option.\n" ) +@pytest.mark.parametrize( + "options", + [ + "--with foo", + "--without foo", + "--with foo,bar --without baz", + "--only foo", + ], +) +def test_all_groups_conflicts_with_only_with_without( + options: str, + tester: CommandTester, + mocker: MockerFixture, +) -> None: + assert isinstance(tester.command, InstallerCommand) + mocker.patch.object(tester.command.installer, "run", return_value=0) + + tester.execute(f"{options} --all-groups") + + assert tester.status_code == 1 + assert ( + tester.io.fetch_error() + == "You cannot specify `--with`, `--without`, or `--only` when using `--all-groups`.\n" + ) + + @pytest.mark.parametrize( ("options", "valid_groups", "should_raise"), [ @@ -322,9 +362,9 @@ def test_invalid_groups_with_without_only( if not should_raise: tester.execute(cmd_args) - assert tester.status_code == 0 + assert tester.status_code == 1 else: - with pytest.raises(GroupNotFound, match=r"^Group\(s\) not found:") as e: + with pytest.raises(GroupNotFoundError, match=r"^Group\(s\) not found:") as e: tester.execute(cmd_args) assert tester.status_code is None for opt, groups in options.items(): @@ -336,22 +376,6 @@ def test_invalid_groups_with_without_only( ) -def test_remove_untracked_outputs_deprecation_warning( - tester: CommandTester, - mocker: MockerFixture, -) -> None: - assert isinstance(tester.command, InstallerCommand) - mocker.patch.object(tester.command.installer, "run", return_value=0) - - tester.execute("--remove-untracked") - - assert tester.status_code == 0 - assert ( - "The `--remove-untracked` option is deprecated, use the `--sync` option" - " instead.\n" in tester.io.fetch_error() - ) - - def test_dry_run_populates_installer( tester: CommandTester, mocker: MockerFixture ) -> None: @@ -440,7 +464,11 @@ def test_install_warning_corrupt_root( tester = command_tester_factory("install", poetry=poetry) tester.execute("" if with_root else "--no-root") - assert tester.status_code == 0 + if error and with_root: + assert tester.status_code == 1 + else: + assert tester.status_code == 0 + if with_root and error: assert "The current project could not be installed: " in tester.io.fetch_error() else: diff --git a/tests/console/commands/test_lock.py b/tests/console/commands/test_lock.py index 38d0c746412..6e258e5f49e 100644 --- a/tests/console/commands/test_lock.py +++ b/tests/console/commands/test_lock.py @@ -87,55 +87,7 @@ def poetry_with_invalid_lockfile( return _project_factory("invalid_lock", project_factory, fixture_dir) -def test_lock_check_outdated_legacy( - command_tester_factory: CommandTesterFactory, - poetry_with_outdated_lockfile: Poetry, -) -> None: - locker = Locker( - lock=poetry_with_outdated_lockfile.pyproject.file.path.parent / "poetry.lock", - pyproject_data=poetry_with_outdated_lockfile.locker._pyproject_data, - ) - poetry_with_outdated_lockfile.set_locker(locker) - - tester = command_tester_factory("lock", poetry=poetry_with_outdated_lockfile) - status_code = tester.execute("--check") - expected = ( - "poetry lock --check is deprecated, use `poetry check --lock` instead.\n" - "Error: pyproject.toml changed significantly since poetry.lock was last generated. " - "Run `poetry lock [--no-update]` to fix the lock file.\n" - ) - - assert tester.io.fetch_error() == expected - - # exit with an error - assert status_code == 1 - - -def test_lock_check_up_to_date_legacy( - command_tester_factory: CommandTesterFactory, - poetry_with_up_to_date_lockfile: Poetry, -) -> None: - locker = Locker( - lock=poetry_with_up_to_date_lockfile.pyproject.file.path.parent / "poetry.lock", - pyproject_data=poetry_with_up_to_date_lockfile.locker._pyproject_data, - ) - poetry_with_up_to_date_lockfile.set_locker(locker) - - tester = command_tester_factory("lock", poetry=poetry_with_up_to_date_lockfile) - status_code = tester.execute("--check") - expected = "poetry.lock is consistent with pyproject.toml.\n" - assert tester.io.fetch_output() == expected - - expected_error = ( - "poetry lock --check is deprecated, use `poetry check --lock` instead.\n" - ) - assert tester.io.fetch_error() == expected_error - - # exit with an error - assert status_code == 0 - - -def test_lock_no_update( +def test_lock_does_not_update_if_not_necessary( command_tester_factory: CommandTesterFactory, poetry_with_old_lockfile: Poetry, repo: TestRepository, @@ -156,7 +108,7 @@ def test_lock_no_update( ) tester = command_tester_factory("lock", poetry=poetry_with_old_lockfile) - tester.execute("--no-update") + tester.execute() locker = Locker( lock=poetry_with_old_lockfile.pyproject.file.path.parent / "poetry.lock", @@ -172,10 +124,12 @@ def test_lock_no_update( assert locked_repository.find_packages(package.to_dependency()) -def test_lock_no_update_path_dependencies( +@pytest.mark.parametrize("regenerate", [True, False]) +def test_lock_always_updates_path_dependencies( command_tester_factory: CommandTesterFactory, poetry_with_nested_path_deps_old_lockfile: Poetry, repo: TestRepository, + regenerate: bool, ) -> None: """ The lock file contains a variant of the directory dependency "quix" that does @@ -195,14 +149,14 @@ def test_lock_no_update_path_dependencies( tester = command_tester_factory( "lock", poetry=poetry_with_nested_path_deps_old_lockfile ) - tester.execute("--no-update") + tester.execute("--regenerate" if regenerate else "") packages = locker.locked_repository().packages assert {p.name for p in packages} == {"quix", "sampleproject"} -@pytest.mark.parametrize("update", [True, False]) +@pytest.mark.parametrize("regenerate", [True, False]) @pytest.mark.parametrize( "project", ["missing_directory_dependency", "missing_file_dependency"] ) @@ -211,7 +165,7 @@ def test_lock_path_dependency_does_not_exist( project_factory: ProjectFactory, fixture_dir: FixtureDirGetter, project: str, - update: bool, + regenerate: bool, ) -> None: poetry = _project_factory(project, project_factory, fixture_dir) locker = Locker( @@ -219,10 +173,10 @@ def test_lock_path_dependency_does_not_exist( pyproject_data=poetry.locker._pyproject_data, ) poetry.set_locker(locker) - options = "" if update else "--no-update" + options = "--regenerate" if regenerate else "" tester = command_tester_factory("lock", poetry=poetry) - if update or "directory" in project: + if regenerate or "directory" in project: # directory dependencies are always updated with pytest.raises(ValueError, match="does not exist"): tester.execute(options) @@ -230,7 +184,7 @@ def test_lock_path_dependency_does_not_exist( tester.execute(options) -@pytest.mark.parametrize("update", [True, False]) +@pytest.mark.parametrize("regenerate", [True, False]) @pytest.mark.parametrize( "project", ["deleted_directory_dependency", "deleted_file_dependency"] ) @@ -239,7 +193,7 @@ def test_lock_path_dependency_deleted_from_pyproject( project_factory: ProjectFactory, fixture_dir: FixtureDirGetter, project: str, - update: bool, + regenerate: bool, ) -> None: poetry = _project_factory(project, project_factory, fixture_dir) locker = Locker( @@ -249,22 +203,19 @@ def test_lock_path_dependency_deleted_from_pyproject( poetry.set_locker(locker) tester = command_tester_factory("lock", poetry=poetry) - if update: - tester.execute("") - else: - tester.execute("--no-update") + tester.execute("--regenerate" if regenerate else "") packages = locker.locked_repository().packages assert {p.name for p in packages} == set() -@pytest.mark.parametrize("is_no_update", [False, True]) +@pytest.mark.parametrize("regenerate", [True, False]) def test_lock_with_incompatible_lockfile( command_tester_factory: CommandTesterFactory, poetry_with_incompatible_lockfile: Poetry, repo: TestRepository, - is_no_update: bool, + regenerate: bool, ) -> None: repo.add_package(get_package("sampleproject", "1.3.1")) @@ -276,26 +227,26 @@ def test_lock_with_incompatible_lockfile( poetry_with_incompatible_lockfile.set_locker(locker) tester = command_tester_factory("lock", poetry=poetry_with_incompatible_lockfile) - if is_no_update: + if regenerate: + # still possible because lock file is not required + status_code = tester.execute("--regenerate") + assert status_code == 0 + else: # not possible because of incompatible lock file expected = ( "(?s)lock file is not compatible .*" " regenerate the lock file with the `poetry lock` command" ) with pytest.raises(RuntimeError, match=expected): - tester.execute("--no-update") - else: - # still possible because lock file is not required - status_code = tester.execute() - assert status_code == 0 + tester.execute() -@pytest.mark.parametrize("is_no_update", [False, True]) +@pytest.mark.parametrize("regenerate", [True, False]) def test_lock_with_invalid_lockfile( command_tester_factory: CommandTesterFactory, poetry_with_invalid_lockfile: Poetry, repo: TestRepository, - is_no_update: bool, + regenerate: bool, ) -> None: repo.add_package(get_package("sampleproject", "1.3.1")) @@ -306,11 +257,11 @@ def test_lock_with_invalid_lockfile( poetry_with_invalid_lockfile.set_locker(locker) tester = command_tester_factory("lock", poetry=poetry_with_invalid_lockfile) - if is_no_update: - # not possible because of broken lock file - with pytest.raises(RuntimeError, match="Unable to read the lock file"): - tester.execute("--no-update") - else: + if regenerate: # still possible because lock file is not required - status_code = tester.execute() + status_code = tester.execute("--regenerate") assert status_code == 0 + else: + # not possible because of broken lock file + with pytest.raises(RuntimeError, match="Unable to read the lock file"): + tester.execute() diff --git a/tests/console/commands/test_new.py b/tests/console/commands/test_new.py index dba738f4e8c..b9b4579ba7e 100644 --- a/tests/console/commands/test_new.py +++ b/tests/console/commands/test_new.py @@ -57,7 +57,7 @@ def verify_project_directory( else: package_include = {"include": package_path.parts[0]} - name = poetry.local_config.get("name", "") + name = poetry.package.name packages = poetry.local_config.get("packages") if not packages: @@ -183,7 +183,9 @@ def test_command_new_with_readme( tester.execute(" ".join(options)) poetry = verify_project_directory(path, package, package, None) - assert poetry.local_config.get("readme") == f"README.{fmt or 'md'}" + project_section = poetry.pyproject.data["project"] + assert isinstance(project_section, dict) + assert project_section["readme"] == f"README.{fmt or 'md'}" @pytest.mark.parametrize( @@ -213,6 +215,10 @@ def mock_check_output(cmd: str, *_: Any, **__: Any) -> str: return output mocker.patch("subprocess.check_output", side_effect=mock_check_output) + mocker.patch( + "poetry.utils.env.python_manager.Python._full_python_path", + return_value=Path(f"/usr/bin/python{python}"), + ) config.config["virtualenvs"]["prefer-active-python"] = prefer_active @@ -224,8 +230,7 @@ def mock_check_output(cmd: str, *_: Any, **__: Any) -> str: pyproject_file = path / "pyproject.toml" expected = f"""\ -[tool.poetry.dependencies] -python = ">={python}" +requires-python = ">={python}" """ assert expected in pyproject_file.read_text(encoding="utf-8") diff --git a/tests/console/commands/test_publish.py b/tests/console/commands/test_publish.py index 78216936409..ca841ad7392 100644 --- a/tests/console/commands/test_publish.py +++ b/tests/console/commands/test_publish.py @@ -10,7 +10,6 @@ import requests from poetry.factory import Factory -from poetry.publishing.uploader import UploadError if TYPE_CHECKING: @@ -82,9 +81,7 @@ def request_callback(*_: Any, **__: Any) -> None: assert exit_code == 1 - expected = str(UploadError(error=requests.ConnectionError())) - - assert expected in app_tester.io.fetch_error() + assert "Error connecting to repository" in app_tester.io.fetch_error() def test_publish_with_cert( diff --git a/tests/console/commands/test_remove.py b/tests/console/commands/test_remove.py index 6991045f82b..fb67774a50e 100644 --- a/tests/console/commands/test_remove.py +++ b/tests/console/commands/test_remove.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING from typing import Any +from typing import Callable from typing import cast import pytest @@ -31,18 +32,21 @@ @pytest.fixture def poetry_with_up_to_date_lockfile( project_factory: ProjectFactory, fixture_dir: FixtureDirGetter -) -> Poetry: - source = fixture_dir("up_to_date_lock") +) -> Callable[[str], Poetry]: + def get_poetry(fixture_name: str) -> Poetry: + source = fixture_dir(fixture_name) - poetry = project_factory( - name="foobar", - pyproject_content=(source / "pyproject.toml").read_text(encoding="utf-8"), - poetry_lock_content=(source / "poetry.lock").read_text(encoding="utf-8"), - ) + poetry = project_factory( + name="foobar", + pyproject_content=(source / "pyproject.toml").read_text(encoding="utf-8"), + poetry_lock_content=(source / "poetry.lock").read_text(encoding="utf-8"), + ) + + assert isinstance(poetry.locker, TestLocker) + poetry.locker.locked(True) + return poetry - assert isinstance(poetry.locker, TestLocker) - poetry.locker.locked(True) - return poetry + return get_poetry @pytest.fixture() @@ -50,6 +54,81 @@ def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("remove") +def test_remove_from_project_and_poetry( + tester: CommandTester, + app: PoetryTestApplication, + repo: TestRepository, + installed: Repository, +) -> None: + repo.add_package(Package("foo", "2.0.0")) + repo.add_package(Package("bar", "1.0.0")) + + pyproject: dict[str, Any] = app.poetry.file.read() + + project_dependencies: dict[str, Any] = tomlkit.parse( + """\ +[project] +dependencies = [ + "foo>=2.0", + "bar>=1.0", +] +""" + ) + + poetry_dependencies: dict[str, Any] = tomlkit.parse( + """\ +[tool.poetry.dependencies] +foo = "^2.0.0" +bar = "^1.0.0" + +""" + ) + + pyproject["project"]["dependencies"] = project_dependencies["project"][ + "dependencies" + ] + pyproject["tool"]["poetry"]["dependencies"] = poetry_dependencies["tool"]["poetry"][ + "dependencies" + ] + pyproject = cast("TOMLDocument", pyproject) + app.poetry.file.write(pyproject) + + app.poetry.package.add_dependency(Factory.create_dependency("foo", "^2.0.0")) + app.poetry.package.add_dependency(Factory.create_dependency("bar", "^1.0.0")) + + tester.execute("foo") + + pyproject = app.poetry.file.read() + pyproject = cast("dict[str, Any]", pyproject) + project_dependencies = pyproject["project"]["dependencies"] + assert "foo>=2.0" not in project_dependencies + assert "bar>=1.0" in project_dependencies + poetry_dependencies = pyproject["tool"]["poetry"]["dependencies"] + assert "foo" not in poetry_dependencies + assert "bar" in poetry_dependencies + + expected_project_string = """\ +dependencies = [ + "bar>=1.0", +] +""" + expected_poetry_string = """\ + +[tool.poetry.dependencies] +bar = "^1.0.0" + +""" + pyproject = cast("TOMLDocument", pyproject) + string_content = pyproject.as_string() + if "\r\n" in string_content: + # consistent line endings + expected_project_string = expected_project_string.replace("\n", "\r\n") + expected_poetry_string = expected_poetry_string.replace("\n", "\r\n") + + assert expected_project_string in string_content + assert expected_poetry_string in string_content + + def test_remove_without_specific_group_removes_from_all_groups( tester: CommandTester, app: PoetryTestApplication, @@ -110,7 +189,7 @@ def test_remove_without_specific_group_removes_from_all_groups( assert expected in string_content -def test_remove_without_specific_group_removes_from_specific_groups( +def test_remove_with_specific_group_removes_from_specific_groups( tester: CommandTester, app: PoetryTestApplication, repo: TestRepository, @@ -169,7 +248,7 @@ def test_remove_without_specific_group_removes_from_specific_groups( assert expected in string_content -def test_remove_does_not_live_empty_groups( +def test_remove_does_not_keep_empty_groups( tester: CommandTester, app: PoetryTestApplication, repo: TestRepository, @@ -299,33 +378,41 @@ def test_remove_command_should_not_write_changes_upon_installer_errors( assert app.poetry.file.read().as_string() == original_content +@pytest.mark.parametrize( + "fixture_name", ["up_to_date_lock", "up_to_date_lock_non_package"] +) def test_remove_with_dry_run_keep_files_intact( - poetry_with_up_to_date_lockfile: Poetry, + fixture_name: str, + poetry_with_up_to_date_lockfile: Callable[[str], Poetry], repo: TestRepository, command_tester_factory: CommandTesterFactory, ) -> None: - tester = command_tester_factory("remove", poetry=poetry_with_up_to_date_lockfile) + poetry = poetry_with_up_to_date_lockfile(fixture_name) + tester = command_tester_factory("remove", poetry=poetry) - original_pyproject_content = poetry_with_up_to_date_lockfile.file.read() - original_lockfile_content = poetry_with_up_to_date_lockfile._locker.lock_data + original_pyproject_content = poetry.file.read() + original_lockfile_content = poetry._locker.lock_data repo.add_package(get_package("docker", "4.3.1")) tester.execute("docker --dry-run") - 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 poetry.file.read() == original_pyproject_content + assert poetry._locker.lock_data == original_lockfile_content +@pytest.mark.parametrize( + "fixture_name", ["up_to_date_lock", "up_to_date_lock_non_package"] +) def test_remove_performs_uninstall_op( - poetry_with_up_to_date_lockfile: Poetry, + fixture_name: str, + poetry_with_up_to_date_lockfile: Callable[[str], Poetry], command_tester_factory: CommandTesterFactory, installed: Repository, ) -> None: installed.add_package(get_package("docker", "4.3.1")) - tester = command_tester_factory("remove", poetry=poetry_with_up_to_date_lockfile) + poetry = poetry_with_up_to_date_lockfile(fixture_name) + tester = command_tester_factory("remove", poetry=poetry) tester.execute("docker") @@ -343,13 +430,18 @@ def test_remove_performs_uninstall_op( assert tester.io.fetch_output() == expected +@pytest.mark.parametrize( + "fixture_name", ["up_to_date_lock", "up_to_date_lock_non_package"] +) def test_remove_with_lock_does_not_perform_uninstall_op( - poetry_with_up_to_date_lockfile: Poetry, + fixture_name: str, + poetry_with_up_to_date_lockfile: Callable[[str], Poetry], command_tester_factory: CommandTesterFactory, installed: Repository, ) -> None: installed.add_package(get_package("docker", "4.3.1")) - tester = command_tester_factory("remove", poetry=poetry_with_up_to_date_lockfile) + poetry = poetry_with_up_to_date_lockfile(fixture_name) + tester = command_tester_factory("remove", poetry=poetry) tester.execute("docker --lock") diff --git a/tests/console/commands/test_show.py b/tests/console/commands/test_show.py index 75bb7792349..22594af6e87 100644 --- a/tests/console/commands/test_show.py +++ b/tests/console/commands/test_show.py @@ -203,12 +203,6 @@ def _configure_project_with_groups(poetry: Poetry, installed: Repository) -> Non f"--only {MAIN_GROUP}", """\ cachy 0.1.0 Cachy package -""", - ), - ( - "--no-dev", - """\ -cachy 0.1.0 Cachy package """, ), ( @@ -1981,7 +1975,7 @@ def test_show_required_by_deps( - msgpack-python >=0.5 <0.6 required by - - pendulum >=0.2.0 <0.3.0 + - pendulum requires >=0.2.0 <0.3.0 """.splitlines() actual = [line.rstrip() for line in tester.io.fetch_output().splitlines()] assert actual == expected diff --git a/tests/console/test_application.py b/tests/console/test_application.py index 4629d87267f..8fbc6d7dc67 100644 --- a/tests/console/test_application.py +++ b/tests/console/test_application.py @@ -1,6 +1,7 @@ from __future__ import annotations import re +import shutil from typing import TYPE_CHECKING from typing import ClassVar @@ -12,14 +13,20 @@ from poetry.console.application import Application from poetry.console.commands.command import Command from poetry.plugins.application_plugin import ApplicationPlugin +from poetry.plugins.plugin_manager import ProjectPluginCache from poetry.repositories.cached_repository import CachedRepository from poetry.utils.authenticator import Authenticator +from poetry.utils.env import EnvManager +from poetry.utils.env import MockEnv from tests.helpers import mock_metadata_entry_points if TYPE_CHECKING: + from pathlib import Path + from pytest_mock import MockerFixture + from tests.types import FixtureDirGetter from tests.types import SetProjectContext @@ -86,6 +93,43 @@ def test_application_execute_plugin_command_with_plugins_disabled( assert tester.status_code == 1 +@pytest.mark.parametrize("with_project_plugins", [False, True]) +@pytest.mark.parametrize("no_plugins", [False, True]) +def test_application_project_plugins( + fixture_dir: FixtureDirGetter, + tmp_path: Path, + no_plugins: bool, + with_project_plugins: bool, + mocker: MockerFixture, + set_project_context: SetProjectContext, +) -> None: + env = MockEnv( + path=tmp_path / "env", version_info=(3, 8, 0), sys_path=[str(tmp_path / "env")] + ) + mocker.patch.object(EnvManager, "get_system_env", return_value=env) + + orig_dir = fixture_dir("project_plugins") + project_path = tmp_path / "project" + project_path.mkdir() + shutil.copy(orig_dir / "pyproject.toml", project_path / "pyproject.toml") + project_plugin_path = project_path / ProjectPluginCache.PATH + if with_project_plugins: + project_plugin_path.mkdir(parents=True) + + with set_project_context(project_path, in_place=True): + app = Application() + + tester = ApplicationTester(app) + tester.execute("--no-plugins" if no_plugins else "") + + assert tester.status_code == 0 + sys_path = EnvManager.get_system_env(naive=True).sys_path + if with_project_plugins and not no_plugins: + assert sys_path[0] == str(project_plugin_path) + else: + assert sys_path[0] != str(project_plugin_path) + + @pytest.mark.parametrize("disable_cache", [True, False]) def test_application_verify_source_cache_flag( disable_cache: bool, set_project_context: SetProjectContext diff --git a/tests/fixtures/private_pyproject/README.md b/tests/fixtures/git/github.com/forked_demo/subdirectories/one-copy/one/__init__.py similarity index 100% rename from tests/fixtures/private_pyproject/README.md rename to tests/fixtures/git/github.com/forked_demo/subdirectories/one-copy/one/__init__.py diff --git a/tests/fixtures/git/github.com/forked_demo/subdirectories/one-copy/pyproject.toml b/tests/fixtures/git/github.com/forked_demo/subdirectories/one-copy/pyproject.toml new file mode 100644 index 00000000000..1548c3a33a1 --- /dev/null +++ b/tests/fixtures/git/github.com/forked_demo/subdirectories/one-copy/pyproject.toml @@ -0,0 +1,13 @@ +[tool.poetry] +name = "one" +version = "1.0.0" +description = "Some description." +authors = [] +license = "MIT" + +[tool.poetry.dependencies] +python = "^3.7" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/git/github.com/forked_demo/subdirectories/one/one/__init__.py b/tests/fixtures/git/github.com/forked_demo/subdirectories/one/one/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/git/github.com/forked_demo/subdirectories/one/pyproject.toml b/tests/fixtures/git/github.com/forked_demo/subdirectories/one/pyproject.toml new file mode 100644 index 00000000000..1548c3a33a1 --- /dev/null +++ b/tests/fixtures/git/github.com/forked_demo/subdirectories/one/pyproject.toml @@ -0,0 +1,13 @@ +[tool.poetry] +name = "one" +version = "1.0.0" +description = "Some description." +authors = [] +license = "MIT" + +[tool.poetry.dependencies] +python = "^3.7" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/git/github.com/forked_demo/subdirectories/two/pyproject.toml b/tests/fixtures/git/github.com/forked_demo/subdirectories/two/pyproject.toml new file mode 100644 index 00000000000..6a54d8938ff --- /dev/null +++ b/tests/fixtures/git/github.com/forked_demo/subdirectories/two/pyproject.toml @@ -0,0 +1,13 @@ +[tool.poetry] +name = "two" +version = "2.0.0" +description = "Some description." +authors = [] +license = "MIT" + +[tool.poetry.dependencies] +python = "~2.7 || ^3.4" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/git/github.com/forked_demo/subdirectories/two/two/__init__.py b/tests/fixtures/git/github.com/forked_demo/subdirectories/two/two/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/invalid_pyproject/pyproject.toml b/tests/fixtures/invalid_pyproject/pyproject.toml index 94c7d9fb4d5..b830211ac6e 100644 --- a/tests/fixtures/invalid_pyproject/pyproject.toml +++ b/tests/fixtures/invalid_pyproject/pyproject.toml @@ -1,17 +1,17 @@ -[tool.poetry] +[project] name = "invalid" version = "1.0.0" -authors = [ - "Foo " -] -readme = "never/exists.md" -license = "INVALID" +license = { text = "INVALID" } classifiers = [ "Environment :: Console", "Intended Audience :: Clowns", "Natural Language :: Ukranian", "Topic :: Communications :: Chat :: AOL Instant Messenger", ] +dynamic = [ "readme", "dependencies", "requires-python" ] + +[tool.poetry] +readme = "never/exists.md" [tool.poetry.dependencies] python = "*" diff --git a/tests/fixtures/no_name_project/pyproject.toml b/tests/fixtures/no_name_project/pyproject.toml index f18fa403c06..10d8f3f3f3d 100644 --- a/tests/fixtures/no_name_project/pyproject.toml +++ b/tests/fixtures/no_name_project/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -name = "" +package-mode = false version = "1.2.3" description = "This project has no name" authors = [ diff --git a/tests/fixtures/outdated_lock/pyproject.toml b/tests/fixtures/outdated_lock/pyproject.toml index 257fbe6ea74..79dd46973fe 100644 --- a/tests/fixtures/outdated_lock/pyproject.toml +++ b/tests/fixtures/outdated_lock/pyproject.toml @@ -1,14 +1,10 @@ -[tool.poetry] +[project] name = "foobar" version = "0.1.0" -description = "" -authors = ["Poetry Developer "] - -[tool.poetry.dependencies] -python = "^3.8" -docker = "4.3.1" - -[tool.poetry.group.dev.dependencies] +requires-python = ">=3.8,<4.0" +dependencies = [ + "docker>=4.3.1", +] [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/fixtures/private_pyproject/pyproject.toml b/tests/fixtures/private_pyproject/pyproject.toml index a572e83c8ff..f3cc460cf0e 100644 --- a/tests/fixtures/private_pyproject/pyproject.toml +++ b/tests/fixtures/private_pyproject/pyproject.toml @@ -1,17 +1,11 @@ -[tool.poetry] +[project] name = "private" version = "0.1.0" -description = "" -authors = ["Your Name "] -readme = "README.md" +requires-python = ">=3.7,<4.0" 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/project_plugins/my_application_plugin-1.0.dist-info/METADATA b/tests/fixtures/project_plugins/my_application_plugin-1.0.dist-info/METADATA new file mode 100644 index 00000000000..2ac98bcc428 --- /dev/null +++ b/tests/fixtures/project_plugins/my_application_plugin-1.0.dist-info/METADATA @@ -0,0 +1,7 @@ +Metadata-Version: 2.1 +Name: my-application-plugin +Version: 1.0 +Summary: description +Requires-Python: >=3.8,<4.0 +Requires-Dist: poetry (>=1.8.0,<3.0.0) +Requires-Dist: some-lib (>=1.7.0,<3.0.0) diff --git a/tests/fixtures/project_plugins/my_application_plugin-1.0.dist-info/entry_points.txt b/tests/fixtures/project_plugins/my_application_plugin-1.0.dist-info/entry_points.txt new file mode 100644 index 00000000000..b0dc8872d01 --- /dev/null +++ b/tests/fixtures/project_plugins/my_application_plugin-1.0.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[poetry.application.plugin] +my-command=my_application_plugin.plugins:MyApplicationPlugin diff --git a/tests/fixtures/project_plugins/my_application_plugin-2.0.dist-info/METADATA b/tests/fixtures/project_plugins/my_application_plugin-2.0.dist-info/METADATA new file mode 100644 index 00000000000..616f2f02d9f --- /dev/null +++ b/tests/fixtures/project_plugins/my_application_plugin-2.0.dist-info/METADATA @@ -0,0 +1,7 @@ +Metadata-Version: 2.1 +Name: my-application-plugin +Version: 2.0 +Summary: description +Requires-Python: >=3.8,<4.0 +Requires-Dist: poetry (>=1.8.0,<3.0.0) +Requires-Dist: some-lib (>=1.7.0,<3.0.0) diff --git a/tests/fixtures/project_plugins/my_application_plugin-2.0.dist-info/entry_points.txt b/tests/fixtures/project_plugins/my_application_plugin-2.0.dist-info/entry_points.txt new file mode 100644 index 00000000000..b0dc8872d01 --- /dev/null +++ b/tests/fixtures/project_plugins/my_application_plugin-2.0.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[poetry.application.plugin] +my-command=my_application_plugin.plugins:MyApplicationPlugin diff --git a/tests/fixtures/project_plugins/my_other_plugin-1.0.dist-info/METADATA b/tests/fixtures/project_plugins/my_other_plugin-1.0.dist-info/METADATA new file mode 100644 index 00000000000..20721382dd0 --- /dev/null +++ b/tests/fixtures/project_plugins/my_other_plugin-1.0.dist-info/METADATA @@ -0,0 +1,7 @@ +Metadata-Version: 2.1 +Name: my-other-plugin +Version: 1.0 +Summary: description +Requires-Python: >=3.8,<4.0 +Requires-Dist: poetry (>=1.8.0,<3.0.0) +Requires-Dist: some-lib (>=1.7.0,<3.0.0) diff --git a/tests/fixtures/project_plugins/my_other_plugin-1.0.dist-info/entry_points.txt b/tests/fixtures/project_plugins/my_other_plugin-1.0.dist-info/entry_points.txt new file mode 100644 index 00000000000..a37ff9af31b --- /dev/null +++ b/tests/fixtures/project_plugins/my_other_plugin-1.0.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[poetry.plugin] +other-plugin=my_application_plugin.plugins:MyOtherPlugin diff --git a/tests/fixtures/project_plugins/pyproject.toml b/tests/fixtures/project_plugins/pyproject.toml new file mode 100644 index 00000000000..9f4453335bf --- /dev/null +++ b/tests/fixtures/project_plugins/pyproject.toml @@ -0,0 +1,6 @@ +[tool.poetry] +package-mode = false + +[tool.poetry.requires-plugins] +my-application-plugin = ">=2.0" +my-other-plugin = ">=1.0" diff --git a/tests/fixtures/project_plugins/some_lib-1.0.dist-info/METADATA b/tests/fixtures/project_plugins/some_lib-1.0.dist-info/METADATA new file mode 100644 index 00000000000..f577113b83e --- /dev/null +++ b/tests/fixtures/project_plugins/some_lib-1.0.dist-info/METADATA @@ -0,0 +1,5 @@ +Metadata-Version: 2.1 +Name: some-lib +Version: 1.0 +Summary: description +Requires-Python: >=3.8,<4.0 diff --git a/tests/fixtures/project_plugins/some_lib-2.0.dist-info/METADATA b/tests/fixtures/project_plugins/some_lib-2.0.dist-info/METADATA new file mode 100644 index 00000000000..a5e948a8bca --- /dev/null +++ b/tests/fixtures/project_plugins/some_lib-2.0.dist-info/METADATA @@ -0,0 +1,5 @@ +Metadata-Version: 2.1 +Name: some-lib +Version: 2.0 +Summary: description +Requires-Python: >=3.8,<4.0 diff --git a/tests/fixtures/self_version_not_ok/pyproject.toml b/tests/fixtures/self_version_not_ok/pyproject.toml new file mode 100644 index 00000000000..19e752a0642 --- /dev/null +++ b/tests/fixtures/self_version_not_ok/pyproject.toml @@ -0,0 +1,6 @@ +[tool.poetry] +package-mode = false +requires-poetry = "<1.2" + +[tool.poetry.dependencies] +python = "^3.8" diff --git a/tests/fixtures/self_version_ok/pyproject.toml b/tests/fixtures/self_version_ok/pyproject.toml new file mode 100644 index 00000000000..9347cb92342 --- /dev/null +++ b/tests/fixtures/self_version_ok/pyproject.toml @@ -0,0 +1,6 @@ +[tool.poetry] +package-mode = false +requires-poetry = ">=1.2" + +[tool.poetry.dependencies] +python = "^3.8" diff --git a/tests/fixtures/simple_project/pyproject.toml b/tests/fixtures/simple_project/pyproject.toml index 45a61d43cad..e85c7a74a20 100644 --- a/tests/fixtures/simple_project/pyproject.toml +++ b/tests/fixtures/simple_project/pyproject.toml @@ -1,20 +1,26 @@ -[tool.poetry] +[project] name = "simple-project" version = "1.2.3" description = "Some description." authors = [ - "Sébastien Eustace " + { name = "Sébastien Eustace", email = "sebastien@eustace.io" } ] -license = "MIT" - -readme = ["README.rst"] +license = { text = "MIT" } +readme = "README.rst" +keywords = ["packaging", "dependency", "poetry"] +dynamic = [ "classifiers", "dependencies", "requires-python" ] +[project.urls] homepage = "https://python-poetry.org" repository = "https://github.com/python-poetry/poetry" documentation = "https://python-poetry.org/docs" -keywords = ["packaging", "dependency", "poetry"] +[project.scripts] +foo = "foo:bar" +baz = "bar:baz.boom.bim" +fox = "fuz.foo:bar.baz" +[tool.poetry] classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules" @@ -24,12 +30,6 @@ classifiers = [ [tool.poetry.dependencies] python = "~2.7 || ^3.4" -[tool.poetry.scripts] -foo = "foo:bar" -baz = "bar:baz.boom.bim" -fox = "fuz.foo:bar.baz" - - [build-system] requires = ["poetry-core>=1.1.0a7"] build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/with_default_source/README.rst b/tests/fixtures/simple_project_legacy/README.rst similarity index 100% rename from tests/fixtures/with_default_source/README.rst rename to tests/fixtures/simple_project_legacy/README.rst diff --git a/tests/fixtures/simple_project_legacy/pyproject.toml b/tests/fixtures/simple_project_legacy/pyproject.toml new file mode 100644 index 00000000000..45a61d43cad --- /dev/null +++ b/tests/fixtures/simple_project_legacy/pyproject.toml @@ -0,0 +1,35 @@ +[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 = "~2.7 || ^3.4" + +[tool.poetry.scripts] +foo = "foo:bar" +baz = "bar:baz.boom.bim" +fox = "fuz.foo:bar.baz" + + +[build-system] +requires = ["poetry-core>=1.1.0a7"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/simple_project_legacy/simple_project/__init__.py b/tests/fixtures/simple_project_legacy/simple_project/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/up_to_date_lock/poetry.lock b/tests/fixtures/up_to_date_lock/poetry.lock index ad184f3353c..b4dd4fd91ac 100644 --- a/tests/fixtures/up_to_date_lock/poetry.lock +++ b/tests/fixtures/up_to_date_lock/poetry.lock @@ -140,4 +140,4 @@ six = "*" [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "ff2489c48d3c858a11c1ce7463ae5dc1524d9d457826c1bf16fd687a7bc1e819" +content-hash = "8f975cfcda1d3c938f9e7013b2e24cb2e43e6a2a573f0c6867acad407b0fb0d9" diff --git a/tests/fixtures/up_to_date_lock/pyproject.toml b/tests/fixtures/up_to_date_lock/pyproject.toml index adaafb9481a..79dd46973fe 100644 --- a/tests/fixtures/up_to_date_lock/pyproject.toml +++ b/tests/fixtures/up_to_date_lock/pyproject.toml @@ -1,14 +1,10 @@ -[tool.poetry] +[project] name = "foobar" version = "0.1.0" -description = "" -authors = ["Poetry Developer "] - -[tool.poetry.dependencies] -python = "^3.8" -docker = ">=4.3.1" - -[tool.poetry.group.dev.dependencies] +requires-python = ">=3.8,<4.0" +dependencies = [ + "docker>=4.3.1", +] [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/fixtures/up_to_date_lock_non_package/poetry.lock b/tests/fixtures/up_to_date_lock_non_package/poetry.lock new file mode 100644 index 00000000000..c3b04ddf2a3 --- /dev/null +++ b/tests/fixtures/up_to_date_lock_non_package/poetry.lock @@ -0,0 +1,143 @@ +# This file is automatically @generated by Poetry 1.5.0.dev0 and should not be changed by hand. + +[[package]] +name = "certifi" +version = "2020.12.5" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = "*" +files = [ + {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, + {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, +] + +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +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 = "docker" +version = "4.3.1" +description = "A Python library for the Docker Engine API." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "docker-4.3.1-py2.py3-none-any.whl", hash = "sha256:13966471e8bc23b36bfb3a6fb4ab75043a5ef1dac86516274777576bed3b9828"}, + {file = "docker-4.3.1.tar.gz", hash = "sha256:bad94b8dd001a8a4af19ce4becc17f41b09f228173ffe6a4e0355389eef142f2"}, +] + +[package.dependencies] +pywin32 = {version = "227", markers = "sys_platform == \"win32\""} +requests = ">=2.14.2,<2.18.0 || >2.18.0" +six = ">=1.4.0" +websocket-client = ">=0.32.0" + +[package.extras] +ssh = ["paramiko (>=2.4.2)"] +tls = ["cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=17.5.0)"] + +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +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 = "pywin32" +version = "227" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-227-cp27-cp27m-win32.whl", hash = "sha256:371fcc39416d736401f0274dd64c2302728c9e034808e37381b5e1b22be4a6b0"}, + {file = "pywin32-227-cp27-cp27m-win_amd64.whl", hash = "sha256:4cdad3e84191194ea6d0dd1b1b9bdda574ff563177d2adf2b4efec2a244fa116"}, + {file = "pywin32-227-cp35-cp35m-win32.whl", hash = "sha256:f4c5be1a293bae0076d93c88f37ee8da68136744588bc5e2be2f299a34ceb7aa"}, + {file = "pywin32-227-cp35-cp35m-win_amd64.whl", hash = "sha256:a929a4af626e530383a579431b70e512e736e9588106715215bf685a3ea508d4"}, + {file = "pywin32-227-cp36-cp36m-win32.whl", hash = "sha256:300a2db938e98c3e7e2093e4491439e62287d0d493fe07cce110db070b54c0be"}, + {file = "pywin32-227-cp36-cp36m-win_amd64.whl", hash = "sha256:9b31e009564fb95db160f154e2aa195ed66bcc4c058ed72850d047141b36f3a2"}, + {file = "pywin32-227-cp37-cp37m-win32.whl", hash = "sha256:47a3c7551376a865dd8d095a98deba954a98f326c6fe3c72d8726ca6e6b15507"}, + {file = "pywin32-227-cp37-cp37m-win_amd64.whl", hash = "sha256:31f88a89139cb2adc40f8f0e65ee56a8c585f629974f9e07622ba80199057511"}, + {file = "pywin32-227-cp38-cp38-win32.whl", hash = "sha256:7f18199fbf29ca99dff10e1f09451582ae9e372a892ff03a28528a24d55875bc"}, + {file = "pywin32-227-cp38-cp38-win_amd64.whl", hash = "sha256:7c1ae32c489dc012930787f06244426f8356e129184a02c25aef163917ce158e"}, + {file = "pywin32-227-cp39-cp39-win32.whl", hash = "sha256:c054c52ba46e7eb6b7d7dfae4dbd987a1bb48ee86debe3f245a2884ece46e295"}, + {file = "pywin32-227-cp39-cp39-win_amd64.whl", hash = "sha256:f27cec5e7f588c3d1051651830ecc00294f90728d19c3bf6916e6dba93ea357c"}, +] + +[[package]] +name = "requests" +version = "2.25.1" +description = "Python HTTP for Humans." +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 = "six" +version = "1.15.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] + +[[package]] +name = "urllib3" +version = "1.26.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +files = [ + {file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"}, + {file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"}, +] + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "websocket-client" +version = "0.58.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "websocket_client-0.58.0-py2.py3-none-any.whl", hash = "sha256:44b5df8f08c74c3d82d28100fdc81f4536809ce98a17f0757557813275fbb663"}, + {file = "websocket_client-0.58.0.tar.gz", hash = "sha256:63509b41d158ae5b7f67eb4ad20fecbb4eee99434e73e140354dc3ff8e09716f"}, +] + +[package.dependencies] +six = "*" + +[metadata] +lock-version = "2.0" +python-versions = "^3.8" +content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" diff --git a/tests/fixtures/up_to_date_lock_non_package/pyproject.toml b/tests/fixtures/up_to_date_lock_non_package/pyproject.toml new file mode 100644 index 00000000000..6760fde5449 --- /dev/null +++ b/tests/fixtures/up_to_date_lock_non_package/pyproject.toml @@ -0,0 +1,10 @@ +[tool.poetry] +package-mode = false + +[tool.poetry.dependencies] +python = "^3.8" +docker = ">=4.3.1" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/with_default_source_and_pypi/pyproject.toml b/tests/fixtures/with_default_source_and_pypi/pyproject.toml deleted file mode 100644 index cf7ec689a8f..00000000000 --- a/tests/fixtures/with_default_source_and_pypi/pyproject.toml +++ /dev/null @@ -1,65 +0,0 @@ -[tool.poetry] -name = "with-default-source-and-pypi" -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.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 = "~3.6" } - -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.group.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/" -priority = "default" - - -[[tool.poetry.source]] -name = "PyPI" diff --git a/tests/fixtures/with_default_source_legacy/README.rst b/tests/fixtures/with_default_source_legacy/README.rst deleted file mode 100644 index f7fe15470f9..00000000000 --- a/tests/fixtures/with_default_source_legacy/README.rst +++ /dev/null @@ -1,2 +0,0 @@ -My Package -========== diff --git a/tests/fixtures/with_default_source_legacy/pyproject.toml b/tests/fixtures/with_default_source_legacy/pyproject.toml deleted file mode 100644 index b30cc53bda6..00000000000 --- a/tests/fixtures/with_default_source_legacy/pyproject.toml +++ /dev/null @@ -1,61 +0,0 @@ -[tool.poetry] -name = "default-source-legacy" -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.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 = "~3.6" } - -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.group.dev.dependencies] -pytest = "~3.4" - - -[tool.poetry.scripts] -my-script = "default_source_legacy: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_default_source_pypi/README.rst b/tests/fixtures/with_default_source_pypi/README.rst deleted file mode 100644 index f7fe15470f9..00000000000 --- a/tests/fixtures/with_default_source_pypi/README.rst +++ /dev/null @@ -1,2 +0,0 @@ -My Package -========== diff --git a/tests/fixtures/with_default_source_pypi/pyproject.toml b/tests/fixtures/with_default_source_pypi/pyproject.toml deleted file mode 100644 index c3fcb9380a6..00000000000 --- a/tests/fixtures/with_default_source_pypi/pyproject.toml +++ /dev/null @@ -1,60 +0,0 @@ -[tool.poetry] -name = "with-default-source-pypi" -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.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 = "~3.6" } - -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.group.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 = "PyPI" -priority = "default" diff --git a/tests/fixtures/with_non_default_multiple_sources_legacy/pyproject.toml b/tests/fixtures/with_multiple_sources/pyproject.toml similarity index 93% rename from tests/fixtures/with_non_default_multiple_sources_legacy/pyproject.toml rename to tests/fixtures/with_multiple_sources/pyproject.toml index 61f8e9bc59d..ecce7e62590 100644 --- a/tests/fixtures/with_non_default_multiple_sources_legacy/pyproject.toml +++ b/tests/fixtures/with_multiple_sources/pyproject.toml @@ -16,7 +16,7 @@ python = "^3.6" [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" -secondary = true +priority = "supplemental" [[tool.poetry.source]] name = "bar" diff --git a/tests/fixtures/with_non_default_multiple_sources_pypi/pyproject.toml b/tests/fixtures/with_multiple_sources_pypi/pyproject.toml similarity index 94% rename from tests/fixtures/with_non_default_multiple_sources_pypi/pyproject.toml rename to tests/fixtures/with_multiple_sources_pypi/pyproject.toml index 9e71ff764c8..1e4da135677 100644 --- a/tests/fixtures/with_non_default_multiple_sources_pypi/pyproject.toml +++ b/tests/fixtures/with_multiple_sources_pypi/pyproject.toml @@ -16,7 +16,7 @@ python = "^3.6" [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" -priority = "secondary" +priority = "supplemental" [[tool.poetry.source]] name = "bar" diff --git a/tests/fixtures/with_non_default_multiple_secondary_sources/pyproject.toml b/tests/fixtures/with_multiple_supplemental_sources/pyproject.toml similarity index 88% rename from tests/fixtures/with_non_default_multiple_secondary_sources/pyproject.toml rename to tests/fixtures/with_multiple_supplemental_sources/pyproject.toml index 517c37cc176..79f974dc780 100644 --- a/tests/fixtures/with_non_default_multiple_secondary_sources/pyproject.toml +++ b/tests/fixtures/with_multiple_supplemental_sources/pyproject.toml @@ -16,9 +16,9 @@ python = "^3.6" [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" -priority = "secondary" +priority = "supplemental" [[tool.poetry.source]] name = "bar" url = "https://bar.baz/simple/" -priority = "secondary" +priority = "supplemental" 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 deleted file mode 100644 index 366db7461a8..00000000000 --- a/tests/fixtures/with_non_default_multiple_secondary_sources_legacy/pyproject.toml +++ /dev/null @@ -1,24 +0,0 @@ -[tool.poetry] -name = "my-package" -version = "1.2.3" -description = "Some description." -authors = [ - "Your Name " -] -license = "MIT" - -# Requirements -[tool.poetry.dependencies] -python = "^3.6" - -[tool.poetry.group.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 deleted file mode 100644 index b3063e47827..00000000000 --- a/tests/fixtures/with_non_default_multiple_sources/pyproject.toml +++ /dev/null @@ -1,23 +0,0 @@ -[tool.poetry] -name = "my-package" -version = "1.2.3" -description = "Some description." -authors = [ - "Your Name " -] -license = "MIT" - -# Requirements -[tool.poetry.dependencies] -python = "^3.6" - -[tool.poetry.group.dev.dependencies] - -[[tool.poetry.source]] -name = "foo" -url = "https://foo.bar/simple/" -priority = "secondary" - -[[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 deleted file mode 100644 index 7a8004cac32..00000000000 --- a/tests/fixtures/with_non_default_secondary_source/pyproject.toml +++ /dev/null @@ -1,19 +0,0 @@ -[tool.poetry] -name = "my-package" -version = "1.2.3" -description = "Some description." -authors = [ - "Your Name " -] -license = "MIT" - -# Requirements -[tool.poetry.dependencies] -python = "^3.6" - -[tool.poetry.group.dev.dependencies] - -[[tool.poetry.source]] -name = "foo" -url = "https://foo.bar/simple/" -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 deleted file mode 100644 index 980d78eecc9..00000000000 --- a/tests/fixtures/with_non_default_secondary_source_legacy/pyproject.toml +++ /dev/null @@ -1,19 +0,0 @@ -[tool.poetry] -name = "my-package" -version = "1.2.3" -description = "Some description." -authors = [ - "Your Name " -] -license = "MIT" - -# Requirements -[tool.poetry.dependencies] -python = "^3.6" - -[tool.poetry.group.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_primary_source_explicit/pyproject.toml similarity index 100% rename from tests/fixtures/with_non_default_source_explicit/pyproject.toml rename to tests/fixtures/with_primary_source_explicit/pyproject.toml diff --git a/tests/fixtures/with_non_default_source_implicit/pyproject.toml b/tests/fixtures/with_primary_source_implicit/pyproject.toml similarity index 100% rename from tests/fixtures/with_non_default_source_implicit/pyproject.toml rename to tests/fixtures/with_primary_source_implicit/pyproject.toml diff --git a/tests/fixtures/with_default_source_and_pypi/README.rst b/tests/fixtures/with_source/README.rst similarity index 100% rename from tests/fixtures/with_default_source_and_pypi/README.rst rename to tests/fixtures/with_source/README.rst diff --git a/tests/fixtures/with_default_source/pyproject.toml b/tests/fixtures/with_source/pyproject.toml similarity index 96% rename from tests/fixtures/with_default_source/pyproject.toml rename to tests/fixtures/with_source/pyproject.toml index bc0c4ac42b1..49fa2f62d71 100644 --- a/tests/fixtures/with_default_source/pyproject.toml +++ b/tests/fixtures/with_source/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -name = "with-default-source" +name = "with-source" version = "1.2.3" description = "Some description." authors = [ @@ -58,4 +58,3 @@ my-script = "with_default_source:main" [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" -priority = "default" diff --git a/tests/fixtures/with_two_default_sources/README.rst b/tests/fixtures/with_two_default_sources/README.rst deleted file mode 100644 index f7fe15470f9..00000000000 --- a/tests/fixtures/with_two_default_sources/README.rst +++ /dev/null @@ -1,2 +0,0 @@ -My Package -========== diff --git a/tests/fixtures/with_two_default_sources/pyproject.toml b/tests/fixtures/with_two_default_sources/pyproject.toml deleted file mode 100644 index 6f05f22eba1..00000000000 --- a/tests/fixtures/with_two_default_sources/pyproject.toml +++ /dev/null @@ -1,66 +0,0 @@ -[tool.poetry] -name = "two-default-sources" -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.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 = "~3.6" } - -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.group.dev.dependencies] -pytest = "~3.4" - - -[tool.poetry.scripts] -my-script = "two_default_sources:main" - - -[tool.poetry.plugins."blogtool.parsers"] -".rst" = "some_module::SomeClass" - - -[[tool.poetry.source]] -name = "foo" -url = "https://foo.bar/simple/" -priority = "default" - -[[tool.poetry.source]] -name = "bar" -url = "https://bar.foo/simple/" -priority = "default" diff --git a/tests/fixtures/with_two_default_sources_legacy/README.rst b/tests/fixtures/with_two_default_sources_legacy/README.rst deleted file mode 100644 index f7fe15470f9..00000000000 --- a/tests/fixtures/with_two_default_sources_legacy/README.rst +++ /dev/null @@ -1,2 +0,0 @@ -My Package -========== diff --git a/tests/fixtures/with_two_default_sources_legacy/pyproject.toml b/tests/fixtures/with_two_default_sources_legacy/pyproject.toml deleted file mode 100644 index 0de036eb8c8..00000000000 --- a/tests/fixtures/with_two_default_sources_legacy/pyproject.toml +++ /dev/null @@ -1,66 +0,0 @@ -[tool.poetry] -name = "two-default-sources-legacy" -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.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 = "~3.6" } - -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.group.dev.dependencies] -pytest = "~3.4" - - -[tool.poetry.scripts] -my-script = "two_default_sources_legacy: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 431b52c5df9..d8211e7fc1a 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -18,7 +18,7 @@ from poetry.installation.executor import Executor from poetry.packages import Locker from poetry.repositories import Repository -from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.exceptions import PackageNotFoundError from poetry.utils._compat import metadata @@ -113,7 +113,8 @@ def mock_clone( if not source_root: source_root = Path(Config.create().get("cache-dir")) / "src" - dest = source_root / path + assert parsed.name is not None + dest = source_root / parsed.name dest.mkdir(parents=True, exist_ok=True) copy_path(folder, dest) @@ -215,7 +216,7 @@ class TestRepository(Repository): def find_packages(self, dependency: Dependency) -> list[Package]: packages = super().find_packages(dependency) if len(packages) == 0: - raise PackageNotFound(f"Package [{dependency.name}] not found.") + raise PackageNotFoundError(f"Package [{dependency.name}] not found.") return packages diff --git a/tests/inspection/test_lazy_wheel.py b/tests/inspection/test_lazy_wheel.py index 1a71cf0b441..4c149a7a0e7 100644 --- a/tests/inspection/test_lazy_wheel.py +++ b/tests/inspection/test_lazy_wheel.py @@ -14,9 +14,9 @@ from requests import codes -from poetry.inspection.lazy_wheel import HTTPRangeRequestNotRespected -from poetry.inspection.lazy_wheel import HTTPRangeRequestUnsupported -from poetry.inspection.lazy_wheel import InvalidWheel +from poetry.inspection.lazy_wheel import HTTPRangeRequestNotRespectedError +from poetry.inspection.lazy_wheel import HTTPRangeRequestUnsupportedError +from poetry.inspection.lazy_wheel import InvalidWheelError from poetry.inspection.lazy_wheel import LazyWheelUnsupportedError from poetry.inspection.lazy_wheel import metadata_from_wheel_url from tests.helpers import http_setup_redirect @@ -377,7 +377,7 @@ def test_metadata_from_wheel_url_range_requests_not_supported_one_request( url = f"https://{domain}/poetry_core-1.5.0-py3-none-any.whl" - with pytest.raises(HTTPRangeRequestUnsupported): + with pytest.raises(HTTPRangeRequestUnsupportedError): metadata_from_wheel_url("poetry-core", url, requests.Session()) latest_requests = http.latest_requests() @@ -407,7 +407,7 @@ def test_metadata_from_wheel_url_range_requests_not_supported_two_requests( url = f"https://{domain}/poetry_core-1.5.0-py3-none-any.whl" - with pytest.raises(HTTPRangeRequestUnsupported): + with pytest.raises(HTTPRangeRequestUnsupportedError): metadata_from_wheel_url("poetry-core", url, requests.Session()) latest_requests = http.latest_requests() @@ -431,7 +431,7 @@ def test_metadata_from_wheel_url_range_requests_supported_but_not_respected( url = f"https://{domain}/poetry_core-1.5.0-py3-none-any.whl" - with pytest.raises(HTTPRangeRequestNotRespected): + with pytest.raises(HTTPRangeRequestNotRespectedError): metadata_from_wheel_url("poetry-core", url, requests.Session()) latest_requests = http.latest_requests() @@ -453,7 +453,7 @@ def test_metadata_from_wheel_url_invalid_wheel( url = f"https://{domain}/demo_missing_dist_info-0.1.0-py2.py3-none-any.whl" - with pytest.raises(InvalidWheel): + with pytest.raises(InvalidWheelError): metadata_from_wheel_url("demo-missing-dist-info", url, requests.Session()) latest_requests = http.latest_requests() diff --git a/tests/installation/fixtures/with-conditional-dependency.test b/tests/installation/fixtures/with-conditional-dependency.test index 090bee4025e..0c8c9c2b5a9 100644 --- a/tests/installation/fixtures/with-conditional-dependency.test +++ b/tests/installation/fixtures/with-conditional-dependency.test @@ -6,20 +6,6 @@ optional = false python-versions = ">=3.5" files = [] -[package.requirements] -python = ">=3.5,<4.0" - -[[package]] -name = "A" -version = "1.0.1" -description = "" -optional = false -python-versions = ">=3.6" -files = [] - -[package.requirements] -python = ">=3.6,<4.0" - [metadata] python-versions = "~2.7 || ^3.4" lock-version = "2.0" diff --git a/tests/installation/test_chef.py b/tests/installation/test_chef.py index ffe0e06b3bd..9a659bc0a2a 100644 --- a/tests/installation/test_chef.py +++ b/tests/installation/test_chef.py @@ -69,7 +69,7 @@ def test_prepare_directory( chef = Chef( artifact_cache, EnvManager.get_system_env(), Factory.create_pool(config) ) - archive = fixture_dir("simple_project").resolve() + archive = fixture_dir("simple_project_legacy").resolve() wheel = chef.prepare(archive) @@ -111,7 +111,7 @@ def test_prepare_directory_editable( chef = Chef( artifact_cache, EnvManager.get_system_env(), Factory.create_pool(config) ) - archive = fixture_dir("simple_project").resolve() + archive = fixture_dir("simple_project_legacy").resolve() wheel = chef.prepare(archive, editable=True) diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py index b54bcdaf022..b9905d6af70 100644 --- a/tests/installation/test_executor.py +++ b/tests/installation/test_executor.py @@ -939,6 +939,13 @@ def mock_get_cached_archive_func( download_spy.assert_not_called() +@pytest.mark.parametrize( + "source_url,written_source_url", + [ + ("https://github.com/demo/demo.git", "https://github.com/demo/demo.git"), + ("git@github.com:demo/demo.git", "ssh://git@github.com/demo/demo.git"), + ], +) @pytest.mark.parametrize("is_artifact_cached", [False, True]) def test_executor_should_write_pep610_url_references_for_git( tmp_venv: VirtualEnv, @@ -949,6 +956,8 @@ def test_executor_should_write_pep610_url_references_for_git( wheel: Path, mocker: MockerFixture, fixture_dir: FixtureDirGetter, + source_url: str, + written_source_url: str, is_artifact_cached: bool, ) -> None: if is_artifact_cached: @@ -960,7 +969,7 @@ def test_executor_should_write_pep610_url_references_for_git( clone_spy = mocker.spy(Git, "clone") source_resolved_reference = "123456" - source_url = "https://github.com/demo/demo.git" + source_url = source_url package = Package( "demo", @@ -971,6 +980,8 @@ def test_executor_should_write_pep610_url_references_for_git( source_url=source_url, ) + assert package.source_url == written_source_url + chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) chef.set_directory_wheel(wheel) prepare_spy = mocker.spy(chef, "prepare") @@ -996,7 +1007,9 @@ def test_executor_should_write_pep610_url_references_for_git( prepare_spy.assert_not_called() else: clone_spy.assert_called_once_with( - url=source_url, source_root=mocker.ANY, revision=source_resolved_reference + url=package.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" @@ -1078,7 +1091,92 @@ def test_executor_should_append_subdirectory_for_git( executor.execute([Install(package)]) archive_arg = spy.call_args[0][0] - assert archive_arg == tmp_venv.path / "src/demo/subdirectories/two" + assert archive_arg == tmp_venv.path / "src/subdirectories/two" + + +def test_executor_should_install_multiple_packages_from_same_git_repository( + mocker: MockerFixture, + tmp_venv: VirtualEnv, + pool: RepositoryPool, + config: Config, + artifact_cache: ArtifactCache, + io: BufferedIO, + wheel: Path, +) -> None: + package_a = Package( + "package_a", + "0.1.2", + source_type="git", + source_reference="master", + source_resolved_reference="123456", + source_url="https://github.com/demo/subdirectories.git", + source_subdirectory="package_a", + ) + package_b = Package( + "package_b", + "0.1.2", + source_type="git", + source_reference="master", + source_resolved_reference="123456", + source_url="https://github.com/demo/subdirectories.git", + source_subdirectory="package_b", + ) + + 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_a), Install(package_b)]) + + archive_arg = spy.call_args_list[0][0][0] + assert archive_arg == tmp_venv.path / "src/subdirectories/package_a" + + archive_arg = spy.call_args_list[1][0][0] + assert archive_arg == tmp_venv.path / "src/subdirectories/package_b" + + +def test_executor_should_install_multiple_packages_from_forked_git_repository( + mocker: MockerFixture, + tmp_venv: VirtualEnv, + pool: RepositoryPool, + config: Config, + artifact_cache: ArtifactCache, + io: BufferedIO, + wheel: Path, +) -> None: + package_a = Package( + "one", + "1.0.0", + source_type="git", + source_reference="master", + source_resolved_reference="123456", + source_url="https://github.com/demo/subdirectories.git", + source_subdirectory="one", + ) + package_b = Package( + "two", + "2.0.0", + source_type="git", + source_reference="master", + source_resolved_reference="123456", + source_url="https://github.com/forked_demo/subdirectories.git", + source_subdirectory="two", + ) + + 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_a), Install(package_b)]) + + # Verify that the repo for package_a is not re-used for package_b. + # both repos must be cloned serially into separate directories. + # If so, executor.prepare() will be called twice. + assert prepare_spy.call_count == 2 def test_executor_should_write_pep610_url_references_for_git_with_subdirectories( @@ -1299,7 +1397,7 @@ def test_build_system_requires_not_available( - Installing {package_name} ({package_version} {package_url}) - SolveFailure + SolveFailureError Because -root- depends on poetry-core (0.999) which doesn't match any versions,\ version solving failed. diff --git a/tests/installation/test_installer.py b/tests/installation/test_installer.py index 681e882b19c..8776482aa4d 100644 --- a/tests/installation/test_installer.py +++ b/tests/installation/test_installer.py @@ -1791,9 +1791,6 @@ def test_run_install_duplicate_dependencies_different_constraints_with_lock_upda assert installer.executor.removals_count == 0 -@pytest.mark.skip( - "This is not working at the moment due to limitations in the resolver" -) def test_installer_test_solver_finds_compatible_package_for_dependency_python_not_fully_compatible_with_package_python( installer: Installer, locker: Locker, diff --git a/tests/installation/test_wheel_installer.py b/tests/installation/test_wheel_installer.py index f74166fdb8c..98e3f4cf97e 100644 --- a/tests/installation/test_wheel_installer.py +++ b/tests/installation/test_wheel_installer.py @@ -14,7 +14,7 @@ if TYPE_CHECKING: - from _pytest.tmpdir import TempPathFactory + from pytest import TempPathFactory from tests.types import FixtureDirGetter diff --git a/tests/integration/test_utils_vcs_git.py b/tests/integration/test_utils_vcs_git.py index 069566a22c5..d8c3f568a08 100644 --- a/tests/integration/test_utils_vcs_git.py +++ b/tests/integration/test_utils_vcs_git.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING from typing import Iterator from typing import TypedDict +from unittest.mock import ANY from urllib.parse import urlparse from urllib.parse import urlunparse @@ -27,9 +28,9 @@ if TYPE_CHECKING: - from _pytest.tmpdir import TempPathFactory from dulwich.client import FetchPackResult from dulwich.client import GitClient + from pytest import TempPathFactory from pytest_mock import MockerFixture from tests.conftest import Config @@ -357,6 +358,7 @@ def test_configured_repository_http_auth( config=dummy_git_config, username=GIT_USERNAME, password=GIT_PASSWORD, + pool_manager=ANY, ) spy_get_transport_and_path.assert_called_once() @@ -383,6 +385,7 @@ def test_username_password_parameter_is_not_passed_to_dulwich( spy_get_transport_and_path.assert_called_with( location=source_url, config=dummy_git_config, + pool_manager=ANY, ) spy_get_transport_and_path.assert_called_once() diff --git a/tests/json/fixtures/self_invalid_plugin.toml b/tests/json/fixtures/self_invalid_plugin.toml new file mode 100644 index 00000000000..3310785cac2 --- /dev/null +++ b/tests/json/fixtures/self_invalid_plugin.toml @@ -0,0 +1,8 @@ +[tool.poetry] +name = "foobar" +version = "0.1.0" +description = "" +authors = ["Your Name "] + +[tool.poetry.requires-plugins] +foo = 5 diff --git a/tests/json/fixtures/self_invalid_version.toml b/tests/json/fixtures/self_invalid_version.toml new file mode 100644 index 00000000000..ddc07369b03 --- /dev/null +++ b/tests/json/fixtures/self_invalid_version.toml @@ -0,0 +1,6 @@ +[tool.poetry] +name = "foobar" +version = "0.1.0" +description = "" +authors = ["Your Name "] +requires-poetry = 2 diff --git a/tests/json/fixtures/self_valid.toml b/tests/json/fixtures/self_valid.toml new file mode 100644 index 00000000000..44f9c200fbb --- /dev/null +++ b/tests/json/fixtures/self_valid.toml @@ -0,0 +1,9 @@ +[tool.poetry] +name = "foobar" +version = "0.1.0" +description = "" +authors = ["Your Name "] +requires-poetry = ">=2.0" + +[tool.poetry.requires-plugins] +foo = ">=1.0" 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 deleted file mode 100644 index 4e2789b49d8..00000000000 --- a/tests/json/fixtures/source/complete_invalid_priority_legacy_and_new.toml +++ /dev/null @@ -1,18 +0,0 @@ -[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_valid_legacy.toml b/tests/json/fixtures/source/complete_valid_legacy.toml deleted file mode 100644 index d0b4565ffa4..00000000000 --- a/tests/json/fixtures/source/complete_valid_legacy.toml +++ /dev/null @@ -1,18 +0,0 @@ -[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 -secondary = false - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" diff --git a/tests/json/test_schema.py b/tests/json/test_schema.py new file mode 100644 index 00000000000..cb039bf409b --- /dev/null +++ b/tests/json/test_schema.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +import json + +from pathlib import Path +from typing import Any + +from poetry.core.json import SCHEMA_DIR as CORE_SCHEMA_DIR + +from poetry.factory import Factory +from poetry.json import SCHEMA_DIR +from poetry.toml import TOMLFile + + +FIXTURE_DIR = Path(__file__).parent / "fixtures" +SOURCE_FIXTURE_DIR = FIXTURE_DIR / "source" + + +def test_pyproject_toml_valid() -> None: + toml: dict[str, Any] = TOMLFile(SOURCE_FIXTURE_DIR / "complete_valid.toml").read() + assert Factory.validate(toml) == {"errors": [], "warnings": []} + + +def test_pyproject_toml_invalid_priority() -> None: + toml: dict[str, Any] = TOMLFile( + SOURCE_FIXTURE_DIR / "complete_invalid_priority.toml" + ).read() + assert Factory.validate(toml) == { + "errors": [ + "data.source[0].priority must be one of ['primary'," + " 'supplemental', 'explicit']" + ], + "warnings": [], + } + + +def test_self_valid() -> None: + toml: dict[str, Any] = TOMLFile(FIXTURE_DIR / "self_valid.toml").read() + assert Factory.validate(toml) == {"errors": [], "warnings": []} + + +def test_self_invalid_version() -> None: + toml: dict[str, Any] = TOMLFile(FIXTURE_DIR / "self_invalid_version.toml").read() + assert Factory.validate(toml) == { + "errors": ["data.requires-poetry must be string"], + "warnings": [], + } + + +def test_self_invalid_plugin() -> None: + toml: dict[str, Any] = TOMLFile(FIXTURE_DIR / "self_invalid_plugin.toml").read() + assert Factory.validate(toml) == { + "errors": [ + "data.requires-plugins.foo must be valid exactly by one definition" + " (0 matches found)" + ], + "warnings": [], + } + + +def test_dependencies_is_consistent_to_poetry_core_schema() -> None: + with (SCHEMA_DIR / "poetry.json").open(encoding="utf-8") as f: + schema = json.load(f) + dependency_definitions = { + key: value for key, value in schema["definitions"].items() if "depend" in key + } + with (CORE_SCHEMA_DIR / "poetry-schema.json").open(encoding="utf-8") as f: + core_schema = json.load(f) + core_dependency_definitions = { + key: value + for key, value in core_schema["definitions"].items() + if "depend" in key + } + assert dependency_definitions == core_dependency_definitions diff --git a/tests/json/test_schema_sources.py b/tests/json/test_schema_sources.py index f0a998276db..e69de29bb2d 100644 --- a/tests/json/test_schema_sources.py +++ b/tests/json/test_schema_sources.py @@ -1,47 +0,0 @@ -from __future__ import annotations - -from pathlib import Path -from typing import Any - -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: dict[str, Any] = 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: dict[str, Any] = TOMLFile(FIXTURE_DIR / "complete_valid.toml").read() - content = toml["tool"]["poetry"] - assert Factory.validate(content) == {"errors": [], "warnings": []} - - -def test_pyproject_toml_invalid_priority() -> None: - toml: dict[str, Any] = TOMLFile( - FIXTURE_DIR / "complete_invalid_priority.toml" - ).read() - content = toml["tool"]["poetry"] - assert Factory.validate(content) == { - "errors": [ - "data.source[0].priority must be one of ['primary', 'default', " - "'secondary', 'supplemental', 'explicit']" - ], - "warnings": [], - } - - -def test_pyproject_toml_invalid_priority_legacy_and_new() -> None: - toml: dict[str, Any] = TOMLFile( - FIXTURE_DIR / "complete_invalid_priority_legacy_and_new.toml" - ).read() - content = toml["tool"]["poetry"] - assert Factory.validate(content) == { - "errors": ["data.source[0] must NOT match a disallowed definition"], - "warnings": [], - } diff --git a/tests/masonry/builders/test_editable_builder.py b/tests/masonry/builders/test_editable_builder.py index 6e2e332fdbb..82c63426b90 100644 --- a/tests/masonry/builders/test_editable_builder.py +++ b/tests/masonry/builders/test_editable_builder.py @@ -108,9 +108,19 @@ def expected_metadata_version() -> str: return metadata.metadata_version +@pytest.mark.parametrize("project", ("simple_project", "simple_project_legacy")) def test_builder_installs_proper_files_for_standard_packages( - simple_poetry: Poetry, tmp_venv: VirtualEnv + project: str, + simple_poetry: Poetry, + tmp_path: Path, + fixture_dir: FixtureDirGetter, ) -> None: + simple_poetry = Factory().create_poetry(fixture_dir(project)) + env_manager = EnvManager(simple_poetry) + venv_path = tmp_path / "venv" + env_manager.build_venv(venv_path) + tmp_venv = VirtualEnv(venv_path) + builder = EditableBuilder(simple_poetry, tmp_venv, NullIO()) builder.build() @@ -262,7 +272,7 @@ def test_builder_setup_generation_runs_with_pip_editable( poetry = Factory().create_poetry(extended_project) # we need a venv with pip and setuptools since we are verifying setup.py builds - with ephemeral_environment(flags={"no-setuptools": False, "no-pip": False}) as venv: + with ephemeral_environment(flags={"no-pip": False}) as venv: builder = EditableBuilder(poetry, venv, NullIO()) builder.build() diff --git a/tests/mixology/helpers.py b/tests/mixology/helpers.py index c2c28e19853..c8dde3e5ae8 100644 --- a/tests/mixology/helpers.py +++ b/tests/mixology/helpers.py @@ -5,7 +5,7 @@ from poetry.core.packages.package import Package from poetry.factory import Factory -from poetry.mixology.failure import SolveFailure +from poetry.mixology.failure import SolveFailureError from poetry.mixology.version_solver import VersionSolver @@ -52,7 +52,7 @@ def check_solver_result( with provider.use_latest_for(use_latest or []): try: solution = solver.solve() - except SolveFailure as e: + except SolveFailureError as e: if error: assert str(e) == error diff --git a/tests/mixology/test_incompatibility.py b/tests/mixology/test_incompatibility.py index d3395f13796..f125abb6347 100644 --- a/tests/mixology/test_incompatibility.py +++ b/tests/mixology/test_incompatibility.py @@ -6,7 +6,7 @@ from poetry.core.packages.url_dependency import URLDependency from poetry.mixology.incompatibility import Incompatibility -from poetry.mixology.incompatibility_cause import DependencyCause +from poetry.mixology.incompatibility_cause import DependencyCauseError from poetry.mixology.term import Term @@ -45,6 +45,6 @@ def test_str_dependency_cause( dependency1: Dependency, dependency2: Dependency, expected: str ) -> None: incompatibility = Incompatibility( - [Term(dependency1, True), Term(dependency2, False)], DependencyCause() + [Term(dependency1, True), Term(dependency2, False)], DependencyCauseError() ) assert str(incompatibility) == expected diff --git a/tests/packages/test_locker.py b/tests/packages/test_locker.py index 0c3203b0b1d..32747c8a26e 100644 --- a/tests/packages/test_locker.py +++ b/tests/packages/test_locker.py @@ -27,7 +27,7 @@ if TYPE_CHECKING: - from _pytest.logging import LogCaptureFixture + from pytest import LogCaptureFixture from pytest_mock import MockerFixture diff --git a/tests/plugins/test_plugin_manager.py b/tests/plugins/test_plugin_manager.py index 44e0d68343d..5d254b1ee6c 100644 --- a/tests/plugins/test_plugin_manager.py +++ b/tests/plugins/test_plugin_manager.py @@ -1,5 +1,7 @@ from __future__ import annotations +import shutil + from pathlib import Path from typing import TYPE_CHECKING from typing import ClassVar @@ -8,14 +10,28 @@ import pytest from cleo.io.buffered_io import BufferedIO +from cleo.io.outputs.output import Verbosity from poetry.core.constraints.version import Version +from poetry.core.packages.dependency import Dependency +from poetry.core.packages.file_dependency import FileDependency +from poetry.core.packages.package import Package from poetry.core.packages.project_package import ProjectPackage +from poetry.factory import Factory +from poetry.installation.wheel_installer import WheelInstaller from poetry.packages.locker import Locker from poetry.plugins import ApplicationPlugin from poetry.plugins import Plugin from poetry.plugins.plugin_manager import PluginManager +from poetry.plugins.plugin_manager import ProjectPluginCache from poetry.poetry import Poetry +from poetry.puzzle.exceptions import SolverProblemError +from poetry.repositories import Repository +from poetry.repositories import RepositoryPool +from poetry.repositories.installed_repository import InstalledRepository +from poetry.utils.env import Env +from poetry.utils.env import EnvManager +from poetry.utils.env import MockEnv from tests.helpers import mock_metadata_entry_points @@ -48,6 +64,33 @@ def activate(self, poetry: Poetry, io: IO) -> None: poetry.package.version = Version.parse("9.9.9") +@pytest.fixture +def repo() -> Repository: + repo = Repository("repo") + repo.add_package(Package("my-other-plugin", "1.0")) + for version in ("1.0", "2.0"): + package = Package("my-application-plugin", version) + package.add_dependency(Dependency("some-lib", version)) + repo.add_package(package) + repo.add_package(Package("some-lib", version)) + return repo + + +@pytest.fixture +def pool(repo: Repository) -> RepositoryPool: + pool = RepositoryPool() + pool.add_repository(repo) + + return pool + + +@pytest.fixture +def system_env(tmp_path: Path, mocker: MockerFixture) -> Env: + env = MockEnv(path=tmp_path, sys_path=[str(tmp_path / "purelib")]) + mocker.patch.object(EnvManager, "get_system_env", return_value=env) + return env + + @pytest.fixture def poetry(fixture_dir: FixtureDirGetter, config: Config) -> Poetry: project_path = fixture_dir("simple_project") @@ -62,8 +105,21 @@ def poetry(fixture_dir: FixtureDirGetter, config: Config) -> Poetry: return poetry +@pytest.fixture +def poetry_with_plugins( + fixture_dir: FixtureDirGetter, pool: RepositoryPool, tmp_path: Path +) -> Poetry: + orig_path = fixture_dir("project_plugins") + project_path = tmp_path / "project" + project_path.mkdir() + shutil.copy(orig_path / "pyproject.toml", project_path / "pyproject.toml") + poetry = Factory().create_poetry(project_path) + poetry.set_pool(pool) + return poetry + + @pytest.fixture() -def io() -> IO: +def io() -> BufferedIO: return BufferedIO() @@ -75,9 +131,14 @@ def _manager(group: str = Plugin.group) -> PluginManager: return _manager -@pytest.fixture() -def no_plugin_manager(poetry: Poetry, io: BufferedIO) -> PluginManager: - return PluginManager(Plugin.group, disable_plugins=True) +@pytest.fixture +def with_my_plugin(mocker: MockerFixture) -> None: + mock_metadata_entry_points(mocker, MyPlugin) + + +@pytest.fixture +def with_invalid_plugin(mocker: MockerFixture) -> None: + mock_metadata_entry_points(mocker, InvalidPlugin) def test_load_plugins_and_activate( @@ -94,16 +155,6 @@ def test_load_plugins_and_activate( assert io.fetch_output() == "Setting readmes\n" -@pytest.fixture -def with_my_plugin(mocker: MockerFixture) -> None: - mock_metadata_entry_points(mocker, MyPlugin) - - -@pytest.fixture -def with_invalid_plugin(mocker: MockerFixture) -> None: - mock_metadata_entry_points(mocker, InvalidPlugin) - - def test_load_plugins_with_invalid_plugin( manager_factory: ManagerFactory, poetry: Poetry, @@ -116,13 +167,408 @@ def test_load_plugins_with_invalid_plugin( manager.load_plugins() -def test_load_plugins_with_plugins_disabled( - no_plugin_manager: PluginManager, - poetry: Poetry, +def test_add_project_plugin_path( + poetry_with_plugins: Poetry, io: BufferedIO, - with_my_plugin: None, + system_env: Env, + fixture_dir: FixtureDirGetter, ) -> None: - no_plugin_manager.load_plugins() + dist_info_1 = "my_application_plugin-1.0.dist-info" + dist_info_2 = "my_application_plugin-2.0.dist-info" + cache = ProjectPluginCache(poetry_with_plugins, io) + shutil.copytree( + fixture_dir("project_plugins") / dist_info_1, cache._path / dist_info_1 + ) + shutil.copytree( + fixture_dir("project_plugins") / dist_info_2, system_env.purelib / dist_info_2 + ) - assert poetry.package.version.text == "1.2.3" + assert { + f"{p.name} {p.version}" for p in InstalledRepository.load(system_env).packages + } == {"my-application-plugin 2.0"} + + PluginManager.add_project_plugin_path(poetry_with_plugins.pyproject_path.parent) + + assert { + f"{p.name} {p.version}" for p in InstalledRepository.load(system_env).packages + } == {"my-application-plugin 1.0"} + + +def test_ensure_plugins_no_plugins_no_output(poetry: Poetry, io: BufferedIO) -> None: + PluginManager.ensure_project_plugins(poetry, io) + + assert not (poetry.pyproject_path.parent / ProjectPluginCache.PATH).exists() assert io.fetch_output() == "" + assert io.fetch_error() == "" + + +def test_ensure_plugins_no_plugins_existing_cache_is_removed( + poetry: Poetry, io: BufferedIO +) -> None: + plugin_path = poetry.pyproject_path.parent / ProjectPluginCache.PATH + plugin_path.mkdir(parents=True) + + PluginManager.ensure_project_plugins(poetry, io) + + assert not plugin_path.exists() + assert io.fetch_output() == ( + "No project plugins defined. Removing the project's plugin cache\n\n" + ) + assert io.fetch_error() == "" + + +@pytest.mark.parametrize("debug_out", [False, True]) +def test_ensure_plugins_no_output_if_fresh( + poetry_with_plugins: Poetry, io: BufferedIO, debug_out: bool +) -> None: + io.set_verbosity(Verbosity.DEBUG if debug_out else Verbosity.NORMAL) + cache = ProjectPluginCache(poetry_with_plugins, io) + cache._write_config() + + cache.ensure_plugins() + + assert cache._config_file.exists() + assert ( + cache._gitignore_file.exists() + and cache._gitignore_file.read_text(encoding="utf-8") == "*" + ) + assert io.fetch_output() == ( + "The project's plugin cache is up to date.\n\n" if debug_out else "" + ) + assert io.fetch_error() == "" + + +@pytest.mark.parametrize("debug_out", [False, True]) +def test_ensure_plugins_ignore_irrelevant_markers( + poetry_with_plugins: Poetry, io: BufferedIO, debug_out: bool +) -> None: + io.set_verbosity(Verbosity.DEBUG if debug_out else Verbosity.NORMAL) + poetry_with_plugins.local_config["requires-plugins"] = { + "irrelevant": {"version": "1.0", "markers": "python_version < '3'"} + } + cache = ProjectPluginCache(poetry_with_plugins, io) + + cache.ensure_plugins() + + assert cache._config_file.exists() + assert ( + cache._gitignore_file.exists() + and cache._gitignore_file.read_text(encoding="utf-8") == "*" + ) + assert io.fetch_output() == ( + "No relevant project plugins for Poetry's environment defined.\n\n" + if debug_out + else "" + ) + assert io.fetch_error() == "" + + +def test_ensure_plugins_remove_outdated( + poetry_with_plugins: Poetry, io: BufferedIO, fixture_dir: FixtureDirGetter +) -> None: + # Test with irrelevant plugins because this is the first return + # where it is relevant that an existing cache is removed. + poetry_with_plugins.local_config["requires-plugins"] = { + "irrelevant": {"version": "1.0", "markers": "python_version < '3'"} + } + fixture_path = fixture_dir("project_plugins") + cache = ProjectPluginCache(poetry_with_plugins, io) + cache._path.mkdir(parents=True) + dist_info = "my_application_plugin-1.0.dist-info" + shutil.copytree(fixture_path / dist_info, cache._path / dist_info) + cache._config_file.touch() + + cache.ensure_plugins() + + assert cache._config_file.exists() + assert not (cache._path / dist_info).exists() + assert io.fetch_output() == ( + "Removing the project's plugin cache because it is outdated\n" + ) + assert io.fetch_error() == "" + + +def test_ensure_plugins_ignore_already_installed_in_system_env( + poetry_with_plugins: Poetry, + io: BufferedIO, + system_env: Env, + fixture_dir: FixtureDirGetter, +) -> None: + fixture_path = fixture_dir("project_plugins") + for dist_info in ( + "my_application_plugin-2.0.dist-info", + "my_other_plugin-1.0.dist-info", + ): + shutil.copytree(fixture_path / dist_info, system_env.purelib / dist_info) + cache = ProjectPluginCache(poetry_with_plugins, io) + + cache.ensure_plugins() + + assert cache._config_file.exists() + assert ( + cache._gitignore_file.exists() + and cache._gitignore_file.read_text(encoding="utf-8") == "*" + ) + assert io.fetch_output() == ( + "Ensuring that the Poetry plugins required by the project are available...\n" + "All required plugins have already been installed in Poetry's environment.\n\n" + ) + assert io.fetch_error() == "" + + +def test_ensure_plugins_install_missing_plugins( + poetry_with_plugins: Poetry, + io: BufferedIO, + system_env: Env, + fixture_dir: FixtureDirGetter, + mocker: MockerFixture, +) -> None: + cache = ProjectPluginCache(poetry_with_plugins, io) + install_spy = mocker.spy(cache, "_install") + execute_mock = mocker.patch( + "poetry.plugins.plugin_manager.Installer._execute", return_value=0 + ) + + cache.ensure_plugins() + + install_spy.assert_called_once_with( + [ + Dependency("my-application-plugin", ">=2.0"), + Dependency("my-other-plugin", ">=1.0"), + ], + system_env, + [], + ) + execute_mock.assert_called_once() + assert [repr(op) for op in execute_mock.call_args.args[0] if not op.skipped] == [ + "", + "", + "", + ] + assert cache._config_file.exists() + assert ( + cache._gitignore_file.exists() + and cache._gitignore_file.read_text(encoding="utf-8") == "*" + ) + assert io.fetch_output() == ( + "Ensuring that the Poetry plugins required by the project are available...\n" + "The following Poetry plugins are required by the project" + " but are not installed in Poetry's environment:\n" + " - my-application-plugin (>=2.0)\n" + " - my-other-plugin (>=1.0)\n" + "Installing Poetry plugins only for the current project...\n" + "Updating dependencies\n" + "Resolving dependencies...\n\n" + "Writing lock file\n\n" + ) + assert io.fetch_error() == "" + + +def test_ensure_plugins_install_only_missing_plugins( + poetry_with_plugins: Poetry, + io: BufferedIO, + system_env: Env, + fixture_dir: FixtureDirGetter, + mocker: MockerFixture, +) -> None: + fixture_path = fixture_dir("project_plugins") + for dist_info in ( + "my_application_plugin-2.0.dist-info", + "some_lib-2.0.dist-info", + ): + shutil.copytree(fixture_path / dist_info, system_env.purelib / dist_info) + cache = ProjectPluginCache(poetry_with_plugins, io) + install_spy = mocker.spy(cache, "_install") + execute_mock = mocker.patch( + "poetry.plugins.plugin_manager.Installer._execute", return_value=0 + ) + + cache.ensure_plugins() + + install_spy.assert_called_once_with( + [Dependency("my-other-plugin", ">=1.0")], + system_env, + [Package("my-application-plugin", "2.0"), Package("some-lib", "2.0")], + ) + execute_mock.assert_called_once() + assert [repr(op) for op in execute_mock.call_args.args[0] if not op.skipped] == [ + "" + ] + assert cache._config_file.exists() + assert ( + cache._gitignore_file.exists() + and cache._gitignore_file.read_text(encoding="utf-8") == "*" + ) + assert io.fetch_output() == ( + "Ensuring that the Poetry plugins required by the project are available...\n" + "The following Poetry plugins are required by the project" + " but are not installed in Poetry's environment:\n" + " - my-other-plugin (>=1.0)\n" + "Installing Poetry plugins only for the current project...\n" + "Updating dependencies\n" + "Resolving dependencies...\n\n" + "Writing lock file\n\n" + ) + assert io.fetch_error() == "" + + +@pytest.mark.parametrize("debug_out", [False, True]) +def test_ensure_plugins_install_overwrite_wrong_version_plugins( + poetry_with_plugins: Poetry, + io: BufferedIO, + system_env: Env, + fixture_dir: FixtureDirGetter, + mocker: MockerFixture, + debug_out: bool, +) -> None: + io.set_verbosity(Verbosity.DEBUG if debug_out else Verbosity.NORMAL) + fixture_path = fixture_dir("project_plugins") + for dist_info in ( + "my_application_plugin-1.0.dist-info", + "some_lib-2.0.dist-info", + ): + shutil.copytree(fixture_path / dist_info, system_env.purelib / dist_info) + cache = ProjectPluginCache(poetry_with_plugins, io) + install_spy = mocker.spy(cache, "_install") + execute_mock = mocker.patch( + "poetry.plugins.plugin_manager.Installer._execute", return_value=0 + ) + + cache.ensure_plugins() + + install_spy.assert_called_once_with( + [ + Dependency("my-application-plugin", ">=2.0"), + Dependency("my-other-plugin", ">=1.0"), + ], + system_env, + [Package("some-lib", "2.0")], + ) + execute_mock.assert_called_once() + assert [repr(op) for op in execute_mock.call_args.args[0] if not op.skipped] == [ + "", + "", + ] + assert cache._config_file.exists() + assert ( + cache._gitignore_file.exists() + and cache._gitignore_file.read_text(encoding="utf-8") == "*" + ) + start = ( + "Ensuring that the Poetry plugins required by the project are available...\n" + ) + opt = ( + "The following Poetry plugins are required by the project" + " but are not satisfied by the installed versions:\n" + " - my-application-plugin (>=2.0)\n" + " installed: my-application-plugin (1.0)\n" + ) + end = ( + "The following Poetry plugins are required by the project" + " but are not installed in Poetry's environment:\n" + " - my-application-plugin (>=2.0)\n" + " - my-other-plugin (>=1.0)\n" + "Installing Poetry plugins only for the current project...\n" + ) + expected = (start + opt + end) if debug_out else (start + end) + assert io.fetch_output().startswith(expected) + assert io.fetch_error() == "" + + +def test_ensure_plugins_pins_other_installed_packages( + poetry_with_plugins: Poetry, + io: BufferedIO, + system_env: Env, + fixture_dir: FixtureDirGetter, + mocker: MockerFixture, +) -> None: + fixture_path = fixture_dir("project_plugins") + for dist_info in ( + "my_application_plugin-1.0.dist-info", + "some_lib-1.0.dist-info", + ): + shutil.copytree(fixture_path / dist_info, system_env.purelib / dist_info) + cache = ProjectPluginCache(poetry_with_plugins, io) + install_spy = mocker.spy(cache, "_install") + execute_mock = mocker.patch( + "poetry.plugins.plugin_manager.Installer._execute", return_value=0 + ) + + with pytest.raises(SolverProblemError): + cache.ensure_plugins() + + install_spy.assert_called_once_with( + [ + Dependency("my-application-plugin", ">=2.0"), + Dependency("my-other-plugin", ">=1.0"), + ], + system_env, + # pinned because it might be a dependency of another plugin or Poetry itself + [Package("some-lib", "1.0")], + ) + execute_mock.assert_not_called() + assert not cache._config_file.exists() + assert ( + cache._gitignore_file.exists() + and cache._gitignore_file.read_text(encoding="utf-8") == "*" + ) + assert io.fetch_output() == ( + "Ensuring that the Poetry plugins required by the project are available...\n" + "The following Poetry plugins are required by the project" + " but are not installed in Poetry's environment:\n" + " - my-application-plugin (>=2.0)\n" + " - my-other-plugin (>=1.0)\n" + "Installing Poetry plugins only for the current project...\n" + "Updating dependencies\n" + "Resolving dependencies...\n" + ) + assert io.fetch_error() == "" + + +@pytest.mark.parametrize("other_version", [False, True]) +def test_project_plugins_are_installed_in_project_folder( + poetry_with_plugins: Poetry, + io: BufferedIO, + system_env: Env, + fixture_dir: FixtureDirGetter, + tmp_path: Path, + other_version: bool, +) -> None: + orig_purelib = system_env.purelib + orig_platlib = system_env.platlib + + # make sure that the path dependency is on the same drive (for Windows tests in CI) + orig_wheel_path = ( + fixture_dir("wheel_with_no_requires_dist") / "demo-0.1.0-py2.py3-none-any.whl" + ) + wheel_path = tmp_path / orig_wheel_path.name + shutil.copy(orig_wheel_path, wheel_path) + + if other_version: + WheelInstaller(system_env).install(wheel_path) + dist_info = orig_purelib / "demo-0.1.0.dist-info" + metadata = dist_info / "METADATA" + metadata.write_text( + metadata.read_text(encoding="utf-8").replace("0.1.0", "0.1.2"), + encoding="utf-8", + ) + dist_info.rename(orig_purelib / "demo-0.1.2.dist-info") + + cache = ProjectPluginCache(poetry_with_plugins, io) + + # just use a file dependency so that we do not have to set up a repository + cache._install([FileDependency("demo", wheel_path)], system_env, []) + + project_site_packages = [p.name for p in cache._path.iterdir()] + assert "demo" in project_site_packages + assert "demo-0.1.0.dist-info" in project_site_packages + + orig_site_packages = [p.name for p in orig_purelib.iterdir()] + if other_version: + assert "demo" in orig_site_packages + assert "demo-0.1.2.dist-info" in orig_site_packages + assert "demo-0.1.0.dist-info" not in orig_site_packages + else: + assert not any(p.startswith("demo") for p in orig_site_packages) + if orig_platlib != orig_purelib: + assert not any(p.name.startswith("demo") for p in orig_platlib.iterdir()) diff --git a/tests/publishing/test_publisher.py b/tests/publishing/test_publisher.py index a68885e9a91..db1c7b4d62f 100644 --- a/tests/publishing/test_publisher.py +++ b/tests/publishing/test_publisher.py @@ -43,7 +43,7 @@ def test_publish_publishes_to_pypi_by_default( ] -@pytest.mark.parametrize("fixture_name", ["sample_project", "with_default_source"]) +@pytest.mark.parametrize("fixture_name", ["sample_project", "with_source"]) def test_publish_can_publish_to_given_repository( fixture_dir: FixtureDirGetter, mocker: MockerFixture, diff --git a/tests/puzzle/test_provider.py b/tests/puzzle/test_provider.py index d79ecdba92d..e4fa5c855a0 100644 --- a/tests/puzzle/test_provider.py +++ b/tests/puzzle/test_provider.py @@ -24,7 +24,7 @@ from poetry.packages import DependencyPackage from poetry.puzzle.provider import IncompatibleConstraintsError from poetry.puzzle.provider import Provider -from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.repository import Repository from poetry.repositories.repository_pool import Priority from poetry.repositories.repository_pool import RepositoryPool @@ -852,7 +852,7 @@ def test_complete_package_raises_packagenotfound_if_locked_source_not_available( locked = provider.get_locked(dependency) assert locked is not None - with pytest.raises(PackageNotFound): + with pytest.raises(PackageNotFoundError): provider.complete_package(locked) diff --git a/tests/puzzle/test_solver.py b/tests/puzzle/test_solver.py index 36f41d88b05..5c49557962d 100644 --- a/tests/puzzle/test_solver.py +++ b/tests/puzzle/test_solver.py @@ -13,6 +13,8 @@ from cleo.io.null_io import NullIO from packaging.utils import canonicalize_name from poetry.core.packages.dependency import Dependency +from poetry.core.packages.dependency_group import MAIN_GROUP +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.packages.vcs_dependency import VCSDependency @@ -661,6 +663,63 @@ def test_solver_returns_extras_when_multiple_extras_use_same_dependency( assert ops[0].package.marker.is_any() +def test_solver_locks_all_extras_when_multiple_extras_require_same_dependency( + solver: Solver, + repo: Repository, + package: ProjectPackage, +) -> None: + """ + - root depends on A[extra-b1] and C + - C depends on A[extra-b2] + - B is required by both extras + -> the locked dependency A on B must have both extra markers + """ + package_a = get_package("A", "1.0") + package_b = get_package("B", "1.0") + package_c = get_package("C", "1.0") + + dep_b1 = get_dependency("B", "*", optional=True) + dep_b1.marker = parse_marker("extra == 'extra-b1'") + + dep_b2 = get_dependency("B", "*", optional=True) + dep_b2.marker = parse_marker("extra == 'extra-b2'") + + package_a.extras = { + canonicalize_name("extra-b1"): [dep_b1], + canonicalize_name("extra-b2"): [dep_b2], + } + package_a.add_dependency(dep_b1) + package_a.add_dependency(dep_b2) + + package.add_dependency( + get_dependency("A", {"version": "*", "extras": ["extra-b1"]}) + ) + package.add_dependency(get_dependency("C", "*")) + package_c.add_dependency( + get_dependency("A", {"version": "*", "extras": ["extra-b2"]}) + ) + + repo.add_package(package_a) + repo.add_package(package_b) + repo.add_package(package_c) + + transaction = solver.solve() + + expected = [ + {"job": "install", "package": package_b}, + {"job": "install", "package": package_a}, + {"job": "install", "package": package_c}, + ] + + ops = check_solver_result(transaction, expected) + locked_a_requires = ops[1].package.requires + assert len(locked_a_requires) == 2 + assert {str(r.marker) for r in locked_a_requires} == { + 'extra == "extra-b1"', + 'extra == "extra-b2"', + } + + @pytest.mark.parametrize("enabled_extra", ["one", "two", None]) def test_solver_returns_extras_only_requested_nested( solver: Solver, @@ -3118,17 +3177,30 @@ def test_solver_chooses_from_correct_repository_if_forced( assert ops[0].package.source_url == legacy_repository.url +@pytest.mark.parametrize("project_dependencies", [True, False]) def test_solver_chooses_from_correct_repository_if_forced_and_transitive_dependency( package: ProjectPackage, io: NullIO, legacy_repository: LegacyRepository, pypi_repository: PyPiRepository, + project_dependencies: bool, ) -> None: package.python_versions = "^3.7" - package.add_dependency(Factory.create_dependency("foo", "^1.0")) - package.add_dependency( - Factory.create_dependency("tomlkit", {"version": "^0.5", "source": "legacy"}) - ) + if project_dependencies: + main_group = DependencyGroup(MAIN_GROUP) + package.add_dependency_group(main_group) + main_group.add_dependency(Factory.create_dependency("foo", "^1.0")) + main_group.add_dependency(Factory.create_dependency("tomlkit", "^0.5")) + main_group.add_poetry_dependency( + Factory.create_dependency("tomlkit", {"source": "legacy"}) + ) + else: + package.add_dependency(Factory.create_dependency("foo", "^1.0")) + package.add_dependency( + Factory.create_dependency( + "tomlkit", {"version": "^0.5", "source": "legacy"} + ) + ) repo = Repository("repo") foo = get_package("foo", "1.0.0") @@ -3164,7 +3236,7 @@ def test_solver_chooses_from_correct_repository_if_forced_and_transitive_depende assert ops[1].package.source_url is None -def test_solver_does_not_choose_from_secondary_repository_by_default( +def test_solver_does_not_choose_from_supplemental_repository_by_default( package: ProjectPackage, io: NullIO, legacy_repository: LegacyRepository, @@ -3174,7 +3246,7 @@ def test_solver_does_not_choose_from_secondary_repository_by_default( package.add_dependency(Factory.create_dependency("clikit", {"version": "^0.2.0"})) pool = RepositoryPool() - pool.add_repository(pypi_repository, priority=Priority.SECONDARY) + pool.add_repository(pypi_repository, priority=Priority.SUPPLEMENTAL) pool.add_repository(legacy_repository) solver = Solver(package, pool, [], [], io) @@ -3214,7 +3286,7 @@ def test_solver_does_not_choose_from_secondary_repository_by_default( assert ops[2].package.source_url == legacy_repository.url -def test_solver_chooses_from_secondary_if_explicit( +def test_solver_chooses_from_supplemental_if_explicit( package: ProjectPackage, io: NullIO, legacy_repository: LegacyRepository, @@ -3226,7 +3298,7 @@ def test_solver_chooses_from_secondary_if_explicit( ) pool = RepositoryPool() - pool.add_repository(pypi_repository, priority=Priority.SECONDARY) + pool.add_repository(pypi_repository, priority=Priority.SUPPLEMENTAL) pool.add_repository(legacy_repository) solver = Solver(package, pool, [], [], io) @@ -3796,20 +3868,6 @@ def test_solver_synchronize_single( ) -@pytest.mark.skip(reason="Poetry no longer has critical package requirements") -def test_solver_with_synchronization_keeps_critical_package( - package: ProjectPackage, - pool: RepositoryPool, - io: NullIO, -) -> None: - package_pip = get_package("setuptools", "1.0") - - solver = Solver(package, pool, [package_pip], [], io) - transaction = solver.solve() - - check_solver_result(transaction, []) - - def test_solver_cannot_choose_another_version_for_directory_dependencies( solver: Solver, repo: Repository, diff --git a/tests/pyproject/test_pyproject_toml_file.py b/tests/pyproject/test_pyproject_toml_file.py index e8fea9000ec..54a326716aa 100644 --- a/tests/pyproject/test_pyproject_toml_file.py +++ b/tests/pyproject/test_pyproject_toml_file.py @@ -4,7 +4,7 @@ import pytest -from poetry.core.exceptions import PoetryCoreException +from poetry.core.exceptions import PoetryCoreError from poetry.toml import TOMLFile @@ -17,13 +17,7 @@ def test_pyproject_toml_file_invalid(pyproject_toml: Path) -> None: with pyproject_toml.open(mode="a", encoding="utf-8") as f: f.write("<<<<<<<<<<<") - with pytest.raises(PoetryCoreException) as excval: + with pytest.raises(PoetryCoreError) 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) - with pytest.warns(DeprecationWarning): - assert file.parent == tmp_path diff --git a/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-src-dir-2.3.4.dist-info/METADATA b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-src-dir-2.3.4.dist-info/METADATA new file mode 100644 index 00000000000..93113ecebfd --- /dev/null +++ b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-src-dir-2.3.4.dist-info/METADATA @@ -0,0 +1,22 @@ +Metadata-Version: 2.1 +Name: editable-src-dir +Version: 2.3.4 +Summary: Editable description. +License: MIT +Keywords: cli,commands +Author: Foo Bar +Author-email: foo@bar.com +Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Description-Content-Type: text/x-rst + +Editable +#### diff --git a/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-src-dir.pth b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-src-dir.pth new file mode 100644 index 00000000000..c40b340604b --- /dev/null +++ b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-src-dir.pth @@ -0,0 +1 @@ +/path/to/editable/src diff --git a/tests/repositories/fixtures/pypi.org/json/attrs.json b/tests/repositories/fixtures/pypi.org/json/attrs.json index 3b0db9b129e..b6ca989bcf9 100644 --- a/tests/repositories/fixtures/pypi.org/json/attrs.json +++ b/tests/repositories/fixtures/pypi.org/json/attrs.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "attrs", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/black.json b/tests/repositories/fixtures/pypi.org/json/black.json index dd8177597f4..9b7eaf3205b 100644 --- a/tests/repositories/fixtures/pypi.org/json/black.json +++ b/tests/repositories/fixtures/pypi.org/json/black.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -67,7 +68,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "black", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/cleo.json b/tests/repositories/fixtures/pypi.org/json/cleo.json index 69ae3afc0d8..ea54aa71951 100644 --- a/tests/repositories/fixtures/pypi.org/json/cleo.json +++ b/tests/repositories/fixtures/pypi.org/json/cleo.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "cleo", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/clikit.json b/tests/repositories/fixtures/pypi.org/json/clikit.json index bd12deb0357..90754da338c 100644 --- a/tests/repositories/fixtures/pypi.org/json/clikit.json +++ b/tests/repositories/fixtures/pypi.org/json/clikit.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "clikit", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/colorama.json b/tests/repositories/fixtures/pypi.org/json/colorama.json index c58033e3674..87016aa6644 100644 --- a/tests/repositories/fixtures/pypi.org/json/colorama.json +++ b/tests/repositories/fixtures/pypi.org/json/colorama.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "colorama", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/discord-py.json b/tests/repositories/fixtures/pypi.org/json/discord-py.json index 1f5ab7ebab1..c51cb10a343 100644 --- a/tests/repositories/fixtures/pypi.org/json/discord-py.json +++ b/tests/repositories/fixtures/pypi.org/json/discord-py.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "discord-py", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/filecache.json b/tests/repositories/fixtures/pypi.org/json/filecache.json index a031d57165f..8c52b03c348 100644 --- a/tests/repositories/fixtures/pypi.org/json/filecache.json +++ b/tests/repositories/fixtures/pypi.org/json/filecache.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -33,7 +34,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "filecache", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/funcsigs.json b/tests/repositories/fixtures/pypi.org/json/funcsigs.json index df218b6264d..9bd98b65abd 100644 --- a/tests/repositories/fixtures/pypi.org/json/funcsigs.json +++ b/tests/repositories/fixtures/pypi.org/json/funcsigs.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -33,7 +34,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "funcsigs", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/futures.json b/tests/repositories/fixtures/pypi.org/json/futures.json index 6e889437873..d51da2b7b86 100644 --- a/tests/repositories/fixtures/pypi.org/json/futures.json +++ b/tests/repositories/fixtures/pypi.org/json/futures.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "futures", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/hbmqtt.json b/tests/repositories/fixtures/pypi.org/json/hbmqtt.json index 8efff2776ac..8f2b2ea5d04 100644 --- a/tests/repositories/fixtures/pypi.org/json/hbmqtt.json +++ b/tests/repositories/fixtures/pypi.org/json/hbmqtt.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": false, @@ -43,7 +44,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "hbmqtt", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/importlib-metadata.json b/tests/repositories/fixtures/pypi.org/json/importlib-metadata.json index 5ac034226d3..7b7b8326db2 100644 --- a/tests/repositories/fixtures/pypi.org/json/importlib-metadata.json +++ b/tests/repositories/fixtures/pypi.org/json/importlib-metadata.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -33,7 +34,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "importlib-metadata", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/ipython.json b/tests/repositories/fixtures/pypi.org/json/ipython.json index d4a0c25b84e..3923464b660 100644 --- a/tests/repositories/fixtures/pypi.org/json/ipython.json +++ b/tests/repositories/fixtures/pypi.org/json/ipython.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -128,7 +129,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "ipython", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/isort.json b/tests/repositories/fixtures/pypi.org/json/isort.json index d2c0cf86c28..97350f08598 100644 --- a/tests/repositories/fixtures/pypi.org/json/isort.json +++ b/tests/repositories/fixtures/pypi.org/json/isort.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -53,7 +54,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "isort", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/jupyter.json b/tests/repositories/fixtures/pypi.org/json/jupyter.json index 7bd87597f92..bc53690ec52 100644 --- a/tests/repositories/fixtures/pypi.org/json/jupyter.json +++ b/tests/repositories/fixtures/pypi.org/json/jupyter.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -49,7 +50,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "jupyter", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/more-itertools.json b/tests/repositories/fixtures/pypi.org/json/more-itertools.json index 9c3f884f82f..3495aa2675e 100644 --- a/tests/repositories/fixtures/pypi.org/json/more-itertools.json +++ b/tests/repositories/fixtures/pypi.org/json/more-itertools.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": false, @@ -53,7 +54,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "more-itertools", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/pastel.json b/tests/repositories/fixtures/pypi.org/json/pastel.json index b5ef49183fe..53d3fbe4dcd 100644 --- a/tests/repositories/fixtures/pypi.org/json/pastel.json +++ b/tests/repositories/fixtures/pypi.org/json/pastel.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "pastel", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/pluggy.json b/tests/repositories/fixtures/pypi.org/json/pluggy.json index 112bbdd40e4..7fa6328f8aa 100644 --- a/tests/repositories/fixtures/pypi.org/json/pluggy.json +++ b/tests/repositories/fixtures/pypi.org/json/pluggy.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -53,7 +54,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "pluggy", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/poetry-core.json b/tests/repositories/fixtures/pypi.org/json/poetry-core.json index 8ae5d6a07aa..a2ba789ca6e 100644 --- a/tests/repositories/fixtures/pypi.org/json/poetry-core.json +++ b/tests/repositories/fixtures/pypi.org/json/poetry-core.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "poetry-core", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/py.json b/tests/repositories/fixtures/pypi.org/json/py.json index 4d0c746b260..a8505d8419a 100644 --- a/tests/repositories/fixtures/pypi.org/json/py.json +++ b/tests/repositories/fixtures/pypi.org/json/py.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "py", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/pylev.json b/tests/repositories/fixtures/pypi.org/json/pylev.json index 80756bda9a1..938ebdbaf76 100644 --- a/tests/repositories/fixtures/pypi.org/json/pylev.json +++ b/tests/repositories/fixtures/pypi.org/json/pylev.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -33,7 +34,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "pylev", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/pytest.json b/tests/repositories/fixtures/pypi.org/json/pytest.json index 40cc11d5521..efd8fcf7df1 100644 --- a/tests/repositories/fixtures/pypi.org/json/pytest.json +++ b/tests/repositories/fixtures/pypi.org/json/pytest.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -67,7 +68,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "pytest", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/python-language-server.json b/tests/repositories/fixtures/pypi.org/json/python-language-server.json index a8e3b08a408..90e914942d7 100644 --- a/tests/repositories/fixtures/pypi.org/json/python-language-server.json +++ b/tests/repositories/fixtures/pypi.org/json/python-language-server.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": false, @@ -17,7 +18,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "python-language-server", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/pyyaml.json b/tests/repositories/fixtures/pypi.org/json/pyyaml.json index cc61c6aec04..565a6a2be2c 100644 --- a/tests/repositories/fixtures/pypi.org/json/pyyaml.json +++ b/tests/repositories/fixtures/pypi.org/json/pyyaml.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -186,7 +187,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "pyyaml", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/requests.json b/tests/repositories/fixtures/pypi.org/json/requests.json index ab38c2b3721..593fda91f15 100644 --- a/tests/repositories/fixtures/pypi.org/json/requests.json +++ b/tests/repositories/fixtures/pypi.org/json/requests.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -185,7 +186,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "requests", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/setuptools.json b/tests/repositories/fixtures/pypi.org/json/setuptools.json index 0600461c7c1..7c53cc48f06 100644 --- a/tests/repositories/fixtures/pypi.org/json/setuptools.json +++ b/tests/repositories/fixtures/pypi.org/json/setuptools.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -65,7 +66,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "setuptools", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/six.json b/tests/repositories/fixtures/pypi.org/json/six.json index 2a0a55d8731..976db9ec16e 100644 --- a/tests/repositories/fixtures/pypi.org/json/six.json +++ b/tests/repositories/fixtures/pypi.org/json/six.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "six", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/sqlalchemy.json b/tests/repositories/fixtures/pypi.org/json/sqlalchemy.json index 23a407178d9..2bbabf10809 100644 --- a/tests/repositories/fixtures/pypi.org/json/sqlalchemy.json +++ b/tests/repositories/fixtures/pypi.org/json/sqlalchemy.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": false, @@ -17,7 +18,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "sqlalchemy", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/toga.json b/tests/repositories/fixtures/pypi.org/json/toga.json index 04a26e3954c..d7561dcb2b5 100644 --- a/tests/repositories/fixtures/pypi.org/json/toga.json +++ b/tests/repositories/fixtures/pypi.org/json/toga.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -123,7 +124,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "toga", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/tomlkit.json b/tests/repositories/fixtures/pypi.org/json/tomlkit.json index d3ba9b77059..9cfa90929c2 100644 --- a/tests/repositories/fixtures/pypi.org/json/tomlkit.json +++ b/tests/repositories/fixtures/pypi.org/json/tomlkit.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -67,7 +68,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "tomlkit", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/twisted.json b/tests/repositories/fixtures/pypi.org/json/twisted.json index 66825247bab..efa14ce6d53 100644 --- a/tests/repositories/fixtures/pypi.org/json/twisted.json +++ b/tests/repositories/fixtures/pypi.org/json/twisted.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": false, @@ -17,7 +18,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "twisted", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/wheel.json b/tests/repositories/fixtures/pypi.org/json/wheel.json index 5138e55f85e..ddd882c8463 100644 --- a/tests/repositories/fixtures/pypi.org/json/wheel.json +++ b/tests/repositories/fixtures/pypi.org/json/wheel.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "wheel", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/zipp.json b/tests/repositories/fixtures/pypi.org/json/zipp.json index 04b282766a3..d34d5523ac4 100644 --- a/tests/repositories/fixtures/pypi.org/json/zipp.json +++ b/tests/repositories/fixtures/pypi.org/json/zipp.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "zipp", "versions": [ diff --git a/tests/repositories/test_http_repository.py b/tests/repositories/test_http_repository.py index 64a5ba6b176..fa1c9cc417c 100644 --- a/tests/repositories/test_http_repository.py +++ b/tests/repositories/test_http_repository.py @@ -14,9 +14,9 @@ from poetry.core.packages.utils.link import Link from poetry.inspection.info import PackageInfoError -from poetry.inspection.lazy_wheel import HTTPRangeRequestUnsupported +from poetry.inspection.lazy_wheel import HTTPRangeRequestUnsupportedError from poetry.repositories.http_repository import HTTPRepository -from poetry.utils.helpers import HTTPRangeRequestSupported +from poetry.utils.helpers import HTTPRangeRequestSupportedError if TYPE_CHECKING: @@ -121,7 +121,7 @@ def test_get_info_from_wheel_state_sequence(mocker: MockerFixture) -> None: repo = MockRepository() # 1. range request and download - mock_metadata_from_wheel_url.side_effect = HTTPRangeRequestUnsupported + mock_metadata_from_wheel_url.side_effect = HTTPRangeRequestUnsupportedError with contextlib.suppress(PackageInfoError): repo._get_info_from_wheel(link) @@ -140,7 +140,7 @@ def test_get_info_from_wheel_state_sequence(mocker: MockerFixture) -> None: # 3. download and range request mock_metadata_from_wheel_url.side_effect = None - mock_download.side_effect = HTTPRangeRequestSupported + mock_download.side_effect = HTTPRangeRequestSupportedError with contextlib.suppress(PackageInfoError): repo._get_info_from_wheel(link) @@ -157,7 +157,7 @@ def test_get_info_from_wheel_state_sequence(mocker: MockerFixture) -> None: assert mock_download.call_count == 3 # 5. range request and download - mock_metadata_from_wheel_url.side_effect = HTTPRangeRequestUnsupported + mock_metadata_from_wheel_url.side_effect = HTTPRangeRequestUnsupportedError mock_download.side_effect = None with contextlib.suppress(PackageInfoError): diff --git a/tests/repositories/test_installed_repository.py b/tests/repositories/test_installed_repository.py index 0645f0256bf..2f309dfea24 100644 --- a/tests/repositories/test_installed_repository.py +++ b/tests/repositories/test_installed_repository.py @@ -4,6 +4,7 @@ import shutil import zipfile +from functools import cached_property from pathlib import Path from typing import TYPE_CHECKING from typing import Iterator @@ -21,8 +22,8 @@ if TYPE_CHECKING: - from _pytest.logging import LogCaptureFixture from poetry.core.packages.package import Package + from pytest import LogCaptureFixture from pytest_mock.plugin import MockerFixture from poetry.poetry import Poetry @@ -69,6 +70,7 @@ def installed_results( ), 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-src-dir-2.3.4.dist-info"), metadata.PathDistribution( site_purelib / "editable-with-import-2.3.4.dist-info" ), @@ -95,7 +97,7 @@ def env( env_dir: Path, site_purelib: Path, site_platlib: Path, src_dir: Path ) -> MockEnv: class _MockEnv(MockEnv): - @property + @cached_property def paths(self) -> dict[str, str]: return { "purelib": site_purelib.as_posix(), @@ -201,6 +203,30 @@ def test_load_successful_with_invalid_distribution( assert str(invalid_dist_info) in message +def test_loads_in_correct_sys_path_order( + tmp_path: Path, current_python: tuple[int, int, int], fixture_dir: FixtureDirGetter +) -> None: + path1 = tmp_path / "path1" + path1.mkdir() + path2 = tmp_path / "path2" + path2.mkdir() + env = MockEnv(path=tmp_path, sys_path=[str(path1), str(path2)]) + fixtures = fixture_dir("project_plugins") + dist_info_1 = "my_application_plugin-1.0.dist-info" + dist_info_2 = "my_application_plugin-2.0.dist-info" + dist_info_other = "my_other_plugin-1.0.dist-info" + shutil.copytree(fixtures / dist_info_1, path1 / dist_info_1) + shutil.copytree(fixtures / dist_info_2, path2 / dist_info_2) + shutil.copytree(fixtures / dist_info_other, path2 / dist_info_other) + + repo = InstalledRepository.load(env) + + assert {f"{p.name} {p.version}" for p in repo.packages} == { + "my-application-plugin 1.0", + "my-other-plugin 1.0", + } + + def test_load_ensure_isolation(repository: InstalledRepository) -> None: package = get_package_from_repository("attrs", repository) assert package is None @@ -262,6 +288,18 @@ def test_load_editable_package( assert editable.source_url == editable_source_directory_path +def test_load_editable_src_dir_package( + repository: InstalledRepository, editable_source_directory_path: str +) -> None: + # test editable package with src layout with text .pth file + editable = get_package_from_repository("editable-src-dir", repository) + assert editable is not None + assert editable.name == "editable-src-dir" + assert editable.version.text == "2.3.4" + assert editable.source_type == "directory" + assert editable.source_url == editable_source_directory_path + + 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) @@ -398,3 +436,30 @@ def test_system_site_packages_source_type( package.name: package.source_type for package in installed_repository.packages } assert source_types == {"cleo": None, "directory-pep-610": "directory"} + + +def test_pipx_shared_lib_site_packages( + tmp_path: Path, + poetry: Poetry, + site_purelib: Path, + caplog: LogCaptureFixture, +) -> None: + """ + Simulate pipx shared/lib/site-packages which is not relative to the venv path. + """ + venv_path = tmp_path / "venv" + shared_lib_site_path = tmp_path / "site" + env = MockEnv( + path=venv_path, sys_path=[str(venv_path / "purelib"), str(shared_lib_site_path)] + ) + dist_info = "cleo-0.7.6.dist-info" + shutil.copytree(site_purelib / dist_info, shared_lib_site_path / dist_info) + installed_repository = InstalledRepository.load(env) + + assert len(installed_repository.packages) == 1 + cleo_package = installed_repository.packages[0] + cleo_package.to_dependency() + # There must not be a warning + # that the package does not seem to be a valid Python package. + assert caplog.messages == [] + assert cleo_package.source_type is None diff --git a/tests/repositories/test_legacy_repository.py b/tests/repositories/test_legacy_repository.py index f4d955fbae6..af9c7bd9cfe 100644 --- a/tests/repositories/test_legacy_repository.py +++ b/tests/repositories/test_legacy_repository.py @@ -14,7 +14,7 @@ from poetry.core.packages.utils.link import Link from poetry.factory import Factory -from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.exceptions import RepositoryError from poetry.repositories.legacy_repository import LegacyRepository @@ -22,7 +22,7 @@ if TYPE_CHECKING: import httpretty - from _pytest.monkeypatch import MonkeyPatch + from pytest import MonkeyPatch from pytest_mock import MockerFixture from poetry.config.config import Config @@ -124,7 +124,7 @@ def test_sdist_format_support(legacy_repository: LegacyRepository) -> None: def test_missing_version(legacy_repository: LegacyRepository) -> None: repo = legacy_repository - with pytest.raises(PackageNotFound): + with pytest.raises(PackageNotFoundError): repo._get_release_info( canonicalize_name("missing_version"), Version.parse("1.1.0") ) @@ -562,7 +562,7 @@ def test_get_40x_and_returns_none( ) -> None: repo = MockHttpRepository({"/foo/": status_code}, http) - with pytest.raises(PackageNotFound): + with pytest.raises(PackageNotFoundError): repo.get_page("foo") diff --git a/tests/repositories/test_repository_pool.py b/tests/repositories/test_repository_pool.py index 87d5782170e..4a08fde1787 100644 --- a/tests/repositories/test_repository_pool.py +++ b/tests/repositories/test_repository_pool.py @@ -6,7 +6,7 @@ from poetry.repositories import Repository from poetry.repositories import RepositoryPool -from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.legacy_repository import LegacyRepository from poetry.repositories.repository_pool import Priority from tests.helpers import get_dependency @@ -17,7 +17,6 @@ def test_pool() -> None: pool = RepositoryPool() assert len(pool.repositories) == 0 - assert not pool.has_default() assert not pool.has_primary_repositories() @@ -26,7 +25,6 @@ def test_pool_with_initial_repositories() -> None: 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 @@ -38,17 +36,6 @@ def test_repository_no_repository() -> None: pool.repository("foo") -def test_repository_deprecated_ignore_repository_names() -> None: - with pytest.warns(DeprecationWarning): - RepositoryPool(ignore_repository_names=True) - with pytest.warns(DeprecationWarning): - RepositoryPool(ignore_repository_names=False) - with pytest.warns(DeprecationWarning): - RepositoryPool(None, True) - with pytest.warns(DeprecationWarning): - RepositoryPool(None, False) - - def test_adding_repositories_with_same_name_twice_raises_value_error() -> None: repo1 = Repository("repo") repo2 = Repository("repo") @@ -71,30 +58,7 @@ def test_repository_from_single_repo_pool(priority: Priority) -> None: 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 = 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_all_prio_repositories() -> None: - secondary = LegacyRepository("secondary", "https://secondary.com") - default = LegacyRepository("default", "https://default.com") supplemental = LegacyRepository("supplemental", "https://supplemental.com") repo1 = LegacyRepository("foo", "https://foo.bar") repo2 = LegacyRepository("bar", "https://bar.baz") @@ -102,47 +66,39 @@ def test_repository_with_all_prio_repositories() -> None: pool = RepositoryPool() pool.add_repository(repo1) - pool.add_repository(secondary, priority=Priority.SECONDARY) pool.add_repository(repo2) pool.add_repository(supplemental, priority=Priority.SUPPLEMENTAL) 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("supplemental") is supplemental assert pool.repository("explicit") is explicit - assert pool.has_default() assert pool.has_primary_repositories() -def test_repository_secondary_and_supplemental_repositories_do_show() -> None: - secondary = LegacyRepository("secondary", "https://secondary.com") +def test_repository_supplemental_repositories_do_show() -> None: supplemental = LegacyRepository("supplemental", "https://supplemental.com") pool = RepositoryPool() - pool.add_repository(secondary, priority=Priority.SECONDARY) pool.add_repository(supplemental, priority=Priority.SUPPLEMENTAL) - assert pool.repository("secondary") is secondary assert pool.repository("supplemental") is supplemental - assert pool.repositories == [secondary, supplemental] + assert pool.repositories == [supplemental] def test_repository_explicit_repositories_do_not_show() -> None: explicit = LegacyRepository("explicit", "https://explicit.com") - default = LegacyRepository("default", "https://default.com") + primary = LegacyRepository("primary", "https://primary.com") pool = RepositoryPool() pool.add_repository(explicit, priority=Priority.EXPLICIT) - pool.add_repository(default, priority=Priority.DEFAULT) + pool.add_repository(primary, priority=Priority.PRIMARY) assert pool.repository("explicit") is explicit - assert pool.repository("default") is default - assert pool.repositories == [default] - assert pool.all_repositories == [default, explicit] + assert pool.repository("primary") is primary + assert pool.repositories == [primary] + assert pool.all_repositories == [primary, explicit] def test_remove_non_existing_repository_raises_indexerror() -> None: @@ -168,65 +124,26 @@ def test_remove_existing_repository_successful() -> None: assert pool.repository("baz") is repo3 -def test_remove_default_repository() -> None: - default = LegacyRepository("default", "https://default.com") - repo1 = LegacyRepository("foo", "https://foo.bar") - repo2 = LegacyRepository("bar", "https://bar.baz") - new_default = LegacyRepository("new_default", "https://new.default.com") - - pool = RepositoryPool() - pool.add_repository(repo1) - pool.add_repository(repo2) - 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, priority=Priority.DEFAULT) - - assert pool.get_priority("new_default") is Priority.DEFAULT - assert pool.has_default() - - def test_repository_ordering() -> None: - default1 = LegacyRepository("default1", "https://default1.com") - default2 = LegacyRepository("default2", "https://default2.com") primary1 = LegacyRepository("primary1", "https://primary1.com") primary2 = LegacyRepository("primary2", "https://primary2.com") primary3 = LegacyRepository("primary3", "https://primary3.com") - secondary1 = LegacyRepository("secondary1", "https://secondary1.com") - secondary2 = LegacyRepository("secondary2", "https://secondary2.com") - secondary3 = LegacyRepository("secondary3", "https://secondary3.com") supplemental = LegacyRepository("supplemental", "https://supplemental.com") pool = RepositoryPool() - pool.add_repository(secondary1, priority=Priority.SECONDARY) pool.add_repository(supplemental, priority=Priority.SUPPLEMENTAL) pool.add_repository(primary1) - pool.add_repository(default1, priority=Priority.DEFAULT) pool.add_repository(primary2) - pool.add_repository(secondary2, priority=Priority.SECONDARY) pool.remove_repository("primary2") - pool.remove_repository("secondary2") pool.add_repository(primary3) - pool.add_repository(secondary3, priority=Priority.SECONDARY) assert pool.repositories == [ - default1, primary1, primary3, - secondary1, - secondary3, supplemental, ] - with pytest.raises(ValueError): - pool.add_repository(default2, priority=Priority.DEFAULT) def test_pool_get_package_in_any_repository() -> None: @@ -281,7 +198,7 @@ def test_pool_no_package_from_any_repository_raises_package_not_found() -> None: pool = RepositoryPool() pool.add_repository(Repository("repo")) - with pytest.raises(PackageNotFound): + with pytest.raises(PackageNotFoundError): pool.package("foo", Version.parse("1.0.0")) @@ -291,7 +208,7 @@ def test_pool_no_package_from_specified_repository_raises_package_not_found() -> repo2 = Repository("repo2", [package]) pool = RepositoryPool([repo1, repo2]) - with pytest.raises(PackageNotFound): + with pytest.raises(PackageNotFoundError): pool.package("foo", Version.parse("1.0.0"), repository_name="repo1") diff --git a/tests/repositories/test_single_page_repository.py b/tests/repositories/test_single_page_repository.py index 05789690874..61b8f48501b 100644 --- a/tests/repositories/test_single_page_repository.py +++ b/tests/repositories/test_single_page_repository.py @@ -7,7 +7,7 @@ from poetry.core.packages.dependency import Dependency -from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.link_sources.html import HTMLPage from poetry.repositories.single_page_repository import SinglePageRepository @@ -30,7 +30,7 @@ def __init__(self, page: str) -> None: def _get_page(self, name: NormalizedName) -> HTMLPage: fixture = self.FIXTURES / self.url.rsplit("/", 1)[-1] if not fixture.exists(): - raise PackageNotFound(f"Package [{name}] not found.") + raise PackageNotFoundError(f"Package [{name}] not found.") with fixture.open(encoding="utf-8") as f: return HTMLPage(self._url, f.read()) diff --git a/tests/test_factory.py b/tests/test_factory.py index 5cc433bb3c0..ddd4ae657a4 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -14,7 +14,8 @@ from poetry.core.packages.package import Package from poetry.core.packages.vcs_dependency import VCSDependency -from poetry.exceptions import PoetryException +from poetry.__version__ import __version__ +from poetry.exceptions import PoetryError from poetry.factory import Factory from poetry.plugins.plugin import Plugin from poetry.repositories.exceptions import InvalidSourceError @@ -153,7 +154,7 @@ def test_create_poetry(fixture_dir: FixtureDirGetter) -> None: @pytest.mark.parametrize( ("project",), [ - ("simple_project",), + ("simple_project_legacy",), ("project_with_extras",), ], ) @@ -206,10 +207,18 @@ def test_create_poetry_with_packages_and_includes( {"include": "src_package", "from": "src"}, ] - assert package.include == [ - {"path": "extra_dir/vcs_excluded.txt", "format": []}, - {"path": "notes.txt", "format": []}, - ] + assert package.include in ( + # with https://github.com/python-poetry/poetry-core/pull/773 + [ + {"path": "extra_dir/vcs_excluded.txt"}, + {"path": "notes.txt"}, + ], + # without https://github.com/python-poetry/poetry-core/pull/773 + [ + {"path": "extra_dir/vcs_excluded.txt", "format": []}, + {"path": "notes.txt", "format": []}, + ], + ) def test_create_poetry_with_multi_constraints_dependency( @@ -230,67 +239,33 @@ def test_create_poetry_non_package_mode(fixture_dir: FixtureDirGetter) -> None: assert not poetry.is_package_mode -def test_poetry_with_default_source_legacy( - fixture_dir: FixtureDirGetter, with_simple_keyring: None -) -> None: +def test_create_poetry_version_ok(fixture_dir: FixtureDirGetter) -> None: io = BufferedIO() - poetry = Factory().create_poetry(fixture_dir("with_default_source_legacy"), io=io) + Factory().create_poetry(fixture_dir("self_version_ok"), io=io) - assert len(poetry.pool.repositories) == 1 - assert "Found deprecated key" in io.fetch_error() + assert io.fetch_output() == "" + assert io.fetch_error() == "" -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 len(poetry.pool.repositories) == 1 +def test_create_poetry_version_not_ok(fixture_dir: FixtureDirGetter) -> None: + with pytest.raises(PoetryError) as e: + Factory().create_poetry(fixture_dir("self_version_not_ok")) assert ( - io.fetch_error().strip() - == "Warning: Found deprecated priority 'default' for source 'foo' in" - " pyproject.toml. You can achieve the same effect by changing the priority" - " to 'primary' and putting the source first." + str(e.value) + == f"This project requires Poetry <1.2, but you are using Poetry {__version__}" ) -def test_poetry_with_default_source_and_pypi( - fixture_dir: FixtureDirGetter, with_simple_keyring: None -) -> None: - io = BufferedIO() - poetry = Factory().create_poetry(fixture_dir("with_default_source_and_pypi"), io=io) - - 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.PRIMARY - assert "Warning: Found deprecated key" not in io.fetch_error() - - -def test_poetry_with_default_source_pypi( - fixture_dir: FixtureDirGetter, with_simple_keyring: None -) -> None: - io = BufferedIO() - poetry = Factory().create_poetry(fixture_dir("with_default_source_pypi"), io=io) - - assert len(poetry.pool.repositories) == 1 - assert poetry.pool.has_repository("PyPI") - assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) - assert poetry.pool.get_priority("PyPI") is Priority.DEFAULT - - @pytest.mark.parametrize( "project", - ("with_non_default_source_implicit", "with_non_default_source_explicit"), + ("with_primary_source_implicit", "with_primary_source_explicit"), ) -def test_poetry_with_non_default_source( +def test_poetry_with_primary_source( project: str, fixture_dir: FixtureDirGetter, with_simple_keyring: None ) -> None: io = BufferedIO() poetry = Factory().create_poetry(fixture_dir(project), io=io) - assert not poetry.pool.has_default() assert not poetry.pool.has_repository("PyPI") assert poetry.pool.has_repository("foo") assert poetry.pool.get_priority("foo") is Priority.PRIMARY @@ -298,58 +273,10 @@ def test_poetry_with_non_default_source( assert {repo.name for repo in poetry.pool.repositories} == {"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 poetry.pool.has_repository("PyPI") - assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) - assert poetry.pool.get_priority("PyPI") is Priority.PRIMARY - 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_secondary_source( - fixture_dir: FixtureDirGetter, with_simple_keyring: None -) -> None: - poetry = Factory().create_poetry(fixture_dir("with_non_default_secondary_source")) - - assert poetry.pool.has_repository("PyPI") - assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) - assert poetry.pool.get_priority("PyPI") is Priority.PRIMARY - 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_legacy( - fixture_dir: FixtureDirGetter, - with_simple_keyring: None, -) -> None: - poetry = Factory().create_poetry( - fixture_dir("with_non_default_multiple_secondary_sources_legacy") - ) - - assert poetry.pool.has_repository("PyPI") - assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) - assert poetry.pool.get_priority("PyPI") is Priority.PRIMARY - 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"} - - -def test_poetry_with_non_default_multiple_secondary_sources( +def test_poetry_with_multiple_supplemental_sources( fixture_dir: FixtureDirGetter, with_simple_keyring: None ) -> None: - poetry = Factory().create_poetry( - fixture_dir("with_non_default_multiple_secondary_sources") - ) + poetry = Factory().create_poetry(fixture_dir("with_multiple_supplemental_sources")) assert poetry.pool.has_repository("PyPI") assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) @@ -361,28 +288,11 @@ def test_poetry_with_non_default_multiple_secondary_sources( assert {repo.name for repo in poetry.pool.repositories} == {"PyPI", "foo", "bar"} -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 not poetry.pool.has_repository("PyPI") - assert poetry.pool.has_repository("foo") - assert isinstance(poetry.pool.repository("foo"), LegacyRepository) - assert {repo.name for repo in poetry.pool.repositories} == {"bar", "foo"} - - -def test_poetry_with_non_default_multiple_sources( +def test_poetry_with_multiple_sources( fixture_dir: FixtureDirGetter, with_simple_keyring: None ) -> None: - poetry = Factory().create_poetry(fixture_dir("with_non_default_multiple_sources")) + poetry = Factory().create_poetry(fixture_dir("with_multiple_sources")) - assert not poetry.pool.has_default() assert not poetry.pool.has_repository("PyPI") assert poetry.pool.has_repository("bar") assert isinstance(poetry.pool.repository("bar"), LegacyRepository) @@ -391,29 +301,19 @@ def test_poetry_with_non_default_multiple_sources( assert {repo.name for repo in poetry.pool.repositories} == {"bar", "foo"} -def test_poetry_with_non_default_multiple_sources_pypi( +def test_poetry_with_multiple_sources_pypi( fixture_dir: FixtureDirGetter, with_simple_keyring: None ) -> None: io = BufferedIO() - poetry = Factory().create_poetry( - fixture_dir("with_non_default_multiple_sources_pypi"), io=io - ) + poetry = Factory().create_poetry(fixture_dir("with_multiple_sources_pypi"), io=io) assert len(poetry.pool.repositories) == 4 - 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.PRIMARY # PyPI must be between bar and baz! expected = ["bar", "PyPI", "baz", "foo"] assert [repo.name for repo in poetry.pool.repositories] == expected - error = io.fetch_error() - assert ( - error.strip() - == "Warning: Found deprecated priority 'secondary' for source 'foo' in" - " pyproject.toml. Consider changing the priority to one of the" - " non-deprecated values: 'default', 'primary', 'supplemental', 'explicit'." - ) def test_poetry_with_no_default_source(fixture_dir: FixtureDirGetter) -> None: @@ -476,49 +376,29 @@ def test_poetry_with_explicit_pypi_and_other( def test_poetry_with_pypi_explicit_only( project: str, fixture_dir: FixtureDirGetter, with_simple_keyring: None ) -> None: - with pytest.raises(PoetryException) as e: + with pytest.raises(PoetryError) as e: Factory().create_poetry(fixture_dir(project)) assert str(e.value) == "At least one source must not be configured as 'explicit'." -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 str(e.value) == "Only one repository can be the default." - - -def test_poetry_with_two_default_sources( - fixture_dir: FixtureDirGetter, with_simple_keyring: None -) -> None: - with pytest.raises(ValueError) as e: - Factory().create_poetry(fixture_dir("with_two_default_sources")) - - assert str(e.value) == "Only one repository can be the default." - - def test_validate(fixture_dir: FixtureDirGetter) -> None: complete = TOMLFile(fixture_dir("complete.toml")) pyproject: dict[str, Any] = complete.read() - content = pyproject["tool"]["poetry"] - assert Factory.validate(content) == {"errors": [], "warnings": []} + assert Factory.validate(pyproject) == {"errors": [], "warnings": []} def test_validate_fails(fixture_dir: FixtureDirGetter) -> None: complete = TOMLFile(fixture_dir("complete.toml")) pyproject: dict[str, Any] = complete.read() - content = pyproject["tool"]["poetry"] - content["this key is not in the schema"] = "" + pyproject["tool"]["poetry"]["this key is not in the schema"] = "" expected = ( "Additional properties are not allowed " "('this key is not in the schema' was unexpected)" ) - assert Factory.validate(content) == {"errors": [expected], "warnings": []} + assert Factory.validate(pyproject) == {"errors": [expected], "warnings": []} def test_create_poetry_fails_on_invalid_configuration( @@ -527,20 +407,12 @@ def test_create_poetry_fails_on_invalid_configuration( with pytest.raises(RuntimeError) as e: Factory().create_poetry(fixture_dir("invalid_pyproject")) - fastjsonschema_error = "data must contain ['description'] properties" - custom_error = "The fields ['description'] are required in package mode." - - expected_template = """\ + expected = """\ The Poetry configuration is invalid: - - {schema_error} - Project name (invalid) is same as one of its dependencies """ - expected = { - expected_template.format(schema_error=schema_error) - for schema_error in (fastjsonschema_error, custom_error) - } - assert str(e.value) in expected + assert str(e.value) == expected def test_create_poetry_fails_on_nameless_project( @@ -549,19 +421,12 @@ def test_create_poetry_fails_on_nameless_project( with pytest.raises(RuntimeError) as e: Factory().create_poetry(fixture_dir("nameless_pyproject")) - fastjsonschema_error = "data must contain ['name'] properties" - custom_error = "The fields ['name'] are required in package mode." - - expected_template = """\ + expected = """\ The Poetry configuration is invalid: - - {schema_error} + - Either [project.name] or [tool.poetry.name] is required in package mode. """ - expected = { - expected_template.format(schema_error=schema_error) - for schema_error in (fastjsonschema_error, custom_error) - } - assert str(e.value) in expected + assert str(e.value) == expected def test_create_poetry_with_local_config(fixture_dir: FixtureDirGetter) -> None: @@ -571,7 +436,6 @@ def test_create_poetry_with_local_config(fixture_dir: FixtureDirGetter) -> None: assert not poetry.config.get("virtualenvs.create") assert not poetry.config.get("virtualenvs.options.always-copy") assert not poetry.config.get("virtualenvs.options.no-pip") - assert not poetry.config.get("virtualenvs.options.no-setuptools") assert not poetry.config.get("virtualenvs.options.system-site-packages") diff --git a/tests/utils/env/test_env.py b/tests/utils/env/test_env.py index 43d30c81d89..15aaf4cfac8 100644 --- a/tests/utils/env/test_env.py +++ b/tests/utils/env/test_env.py @@ -313,15 +313,7 @@ def test_env_system_packages_are_relative_to_lib( ("flags", "packages"), [ ({"no-pip": False}, {"pip"}), - ({"no-pip": False, "no-wheel": True}, {"pip"}), - ({"no-pip": False, "no-wheel": False}, {"pip", "wheel"}), ({"no-pip": True}, set()), - ({"no-setuptools": False}, {"setuptools"}), - ({"no-setuptools": True}, set()), - ({"setuptools": "bundle"}, {"setuptools"}), - ({"no-pip": True, "no-setuptools": False}, {"setuptools"}), - ({"no-wheel": False}, {"wheel"}), - ({"wheel": "bundle"}, {"wheel"}), ({}, set()), ], ) @@ -339,14 +331,6 @@ def test_env_no_pip( if package.name != "sqlite3" } - # For python >= 3.12, virtualenv defaults to "--no-setuptools" and "--no-wheel" - # behaviour, so setting these values to False becomes meaningless. - if sys.version_info >= (3, 12): - if not flags.get("no-setuptools", True): - packages.discard("setuptools") - if not flags.get("no-wheel", True): - packages.discard("wheel") - assert installed_packages == packages @@ -508,35 +492,6 @@ def test_build_environment_not_called_without_build_script_specified( assert not env.executed # type: ignore[attr-defined] -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") - encoding = "locale" if sys.version_info >= (3, 10) else None - with python_wrapper.open("w", encoding=encoding) 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 - - def test_command_from_bin_preserves_relative_path(manager: EnvManager) -> None: # https://github.com/python-poetry/poetry/issues/7959 env = manager.get() diff --git a/tests/utils/env/test_env_manager.py b/tests/utils/env/test_env_manager.py index a7b02c54513..e607e239bd5 100644 --- a/tests/utils/env/test_env_manager.py +++ b/tests/utils/env/test_env_manager.py @@ -19,8 +19,8 @@ from poetry.utils.env import EnvManager from poetry.utils.env import IncorrectEnvError from poetry.utils.env import InvalidCurrentPythonVersionError -from poetry.utils.env import NoCompatiblePythonVersionFound -from poetry.utils.env import PythonVersionNotFound +from poetry.utils.env import NoCompatiblePythonVersionFoundError +from poetry.utils.env import PythonVersionNotFoundError from poetry.utils.env.env_manager import EnvsFile from poetry.utils.helpers import remove_directory @@ -29,7 +29,7 @@ from collections.abc import Callable from collections.abc import Iterator - from _pytest.logging import LogCaptureFixture + from pytest import LogCaptureFixture from pytest_mock import MockerFixture from poetry.poetry import Poetry @@ -150,7 +150,6 @@ def test_activate_in_project_venv_no_explicit_config( "always-copy": False, "system-site-packages": False, "no-pip": False, - "no-setuptools": False, }, prompt="simple-project-py3.7", ) @@ -217,7 +216,7 @@ def test_activate_fails_when_python_cannot_be_found( mocker.patch("shutil.which", return_value=None) - with pytest.raises(PythonVersionNotFound) as e: + with pytest.raises(PythonVersionNotFoundError) as e: manager.activate("python3.7") expected_message = "Could not find the python executable python3.7" @@ -932,7 +931,7 @@ def test_create_venv_finds_no_python_executable( poetry.package.python_versions = "^999" - with pytest.raises(NoCompatiblePythonVersionFound) as e: + with pytest.raises(NoCompatiblePythonVersionFoundError) as e: manager.create_venv() expected_message = ( @@ -993,12 +992,15 @@ def test_create_venv_fails_if_no_compatible_python_version_could_be_found( poetry.package.python_versions = "^4.8" - mocker.patch("subprocess.check_output", side_effect=[sys.base_prefix]) + mocker.patch( + "subprocess.check_output", + side_effect=[sys.base_prefix, "/usr/bin/python", "3.9.0"], + ) m = mocker.patch( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) - with pytest.raises(NoCompatiblePythonVersionFound) as e: + with pytest.raises(NoCompatiblePythonVersionFoundError) as e: manager.create_venv() expected_message = ( @@ -1024,7 +1026,7 @@ def test_create_venv_does_not_try_to_find_compatible_versions_with_executable( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) - with pytest.raises(NoCompatiblePythonVersionFound) as e: + with pytest.raises(NoCompatiblePythonVersionFoundError) as e: manager.create_venv(executable=Path("python3.8")) expected_message = ( @@ -1068,7 +1070,7 @@ def test_create_venv_uses_patch_version_to_detect_compatibility( m.assert_called_with( config_virtualenvs_path / f"{venv_name}-py{version.major}.{version.minor}", - executable=None, + executable=Path(sys.executable), flags=venv_flags_default, prompt=f"simple-project-py{version.major}.{version.minor}", ) @@ -1156,7 +1158,9 @@ def test_create_venv_project_name_empty_sets_correct_prompt( manager = EnvManager(poetry) poetry.package.python_versions = "^3.7" - venv_name = manager.generate_env_name("", str(poetry.file.path.parent)) + venv_name = manager.generate_env_name( + "non-package-mode", str(poetry.file.path.parent) + ) mocker.patch("sys.version_info", (2, 7, 16)) mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") @@ -1177,9 +1181,8 @@ def test_create_venv_project_name_empty_sets_correct_prompt( "always-copy": False, "system-site-packages": False, "no-pip": False, - "no-setuptools": False, }, - prompt="virtualenv-py3.7", + prompt="non-package-mode-py3.7", ) @@ -1227,7 +1230,6 @@ def mock_check_output(cmd: str, *args: Any, **kwargs: Any) -> str: "always-copy": False, "system-site-packages": False, "no-pip": False, - "no-setuptools": False, }, prompt="simple-project-py3.5", ) diff --git a/tests/utils/test_authenticator.py b/tests/utils/test_authenticator.py index 23df1f97922..bd25702e296 100644 --- a/tests/utils/test_authenticator.py +++ b/tests/utils/test_authenticator.py @@ -21,8 +21,8 @@ if TYPE_CHECKING: - from _pytest.logging import LogCaptureFixture - from _pytest.monkeypatch import MonkeyPatch + from pytest import LogCaptureFixture + from pytest import MonkeyPatch from pytest_mock import MockerFixture from tests.conftest import Config @@ -190,7 +190,7 @@ def test_authenticator_falls_back_to_keyring_url( dummy_keyring.set_default_service_credential( "https://foo.bar/simple/", - SimpleCredential("foo", "bar"), # type: ignore[no-untyped-call] + SimpleCredential("foo", "bar"), ) authenticator = Authenticator(config, NullIO()) @@ -217,7 +217,7 @@ def test_authenticator_falls_back_to_keyring_netloc( dummy_keyring.set_default_service_credential( "foo.bar", - SimpleCredential("foo", "bar"), # type: ignore[no-untyped-call] + SimpleCredential("foo", "bar"), ) authenticator = Authenticator(config, NullIO()) @@ -483,11 +483,11 @@ def test_authenticator_falls_back_to_keyring_url_matched_by_path( dummy_keyring.set_default_service_credential( "https://foo.bar/alpha/files/simple/", - SimpleCredential("foo", "bar"), # type: ignore[no-untyped-call] + SimpleCredential("foo", "bar"), ) dummy_keyring.set_default_service_credential( "https://foo.bar/beta/files/simple/", - SimpleCredential("foo", "baz"), # type: ignore[no-untyped-call] + SimpleCredential("foo", "baz"), ) authenticator = Authenticator(config, NullIO()) diff --git a/tests/utils/test_helpers.py b/tests/utils/test_helpers.py index a2aa909e936..462cfb636df 100644 --- a/tests/utils/test_helpers.py +++ b/tests/utils/test_helpers.py @@ -12,7 +12,7 @@ from requests.exceptions import ChunkedEncodingError from poetry.utils.helpers import Downloader -from poetry.utils.helpers import HTTPRangeRequestSupported +from poetry.utils.helpers import HTTPRangeRequestSupportedError from poetry.utils.helpers import download_file from poetry.utils.helpers import get_file_hash from poetry.utils.helpers import get_highest_priority_hash_type @@ -261,7 +261,7 @@ def handle_request( dest = tmp_path / filename if accepts_ranges and raise_accepts_ranges: - with pytest.raises(HTTPRangeRequestSupported): + with pytest.raises(HTTPRangeRequestSupportedError): download_file(url, dest, raise_accepts_ranges=raise_accepts_ranges) assert not dest.exists() else: diff --git a/tests/utils/test_password_manager.py b/tests/utils/test_password_manager.py index 6e65322c847..836ca004478 100644 --- a/tests/utils/test_password_manager.py +++ b/tests/utils/test_password_manager.py @@ -14,7 +14,7 @@ if TYPE_CHECKING: - from _pytest.logging import LogCaptureFixture + from pytest import LogCaptureFixture from pytest_mock import MockerFixture from tests.conftest import Config diff --git a/tests/utils/test_python_manager.py b/tests/utils/test_python_manager.py new file mode 100644 index 00000000000..263cd4d572c --- /dev/null +++ b/tests/utils/test_python_manager.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +import os +import subprocess +import sys + +from pathlib import Path +from typing import TYPE_CHECKING + +import pytest + +from cleo.io.null_io import NullIO +from poetry.core.constraints.version import Version + +from poetry.utils.env.python_manager import Python +from tests.utils.env.test_env_manager import check_output_wrapper + + +if TYPE_CHECKING: + from pytest_mock import MockerFixture + + from poetry.config.config import Config + from tests.types import ProjectFactory + + +def test_python_get_version_on_the_fly() -> None: + python = Python(executable=sys.executable) + + assert python.executable == Path(sys.executable) + assert python.version == Version.parse( + ".".join([str(s) for s in sys.version_info[:3]]) + ) + assert python.patch_version == Version.parse( + ".".join([str(s) for s in sys.version_info[:3]]) + ) + assert python.minor_version == Version.parse( + ".".join([str(s) for s in sys.version_info[:2]]) + ) + + +def test_python_get_system_python() -> None: + python = Python.get_system_python() + + assert python.executable == Path(sys.executable) + assert python.version == Version.parse( + ".".join(str(v) for v in sys.version_info[:3]) + ) + + +def test_python_get_preferred_default(config: Config) -> None: + python = Python.get_preferred_python(config) + + assert python.executable == Path(sys.executable) + assert python.version == Version.parse( + ".".join(str(v) for v in sys.version_info[:3]) + ) + + +def test_python_get_preferred_activated(config: Config, mocker: MockerFixture) -> None: + mocker.patch( + "subprocess.check_output", + side_effect=check_output_wrapper(Version.parse("3.7.1")), + ) + config.config["virtualenvs"]["prefer-active-python"] = True + python = Python.get_preferred_python(config) + + assert python.executable.as_posix().startswith("/usr/bin/python") + assert python.version == Version.parse("3.7.1") + + +def test_python_get_preferred_activated_fallback( + config: Config, mocker: MockerFixture +) -> None: + config.config["virtualenvs"]["prefer-active-python"] = True + with mocker.patch( + "subprocess.check_output", + side_effect=subprocess.CalledProcessError(1, "some command"), + ): + python = Python.get_preferred_python(config) + + assert python.executable == Path(sys.executable) + + +def test_fallback_on_detect_active_python(mocker: MockerFixture) -> None: + m = mocker.patch( + "subprocess.check_output", + side_effect=subprocess.CalledProcessError(1, "some command"), + ) + + active_python = Python._detect_active_python(NullIO()) + + 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(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") + encoding = "locale" if sys.version_info >= (3, 10) else None + with python_wrapper.open("w", encoding=encoding) as f: + f.write(f"@echo {wrapped_python}") + os.environ["PATH"] = str(python_wrapper.parent) + os.pathsep + os.environ["PATH"] + + active_python = Python._detect_active_python(NullIO()) + + assert active_python == wrapped_python + + +def test_python_find_compatible(project_factory: ProjectFactory) -> None: + # Note: This test may fail on Windows systems using Python from the Microsoft Store, + # as the executable is named `py.exe`, which is not currently recognized by + # Python.get_compatible_python. This issue will be resolved in #2117. + # However, this does not cause problems in our case because Poetry's own + # Python interpreter is used before attempting to find another compatible version. + fixture = Path(__file__).parent.parent / "fixtures" / "simple_project" + poetry = project_factory("simple-project", source=fixture) + python = Python.get_compatible_python(poetry) + + assert Version.from_parts(3, 4) <= python.version <= Version.from_parts(4, 0)