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.
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.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.
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 00000000..5b31d52a
Binary files /dev/null and b/py/tests/virtual/django/proj/db.sqlite3 differ
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