%pyproject_buildrequires: Add support for dependency groups (PEP 735), via the -g flag
This commit is contained in:
		
							parent
							
								
									80d9abe0f4
								
							
						
					
					
						commit
						307d2bef63
					
				
							
								
								
									
										13
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								README.md
									
									
									
									
									
								
							| @ -124,6 +124,14 @@ For example, if upstream suggests installing test dependencies with | |||||||
|     %generate_buildrequires |     %generate_buildrequires | ||||||
|     %pyproject_buildrequires -x testing |     %pyproject_buildrequires -x testing | ||||||
| 
 | 
 | ||||||
|  | For projects that specify test requirements using [PEP 735] dependency groups, | ||||||
|  | these can be added using the `-g` flag. | ||||||
|  | Multiple groups can be supplied by repeating the flag or as a comma separated list. | ||||||
|  | For example, if upstream uses a dependency group called `tests`, the test deps would be generated by: | ||||||
|  | 
 | ||||||
|  |     %generate_buildrequires | ||||||
|  |     %pyproject_buildrequires -g tests | ||||||
|  | 
 | ||||||
| For projects that specify test requirements in their [tox] configuration, | For projects that specify test requirements in their [tox] configuration, | ||||||
| these can be added using the `-t` flag (default tox environment) | these can be added using the `-t` flag (default tox environment) | ||||||
| or the `-e` flag followed by the tox environment. | or the `-e` flag followed by the tox environment. | ||||||
| @ -153,10 +161,12 @@ such plugins will be BuildRequired as well. | |||||||
| Not all plugins are guaranteed to play well with [tox-current-env], | Not all plugins are guaranteed to play well with [tox-current-env], | ||||||
| in worst case, patch/sed the requirement out from the tox configuration. | in worst case, patch/sed the requirement out from the tox configuration. | ||||||
| 
 | 
 | ||||||
| Note that neither `-x` or `-t` can be used with `-R`, | Note that neither `-x` or `-t` can be used with `-R` or `-N`, | ||||||
| because runtime dependencies are always required for testing. | because runtime dependencies are always required for testing. | ||||||
| You can only use those options if the build backend  supports the [prepare-metadata-for-build-wheel hook], | You can only use those options if the build backend  supports the [prepare-metadata-for-build-wheel hook], | ||||||
| or together with `-p` or `-w`. | or together with `-p` or `-w`. | ||||||
|  | However, using `-g` with `-R` or `-N` is supported because dependency groups don't need to be used for testing | ||||||
|  | and can be obtained by reading `pyproject.toml` only. | ||||||
| 
 | 
 | ||||||
| [tox]: https://tox.readthedocs.io/ | [tox]: https://tox.readthedocs.io/ | ||||||
| [tox-current-env]: https://github.com/fedora-python/tox-current-env/ | [tox-current-env]: https://github.com/fedora-python/tox-current-env/ | ||||||
| @ -520,6 +530,7 @@ so be prepared for problems. | |||||||
| [PEP 517]: https://www.python.org/dev/peps/pep-0517/ | [PEP 517]: https://www.python.org/dev/peps/pep-0517/ | ||||||
| [PEP 518]: https://www.python.org/dev/peps/pep-0518/ | [PEP 518]: https://www.python.org/dev/peps/pep-0518/ | ||||||
| [PEP 639]: https://www.python.org/dev/peps/pep-0639/ | [PEP 639]: https://www.python.org/dev/peps/pep-0639/ | ||||||
|  | [PEP 735]: https://www.python.org/dev/peps/pep-0735/ | ||||||
| [pip's documentation]: https://pip.pypa.io/en/stable/cli/pip_install/#vcs-support | [pip's documentation]: https://pip.pypa.io/en/stable/cli/pip_install/#vcs-support | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ | |||||||
| # this macro will cause the package with the real macro to be installed. | # this macro will cause the package with the real macro to be installed. | ||||||
| # When macros.pyproject is installed, it overrides this macro. | # When macros.pyproject is installed, it overrides this macro. | ||||||
| # Note: This needs to maintain the same set of options as the real macro. | # Note: This needs to maintain the same set of options as the real macro. | ||||||
| %pyproject_buildrequires(rRxtNwpe:C:) echo 'pyproject-rpm-macros' && exit 0 | %pyproject_buildrequires(rRxtNwpe:g:C:) echo 'pyproject-rpm-macros' && exit 0 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # Declarative buildsystem, requires RPM 4.20+ to work | # Declarative buildsystem, requires RPM 4.20+ to work | ||||||
|  | |||||||
| @ -158,9 +158,16 @@ fi | |||||||
| %default_toxenv py%{python3_version_nodots} | %default_toxenv py%{python3_version_nodots} | ||||||
| %toxenv %{default_toxenv} | %toxenv %{default_toxenv} | ||||||
| 
 | 
 | ||||||
|  | %_pyproject_tomlidep %["%{python3_pkgversion}" == "3"\ | ||||||
|  |     ? "echo '(python%{python3_pkgversion}dist(tomli) if python%{python3_pkgversion}-devel < 3.11)'"\ | ||||||
|  |     : "%[v"%{python3_pkgversion}" < v"3.11"\ | ||||||
|  |        ? "echo 'python%{python3_pkgversion}dist(tomli)'"\ | ||||||
|  |        : "true # will use tomllib, echo nothing"\ | ||||||
|  |     ]"\ | ||||||
|  |   ] | ||||||
| 
 | 
 | ||||||
| # Note: Keep the options in sync with this macro from macros.aaa-pyproject-srpm | # Note: Keep the options in sync with this macro from macros.aaa-pyproject-srpm | ||||||
| %pyproject_buildrequires(rRxtNwpe:C:) %{expand:\\\ | %pyproject_buildrequires(rRxtNwpe:g:C:) %{expand:\\\ | ||||||
| %_set_pytest_addopts | %_set_pytest_addopts | ||||||
| # The default flags expect the package note file to exist | # The default flags expect the package note file to exist | ||||||
| # see https://bugzilla.redhat.com/show_bug.cgi?id=2097535 | # see https://bugzilla.redhat.com/show_bug.cgi?id=2097535 | ||||||
| @ -181,6 +188,9 @@ fi | |||||||
| %{-w:%{error:The -N and -w options are mutually exclusive}} | %{-w:%{error:The -N and -w options are mutually exclusive}} | ||||||
| %{-p:%{error:The -N and -p options are mutually exclusive}} | %{-p:%{error:The -N and -p options are mutually exclusive}} | ||||||
| %{-C:%{error:The -N and -C options are mutually exclusive}} | %{-C:%{error:The -N and -C options are mutually exclusive}} | ||||||
|  | %{-g:if [ -f pyproject.toml ]; then | ||||||
|  |   %_pyproject_tomlidep | ||||||
|  | fi} | ||||||
| } | } | ||||||
| %{-w: | %{-w: | ||||||
| %{-p:%{error:The -w and -p options are mutually exclusive}} | %{-p:%{error:The -w and -p options are mutually exclusive}} | ||||||
| @ -191,13 +201,7 @@ echo 'python%{python3_pkgversion}-devel' | |||||||
| echo 'python%{python3_pkgversion}dist(packaging)' | echo 'python%{python3_pkgversion}dist(packaging)' | ||||||
| %{!-N:echo 'python%{python3_pkgversion}dist(pip) >= 19' | %{!-N:echo 'python%{python3_pkgversion}dist(pip) >= 19' | ||||||
| if [ -f pyproject.toml ]; then | if [ -f pyproject.toml ]; then | ||||||
|   %["%{python3_pkgversion}" == "3" |   %_pyproject_tomlidep | ||||||
|     ? "echo '(python%{python3_pkgversion}dist(tomli) if python%{python3_pkgversion}-devel < 3.11)'" |  | ||||||
|     : "%[v"%{python3_pkgversion}" < v"3.11" |  | ||||||
|        ? "echo 'python%{python3_pkgversion}dist(tomli)'" |  | ||||||
|        : "true # will use tomllib, echo nothing" |  | ||||||
|     ]" |  | ||||||
|   ] |  | ||||||
| elif [ -f setup.py ]; then | elif [ -f setup.py ]; then | ||||||
|   # Note: If the default requirements change, also change them in the script! |   # Note: If the default requirements change, also change them in the script! | ||||||
|   echo 'python%{python3_pkgversion}dist(setuptools) >= 40.8' |   echo 'python%{python3_pkgversion}dist(setuptools) >= 40.8' | ||||||
|  | |||||||
| @ -14,7 +14,7 @@ License:        MIT | |||||||
| #   Increment Y and reset Z when new macros or features are added | #   Increment Y and reset Z when new macros or features are added | ||||||
| #   Increment Z when this is a bugfix or a cosmetic change | #   Increment Z when this is a bugfix or a cosmetic change | ||||||
| # Dropping support for EOL Fedoras is *not* considered a breaking change | # Dropping support for EOL Fedoras is *not* considered a breaking change | ||||||
| Version:        1.15.1 | Version:        1.16.0 | ||||||
| Release:        1%{?dist} | Release:        1%{?dist} | ||||||
| 
 | 
 | ||||||
| # Macro files | # Macro files | ||||||
| @ -173,6 +173,10 @@ export HOSTNAME="rpmbuild"  # to speedup tox in network-less mock, see rhbz#1856 | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| %changelog | %changelog | ||||||
|  | * Mon Nov 04 2024 Miro Hrončok <mhroncok@redhat.com> - 1.16.0-1 | ||||||
|  | - %%pyproject_buildrequires: Add support for dependency groups (PEP 735), via the -g flag | ||||||
|  | - Fixes: rhbz#2318849 | ||||||
|  | 
 | ||||||
| * Thu Oct 03 2024 Karolina Surma <ksurma@redhat.com> - 1.15.1-1 | * Thu Oct 03 2024 Karolina Surma <ksurma@redhat.com> - 1.15.1-1 | ||||||
| - Fix handling of self-referencing extras when reading pyproject.toml | - Fix handling of self-referencing extras when reading pyproject.toml | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -468,6 +468,80 @@ def generate_tox_requirements(toxenv, requirements): | |||||||
|                             source=f'tox --print-deps-only: {toxenv}') |                             source=f'tox --print-deps-only: {toxenv}') | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def generate_dependency_groups(requested_groups, requirements): | ||||||
|  |     """Adapted from https://peps.python.org/pep-0735/#reference-implementation (public domain)""" | ||||||
|  |     from collections import defaultdict | ||||||
|  | 
 | ||||||
|  |     def _normalize_name(name: str) -> str: | ||||||
|  |         return re.sub(r"[-_.]+", "-", name).lower() | ||||||
|  | 
 | ||||||
|  |     def _normalize_group_names(dependency_groups: dict) -> dict: | ||||||
|  |         original_names = defaultdict(list) | ||||||
|  |         normalized_groups = {} | ||||||
|  | 
 | ||||||
|  |         for group_name, value in dependency_groups.items(): | ||||||
|  |             normed_group_name = _normalize_name(group_name) | ||||||
|  |             original_names[normed_group_name].append(group_name) | ||||||
|  |             normalized_groups[normed_group_name] = value | ||||||
|  | 
 | ||||||
|  |         errors = [] | ||||||
|  |         for normed_name, names in original_names.items(): | ||||||
|  |             if len(names) > 1: | ||||||
|  |                 errors.append(f"{normed_name} ({', '.join(names)})") | ||||||
|  |         if errors: | ||||||
|  |             raise ValueError(f"Duplicate dependency group names: {', '.join(errors)}") | ||||||
|  | 
 | ||||||
|  |         return normalized_groups | ||||||
|  | 
 | ||||||
|  |     def _resolve_dependency_group( | ||||||
|  |         dependency_groups: dict, group: str, past_groups: tuple[str, ...] = () | ||||||
|  |     ) -> list[str]: | ||||||
|  |         if group in past_groups: | ||||||
|  |             raise ValueError(f"Cyclic dependency group include: {group} -> {past_groups}") | ||||||
|  | 
 | ||||||
|  |         if group not in dependency_groups: | ||||||
|  |             raise LookupError(f"Dependency group '{group}' not found") | ||||||
|  | 
 | ||||||
|  |         raw_group = dependency_groups[group] | ||||||
|  |         if not isinstance(raw_group, list): | ||||||
|  |             raise ValueError(f"Dependency group '{group}' is not a list") | ||||||
|  | 
 | ||||||
|  |         realized_group = [] | ||||||
|  |         for item in raw_group: | ||||||
|  |             if isinstance(item, str): | ||||||
|  |                 realized_group.append(item) | ||||||
|  |             elif isinstance(item, dict): | ||||||
|  |                 if tuple(item.keys()) != ("include-group",): | ||||||
|  |                     raise ValueError(f"Invalid dependency group item: {item}") | ||||||
|  | 
 | ||||||
|  |                 include_group = _normalize_name(next(iter(item.values()))) | ||||||
|  |                 realized_group.extend( | ||||||
|  |                     _resolve_dependency_group( | ||||||
|  |                         dependency_groups, include_group, past_groups + (group,) | ||||||
|  |                     ) | ||||||
|  |                 ) | ||||||
|  |             else: | ||||||
|  |                 raise ValueError(f"Invalid dependency group item: {item}") | ||||||
|  | 
 | ||||||
|  |         return realized_group | ||||||
|  | 
 | ||||||
|  |     def resolve(dependency_groups: dict, group: str) -> list[str]: | ||||||
|  |         if not isinstance(dependency_groups, dict): | ||||||
|  |             raise TypeError("Dependency Groups table is not a dict") | ||||||
|  |         return _resolve_dependency_group(dependency_groups, _normalize_name(group)) | ||||||
|  | 
 | ||||||
|  |     pyproject_data = load_pyproject() | ||||||
|  |     dependency_groups_raw = pyproject_data.get("dependency-groups", {}) | ||||||
|  |     dependency_groups = _normalize_group_names(dependency_groups_raw) | ||||||
|  | 
 | ||||||
|  |     for group_names in requested_groups: | ||||||
|  |         for group_name in group_names.split(","): | ||||||
|  |             requirements.extend( | ||||||
|  |                 resolve(dependency_groups, group_name), | ||||||
|  |                 source=f"Dependency group {group_name}", | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def python3dist(name, op=None, version=None, python3_pkgversion="3"): | def python3dist(name, op=None, version=None, python3_pkgversion="3"): | ||||||
|     prefix = f"python{python3_pkgversion}dist" |     prefix = f"python{python3_pkgversion}dist" | ||||||
| 
 | 
 | ||||||
| @ -480,7 +554,7 @@ def python3dist(name, op=None, version=None, python3_pkgversion="3"): | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def generate_requires( | def generate_requires( | ||||||
|     *, include_runtime=False, build_wheel=False, wheeldir=None, toxenv=None, extras=None, |     *, include_runtime=False, build_wheel=False, wheeldir=None, toxenv=None, extras=None, dependency_groups=None, | ||||||
|     get_installed_version=importlib.metadata.version,  # for dep injection |     get_installed_version=importlib.metadata.version,  # for dep injection | ||||||
|     generate_extras=False, python3_pkgversion="3", requirement_files=None, use_build_system=True, |     generate_extras=False, python3_pkgversion="3", requirement_files=None, use_build_system=True, | ||||||
|     read_pyproject_dependencies=False, |     read_pyproject_dependencies=False, | ||||||
| @ -514,7 +588,9 @@ def generate_requires( | |||||||
|             generate_build_requirements(backend, requirements) |             generate_build_requirements(backend, requirements) | ||||||
|         if toxenv: |         if toxenv: | ||||||
|             include_runtime = True |             include_runtime = True | ||||||
|             generate_tox_requirements(toxenv, requirements) |             generate_tox_requirements(toxenv, requirements)  # TODO extend dependency_groups | ||||||
|  |         if dependency_groups: | ||||||
|  |             generate_dependency_groups(dependency_groups, requirements) | ||||||
|         if include_runtime: |         if include_runtime: | ||||||
|             generate_run_requirements(backend, requirements, build_wheel=build_wheel, |             generate_run_requirements(backend, requirements, build_wheel=build_wheel, | ||||||
|                 read_pyproject_dependencies=read_pyproject_dependencies, wheeldir=wheeldir) |                 read_pyproject_dependencies=read_pyproject_dependencies, wheeldir=wheeldir) | ||||||
| @ -559,6 +635,11 @@ def main(argv): | |||||||
|         help='comma separated list of "extras" for runtime requirements ' |         help='comma separated list of "extras" for runtime requirements ' | ||||||
|              '(e.g. -x testing,feature-x) (implies --runtime, can be repeated)', |              '(e.g. -x testing,feature-x) (implies --runtime, can be repeated)', | ||||||
|     ) |     ) | ||||||
|  |     parser.add_argument( | ||||||
|  |         '-g', '--dependency-groups', metavar='GROUPS', action='append', | ||||||
|  |         help='comma separated list of dependency groups (PEP 735) for requirements ' | ||||||
|  |              '(e.g. -g tests,docs) (can be repeated)', | ||||||
|  |     ) | ||||||
|     parser.add_argument( |     parser.add_argument( | ||||||
|         '-t', '--tox', action='store_true', |         '-t', '--tox', action='store_true', | ||||||
|         help=('generate test tequirements from tox environment ' |         help=('generate test tequirements from tox environment ' | ||||||
| @ -627,6 +708,7 @@ def main(argv): | |||||||
|             wheeldir=args.wheeldir, |             wheeldir=args.wheeldir, | ||||||
|             toxenv=args.toxenv, |             toxenv=args.toxenv, | ||||||
|             extras=args.extras, |             extras=args.extras, | ||||||
|  |             dependency_groups=args.dependency_groups, | ||||||
|             generate_extras=args.generate_extras, |             generate_extras=args.generate_extras, | ||||||
|             python3_pkgversion=args.python3_pkgversion, |             python3_pkgversion=args.python3_pkgversion, | ||||||
|             requirement_files=args.requirement_files, |             requirement_files=args.requirement_files, | ||||||
|  | |||||||
| @ -1278,3 +1278,110 @@ pyproject.toml with self-referencing extras: | |||||||
|     python3dist(pytest-rerunfailures) |     python3dist(pytest-rerunfailures) | ||||||
|     python3dist(wurlitzer) |     python3dist(wurlitzer) | ||||||
|   result: 0 |   result: 0 | ||||||
|  | 
 | ||||||
|  | pyproject.toml with dependency-groups not requested: | ||||||
|  |   use_build_system: false | ||||||
|  |   pyproject.toml: | | ||||||
|  |     [dependency-groups] | ||||||
|  |     tests = ["pytest>=5", "pytest-mock"] | ||||||
|  |     docs = ["sphinx", "python-docs-theme"] | ||||||
|  |   expected: "\n" | ||||||
|  |   result: 0 | ||||||
|  | 
 | ||||||
|  | pyproject.toml with dependency-groups and build system: | ||||||
|  |   skipif: not SETUPTOOLS_60 | ||||||
|  |   use_build_system: true | ||||||
|  |   installed: | ||||||
|  |     setuptools: 50 | ||||||
|  |     wheel: 1 | ||||||
|  |     tomli: 1 | ||||||
|  |   dependency_groups: | ||||||
|  |     - tests | ||||||
|  |   pyproject.toml: | | ||||||
|  |     [build-system] | ||||||
|  |     requires = ["setuptools"] | ||||||
|  |     build-backend = "setuptools.build_meta" | ||||||
|  |     [project] | ||||||
|  |     name = "my_package" | ||||||
|  |     version = "0.1" | ||||||
|  |     [dependency-groups] | ||||||
|  |     tests = ["pytest>=5", "pytest-mock"] | ||||||
|  |     docs = ["sphinx", "python-docs-theme"] | ||||||
|  |   expected: | | ||||||
|  |     python3dist(setuptools) | ||||||
|  |     python3dist(wheel) | ||||||
|  |     python3dist(pytest) >= 5 | ||||||
|  |     python3dist(pytest-mock) | ||||||
|  |   result: 0 | ||||||
|  | 
 | ||||||
|  | pyproject.toml with dependency-groups one requested: | ||||||
|  |   use_build_system: false | ||||||
|  |   dependency_groups: | ||||||
|  |     - tests | ||||||
|  |   pyproject.toml: | | ||||||
|  |     [dependency-groups] | ||||||
|  |     tests = ["pytest>=5", "pytest-mock"] | ||||||
|  |     docs = ["sphinx", "python-docs-theme"] | ||||||
|  |   expected: | | ||||||
|  |     python3dist(pytest) >= 5 | ||||||
|  |     python3dist(pytest-mock) | ||||||
|  |   result: 0 | ||||||
|  | 
 | ||||||
|  | pyproject.toml with dependency-groups two requested: | ||||||
|  |   use_build_system: false | ||||||
|  |   dependency_groups: | ||||||
|  |     - tests | ||||||
|  |     - docs | ||||||
|  |   pyproject.toml: | | ||||||
|  |     [dependency-groups] | ||||||
|  |     tests = ["pytest>=5", "pytest-mock"] | ||||||
|  |     docs = ["sphinx", "python-docs-theme"] | ||||||
|  |   expected: | | ||||||
|  |     python3dist(pytest) >= 5 | ||||||
|  |     python3dist(pytest-mock) | ||||||
|  |     python3dist(sphinx) | ||||||
|  |     python3dist(python-docs-theme) | ||||||
|  |   result: 0 | ||||||
|  | 
 | ||||||
|  | pyproject.toml with dependency-groups two requested via comma: | ||||||
|  |   use_build_system: false | ||||||
|  |   dependency_groups: | ||||||
|  |     - tests,docs | ||||||
|  |   pyproject.toml: | | ||||||
|  |     [dependency-groups] | ||||||
|  |     tests = ["pytest>=5", "pytest-mock"] | ||||||
|  |     docs = ["sphinx", "python-docs-theme"] | ||||||
|  |   expected: | | ||||||
|  |     python3dist(pytest) >= 5 | ||||||
|  |     python3dist(pytest-mock) | ||||||
|  |     python3dist(sphinx) | ||||||
|  |     python3dist(python-docs-theme) | ||||||
|  |   result: 0 | ||||||
|  | 
 | ||||||
|  | pyproject.toml with include-group: | ||||||
|  |   use_build_system: false | ||||||
|  |   dependency_groups: | ||||||
|  |     - tests_docs | ||||||
|  |   pyproject.toml: | | ||||||
|  |     [dependency-groups] | ||||||
|  |     tests = ["pytest>=5", "pytest-mock"] | ||||||
|  |     docs = ["sphinx", "python-docs-theme"] | ||||||
|  |     typing = ["mypy"] | ||||||
|  |     tests-docs = [{include-group = "tests"}, {include-group = "docs"}, "pytest-sphinx"] | ||||||
|  |   expected: | | ||||||
|  |     python3dist(pytest) >= 5 | ||||||
|  |     python3dist(pytest-mock) | ||||||
|  |     python3dist(sphinx) | ||||||
|  |     python3dist(python-docs-theme) | ||||||
|  |     python3dist(pytest-sphinx) | ||||||
|  |   result: 0 | ||||||
|  | 
 | ||||||
|  | pyproject.toml with dependency-groups nonexisting requested: | ||||||
|  |   use_build_system: false | ||||||
|  |   dependency_groups: | ||||||
|  |     - typing | ||||||
|  |   pyproject.toml: | | ||||||
|  |     [dependency-groups] | ||||||
|  |     tests = ["pytest>=5", "pytest-mock"] | ||||||
|  |     docs = ["sphinx", "python-docs-theme"] | ||||||
|  |   except: LookupError | ||||||
|  | |||||||
| @ -71,6 +71,7 @@ def test_data(case_name, capfd, tmp_path, monkeypatch): | |||||||
|             build_wheel=case.get('build_wheel', False), |             build_wheel=case.get('build_wheel', False), | ||||||
|             wheeldir=str(wheeldir), |             wheeldir=str(wheeldir), | ||||||
|             extras=case.get('extras', []), |             extras=case.get('extras', []), | ||||||
|  |             dependency_groups=case.get('dependency_groups', []), | ||||||
|             toxenv=case.get('toxenv', None), |             toxenv=case.get('toxenv', None), | ||||||
|             generate_extras=case.get('generate_extras', False), |             generate_extras=case.get('generate_extras', False), | ||||||
|             requirement_files=requirement_files, |             requirement_files=requirement_files, | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user