Repository: pypa/setuptools
Pull request #1131 (2017-08-09 20:31:28)

Allow combining several files in metadata.long_description
This basically allows one to include several files into `long_description` via `setup.cfg` in a declarative way.

#### Use case

Sometimes one wants to have their README and CHANGELOG stored as separate files, so that GitHub would only render README contents on a main repo page. But it is nice to [list changes](https://github.com/aio-libs/aiohttp/blob/v2.2.5/setup.py#L90) on PYPI as well.

For such cases I suggest parsing `long_description` config value as a list with possibility to inject static files contents via `file:` directive.

This would've been possible with [RST `.. include::`](https://github.com/aio-libs/aiohttp/blob/master/docs/changes.rst#L3-L5), but setuptools doesn't process RST internally. So it's not an option.

In general, suggested change doesn't break compatibility.

/cc: @jaraco it would be interesting to see your feedback :)

webknjaz: @jaraco ping
jaraco: @idlesign Would you review this PR?

I would add you as a reviewer, but I've only just invited you to the pypa org, so I can't add you as a reviewer formally.
idlesign: @jaraco Sure. Thank you.

@webknjaz, @jaraco I'd like to discuss it. I don't think `file` directive interpolation is a good approach, because it won't be easy to port to PEP 518 (if/when time comes); it'll provoke users putting formatted texts in `setup.cfg`; the problem seems be be solvable in a more generic way.

When I've started this declarative thing, I've been thinking about `file:` (which was `include:` at the beginning, thank you for docstring fix by the way) accepting multiple comma-separated arguments:

```
long_description = file: README.rst, CHANGELOG
```
This would allow file contents concatenation in a simple manner. Note that we deliberately simplify directive by not accepting joiner string, which is `\n` is this very pull request; that's because formatting and headings are usually done in files themselves. 

The reason it has not been implemented is my personal dislike of very long descriptions: I believe in that what we see in cheese shop should be concise and changelogs (and license texts for that matter) don't really belong there — see how long-paced https://github.com/pypa/warehouse puts it on a separate Release History page.

Now I'd like to hear you thoughts.
jaraco: I also used to include the changelog as a matter of course in my packages but found that it grew unwieldy after a few releases, so eventually dropped changelogs from the package metadata and left the either in the repository or published with the docs. Still, I think it's common enough that it might be nice to support.

I also like the constraint that @idlesign's proposal suggests, which allows for multiple files, but doesn't introduce a mixed-mode design (where some content is in files and some content is inline).
webknjaz: Well, I'd add that sometimes people may want to add a delimiter between files and include those files into Sphinx docs, for example, in a different way. Does it make sense to create additional one-liner file in such case?

P.S. @jaraco @idlesign I'll push suggested changes soon. Thanks for your feedback!

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 19afc8cb1..1392e2a68 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,9 @@
+v36.3.0
+-------
+
+* #1131: Make possible using several files within ``file:`` directive
+  in metadata.long_description in ``setup.cfg``.
+
 v36.2.7
 -------
 
diff --git a/docs/setuptools.txt b/docs/setuptools.txt
index eb9fdbd3c..45d746d26 100644
--- a/docs/setuptools.txt
+++ b/docs/setuptools.txt
@@ -2306,7 +2306,7 @@ boilerplate code in some cases.
     name = my_package
     version = attr: src.VERSION
     description = My package description
-    long_description = file: README.rst
+    long_description = file: README.rst, CHANGELOG.rst, LICENSE.rst
     keywords = one, two
     license = BSD 3-Clause License
     classifiers =
@@ -2379,7 +2379,7 @@ Type names used below:
 Special directives:
 
 * ``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::
diff --git a/setuptools/config.py b/setuptools/config.py
index 06a61d160..9a62e2ec5 100644
--- a/setuptools/config.py
+++ b/setuptools/config.py
@@ -245,33 +245,39 @@ class ConfigHandler(object):
         directory with setup.py.
 
         Examples:
-            include: LICENSE
-            include: src/file.txt
+            file: LICENSE
+            file: README.rst, CHANGELOG.md, src/file.txt
 
         :param str value:
         :rtype: str
         """
+        include_directive = 'file:'
+
         if not isinstance(value, string_types):
             return value
 
-        include_directive = 'file:'
         if not value.startswith(include_directive):
             return value
 
-        current_directory = os.getcwd()
-
-        filepath = value.replace(include_directive, '').strip()
-        filepath = os.path.abspath(filepath)
-
-        if not filepath.startswith(current_directory):
+        spec = value[len(include_directive):]
+        filepaths = (os.path.abspath(path.strip()) for path in spec.split(','))
+        return '\n'.join(
+            cls._read_file(path)
+            for path in filepaths
+            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(
                 '`file:` directive can not access %s' % filepath)
 
-        if os.path.isfile(filepath):
-            with io.open(filepath, encoding='utf-8') as f:
-                value = f.read()
-
-        return value
+    @staticmethod
+    def _read_file(filepath):
+        with io.open(filepath, encoding='utf-8') as f:
+            return f.read()
 
     @classmethod
     def _parse_attr(cls, value):


Test patch:
diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py
index dbabd69e6..cdfa5af43 100644
--- a/setuptools/tests/test_config.py
+++ b/setuptools/tests/test_config.py
@@ -139,6 +139,24 @@ class TestMetadata:
             assert metadata.download_url == 'http://test.test.com/test/'
             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):
 
         fake_env(