diff --git a/examples/virtual_deps/BUILD.bazel b/examples/virtual_deps/BUILD.bazel new file mode 100644 index 00000000..64918361 --- /dev/null +++ b/examples/virtual_deps/BUILD.bazel @@ -0,0 +1,65 @@ +load("@aspect_rules_py//py:defs.bzl", "py_binary", "py_library", "py_pytest_main", "py_test", "resolutions") +load("@pypi//:requirements.bzl", "requirement") + +py_library( + name = "greet", + imports = ["."], + srcs = ["greet.py"], + virtual_deps = ["cowsay"], +) + +# A library that looks like cowsay... but isn't! +py_library( + name = "cowsnake", + imports = ["cowsnake"], + srcs = ["cowsnake/cowsay.py"], + virtual_deps = ["snakesay"], +) + +py_binary( + name = "app", + srcs = ["main.py"], + resolutions = resolutions.from_requirements([ + "cowsay", + ], requirement), + deps = [ + ":greet", + ], + python_version = "3.8.12", +) + +# Here we swap out the cowsay module for our own implementation +py_binary( + name = "app_snake", + srcs = ["main.py"], + deps = [ + ":greet", + ], + resolutions = resolutions.from_requirements([ + "snakesay", + ], requirement).override({ + "cowsay": ":cowsnake", + }), + python_version = "3.8.12", +) + +py_pytest_main( + name = "__test__", +) + +py_test( + name = "pytest_test", + srcs = [ + "greet_test.py", + ":__test__", + ], + main = ":__test__.py", + package_collisions = "error", + resolutions = resolutions.from_requirements([ + "cowsay", + ], requirement), + deps = [ + requirement("pytest"), + ":greet", + ], +) diff --git a/examples/virtual_deps/README.md b/examples/virtual_deps/README.md new file mode 100644 index 00000000..70ed542e --- /dev/null +++ b/examples/virtual_deps/README.md @@ -0,0 +1,9 @@ +# virtual_deps + +The example shows how to use `virtual_deps` feature. + + - `greet` is a library that has a virtual dependency on `cowsay` + - `cowsnake` is a library that implements some of the `cowsay` API + - `app` is a binary that uses `greet` and resolves the `cowsay` virtual dependency + - `app_snake` is like `app`, but swaps out `cowsay` for `cowsnake`! + - `pytest_test` tests `greet` using a resolved `cowsay` diff --git a/examples/virtual_deps/cowsnake/cowsay.py b/examples/virtual_deps/cowsnake/cowsay.py new file mode 100644 index 00000000..16842cb9 --- /dev/null +++ b/examples/virtual_deps/cowsnake/cowsay.py @@ -0,0 +1,4 @@ +import snakesay + +def get_output_string(_, x): + return snakesay.snakesay(x) diff --git a/examples/virtual_deps/greet.py b/examples/virtual_deps/greet.py new file mode 100644 index 00000000..075ddda6 --- /dev/null +++ b/examples/virtual_deps/greet.py @@ -0,0 +1,4 @@ +import cowsay + +def greet(x): + return cowsay.get_output_string("cow", x) diff --git a/examples/virtual_deps/greet_test.py b/examples/virtual_deps/greet_test.py new file mode 100644 index 00000000..b53a9900 --- /dev/null +++ b/examples/virtual_deps/greet_test.py @@ -0,0 +1,6 @@ +import pytest +from greet import greet + +def test_greet_contains_input(): + input = "Hello Alice!" + assert input in greet(input), "" diff --git a/examples/virtual_deps/main.py b/examples/virtual_deps/main.py new file mode 100644 index 00000000..8305451b --- /dev/null +++ b/examples/virtual_deps/main.py @@ -0,0 +1,3 @@ +from examples.virtual_deps.greet import greet + +print(greet("Hello virtual_deps!")) diff --git a/py/defs.bzl b/py/defs.bzl index fa250767..334f673c 100644 --- a/py/defs.bzl +++ b/py/defs.bzl @@ -14,12 +14,12 @@ load("@rules_python//python:repositories.bzl", "py_repositories", "python_regist 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 + # 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. +# 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", @@ -116,9 +116,26 @@ 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](./py_binary.md), but produces a target that can be used with `bazel test`." +def py_test(name, srcs = [], main = None, **kwargs): + """Identical to [py_binary](./py_binary.md), but produces a target that can be used with `bazel test`. + + 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, + that is used as the main. + **kwargs: additional named parameters to `py_binary_rule`. + """ # Ensure that any other targets we write will be testonly like the py_test target kwargs["testonly"] = True - _py_binary_or_test(name = name, rule = _py_test, srcs = srcs, main = main, **kwargs) + + # For a clearer DX when updating resolutions, the resolutions dict is "string" -> "label", + # where the rule attribute is a label-keyed-dict, so reverse them here. + resolutions = kwargs.pop("resolutions", None) + if resolutions: + resolutions = resolutions.to_label_keyed_dict() + + _py_binary_or_test(name = name, rule = _py_test, srcs = srcs, main = main, resolutions = resolutions, **kwargs) diff --git a/py/private/virtual.bzl b/py/private/virtual.bzl index 48ff813d..a92926ed 100644 --- a/py/private/virtual.bzl +++ b/py/private/virtual.bzl @@ -60,7 +60,12 @@ def _make_resolution(name, requirement): requirement = requirement, ) +def _from_requirements(base, requirement_fn = lambda r: r): + if type(base) == "list": + base = { k: None for k in base } + return _make_resolutions(base, requirement_fn) + resolutions = struct( - from_requirements = _make_resolutions, + from_requirements = _from_requirements, empty = lambda: _make_resolutions({}), ) diff --git a/requirements.in b/requirements.in index 29cbd745..a9b735dc 100644 --- a/requirements.in +++ b/requirements.in @@ -3,6 +3,7 @@ colorama~=0.4.0 click pytest cowsay +snakesay ftfy==6.2.0 neptune==1.10.2 six diff --git a/requirements.txt b/requirements.txt index e6f9c903..f830c054 100644 --- a/requirements.txt +++ b/requirements.txt @@ -780,6 +780,10 @@ smmap==5.0.1 \ --hash=sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62 \ --hash=sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da # via gitdb +snakesay==0.10.3 \ + --hash=sha256:0a601a0c408deba05a20b11ba2f0db336b1915274601053ef8de3a6b354c60fc \ + --hash=sha256:6346aa7231b1970efc6fa8b3ea78bd015b3d5a7e33ba709c17e00bcc3328f93f + # via -r requirements.in sqlparse==0.5.1 \ --hash=sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4 \ --hash=sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e