#7 Generate run/test requirements
Merged 2 months ago by churchyard. Opened 2 months ago by pviktori.
rpms/ pviktori/pyproject-rpm-macros runtime  into  master

file modified
+23

@@ -34,6 +34,28 @@ 

      %pyproject_install

  

  

+ Adding run-time and test-time dependencies

+ ------------------------------------------

+ 

+ To run tests in the `%check` section, the package's runtime dependencies

+ often need to also be included as build requirements.

+ If the project's build system supports the [`prepare-metadata-for-build-wheel`

+ hook](https://www.python.org/dev/peps/pep-0517/#prepare-metadata-for-build-wheel),

+ this can be done using the `-r` flag:

+ 

+     %generate_buildrequires

+     %pyproject_buildrequires -r

+ 

+ For projects that specify test requirements using an [`extra`

+ provide](https://packaging.python.org/specifications/core-metadata/#provides-extra-multiple-use),

+ these can be added using the `-x` flag.

+ For example, if upstream suggests installing test dependencies with

+ `pip install mypackage[testing]`, the test deps would be generated by:

+ 

+     %generate_buildrequires

+     %pyproject_buildrequires -r -x testing

+ 

+ 

  Limitations

  -----------

  

@@ -49,6 +71,7 @@ 

  

  Some valid Python version specifiers are not supported.

  

+ The `-x` flag does not yet support multiple (comma-separated) extras.

  

  [PEP 517]: https://www.python.org/dev/peps/pep-0517/

  [PEP 518]: https://www.python.org/dev/peps/pep-0518/

file modified
+4 -2

@@ -17,12 +17,14 @@ 

  fi

  }

  

- %pyproject_buildrequires() %{expand:\\\

+ %pyproject_buildrequires(rx:) %{expand:\\\

  echo 'python3-devel'

  echo 'python3dist(packaging)'

  echo 'python3dist(pip) >= 19'

  echo 'python3dist(pytoml)'

+ # setuptools assumes no pre-existing dist-info

+ rm -rfv *.dist-info/

  if [ -f %{__python3} ]; then

-   %{__python3} -I %{_rpmconfigdir}/redhat/pyproject_buildrequires.py %{?*}

+   %{__python3} -I %{_rpmconfigdir}/redhat/pyproject_buildrequires.py %{?**}

  fi

  }

file modified
+8 -1

@@ -2,6 +2,8 @@ 

  Summary:        RPM macros for PEP 517 Python packages

  License:        MIT

  

+ %bcond_without tests

+ 

  # Keep the version at zero and increment only release

  Version:        0

  Release:        3%{?dist}

@@ -26,12 +28,15 @@ 

  Requires: python3-pip >= 19

  Requires: python3-devel

  

- # Test dependencies

+ %if %{with tests}

  BuildRequires: python3dist(pytest)

  BuildRequires: python3dist(pyyaml)

  BuildRequires: python3dist(packaging)

  BuildRequires: python3dist(pytoml)

  BuildRequires: python3dist(pip)

+ BuildRequires: python3dist(setuptools)

+ BuildRequires: python3dist(wheel)

+ %endif

  

  

  %description

@@ -56,8 +61,10 @@ 

  install -m 644 macros.pyproject %{buildroot}%{_rpmmacrodir}/

  install -m 644 pyproject_buildrequires.py %{buildroot}%{_rpmconfigdir}/redhat/

  

+ %if %{with tests}

  %check

  %{__python3} -m pytest -vv

+ %endif

  

  

  %files

file modified
+48 -11

@@ -8,6 +8,7 @@ 

  import subprocess

  import pathlib

  import re

+ import email.parser

  

  print_err = functools.partial(print, file=sys.stderr)

  

@@ -41,7 +42,7 @@ 

  

  class Requirements:

      """Requirement printer"""

-     def __init__(self, freeze_output):

+     def __init__(self, freeze_output, extras=''):

          self.installed_packages = {}

          for line in freeze_output.splitlines():

              line = line.strip()

@@ -50,6 +51,8 @@ 

              name, version = line.split('==')

              self.installed_packages[name.strip()] = Version(version)

  

+         self.marker_env = {'extra': extras}

+ 

          self.missing_requirements = False

  

      def add(self, requirement_str, *, source=None):

@@ -66,7 +69,9 @@ 

              return

  

          name = canonicalize_name(requirement.name)

-         if requirement.marker is not None and not requirement.marker.evaluate():

+         if (requirement.marker is not None

+             and not requirement.marker.evaluate(environment=self.marker_env)

+         ):

              print_err(f'Ignoring alien requirement:', requirement_str)

              return

  

@@ -83,7 +88,6 @@ 

              key=lambda s: (s.operator, s.version),

          ):

              version = canonicalize_version(specifier.version)

-             print_err(version)

              if not VERSION_RE.fullmatch(str(specifier.version)):

                  raise ValueError(

                      f'Unknown character in version: {specifier.version}. '

@@ -154,6 +158,22 @@ 

          requirements.extend(new_reqs, source='get_requires_for_build_wheel')

  

  

+ def generate_run_requirements(backend, requirements):

+     prepare_metadata = getattr(backend, "prepare_metadata_for_build_wheel", None)

+     if not prepare_metadata:

+         raise ValueError(

+             'build backend cannot provide build metadata '

+             + '(incl. runtime requirements) before buld'

+         )

+     with hook_call():

+         dir_basename = prepare_metadata('.')

+     with open(dir_basename + '/METADATA') as f:

+         message = email.parser.Parser().parse(f, headersonly=True)

+     for key in 'Requires', 'Requires-Dist':

+         requires = message.get_all(key, ())

+         requirements.extend(requires, source=f'wheel metadata: {key}')

+ 

+ 

  def python3dist(name, op=None, version=None):

      if op is None:

          if version is not None:

@@ -163,12 +183,16 @@ 

          return f'python3dist({name}) {op} {version}'

  

  

- def generate_requires(freeze_output):

-     requirements = Requirements(freeze_output)

+ def generate_requires(

+     freeze_output, *, include_runtime=False, toxenv=None, extras='',

+ ):

+     requirements = Requirements(freeze_output, extras=extras)

  

      try:

          backend = get_backend(requirements)

          generate_build_requirements(backend, requirements)

+         if include_runtime:

+             generate_run_requirements(backend, requirements)

      except EndPass:

          return

  

@@ -178,20 +202,29 @@ 

          description='Generate BuildRequires for a Python project.'

      )

      parser.add_argument(

-         '--runtime', action='store_true',

-         help='Generate run-time requirements (not implemented)',

+         '-r', '--runtime', action='store_true',

+         help='Generate run-time requirements',

      )

      parser.add_argument(

-         '--toxenv', metavar='TOXENVS',

+         '-t', '--toxenv', metavar='TOXENVS',

          help='generate test tequirements from tox environment '

              + '(not implemented; implies --runtime)',

      )

+     parser.add_argument(

+         '-x', '--extras', metavar='EXTRAS', default='',

+         help='extra for runtime requirements (e.g. -x testing)',

+         # XXX: a comma-separated list should be possible here

+         #help='comma separated list of "extras" for runtime requirements '

+         #    + '(e.g. -x testing,feature-x)',

+     )

  

      args = parser.parse_args(argv)

      if args.toxenv:

          args.runtime = True

-     if args.runtime:

-         print_err('--runtime is not implemented')

+         print_err('-t (--toxenv) is not implemented')

+         exit(1)

+     if args.extras and not args.runtime:

+         print_err('-x (--extras) are only useful with -r (--runtime)')

          exit(1)

  

      freeze_output = subprocess.run(

@@ -202,7 +235,11 @@ 

      ).stdout

  

      try:

-         generate_requires(freeze_output)

+         generate_requires(

+             freeze_output,

+             include_runtime=args.runtime,

+             extras=args.extras,

+         )

      except Exception as e:

          # Log the traceback explicitly (it's useful debug info)

          traceback.print_exc()

@@ -19,6 +19,9 @@ 

      cwd.mkdir()

      monkeypatch.chdir(cwd)

  

+     if case.get('xfail'):

+         pytest.xfail(case.get('xfail'))

+ 

      if 'pyproject.toml' in case:

          cwd.joinpath('pyproject.toml').write_text(case['pyproject.toml'])

  

@@ -28,10 +31,14 @@ 

      try:

          generate_requires(

              case['freeze_output'],

+             include_runtime=case.get('include_runtime', False),

+             extras=case.get('extras', ''),

          )

      except SystemExit as e:

          assert e.code == case['result']

      except Exception as e:

+         if 'except' not in case:

+             raise

          assert type(e).__name__ == case['except']

      else:

          assert 0 == case['result']

file modified
+136 -1

@@ -99,7 +99,7 @@ 

      python3dist(wheel)

    result: 0

  

- Default build system, dependencies in setup.py:

+ Default build system, build dependencies in setup.py:

    freeze_output: |

      setuptools==50

      wheel==1

@@ -109,6 +109,7 @@ 

          name='test',

          version='0.1',

          setup_requires=['foo', 'bar!=2'],

+         install_requires=['inst'],

      )

    expected: |

      python3dist(setuptools) >= 40.8

@@ -117,3 +118,137 @@ 

      python3dist(foo)

      (python3dist(bar) < 2 or python3dist(bar) > 2.0)

    result: 0

+ 

+ Default build system, run dependencies in setup.py:

+   freeze_output: |

+     setuptools==50

+     wheel==1

+     pyyaml==1

+   include_runtime: true

+   setup.py: |

+     from setuptools import setup

+     setup(

+         name='test',

+         version='0.1',

+         setup_requires=['pyyaml'],  # nb. setuptools will try to install this

+         install_requires=['inst > 1', 'inst2 < 3'],

+     )

+   expected: |

+     python3dist(setuptools) >= 40.8

+     python3dist(wheel)

+     python3dist(wheel)

+     python3dist(pyyaml)

+     python3dist(inst) > 1

+     python3dist(inst2) < 3

+   result: 0

+ 

+ Run dependencies with extras (not selected):

+   freeze_output: |

+     setuptools==50

+     wheel==1

+     pyyaml==1

+   include_runtime: true

+   setup.py: &pytest_setup_py |

+         # slightly abriged copy of pytest's setup.py

+         from setuptools import setup

+ 

+         INSTALL_REQUIRES = [

+             "py>=1.5.0",

+             "six>=1.10.0",

+             "setuptools",

+             "attrs>=17.4.0",

+             'more-itertools>=4.0.0,<6.0.0;python_version<="2.7"',

+             'more-itertools>=4.0.0;python_version>"2.7"',

+             "atomicwrites>=1.0",

+             'funcsigs>=1.0;python_version<"3.0"',

+             'pathlib2>=2.2.0;python_version<"3.6"',

+             'colorama;sys_platform=="win32"',

+             "pluggy>=0.11",

+         ]

+ 

+         def main():

+             setup(

+                 setup_requires=["setuptools>=40.0"],

+                 # fmt: off

+                 extras_require={

+                     "testing": [

+                         "argcomplete",

+                         "hypothesis>=3.56",

+                         "nose",

+                         "requests",

+                         "mock;python_version=='2.7'",

+                     ],

+                 },

+                 # fmt: on

+                 install_requires=INSTALL_REQUIRES,

+             )

+ 

+         if __name__ == "__main__":

+             main()

+   expected: |

+     python3dist(setuptools) >= 40.8

+     python3dist(wheel)

+     python3dist(wheel)

+     python3dist(setuptools) >= 40

+     python3dist(py) >= 1.5

+     python3dist(six) >= 1.10

+     python3dist(setuptools)

+     python3dist(attrs) >= 17.4

+     python3dist(atomicwrites) >= 1

+     python3dist(pluggy) >= 0.11

+     python3dist(more-itertools) >= 4

+   result: 0

+ 

+ Run dependencies with extras (selected):

+   freeze_output: |

+     setuptools==50

+     wheel==1

+     pyyaml==1

+   include_runtime: true

+   extras: testing

+   setup.py: *pytest_setup_py

+   expected: |

+     python3dist(setuptools) >= 40.8

+     python3dist(wheel)

+     python3dist(wheel)

+     python3dist(setuptools) >= 40

+     python3dist(py) >= 1.5

+     python3dist(six) >= 1.10

+     python3dist(setuptools)

+     python3dist(attrs) >= 17.4

+     python3dist(atomicwrites) >= 1

+     python3dist(pluggy) >= 0.11

+     python3dist(more-itertools) >= 4

+     python3dist(argcomplete)

+     python3dist(hypothesis) >= 3.56

+     python3dist(nose)

+     python3dist(requests)

+   result: 0

+ 

+ Run dependencies with multiple extras:

+   xfail: requirement.marker.evaluate seems to not support multiple extras

+   freeze_output: |

+     setuptools==50

+     wheel==1

+     pyyaml==1

+   include_runtime: true

+   extras: testing,more-testing, even-more-testing , cool-feature

+   setup.py: |

+     from setuptools import setup

+     setup(

+         extras_require={

+             'testing': ['dep1'],

+             'more-testing': ['dep2'],

+             'even-more-testing': ['dep3'],

+             'cool-feature': ['dep4'],

+         },

+     )

+   expected: |

+     python3dist(setuptools) >= 40.8

+     python3dist(wheel)

+     python3dist(wheel)

+     python3dist(dep1)

+     python3dist(dep2)

+     python3dist(dep3)

+     python3dist(dep4)

+   result: 0

file modified
+7 -1

@@ -27,7 +27,7 @@ 

  

  

  %generate_buildrequires

- %pyproject_buildrequires

+ %pyproject_buildrequires -r -x testing

  

  

  %build

@@ -37,6 +37,12 @@ 

  %install

  %pyproject_install

  

+ %check

+ # Only run one test (which uses a test-only dependency, hypothesis).

+ # (Unfortunately, some other tests still fail.)

+ export PYTHONPATH=%{buildroot}%{python3_sitelib}

+ %{__python3} -m pytest -k metafunc

+ 

  

  %files -n python3-%{pypi_name}

  %doc README.rst

Adding run-time and test-time dependencies

To run tests in the %check section, the package's runtime dependencies
often need to also be included as build requirements.
If the project's build system supports the prepare-metadata-for-build-wheel
hook
, this can be done using the -r flag:

%generate_buildrequires
%pyproject_buildrequires -r

For projects that specify test requirements using an extra
provide
, these can be added using the -x flag.
For example, if upstream suggests installing test dependencies with
pip install mypackage[testing], the test deps would be generated by:

%generate_buildrequires
%pyproject_buildrequires -r -x testing

1 new commit added

  • Clean up python-entrypoints.spec
2 months ago

Pull-Request has been merged by churchyard

2 months ago