

5 changed files with 301 additions and 1 deletions
@ -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/ |
@ -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 |
@ -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 |
Loading…
Reference in new issue