diff --git a/.gitattributes b/.gitattributes index 151144a8..5e3c1501 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ docs/*.md linguist-generated=true +docs/migrating.md linguist-generated=false # Configuration for 'git archive' # see https://git-scm.com/docs/git-archive/2.40.0#ATTRIBUTES diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7a00925b..de2e794f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,11 +30,11 @@ artifact or a version fetched from the internet, run this from this directory: ```sh -OVERRIDE="--override_repository=rules_py=$(pwd)/rules_py" +OVERRIDE="--override_repository=aspect_rules_py=$(pwd)/rules_py" echo "common $OVERRIDE" >> ~/.bazelrc ``` -This means that any usage of `@rules_py` on your system will point to this folder. +This means that any usage of `@aspect_rules_py` on your system will point to this folder. ## Releasing diff --git a/README.md b/README.md index 2b51fb4d..809e6739 100644 --- a/README.md +++ b/README.md @@ -7,21 +7,25 @@ The lower layer of `rules_python` is currently reused, dealing with the toolchai However, this ruleset introduces a new implementation of `py_library`, `py_binary`, and `py_test`. Our philosophy is to behave more like idiomatic python ecosystem tools, where rules_python is closely tied to the way Google does Python development in their internal monorepo, google3. +However we try to maintain compatibility with rules_python's rules for most use cases. -| Layer | Legacy | Recommended | -| ------------------------------------------- | ------------ | ---------------- | -| rules: BUILD file UI | rules_python | **rules_py** | -| gazelle: generate BUILD files | rules_python | rules_python [1] | -| pip_parse: fetch and install deps from pypi | rules_python | rules_python | -| toolchain: fetch hermetic interpreter | rules_python | rules_python | +| Layer | Legacy | Recommended | +| ------------------------------------------- | ------------ | -------------------- | +| toolchain: fetch hermetic interpreter | rules_python | rules_python | +| pip.parse: fetch and install deps from pypi | rules_python | rules_python | +| gazelle: generate BUILD files | rules_python | [`aspect configure`] | +| rules: user-facing implementations | rules_python | **rules_py** | + +Watch our video series for a quick tutorial on how rules_py makes it easy to do Python with Bazel: +[](https://www.youtube.com/playlist?list=PLLU28e_DRwdu46fldnYzyFYvSJLjVFICd) _Need help?_ This ruleset has support provided by https://aspect.dev. -[1] we will likely fork the extension for performance, using TreeSitter to parse Python code rather than a Python program. +[`aspect configure`]: https://docs.aspect.build/cli/commands/aspect_configure ## Differences -We think you'll love rules_py because: +We think you'll love rules_py because it fixes many issues with rules_python's rule implementations: - The launcher uses the Bash toolchain rather than Python, so we have no dependency on a system interpreter. Fixes: - [py_binary with hermetic toolchain requires a system interpreter](https://github.com/bazelbuild/rules_python/issues/691) @@ -32,8 +36,8 @@ We think you'll love rules_py because: - [sys.path[0] breaks out of runfile tree.](https://github.com/bazelbuild/rules_python/issues/382) - [User site-packages directory should be ignored](https://github.com/bazelbuild/rules_python/issues/1059) - We create a python-idiomatic virtualenv to run actions, which means better compatibility with userland implementations of [importlib](https://docs.python.org/3/library/importlib.html). -- Thanks to the virtualenv, you can open the project in an editor like PyCharm and have working auto-complete, jump-to-definition, etc. Fixes: - - [Smooth IDE support for python_rules](https://github.com/bazelbuild/rules_python/issues/1401) +- Thanks to the virtualenv, you can open the project in an editor like PyCharm or VSCode and have working auto-complete, jump-to-definition, etc. + - Fixes [Smooth IDE support for python_rules](https://github.com/bazelbuild/rules_python/issues/1401) > [!NOTE] > What about the "starlarkification" effort in rules_python? @@ -50,7 +54,7 @@ Follow instructions from the release you wish to use: ### Using with Gazelle -In any ancestor `BUILD` file of the Python code, add these lines to instruct [Gazelle] to create rules_py variants of the `py_*` rules: +In any ancestor `BUILD` file of the Python code, add these lines to instruct [Gazelle] to create `rules_py` variants of the `py_*` rules: ``` # gazelle:map_kind py_library py_library @aspect_rules_py//py:defs.bzl @@ -58,4 +62,20 @@ In any ancestor `BUILD` file of the Python code, add these lines to instruct [Ga # gazelle:map_kind py_test py_test @aspect_rules_py//py:defs.bzl ``` -[Gazelle]: https://github.com/bazelbuild/rules_python/blob/main/gazelle/README.md +[gazelle]: https://github.com/bazelbuild/rules_python/blob/main/gazelle/README.md + +# Public API + +## Executables + +- [py_binary](docs/py_binary.md) an executable Python program, used with `bazel run` or as a tool. +- [py_test](docs/py_test.md) a Python program that executes a test runner such as `unittest` or `pytest`, to be used with `bazel test`. +- [py_venv](docs/venv.md) create a virtualenv for a `py_binary` or `py_test` target for use outside Bazel, such as in an editor/IDE. + +## Packaging + +- [py_pex_binary](docs/pex.md) Create a zip file containing a full Python application. + +## Packages + +- [py_library](docs/py_library.md) a unit of Python code, used as a dependency of other rules. diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel index 763a7279..59785a2d 100644 --- a/docs/BUILD.bazel +++ b/docs/BUILD.bazel @@ -3,8 +3,37 @@ load("@aspect_bazel_lib//lib:docs.bzl", "stardoc_with_diff_test", "update_docs") stardoc_with_diff_test( - name = "rules", + name = "py_library", + bzl_library_target = "//py/private:py_library", +) + +stardoc_with_diff_test( + name = "py_binary", + bzl_library_target = "//py:defs", + symbol_names = [ + "py_binary", + "py_binary_rule", + ], +) + +stardoc_with_diff_test( + name = "py_test", bzl_library_target = "//py:defs", + symbol_names = [ + "py_test", + "py_test_rule", + "py_pytest_main", + ], +) + +stardoc_with_diff_test( + name = "pex", + bzl_library_target = "//py/private:py_pex_binary", +) + +stardoc_with_diff_test( + name = "venv", + bzl_library_target = "//py/private:py_venv", ) update_docs(name = "update") diff --git a/docs/migrating.md b/docs/migrating.md new file mode 100644 index 00000000..c1a13145 --- /dev/null +++ b/docs/migrating.md @@ -0,0 +1,15 @@ +# Migrating rules from rules_python to rules_py + +rules_py tries to closely mirror the API of rules_python. +Migration is a "drop-in replacement" for the majority of use cases. + +## Replace load statements + +Instead of loading from `@rules_python//python:defs.bzl`, load from `@aspect_rules_py//py:defs.bzl`. +The rest of the BUILD file can remain the same. + +If using Gazelle, see the note on [using with Gazelle](/README.md#using-with-gazelle) + +## Remaining notes + +Users are encouraged to send a Pull Request to add more documentation as they uncover issues during migrations. diff --git a/docs/pex.md b/docs/pex.md new file mode 100644 index 00000000..92504a3e --- /dev/null +++ b/docs/pex.md @@ -0,0 +1,44 @@ + + +Create a zip file containing a full Python application. + +Follows [PEP-441 (PEX)](https://peps.python.org/pep-0441/) + +## Ensuring a compatible interpreter is used + +The resulting zip file does *not* contain a Python interpreter. +Users are expected to execute the PEX with a compatible interpreter on the runtime system. + +Use the `python_interpreter_constraints` to provide an error if a wrong interpreter tries to execute the PEX, for example: + +```starlark +py_pex_binary( + python_interpreter_constraints = [ + "CPython=={major}.{minor}.{patch}", + ] +) +``` + + + + +## py_pex_binary + +
+py_pex_binary(name, binary, inject_env, python_interpreter_constraints, python_shebang) ++ +Build a pex executable from a py_binary + +**ATTRIBUTES** + + +| Name | Description | Type | Mandatory | Default | +| :------------- | :------------- | :------------- | :------------- | :------------- | +| name | A unique name for this target. | Name | required | | +| binary | A py_binary target | Label | required | | +| inject_env | Environment variables to set when running the pex binary. | Dictionary: String -> String | optional |
{}
|
+| python_interpreter_constraints | Python interpreter versions this PEX binary is compatible with. A list of semver strings. The placeholder strings {major}
, {minor}
, {patch}
can be used for gathering version information from the hermetic python toolchain. | List of strings | optional | ["CPython=={major}.{minor}.*"]
|
+| python_shebang | - | String | optional | "#!/usr/bin/env python3"
|
+
+
diff --git a/docs/py_binary.md b/docs/py_binary.md
new file mode 100644
index 00000000..a384c2f0
--- /dev/null
+++ b/docs/py_binary.md
@@ -0,0 +1,92 @@
+
+
+Re-implementations of [py_binary](https://bazel.build/reference/be/python#py_binary)
+and [py_test](https://bazel.build/reference/be/python#py_test)
+
+## Choosing the Python version
+
+The `python_version` attribute must refer to a python toolchain version
+which has been registered in the WORKSPACE or MODULE.bazel file.
+
+When using WORKSPACE, this may look like this:
+
+```starlark
+load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains")
+
+python_register_toolchains(
+ name = "python_toolchain_3_8",
+ python_version = "3.8.12",
+ # setting set_python_version_constraint makes it so that only matches py_* rule
+ # which has this exact version set in the `python_version` attribute.
+ set_python_version_constraint = True,
+)
+
+# It's important to register the default toolchain last it will match any py_* target.
+python_register_toolchains(
+ name = "python_toolchain",
+ python_version = "3.9",
+)
+```
+
+Configuring for MODULE.bazel may look like this:
+
+```starlark
+python = use_extension("@rules_python//python/extensions:python.bzl", "python")
+python.toolchain(python_version = "3.8.12", is_default = False)
+python.toolchain(python_version = "3.9", is_default = True)
+```
+
+
+
+
+## py_binary_rule
+
++py_binary_rule(name, data, deps, env, imports, main, package_collisions, python_version, + resolutions, srcs) ++ +Run a Python program under Bazel. Most users should use the [py_binary macro](#py_binary) instead of loading this directly. + +**ATTRIBUTES** + + +| Name | Description | Type | Mandatory | Default | +| :------------- | :------------- | :------------- | :------------- | :------------- | +| name | A unique name for this target. | Name | required | | +| data | Runtime dependencies of the program.
data
dependencies will be available in the .runfiles
folder for this binary/test. The program may optionally use the Runfiles lookup library to locate the data files, see https://pypi.org/project/bazel-runfiles/. | List of labels | optional | []
|
+| deps | Targets that produce Python code, commonly py_library
rules. | List of labels | optional | []
|
+| env | Environment variables to set when running the binary. | Dictionary: String -> String | optional | {}
|
+| imports | List of import directories to be added to the PYTHONPATH. | List of strings | optional | []
|
+| main | Script to execute with the Python interpreter. | Label | required | |
+| package_collisions | The action that should be taken when a symlink collision is encountered when creating the venv. A collision can occour when multiple packages providing the same file are installed into the venv. The possible values are:"error"
|
+| python_version | Whether to build this target and its transitive deps for a specific python version. | String | optional | ""
|
+| resolutions | FIXME | Dictionary: Label -> String | optional | {}
|
+| srcs | Python source files. | List of labels | optional | []
|
+
+
+
+
+## py_binary
+
++py_binary(name, srcs, main, kwargs) ++ +Wrapper macro for [`py_binary_rule`](#py_binary_rule). + +Creates a [py_venv](./venv.md) target to constrain the interpreter and packages used at runtime. +Users can `bazel run [name].venv` to create this virtualenv, then use it in the editor or other tools. + + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| name | Name of the rule. | none | +| srcs | Python source files. |
[]
|
+| main | Entry point. Like rules_python, this is treated as a suffix of a file that should appear among the srcs. If absent, then [name].py
is tried. As a final fallback, if the srcs has a single file, that is used as the main. | None
|
+| kwargs | additional named parameters to py_binary_rule
. | none |
+
+
diff --git a/docs/py_library.md b/docs/py_library.md
new file mode 100644
index 00000000..2f8b2721
--- /dev/null
+++ b/docs/py_library.md
@@ -0,0 +1,149 @@
+
+
+A re-implementation of [py_library](https://bazel.build/reference/be/python#py_library).
+
+Supports "virtual" dependencies with a `virtual_deps` attribute, which lists packages which are required
+without binding them to a particular version of that package.
+
+
+
+
+## py_library
+
++py_library(name, data, deps, imports, resolutions, srcs, virtual_deps) ++ + + +**ATTRIBUTES** + + +| Name | Description | Type | Mandatory | Default | +| :------------- | :------------- | :------------- | :------------- | :------------- | +| name | A unique name for this target. | Name | required | | +| data | Runtime dependencies of the program.
data
dependencies will be available in the .runfiles
folder for this binary/test. The program may optionally use the Runfiles lookup library to locate the data files, see https://pypi.org/project/bazel-runfiles/. | List of labels | optional | []
|
+| deps | Targets that produce Python code, commonly py_library
rules. | List of labels | optional | []
|
+| imports | List of import directories to be added to the PYTHONPATH. | List of strings | optional | []
|
+| resolutions | FIXME | Dictionary: Label -> String | optional | {}
|
+| srcs | Python source files. | List of labels | optional | []
|
+| virtual_deps | - | List of strings | optional | []
|
+
+
+
+
+## py_library_utils.implementation
+
++py_library_utils.implementation(ctx) ++ + + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| ctx |
-
| none | + + + + +## py_library_utils.make_imports_depset + ++py_library_utils.make_imports_depset(ctx, imports, extra_imports_depsets) ++ + + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| ctx |
-
| none | +| imports |-
|[]
|
+| extra_imports_depsets | -
|[]
|
+
+
+
+
+## py_library_utils.make_instrumented_files_info
+
++py_library_utils.make_instrumented_files_info(ctx, extra_source_attributes, + extra_dependency_attributes) ++ + + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| ctx |
-
| none | +| extra_source_attributes |-
|[]
|
+| extra_dependency_attributes | -
|[]
|
+
+
+
+
+## py_library_utils.make_merged_runfiles
+
++py_library_utils.make_merged_runfiles(ctx, extra_depsets, extra_runfiles, extra_runfiles_depsets) ++ + + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| ctx |
-
| none | +| extra_depsets |-
|[]
|
+| extra_runfiles | -
|[]
|
+| extra_runfiles_depsets | -
|[]
|
+
+
+
+
+## py_library_utils.make_srcs_depset
+
++py_library_utils.make_srcs_depset(ctx) ++ + + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| ctx |
-
| none | + + + + +## py_library_utils.resolve_virtuals + ++py_library_utils.resolve_virtuals(ctx, ignore_missing) ++ + + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| ctx |
-
| none | +| ignore_missing |-
|False
|
+
+
diff --git a/docs/py_test.md b/docs/py_test.md
new file mode 100644
index 00000000..d194fd5e
--- /dev/null
+++ b/docs/py_test.md
@@ -0,0 +1,111 @@
+
+
+Re-implementations of [py_binary](https://bazel.build/reference/be/python#py_binary)
+and [py_test](https://bazel.build/reference/be/python#py_test)
+
+## Choosing the Python version
+
+The `python_version` attribute must refer to a python toolchain version
+which has been registered in the WORKSPACE or MODULE.bazel file.
+
+When using WORKSPACE, this may look like this:
+
+```starlark
+load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains")
+
+python_register_toolchains(
+ name = "python_toolchain_3_8",
+ python_version = "3.8.12",
+ # setting set_python_version_constraint makes it so that only matches py_* rule
+ # which has this exact version set in the `python_version` attribute.
+ set_python_version_constraint = True,
+)
+
+# It's important to register the default toolchain last it will match any py_* target.
+python_register_toolchains(
+ name = "python_toolchain",
+ python_version = "3.9",
+)
+```
+
+Configuring for MODULE.bazel may look like this:
+
+```starlark
+python = use_extension("@rules_python//python/extensions:python.bzl", "python")
+python.toolchain(python_version = "3.8.12", is_default = False)
+python.toolchain(python_version = "3.9", is_default = True)
+```
+
+
+
+
+## py_test_rule
+
++py_test_rule(name, data, deps, env, imports, main, package_collisions, python_version, resolutions, + srcs) ++ +Run a Python program under Bazel. Most users should use the [py_test macro](#py_test) instead of loading this directly. + +**ATTRIBUTES** + + +| Name | Description | Type | Mandatory | Default | +| :------------- | :------------- | :------------- | :------------- | :------------- | +| name | A unique name for this target. | Name | required | | +| data | Runtime dependencies of the program.
data
dependencies will be available in the .runfiles
folder for this binary/test. The program may optionally use the Runfiles lookup library to locate the data files, see https://pypi.org/project/bazel-runfiles/. | List of labels | optional | []
|
+| deps | Targets that produce Python code, commonly py_library
rules. | List of labels | optional | []
|
+| env | Environment variables to set when running the binary. | Dictionary: String -> String | optional | {}
|
+| imports | List of import directories to be added to the PYTHONPATH. | List of strings | optional | []
|
+| main | Script to execute with the Python interpreter. | Label | required | |
+| package_collisions | The action that should be taken when a symlink collision is encountered when creating the venv. A collision can occour when multiple packages providing the same file are installed into the venv. The possible values are:"error"
|
+| python_version | Whether to build this target and its transitive deps for a specific python version. | String | optional | ""
|
+| resolutions | FIXME | Dictionary: Label -> String | optional | {}
|
+| srcs | Python source files. | List of labels | optional | []
|
+
+
+
+
+## py_pytest_main
+
++py_pytest_main(name, py_library, deps, data, testonly, kwargs) ++ +py_pytest_main wraps the template rendering target and the final py_library. + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| name | The name of the runable target that updates the test entry file. | none | +| py_library | Use this attribute to override the default py_library rule. |
<unknown object com.google.devtools.build.skydoc.fakebuildapi.FakeStarlarkRuleFunctionsApi$RuleDefinitionIdentifier>
|
+| deps | A list containing the pytest library target, e.g., @pypi_pytest//:pkg. | []
|
+| data | A list of data dependencies to pass to the py_library target. | []
|
+| testonly | A boolean indicating if the py_library target is testonly. | True
|
+| kwargs | The extra arguments passed to the template rendering target. | none |
+
+
+
+
+## py_test
+
++py_test(name, main, srcs, kwargs) ++ +Identical to [py_binary](./py_binary.md), but produces a target that can be used with `bazel test`. + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| name |
-
| none | +| main |-
|None
|
+| srcs | -
|[]
|
+| kwargs | -
| none | + + diff --git a/docs/rules.md b/docs/rules.md deleted file mode 100644 index 9078fd76..00000000 --- a/docs/rules.md +++ /dev/null @@ -1,249 +0,0 @@ - - -Public API re-exports - - - -## py_binary_rule - --py_binary_rule(name, data, deps, env, imports, main, package_collisions, python_version, - resolutions, srcs) -- -Run a Python program under Bazel. Most users should use the [py_binary macro](#py_binary) instead of loading this directly. - -**ATTRIBUTES** - - -| Name | Description | Type | Mandatory | Default | -| :------------- | :------------- | :------------- | :------------- | :------------- | -| name | A unique name for this target. | Name | required | | -| data | Runtime dependencies of the program.
data
dependencies will be available in the .runfiles
folder for this binary/test. The program may optionally use the Runfiles lookup library to locate the data files, see https://pypi.org/project/bazel-runfiles/. | List of labels | optional | []
|
-| deps | Targets that produce Python code, commonly py_library
rules. | List of labels | optional | []
|
-| env | Environment variables to set when running the binary. | Dictionary: String -> String | optional | {}
|
-| imports | List of import directories to be added to the PYTHONPATH. | List of strings | optional | []
|
-| main | Script to execute with the Python interpreter. | Label | required | |
-| package_collisions | The action that should be taken when a symlink collision is encountered when creating the venv. A collision can occour when multiple packages providing the same file are installed into the venv. The possible values are:"error"
|
-| python_version | Whether to build this target and its transitive deps for a specific python version. load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains")
python_register_toolchains( name = "python_toolchain_3_8", python_version = "3.8.12", # setting set_python_version_constraint makes it so that only matches py_* rule # which has this exact version set in the python_version
attribute. set_python_version_constraint = True, )
# It's important to register the default toolchain last it will match any py_* target. python_register_toolchains( name = "python_toolchain", python_version = "3.9", )
python = use_extension("@rules_python//python/extensions:python.bzl", "python") python.toolchain(python_version = "3.8.12", is_default = False) python.toolchain(python_version = "3.9", is_default = True)
| String | optional | ""
|
-| resolutions | FIXME | Dictionary: Label -> String | optional | {}
|
-| srcs | Python source files. | List of labels | optional | []
|
-
-
-
-
-## py_library
-
--py_library(name, data, deps, imports, resolutions, srcs, virtual_deps) -- - - -**ATTRIBUTES** - - -| Name | Description | Type | Mandatory | Default | -| :------------- | :------------- | :------------- | :------------- | :------------- | -| name | A unique name for this target. | Name | required | | -| data | Runtime dependencies of the program.
data
dependencies will be available in the .runfiles
folder for this binary/test. The program may optionally use the Runfiles lookup library to locate the data files, see https://pypi.org/project/bazel-runfiles/. | List of labels | optional | []
|
-| deps | Targets that produce Python code, commonly py_library
rules. | List of labels | optional | []
|
-| imports | List of import directories to be added to the PYTHONPATH. | List of strings | optional | []
|
-| resolutions | FIXME | Dictionary: Label -> String | optional | {}
|
-| srcs | Python source files. | List of labels | optional | []
|
-| virtual_deps | - | List of strings | optional | []
|
-
-
-
-
-## py_pex_binary
-
--py_pex_binary(name, binary, inject_env, python_interpreter_constraints, python_shebang) -- -Build a pex executable from a py_binary - -**ATTRIBUTES** - - -| Name | Description | Type | Mandatory | Default | -| :------------- | :------------- | :------------- | :------------- | :------------- | -| name | A unique name for this target. | Name | required | | -| binary | A py_binary target | Label | required | | -| inject_env | Environment variables to set when running the pex binary. | Dictionary: String -> String | optional |
{}
|
-| python_interpreter_constraints | Python interpreter versions this PEX binary is compatible with. A list of semver strings. The placeholder strings {major}
, {minor}
, {patch}
can be used for gathering version information from the hermetic python toolchain.starlark py_pex_binary python_interpreter_constraints = [ "CPython=={major}.{minor}.{patch}" ] )
| List of strings | optional | ["CPython=={major}.{minor}.*"]
|
-| python_shebang | - | String | optional | "#!/usr/bin/env python3"
|
-
-
-
-
-## py_test_rule
-
--py_test_rule(name, data, deps, env, imports, main, package_collisions, python_version, resolutions, - srcs) -- -Run a Python program under Bazel. Most users should use the [py_test macro](#py_test) instead of loading this directly. - -**ATTRIBUTES** - - -| Name | Description | Type | Mandatory | Default | -| :------------- | :------------- | :------------- | :------------- | :------------- | -| name | A unique name for this target. | Name | required | | -| data | Runtime dependencies of the program.
data
dependencies will be available in the .runfiles
folder for this binary/test. The program may optionally use the Runfiles lookup library to locate the data files, see https://pypi.org/project/bazel-runfiles/. | List of labels | optional | []
|
-| deps | Targets that produce Python code, commonly py_library
rules. | List of labels | optional | []
|
-| env | Environment variables to set when running the binary. | Dictionary: String -> String | optional | {}
|
-| imports | List of import directories to be added to the PYTHONPATH. | List of strings | optional | []
|
-| main | Script to execute with the Python interpreter. | Label | required | |
-| package_collisions | The action that should be taken when a symlink collision is encountered when creating the venv. A collision can occour when multiple packages providing the same file are installed into the venv. The possible values are:"error"
|
-| python_version | Whether to build this target and its transitive deps for a specific python version. load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains")
python_register_toolchains( name = "python_toolchain_3_8", python_version = "3.8.12", # setting set_python_version_constraint makes it so that only matches py_* rule # which has this exact version set in the python_version
attribute. set_python_version_constraint = True, )
# It's important to register the default toolchain last it will match any py_* target. python_register_toolchains( name = "python_toolchain", python_version = "3.9", )
python = use_extension("@rules_python//python/extensions:python.bzl", "python") python.toolchain(python_version = "3.8.12", is_default = False) python.toolchain(python_version = "3.9", is_default = True)
| String | optional | ""
|
-| resolutions | FIXME | Dictionary: Label -> String | optional | {}
|
-| srcs | Python source files. | List of labels | optional | []
|
-
-
-
-
-## py_unpacked_wheel
-
--py_unpacked_wheel(name, py_package_name, src) -- - - -**ATTRIBUTES** - - -| Name | Description | Type | Mandatory | Default | -| :------------- | :------------- | :------------- | :------------- | :------------- | -| name | A unique name for this target. | Name | required | | -| py_package_name | - | String | required | | -| src | The Wheel file, as defined by https://packaging.python.org/en/latest/specifications/binary-distribution-format/#binary-distribution-format | Label | required | | - - - - -## py_binary - -
-py_binary(name, srcs, main, kwargs) -- -Wrapper macro for [`py_binary_rule`](#py_binary_rule). - -Creates a virtualenv to constrain the interpreter and packages used at runtime. -Users can `bazel run [name].venv` to produce this, then use it in the editor. - - -**PARAMETERS** - - -| Name | Description | Default Value | -| :------------- | :------------- | :------------- | -| name | Name of the rule. | none | -| srcs | Python source files. |
[]
|
-| main | Entry point. Like rules_python, this is treated as a suffix of a file that should appear among the srcs. If absent, then "[name].py" is tried. As a final fallback, if the srcs has a single file, that is used as the main. | None
|
-| kwargs | additional named parameters to the py_binary_rule. | none |
-
-
-
-
-## py_pytest_main
-
--py_pytest_main(name, py_library, deps, data, testonly, kwargs) -- -py_pytest_main wraps the template rendering target and the final py_library. - -**PARAMETERS** - - -| Name | Description | Default Value | -| :------------- | :------------- | :------------- | -| name | The name of the runable target that updates the test entry file. | none | -| py_library | Use this attribute to override the default py_library rule. |
<unknown object com.google.devtools.build.skydoc.fakebuildapi.FakeStarlarkRuleFunctionsApi$RuleDefinitionIdentifier>
|
-| deps | A list containing the pytest library target, e.g., @pypi_pytest//:pkg. | []
|
-| data | A list of data dependencies to pass to the py_library target. | []
|
-| testonly | A boolean indicating if the py_library target is testonly. | True
|
-| kwargs | The extra arguments passed to the template rendering target. | none |
-
-
-
-
-## py_test
-
--py_test(name, main, srcs, kwargs) -- -Identical to py_binary, but produces a target that can be used with `bazel test`. - -**PARAMETERS** - - -| Name | Description | Default Value | -| :------------- | :------------- | :------------- | -| name |
-
| none | -| main |-
|None
|
-| srcs | -
|[]
|
-| kwargs | -
| none | - - - - -## py_venv - --py_venv(name, kwargs) -- - - -**PARAMETERS** - - -| Name | Description | Default Value | -| :------------- | :------------- | :------------- | -| name |
-
| none | -| kwargs |-
| none | - - - - -## resolutions.from_requirements - --resolutions.from_requirements(base, requirement_fn) -- -Returns data representing the resolution for a given set of dependencies - -**PARAMETERS** - - -| Name | Description | Default Value | -| :------------- | :------------- | :------------- | -| base | Base set of requirements to turn into resolutions. | none | -| requirement_fn | Optional function to transform the Python package name into a requirement label. |
<function lambda>
|
-
-**RETURNS**
-
-A resolution struct for use with virtual deps.
-
-
-
-
-## resolutions.empty
-
--resolutions.empty() -- - - - - diff --git a/docs/venv.md b/docs/venv.md new file mode 100644 index 00000000..ab6595a2 --- /dev/null +++ b/docs/venv.md @@ -0,0 +1,62 @@ + + +Create a Python virtualenv directory structure. + +Note that [py_binary](./py_binary.md#py_binary) and [py_test](./py_test.md#py_test) macros automatically provide `[name].venv` targets. +Using `py_venv` directly is only required for cases where those defaults do not apply. + +> [!NOTE] +> As an implementation detail, this currently uses <https://github.com/prefix-dev/rip> which is a very fast Rust-based tool. + + + + +## py_venv_rule + +
+py_venv_rule(name, deps, imports, location, package_collisions, resolutions, venv_name) ++ +Create a Python virtual environment with the dependencies listed. + +**ATTRIBUTES** + + +| Name | Description | Type | Mandatory | Default | +| :------------- | :------------- | :------------- | :------------- | :------------- | +| name | A unique name for this target. | Name | required | | +| deps | Targets that produce Python code, commonly
py_library
rules. | List of labels | optional | []
|
+| imports | List of import directories to be added to the PYTHONPATH. | List of strings | optional | []
|
+| location | Path from the workspace root for where to root the virtial environment | String | optional | ""
|
+| package_collisions | The action that should be taken when a symlink collision is encountered when creating the venv. A collision can occour when multiple packages providing the same file are installed into the venv. The possible values are:"error"
|
+| resolutions | FIXME | Dictionary: Label -> String | optional | {}
|
+| venv_name | Outer folder name for the generated virtual environment | String | optional | ""
|
+
+
+
+
+## py_venv
+
++py_venv(name, kwargs) ++ +Wrapper macro for [`py_venv_rule`](#py_venv_rule). + +Chooses a suitable default location for the resulting directory. + +By default, VSCode (and likely other tools) expect to find virtualenv's in the root of the project opened in the editor. +They also provide a nice name to see "which one is open" when discovered this way. +See https://github.com/aspect-build/rules_py/issues/395 + +Use py_venv_rule directly to have more control over the location. + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| name |
-
| none | +| kwargs |-
| none | + + diff --git a/py/defs.bzl b/py/defs.bzl index 6fabf75a..abd76926 100644 --- a/py/defs.bzl +++ b/py/defs.bzl @@ -1,4 +1,39 @@ -"Public API re-exports" +"""Re-implementations of [py_binary](https://bazel.build/reference/be/python#py_binary) +and [py_test](https://bazel.build/reference/be/python#py_test) + +## Choosing the Python version + +The `python_version` attribute must refer to a python toolchain version +which has been registered in the WORKSPACE or MODULE.bazel file. + +When using WORKSPACE, this may look like this: + +```starlark +load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains") + +python_register_toolchains( + name = "python_toolchain_3_8", + python_version = "3.8.12", + # setting set_python_version_constraint makes it so that only matches py_* rule + # which has this exact version set in the `python_version` attribute. + set_python_version_constraint = True, +) + +# It's important to register the default toolchain last it will match any py_* target. +python_register_toolchains( + name = "python_toolchain", + python_version = "3.9", +) +``` + +Configuring for MODULE.bazel may look like this: + +```starlark +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain(python_version = "3.8.12", is_default = False) +python.toolchain(python_version = "3.9", is_default = True) +``` +""" load("@aspect_bazel_lib//lib:utils.bzl", "propagate_common_rule_attributes") load("//py/private:py_binary.bzl", _py_binary = "py_binary", _py_test = "py_test") @@ -57,17 +92,17 @@ def _py_binary_or_test(name, rule, srcs, main, deps = [], resolutions = {}, **kw def py_binary(name, srcs = [], main = None, **kwargs): """Wrapper macro for [`py_binary_rule`](#py_binary_rule). - Creates a virtualenv to constrain the interpreter and packages used at runtime. - Users can `bazel run [name].venv` to produce this, then use it in the editor. + Creates a [py_venv](./venv.md) target to constrain the interpreter and packages used at runtime. + Users can `bazel run [name].venv` to create this virtualenv, then use it in the editor or other tools. Args: name: Name of the rule. srcs: Python source files. main: Entry point. Like rules_python, this is treated as a suffix of a file that should appear among the srcs. - If absent, then "[name].py" is tried. As a final fallback, if the srcs has a single file, + If absent, then `[name].py` is tried. As a final fallback, if the srcs has a single file, that is used as the main. - **kwargs: additional named parameters to the py_binary_rule. + **kwargs: additional named parameters to `py_binary_rule`. """ # For a clearer DX when updating resolutions, the resolutions dict is "string" -> "label", @@ -79,7 +114,7 @@ def py_binary(name, srcs = [], main = None, **kwargs): _py_binary_or_test(name = name, rule = _py_binary, srcs = srcs, main = main, resolutions = resolutions, **kwargs) def py_test(name, main = None, srcs = [], **kwargs): - "Identical to py_binary, but produces a target that can be used with `bazel test`." + "Identical to [py_binary](./py_binary.md), but produces a target that can be used with `bazel test`." # Ensure that any other targets we write will be testonly like the py_test target kwargs["testonly"] = True diff --git a/py/private/BUILD.bazel b/py/private/BUILD.bazel index 7cd39349..943d0fb8 100644 --- a/py/private/BUILD.bazel +++ b/py/private/BUILD.bazel @@ -1,5 +1,10 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +package(default_visibility = [ + "//docs:__pkg__", + "//py:__subpackages__", +]) + exports_files( [ "run.tmpl.sh", @@ -20,7 +25,6 @@ exports_files( bzl_library( name = "py_binary", srcs = ["py_binary.bzl"], - visibility = ["//:__subpackages__"], deps = [ ":py_library", "@aspect_bazel_lib//lib:expand_make_vars", @@ -31,7 +35,6 @@ bzl_library( bzl_library( name = "py_library", srcs = ["py_library.bzl"], - visibility = ["//:__subpackages__"], deps = [ ":providers", ":py_semantics", @@ -39,19 +42,18 @@ bzl_library( "@bazel_skylib//lib:new_sets", "@bazel_skylib//lib:paths", "@bazel_skylib//lib:types", + "@rules_python//python:defs_bzl", ], ) bzl_library( name = "py_wheel", - visibility = ["//py:__subpackages__"], deps = [":providers"], ) bzl_library( name = "py_pytest_main", srcs = ["py_pytest_main.bzl"], - visibility = ["//py:__subpackages__"], deps = [ "@rules_python//docs:bazel_repo_tools", "@rules_python//python:defs_bzl", @@ -61,7 +63,6 @@ bzl_library( bzl_library( name = "py_semantics", srcs = ["py_semantics.bzl"], - visibility = ["//py:__subpackages__"], deps = [ "//py/private/toolchain:types", "@bazel_skylib//rules:common_settings", @@ -71,7 +72,6 @@ bzl_library( bzl_library( name = "py_unpacked_wheel", srcs = ["py_unpacked_wheel.bzl"], - visibility = ["//py:__subpackages__"], deps = [ ":py_library", ":py_semantics", @@ -83,7 +83,6 @@ bzl_library( bzl_library( name = "py_venv", srcs = ["py_venv.bzl"], - visibility = ["//py:__subpackages__"], deps = [ ":providers", ":py_library", @@ -109,15 +108,14 @@ bzl_library( bzl_library( name = "py_pex_binary", srcs = ["py_pex_binary.bzl"], - visibility = ["//py:__subpackages__"], deps = [ ":py_semantics", "//py/private/toolchain:types", + "@rules_python//python:defs_bzl", ], ) bzl_library( name = "virtual", srcs = ["virtual.bzl"], - visibility = ["//py:__subpackages__"], ) diff --git a/py/private/py_binary.bzl b/py/private/py_binary.bzl index 8fa8e74c..645f227e 100644 --- a/py/private/py_binary.bzl +++ b/py/private/py_binary.bzl @@ -141,39 +141,7 @@ _attrs = dict({ mandatory = True, ), "python_version": attr.string( - doc = """Whether to build this target and its transitive deps for a specific python version. - -Note that setting this attribute alone will not be enough as the python toolchain for the desired version -also needs to be registered in the WORKSPACE or MODULE.bazel file. - -When using WORKSPACE, this may look like this, - -``` -load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains") - -python_register_toolchains( - name = "python_toolchain_3_8", - python_version = "3.8.12", - # setting set_python_version_constraint makes it so that only matches py_* rule - # which has this exact version set in the `python_version` attribute. - set_python_version_constraint = True, -) - -# It's important to register the default toolchain last it will match any py_* target. -python_register_toolchains( - name = "python_toolchain", - python_version = "3.9", -) -``` - -Configuring for MODULE.bazel may look like this: - -``` -python = use_extension("@rules_python//python/extensions:python.bzl", "python") -python.toolchain(python_version = "3.8.12", is_default = False) -python.toolchain(python_version = "3.9", is_default = True) -``` -""", + doc = """Whether to build this target and its transitive deps for a specific python version.""", ), "package_collisions": attr.string( doc = """The action that should be taken when a symlink collision is encountered when creating the venv. diff --git a/py/private/py_library.bzl b/py/private/py_library.bzl index 2b52bc8f..ca570a58 100644 --- a/py/private/py_library.bzl +++ b/py/private/py_library.bzl @@ -1,4 +1,8 @@ -"""Implementation for the py_library rule""" +"""A re-implementation of [py_library](https://bazel.build/reference/be/python#py_library). + +Supports "virtual" dependencies with a `virtual_deps` attribute, which lists packages which are required +without binding them to a particular version of that package. +""" load("@rules_python//python:defs.bzl", "PyInfo") load("@bazel_skylib//lib:paths.bzl", "paths") diff --git a/py/private/py_pex_binary.bzl b/py/private/py_pex_binary.bzl index 92af316f..90fcf114 100644 --- a/py/private/py_pex_binary.bzl +++ b/py/private/py_pex_binary.bzl @@ -1,4 +1,22 @@ -"Create python zip file https://peps.python.org/pep-0441/ (PEX)" +"""Create a zip file containing a full Python application. + +Follows [PEP-441 (PEX)](https://peps.python.org/pep-0441/) + +## Ensuring a compatible interpreter is used + +The resulting zip file does *not* contain a Python interpreter. +Users are expected to execute the PEX with a compatible interpreter on the runtime system. + +Use the `python_interpreter_constraints` to provide an error if a wrong interpreter tries to execute the PEX, for example: + +```starlark +py_pex_binary( + python_interpreter_constraints = [ + "CPython=={major}.{minor}.{patch}", + ] +) +``` +""" load("@rules_python//python:defs.bzl", "PyInfo") load("//py/private:py_semantics.bzl", _py_semantics = "semantics") @@ -52,10 +70,9 @@ def _map_srcs(f, workspace): return ["--distinfo={}".format(f.dirname)] return ["--dep={}".format(f.dirname)] + elif site_packages_i == -1: # If the path does not have a `site-packages` in it, then put it into # the standard runfiles tree. - - elif site_packages_i == -1: return ["--source={}={}".format(f.path, dest_path)] return [] @@ -133,16 +150,6 @@ _attrs = dict({ Python interpreter versions this PEX binary is compatible with. A list of semver strings. The placeholder strings `{major}`, `{minor}`, `{patch}` can be used for gathering version information from the hermetic python toolchain. - -For example, to enforce same interpreter version that Bazel uses, following can be used. - -```starlark -py_pex_binary - python_interpreter_constraints = [ - "CPython=={major}.{minor}.{patch}" - ] -) -``` """, ), # NB: this is read by _resolve_toolchain in py_semantics. diff --git a/py/private/py_venv.bzl b/py/private/py_venv.bzl index 5ccd7c63..234ccd57 100644 --- a/py/private/py_venv.bzl +++ b/py/private/py_venv.bzl @@ -1,4 +1,11 @@ -"""Implementation for the py_binary and py_test rules.""" +"""Create a Python virtualenv directory structure. + +Note that [py_binary](./py_binary.md#py_binary) and [py_test](./py_test.md#py_test) macros automatically provide `[name].venv` targets. +Using `py_venv` directly is only required for cases where those defaults do not apply. + +> [!NOTE] +> As an implementation detail, this currently uses