| # Copyright 2022 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| """Check Python package install_requires are covered.""" |
| |
| import argparse |
| import configparser |
| from pathlib import Path |
| import sys |
| |
| try: |
| from pw_build.python_package import load_packages |
| from pw_build.create_python_tree import update_config_with_packages |
| except ImportError: |
| # Load from python_package from this directory if pw_build is not available. |
| from python_package import load_packages # type: ignore |
| from create_python_tree import update_config_with_packages # type: ignore |
| |
| |
| def _parse_args() -> argparse.Namespace: |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument( |
| '--python-dep-list-files', |
| type=Path, |
| required=True, |
| help= |
| 'Path to a text file containing the list of Python package metadata ' |
| 'json files.', |
| ) |
| parser.add_argument( |
| '--requirement', |
| type=Path, |
| required=True, |
| help='requirement file to generate', |
| ) |
| parser.add_argument('--gn-packages', |
| required=True, |
| help=('Comma separated list of GN python package ' |
| 'targets to check for requirements.')) |
| parser.add_argument( |
| '--exclude-transitive-deps', |
| action='store_true', |
| help='Exclude checking transitive deps of the specified --gn-packages', |
| ) |
| return parser.parse_args() |
| |
| |
| class NoMatchingGnPythonDependency(Exception): |
| """An error occurred while processing a Python dependency.""" |
| |
| |
| def main( |
| python_dep_list_files: Path, |
| requirement: Path, |
| gn_packages: str, |
| exclude_transitive_deps: bool, |
| ) -> int: |
| """Check Python package setup.cfg correctness.""" |
| |
| # Split the comma separated string and remove leading slashes. |
| gn_target_names = [ |
| target.lstrip('/') for target in gn_packages.split(',') |
| if target # The last target may be an empty string. |
| ] |
| for i, gn_target in enumerate(gn_target_names): |
| # Remove metadata subtarget if present. |
| python_package_target = gn_target.replace('._package_metadata(', '(', |
| 1) |
| # Split on the first paren to ignore the toolchain. |
| gn_target_names[i] = python_package_target.split('(')[0] |
| |
| py_packages = load_packages([python_dep_list_files], ignore_missing=False) |
| |
| target_py_packages = py_packages |
| if exclude_transitive_deps: |
| target_py_packages = [] |
| for pkg in py_packages: |
| valid_target = [ |
| target in pkg.gn_target_name for target in gn_target_names |
| ] |
| if not any(valid_target): |
| continue |
| target_py_packages.append(pkg) |
| |
| if not target_py_packages: |
| gn_targets_to_include = '\n'.join(gn_target_names) |
| declared_py_deps = '\n'.join(pkg.gn_target_name for pkg in py_packages) |
| raise NoMatchingGnPythonDependency( |
| 'No matching GN Python dependency found.\n' |
| 'GN Targets to include:\n' |
| f'{gn_targets_to_include}\n\n' |
| 'Declared Python Dependencies:\n' |
| f'{declared_py_deps}\n\n') |
| |
| config = configparser.ConfigParser() |
| config['options'] = {} |
| update_config_with_packages(config=config, |
| python_packages=target_py_packages) |
| |
| output = ( |
| '# Auto-generated requirements.txt from the following packages:\n' |
| '#\n') |
| output += '\n'.join('# ' + pkg.gn_target_name |
| for pkg in sorted(target_py_packages, |
| key=lambda pkg: pkg.gn_target_name)) |
| |
| output += config['options']['install_requires'] |
| output += '\n' |
| requirement.write_text(output) |
| |
| return 0 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main(**vars(_parse_args()))) |