diff --git a/flit/upload.py b/flit/upload.py index afd5f798..4a9697c8 100644 --- a/flit/upload.py +++ b/flit/upload.py @@ -226,7 +226,7 @@ def build_post_data(action, metadata:Metadata): "version": metadata.version, # additional meta-data - "metadata_version": '2.1', + "metadata_version": '2.3', "summary": metadata.summary, "home_page": metadata.home_page, "author": metadata.author, diff --git a/flit_core/flit_core/common.py b/flit_core/flit_core/common.py index 9f2b9204..6625224b 100644 --- a/flit_core/flit_core/common.py +++ b/flit_core/flit_core/common.py @@ -347,8 +347,9 @@ class Metadata(object): obsoletes_dist = () requires_external = () provides_extra = () + dynamic = () - metadata_version = "2.1" + metadata_version = "2.3" def __init__(self, data): data = data.copy() @@ -359,9 +360,31 @@ def __init__(self, data): assert hasattr(self, k), "data does not have attribute '{}'".format(k) setattr(self, k, v) - def _normalise_name(self, n): + def _normalise_field_name(self, n): return n.lower().replace('-', '_') + def _normalise_core_metadata_name(self, name): + # Normalized Names (PEP 503) + return re.sub(r"[-_.]+", "-", name).lower() + + def _extract_extras(self, req): + match = re.search(r'\[([^]]*)\]', req) + if match: + list_str = match.group(1) + return [item.strip() for item in list_str.split(',')] + else: + return None + + def _normalise_requires_dist(self, req): + extras = self._extract_extras(req) + if extras: + normalised_extras = [self._normalise_core_metadata_name(extra) for extra in extras] + normalised_extras_str = ', '.join(normalised_extras) + normalised_req = re.sub(r'\[([^]]*)\]', f"[{normalised_extras_str}]", req) + return normalised_req + else: + return req + def write_metadata_file(self, fp): """Write out metadata in the email headers format""" fields = [ @@ -383,11 +406,11 @@ def write_metadata_file(self, fp): ] for field in fields: - value = getattr(self, self._normalise_name(field)) + value = getattr(self, self._normalise_field_name(field)) fp.write(u"{}: {}\n".format(field, value)) for field in optional_fields: - value = getattr(self, self._normalise_name(field)) + value = getattr(self, self._normalise_field_name(field)) if value is not None: # TODO: verify which fields can be multiline # The spec has multiline examples for Author, Maintainer & @@ -400,13 +423,15 @@ def write_metadata_file(self, fp): fp.write(u'Classifier: {}\n'.format(clsfr)) for req in self.requires_dist: - fp.write(u'Requires-Dist: {}\n'.format(req)) + normalised_req = self._normalise_requires_dist(req) + fp.write(u'Requires-Dist: {}\n'.format(normalised_req)) for url in self.project_urls: fp.write(u'Project-URL: {}\n'.format(url)) for extra in self.provides_extra: - fp.write(u'Provides-Extra: {}\n'.format(extra)) + normalised_extra = self._normalise_core_metadata_name(extra) + fp.write(u'Provides-Extra: {}\n'.format(normalised_extra)) if self.description is not None: fp.write(u'\n' + self.description + u'\n') diff --git a/flit_core/flit_core/tests/test_common.py b/flit_core/flit_core/tests/test_common.py index b6d62905..824fa5de 100644 --- a/flit_core/flit_core/tests/test_common.py +++ b/flit_core/flit_core/tests/test_common.py @@ -156,3 +156,52 @@ def test_metadata_multiline(tmp_path): assert msg['Version'] == d['version'] assert [l.lstrip() for l in msg['Author'].splitlines()] == d['author'].splitlines() assert not msg.defects + +@pytest.mark.parametrize( + ("requires_dist", "expected_result"), + [ + ('foo [extra_1, extra.2, extra-3, extra__4, extra..5, extra--6]', 'foo [extra-1, extra-2, extra-3, extra-4, extra-5, extra-6]'), + ('foo', 'foo'), + ('foo[bar]', 'foo[bar]'), + # https://packaging.python.org/en/latest/specifications/core-metadata/#requires-dist-multiple-use + ('pkginfo', 'pkginfo'), + ('zope.interface (>3.5.0)', 'zope.interface (>3.5.0)'), + ("pywin32 >1.0; sys_platform == 'win32'", "pywin32 >1.0; sys_platform == 'win32'"), + ], +) +def test_metadata_2_3_requires_dist(requires_dist, expected_result): + d = { + 'name': 'foo', + 'version': '1.0', + 'requires_dist': [requires_dist], + } + md = Metadata(d) + sio = StringIO() + md.write_metadata_file(sio) + sio.seek(0) + + msg = email.parser.Parser(policy=email.policy.compat32).parse(sio) + assert msg['Requires-Dist'] == expected_result + assert not msg.defects + +@pytest.mark.parametrize( + ("provides_extra", "expected_result"), + [ + ('foo', 'foo'), + ('foo__bar..baz', 'foo-bar-baz'), + ], +) +def test_metadata_2_3_provides_extra(provides_extra, expected_result): + d = { + 'name': 'foo', + 'version': '1.0', + 'provides_extra': [provides_extra], + } + md = Metadata(d) + sio = StringIO() + md.write_metadata_file(sio) + sio.seek(0) + + msg = email.parser.Parser(policy=email.policy.compat32).parse(sio) + assert msg['Provides-Extra'] == expected_result + assert not msg.defects