Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/poetry/core/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,17 @@ def validate(
for e in validate_object(project, "project-schema")
]
result["errors"] += project_validation_errors

# Ensure license file exists, if project.license.file is set
if project is not None and project.get("license") is not None:
license_data = project.get("license")
if isinstance(license_data, dict) and "file" in license_data:
license_path = Path(license_data["file"]).absolute()
if not license_path.exists():
result["errors"].append(
f"project.license.file '{license_path}' does not exist"
)

# With PEP 621 [tool.poetry] is not mandatory anymore. We still create and
# validate it so that default values (e.g. for package-mode) are set.
tool_poetry = toml_data.setdefault("tool", {}).setdefault("poetry", {})
Expand Down
5 changes: 5 additions & 0 deletions tests/fixtures/missing_license_file/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[project]
name = "my-package"
version = "0.1"
license = { file = "LICENSE" } # License file is intentionally missing
keywords = ["special"] # field that comes after license in core metadata
26 changes: 26 additions & 0 deletions tests/test_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from poetry.core.pyproject.tables import BuildSystem
from poetry.core.utils._compat import tomllib
from poetry.core.version.markers import SingleMarker
from tests.testutils import temporary_cd


if TYPE_CHECKING:
Expand Down Expand Up @@ -636,6 +637,31 @@ def test_validate_fails() -> None:
assert Factory.validate(content) == {"errors": [expected], "warnings": []}


def test_validate_missing_license_file() -> None:
complete = fixtures_dir / "missing_license_file/pyproject.toml"
with complete.open("rb") as f:
content = tomllib.load(f)

with temporary_cd(complete.parent):
license_path = Path(content["project"]["license"]["file"]).absolute()
expected = f"project.license.file '{license_path}' does not exist"

assert Factory.validate(content) == {"errors": [expected], "warnings": []}


def test_validate_existing_license_file() -> None:
# Ensure that no error is reported when a project.license.file is given,
# and the file does exist.
complete = fixtures_dir / "with_license_type_file/pyproject.toml"
with complete.open("rb") as f:
content = tomllib.load(f)

# Still need to cd to avoid spurious pass due to the project's root LICENSE
# file.
with temporary_cd(complete.parent):
assert Factory.validate(content) == {"errors": [], "warnings": []}


def test_validate_without_strict_fails_only_non_strict() -> None:
project_failing_strict_validation = (
fixtures_dir / "project_failing_strict_validation" / "pyproject.toml"
Expand Down
18 changes: 18 additions & 0 deletions tests/testutils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import os
import shutil
import subprocess
import sys
Expand Down Expand Up @@ -29,6 +30,23 @@
}


@contextmanager
def temporary_cd(path: Path) -> Generator[None, None, None]:
"""
Context manager that temporarily CDs into the given directory.

Once the context exits, the cwd is returned to its previous state.

:param path: Path to cd into.
"""
old_path = Path.cwd()
os.chdir(path)
try:
yield
finally:
os.chdir(old_path)


@contextmanager
def temporary_project_directory(
path: Path, toml_patch: dict[str, Any] | None = None
Expand Down
Loading