-
Notifications
You must be signed in to change notification settings - Fork 250
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a
metadata
module with a data class for core metadata (#518)
- Loading branch information
1 parent
c4647b9
commit c533201
Showing
5 changed files
with
301 additions
and
1 deletion.
There are no files selected for viewing
This file contains 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 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 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 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 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 |