pw_build: Support declaring standalone Python scripts
- Standalone Python scripts may be declared with pw_python_script. A
pw_python_script supports a subset of pw_python_package features.
- Check for an __init__.py file when declaring a package.
Change-Id: I124a9d1f1b7992f619796910aa33b1eb3df382bc
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/22301
Commit-Queue: Wyatt Hepler <hepler@google.com>
Reviewed-by: Alexei Frolov <frolv@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index ec3d18b..2ea1172 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -114,7 +114,8 @@
}
pw_python_group("python") {
- _python_packages = [
+ _python_gn_targets = [
+ # Python packages
"$dir_pw_allocator/py",
"$dir_pw_arduino_build/py",
"$dir_pw_build/py",
@@ -134,6 +135,9 @@
"$dir_pw_unit_test/py",
"$dir_pw_watch/py",
+ # Standalone scripts
+ "$dir_pw_hdlc_lite/rpc_example:example_script",
+
# TODO(pwbug/239): Structure pw_docgen as Python packages.
# "$dir_pw_bloat/py",
# "$dir_pw_docgen/py",
@@ -142,7 +146,7 @@
_toolchain = "$_default_toolchain_prefix$pw_default_optimization_level"
python_deps = []
- foreach(dep, _python_packages) {
+ foreach(dep, _python_gn_targets) {
python_deps += [ "$dep($_toolchain)" ]
}
}
diff --git a/pw_build/python.gni b/pw_build/python.gni
index 080a601..0bdf059 100644
--- a/pw_build/python.gni
+++ b/pw_build/python.gni
@@ -41,9 +41,54 @@
# inputs: Other files to track, such as package_data.
#
template("pw_python_package") {
- assert(defined(invoker.setup) && invoker.setup != [],
- "pw_python_package requires 'setup' to point to a setup.py or " +
- "pyproject.toml and setup.cfg file")
+ if (defined(invoker.sources)) {
+ _all_py_files = invoker.sources
+ } else {
+ _all_py_files = []
+ }
+
+ if (defined(invoker.tests)) {
+ _test_sources = invoker.tests
+ } else {
+ _test_sources = []
+ }
+
+ _all_py_files += _test_sources
+
+ assert(_all_py_files != [], "At least one source or test must be provided")
+
+ # pw_python_script uses pw_python_package, but with a limited set of features.
+ # _pw_standalone signals that this target is actually a pw_python_script.
+ _is_package = !(defined(invoker._pw_standalone) && invoker._pw_standalone)
+
+ if (_is_package) {
+ assert(defined(invoker.setup) && invoker.setup != [],
+ "pw_python_package requires 'setup' to point to a setup.py file " +
+ "or pyproject.toml and setup.cfg files")
+
+ _all_py_files += invoker.setup
+
+ # Get the directories of the setup files. All files must be in the same dir.
+ _setup_dirs = get_path_info(invoker.setup, "dir")
+ _setup_dir = _setup_dirs[0]
+
+ foreach(dir, _setup_dirs) {
+ assert(dir == _setup_dir,
+ "All files in 'setup' must be in the same directory")
+ }
+
+ # If sources are provided, make sure there is an __init__.py file.
+ if (defined(invoker.sources)) {
+ _found_init_py_in_sources = false
+ foreach(source, invoker.sources) {
+ if (get_path_info(source, "file") == "__init__.py") {
+ _found_init_py_in_sources = true
+ }
+ }
+ assert(_found_init_py_in_sources,
+ "Python packages must have at least one __init__.py file")
+ }
+ }
_python_deps = []
if (defined(invoker.python_deps)) {
@@ -53,36 +98,11 @@
}
}
- if (defined(invoker.sources)) {
- _all_sources = invoker.sources
- } else {
- _all_sources = []
- }
-
- if (defined(invoker.tests)) {
- _test_sources = invoker.tests
- } else {
- _test_sources = []
- }
-
- _all_sources += _test_sources
-
- assert(_all_sources != [], "At least one source or test must be provided")
-
- # Get the directory of the setup files. All files must be in the same dir.
- _setup_dirs = get_path_info(invoker.setup, "dir")
- _setup_dir = _setup_dirs[0]
-
- foreach(dir, _setup_dirs) {
- assert(dir == _setup_dir,
- "All files in 'setup' must be in the same directory")
- }
-
# Declare the main Python package group. This represents the Python files, but
# does not take any actions. GN targets can depend on the package name to run
# when any files in the package change.
pw_input_group(target_name) {
- inputs = _all_sources + invoker.setup
+ inputs = _all_py_files
if (defined(invoker.inputs)) {
inputs += invoker.inputs
}
@@ -96,40 +116,56 @@
_package_target = ":$target_name"
- # Install this Python package and its dependencies in the current Python
- # environment.
- pw_python_action("$target_name.install") {
- module = "pip"
- args = [
- "install",
- "--editable",
- rebase_path(_setup_dir),
- ]
+ if (_is_package) {
+ # Install this Python package and its dependencies in the current Python
+ # environment.
+ pw_python_action("$target_name.install") {
+ module = "pip"
+ args = [
+ "install",
+ "--editable",
+ rebase_path(_setup_dir),
+ ]
- stamp = true
+ stamp = true
- # Parallel pip installations don't work, so serialize pip invocations.
- pool = "$dir_pw_build:pip_pool"
+ # Parallel pip installations don't work, so serialize pip invocations.
+ pool = "$dir_pw_build:pip_pool"
- deps = [ _package_target ]
- foreach(dep, _python_deps) {
- deps += [ "$dep.install" ]
+ deps = [ _package_target ]
+ foreach(dep, _python_deps) {
+ deps += [ "$dep.install" ]
+ }
}
- }
- # TODO(pwbug/239): Add support for building groups of wheels. The code below
- # is incomplete and untested.
- pw_python_action("$target_name.wheel") {
- script = "$dir_pw_build/py/pw_build/python_wheels.py"
+ # TODO(pwbug/239): Add support for building groups of wheels. The code below
+ # is incomplete and untested.
+ pw_python_action("$target_name.wheel") {
+ script = "$dir_pw_build/py/pw_build/python_wheels.py"
- args = [
- "--out_dir",
- rebase_path(target_out_dir),
- ]
- args += rebase_path(_all_sources)
+ args = [
+ "--out_dir",
+ rebase_path(target_out_dir),
+ ]
+ args += rebase_path(_all_py_files)
- deps = [ _package_target ]
- stamp = true
+ deps = [ _package_target ]
+ stamp = true
+ }
+ } else {
+ # If this is not a package, install or build wheels for its deps only.
+ group("$target_name.install") {
+ deps = []
+ foreach(dep, _python_deps) {
+ deps += [ "$dep.install" ]
+ }
+ }
+ group("$target_name.wheel") {
+ deps = []
+ foreach(dep, _python_deps) {
+ deps += [ "$dep.wheel" ]
+ }
+ }
}
# Define the static analysis targets for this package.
@@ -146,8 +182,12 @@
"--pretty",
"--show-error-codes",
"--color-output",
- rebase_path(_setup_dir),
]
+ if (_is_package) {
+ args += [ rebase_path(_setup_dir) ]
+ } else {
+ args += rebase_path(_all_py_files)
+ }
# Use this environment variable to force mypy to colorize output.
# See https://github.com/python/mypy/issues/7771
@@ -174,7 +214,7 @@
args += [ "--disable=unexpected-line-ending-format" ]
}
- sources = _all_sources
+ sources = _all_py_files
stamp = "$target_gen_dir/{{source_target_relative}}.pylint.pw_pystamp"
@@ -249,3 +289,27 @@
}
}
}
+
+# Declares Python scripts or tests that are not part of a Python package.
+# Similar to pw_python_package, but only supports a subset of its features.
+#
+# pw_python_script accepts the same arguments as pw_python_package, except
+# `setup` cannot be provided.
+#
+# pw_python_script provides the same subtargets as pw_python_package, but
+# $target_name.install and $target_name.wheel only affect the python_deps of
+# this GN target, not the target itself.
+template("pw_python_script") {
+ _supported_variables = [
+ "sources",
+ "tests",
+ "python_deps",
+ "other_deps",
+ "inputs",
+ ]
+
+ pw_python_package(target_name) {
+ _pw_standalone = true
+ forward_variables_from(invoker, _supported_variables)
+ }
+}
diff --git a/pw_hdlc_lite/rpc_example/BUILD.gn b/pw_hdlc_lite/rpc_example/BUILD.gn
index 375021f..495b168 100644
--- a/pw_hdlc_lite/rpc_example/BUILD.gn
+++ b/pw_hdlc_lite/rpc_example/BUILD.gn
@@ -14,6 +14,7 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/python.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_third_party/nanopb/nanopb.gni")
@@ -35,3 +36,7 @@
]
}
}
+
+pw_python_script("example_script") {
+ sources = [ "example_script.py" ]
+}
diff --git a/pw_hdlc_lite/rpc_example/example_script.py b/pw_hdlc_lite/rpc_example/example_script.py
index b8b2b0f..57726de 100755
--- a/pw_hdlc_lite/rpc_example/example_script.py
+++ b/pw_hdlc_lite/rpc_example/example_script.py
@@ -18,7 +18,7 @@
import os
from pathlib import Path
-import serial
+import serial # type: ignore
from pw_hdlc_lite.rpc import HdlcRpcClient