From 6d57e843b4a709f5a5156c9cc5661cdd0f718979 Mon Sep 17 00:00:00 2001 From: samypr100 <3933065+samypr100@users.noreply.github.com> Date: Wed, 14 Jun 2023 11:44:56 -0400 Subject: [PATCH 01/10] Moving "poetry lock --check" to "poetry check --lock" (#8015) --- docs/cli.md | 13 ++- src/poetry/console/commands/check.py | 27 +++++- src/poetry/console/commands/lock.py | 7 +- tests/console/commands/test_check.py | 124 ++++++++++++++++++++++++++- tests/console/commands/test_lock.py | 10 ++- 5 files changed, 172 insertions(+), 9 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index b5913b69040..5a5fd878f7e 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -625,8 +625,9 @@ As such, `exit` should be used to properly exit the shell and the virtual enviro ## check -The `check` command validates the structure of the `pyproject.toml` file -and returns a detailed report if there are any errors. +The `check` command validates the content of the `pyproject.toml` file +and its consistency with the `poetry.lock` file. +It returns a detailed report if there are any errors. {{% note %}} This command is also available as a pre-commit hook. See [pre-commit hooks]({{< relref "pre-commit-hooks#poetry-check">}}) for more information. @@ -636,6 +637,10 @@ This command is also available as a pre-commit hook. See [pre-commit hooks]({{< poetry check ``` +### Options + +* `--lock`: Verifies that `poetry.lock` exists for the current `pyproject.toml`. + ## search This command searches for packages on a remote index. @@ -659,7 +664,7 @@ poetry lock ### Options -* `--check`: Verify that `poetry.lock` is consistent with `pyproject.toml` +* `--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. ## version @@ -944,7 +949,7 @@ poetry self lock #### Options -* `--check`: Verify that `poetry.lock` is consistent with `pyproject.toml` +* `--check`: Verify that `poetry.lock` is consistent with `pyproject.toml`. (**Deprecated**) * `--no-update`: Do not update locked versions, only refresh lock file. ### self show diff --git a/src/poetry/console/commands/check.py b/src/poetry/console/commands/check.py index 3ca831e7213..733f0a7c5d5 100644 --- a/src/poetry/console/commands/check.py +++ b/src/poetry/console/commands/check.py @@ -1,11 +1,27 @@ from __future__ import annotations +from cleo.helpers import option + from poetry.console.commands.command import Command class CheckCommand(Command): name = "check" - description = "Checks the validity of the pyproject.toml file." + description = ( + "Validates the content of the pyproject.toml file and its" + " consistency with the poetry.lock file." + ) + + options = [ + option( + "lock", + None, + ( + "Checks that poetry.lock exists for the current" + " version of pyproject.toml." + ), + ), + ] def validate_classifiers( self, project_classifiers: set[str] @@ -72,6 +88,15 @@ def handle(self) -> int: check_result["errors"].extend(errors) check_result["warnings"].extend(warnings) + # Verify that lock file is consistent + if self.option("lock") and not self.poetry.locker.is_locked(): + check_result["errors"] += ["poetry.lock was not found."] + if self.poetry.locker.is_locked() and not self.poetry.locker.is_fresh(): + check_result["errors"] += [ + "poetry.lock is not consistent with pyproject.toml. Run `poetry" + " lock [--no-update]` to fix it." + ] + if not check_result["errors"] and not check_result["warnings"]: self.info("All set!") diff --git a/src/poetry/console/commands/lock.py b/src/poetry/console/commands/lock.py index c87f1359dc7..87273a66776 100644 --- a/src/poetry/console/commands/lock.py +++ b/src/poetry/console/commands/lock.py @@ -18,7 +18,8 @@ class LockCommand(InstallerCommand): None, ( "Check that the poetry.lock file corresponds to the current" - " version of pyproject.toml." + " version of pyproject.toml. (Deprecated) Use" + " poetry check --lock instead." ), ), ] @@ -36,6 +37,10 @@ class LockCommand(InstallerCommand): 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 diff --git a/tests/console/commands/test_check.py b/tests/console/commands/test_check.py index 323364c8def..6347d4585fb 100644 --- a/tests/console/commands/test_check.py +++ b/tests/console/commands/test_check.py @@ -4,13 +4,19 @@ import pytest +from poetry.packages import Locker + if TYPE_CHECKING: + import httpretty + from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture + from poetry.poetry import Poetry from tests.types import CommandTesterFactory from tests.types import FixtureDirGetter + from tests.types import ProjectFactory @pytest.fixture() @@ -18,6 +24,36 @@ def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("check") +def _project_factory( + fixture_name: str, + project_factory: ProjectFactory, + fixture_dir: FixtureDirGetter, +) -> Poetry: + source = fixture_dir(fixture_name) + pyproject_content = (source / "pyproject.toml").read_text(encoding="utf-8") + poetry_lock_content = (source / "poetry.lock").read_text(encoding="utf-8") + return project_factory( + name="foobar", + pyproject_content=pyproject_content, + poetry_lock_content=poetry_lock_content, + source=source, + ) + + +@pytest.fixture +def poetry_with_outdated_lockfile( + project_factory: ProjectFactory, fixture_dir: FixtureDirGetter +) -> Poetry: + return _project_factory("outdated_lock", project_factory, fixture_dir) + + +@pytest.fixture +def poetry_with_up_to_date_lockfile( + project_factory: ProjectFactory, fixture_dir: FixtureDirGetter +) -> Poetry: + return _project_factory("up_to_date_lock", project_factory, fixture_dir) + + def test_check_valid(tester: CommandTester) -> None: tester.execute() @@ -39,12 +75,13 @@ def test_check_invalid( new_callable=mocker.PropertyMock, ) - tester.execute() + tester.execute("--lock") expected = """\ Error: 'description' is a required property Error: Project name (invalid) is same as one of its dependencies Error: Unrecognized classifiers: ['Intended Audience :: Clowns']. +Error: poetry.lock was not found. Warning: A wildcard Python dependency is ambiguous.\ Consider specifying a more explicit one. Warning: The "pendulum" dependency specifies the "allows-prereleases" property,\ @@ -74,3 +111,88 @@ def test_check_private( """ assert tester.io.fetch_output() == expected + + +@pytest.mark.parametrize( + ("options", "expected", "expected_status"), + [ + ("", "All set!\n", 0), + ("--lock", "Error: poetry.lock was not found.\n", 1), + ], +) +def test_check_lock_missing( + mocker: MockerFixture, + tester: CommandTester, + fixture_dir: FixtureDirGetter, + options: str, + expected: str, + expected_status: int, +) -> None: + from poetry.toml import TOMLFile + + mocker.patch( + "poetry.poetry.Poetry.file", + return_value=TOMLFile(fixture_dir("private_pyproject") / "pyproject.toml"), + new_callable=mocker.PropertyMock, + ) + + status_code = tester.execute(options) + + assert status_code == expected_status + + if status_code == 0: + assert tester.io.fetch_output() == expected + else: + assert tester.io.fetch_error() == expected + + +@pytest.mark.parametrize("options", ["", "--lock"]) +def test_check_lock_outdated( + command_tester_factory: CommandTesterFactory, + poetry_with_outdated_lockfile: Poetry, + http: type[httpretty.httpretty], + options: str, +) -> None: + http.disable() + + locker = Locker( + lock=poetry_with_outdated_lockfile.pyproject.file.path.parent / "poetry.lock", + local_config=poetry_with_outdated_lockfile.locker._local_config, + ) + poetry_with_outdated_lockfile.set_locker(locker) + + tester = command_tester_factory("check", poetry=poetry_with_outdated_lockfile) + status_code = tester.execute(options) + expected = ( + "Error: poetry.lock is not consistent with pyproject.toml. " + "Run `poetry lock [--no-update]` to fix it.\n" + ) + + assert tester.io.fetch_error() == expected + + # exit with an error + assert status_code == 1 + + +@pytest.mark.parametrize("options", ["", "--lock"]) +def test_check_lock_up_to_date( + command_tester_factory: CommandTesterFactory, + poetry_with_up_to_date_lockfile: Poetry, + http: type[httpretty.httpretty], + options: str, +) -> None: + http.disable() + + locker = Locker( + lock=poetry_with_up_to_date_lockfile.pyproject.file.path.parent / "poetry.lock", + local_config=poetry_with_up_to_date_lockfile.locker._local_config, + ) + poetry_with_up_to_date_lockfile.set_locker(locker) + + tester = command_tester_factory("check", poetry=poetry_with_up_to_date_lockfile) + status_code = tester.execute(options) + expected = "All set!\n" + assert tester.io.fetch_output() == expected + + # exit with an error + assert status_code == 0 diff --git a/tests/console/commands/test_lock.py b/tests/console/commands/test_lock.py index 7a910caccf7..26e0b98277c 100644 --- a/tests/console/commands/test_lock.py +++ b/tests/console/commands/test_lock.py @@ -89,7 +89,7 @@ def poetry_with_invalid_lockfile( return _project_factory("invalid_lock", project_factory, fixture_dir) -def test_lock_check_outdated( +def test_lock_check_outdated_legacy( command_tester_factory: CommandTesterFactory, poetry_with_outdated_lockfile: Poetry, http: type[httpretty.httpretty], @@ -105,6 +105,7 @@ def test_lock_check_outdated( 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: poetry.lock is not consistent with pyproject.toml. " "Run `poetry lock [--no-update]` to fix it.\n" ) @@ -115,7 +116,7 @@ def test_lock_check_outdated( assert status_code == 1 -def test_lock_check_up_to_date( +def test_lock_check_up_to_date_legacy( command_tester_factory: CommandTesterFactory, poetry_with_up_to_date_lockfile: Poetry, http: type[httpretty.httpretty], @@ -133,6 +134,11 @@ def test_lock_check_up_to_date( 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 From f691320294cf076349d8a915687716b43b087bf7 Mon Sep 17 00:00:00 2001 From: "Y.D.X" <73375426+YDX-2147483647@users.noreply.github.com> Date: Sat, 17 Jun 2023 15:40:39 +0800 Subject: [PATCH 02/10] `poetry check` command validates readme (files must exist) (#7444) --- docs/pyproject.md | 6 +++++ src/poetry/console/commands/check.py | 26 +++++++++++++++++-- tests/console/commands/test_check.py | 1 + .../fixtures/invalid_pyproject/pyproject.toml | 1 + tests/fixtures/private_pyproject/README.md | 0 5 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/private_pyproject/README.md diff --git a/docs/pyproject.md b/docs/pyproject.md index a344ffc0db5..f123313d883 100644 --- a/docs/pyproject.md +++ b/docs/pyproject.md @@ -114,6 +114,12 @@ The file(s) can be of any format, but if you intend to publish to PyPI keep the https://packaging.python.org/en/latest/guides/making-a-pypi-friendly-readme/) in mind. README paths are implicitly relative to `pyproject.toml`. +{{% note %}} +Whether paths are case-sensitive follows platform defaults, but it is recommended to keep cases. + +To be specific, you can set `readme = "rEaDmE.mD"` for `README.md` on macOS and Windows, but Linux users can't `poetry install` after cloning your repo. This is because macOS and Windows are case-insensitive and case-preserving. +{{% /note %}} + The contents of the README file(s) are used to populate the [Description field](https://packaging.python.org/en/latest/specifications/core-metadata/#description-optional) of your distribution's metadata (similar to `long_description` in setuptools). diff --git a/src/poetry/console/commands/check.py b/src/poetry/console/commands/check.py index 733f0a7c5d5..95bd9912195 100644 --- a/src/poetry/console/commands/check.py +++ b/src/poetry/console/commands/check.py @@ -1,10 +1,16 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from cleo.helpers import option from poetry.console.commands.command import Command +if TYPE_CHECKING: + from pathlib import Path + + class CheckCommand(Command): name = "check" description = ( @@ -23,7 +29,7 @@ class CheckCommand(Command): ), ] - def validate_classifiers( + def _validate_classifiers( self, project_classifiers: set[str] ) -> tuple[list[str], list[str]]: """Identify unrecognized and deprecated trove classifiers. @@ -73,6 +79,17 @@ def validate_classifiers( return errors, warnings + def _validate_readme(self, readme: str | list[str], poetry_file: Path) -> list[str]: + """Check existence of referenced readme files""" + + readmes = [readme] if isinstance(readme, str) else readme + + errors = [] + for name in readmes: + if not (poetry_file.parent / name).exists(): + errors.append(f"Declared README file does not exist: {name}") + return errors + def handle(self) -> int: from poetry.factory import Factory from poetry.pyproject.toml import PyProjectTOML @@ -84,10 +101,15 @@ def handle(self) -> int: # Validate trove classifiers project_classifiers = set(config.get("classifiers", [])) - errors, warnings = self.validate_classifiers(project_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) + check_result["errors"].extend(errors) + # Verify that lock file is consistent if self.option("lock") and not self.poetry.locker.is_locked(): check_result["errors"] += ["poetry.lock was not found."] diff --git a/tests/console/commands/test_check.py b/tests/console/commands/test_check.py index 6347d4585fb..aeed833002a 100644 --- a/tests/console/commands/test_check.py +++ b/tests/console/commands/test_check.py @@ -81,6 +81,7 @@ def test_check_invalid( Error: 'description' is a required property 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 Error: poetry.lock was not found. Warning: A wildcard Python dependency is ambiguous.\ Consider specifying a more explicit one. diff --git a/tests/fixtures/invalid_pyproject/pyproject.toml b/tests/fixtures/invalid_pyproject/pyproject.toml index 55b0b6282af..bafa0936489 100644 --- a/tests/fixtures/invalid_pyproject/pyproject.toml +++ b/tests/fixtures/invalid_pyproject/pyproject.toml @@ -4,6 +4,7 @@ version = "1.0.0" authors = [ "Foo " ] +readme = "never/exists.md" license = "INVALID" classifiers = [ "Environment :: Console", diff --git a/tests/fixtures/private_pyproject/README.md b/tests/fixtures/private_pyproject/README.md new file mode 100644 index 00000000000..e69de29bb2d From 7f79fac5df1768fc041a6a42430bbc30207baea0 Mon Sep 17 00:00:00 2001 From: David Hotham Date: Sun, 18 Jun 2023 12:32:45 +0100 Subject: [PATCH 03/10] have subprocess deal with text (#8060) cf python-poetry#7313, python-poetry#7643 --- src/poetry/utils/env/base_env.py | 13 +++---- src/poetry/utils/env/env_manager.py | 54 +++++++++++------------------ src/poetry/vcs/git/system.py | 15 ++++---- tests/utils/test_env.py | 2 +- 4 files changed, 34 insertions(+), 50 deletions(-) diff --git a/src/poetry/utils/env/base_env.py b/src/poetry/utils/env/base_env.py index 00957285b62..487a0aa2276 100644 --- a/src/poetry/utils/env/base_env.py +++ b/src/poetry/utils/env/base_env.py @@ -14,8 +14,6 @@ from virtualenv.seed.wheels.embed import get_embed_wheel -from poetry.utils._compat import decode -from poetry.utils._compat import encode from poetry.utils.env.exceptions import EnvCommandError from poetry.utils.env.site_packages import SitePackages from poetry.utils.helpers import get_real_windows_path @@ -343,13 +341,14 @@ def _run(self, cmd: list[str], **kwargs: Any) -> str: try: if input_: - output = subprocess.run( + output: str = subprocess.run( cmd, stdout=subprocess.PIPE, stderr=stderr, - input=encode(input_), + input=input_, check=True, env=env, + text=True, **kwargs, ).stdout elif call: @@ -357,11 +356,13 @@ def _run(self, cmd: list[str], **kwargs: Any) -> str: subprocess.check_call(cmd, stderr=stderr, env=env, **kwargs) output = "" else: - output = subprocess.check_output(cmd, stderr=stderr, env=env, **kwargs) + output = subprocess.check_output( + cmd, stderr=stderr, env=env, text=True, **kwargs + ) except CalledProcessError as e: raise EnvCommandError(e, input=input_) - return decode(output) + return output def execute(self, bin: str, *args: str, **kwargs: Any) -> int: command = self.get_command_from_bin(bin) + list(args) diff --git a/src/poetry/utils/env/env_manager.py b/src/poetry/utils/env/env_manager.py index a2f45ad799e..438dc7455f5 100644 --- a/src/poetry/utils/env/env_manager.py +++ b/src/poetry/utils/env/env_manager.py @@ -23,7 +23,6 @@ from poetry.toml.file import TOMLFile from poetry.utils._compat import WINDOWS -from poetry.utils._compat import decode from poetry.utils._compat import encode from poetry.utils.env.exceptions import EnvCommandError from poetry.utils.env.exceptions import IncorrectEnvError @@ -67,11 +66,9 @@ def _full_python_path(python: str) -> Path | None: return None try: - executable = decode( - subprocess.check_output( - [path_python, "-c", "import sys; print(sys.executable)"], - ).strip() - ) + executable = subprocess.check_output( + [path_python, "-c", "import sys; print(sys.executable)"], text=True + ).strip() return Path(executable) except CalledProcessError: @@ -115,11 +112,9 @@ def get_python_version( executable = EnvManager._detect_active_python(io) if executable: - python_patch = decode( - subprocess.check_output( - [executable, "-c", GET_PYTHON_VERSION_ONELINER], - ).strip() - ) + python_patch = subprocess.check_output( + [executable, "-c", GET_PYTHON_VERSION_ONELINER], text=True + ).strip() version = ".".join(str(v) for v in python_patch.split(".")[:precision]) @@ -150,10 +145,8 @@ def activate(self, python: str) -> Env: raise PythonVersionNotFound(python) try: - python_version_string = decode( - subprocess.check_output( - [python_path, "-c", GET_PYTHON_VERSION_ONELINER], - ) + python_version_string = subprocess.check_output( + [python_path, "-c", GET_PYTHON_VERSION_ONELINER], text=True ) except CalledProcessError as e: raise EnvCommandError(e) @@ -334,10 +327,8 @@ def remove(self, python: str) -> Env: if python_path.is_file(): # Validate env name if provided env is a full path to python try: - env_dir = decode( - subprocess.check_output( - [python, "-c", GET_ENV_PATH_ONELINER], - ) + env_dir = subprocess.check_output( + [python, "-c", GET_ENV_PATH_ONELINER], text=True ).strip("\n") env_name = Path(env_dir).name if not self.check_env_is_for_current_project(env_name, base_env_name): @@ -393,10 +384,8 @@ def remove(self, python: str) -> Env: pass try: - python_version_string = decode( - subprocess.check_output( - [python, "-c", GET_PYTHON_VERSION_ONELINER], - ) + python_version_string = subprocess.check_output( + [python, "-c", GET_PYTHON_VERSION_ONELINER], text=True ) except CalledProcessError as e: raise EnvCommandError(e) @@ -485,11 +474,9 @@ def create_venv( 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: - python_patch = decode( - subprocess.check_output( - [executable, "-c", GET_PYTHON_VERSION_ONELINER], - ).strip() - ) + python_patch = subprocess.check_output( + [executable, "-c", GET_PYTHON_VERSION_ONELINER], text=True + ).strip() python_minor = ".".join(python_patch.split(".")[:2]) supported_python = self._poetry.package.python_constraint @@ -533,12 +520,11 @@ def create_venv( continue try: - python_patch = decode( - subprocess.check_output( - [python, "-c", GET_PYTHON_VERSION_ONELINER], - stderr=subprocess.STDOUT, - ).strip() - ) + python_patch = subprocess.check_output( + [python, "-c", GET_PYTHON_VERSION_ONELINER], + stderr=subprocess.STDOUT, + text=True, + ).strip() except CalledProcessError: continue diff --git a/src/poetry/vcs/git/system.py b/src/poetry/vcs/git/system.py index 6be52747ddf..935fc732344 100644 --- a/src/poetry/vcs/git/system.py +++ b/src/poetry/vcs/git/system.py @@ -53,15 +53,12 @@ def run(*args: Any, **kwargs: Any) -> str: git_command = find_git_command() env = os.environ.copy() env["GIT_TERMINAL_PROMPT"] = "0" - return ( - subprocess.check_output( - git_command + list(args), - stderr=subprocess.STDOUT, - env=env, - ) - .decode() - .strip() - ) + return subprocess.check_output( + git_command + list(args), + stderr=subprocess.STDOUT, + env=env, + text=True, + ).strip() @staticmethod def _check_parameter(parameter: str) -> None: diff --git a/tests/utils/test_env.py b/tests/utils/test_env.py index 0b4395e1c9e..269287dea5e 100644 --- a/tests/utils/test_env.py +++ b/tests/utils/test_env.py @@ -968,7 +968,7 @@ def test_env_has_symlinks_on_nix(tmp_path: Path, tmp_venv: VirtualEnv) -> None: def test_run_with_input(tmp_path: Path, tmp_venv: VirtualEnv) -> None: result = tmp_venv.run("python", "-", input_=MINIMAL_SCRIPT) - assert result == "Minimal Output" + os.linesep + assert result == "Minimal Output\n" def test_run_with_input_non_zero_return(tmp_path: Path, tmp_venv: VirtualEnv) -> None: From fedff6da43f33a350598ec0ea85b5dca30178a03 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Jun 2023 11:01:34 +0200 Subject: [PATCH 04/10] [pre-commit.ci] pre-commit autoupdate (#8116) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d1abfbab76c..812efe2f970 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ repos: - id: black - repo: https://github.com/pre-commit/pre-commit - rev: v3.3.2 + rev: v3.3.3 hooks: - id: validate_manifest From c719dcedb8b39965bb03e69cee63129a18143331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Sat, 24 Jun 2023 02:45:56 -0700 Subject: [PATCH 05/10] Support non SHA256/SHA384/SHA512 HTTPRepository (#8118) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Bernát Gábor --- src/poetry/installation/chooser.py | 7 ++++ src/poetry/repositories/http_repository.py | 38 ++++++++++---------- tests/helpers.py | 3 +- tests/installation/test_chooser.py | 22 ++++++++++++ tests/repositories/fixtures/legacy/demo.html | 6 ++++ 5 files changed, 57 insertions(+), 19 deletions(-) create mode 100644 tests/repositories/fixtures/legacy/demo.html diff --git a/src/poetry/installation/chooser.py b/src/poetry/installation/chooser.py index b484504ed9a..4601189d16d 100644 --- a/src/poetry/installation/chooser.py +++ b/src/poetry/installation/chooser.py @@ -8,6 +8,7 @@ from poetry.config.config import Config from poetry.config.config import PackageFilterPolicy +from poetry.repositories.http_repository import HTTPRepository from poetry.utils.wheel import Wheel @@ -103,6 +104,12 @@ def _get_links(self, package: Package) -> list[Link]: assert link.hash_name is not None h = link.hash_name + ":" + link.hash + if ( + h not in hashes + and link.hash_name not in ("sha256", "sha384", "sha512") + and isinstance(repository, HTTPRepository) + ): + h = repository.calculate_sha256(link) or h if h not in hashes: logger.debug( "Skipping %s as %s checksum does not match expected value", diff --git a/src/poetry/repositories/http_repository.py b/src/poetry/repositories/http_repository.py index 9427d72bfe7..ebf8b1a4d77 100644 --- a/src/poetry/repositories/http_repository.py +++ b/src/poetry/repositories/http_repository.py @@ -226,24 +226,7 @@ def _links_to_data(self, links: list[Link], data: PackageInfo) -> dict[str, Any] and link.hash_name not in ("sha256", "sha384", "sha512") and hasattr(hashlib, link.hash_name) ): - with self._cached_or_downloaded_file(link) as filepath: - known_hash = ( - getattr(hashlib, link.hash_name)() if link.hash_name else None - ) - required_hash = hashlib.sha256() - - chunksize = 4096 - with filepath.open("rb") as f: - while True: - chunk = f.read(chunksize) - if not chunk: - break - if known_hash: - known_hash.update(chunk) - required_hash.update(chunk) - - if not known_hash or known_hash.hexdigest() == link.hash: - file_hash = f"{required_hash.name}:{required_hash.hexdigest()}" + file_hash = self.calculate_sha256(link) or file_hash files.append({"file": link.filename, "hash": file_hash}) @@ -257,6 +240,25 @@ def _links_to_data(self, links: list[Link], data: PackageInfo) -> dict[str, Any] return data.asdict() + def calculate_sha256(self, link: Link) -> str | None: + with self._cached_or_downloaded_file(link) as filepath: + known_hash = getattr(hashlib, link.hash_name)() if link.hash_name else None + required_hash = hashlib.sha256() + + chunksize = 4096 + with filepath.open("rb") as f: + while True: + chunk = f.read(chunksize) + if not chunk: + break + if known_hash: + known_hash.update(chunk) + required_hash.update(chunk) + + if not known_hash or known_hash.hexdigest() == link.hash: + return f"{required_hash.name}:{required_hash.hexdigest()}" + return None + def _get_response(self, endpoint: str) -> requests.Response | None: url = self._url + endpoint try: diff --git a/tests/helpers.py b/tests/helpers.py index eefe9c74c1a..3dd5b1d8915 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -36,6 +36,7 @@ from poetry.installation.operations.operation import Operation from poetry.poetry import Poetry + from poetry.utils.authenticator import Authenticator FIXTURE_PATH = Path(__file__).parent / "fixtures" @@ -120,7 +121,7 @@ def mock_clone( return MockDulwichRepo(dest) -def mock_download(url: str, dest: Path) -> None: +def mock_download(url: str, dest: Path, session: Authenticator | None = None) -> None: parts = urllib.parse.urlparse(url) fixture = FIXTURE_PATH / parts.path.lstrip("/") diff --git a/tests/installation/test_chooser.py b/tests/installation/test_chooser.py index fbd3861ad05..6c9930f899a 100644 --- a/tests/installation/test_chooser.py +++ b/tests/installation/test_chooser.py @@ -405,3 +405,25 @@ def test_chooser_throws_an_error_if_package_hashes_do_not_match( with pytest.raises(RuntimeError) as e: chooser.choose_for(package) assert files[0]["hash"] in str(e) + + +@pytest.mark.usefixtures("mock_legacy") +def test_chooser_md5_remote_fallback_to_sha256_inline_calculation( + env: MockEnv, pool: RepositoryPool +) -> None: + chooser = Chooser(pool, env) + package = Package( + "demo", + "0.1.0", + source_type="legacy", + source_reference="foo", + source_url="https://foo.bar/simple/", + ) + package.files = [ + { + "hash": "sha256:9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad", # noqa: E501 + "filename": "demo-0.1.0.tar.gz", + } + ] + res = chooser.choose_for(package) + assert res.filename == "demo-0.1.0.tar.gz" diff --git a/tests/repositories/fixtures/legacy/demo.html b/tests/repositories/fixtures/legacy/demo.html new file mode 100644 index 00000000000..ae9506cd5a1 --- /dev/null +++ b/tests/repositories/fixtures/legacy/demo.html @@ -0,0 +1,6 @@ + +Simple Index + +demo-0.1.0.tar.gz
+ + From 7ee2a8b4944fc15afd8a6c40cc399974fe89f80e Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 26 Jun 2023 02:21:47 +1000 Subject: [PATCH 06/10] Add warning about pip ignoring lock files (#8117) --- docs/basic-usage.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/basic-usage.md b/docs/basic-usage.md index a22d5280f71..dd872f4a1c5 100644 --- a/docs/basic-usage.md +++ b/docs/basic-usage.md @@ -234,6 +234,10 @@ Even if you develop alone, in six months when reinstalling the project you can f the dependencies installed are still working even if your dependencies released many new versions since then. (See note below about using the update command.) +{{% warning %}} If you have added the recommended [`[build-system]`]({{< relref "pyproject#poetry-and-pep-517" >}}) section to your project's pyproject.toml then you _can_ successfully install your project and its dependencies into a virtual environment using a command like `pip install -e .`. However, pip will not use the lock file to determine dependency versions as the poetry-core build system is intended for library developers (see next section). +{{% /warning %}} + + #### As a library developer Library developers have more to consider. Your users are application developers, and your library will run in a Python environment you don't control. From 5b0cb4be5931a28d7b23083a31e4e2103af44e15 Mon Sep 17 00:00:00 2001 From: Davi Gray <124840911+Davi-Gray@users.noreply.github.com> Date: Tue, 27 Jun 2023 10:35:50 -0500 Subject: [PATCH 07/10] Clarify use of virtualenvs.in-project setting. (#8126) --- docs/configuration.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index d9c074262c3..43be234a587 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -309,12 +309,21 @@ might contain additional Python packages as well. Create the virtualenv inside the project's root directory. -If not set explicitly, `poetry` by default will create virtual environment under -`{cache-dir}/virtualenvs` or use the `{project-dir}/.venv` directory when one is available. +If not set explicitly, `poetry` by default will create a virtual environment under +`{cache-dir}/virtualenvs` or use the `{project-dir}/.venv` directory if one already exists. If set to `true`, the virtualenv will be created and expected in a folder named `.venv` within the root directory of the project. +{{% note %}} +If a virtual environment has already been created for the project under `{cache-dir}/virtualenvs`, setting this variable to `true` will not cause `poetry` to create or use a local virtual environment. + +In order for this setting to take effect for a project already in that state, you must delete the virtual environment folder located in `{cache-dir}/virtualenvs`. + +You can find out where the current project's virtual environment (if there is one) is stored +with the command `poetry env info --path`. +{{% /note %}} + If set to `false`, `poetry` will ignore any existing `.venv` directory. ### `virtualenvs.options.always-copy` From d508c9f05dbebdede74fdee5a995d81bb405d7df Mon Sep 17 00:00:00 2001 From: Micael Jarniac Date: Mon, 3 Jul 2023 19:11:21 -0300 Subject: [PATCH 08/10] docs(pre-commit): fix FAQ grammar (#8150) --- docs/pre-commit-hooks.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/pre-commit-hooks.md b/docs/pre-commit-hooks.md index ab833dab3f3..e526a739079 100644 --- a/docs/pre-commit-hooks.md +++ b/docs/pre-commit-hooks.md @@ -109,16 +109,16 @@ repos: `pre-commit autoupdate` updates the `rev` for each repository defined in your `.pre-commit-config.yaml` to the latest available tag in the default branch. -Poetry follows a branching strategy, where the default branch is the active development branch -and fixes gets back ported to stable branches. New tags are assigned in these stable branches. +Poetry follows a branching strategy where the default branch is the active development branch, +and fixes get backported to stable branches. New tags are assigned in these stable branches. `pre-commit` does not support such a branching strategy and has decided to not implement -an option, either on the [user side](https://github.com/pre-commit/pre-commit/issues/2512) -or [hook author side](https://github.com/pre-commit/pre-commit/issues/2508), to define a branch for lookup the latest -available tag. +an option, either on the [user's side](https://github.com/pre-commit/pre-commit/issues/2512) +or the [hook author's side](https://github.com/pre-commit/pre-commit/issues/2508), to define a branch for looking +up the latest available tag. 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 explicit list repositories that should be updated. An option to explicit exclude +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`. From 79f26683423f12d701d739ec7cab5bdf12e9d97a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Tue, 4 Jul 2023 06:36:28 +0200 Subject: [PATCH 09/10] sources: change future warning that PyPI will only be disabled automatically if there are no primary sources (#8151) --- docs/repositories.md | 2 +- src/poetry/factory.py | 6 +++--- tests/test_factory.py | 8 ++++++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/repositories.md b/docs/repositories.md index 541236c8d04..0446d3ec1cc 100644 --- a/docs/repositories.md +++ b/docs/repositories.md @@ -164,7 +164,7 @@ poetry source add --priority=default foo https://foo.bar/simple/ {{% warning %}} In a future version of Poetry, PyPI will be disabled automatically -if there is at least one custom source configured with another priority than `explicit`. +if at least one custom primary source is configured. If you are using custom sources in addition to PyPI, you should configure PyPI explicitly with a certain priority, e.g. diff --git a/src/poetry/factory.py b/src/poetry/factory.py index 5e926b6177a..828ce26bc24 100644 --- a/src/poetry/factory.py +++ b/src/poetry/factory.py @@ -181,12 +181,12 @@ def create_pool( else: from poetry.repositories.pypi_repository import PyPiRepository - if pool.repositories: + if pool.has_primary_repositories(): io.write_error_line( "" "Warning: In a future version of Poetry, PyPI will be disabled" - " automatically if at least one custom source is configured" - " with another priority than 'explicit'. In order to avoid" + " automatically if at least one custom primary source is" + " configured. In order to avoid" " a breaking change and make your pyproject.toml forward" " compatible, add PyPI explicitly via 'poetry source add pypi'." " By the way, this has the advantage that you can set the" diff --git a/tests/test_factory.py b/tests/test_factory.py index b57030709ea..fcb0bbbc1d2 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -420,7 +420,8 @@ def test_poetry_with_no_default_source(fixture_dir: FixtureDirGetter) -> None: def test_poetry_with_supplemental_source( fixture_dir: FixtureDirGetter, with_simple_keyring: None ) -> None: - poetry = Factory().create_poetry(fixture_dir("with_supplemental_source")) + io = BufferedIO() + poetry = Factory().create_poetry(fixture_dir("with_supplemental_source"), io=io) assert poetry.pool.has_repository("PyPI") assert poetry.pool.get_priority("PyPI") is Priority.DEFAULT @@ -429,12 +430,14 @@ def test_poetry_with_supplemental_source( assert poetry.pool.get_priority("supplemental") is Priority.SUPPLEMENTAL assert isinstance(poetry.pool.repository("supplemental"), LegacyRepository) assert {repo.name for repo in poetry.pool.repositories} == {"PyPI", "supplemental"} + assert io.fetch_error() == "" def test_poetry_with_explicit_source( fixture_dir: FixtureDirGetter, with_simple_keyring: None ) -> None: - poetry = Factory().create_poetry(fixture_dir("with_explicit_source")) + io = BufferedIO() + poetry = Factory().create_poetry(fixture_dir("with_explicit_source"), io=io) assert len(poetry.pool.repositories) == 1 assert len(poetry.pool.all_repositories) == 2 @@ -444,6 +447,7 @@ def test_poetry_with_explicit_source( assert poetry.pool.has_repository("explicit") assert isinstance(poetry.pool.repository("explicit"), LegacyRepository) assert {repo.name for repo in poetry.pool.repositories} == {"PyPI"} + assert io.fetch_error() == "" def test_poetry_with_explicit_pypi_and_other( From d31dfa8312e72f6f16fefb5203a5e3bcac7daece Mon Sep 17 00:00:00 2001 From: johnthagen Date: Thu, 6 Jul 2023 09:09:24 -0400 Subject: [PATCH 10/10] Document official Poetry badge (#8066) --- README.md | 1 + docs/community.md | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 docs/community.md diff --git a/README.md b/README.md index 4de19ca2f08..d0669832d13 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Poetry: Python packaging and dependency management made easy +[![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/) [![Stable Version](https://img.shields.io/pypi/v/poetry?label=stable)][PyPI Releases] [![Pre-release Version](https://img.shields.io/github/v/release/python-poetry/poetry?label=pre-release&include_prereleases&sort=semver)][PyPI Releases] [![Python Versions](https://img.shields.io/pypi/pyversions/poetry)][PyPI] diff --git a/docs/community.md b/docs/community.md new file mode 100644 index 00000000000..d29a08f7936 --- /dev/null +++ b/docs/community.md @@ -0,0 +1,30 @@ +--- +title: "Community" +draft: false +type: docs +layout: single + +menu: + docs: + weight: 105 +--- + +# Community + +## Badge + +For any projects using Poetry, you may add its official badge somewhere prominent like the README. + +[![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/) + +**Markdown** +```md +[![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/) +``` + +**reStructuredText** +```rst +.. image:: https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json + :alt: Poetry + :target: https://python-poetry.org/ +```