Commit 17a5df2f by Sébastien Eustace

Fix metadata inconsistency between wheels and sdists

parent e2ca0611
...@@ -13,6 +13,7 @@ from poetry.utils._compat import Path ...@@ -13,6 +13,7 @@ from poetry.utils._compat import Path
from poetry.utils._compat import basestring from poetry.utils._compat import basestring
from poetry.utils._compat import glob from poetry.utils._compat import glob
from poetry.utils._compat import lru_cache from poetry.utils._compat import lru_cache
from poetry.utils._compat import to_str
from poetry.vcs import get_vcs from poetry.vcs import get_vcs
from ..metadata import Metadata from ..metadata import Metadata
...@@ -22,6 +23,13 @@ from ..utils.package_include import PackageInclude ...@@ -22,6 +23,13 @@ from ..utils.package_include import PackageInclude
AUTHOR_REGEX = re.compile(r"(?u)^(?P<name>[- .,\w\d'’\"()]+) <(?P<email>.+?)>$") AUTHOR_REGEX = re.compile(r"(?u)^(?P<name>[- .,\w\d'’\"()]+) <(?P<email>.+?)>$")
METADATA_BASE = """\
Metadata-Version: 2.1
Name: {name}
Version: {version}
Summary: {summary}
"""
class Builder(object): class Builder(object):
...@@ -147,6 +155,54 @@ class Builder(object): ...@@ -147,6 +155,54 @@ class Builder(object):
return sorted(to_add) return sorted(to_add)
def get_metadata_content(self): # type: () -> bytes
content = METADATA_BASE.format(
name=self._meta.name,
version=self._meta.version,
summary=to_str(self._meta.summary),
)
# Optional fields
if self._meta.home_page:
content += "Home-page: {}\n".format(self._meta.home_page)
if self._meta.license:
content += "License: {}\n".format(self._meta.license)
if self._meta.keywords:
content += "Keywords: {}\n".format(self._meta.keywords)
if self._meta.author:
content += "Author: {}\n".format(to_str(self._meta.author))
if self._meta.author_email:
content += "Author-email: {}\n".format(to_str(self._meta.author_email))
if self._meta.requires_python:
content += "Requires-Python: {}\n".format(self._meta.requires_python)
for classifier in self._meta.classifiers:
content += "Classifier: {}\n".format(classifier)
for extra in sorted(self._meta.provides_extra):
content += "Provides-Extra: {}\n".format(extra)
for dep in sorted(self._meta.requires_dist):
content += "Requires-Dist: {}\n".format(dep)
for url in sorted(self._meta.project_urls, key=lambda u: u[0]):
content += "Project-URL: {}\n".format(to_str(url))
if self._meta.description_content_type:
content += "Description-Content-Type: {}\n".format(
self._meta.description_content_type
)
if self._meta.description is not None:
content += "\n" + to_str(self._meta.description) + "\n"
return content
def convert_entry_points(self): # type: () -> dict def convert_entry_points(self): # type: () -> dict
result = defaultdict(list) result = defaultdict(list)
......
...@@ -41,17 +41,6 @@ setup(**setup_kwargs) ...@@ -41,17 +41,6 @@ setup(**setup_kwargs)
""" """
PKG_INFO = """\
Metadata-Version: 2.1
Name: {name}
Version: {version}
Summary: {summary}
Home-page: {home_page}
Author: {author}
Author-email: {author_email}
"""
class SdistBuilder(Builder): class SdistBuilder(Builder):
def build(self, target_dir=None): # type: (Path) -> Path def build(self, target_dir=None): # type: (Path) -> Path
self._io.writeln(" - Building <info>sdist</info>") self._io.writeln(" - Building <info>sdist</info>")
...@@ -195,34 +184,7 @@ class SdistBuilder(Builder): ...@@ -195,34 +184,7 @@ class SdistBuilder(Builder):
) )
def build_pkg_info(self): def build_pkg_info(self):
pkg_info = PKG_INFO.format( return encode(self.get_metadata_content())
name=self._meta.name,
version=self._meta.version,
summary=self._meta.summary,
home_page=self._meta.home_page,
author=to_str(self._meta.author),
author_email=to_str(self._meta.author_email),
)
if self._meta.keywords:
pkg_info += "Keywords: {}\n".format(self._meta.keywords)
if self._meta.requires_python:
pkg_info += "Requires-Python: {}\n".format(self._meta.requires_python)
for classifier in self._meta.classifiers:
pkg_info += "Classifier: {}\n".format(classifier)
for extra in sorted(self._meta.provides_extra):
pkg_info += "Provides-Extra: {}\n".format(extra)
for dep in sorted(self._meta.requires_dist):
pkg_info += "Requires-Dist: {}\n".format(dep)
for url in sorted(self._meta.project_urls, key=lambda u: u[0]):
pkg_info += "Project-URL: {}\n".format(url)
return encode(pkg_info)
def find_packages(self, include): def find_packages(self, include):
""" """
......
...@@ -15,6 +15,7 @@ from typing import Set ...@@ -15,6 +15,7 @@ from typing import Set
from poetry.__version__ import __version__ from poetry.__version__ import __version__
from poetry.semver import parse_constraint from poetry.semver import parse_constraint
from poetry.utils._compat import decode
from ..utils.helpers import normalize_file_permissions from ..utils.helpers import normalize_file_permissions
from ..utils.package_include import PackageInclude from ..utils.package_include import PackageInclude
...@@ -309,43 +310,4 @@ class WheelBuilder(Builder): ...@@ -309,43 +310,4 @@ class WheelBuilder(Builder):
""" """
Write out metadata in the 2.x format (email like) Write out metadata in the 2.x format (email like)
""" """
fp.write("Metadata-Version: 2.1\n") fp.write(decode(self.get_metadata_content()))
fp.write("Name: {}\n".format(self._meta.name))
fp.write("Version: {}\n".format(self._meta.version))
fp.write("Summary: {}\n".format(self._meta.summary))
fp.write("Home-page: {}\n".format(self._meta.home_page or "UNKNOWN"))
fp.write("License: {}\n".format(self._meta.license or "UNKNOWN"))
# Optional fields
if self._meta.keywords:
fp.write("Keywords: {}\n".format(self._meta.keywords))
if self._meta.author:
fp.write("Author: {}\n".format(self._meta.author))
if self._meta.author_email:
fp.write("Author-email: {}\n".format(self._meta.author_email))
if self._meta.requires_python:
fp.write("Requires-Python: {}\n".format(self._meta.requires_python))
for classifier in self._meta.classifiers:
fp.write("Classifier: {}\n".format(classifier))
for extra in sorted(self._meta.provides_extra):
fp.write("Provides-Extra: {}\n".format(extra))
for dep in sorted(self._meta.requires_dist):
fp.write("Requires-Dist: {}\n".format(dep))
for url in sorted(self._meta.project_urls, key=lambda u: u[0]):
fp.write("Project-URL: {}\n".format(url))
if self._meta.description_content_type:
fp.write(
"Description-Content-Type: "
"{}\n".format(self._meta.description_content_type)
)
if self._meta.description is not None:
fp.write("\n" + self._meta.description + "\n")
...@@ -5,12 +5,9 @@ description = "Some description." ...@@ -5,12 +5,9 @@ description = "Some description."
authors = [ authors = [
"Sébastien Eustace <sebastien@eustace.io>" "Sébastien Eustace <sebastien@eustace.io>"
] ]
license = "MIT"
readme = "README.rst" readme = "README.rst"
homepage = "https://poetry.eustace.io/"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "3.6" python = "3.6"
# -*- coding: utf-8 -*-
from email.parser import Parser
from poetry.io import NullIO from poetry.io import NullIO
from poetry.masonry.builders.builder import Builder from poetry.masonry.builders.builder import Builder
from poetry.poetry import Poetry from poetry.poetry import Poetry
...@@ -52,3 +55,79 @@ def test_builder_find_invalid_case_sensitive_excluded_files(mocker): ...@@ -52,3 +55,79 @@ def test_builder_find_invalid_case_sensitive_excluded_files(mocker):
) )
assert {"my_package/Bar/foo/bar/Foo.py"} == builder.find_excluded_files() assert {"my_package/Bar/foo/bar/Foo.py"} == builder.find_excluded_files()
def test_get_metadata_content():
builder = Builder(
Poetry.create(Path(__file__).parent / "fixtures" / "complete"),
NullEnv(),
NullIO(),
)
metadata = builder.get_metadata_content()
p = Parser()
parsed = p.parsestr(metadata)
assert parsed["Metadata-Version"] == "2.1"
assert parsed["Name"] == "my-package"
assert parsed["Version"] == "1.2.3"
assert parsed["Summary"] == "Some description."
assert parsed["Author"] == "Sébastien Eustace"
assert parsed["Author-email"] == "sebastien@eustace.io"
assert parsed["Keywords"] == "packaging,dependency,poetry"
assert parsed["Requires-Python"] == ">=3.6,<4.0"
assert parsed["License"] == "MIT"
assert parsed["Home-page"] == "https://poetry.eustace.io/"
classifiers = parsed.get_all("Classifier")
assert classifiers == [
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Libraries :: Python Modules",
]
extras = parsed.get_all("Provides-Extra")
assert extras == ["time"]
requires = parsed.get_all("Requires-Dist")
assert requires == [
"cachy[msgpack] (>=0.2.0,<0.3.0)",
"cleo (>=0.6,<0.7)",
'pendulum (>=1.4,<2.0); extra == "time"',
]
urls = parsed.get_all("Project-URL")
assert urls == [
"Documentation, https://poetry.eustace.io/docs",
"Repository, https://github.com/sdispater/poetry",
]
def test_metadata_homepage_default():
builder = Builder(
Poetry.create(Path(__file__).parent / "fixtures" / "simple_version"),
NullEnv(),
NullIO(),
)
metadata = Parser().parsestr(builder.get_metadata_content())
assert metadata["Home-page"] is None
def test_metadata_with_vcs_dependencies():
builder = Builder(
Poetry.create(Path(__file__).parent / "fixtures" / "with_vcs_dependency"),
NullEnv(),
NullIO(),
)
metadata = Parser().parsestr(builder.get_metadata_content())
requires_dist = metadata["Requires-Dist"]
assert "cleo @ git+https://github.com/sdispater/cleo.git@master" == requires_dist
...@@ -135,48 +135,16 @@ def test_make_setup(): ...@@ -135,48 +135,16 @@ def test_make_setup():
assert ns["extras_require"] == {"time": ["pendulum>=1.4,<2.0"]} assert ns["extras_require"] == {"time": ["pendulum>=1.4,<2.0"]}
def test_make_pkg_info(): def test_make_pkg_info(mocker):
get_metadata_content = mocker.patch(
"poetry.masonry.builders.builder.Builder.get_metadata_content"
)
poetry = Poetry.create(project("complete")) poetry = Poetry.create(project("complete"))
builder = SdistBuilder(poetry, NullEnv(), NullIO()) builder = SdistBuilder(poetry, NullEnv(), NullIO())
pkg_info = builder.build_pkg_info() builder.build_pkg_info()
p = Parser()
parsed = p.parsestr(to_str(pkg_info))
assert parsed["Metadata-Version"] == "2.1"
assert parsed["Name"] == "my-package"
assert parsed["Version"] == "1.2.3"
assert parsed["Summary"] == "Some description."
assert parsed["Author"] == "Sébastien Eustace"
assert parsed["Author-email"] == "sebastien@eustace.io"
assert parsed["Keywords"] == "packaging,dependency,poetry"
assert parsed["Requires-Python"] == ">=3.6,<4.0"
classifiers = parsed.get_all("Classifier")
assert classifiers == [
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Libraries :: Python Modules",
]
extras = parsed.get_all("Provides-Extra") assert get_metadata_content.called
assert extras == ["time"]
requires = parsed.get_all("Requires-Dist")
assert requires == [
"cachy[msgpack] (>=0.2.0,<0.3.0)",
"cleo (>=0.6,<0.7)",
'pendulum (>=1.4,<2.0); extra == "time"',
]
urls = parsed.get_all("Project-URL")
assert urls == [
"Documentation, https://poetry.eustace.io/docs",
"Repository, https://github.com/sdispater/poetry",
]
def test_make_pkg_info_any_python(): def test_make_pkg_info_any_python():
......
...@@ -128,43 +128,3 @@ def test_package_with_include(mocker): ...@@ -128,43 +128,3 @@ def test_package_with_include(mocker):
assert "my_module.py" in names assert "my_module.py" in names
assert "notes.txt" in names assert "notes.txt" in names
assert "package_with_include/__init__.py" in names assert "package_with_include/__init__.py" in names
def test_write_metadata_file_license_homepage_default(mocker):
# Preparation
mocked_poetry = mocker.Mock()
mocked_poetry.file.parent = Path(".")
mocked_poetry.package = ProjectPackage("pkg_name", "1.0.0")
mocked_file = mocker.Mock()
mocked_venv = mocker.Mock()
mocked_io = mocker.Mock()
# patch Module init inside Builder class
mocker.patch("poetry.masonry.builders.builder.Module")
w = WheelBuilder(mocked_poetry, mocked_venv, mocked_io)
# Action
w._write_metadata_file(mocked_file)
# Assertion
mocked_file.write.assert_any_call("Home-page: UNKNOWN\n")
mocked_file.write.assert_any_call("License: UNKNOWN\n")
def test_metadata_file_with_vcs_dependencies():
project_path = fixtures_dir / "with_vcs_dependency"
WheelBuilder.make(Poetry.create(str(project_path)), NullEnv(), NullIO())
whl = project_path / "dist" / "with_vcs_dependency-1.2.3-py3-none-any.whl"
assert whl.exists()
p = Parser()
with zipfile.ZipFile(str(whl)) as z:
metadata = p.parsestr(
to_str(z.read("with_vcs_dependency-1.2.3.dist-info/METADATA"))
)
requires_dist = metadata["Requires-Dist"]
assert "cleo @ git+https://github.com/sdispater/cleo.git@master" == requires_dist
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment