From 217e7246b4f9b7244e0fe75b1ef5ed7c44fbf716 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sun, 26 Jul 2026 15:13:45 +0900 Subject: [PATCH 01/13] feat(pypi): add whl_deps_library repo rule Summary: - Add a new repo rule to just read metadata.json - Add integration tests for the repository rules in `whl_library.bzl` file. - Make some of the arguments optional in the BUILD.bazel code generation. Split out of #3856 Work towards #2948 --- .bazelrc.deleted_packages | 1 + .../pypi/generate_whl_library_build_bazel.bzl | 32 ++- python/private/pypi/pep508_deps.bzl | 6 + python/private/pypi/whl_library.bzl | 90 ++++++- python/private/pypi/whl_library_targets.bzl | 249 ++++++++++-------- tests/integration/BUILD.bazel | 7 +- tests/integration/whl_library/.bazelrc | 2 + tests/integration/whl_library/BUILD.bazel | 47 ++++ tests/integration/whl_library/MODULE.bazel | 57 ++++ .../integration/whl_library/test_contents.py | 155 +++++++++++ ...generate_whl_library_build_bazel_tests.bzl | 36 ++- .../whl_library_targets_tests.bzl | 11 +- 12 files changed, 549 insertions(+), 144 deletions(-) create mode 100644 tests/integration/whl_library/.bazelrc create mode 100644 tests/integration/whl_library/BUILD.bazel create mode 100644 tests/integration/whl_library/MODULE.bazel create mode 100644 tests/integration/whl_library/test_contents.py diff --git a/.bazelrc.deleted_packages b/.bazelrc.deleted_packages index db42040b5c..9c4745c178 100644 --- a/.bazelrc.deleted_packages +++ b/.bazelrc.deleted_packages @@ -44,6 +44,7 @@ common --deleted_packages=tests/integration/toolchain_target_settings common --deleted_packages=tests/integration/unified_pypi common --deleted_packages=tests/integration/uv_lock common --deleted_packages=tests/integration/validate_test_main +common --deleted_packages=tests/integration/whl_library common --deleted_packages=tests/modules/another_module common --deleted_packages=tests/modules/other common --deleted_packages=tests/modules/other/nspkg_delta diff --git a/python/private/pypi/generate_whl_library_build_bazel.bzl b/python/private/pypi/generate_whl_library_build_bazel.bzl index 71eab26c0e..47611a62b1 100644 --- a/python/private/pypi/generate_whl_library_build_bazel.bzl +++ b/python/private/pypi/generate_whl_library_build_bazel.bzl @@ -25,6 +25,7 @@ _RENDER = { "extras": render.list, "group_deps": render.list, "include": str, + "repo": lambda maybe_label: repr(str(maybe_label)), "requires_dist": render.list, "srcs_exclude": render.list, "tags": render.list, @@ -38,19 +39,22 @@ _TEMPLATE = """\ package(default_visibility = ["//visibility:public"]) +{fn}( +{kwargs} +) +""" + +_PURL = """\ package_metadata( name = "package_metadata", purl = {purl}, visibility = ["//:__subpackages__"], ) - -{fn}( -{kwargs} -) """ def generate_whl_library_build_bazel( *, + metadata_version, annotation = None, config_load, purl = None, @@ -59,6 +63,7 @@ def generate_whl_library_build_bazel( """Generate a BUILD file for an unzipped Wheel Args: + metadata_version: The version to use for tag generation. annotation: The annotation for the build file. config_load: {type}`str` The location from where to load the config. purl: The purl. @@ -74,14 +79,26 @@ def generate_whl_library_build_bazel( """load("@package_metadata//rules:package_metadata.bzl", "package_metadata")""", ] - fn = "whl_library_targets_from_requires" + if kwargs.get("repo"): + fn = "whl_library_deps_targets" + else: + fn = "whl_library_targets" + + tags = [ + "pypi_name={}".format(kwargs.get("metadata_name")), + "pypi_version={}".format(metadata_version), + ] + kwargs["tags"] = tags + if not requires_dist: # no deps, we can leave the extra loads out pass - else: + elif config_load: loads.append("""load("{}", "{}")""".format(config_load, "packages")) kwargs["include"] = "packages" kwargs["requires_dist"] = requires_dist + else: + kwargs["requires_dist"] = requires_dist loads.extend([ """load("@rules_python//python/private/pypi:whl_library_targets.bzl", "{}")""".format(fn), @@ -106,8 +123,9 @@ def generate_whl_library_build_bazel( "{} = {},".format(k, _RENDER.get(k, repr)(v)) for k, v in sorted(kwargs.items()) ])), - purl = repr(purl), ), + ] + [ + _PURL.format(purl = repr(purl)) if purl else "", ] + additional_content, ) diff --git a/python/private/pypi/pep508_deps.bzl b/python/private/pypi/pep508_deps.bzl index c004334d96..fd6d961bf5 100644 --- a/python/private/pypi/pep508_deps.bzl +++ b/python/private/pypi/pep508_deps.bzl @@ -44,6 +44,12 @@ def deps( * deps_select: {type}`dict[str, list[str]]` dependencies to include on particular subset of target platforms. """ + if not requires_dist: + return struct( + deps = [], + deps_select = {}, + ) + reqs = sorted( [requirement(r) for r in requires_dist], key = lambda x: "{}:{}:".format(x.name, sorted(x.extras), x.marker), diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 9a150db9e8..53edd62efb 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -332,6 +332,12 @@ def _whl_extract(rctx, *, whl_path, logger, sdist_filename = None): read_fn = rctx.read, logger = logger, ) + rctx.file("metadata.json", json.encode_indent({ + "name": metadata.name, + "provides_extra": metadata.provides_extra, + "requires_dist": metadata.requires_dist, + "version": metadata.version, + })) namespace_package_files = pypi_repo_utils.find_namespace_package_files(rctx, install_dir_path) entry_points = _get_entry_points(rctx, install_dir_path, metadata) @@ -429,6 +435,8 @@ def _whl_archive_impl(rctx): whl_path = rctx.path(filename) else: fail("Only wheels are supported") + else: + fail("Either 'whl_file' or 'urls' and 'filename' needs to be specified") return _whl_extract(rctx, whl_path = whl_path, logger = logger) @@ -571,9 +579,6 @@ For example if your whl depends on `numpy` and your Python package repo is named "index_url": attr.string( doc = "The index_url that the package will be downloaded from.", ), - "repo": attr.string( - doc = "Pointer to parent repo name. Used to make these rules rerun if the parent repo changes.", - ), "repo_prefix": attr.string( doc = """ Prefix for the generated packages will be of the form `@//...` @@ -676,7 +681,6 @@ whl_archive = repository_rule( "group_deps", "group_name", "index_url", - "repo", "repo_prefix", "requirement", "sha256", @@ -702,7 +706,74 @@ Does not depend on any python. ], ) -def whl_library(name, **kwargs): +def _whl_deps_library_impl(rctx): + logger = repo_utils.logger(rctx) + + if rctx.attr.metadata_file and rctx.attr.metadata: + logger.fail("Only one of 'metadata_file' and 'metadata' can be specified") + return + if not (rctx.attr.metadata_file or rctx.attr.metadata): + logger.fail("At least one of 'metadata_file' and 'metadata' must be specified") + return + + if rctx.attr.metadata_file: + metadata_contents = rctx.read(rctx.attr.metadata_file) + else: + metadata_contents = rctx.attr.metadata + + metadata = struct(**json.decode(metadata_contents)) + + build_file_contents = generate_whl_library_build_bazel( + dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format( + rctx.attr.repo_prefix, + ), + config_load = rctx.attr.config_load, + metadata_name = metadata.name, + metadata_version = metadata.version, + requires_dist = metadata.requires_dist, + group_deps = rctx.attr.group_deps, + group_name = rctx.attr.group_name, + repo = rctx.attr.repo or ( + str(rctx.attr.metadata_file) if rctx.attr.metadata_file else None + ), + extras = requirement(rctx.attr.requirement).extras, + ) + rctx.file("BUILD.bazel", build_file_contents) + +whl_deps_library = repository_rule( + attrs = { + k: _pip_archive_attrs[k] + for k in [ + "config_load", + "dep_template", + "group_deps", + "group_name", + "requirement", + ] + } | { + "metadata": attr.string( + doc = """ +The subset of the METADATA contents that is needed for generation of the dependencies. +* name: {type}`str` +* version: {type}`str` +* provides_extra: {type}`list[str]` +* requires_dist: {type}`list[str]` +""", + ), + "metadata_file": attr.label(doc = "An alternative way to pass {attr}`metadata` but as a file."), + "repo": attr.label(doc = "A label at the root of the repo to get stuff from."), + }, + doc = """ +A repo rule that reuses the sources from a different place and then creates the necessary targets +so that this can be used in the repo. + +Does not depend on any python. +""", + implementation = _whl_deps_library_impl, + environ = [REPO_DEBUG_ENV_VAR], +) + +def whl_library(name, repo = None, **kwargs): """Create a whl_library. This proxies to one of the underlying implementations: @@ -711,15 +782,18 @@ def whl_library(name, **kwargs): Args: name: {type}`str` The name of the repo. + repo: Unused, will be dropped in the next major release. **kwargs: The args passed to the underlying implementation. Returns: the repo metadata. """ + _ = repo # buildifier: disable=unused-variable + whl_file = kwargs.get("whl_file") urls = kwargs.get("urls", []) filename = kwargs.get("filename") if whl_file or (urls and filename and filename.endswith(".whl")): - return whl_archive(name = name, **kwargs) - - return pip_archive(name = name, **kwargs) + whl_archive(name = name, **kwargs) + else: + pip_archive(name = name, **kwargs) diff --git a/python/private/pypi/whl_library_targets.bzl b/python/private/pypi/whl_library_targets.bzl index 217210e782..18c84511df 100644 --- a/python/private/pypi/whl_library_targets.bzl +++ b/python/private/pypi/whl_library_targets.bzl @@ -49,11 +49,10 @@ _BAZEL_REPO_FILE_GLOBS = [ _IS_VENV_SITE_PACKAGES_YES = Label("//python/config_settings:_is_venvs_site_packages_yes") _VENV_SITE_PACKAGES_FLAG = Label("//python/config_settings:venvs_site_packages") -def whl_library_targets_from_requires( +def whl_library_targets( *, name, metadata_name = "", - metadata_version = "", requires_dist = [], extras = [], entry_points = {}, @@ -71,14 +70,12 @@ def whl_library_targets_from_requires( srcs_exclude = [], data = [], visibility = ["//visibility:public"], - tags = [], **kwargs): """The macro to create whl targets from the METADATA. Args: name: {type}`str` The wheel filename metadata_name: {type}`str` The package name as written in wheel `METADATA`. - metadata_version: {type}`str` The package version as written in wheel `METADATA`. group_deps: {type}`list[str]` names of fellow members of the group (if any). These will be excluded from generated deps lists so as to avoid direct cycles. These dependencies will be provided at runtime by the @@ -100,21 +97,14 @@ def whl_library_targets_from_requires( srcs_exclude: {type}`list[str]` The globs for srcs attribute exclusion. data: {type}`list[str]` A list of labels to include as part of the `data` attribute. visibility: {type}`list[str]` The visibility of the targets. - tags: {type}`list[str]` The tags set on the targets. - **kwargs: Extra args passed to the {obj}`whl_library_targets` and {obj}`whl_library_srcs`. + **kwargs: Extra args passed to the {obj}`whl_library_deps_targets` and {obj}`whl_library_srcs`. """ - pypi_tags = [ - "pypi_name={}".format(metadata_name), - "pypi_version={}".format(metadata_version), - ] - all_tags = sorted(tags + pypi_tags) - + create_extra_targets = bool(requires_dist or group_name) whl_library_srcs( name = name, sdist_filename = sdist_filename, data_exclude = data_exclude, srcs_exclude = srcs_exclude, - tags = all_tags, filegroups = filegroups, entry_points = entry_points, visibility = visibility, @@ -123,26 +113,27 @@ def whl_library_targets_from_requires( copy_executables = copy_executables, enable_implicit_namespace_pkgs = enable_implicit_namespace_pkgs, namespace_package_files = namespace_package_files, + # If there are no dependencies, then let's create the targets with public labels. + # Note, we are not supporting grouping the packages in this case, but that is fine. + whl_name = WHEEL_FILE if create_extra_targets else WHEEL_FILE_PUBLIC_LABEL, + pkg_name = PY_SRCS_LABEL if create_extra_targets else PY_LIBRARY_PUBLIC_LABEL, **kwargs ) - package_deps = _parse_requires_dist( - name = metadata_name, - requires_dist = requires_dist, - excludes = group_deps, - extras = extras, - include = include, - ) - - whl_library_targets( - name = name, - dependencies = package_deps.deps, - dependencies_with_markers = package_deps.deps_select, - group_name = group_name, - dep_template = dep_template, - tags = all_tags, - **kwargs - ) + if create_extra_targets and dep_template: + whl_library_deps_targets( + name = name, + metadata_name = metadata_name, + requires_dist = requires_dist, + group_deps = group_deps, # only needed if requires_dist is present + extras = extras, # only needed if requires_dist is present + include = include, # only needed if requires_dist is present + group_name = group_name, # only needed if requires_dist is present + dep_template = dep_template, # only needed if requires_dist is present + repo = None, + aliases = {}, + **kwargs + ) def whl_library_srcs( *, @@ -153,13 +144,15 @@ def whl_library_srcs( tags = [], filegroups = None, entry_points = {}, - visibility = ["//visibility:public"], data = [], copy_files = {}, copy_executables = {}, native = native, enable_implicit_namespace_pkgs = False, namespace_package_files = [], + whl_name = WHEEL_FILE, + pkg_name = PY_SRCS_LABEL, + visibility = ["//visibility:public"], rules = struct( copy_file = copy_file, py_binary = py_binary, @@ -192,9 +185,11 @@ def whl_library_srcs( data: {type}`list[str]` A list of labels to include as part of the `data` attribute in `py_library`. enable_implicit_namespace_pkgs: {type}`boolean` generate __init__.py files for namespace pkgs. - native: {type}`native` The native struct for overriding in tests. namespace_package_files: {type}`list[str]` A list of labels of files whose directories are namespace packages. + whl_name: {type}`str` The label name to use for the wheel filegroup target. + pkg_name: {type}`str` The label name to use for the py_library target. + native: {type}`native` The native struct for overriding in tests. rules: {type}`struct` A struct with references to rules for creating targets. """ tags = sorted(tags) @@ -278,7 +273,7 @@ def whl_library_srcs( if hasattr(native, "filegroup"): native.filegroup( - name = WHEEL_FILE, + name = whl_name, srcs = [name], visibility = visibility, ) @@ -334,7 +329,10 @@ def whl_library_srcs( data = data + [DATA_LABEL] rules.py_library( - name = PY_SRCS_LABEL, + # TODO @aignas 2026-07-26: once the srcs and library repos are separated, we should + # remove `PY_SRCS_LABEL and just use PY_LIBRARY_PUBLIC_LABEL. That will improve the + # amount of labels we are creating. + name = pkg_name, srcs = srcs, pyi_srcs = pyi_srcs, data = data, @@ -347,29 +345,20 @@ def whl_library_srcs( namespace_package_files = namespace_package_files, ) -def _parse_requires_dist( +def whl_library_deps_targets( *, - name, + name = None, + repo, + aliases = None, + metadata_name, requires_dist, - excludes, - include, - extras): - return deps( - name = normalize_name(name), - requires_dist = requires_dist, - excludes = excludes, - include = include, - extras = extras, - ) - -def whl_library_targets( - *, - name, + extras, + include = [], + group_deps = [], + group_name = None, dep_template, tags = [], - dependencies = [], - dependencies_with_markers = {}, - group_name = "", + visibility = ["//visibility:public"], native = native, rules = struct( copy_file = copy_file, @@ -379,39 +368,41 @@ def whl_library_targets( venv_rewrite_shebang = venv_rewrite_shebang, env_marker_setting = env_marker_setting, create_inits = _create_inits, - ), - **_kwargs): + )): """Create all of the whl_library targets. Args: - name: {type}`str` The file to match for including it into the `whl` - filegroup. This may be also parsed to generate extra metadata. - dep_template: {type}`str` The dep_template to use for dependency - interpolation. - tags: {type}`list[str]` The tags set on the `py_library`. - dependencies: {type}`list[str]` A list of dependencies. - dependencies_with_markers: {type}`dict[str, str]` A marker to evaluate - in order for the dep to be included. - group_name: {type}`str` name of the dependency group (if any) which - contains this library. If set, this library will behave as a shim - to group implementation rules which will provide simultaneously - installed dependencies which would otherwise form a cycle. + name: {type}`str` The wheel filename + metadata_name: {type}`str` The package name as written in wheel `METADATA`. + group_deps: {type}`list[str]` names of fellow members of the group (if + any). These will be excluded from generated deps lists so as to avoid + direct cycles. These dependencies will be provided at runtime by the + group rules which wrap this library and its fellows together. + requires_dist: {type}`list[str]` The list of `Requires-Dist` values from + the whl `METADATA`. + extras: {type}`list[str]` The list of requested extras. This essentially includes extra transitive dependencies in the final targets depending on the wheel `METADATA`. + include: {type}`list[str]` The list of packages to include. + group_name: {type}`str | None` name of the dependency group (if any). + dep_template: {type}`str | None` The dep_template to use. + tags: {type}`list[str]` The tags set on the targets. + repo: {type}`str | Label | None` The BUILD.bazel label to the parent repo that has the + sources. If none, then will take the targets from the current dir. + aliases: {type}`dict[str, str] | None` The list of aliases to create in the parent repo. If None, will create + the default values. Empty list means no aliases. + visibility: {type}`list[str]` The visibility of the targets. native: {type}`native` The native struct for overriding in tests. rules: {type}`struct` A struct with references to rules for creating targets. - **_kwargs: ignored args that are not needed. """ - dependencies = sorted([normalize_name(d) for d in dependencies]) - tags = sorted(tags) + repo_label = Label(repo).same_package_label if repo else (lambda x: x) + if aliases == None: + aliases = { + EXTRACTED_WHEEL_FILES: repo_label(EXTRACTED_WHEEL_FILES), + DIST_INFO_LABEL: repo_label(DIST_INFO_LABEL), + DATA_LABEL: repo_label(DATA_LABEL), + } - _config_settings( - dependencies_with_markers = dependencies_with_markers, - rules = rules, - visibility = ["//visibility:private"], - ) - deps_conditional = { - d: "is_include_{}_true".format(d) - for d in dependencies_with_markers - } + # TODO @aignas 2026-07-26: add a test when the group_name is specified and the requires_dist is + # empty list. We should essentially include in the group with the least amount of targets. # If this library is a member of a group, its public label aliases need to # point to the group implementation rule not the implementation rules. We @@ -430,6 +421,10 @@ def whl_library_targets( "//:", "//_groups:", ) + aliases = aliases | { + PY_LIBRARY_PUBLIC_LABEL: label_tmpl.format(PY_LIBRARY_PUBLIC_LABEL), + WHEEL_FILE_PUBLIC_LABEL: label_tmpl.format(WHEEL_FILE_PUBLIC_LABEL), + } impl_vis = [dep_template.format( name = "_config", target = "__pkg__", @@ -438,37 +433,56 @@ def whl_library_targets( "//_groups:", )] - native.alias( - name = PY_LIBRARY_PUBLIC_LABEL, - actual = label_tmpl.format(PY_LIBRARY_PUBLIC_LABEL), - visibility = ["//visibility:public"], - ) - native.alias( - name = WHEEL_FILE_PUBLIC_LABEL, - actual = label_tmpl.format(WHEEL_FILE_PUBLIC_LABEL), - visibility = ["//visibility:public"], - ) py_library_label = PY_LIBRARY_IMPL_LABEL whl_file_label = WHEEL_FILE_IMPL_LABEL - elif group_name: - py_library_label = PY_LIBRARY_PUBLIC_LABEL - whl_file_label = WHEEL_FILE_PUBLIC_LABEL + py_library_label = PY_LIBRARY_IMPL_LABEL + whl_file_label = WHEEL_FILE_IMPL_LABEL impl_vis = [dep_template.format(name = "", target = "__subpackages__")] - else: py_library_label = PY_LIBRARY_PUBLIC_LABEL whl_file_label = WHEEL_FILE_PUBLIC_LABEL - impl_vis = ["//visibility:public"] + impl_vis = visibility + + if not requires_dist: + # If the package is in a group but has no deps, we will correctly alias the right + # thing. + aliases = { + py_library_label: repo_label(PY_LIBRARY_PUBLIC_LABEL), + whl_file_label: repo_label(WHEEL_FILE_PUBLIC_LABEL), + } + + for alias, actual in aliases.items(): + native.alias( + name = alias, + actual = actual, + visibility = visibility, + ) + + if not requires_dist: + # If there are extras, then they will be visible in requires_dist. + return + + package_deps = _parse_requires_dist( + name = metadata_name, + requires_dist = requires_dist, + excludes = group_deps, + extras = extras, + include = include, + ) + + _config_settings( + dependencies_with_markers = package_deps.deps_select, + rules = rules, + visibility = ["//visibility:private"], + ) if hasattr(native, "filegroup"): native.filegroup( name = whl_file_label, - data = [ - WHEEL_FILE, - ] + _deps( - deps = dependencies, - deps_conditional = deps_conditional, + data = _deps( + deps = [repo_label(WHEEL_FILE)], + package_deps = package_deps, tmpl = dep_template.format(name = "{}", target = WHEEL_FILE_PUBLIC_LABEL), ), visibility = impl_vis, @@ -477,23 +491,34 @@ def whl_library_targets( if hasattr(rules, "py_library"): rules.py_library( name = py_library_label, - srcs = [ - # We include as srcs to ensure that the (locations :pkg) works as expected. - PY_SRCS_LABEL, - ], - deps = [ + # We include as srcs to ensure that the (locations :pkg) works as expected. + srcs = [repo_label(PY_SRCS_LABEL)], + deps = _deps( # We include as deps, so that `PyInfo` and friends get propagated as deps. # not sure if just including it as `srcs` is enough. - PY_SRCS_LABEL, - ] + _deps( - deps = dependencies, - deps_conditional = deps_conditional, + deps = [repo_label(PY_SRCS_LABEL)], + package_deps = package_deps, tmpl = dep_template.format(name = "{}", target = PY_LIBRARY_PUBLIC_LABEL), ), tags = tags, visibility = impl_vis, ) +def _parse_requires_dist( + *, + name, + requires_dist, + excludes, + include, + extras): + return deps( + name = normalize_name(name), + requires_dist = requires_dist, + excludes = excludes, + include = include, + extras = extras, + ) + def _config_settings(dependencies_with_markers, rules, **kwargs): """Generate config settings for the targets. @@ -510,12 +535,12 @@ def _config_settings(dependencies_with_markers, rules, **kwargs): **kwargs ) -def _deps(deps, deps_conditional, tmpl): - deps = [tmpl.format(d) for d in sorted(deps)] +def _deps(deps, package_deps, tmpl): + deps = [] + deps + [tmpl.format(d) for d in sorted(package_deps.deps)] - for dep, setting in deps_conditional.items(): + for dep in package_deps.deps_select: deps = deps + select({ - ":{}".format(setting): [tmpl.format(dep)], + ":is_include_{}_true".format(dep): [tmpl.format(dep)], "//conditions:default": [], }) diff --git a/tests/integration/BUILD.bazel b/tests/integration/BUILD.bazel index abdb37be57..ad2cd650d4 100644 --- a/tests/integration/BUILD.bazel +++ b/tests/integration/BUILD.bazel @@ -129,12 +129,7 @@ rules_python_integration_test( ) rules_python_integration_test( - name = "uv_lock_test", - py_deps = [ - "@pypiserver//pypiserver", - ":uv_lock_pypi_server_lib", - ], - py_main = "uv_lock_test.py", + name = "pip_archive_sdist_test", ) py_library( diff --git a/tests/integration/whl_library/.bazelrc b/tests/integration/whl_library/.bazelrc new file mode 100644 index 0000000000..eee47a8f19 --- /dev/null +++ b/tests/integration/whl_library/.bazelrc @@ -0,0 +1,2 @@ +common --incompatible_default_to_explicit_init_py +test --test_output=errors diff --git a/tests/integration/whl_library/BUILD.bazel b/tests/integration/whl_library/BUILD.bazel new file mode 100644 index 0000000000..56ba61a7c7 --- /dev/null +++ b/tests/integration/whl_library/BUILD.bazel @@ -0,0 +1,47 @@ +load("@rules_python//python:py_test.bzl", "py_test") + +[ + alias( + name = "{}_pkg".format(pkg), + actual = "@rules_python//python:none", + ) + for pkg in [ + "charset_normalizer", + "certifi", + "idna", + "urllib3", + ] +] + +# Extract all deps +genquery( + name = "whl_target_deps", + expression = "deps(@whl_archive//:pkg)", + scope = ["@whl_archive//:pkg"], +) + +# Extract all deps +genquery( + name = "whl_deps_target_deps", + expression = "deps(@whl_deps_library//:pkg)", + scope = ["@whl_deps_library//:pkg"], +) + +py_test( + name = "test_contents", + srcs = ["test_contents.py"], + data = [ + ":whl_deps_target_deps", + ":whl_target_deps", + "@pip_archive//:srcs", + "@pip_sdist_archive//:srcs", + "@whl_archive//:srcs", + ], + env = { + "SDIST_SRC_FILES": "$(locations @pip_sdist_archive//:srcs)", + "SRC_FILES": "$(locations @pip_archive//:srcs)", + "WHL_DEPS": "$(location :whl_target_deps)", + "WHL_FILES": "$(locations @whl_archive//:srcs)", + "WHL_TARGET_DEPS": "$(location :whl_deps_target_deps)", + }, +) diff --git a/tests/integration/whl_library/MODULE.bazel b/tests/integration/whl_library/MODULE.bazel new file mode 100644 index 0000000000..39a579f79e --- /dev/null +++ b/tests/integration/whl_library/MODULE.bazel @@ -0,0 +1,57 @@ +module(name = "integration_test") + +bazel_dep(name = "package_metadata", version = "0.0.7") +bazel_dep(name = "rules_python", version = "0.0.0") +local_path_override( + module_name = "rules_python", + path = "../../..", +) + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain( + python_version = "3.14", +) +use_repo(python, pbs_host = "python_3_14_host") + +pip_archive = use_repo_rule("@rules_python//python/private/pypi:whl_library.bzl", "pip_archive") + +pip_archive( + name = "pip_sdist_archive", + filename = "requests-2.34.2.tar.gz", + python_interpreter_target = "@pbs_host//:python", + requirement = "requests", + # https://pypi.org/project/requests/#requests-2.34.2.tar.gz + sha256 = "f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", + urls = [ + "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", + ], +) + +pip_archive( + name = "pip_archive", + python_interpreter_target = "@pbs_host//:python", + requirement = "requests", +) + +whl_archive = use_repo_rule("@rules_python//python/private/pypi:whl_library.bzl", "whl_archive") + +whl_archive( + name = "whl_archive", + dep_template = "@integration_test//:{name}_{target}", + filename = "requests-2.34.2-py3-none-any.whl", + # https://pypi.org/project/requests/#requests-2.34.2-py3-none-any.whl + requirement = "requests", + sha256 = "2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", + urls = [ + "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", + ], +) + +whl_deps_library = use_repo_rule("@rules_python//python/private/pypi:whl_library.bzl", "whl_deps_library") + +whl_deps_library( + name = "whl_deps_library", + dep_template = "@integration_test//:{name}_{target}", + metadata_file = "@whl_archive//:metadata.json", + requirement = "requests", +) diff --git a/tests/integration/whl_library/test_contents.py b/tests/integration/whl_library/test_contents.py new file mode 100644 index 0000000000..82c227067f --- /dev/null +++ b/tests/integration/whl_library/test_contents.py @@ -0,0 +1,155 @@ +import os +import unittest +from pathlib import Path + + +class TestContents(unittest.TestCase): + maxDiff = None + + @staticmethod + def _get_files(env_var: str) -> list[str]: + return [ + f.partition("site-packages/")[-1] for f in os.environ[env_var].split(" ") + ] + + def test_sdist_srcs(self): + self.assertEqual( + self._get_files("SDIST_SRC_FILES"), + [ + "requests/__init__.py", + "requests/__version__.py", + "requests/_internal_utils.py", + "requests/_types.py", + "requests/adapters.py", + "requests/api.py", + "requests/auth.py", + "requests/certs.py", + "requests/compat.py", + "requests/cookies.py", + "requests/exceptions.py", + "requests/help.py", + "requests/hooks.py", + "requests/models.py", + "requests/packages.py", + "requests/sessions.py", + "requests/status_codes.py", + "requests/structures.py", + "requests/utils.py", + ], + ) + + def test_srcs(self): + self.assertEqual( + self._get_files("SRC_FILES"), + [ + "requests/__init__.py", + "requests/__version__.py", + "requests/_internal_utils.py", + "requests/_types.py", + "requests/adapters.py", + "requests/api.py", + "requests/auth.py", + "requests/certs.py", + "requests/compat.py", + "requests/cookies.py", + "requests/exceptions.py", + "requests/help.py", + "requests/hooks.py", + "requests/models.py", + "requests/packages.py", + "requests/sessions.py", + "requests/status_codes.py", + "requests/structures.py", + "requests/utils.py", + ], + ) + + def test_whl_srcs(self): + self.assertEqual( + self._get_files("WHL_FILES"), + [ + "requests/__init__.py", + "requests/__version__.py", + "requests/_internal_utils.py", + "requests/_types.py", + "requests/adapters.py", + "requests/api.py", + "requests/auth.py", + "requests/certs.py", + "requests/compat.py", + "requests/cookies.py", + "requests/exceptions.py", + "requests/help.py", + "requests/hooks.py", + "requests/models.py", + "requests/packages.py", + "requests/sessions.py", + "requests/status_codes.py", + "requests/structures.py", + "requests/utils.py", + ], + ) + + @staticmethod + def _read_file(env_var: str) -> list[str]: + return set(Path(os.environ[env_var]).read_text().splitlines()) + + def test_whl_deps_ar_the_same(self): + for var, main_dep in { + "WHL_DEPS": "@@+whl_archive+whl_archive//:pkg", + "WHL_TARGET_DEPS": "@@+whl_deps_library+whl_deps_library//:pkg", + }.items(): + self.assertEqual( + self._read_file(var), + { + main_dep, + "//:certifi_pkg", + "//:charset_normalizer_pkg", + "//:idna_pkg", + "//:urllib3_pkg", + "@@+whl_archive+whl_archive//:data", + "@@+whl_archive+whl_archive//:package_metadata", + "@@+whl_archive+whl_archive//:site-packages/requests-2.34.2.dist-info/INSTALLER", + "@@+whl_archive+whl_archive//:site-packages/requests-2.34.2.dist-info/METADATA", + "@@+whl_archive+whl_archive//:site-packages/requests-2.34.2.dist-info/RECORD", + "@@+whl_archive+whl_archive//:site-packages/requests-2.34.2.dist-info/WHEEL", + "@@+whl_archive+whl_archive//:site-packages/requests-2.34.2.dist-info/licenses/LICENSE", + "@@+whl_archive+whl_archive//:site-packages/requests-2.34.2.dist-info/licenses/NOTICE", + "@@+whl_archive+whl_archive//:site-packages/requests-2.34.2.dist-info/top_level.txt", + "@@+whl_archive+whl_archive//:site-packages/requests/__init__.py", + "@@+whl_archive+whl_archive//:site-packages/requests/__version__.py", + "@@+whl_archive+whl_archive//:site-packages/requests/_internal_utils.py", + "@@+whl_archive+whl_archive//:site-packages/requests/_types.py", + "@@+whl_archive+whl_archive//:site-packages/requests/adapters.py", + "@@+whl_archive+whl_archive//:site-packages/requests/api.py", + "@@+whl_archive+whl_archive//:site-packages/requests/auth.py", + "@@+whl_archive+whl_archive//:site-packages/requests/certs.py", + "@@+whl_archive+whl_archive//:site-packages/requests/compat.py", + "@@+whl_archive+whl_archive//:site-packages/requests/cookies.py", + "@@+whl_archive+whl_archive//:site-packages/requests/exceptions.py", + "@@+whl_archive+whl_archive//:site-packages/requests/help.py", + "@@+whl_archive+whl_archive//:site-packages/requests/hooks.py", + "@@+whl_archive+whl_archive//:site-packages/requests/models.py", + "@@+whl_archive+whl_archive//:site-packages/requests/packages.py", + "@@+whl_archive+whl_archive//:site-packages/requests/py.typed", + "@@+whl_archive+whl_archive//:site-packages/requests/sessions.py", + "@@+whl_archive+whl_archive//:site-packages/requests/status_codes.py", + "@@+whl_archive+whl_archive//:site-packages/requests/structures.py", + "@@+whl_archive+whl_archive//:site-packages/requests/utils.py", + "@@+whl_archive+whl_archive//:srcs", + "@@bazel_tools//tools/python:toolchain_type", + "@@rules_python+//python:exec_tools_toolchain_type", + "@@rules_python+//python:none", + "@@rules_python+//python:toolchain_type", + "@@rules_python+//python/config_settings:_is_venvs_site_packages_yes", + "@@rules_python+//python/config_settings:add_srcs_to_runfiles", + "@@rules_python+//python/config_settings:precompile", + "@@rules_python+//python/config_settings:precompile_source_retention", + "@@rules_python+//python/config_settings:venvs_site_packages", + "@@rules_python+//python/private:sentinel", + }, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl index 1fd99205b1..9979707e6f 100644 --- a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl +++ b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl @@ -23,7 +23,7 @@ def _test_all_workspace(env): want = """\ load("@package_metadata//rules:package_metadata.bzl", "package_metadata") load("@pypi//:config.bzl", "packages") -load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets_from_requires") +load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets") package(default_visibility = ["//visibility:public"]) @@ -33,7 +33,7 @@ package_metadata( visibility = ["//:__subpackages__"], ) -whl_library_targets_from_requires( +whl_library_targets( copy_executables = { "exec_src": "exec_dest", }, @@ -53,6 +53,7 @@ whl_library_targets_from_requires( ], group_name = "qux", include = packages, + metadata_name = "foo", name = "foo.whl", requires_dist = [ "foo", @@ -60,11 +61,18 @@ whl_library_targets_from_requires( "qux", ], srcs_exclude = ["srcs_exclude_all"], + tags = [ + "pypi_name=foo", + "pypi_version=0", + ], ) + # SOMETHING SPECIAL AT THE END """ actual = generate_whl_library_build_bazel( + metadata_version = "0", + metadata_name = "foo", dep_template = "@pypi//{name}:{target}", name = "foo.whl", requires_dist = ["foo", "bar-baz", "qux"], @@ -89,7 +97,7 @@ def _test_all(env): want = """\ load("@package_metadata//rules:package_metadata.bzl", "package_metadata") load("@pypi//:config.bzl", "packages") -load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets_from_requires") +load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets") package(default_visibility = ["//visibility:public"]) @@ -99,7 +107,7 @@ package_metadata( visibility = ["//:__subpackages__"], ) -whl_library_targets_from_requires( +whl_library_targets( copy_executables = { "exec_src": "exec_dest", }, @@ -119,6 +127,7 @@ whl_library_targets_from_requires( ], group_name = "qux", include = packages, + metadata_name = "foo", name = "foo.whl", requires_dist = [ "foo", @@ -126,11 +135,18 @@ whl_library_targets_from_requires( "qux", ], srcs_exclude = ["srcs_exclude_all"], + tags = [ + "pypi_name=foo", + "pypi_version=0", + ], ) + # SOMETHING SPECIAL AT THE END """ actual = generate_whl_library_build_bazel( + metadata_version = "0", + metadata_name = "foo", dep_template = "@pypi//{name}:{target}", name = "foo.whl", requires_dist = ["foo", "bar-baz", "qux"], @@ -155,7 +171,7 @@ def _test_all_with_loads(env): want = """\ load("@package_metadata//rules:package_metadata.bzl", "package_metadata") load("@pypi//:config.bzl", "packages") -load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets_from_requires") +load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets") package(default_visibility = ["//visibility:public"]) @@ -165,7 +181,7 @@ package_metadata( visibility = ["//:__subpackages__"], ) -whl_library_targets_from_requires( +whl_library_targets( copy_executables = { "exec_src": "exec_dest", }, @@ -185,6 +201,7 @@ whl_library_targets_from_requires( ], group_name = "qux", include = packages, + metadata_name = "foo", name = "foo.whl", requires_dist = [ "foo", @@ -192,11 +209,18 @@ whl_library_targets_from_requires( "qux", ], srcs_exclude = ["srcs_exclude_all"], + tags = [ + "pypi_name=foo", + "pypi_version=0", + ], ) + # SOMETHING SPECIAL AT THE END """ actual = generate_whl_library_build_bazel( + metadata_version = "0", + metadata_name = "foo", dep_template = "@pypi//{name}:{target}", name = "foo.whl", requires_dist = ["foo", "bar-baz", "qux"], diff --git a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl index 1f4aac1ba8..305fb02bdf 100644 --- a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl +++ b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl @@ -17,8 +17,8 @@ load("@rules_testing//lib:test_suite.bzl", "test_suite") load( "//python/private/pypi:whl_library_targets.bzl", + "whl_library_deps_targets", "whl_library_srcs", - "whl_library_targets_from_requires", ) # buildifier: disable=bzl-visibility load("//tests/support/mocks:mocks.bzl", "mocks") @@ -118,10 +118,9 @@ def _test_whl_and_library_deps_from_requires(env): m_glob.results.append(["site-packages/foo/DATA.txt"]) # data m_glob.results.append(["site-packages/foo/PYI.pyi"]) # pyi - whl_library_targets_from_requires( + whl_library_deps_targets( name = "foo-0-py3-none-any.whl", metadata_name = "Foo", - metadata_version = "0", dep_template = "@pypi//{name}:{target}", requires_dist = [ "foo", # this self-edge will be ignored @@ -130,11 +129,13 @@ def _test_whl_and_library_deps_from_requires(env): "booo", # this is effectively excluded due to the list below ], include = ["foo", "bar", "bar_baz"], - data_exclude = [], # Overrides for testing - filegroups = {}, + repo = None, + aliases = None, + extras = [], native = struct( filegroup = lambda **kwargs: filegroup_calls.append(kwargs), + alias = lambda **kwargs: None, config_setting = lambda **_: None, glob = m_glob.glob, ), From 3c48c3baf180b4278d9f5c73c775e072aaaf9d33 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:05:50 +0900 Subject: [PATCH 02/13] fix tests --- .../pypi/generate_whl_library_build_bazel.bzl | 11 +++++++---- .../generate_whl_library_build_bazel_tests.bzl | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/python/private/pypi/generate_whl_library_build_bazel.bzl b/python/private/pypi/generate_whl_library_build_bazel.bzl index 47611a62b1..a60bfa3a98 100644 --- a/python/private/pypi/generate_whl_library_build_bazel.bzl +++ b/python/private/pypi/generate_whl_library_build_bazel.bzl @@ -34,11 +34,13 @@ _RENDER = { # NOTE @aignas 2024-10-25: We have to keep this so that files in # this repository can be publicly visible without the need for # export_files -_TEMPLATE = """\ +_TEMPLATE_START = """\ {loads} package(default_visibility = ["//visibility:public"]) +""" +_TEMPLATE_END = """\ {fn}( {kwargs} ) @@ -116,16 +118,17 @@ def generate_whl_library_build_bazel( contents = "\n".join( [ - _TEMPLATE.format( + _TEMPLATE_START.format( loads = "\n".join(loads), + ), + _PURL.format(purl = repr(purl)), + _TEMPLATE_END.format( fn = fn, kwargs = render.indent("\n".join([ "{} = {},".format(k, _RENDER.get(k, repr)(v)) for k, v in sorted(kwargs.items()) ])), ), - ] + [ - _PURL.format(purl = repr(purl)) if purl else "", ] + additional_content, ) diff --git a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl index 9979707e6f..33a61323e8 100644 --- a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl +++ b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl @@ -67,6 +67,11 @@ whl_library_targets( ], ) +package_metadata( + name = "package_metadata", + purl = None, + visibility = ["//:__subpackages__"], +) # SOMETHING SPECIAL AT THE END """ @@ -141,6 +146,11 @@ whl_library_targets( ], ) +package_metadata( + name = "package_metadata", + purl = None, + visibility = ["//:__subpackages__"], +) # SOMETHING SPECIAL AT THE END """ @@ -215,6 +225,11 @@ whl_library_targets( ], ) +package_metadata( + name = "package_metadata", + purl = None, + visibility = ["//:__subpackages__"], +) # SOMETHING SPECIAL AT THE END """ From 2b4cd4841784f17bd889e379b6fafabc30f4893e Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:10:01 +0900 Subject: [PATCH 03/13] fixup --- python/private/pypi/generate_whl_library_build_bazel.bzl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python/private/pypi/generate_whl_library_build_bazel.bzl b/python/private/pypi/generate_whl_library_build_bazel.bzl index a60bfa3a98..7a38bcc194 100644 --- a/python/private/pypi/generate_whl_library_build_bazel.bzl +++ b/python/private/pypi/generate_whl_library_build_bazel.bzl @@ -121,7 +121,6 @@ def generate_whl_library_build_bazel( _TEMPLATE_START.format( loads = "\n".join(loads), ), - _PURL.format(purl = repr(purl)), _TEMPLATE_END.format( fn = fn, kwargs = render.indent("\n".join([ @@ -129,7 +128,7 @@ def generate_whl_library_build_bazel( for k, v in sorted(kwargs.items()) ])), ), - ] + additional_content, + ] + additional_content + ([_PURL.format(purl = repr(purl))] if purl else []), ) # NOTE: Ensure that we terminate with a new line From f9be042c8064189cbfc569bdb7d6d9d99a646f26 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:05:00 +0900 Subject: [PATCH 04/13] fix tests --- .../pypi/generate_whl_library_build_bazel.bzl | 2 +- ...generate_whl_library_build_bazel_tests.bzl | 27 ++------ .../whl_library_targets_tests.bzl | 63 +++---------------- 3 files changed, 16 insertions(+), 76 deletions(-) diff --git a/python/private/pypi/generate_whl_library_build_bazel.bzl b/python/private/pypi/generate_whl_library_build_bazel.bzl index 7a38bcc194..49070eb121 100644 --- a/python/private/pypi/generate_whl_library_build_bazel.bzl +++ b/python/private/pypi/generate_whl_library_build_bazel.bzl @@ -128,7 +128,7 @@ def generate_whl_library_build_bazel( for k, v in sorted(kwargs.items()) ])), ), - ] + additional_content + ([_PURL.format(purl = repr(purl))] if purl else []), + ] + ([_PURL.format(purl = repr(purl))] if purl else []) + additional_content, ) # NOTE: Ensure that we terminate with a new line diff --git a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl index 33a61323e8..bb03d9a589 100644 --- a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl +++ b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl @@ -27,12 +27,6 @@ load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_ package(default_visibility = ["//visibility:public"]) -package_metadata( - name = "package_metadata", - purl = None, - visibility = ["//:__subpackages__"], -) - whl_library_targets( copy_executables = { "exec_src": "exec_dest", @@ -69,7 +63,7 @@ whl_library_targets( package_metadata( name = "package_metadata", - purl = None, + purl = "foo", visibility = ["//:__subpackages__"], ) @@ -93,6 +87,7 @@ package_metadata( config_load = "@pypi//:config.bzl", group_name = "qux", group_deps = ["foo", "fox", "qux"], + purl = "foo", ) env.expect.that_str(actual.replace("@@", "@")).equals(want) @@ -106,12 +101,6 @@ load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_ package(default_visibility = ["//visibility:public"]) -package_metadata( - name = "package_metadata", - purl = None, - visibility = ["//:__subpackages__"], -) - whl_library_targets( copy_executables = { "exec_src": "exec_dest", @@ -148,7 +137,7 @@ whl_library_targets( package_metadata( name = "package_metadata", - purl = None, + purl = "foo", visibility = ["//:__subpackages__"], ) @@ -171,6 +160,7 @@ package_metadata( ), config_load = "@pypi//:config.bzl", group_name = "qux", + purl = "foo", group_deps = ["foo", "fox", "qux"], ) env.expect.that_str(actual.replace("@@", "@")).equals(want) @@ -185,12 +175,6 @@ load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_ package(default_visibility = ["//visibility:public"]) -package_metadata( - name = "package_metadata", - purl = None, - visibility = ["//:__subpackages__"], -) - whl_library_targets( copy_executables = { "exec_src": "exec_dest", @@ -227,7 +211,7 @@ whl_library_targets( package_metadata( name = "package_metadata", - purl = None, + purl = "foo", visibility = ["//:__subpackages__"], ) @@ -251,6 +235,7 @@ package_metadata( group_name = "qux", config_load = "@pypi//:config.bzl", group_deps = ["foo", "fox", "qux"], + purl = "foo", ) env.expect.that_str(actual.replace("@@", "@")).equals(want) diff --git a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl index 305fb02bdf..dcef1944f3 100644 --- a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl +++ b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl @@ -105,7 +105,7 @@ def _test_copy(env): _tests.append(_test_copy) -def _test_whl_and_library_deps_from_requires(env): +def _test_whl_library_deps_targets(env): filegroup_calls = [] py_library_calls = [] env_marker_setting_calls = [] @@ -148,80 +148,35 @@ def _test_whl_and_library_deps_from_requires(env): ) env.expect.that_collection(filegroup_calls).contains_exactly([ - { - "name": "whl_file", - "srcs": ["foo-0-py3-none-any.whl"], - "visibility": ["//visibility:public"], - }, { "name": "whl", # NOTE @aignas 2026-07-25: depending on the brackets position one may get different # results in the expectation. - "data": ["whl_file"] + (["@pypi//bar:whl"] + select({ + "data": ["whl_file", "@pypi//bar:whl"] + select({ ":is_include_bar_baz_true": ["@pypi//bar_baz:whl"], "//conditions:default": [], - })), + }), "visibility": ["//visibility:public"], }, ]) # buildifier: @unsorted-dict-items - env.expect.that_collection(py_library_calls).has_size(2) + env.expect.that_collection(py_library_calls).has_size(1) if len(py_library_calls) != 1: return py_library_call = py_library_calls[0] env.expect.that_dict(py_library_call).contains_exactly({ "name": "pkg", - "srcs": ["site-packages/foo/SRCS.py"] + select({ - Label("//python/config_settings:_is_venvs_site_packages_yes"): [], - "//conditions:default": ["_create_inits_target"], - }), - "pyi_srcs": ["site-packages/foo/PYI.pyi"], - "data": ["site-packages/foo/DATA.txt", "data"], - "imports": ["site-packages"], - "deps": ["@pypi//bar:pkg"] + select({ + "srcs": ["srcs"], + "deps": ["srcs", "@pypi//bar:pkg"] + select({ ":is_include_bar_baz_true": ["@pypi//bar_baz:pkg"], "//conditions:default": [], }), - "tags": ["pypi_name=Foo", "pypi_version=0"], + "tags": [], "visibility": ["//visibility:public"], - "experimental_venvs_site_packages": Label("//python/config_settings:venvs_site_packages"), - "namespace_package_files": [] + select({ - Label("//python/config_settings:_is_venvs_site_packages_yes"): [], - "//conditions:default": ["_create_inits_target"], - }), }) # buildifier: @unsorted-dict-items - env.expect.that_collection(m_glob.calls).contains_exactly([ - # bin call - mocks.glob_call( - ["bin/*"], - allow_empty = True, - ), - # rewrite-bin call - mocks.glob_call( - ["rewrite-bin/*"], - allow_empty = True, - ), - # srcs call - mocks.glob_call( - ["site-packages/**/*.py"], - exclude = [], - allow_empty = True, - ), - # data call - mocks.glob_call( - ["site-packages/**/*"], - exclude = [ - "**/*.py", - "**/*.pyc", - "**/*.pyc.*", - ], - allow_empty = True, - ), - # pyi call - mocks.glob_call(["site-packages/**/*.pyi"], allow_empty = True), - ]) + env.expect.that_collection(m_glob.calls).contains_exactly([]) env.expect.that_collection(env_marker_setting_calls).contains_exactly([ { @@ -231,7 +186,7 @@ def _test_whl_and_library_deps_from_requires(env): }, ]) # buildifier: @unsorted-dict-items -_tests.append(_test_whl_and_library_deps_from_requires) +_tests.append(_test_whl_library_deps_targets) def _test_sdist_excludes_record(env): py_library_calls = [] From c4640a6dc6abbb3994cff9fc7dfae1d36755ad98 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:09:59 +0900 Subject: [PATCH 05/13] remove warnings --- tests/integration/bzlmod_lockfile/MODULE.bazel | 4 ++-- .../MODULE.bazel | 2 +- tests/integration/pip_parse_isolated/MODULE.bazel | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/integration/bzlmod_lockfile/MODULE.bazel b/tests/integration/bzlmod_lockfile/MODULE.bazel index 6c074bcdd4..bfd43cb895 100644 --- a/tests/integration/bzlmod_lockfile/MODULE.bazel +++ b/tests/integration/bzlmod_lockfile/MODULE.bazel @@ -12,8 +12,8 @@ python.toolchain(python_version = "3.13") # TODO: This test module should also verify that isolate = True works, will do in a followup PR. pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") pip.parse( - hub_name = "pypi", + hub_name = "pypi_lockfile", python_version = "3.13", requirements_lock = "//:requirements_lock.txt", ) -use_repo(pip, "pypi") +use_repo(pip, pypi = "pypi_lockfile") diff --git a/tests/integration/compile_pip_requirements_test_from_external_repo/MODULE.bazel b/tests/integration/compile_pip_requirements_test_from_external_repo/MODULE.bazel index 596a0bcfc8..201cb4a1ab 100644 --- a/tests/integration/compile_pip_requirements_test_from_external_repo/MODULE.bazel +++ b/tests/integration/compile_pip_requirements_test_from_external_repo/MODULE.bazel @@ -19,7 +19,7 @@ local_path_override( pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") pip.parse( - hub_name = "pypi", + hub_name = "pypi_compile", python_version = "3.9", requirements_lock = "@compile_pip_requirements//:requirements_lock.txt", ) diff --git a/tests/integration/pip_parse_isolated/MODULE.bazel b/tests/integration/pip_parse_isolated/MODULE.bazel index 6c44257acb..c25e9dc024 100644 --- a/tests/integration/pip_parse_isolated/MODULE.bazel +++ b/tests/integration/pip_parse_isolated/MODULE.bazel @@ -12,8 +12,8 @@ python.toolchain(python_version = "3.13") # This test module verifies that dependencies can be used with `isolate = True`. pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip", isolate = True) pip.parse( - hub_name = "pypi", + hub_name = "pypi_isolated", python_version = "3.13", requirements_lock = "//:requirements_lock.txt", ) -use_repo(pip, "pypi") +use_repo(pip, pypi = "pypi_isolated") From 04b1f6d6d092d7c787d33fde6d8f0dd9f3aa83b5 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:18:10 +0900 Subject: [PATCH 06/13] fixup the name --- tests/integration/BUILD.bazel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/BUILD.bazel b/tests/integration/BUILD.bazel index ad2cd650d4..e56e37e75b 100644 --- a/tests/integration/BUILD.bazel +++ b/tests/integration/BUILD.bazel @@ -129,7 +129,7 @@ rules_python_integration_test( ) rules_python_integration_test( - name = "pip_archive_sdist_test", + name = "whl_library_test", ) py_library( From 8b9fee5ff9394a2603d24bd2cb6ff3e6838f59a7 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:30:50 +0900 Subject: [PATCH 07/13] fixup --- python/private/pypi/whl_library_targets.bzl | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/python/private/pypi/whl_library_targets.bzl b/python/private/pypi/whl_library_targets.bzl index 18c84511df..f0fc943439 100644 --- a/python/private/pypi/whl_library_targets.bzl +++ b/python/private/pypi/whl_library_targets.bzl @@ -115,8 +115,8 @@ def whl_library_targets( namespace_package_files = namespace_package_files, # If there are no dependencies, then let's create the targets with public labels. # Note, we are not supporting grouping the packages in this case, but that is fine. - whl_name = WHEEL_FILE if create_extra_targets else WHEEL_FILE_PUBLIC_LABEL, - pkg_name = PY_SRCS_LABEL if create_extra_targets else PY_LIBRARY_PUBLIC_LABEL, + whl_name = WHEEL_FILE if (create_extra_targets and dep_template) else WHEEL_FILE_PUBLIC_LABEL, + pkg_name = PY_SRCS_LABEL if (create_extra_targets and dep_template) else PY_LIBRARY_PUBLIC_LABEL, **kwargs ) @@ -435,19 +435,18 @@ def whl_library_deps_targets( py_library_label = PY_LIBRARY_IMPL_LABEL whl_file_label = WHEEL_FILE_IMPL_LABEL - elif group_name: - py_library_label = PY_LIBRARY_IMPL_LABEL - whl_file_label = WHEEL_FILE_IMPL_LABEL - impl_vis = [dep_template.format(name = "", target = "__subpackages__")] else: py_library_label = PY_LIBRARY_PUBLIC_LABEL whl_file_label = WHEEL_FILE_PUBLIC_LABEL - impl_vis = visibility + if group_name: + impl_vis = [dep_template.format(name = "", target = "__subpackages__")] + else: + impl_vis = visibility if not requires_dist: # If the package is in a group but has no deps, we will correctly alias the right # thing. - aliases = { + aliases = aliases | { py_library_label: repo_label(PY_LIBRARY_PUBLIC_LABEL), whl_file_label: repo_label(WHEEL_FILE_PUBLIC_LABEL), } From 3bd722cd290ed068928e34486ac36125bd8c5745 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:37:37 +0900 Subject: [PATCH 08/13] add an empty workspace file --- tests/integration/whl_library/WORKSPACE | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/integration/whl_library/WORKSPACE diff --git a/tests/integration/whl_library/WORKSPACE b/tests/integration/whl_library/WORKSPACE new file mode 100644 index 0000000000..e69de29bb2 From be4d89e25beb0f1242af5de65a21cffd34a1686e Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:45:58 +0900 Subject: [PATCH 09/13] revert unintended change --- tests/integration/BUILD.bazel | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/integration/BUILD.bazel b/tests/integration/BUILD.bazel index e56e37e75b..ef7681ebe3 100644 --- a/tests/integration/BUILD.bazel +++ b/tests/integration/BUILD.bazel @@ -128,6 +128,15 @@ rules_python_integration_test( py_main = "unified_pypi_test.py", ) +rules_python_integration_test( + name = "uv_lock_test", + py_deps = [ + "@pypiserver//pypiserver", + ":uv_lock_pypi_server_lib", + ], + py_main = "uv_lock_test.py", +) + rules_python_integration_test( name = "whl_library_test", ) From 5dcc33cea9a7cf80f57053fcf54494d6a37750b3 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 28 Jul 2026 19:11:12 +0000 Subject: [PATCH 10/13] test(pypi): normalize canonical repo names in whl_library integration tests To prevent test failures across Bazel 7, 8, and 9 where Bzlmod canonical repository prefixes vary, dynamically parse canonical repository strings by splitting on '~' and '+'. Also filter out version-dependent toolchain type targets from dependency comparisons. --- .../integration/whl_library/test_contents.py | 98 +++++++++++-------- 1 file changed, 55 insertions(+), 43 deletions(-) diff --git a/tests/integration/whl_library/test_contents.py b/tests/integration/whl_library/test_contents.py index 82c227067f..1ae8433c96 100644 --- a/tests/integration/whl_library/test_contents.py +++ b/tests/integration/whl_library/test_contents.py @@ -1,4 +1,5 @@ import os +import re import unittest from pathlib import Path @@ -94,59 +95,70 @@ def test_whl_srcs(self): def _read_file(env_var: str) -> list[str]: return set(Path(os.environ[env_var]).read_text().splitlines()) + @staticmethod + def _normalize_label(label: str) -> str: + if not label.startswith("@@"): + return label + repo, _, rest = label.partition("//") + parts = [p for p in re.split(r"[~+]", repo[2:]) if p] + if parts: + return f"@{parts[-1]}//{rest}" + return label + def test_whl_deps_ar_the_same(self): for var, main_dep in { - "WHL_DEPS": "@@+whl_archive+whl_archive//:pkg", - "WHL_TARGET_DEPS": "@@+whl_deps_library+whl_deps_library//:pkg", + "WHL_DEPS": "@whl_archive//:pkg", + "WHL_TARGET_DEPS": "@whl_deps_library//:pkg", }.items(): self.assertEqual( - self._read_file(var), + { + self._normalize_label(x) + for x in self._read_file(var) + if not x.endswith("toolchain_type") + }, { main_dep, "//:certifi_pkg", "//:charset_normalizer_pkg", "//:idna_pkg", "//:urllib3_pkg", - "@@+whl_archive+whl_archive//:data", - "@@+whl_archive+whl_archive//:package_metadata", - "@@+whl_archive+whl_archive//:site-packages/requests-2.34.2.dist-info/INSTALLER", - "@@+whl_archive+whl_archive//:site-packages/requests-2.34.2.dist-info/METADATA", - "@@+whl_archive+whl_archive//:site-packages/requests-2.34.2.dist-info/RECORD", - "@@+whl_archive+whl_archive//:site-packages/requests-2.34.2.dist-info/WHEEL", - "@@+whl_archive+whl_archive//:site-packages/requests-2.34.2.dist-info/licenses/LICENSE", - "@@+whl_archive+whl_archive//:site-packages/requests-2.34.2.dist-info/licenses/NOTICE", - "@@+whl_archive+whl_archive//:site-packages/requests-2.34.2.dist-info/top_level.txt", - "@@+whl_archive+whl_archive//:site-packages/requests/__init__.py", - "@@+whl_archive+whl_archive//:site-packages/requests/__version__.py", - "@@+whl_archive+whl_archive//:site-packages/requests/_internal_utils.py", - "@@+whl_archive+whl_archive//:site-packages/requests/_types.py", - "@@+whl_archive+whl_archive//:site-packages/requests/adapters.py", - "@@+whl_archive+whl_archive//:site-packages/requests/api.py", - "@@+whl_archive+whl_archive//:site-packages/requests/auth.py", - "@@+whl_archive+whl_archive//:site-packages/requests/certs.py", - "@@+whl_archive+whl_archive//:site-packages/requests/compat.py", - "@@+whl_archive+whl_archive//:site-packages/requests/cookies.py", - "@@+whl_archive+whl_archive//:site-packages/requests/exceptions.py", - "@@+whl_archive+whl_archive//:site-packages/requests/help.py", - "@@+whl_archive+whl_archive//:site-packages/requests/hooks.py", - "@@+whl_archive+whl_archive//:site-packages/requests/models.py", - "@@+whl_archive+whl_archive//:site-packages/requests/packages.py", - "@@+whl_archive+whl_archive//:site-packages/requests/py.typed", - "@@+whl_archive+whl_archive//:site-packages/requests/sessions.py", - "@@+whl_archive+whl_archive//:site-packages/requests/status_codes.py", - "@@+whl_archive+whl_archive//:site-packages/requests/structures.py", - "@@+whl_archive+whl_archive//:site-packages/requests/utils.py", - "@@+whl_archive+whl_archive//:srcs", - "@@bazel_tools//tools/python:toolchain_type", - "@@rules_python+//python:exec_tools_toolchain_type", - "@@rules_python+//python:none", - "@@rules_python+//python:toolchain_type", - "@@rules_python+//python/config_settings:_is_venvs_site_packages_yes", - "@@rules_python+//python/config_settings:add_srcs_to_runfiles", - "@@rules_python+//python/config_settings:precompile", - "@@rules_python+//python/config_settings:precompile_source_retention", - "@@rules_python+//python/config_settings:venvs_site_packages", - "@@rules_python+//python/private:sentinel", + "@whl_archive//:data", + "@whl_archive//:package_metadata", + "@whl_archive//:site-packages/requests-2.34.2.dist-info/INSTALLER", + "@whl_archive//:site-packages/requests-2.34.2.dist-info/METADATA", + "@whl_archive//:site-packages/requests-2.34.2.dist-info/RECORD", + "@whl_archive//:site-packages/requests-2.34.2.dist-info/WHEEL", + "@whl_archive//:site-packages/requests-2.34.2.dist-info/licenses/LICENSE", + "@whl_archive//:site-packages/requests-2.34.2.dist-info/licenses/NOTICE", + "@whl_archive//:site-packages/requests-2.34.2.dist-info/top_level.txt", + "@whl_archive//:site-packages/requests/__init__.py", + "@whl_archive//:site-packages/requests/__version__.py", + "@whl_archive//:site-packages/requests/_internal_utils.py", + "@whl_archive//:site-packages/requests/_types.py", + "@whl_archive//:site-packages/requests/adapters.py", + "@whl_archive//:site-packages/requests/api.py", + "@whl_archive//:site-packages/requests/auth.py", + "@whl_archive//:site-packages/requests/certs.py", + "@whl_archive//:site-packages/requests/compat.py", + "@whl_archive//:site-packages/requests/cookies.py", + "@whl_archive//:site-packages/requests/exceptions.py", + "@whl_archive//:site-packages/requests/help.py", + "@whl_archive//:site-packages/requests/hooks.py", + "@whl_archive//:site-packages/requests/models.py", + "@whl_archive//:site-packages/requests/packages.py", + "@whl_archive//:site-packages/requests/py.typed", + "@whl_archive//:site-packages/requests/sessions.py", + "@whl_archive//:site-packages/requests/status_codes.py", + "@whl_archive//:site-packages/requests/structures.py", + "@whl_archive//:site-packages/requests/utils.py", + "@whl_archive//:srcs", + "@rules_python//python:none", + "@rules_python//python/config_settings:_is_venvs_site_packages_yes", + "@rules_python//python/config_settings:add_srcs_to_runfiles", + "@rules_python//python/config_settings:precompile", + "@rules_python//python/config_settings:precompile_source_retention", + "@rules_python//python/config_settings:venvs_site_packages", + "@rules_python//python/private:sentinel", }, ) From aaa7e040090520f80612538eef1d328cdbcefe9c Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sat, 1 Aug 2026 08:47:33 +0900 Subject: [PATCH 11/13] cleanup the templating --- python/private/pypi/generate_whl_library_build_bazel.bzl | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/python/private/pypi/generate_whl_library_build_bazel.bzl b/python/private/pypi/generate_whl_library_build_bazel.bzl index 49070eb121..ceee319cd7 100644 --- a/python/private/pypi/generate_whl_library_build_bazel.bzl +++ b/python/private/pypi/generate_whl_library_build_bazel.bzl @@ -34,13 +34,11 @@ _RENDER = { # NOTE @aignas 2024-10-25: We have to keep this so that files in # this repository can be publicly visible without the need for # export_files -_TEMPLATE_START = """\ +_TEMPLATE = """\ {loads} package(default_visibility = ["//visibility:public"]) -""" -_TEMPLATE_END = """\ {fn}( {kwargs} ) @@ -118,10 +116,8 @@ def generate_whl_library_build_bazel( contents = "\n".join( [ - _TEMPLATE_START.format( + _TEMPLATE.format( loads = "\n".join(loads), - ), - _TEMPLATE_END.format( fn = fn, kwargs = render.indent("\n".join([ "{} = {},".format(k, _RENDER.get(k, repr)(v)) From cce0b68b1b9db9bacf33a02c793dc15ec45be2eb Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sat, 1 Aug 2026 09:04:57 +0900 Subject: [PATCH 12/13] fixup --- python/private/pypi/whl_library_targets.bzl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/python/private/pypi/whl_library_targets.bzl b/python/private/pypi/whl_library_targets.bzl index f0fc943439..013a17b65e 100644 --- a/python/private/pypi/whl_library_targets.bzl +++ b/python/private/pypi/whl_library_targets.bzl @@ -99,7 +99,7 @@ def whl_library_targets( visibility: {type}`list[str]` The visibility of the targets. **kwargs: Extra args passed to the {obj}`whl_library_deps_targets` and {obj}`whl_library_srcs`. """ - create_extra_targets = bool(requires_dist or group_name) + create_extra_targets = bool(requires_dist or group_name) and dep_template whl_library_srcs( name = name, sdist_filename = sdist_filename, @@ -115,12 +115,12 @@ def whl_library_targets( namespace_package_files = namespace_package_files, # If there are no dependencies, then let's create the targets with public labels. # Note, we are not supporting grouping the packages in this case, but that is fine. - whl_name = WHEEL_FILE if (create_extra_targets and dep_template) else WHEEL_FILE_PUBLIC_LABEL, - pkg_name = PY_SRCS_LABEL if (create_extra_targets and dep_template) else PY_LIBRARY_PUBLIC_LABEL, + whl_name = WHEEL_FILE if create_extra_targets else WHEEL_FILE_PUBLIC_LABEL, + pkg_name = PY_SRCS_LABEL if create_extra_targets else PY_LIBRARY_PUBLIC_LABEL, **kwargs ) - if create_extra_targets and dep_template: + if create_extra_targets: whl_library_deps_targets( name = name, metadata_name = metadata_name, @@ -130,7 +130,7 @@ def whl_library_targets( include = include, # only needed if requires_dist is present group_name = group_name, # only needed if requires_dist is present dep_template = dep_template, # only needed if requires_dist is present - repo = None, + repo = None, # set aliases in the same repo aliases = {}, **kwargs ) From 7d91913640770ed596e9a7ec3ab624944f4366d4 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sat, 1 Aug 2026 09:51:26 +0900 Subject: [PATCH 13/13] address TODO items --- python/private/pypi/whl_library_targets.bzl | 25 ++++---- .../py_library/py_library_tests.bzl | 59 +++++++++++++++++++ tests/integration/whl_library/BUILD.bazel | 18 ++++++ .../integration/whl_library/test_contents.py | 12 ++++ .../whl_library_targets_tests.bzl | 51 +++++++++++++++- 5 files changed, 151 insertions(+), 14 deletions(-) diff --git a/python/private/pypi/whl_library_targets.bzl b/python/private/pypi/whl_library_targets.bzl index 013a17b65e..737a739145 100644 --- a/python/private/pypi/whl_library_targets.bzl +++ b/python/private/pypi/whl_library_targets.bzl @@ -329,9 +329,6 @@ def whl_library_srcs( data = data + [DATA_LABEL] rules.py_library( - # TODO @aignas 2026-07-26: once the srcs and library repos are separated, we should - # remove `PY_SRCS_LABEL and just use PY_LIBRARY_PUBLIC_LABEL. That will improve the - # amount of labels we are creating. name = pkg_name, srcs = srcs, pyi_srcs = pyi_srcs, @@ -401,9 +398,6 @@ def whl_library_deps_targets( DATA_LABEL: repo_label(DATA_LABEL), } - # TODO @aignas 2026-07-26: add a test when the group_name is specified and the requires_dist is - # empty list. We should essentially include in the group with the least amount of targets. - # If this library is a member of a group, its public label aliases need to # point to the group implementation rule not the implementation rules. We # also need to mark the implementation rules as visible to the group @@ -444,11 +438,12 @@ def whl_library_deps_targets( impl_vis = visibility if not requires_dist: - # If the package is in a group but has no deps, we will correctly alias the right - # thing. + # If the package is in a group but has no deps, we still need the public labels to + # point at the srcs targets so that the group implementation can use them. We don't + # need any of the extra targets, so just create the aliases. aliases = aliases | { - py_library_label: repo_label(PY_LIBRARY_PUBLIC_LABEL), - whl_file_label: repo_label(WHEEL_FILE_PUBLIC_LABEL), + py_library_label: repo_label(PY_SRCS_LABEL), + whl_file_label: repo_label(WHEEL_FILE), } for alias, actual in aliases.items(): @@ -477,10 +472,13 @@ def whl_library_deps_targets( ) if hasattr(native, "filegroup"): + # We include the whl file as srcs so that `$(location :whl)` expands to the whl file. + # The transitive dependencies are available via the `data` attribute. native.filegroup( name = whl_file_label, + srcs = [repo_label(WHEEL_FILE)], data = _deps( - deps = [repo_label(WHEEL_FILE)], + deps = [], package_deps = package_deps, tmpl = dep_template.format(name = "{}", target = WHEEL_FILE_PUBLIC_LABEL), ), @@ -493,8 +491,9 @@ def whl_library_deps_targets( # We include as srcs to ensure that the (locations :pkg) works as expected. srcs = [repo_label(PY_SRCS_LABEL)], deps = _deps( - # We include as deps, so that `PyInfo` and friends get propagated as deps. - # not sure if just including it as `srcs` is enough. + # We include as deps, so that `PyInfo` and friends (e.g. `pyi_srcs`) get + # propagated. Just passing the target as `srcs` is not enough to propagate + # `pyi_srcs`, see `tests/base_rules/py_library`. deps = [repo_label(PY_SRCS_LABEL)], package_deps = package_deps, tmpl = dep_template.format(name = "{}", target = PY_LIBRARY_PUBLIC_LABEL), diff --git a/tests/base_rules/py_library/py_library_tests.bzl b/tests/base_rules/py_library/py_library_tests.bzl index 3726ff1f41..c375e72794 100644 --- a/tests/base_rules/py_library/py_library_tests.bzl +++ b/tests/base_rules/py_library/py_library_tests.bzl @@ -3,10 +3,12 @@ load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:truth.bzl", "matching") load("@rules_testing//lib:util.bzl", rt_util = "util") +load("//python:py_info.bzl", "PyInfo") load("//python:py_library.bzl", "py_library") load("//python:py_runtime_info.bzl", "PyRuntimeInfo") load("//tests/base_rules:base_tests.bzl", "create_base_tests") load("//tests/base_rules:util.bzl", pt_util = "util") +load("//tests/support:py_info_subject.bzl", "py_info_subject") _tests = [] @@ -141,6 +143,63 @@ def _test_files_to_compile_impl(env, target): _tests.append(_test_files_to_compile) +def _test_pyi_srcs_not_propagated_via_srcs(name, config): + rt_util.helper_target( + config.rule, + name = name + "_lib", + srcs = ["lib.py"], + pyi_srcs = ["lib.pyi"], + ) + rt_util.helper_target( + config.rule, + name = name + "_subject", + srcs = [name + "_lib"], + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_pyi_srcs_not_propagated_via_srcs_impl, + ) + +def _test_pyi_srcs_not_propagated_via_srcs_impl(env, target): + info = env.expect.that_target(target).provider( + PyInfo, + factory = py_info_subject, + ) + info.transitive_pyi_files().contains_exactly([]) + +_tests.append(_test_pyi_srcs_not_propagated_via_srcs) + +def _test_pyi_srcs_propagated_via_deps(name, config): + rt_util.helper_target( + config.rule, + name = name + "_lib", + srcs = ["lib.py"], + pyi_srcs = ["lib.pyi"], + ) + rt_util.helper_target( + config.rule, + name = name + "_subject", + srcs = [name + "_lib"], + deps = [name + "_lib"], + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_pyi_srcs_propagated_via_deps_impl, + ) + +def _test_pyi_srcs_propagated_via_deps_impl(env, target): + info = env.expect.that_target(target).provider( + PyInfo, + factory = py_info_subject, + ) + info.transitive_pyi_files().contains_exactly([ + "{package}/lib.pyi", + ]) + +_tests.append(_test_pyi_srcs_propagated_via_deps) + def py_library_test_suite(name): config = struct(rule = py_library, base_test_rule = py_library) native.test_suite( diff --git a/tests/integration/whl_library/BUILD.bazel b/tests/integration/whl_library/BUILD.bazel index 56ba61a7c7..0c26fee4cf 100644 --- a/tests/integration/whl_library/BUILD.bazel +++ b/tests/integration/whl_library/BUILD.bazel @@ -13,6 +13,20 @@ load("@rules_python//python:py_test.bzl", "py_test") ] ] +[ + filegroup( + name = "{}_whl".format(pkg), + srcs = [], + visibility = ["//visibility:public"], + ) + for pkg in [ + "charset_normalizer", + "certifi", + "idna", + "urllib3", + ] +] + # Extract all deps genquery( name = "whl_target_deps", @@ -36,12 +50,16 @@ py_test( "@pip_archive//:srcs", "@pip_sdist_archive//:srcs", "@whl_archive//:srcs", + "@whl_archive//:whl", + "@whl_deps_library//:whl", ], env = { "SDIST_SRC_FILES": "$(locations @pip_sdist_archive//:srcs)", "SRC_FILES": "$(locations @pip_archive//:srcs)", "WHL_DEPS": "$(location :whl_target_deps)", + "WHL_DEPS_LOCATION": "$(location @whl_deps_library//:whl)", "WHL_FILES": "$(locations @whl_archive//:srcs)", + "WHL_LOCATION": "$(location @whl_archive//:whl)", "WHL_TARGET_DEPS": "$(location :whl_deps_target_deps)", }, ) diff --git a/tests/integration/whl_library/test_contents.py b/tests/integration/whl_library/test_contents.py index 1ae8433c96..ab352a5d98 100644 --- a/tests/integration/whl_library/test_contents.py +++ b/tests/integration/whl_library/test_contents.py @@ -91,6 +91,18 @@ def test_whl_srcs(self): ], ) + def test_whl_location(self): + self.assertTrue( + os.environ["WHL_LOCATION"].endswith("requests-2.34.2-py3-none-any.whl"), + msg=os.environ["WHL_LOCATION"], + ) + self.assertTrue( + os.environ["WHL_DEPS_LOCATION"].endswith( + "requests-2.34.2-py3-none-any.whl" + ), + msg=os.environ["WHL_DEPS_LOCATION"], + ) + @staticmethod def _read_file(env_var: str) -> list[str]: return set(Path(os.environ[env_var]).read_text().splitlines()) diff --git a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl index dcef1944f3..0a059c9d6a 100644 --- a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl +++ b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl @@ -152,7 +152,8 @@ def _test_whl_library_deps_targets(env): "name": "whl", # NOTE @aignas 2026-07-25: depending on the brackets position one may get different # results in the expectation. - "data": ["whl_file", "@pypi//bar:whl"] + select({ + "srcs": ["whl_file"], + "data": ["@pypi//bar:whl"] + select({ ":is_include_bar_baz_true": ["@pypi//bar_baz:whl"], "//conditions:default": [], }), @@ -188,6 +189,54 @@ def _test_whl_library_deps_targets(env): _tests.append(_test_whl_library_deps_targets) +def _test_whl_library_deps_targets_no_deps(env): + alias_calls = [] + filegroup_calls = [] + py_library_calls = [] + env_marker_setting_calls = [] + + whl_library_deps_targets( + name = "foo-0-py3-none-any.whl", + metadata_name = "Foo", + dep_template = "@pypi//{name}:{target}", + requires_dist = [], + group_name = "qux", + repo = None, + aliases = {}, + extras = [], + native = struct( + filegroup = lambda **kwargs: filegroup_calls.append(kwargs), + alias = lambda **kwargs: alias_calls.append(kwargs), + config_setting = lambda **_: None, + glob = lambda **_: [], + ), + rules = struct( + py_library = lambda **kwargs: py_library_calls.append(kwargs), + env_marker_setting = lambda **kwargs: env_marker_setting_calls.append(kwargs), + ), + ) + + # If the package is in a group but has no deps, then the public labels should be aliases + # to the srcs targets and no other targets should be created. + env.expect.that_collection(alias_calls).contains_exactly([ + { + "name": "pkg", + "actual": "srcs", + "visibility": ["//visibility:public"], + }, + { + "name": "whl", + "actual": "whl_file", + "visibility": ["//visibility:public"], + }, + ]) # buildifier: @unsorted-dict-items + + env.expect.that_collection(filegroup_calls).contains_exactly([]) + env.expect.that_collection(py_library_calls).contains_exactly([]) + env.expect.that_collection(env_marker_setting_calls).contains_exactly([]) + +_tests.append(_test_whl_library_deps_targets_no_deps) + def _test_sdist_excludes_record(env): py_library_calls = [] m_glob = mocks.glob()