summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnderson Bravalheri <andersonbravalheri@gmail.com>2023-03-08 16:27:59 +0000
committerAnderson Bravalheri <andersonbravalheri@gmail.com>2023-05-03 14:09:16 +0100
commit933db3395074ded71336ad813160ede2ec8fb437 (patch)
tree0a98234596e6dcc72841b605724a5c209fe367e9
parent64f35f051a26631fdce0437099d86e1ca524e19d (diff)
downloadpython-setuptools-git-933db3395074ded71336ad813160ede2ec8fb437.tar.gz
Extract core metadata logic into dedicated module
-rw-r--r--setuptools/_core_metadata.py191
-rw-r--r--setuptools/dist.py186
-rw-r--r--setuptools/monkey.py3
-rw-r--r--setuptools/tests/test_core_metadata.py245
-rw-r--r--setuptools/tests/test_dist.py231
5 files changed, 439 insertions, 417 deletions
diff --git a/setuptools/_core_metadata.py b/setuptools/_core_metadata.py
new file mode 100644
index 00000000..750b09b3
--- /dev/null
+++ b/setuptools/_core_metadata.py
@@ -0,0 +1,191 @@
+"""
+Handling of Core Metadata for Python packages (including reading and writing).
+
+See: https://packaging.python.org/en/latest/specifications/core-metadata/
+"""
+import textwrap
+from email import message_from_file
+from email.message import Message
+from typing import Optional, List
+
+from distutils.util import rfc822_escape
+
+from .extern.packaging.version import Version
+from .warnings import SetuptoolsDeprecationWarning
+
+
+def get_metadata_version(self):
+ mv = getattr(self, 'metadata_version', None)
+ if mv is None:
+ mv = Version('2.1')
+ self.metadata_version = mv
+ return mv
+
+
+def rfc822_unescape(content: str) -> str:
+ """Reverse RFC-822 escaping by removing leading whitespaces from content."""
+ lines = content.splitlines()
+ if len(lines) == 1:
+ return lines[0].lstrip()
+ return '\n'.join((lines[0].lstrip(), textwrap.dedent('\n'.join(lines[1:]))))
+
+
+def _read_field_from_msg(msg: Message, field: str) -> Optional[str]:
+ """Read Message header field."""
+ value = msg[field]
+ if value == 'UNKNOWN':
+ return None
+ return value
+
+
+def _read_field_unescaped_from_msg(msg: Message, field: str) -> Optional[str]:
+ """Read Message header field and apply rfc822_unescape."""
+ value = _read_field_from_msg(msg, field)
+ if value is None:
+ return value
+ return rfc822_unescape(value)
+
+
+def _read_list_from_msg(msg: Message, field: str) -> Optional[List[str]]:
+ """Read Message header field and return all results as list."""
+ values = msg.get_all(field, None)
+ if values == []:
+ return None
+ return values
+
+
+def _read_payload_from_msg(msg: Message) -> Optional[str]:
+ value = msg.get_payload().strip()
+ if value == 'UNKNOWN' or not value:
+ return None
+ return value
+
+
+def read_pkg_file(self, file):
+ """Reads the metadata values from a file object."""
+ msg = message_from_file(file)
+
+ self.metadata_version = Version(msg['metadata-version'])
+ self.name = _read_field_from_msg(msg, 'name')
+ self.version = _read_field_from_msg(msg, 'version')
+ self.description = _read_field_from_msg(msg, 'summary')
+ # we are filling author only.
+ self.author = _read_field_from_msg(msg, 'author')
+ self.maintainer = None
+ self.author_email = _read_field_from_msg(msg, 'author-email')
+ self.maintainer_email = None
+ self.url = _read_field_from_msg(msg, 'home-page')
+ self.download_url = _read_field_from_msg(msg, 'download-url')
+ self.license = _read_field_unescaped_from_msg(msg, 'license')
+
+ self.long_description = _read_field_unescaped_from_msg(msg, 'description')
+ if (
+ self.long_description is None and
+ self.metadata_version >= Version('2.1')
+ ):
+ self.long_description = _read_payload_from_msg(msg)
+ self.description = _read_field_from_msg(msg, 'summary')
+
+ if 'keywords' in msg:
+ self.keywords = _read_field_from_msg(msg, 'keywords').split(',')
+
+ self.platforms = _read_list_from_msg(msg, 'platform')
+ self.classifiers = _read_list_from_msg(msg, 'classifier')
+
+ # PEP 314 - these fields only exist in 1.1
+ if self.metadata_version == Version('1.1'):
+ self.requires = _read_list_from_msg(msg, 'requires')
+ self.provides = _read_list_from_msg(msg, 'provides')
+ self.obsoletes = _read_list_from_msg(msg, 'obsoletes')
+ else:
+ self.requires = None
+ self.provides = None
+ self.obsoletes = None
+
+ self.license_files = _read_list_from_msg(msg, 'license-file')
+
+
+def single_line(val):
+ """
+ Quick and dirty validation for Summary pypa/setuptools#1390.
+ """
+ if '\n' in val:
+ # TODO: Replace with `raise ValueError("newlines not allowed")`
+ # after reviewing #2893.
+ msg = "newlines are not allowed in `summary` and will break in the future"
+ SetuptoolsDeprecationWarning.emit("Invalid config.", msg)
+ # due_date is undefined. Controversial change, there was a lot of push back.
+ val = val.strip().split('\n')[0]
+ return val
+
+
+# Based on Python 3.5 version
+def write_pkg_file(self, file): # noqa: C901 # is too complex (14) # FIXME
+ """Write the PKG-INFO format data to a file object."""
+ version = self.get_metadata_version()
+
+ def write_field(key, value):
+ file.write("%s: %s\n" % (key, value))
+
+ write_field('Metadata-Version', str(version))
+ write_field('Name', self.get_name())
+ write_field('Version', self.get_version())
+
+ summary = self.get_description()
+ if summary:
+ write_field('Summary', single_line(summary))
+
+ optional_fields = (
+ ('Home-page', 'url'),
+ ('Download-URL', 'download_url'),
+ ('Author', 'author'),
+ ('Author-email', 'author_email'),
+ ('Maintainer', 'maintainer'),
+ ('Maintainer-email', 'maintainer_email'),
+ )
+
+ for field, attr in optional_fields:
+ attr_val = getattr(self, attr, None)
+ if attr_val is not None:
+ write_field(field, attr_val)
+
+ license = self.get_license()
+ if license:
+ write_field('License', rfc822_escape(license))
+
+ for project_url in self.project_urls.items():
+ write_field('Project-URL', '%s, %s' % project_url)
+
+ keywords = ','.join(self.get_keywords())
+ if keywords:
+ write_field('Keywords', keywords)
+
+ platforms = self.get_platforms() or []
+ for platform in platforms:
+ write_field('Platform', platform)
+
+ self._write_list(file, 'Classifier', self.get_classifiers())
+
+ # PEP 314
+ self._write_list(file, 'Requires', self.get_requires())
+ self._write_list(file, 'Provides', self.get_provides())
+ self._write_list(file, 'Obsoletes', self.get_obsoletes())
+
+ # Setuptools specific for PEP 345
+ if hasattr(self, 'python_requires'):
+ write_field('Requires-Python', self.python_requires)
+
+ # PEP 566
+ if self.long_description_content_type:
+ write_field('Description-Content-Type', self.long_description_content_type)
+ if self.provides_extras:
+ for extra in self.provides_extras:
+ write_field('Provides-Extra', extra)
+
+ self._write_list(file, 'License-File', self.license_files or [])
+
+ long_description = self.get_long_description()
+ if long_description:
+ file.write("\n%s" % long_description)
+ if not long_description.endswith("\n"):
+ file.write("\n")
diff --git a/setuptools/dist.py b/setuptools/dist.py
index a24e256e..c5c1bc2c 100644
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -15,15 +15,11 @@ from distutils.debug import DEBUG
from distutils.fancy_getopt import translate_longopt
from glob import iglob
import itertools
-import textwrap
from contextlib import suppress
-from typing import List, Optional, Set, TYPE_CHECKING
+from typing import List, Optional, Set
from pathlib import Path
-from email import message_from_file
-
from distutils.errors import DistutilsOptionError, DistutilsSetupError
-from distutils.util import rfc822_escape
from setuptools.extern import packaging
from setuptools.extern import ordered_set
@@ -36,15 +32,12 @@ from setuptools.monkey import get_unpatched
from setuptools.config import setupcfg, pyprojecttoml
from setuptools.discovery import ConfigDiscovery
-from setuptools.extern.packaging import version
from . import _reqs
from . import _entry_points
from . import _normalization
from ._importlib import metadata
from .warnings import InformationOnly, SetuptoolsDeprecationWarning
-if TYPE_CHECKING:
- from email.message import Message
__import__('setuptools.extern.packaging.specifiers')
__import__('setuptools.extern.packaging.version')
@@ -60,183 +53,6 @@ def _get_unpatched(cls):
return get_unpatched(cls)
-def get_metadata_version(self):
- mv = getattr(self, 'metadata_version', None)
- if mv is None:
- mv = version.Version('2.1')
- self.metadata_version = mv
- return mv
-
-
-def rfc822_unescape(content: str) -> str:
- """Reverse RFC-822 escaping by removing leading whitespaces from content."""
- lines = content.splitlines()
- if len(lines) == 1:
- return lines[0].lstrip()
- return '\n'.join((lines[0].lstrip(), textwrap.dedent('\n'.join(lines[1:]))))
-
-
-def _read_field_from_msg(msg: "Message", field: str) -> Optional[str]:
- """Read Message header field."""
- value = msg[field]
- if value == 'UNKNOWN':
- return None
- return value
-
-
-def _read_field_unescaped_from_msg(msg: "Message", field: str) -> Optional[str]:
- """Read Message header field and apply rfc822_unescape."""
- value = _read_field_from_msg(msg, field)
- if value is None:
- return value
- return rfc822_unescape(value)
-
-
-def _read_list_from_msg(msg: "Message", field: str) -> Optional[List[str]]:
- """Read Message header field and return all results as list."""
- values = msg.get_all(field, None)
- if values == []:
- return None
- return values
-
-
-def _read_payload_from_msg(msg: "Message") -> Optional[str]:
- value = msg.get_payload().strip()
- if value == 'UNKNOWN' or not value:
- return None
- return value
-
-
-def read_pkg_file(self, file):
- """Reads the metadata values from a file object."""
- msg = message_from_file(file)
-
- self.metadata_version = version.Version(msg['metadata-version'])
- self.name = _read_field_from_msg(msg, 'name')
- self.version = _read_field_from_msg(msg, 'version')
- self.description = _read_field_from_msg(msg, 'summary')
- # we are filling author only.
- self.author = _read_field_from_msg(msg, 'author')
- self.maintainer = None
- self.author_email = _read_field_from_msg(msg, 'author-email')
- self.maintainer_email = None
- self.url = _read_field_from_msg(msg, 'home-page')
- self.download_url = _read_field_from_msg(msg, 'download-url')
- self.license = _read_field_unescaped_from_msg(msg, 'license')
-
- self.long_description = _read_field_unescaped_from_msg(msg, 'description')
- if (
- self.long_description is None and
- self.metadata_version >= version.Version('2.1')
- ):
- self.long_description = _read_payload_from_msg(msg)
- self.description = _read_field_from_msg(msg, 'summary')
-
- if 'keywords' in msg:
- self.keywords = _read_field_from_msg(msg, 'keywords').split(',')
-
- self.platforms = _read_list_from_msg(msg, 'platform')
- self.classifiers = _read_list_from_msg(msg, 'classifier')
-
- # PEP 314 - these fields only exist in 1.1
- if self.metadata_version == version.Version('1.1'):
- self.requires = _read_list_from_msg(msg, 'requires')
- self.provides = _read_list_from_msg(msg, 'provides')
- self.obsoletes = _read_list_from_msg(msg, 'obsoletes')
- else:
- self.requires = None
- self.provides = None
- self.obsoletes = None
-
- self.license_files = _read_list_from_msg(msg, 'license-file')
-
-
-def single_line(val):
- """
- Quick and dirty validation for Summary pypa/setuptools#1390.
- """
- if '\n' in val:
- # TODO: Replace with `raise ValueError("newlines not allowed")`
- # after reviewing #2893.
- msg = "newlines are not allowed in `summary` and will break in the future"
- SetuptoolsDeprecationWarning.emit("Invalid config.", msg)
- # due_date is undefined. Controversial change, there was a lot of push back.
- val = val.strip().split('\n')[0]
- return val
-
-
-# Based on Python 3.5 version
-def write_pkg_file(self, file): # noqa: C901 # is too complex (14) # FIXME
- """Write the PKG-INFO format data to a file object."""
- version = self.get_metadata_version()
-
- def write_field(key, value):
- file.write("%s: %s\n" % (key, value))
-
- write_field('Metadata-Version', str(version))
- write_field('Name', self.get_name())
- write_field('Version', self.get_version())
-
- summary = self.get_description()
- if summary:
- write_field('Summary', single_line(summary))
-
- optional_fields = (
- ('Home-page', 'url'),
- ('Download-URL', 'download_url'),
- ('Author', 'author'),
- ('Author-email', 'author_email'),
- ('Maintainer', 'maintainer'),
- ('Maintainer-email', 'maintainer_email'),
- )
-
- for field, attr in optional_fields:
- attr_val = getattr(self, attr, None)
- if attr_val is not None:
- write_field(field, attr_val)
-
- license = self.get_license()
- if license:
- write_field('License', rfc822_escape(license))
-
- for project_url in self.project_urls.items():
- write_field('Project-URL', '%s, %s' % project_url)
-
- keywords = ','.join(self.get_keywords())
- if keywords:
- write_field('Keywords', keywords)
-
- platforms = self.get_platforms() or []
- for platform in platforms:
- write_field('Platform', platform)
-
- self._write_list(file, 'Classifier', self.get_classifiers())
-
- # PEP 314
- self._write_list(file, 'Requires', self.get_requires())
- self._write_list(file, 'Provides', self.get_provides())
- self._write_list(file, 'Obsoletes', self.get_obsoletes())
-
- # Setuptools specific for PEP 345
- if hasattr(self, 'python_requires'):
- write_field('Requires-Python', self.python_requires)
-
- # PEP 566
- if self.long_description_content_type:
- write_field('Description-Content-Type', self.long_description_content_type)
- if self.provides_extras:
- for extra in self.provides_extras:
- write_field('Provides-Extra', extra)
-
- self._write_list(file, 'License-File', self.license_files or [])
-
- long_description = self.get_long_description()
- if long_description:
- file.write("\n%s" % long_description)
- if not long_description.endswith("\n"):
- file.write("\n")
-
-
sequence = tuple, list
diff --git a/setuptools/monkey.py b/setuptools/monkey.py
index 50653fc7..23a5832d 100644
--- a/setuptools/monkey.py
+++ b/setuptools/monkey.py
@@ -11,6 +11,7 @@ from importlib import import_module
import inspect
import setuptools
+import setuptools._core_metadata
__all__ = []
"""
@@ -100,7 +101,7 @@ def patch_all():
def _patch_distribution_metadata():
"""Patch write_pkg_file and read_pkg_file for higher metadata standards"""
for attr in ('write_pkg_file', 'read_pkg_file', 'get_metadata_version'):
- new_val = getattr(setuptools.dist, attr)
+ new_val = getattr(setuptools._core_metadata, attr)
setattr(distutils.dist.DistributionMetadata, attr, new_val)
diff --git a/setuptools/tests/test_core_metadata.py b/setuptools/tests/test_core_metadata.py
new file mode 100644
index 00000000..19d1af1e
--- /dev/null
+++ b/setuptools/tests/test_core_metadata.py
@@ -0,0 +1,245 @@
+import functools
+import io
+
+import pytest
+
+from setuptools import sic
+from setuptools.dist import Distribution
+from setuptools._core_metadata import rfc822_escape, rfc822_unescape
+
+
+EXAMPLE_BASE_INFO = dict(
+ name="package",
+ version="0.0.1",
+ author="Foo Bar",
+ author_email="foo@bar.net",
+ long_description="Long\ndescription",
+ description="Short description",
+ keywords=["one", "two"],
+)
+
+
+@pytest.mark.parametrize(
+ 'content, result',
+ (
+ pytest.param(
+ "Just a single line",
+ None,
+ id="single_line",
+ ),
+ pytest.param(
+ "Multiline\nText\nwithout\nextra indents\n",
+ None,
+ id="multiline",
+ ),
+ pytest.param(
+ "Multiline\n With\n\nadditional\n indentation",
+ None,
+ id="multiline_with_indentation",
+ ),
+ pytest.param(
+ " Leading whitespace",
+ "Leading whitespace",
+ id="remove_leading_whitespace",
+ ),
+ pytest.param(
+ " Leading whitespace\nIn\n Multiline comment",
+ "Leading whitespace\nIn\n Multiline comment",
+ id="remove_leading_whitespace_multiline",
+ ),
+ )
+)
+def test_rfc822_unescape(content, result):
+ assert (result or content) == rfc822_unescape(rfc822_escape(content))
+
+
+def __read_test_cases():
+ base = EXAMPLE_BASE_INFO
+
+ params = functools.partial(dict, base)
+
+ test_cases = [
+ ('Metadata version 1.0', params()),
+ ('Metadata Version 1.0: Short long description', params(
+ long_description='Short long description',
+ )),
+ ('Metadata version 1.1: Classifiers', params(
+ classifiers=[
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.7',
+ 'License :: OSI Approved :: MIT License',
+ ],
+ )),
+ ('Metadata version 1.1: Download URL', params(
+ download_url='https://example.com',
+ )),
+ ('Metadata Version 1.2: Requires-Python', params(
+ python_requires='>=3.7',
+ )),
+ pytest.param(
+ 'Metadata Version 1.2: Project-Url',
+ params(project_urls=dict(Foo='https://example.bar')),
+ marks=pytest.mark.xfail(
+ reason="Issue #1578: project_urls not read",
+ ),
+ ),
+ ('Metadata Version 2.1: Long Description Content Type', params(
+ long_description_content_type='text/x-rst; charset=UTF-8',
+ )),
+ ('License', params(license='MIT', )),
+ ('License multiline', params(
+ license='This is a long license \nover multiple lines',
+ )),
+ pytest.param(
+ 'Metadata Version 2.1: Provides Extra',
+ params(provides_extras=['foo', 'bar']),
+ marks=pytest.mark.xfail(reason="provides_extras not read"),
+ ),
+ ('Missing author', dict(
+ name='foo',
+ version='1.0.0',
+ author_email='snorri@sturluson.name',
+ )),
+ ('Missing author e-mail', dict(
+ name='foo',
+ version='1.0.0',
+ author='Snorri Sturluson',
+ )),
+ ('Missing author and e-mail', dict(
+ name='foo',
+ version='1.0.0',
+ )),
+ ('Bypass normalized version', dict(
+ name='foo',
+ version=sic('1.0.0a'),
+ )),
+ ]
+
+ return test_cases
+
+
+@pytest.mark.parametrize('name,attrs', __read_test_cases())
+def test_read_metadata(name, attrs):
+ dist = Distribution(attrs)
+ metadata_out = dist.metadata
+ dist_class = metadata_out.__class__
+
+ # Write to PKG_INFO and then load into a new metadata object
+ PKG_INFO = io.StringIO()
+
+ metadata_out.write_pkg_file(PKG_INFO)
+
+ PKG_INFO.seek(0)
+ metadata_in = dist_class()
+ metadata_in.read_pkg_file(PKG_INFO)
+
+ tested_attrs = [
+ ('name', dist_class.get_name),
+ ('version', dist_class.get_version),
+ ('author', dist_class.get_contact),
+ ('author_email', dist_class.get_contact_email),
+ ('metadata_version', dist_class.get_metadata_version),
+ ('provides', dist_class.get_provides),
+ ('description', dist_class.get_description),
+ ('long_description', dist_class.get_long_description),
+ ('download_url', dist_class.get_download_url),
+ ('keywords', dist_class.get_keywords),
+ ('platforms', dist_class.get_platforms),
+ ('obsoletes', dist_class.get_obsoletes),
+ ('requires', dist_class.get_requires),
+ ('classifiers', dist_class.get_classifiers),
+ ('project_urls', lambda s: getattr(s, 'project_urls', {})),
+ ('provides_extras', lambda s: getattr(s, 'provides_extras', set())),
+ ]
+
+ for attr, getter in tested_attrs:
+ assert getter(metadata_in) == getter(metadata_out)
+
+
+def __maintainer_test_cases():
+ attrs = {"name": "package",
+ "version": "1.0",
+ "description": "xxx"}
+
+ def merge_dicts(d1, d2):
+ d1 = d1.copy()
+ d1.update(d2)
+
+ return d1
+
+ test_cases = [
+ ('No author, no maintainer', attrs.copy()),
+ ('Author (no e-mail), no maintainer', merge_dicts(
+ attrs,
+ {'author': 'Author Name'})),
+ ('Author (e-mail), no maintainer', merge_dicts(
+ attrs,
+ {'author': 'Author Name',
+ 'author_email': 'author@name.com'})),
+ ('No author, maintainer (no e-mail)', merge_dicts(
+ attrs,
+ {'maintainer': 'Maintainer Name'})),
+ ('No author, maintainer (e-mail)', merge_dicts(
+ attrs,
+ {'maintainer': 'Maintainer Name',
+ 'maintainer_email': 'maintainer@name.com'})),
+ ('Author (no e-mail), Maintainer (no-email)', merge_dicts(
+ attrs,
+ {'author': 'Author Name',
+ 'maintainer': 'Maintainer Name'})),
+ ('Author (e-mail), Maintainer (e-mail)', merge_dicts(
+ attrs,
+ {'author': 'Author Name',
+ 'author_email': 'author@name.com',
+ 'maintainer': 'Maintainer Name',
+ 'maintainer_email': 'maintainer@name.com'})),
+ ('No author (e-mail), no maintainer (e-mail)', merge_dicts(
+ attrs,
+ {'author_email': 'author@name.com',
+ 'maintainer_email': 'maintainer@name.com'})),
+ ('Author unicode', merge_dicts(
+ attrs,
+ {'author': '鉄沢寛'})),
+ ('Maintainer unicode', merge_dicts(
+ attrs,
+ {'maintainer': 'Jan Łukasiewicz'})),
+ ]
+
+ return test_cases
+
+
+@pytest.mark.parametrize('name,attrs', __maintainer_test_cases())
+def test_maintainer_author(name, attrs, tmpdir):
+ tested_keys = {
+ 'author': 'Author',
+ 'author_email': 'Author-email',
+ 'maintainer': 'Maintainer',
+ 'maintainer_email': 'Maintainer-email',
+ }
+
+ # Generate a PKG-INFO file
+ dist = Distribution(attrs)
+ fn = tmpdir.mkdir('pkg_info')
+ fn_s = str(fn)
+
+ dist.metadata.write_pkg_info(fn_s)
+
+ with io.open(str(fn.join('PKG-INFO')), 'r', encoding='utf-8') as f:
+ raw_pkg_lines = f.readlines()
+
+ # Drop blank lines and strip lines from default description
+ pkg_lines = list(filter(None, raw_pkg_lines[:-2]))
+
+ pkg_lines_set = set(pkg_lines)
+
+ # Duplicate lines should not be generated
+ assert len(pkg_lines) == len(pkg_lines_set)
+
+ for fkey, dkey in tested_keys.items():
+ val = attrs.get(dkey, None)
+ if val is None:
+ for line in pkg_lines:
+ assert not line.startswith(fkey + ':')
+ else:
+ line = '%s: %s' % (fkey, val)
+ assert line in pkg_lines_set
diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py
index e7d2f5ca..b87c3704 100644
--- a/setuptools/tests/test_dist.py
+++ b/setuptools/tests/test_dist.py
@@ -1,7 +1,5 @@
-import io
import collections
import re
-import functools
import os
import urllib.request
import urllib.parse
@@ -11,10 +9,7 @@ from setuptools.dist import (
check_package_data,
DistDeprecationWarning,
check_specifier,
- rfc822_escape,
- rfc822_unescape,
)
-from setuptools import sic
from setuptools import Distribution
from .textwrap import DALS
@@ -82,198 +77,6 @@ EXAMPLE_BASE_INFO = dict(
)
-def __read_test_cases():
- base = EXAMPLE_BASE_INFO
-
- params = functools.partial(dict, base)
-
- test_cases = [
- ('Metadata version 1.0', params()),
- ('Metadata Version 1.0: Short long description', params(
- long_description='Short long description',
- )),
- ('Metadata version 1.1: Classifiers', params(
- classifiers=[
- 'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.7',
- 'License :: OSI Approved :: MIT License',
- ],
- )),
- ('Metadata version 1.1: Download URL', params(
- download_url='https://example.com',
- )),
- ('Metadata Version 1.2: Requires-Python', params(
- python_requires='>=3.7',
- )),
- pytest.param(
- 'Metadata Version 1.2: Project-Url',
- params(project_urls=dict(Foo='https://example.bar')),
- marks=pytest.mark.xfail(
- reason="Issue #1578: project_urls not read",
- ),
- ),
- ('Metadata Version 2.1: Long Description Content Type', params(
- long_description_content_type='text/x-rst; charset=UTF-8',
- )),
- ('License', params(license='MIT', )),
- ('License multiline', params(
- license='This is a long license \nover multiple lines',
- )),
- pytest.param(
- 'Metadata Version 2.1: Provides Extra',
- params(provides_extras=['foo', 'bar']),
- marks=pytest.mark.xfail(reason="provides_extras not read"),
- ),
- ('Missing author', dict(
- name='foo',
- version='1.0.0',
- author_email='snorri@sturluson.name',
- )),
- ('Missing author e-mail', dict(
- name='foo',
- version='1.0.0',
- author='Snorri Sturluson',
- )),
- ('Missing author and e-mail', dict(
- name='foo',
- version='1.0.0',
- )),
- ('Bypass normalized version', dict(
- name='foo',
- version=sic('1.0.0a'),
- )),
- ]
-
- return test_cases
-
-
-@pytest.mark.parametrize('name,attrs', __read_test_cases())
-def test_read_metadata(name, attrs):
- dist = Distribution(attrs)
- metadata_out = dist.metadata
- dist_class = metadata_out.__class__
-
- # Write to PKG_INFO and then load into a new metadata object
- PKG_INFO = io.StringIO()
-
- metadata_out.write_pkg_file(PKG_INFO)
-
- PKG_INFO.seek(0)
- metadata_in = dist_class()
- metadata_in.read_pkg_file(PKG_INFO)
-
- tested_attrs = [
- ('name', dist_class.get_name),
- ('version', dist_class.get_version),
- ('author', dist_class.get_contact),
- ('author_email', dist_class.get_contact_email),
- ('metadata_version', dist_class.get_metadata_version),
- ('provides', dist_class.get_provides),
- ('description', dist_class.get_description),
- ('long_description', dist_class.get_long_description),
- ('download_url', dist_class.get_download_url),
- ('keywords', dist_class.get_keywords),
- ('platforms', dist_class.get_platforms),
- ('obsoletes', dist_class.get_obsoletes),
- ('requires', dist_class.get_requires),
- ('classifiers', dist_class.get_classifiers),
- ('project_urls', lambda s: getattr(s, 'project_urls', {})),
- ('provides_extras', lambda s: getattr(s, 'provides_extras', set())),
- ]
-
- for attr, getter in tested_attrs:
- assert getter(metadata_in) == getter(metadata_out)
-
-
-def __maintainer_test_cases():
- attrs = {"name": "package",
- "version": "1.0",
- "description": "xxx"}
-
- def merge_dicts(d1, d2):
- d1 = d1.copy()
- d1.update(d2)
-
- return d1
-
- test_cases = [
- ('No author, no maintainer', attrs.copy()),
- ('Author (no e-mail), no maintainer', merge_dicts(
- attrs,
- {'author': 'Author Name'})),
- ('Author (e-mail), no maintainer', merge_dicts(
- attrs,
- {'author': 'Author Name',
- 'author_email': 'author@name.com'})),
- ('No author, maintainer (no e-mail)', merge_dicts(
- attrs,
- {'maintainer': 'Maintainer Name'})),
- ('No author, maintainer (e-mail)', merge_dicts(
- attrs,
- {'maintainer': 'Maintainer Name',
- 'maintainer_email': 'maintainer@name.com'})),
- ('Author (no e-mail), Maintainer (no-email)', merge_dicts(
- attrs,
- {'author': 'Author Name',
- 'maintainer': 'Maintainer Name'})),
- ('Author (e-mail), Maintainer (e-mail)', merge_dicts(
- attrs,
- {'author': 'Author Name',
- 'author_email': 'author@name.com',
- 'maintainer': 'Maintainer Name',
- 'maintainer_email': 'maintainer@name.com'})),
- ('No author (e-mail), no maintainer (e-mail)', merge_dicts(
- attrs,
- {'author_email': 'author@name.com',
- 'maintainer_email': 'maintainer@name.com'})),
- ('Author unicode', merge_dicts(
- attrs,
- {'author': '鉄沢寛'})),
- ('Maintainer unicode', merge_dicts(
- attrs,
- {'maintainer': 'Jan Łukasiewicz'})),
- ]
-
- return test_cases
-
-
-@pytest.mark.parametrize('name,attrs', __maintainer_test_cases())
-def test_maintainer_author(name, attrs, tmpdir):
- tested_keys = {
- 'author': 'Author',
- 'author_email': 'Author-email',
- 'maintainer': 'Maintainer',
- 'maintainer_email': 'Maintainer-email',
- }
-
- # Generate a PKG-INFO file
- dist = Distribution(attrs)
- fn = tmpdir.mkdir('pkg_info')
- fn_s = str(fn)
-
- dist.metadata.write_pkg_info(fn_s)
-
- with io.open(str(fn.join('PKG-INFO')), 'r', encoding='utf-8') as f:
- raw_pkg_lines = f.readlines()
-
- # Drop blank lines and strip lines from default description
- pkg_lines = list(filter(None, raw_pkg_lines[:-2]))
-
- pkg_lines_set = set(pkg_lines)
-
- # Duplicate lines should not be generated
- assert len(pkg_lines) == len(pkg_lines_set)
-
- for fkey, dkey in tested_keys.items():
- val = attrs.get(dkey, None)
- if val is None:
- for line in pkg_lines:
- assert not line.startswith(fkey + ':')
- else:
- line = '%s: %s' % (fkey, val)
- assert line in pkg_lines_set
-
-
def test_provides_extras_deterministic_order():
extras = collections.OrderedDict()
extras['a'] = ['foo']
@@ -347,40 +150,6 @@ def test_check_specifier():
dist = Distribution(attrs)
-@pytest.mark.parametrize(
- 'content, result',
- (
- pytest.param(
- "Just a single line",
- None,
- id="single_line",
- ),
- pytest.param(
- "Multiline\nText\nwithout\nextra indents\n",
- None,
- id="multiline",
- ),
- pytest.param(
- "Multiline\n With\n\nadditional\n indentation",
- None,
- id="multiline_with_indentation",
- ),
- pytest.param(
- " Leading whitespace",
- "Leading whitespace",
- id="remove_leading_whitespace",
- ),
- pytest.param(
- " Leading whitespace\nIn\n Multiline comment",
- "Leading whitespace\nIn\n Multiline comment",
- id="remove_leading_whitespace_multiline",
- ),
- )
-)
-def test_rfc822_unescape(content, result):
- assert (result or content) == rfc822_unescape(rfc822_escape(content))
-
-
def test_metadata_name():
with pytest.raises(DistutilsSetupError, match='missing.*name'):
Distribution()._validate_metadata()