From cb5fef783cbb441bc463a67b9554a253d5238365 Mon Sep 17 00:00:00 2001 From: Matt Mackay Date: Tue, 14 Nov 2023 14:39:37 -0500 Subject: [PATCH] feat: support 'virtual' dependencies (#201) Fixes https://github.com/aspect-build/rules_py/issues/213 --------- Co-authored-by: Alex Eagle --- .bazelrc | 4 + WORKSPACE | 8 +- docs/rules.md | 111 ++++++++++++++-- internal_deps.bzl | 23 +++- internal_python_deps.bzl | 9 +- py/defs.bzl | 119 ++++++++++++++++- py/private/providers.bzl | 8 ++ py/private/py_library.bzl | 44 ++++++- py/private/venv/BUILD.bazel | 1 + py/private/venv/venv.bzl | 25 +++- py/repositories.bzl | 6 +- py/tests/external-deps/expected_pathing | 8 +- py/tests/virtual/django/BUILD.bazel | 33 +++++ py/tests/virtual/django/proj/db.sqlite3 | Bin 0 -> 131072 bytes py/tests/virtual/django/proj/manage.py | 25 ++++ py/tests/virtual/django/proj/proj/__init__.py | 0 py/tests/virtual/django/proj/proj/asgi.py | 16 +++ py/tests/virtual/django/proj/proj/settings.py | 123 ++++++++++++++++++ py/tests/virtual/django/proj/proj/urls.py | 22 ++++ py/tests/virtual/django/proj/proj/wsgi.py | 16 +++ py/tests/virtual/django/requirements.in | 1 + py/tests/virtual/django/requirements.txt | 22 ++++ 22 files changed, 593 insertions(+), 31 deletions(-) create mode 100644 py/tests/virtual/django/BUILD.bazel create mode 100644 py/tests/virtual/django/proj/db.sqlite3 create mode 100755 py/tests/virtual/django/proj/manage.py create mode 100644 py/tests/virtual/django/proj/proj/__init__.py create mode 100644 py/tests/virtual/django/proj/proj/asgi.py create mode 100644 py/tests/virtual/django/proj/proj/settings.py create mode 100644 py/tests/virtual/django/proj/proj/urls.py create mode 100644 py/tests/virtual/django/proj/proj/wsgi.py create mode 100644 py/tests/virtual/django/requirements.in create mode 100644 py/tests/virtual/django/requirements.txt diff --git a/.bazelrc b/.bazelrc index f6b338c5..bbd97766 100644 --- a/.bazelrc +++ b/.bazelrc @@ -7,6 +7,10 @@ test --test_output=errors # Define value used by tests build --define=SOME_VAR=SOME_VALUE +# Use local rules_python +# Enable with --config=dev +common:dev --override_repository=rules_python=~/workspace/rules_python + # Load any settings specific to the current user. # .bazelrc.user should appear in .gitignore so that settings are not shared with team members # This needs to be last statement in this diff --git a/WORKSPACE b/WORKSPACE index b74a0b9e..3a2478de 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -14,13 +14,15 @@ rules_py_dependencies() # Load the Python toolchain for rules_docker register_toolchains("//:container_py_toolchain") -load("@rules_python//python:repositories.bzl", "python_register_toolchains") +load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains") python_register_toolchains( name = "python_toolchain", python_version = "3.9", ) +py_repositories() + ############################################ # Aspect gcc toolchain load("@aspect_gcc_toolchain//toolchain:repositories.bzl", "gcc_toolchain_dependencies") @@ -47,6 +49,10 @@ load("@pypi//:requirements.bzl", "install_deps") install_deps() +load("@django//:requirements.bzl", install_django_deps = "install_deps") + +install_django_deps() + ################################ # For running our own unit tests load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace") diff --git a/docs/rules.md b/docs/rules.md index 4e0a1f4b..bd061eb1 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -7,7 +7,7 @@ Public API re-exports ## py_binary_rule
-py_binary_rule(name, data, deps, env, imports, main, srcs)
+py_binary_rule(name, data, deps, env, imports, main, resolutions, srcs)
 
Run a Python program under Bazel. Most users should use the [py_binary macro](#py_binary) instead of loading this directly. @@ -23,15 +23,16 @@ Run a Python program under Bazel. Most users should use the [py_binary macro](#p | 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 | | +| resolutions | FIXME | Dictionary: Label -> String | optional | {} | | srcs | Python source files. | List of labels | optional | [] | - + -## py_library +## py_library_rule
-py_library(name, data, deps, imports, srcs)
+py_library_rule(name, data, deps, imports, resolutions, srcs, virtual_deps)
 
@@ -41,11 +42,13 @@ py_library(name, data< | Name | Description | Type | Mandatory | Default | | :------------- | :------------- | :------------- | :------------- | :------------- | -| name | A unique name for this target. | Name | required | | -| data | Runtime dependencies of the program.

The transitive closure of the 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 | [] | -| srcs | Python source files. | List of labels | optional | [] | +| name | A unique name for this target. | Name | required | | +| data | Runtime dependencies of the program.

The transitive closure of the 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 | [] | @@ -53,7 +56,7 @@ py_library(name, data< ## py_test_rule
-py_test_rule(name, data, deps, env, imports, main, srcs)
+py_test_rule(name, data, deps, env, imports, main, resolutions, srcs)
 
Run a Python program under Bazel. Most users should use the [py_test macro](#py_test) instead of loading this directly. @@ -69,6 +72,7 @@ Run a Python program under Bazel. Most users should use the [py_test macro](#py_ | 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 | | +| resolutions | FIXME | Dictionary: Label -> String | optional | {} | | srcs | Python source files. | List of labels | optional | [] | @@ -77,7 +81,7 @@ Run a Python program under Bazel. Most users should use the [py_test macro](#py_ ## py_venv
-py_venv(name, data, deps, imports, srcs, strip_pth_workspace_root)
+py_venv(name, data, deps, imports, resolutions, srcs, strip_pth_workspace_root)
 
@@ -91,6 +95,7 @@ py_venv(name, data, data | Runtime dependencies of the program.

The transitive closure of the 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 | [] | | strip_pth_workspace_root | - | Boolean | optional | True | @@ -114,12 +119,53 @@ py_wheel(name, src) | src | The Wheel file, as defined by https://packaging.python.org/en/latest/specifications/binary-distribution-format/#binary-distribution-format | Label | optional | None | + + +## dep + +
+dep(name, virtual, constraint, prefix, default, from_label)
+
+ +Creates a Python dependency reference from the libraries name. + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| name | Name of the dependency to include | none | +| virtual | If true, the dependency is considered "virtual", and the terminal py_* rule must provide a concrete dependency label | False | +| constraint | If the dependency is considered virtual, provide an optional constraint over the version range that the virtual dependency can be satisfied by. | None | +| prefix | The dependency label prefix, defaults to "pypi" | "pypi" | +| default | Default target that will provide this dependency if none is provided at the terminal rule. | None | +| from_label | When given in conjunction with name, maps the name to a concrete dependency label, can be used to override the default resolved via this helper. | None | + + + + +## make_dep_helper + +
+make_dep_helper(prefix)
+
+ +Returns a function that assists in making dependency references when using virtual dependencies. + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| prefix | The prefix to attach to all dependency labels, representing the external repository that the external dependency is defined in. | "pypi" | + + ## py_binary
-py_binary(name, srcs, main, imports, kwargs)
+py_binary(name, srcs, main, imports, resolutions, kwargs)
 
Wrapper macro for [`py_binary_rule`](#py_binary_rule), setting a default for imports. @@ -137,9 +183,31 @@ you can `bazel run [name].venv` to produce this, then use it in the editor. | srcs | python source files | [] | | main | the entry point. If absent, then the first entry in srcs is used. | None | | imports | List of import paths to add for this binary. | ["."] | +| resolutions | FIXME | {} | | kwargs | additional named parameters to the py_binary_rule | none | + + +## py_library + +
+py_library(name, imports, deps, kwargs)
+
+ +Wrapper macro for the [py_library_rule](./py_library_rule), supporting virtual deps. + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| name | name of resulting py_library_rule | none | +| imports | List of import paths to add for this library. | ["."] | +| deps | Dependencies for this Python library. | [] | +| kwargs | additional named parameters to py_library_rule | none | + + ## py_pytest_main @@ -185,3 +253,22 @@ Identical to py_binary, but produces a target that can be used with `bazel test` | kwargs |

-

| none | + + +## resolutions + +
+resolutions(base, overrides)
+
+ + + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| base |

-

| none | +| overrides |

-

| {} | + + diff --git a/internal_deps.bzl b/internal_deps.bzl index 73a6d517..b70947f2 100644 --- a/internal_deps.bzl +++ b/internal_deps.bzl @@ -4,7 +4,7 @@ Users should *not* need to install these. If users see a load() statement from these, that's a bug in our distribution. """ -load("@bazel_tools//tools/build_defs/repo:http.bzl", _http_archive = "http_archive") +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file", _http_archive = "http_archive") load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") def http_archive(name, **kwargs): @@ -90,3 +90,24 @@ def rules_py_internal_deps(): strip_prefix = "rules_python-0.18.0/gazelle", url = "https://github.com/bazelbuild/rules_python/releases/download/0.18.0/rules_python-0.18.0.tar.gz", ) + + http_file( + name = "django_4_2_4", + urls = ["https://files.pythonhosted.org/packages/7f/9e/fc6bab255ae10bc57fa2f65646eace3d5405fbb7f5678b90140052d1db0f/Django-4.2.4-py3-none-any.whl"], + sha256 = "860ae6a138a238fc4f22c99b52f3ead982bb4b1aad8c0122bcd8c8a3a02e409d", + downloaded_file_path = "Django-4.2.4-py3-none-any.whl", + ) + + http_file( + name = "django_4_1_10", + urls = ["https://files.pythonhosted.org/packages/34/25/8a218de57fc9853297a1a8e4927688eff8107d5bc6dcf6c964c59801f036/Django-4.1.10-py3-none-any.whl"], + sha256 = "26d0260c2fb8121009e62ffc548b2398dea2522b6454208a852fb0ef264c206c", + downloaded_file_path = "Django-4.1.10-py3-none-any.whl", + ) + + http_file( + name = "sqlparse_0_4_0", + urls = ["https://files.pythonhosted.org/packages/10/96/36c136013c4a6ecb8c6aa3eed66e6dcea838f85fd80e1446499f1dabfac7/sqlparse-0.4.0-py3-none-any.whl"], + sha256 = "0523026398aea9c8b5f7a4a6d5c0829c285b4fbd960c17b5967a369342e21e01", + downloaded_file_path = "sqlparse-0.4.0-py3-none-any.whl", + ) diff --git a/internal_python_deps.bzl b/internal_python_deps.bzl index d84440d9..dc91f2a0 100644 --- a/internal_python_deps.bzl +++ b/internal_python_deps.bzl @@ -16,12 +16,13 @@ py_wheel( ) """ +# buildifier: disable=function-docstring def rules_py_internal_pypi_deps(interpreter): # Here we can see an example of annotations being applied to an arbitrary # package. For details on `package_annotation` and it's uses, see the # docs at @rules_python//docs:pip.md`. - PACKAGES = ["django", "colorama", "django"] + PACKAGES = ["django", "colorama"] ANNOTATIONS = { pkg: package_annotation(additive_build_content = PY_WHEEL_RULE_CONTENT) for pkg in PACKAGES @@ -33,3 +34,9 @@ def rules_py_internal_pypi_deps(interpreter): python_interpreter_target = interpreter, requirements_lock = "//:requirements.txt", ) + + pip_parse( + name = "django", + python_interpreter_target = interpreter, + requirements_lock = "//py/tests/virtual/django:requirements.txt", + ) diff --git a/py/defs.bzl b/py/defs.bzl index 39c436eb..d3dc6244 100644 --- a/py/defs.bzl +++ b/py/defs.bzl @@ -6,13 +6,16 @@ load("//py/private:py_pytest_main.bzl", _py_pytest_main = "py_pytest_main") load("//py/private:py_wheel.bzl", "py_wheel_lib") load("//py/private/venv:venv.bzl", _py_venv = "py_venv") -py_library = _py_library py_pytest_main = _py_pytest_main py_venv = _py_venv py_binary_rule = _py_binary py_test_rule = _py_test +py_library_rule = _py_library -def _py_binary_or_test(name, rule, srcs, main, imports, **kwargs): +_a_struct_type = type(struct()) +_a_string_type = type("") + +def _py_binary_or_test(name, rule, srcs, main, imports, deps = [], resolutions = {}, **kwargs): if not main and not len(srcs): fail("When 'main' is not specified, 'srcs' must be non-empty") rule( @@ -20,18 +23,21 @@ def _py_binary_or_test(name, rule, srcs, main, imports, **kwargs): srcs = srcs, main = main if main != None else srcs[0], imports = imports, + deps = deps, + resolutions = resolutions, **kwargs ) _py_venv( name = "%s.venv" % name, - deps = kwargs.pop("deps", []), + deps = deps, imports = imports, srcs = srcs, + resolutions = resolutions, tags = ["manual"], ) -def py_binary(name, srcs = [], main = None, imports = ["."], **kwargs): +def py_binary(name, srcs = [], main = None, imports = ["."], resolutions = {}, **kwargs): """Wrapper macro for [`py_binary_rule`](#py_binary_rule), setting a default for imports. It also creates a virtualenv to constrain the interpreter and packages used at runtime, @@ -42,9 +48,33 @@ def py_binary(name, srcs = [], main = None, imports = ["."], **kwargs): srcs: python source files main: the entry point. If absent, then the first entry in srcs is used. imports: List of import paths to add for this binary. + resolutions: FIXME **kwargs: additional named parameters to the py_binary_rule """ - _py_binary_or_test(name = name, rule = _py_binary, srcs = srcs, main = main, imports = imports, **kwargs) + + deps = kwargs.pop("deps", []) + concrete = [] + + # For a clearer DX when updating resolutions, the resolutions dict is "string" -> "label", + # where the rule attribute is a label-typed-dict, so reverse them here. + resolutions = {v: k for k, v in resolutions.items()} + + for dep in deps: + if type(dep) == _a_struct_type: + if dep.virtual: + fail("only non-virtual deps are allowed at a py_binary or py_test rule") + else: + # constraint here must be concrete, ie == or no specifier + resolutions.update([["@{}_{}//:wheel".format(dep.prefix, "foo"), dep.name]]) + elif type(dep) == _a_string_type: + concrete.append(dep) + else: + fail("dep element {} is of type {} but should be a struct or a string".format( + dep, + type(dep), + )) + + _py_binary_or_test(name = name, rule = _py_binary, srcs = srcs, main = main, imports = imports, resolutions = resolutions, deps = concrete, **kwargs) def py_test(name, main = None, srcs = [], imports = ["."], **kwargs): "Identical to py_binary, but produces a target that can be used with `bazel test`." @@ -55,3 +85,82 @@ py_wheel = rule( attrs = py_wheel_lib.attrs, provides = py_wheel_lib.provides, ) + +def resolutions(base, overrides = {}): + return dict(base, **overrides) + +def make_dep_helper(prefix = "pypi"): + """Returns a function that assists in making dependency references when using virtual dependencies. + + Args: + prefix: The prefix to attach to all dependency labels, representing the external repository that the external dependency is defined in. + """ + + return lambda name, **kwargs: dep(name, prefix = prefix, **kwargs) + +def dep(name, *, virtual = False, constraint = None, prefix = "pypi", default = None, from_label = None): + """Creates a Python dependency reference from the libraries name. + + Args: + name: Name of the dependency to include + virtual: If true, the dependency is considered "virtual", and the terminal py_* rule must provide a concrete dependency label + constraint: If the dependency is considered virtual, provide an optional constraint over the version range that the virtual dependency can be satisfied by. + prefix: The dependency label prefix, defaults to "pypi" + default: Default target that will provide this dependency if none is provided at the terminal rule. + from_label: When given in conjunction with name, maps the name to a concrete dependency label, can be used to override the default resolved via this helper. + """ + + return struct( + name = name, + virtual = virtual, + constraint = constraint, + prefix = prefix, + default = default, + from_label = from_label, + ) + +def py_library(name, imports = ["."], deps = [], **kwargs): + """Wrapper macro for the [py_library_rule](./py_library_rule), supporting virtual deps. + + Args: + name: name of resulting py_library_rule + imports: List of import paths to add for this library. + deps: Dependencies for this Python library. + **kwargs: additional named parameters to py_library_rule + """ + + concrete = [] + virtual = [] + + # Allow users to pass a list of virtual dependencies via the virtual attr. + virtual.extend(kwargs.pop("virtual_deps", [])) + + for dep in deps: + if type(dep) == _a_struct_type: + # { name: "requests", virtual = True | False, constraint = "" } + if dep.virtual: + # deal with constraint + virtual.append(dep.name) + else: + if dep.constraint: + fail("Illegal constraint on a non-virtual dependency") + if dep.from_label: + concrete.append(dep.from_label) + else: + # FIXME: looks like this may not work with bzlmod where the naming convention is different? + concrete.append("@{}_{}//:wheel".format(dep.prefix, dep.name)) + elif type(dep) == _a_string_type: + concrete.append(dep) + else: + fail("dep element {} is of type {} but should be a struct or a string".format( + dep, + type(dep), + )) + + py_library_rule( + name = name, + deps = concrete, + virtual_deps = virtual, + imports = imports, + **kwargs + ) diff --git a/py/private/providers.bzl b/py/private/providers.bzl index 671caeab..c3d7b5f5 100644 --- a/py/private/providers.bzl +++ b/py/private/providers.bzl @@ -7,3 +7,11 @@ PyWheelInfo = provider( "default_runfiles": "Runfiles of all files including deps for this wheel", }, ) + +PyVirtualInfo = provider( + doc = "FIXME", + fields = { + "dependencies": "Depset of required virtual dependencies, independant of their resolution status", + "resolutions": "FIXME", + }, +) diff --git a/py/private/py_library.bzl b/py/private/py_library.bzl index 62eab905..fa9451ac 100644 --- a/py/private/py_library.bzl +++ b/py/private/py_library.bzl @@ -1,7 +1,7 @@ "Implementation for the py_library rule" load("@bazel_skylib//lib:paths.bzl", "paths") -load("//py/private:providers.bzl", "PyWheelInfo") +load("//py/private:providers.bzl", "PyVirtualInfo", "PyWheelInfo") load("//py/private:py_wheel.bzl", py_wheel = "py_wheel_lib") def _make_instrumented_files_info(ctx, extra_source_attributes = [], extra_dependency_attributes = []): @@ -23,6 +23,31 @@ def _make_srcs_depset(ctx): ], ) +def _make_virtual_depset(ctx): + return depset( + order = "postorder", + direct = getattr(ctx.attr, "virtual", []), + transitive = [ + target[PyVirtualInfo].dependencies + for target in ctx.attr.deps + if PyVirtualInfo in target + ], + ) + +def _make_virtual_resolutions_depset(ctx): + return depset( + order = "postorder", + direct = [ + struct(virtual = v, target = k) + for k, v in ctx.attr.resolutions.items() + ], + transitive = [ + target[PyVirtualInfo].resolutions + for target in ctx.attr.deps + if PyVirtualInfo in target + ], + ) + def _make_import_path(label, workspace, base, imp): if imp.startswith("/"): fail( @@ -96,6 +121,8 @@ def _make_merged_runfiles(ctx, extra_depsets = [], extra_runfiles = [], extra_ru def _py_library_impl(ctx): transitive_srcs = _make_srcs_depset(ctx) imports = _make_imports_depset(ctx) + virtuals = _make_virtual_depset(ctx) + resolutions = _make_virtual_resolutions_depset(ctx) runfiles = _make_merged_runfiles(ctx) instrumented_files_info = _make_instrumented_files_info(ctx) py_wheel_info = py_wheel.make_py_wheel_info(ctx, ctx.attr.deps) @@ -112,6 +139,10 @@ def _py_library_impl(ctx): has_py3_only_sources = True, uses_shared_libraries = False, ), + PyVirtualInfo( + dependencies = virtuals, + resolutions = resolutions, + ), py_wheel_info, instrumented_files_info, ] @@ -124,7 +155,7 @@ _attrs = dict({ "deps": attr.label_list( doc = "Targets that produce Python code, commonly `py_library` rules.", allow_files = True, - providers = [[PyInfo], [PyWheelInfo]], + providers = [[PyInfo], [PyWheelInfo], [PyVirtualInfo]], ), "data": attr.label_list( doc = """Runtime dependencies of the program. @@ -138,6 +169,9 @@ _attrs = dict({ "imports": attr.string_list( doc = "List of import directories to be added to the PYTHONPATH.", ), + "resolutions": attr.label_keyed_string_dict( + doc = "FIXME", + ), }) _providers = [ @@ -153,11 +187,15 @@ py_library_utils = struct( make_instrumented_files_info = _make_instrumented_files_info, make_merged_runfiles = _make_merged_runfiles, make_srcs_depset = _make_srcs_depset, + make_virtual_depset = _make_virtual_depset, + make_virtual_resolutions_depset = _make_virtual_resolutions_depset, py_library_providers = _providers, ) py_library = rule( implementation = py_library_utils.implementation, - attrs = py_library_utils.attrs, + attrs = dict({ + "virtual_deps": attr.string_list(allow_empty = True, default = []), + }, **py_library_utils.attrs), provides = py_library_utils.py_library_providers, ) diff --git a/py/private/venv/BUILD.bazel b/py/private/venv/BUILD.bazel index 120e6618..da272328 100644 --- a/py/private/venv/BUILD.bazel +++ b/py/private/venv/BUILD.bazel @@ -11,5 +11,6 @@ bzl_library( "//py/private:py_library", "//py/private:utils", "@aspect_bazel_lib//lib:paths", + "@bazel_skylib//lib:sets", ], ) diff --git a/py/private/venv/venv.bzl b/py/private/venv/venv.bzl index 2c48f10b..756f7908 100644 --- a/py/private/venv/venv.bzl +++ b/py/private/venv/venv.bzl @@ -1,6 +1,7 @@ "Implementations for the py_venv rule." load("@aspect_bazel_lib//lib:paths.bzl", "BASH_RLOCATION_FUNCTION", "to_rlocation_path") +load("@bazel_skylib//lib:new_sets.bzl", "sets") load("//py/private:providers.bzl", "PyWheelInfo") load("//py/private:py_library.bzl", _py_library = "py_library_utils") load("//py/private:utils.bzl", "PY_TOOLCHAIN", "SH_TOOLCHAIN", "resolve_toolchain") @@ -33,8 +34,30 @@ def _make_venv(ctx, name = None, strip_pth_workspace_root = None): for target in ctx.attr.deps if PyWheelInfo in target ] + + virtual = _py_library.make_virtual_depset(ctx).to_list() + resolutions = _py_library.make_virtual_resolutions_depset(ctx).to_list() + + # Check for duplicate virtual dependency names. Those that map to the same resolution target would have been merged by the depset for us. + seen = {} + wheels = [] + for i, resolution in enumerate(resolutions): + if resolution.virtual in seen: + conflicts_with = resolutions[seen[resolution.virtual]].target + fail("Conflict in virtual dependency resolutions while resolving '{}'. Dependency is resolved by {} and {}".format(resolution.virtual, str(resolution.target), str(conflicts_with))) + seen.update([[resolution.virtual, i]]) + wheels.append(resolution.target[DefaultInfo].files) + + if PyWheelInfo in resolution.target: + wheels.append(resolution.target[PyWheelInfo].files) + + missing = sets.to_list(sets.difference(sets.make(virtual), sets.make(seen.keys()))) + if len(missing) > 0: + fail("The following dependencies were marked as virtual, but no concrete label providing them was given: {}".format(", ".join(missing))) + wheels_depset = depset( - transitive = wheels_depsets, + direct = ctx.files.resolutions, + transitive = wheels_depsets + wheels, ) # To avoid calling to_list, and then either creating a lot of extra symlinks or adding a large number diff --git a/py/repositories.bzl b/py/repositories.bzl index 7e44e815..438d4b6b 100644 --- a/py/repositories.bzl +++ b/py/repositories.bzl @@ -38,7 +38,7 @@ def rules_py_dependencies(): http_archive( name = "rules_python", - sha256 = "5868e73107a8e85d8f323806e60cad7283f34b32163ea6ff1020cf27abef6036", - strip_prefix = "rules_python-0.25.0", - url = "https://github.com/bazelbuild/rules_python/archive/refs/tags/0.25.0.tar.gz", + sha256 = "cff4c0ac0873ce089557b72828f34b82e67f35e9accfe414b5c3230907104a87", + strip_prefix = "rules_python-9facc3e3341f156377c61afbaa1dfb79a3843b78", + url = "https://github.com/bazelbuild/rules_python/archive/9facc3e3341f156377c61afbaa1dfb79a3843b78.tar.gz", ) diff --git a/py/tests/external-deps/expected_pathing b/py/tests/external-deps/expected_pathing index 063c2552..8c1ab0a3 100644 --- a/py/tests/external-deps/expected_pathing +++ b/py/tests/external-deps/expected_pathing @@ -1,7 +1,7 @@ Python: (pwd)/bazel-out/[exec]/bin/py/tests/external-deps/pathing.runfiles/pathing.venv/bin/python -version: 3.9.17 (main, REDACTED) -[Clang 16.0.3 ] -version info: sys.version_info(major=3, minor=9, micro=17, releaselevel='final', serial=0) +version: 3.9.18 (main, REDACTED) +[Clang 17.0.1 ] +version info: sys.version_info(major=3, minor=9, micro=18, releaselevel='final', serial=0) cwd: (pwd) site-packages folder: ['(pwd)/bazel-out/[exec]/bin/py/tests/external-deps/pathing.runfiles/pathing.venv/lib/python3.9/site-packages'] @@ -11,8 +11,8 @@ sys path: (py_toolchain)/lib/python3.9/lib-dynload (pwd)/bazel-out/[exec]/bin/py/tests/external-deps/pathing.runfiles/pathing.venv/lib/python3.9/site-packages (pwd)/bazel-out/[exec]/bin/py/tests/external-deps/pathing.runfiles -(pwd)/bazel-out/[exec]/bin/py/tests/external-deps/pathing.runfiles/aspect_rules_py (pwd)/bazel-out/[exec]/bin/py/tests/external-deps/pathing.runfiles/aspect_rules_py/py/tests/external-deps +(pwd)/bazel-out/[exec]/bin/py/tests/external-deps/pathing.runfiles/aspect_rules_py Entrypoint Path: (pwd)/bazel-out/[exec]/bin/py/tests/external-deps/pathing.runfiles/aspect_rules_py/py/tests/external-deps/pathing.py diff --git a/py/tests/virtual/django/BUILD.bazel b/py/tests/virtual/django/BUILD.bazel new file mode 100644 index 00000000..a10ea011 --- /dev/null +++ b/py/tests/virtual/django/BUILD.bazel @@ -0,0 +1,33 @@ +load("//py:defs.bzl", "py_binary", "py_library", "resolutions") +load("@rules_python//python/pip_install:requirements.bzl", "compile_pip_requirements") +load("@django//:requirements.bzl", django_resolutions = "all_whl_requirements_by_package") + +compile_pip_requirements( + name = "requirements", + requirements_in = "requirements.in", + requirements_txt = "requirements.txt", +) + +py_library( + name = "proj", + srcs = glob(["proj/**/*.py"]), + imports = ["./proj"], + # Depend on django, but not at a particular version, any binary/test rules that + # depend on this (directly or transitively) will need to resolve it to a version + # of their choosing. + virtual_deps = ["django"], +) + +py_binary( + name = "manage", + srcs = [ + "proj/manage.py", + ], + resolutions = resolutions( + # Use the wheels that the pip_parse rule defined as defaults... + django_resolutions, + # ...but replace the resolution of django with a specific wheel fetched by http_file. + {"django": "@django_4_2_4//file"}, + ), + deps = [":proj"], +) diff --git a/py/tests/virtual/django/proj/db.sqlite3 b/py/tests/virtual/django/proj/db.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..5b31d52ac4393058571c0a32541b6f5793e1f2aa GIT binary patch literal 131072 zcmeI5eQX=&eaCqsMT(ZllO-$XHF12R?Zm7s>&WAa=qzg~nYQH@Dwd_h3Km?C>wMldNR9dLQyYQ*e&EFLcZZ)F{v-dpzVG;d z&HLrSb%~HQQz0vie#QeN4r6gTbW!$U=KY#Rn^bfymP}}|9IG0`j(4aNBUS0=#iZM?rtQa;fylfS@Co;) zxLw|;5+h5`0rJ&?R#D5EB1P1koQ$hRZ5X+N{j0$+I-u(VmxRSA_rS*O%~ zEjvQ?w9v|Gv1A-688?nZ@>8Krb#1LzBuP>8&Eh?x(ypJDSlOr+D;h!6Q~Yq`h*!v* z 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 WB27nRF_OFxCI1rVld=?-QvVARyGEP< literal 0 HcmV?d00001 diff --git a/py/tests/virtual/django/proj/manage.py b/py/tests/virtual/django/proj/manage.py new file mode 100755 index 00000000..51fe0696 --- /dev/null +++ b/py/tests/virtual/django/proj/manage.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'proj.settings') + try: + from django.core.management import execute_from_command_line + from django import __version__ + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + + print(f"Django Version: {__version__}") + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/py/tests/virtual/django/proj/proj/__init__.py b/py/tests/virtual/django/proj/proj/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/py/tests/virtual/django/proj/proj/asgi.py b/py/tests/virtual/django/proj/proj/asgi.py new file mode 100644 index 00000000..1ed53324 --- /dev/null +++ b/py/tests/virtual/django/proj/proj/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for proj project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'proj.settings') + +application = get_asgi_application() diff --git a/py/tests/virtual/django/proj/proj/settings.py b/py/tests/virtual/django/proj/proj/settings.py new file mode 100644 index 00000000..96b90c04 --- /dev/null +++ b/py/tests/virtual/django/proj/proj/settings.py @@ -0,0 +1,123 @@ +""" +Django settings for proj project. + +Generated by 'django-admin startproject' using Django 4.2.5. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-g4ti9q90cpz3a58)9mz%@s3w)!-vm1g227yw)-7qjr-!f$*ame' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'proj.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'proj.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/py/tests/virtual/django/proj/proj/urls.py b/py/tests/virtual/django/proj/proj/urls.py new file mode 100644 index 00000000..74d04b02 --- /dev/null +++ b/py/tests/virtual/django/proj/proj/urls.py @@ -0,0 +1,22 @@ +""" +URL configuration for proj project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('admin/', admin.site.urls), +] diff --git a/py/tests/virtual/django/proj/proj/wsgi.py b/py/tests/virtual/django/proj/proj/wsgi.py new file mode 100644 index 00000000..edac8003 --- /dev/null +++ b/py/tests/virtual/django/proj/proj/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for proj project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'proj.settings') + +application = get_wsgi_application() diff --git a/py/tests/virtual/django/requirements.in b/py/tests/virtual/django/requirements.in new file mode 100644 index 00000000..3852fd97 --- /dev/null +++ b/py/tests/virtual/django/requirements.in @@ -0,0 +1 @@ +django==4.2.0 \ No newline at end of file diff --git a/py/tests/virtual/django/requirements.txt b/py/tests/virtual/django/requirements.txt new file mode 100644 index 00000000..310a55d9 --- /dev/null +++ b/py/tests/virtual/django/requirements.txt @@ -0,0 +1,22 @@ +# +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: +# +# bazel run //py/tests/virtual/django:requirements.update +# +asgiref==3.7.2 \ + --hash=sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e \ + --hash=sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed + # via django +django==4.2 \ + --hash=sha256:ad33ed68db9398f5dfb33282704925bce044bef4261cd4fb59e4e7f9ae505a78 \ + --hash=sha256:c36e2ab12824e2ac36afa8b2515a70c53c7742f0d6eaefa7311ec379558db997 + # via -r py/tests/virtual/django/requirements.in +sqlparse==0.4.4 \ + --hash=sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3 \ + --hash=sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c + # via django +typing-extensions==4.8.0 \ + --hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \ + --hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef + # via asgiref