summaryrefslogtreecommitdiff
path: root/setuptools/command/_requirestxt.py
diff options
context:
space:
mode:
Diffstat (limited to 'setuptools/command/_requirestxt.py')
-rw-r--r--setuptools/command/_requirestxt.py96
1 files changed, 96 insertions, 0 deletions
diff --git a/setuptools/command/_requirestxt.py b/setuptools/command/_requirestxt.py
new file mode 100644
index 00000000..ebed70bc
--- /dev/null
+++ b/setuptools/command/_requirestxt.py
@@ -0,0 +1,96 @@
+"""Helper code used to generate ``requires.txt`` files in the egg-info directory.
+
+The ``requires.txt`` file has an specific format:
+ - Environment markers need to be part of the section headers and
+ should not be part of the requirement spec itself.
+
+See https://setuptools.pypa.io/en/latest/deprecated/python_eggs.html#requires-txt
+"""
+from collections import defaultdict
+from itertools import filterfalse
+from typing import Dict, List, Tuple, Mapping, TypeVar
+
+from .. import _reqs
+from ..extern.packaging.requirements import Requirement
+
+
+# dict can work as an ordered set
+_T = TypeVar("_T")
+_Ordered = Dict[_T, None]
+_ordered = dict
+_StrOrIter = _reqs._StrOrIter
+
+
+def _prepare(
+ install_requires: _StrOrIter, extras_require: Mapping[str, _StrOrIter]
+) -> Tuple[List[str], Dict[str, List[str]]]:
+ """Given values for ``install_requires`` and ``extras_require``
+ create modified versions in a way that can be written in ``requires.txt``
+ """
+ extras = _convert_extras_requirements(extras_require)
+ return _move_install_requirements_markers(install_requires, extras)
+
+
+def _convert_extras_requirements(
+ extras_require: _StrOrIter,
+) -> Mapping[str, _Ordered[Requirement]]:
+ """
+ Convert requirements in `extras_require` of the form
+ `"extra": ["barbazquux; {marker}"]` to
+ `"extra:{marker}": ["barbazquux"]`.
+ """
+ output: Mapping[str, _Ordered[Requirement]] = defaultdict(dict)
+ for section, v in extras_require.items():
+ # Do not strip empty sections.
+ output[section]
+ for r in _reqs.parse(v):
+ output[section + _suffix_for(r)].setdefault(r)
+
+ return output
+
+
+def _move_install_requirements_markers(
+ install_requires: _StrOrIter, extras_require: Mapping[str, _Ordered[Requirement]]
+) -> Tuple[List[str], Dict[str, List[str]]]:
+ """
+ The ``requires.txt`` file has an specific format:
+ - Environment markers need to be part of the section headers and
+ should not be part of the requirement spec itself.
+
+ Move requirements in ``install_requires`` that are using environment
+ markers ``extras_require``.
+ """
+
+ # divide the install_requires into two sets, simple ones still
+ # handled by install_requires and more complex ones handled by extras_require.
+
+ inst_reqs = list(_reqs.parse(install_requires))
+ simple_reqs = filter(_no_marker, inst_reqs)
+ complex_reqs = filterfalse(_no_marker, inst_reqs)
+ simple_install_requires = list(map(str, simple_reqs))
+
+ for r in complex_reqs:
+ extras_require[':' + str(r.marker)].setdefault(r)
+
+ expanded_extras = dict(
+ # list(dict.fromkeys(...)) ensures a list of unique strings
+ (k, list(dict.fromkeys(str(r) for r in map(_clean_req, v))))
+ for k, v in extras_require.items()
+ )
+
+ return simple_install_requires, expanded_extras
+
+
+def _suffix_for(req):
+ """Return the 'extras_require' suffix for a given requirement."""
+ return ':' + str(req.marker) if req.marker else ''
+
+
+def _clean_req(req):
+ """Given a Requirement, remove environment markers and return it"""
+ req.marker = None
+ return req
+
+
+def _no_marker(req):
+ return not req.marker