aboutsummaryrefslogtreecommitdiff
path: root/pw_build/py/pw_build/generate_python_requirements.py
diff options
context:
space:
mode:
Diffstat (limited to 'pw_build/py/pw_build/generate_python_requirements.py')
-rw-r--r--pw_build/py/pw_build/generate_python_requirements.py135
1 files changed, 135 insertions, 0 deletions
diff --git a/pw_build/py/pw_build/generate_python_requirements.py b/pw_build/py/pw_build/generate_python_requirements.py
new file mode 100644
index 000000000..3a7bc67fc
--- /dev/null
+++ b/pw_build/py/pw_build/generate_python_requirements.py
@@ -0,0 +1,135 @@
+# 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())))