Skip to content

Commit

Permalink
fix: Handle parsing errors when converting from poetry-style metadata (
Browse files Browse the repository at this point in the history
  • Loading branch information
frostming authored Aug 30, 2023
1 parent 7fd3f43 commit 19b6a1f
Show file tree
Hide file tree
Showing 8 changed files with 57 additions and 4 deletions.
1 change: 1 addition & 0 deletions news/2203.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Handle parsing errors when converting from poetry-style metadata.
1 change: 1 addition & 0 deletions src/pdm/formats/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import TYPE_CHECKING, cast

from pdm.formats import flit, pipfile, poetry, requirements, setup_py
from pdm.formats.base import MetaConvertError as MetaConvertError

if TYPE_CHECKING:
from argparse import Namespace
Expand Down
17 changes: 17 additions & 0 deletions src/pdm/formats/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ class Unset(Exception):
pass


class MetaConvertError(Exception):
"""A special exception that preserves the partial metadata that are already resolved."""

def __init__(self, errors: list[str], *, data: dict[str, Any], settings: dict[str, Any]) -> None:
self.errors = errors
self.data = data
self.settings = settings

def __str__(self) -> str:
return "\n" + "\n".join(self.errors)


class _MetaConverterMeta(type):
def __init__(cls, name: str, bases: tuple[type, ...], ns: dict[str, Any]) -> None:
super().__init__(name, bases, ns)
Expand All @@ -47,6 +59,7 @@ def __init__(self, source: dict, ui: termui.UI | None = None) -> None:

def convert(self) -> tuple[Mapping[str, Any], Mapping[str, Any]]:
source = self.source
errors: list[str] = []
for key, func in self._converters.items():
if func._convert_from and func._convert_from not in source: # type: ignore[attr-defined]
continue
Expand All @@ -55,6 +68,8 @@ def convert(self) -> tuple[Mapping[str, Any], Mapping[str, Any]]:
self._data[key] = func(self, value)
except Unset:
pass
except Exception as e:
errors.append(f"{key}: {e}")

# Delete all used fields
for func in self._converters.values():
Expand All @@ -66,6 +81,8 @@ def convert(self) -> tuple[Mapping[str, Any], Mapping[str, Any]]:
pass
# Add remaining items to the data
self._data.update(source)
if errors:
raise MetaConvertError(errors, data=self._data, settings=self.settings)
return self._data, self.settings


Expand Down
7 changes: 6 additions & 1 deletion src/pdm/models/candidates.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,9 +498,14 @@ def _get_metadata_from_wheel(self, wheel: Path, metadata_parent: str) -> im.Dist

def _get_metadata_from_project(self, pyproject_toml: Path) -> im.Distribution | None:
# Try getting from PEP 621 metadata
from pdm.formats import MetaConvertError
from pdm.project.project_file import PyProject

pyproject = PyProject(pyproject_toml, ui=self.environment.project.core.ui)
try:
pyproject = PyProject(pyproject_toml, ui=self.environment.project.core.ui)
except MetaConvertError as e:
termui.logger.warning("Failed to parse pyproject.toml: %s", e)
return None
metadata = pyproject.metadata.unwrap()
if not metadata:
termui.logger.warning("Failed to parse pyproject.toml")
Expand Down
4 changes: 4 additions & 0 deletions src/pdm/models/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,16 @@ def read_from_directory(cls, directory: Path) -> Setup:
def read_pyproject_toml(file: Path) -> Setup:
from pdm import termui
from pdm.exceptions import ProjectError
from pdm.formats import MetaConvertError
from pdm.project.project_file import PyProject

try:
metadata = PyProject(file, ui=termui.UI()).metadata.unwrap()
except ProjectError:
return Setup()
except MetaConvertError as e:
termui.logger.warning("Error parsing pyproject.toml, metadata may be incomplete. %s", e)
metadata = e.data
return Setup(
name=metadata.get("name"),
summary=metadata.get("description"),
Expand Down
14 changes: 14 additions & 0 deletions tests/fixtures/poetry-error.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[tool.poetry]
name = "test-poetry"
version = "0.1.0"
description = ""
authors = ["Frost Ming <[email protected]>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.11"
foo = ">=1.0||^2.1"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
13 changes: 12 additions & 1 deletion tests/test_formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import pytest

from pdm.formats import flit, pipfile, poetry, requirements, setup_py
from pdm.formats import MetaConvertError, flit, pipfile, poetry, requirements, setup_py
from pdm.models.requirements import parse_requirement
from pdm.utils import cd
from tests import FIXTURES
Expand Down Expand Up @@ -121,6 +121,17 @@ def test_convert_flit(project):
assert build["excludes"] == ["doc/*.html"]


def test_convert_error_preserve_metadata(project):
pyproject_file = FIXTURES / "poetry-error.toml"
try:
poetry.convert(project, pyproject_file, Namespace(dev=False, group=None))
except MetaConvertError as e:
assert e.data["name"] == "test-poetry"
assert "dependencies: Invalid specifier" in str(e)
else:
pytest.fail("Should raise MetaConvertError")


def test_import_requirements_with_group(project):
golden_file = FIXTURES / "requirements.txt"
assert requirements.check_fingerprint(project, golden_file)
Expand Down
4 changes: 2 additions & 2 deletions tests/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def test_project_use_venv(project):
project._python = None
scripts = "Scripts" if os.name == "nt" else "bin"
suffix = ".exe" if os.name == "nt" else ""
venv.create(project.root / "venv")
venv.create(project.root / "venv", symlinks=True)

project.project_config["python.use_venv"] = True
env = project.environment
Expand Down Expand Up @@ -149,7 +149,7 @@ def test_ignore_saved_python(project, monkeypatch):
project._python = None
scripts = "Scripts" if os.name == "nt" else "bin"
suffix = ".exe" if os.name == "nt" else ""
venv.create(project.root / "venv")
venv.create(project.root / "venv", symlinks=True)
monkeypatch.setenv("PDM_IGNORE_SAVED_PYTHON", "1")
assert project.python.executable != project._saved_python
assert project.python.executable == project.root / "venv" / scripts / f"python{suffix}"
Expand Down

0 comments on commit 19b6a1f

Please sign in to comment.