-
Notifications
You must be signed in to change notification settings - Fork 273
A metadata
module with a data class for core metadata
#518
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
896c88b
A bare-bones `metadata` module
brettcannon 2619b0c
Add `display_name` and `canonical_name`
brettcannon 0c35b52
Update for PEP 685
brettcannon f80b8d9
Add tests for name normalization
brettcannon c8b41a3
Merge branch 'main' into metadata
brettcannon cd8f2e6
Address some feedback
brettcannon bf99308
Prep as a data class
brettcannon 7df02e1
Merge branch 'main' into metadata
brettcannon e50086a
Drop `InvalidMetadata` as it isn't used anywhere
brettcannon 6932abe
Document `packaging.metadata.Metadata1
brettcannon 64a5cef
Update pre-commit for flake8
brettcannon afbc122
Merge branch 'main' into metadata
brettcannon 65a95ed
Merge branch 'main' into metadata
brettcannon File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,7 @@ You can install packaging with ``pip``: | |
markers | ||
requirements | ||
tags | ||
metadata | ||
utils | ||
|
||
.. toctree:: | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
Metadata | ||
========== | ||
|
||
.. currentmodule:: packaging.metadata | ||
|
||
A data representation for `core metadata`_. | ||
|
||
|
||
Reference | ||
--------- | ||
|
||
.. class:: DynamicField | ||
|
||
An :class:`enum.Enum` representing fields which can be listed in | ||
the ``Dynamic`` field of `core metadata`_. Every valid field is | ||
a name on this enum, upper-cased with any ``-`` replaced with ``_``. | ||
Each value is the field name lower-cased (``-`` are kept). For | ||
example, the ``Home-page`` field has a name of ``HOME_PAGE`` and a | ||
value of ``home-page``. | ||
|
||
|
||
.. class:: Metadata(name, version, *, platforms=None, summary=None, description=None, keywords=None, home_page=None, author=None, author_emails=None, license=None, supported_platforms=None, download_url=None, classifiers=None, maintainer=None, maintainer_emails=None, requires_dists=None, requires_python=None, requires_externals=None, project_urls=None, provides_dists= None, obsoletes_dists= None, description_content_type=None, provides_extras=None, dynamic_fields=None) | ||
|
||
A class representing the `core metadata`_ for a project. | ||
|
||
Every potential metadata field except for ``Metadata-Version`` is | ||
represented by a parameter to the class' constructor. The required | ||
metadata can be passed in positionally or via keyword, while all | ||
optional metadata can only be passed in via keyword. | ||
|
||
Every parameter has a matching attribute on instances, | ||
except for *name* (see :attr:`display_name` and | ||
:attr:`canonical_name`). Any parameter that accepts an | ||
:class:`~collections.abc.Iterable` is represented as a | ||
:class:`list` on the corresponding attribute. | ||
|
||
:param str name: ``Name``. | ||
:param packaging.version.Version version: ``Version`` (note | ||
that this is different than ``Metadata-Version``). | ||
:param Iterable[str] platforms: ``Platform``. | ||
:param str summary: ``Summary``. | ||
:param str description: ``Description``. | ||
:param Iterable[str] keywords: ``Keywords``. | ||
:param str home_page: ``Home-Page``. | ||
:param str author: ``Author``. | ||
:param Iterable[tuple[str | None, str]] author_emails: ``Author-Email`` | ||
where the two-item tuple represents the name and email of the author, | ||
respectively. | ||
:param str license: ``License``. | ||
:param Iterable[str] supported_platforms: ``Supported-Platform``. | ||
:param str download_url: ``Download-URL``. | ||
:param Iterable[str] classifiers: ``Classifier``. | ||
:param str maintainer: ``Maintainer``. | ||
:param Iterable[tuple[str | None, str]] maintainer_emails: ``Maintainer-Email``, | ||
where the two-item tuple represents the name and email of the maintainer, | ||
respectively. | ||
:param Iterable[packaging.requirements.Requirement] requires_dists: ``Requires-Dist``. | ||
:param packaging.specifiers.SpecifierSet requires_python: ``Requires-Python``. | ||
:param Iterable[str] requires_externals: ``Requires-External``. | ||
:param tuple[str, str] project_urls: ``Project-URL``. | ||
:param Iterable[str] provides_dists: ``Provides-Dist``. | ||
:param Iterable[str] obsoletes_dists: ``Obsoletes-Dist``. | ||
:param str description_content_type: ``Description-Content-Type``. | ||
:param Iterable[packaging.utils.NormalizedName] provides_extras: ``Provides-Extra``. | ||
:param Iterable[DynamicField] dynamic_fields: ``Dynamic``. | ||
|
||
Attributes not directly corresponding to a parameter are: | ||
|
||
.. attribute:: display_name | ||
|
||
The project name to be displayed to users (i.e. not normalized). | ||
Initially set based on the *name* parameter. | ||
Setting this attribute will also update :attr:`canonical_name`. | ||
|
||
.. attribute:: canonical_name | ||
|
||
The normalized project name as per | ||
:func:`packaging.utils.canonicalize_name`. The attribute is | ||
read-only and automatically calculated based on the value of | ||
:attr:`display_name`. | ||
|
||
|
||
.. _`core metadata`: https://packaging.python.org/en/latest/specifications/core-metadata/ | ||
.. _`project metadata`: https://packaging.python.org/en/latest/specifications/declaring-project-metadata/ | ||
.. _`source distribution`: https://packaging.python.org/en/latest/specifications/source-distribution-format/ | ||
.. _`binary distrubtion`: https://packaging.python.org/en/latest/specifications/binary-distribution-format/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
from __future__ import annotations | ||
|
||
import enum | ||
from collections.abc import Iterable | ||
from typing import Optional, Tuple | ||
|
||
from . import ( # Alt name avoids shadowing. | ||
requirements, | ||
specifiers, | ||
utils, | ||
version as packaging_version, | ||
) | ||
|
||
# Type aliases. | ||
_NameAndEmail = Tuple[Optional[str], str] | ||
_LabelAndURL = Tuple[str, str] | ||
|
||
|
||
@enum.unique | ||
class DynamicField(enum.Enum): | ||
|
||
""" | ||
Field names for the `dynamic` field. | ||
|
||
All values are lower-cased for easy comparison. | ||
""" | ||
|
||
# `Name`, `Version`, and `Metadata-Version` are invalid in `Dynamic`. | ||
# 1.0 | ||
PLATFORM = "platform" | ||
SUMMARY = "summary" | ||
DESCRIPTION = "description" | ||
KEYWORDS = "keywords" | ||
HOME_PAGE = "home-page" | ||
AUTHOR = "author" | ||
AUTHOR_EMAIL = "author-email" | ||
LICENSE = "license" | ||
# 1.1 | ||
SUPPORTED_PLATFORM = "supported-platform" | ||
DOWNLOAD_URL = "download-url" | ||
CLASSIFIER = "classifier" | ||
# 1.2 | ||
MAINTAINER = "maintainer" | ||
MAINTAINER_EMAIL = "maintainer-email" | ||
REQUIRES_DIST = "requires-dist" | ||
REQUIRES_PYTHON = "requires-python" | ||
REQUIRES_EXTERNAL = "requires-external" | ||
PROJECT_URL = "project-url" | ||
PROVIDES_DIST = "provides-dist" | ||
OBSOLETES_DIST = "obsoletes-dist" | ||
# 2.1 | ||
DESCRIPTION_CONTENT_TYPE = "description-content-type" | ||
PROVIDES_EXTRA = "provides-extra" | ||
|
||
|
||
class Metadata: | ||
|
||
""" | ||
A representation of core metadata. | ||
""" | ||
|
||
# A property named `display_name` exposes the value. | ||
_display_name: str | ||
# A property named `canonical_name` exposes the value. | ||
_canonical_name: utils.NormalizedName | ||
version: packaging_version.Version | ||
platforms: list[str] | ||
summary: str | ||
description: str | ||
keywords: list[str] | ||
home_page: str | ||
author: str | ||
author_emails: list[_NameAndEmail] | ||
license: str | ||
supported_platforms: list[str] | ||
download_url: str | ||
classifiers: list[str] | ||
maintainer: str | ||
maintainer_emails: list[_NameAndEmail] | ||
requires_dists: list[requirements.Requirement] | ||
requires_python: specifiers.SpecifierSet | ||
requires_externals: list[str] | ||
project_urls: list[_LabelAndURL] | ||
provides_dists: list[str] | ||
obsoletes_dists: list[str] | ||
description_content_type: str | ||
provides_extras: list[utils.NormalizedName] | ||
dynamic_fields: list[DynamicField] | ||
|
||
def __init__( | ||
self, | ||
name: str, | ||
version: packaging_version.Version, | ||
*, | ||
# 1.0 | ||
platforms: Iterable[str] | None = None, | ||
summary: str | None = None, | ||
description: str | None = None, | ||
keywords: Iterable[str] | None = None, | ||
home_page: str | None = None, | ||
author: str | None = None, | ||
author_emails: Iterable[_NameAndEmail] | None = None, | ||
license: str | None = None, | ||
# 1.1 | ||
supported_platforms: Iterable[str] | None = None, | ||
download_url: str | None = None, | ||
classifiers: Iterable[str] | None = None, | ||
# 1.2 | ||
maintainer: str | None = None, | ||
maintainer_emails: Iterable[_NameAndEmail] | None = None, | ||
requires_dists: Iterable[requirements.Requirement] | None = None, | ||
requires_python: specifiers.SpecifierSet | None = None, | ||
requires_externals: Iterable[str] | None = None, | ||
project_urls: Iterable[_LabelAndURL] | None = None, | ||
provides_dists: Iterable[str] | None = None, | ||
obsoletes_dists: Iterable[str] | None = None, | ||
# 2.1 | ||
description_content_type: str | None = None, | ||
provides_extras: Iterable[utils.NormalizedName] | None = None, | ||
# 2.2 | ||
dynamic_fields: Iterable[DynamicField] | None = None, | ||
) -> None: | ||
""" | ||
Set all attributes on the instance. | ||
|
||
An argument of `None` will be converted to an appropriate, false-y value | ||
(e.g. the empty string). | ||
""" | ||
self.display_name = name | ||
self.version = version | ||
self.platforms = list(platforms or []) | ||
self.summary = summary or "" | ||
self.description = description or "" | ||
self.keywords = list(keywords or []) | ||
self.home_page = home_page or "" | ||
self.author = author or "" | ||
self.author_emails = list(author_emails or []) | ||
self.license = license or "" | ||
self.supported_platforms = list(supported_platforms or []) | ||
self.download_url = download_url or "" | ||
self.classifiers = list(classifiers or []) | ||
self.maintainer = maintainer or "" | ||
self.maintainer_emails = list(maintainer_emails or []) | ||
self.requires_dists = list(requires_dists or []) | ||
self.requires_python = requires_python or specifiers.SpecifierSet() | ||
self.requires_externals = list(requires_externals or []) | ||
self.project_urls = list(project_urls or []) | ||
self.provides_dists = list(provides_dists or []) | ||
self.obsoletes_dists = list(obsoletes_dists or []) | ||
self.description_content_type = description_content_type or "" | ||
self.provides_extras = list(provides_extras or []) | ||
self.dynamic_fields = list(dynamic_fields or []) | ||
|
||
@property | ||
def display_name(self) -> str: | ||
return self._display_name | ||
|
||
@display_name.setter | ||
def display_name(self, value: str) -> None: | ||
""" | ||
Set the value for self.display_name and self.canonical_name. | ||
""" | ||
self._display_name = value | ||
self._canonical_name = utils.canonicalize_name(value) | ||
|
||
# Use functools.cached_property once Python 3.7 support is dropped. | ||
# Value is set by self.display_name.setter to keep in sync with self.display_name. | ||
@property | ||
def canonical_name(self) -> utils.NormalizedName: | ||
return self._canonical_name | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import pytest | ||
|
||
from packaging import metadata, utils, version | ||
|
||
|
||
class TestInit: | ||
def test_defaults(self): | ||
specified_attributes = {"display_name", "canonical_name", "version"} | ||
metadata_ = metadata.Metadata("packaging", version.Version("2023.0.0")) | ||
for attr in dir(metadata_): | ||
if attr in specified_attributes or attr.startswith("_"): | ||
continue | ||
assert not getattr(metadata_, attr) | ||
|
||
|
||
class TestNameNormalization: | ||
|
||
version = version.Version("1.0.0") | ||
display_name = "A--B" | ||
canonical_name = utils.canonicalize_name(display_name) | ||
|
||
def test_via_init(self): | ||
metadata_ = metadata.Metadata(self.display_name, self.version) | ||
|
||
assert metadata_.display_name == self.display_name | ||
assert metadata_.canonical_name == self.canonical_name | ||
|
||
def test_via_display_name_setter(self): | ||
metadata_ = metadata.Metadata("a", self.version) | ||
|
||
assert metadata_.display_name == "a" | ||
assert metadata_.canonical_name == "a" | ||
|
||
metadata_.display_name = self.display_name | ||
|
||
assert metadata_.display_name == self.display_name | ||
assert metadata_.canonical_name == self.canonical_name | ||
|
||
def test_no_canonical_name_setter(self): | ||
metadata_ = metadata.Metadata("a", self.version) | ||
|
||
with pytest.raises(AttributeError): | ||
metadata_.canonical_name = "b" # type: ignore |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.