Skip to content

Commit

Permalink
Update supported pkg-info metadata and fix data export (#487)
Browse files Browse the repository at this point in the history
* Update supported pkg-info metadata and fix data export

- Add support for pkginfo metadata versions 2.2, 2.3 and 2.4
- Parse License-File field from broken setuptools metadata
- Do not export max metadata version by default
- Make default metadata version to 2.1 (when not defined)
- Support exporting the description as the metadata body
- Try to produce a sensible layout at export
- Rename pkg_info.platforms -> pkg_info.platform
- Rename pkg_info.supported_platforms -> pkg_info.support_platform

* Update major version since changes are incompatible
* Add comments to clarify some behaviour
  • Loading branch information
itziakos authored Feb 6, 2025
1 parent 6a7b604 commit 30c07fa
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 103 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.1.0.dev
3.0.0.dev
148 changes: 101 additions & 47 deletions okonomiyaki/file_formats/_package_info.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"""
Most of the code below is adapted from pkg-info 1.2.1
We support 1.0, 1.1, 1.2 and 2.0.
We support 1.0, 1.1, 1.2, 2.0, 2.1, 2.2, 2.3, 2.4
"""
import contextlib
import io
import os
import os.path
import warnings
import textwrap

import zipfile2

Expand All @@ -29,8 +30,8 @@
('Metadata-Version', 'metadata_version', False),
('Name', 'name', False),
('Version', 'version', False),
('Platform', 'platforms', True),
('Supported-Platform', 'supported_platforms', True),
('Platform', 'platform', True),
('Supported-Platform', 'supported_platform', True),
('Summary', 'summary', False),
('Description', 'description', False),
('Keywords', 'keywords', False),
Expand Down Expand Up @@ -67,16 +68,32 @@
HEADER_ATTRS_2_1 = HEADER_ATTRS_1_2 + ( # PEP 566
('Description-Content-Type', 'description_content_type', False),
('Provides-Extra', 'provides_extra', True),
# Setuptools is broken and produces License-File for metadata >= 2.1
# Note that we parse the info in but will not export it unless
# metadata is 2.4
('License-File', 'license_file', True),
)

HEADER_ATTRS_2_2 = HEADER_ATTRS_2_1 + ( # PEP 643
('Dynamic', 'dynamic', True),)

HEADER_ATTRS_2_3 = HEADER_ATTRS_2_2 # PEP 685

HEADER_ATTRS_2_4 = HEADER_ATTRS_2_2 + ( # PEP 639
('License-Expression', 'license_expression', False),)

HEADER_ATTRS = {
(1, 0): HEADER_ATTRS_1_0,
(1, 1): HEADER_ATTRS_1_1,
(1, 2): HEADER_ATTRS_1_2,
(2, 0): HEADER_ATTRS_2_0,
(2, 1): HEADER_ATTRS_2_1,
(2, 2): HEADER_ATTRS_2_2,
(2, 3): HEADER_ATTRS_2_3,
(2, 4): HEADER_ATTRS_2_4,
}


MAX_SUPPORTED_VERSION = max(HEADER_ATTRS.keys())


Expand Down Expand Up @@ -154,13 +171,12 @@ def from_string(cls, s):
raise ValueError("Expected text value, got {0!r}".format(type(s)))
fp = io.StringIO(s)
msg = _parse(fp)

kw = {}

if 'Metadata-Version' in msg:
metadata_version = _get(msg, 'Metadata-Version')
else:
metadata_version = "1.0"
metadata_version = "2.1" # Default metadata version

_ensure_supported_version(metadata_version)
metadata_version_info = _string_to_version_info(metadata_version)
Expand All @@ -173,8 +189,8 @@ def from_string(cls, s):
if header_name in msg:
if header_name == "Keywords":
if msg != "UNKNOWN":
value = _collapse_leading_ws(header_name,
msg.get(header_name))
value = _collapse_leading_ws(
header_name, msg.get(header_name))
kw[attr_name] = tuple(value.split())
elif multiple:
values = _get_all(msg, header_name)
Expand All @@ -197,28 +213,43 @@ def from_string(cls, s):
else:
kw['description'] = msg_body

if metadata_version_info >= (2, 4):
if 'License' in kw and 'License-Expression' in kw:
warnings.warn(
'As of Metadata 2.4, License and License-Expression are mutually exclusive. '
'License field is ignored',
RuntimeWarning)
del kw['License']
elif 'License-File' in kw:
warnings.warn(
'License-File was introduced in Metadata 2.4. '
'The value is parsed because setuptools adds it to Metadata 2.1. '
'The field will not be exported unless export Metadata >= 2.4')

name = kw.pop("name")
version = kw.pop("version")
return cls(metadata_version, name, version, **kw)

def __init__(self, metadata_version, name, version, platforms=None,
supported_platforms=None, summary="", description="",
keywords=None, home_page="", download_url="", author="",
author_email="", license="", classifiers=None, requires=None,
provides=None, obsoletes=None, maintainer="",
maintainer_email="", requires_python=None,
requires_external=None, requires_dist=None,
provides_dist=None, obsoletes_dist=None, project_urls=None,
description_content_type="", provides_extra=None):
def __init__(
self, metadata_version, name, version, platform=None,
supported_platform=None, summary="", description="",
keywords=None, home_page="", download_url="", author="",
author_email="", license="", classifiers=None, requires=None,
provides=None, obsoletes=None, maintainer="",
maintainer_email="", requires_python=None,
requires_external=None, requires_dist=None,
provides_dist=None, obsoletes_dist=None, project_urls=None,
description_content_type="", provides_extra=None,
dynamic=None, license_file=None, license_expression=None):
_ensure_supported_version(metadata_version)

self.metadata_version = metadata_version

# version 1.0
self.name = name
self.version = version
self.platforms = platforms or ()
self.supported_platforms = supported_platforms or ()
self.platform = platform or ()
self.supported_platform = supported_platform or ()
self.summary = summary
self.description = description
self.keywords = keywords or ()
Expand Down Expand Up @@ -248,9 +279,23 @@ def __init__(self, metadata_version, name, version, platforms=None,
self.description_content_type = description_content_type or ""
self.provides_extra = provides_extra or ()

def to_string(self, metadata_version_info=MAX_SUPPORTED_VERSION):
# version 2.2
self.dynamic = dynamic or ()

# version 2.4
self.license_file = license_file or ()
self.license_expression = license_expression or ()

def to_string(self, metadata_version_info=None, description_field=True):
if metadata_version_info is None:
metadata_version = self.metadata_version
metadata_version_info = _string_to_version_info(self.metadata_version)
else:
metadata_version = '.'.join(str(_) for _ in metadata_version_info)
_ensure_supported_version(metadata_version)

s = io.StringIO()
self._write_field(s, 'Metadata-Version', self.metadata_version)
self._write_field(s, 'Metadata-Version', metadata_version)
self._write_field(s, 'Name', self.name)
self._write_field(s, 'Version', self.version)
self._write_field(s, 'Summary', self.summary)
Expand All @@ -264,29 +309,27 @@ def to_string(self, metadata_version_info=MAX_SUPPORTED_VERSION):
if self.maintainer_email:
self._write_field(s, 'Maintainer-email', self.maintainer_email)

if self.license:
self._write_field(s, 'License', self.license)
else:
self._write_field(s, 'License', "UNKNOWN")
self._write_field(s, 'License', self.license)

if metadata_version_info >= (1, 1):
if self.download_url:
self._write_field(s, 'Download-URL', self.download_url)
self._write_field(s, 'Download-URL', self.download_url)

if metadata_version_info >= (1, 2):
self._write_list(s, 'Project-URL', self.project_urls)

if metadata_version_info >= (2, 1):
self._write_field(s, 'Description-Content-Type', self.description_content_type)

description = _rfc822_escape(self.description)
self._write_field(s, 'Description', description)
if description_field:
# Description as a metadata field
self._write_field(s, 'Description', description)

keywords = ' '.join(self.keywords)
if keywords:
self._write_field(s, 'Keywords', keywords)

if len(self.platforms) == 0:
self._write_list(s, 'Platform', ("UNKNOWN",))
else:
self._write_list(s, 'Platform', self.platforms)
self._write_list(s, 'Platform', self.platform)
self._write_list(s, 'Supported Platforms', self.supported_platform)

if metadata_version_info >= (1, 1):
self._write_list(s, 'Classifier', self.classifiers)
Expand All @@ -297,31 +340,44 @@ def to_string(self, metadata_version_info=MAX_SUPPORTED_VERSION):
self._write_list(s, 'Requires', self.requires)
self._write_list(s, 'Provides', self.provides)
self._write_list(s, 'Obsoletes', self.obsoletes)

elif metadata_version_info >= (1, 2):
if self.requires_python:
self._write_field(s, 'Requires-Python', self.requires_python)
self._write_field(s, 'Requires-Python', self.requires_python)
self._write_list(s, 'Requires-External', self.requires_external)

if metadata_version_info >= (2, 1):
self._write_list(s, 'Provides-Extra', self.provides_extra)

self._write_list(s, 'Requires-Dist', self.requires_dist)
self._write_list(s, 'Provides-Dist', self.provides_dist)
self._write_list(s, 'Obsoletes-Dist', self.obsoletes_dist)

if metadata_version_info >= (2, 1):
if self.description_content_type:
self._write_field(s, 'Description-Content-Type', self.description_content_type)
self._write_list(s, 'Provides-Extra', self.provides_extra)
if metadata_version_info >= (2, 4):
self._write_list(s, 'License-File', self.license_file)
self._write_list(s, 'License-Expression', self.license_expression)

if not description_field:
# Description as metadata body
self._write_description(s, description)

return s.getvalue()

def _dump_as_zip(self, zp, metadata_version_info=MAX_SUPPORTED_VERSION):
def _dump_as_zip(self, zp, metadata_version_info=None):
zp.writestr(
_PKG_INFO_LOCATION,
self.to_string(metadata_version_info).encode(PKG_INFO_ENCODING)
)
self.to_string(metadata_version_info).encode(PKG_INFO_ENCODING))

def _write_field(self, s, name, value):
value = '%s: %s\n' % (name, value)
s.write(value)
if value and value != 'UNKNOWN':
value = '%s: %s\n' % (name, value)
s.write(value)

def _write_description(self, s, value):
if value:
s.write('\n')
# FIXME I am not sure why the first line is like this
value = textwrap.dedent(' ' * 8 + value)
s.write(value)

def _write_list(self, s, name, values):
for value in values:
Expand All @@ -332,17 +388,15 @@ def __eq__(self, other):
if isinstance(other, self.__class__):
return (
self.metadata_version == other.metadata_version
and self.to_string() == other.to_string()
)
and self.to_string() == other.to_string())
elif other is None:
# We special-case None because EggMetadata.pkg_info may be None,
# and we want to support foo.pkg_info == foo2.pkg_info when one may
# be None
return False
else:
raise TypeError(
"Only equality between PackageInfo instances is supported"
)
"Only equality between PackageInfo instances is supported")

def __ne__(self, other):
return not self == other
Expand Down
Loading

0 comments on commit 30c07fa

Please sign in to comment.