#74 Add full %%py3_check_import macro
Merged 8 months ago by orion. Opened 8 months ago by orion.
rpms/ orion/epel-rpm-macros py3_check_import  into  epel8

file modified
+16 -2
@@ -1,9 +1,10 @@ 

  Name:           epel-rpm-macros

  Version:        8

- Release:        39

+ Release:        40

  Summary:        Extra Packages for Enterprise Linux RPM macros

  

- License:        GPLv2

+ # import_all_modules.py: MIT

+ License:       GPLv2 and MIT

  

  # This is a EPEL maintained package which is specific to

  # our distribution.  Thus the source is only available from
@@ -27,6 +28,9 @@ 

  %global rpmautospec_commit 52f3c2017e10c5ab5a183fed772e9fe8a86a20fb

  Source152:      https://pagure.io/fedora-infra/rpmautospec/raw/%{rpmautospec_commit}/f/rpm/macros.d/macros.rpmautospec

  

+ # Python code

+ Source302:      import_all_modules.py

+ 

  BuildArch:      noarch

  Requires:       redhat-release >= %{version}

  # For FPC buildroot macros
@@ -106,6 +110,10 @@ 

  install -Dpm 644 %{SOURCE152} \

      %{buildroot}%{_rpmmacrodir}/macros.rpmautospec

  

+ # python scripts

+ mkdir -p %{buildroot}%{_rpmconfigdir}/redhat

+ install -Dpm 644 %{SOURCE302} %{buildroot}%{_rpmconfigdir}/redhat/

+ 

  %files

  %license GPL

  %{_rpmmacrodir}/macros.epel-rpm-macros
@@ -117,6 +125,9 @@ 

  %{_rpmmacrodir}/macros.build-constraints

  %{_rpmmacrodir}/macros.shell-completions

  

+ # python scripts

+ %{_rpmconfigdir}/redhat/import_all_modules.py

+ 

  %files systemd

  # sysusers

  %{_rpmconfigdir}/macros.d/macros.sysusers
@@ -126,6 +137,9 @@ 

  

  

  %changelog

+ * Fri Oct 06 2023 Orion Poplawski <orion@nwra.com> - 8-40

+ - Add full %%py3_check_import macro

+ 

  * Fri Apr 07 2023 Miro Hrončok <mhroncok@redhat.com> - 8-39

  - Prepare support for Python 3.11

  

file added
+171
@@ -0,0 +1,171 @@ 

+ '''Script to perform import of each module given to %%py_check_import

+ '''

+ import argparse

+ import importlib

+ import fnmatch

+ import os

+ import re

+ import site

+ import sys

+ 

+ from contextlib import contextmanager

+ from pathlib import Path

+ 

+ 

+ def read_modules_files(file_paths):

+     '''Read module names from the files (modules must be newline separated).

+ 

+     Return the module names list or, if no files were provided, an empty list.

+     '''

+ 

+     if not file_paths:

+         return []

+ 

+     modules = []

+     for file in file_paths:

+         file_contents = file.read_text()

+         modules.extend(file_contents.split())

+     return modules

+ 

+ 

+ def read_modules_from_cli(argv):

+     '''Read module names from command-line arguments (space or comma separated).

+ 

+     Return the module names list.

+     '''

+ 

+     if not argv:

+         return []

+ 

+     # %%py3_check_import allows to separate module list with comma or whitespace,

+     # we need to unify the output to a list of particular elements

+     modules_as_str = ' '.join(argv)

+     modules = re.split(r'[\s,]+', modules_as_str)

+     # Because of shell expansion in some less typical cases it may happen

+     # that a trailing space will occur at the end of the list.

+     # Remove the empty items from the list before passing it further

+     modules = [m for m in modules if m]

+     return modules

+ 

+ 

+ def filter_top_level_modules_only(modules):

+     '''Filter out entries with nested modules (containing dot) ie. 'foo.bar'.

+ 

+     Return the list of top-level modules.

+     '''

+ 

+     return [module for module in modules if '.' not in module]

+ 

+ 

+ def any_match(text, globs):

+     '''Return True if any of given globs fnmatchcase's the given text.'''

+ 

+     return any(fnmatch.fnmatchcase(text, g) for g in globs)

+ 

+ 

+ def exclude_unwanted_module_globs(globs, modules):

+     '''Filter out entries which match the either of the globs given as argv.

+ 

+     Return the list of filtered modules.

+     '''

+ 

+     return [m for m in modules if not any_match(m, globs)]

+ 

+ 

+ def read_modules_from_all_args(args):

+     '''Return a joined list of modules from all given command-line arguments.

+     '''

+ 

+     modules = read_modules_files(args.filename)

+     modules.extend(read_modules_from_cli(args.modules))

+     if args.exclude:

+         modules = exclude_unwanted_module_globs(args.exclude, modules)

+ 

+     if args.top_level:

+         modules = filter_top_level_modules_only(modules)

+ 

+     # Error when someone accidentally managed to filter out everything

+     if len(modules) == 0:

+         raise ValueError('No modules to check were left')

+ 

+     return modules

+ 

+ 

+ def import_modules(modules):

+     '''Procedure to perform import check for each module name from the given list of modules.

+     '''

+ 

+     for module in modules:

+         print('Check import:', module, file=sys.stderr)

+         importlib.import_module(module)

+ 

+ 

+ def argparser():

+     parser = argparse.ArgumentParser(

+         description='Generate list of all importable modules for import check.'

+     )

+     parser.add_argument(

+         'modules', nargs='*',

+         help=('Add modules to check the import (space or comma separated).'),

+     )

+     parser.add_argument(

+         '-f', '--filename', action='append', type=Path,

+         help='Add importable module names list from file.',

+     )

+     parser.add_argument(

+         '-t', '--top-level', action='store_true',

+         help='Check only top-level modules.',

+     )

+     parser.add_argument(

+         '-e', '--exclude', action='append',

+         help='Provide modules globs to be excluded from the check.',

+     )

+     return parser

+ 

+ 

+ @contextmanager

+ def remove_unwanteds_from_sys_path():

+     '''Remove cwd and this script's parent from sys.path for the import test.

+     Bring the original contents back after import is done (or failed)

+     '''

+ 

+     cwd_absolute = Path.cwd().absolute()

+     this_file_parent = Path(__file__).parent.absolute()

+     old_sys_path = list(sys.path)

+     for path in old_sys_path:

+         if Path(path).absolute() in (cwd_absolute, this_file_parent):

+             sys.path.remove(path)

+     try:

+         yield

+     finally:

+         sys.path = old_sys_path

+ 

+ 

+ def addsitedirs_from_environ():

+     '''Load directories from the _PYTHONSITE environment variable (separated by :)

+     and load the ones already present in sys.path via site.addsitedir()

+     to handle .pth files in them.

+ 

+     This is needed to properly import old-style namespace packages with nspkg.pth files.

+     See https://bugzilla.redhat.com/2018551 for a more detailed rationale.'''

+     for path in os.getenv('_PYTHONSITE', '').split(':'):

+         if path in sys.path:

+             site.addsitedir(path)

+ 

+ 

+ def main(argv=None):

+ 

+     cli_args = argparser().parse_args(argv)

+ 

+     if not cli_args.modules and not cli_args.filename:

+         raise ValueError('No modules to check were provided')

+ 

+     modules = read_modules_from_all_args(cli_args)

+ 

+     with remove_unwanteds_from_sys_path():

+         addsitedirs_from_environ()

+         import_modules(modules)

+ 

+ 

+ if __name__ == '__main__':

+     main()

file modified
+19 -6
@@ -52,16 +52,29 @@ 

    %{__python2} -c "import %{lua:local m=rpm.expand('%{?*}'):gsub('[%s,]+', ', ');print(m)}"

    )

  }

+ # With $PATH and $PYTHONPATH set to the %%buildroot,

+ # try to import the Python 3 module(s) given as command-line args or read from file (-f).

+ # Respect the custom values of %%py3_shebang_flags or set nothing if it's undefined.

+ # Filter and check import on only top-level modules using -t flag.

+ # Exclude unwanted modules by passing their globs to -e option.

+ # Useful as a smoke test in %%check when running tests is not feasible.

+ # Use spaces or commas as separators if providing list directly.

+ # Use newlines as separators if providing list in a file.

  %py3_check_import(e:tf:) %{expand:\\\

-   %{-e:echo 'WARNING: The -e option of %%%%py3_check_import is not currently supported on EPEL.' >&2}

-   %{-t:echo 'WARNING: The -t option of %%%%py3_check_import is not currently supported on EPEL.' >&2}

-   %{-f:echo 'WARNING: The -f option of %%%%py3_check_import is not currently supported on EPEL.' >&2}

-   (cd %{_topdir} &&\\\

    PATH="%{buildroot}%{_bindir}:$PATH"\\\

    PYTHONPATH="${PYTHONPATH:-%{buildroot}%{python3_sitearch}:%{buildroot}%{python3_sitelib}}"\\\

+   _PYTHONSITE="%{buildroot}%{python3_sitearch}:%{buildroot}%{python3_sitelib}"\\\

    PYTHONDONTWRITEBYTECODE=1\\\

-   %{__python3} -c "import %{lua:local m=rpm.expand('%{?*}'):gsub('[%s,]+', ', ');print(m)}"

-   )

+   %{lua:

+   local command = "%{__python3} "

+   if rpm.expand("%{?py3_shebang_flags}") ~= "" then

+     command = command .. "-%{py3_shebang_flags}"

+   end

+   command = command .. " %{_rpmconfigdir}/redhat/import_all_modules.py "

+   -- handle multiline arguments correctly, see https://bugzilla.redhat.com/2018809

+   local args=rpm.expand('%{?**}'):gsub("[%s\\\\]*%s+", " ")

+   print(command .. args)

+   }

  }

  

  # When packagers go against the Packaging Guidelines and disable the runtime

Build failed. More information on how to proceed and troubleshoot errors available at https://fedoraproject.org/wiki/Zuul-based-ci
https://fedora.softwarefactory-project.io/zuul/buildset/5488fb8884db48fabcca0d0014de5a13

rebased onto b08f03c

8 months ago

Build failed. More information on how to proceed and troubleshoot errors available at https://fedoraproject.org/wiki/Zuul-based-ci
https://fedora.softwarefactory-project.io/zuul/buildset/9b1877e0c6fc4afaa8b23248cb466d7c

Open questions (non-blocking):

  • Should %py_check_import be changed as well? Or is it not changed because the script is not Python 2 compatible?

Concerns (better check before merging):

  • We have never tested this script on Python 3.6. Does it work? Were there any changes necessary or is it copied verbatim?

It's Python 3 only:

$ python2 import_all_modules.py
  File "import_all_modules.py", line 99
    print('Check import:', module, file=sys.stderr)
                                       ^
SyntaxError: invalid syntax

I did a basic test with %pyproject_check_import and it seemed to work fine. It was copied verbatim.

I assume the basic test with %pyproject_check_import was with Python 3.8 or 3.11, however, this also supports 3.6 here. We need to make sure it works. I can check later.

Seems to work:

$ python3.6 import_all_modules.py conda
Check import: conda
$ cat modules
conda
conda.cli
$ python3.6 import_all_modules.py -f modules
Check import: conda
Check import: conda.cli
$ python3.6 import_all_modules.py -f modules -t
Check import: conda
$ python3.6 import_all_modules.py -f modules -e cli
Check import: conda
Check import: conda.cli
$ python3.6 import_all_modules.py -f modules -e \*cli
Check import: conda

Highly opinionated nitpick: I would keep this as GPLv2 and MIT. Because it keeps the intended license of this package first. (And it's coincidently also alphabetically sorted.)

Seems to work: ...

Awesome, thanks for checking.

Looks good then.

rebased onto b52ef20

8 months ago

Pull-Request has been merged by orion

8 months ago

Highly opinionated nitpick: I would keep this as GPLv2 and MIT. Because it keeps the intended license of this package first. (And it's coincidently also alphabetically sorted.)

swapped. Thanks for the review.

Build failed. More information on how to proceed and troubleshoot errors available at https://fedoraproject.org/wiki/Zuul-based-ci
https://fedora.softwarefactory-project.io/zuul/buildset/b085be55319e46cd8d75869c505261b2