Skip to content

feat(gazelle): add Python tags directives #3146

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

adam-singer
Copy link

@adam-singer adam-singer commented Aug 6, 2025

Summary

Adds four new Gazelle directives for adding Bazel tags to generated Python targets. This addresses the need for better target organization, build control, and test execution filtering in Python codebases.

Why this change?

Tags are essential for controlling Bazel behavior, especially in large codebases where you need to:

  • Prevent certain targets from building with bazel build //... (using manual tag)
  • Categorize tests for selective execution (unit, integration, fast, etc.)
  • Mark deployment binaries or shared libraries for special handling
  • Control resource usage with tags like exclusive

Previously, users had no way to automatically add tags to Gazelle-generated Python targets, requiring manual maintenance of BUILD files.

New Directives

  • # gazelle:python_tags - Adds tags to all generated Python targets (py_library, py_binary, py_test)
  • # gazelle:python_library_tags - Adds tags specifically to py_library targets
  • # gazelle:python_binary_tags - Adds tags specifically to py_binary targets
  • # gazelle:python_test_tags - Adds tags specifically to py_test targets

Before and After

Before: No way to add tags to generated targets

py_library(
    name = "mylib",
    srcs = ["mylib.py"],
    visibility = ["//:__subpackages__"],
)

py_test(
    name = "mylib_test", 
    srcs = ["mylib_test.py"],
    main = "mylib_test.py",
)

After: Automatic tag application with fine-grained control

# Add tags to all Python targets
# gazelle:python_tags manual,integration

# Add specific tags to different target types  
# gazelle:python_library_tags reusable,shared
# gazelle:python_test_tags unit,fast

Generates:

py_library(
    name = "mylib",
    srcs = ["mylib.py"],
    tags = [
        "integration", 
        "manual",
        "reusable",
        "shared",
    ],
    visibility = ["//:__subpackages__"],
)

py_test(
    name = "mylib_test",
    srcs = ["mylib_test.py"], 
    main = "mylib_test.py",
    tags = [
        "fast",
        "integration",
        "manual", 
        "unit",
    ],
)

Key Features

  • Tag Combination: General tags (python_tags) are combined with specific target-type tags and sorted alphabetically
  • Whitespace Handling: Automatic trimming of whitespace around commas in tag lists
  • Empty Values: Empty directive values result in no tags being added (clean handling)
  • All Generation Modes: Works with package, file, and project generation modes
  • Inheritance: Child BUILD files inherit tag directives from parent directories

Implementation

  • Added configuration support in gazelle/pythonconfig/pythonconfig.go
  • Integrated directive processing in gazelle/python/configure.go
  • Updated target generation in gazelle/python/generate.go and gazelle/python/target.go
  • Comprehensive documentation in gazelle/docs/directives.md

Testing

Added extensive test coverage with 6 test scenarios:

  1. General tags - Tags applied to all target types
  2. Specific tags - Different tags for libraries, binaries, and tests
  3. Combined tags - General + specific tag combination
  4. No tags - Baseline behavior with no directives
  5. Per-file generation - Compatibility with per-file mode
  6. Edge cases - Whitespace handling and empty values

All tests verify proper tag inheritance, combination, and alphabetical sorting.

Common Use Cases

  • manual - Prevent targets from bazel build //...
  • integration / unit - Categorize tests for selective execution
  • exclusive - Mark tests needing exclusive resource access
  • deploy - Tag production binaries
  • fast / slow - Control test execution based on speed
  • shared / reusable - Mark library components

Breaking Changes

None. This is a purely additive feature that doesn't change existing behavior.

@dougthor42
Copy link
Collaborator

I don't have time to review this immediately, but the first thing that pops into my mind is:

Is there a way to reset or stop tagging in child packages? And how do those interact between the general python_tags and specific python_*_tags directives?

For example:

BUILD.bazel         # gazelle:python_tags foo                  # tests get tags=["foo"]
src/BUILD.bazel     # gazelle:python_test_tags bar             # tests get tags=["foo", "bar"]
src/a/BUILD.bazel   # gazelle:python_tags SPECIAL_RESET        # tests get tags=["bar"]
src/b/BUILD.bazel   # gazelle:python_test_tags SPECIAL_RESET   # tests get tags=["foo"]

@adam-singer
Copy link
Author

I don't have time to review this immediately, but the first thing that pops into my mind is:

Thats fine, we are testing this out for our needs, if the directive changes slightly it should be easy to adapt.

Is there a way to reset or stop tagging in child packages? And how do those interact between the general python_tags and specific python_*_tags directives?

Good question, we could implement by pushing a struct with additional meta information.

Interested in clarifying the desired behavior when we ask the broader question: "How should we manage tags for Python targets using Gazelle?"

Is resetting the stack of tags a scenario we would commonly need to address? It seems like a valuable feature to prevent unintended propagation of tags. Perhaps we should define tag behavior to be either recursive or local to the specific BUILD.bazel file.

This distinction could be made in the implementation by using the plural form (ending with s) to indicate recursion (or some other way).

For example, the following directives would be recursive and apply to all nested targets:

# gazelle:python_tags
# gazelle:python_library_tags
# gazelle:python_test_tags
# gazelle:python_binary_tags

In contrast, these directives would be local and only apply to the immediate BUILD.bazel file:

# gazelle:python_tag
# gazelle:python_library_tag
# gazelle:python_test_tag
# gazelle:python_binary_tag

This approach might be a bit subtle, but it provides a clear and flexible way to control tag propagation.

Regarding recursive tag termination, child elements could implement a RESET:<tag> directive to stop tag propagation. While there is some concern that this approach might be excessive, it also seems like a reasonable solution.

# gazelle:python_tags RESET:<tag>
# gazelle:python_library_tags RESET:<tag>
# gazelle:python_test_tags RESET:<tag>
# gazelle:python_binary_tags RESET:<tag>

@dougthor42
Copy link
Collaborator

Prior Work:

For the python_default_visibility directive we use special values NONE and DEFAULT.

For python_test_file_pattern people must manually set things back to the default value to reset.

Resetting

I think that having plural and non-plural directives is too much. The proposal already adds 4 directives (should be 5 because of py_proto_library); the maintenance burden of 8 (10) directives is quite high. Plus the plural vs non-plural distinction is IMO prone to hard-to-spot typos.

child elements could implement a RESET:<tag> directive

I don't expect people to need that fine-grained control. I bet its more prudent to have a general RESET value and tell users to re-implement the ones they're interested in.

# in foo/BUILD.bazel
# gazelle:python_tags foo,bar,baz

# in foo/bar/baz/BUILD.bazel
# gazelle:python_tags RESET
# gazelle:python_tags foo,testing

If we don't want to use RESET (because maybe someone uses that as a valid tag?) then perhaps we should consider allowing the empty value as the reset:

# in foo/BUILD.bazel
# gazelle:python_tags foo,bar,baz

# in foo/bar/baz/BUILD.bazel
# gazelle:python_tags
# gazelle:python_tags foo,testing

Alternative Proposal

The proposal already adds 4 (5) directives

There's precedence for multi-parameter directives (eg resolve). What if there was a single directive that handled everything? One directive to maintain, even if it's more complex, is IMO nicer than 4 (5) or 8 (10).

# gazelle_python_tags TARGET_TYPE TAGS
  • TARGET_TYPE: one of all, py_binary, py_library, py_test, py_proto_library
    • Heck we could even support a comma-separated list py_binary,py_test if we wanted to, with all just being a shorthand for py_binary,py_library,py_test,py_proto_library and invalid if used within a comma-separated list (eg no py_binary,all).
    • Maybe because all is special we use ALL instead.
  • TAGS: A comma-separated list of tags to apply or the special value RESET. Eg foo,bar, manual,block-network,no-sandbox
    • Do not support tags with spaces. I don't think that's a specific restriction with Bazel tags, but I don't know of any cases of tags-with-spaces.
    • I don't know of any cases of the tag literal "RESET" so we're probably safe to use that. If we're worried, we could use something like __RESET__.
      • as above, we could also use the empty value as "reset", but personally I prefer an explicit string.

Example:

# in foo/BUILD.bazel
# gazelle_python_tags py_library foo,bar
# gazelle_python_tags py_test foo,bar,baz

# in foo/bar/baz/BUILD.bazel
# gazelle:python_tags ALL RESET
# gazelle:python_tags ALL foo,testing

# in foo/bar/baz/subdir/BUILD.bazel
# gazelle:python_tags py_binary,py_library RESET  # now only py_test and py_proto_library get foo,testing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants