Skip to content

Commit

Permalink
feat: update pyproject-metadata to support latest version of PEP 639
Browse files Browse the repository at this point in the history
Signed-off-by: Frost Ming <me@frostming.com>
  • Loading branch information
frostming committed Sep 20, 2024
1 parent a35cdb0 commit 290a1bf
Show file tree
Hide file tree
Showing 36 changed files with 135 additions and 204 deletions.
3 changes: 1 addition & 2 deletions docs/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ pdm-build-mypyc
version = "0.1.0"
description = "A pdm build hook to compile Python code with mypyc"
authors = [{name = "...", email = "..."}]
license = {text = "MIT"}
license = "MIT"
readme = "README.md"

[project.entry-points."pdm.build.hook"]
Expand All @@ -156,7 +156,6 @@ pdm-build-mypyc
mypyc_build(context.build_dir)
```


The plugin must be distributed with an entry point under `pdm.build.hook` group. The entry point value can be any of the following:

- A module containing hook functions
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ authors = [{name = "John Doe", email="me@johndoe.org"}]
dependencies = ["requests"]
requires-python = ">=3.8"
readme = "README.md"
license = {text = "MIT"}
license = "MIT"
```

Then run the build command to build the project as wheel and sdist:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description = "The build backend used by PDM that supports latest packaging stan
authors = [
{ name = "Frost Ming", email = "me@frostming.com" }
]
license = {text = "MIT"}
license = "MIT"
requires-python = ">=3.8"
readme = "README.md"
keywords = ["packaging", "PEP 517", "build"]
Expand Down
45 changes: 15 additions & 30 deletions src/pdm/backend/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import os
import shutil
import sys
import warnings
from fnmatch import fnmatch
from pathlib import Path
from typing import (
Expand All @@ -17,8 +16,8 @@
cast,
)

from pdm.backend._vendor.pyproject_metadata import StandardMetadata
from pdm.backend.config import Config
from pdm.backend.exceptions import PDMWarning, ValidationError
from pdm.backend.hooks import BuildHookInterface, Context
from pdm.backend.hooks.version import DynamicVersionBuildHook
from pdm.backend.structures import FileMap
Expand Down Expand Up @@ -275,34 +274,20 @@ def _collect_build_files(self, context: Context) -> FileMap:
files[rel_path] = p
return files

def find_license_files(self) -> list[str]:
"""Return a list of license files from the PEP 639 metadata."""
root = self.location
license_files = self.config.metadata.license_files
if "paths" in license_files:
invalid_paths = [
p for p in license_files["paths"] if not (root / p).is_file()
]
if invalid_paths:
raise ValidationError(
"license-files", f"License files not found: {invalid_paths}"
)
return license_files["paths"]
else:
paths = [
p.relative_to(root).as_posix()
for pattern in license_files["globs"]
for p in root.glob(pattern)
if (root / p).is_file()
]
if license_files["globs"] and not paths:
warnings.warn(
f"No license files are matched with glob patterns "
f"{license_files['globs']}.",
PDMWarning,
stacklevel=2,
)
return paths
def find_license_files(self, metadata: StandardMetadata) -> list[str]:
result: list[str] = []
if file := getattr(metadata.license, "file", None):
result.append(file.relative_to(self.location).as_posix())
if metadata.license_files:
for file in metadata.license_files:
result.append(file.as_posix())
if (
not result and metadata.license_files is None
): # no license files specified, find from default patterns for backward compatibility
for pattern in ["LICEN[CS]E*", "COPYING*", "NOTICE*"]:
for path in self.location.glob(pattern):
result.append(path.relative_to(self.location).as_posix())
return result

def _get_include_and_exclude(self) -> tuple[set[str], set[str]]:
includes = set()
Expand Down
92 changes: 14 additions & 78 deletions src/pdm/backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import glob
import os
import sys
from functools import cached_property
from pathlib import Path
from typing import TYPE_CHECKING, Any, TypeVar

Expand Down Expand Up @@ -41,29 +42,27 @@ class Config:
"""

def __init__(self, root: Path, data: dict[str, Any]) -> None:
self.validate(data, root)
self.root = root
self.data = data
self.metadata = Metadata(data["project"])
self.build_config = BuildConfig(
root, data.setdefault("tool", {}).get("pdm", {}).get("build", {})
)

def to_coremetadata(self) -> str:
"""Return the metadata as a Core Metadata string."""
metadata = StandardMetadata.from_pyproject(self.data, project_dir=self.root)
# Fix the name field to unnormalized form.
metadata.name = self.metadata["name"]
return str(metadata.as_rfc822())
self.validate()

@classmethod
def validate(cls, data: dict[str, Any], root: Path) -> None:
def validate(self) -> StandardMetadata:
"""Validate the pyproject.toml data."""
try:
StandardMetadata.from_pyproject(data, project_dir=root)
return StandardMetadata.from_pyproject(self.data, project_dir=self.root)
except ConfigurationError as e:
raise ValidationError(e.args[0], e.key) from e

@property
def metadata(self) -> dict[str, Any]:
return self.data["project"]

@cached_property
def build_config(self) -> BuildConfig:
return BuildConfig(
self.root, self.data.setdefault("tool", {}).get("pdm", {}).get("build", {})
)

@classmethod
def from_pyproject(cls, root: str | Path) -> Config:
"""Load the pyproject.toml file from the given project root."""
Expand Down Expand Up @@ -154,69 +153,6 @@ def convert_package_paths(self) -> dict[str, list | dict]:
}


class Metadata(Table):
"""The project metadata table"""

@property
def readme_file(self) -> str | None:
"""The readme file path, if not exists, returns None"""
readme = self.get("readme")
if not readme:
return None
if isinstance(readme, str):
return readme
if isinstance(readme, dict) and "file" in readme:
return readme["file"]
return None

@property
def license_files(self) -> dict[str, list[str]]:
"""The license files configuration"""
subtable_files = None
if (
"license" in self
and isinstance(self["license"], dict)
and "files" in self["license"]
):
subtable_files = self["license"]["files"]
if "license-files" not in self:
if subtable_files is not None:
return {"paths": [self["license"]["file"]]}
return {
"globs": [
"LICENSES/*",
"LICEN[CS]E*",
"COPYING*",
"NOTICE*",
"AUTHORS*",
]
}
if subtable_files is not None:
raise ValidationError(
"license-files",
"Can't specify both 'license.files' and 'license-files' fields",
)
rv = self["license-files"]
valid_keys = {"globs", "paths"} & set(rv)
if len(valid_keys) == 2:
raise ValidationError(
"license-files", "Can't specify both 'paths' and 'globs'"
)
if not valid_keys:
raise ValidationError("license-files", "Must specify 'paths' or 'globs'")
return rv

@property
def entry_points(self) -> dict[str, dict[str, str]]:
"""The entry points mapping"""
entry_points: dict[str, dict[str, str]] = self.get("entry-points", {})
if "scripts" in self:
entry_points["console_scripts"] = self["scripts"]
if "gui-scripts" in self:
entry_points["gui_scripts"] = self["gui-scripts"]
return entry_points


class BuildConfig(Table):
"""The `[tool.pdm.build]` table"""

Expand Down
29 changes: 15 additions & 14 deletions src/pdm/backend/hooks/setuptools.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,12 @@ def cleanup() -> None:

def format_setup_py(self, context: Context) -> str:
before, extra, after = [], [], []
meta = context.config.metadata
meta = context.config.validate()
kwargs = {
"name": meta["name"],
"version": meta.get("version", "0.0.0"),
"description": meta.get("description", "UNKNOWN"),
"url": (meta.get("project-urls", {})).get("homepage", ""),
"name": meta.name,
"version": str(meta.version or "0.0.0"),
"description": meta.description or "UNKNOWN",
"url": meta.urls.get("homepage", ""),
}

# Run the pdm_build_update_setup_kwargs hook to update the kwargs
Expand Down Expand Up @@ -160,19 +160,20 @@ def format_setup_py(self, context: Context) -> str:
)
)

if meta.get("dependencies"):
before.append(f"INSTALL_REQUIRES = {_format_list(meta['dependencies'])}\n")
if meta.dependencies:
before.append(f"INSTALL_REQUIRES = {_format_list(meta.dependencies)}\n")
extra.append(" 'install_requires': INSTALL_REQUIRES,\n")
if meta.get("optional-dependencies"):
if meta.optional_dependencies:
before.append(
"EXTRAS_REQUIRE = {}\n".format(
_format_dict_list(meta["optional-dependencies"])
)
f"EXTRAS_REQUIRE = {_format_dict_list(meta.optional_dependencies)}\n"
)
extra.append(" 'extras_require': EXTRAS_REQUIRE,\n")
if meta.get("requires-python"):
extra.append(f" 'python_requires': {meta['requires-python']!r},\n")
entry_points = meta.entry_points
if meta.requires_python is not None:
extra.append(f" 'python_requires': '{meta.requires_python}',\n")
entry_points = meta.entrypoints.copy()
entry_points.update(
{"console_scripts": meta.scripts, "gui_scripts": meta.gui_scripts}
)
if entry_points:
entry_points_list = {
group: [f"{k} = {v}" for k, v in values.items()]
Expand Down
23 changes: 12 additions & 11 deletions src/pdm/backend/sdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,22 @@ class SdistBuilder(Builder):

def get_files(self, context: Context) -> Iterable[tuple[str, Path]]:
collected = dict(super().get_files(context))
local_hook = self.config.build_config.custom_hook
context.ensure_build_dir()
context.config.write_to(context.build_dir / "pyproject.toml")
collected["pyproject.toml"] = context.build_dir / "pyproject.toml"
metadata = self.config.validate()

def gen_additional_files() -> Iterable[str]:
if local_hook := self.config.build_config.custom_hook:
yield local_hook
if metadata.readme and metadata.readme.file:
yield metadata.readme.file.relative_to(self.location).as_posix()
yield from self.find_license_files(metadata)

additional_files: Iterable[str] = filter(
lambda f: f is not None and f not in collected,
(
local_hook,
self.config.metadata.readme_file,
*self.find_license_files(),
),
)
root = self.location
for file in additional_files:
for file in gen_additional_files():
if file in collected:
continue
if root.joinpath(file).exists():
collected[file] = root / file
return collected.items()
Expand All @@ -82,7 +83,7 @@ def build_artifact(
tar.addfile(tar_info)
self._show_add_file(relpath, path)

pkg_info = self.config.to_coremetadata().encode("utf-8")
pkg_info = str(self.config.validate().as_rfc822()).encode("utf-8")
tar_info = tarfile.TarInfo(pjoin(dist_info, "PKG-INFO"))
tar_info.size = len(pkg_info)
tar_info = clean_tarinfo(tar_info)
Expand Down
11 changes: 7 additions & 4 deletions src/pdm/backend/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,11 @@ def _write_dist_info(self, parent: Path) -> Path:
"""write the dist-info directory and return the path to it"""
dist_info = parent / self.dist_info_name
dist_info.mkdir(0o700, exist_ok=True)
meta = self.config.metadata
entry_points = meta.entry_points
meta = self.config.validate()
entry_points = meta.entrypoints.copy()
entry_points.update(
{"console_scripts": meta.scripts, "gui_scripts": meta.gui_scripts}
)
if entry_points:
with _open_for_write(dist_info / "entry_points.txt") as f:
self._write_entry_points(f, entry_points)
Expand All @@ -238,9 +241,9 @@ def _write_dist_info(self, parent: Path) -> Path:
self._write_wheel_file(f, is_purelib=self.config.build_config.is_purelib)

with _open_for_write(dist_info / "METADATA") as f:
f.write(self.config.to_coremetadata())
f.write(str(meta.as_rfc822()))

for file in self.find_license_files():
for file in self.find_license_files(meta):
target = dist_info / "licenses" / file
target.parent.mkdir(0o700, parents=True, exist_ok=True)
shutil.copy2(self.location / file, target)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ authors = [
]
dynamic = ["version"]
requires-python = ">=3.5"
license = {text = "MIT"}
license = { text = "MIT" }
dependencies = []
description = ""
name = "demo-package"
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/projects/demo-cextension/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ authors = [
]
dynamic = ["version"]
requires-python = ">=3.5"
license = {text = "MIT"}
license = { text = "MIT" }
dependencies = []
description = ""
name = "demo-package"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ build-backend = "pdm.backend"
[project]
authors = [{name = "frostming", email = "mianghong@gmail.com"}]
description = ""
license = {text = "MIT"}
license = "MIT"
name = "demo-package-extra"
requires-python = ">=3.5"
version = "0.1.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ authors = [
]
dynamic = ["version"]
requires-python = ">=3.5"
license = {text = "MIT"}
license = "MIT"
dependencies = []
description = ""
name = "demo-package"
Expand Down
Empty file.
Empty file.
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ authors = [
]
dynamic = ["version"]
requires-python = ">=3.5"
license = {text = "MIT"}
license-files = ["LICENSE", "licenses/LICENSE*"]
dependencies = []
description = ""
name = "demo-module"
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/projects/demo-metadata-test/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ authors = [
]
name = "demo-metadata-test"
requires-python = ">=3.8"
license = {text = "MIT"}
license = "MIT"
dependencies = []
description = ""
readme = "README.md"
Expand Down
Loading

0 comments on commit 290a1bf

Please sign in to comment.