diff --git a/docs/uv.md b/docs/uv.md index c964150ca..b618fbf31 100644 --- a/docs/uv.md +++ b/docs/uv.md @@ -410,6 +410,7 @@ load("@aspect_rules_py//uv:defs.bzl", "gazelle_python_manifest") gazelle_python_manifest( name = "gazelle_python_manifest", hub = "pypi", + include_stub_packages = True, venvs = ["default"], ) ``` @@ -417,6 +418,9 @@ gazelle_python_manifest( **Parameters:** - `hub` — The name of your uv hub (must match `uv.declare_hub(hub_name = ...)`). +- `include_stub_packages` — Whether to index conventional stub distributions such as + `types-requests` and `asyncpg-stubs`. The Gazelle Python extension then adds matching + stub dependencies automatically. Defaults to `False`. - `venvs` — List of dependency group names whose wheels should be indexed. Module mappings from all listed dependency groups are merged into a single manifest. diff --git a/e2e/cases/uv-gazelle-778/BUILD.bazel b/e2e/cases/uv-gazelle-778/BUILD.bazel index 96823e32e..fc64a8291 100644 --- a/e2e/cases/uv-gazelle-778/BUILD.bazel +++ b/e2e/cases/uv-gazelle-778/BUILD.bazel @@ -10,6 +10,7 @@ load("@aspect_rules_py//uv:defs.bzl", "gazelle_python_manifest") gazelle_python_manifest( name = "gazelle_python_manifest", hub = "pypi_uv_deps_650", + include_stub_packages = True, # Note that we _merge_ the package mappings from several configurations. venvs = [ "airflow", diff --git a/e2e/cases/uv-gazelle-778/gazelle_python.yaml b/e2e/cases/uv-gazelle-778/gazelle_python.yaml index 0bec7702e..107412d64 100644 --- a/e2e/cases/uv-gazelle-778/gazelle_python.yaml +++ b/e2e/cases/uv-gazelle-778/gazelle_python.yaml @@ -167,6 +167,7 @@ manifest: text_unidecode: text_unidecode tools: cron_descriptor typer: typer + types_requests: types_requests typing_extensions: typing_extensions typing_inspection: typing_inspection tzdata: tzdata diff --git a/uv/private/gazelle_manifest/defs.bzl b/uv/private/gazelle_manifest/defs.bzl index 7cd2ae443..25d0727b9 100644 --- a/uv/private/gazelle_manifest/defs.bzl +++ b/uv/private/gazelle_manifest/defs.bzl @@ -37,7 +37,7 @@ def _modules_mapping_impl(ctx): args_file.path, "--output", out.path, - ], + ] + (["--include_stub_packages"] if ctx.attr.include_stub_packages else []), inputs = [ args_file, ] + whl_files, @@ -59,6 +59,7 @@ _modules_mapping = rule( attrs = { "wheels": attr.label_list(providers = [[DefaultInfo]]), "hub": attr.string(), + "include_stub_packages": attr.bool(), "_generator": attr.label( default = Label(":generator"), executable = True, @@ -69,7 +70,16 @@ _modules_mapping = rule( update = Label(":update.sh") -def gazelle_python_manifest(name, hub, venvs = []): +def gazelle_python_manifest(name, hub, venvs = [], include_stub_packages = False): + """Generates a Gazelle Python manifest from uv-managed wheels. + + Args: + name: Name of the generated manifest target. + hub: Name of the uv hub containing the wheels. + venvs: Dependency groups whose wheels should be indexed. + include_stub_packages: Whether conventional stub distributions should be + indexed for Gazelle's automatic stub dependency resolution. + """ file = "gazelle_python.yaml" hub = hub.lstrip("@") @@ -98,6 +108,7 @@ def gazelle_python_manifest(name, hub, venvs = []): name = name, wheels = whls, hub = hub, + include_stub_packages = include_stub_packages, ) dest = native.package_name() diff --git a/uv/private/gazelle_manifest/generate.py b/uv/private/gazelle_manifest/generate.py index 09ee7ff76..0a9f46eb8 100644 --- a/uv/private/gazelle_manifest/generate.py +++ b/uv/private/gazelle_manifest/generate.py @@ -48,6 +48,14 @@ def normalize_name(name: str) -> str: ]) +def is_stub_package(name: str) -> bool: + """Return whether a normalized distribution name follows a stub-package convention.""" + return ( + name.endswith(("_stubs", "_types")) + or name.startswith(("types_", "stubs_")) + ) + + def extract_package_name(whl_path: Path) -> Optional[str]: """ Opens a .whl file, finds the METADATA file in .dist-info/, and extracts @@ -281,6 +289,12 @@ def main() -> None: '--hub_name', ) + parser.add_argument( + '--include_stub_packages', + action='store_true', + help="Include conventional stub distributions for Gazelle's automatic stub dependency resolution.", + ) + args = parser.parse_args() # Read wheel paths @@ -317,6 +331,11 @@ def main() -> None: if not package_name: continue + # The Gazelle Python resolver looks up these synthetic module names + # after resolving the corresponding runtime distribution. + if args.include_stub_packages and is_stub_package(package_name): + all_module_package_pairs.append((package_name, package_name)) + # Identify importable modules for this package modules = identify_modules(whl_path, package_name) diff --git a/uv/private/gazelle_manifest/test.py b/uv/private/gazelle_manifest/test.py index c1825f4bc..746380ebc 100644 --- a/uv/private/gazelle_manifest/test.py +++ b/uv/private/gazelle_manifest/test.py @@ -1,6 +1,18 @@ #!/usr/bin/env python3 -from generate import get_importable_module_name, find_unique_shallowest_prefixes +from generate import find_unique_shallowest_prefixes, get_importable_module_name, is_stub_package + + +def test_stub_package_names() -> None: + # Given: normalized distribution names covering supported stub conventions. + package_names = ["asyncpg_stubs", "types_requests", "foo_types", "stubs_foo", "requests"] + + # When: each name is classified. + classifications = [is_stub_package(name) for name in package_names] + + # Then: conventional stub names are recognized without matching runtime packages. + assert classifications == [True, True, True, True, False] + def test_parse_names() -> None: assert get_importable_module_name("foo.py") == "foo"