Commit 45bc4b99 authored by Jason R. Coombs's avatar Jason R. Coombs

Merge branch 'master' into pr1127

parents e5461b6c 09e14c77
v36.4.0
-------
* #1075: Add new ``Description-Content-Type`` metadata field. `See here for
documentation on how to use this field.
<https://packaging.python.org/specifications/#description-content-type>`_
* #1068: Sort files and directories when building eggs for
deterministic order.
v36.3.0
-------
* #1131: Make possible using several files within ``file:`` directive
in metadata.long_description in ``setup.cfg``.
v36.2.7 v36.2.7
------- -------
......
...@@ -2306,7 +2306,7 @@ boilerplate code in some cases. ...@@ -2306,7 +2306,7 @@ boilerplate code in some cases.
name = my_package name = my_package
version = attr: src.VERSION version = attr: src.VERSION
description = My package description description = My package description
long_description = file: README.rst long_description = file: README.rst, CHANGELOG.rst, LICENSE.rst
keywords = one, two keywords = one, two
license = BSD 3-Clause License license = BSD 3-Clause License
classifiers = classifiers =
...@@ -2379,7 +2379,7 @@ Type names used below: ...@@ -2379,7 +2379,7 @@ Type names used below:
Special directives: Special directives:
* ``attr:`` - value could be read from module attribute * ``attr:`` - value could be read from module attribute
* ``file:`` - value could be read from a file * ``file:`` - value could be read from a list of files and then concatenated
.. note:: .. note::
...@@ -2394,9 +2394,9 @@ Metadata ...@@ -2394,9 +2394,9 @@ Metadata
Aliases given below are supported for compatibility reasons, Aliases given below are supported for compatibility reasons,
but not advised. but not advised.
================= ================= ===== ============================== ================= =====
Key Aliases Accepted value type Key Aliases Accepted value type
================= ================= ===== ============================== ================= =====
name str name str
version attr:, str version attr:, str
url home-page str url home-page str
...@@ -2409,12 +2409,13 @@ classifiers classifier file:, list-comma ...@@ -2409,12 +2409,13 @@ classifiers classifier file:, list-comma
license file:, str license file:, str
description summary file:, str description summary file:, str
long_description long-description file:, str long_description long-description file:, str
long_description_content_type str
keywords list-comma keywords list-comma
platforms platform list-comma platforms platform list-comma
provides list-comma provides list-comma
requires list-comma requires list-comma
obsoletes list-comma obsoletes list-comma
================= ================= ===== ============================== ================= =====
.. note:: .. note::
......
[bumpversion] [bumpversion]
current_version = 36.2.7 current_version = 36.3.0
commit = True commit = True
tag = True tag = True
...@@ -19,7 +19,7 @@ repository = https://upload.pypi.org/legacy/ ...@@ -19,7 +19,7 @@ repository = https://upload.pypi.org/legacy/
[sdist] [sdist]
formats = zip formats = zip
[wheel] [bdist_wheel]
universal = 1 universal = 1
[bumpversion:file:setup.py] [bumpversion:file:setup.py]
......
...@@ -89,12 +89,13 @@ def pypi_link(pkg_filename): ...@@ -89,12 +89,13 @@ def pypi_link(pkg_filename):
setup_params = dict( setup_params = dict(
name="setuptools", name="setuptools",
version="36.2.7", version="36.3.0",
description="Easily download, build, install, upgrade, and uninstall " description="Easily download, build, install, upgrade, and uninstall "
"Python packages", "Python packages",
author="Python Packaging Authority", author="Python Packaging Authority",
author_email="distutils-sig@python.org", author_email="distutils-sig@python.org",
long_description=long_description, long_description=long_description,
long_description_content_type='text/x-rst; charset=UTF-8',
keywords="CPAN PyPI distutils eggs package management", keywords="CPAN PyPI distutils eggs package management",
url="https://github.com/pypa/setuptools", url="https://github.com/pypa/setuptools",
src_root=None, src_root=None,
......
...@@ -38,6 +38,14 @@ def strip_module(filename): ...@@ -38,6 +38,14 @@ def strip_module(filename):
filename = filename[:-6] filename = filename[:-6]
return filename return filename
def sorted_walk(dir):
"""Do os.walk in a reproducible way,
independent of indeterministic filesystem readdir order
"""
for base, dirs, files in os.walk(dir):
dirs.sort()
files.sort()
yield base, dirs, files
def write_stub(resource, pyfile): def write_stub(resource, pyfile):
_stub_template = textwrap.dedent(""" _stub_template = textwrap.dedent("""
...@@ -302,7 +310,7 @@ class bdist_egg(Command): ...@@ -302,7 +310,7 @@ class bdist_egg(Command):
ext_outputs = [] ext_outputs = []
paths = {self.bdist_dir: ''} paths = {self.bdist_dir: ''}
for base, dirs, files in os.walk(self.bdist_dir): for base, dirs, files in sorted_walk(self.bdist_dir):
for filename in files: for filename in files:
if os.path.splitext(filename)[1].lower() in NATIVE_EXTENSIONS: if os.path.splitext(filename)[1].lower() in NATIVE_EXTENSIONS:
all_outputs.append(paths[base] + filename) all_outputs.append(paths[base] + filename)
...@@ -329,7 +337,7 @@ NATIVE_EXTENSIONS = dict.fromkeys('.dll .so .dylib .pyd'.split()) ...@@ -329,7 +337,7 @@ NATIVE_EXTENSIONS = dict.fromkeys('.dll .so .dylib .pyd'.split())
def walk_egg(egg_dir): def walk_egg(egg_dir):
"""Walk an unpacked egg's contents, skipping the metadata directory""" """Walk an unpacked egg's contents, skipping the metadata directory"""
walker = os.walk(egg_dir) walker = sorted_walk(egg_dir)
base, dirs, files = next(walker) base, dirs, files = next(walker)
if 'EGG-INFO' in dirs: if 'EGG-INFO' in dirs:
dirs.remove('EGG-INFO') dirs.remove('EGG-INFO')
...@@ -463,10 +471,10 @@ def make_zipfile(zip_filename, base_dir, verbose=0, dry_run=0, compress=True, ...@@ -463,10 +471,10 @@ def make_zipfile(zip_filename, base_dir, verbose=0, dry_run=0, compress=True,
compression = zipfile.ZIP_DEFLATED if compress else zipfile.ZIP_STORED compression = zipfile.ZIP_DEFLATED if compress else zipfile.ZIP_STORED
if not dry_run: if not dry_run:
z = zipfile.ZipFile(zip_filename, mode, compression=compression) z = zipfile.ZipFile(zip_filename, mode, compression=compression)
for dirname, dirs, files in os.walk(base_dir): for dirname, dirs, files in sorted_walk(base_dir):
visit(z, dirname, files) visit(z, dirname, files)
z.close() z.close()
else: else:
for dirname, dirs, files in os.walk(base_dir): for dirname, dirs, files in sorted_walk(base_dir):
visit(None, dirname, files) visit(None, dirname, files)
return zip_filename return zip_filename
...@@ -599,6 +599,10 @@ def write_pkg_info(cmd, basename, filename): ...@@ -599,6 +599,10 @@ def write_pkg_info(cmd, basename, filename):
metadata = cmd.distribution.metadata metadata = cmd.distribution.metadata
metadata.version, oldver = cmd.egg_version, metadata.version metadata.version, oldver = cmd.egg_version, metadata.version
metadata.name, oldname = cmd.egg_name, metadata.name metadata.name, oldname = cmd.egg_name, metadata.name
metadata.long_description_content_type = getattr(
cmd.distribution,
'long_description_content_type'
)
try: try:
# write unescaped data to PKG-INFO, so older pkg_resources # write unescaped data to PKG-INFO, so older pkg_resources
# can still parse it # can still parse it
......
...@@ -245,33 +245,39 @@ class ConfigHandler(object): ...@@ -245,33 +245,39 @@ class ConfigHandler(object):
directory with setup.py. directory with setup.py.
Examples: Examples:
include: LICENSE file: LICENSE
include: src/file.txt file: README.rst, CHANGELOG.md, src/file.txt
:param str value: :param str value:
:rtype: str :rtype: str
""" """
include_directive = 'file:'
if not isinstance(value, string_types): if not isinstance(value, string_types):
return value return value
include_directive = 'file:'
if not value.startswith(include_directive): if not value.startswith(include_directive):
return value return value
current_directory = os.getcwd() spec = value[len(include_directive):]
filepaths = (os.path.abspath(path.strip()) for path in spec.split(','))
filepath = value.replace(include_directive, '').strip() return '\n'.join(
filepath = os.path.abspath(filepath) cls._read_file(path)
for path in filepaths
if not filepath.startswith(current_directory): if (cls._assert_local(path) or True)
and os.path.isfile(path)
)
@staticmethod
def _assert_local(filepath):
if not filepath.startswith(os.getcwd()):
raise DistutilsOptionError( raise DistutilsOptionError(
'`file:` directive can not access %s' % filepath) '`file:` directive can not access %s' % filepath)
if os.path.isfile(filepath): @staticmethod
def _read_file(filepath):
with io.open(filepath, encoding='utf-8') as f: with io.open(filepath, encoding='utf-8') as f:
value = f.read() return f.read()
return value
@classmethod @classmethod
def _parse_attr(cls, value): def _parse_attr(cls, value):
......
...@@ -58,6 +58,13 @@ def write_pkg_file(self, file): ...@@ -58,6 +58,13 @@ def write_pkg_file(self, file):
if self.download_url: if self.download_url:
file.write('Download-URL: %s\n' % self.download_url) file.write('Download-URL: %s\n' % self.download_url)
long_desc_content_type = getattr(
self,
'long_description_content_type',
None
) or 'UNKNOWN'
file.write('Description-Content-Type: %s\n' % long_desc_content_type)
long_desc = rfc822_escape(self.get_long_description()) long_desc = rfc822_escape(self.get_long_description())
file.write('Description: %s\n' % long_desc) file.write('Description: %s\n' % long_desc)
...@@ -317,6 +324,9 @@ class Distribution(Distribution_parse_config_files, _Distribution): ...@@ -317,6 +324,9 @@ class Distribution(Distribution_parse_config_files, _Distribution):
self.dist_files = [] self.dist_files = []
self.src_root = attrs and attrs.pop("src_root", None) self.src_root = attrs and attrs.pop("src_root", None)
self.patch_missing_pkg_info(attrs) self.patch_missing_pkg_info(attrs)
self.long_description_content_type = _attrs_dict.get(
'long_description_content_type'
)
# Make sure we have any eggs needed to interpret 'attrs' # Make sure we have any eggs needed to interpret 'attrs'
if attrs is not None: if attrs is not None:
self.dependency_links = attrs.pop('dependency_links', []) self.dependency_links = attrs.pop('dependency_links', [])
......
...@@ -139,6 +139,24 @@ class TestMetadata: ...@@ -139,6 +139,24 @@ class TestMetadata:
assert metadata.download_url == 'http://test.test.com/test/' assert metadata.download_url == 'http://test.test.com/test/'
assert metadata.maintainer_email == 'test@test.com' assert metadata.maintainer_email == 'test@test.com'
def test_file_mixed(self, tmpdir):
fake_env(
tmpdir,
'[metadata]\n'
'long_description = file: README.rst, CHANGES.rst\n'
'\n'
)
tmpdir.join('README.rst').write('readme contents\nline2')
tmpdir.join('CHANGES.rst').write('changelog contents\nand stuff')
with get_dist(tmpdir) as dist:
assert dist.metadata.long_description == (
'readme contents\nline2\n'
'changelog contents\nand stuff'
)
def test_file_sandboxed(self, tmpdir): def test_file_sandboxed(self, tmpdir):
fake_env( fake_env(
......
...@@ -398,6 +398,31 @@ class TestEggInfo(object): ...@@ -398,6 +398,31 @@ class TestEggInfo(object):
self._run_install_command(tmpdir_cwd, env) self._run_install_command(tmpdir_cwd, env)
assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == [] assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
def test_long_description_content_type(self, tmpdir_cwd, env):
# Test that specifying a `long_description_content_type` keyword arg to
# the `setup` function results in writing a `Description-Content-Type`
# line to the `PKG-INFO` file in the `<distribution>.egg-info`
# directory.
# `Description-Content-Type` is described at
# https://github.com/pypa/python-packaging-user-guide/pull/258
self._setup_script_with_requires(
"""long_description_content_type='text/markdown',""")
environ = os.environ.copy().update(
HOME=env.paths['home'],
)
code, data = environment.run_setup_py(
cmd=['egg_info'],
pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
data_stream=1,
env=environ,
)
egg_info_dir = os.path.join('.', 'foo.egg-info')
with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file:
pkg_info_lines = pkginfo_file.read().split('\n')
expected_line = 'Description-Content-Type: text/markdown'
assert expected_line in pkg_info_lines
def test_python_requires_egg_info(self, tmpdir_cwd, env): def test_python_requires_egg_info(self, tmpdir_cwd, env):
self._setup_script_with_requires( self._setup_script_with_requires(
"""python_requires='>=2.7.12',""") """python_requires='>=2.7.12',""")
......
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