Gazelle extension for Python (#514)
Gazelle plugin
* Add new example to --deleted_packages
* Update examples/build_file_generation/BUILD
Co-authored-by: Jonathon Belotti <jonathon@canva.com>
* fix: gazelle:exclude on coarse-grained
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: comment on Kinds()
Co-authored-by: Jonathon Belotti <jonathon@canva.com>
* owner: f0rmiga
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: build and setuptools pinned versions
With the recent change in pypa/setuptools#2769, some wheels started to
fail build immediately with an unpinned setuptools in isolation mode.
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* refactor: use local_repository in examples
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* bump: examples Bazel version
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: add missing .gitignore to example
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* refactor: remove python_coarse_grained_generation
Also add the python_generation_mode directive.
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: gazelle spam from org_golang_x_tools
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* revert: example .bazelversion
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: simplify std_modules.py
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* feat: test py_library without __init__.py
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* feat: manifest generation tag manual
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: check std modules last
Performing the check last is more correct and yields better performance,
noticeable on large repositories.
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
Co-authored-by: Alex Eagle <eagle@post.harvard.edu>
Co-authored-by: Jonathon Belotti <jonathon@canva.com>
diff --git a/.bazelrc b/.bazelrc
index b7f29ab..8c931b7 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -3,7 +3,15 @@
# This lets us glob() up all the files inside the examples to make them inputs to tests
# (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it)
# To update these lines, run tools/bazel_integration_test/update_deleted_packages.sh
-build --deleted_packages=examples/legacy_pip_import/boto,examples/legacy_pip_import/extras,examples/legacy_pip_import/helloworld,examples/pip_install,examples/pip_parse,examples/py_import,examples/relative_requirements
-query --deleted_packages=examples/legacy_pip_import/boto,examples/legacy_pip_import/extras,examples/legacy_pip_import/helloworld,examples/pip_install,examples/pip_parse,examples/py_import,examples/relative_requirements
+build --deleted_packages=examples/build_file_generation,examples/legacy_pip_import/boto,examples/legacy_pip_import/extras,examples/legacy_pip_import/helloworld,examples/pip_install,examples/pip_parse,examples/py_import,examples/relative_requirements
+query --deleted_packages=examples/build_file_generation,examples/legacy_pip_import/boto,examples/legacy_pip_import/extras,examples/legacy_pip_import/helloworld,examples/pip_install,examples/pip_parse,examples/py_import,examples/relative_requirements
test --test_output=errors
+
+# Do NOT implicitly create empty __init__.py files in the runfiles tree.
+# By default, these are created in every directory containing Python source code
+# or shared libraries, and every parent directory of those directories,
+# excluding the repo root directory. With this flag set, we are responsible for
+# creating (possibly empty) __init__.py files and adding them to the srcs of
+# Python targets as required.
+build --incompatible_default_to_explicit_init_py
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 79b887c..cf6c962 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -10,6 +10,9 @@
/python/whl.bzl @thundergolfer @andyscott
/python/requirements.txt @thundergolfer @andyscott
+# Directory containing the Gazelle extension and Go code.
+/gazelle/ @f0rmiga
+
# The proposals dir corresponds to the Bazel proposals process, documented
# here: https://bazel.build/designs/index.html
/proposals/ @brandjon @lberki
diff --git a/.gitignore b/.gitignore
index e3bb55e..3099b78 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,3 +41,8 @@
# vim swap files
*.swp
*.swo
+
+# Go/Gazelle files
+# These otherwise match patterns above
+!go.mod
+!BUILD.out
\ No newline at end of file
diff --git a/BUILD b/BUILD
index f548715..f6d68e0 100644
--- a/BUILD
+++ b/BUILD
@@ -11,6 +11,8 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
+load("@bazel_gazelle//:def.bzl", "gazelle")
+
package(default_visibility = ["//visibility:public"])
licenses(["notice"]) # Apache 2.0
@@ -51,3 +53,19 @@
],
visibility = ["//visibility:public"],
)
+
+# Gazelle configuration options.
+# See https://github.com/bazelbuild/bazel-gazelle#running-gazelle-with-bazel
+# gazelle:prefix github.com/bazelbuild/rules_python
+# gazelle:exclude bazel-out
+gazelle(name = "gazelle")
+
+gazelle(
+ name = "update_go_deps",
+ args = [
+ "-from_file=go.mod",
+ "-to_macro=gazelle/deps.bzl%gazelle_deps",
+ "-prune",
+ ],
+ command = "update-repos",
+)
diff --git a/WORKSPACE b/WORKSPACE
index 97c67eb..c1c58ec 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -24,3 +24,8 @@
load("//:internal_setup.bzl", "rules_python_internal_setup")
rules_python_internal_setup()
+
+load("//gazelle:deps.bzl", "gazelle_deps")
+
+# gazelle:repository_macro gazelle/deps.bzl%gazelle_deps
+gazelle_deps()
diff --git a/examples/build_file_generation/.bazelversion b/examples/build_file_generation/.bazelversion
new file mode 100644
index 0000000..fcdb2e1
--- /dev/null
+++ b/examples/build_file_generation/.bazelversion
@@ -0,0 +1 @@
+4.0.0
diff --git a/examples/build_file_generation/.gitignore b/examples/build_file_generation/.gitignore
new file mode 100644
index 0000000..ac51a05
--- /dev/null
+++ b/examples/build_file_generation/.gitignore
@@ -0,0 +1 @@
+bazel-*
diff --git a/examples/build_file_generation/BUILD b/examples/build_file_generation/BUILD
new file mode 100644
index 0000000..ec31255
--- /dev/null
+++ b/examples/build_file_generation/BUILD
@@ -0,0 +1,37 @@
+load("@bazel_gazelle//:def.bzl", "gazelle")
+load("@rules_python//gazelle:def.bzl", "GAZELLE_PYTHON_RUNTIME_DEPS")
+load("@rules_python//gazelle/manifest:defs.bzl", "gazelle_python_manifest")
+load("@rules_python//python:defs.bzl", "py_library")
+
+# Gazelle python extension needs a manifest file mapping from
+# an import to the installed package that provides it.
+# This macro produces two targets:
+# - //:gazelle_python_manifest.update can be used with `bazel run`
+# to recalculate the manifest
+# - //:gazelle_python_manifest.test is a test target ensuring that
+# the manifest doesn't need to be updated
+gazelle_python_manifest(
+ name = "gazelle_python_manifest",
+ modules_mapping = "@modules_map//:modules_mapping.json",
+ pip_deps_repository_name = "pip",
+ requirements = "//:requirements_lock.txt",
+)
+
+# Our gazelle target points to the python gazelle binary.
+# This is the simple case where we only need one language supported.
+# If you also had proto, go, or other gazelle-supported languages,
+# you would also need a gazelle_binary rule.
+# See https://github.com/bazelbuild/bazel-gazelle/blob/master/extend.rst#example
+gazelle(
+ name = "gazelle",
+ data = GAZELLE_PYTHON_RUNTIME_DEPS,
+ gazelle = "@rules_python//gazelle:gazelle_python_binary",
+)
+
+# This rule is auto-generated and managed by Gazelle,
+# because it found the __init__.py file in this folder.
+py_library(
+ name = "build_file_generation",
+ srcs = ["__init__.py"],
+ visibility = ["//:__subpackages__"],
+)
diff --git a/examples/build_file_generation/README.md b/examples/build_file_generation/README.md
new file mode 100644
index 0000000..9b2fe1a
--- /dev/null
+++ b/examples/build_file_generation/README.md
@@ -0,0 +1,20 @@
+# Build file generation with Gazelle
+
+This example shows a project that has Gazelle setup with the rules_python
+extension, so that targets like `py_library` and `py_binary` can be
+automatically created just by running
+
+```sh
+$ bazel run //:gazelle
+```
+
+As a demo, try creating a `__main__.py` file in this directory, then
+re-run that gazelle command. You'll see that a `py_binary` target
+is created in the `BUILD` file.
+
+Or, try importing the `requests` library in `__init__.py`.
+You'll see that `deps = ["@pip//pypi__requests"]` is automatically
+added to the `py_library` target in the `BUILD` file.
+
+For more information on the behavior of the rules_python gazelle extension,
+see the README.md file in the /gazelle folder.
diff --git a/examples/build_file_generation/WORKSPACE b/examples/build_file_generation/WORKSPACE
new file mode 100644
index 0000000..4255932
--- /dev/null
+++ b/examples/build_file_generation/WORKSPACE
@@ -0,0 +1,73 @@
+workspace(name = "build_file_generation_example")
+
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+######################################################################
+# We need rules_go and bazel_gazelle, to build the gazelle plugin from source.
+# Setup instructions for this section are at
+# https://github.com/bazelbuild/bazel-gazelle#running-gazelle-with-bazel
+
+# Note, you could omit the rules_go dependency, if you have some way to statically
+# compile the gazelle binary for your workspace and distribute it to users on all
+# needed platforms.
+http_archive(
+ name = "io_bazel_rules_go",
+ sha256 = "69de5c704a05ff37862f7e0f5534d4f479418afc21806c887db544a316f3cb6b",
+ urls = [
+ "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.27.0/rules_go-v0.27.0.tar.gz",
+ "https://github.com/bazelbuild/rules_go/releases/download/v0.27.0/rules_go-v0.27.0.tar.gz",
+ ],
+)
+
+# NB: bazel-gazelle version must be after 18 August 2021
+# to include https://github.com/bazelbuild/bazel-gazelle/commit/2834ea4
+http_archive(
+ name = "bazel_gazelle",
+ sha256 = "0bb8056ab9ed4cbcab5b74348d8530c0e0b939987b0cfe36c1ab53d35a99e4de",
+ strip_prefix = "bazel-gazelle-2834ea44b3ec6371c924baaf28704730ec9d4559",
+ urls = [
+ # No release since March, and we need subsequent fixes
+ "https://github.com/bazelbuild/bazel-gazelle/archive/2834ea44b3ec6371c924baaf28704730ec9d4559.zip",
+ ],
+)
+
+load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
+load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
+
+go_rules_dependencies()
+
+go_register_toolchains(version = "1.16.5")
+
+gazelle_dependencies()
+
+######################################################################
+# Remaining setup is for rules_python
+
+local_repository(
+ name = "rules_python",
+ path = "../..",
+)
+
+load("@rules_python//python:pip.bzl", "pip_install")
+
+pip_install(
+ # Uses the default repository name "pip"
+ requirements = "//:requirements_lock.txt",
+)
+
+# The rules_python gazelle extension has some third-party go dependencies
+# which we need to fetch in order to compile it.
+load("@rules_python//gazelle:deps.bzl", _py_gazelle_deps = "gazelle_deps")
+
+_py_gazelle_deps()
+
+load("@rules_python//gazelle/modules_mapping:def.bzl", "modules_mapping")
+
+# This repository rule fetches the metadata for python packages we
+# depend on. That data is required for the gazelle_python_manifest
+# rule to update our manifest file.
+# To see what this rule does, try `bazel run @modules_map//:print`
+modules_mapping(
+ name = "modules_map",
+ requirements = "//:requirements_lock.txt",
+)
diff --git a/examples/build_file_generation/__init__.py b/examples/build_file_generation/__init__.py
new file mode 100644
index 0000000..ce47b77
--- /dev/null
+++ b/examples/build_file_generation/__init__.py
@@ -0,0 +1 @@
+print("hello")
\ No newline at end of file
diff --git a/examples/build_file_generation/gazelle_python.yaml b/examples/build_file_generation/gazelle_python.yaml
new file mode 100644
index 0000000..39eaccc
--- /dev/null
+++ b/examples/build_file_generation/gazelle_python.yaml
@@ -0,0 +1,130 @@
+# GENERATED FILE - DO NOT EDIT!
+#
+# To update this file, run:
+# bazel run //:gazelle_python_manifest.update
+
+manifest:
+ modules_mapping:
+ certifi: certifi
+ certifi.__init__: certifi
+ certifi.__main__: certifi
+ certifi.core: certifi
+ chardet: chardet
+ chardet.__init__: chardet
+ chardet.big5freq: chardet
+ chardet.big5prober: chardet
+ chardet.chardistribution: chardet
+ chardet.charsetgroupprober: chardet
+ chardet.charsetprober: chardet
+ chardet.cli: chardet
+ chardet.cli.__init__: chardet
+ chardet.cli.chardetect: chardet
+ chardet.codingstatemachine: chardet
+ chardet.compat: chardet
+ chardet.cp949prober: chardet
+ chardet.enums: chardet
+ chardet.escprober: chardet
+ chardet.escsm: chardet
+ chardet.eucjpprober: chardet
+ chardet.euckrfreq: chardet
+ chardet.euckrprober: chardet
+ chardet.euctwfreq: chardet
+ chardet.euctwprober: chardet
+ chardet.gb2312freq: chardet
+ chardet.gb2312prober: chardet
+ chardet.hebrewprober: chardet
+ chardet.jisfreq: chardet
+ chardet.jpcntx: chardet
+ chardet.langbulgarianmodel: chardet
+ chardet.langcyrillicmodel: chardet
+ chardet.langgreekmodel: chardet
+ chardet.langhebrewmodel: chardet
+ chardet.langhungarianmodel: chardet
+ chardet.langthaimodel: chardet
+ chardet.langturkishmodel: chardet
+ chardet.latin1prober: chardet
+ chardet.mbcharsetprober: chardet
+ chardet.mbcsgroupprober: chardet
+ chardet.mbcssm: chardet
+ chardet.sbcharsetprober: chardet
+ chardet.sbcsgroupprober: chardet
+ chardet.sjisprober: chardet
+ chardet.universaldetector: chardet
+ chardet.utf8prober: chardet
+ chardet.version: chardet
+ idna: idna
+ idna.__init__: idna
+ idna.codec: idna
+ idna.compat: idna
+ idna.core: idna
+ idna.idnadata: idna
+ idna.intranges: idna
+ idna.package_data: idna
+ idna.uts46data: idna
+ requests: requests
+ requests.__init__: requests
+ requests.__version__: requests
+ requests._internal_utils: requests
+ requests.adapters: requests
+ requests.api: requests
+ requests.auth: requests
+ requests.certs: requests
+ requests.compat: requests
+ requests.cookies: requests
+ requests.exceptions: requests
+ requests.help: requests
+ requests.hooks: requests
+ requests.models: requests
+ requests.packages: requests
+ requests.sessions: requests
+ requests.status_codes: requests
+ requests.structures: requests
+ requests.utils: requests
+ urllib3: urllib3
+ urllib3.__init__: urllib3
+ urllib3._collections: urllib3
+ urllib3._version: urllib3
+ urllib3.connection: urllib3
+ urllib3.connectionpool: urllib3
+ urllib3.contrib: urllib3
+ urllib3.contrib.__init__: urllib3
+ urllib3.contrib._appengine_environ: urllib3
+ urllib3.contrib._securetransport: urllib3
+ urllib3.contrib._securetransport.__init__: urllib3
+ urllib3.contrib._securetransport.bindings: urllib3
+ urllib3.contrib._securetransport.low_level: urllib3
+ urllib3.contrib.appengine: urllib3
+ urllib3.contrib.ntlmpool: urllib3
+ urllib3.contrib.pyopenssl: urllib3
+ urllib3.contrib.securetransport: urllib3
+ urllib3.contrib.socks: urllib3
+ urllib3.exceptions: urllib3
+ urllib3.fields: urllib3
+ urllib3.filepost: urllib3
+ urllib3.packages: urllib3
+ urllib3.packages.__init__: urllib3
+ urllib3.packages.backports: urllib3
+ urllib3.packages.backports.__init__: urllib3
+ urllib3.packages.backports.makefile: urllib3
+ urllib3.packages.six: urllib3
+ urllib3.packages.ssl_match_hostname: urllib3
+ urllib3.packages.ssl_match_hostname.__init__: urllib3
+ urllib3.packages.ssl_match_hostname._implementation: urllib3
+ urllib3.poolmanager: urllib3
+ urllib3.request: urllib3
+ urllib3.response: urllib3
+ urllib3.util: urllib3
+ urllib3.util.__init__: urllib3
+ urllib3.util.connection: urllib3
+ urllib3.util.proxy: urllib3
+ urllib3.util.queue: urllib3
+ urllib3.util.request: urllib3
+ urllib3.util.response: urllib3
+ urllib3.util.retry: urllib3
+ urllib3.util.ssl_: urllib3
+ urllib3.util.ssltransport: urllib3
+ urllib3.util.timeout: urllib3
+ urllib3.util.url: urllib3
+ urllib3.util.wait: urllib3
+ pip_deps_repository_name: pip
+integrity: 575d259c512b4b80f9923d1623d2aae3038654b731a4e088bf268e01138b6411
diff --git a/examples/build_file_generation/requirements.txt b/examples/build_file_generation/requirements.txt
new file mode 100644
index 0000000..9d84d35
--- /dev/null
+++ b/examples/build_file_generation/requirements.txt
@@ -0,0 +1 @@
+requests==2.25.1
diff --git a/examples/build_file_generation/requirements_lock.txt b/examples/build_file_generation/requirements_lock.txt
new file mode 100644
index 0000000..7573a6f
--- /dev/null
+++ b/examples/build_file_generation/requirements_lock.txt
@@ -0,0 +1,26 @@
+#
+# This file is autogenerated by pip-compile
+# To update, run:
+#
+# pip-compile --generate-hashes --output-file=requirements_lock.txt requirements.txt
+#
+certifi==2020.12.5 \
+ --hash=sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c \
+ --hash=sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830
+ # via requests
+chardet==3.0.4 \
+ --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \
+ --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691
+ # via requests
+idna==2.10 \
+ --hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6 \
+ --hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0
+ # via requests
+requests==2.25.1 \
+ --hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804 \
+ --hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e
+ # via -r requirements.txt
+urllib3==1.26.5 \
+ --hash=sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c \
+ --hash=sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098
+ # via requests
diff --git a/gazelle/BUILD.bazel b/gazelle/BUILD.bazel
new file mode 100644
index 0000000..ec0c0e5
--- /dev/null
+++ b/gazelle/BUILD.bazel
@@ -0,0 +1,72 @@
+load("@bazel_gazelle//:def.bzl", "gazelle_binary")
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+load("@rules_python//python:defs.bzl", "py_binary")
+
+go_library(
+ name = "gazelle",
+ srcs = [
+ "configure.go",
+ "fix.go",
+ "generate.go",
+ "kinds.go",
+ "language.go",
+ "parser.go",
+ "resolve.go",
+ "std_modules.go",
+ "target.go",
+ ],
+ importpath = "github.com/bazelbuild/rules_python/gazelle",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//gazelle/manifest",
+ "//gazelle/pythonconfig",
+ "@bazel_gazelle//config:go_default_library",
+ "@bazel_gazelle//label:go_default_library",
+ "@bazel_gazelle//language:go_default_library",
+ "@bazel_gazelle//repo:go_default_library",
+ "@bazel_gazelle//resolve:go_default_library",
+ "@bazel_gazelle//rule:go_default_library",
+ "@com_github_bazelbuild_buildtools//build:go_default_library",
+ "@com_github_bmatcuk_doublestar//:doublestar",
+ "@com_github_emirpasic_gods//lists/singlylinkedlist",
+ "@com_github_emirpasic_gods//sets/treeset",
+ "@com_github_emirpasic_gods//utils",
+ "@com_github_google_uuid//:uuid",
+ "@io_bazel_rules_go//go/tools/bazel:go_default_library",
+ ],
+)
+
+py_binary(
+ name = "parse",
+ srcs = ["parse.py"],
+ visibility = ["//visibility:public"],
+)
+
+py_binary(
+ name = "std_modules",
+ srcs = ["std_modules.py"],
+ visibility = ["//visibility:public"],
+)
+
+go_test(
+ name = "gazelle_test",
+ srcs = ["python_test.go"],
+ data = [
+ ":gazelle_python_binary",
+ ":parse",
+ ":std_modules",
+ #"@python_interpreter//:bazel_install/bin/python3",
+ ] + glob(["testdata/**"]),
+ deps = [
+ "@bazel_gazelle//testtools:go_default_library",
+ "@com_github_emirpasic_gods//lists/singlylinkedlist",
+ "@com_github_ghodss_yaml//:yaml",
+ "@io_bazel_rules_go//go/tools/bazel:go_default_library",
+ ],
+)
+
+gazelle_binary(
+ name = "gazelle_python_binary",
+ languages = ["//gazelle"],
+ visibility = ["//visibility:public"],
+)
diff --git a/gazelle/README.md b/gazelle/README.md
new file mode 100644
index 0000000..9edf773
--- /dev/null
+++ b/gazelle/README.md
@@ -0,0 +1,194 @@
+# Python Gazelle plugin
+
+This directory contains a plugin for
+[Gazelle](https://github.com/bazelbuild/bazel-gazelle)
+that generates BUILD file content for Python code.
+
+## Installation
+
+First, you'll need to add Gazelle to your `WORKSPACE` file.
+Follow the instructions at https://github.com/bazelbuild/bazel-gazelle#running-gazelle-with-bazel
+
+Next, we need to add two more things to the `WORKSPACE`:
+
+1. fetch the third-party Go libraries that the python extension depends on
+1. fetch metadata about your Python dependencies, so that gazelle can
+ determine which package a given import statement comes from.
+
+Add this to your `WORKSPACE`:
+
+```starlark
+# To compile the rules_python gazelle extension from source,
+# we must fetch some third-party go dependencies that it uses.
+load("@rules_python//gazelle:deps.bzl", _py_gazelle_deps = "gazelle_deps")
+
+_py_gazelle_deps()
+
+load("@rules_python//gazelle/modules_mapping:def.bzl", "modules_mapping")
+
+# This repository rule fetches the metadata for python packages we
+# depend on. That data is required for the gazelle_python_manifest
+# rule to update our manifest file.
+# To see what this rule does, try `bazel run @modules_map//:print`
+modules_mapping(
+ name = "modules_map",
+ # This should point to wherever we declare our python dependencies
+ requirements = "//:requirements_lock.txt",
+)
+```
+
+Next, we'll make a pair of targets for consuming that `modules_mapping` we
+fetched, and writing it as a manifest file for Gazelle to read.
+This is checked into the repo for speed, as it takes some time to calculate
+in a large monorepo.
+
+Create a file `gazelle_python.yaml` next to your `requirements.txt`
+file. (You can just use `touch` at this point, it just needs to exist.)
+
+Then put this in your `BUILD.bazel` file next to the `requirements.txt`:
+
+```starlark
+load("@rules_python//gazelle/manifest:defs.bzl", "gazelle_python_manifest")
+
+# Gazelle python extension needs a manifest file mapping from
+# an import to the installed package that provides it.
+# This macro produces two targets:
+# - //:gazelle_python_manifest.update can be used with `bazel run`
+# to recalculate the manifest
+# - //:gazelle_python_manifest.test is a test target ensuring that
+# the manifest doesn't need to be updated
+gazelle_python_manifest(
+ name = "gazelle_python_manifest",
+ # The @modules_map refers to the name we gave in the modules_mapping
+ # rule in the WORKSPACE
+ modules_mapping = "@modules_map//:modules_mapping.json",
+ # This is what we called our `pip_install` rule, where third-party
+ # python libraries are loaded in BUILD files.
+ pip_deps_repository_name = "pip",
+ # This should point to wherever we declare our python dependencies
+ # (the same as what we passed to the modules_mapping rule in WORKSPACE)
+ requirements = "//:requirements_lock.txt",
+)
+```
+
+Finally, you create a target that you'll invoke to run the Gazelle tool
+with the rules_python extension included. This typically goes in your root
+`/BUILD.bazel` file:
+
+```
+load("@bazel_gazelle//:def.bzl", "gazelle")
+load("@rules_python//gazelle:def.bzl", "GAZELLE_PYTHON_RUNTIME_DEPS")
+
+# Our gazelle target points to the python gazelle binary.
+# This is the simple case where we only need one language supported.
+# If you also had proto, go, or other gazelle-supported languages,
+# you would also need a gazelle_binary rule.
+# See https://github.com/bazelbuild/bazel-gazelle/blob/master/extend.rst#example
+gazelle(
+ name = "gazelle",
+ data = GAZELLE_PYTHON_RUNTIME_DEPS,
+ gazelle = "@rules_python//gazelle:gazelle_python_binary",
+)
+```
+
+That's it, now you can finally run `bazel run //:gazelle` anytime
+you edit Python code, and it should update your `BUILD` files correctly.
+
+A fully-working example is in [`examples/build_file_generation`](examples/build_file_generation).
+
+## Usage
+
+Gazelle is non-destructive.
+It will try to leave your edits to BUILD files alone, only making updates to `py_*` targets.
+However it will remove dependencies that appear to be unused, so it's a
+good idea to check in your work before running Gazelle so you can easily
+revert any changes it made.
+
+The rules_python extension assumes some conventions about your Python code.
+These are noted below, and might require changes to your existing code.
+
+Note that the `gazelle` program has multiple commands. At present, only the `update` command (the default) does anything for Python code.
+
+### Directives
+
+You can configure the extension using directives, just like for other
+languages. These are just comments in the `BUILD.bazel` file which
+govern behavior of the extension when processing files under that
+folder.
+
+See https://github.com/bazelbuild/bazel-gazelle#directives
+for some general directives that may be useful.
+In particular, the `resolve` directive is language-specific
+and can be used with Python.
+Examples of these directives in use can be found in the
+/gazelle/testdata folder in the rules_python repo.
+
+Python-specific directives are as follows:
+
+| **Directive** | **Default value** |
+|--------------------------------------|-------------------|
+| `# gazelle:python_extension` | `enabled` |
+| Controls whether the Python extension is enabled or not. Sub-packages inherit this value. Can be either "enabled" or "disabled". | |
+| `# gazelle:python_root` | n/a |
+| Sets a Bazel package as a Python root. This is used on monorepos with multiple Python projects that don't share the top-level of the workspace as the root. | |
+| `# gazelle:python_manifest_file_name`| `gazelle_python.yaml` |
+| Overrides the default manifest file name. | |
+| `# gazelle:python_ignore_files` | n/a |
+| Controls the files which are ignored from the generated targets. | |
+| `# gazelle:python_ignore_dependencies`| n/a |
+| Controls the ignored dependencies from the generated targets. | |
+| `# gazelle:python_validate_import_statements`| `true` |
+| Controls whether the Python import statements should be validated. Can be "true" or "false" | |
+| `# gazelle:python_generation_mode`| `package` |
+| Controls the target generation mode. Can be "package" or "project" | |
+| `# gazelle:python_library_naming_convention`| `$package_name$` |
+| Controls the `py_library` naming convention. It interpolates $package_name$ with the Bazel package name. E.g. if the Bazel package name is `foo`, setting this to `$package_name$_my_lib` would result in a generated target named `foo_my_lib`. | |
+| `# gazelle:python_binary_naming_convention` | `$package_name$_bin` |
+| Controls the `py_binary` naming convention. Follows the same interpolation rules as `python_library_naming_convention`. | |
+| `# gazelle:python_test_naming_convention` | `$package_name$_test` |
+| Controls the `py_test` naming convention. Follows the same interpolation rules as `python_library_naming_convention`. | |
+| `# gazelle:resolve py ...` | n/a |
+| Instructs the plugin what target to add as a dependency to satisfy a given import statement. The syntax is `# gazelle:resolve py import-string label` where `import-string` is the symbol in the python `import` statement, and `label` is the Bazel label that Gazelle should write in `deps`. | |
+
+### Libraries
+
+Python source files are those ending in `.py` but not ending in `_test.py`.
+
+First, we look for the nearest ancestor BUILD file starting from the folder
+containing the Python source file.
+
+If there is no `py_library` in this BUILD file, one is created, using the
+package name as the target's name. This makes it the default target in the
+package.
+
+Next, all source files are collected into the `srcs` of the `py_library`.
+
+Finally, the `import` statements in the source files are parsed, and
+dependencies are added to the `deps` attribute.
+
+### Tests
+
+Python test files are those ending in `_test.py`.
+
+A `py_test` target is added containing all test files as `srcs`.
+
+### Binaries
+
+When a `__main__.py` file is encountered, this indicates the entry point
+of a Python program.
+
+A `py_binary` target will be created, named `[package]_bin`.
+
+## Developing on the extension
+
+Gazelle extensions are written in Go. Ours is a hybrid, which also spawns
+a Python interpreter as a subprocess to parse python files.
+
+The Go dependencies are managed by the go.mod file.
+After changing that file, run `go mod tidy` to get a `go.sum` file,
+then run `bazel run //:update_go_deps` to convert that to the `gazelle/deps.bzl` file.
+The latter is loaded in our `/WORKSPACE` to define the external repos
+that we can load Go dependencies from.
+
+Then after editing Go code, run `bazel run //:gazelle` to generate/update
+go_* rules in the BUILD.bazel files in our repo.
diff --git a/gazelle/bazel_gazelle.pr1095.patch b/gazelle/bazel_gazelle.pr1095.patch
new file mode 100644
index 0000000..a417c94
--- /dev/null
+++ b/gazelle/bazel_gazelle.pr1095.patch
@@ -0,0 +1,19 @@
+commit b1c61c0b77648f7345a7c42cce941e32d87c84bf
+Author: Alex Eagle <eagle@post.harvard.edu>
+Date: Wed Aug 18 17:55:13 2021 -0700
+
+ Merge the private attribute
+
+diff --git a/rule/merge.go b/rule/merge.go
+index d5fbe94..e13e547 100644
+--- a/rule/merge.go
++++ b/rule/merge.go
+@@ -79,6 +79,8 @@ func MergeRules(src, dst *Rule, mergeable map[string]bool, filename string) {
+ }
+ }
+ }
++
++ dst.private = src.private
+ }
+
+ // mergeExprs combines information from src and dst and returns a merged
diff --git a/gazelle/configure.go b/gazelle/configure.go
new file mode 100644
index 0000000..64c2b51
--- /dev/null
+++ b/gazelle/configure.go
@@ -0,0 +1,164 @@
+package python
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "github.com/bazelbuild/bazel-gazelle/config"
+ "github.com/bazelbuild/bazel-gazelle/rule"
+
+ "github.com/bazelbuild/rules_python/gazelle/manifest"
+ "github.com/bazelbuild/rules_python/gazelle/pythonconfig"
+)
+
+// Configurer satisfies the config.Configurer interface. It's the
+// language-specific configuration extension.
+type Configurer struct{}
+
+// RegisterFlags registers command-line flags used by the extension. This
+// method is called once with the root configuration when Gazelle
+// starts. RegisterFlags may set an initial values in Config.Exts. When flags
+// are set, they should modify these values.
+func (py *Configurer) RegisterFlags(fs *flag.FlagSet, cmd string, c *config.Config) {}
+
+// CheckFlags validates the configuration after command line flags are parsed.
+// This is called once with the root configuration when Gazelle starts.
+// CheckFlags may set default values in flags or make implied changes.
+func (py *Configurer) CheckFlags(fs *flag.FlagSet, c *config.Config) error {
+ return nil
+}
+
+// KnownDirectives returns a list of directive keys that this Configurer can
+// interpret. Gazelle prints errors for directives that are not recoginized by
+// any Configurer.
+func (py *Configurer) KnownDirectives() []string {
+ return []string{
+ pythonconfig.PythonExtensionDirective,
+ pythonconfig.PythonRootDirective,
+ pythonconfig.PythonManifestFileNameDirective,
+ pythonconfig.IgnoreFilesDirective,
+ pythonconfig.IgnoreDependenciesDirective,
+ pythonconfig.ValidateImportStatementsDirective,
+ pythonconfig.GenerationMode,
+ pythonconfig.LibraryNamingConvention,
+ pythonconfig.BinaryNamingConvention,
+ pythonconfig.TestNamingConvention,
+ }
+}
+
+// Configure modifies the configuration using directives and other information
+// extracted from a build file. Configure is called in each directory.
+//
+// c is the configuration for the current directory. It starts out as a copy
+// of the configuration for the parent directory.
+//
+// rel is the slash-separated relative path from the repository root to
+// the current directory. It is "" for the root directory itself.
+//
+// f is the build file for the current directory or nil if there is no
+// existing build file.
+func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) {
+ // Create the root config.
+ if _, exists := c.Exts[languageName]; !exists {
+ rootConfig := pythonconfig.New(c.RepoRoot, "")
+ c.Exts[languageName] = pythonconfig.Configs{"": rootConfig}
+ }
+
+ configs := c.Exts[languageName].(pythonconfig.Configs)
+
+ config, exists := configs[rel]
+ if !exists {
+ parent := configs.ParentForPackage(rel)
+ config = parent.NewChild()
+ configs[rel] = config
+ }
+
+ if f == nil {
+ return
+ }
+
+ gazelleManifestFilename := "gazelle_python.yaml"
+
+ for _, d := range f.Directives {
+ switch d.Key {
+ case "exclude":
+ // We record the exclude directive for coarse-grained packages
+ // since we do manual tree traversal in this mode.
+ config.AddExcludedPattern(strings.TrimSpace(d.Value))
+ case pythonconfig.PythonExtensionDirective:
+ switch d.Value {
+ case "enabled":
+ config.SetExtensionEnabled(true)
+ case "disabled":
+ config.SetExtensionEnabled(false)
+ default:
+ err := fmt.Errorf("invalid value for directive %q: %s: possible values are enabled/disabled",
+ pythonconfig.PythonExtensionDirective, d.Value)
+ log.Fatal(err)
+ }
+ case pythonconfig.PythonRootDirective:
+ config.SetPythonProjectRoot(rel)
+ case pythonconfig.PythonManifestFileNameDirective:
+ gazelleManifestFilename = strings.TrimSpace(d.Value)
+ case pythonconfig.IgnoreFilesDirective:
+ for _, ignoreFile := range strings.Split(d.Value, ",") {
+ config.AddIgnoreFile(ignoreFile)
+ }
+ case pythonconfig.IgnoreDependenciesDirective:
+ for _, ignoreDependency := range strings.Split(d.Value, ",") {
+ config.AddIgnoreDependency(ignoreDependency)
+ }
+ case pythonconfig.ValidateImportStatementsDirective:
+ v, err := strconv.ParseBool(strings.TrimSpace(d.Value))
+ if err != nil {
+ log.Fatal(err)
+ }
+ config.SetValidateImportStatements(v)
+ case pythonconfig.GenerationMode:
+ switch pythonconfig.GenerationModeType(strings.TrimSpace(d.Value)) {
+ case pythonconfig.GenerationModePackage:
+ config.SetCoarseGrainedGeneration(false)
+ case pythonconfig.GenerationModeProject:
+ config.SetCoarseGrainedGeneration(true)
+ default:
+ err := fmt.Errorf("invalid value for directive %q: %s",
+ pythonconfig.GenerationMode, d.Value)
+ log.Fatal(err)
+ }
+ case pythonconfig.LibraryNamingConvention:
+ config.SetLibraryNamingConvention(strings.TrimSpace(d.Value))
+ case pythonconfig.BinaryNamingConvention:
+ config.SetBinaryNamingConvention(strings.TrimSpace(d.Value))
+ case pythonconfig.TestNamingConvention:
+ config.SetTestNamingConvention(strings.TrimSpace(d.Value))
+ }
+ }
+
+ gazelleManifestPath := filepath.Join(c.RepoRoot, rel, gazelleManifestFilename)
+ gazelleManifest, err := py.loadGazelleManifest(gazelleManifestPath)
+ if err != nil {
+ log.Fatal(err)
+ }
+ if gazelleManifest != nil {
+ config.SetGazelleManifest(gazelleManifest)
+ }
+}
+
+func (py *Configurer) loadGazelleManifest(gazelleManifestPath string) (*manifest.Manifest, error) {
+ if _, err := os.Stat(gazelleManifestPath); err != nil {
+ if os.IsNotExist(err) {
+ return nil, nil
+ }
+ return nil, fmt.Errorf("failed to load Gazelle manifest: %w", err)
+ }
+ manifestFile := new(manifest.File)
+ if err := manifestFile.Decode(gazelleManifestPath); err != nil {
+ return nil, fmt.Errorf("failed to load Gazelle manifest: %w", err)
+ }
+ return manifestFile.Manifest, nil
+}
diff --git a/gazelle/def.bzl b/gazelle/def.bzl
new file mode 100644
index 0000000..a402fc7
--- /dev/null
+++ b/gazelle/def.bzl
@@ -0,0 +1,7 @@
+"""This module contains the Gazelle runtime dependencies for the Python extension.
+"""
+
+GAZELLE_PYTHON_RUNTIME_DEPS = [
+ "@rules_python//gazelle:parse",
+ "@rules_python//gazelle:std_modules",
+]
diff --git a/gazelle/deps.bzl b/gazelle/deps.bzl
new file mode 100644
index 0000000..1d53fdd
--- /dev/null
+++ b/gazelle/deps.bzl
@@ -0,0 +1,172 @@
+"This file managed by `bazel run //:update_go_deps`"
+
+load("@bazel_gazelle//:deps.bzl", _go_repository = "go_repository")
+
+def go_repository(name, **kwargs):
+ if name not in native.existing_rules():
+ _go_repository(name = name, **kwargs)
+
+def gazelle_deps():
+ "Fetch go dependencies"
+ go_repository(
+ name = "com_github_bazelbuild_bazel_gazelle",
+ importpath = "github.com/bazelbuild/bazel-gazelle",
+ sum = "h1:Ks6YN+WkOv2lYWlvf7ksxUpLvrDbBHPBXXUrBFQ3BZM=",
+ version = "v0.23.0",
+ )
+ go_repository(
+ name = "com_github_bazelbuild_buildtools",
+ build_naming_convention = "go_default_library",
+ importpath = "github.com/bazelbuild/buildtools",
+ sum = "h1:Et1IIXrXwhpDvR5wH9REPEZ0sUtzUoJSq19nfmBqzBY=",
+ version = "v0.0.0-20200718160251-b1667ff58f71",
+ )
+ go_repository(
+ name = "com_github_bazelbuild_rules_go",
+ importpath = "github.com/bazelbuild/rules_go",
+ sum = "h1:wzbawlkLtl2ze9w/312NHZ84c7kpUCtlkD8HgFY27sw=",
+ version = "v0.0.0-20190719190356-6dae44dc5cab",
+ )
+ go_repository(
+ name = "com_github_bmatcuk_doublestar",
+ importpath = "github.com/bmatcuk/doublestar",
+ sum = "h1:oC24CykoSAB8zd7XgruHo33E0cHJf/WhQA/7BeXj+x0=",
+ version = "v1.2.2",
+ )
+ go_repository(
+ name = "com_github_burntsushi_toml",
+ importpath = "github.com/BurntSushi/toml",
+ sum = "h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=",
+ version = "v0.3.1",
+ )
+ go_repository(
+ name = "com_github_davecgh_go_spew",
+ importpath = "github.com/davecgh/go-spew",
+ sum = "h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=",
+ version = "v1.1.1",
+ )
+ go_repository(
+ name = "com_github_emirpasic_gods",
+ importpath = "github.com/emirpasic/gods",
+ sum = "h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=",
+ version = "v1.12.0",
+ )
+ go_repository(
+ name = "com_github_fsnotify_fsnotify",
+ importpath = "github.com/fsnotify/fsnotify",
+ sum = "h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=",
+ version = "v1.4.7",
+ )
+ go_repository(
+ name = "com_github_ghodss_yaml",
+ importpath = "github.com/ghodss/yaml",
+ sum = "h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=",
+ version = "v1.0.0",
+ )
+ go_repository(
+ name = "com_github_google_go_cmp",
+ importpath = "github.com/google/go-cmp",
+ sum = "h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=",
+ version = "v0.5.4",
+ )
+ go_repository(
+ name = "com_github_google_uuid",
+ importpath = "github.com/google/uuid",
+ sum = "h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=",
+ version = "v1.3.0",
+ )
+
+ go_repository(
+ name = "com_github_kr_pretty",
+ importpath = "github.com/kr/pretty",
+ sum = "h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=",
+ version = "v0.1.0",
+ )
+ go_repository(
+ name = "com_github_kr_pty",
+ importpath = "github.com/kr/pty",
+ sum = "h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=",
+ version = "v1.1.1",
+ )
+ go_repository(
+ name = "com_github_kr_text",
+ importpath = "github.com/kr/text",
+ sum = "h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=",
+ version = "v0.1.0",
+ )
+ go_repository(
+ name = "com_github_pelletier_go_toml",
+ importpath = "github.com/pelletier/go-toml",
+ sum = "h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=",
+ version = "v1.2.0",
+ )
+ go_repository(
+ name = "com_github_pmezard_go_difflib",
+ importpath = "github.com/pmezard/go-difflib",
+ sum = "h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=",
+ version = "v1.0.0",
+ )
+
+ go_repository(
+ name = "in_gopkg_check_v1",
+ importpath = "gopkg.in/check.v1",
+ sum = "h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=",
+ version = "v1.0.0-20180628173108-788fd7840127",
+ )
+ go_repository(
+ name = "in_gopkg_yaml_v2",
+ importpath = "gopkg.in/yaml.v2",
+ sum = "h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=",
+ version = "v2.2.2",
+ )
+ go_repository(
+ name = "org_golang_x_crypto",
+ importpath = "golang.org/x/crypto",
+ sum = "h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=",
+ version = "v0.0.0-20191011191535-87dc89f01550",
+ )
+ go_repository(
+ name = "org_golang_x_mod",
+ importpath = "golang.org/x/mod",
+ sum = "h1:Kvvh58BN8Y9/lBi7hTekvtMpm07eUZ0ck5pRHpsMWrY=",
+ version = "v0.4.1",
+ )
+ go_repository(
+ name = "org_golang_x_net",
+ importpath = "golang.org/x/net",
+ sum = "h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=",
+ version = "v0.0.0-20190620200207-3b0461eec859",
+ )
+ go_repository(
+ name = "org_golang_x_sync",
+ importpath = "golang.org/x/sync",
+ sum = "h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=",
+ version = "v0.0.0-20190911185100-cd5d95a43a6e",
+ )
+ go_repository(
+ name = "org_golang_x_sys",
+ importpath = "golang.org/x/sys",
+ sum = "h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=",
+ version = "v0.0.0-20190412213103-97732733099d",
+ )
+ go_repository(
+ name = "org_golang_x_text",
+ importpath = "golang.org/x/text",
+ sum = "h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=",
+ version = "v0.3.0",
+ )
+ go_repository(
+ name = "org_golang_x_tools",
+ build_directives = [
+ "gazelle:exclude **/testdata/**/*",
+ ],
+ importpath = "golang.org/x/tools",
+ sum = "h1:aZzprAO9/8oim3qStq3wc1Xuxx4QmAGriC4VU4ojemQ=",
+ version = "v0.0.0-20191119224855-298f0cb1881e",
+ )
+ go_repository(
+ name = "org_golang_x_xerrors",
+ importpath = "golang.org/x/xerrors",
+ sum = "h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=",
+ version = "v0.0.0-20191204190536-9bdfabe68543",
+ )
diff --git a/gazelle/fix.go b/gazelle/fix.go
new file mode 100644
index 0000000..c929929
--- /dev/null
+++ b/gazelle/fix.go
@@ -0,0 +1,13 @@
+package python
+
+import (
+ "github.com/bazelbuild/bazel-gazelle/config"
+ "github.com/bazelbuild/bazel-gazelle/rule"
+)
+
+// Fix repairs deprecated usage of language-specific rules in f. This is
+// called before the file is indexed. Unless c.ShouldFix is true, fixes
+// that delete or rename rules should not be performed.
+func (py *Python) Fix(c *config.Config, f *rule.File) {
+ // TODO(f0rmiga): implement.
+}
\ No newline at end of file
diff --git a/gazelle/generate.go b/gazelle/generate.go
new file mode 100644
index 0000000..6a0c3b6
--- /dev/null
+++ b/gazelle/generate.go
@@ -0,0 +1,379 @@
+package python
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/bazelbuild/bazel-gazelle/config"
+ "github.com/bazelbuild/bazel-gazelle/label"
+ "github.com/bazelbuild/bazel-gazelle/language"
+ "github.com/bazelbuild/bazel-gazelle/rule"
+ "github.com/bmatcuk/doublestar"
+ "github.com/emirpasic/gods/lists/singlylinkedlist"
+ "github.com/emirpasic/gods/sets/treeset"
+ godsutils "github.com/emirpasic/gods/utils"
+ "github.com/google/uuid"
+
+ "github.com/bazelbuild/rules_python/gazelle/pythonconfig"
+)
+
+const (
+ pyLibraryEntrypointFilename = "__init__.py"
+ pyBinaryEntrypointFilename = "__main__.py"
+ pyTestEntrypointFilename = "__test__.py"
+ pyTestEntrypointTargetname = "__test__"
+)
+
+var (
+ buildFilenames = []string{"BUILD", "BUILD.bazel"}
+ // errHaltDigging is an error that signals whether the generator should halt
+ // digging the source tree searching for modules in subdirectories.
+ errHaltDigging = fmt.Errorf("halt digging")
+)
+
+// GenerateRules extracts build metadata from source files in a directory.
+// GenerateRules is called in each directory where an update is requested
+// in depth-first post-order.
+func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateResult {
+ cfgs := args.Config.Exts[languageName].(pythonconfig.Configs)
+ cfg := cfgs[args.Rel]
+
+ if !cfg.ExtensionEnabled() {
+ return language.GenerateResult{}
+ }
+
+ if !isBazelPackage(args.Dir) {
+ if cfg.CoarseGrainedGeneration() {
+ // Determine if the current directory is the root of the coarse-grained
+ // generation. If not, return without generating anything.
+ parent := cfg.Parent()
+ if parent != nil && parent.CoarseGrainedGeneration() {
+ return language.GenerateResult{}
+ }
+ } else if !hasEntrypointFile(args.Dir) {
+ return language.GenerateResult{}
+ }
+ }
+
+ pythonProjectRoot := cfg.PythonProjectRoot()
+
+ packageName := filepath.Base(args.Dir)
+
+ pyLibraryFilenames := treeset.NewWith(godsutils.StringComparator)
+ pyTestFilenames := treeset.NewWith(godsutils.StringComparator)
+
+ // hasPyBinary controls whether a py_binary target should be generated for
+ // this package or not.
+ hasPyBinary := false
+
+ // hasPyTestFile and hasPyTestTarget control whether a py_test target should
+ // be generated for this package or not.
+ hasPyTestFile := false
+ hasPyTestTarget := false
+
+ for _, f := range args.RegularFiles {
+ if cfg.IgnoresFile(filepath.Base(f)) {
+ continue
+ }
+ ext := filepath.Ext(f)
+ if !hasPyBinary && f == pyBinaryEntrypointFilename {
+ hasPyBinary = true
+ } else if !hasPyTestFile && f == pyTestEntrypointFilename {
+ hasPyTestFile = true
+ } else if strings.HasSuffix(f, "_test.py") || (strings.HasPrefix(f, "test_") && ext == ".py") {
+ pyTestFilenames.Add(f)
+ } else if ext == ".py" {
+ pyLibraryFilenames.Add(f)
+ }
+ }
+
+ // If a __test__.py file was not found on disk, search for targets that are
+ // named __test__.
+ if !hasPyTestFile && args.File != nil {
+ for _, rule := range args.File.Rules {
+ if rule.Name() == pyTestEntrypointTargetname {
+ hasPyTestTarget = true
+ break
+ }
+ }
+ }
+
+ // Add files from subdirectories if they meet the criteria.
+ for _, d := range args.Subdirs {
+ // boundaryPackages represents child Bazel packages that are used as a
+ // boundary to stop processing under that tree.
+ boundaryPackages := make(map[string]struct{})
+ err := filepath.Walk(
+ filepath.Join(args.Dir, d),
+ func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ // Ignore the path if it crosses any boundary package. Walking
+ // the tree is still important because subsequent paths can
+ // represent files that have not crossed any boundaries.
+ for bp := range boundaryPackages {
+ if strings.HasPrefix(path, bp) {
+ return nil
+ }
+ }
+ if info.IsDir() {
+ // If we are visiting a directory, we determine if we should
+ // halt digging the tree based on a few criterias:
+ // 1. The directory has a BUILD or BUILD.bazel files. Then
+ // it doesn't matter at all what it has since it's a
+ // separate Bazel package.
+ // 2. (only for fine-grained generation) The directory has
+ // an __init__.py, __main__.py or __test__.py, meaning
+ // a BUILD file will be generated.
+ if isBazelPackage(path) {
+ boundaryPackages[path] = struct{}{}
+ return nil
+ }
+
+ if !cfg.CoarseGrainedGeneration() && hasEntrypointFile(path) {
+ return errHaltDigging
+ }
+
+ return nil
+ }
+ if filepath.Ext(path) == ".py" {
+ if cfg.CoarseGrainedGeneration() || !isEntrypointFile(path) {
+ f, _ := filepath.Rel(args.Dir, path)
+ excludedPatterns := cfg.ExcludedPatterns()
+ if excludedPatterns != nil {
+ it := excludedPatterns.Iterator()
+ for it.Next() {
+ excludedPattern := it.Value().(string)
+ isExcluded, err := doublestar.Match(excludedPattern, f)
+ if err != nil {
+ return err
+ }
+ if isExcluded {
+ return nil
+ }
+ }
+ }
+ baseName := filepath.Base(path)
+ if strings.HasSuffix(baseName, "_test.py") || strings.HasPrefix(baseName, "test_") {
+ pyTestFilenames.Add(f)
+ } else {
+ pyLibraryFilenames.Add(f)
+ }
+ }
+ }
+ return nil
+ },
+ )
+ if err != nil && err != errHaltDigging {
+ log.Printf("ERROR: %v\n", err)
+ return language.GenerateResult{}
+ }
+ }
+
+ parser := newPython3Parser(args.Config.RepoRoot, args.Rel, cfg.IgnoresDependency)
+ visibility := fmt.Sprintf("//%s:__subpackages__", pythonProjectRoot)
+
+ var result language.GenerateResult
+ result.Gen = make([]*rule.Rule, 0)
+
+ collisionErrors := singlylinkedlist.New()
+
+ if !hasPyTestFile && !hasPyTestTarget {
+ it := pyTestFilenames.Iterator()
+ for it.Next() {
+ pyLibraryFilenames.Add(it.Value())
+ }
+ }
+
+ var pyLibrary *rule.Rule
+ if !pyLibraryFilenames.Empty() {
+ deps, err := parser.parseAll(pyLibraryFilenames)
+ if err != nil {
+ log.Fatalf("ERROR: %v\n", err)
+ }
+
+ pyLibraryTargetName := cfg.RenderLibraryName(packageName)
+
+ // Check if a target with the same name we are generating alredy exists,
+ // and if it is of a different kind from the one we are generating. If
+ // so, we have to throw an error since Gazelle won't generate it
+ // correctly.
+ if args.File != nil {
+ for _, t := range args.File.Rules {
+ if t.Name() == pyLibraryTargetName && t.Kind() != pyLibraryKind {
+ fqTarget := label.New("", args.Rel, pyLibraryTargetName)
+ err := fmt.Errorf("failed to generate target %q of kind %q: "+
+ "a target of kind %q with the same name already exists. "+
+ "Use the '# gazelle:%s' directive to change the naming convention.",
+ fqTarget.String(), pyLibraryKind, t.Kind(), pythonconfig.LibraryNamingConvention)
+ collisionErrors.Add(err)
+ }
+ }
+ }
+
+ pyLibrary = newTargetBuilder(pyLibraryKind, pyLibraryTargetName, pythonProjectRoot, args.Rel).
+ setUUID(uuid.Must(uuid.NewUUID()).String()).
+ addVisibility(visibility).
+ addSrcs(pyLibraryFilenames).
+ addModuleDependencies(deps).
+ generateImportsAttribute().
+ build()
+
+ result.Gen = append(result.Gen, pyLibrary)
+ result.Imports = append(result.Imports, pyLibrary.PrivateAttr(config.GazelleImportsKey))
+ }
+
+ if hasPyBinary {
+ deps, err := parser.parse(pyBinaryEntrypointFilename)
+ if err != nil {
+ log.Fatalf("ERROR: %v\n", err)
+ }
+
+ pyBinaryTargetName := cfg.RenderBinaryName(packageName)
+
+ // Check if a target with the same name we are generating alredy exists,
+ // and if it is of a different kind from the one we are generating. If
+ // so, we have to throw an error since Gazelle won't generate it
+ // correctly.
+ if args.File != nil {
+ for _, t := range args.File.Rules {
+ if t.Name() == pyBinaryTargetName && t.Kind() != pyBinaryKind {
+ fqTarget := label.New("", args.Rel, pyBinaryTargetName)
+ err := fmt.Errorf("failed to generate target %q of kind %q: "+
+ "a target of kind %q with the same name already exists. "+
+ "Use the '# gazelle:%s' directive to change the naming convention.",
+ fqTarget.String(), pyBinaryKind, t.Kind(), pythonconfig.BinaryNamingConvention)
+ collisionErrors.Add(err)
+ }
+ }
+ }
+
+ pyBinaryTarget := newTargetBuilder(pyBinaryKind, pyBinaryTargetName, pythonProjectRoot, args.Rel).
+ setMain(pyBinaryEntrypointFilename).
+ addVisibility(visibility).
+ addSrc(pyBinaryEntrypointFilename).
+ addModuleDependencies(deps).
+ generateImportsAttribute()
+
+ if pyLibrary != nil {
+ pyBinaryTarget.addModuleDependency(module{Name: pyLibrary.PrivateAttr(uuidKey).(string)})
+ }
+
+ pyBinary := pyBinaryTarget.build()
+
+ result.Gen = append(result.Gen, pyBinary)
+ result.Imports = append(result.Imports, pyBinary.PrivateAttr(config.GazelleImportsKey))
+ }
+
+ if hasPyTestFile || hasPyTestTarget {
+ if hasPyTestFile {
+ // Only add the pyTestEntrypointFilename to the pyTestFilenames if
+ // the file exists on disk.
+ pyTestFilenames.Add(pyTestEntrypointFilename)
+ }
+ deps, err := parser.parseAll(pyTestFilenames)
+ if err != nil {
+ log.Fatalf("ERROR: %v\n", err)
+ }
+
+ pyTestTargetName := cfg.RenderTestName(packageName)
+
+ // Check if a target with the same name we are generating alredy exists,
+ // and if it is of a different kind from the one we are generating. If
+ // so, we have to throw an error since Gazelle won't generate it
+ // correctly.
+ if args.File != nil {
+ for _, t := range args.File.Rules {
+ if t.Name() == pyTestTargetName && t.Kind() != pyTestKind {
+ fqTarget := label.New("", args.Rel, pyTestTargetName)
+ err := fmt.Errorf("failed to generate target %q of kind %q: "+
+ "a target of kind %q with the same name already exists. "+
+ "Use the '# gazelle:%s' directive to change the naming convention.",
+ fqTarget.String(), pyTestKind, t.Kind(), pythonconfig.TestNamingConvention)
+ collisionErrors.Add(err)
+ }
+ }
+ }
+
+ pyTestTarget := newTargetBuilder(pyTestKind, pyTestTargetName, pythonProjectRoot, args.Rel).
+ addSrcs(pyTestFilenames).
+ addModuleDependencies(deps).
+ generateImportsAttribute()
+
+ if hasPyTestTarget {
+ entrypointTarget := fmt.Sprintf(":%s", pyTestEntrypointTargetname)
+ main := fmt.Sprintf(":%s", pyTestEntrypointFilename)
+ pyTestTarget.
+ addSrc(entrypointTarget).
+ addResolvedDependency(entrypointTarget).
+ setMain(main)
+ } else {
+ pyTestTarget.setMain(pyTestEntrypointFilename)
+ }
+
+ if pyLibrary != nil {
+ pyTestTarget.addModuleDependency(module{Name: pyLibrary.PrivateAttr(uuidKey).(string)})
+ }
+
+ pyTest := pyTestTarget.build()
+
+ result.Gen = append(result.Gen, pyTest)
+ result.Imports = append(result.Imports, pyTest.PrivateAttr(config.GazelleImportsKey))
+ }
+
+ if !collisionErrors.Empty() {
+ it := collisionErrors.Iterator()
+ for it.Next() {
+ log.Printf("ERROR: %v\n", it.Value())
+ }
+ os.Exit(1)
+ }
+
+ return result
+}
+
+// isBazelPackage determines if the directory is a Bazel package by probing for
+// the existence of a known BUILD file name.
+func isBazelPackage(dir string) bool {
+ for _, buildFilename := range buildFilenames {
+ path := filepath.Join(dir, buildFilename)
+ if _, err := os.Stat(path); err == nil {
+ return true
+ }
+ }
+ return false
+}
+
+// hasEntrypointFile determines if the directory has any of the established
+// entrypoint filenames.
+func hasEntrypointFile(dir string) bool {
+ for _, entrypointFilename := range []string{
+ pyLibraryEntrypointFilename,
+ pyBinaryEntrypointFilename,
+ pyTestEntrypointFilename,
+ } {
+ path := filepath.Join(dir, entrypointFilename)
+ if _, err := os.Stat(path); err == nil {
+ return true
+ }
+ }
+ return false
+}
+
+// isEntrypointFile returns whether the given path is an entrypoint file. The
+// given path can be absolute or relative.
+func isEntrypointFile(path string) bool {
+ basePath := filepath.Base(path)
+ switch basePath {
+ case pyLibraryEntrypointFilename,
+ pyBinaryEntrypointFilename,
+ pyTestEntrypointFilename:
+ return true
+ default:
+ return false
+ }
+}
diff --git a/gazelle/kinds.go b/gazelle/kinds.go
new file mode 100644
index 0000000..36fcf6e
--- /dev/null
+++ b/gazelle/kinds.go
@@ -0,0 +1,88 @@
+package python
+
+import (
+ "github.com/bazelbuild/bazel-gazelle/rule"
+)
+
+const (
+ pyBinaryKind = "py_binary"
+ pyLibraryKind = "py_library"
+ pyTestKind = "py_test"
+)
+
+// Kinds returns a map that maps rule names (kinds) and information on how to
+// match and merge attributes that may be found in rules of those kinds.
+func (*Python) Kinds() map[string]rule.KindInfo {
+ return pyKinds
+}
+
+var pyKinds = map[string]rule.KindInfo{
+ pyBinaryKind: {
+ MatchAny: true,
+ NonEmptyAttrs: map[string]bool{
+ "deps": true,
+ "main": true,
+ "srcs": true,
+ "imports": true,
+ "visibility": true,
+ },
+ SubstituteAttrs: map[string]bool{},
+ MergeableAttrs: map[string]bool{
+ "srcs": true,
+ },
+ ResolveAttrs: map[string]bool{
+ "deps": true,
+ },
+ },
+ pyLibraryKind: {
+ MatchAny: true,
+ NonEmptyAttrs: map[string]bool{
+ "deps": true,
+ "srcs": true,
+ "imports": true,
+ "visibility": true,
+ },
+ SubstituteAttrs: map[string]bool{},
+ MergeableAttrs: map[string]bool{
+ "srcs": true,
+ },
+ ResolveAttrs: map[string]bool{
+ "deps": true,
+ },
+ },
+ pyTestKind: {
+ MatchAny: true,
+ NonEmptyAttrs: map[string]bool{
+ "deps": true,
+ "main": true,
+ "srcs": true,
+ "imports": true,
+ "visibility": true,
+ },
+ SubstituteAttrs: map[string]bool{},
+ MergeableAttrs: map[string]bool{
+ "srcs": true,
+ },
+ ResolveAttrs: map[string]bool{
+ "deps": true,
+ },
+ },
+}
+
+// Loads returns .bzl files and symbols they define. Every rule generated by
+// GenerateRules, now or in the past, should be loadable from one of these
+// files.
+func (py *Python) Loads() []rule.LoadInfo {
+ return pyLoads
+}
+
+var pyLoads = []rule.LoadInfo{
+ {
+ Name: "@rules_python//python:defs.bzl",
+ Symbols: []string{
+ pyBinaryKind,
+ pyLibraryKind,
+ pyTestKind,
+ },
+ },
+}
\ No newline at end of file
diff --git a/gazelle/language.go b/gazelle/language.go
new file mode 100644
index 0000000..39ca6b3
--- /dev/null
+++ b/gazelle/language.go
@@ -0,0 +1,18 @@
+package python
+
+import (
+ "github.com/bazelbuild/bazel-gazelle/language"
+)
+
+// Python satisfies the language.Language interface. It is the Gazelle extension
+// for Python rules.
+type Python struct {
+ Configurer
+ Resolver
+}
+
+// NewLanguage initializes a new Python that satisfies the language.Language
+// interface. This is the entrypoint for the extension initialization.
+func NewLanguage() language.Language {
+ return &Python{}
+}
\ No newline at end of file
diff --git a/gazelle/manifest/BUILD.bazel b/gazelle/manifest/BUILD.bazel
new file mode 100644
index 0000000..2e5b6b8
--- /dev/null
+++ b/gazelle/manifest/BUILD.bazel
@@ -0,0 +1,16 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+ name = "manifest",
+ srcs = ["manifest.go"],
+ importpath = "github.com/bazelbuild/rules_python/gazelle/manifest",
+ visibility = ["//visibility:public"],
+ deps = ["@in_gopkg_yaml_v2//:yaml_v2"],
+)
+
+go_test(
+ name = "manifest_test",
+ srcs = ["manifest_test.go"],
+ data = glob(["testdata/**"]),
+ deps = [":manifest"],
+)
diff --git a/gazelle/manifest/defs.bzl b/gazelle/manifest/defs.bzl
new file mode 100644
index 0000000..fd555db
--- /dev/null
+++ b/gazelle/manifest/defs.bzl
@@ -0,0 +1,71 @@
+"""This module provides the gazelle_python_manifest macro that contains targets
+for updating and testing the Gazelle manifest file.
+"""
+
+load("@io_bazel_rules_go//go:def.bzl", "go_binary")
+
+def gazelle_python_manifest(
+ name,
+ requirements,
+ pip_deps_repository_name,
+ modules_mapping,
+ manifest = ":gazelle_python.yaml"):
+ """A macro for defining the updating and testing targets for the Gazelle manifest file.
+
+ Args:
+ name: the name used as a base for the targets.
+ requirements: the target for the requirements.txt file.
+ pip_deps_repository_name: the name of the pip_install repository target.
+ modules_mapping: the target for the generated modules_mapping.json file.
+ manifest: the target for the Gazelle manifest file.
+ """
+ update_target = "{}.update".format(name)
+ update_target_label = "//{}:{}".format(native.package_name(), update_target)
+
+ go_binary(
+ name = update_target,
+ embed = ["@rules_python//gazelle/manifest/generate:generate_lib"],
+ data = [
+ manifest,
+ modules_mapping,
+ requirements,
+ ],
+ args = [
+ "--requirements",
+ "$(rootpath {})".format(requirements),
+ "--pip-deps-repository-name",
+ pip_deps_repository_name,
+ "--modules-mapping",
+ "$(rootpath {})".format(modules_mapping),
+ "--output",
+ "$(rootpath {})".format(manifest),
+ "--update-target",
+ update_target_label,
+ ],
+ visibility = ["//visibility:private"],
+ tags = ["manual"],
+ )
+
+ test_binary = "_{}_test_bin".format(name)
+
+ go_binary(
+ name = test_binary,
+ embed = ["@rules_python//gazelle/manifest/test:test_lib"],
+ visibility = ["//visibility:private"],
+ )
+
+ native.sh_test(
+ name = "{}.test".format(name),
+ srcs = ["@rules_python//gazelle/manifest/test:run.sh"],
+ data = [
+ ":{}".format(test_binary),
+ manifest,
+ requirements,
+ ],
+ env = {
+ "_TEST_BINARY": "$(rootpath :{})".format(test_binary),
+ "_TEST_MANIFEST": "$(rootpath {})".format(manifest),
+ "_TEST_REQUIREMENTS": "$(rootpath {})".format(requirements),
+ },
+ visibility = ["//visibility:private"],
+ )
diff --git a/gazelle/manifest/generate/BUILD.bazel b/gazelle/manifest/generate/BUILD.bazel
new file mode 100644
index 0000000..29b9f15
--- /dev/null
+++ b/gazelle/manifest/generate/BUILD.bazel
@@ -0,0 +1,15 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "generate_lib",
+ srcs = ["generate.go"],
+ importpath = "github.com/bazelbuild/rules_python/gazelle/manifest/generate",
+ visibility = ["//visibility:public"],
+ deps = ["//gazelle/manifest"],
+)
+
+go_binary(
+ name = "generate",
+ embed = [":generate_lib"],
+ visibility = ["//visibility:public"],
+)
diff --git a/gazelle/manifest/generate/generate.go b/gazelle/manifest/generate/generate.go
new file mode 100644
index 0000000..1ed91bf
--- /dev/null
+++ b/gazelle/manifest/generate/generate.go
@@ -0,0 +1,145 @@
+/*
+generate.go is a program that generates the Gazelle YAML manifest.
+
+The Gazelle manifest is a file that contains extra information required when
+generating the Bazel BUILD files.
+*/
+package main
+
+import (
+ "encoding/json"
+ "flag"
+ "fmt"
+ "log"
+ "os"
+ "strings"
+
+ "github.com/bazelbuild/rules_python/gazelle/manifest"
+)
+
+func init() {
+ if os.Getenv("BUILD_WORKSPACE_DIRECTORY") == "" {
+ log.Fatalln("ERROR: this program must run under Bazel")
+ }
+}
+
+func main() {
+ var requirementsPath string
+ var pipDepsRepositoryName string
+ var modulesMappingPath string
+ var outputPath string
+ var updateTarget string
+ flag.StringVar(
+ &requirementsPath,
+ "requirements",
+ "",
+ "The requirements.txt file.")
+ flag.StringVar(
+ &pipDepsRepositoryName,
+ "pip-deps-repository-name",
+ "",
+ "The name of the pip_install repository target.")
+ flag.StringVar(
+ &modulesMappingPath,
+ "modules-mapping",
+ "",
+ "The modules_mapping.json file.")
+ flag.StringVar(
+ &outputPath,
+ "output",
+ "",
+ "The output YAML manifest file.")
+ flag.StringVar(
+ &updateTarget,
+ "update-target",
+ "",
+ "The Bazel target to update the YAML manifest file.")
+ flag.Parse()
+
+ if requirementsPath == "" {
+ log.Fatalln("ERROR: --requirements must be set")
+ }
+
+ if modulesMappingPath == "" {
+ log.Fatalln("ERROR: --modules-mapping must be set")
+ }
+
+ if outputPath == "" {
+ log.Fatalln("ERROR: --output must be set")
+ }
+
+ if updateTarget == "" {
+ log.Fatalln("ERROR: --update-target must be set")
+ }
+
+ modulesMapping, err := unmarshalJSON(modulesMappingPath)
+ if err != nil {
+ log.Fatalf("ERROR: %v\n", err)
+ }
+
+ header := generateHeader(updateTarget)
+
+ manifestFile := manifest.NewFile(&manifest.Manifest{
+ ModulesMapping: modulesMapping,
+ PipDepsRepositoryName: pipDepsRepositoryName,
+ })
+ if err := writeOutput(outputPath, header, manifestFile, requirementsPath); err != nil {
+ log.Fatalf("ERROR: %v\n", err)
+ }
+}
+
+// unmarshalJSON returns the parsed mapping from the given JSON file path.
+func unmarshalJSON(jsonPath string) (map[string]string, error) {
+ file, err := os.Open(jsonPath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to unmarshal JSON file: %w", err)
+ }
+ defer file.Close()
+
+ decoder := json.NewDecoder(file)
+ output := make(map[string]string)
+ if err := decoder.Decode(&output); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal JSON file: %w", err)
+ }
+
+ return output, nil
+}
+
+// generateHeader generates the YAML header human-readable comment.
+func generateHeader(updateTarget string) string {
+ var header strings.Builder
+ header.WriteString("# GENERATED FILE - DO NOT EDIT!\n")
+ header.WriteString("#\n")
+ header.WriteString("# To update this file, run:\n")
+ header.WriteString(fmt.Sprintf("# bazel run %s\n", updateTarget))
+ return header.String()
+}
+
+// writeOutput writes to the final file the header and manifest structure.
+func writeOutput(
+ outputPath string,
+ header string,
+ manifestFile *manifest.File,
+ requirementsPath string,
+) error {
+ stat, err := os.Stat(outputPath)
+ if err != nil {
+ return fmt.Errorf("failed to write output: %w", err)
+ }
+
+ outputFile, err := os.OpenFile(outputPath, os.O_WRONLY|os.O_TRUNC, stat.Mode())
+ if err != nil {
+ return fmt.Errorf("failed to write output: %w", err)
+ }
+ defer outputFile.Close()
+
+ if _, err := fmt.Fprintf(outputFile, "%s\n", header); err != nil {
+ return fmt.Errorf("failed to write output: %w", err)
+ }
+
+ if err := manifestFile.Encode(outputFile, requirementsPath); err != nil {
+ return fmt.Errorf("failed to write output: %w", err)
+ }
+
+ return nil
+}
\ No newline at end of file
diff --git a/gazelle/manifest/manifest.go b/gazelle/manifest/manifest.go
new file mode 100644
index 0000000..4d432da
--- /dev/null
+++ b/gazelle/manifest/manifest.go
@@ -0,0 +1,120 @@
+package manifest
+
+import (
+ "crypto/sha256"
+ "fmt"
+ "io"
+ "os"
+
+ yaml "gopkg.in/yaml.v2"
+)
+
+// File represents the gazelle_python.yaml file.
+type File struct {
+ Manifest *Manifest `yaml:"manifest,omitempty"`
+ // Integrity is the hash of the requirements.txt file and the Manifest for
+ // ensuring the integrity of the entire gazelle_python.yaml file. This
+ // controls the testing to keep the gazelle_python.yaml file up-to-date.
+ Integrity string `yaml:"integrity"`
+}
+
+// NewFile creates a new File with a given Manifest.
+func NewFile(manifest *Manifest) *File {
+ return &File{Manifest: manifest}
+}
+
+// Encode encodes the manifest file to the given writer.
+func (f *File) Encode(w io.Writer, requirementsPath string) error {
+ requirementsChecksum, err := sha256File(requirementsPath)
+ if err != nil {
+ return fmt.Errorf("failed to encode manifest file: %w", err)
+ }
+ integrityBytes, err := f.calculateIntegrity(requirementsChecksum)
+ if err != nil {
+ return fmt.Errorf("failed to encode manifest file: %w", err)
+ }
+ f.Integrity = fmt.Sprintf("%x", integrityBytes)
+ encoder := yaml.NewEncoder(w)
+ defer encoder.Close()
+ if err := encoder.Encode(f); err != nil {
+ return fmt.Errorf("failed to encode manifest file: %w", err)
+ }
+ return nil
+}
+
+// VerifyIntegrity verifies if the integrity set in the File is valid.
+func (f *File) VerifyIntegrity(requirementsPath string) (bool, error) {
+ requirementsChecksum, err := sha256File(requirementsPath)
+ if err != nil {
+ return false, fmt.Errorf("failed to verify integrity: %w", err)
+ }
+ integrityBytes, err := f.calculateIntegrity(requirementsChecksum)
+ if err != nil {
+ return false, fmt.Errorf("failed to verify integrity: %w", err)
+ }
+ valid := (f.Integrity == fmt.Sprintf("%x", integrityBytes))
+ return valid, nil
+}
+
+// calculateIntegrity calculates the integrity of the manifest file based on the
+// provided checksum for the requirements.txt file used as input to the modules
+// mapping, plus the manifest structure in the manifest file. This integrity
+// calculation ensures the manifest files are kept up-to-date.
+func (f *File) calculateIntegrity(requirementsChecksum []byte) ([]byte, error) {
+ hash := sha256.New()
+
+ // Sum the manifest part of the file.
+ encoder := yaml.NewEncoder(hash)
+ defer encoder.Close()
+ if err := encoder.Encode(f.Manifest); err != nil {
+ return nil, fmt.Errorf("failed to calculate integrity: %w", err)
+ }
+
+ // Sum the requirements.txt checksum bytes.
+ if _, err := hash.Write(requirementsChecksum); err != nil {
+ return nil, fmt.Errorf("failed to calculate integrity: %w", err)
+ }
+
+ return hash.Sum(nil), nil
+}
+
+// Decode decodes the manifest file from the given path.
+func (f *File) Decode(manifestPath string) error {
+ file, err := os.Open(manifestPath)
+ if err != nil {
+ return fmt.Errorf("failed to decode manifest file: %w", err)
+ }
+ defer file.Close()
+
+ decoder := yaml.NewDecoder(file)
+ if err := decoder.Decode(f); err != nil {
+ return fmt.Errorf("failed to decode manifest file: %w", err)
+ }
+
+ return nil
+}
+
+// Manifest represents the structure of the Gazelle manifest file.
+type Manifest struct {
+ // ModulesMapping is the mapping from importable modules to which Python
+ // wheel name provides these modules.
+ ModulesMapping map[string]string `yaml:"modules_mapping"`
+ // PipDepsRepositoryName is the name of the pip_install repository target.
+ PipDepsRepositoryName string `yaml:"pip_deps_repository_name"`
+}
+
+// sha256File calculates the checksum of a given file path.
+func sha256File(filePath string) ([]byte, error) {
+ file, err := os.Open(filePath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to calculate sha256 sum for file: %w", err)
+ }
+ defer file.Close()
+
+ hash := sha256.New()
+ if _, err := io.Copy(hash, file); err != nil {
+ return nil, fmt.Errorf("failed to calculate sha256 sum for file: %w", err)
+ }
+
+ return hash.Sum(nil), nil
+}
diff --git a/gazelle/manifest/manifest_test.go b/gazelle/manifest/manifest_test.go
new file mode 100644
index 0000000..40a231f
--- /dev/null
+++ b/gazelle/manifest/manifest_test.go
@@ -0,0 +1,79 @@
+package manifest_test
+
+import (
+ "bytes"
+ "io/ioutil"
+ "log"
+ "reflect"
+ "testing"
+
+ "github.com/bazelbuild/rules_python/gazelle/manifest"
+)
+
+var modulesMapping = map[string]string{
+ "arrow": "arrow",
+ "arrow.__init__": "arrow",
+ "arrow.api": "arrow",
+ "arrow.arrow": "arrow",
+ "arrow.factory": "arrow",
+ "arrow.formatter": "arrow",
+ "arrow.locales": "arrow",
+ "arrow.parser": "arrow",
+ "arrow.util": "arrow",
+}
+
+const pipDepsRepositoryName = "test_repository_name"
+
+func TestFile(t *testing.T) {
+ t.Run("Encode", func(t *testing.T) {
+ f := manifest.NewFile(&manifest.Manifest{
+ ModulesMapping: modulesMapping,
+ PipDepsRepositoryName: pipDepsRepositoryName,
+ })
+ var b bytes.Buffer
+ if err := f.Encode(&b, "testdata/requirements.txt"); err != nil {
+ log.Println(err)
+ t.FailNow()
+ }
+ expected, err := ioutil.ReadFile("testdata/gazelle_python.yaml")
+ if err != nil {
+ log.Println(err)
+ t.FailNow()
+ }
+ if !bytes.Equal(expected, b.Bytes()) {
+ log.Printf("encoded manifest doesn't match expected output: %v\n", b.String())
+ t.FailNow()
+ }
+ })
+ t.Run("Decode", func(t *testing.T) {
+ f := manifest.NewFile(&manifest.Manifest{})
+ if err := f.Decode("testdata/gazelle_python.yaml"); err != nil {
+ log.Println(err)
+ t.FailNow()
+ }
+ if !reflect.DeepEqual(modulesMapping, f.Manifest.ModulesMapping) {
+ log.Println("decoded modules_mapping doesn't match expected value")
+ t.FailNow()
+ }
+ if f.Manifest.PipDepsRepositoryName != pipDepsRepositoryName {
+ log.Println("decoded pip repository name doesn't match expected value")
+ t.FailNow()
+ }
+ })
+ t.Run("VerifyIntegrity", func(t *testing.T) {
+ f := manifest.NewFile(&manifest.Manifest{})
+ if err := f.Decode("testdata/gazelle_python.yaml"); err != nil {
+ log.Println(err)
+ t.FailNow()
+ }
+ valid, err := f.VerifyIntegrity("testdata/requirements.txt")
+ if err != nil {
+ log.Println(err)
+ t.FailNow()
+ }
+ if !valid {
+ log.Println("decoded manifest file is not valid")
+ t.FailNow()
+ }
+ })
+}
\ No newline at end of file
diff --git a/gazelle/manifest/test/BUILD.bazel b/gazelle/manifest/test/BUILD.bazel
new file mode 100644
index 0000000..f14845f
--- /dev/null
+++ b/gazelle/manifest/test/BUILD.bazel
@@ -0,0 +1,17 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "test_lib",
+ srcs = ["test.go"],
+ importpath = "github.com/bazelbuild/rules_python/gazelle/manifest/test",
+ visibility = ["//visibility:public"],
+ deps = ["//gazelle/manifest"],
+)
+
+go_binary(
+ name = "test",
+ embed = [":test_lib"],
+ visibility = ["//visibility:public"],
+)
+
+exports_files(["run.sh"])
diff --git a/gazelle/manifest/test/run.sh b/gazelle/manifest/test/run.sh
new file mode 100755
index 0000000..4b24b51
--- /dev/null
+++ b/gazelle/manifest/test/run.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+# This file exists to allow passing the runfile paths to the Go program via
+# environment variables.
+
+set -o errexit -o nounset
+
+"${_TEST_BINARY}" --requirements "${_TEST_REQUIREMENTS}" --manifest "${_TEST_MANIFEST}"
\ No newline at end of file
diff --git a/gazelle/manifest/test/test.go b/gazelle/manifest/test/test.go
new file mode 100644
index 0000000..518fe06
--- /dev/null
+++ b/gazelle/manifest/test/test.go
@@ -0,0 +1,63 @@
+/*
+test.go is a program that asserts the Gazelle YAML manifest is up-to-date in
+regards to the requirements.txt.
+
+It re-hashes the requirements.txt and compares it to the recorded one in the
+existing generated Gazelle manifest.
+*/
+package main
+
+import (
+ "flag"
+ "log"
+ "path/filepath"
+
+ "github.com/bazelbuild/rules_python/gazelle/manifest"
+)
+
+func main() {
+ var requirementsPath string
+ var manifestPath string
+ flag.StringVar(
+ &requirementsPath,
+ "requirements",
+ "",
+ "The requirements.txt file.")
+ flag.StringVar(
+ &manifestPath,
+ "manifest",
+ "",
+ "The manifest YAML file.")
+ flag.Parse()
+
+ if requirementsPath == "" {
+ log.Fatalln("ERROR: --requirements must be set")
+ }
+
+ if manifestPath == "" {
+ log.Fatalln("ERROR: --manifest must be set")
+ }
+
+ manifestFile := new(manifest.File)
+ if err := manifestFile.Decode(manifestPath); err != nil {
+ log.Fatalf("ERROR: %v\n", err)
+ }
+
+ if manifestFile.Integrity == "" {
+ log.Fatalln("ERROR: failed to find the Gazelle manifest file integrity")
+ }
+
+ valid, err := manifestFile.VerifyIntegrity(requirementsPath)
+ if err != nil {
+ log.Fatalf("ERROR: %v\n", err)
+ }
+ if !valid {
+ manifestRealpath, err := filepath.EvalSymlinks(manifestPath)
+ if err != nil {
+ log.Fatalf("ERROR: %v\n", err)
+ }
+ log.Fatalf(
+ "ERROR: %q is out-of-date, follow the intructions on this file for updating.\n",
+ manifestRealpath)
+ }
+}
\ No newline at end of file
diff --git a/gazelle/manifest/testdata/gazelle_python.yaml b/gazelle/manifest/testdata/gazelle_python.yaml
new file mode 100644
index 0000000..4dc1f2c
--- /dev/null
+++ b/gazelle/manifest/testdata/gazelle_python.yaml
@@ -0,0 +1,13 @@
+manifest:
+ modules_mapping:
+ arrow: arrow
+ arrow.__init__: arrow
+ arrow.api: arrow
+ arrow.arrow: arrow
+ arrow.factory: arrow
+ arrow.formatter: arrow
+ arrow.locales: arrow
+ arrow.parser: arrow
+ arrow.util: arrow
+ pip_deps_repository_name: test_repository_name
+integrity: 624f5f6c078eb44b907efd5a64e308354ac3620c568232b815668bcdf3e3366a
diff --git a/gazelle/manifest/testdata/requirements.txt b/gazelle/manifest/testdata/requirements.txt
new file mode 100644
index 0000000..9dd49a6
--- /dev/null
+++ b/gazelle/manifest/testdata/requirements.txt
@@ -0,0 +1,3 @@
+# This is a file for testing only.
+
+arrow==0.12.1
\ No newline at end of file
diff --git a/gazelle/modules_mapping/BUILD.bazel b/gazelle/modules_mapping/BUILD.bazel
new file mode 100644
index 0000000..4ce6a00
--- /dev/null
+++ b/gazelle/modules_mapping/BUILD.bazel
@@ -0,0 +1,4 @@
+exports_files([
+ "builder.py",
+ "generator.py",
+])
diff --git a/gazelle/modules_mapping/builder.py b/gazelle/modules_mapping/builder.py
new file mode 100644
index 0000000..352bfcb
--- /dev/null
+++ b/gazelle/modules_mapping/builder.py
@@ -0,0 +1,70 @@
+import argparse
+import multiprocessing
+import subprocess
+import sys
+from datetime import datetime
+
+mutex = multiprocessing.Lock()
+
+
+def build(wheel):
+ print("{}: building {}".format(datetime.now(), wheel), file=sys.stderr)
+ process = subprocess.run(
+ [sys.executable, "-m", "build", "--wheel", "--no-isolation"], cwd=wheel
+ )
+ if process.returncode != 0:
+ # If the build without isolation fails, try to build it again with
+ # isolation. We need to protect this following logic in two ways:
+ # 1. Only build one at a time in this process.
+ # 2. Retry a few times to get around flakiness.
+ success = False
+ for _ in range(0, 3):
+ with mutex:
+ process = subprocess.run(
+ [sys.executable, "-m", "build", "--wheel"],
+ encoding="utf-8",
+ cwd=wheel,
+ capture_output=True,
+ )
+ if process.returncode != 0:
+ continue
+ success = True
+ break
+ if not success:
+ print("STDOUT:", file=sys.stderr)
+ print(process.stdout, file=sys.stderr)
+ print("STDERR:", file=sys.stderr)
+ print(process.stderr, file=sys.stderr)
+ raise RuntimeError(
+ "{}: ERROR: failed to build {}".format(datetime.now(), wheel)
+ )
+
+
+def main(jobs, wheels):
+ with multiprocessing.Pool(jobs) as pool:
+ results = []
+ for wheel in wheels:
+ result = pool.apply_async(build, args=(wheel,))
+ results.append(result)
+ pool.close()
+ for result in results:
+ result.get()
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description="Builds Python wheels.")
+ parser.add_argument(
+ "wheels",
+ metavar="wheel",
+ type=str,
+ nargs="+",
+ help="A path to the extracted wheel directory.",
+ )
+ parser.add_argument(
+ "--jobs",
+ type=int,
+ default=8,
+ help="The number of concurrent build jobs to be executed.",
+ )
+ args = parser.parse_args()
+ exit(main(args.jobs, args.wheels))
\ No newline at end of file
diff --git a/gazelle/modules_mapping/def.bzl b/gazelle/modules_mapping/def.bzl
new file mode 100644
index 0000000..e01ebd3
--- /dev/null
+++ b/gazelle/modules_mapping/def.bzl
@@ -0,0 +1,331 @@
+"""Definitions for the modules_mapping.json generation.
+
+The modules_mapping.json file is a mapping from Python modules to the wheel
+names that provide those modules. It is used for determining which wheel
+distribution should be used in the `deps` attribute of `py_*` targets.
+
+This mapping is necessary when reading Python import statements and determining
+if they are provided by third-party dependencies. Most importantly, when the
+module name doesn't match the wheel distribution name.
+
+Currently, this module only works with requirements.txt files locked using
+pip-tools (https://github.com/jazzband/pip-tools) with hashes. This is necessary
+in order to keep downloaded wheels in the Bazel cache. Also, the
+modules_mapping rule does not consider extras as specified by PEP 508.
+"""
+
+# _modules_mapping_impl is the root entry for the modules_mapping rule
+# implementation.
+def _modules_mapping_impl(rctx):
+ requirements_data = rctx.read(rctx.attr.requirements)
+ python_interpreter = _get_python_interpreter(rctx)
+ pythonpath = "{}/__pythonpath".format(rctx.path(""))
+ res = rctx.execute(
+ [
+ python_interpreter,
+ "-m",
+ "pip",
+ "--verbose",
+ "--isolated",
+ "install",
+ "--target={}".format(pythonpath),
+ "--upgrade",
+ "--no-build-isolation",
+ "--no-cache-dir",
+ "--disable-pip-version-check",
+ "--index-url={}".format(rctx.attr.pip_index_url),
+ "build=={}".format(rctx.attr.build_wheel_version),
+ "setuptools=={}".format(rctx.attr.setuptools_wheel_version),
+ ],
+ quiet = rctx.attr.quiet,
+ timeout = rctx.attr.install_build_timeout,
+ )
+ if res.return_code != 0:
+ fail(res.stderr)
+ parsed_requirements = _parse_requirements_txt(requirements_data)
+ wheels = _get_wheels(rctx, python_interpreter, pythonpath, parsed_requirements)
+ res = rctx.execute(
+ [
+ python_interpreter,
+ rctx.path(rctx.attr._generator),
+ ] + wheels,
+ quiet = rctx.attr.quiet,
+ timeout = rctx.attr.generate_timeout,
+ )
+ if res.return_code != 0:
+ fail(res.stderr)
+ rctx.file("modules_mapping.json", content = res.stdout)
+ rctx.file("print.sh", content = "#!/usr/bin/env bash\ncat $1", executable = True)
+ rctx.file("BUILD", """\
+exports_files(["modules_mapping.json"])
+
+sh_binary(
+ name = "print",
+ srcs = ["print.sh"],
+ data = [":modules_mapping.json"],
+ args = ["$(rootpath :modules_mapping.json)"],
+)
+""")
+
+# _get_python_interpreter determines whether the system or the user-provided
+# Python interpreter should be used and returns the path to be called.
+def _get_python_interpreter(rctx):
+ if rctx.attr.python_interpreter == None:
+ return "python"
+ return rctx.path(rctx.attr.python_interpreter)
+
+# _parse_requirements_txt parses the requirements.txt data into structs with the
+# information needed to download them using Bazel.
+def _parse_requirements_txt(data):
+ result = []
+ lines = data.split("\n")
+ current_requirement = ""
+ continue_previous_line = False
+ for line in lines:
+ # Ignore empty lines and comments.
+ if len(line) == 0 or line.startswith("#"):
+ continue
+
+ line = line.strip()
+
+ stripped_backslash = False
+ if line.endswith("\\"):
+ line = line[:-1]
+ stripped_backslash = True
+
+ # If this line is a continuation of the previous one, append the current
+ # line to the current requirement being processed, otherwise, start a
+ # new requirement.
+ if continue_previous_line:
+ current_requirement += line
+ else:
+ current_requirement = line
+
+ # Control whether the next line in the requirements.txt should be a
+ # continuation of the current requirement being processed or not.
+ continue_previous_line = stripped_backslash
+ if not continue_previous_line:
+ result.append(_parse_requirement(current_requirement))
+ return result
+
+# _parse_requirement parses a single requirement line.
+def _parse_requirement(requirement_line):
+ split = requirement_line.split("==")
+ requirement = {}
+
+ # Removing the extras (https://www.python.org/dev/peps/pep-0508/#extras)
+ # from the requirement name is fine since it's expected that the
+ # requirements.txt was compiled with pip-tools, which includes the extras as
+ # direct dependencies.
+ name = _remove_extras_from_name(split[0])
+ requirement["name"] = name
+ if len(split) == 1:
+ return struct(**requirement)
+ split = split[1].split(" ")
+ requirement["version"] = split[0]
+ if len(split) == 1:
+ return struct(**requirement)
+ args = split[1:]
+ hashes = []
+ for arg in args:
+ arg = arg.strip()
+
+ # Skip empty arguments.
+ if len(arg) == 0:
+ continue
+
+ # Halt processing if it hits a comment.
+ if arg.startswith("#"):
+ break
+ if arg.startswith("--hash="):
+ hashes.append(arg[len("--hash="):])
+ requirement["hashes"] = hashes
+ return struct(**requirement)
+
+# _remove_extras_from_name removes the [extras] from a requirement.
+# https://www.python.org/dev/peps/pep-0508/#extras
+def _remove_extras_from_name(name):
+ bracket_index = name.find("[")
+ if bracket_index == -1:
+ return name
+ return name[:bracket_index]
+
+# _get_wheels returns the wheel distributions for the given requirements. It
+# uses a few different strategies depending on whether compiled wheel
+# distributions exist on the remote index or not. The order in which it
+# operates:
+#
+# 1. Try to use the platform-independent compiled wheel (*-none-any.whl).
+# 2. Try to use the first match of the linux-dependent compiled wheel from the
+# sorted releases list. This is valid as it's deterministic and the Python
+# extension for Gazelle doesn't support other platform-specific wheels
+# (one must use manual means to accomplish platform-specific dependency
+# resolution).
+# 3. Use the published source for the wheel.
+def _get_wheels(rctx, python_interpreter, pythonpath, requirements):
+ wheels = []
+ to_build = []
+ for requirement in requirements:
+ if not hasattr(requirement, "hashes"):
+ if hasattr(requirement, "name") and requirement.name.startswith("#"):
+ # This is a comment in the requirements file.
+ continue
+ else:
+ fail("missing requirement hash for {}-{}: use pip-tools to produce a locked file".format(
+ requirement.name,
+ requirement.version,
+ ))
+
+ wheel = {}
+ wheel["name"] = requirement.name
+
+ requirement_info_url = "{index_base}/{name}/{version}/json".format(
+ index_base = rctx.attr.index_base,
+ name = requirement.name,
+ version = requirement.version,
+ )
+ requirement_info_path = "{}_info.json".format(requirement.name)
+
+ # TODO(f0rmiga): if the logs are too spammy, use rctx.execute with
+ # Python to perform the downloads since it's impossible to get the
+ # checksums of these JSON files and there's no option to mute Bazel
+ # here.
+ rctx.download(requirement_info_url, output = requirement_info_path)
+ requirement_info = json.decode(rctx.read(requirement_info_path))
+ if requirement.version in requirement_info["releases"]:
+ wheel["version"] = requirement.version
+ elif requirement.version.endswith(".0") and requirement.version[:-len(".0")] in requirement_info["releases"]:
+ wheel["version"] = requirement.version[:-len(".0")]
+ else:
+ fail("missing requirement version \"{}\" for wheel \"{}\" in fetched releases: available {}".format(
+ requirement.version,
+ requirement.name,
+ [version for version in requirement_info["releases"]],
+ ))
+ releases = sorted(requirement_info["releases"][wheel["version"]], key = _sort_release_by_url)
+ (wheel_url, sha256) = _search_url(releases, "-none-any.whl")
+
+ # TODO(f0rmiga): handle PEP 600.
+ # https://www.python.org/dev/peps/pep-0600/
+ if not wheel_url:
+ # Search for the Linux tag as defined in PEP 599.
+ (wheel_url, sha256) = _search_url(releases, "manylinux2014_x86_64")
+ if not wheel_url:
+ # Search for the Linux tag as defined in PEP 571.
+ (wheel_url, sha256) = _search_url(releases, "manylinux2010_x86_64")
+ if not wheel_url:
+ # Search for the Linux tag as defined in PEP 513.
+ (wheel_url, sha256) = _search_url(releases, "manylinux1_x86_64")
+ if not wheel_url:
+ # Search for the MacOS tag
+ (wheel_url, sha256) = _search_url(releases, "macosx_10_9_x86_64")
+
+ if wheel_url:
+ wheel_path = wheel_url.split("/")[-1]
+ rctx.download(wheel_url, output = wheel_path, sha256 = sha256)
+ wheel["path"] = wheel_path
+ else:
+ extension = ".tar.gz"
+ (src_url, sha256) = _search_url(releases, extension)
+ if not src_url:
+ extension = ".zip"
+ (src_url, sha256) = _search_url(releases, extension)
+ if not src_url:
+ fail("requirement URL for {}-{} not found".format(requirement.name, wheel["version"]))
+ rctx.download_and_extract(src_url, sha256 = sha256)
+ sanitized_name = requirement.name.lower().replace("-", "_")
+ requirement_path = src_url.split("/")[-1]
+ requirement_path = requirement_path[:-len(extension)]
+
+ # The resulting filename for the .whl file is not feasible to
+ # predict as it has too many variations, so we defer it to the
+ # Python globing to find the right file name since only one .whl
+ # file should be generated by the compilation.
+ wheel_path = "{}/**/*.whl".format(requirement_path)
+ wheel["path"] = wheel_path
+ to_build.append(requirement_path)
+
+ wheels.append(json.encode(wheel))
+
+ if len(to_build) > 0:
+ res = rctx.execute(
+ [python_interpreter, rctx.path(rctx.attr._builder)] + to_build,
+ quiet = rctx.attr.quiet,
+ environment = {
+ # To avoid use local "pip.conf"
+ "HOME": str(rctx.path("").realpath),
+ # Make uses of pip to use the requested index
+ "PIP_INDEX_URL": rctx.attr.pip_index_url,
+ "PYTHONPATH": pythonpath,
+ },
+ )
+ if res.return_code != 0:
+ fail(res.stderr)
+
+ return wheels
+
+# _sort_release_by_url is the custom function for the key property of the sorted
+# releases.
+def _sort_release_by_url(release):
+ return release["url"]
+
+# _search_url searches for a release in the list of releases that has a url
+# matching the provided extension.
+def _search_url(releases, extension):
+ for release in releases:
+ url = release["url"]
+ if url.find(extension) >= 0:
+ return (url, release["digests"]["sha256"])
+ return (None, None)
+
+modules_mapping = repository_rule(
+ _modules_mapping_impl,
+ attrs = {
+ "build_wheel_version": attr.string(
+ default = "0.5.1",
+ doc = "The build wheel version.",
+ ),
+ "generate_timeout": attr.int(
+ default = 30,
+ doc = "The timeout for the generator.py command.",
+ ),
+ "index_base": attr.string(
+ default = "https://pypi.org/pypi",
+ doc = "The base URL used for querying releases data as JSON.",
+ ),
+ "install_build_timeout": attr.int(
+ default = 30,
+ doc = "The timeout for the `pip install build` command.",
+ ),
+ "pip_index_url": attr.string(
+ default = "https://pypi.python.org/simple",
+ doc = "The index URL used for any pip install actions",
+ ),
+ "python_interpreter": attr.label(
+ allow_single_file = True,
+ doc = "If set, uses the custom-built Python interpreter, otherwise, uses the system one.",
+ ),
+ "quiet": attr.bool(
+ default = True,
+ doc = "Toggle this attribute to get verbose output from this rule.",
+ ),
+ "requirements": attr.label(
+ allow_single_file = True,
+ doc = "The requirements.txt file with hashes locked using pip-tools.",
+ mandatory = True,
+ ),
+ "setuptools_wheel_version": attr.string(
+ default = "v57.5.0",
+ doc = "The setuptools wheel version.",
+ ),
+ "_builder": attr.label(
+ allow_single_file = True,
+ default = "//gazelle/modules_mapping:builder.py",
+ ),
+ "_generator": attr.label(
+ allow_single_file = True,
+ default = "//gazelle/modules_mapping:generator.py",
+ ),
+ },
+ doc = "Creates a modules_mapping.json file for mapping module names to wheel distribution names.",
+)
diff --git a/gazelle/modules_mapping/generator.py b/gazelle/modules_mapping/generator.py
new file mode 100644
index 0000000..44cfcf6
--- /dev/null
+++ b/gazelle/modules_mapping/generator.py
@@ -0,0 +1,80 @@
+import glob
+import json
+import pathlib
+import sys
+import zipfile
+
+
+# Generator is the modules_mapping.json file generator.
+class Generator:
+ stdout = None
+ stderr = None
+
+ def __init__(self, stdout, stderr):
+ self.stdout = stdout
+ self.stderr = stderr
+
+ # dig_wheel analyses the wheel .whl file determining the modules it provides
+ # by looking at the directory structure.
+ def dig_wheel(self, wheel):
+ mapping = {}
+ wheel_paths = glob.glob(wheel["path"])
+ assert (
+ len(wheel_paths) != 0
+ ), "wheel not found for {}: searched for {}".format(
+ wheel["name"], wheel["path"],
+ )
+ wheel_path = wheel_paths[0]
+ assert (
+ "UNKNOWN" not in wheel_path
+ ), "unknown-named wheel found for {}: possibly bad compilation".format(
+ wheel["name"],
+ )
+ with zipfile.ZipFile(wheel_path, "r") as zip_file:
+ for path in zip_file.namelist():
+ if is_metadata(path):
+ continue
+ ext = pathlib.Path(path).suffix
+ if ext == ".py" or ext == ".so":
+ # Note the '/' here means that the __init__.py is not in the
+ # root of the wheel, therefore we can index the directory
+ # where this file is as an importable package.
+ if path.endswith("/__init__.py"):
+ module = path[: -len("/__init__.py")].replace("/", ".")
+ mapping[module] = wheel["name"]
+ # Always index the module file.
+ if ext == ".so":
+ # Also remove extra metadata that is embeded as part of
+ # the file name as an extra extension.
+ ext = ''.join(pathlib.Path(path).suffixes)
+ module = path[: -len(ext)].replace("/", ".")
+ mapping[module] = wheel["name"]
+ return mapping
+
+ # run is the entrypoint for the generator.
+ def run(self, wheels):
+ mapping = {}
+ for wheel_json in wheels:
+ wheel = json.loads(wheel_json)
+ try:
+ mapping.update(self.dig_wheel(wheel))
+ except AssertionError as error:
+ print(error, file=self.stderr)
+ return 1
+ mapping_json = json.dumps(mapping)
+ print(mapping_json, file=self.stdout)
+ self.stdout.flush()
+ return 0
+
+
+# is_metadata checks if the path is in a metadata directory.
+# Ref: https://www.python.org/dev/peps/pep-0427/#file-contents.
+def is_metadata(path):
+ top_level = path.split("/")[0].lower()
+ return top_level.endswith(".dist-info") or top_level.endswith(".data")
+
+
+if __name__ == "__main__":
+ wheels = sys.argv[1:]
+ generator = Generator(sys.stdout, sys.stderr)
+ exit(generator.run(wheels))
\ No newline at end of file
diff --git a/gazelle/parse.py b/gazelle/parse.py
new file mode 100644
index 0000000..bbc9e97
--- /dev/null
+++ b/gazelle/parse.py
@@ -0,0 +1,63 @@
+# parse.py is a long-living program that communicates over STDIN and STDOUT.
+# STDIN receives filepaths, one per line. For each parsed file, it outputs to
+# STDOUT the modules parsed out of the import statements.
+
+import ast
+import json
+import sys
+from io import BytesIO
+from tokenize import COMMENT, tokenize
+
+
+def parse_import_statements(content):
+ modules = list()
+ tree = ast.parse(content)
+ for node in ast.walk(tree):
+ if isinstance(node, ast.Import):
+ for subnode in node.names:
+ module = {
+ "name": subnode.name,
+ "lineno": node.lineno,
+ }
+ modules.append(module)
+ elif isinstance(node, ast.ImportFrom) and node.level == 0:
+ module = {
+ "name": node.module,
+ "lineno": node.lineno,
+ }
+ modules.append(module)
+ return modules
+
+
+def parse_comments(content):
+ comments = list()
+ g = tokenize(BytesIO(content.encode("utf-8")).readline)
+ for toknum, tokval, _, _, _ in g:
+ if toknum == COMMENT:
+ comments.append(tokval)
+ return comments
+
+
+def parse(stdout, filepath):
+ with open(filepath, "r") as file:
+ content = file.read()
+ modules = parse_import_statements(content)
+ comments = parse_comments(content)
+ output = {
+ "modules": modules,
+ "comments": comments,
+ }
+ print(json.dumps(output), end="", file=stdout)
+ stdout.flush()
+ stdout.buffer.write(bytes([0]))
+ stdout.flush()
+
+
+def main(stdin, stdout):
+ for filepath in stdin:
+ filepath = filepath.rstrip()
+ parse(stdout, filepath)
+
+
+if __name__ == "__main__":
+ exit(main(sys.stdin, sys.stdout))
\ No newline at end of file
diff --git a/gazelle/parser.go b/gazelle/parser.go
new file mode 100644
index 0000000..5013ce4
--- /dev/null
+++ b/gazelle/parser.go
@@ -0,0 +1,265 @@
+package python
+
+import (
+ "bufio"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/bazelbuild/rules_go/go/tools/bazel"
+ "github.com/emirpasic/gods/sets/treeset"
+ godsutils "github.com/emirpasic/gods/utils"
+)
+
+var (
+ parserStdin io.Writer
+ parserStdout io.Reader
+ parserMutex sync.Mutex
+)
+
+func init() {
+ parseScriptRunfile, err := bazel.Runfile("gazelle/parse")
+ if err != nil {
+ log.Printf("failed to initialize parser: %v\n", err)
+ os.Exit(1)
+ }
+
+ ctx := context.Background()
+ ctx, parserCancel := context.WithTimeout(ctx, time.Minute*5)
+ cmd := exec.CommandContext(ctx, parseScriptRunfile)
+
+ cmd.Stderr = os.Stderr
+
+ stdin, err := cmd.StdinPipe()
+ if err != nil {
+ log.Printf("failed to initialize parser: %v\n", err)
+ os.Exit(1)
+ }
+ parserStdin = stdin
+
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ log.Printf("failed to initialize parser: %v\n", err)
+ os.Exit(1)
+ }
+ parserStdout = stdout
+
+ if err := cmd.Start(); err != nil {
+ log.Printf("failed to initialize parser: %v\n", err)
+ os.Exit(1)
+ }
+
+ go func() {
+ defer parserCancel()
+ if err := cmd.Wait(); err != nil {
+ log.Printf("failed to wait for parser: %v\n", err)
+ os.Exit(1)
+ }
+ }()
+}
+
+// python3Parser implements a parser for Python files that extracts the modules
+// as seen in the import statements.
+type python3Parser struct {
+ // The value of language.GenerateArgs.Config.RepoRoot.
+ repoRoot string
+ // The value of language.GenerateArgs.Rel.
+ relPackagePath string
+ // The function that determines if a dependency is ignored from a Gazelle
+ // directive. It's the signature of pythonconfig.Config.IgnoresDependency.
+ ignoresDependency func(dep string) bool
+}
+
+// newPython3Parser constructs a new python3Parser.
+func newPython3Parser(
+ repoRoot string,
+ relPackagePath string,
+ ignoresDependency func(dep string) bool,
+) *python3Parser {
+ return &python3Parser{
+ repoRoot: repoRoot,
+ relPackagePath: relPackagePath,
+ ignoresDependency: ignoresDependency,
+ }
+}
+
+// parseAll parses all provided Python files by consecutively calling p.parse.
+func (p *python3Parser) parseAll(pyFilepaths *treeset.Set) (*treeset.Set, error) {
+ allModules := treeset.NewWith(moduleComparator)
+ it := pyFilepaths.Iterator()
+ for it.Next() {
+ modules, err := p.parse(it.Value().(string))
+ if err != nil {
+ return nil, err
+ }
+ modulesIt := modules.Iterator()
+ for modulesIt.Next() {
+ allModules.Add(modulesIt.Value())
+ }
+ }
+ return allModules, nil
+}
+
+// parse parses a Python file and returns the extracted modules from the import
+// statements. An error is raised if communicating with the long-lived Python
+// parser over stdin and stdout fails.
+func (p *python3Parser) parse(pyFilepath string) (*treeset.Set, error) {
+ parserMutex.Lock()
+ defer parserMutex.Unlock()
+
+ modules := treeset.NewWith(moduleComparator)
+
+ relFilepath := filepath.Join(p.relPackagePath, pyFilepath)
+ absFilepath := filepath.Join(p.repoRoot, relFilepath)
+ fmt.Fprintln(parserStdin, absFilepath)
+ reader := bufio.NewReader(parserStdout)
+ data, err := reader.ReadBytes(0)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse %s: %w", pyFilepath, err)
+ }
+ data = data[:len(data)-1]
+ var res parserResponse
+ if err := json.Unmarshal(data, &res); err != nil {
+ return nil, fmt.Errorf("failed to parse %s: %w", pyFilepath, err)
+ }
+
+ annotations := annotationsFromComments(res.Comments)
+
+ for _, m := range res.Modules {
+ // Check for ignored dependencies set via an annotation to the Python
+ // module.
+ if annotations.ignores(m.Name) {
+ continue
+ }
+
+ // Check for ignored dependencies set via a Gazelle directive in a BUILD
+ // file.
+ if p.ignoresDependency(m.Name) {
+ continue
+ }
+
+ m.Filepath = relFilepath
+
+ modules.Add(m)
+ }
+
+ return modules, nil
+}
+
+// parserResponse represents a response returned by the parser.py for a given
+// parsed Python module.
+type parserResponse struct {
+ // The modules depended by the parsed module.
+ Modules []module `json:"modules"`
+ // The comments contained in the parsed module. This contains the
+ // annotations as they are comments in the Python module.
+ Comments []comment `json:"comments"`
+}
+
+// module represents a fully-qualified, dot-separated, Python module as seen on
+// the import statement, alongside the line number where it happened.
+type module struct {
+ // The fully-qualified, dot-separated, Python module name as seen on import
+ // statements.
+ Name string `json:"name"`
+ // The line number where the import happened.
+ LineNumber uint32 `json:"lineno"`
+ // The path to the module file relative to the Bazel workspace root.
+ Filepath string
+}
+
+// path returns the replaced dots with the os-specific path separator.
+func (m *module) path() string {
+ return filepath.Join(strings.Split(m.Name, ".")...)
+}
+
+// bazelPath returns the replaced dots with forward slashes.
+func (m *module) bazelPath() string {
+ return strings.ReplaceAll(m.Name, ".", "/")
+}
+
+// moduleComparator compares modules by name.
+func moduleComparator(a, b interface{}) int {
+ return godsutils.StringComparator(a.(module).Name, b.(module).Name)
+}
+
+// annotationKind represents Gazelle annotation kinds.
+type annotationKind string
+
+const (
+ // The Gazelle annotation prefix.
+ annotationPrefix string = "gazelle:"
+ // The ignore annotation kind. E.g. '# gazelle:ignore <module_name>'.
+ annotationKindIgnore annotationKind = "ignore"
+)
+
+// comment represents a Python comment.
+type comment string
+
+// asAnnotation returns an annotation object if the comment has the
+// annotationPrefix.
+func (c *comment) asAnnotation() *annotation {
+ uncomment := strings.TrimLeft(string(*c), "# ")
+ if !strings.HasPrefix(uncomment, annotationPrefix) {
+ return nil
+ }
+ withoutPrefix := strings.TrimPrefix(uncomment, annotationPrefix)
+ annotationParts := strings.SplitN(withoutPrefix, " ", 2)
+ return &annotation{
+ kind: annotationKind(annotationParts[0]),
+ value: annotationParts[1],
+ }
+}
+
+// annotation represents a single Gazelle annotation parsed from a Python
+// comment.
+type annotation struct {
+ kind annotationKind
+ value string
+}
+
+// annotations represent the collection of all Gazelle annotations parsed out of
+// the comments of a Python module.
+type annotations struct {
+ // The parsed modules to be ignored by Gazelle.
+ ignore map[string]struct{}
+}
+
+// annotationsFromComments returns all the annotations parsed out of the
+// comments of a Python module.
+func annotationsFromComments(comments []comment) *annotations {
+ ignore := make(map[string]struct{})
+ for _, comment := range comments {
+ annotation := comment.asAnnotation()
+ if annotation != nil {
+ if annotation.kind == annotationKindIgnore {
+ modules := strings.Split(annotation.value, ",")
+ for _, m := range modules {
+ if m == "" {
+ continue
+ }
+ m = strings.TrimSpace(m)
+ ignore[m] = struct{}{}
+ }
+ }
+ }
+ }
+ return &annotations{
+ ignore: ignore,
+ }
+}
+
+// ignored returns true if the given module was ignored via the ignore
+// annotation.
+func (a *annotations) ignores(module string) bool {
+ _, ignores := a.ignore[module]
+ return ignores
+}
diff --git a/gazelle/python_test.go b/gazelle/python_test.go
new file mode 100644
index 0000000..967ce45
--- /dev/null
+++ b/gazelle/python_test.go
@@ -0,0 +1,211 @@
+/* Copyright 2020 The Bazel Authors. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// This test file was first seen on:
+// https://github.com/bazelbuild/bazel-skylib/blob/f80bc733d4b9f83d427ce3442be2e07427b2cc8d/gazelle/bzl/BUILD.
+// It was modified for the needs of this extension.
+
+package python_test
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/bazelbuild/bazel-gazelle/testtools"
+ "github.com/bazelbuild/rules_go/go/tools/bazel"
+ "github.com/emirpasic/gods/lists/singlylinkedlist"
+ "github.com/ghodss/yaml"
+)
+
+const (
+ extensionDir = "gazelle/"
+ testDataPath = extensionDir + "testdata/"
+ gazelleBinaryName = "gazelle_python_binary"
+)
+
+var gazellePath = mustFindGazelle()
+
+func TestGazelleBinary(t *testing.T) {
+ tests := map[string][]bazel.RunfileEntry{}
+
+ runfiles, err := bazel.ListRunfiles()
+ if err != nil {
+ t.Fatalf("bazel.ListRunfiles() error: %v", err)
+ }
+ for _, f := range runfiles {
+ if strings.HasPrefix(f.ShortPath, testDataPath) {
+ relativePath := strings.TrimPrefix(f.ShortPath, testDataPath)
+ parts := strings.SplitN(relativePath, "/", 2)
+ if len(parts) < 2 {
+ // This file is not a part of a testcase since it must be in a dir that
+ // is the test case and then have a path inside of that.
+ continue
+ }
+
+ tests[parts[0]] = append(tests[parts[0]], f)
+ }
+ }
+ if len(tests) == 0 {
+ t.Fatal("no tests found")
+ }
+
+ for testName, files := range tests {
+ testPath(t, testName, files)
+ }
+}
+
+func testPath(t *testing.T, name string, files []bazel.RunfileEntry) {
+ t.Run(name, func(t *testing.T) {
+ var inputs []testtools.FileSpec
+ var goldens []testtools.FileSpec
+
+ var config *testYAML
+ for _, f := range files {
+ path := f.Path
+ trim := testDataPath + name + "/"
+ shortPath := strings.TrimPrefix(f.ShortPath, trim)
+ info, err := os.Stat(path)
+ if err != nil {
+ t.Fatalf("os.Stat(%q) error: %v", path, err)
+ }
+
+ if info.IsDir() {
+ continue
+ }
+
+ content, err := ioutil.ReadFile(path)
+ if err != nil {
+ t.Errorf("ioutil.ReadFile(%q) error: %v", path, err)
+ }
+
+ if filepath.Base(shortPath) == "test.yaml" {
+ if config != nil {
+ t.Fatal("only 1 test.yaml is supported")
+ }
+ config = new(testYAML)
+ if err := yaml.Unmarshal(content, config); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ if strings.HasSuffix(shortPath, ".in") {
+ inputs = append(inputs, testtools.FileSpec{
+ Path: filepath.Join(name, strings.TrimSuffix(shortPath, ".in")),
+ Content: string(content),
+ })
+ } else if strings.HasSuffix(shortPath, ".out") {
+ goldens = append(goldens, testtools.FileSpec{
+ Path: filepath.Join(name, strings.TrimSuffix(shortPath, ".out")),
+ Content: string(content),
+ })
+ } else {
+ inputs = append(inputs, testtools.FileSpec{
+ Path: filepath.Join(name, shortPath),
+ Content: string(content),
+ })
+ goldens = append(goldens, testtools.FileSpec{
+ Path: filepath.Join(name, shortPath),
+ Content: string(content),
+ })
+ }
+ }
+
+ testdataDir, cleanup := testtools.CreateFiles(t, inputs)
+ defer cleanup()
+ defer func() {
+ if t.Failed() {
+ filepath.Walk(testdataDir, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ t.Logf("%q exists", strings.TrimPrefix(path, testdataDir))
+ return nil
+ })
+ }
+ }()
+
+ workspaceRoot := filepath.Join(testdataDir, name)
+
+ args := []string{"-build_file_name=BUILD,BUILD.bazel"}
+
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+ defer cancel()
+ cmd := exec.CommandContext(ctx, gazellePath, args...)
+ var stdout, stderr bytes.Buffer
+ cmd.Stdout = &stdout
+ cmd.Stderr = &stderr
+ cmd.Dir = workspaceRoot
+ if err := cmd.Run(); err != nil {
+ var e *exec.ExitError
+ if !errors.As(err, &e) {
+ t.Fatal(err)
+ }
+ }
+ errs := singlylinkedlist.New()
+ actualExitCode := cmd.ProcessState.ExitCode()
+ if config.Expect.ExitCode != actualExitCode {
+ errs.Add(fmt.Errorf("expected gazelle exit code: %d\ngot: %d",
+ config.Expect.ExitCode, actualExitCode,
+ ))
+ }
+ actualStdout := stdout.String()
+ if strings.TrimSpace(config.Expect.Stdout) != strings.TrimSpace(actualStdout) {
+ errs.Add(fmt.Errorf("expected gazelle stdout: %s\ngot: %s",
+ config.Expect.Stdout, actualStdout,
+ ))
+ }
+ actualStderr := stderr.String()
+ if strings.TrimSpace(config.Expect.Stderr) != strings.TrimSpace(actualStderr) {
+ errs.Add(fmt.Errorf("expected gazelle stderr: %s\ngot: %s",
+ config.Expect.Stderr, actualStderr,
+ ))
+ }
+ if !errs.Empty() {
+ errsIt := errs.Iterator()
+ for errsIt.Next() {
+ err := errsIt.Value().(error)
+ t.Log(err)
+ }
+ t.FailNow()
+ }
+
+ testtools.CheckFiles(t, testdataDir, goldens)
+ })
+}
+
+func mustFindGazelle() string {
+ gazellePath, ok := bazel.FindBinary(extensionDir, gazelleBinaryName)
+ if !ok {
+ panic("could not find gazelle binary")
+ }
+ return gazellePath
+}
+
+type testYAML struct {
+ Expect struct {
+ ExitCode int `json:"exit_code"`
+ Stdout string `json:"stdout"`
+ Stderr string `json:"stderr"`
+ } `json:"expect"`
+}
\ No newline at end of file
diff --git a/gazelle/pythonconfig/BUILD.bazel b/gazelle/pythonconfig/BUILD.bazel
new file mode 100644
index 0000000..4fab8c9
--- /dev/null
+++ b/gazelle/pythonconfig/BUILD.bazel
@@ -0,0 +1,15 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "pythonconfig",
+ srcs = [
+ "pythonconfig.go",
+ "types.go",
+ ],
+ importpath = "github.com/bazelbuild/rules_python/gazelle/pythonconfig",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//gazelle/manifest",
+ "@com_github_emirpasic_gods//lists/singlylinkedlist",
+ ],
+)
diff --git a/gazelle/pythonconfig/pythonconfig.go b/gazelle/pythonconfig/pythonconfig.go
new file mode 100644
index 0000000..550e66b
--- /dev/null
+++ b/gazelle/pythonconfig/pythonconfig.go
@@ -0,0 +1,323 @@
+package pythonconfig
+
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+
+ "github.com/emirpasic/gods/lists/singlylinkedlist"
+
+ "github.com/bazelbuild/rules_python/gazelle/manifest"
+)
+
+// Directives
+const (
+ // PythonExtensionDirective represents the directive that controls whether
+ // this Python extension is enabled or not. Sub-packages inherit this value.
+ // Can be either "enabled" or "disabled". Defaults to "enabled".
+ PythonExtensionDirective = "python_extension"
+ // PythonRootDirective represents the directive that sets a Bazel package as
+ // a Python root. This is used on monorepos with multiple Python projects
+ // that don't share the top-level of the workspace as the root.
+ PythonRootDirective = "python_root"
+ // PythonManifestFileNameDirective represents the directive that overrides
+ // the default gazelle_python.yaml manifest file name.
+ PythonManifestFileNameDirective = "python_manifest_file_name"
+ // IgnoreFilesDirective represents the directive that controls the ignored
+ // files from the generated targets.
+ IgnoreFilesDirective = "python_ignore_files"
+ // IgnoreDependenciesDirective represents the directive that controls the
+ // ignored dependencies from the generated targets.
+ IgnoreDependenciesDirective = "python_ignore_dependencies"
+ // ValidateImportStatementsDirective represents the directive that controls
+ // whether the Python import statements should be validated.
+ ValidateImportStatementsDirective = "python_validate_import_statements"
+ // GenerationMode represents the directive that controls the target generation
+ // mode. See below for the GenerationModeType constants.
+ GenerationMode = "python_generation_mode"
+ // LibraryNamingConvention represents the directive that controls the
+ // py_library naming convention. It interpolates $package_name$ with the
+ // Bazel package name. E.g. if the Bazel package name is `foo`, setting this
+ // to `$package_name$_my_lib` would render to `foo_my_lib`.
+ LibraryNamingConvention = "python_library_naming_convention"
+ // BinaryNamingConvention represents the directive that controls the
+ // py_binary naming convention. See python_library_naming_convention for
+ // more info on the package name interpolation.
+ BinaryNamingConvention = "python_binary_naming_convention"
+ // TestNamingConvention represents the directive that controls the py_test
+ // naming convention. See python_library_naming_convention for more info on
+ // the package name interpolation.
+ TestNamingConvention = "python_test_naming_convention"
+)
+
+// GenerationModeType represents one of the generation modes for the Python
+// extension.
+type GenerationModeType string
+
+// Generation modes
+const (
+ // GenerationModePackage defines the mode in which targets will be generated
+ // for each __init__.py, or when an existing BUILD or BUILD.bazel file already
+ // determines a Bazel package.
+ GenerationModePackage GenerationModeType = "package"
+ // GenerationModeProject defines the mode in which a coarse-grained target will
+ // be generated englobing sub-directories containing Python files.
+ GenerationModeProject GenerationModeType = "project"
+)
+
+const (
+ packageNameNamingConventionSubstitution = "$package_name$"
+)
+
+// defaultIgnoreFiles is the list of default values used in the
+// python_ignore_files option.
+var defaultIgnoreFiles = map[string]struct{}{
+ "setup.py": {},
+}
+
+// Configs is an extension of map[string]*Config. It provides finding methods
+// on top of the mapping.
+type Configs map[string]*Config
+
+// ParentForPackage returns the parent Config for the given Bazel package.
+func (c *Configs) ParentForPackage(pkg string) *Config {
+ dir := filepath.Dir(pkg)
+ if dir == "." {
+ dir = ""
+ }
+ parent := (map[string]*Config)(*c)[dir]
+ return parent
+}
+
+// Config represents a config extension for a specific Bazel package.
+type Config struct {
+ parent *Config
+
+ extensionEnabled bool
+ repoRoot string
+ pythonProjectRoot string
+ gazelleManifest *manifest.Manifest
+
+ excludedPatterns *singlylinkedlist.List
+ ignoreFiles map[string]struct{}
+ ignoreDependencies map[string]struct{}
+ validateImportStatements bool
+ coarseGrainedGeneration bool
+ libraryNamingConvention string
+ binaryNamingConvention string
+ testNamingConvention string
+}
+
+// New creates a new Config.
+func New(
+ repoRoot string,
+ pythonProjectRoot string,
+) *Config {
+ return &Config{
+ extensionEnabled: true,
+ repoRoot: repoRoot,
+ pythonProjectRoot: pythonProjectRoot,
+ excludedPatterns: singlylinkedlist.New(),
+ ignoreFiles: make(map[string]struct{}),
+ ignoreDependencies: make(map[string]struct{}),
+ validateImportStatements: true,
+ coarseGrainedGeneration: false,
+ libraryNamingConvention: packageNameNamingConventionSubstitution,
+ binaryNamingConvention: fmt.Sprintf("%s_bin", packageNameNamingConventionSubstitution),
+ testNamingConvention: fmt.Sprintf("%s_test", packageNameNamingConventionSubstitution),
+ }
+}
+
+// Parent returns the parent config.
+func (c *Config) Parent() *Config {
+ return c.parent
+}
+
+// NewChild creates a new child Config. It inherits desired values from the
+// current Config and sets itself as the parent to the child.
+func (c *Config) NewChild() *Config {
+ return &Config{
+ parent: c,
+ extensionEnabled: c.extensionEnabled,
+ repoRoot: c.repoRoot,
+ pythonProjectRoot: c.pythonProjectRoot,
+ gazelleManifest: c.gazelleManifest,
+ excludedPatterns: c.excludedPatterns,
+ ignoreFiles: make(map[string]struct{}),
+ ignoreDependencies: make(map[string]struct{}),
+ validateImportStatements: c.validateImportStatements,
+ coarseGrainedGeneration: c.coarseGrainedGeneration,
+ libraryNamingConvention: c.libraryNamingConvention,
+ binaryNamingConvention: c.binaryNamingConvention,
+ testNamingConvention: c.testNamingConvention,
+ }
+}
+
+// AddExcludedPattern adds a glob pattern parsed from the standard
+// gazelle:exclude directive.
+func (c *Config) AddExcludedPattern(pattern string) {
+ c.excludedPatterns.Add(pattern)
+}
+
+// ExcludedPatterns returns the excluded patterns list.
+func (c *Config) ExcludedPatterns() *singlylinkedlist.List {
+ return c.excludedPatterns
+}
+
+// SetExtensionEnabled sets whether the extension is enabled or not.
+func (c *Config) SetExtensionEnabled(enabled bool) {
+ c.extensionEnabled = enabled
+}
+
+// ExtensionEnabled returns whether the extension is enabled or not.
+func (c *Config) ExtensionEnabled() bool {
+ return c.extensionEnabled
+}
+
+// SetPythonProjectRoot sets the Python project root.
+func (c *Config) SetPythonProjectRoot(pythonProjectRoot string) {
+ c.pythonProjectRoot = pythonProjectRoot
+}
+
+// PythonProjectRoot returns the Python project root.
+func (c *Config) PythonProjectRoot() string {
+ return c.pythonProjectRoot
+}
+
+// SetGazelleManifest sets the Gazelle manifest parsed from the
+// gazelle_python.yaml file.
+func (c *Config) SetGazelleManifest(gazelleManifest *manifest.Manifest) {
+ c.gazelleManifest = gazelleManifest
+}
+
+// PipRepository returns the pip repository name from the manifest.
+func (c *Config) PipRepository() string {
+ if c.gazelleManifest != nil {
+ return c.gazelleManifest.PipDepsRepositoryName
+ }
+ return ""
+}
+
+// ModulesMapping returns the modules mapping from the manifest.
+func (c *Config) ModulesMapping() map[string]string {
+ if c.gazelleManifest != nil {
+ return c.gazelleManifest.ModulesMapping
+ }
+ return map[string]string{}
+}
+
+// AddIgnoreFile adds a file to the list of ignored files for a given package.
+// Adding an ignored file to a package also makes it ignored on a subpackage.
+func (c *Config) AddIgnoreFile(file string) {
+ c.ignoreFiles[strings.TrimSpace(file)] = struct{}{}
+}
+
+// IgnoresFile checks if a file is ignored in the given package or in one of the
+// parent packages up to the workspace root.
+func (c *Config) IgnoresFile(file string) bool {
+ trimmedFile := strings.TrimSpace(file)
+
+ if _, ignores := defaultIgnoreFiles[trimmedFile]; ignores {
+ return true
+ }
+
+ if _, ignores := c.ignoreFiles[trimmedFile]; ignores {
+ return true
+ }
+
+ parent := c.parent
+ for parent != nil {
+ if _, ignores := parent.ignoreFiles[trimmedFile]; ignores {
+ return true
+ }
+ parent = parent.parent
+ }
+
+ return false
+}
+
+// AddIgnoreDependency adds a dependency to the list of ignored dependencies for
+// a given package. Adding an ignored dependency to a package also makes it
+// ignored on a subpackage.
+func (c *Config) AddIgnoreDependency(dep string) {
+ c.ignoreDependencies[strings.TrimSpace(dep)] = struct{}{}
+}
+
+// IgnoresDependency checks if a dependency is ignored in the given package or
+// in one of the parent packages up to the workspace root.
+func (c *Config) IgnoresDependency(dep string) bool {
+ trimmedDep := strings.TrimSpace(dep)
+
+ if _, ignores := c.ignoreDependencies[trimmedDep]; ignores {
+ return true
+ }
+
+ parent := c.parent
+ for parent != nil {
+ if _, ignores := parent.ignoreDependencies[trimmedDep]; ignores {
+ return true
+ }
+ parent = parent.parent
+ }
+
+ return false
+}
+
+// SetValidateImportStatements sets whether Python import statements should be
+// validated or not. It throws an error if this is set multiple times, i.e. if
+// the directive is specified multiple times in the Bazel workspace.
+func (c *Config) SetValidateImportStatements(validate bool) {
+ c.validateImportStatements = validate
+}
+
+// ValidateImportStatements returns whether the Python import statements should
+// be validated or not. If this option was not explicitly specified by the user,
+// it defaults to true.
+func (c *Config) ValidateImportStatements() bool {
+ return c.validateImportStatements
+}
+
+// SetCoarseGrainedGeneration sets whether coarse-grained targets should be
+// generated or not.
+func (c *Config) SetCoarseGrainedGeneration(coarseGrained bool) {
+ c.coarseGrainedGeneration = coarseGrained
+}
+
+// CoarseGrainedGeneration returns whether coarse-grained targets should be
+// generated or not.
+func (c *Config) CoarseGrainedGeneration() bool {
+ return c.coarseGrainedGeneration
+}
+
+// SetLibraryNamingConvention sets the py_library target naming convention.
+func (c *Config) SetLibraryNamingConvention(libraryNamingConvention string) {
+ c.libraryNamingConvention = libraryNamingConvention
+}
+
+// RenderLibraryName returns the py_library target name by performing all
+// substitutions.
+func (c *Config) RenderLibraryName(packageName string) string {
+ return strings.ReplaceAll(c.libraryNamingConvention, packageNameNamingConventionSubstitution, packageName)
+}
+
+// SetBinaryNamingConvention sets the py_binary target naming convention.
+func (c *Config) SetBinaryNamingConvention(binaryNamingConvention string) {
+ c.binaryNamingConvention = binaryNamingConvention
+}
+
+// RenderBinaryName returns the py_binary target name by performing all
+// substitutions.
+func (c *Config) RenderBinaryName(packageName string) string {
+ return strings.ReplaceAll(c.binaryNamingConvention, packageNameNamingConventionSubstitution, packageName)
+}
+
+// SetTestNamingConvention sets the py_test target naming convention.
+func (c *Config) SetTestNamingConvention(testNamingConvention string) {
+ c.testNamingConvention = testNamingConvention
+}
+
+// RenderTestName returns the py_test target name by performing all
+// substitutions.
+func (c *Config) RenderTestName(packageName string) string {
+ return strings.ReplaceAll(c.testNamingConvention, packageNameNamingConventionSubstitution, packageName)
+}
diff --git a/gazelle/pythonconfig/types.go b/gazelle/pythonconfig/types.go
new file mode 100644
index 0000000..bdb535b
--- /dev/null
+++ b/gazelle/pythonconfig/types.go
@@ -0,0 +1,103 @@
+package pythonconfig
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+)
+
+// StringSet satisfies the flag.Value interface. It constructs a set backed by
+// a hashmap by parsing the flag string value using the provided separator.
+type StringSet struct {
+ set map[string]struct{}
+ separator string
+}
+
+// NewStringSet constructs a new StringSet with the given separator.
+func NewStringSet(separator string) *StringSet {
+ return &StringSet{
+ set: make(map[string]struct{}),
+ separator: separator,
+ }
+}
+
+// String satisfies flag.Value.String.
+func (ss *StringSet) String() string {
+ keys := make([]string, 0, len(ss.set))
+ for key := range ss.set {
+ keys = append(keys, key)
+ }
+ return fmt.Sprintf("%v", sort.StringSlice(keys))
+}
+
+// Set satisfies flag.Value.Set.
+func (ss *StringSet) Set(s string) error {
+ list := strings.Split(s, ss.separator)
+ for _, v := range list {
+ trimmed := strings.TrimSpace(v)
+ if trimmed == "" {
+ continue
+ }
+ ss.set[trimmed] = struct{}{}
+ }
+ return nil
+}
+
+// Contains returns whether the StringSet contains the provided element or not.
+func (ss *StringSet) Contains(s string) bool {
+ _, contains := ss.set[s]
+ return contains
+}
+
+// StringMapList satisfies the flag.Value interface. It constructs a string map
+// by parsing the flag string value using the provided list and map separators.
+type StringMapList struct {
+ mapping map[string]string
+ listSeparator string
+ mapSeparator string
+}
+
+// NewStringMapList constructs a new StringMapList with the given separators.
+func NewStringMapList(listSeparator, mapSeparator string) *StringMapList {
+ return &StringMapList{
+ mapping: make(map[string]string),
+ listSeparator: listSeparator,
+ mapSeparator: mapSeparator,
+ }
+}
+
+// String satisfies flag.Value.String.
+func (sml *StringMapList) String() string {
+ return fmt.Sprintf("%v", sml.mapping)
+}
+
+// Set satisfies flag.Value.Set.
+func (sml *StringMapList) Set(s string) error {
+ list := strings.Split(s, sml.listSeparator)
+ for _, v := range list {
+ trimmed := strings.TrimSpace(v)
+ if trimmed == "" {
+ continue
+ }
+ mapList := strings.SplitN(trimmed, sml.mapSeparator, 2)
+ if len(mapList) < 2 {
+ return fmt.Errorf(
+ "%q is not a valid map using %q as a separator",
+ trimmed, sml.mapSeparator,
+ )
+ }
+ key := mapList[0]
+ if _, exists := sml.mapping[key]; exists {
+ return fmt.Errorf("key %q already set", key)
+ }
+ val := mapList[1]
+ sml.mapping[key] = val
+ }
+ return nil
+}
+
+// Get returns the value for the given key.
+func (sml *StringMapList) Get(key string) (string, bool) {
+ val, exists := sml.mapping[key]
+ return val, exists
+}
\ No newline at end of file
diff --git a/gazelle/resolve.go b/gazelle/resolve.go
new file mode 100644
index 0000000..b3bdda1
--- /dev/null
+++ b/gazelle/resolve.go
@@ -0,0 +1,289 @@
+package python
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/bazelbuild/bazel-gazelle/config"
+ "github.com/bazelbuild/bazel-gazelle/label"
+ "github.com/bazelbuild/bazel-gazelle/repo"
+ "github.com/bazelbuild/bazel-gazelle/resolve"
+ "github.com/bazelbuild/bazel-gazelle/rule"
+ bzl "github.com/bazelbuild/buildtools/build"
+ "github.com/emirpasic/gods/sets/treeset"
+ godsutils "github.com/emirpasic/gods/utils"
+
+ "github.com/bazelbuild/rules_python/gazelle/pythonconfig"
+)
+
+const languageName = "py"
+
+const (
+ // resolvedDepsKey is the attribute key used to pass dependencies that don't
+ // need to be resolved by the dependency resolver in the Resolver step.
+ resolvedDepsKey = "_gazelle_python_resolved_deps"
+ // uuidKey is the attribute key used to uniquely identify a py_library
+ // target that should be imported by a py_test or py_binary in the same
+ // Bazel package.
+ uuidKey = "_gazelle_python_library_uuid"
+)
+
+// Resolver satisfies the resolve.Resolver interface. It resolves dependencies
+// in rules generated by this extension.
+type Resolver struct{}
+
+// Name returns the name of the language. This is the prefix of the kinds of
+// rules generated. E.g. py_library and py_binary.
+func (*Resolver) Name() string { return languageName }
+
+// Imports returns a list of ImportSpecs that can be used to import the rule
+// r. This is used to populate RuleIndex.
+//
+// If nil is returned, the rule will not be indexed. If any non-nil slice is
+// returned, including an empty slice, the rule will be indexed.
+func (py *Resolver) Imports(c *config.Config, r *rule.Rule, f *rule.File) []resolve.ImportSpec {
+ cfgs := c.Exts[languageName].(pythonconfig.Configs)
+ cfg := cfgs[f.Pkg]
+ srcs := r.AttrStrings("srcs")
+ provides := make([]resolve.ImportSpec, 0, len(srcs)+1)
+ for _, src := range srcs {
+ ext := filepath.Ext(src)
+ if ext == ".py" {
+ pythonProjectRoot := cfg.PythonProjectRoot()
+ provide := importSpecFromSrc(pythonProjectRoot, f.Pkg, src)
+ provides = append(provides, provide)
+ }
+ }
+ if r.PrivateAttr(uuidKey) != nil {
+ provide := resolve.ImportSpec{
+ Lang: languageName,
+ Imp: r.PrivateAttr(uuidKey).(string),
+ }
+ provides = append(provides, provide)
+ }
+ if len(provides) == 0 {
+ return nil
+ }
+ return provides
+}
+
+// importSpecFromSrc determines the ImportSpec based on the target that contains the src so that
+// the target can be indexed for import statements that match the calculated src relative to the its
+// Python project root.
+func importSpecFromSrc(pythonProjectRoot, bzlPkg, src string) resolve.ImportSpec {
+ pythonPkgDir := filepath.Join(bzlPkg, filepath.Dir(src))
+ relPythonPkgDir, err := filepath.Rel(pythonProjectRoot, pythonPkgDir)
+ if err != nil {
+ panic(fmt.Errorf("unexpected failure: %v", err))
+ }
+ if relPythonPkgDir == "." {
+ relPythonPkgDir = ""
+ }
+ pythonPkg := strings.ReplaceAll(relPythonPkgDir, "/", ".")
+ filename := filepath.Base(src)
+ if filename == pyLibraryEntrypointFilename {
+ if pythonPkg != "" {
+ return resolve.ImportSpec{
+ Lang: languageName,
+ Imp: pythonPkg,
+ }
+ }
+ }
+ moduleName := strings.TrimSuffix(filename, ".py")
+ var imp string
+ if pythonPkg == "" {
+ imp = moduleName
+ } else {
+ imp = fmt.Sprintf("%s.%s", pythonPkg, moduleName)
+ }
+ return resolve.ImportSpec{
+ Lang: languageName,
+ Imp: imp,
+ }
+}
+
+// Embeds returns a list of labels of rules that the given rule embeds. If
+// a rule is embedded by another importable rule of the same language, only
+// the embedding rule will be indexed. The embedding rule will inherit
+// the imports of the embedded rule.
+func (py *Resolver) Embeds(r *rule.Rule, from label.Label) []label.Label {
+ // TODO(f0rmiga): implement.
+ return make([]label.Label, 0)
+}
+
+// Resolve translates imported libraries for a given rule into Bazel
+// dependencies. Information about imported libraries is returned for each
+// rule generated by language.GenerateRules in
+// language.GenerateResult.Imports. Resolve generates a "deps" attribute (or
+// the appropriate language-specific equivalent) for each import according to
+// language-specific rules and heuristics.
+func (py *Resolver) Resolve(
+ c *config.Config,
+ ix *resolve.RuleIndex,
+ rc *repo.RemoteCache,
+ r *rule.Rule,
+ modulesRaw interface{},
+ from label.Label,
+) {
+ // TODO(f0rmiga): may need to be defensive here once this Gazelle extension
+ // join with the main Gazelle binary with other rules. It may conflict with
+ // other generators that generate py_* targets.
+ deps := treeset.NewWith(godsutils.StringComparator)
+ if modulesRaw != nil {
+ cfgs := c.Exts[languageName].(pythonconfig.Configs)
+ cfg := cfgs[from.Pkg]
+ pythonProjectRoot := cfg.PythonProjectRoot()
+ modules := modulesRaw.(*treeset.Set)
+ pipRepository := cfg.PipRepository()
+ modulesMapping := cfg.ModulesMapping()
+ it := modules.Iterator()
+ explainDependency := os.Getenv("EXPLAIN_DEPENDENCY")
+ hasFatalError := false
+ MODULE_LOOP:
+ for it.Next() {
+ mod := it.Value().(module)
+ imp := resolve.ImportSpec{Lang: languageName, Imp: mod.Name}
+ if override, ok := resolve.FindRuleWithOverride(c, imp, languageName); ok {
+ if override.Repo == "" {
+ override.Repo = from.Repo
+ }
+ if !override.Equal(from) {
+ if override.Repo == from.Repo {
+ override.Repo = ""
+ }
+ dep := override.String()
+ deps.Add(dep)
+ if explainDependency == dep {
+ log.Printf("Explaining dependency (%s): "+
+ "in the target %q, the file %q imports %q at line %d, "+
+ "which resolves using the \"gazelle:resolve\" directive.\n",
+ explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber)
+ }
+ }
+ } else {
+ if distribution, ok := modulesMapping[mod.Name]; ok {
+ distributionPackage := rulesPythonDistributionPackage(distribution)
+ dep := label.New(pipRepository, distributionPackage, distributionPackage).String()
+ deps.Add(dep)
+ if explainDependency == dep {
+ log.Printf("Explaining dependency (%s): "+
+ "in the target %q, the file %q imports %q at line %d, "+
+ "which resolves from the third-party module %q from the wheel %q.\n",
+ explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber, mod.Name, distribution)
+ }
+ } else {
+ matches := ix.FindRulesByImportWithConfig(c, imp, languageName)
+ if len(matches) == 0 {
+ // Check if the imported module is part of the standard library.
+ if isStd, err := isStdModule(mod); err != nil {
+ log.Println("ERROR: ", err)
+ hasFatalError = true
+ continue MODULE_LOOP
+ } else if isStd {
+ continue MODULE_LOOP
+ }
+ if cfg.ValidateImportStatements() {
+ err := fmt.Errorf(
+ "%[1]q at line %[2]d from %[3]q is an invalid dependency: possible solutions:\n"+
+ "\t1. Add it as a dependency in the requirements.txt file.\n"+
+ "\t2. Instruct Gazelle to resolve to a known dependency using the gazelle:resolve directive.\n"+
+ "\t3. Ignore it with a comment '# gazelle:ignore %[1]s' in the Python file.\n",
+ mod.Name, mod.LineNumber, mod.Filepath,
+ )
+ log.Printf("ERROR: failed to validate dependencies for target %q: %v\n", from.String(), err)
+ hasFatalError = true
+ continue MODULE_LOOP
+ }
+ }
+ filteredMatches := make([]resolve.FindResult, 0, len(matches))
+ for _, match := range matches {
+ if match.IsSelfImport(from) {
+ // Prevent from adding itself as a dependency.
+ continue MODULE_LOOP
+ }
+ filteredMatches = append(filteredMatches, match)
+ }
+ if len(filteredMatches) == 0 {
+ continue
+ }
+ if len(filteredMatches) > 1 {
+ sameRootMatches := make([]resolve.FindResult, 0, len(filteredMatches))
+ for _, match := range filteredMatches {
+ if strings.HasPrefix(match.Label.Pkg, pythonProjectRoot) {
+ sameRootMatches = append(sameRootMatches, match)
+ }
+ }
+ if len(sameRootMatches) != 1 {
+ err := fmt.Errorf(
+ "multiple targets (%s) may be imported with %q at line %d in %q "+
+ "- this must be fixed using the \"gazelle:resolve\" directive",
+ targetListFromResults(filteredMatches), mod.Name, mod.LineNumber, mod.Filepath)
+ log.Println("ERROR: ", err)
+ hasFatalError = true
+ continue MODULE_LOOP
+ }
+ filteredMatches = sameRootMatches
+ }
+ matchLabel := filteredMatches[0].Label.Rel(from.Repo, from.Pkg)
+ dep := matchLabel.String()
+ deps.Add(dep)
+ if explainDependency == dep {
+ log.Printf("Explaining dependency (%s): "+
+ "in the target %q, the file %q imports %q at line %d, "+
+ "which resolves from the first-party indexed labels.\n",
+ explainDependency, from.String(), mod.Filepath, mod.Name, mod.LineNumber)
+ }
+ }
+ }
+ }
+ if hasFatalError {
+ os.Exit(1)
+ }
+ }
+ resolvedDeps := r.PrivateAttr(resolvedDepsKey).(*treeset.Set)
+ if !resolvedDeps.Empty() {
+ it := resolvedDeps.Iterator()
+ for it.Next() {
+ deps.Add(it.Value())
+ }
+ }
+ if !deps.Empty() {
+ r.SetAttr("deps", convertDependencySetToExpr(deps))
+ }
+}
+
+// rulesPythonDistributionPackage builds a token that mimics how the
+// rules_python does it for the generated requirement function. By doing this,
+// we avoid having to generate the load statement for this function and the
+// third-party dependency becomes an explicit Bazel target.
+// https://github.com/bazelbuild/rules_python/blob/c639955c/packaging/piptool.py#L238-L245
+func rulesPythonDistributionPackage(distribution string) string {
+ sanitizedDistribution := strings.ToLower(distribution)
+ sanitizedDistribution = strings.ReplaceAll(sanitizedDistribution, "-", "_")
+ return "pypi__" + sanitizedDistribution
+}
+
+// targetListFromResults returns a string with the human-readable list of
+// targets contained in the given results.
+func targetListFromResults(results []resolve.FindResult) string {
+ list := make([]string, len(results))
+ for i, result := range results {
+ list[i] = result.Label.String()
+ }
+ return strings.Join(list, ", ")
+}
+
+// convertDependencySetToExpr converts the given set of dependencies to an
+// expression to be used in the deps attribute.
+func convertDependencySetToExpr(set *treeset.Set) bzl.Expr {
+ deps := make([]bzl.Expr, set.Size())
+ it := set.Iterator()
+ for it.Next() {
+ dep := it.Value().(string)
+ deps[it.Index()] = &bzl.StringExpr{Value: dep}
+ }
+ return &bzl.ListExpr{List: deps}
+}
diff --git a/gazelle/std_modules.go b/gazelle/std_modules.go
new file mode 100644
index 0000000..8c2cd35
--- /dev/null
+++ b/gazelle/std_modules.go
@@ -0,0 +1,98 @@
+package python
+
+import (
+ "bufio"
+ "context"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "os/exec"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/bazelbuild/rules_go/go/tools/bazel"
+)
+
+var (
+ stdModulesStdin io.Writer
+ stdModulesStdout io.Reader
+ stdModulesMutex sync.Mutex
+ stdModulesSeen map[string]struct{}
+)
+
+func init() {
+ stdModulesSeen = make(map[string]struct{})
+
+ stdModulesScriptRunfile, err := bazel.Runfile("gazelle/std_modules")
+ if err != nil {
+ log.Printf("failed to initialize std_modules: %v\n", err)
+ os.Exit(1)
+ }
+
+ ctx := context.Background()
+ ctx, stdModulesCancel := context.WithTimeout(ctx, time.Minute*5)
+ cmd := exec.CommandContext(ctx, stdModulesScriptRunfile)
+
+ cmd.Stderr = os.Stderr
+ cmd.Env = []string{}
+
+ stdin, err := cmd.StdinPipe()
+ if err != nil {
+ log.Printf("failed to initialize std_modules: %v\n", err)
+ os.Exit(1)
+ }
+ stdModulesStdin = stdin
+
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ log.Printf("failed to initialize std_modules: %v\n", err)
+ os.Exit(1)
+ }
+ stdModulesStdout = stdout
+
+ if err := cmd.Start(); err != nil {
+ log.Printf("failed to initialize std_modules: %v\n", err)
+ os.Exit(1)
+ }
+
+ go func() {
+ defer stdModulesCancel()
+ if err := cmd.Wait(); err != nil {
+ log.Printf("failed to wait for std_modules: %v\n", err)
+ os.Exit(1)
+ }
+ }()
+}
+
+func isStdModule(m module) (bool, error) {
+ if _, seen := stdModulesSeen[m.Name]; seen {
+ return true, nil
+ }
+ stdModulesMutex.Lock()
+ defer stdModulesMutex.Unlock()
+
+ fmt.Fprintf(stdModulesStdin, "%s\n", m.Name)
+
+ stdoutReader := bufio.NewReader(stdModulesStdout)
+ line, err := stdoutReader.ReadString('\n')
+ if err != nil {
+ return false, err
+ }
+ if len(line) == 0 {
+ return false, fmt.Errorf("unexpected empty output from std_modules")
+ }
+
+ isStd, err := strconv.ParseBool(strings.TrimSpace(line))
+ if err != nil {
+ return false, err
+ }
+
+ if isStd {
+ stdModulesSeen[m.Name] = struct{}{}
+ return true, nil
+ }
+ return false, nil
+}
\ No newline at end of file
diff --git a/gazelle/std_modules.py b/gazelle/std_modules.py
new file mode 100644
index 0000000..59e132d
--- /dev/null
+++ b/gazelle/std_modules.py
@@ -0,0 +1,38 @@
+# std_modules.py is a long-living program that communicates over STDIN and
+# STDOUT. STDIN receives module names, one per line. For each module statement
+# it evaluates, it outputs true/false for whether the module is part of the
+# standard library or not.
+
+import site
+import sys
+
+
+# Don't return any paths, all userland site-packages should be ignored.
+def __override_getusersitepackages__():
+ return ''
+
+
+site.getusersitepackages = __override_getusersitepackages__
+
+def is_std_modules(module):
+ try:
+ __import__(module, globals(), locals(), [], 0)
+ return True
+ except Exception:
+ return False
+
+
+def main(stdin, stdout):
+ for module in stdin:
+ module = module.strip()
+ # Don't print the boolean directly as it is captilized in Python.
+ print(
+ "true" if is_std_modules(module) else "false",
+ end="\n",
+ file=stdout,
+ )
+ stdout.flush()
+
+
+if __name__ == "__main__":
+ exit(main(sys.stdin, sys.stdout))
diff --git a/gazelle/target.go b/gazelle/target.go
new file mode 100644
index 0000000..60abd0c
--- /dev/null
+++ b/gazelle/target.go
@@ -0,0 +1,136 @@
+package python
+
+import (
+ "path/filepath"
+
+ "github.com/bazelbuild/bazel-gazelle/config"
+ "github.com/bazelbuild/bazel-gazelle/rule"
+ "github.com/emirpasic/gods/sets/treeset"
+ godsutils "github.com/emirpasic/gods/utils"
+)
+
+// targetBuilder builds targets to be generated by Gazelle.
+type targetBuilder struct {
+ kind string
+ name string
+ pythonProjectRoot string
+ bzlPackage string
+ uuid string
+ srcs *treeset.Set
+ deps *treeset.Set
+ resolvedDeps *treeset.Set
+ visibility *treeset.Set
+ main *string
+ imports []string
+}
+
+// newTargetBuilder constructs a new targetBuilder.
+func newTargetBuilder(kind, name, pythonProjectRoot, bzlPackage string) *targetBuilder {
+ return &targetBuilder{
+ kind: kind,
+ name: name,
+ pythonProjectRoot: pythonProjectRoot,
+ bzlPackage: bzlPackage,
+ srcs: treeset.NewWith(godsutils.StringComparator),
+ deps: treeset.NewWith(moduleComparator),
+ resolvedDeps: treeset.NewWith(godsutils.StringComparator),
+ visibility: treeset.NewWith(godsutils.StringComparator),
+ }
+}
+
+// setUUID sets the given UUID for the target. It's used to index the generated
+// target based on this value in addition to the other ways the targets can be
+// imported. py_{binary,test} targets in the same Bazel package can add a
+// virtual dependency to this UUID that gets resolved in the Resolver interface.
+func (t *targetBuilder) setUUID(uuid string) *targetBuilder {
+ t.uuid = uuid
+ return t
+}
+
+// addSrc adds a single src to the target.
+func (t *targetBuilder) addSrc(src string) *targetBuilder {
+ t.srcs.Add(src)
+ return t
+}
+
+// addSrcs copies all values from the provided srcs to the target.
+func (t *targetBuilder) addSrcs(srcs *treeset.Set) *targetBuilder {
+ it := srcs.Iterator()
+ for it.Next() {
+ t.srcs.Add(it.Value().(string))
+ }
+ return t
+}
+
+// addModuleDependency adds a single module dep to the target.
+func (t *targetBuilder) addModuleDependency(dep module) *targetBuilder {
+ t.deps.Add(dep)
+ return t
+}
+
+// addModuleDependencies copies all values from the provided deps to the target.
+func (t *targetBuilder) addModuleDependencies(deps *treeset.Set) *targetBuilder {
+ it := deps.Iterator()
+ for it.Next() {
+ t.deps.Add(it.Value().(module))
+ }
+ return t
+}
+
+// addResolvedDependency adds a single dependency the target that has already
+// been resolved or generated. The Resolver step doesn't process it further.
+func (t *targetBuilder) addResolvedDependency(dep string) *targetBuilder {
+ t.resolvedDeps.Add(dep)
+ return t
+}
+
+// addVisibility adds a visibility to the target.
+func (t *targetBuilder) addVisibility(visibility string) *targetBuilder {
+ t.visibility.Add(visibility)
+ return t
+}
+
+// setMain sets the main file to the target.
+func (t *targetBuilder) setMain(main string) *targetBuilder {
+ t.main = &main
+ return t
+}
+
+// generateImportsAttribute generates the imports attribute.
+// These are a list of import directories to be added to the PYTHONPATH. In our
+// case, the value we add is on Bazel sub-packages to be able to perform imports
+// relative to the root project package.
+func (t *targetBuilder) generateImportsAttribute() *targetBuilder {
+ p, _ := filepath.Rel(t.bzlPackage, t.pythonProjectRoot)
+ p = filepath.Clean(p)
+ if p == "." {
+ return t
+ }
+ t.imports = []string{p}
+ return t
+}
+
+// build returns the assembled *rule.Rule for the target.
+func (t *targetBuilder) build() *rule.Rule {
+ r := rule.NewRule(t.kind, t.name)
+ if t.uuid != "" {
+ r.SetPrivateAttr(uuidKey, t.uuid)
+ }
+ if !t.srcs.Empty() {
+ r.SetAttr("srcs", t.srcs.Values())
+ }
+ if !t.visibility.Empty() {
+ r.SetAttr("visibility", t.visibility.Values())
+ }
+ if t.main != nil {
+ r.SetAttr("main", *t.main)
+ }
+ if t.imports != nil {
+ r.SetAttr("imports", t.imports)
+ }
+ if !t.deps.Empty() {
+ r.SetPrivateAttr(config.GazelleImportsKey, t.deps)
+ }
+ r.SetPrivateAttr(resolvedDepsKey, t.resolvedDeps)
+ return r
+}
\ No newline at end of file
diff --git a/gazelle/testdata/README.md b/gazelle/testdata/README.md
new file mode 100644
index 0000000..6c25d48
--- /dev/null
+++ b/gazelle/testdata/README.md
@@ -0,0 +1,12 @@
+# Gazelle Python extension test cases
+
+Each directory is a test case that contains `BUILD.in` and `BUILD.out` files for
+assertion. `BUILD.in` is used as how the build file looks before running
+Gazelle, and `BUILD.out` how the build file should look like after running
+Gazelle.
+
+Each test case is a Bazel workspace and Gazelle will run with its working
+directory set to the root of this workspace, though, the test runner will find
+`test.yaml` files and use them to determine the directory Gazelle should use for
+each inner Python project. The `test.yaml` file is a manifest for the test -
+check for the existing ones for examples.
diff --git a/gazelle/testdata/dependency_resolution_order/BUILD.in b/gazelle/testdata/dependency_resolution_order/BUILD.in
new file mode 100644
index 0000000..71a5c5a
--- /dev/null
+++ b/gazelle/testdata/dependency_resolution_order/BUILD.in
@@ -0,0 +1 @@
+# gazelle:resolve py bar //somewhere/bar
diff --git a/gazelle/testdata/dependency_resolution_order/BUILD.out b/gazelle/testdata/dependency_resolution_order/BUILD.out
new file mode 100644
index 0000000..2ba2c84
--- /dev/null
+++ b/gazelle/testdata/dependency_resolution_order/BUILD.out
@@ -0,0 +1,14 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+# gazelle:resolve py bar //somewhere/bar
+
+py_library(
+ name = "dependency_resolution_order",
+ srcs = ["__init__.py"],
+ visibility = ["//:__subpackages__"],
+ deps = [
+ "//baz",
+ "//somewhere/bar",
+ "@gazelle_python_test//pypi__some_foo",
+ ],
+)
diff --git a/gazelle/testdata/dependency_resolution_order/README.md b/gazelle/testdata/dependency_resolution_order/README.md
new file mode 100644
index 0000000..75ceb0b
--- /dev/null
+++ b/gazelle/testdata/dependency_resolution_order/README.md
@@ -0,0 +1,7 @@
+# Dependency resolution order
+
+This asserts that the generator resolves the dependencies in the right order:
+
+1. Explicit resolution via gazelle:resolve.
+2. Third-party dependencies matching in the `modules_mapping.json`.
+3. Indexed generated first-party dependencies.
diff --git a/gazelle/testdata/dependency_resolution_order/WORKSPACE b/gazelle/testdata/dependency_resolution_order/WORKSPACE
new file mode 100644
index 0000000..4959898
--- /dev/null
+++ b/gazelle/testdata/dependency_resolution_order/WORKSPACE
@@ -0,0 +1 @@
+# This is a test data Bazel workspace.
diff --git a/gazelle/testdata/dependency_resolution_order/__init__.py b/gazelle/testdata/dependency_resolution_order/__init__.py
new file mode 100644
index 0000000..f2a1c08
--- /dev/null
+++ b/gazelle/testdata/dependency_resolution_order/__init__.py
@@ -0,0 +1,10 @@
+import sys
+
+import bar
+import baz
+import foo
+
+_ = sys
+_ = bar
+_ = baz
+_ = foo
diff --git a/gazelle/testdata/dependency_resolution_order/bar/BUILD.in b/gazelle/testdata/dependency_resolution_order/bar/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/dependency_resolution_order/bar/BUILD.in
diff --git a/gazelle/testdata/dependency_resolution_order/bar/BUILD.out b/gazelle/testdata/dependency_resolution_order/bar/BUILD.out
new file mode 100644
index 0000000..da9915d
--- /dev/null
+++ b/gazelle/testdata/dependency_resolution_order/bar/BUILD.out
@@ -0,0 +1,8 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "bar",
+ srcs = ["__init__.py"],
+ imports = [".."],
+ visibility = ["//:__subpackages__"],
+)
diff --git a/gazelle/testdata/dependency_resolution_order/bar/__init__.py b/gazelle/testdata/dependency_resolution_order/bar/__init__.py
new file mode 100644
index 0000000..76c3313
--- /dev/null
+++ b/gazelle/testdata/dependency_resolution_order/bar/__init__.py
@@ -0,0 +1,3 @@
+import os
+
+_ = os
diff --git a/gazelle/testdata/dependency_resolution_order/baz/BUILD.in b/gazelle/testdata/dependency_resolution_order/baz/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/dependency_resolution_order/baz/BUILD.in
diff --git a/gazelle/testdata/dependency_resolution_order/baz/BUILD.out b/gazelle/testdata/dependency_resolution_order/baz/BUILD.out
new file mode 100644
index 0000000..749fd3d
--- /dev/null
+++ b/gazelle/testdata/dependency_resolution_order/baz/BUILD.out
@@ -0,0 +1,8 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "baz",
+ srcs = ["__init__.py"],
+ imports = [".."],
+ visibility = ["//:__subpackages__"],
+)
diff --git a/gazelle/testdata/dependency_resolution_order/baz/__init__.py b/gazelle/testdata/dependency_resolution_order/baz/__init__.py
new file mode 100644
index 0000000..76c3313
--- /dev/null
+++ b/gazelle/testdata/dependency_resolution_order/baz/__init__.py
@@ -0,0 +1,3 @@
+import os
+
+_ = os
diff --git a/gazelle/testdata/dependency_resolution_order/foo/BUILD.in b/gazelle/testdata/dependency_resolution_order/foo/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/dependency_resolution_order/foo/BUILD.in
diff --git a/gazelle/testdata/dependency_resolution_order/foo/BUILD.out b/gazelle/testdata/dependency_resolution_order/foo/BUILD.out
new file mode 100644
index 0000000..4404d30
--- /dev/null
+++ b/gazelle/testdata/dependency_resolution_order/foo/BUILD.out
@@ -0,0 +1,8 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "foo",
+ srcs = ["__init__.py"],
+ imports = [".."],
+ visibility = ["//:__subpackages__"],
+)
diff --git a/gazelle/testdata/dependency_resolution_order/foo/__init__.py b/gazelle/testdata/dependency_resolution_order/foo/__init__.py
new file mode 100644
index 0000000..76c3313
--- /dev/null
+++ b/gazelle/testdata/dependency_resolution_order/foo/__init__.py
@@ -0,0 +1,3 @@
+import os
+
+_ = os
diff --git a/gazelle/testdata/dependency_resolution_order/gazelle_python.yaml b/gazelle/testdata/dependency_resolution_order/gazelle_python.yaml
new file mode 100644
index 0000000..7e911bf
--- /dev/null
+++ b/gazelle/testdata/dependency_resolution_order/gazelle_python.yaml
@@ -0,0 +1,4 @@
+manifest:
+ modules_mapping:
+ foo: some_foo
+ pip_deps_repository_name: gazelle_python_test
diff --git a/gazelle/testdata/dependency_resolution_order/somewhere/bar/BUILD.in b/gazelle/testdata/dependency_resolution_order/somewhere/bar/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/dependency_resolution_order/somewhere/bar/BUILD.in
diff --git a/gazelle/testdata/dependency_resolution_order/somewhere/bar/BUILD.out b/gazelle/testdata/dependency_resolution_order/somewhere/bar/BUILD.out
new file mode 100644
index 0000000..a0d421b
--- /dev/null
+++ b/gazelle/testdata/dependency_resolution_order/somewhere/bar/BUILD.out
@@ -0,0 +1,8 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "bar",
+ srcs = ["__init__.py"],
+ imports = ["../.."],
+ visibility = ["//:__subpackages__"],
+)
diff --git a/gazelle/testdata/dependency_resolution_order/somewhere/bar/__init__.py b/gazelle/testdata/dependency_resolution_order/somewhere/bar/__init__.py
new file mode 100644
index 0000000..76c3313
--- /dev/null
+++ b/gazelle/testdata/dependency_resolution_order/somewhere/bar/__init__.py
@@ -0,0 +1,3 @@
+import os
+
+_ = os
diff --git a/gazelle/testdata/dependency_resolution_order/test.yaml b/gazelle/testdata/dependency_resolution_order/test.yaml
new file mode 100644
index 0000000..ed97d53
--- /dev/null
+++ b/gazelle/testdata/dependency_resolution_order/test.yaml
@@ -0,0 +1 @@
+---
diff --git a/gazelle/testdata/disable_import_statements_validation/BUILD.in b/gazelle/testdata/disable_import_statements_validation/BUILD.in
new file mode 100644
index 0000000..741aff6
--- /dev/null
+++ b/gazelle/testdata/disable_import_statements_validation/BUILD.in
@@ -0,0 +1 @@
+# gazelle:python_validate_import_statements false
diff --git a/gazelle/testdata/disable_import_statements_validation/BUILD.out b/gazelle/testdata/disable_import_statements_validation/BUILD.out
new file mode 100644
index 0000000..964db6d
--- /dev/null
+++ b/gazelle/testdata/disable_import_statements_validation/BUILD.out
@@ -0,0 +1,9 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+# gazelle:python_validate_import_statements false
+
+py_library(
+ name = "disable_import_statements_validation",
+ srcs = ["__init__.py"],
+ visibility = ["//:__subpackages__"],
+)
diff --git a/gazelle/testdata/disable_import_statements_validation/README.md b/gazelle/testdata/disable_import_statements_validation/README.md
new file mode 100644
index 0000000..a80fffe
--- /dev/null
+++ b/gazelle/testdata/disable_import_statements_validation/README.md
@@ -0,0 +1,3 @@
+# Disable import statements validation
+
+This test case asserts that the module's validation step is not performed.
diff --git a/gazelle/testdata/disable_import_statements_validation/WORKSPACE b/gazelle/testdata/disable_import_statements_validation/WORKSPACE
new file mode 100644
index 0000000..faff6af
--- /dev/null
+++ b/gazelle/testdata/disable_import_statements_validation/WORKSPACE
@@ -0,0 +1 @@
+# This is a Bazel workspace for the Gazelle test data.
diff --git a/gazelle/testdata/disable_import_statements_validation/__init__.py b/gazelle/testdata/disable_import_statements_validation/__init__.py
new file mode 100644
index 0000000..88eba74
--- /dev/null
+++ b/gazelle/testdata/disable_import_statements_validation/__init__.py
@@ -0,0 +1,3 @@
+import abcdefg
+
+_ = abcdefg
diff --git a/gazelle/testdata/disable_import_statements_validation/test.yaml b/gazelle/testdata/disable_import_statements_validation/test.yaml
new file mode 100644
index 0000000..36dd656
--- /dev/null
+++ b/gazelle/testdata/disable_import_statements_validation/test.yaml
@@ -0,0 +1,3 @@
+---
+expect:
+ exit_code: 0
diff --git a/gazelle/testdata/dont_rename_target/BUILD.in b/gazelle/testdata/dont_rename_target/BUILD.in
new file mode 100644
index 0000000..33e8ec2
--- /dev/null
+++ b/gazelle/testdata/dont_rename_target/BUILD.in
@@ -0,0 +1,5 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "my_custom_target",
+)
diff --git a/gazelle/testdata/dont_rename_target/BUILD.out b/gazelle/testdata/dont_rename_target/BUILD.out
new file mode 100644
index 0000000..62772e3
--- /dev/null
+++ b/gazelle/testdata/dont_rename_target/BUILD.out
@@ -0,0 +1,7 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "my_custom_target",
+ srcs = ["__init__.py"],
+ visibility = ["//:__subpackages__"],
+)
diff --git a/gazelle/testdata/dont_rename_target/README.md b/gazelle/testdata/dont_rename_target/README.md
new file mode 100644
index 0000000..19f9d66
--- /dev/null
+++ b/gazelle/testdata/dont_rename_target/README.md
@@ -0,0 +1,4 @@
+# Don't rename target
+
+This test case asserts that an existing target with a custom name doesn't get
+renamed by the Gazelle extension.
diff --git a/gazelle/testdata/dont_rename_target/WORKSPACE b/gazelle/testdata/dont_rename_target/WORKSPACE
new file mode 100644
index 0000000..faff6af
--- /dev/null
+++ b/gazelle/testdata/dont_rename_target/WORKSPACE
@@ -0,0 +1 @@
+# This is a Bazel workspace for the Gazelle test data.
diff --git a/gazelle/testdata/dont_rename_target/__init__.py b/gazelle/testdata/dont_rename_target/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/dont_rename_target/__init__.py
diff --git a/gazelle/testdata/dont_rename_target/test.yaml b/gazelle/testdata/dont_rename_target/test.yaml
new file mode 100644
index 0000000..ed97d53
--- /dev/null
+++ b/gazelle/testdata/dont_rename_target/test.yaml
@@ -0,0 +1 @@
+---
diff --git a/gazelle/testdata/file_name_matches_import_statement/BUILD.in b/gazelle/testdata/file_name_matches_import_statement/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/file_name_matches_import_statement/BUILD.in
diff --git a/gazelle/testdata/file_name_matches_import_statement/BUILD.out b/gazelle/testdata/file_name_matches_import_statement/BUILD.out
new file mode 100644
index 0000000..fd6c485
--- /dev/null
+++ b/gazelle/testdata/file_name_matches_import_statement/BUILD.out
@@ -0,0 +1,11 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "file_name_matches_import_statement",
+ srcs = [
+ "__init__.py",
+ "rest_framework.py",
+ ],
+ visibility = ["//:__subpackages__"],
+ deps = ["@gazelle_python_test//pypi__djangorestframework"],
+)
diff --git a/gazelle/testdata/file_name_matches_import_statement/README.md b/gazelle/testdata/file_name_matches_import_statement/README.md
new file mode 100644
index 0000000..591adc1
--- /dev/null
+++ b/gazelle/testdata/file_name_matches_import_statement/README.md
@@ -0,0 +1,4 @@
+# File name matches import statement
+
+This test case asserts that a file with an import statement that matches its own
+name does the right thing of resolving the third-party package.
diff --git a/gazelle/testdata/file_name_matches_import_statement/WORKSPACE b/gazelle/testdata/file_name_matches_import_statement/WORKSPACE
new file mode 100644
index 0000000..faff6af
--- /dev/null
+++ b/gazelle/testdata/file_name_matches_import_statement/WORKSPACE
@@ -0,0 +1 @@
+# This is a Bazel workspace for the Gazelle test data.
diff --git a/gazelle/testdata/file_name_matches_import_statement/__init__.py b/gazelle/testdata/file_name_matches_import_statement/__init__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/file_name_matches_import_statement/__init__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/file_name_matches_import_statement/gazelle_python.yaml b/gazelle/testdata/file_name_matches_import_statement/gazelle_python.yaml
new file mode 100644
index 0000000..63e6966
--- /dev/null
+++ b/gazelle/testdata/file_name_matches_import_statement/gazelle_python.yaml
@@ -0,0 +1,4 @@
+manifest:
+ modules_mapping:
+ rest_framework: djangorestframework
+ pip_deps_repository_name: gazelle_python_test
diff --git a/gazelle/testdata/file_name_matches_import_statement/rest_framework.py b/gazelle/testdata/file_name_matches_import_statement/rest_framework.py
new file mode 100644
index 0000000..9bede69
--- /dev/null
+++ b/gazelle/testdata/file_name_matches_import_statement/rest_framework.py
@@ -0,0 +1,3 @@
+import rest_framework
+
+_ = rest_framework
diff --git a/gazelle/testdata/file_name_matches_import_statement/test.yaml b/gazelle/testdata/file_name_matches_import_statement/test.yaml
new file mode 100644
index 0000000..ed97d53
--- /dev/null
+++ b/gazelle/testdata/file_name_matches_import_statement/test.yaml
@@ -0,0 +1 @@
+---
diff --git a/gazelle/testdata/first_party_dependencies/BUILD.in b/gazelle/testdata/first_party_dependencies/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/first_party_dependencies/BUILD.in
diff --git a/gazelle/testdata/first_party_dependencies/BUILD.out b/gazelle/testdata/first_party_dependencies/BUILD.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/first_party_dependencies/BUILD.out
diff --git a/gazelle/testdata/first_party_dependencies/README.md b/gazelle/testdata/first_party_dependencies/README.md
new file mode 100644
index 0000000..f57e255
--- /dev/null
+++ b/gazelle/testdata/first_party_dependencies/README.md
@@ -0,0 +1,11 @@
+# First-party dependencies
+
+There are 2 different scenarios that the extension needs to handle:
+
+1. Import statements that match sub-directory names.
+2. Import statements that don't match sub-directory names and need a hint from
+ the user via directives.
+
+This test case asserts that the generated targets cover both scenarios.
+
+With the hint we need to check if it's a .py file or a directory with `__init__.py` file.
diff --git a/gazelle/testdata/first_party_dependencies/WORKSPACE b/gazelle/testdata/first_party_dependencies/WORKSPACE
new file mode 100644
index 0000000..4959898
--- /dev/null
+++ b/gazelle/testdata/first_party_dependencies/WORKSPACE
@@ -0,0 +1 @@
+# This is a test data Bazel workspace.
diff --git a/gazelle/testdata/first_party_dependencies/one/BUILD.in b/gazelle/testdata/first_party_dependencies/one/BUILD.in
new file mode 100644
index 0000000..6948b47
--- /dev/null
+++ b/gazelle/testdata/first_party_dependencies/one/BUILD.in
@@ -0,0 +1 @@
+# gazelle:python_root
diff --git a/gazelle/testdata/first_party_dependencies/one/BUILD.out b/gazelle/testdata/first_party_dependencies/one/BUILD.out
new file mode 100644
index 0000000..c96a561
--- /dev/null
+++ b/gazelle/testdata/first_party_dependencies/one/BUILD.out
@@ -0,0 +1,15 @@
+load("@rules_python//python:defs.bzl", "py_binary")
+
+# gazelle:python_root
+
+py_binary(
+ name = "one_bin",
+ srcs = ["__main__.py"],
+ main = "__main__.py",
+ visibility = ["//one:__subpackages__"],
+ deps = [
+ "//one/bar",
+ "//one/bar/baz",
+ "//one/foo",
+ ],
+)
diff --git a/gazelle/testdata/first_party_dependencies/one/__main__.py b/gazelle/testdata/first_party_dependencies/one/__main__.py
new file mode 100644
index 0000000..2d241cc
--- /dev/null
+++ b/gazelle/testdata/first_party_dependencies/one/__main__.py
@@ -0,0 +1,12 @@
+import os
+
+from bar import bar
+from bar.baz import baz
+from foo import foo
+
+if __name__ == "__main__":
+ INIT_FILENAME = "__init__.py"
+ dirname = os.path.dirname(os.path.abspath(__file__))
+ assert bar() == os.path.join(dirname, "bar", INIT_FILENAME)
+ assert baz() == os.path.join(dirname, "bar", "baz", INIT_FILENAME)
+ assert foo() == os.path.join(dirname, "foo", INIT_FILENAME)
diff --git a/gazelle/testdata/first_party_dependencies/one/bar/BUILD.in b/gazelle/testdata/first_party_dependencies/one/bar/BUILD.in
new file mode 100644
index 0000000..7fe1f49
--- /dev/null
+++ b/gazelle/testdata/first_party_dependencies/one/bar/BUILD.in
@@ -0,0 +1,10 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "bar",
+ srcs = ["__init__.py"],
+ visibility = [
+ "//one:__subpackages__",
+ "//three:__subpackages__",
+ ],
+)
diff --git a/gazelle/testdata/first_party_dependencies/one/bar/BUILD.out b/gazelle/testdata/first_party_dependencies/one/bar/BUILD.out
new file mode 100644
index 0000000..470bf82
--- /dev/null
+++ b/gazelle/testdata/first_party_dependencies/one/bar/BUILD.out
@@ -0,0 +1,11 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "bar",
+ srcs = ["__init__.py"],
+ imports = [".."],
+ visibility = [
+ "//one:__subpackages__",
+ "//three:__subpackages__",
+ ],
+)
diff --git a/gazelle/testdata/first_party_dependencies/one/bar/__init__.py b/gazelle/testdata/first_party_dependencies/one/bar/__init__.py
new file mode 100644
index 0000000..e311ff1
--- /dev/null
+++ b/gazelle/testdata/first_party_dependencies/one/bar/__init__.py
@@ -0,0 +1,5 @@
+import os
+
+
+def bar():
+ return os.path.abspath(__file__)
diff --git a/gazelle/testdata/first_party_dependencies/one/bar/baz/BUILD.in b/gazelle/testdata/first_party_dependencies/one/bar/baz/BUILD.in
new file mode 100644
index 0000000..886a89c
--- /dev/null
+++ b/gazelle/testdata/first_party_dependencies/one/bar/baz/BUILD.in
@@ -0,0 +1,10 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "baz",
+ srcs = ["__init__.py"],
+ visibility = [
+ "//one:__subpackages__",
+ "//three:__subpackages__",
+ ],
+)
diff --git a/gazelle/testdata/first_party_dependencies/one/bar/baz/BUILD.out b/gazelle/testdata/first_party_dependencies/one/bar/baz/BUILD.out
new file mode 100644
index 0000000..a017245
--- /dev/null
+++ b/gazelle/testdata/first_party_dependencies/one/bar/baz/BUILD.out
@@ -0,0 +1,11 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "baz",
+ srcs = ["__init__.py"],
+ imports = ["../.."],
+ visibility = [
+ "//one:__subpackages__",
+ "//three:__subpackages__",
+ ],
+)
diff --git a/gazelle/testdata/first_party_dependencies/one/bar/baz/__init__.py b/gazelle/testdata/first_party_dependencies/one/bar/baz/__init__.py
new file mode 100644
index 0000000..e74f519
--- /dev/null
+++ b/gazelle/testdata/first_party_dependencies/one/bar/baz/__init__.py
@@ -0,0 +1,5 @@
+import os
+
+
+def baz():
+ return os.path.abspath(__file__)
diff --git a/gazelle/testdata/first_party_dependencies/one/foo/BUILD.in b/gazelle/testdata/first_party_dependencies/one/foo/BUILD.in
new file mode 100644
index 0000000..0ee9a30
--- /dev/null
+++ b/gazelle/testdata/first_party_dependencies/one/foo/BUILD.in
@@ -0,0 +1,11 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "foo",
+ srcs = ["__init__.py"],
+ visibility = [
+ "//one:__subpackages__",
+ "//three:__subpackages__",
+ "//two:__subpackages__",
+ ],
+)
diff --git a/gazelle/testdata/first_party_dependencies/one/foo/BUILD.out b/gazelle/testdata/first_party_dependencies/one/foo/BUILD.out
new file mode 100644
index 0000000..464fabb
--- /dev/null
+++ b/gazelle/testdata/first_party_dependencies/one/foo/BUILD.out
@@ -0,0 +1,12 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "foo",
+ srcs = ["__init__.py"],
+ imports = [".."],
+ visibility = [
+ "//one:__subpackages__",
+ "//three:__subpackages__",
+ "//two:__subpackages__",
+ ],
+)
diff --git a/gazelle/testdata/first_party_dependencies/one/foo/__init__.py b/gazelle/testdata/first_party_dependencies/one/foo/__init__.py
new file mode 100644
index 0000000..8aeca3d
--- /dev/null
+++ b/gazelle/testdata/first_party_dependencies/one/foo/__init__.py
@@ -0,0 +1,5 @@
+import os
+
+
+def foo():
+ return os.path.abspath(__file__)
diff --git a/gazelle/testdata/first_party_dependencies/test.yaml b/gazelle/testdata/first_party_dependencies/test.yaml
new file mode 100644
index 0000000..ed97d53
--- /dev/null
+++ b/gazelle/testdata/first_party_dependencies/test.yaml
@@ -0,0 +1 @@
+---
diff --git a/gazelle/testdata/first_party_dependencies/three/BUILD.in b/gazelle/testdata/first_party_dependencies/three/BUILD.in
new file mode 100644
index 0000000..6948b47
--- /dev/null
+++ b/gazelle/testdata/first_party_dependencies/three/BUILD.in
@@ -0,0 +1 @@
+# gazelle:python_root
diff --git a/gazelle/testdata/first_party_dependencies/three/BUILD.out b/gazelle/testdata/first_party_dependencies/three/BUILD.out
new file mode 100644
index 0000000..ccfb3e0
--- /dev/null
+++ b/gazelle/testdata/first_party_dependencies/three/BUILD.out
@@ -0,0 +1,14 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+# gazelle:python_root
+
+py_library(
+ name = "three",
+ srcs = ["__init__.py"],
+ visibility = ["//three:__subpackages__"],
+ deps = [
+ "//one/bar",
+ "//one/bar/baz",
+ "//one/foo",
+ ],
+)
diff --git a/gazelle/testdata/first_party_dependencies/three/__init__.py b/gazelle/testdata/first_party_dependencies/three/__init__.py
new file mode 100644
index 0000000..41bec88
--- /dev/null
+++ b/gazelle/testdata/first_party_dependencies/three/__init__.py
@@ -0,0 +1,10 @@
+import os
+
+from bar import bar
+from bar.baz import baz
+from foo import foo
+
+_ = os
+_ = bar
+_ = baz
+_ = foo
diff --git a/gazelle/testdata/first_party_dependencies/two/BUILD.in b/gazelle/testdata/first_party_dependencies/two/BUILD.in
new file mode 100644
index 0000000..6948b47
--- /dev/null
+++ b/gazelle/testdata/first_party_dependencies/two/BUILD.in
@@ -0,0 +1 @@
+# gazelle:python_root
diff --git a/gazelle/testdata/first_party_dependencies/two/BUILD.out b/gazelle/testdata/first_party_dependencies/two/BUILD.out
new file mode 100644
index 0000000..182db08
--- /dev/null
+++ b/gazelle/testdata/first_party_dependencies/two/BUILD.out
@@ -0,0 +1,10 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+# gazelle:python_root
+
+py_library(
+ name = "two",
+ srcs = ["__init__.py"],
+ visibility = ["//two:__subpackages__"],
+ deps = ["//one/foo"],
+)
diff --git a/gazelle/testdata/first_party_dependencies/two/__init__.py b/gazelle/testdata/first_party_dependencies/two/__init__.py
new file mode 100644
index 0000000..a0bb5c8
--- /dev/null
+++ b/gazelle/testdata/first_party_dependencies/two/__init__.py
@@ -0,0 +1,6 @@
+import os
+
+from foo import foo
+
+_ = os
+_ = foo
diff --git a/gazelle/testdata/first_party_file_and_directory_modules/BUILD.in b/gazelle/testdata/first_party_file_and_directory_modules/BUILD.in
new file mode 100644
index 0000000..fb90e4c
--- /dev/null
+++ b/gazelle/testdata/first_party_file_and_directory_modules/BUILD.in
@@ -0,0 +1 @@
+# gazelle:resolve py foo //foo
diff --git a/gazelle/testdata/first_party_file_and_directory_modules/BUILD.out b/gazelle/testdata/first_party_file_and_directory_modules/BUILD.out
new file mode 100644
index 0000000..264205b
--- /dev/null
+++ b/gazelle/testdata/first_party_file_and_directory_modules/BUILD.out
@@ -0,0 +1,25 @@
+load("@rules_python//python:defs.bzl", "py_binary", "py_library")
+
+# gazelle:resolve py foo //foo
+
+py_library(
+ name = "first_party_file_and_directory_modules",
+ srcs = [
+ "baz.py",
+ "foo.py",
+ ],
+ visibility = ["//:__subpackages__"],
+)
+
+py_binary(
+ name = "first_party_file_and_directory_modules_bin",
+ srcs = ["__main__.py"],
+ main = "__main__.py",
+ visibility = ["//:__subpackages__"],
+ deps = [
+ ":first_party_file_and_directory_modules",
+ "//foo",
+ "//one",
+ "//undiscoverable/package1/subpackage1",
+ ],
+)
diff --git a/gazelle/testdata/first_party_file_and_directory_modules/README.md b/gazelle/testdata/first_party_file_and_directory_modules/README.md
new file mode 100644
index 0000000..2a173b4
--- /dev/null
+++ b/gazelle/testdata/first_party_file_and_directory_modules/README.md
@@ -0,0 +1,9 @@
+# First-party file and directory module dependencies
+
+This test case asserts that a `py_library` is generated with the dependencies
+pointing to the correct first-party target that contains a Python module file
+that was imported directly instead of a directory containing `__init__.py`.
+
+Also, it asserts that the directory with the `__init__.py` file is selected
+instead of a module file with same. E.g. `foo/__init__.py` takes precedence over
+`foo.py` when `import foo` exists.
diff --git a/gazelle/testdata/first_party_file_and_directory_modules/WORKSPACE b/gazelle/testdata/first_party_file_and_directory_modules/WORKSPACE
new file mode 100644
index 0000000..faff6af
--- /dev/null
+++ b/gazelle/testdata/first_party_file_and_directory_modules/WORKSPACE
@@ -0,0 +1 @@
+# This is a Bazel workspace for the Gazelle test data.
diff --git a/gazelle/testdata/first_party_file_and_directory_modules/__main__.py b/gazelle/testdata/first_party_file_and_directory_modules/__main__.py
new file mode 100644
index 0000000..6aca4f0
--- /dev/null
+++ b/gazelle/testdata/first_party_file_and_directory_modules/__main__.py
@@ -0,0 +1,11 @@
+import foo
+from baz import baz as another_baz
+from foo.bar import baz
+from one.two import two
+from package1.subpackage1.module1 import find_me
+
+assert not hasattr(foo, 'foo')
+assert baz() == 'baz from foo/bar.py'
+assert another_baz() == 'baz from baz.py'
+assert two() == 'two'
+assert find_me() == 'found'
diff --git a/gazelle/testdata/first_party_file_and_directory_modules/baz.py b/gazelle/testdata/first_party_file_and_directory_modules/baz.py
new file mode 100644
index 0000000..cc29925
--- /dev/null
+++ b/gazelle/testdata/first_party_file_and_directory_modules/baz.py
@@ -0,0 +1,2 @@
+def baz():
+ return 'baz from baz.py'
diff --git a/gazelle/testdata/first_party_file_and_directory_modules/foo.py b/gazelle/testdata/first_party_file_and_directory_modules/foo.py
new file mode 100644
index 0000000..81d3ef1
--- /dev/null
+++ b/gazelle/testdata/first_party_file_and_directory_modules/foo.py
@@ -0,0 +1,2 @@
+def foo():
+ print('foo')
diff --git a/gazelle/testdata/first_party_file_and_directory_modules/foo/BUILD.in b/gazelle/testdata/first_party_file_and_directory_modules/foo/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/first_party_file_and_directory_modules/foo/BUILD.in
diff --git a/gazelle/testdata/first_party_file_and_directory_modules/foo/BUILD.out b/gazelle/testdata/first_party_file_and_directory_modules/foo/BUILD.out
new file mode 100644
index 0000000..3decd90
--- /dev/null
+++ b/gazelle/testdata/first_party_file_and_directory_modules/foo/BUILD.out
@@ -0,0 +1,12 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "foo",
+ srcs = [
+ "__init__.py",
+ "bar.py",
+ ],
+ imports = [".."],
+ visibility = ["//:__subpackages__"],
+ deps = ["//one"],
+)
diff --git a/gazelle/testdata/first_party_file_and_directory_modules/foo/__init__.py b/gazelle/testdata/first_party_file_and_directory_modules/foo/__init__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/first_party_file_and_directory_modules/foo/__init__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/first_party_file_and_directory_modules/foo/bar.py b/gazelle/testdata/first_party_file_and_directory_modules/foo/bar.py
new file mode 100644
index 0000000..4b6419f
--- /dev/null
+++ b/gazelle/testdata/first_party_file_and_directory_modules/foo/bar.py
@@ -0,0 +1,7 @@
+import one.two as two
+
+_ = two
+
+
+def baz():
+ return 'baz from foo/bar.py'
diff --git a/gazelle/testdata/first_party_file_and_directory_modules/one/BUILD.in b/gazelle/testdata/first_party_file_and_directory_modules/one/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/first_party_file_and_directory_modules/one/BUILD.in
diff --git a/gazelle/testdata/first_party_file_and_directory_modules/one/BUILD.out b/gazelle/testdata/first_party_file_and_directory_modules/one/BUILD.out
new file mode 100644
index 0000000..7063141
--- /dev/null
+++ b/gazelle/testdata/first_party_file_and_directory_modules/one/BUILD.out
@@ -0,0 +1,11 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "one",
+ srcs = [
+ "__init__.py",
+ "two.py",
+ ],
+ imports = [".."],
+ visibility = ["//:__subpackages__"],
+)
diff --git a/gazelle/testdata/first_party_file_and_directory_modules/one/__init__.py b/gazelle/testdata/first_party_file_and_directory_modules/one/__init__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/first_party_file_and_directory_modules/one/__init__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/first_party_file_and_directory_modules/one/two.py b/gazelle/testdata/first_party_file_and_directory_modules/one/two.py
new file mode 100644
index 0000000..ce53b87
--- /dev/null
+++ b/gazelle/testdata/first_party_file_and_directory_modules/one/two.py
@@ -0,0 +1,2 @@
+def two():
+ return 'two'
diff --git a/gazelle/testdata/first_party_file_and_directory_modules/test.yaml b/gazelle/testdata/first_party_file_and_directory_modules/test.yaml
new file mode 100644
index 0000000..ed97d53
--- /dev/null
+++ b/gazelle/testdata/first_party_file_and_directory_modules/test.yaml
@@ -0,0 +1 @@
+---
diff --git a/gazelle/testdata/first_party_file_and_directory_modules/undiscoverable/BUILD.in b/gazelle/testdata/first_party_file_and_directory_modules/undiscoverable/BUILD.in
new file mode 100644
index 0000000..6948b47
--- /dev/null
+++ b/gazelle/testdata/first_party_file_and_directory_modules/undiscoverable/BUILD.in
@@ -0,0 +1 @@
+# gazelle:python_root
diff --git a/gazelle/testdata/first_party_file_and_directory_modules/undiscoverable/BUILD.out b/gazelle/testdata/first_party_file_and_directory_modules/undiscoverable/BUILD.out
new file mode 100644
index 0000000..6948b47
--- /dev/null
+++ b/gazelle/testdata/first_party_file_and_directory_modules/undiscoverable/BUILD.out
@@ -0,0 +1 @@
+# gazelle:python_root
diff --git a/gazelle/testdata/first_party_file_and_directory_modules/undiscoverable/package1/subpackage1/BUILD.in b/gazelle/testdata/first_party_file_and_directory_modules/undiscoverable/package1/subpackage1/BUILD.in
new file mode 100644
index 0000000..c7d0e48
--- /dev/null
+++ b/gazelle/testdata/first_party_file_and_directory_modules/undiscoverable/package1/subpackage1/BUILD.in
@@ -0,0 +1,12 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "subpackage1",
+ srcs = [
+ "__init__.py",
+ "module1.py",
+ ],
+ imports = ["../.."],
+ # Manual fix to visibility after initial generation.
+ visibility = ["//:__subpackages__"],
+)
diff --git a/gazelle/testdata/first_party_file_and_directory_modules/undiscoverable/package1/subpackage1/BUILD.out b/gazelle/testdata/first_party_file_and_directory_modules/undiscoverable/package1/subpackage1/BUILD.out
new file mode 100644
index 0000000..c7d0e48
--- /dev/null
+++ b/gazelle/testdata/first_party_file_and_directory_modules/undiscoverable/package1/subpackage1/BUILD.out
@@ -0,0 +1,12 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "subpackage1",
+ srcs = [
+ "__init__.py",
+ "module1.py",
+ ],
+ imports = ["../.."],
+ # Manual fix to visibility after initial generation.
+ visibility = ["//:__subpackages__"],
+)
diff --git a/gazelle/testdata/first_party_file_and_directory_modules/undiscoverable/package1/subpackage1/__init__.py b/gazelle/testdata/first_party_file_and_directory_modules/undiscoverable/package1/subpackage1/__init__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/first_party_file_and_directory_modules/undiscoverable/package1/subpackage1/__init__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/first_party_file_and_directory_modules/undiscoverable/package1/subpackage1/module1.py b/gazelle/testdata/first_party_file_and_directory_modules/undiscoverable/package1/subpackage1/module1.py
new file mode 100644
index 0000000..668c700
--- /dev/null
+++ b/gazelle/testdata/first_party_file_and_directory_modules/undiscoverable/package1/subpackage1/module1.py
@@ -0,0 +1,2 @@
+def find_me():
+ return 'found'
diff --git a/gazelle/testdata/generated_test_entrypoint/BUILD.in b/gazelle/testdata/generated_test_entrypoint/BUILD.in
new file mode 100644
index 0000000..06616fb
--- /dev/null
+++ b/gazelle/testdata/generated_test_entrypoint/BUILD.in
@@ -0,0 +1,10 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+something(
+ name = "__test__",
+)
+
+py_library(
+ name = "generated_test_entrypoint",
+ srcs = ["__init__.py"],
+)
diff --git a/gazelle/testdata/generated_test_entrypoint/BUILD.out b/gazelle/testdata/generated_test_entrypoint/BUILD.out
new file mode 100644
index 0000000..48df068
--- /dev/null
+++ b/gazelle/testdata/generated_test_entrypoint/BUILD.out
@@ -0,0 +1,24 @@
+load("@rules_python//python:defs.bzl", "py_library", "py_test")
+
+something(
+ name = "__test__",
+)
+
+py_library(
+ name = "generated_test_entrypoint",
+ srcs = [
+ "__init__.py",
+ "foo.py",
+ ],
+ visibility = ["//:__subpackages__"],
+)
+
+py_test(
+ name = "generated_test_entrypoint_test",
+ srcs = [":__test__"],
+ main = ":__test__.py",
+ deps = [
+ ":__test__",
+ ":generated_test_entrypoint",
+ ],
+)
diff --git a/gazelle/testdata/generated_test_entrypoint/README.md b/gazelle/testdata/generated_test_entrypoint/README.md
new file mode 100644
index 0000000..69f8415
--- /dev/null
+++ b/gazelle/testdata/generated_test_entrypoint/README.md
@@ -0,0 +1,4 @@
+# Generated test entrypoint
+
+This test case asserts that a `py_test` is generated using a target named
+`__test__` as its `main` entrypoint.
diff --git a/gazelle/testdata/generated_test_entrypoint/WORKSPACE b/gazelle/testdata/generated_test_entrypoint/WORKSPACE
new file mode 100644
index 0000000..faff6af
--- /dev/null
+++ b/gazelle/testdata/generated_test_entrypoint/WORKSPACE
@@ -0,0 +1 @@
+# This is a Bazel workspace for the Gazelle test data.
diff --git a/gazelle/testdata/generated_test_entrypoint/__init__.py b/gazelle/testdata/generated_test_entrypoint/__init__.py
new file mode 100644
index 0000000..6a49193
--- /dev/null
+++ b/gazelle/testdata/generated_test_entrypoint/__init__.py
@@ -0,0 +1,3 @@
+from foo import foo
+
+_ = foo
diff --git a/gazelle/testdata/generated_test_entrypoint/foo.py b/gazelle/testdata/generated_test_entrypoint/foo.py
new file mode 100644
index 0000000..a266b7c
--- /dev/null
+++ b/gazelle/testdata/generated_test_entrypoint/foo.py
@@ -0,0 +1,2 @@
+def foo():
+ return 'foo'
diff --git a/gazelle/testdata/generated_test_entrypoint/test.yaml b/gazelle/testdata/generated_test_entrypoint/test.yaml
new file mode 100644
index 0000000..ed97d53
--- /dev/null
+++ b/gazelle/testdata/generated_test_entrypoint/test.yaml
@@ -0,0 +1 @@
+---
diff --git a/gazelle/testdata/ignored_invalid_imported_module/BUILD.in b/gazelle/testdata/ignored_invalid_imported_module/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/ignored_invalid_imported_module/BUILD.in
diff --git a/gazelle/testdata/ignored_invalid_imported_module/BUILD.out b/gazelle/testdata/ignored_invalid_imported_module/BUILD.out
new file mode 100644
index 0000000..3cd47a6
--- /dev/null
+++ b/gazelle/testdata/ignored_invalid_imported_module/BUILD.out
@@ -0,0 +1,8 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "ignored_invalid_imported_module",
+ srcs = ["__init__.py"],
+ visibility = ["//:__subpackages__"],
+ deps = ["@gazelle_python_test//pypi__foo"],
+)
diff --git a/gazelle/testdata/ignored_invalid_imported_module/README.md b/gazelle/testdata/ignored_invalid_imported_module/README.md
new file mode 100644
index 0000000..55dcc9b
--- /dev/null
+++ b/gazelle/testdata/ignored_invalid_imported_module/README.md
@@ -0,0 +1,3 @@
+# Ignored invalid imported module
+
+This test case asserts that the module's validation step succeeds as expected.
diff --git a/gazelle/testdata/ignored_invalid_imported_module/WORKSPACE b/gazelle/testdata/ignored_invalid_imported_module/WORKSPACE
new file mode 100644
index 0000000..faff6af
--- /dev/null
+++ b/gazelle/testdata/ignored_invalid_imported_module/WORKSPACE
@@ -0,0 +1 @@
+# This is a Bazel workspace for the Gazelle test data.
diff --git a/gazelle/testdata/ignored_invalid_imported_module/__init__.py b/gazelle/testdata/ignored_invalid_imported_module/__init__.py
new file mode 100644
index 0000000..4301453
--- /dev/null
+++ b/gazelle/testdata/ignored_invalid_imported_module/__init__.py
@@ -0,0 +1,22 @@
+# gazelle:ignore abcdefg1,abcdefg2
+# gazelle:ignore abcdefg3
+
+import abcdefg1
+import abcdefg2
+import abcdefg3
+import foo
+
+_ = abcdefg1
+_ = abcdefg2
+_ = abcdefg3
+_ = foo
+
+try:
+ # gazelle:ignore grpc
+ import grpc
+
+ grpc_available = True
+except ImportError:
+ grpc_available = False
+
+_ = grpc
diff --git a/gazelle/testdata/ignored_invalid_imported_module/gazelle_python.yaml b/gazelle/testdata/ignored_invalid_imported_module/gazelle_python.yaml
new file mode 100644
index 0000000..54b3148
--- /dev/null
+++ b/gazelle/testdata/ignored_invalid_imported_module/gazelle_python.yaml
@@ -0,0 +1,4 @@
+manifest:
+ modules_mapping:
+ foo: foo
+ pip_deps_repository_name: gazelle_python_test
diff --git a/gazelle/testdata/ignored_invalid_imported_module/test.yaml b/gazelle/testdata/ignored_invalid_imported_module/test.yaml
new file mode 100644
index 0000000..36dd656
--- /dev/null
+++ b/gazelle/testdata/ignored_invalid_imported_module/test.yaml
@@ -0,0 +1,3 @@
+---
+expect:
+ exit_code: 0
diff --git a/gazelle/testdata/invalid_imported_module/BUILD.in b/gazelle/testdata/invalid_imported_module/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/invalid_imported_module/BUILD.in
diff --git a/gazelle/testdata/invalid_imported_module/BUILD.out b/gazelle/testdata/invalid_imported_module/BUILD.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/invalid_imported_module/BUILD.out
diff --git a/gazelle/testdata/invalid_imported_module/README.md b/gazelle/testdata/invalid_imported_module/README.md
new file mode 100644
index 0000000..85e6f45
--- /dev/null
+++ b/gazelle/testdata/invalid_imported_module/README.md
@@ -0,0 +1,3 @@
+# Invalid imported module
+
+This test case asserts that the module's validation step fails as expected.
diff --git a/gazelle/testdata/invalid_imported_module/WORKSPACE b/gazelle/testdata/invalid_imported_module/WORKSPACE
new file mode 100644
index 0000000..faff6af
--- /dev/null
+++ b/gazelle/testdata/invalid_imported_module/WORKSPACE
@@ -0,0 +1 @@
+# This is a Bazel workspace for the Gazelle test data.
diff --git a/gazelle/testdata/invalid_imported_module/__init__.py b/gazelle/testdata/invalid_imported_module/__init__.py
new file mode 100644
index 0000000..c100931
--- /dev/null
+++ b/gazelle/testdata/invalid_imported_module/__init__.py
@@ -0,0 +1,8 @@
+try:
+ import grpc
+
+ grpc_available = True
+except ImportError:
+ grpc_available = False
+
+_ = grpc
diff --git a/gazelle/testdata/invalid_imported_module/test.yaml b/gazelle/testdata/invalid_imported_module/test.yaml
new file mode 100644
index 0000000..f12c36b
--- /dev/null
+++ b/gazelle/testdata/invalid_imported_module/test.yaml
@@ -0,0 +1,8 @@
+---
+expect:
+ exit_code: 1
+ stderr: |
+ gazelle: ERROR: failed to validate dependencies for target "//:invalid_imported_module": "grpc" at line 2 from "__init__.py" is an invalid dependency: possible solutions:
+ 1. Add it as a dependency in the requirements.txt file.
+ 2. Instruct Gazelle to resolve to a known dependency using the gazelle:resolve directive.
+ 3. Ignore it with a comment '# gazelle:ignore grpc' in the Python file.
diff --git a/gazelle/testdata/monorepo/BUILD.in b/gazelle/testdata/monorepo/BUILD.in
new file mode 100644
index 0000000..adc9e83
--- /dev/null
+++ b/gazelle/testdata/monorepo/BUILD.in
@@ -0,0 +1 @@
+# gazelle:python_extension disabled
diff --git a/gazelle/testdata/monorepo/BUILD.out b/gazelle/testdata/monorepo/BUILD.out
new file mode 100644
index 0000000..adc9e83
--- /dev/null
+++ b/gazelle/testdata/monorepo/BUILD.out
@@ -0,0 +1 @@
+# gazelle:python_extension disabled
diff --git a/gazelle/testdata/monorepo/README.md b/gazelle/testdata/monorepo/README.md
new file mode 100644
index 0000000..b3ac3d2
--- /dev/null
+++ b/gazelle/testdata/monorepo/README.md
@@ -0,0 +1,4 @@
+# Monorepo
+
+This test case focuses on having multiple configurations tweaked in combination
+to simulate a monorepo.
diff --git a/gazelle/testdata/monorepo/WORKSPACE b/gazelle/testdata/monorepo/WORKSPACE
new file mode 100644
index 0000000..4959898
--- /dev/null
+++ b/gazelle/testdata/monorepo/WORKSPACE
@@ -0,0 +1 @@
+# This is a test data Bazel workspace.
diff --git a/gazelle/testdata/monorepo/coarse_grained/BUILD.in b/gazelle/testdata/monorepo/coarse_grained/BUILD.in
new file mode 100644
index 0000000..b85b321
--- /dev/null
+++ b/gazelle/testdata/monorepo/coarse_grained/BUILD.in
@@ -0,0 +1,12 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+# gazelle:python_extension enabled
+# gazelle:python_root
+# gazelle:python_generation_mode project
+
+# gazelle:exclude bar/baz/*_excluded.py
+
+py_library(
+ name = "coarse_grained",
+ visibility = ["//:__subpackages__"],
+)
diff --git a/gazelle/testdata/monorepo/coarse_grained/BUILD.out b/gazelle/testdata/monorepo/coarse_grained/BUILD.out
new file mode 100644
index 0000000..0fba951
--- /dev/null
+++ b/gazelle/testdata/monorepo/coarse_grained/BUILD.out
@@ -0,0 +1,20 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+# gazelle:python_extension enabled
+# gazelle:python_root
+# gazelle:python_generation_mode project
+
+# gazelle:exclude bar/baz/*_excluded.py
+
+py_library(
+ name = "coarse_grained",
+ srcs = [
+ "__init__.py",
+ "bar/__init__.py",
+ "bar/baz/__init__.py",
+ "bar/baz/hue.py",
+ "foo/__init__.py",
+ ],
+ visibility = ["//:__subpackages__"],
+ deps = ["@root_pip_deps//pypi__rootboto3"],
+)
diff --git a/gazelle/testdata/monorepo/coarse_grained/__init__.py b/gazelle/testdata/monorepo/coarse_grained/__init__.py
new file mode 100644
index 0000000..2b5b044
--- /dev/null
+++ b/gazelle/testdata/monorepo/coarse_grained/__init__.py
@@ -0,0 +1,12 @@
+import os
+
+import boto3
+from bar import bar
+from bar.baz import baz
+from foo import foo
+
+_ = os
+_ = boto3
+_ = bar
+_ = baz
+_ = foo
diff --git a/gazelle/testdata/monorepo/coarse_grained/_boundary/BUILD.in b/gazelle/testdata/monorepo/coarse_grained/_boundary/BUILD.in
new file mode 100644
index 0000000..421b486
--- /dev/null
+++ b/gazelle/testdata/monorepo/coarse_grained/_boundary/BUILD.in
@@ -0,0 +1 @@
+# gazelle:python_generation_mode package
diff --git a/gazelle/testdata/monorepo/coarse_grained/_boundary/BUILD.out b/gazelle/testdata/monorepo/coarse_grained/_boundary/BUILD.out
new file mode 100644
index 0000000..837e59f
--- /dev/null
+++ b/gazelle/testdata/monorepo/coarse_grained/_boundary/BUILD.out
@@ -0,0 +1,10 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+# gazelle:python_generation_mode package
+
+py_library(
+ name = "_boundary",
+ srcs = ["__init__.py"],
+ imports = [".."],
+ visibility = ["//coarse_grained:__subpackages__"],
+)
diff --git a/gazelle/testdata/monorepo/coarse_grained/_boundary/README.md b/gazelle/testdata/monorepo/coarse_grained/_boundary/README.md
new file mode 100644
index 0000000..0e67695
--- /dev/null
+++ b/gazelle/testdata/monorepo/coarse_grained/_boundary/README.md
@@ -0,0 +1,5 @@
+# \_boundary
+
+This Bazel package must be before other packages in the `coarse_grained`
+directory so that we assert that walking the tree still happens after ignoring
+this package from the parent coarse-grained generation.
diff --git a/gazelle/testdata/monorepo/coarse_grained/_boundary/__init__.py b/gazelle/testdata/monorepo/coarse_grained/_boundary/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/monorepo/coarse_grained/_boundary/__init__.py
diff --git a/gazelle/testdata/monorepo/coarse_grained/bar/__init__.py b/gazelle/testdata/monorepo/coarse_grained/bar/__init__.py
new file mode 100644
index 0000000..f6ec214
--- /dev/null
+++ b/gazelle/testdata/monorepo/coarse_grained/bar/__init__.py
@@ -0,0 +1,9 @@
+import os
+
+import boto3
+
+_ = boto3
+
+
+def bar():
+ return os.path.abspath(__file__)
diff --git a/gazelle/testdata/monorepo/coarse_grained/bar/baz/__init__.py b/gazelle/testdata/monorepo/coarse_grained/bar/baz/__init__.py
new file mode 100644
index 0000000..e74f519
--- /dev/null
+++ b/gazelle/testdata/monorepo/coarse_grained/bar/baz/__init__.py
@@ -0,0 +1,5 @@
+import os
+
+
+def baz():
+ return os.path.abspath(__file__)
diff --git a/gazelle/testdata/monorepo/coarse_grained/bar/baz/first_excluded.py b/gazelle/testdata/monorepo/coarse_grained/bar/baz/first_excluded.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/monorepo/coarse_grained/bar/baz/first_excluded.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/monorepo/coarse_grained/bar/baz/hue.py b/gazelle/testdata/monorepo/coarse_grained/bar/baz/hue.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/monorepo/coarse_grained/bar/baz/hue.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/monorepo/coarse_grained/bar/baz/second_excluded.py b/gazelle/testdata/monorepo/coarse_grained/bar/baz/second_excluded.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/monorepo/coarse_grained/bar/baz/second_excluded.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/monorepo/coarse_grained/foo/__init__.py b/gazelle/testdata/monorepo/coarse_grained/foo/__init__.py
new file mode 100644
index 0000000..8aeca3d
--- /dev/null
+++ b/gazelle/testdata/monorepo/coarse_grained/foo/__init__.py
@@ -0,0 +1,5 @@
+import os
+
+
+def foo():
+ return os.path.abspath(__file__)
diff --git a/gazelle/testdata/monorepo/coarse_grained/packages_mapping.json b/gazelle/testdata/monorepo/coarse_grained/packages_mapping.json
new file mode 100644
index 0000000..fe89518
--- /dev/null
+++ b/gazelle/testdata/monorepo/coarse_grained/packages_mapping.json
@@ -0,0 +1 @@
+{ "boto3": "threeboto3" }
diff --git a/gazelle/testdata/monorepo/gazelle_python.yaml b/gazelle/testdata/monorepo/gazelle_python.yaml
new file mode 100644
index 0000000..527b6ea
--- /dev/null
+++ b/gazelle/testdata/monorepo/gazelle_python.yaml
@@ -0,0 +1,4 @@
+manifest:
+ modules_mapping:
+ boto3: rootboto3
+ pip_deps_repository_name: root_pip_deps
diff --git a/gazelle/testdata/monorepo/one/BUILD.in b/gazelle/testdata/monorepo/one/BUILD.in
new file mode 100644
index 0000000..b11b373
--- /dev/null
+++ b/gazelle/testdata/monorepo/one/BUILD.in
@@ -0,0 +1,2 @@
+# gazelle:python_extension enabled
+# gazelle:python_root
diff --git a/gazelle/testdata/monorepo/one/BUILD.out b/gazelle/testdata/monorepo/one/BUILD.out
new file mode 100644
index 0000000..a957227
--- /dev/null
+++ b/gazelle/testdata/monorepo/one/BUILD.out
@@ -0,0 +1,17 @@
+load("@rules_python//python:defs.bzl", "py_binary")
+
+# gazelle:python_extension enabled
+# gazelle:python_root
+
+py_binary(
+ name = "one_bin",
+ srcs = ["__main__.py"],
+ main = "__main__.py",
+ visibility = ["//one:__subpackages__"],
+ deps = [
+ "//one/bar",
+ "//one/bar/baz:modified_name_baz",
+ "//one/foo",
+ "@one_pip_deps//pypi__oneboto3",
+ ],
+)
diff --git a/gazelle/testdata/monorepo/one/__main__.py b/gazelle/testdata/monorepo/one/__main__.py
new file mode 100644
index 0000000..f08f5e8
--- /dev/null
+++ b/gazelle/testdata/monorepo/one/__main__.py
@@ -0,0 +1,15 @@
+import os
+
+import boto3
+from bar import bar
+from bar.baz import baz
+from foo import foo
+
+_ = boto3
+
+if __name__ == "__main__":
+ INIT_FILENAME = "__init__.py"
+ dirname = os.path.dirname(os.path.abspath(__file__))
+ assert bar() == os.path.join(dirname, "bar", INIT_FILENAME)
+ assert baz() == os.path.join(dirname, "bar", "baz", INIT_FILENAME)
+ assert foo() == os.path.join(dirname, "foo", INIT_FILENAME)
diff --git a/gazelle/testdata/monorepo/one/bar/BUILD.in b/gazelle/testdata/monorepo/one/bar/BUILD.in
new file mode 100644
index 0000000..7fe1f49
--- /dev/null
+++ b/gazelle/testdata/monorepo/one/bar/BUILD.in
@@ -0,0 +1,10 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "bar",
+ srcs = ["__init__.py"],
+ visibility = [
+ "//one:__subpackages__",
+ "//three:__subpackages__",
+ ],
+)
diff --git a/gazelle/testdata/monorepo/one/bar/BUILD.out b/gazelle/testdata/monorepo/one/bar/BUILD.out
new file mode 100644
index 0000000..0e85623
--- /dev/null
+++ b/gazelle/testdata/monorepo/one/bar/BUILD.out
@@ -0,0 +1,12 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "bar",
+ srcs = ["__init__.py"],
+ imports = [".."],
+ visibility = [
+ "//one:__subpackages__",
+ "//three:__subpackages__",
+ ],
+ deps = ["@one_pip_deps//pypi__oneboto3"],
+)
diff --git a/gazelle/testdata/monorepo/one/bar/__init__.py b/gazelle/testdata/monorepo/one/bar/__init__.py
new file mode 100644
index 0000000..f6ec214
--- /dev/null
+++ b/gazelle/testdata/monorepo/one/bar/__init__.py
@@ -0,0 +1,9 @@
+import os
+
+import boto3
+
+_ = boto3
+
+
+def bar():
+ return os.path.abspath(__file__)
diff --git a/gazelle/testdata/monorepo/one/bar/baz/BUILD.in b/gazelle/testdata/monorepo/one/bar/baz/BUILD.in
new file mode 100644
index 0000000..00ba8ed
--- /dev/null
+++ b/gazelle/testdata/monorepo/one/bar/baz/BUILD.in
@@ -0,0 +1,10 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "modified_name_baz",
+ srcs = ["__init__.py"],
+ visibility = [
+ "//one:__subpackages__",
+ "//three:__subpackages__",
+ ],
+)
diff --git a/gazelle/testdata/monorepo/one/bar/baz/BUILD.out b/gazelle/testdata/monorepo/one/bar/baz/BUILD.out
new file mode 100644
index 0000000..1eb52fc
--- /dev/null
+++ b/gazelle/testdata/monorepo/one/bar/baz/BUILD.out
@@ -0,0 +1,11 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "modified_name_baz",
+ srcs = ["__init__.py"],
+ imports = ["../.."],
+ visibility = [
+ "//one:__subpackages__",
+ "//three:__subpackages__",
+ ],
+)
diff --git a/gazelle/testdata/monorepo/one/bar/baz/__init__.py b/gazelle/testdata/monorepo/one/bar/baz/__init__.py
new file mode 100644
index 0000000..e74f519
--- /dev/null
+++ b/gazelle/testdata/monorepo/one/bar/baz/__init__.py
@@ -0,0 +1,5 @@
+import os
+
+
+def baz():
+ return os.path.abspath(__file__)
diff --git a/gazelle/testdata/monorepo/one/foo/BUILD.in b/gazelle/testdata/monorepo/one/foo/BUILD.in
new file mode 100644
index 0000000..0ee9a30
--- /dev/null
+++ b/gazelle/testdata/monorepo/one/foo/BUILD.in
@@ -0,0 +1,11 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "foo",
+ srcs = ["__init__.py"],
+ visibility = [
+ "//one:__subpackages__",
+ "//three:__subpackages__",
+ "//two:__subpackages__",
+ ],
+)
diff --git a/gazelle/testdata/monorepo/one/foo/BUILD.out b/gazelle/testdata/monorepo/one/foo/BUILD.out
new file mode 100644
index 0000000..464fabb
--- /dev/null
+++ b/gazelle/testdata/monorepo/one/foo/BUILD.out
@@ -0,0 +1,12 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "foo",
+ srcs = ["__init__.py"],
+ imports = [".."],
+ visibility = [
+ "//one:__subpackages__",
+ "//three:__subpackages__",
+ "//two:__subpackages__",
+ ],
+)
diff --git a/gazelle/testdata/monorepo/one/foo/__init__.py b/gazelle/testdata/monorepo/one/foo/__init__.py
new file mode 100644
index 0000000..8aeca3d
--- /dev/null
+++ b/gazelle/testdata/monorepo/one/foo/__init__.py
@@ -0,0 +1,5 @@
+import os
+
+
+def foo():
+ return os.path.abspath(__file__)
diff --git a/gazelle/testdata/monorepo/one/gazelle_python.yaml b/gazelle/testdata/monorepo/one/gazelle_python.yaml
new file mode 100644
index 0000000..67c5345
--- /dev/null
+++ b/gazelle/testdata/monorepo/one/gazelle_python.yaml
@@ -0,0 +1,4 @@
+manifest:
+ modules_mapping:
+ boto3: oneboto3
+ pip_deps_repository_name: one_pip_deps
diff --git a/gazelle/testdata/monorepo/test.yaml b/gazelle/testdata/monorepo/test.yaml
new file mode 100644
index 0000000..ed97d53
--- /dev/null
+++ b/gazelle/testdata/monorepo/test.yaml
@@ -0,0 +1 @@
+---
diff --git a/gazelle/testdata/monorepo/three/BUILD.in b/gazelle/testdata/monorepo/three/BUILD.in
new file mode 100644
index 0000000..79bb63f
--- /dev/null
+++ b/gazelle/testdata/monorepo/three/BUILD.in
@@ -0,0 +1,5 @@
+# gazelle:python_extension enabled
+# gazelle:python_root
+# gazelle:resolve py bar //one/bar
+# gazelle:resolve py bar.baz //one/bar/baz:modified_name_baz
+# gazelle:resolve py foo //one/foo
diff --git a/gazelle/testdata/monorepo/three/BUILD.out b/gazelle/testdata/monorepo/three/BUILD.out
new file mode 100644
index 0000000..bbb03b1
--- /dev/null
+++ b/gazelle/testdata/monorepo/three/BUILD.out
@@ -0,0 +1,20 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+# gazelle:python_extension enabled
+# gazelle:python_root
+# gazelle:resolve py bar //one/bar
+# gazelle:resolve py bar.baz //one/bar/baz:modified_name_baz
+# gazelle:resolve py foo //one/foo
+
+py_library(
+ name = "three",
+ srcs = ["__init__.py"],
+ visibility = ["//three:__subpackages__"],
+ deps = [
+ "//coarse_grained",
+ "//one/bar",
+ "//one/bar/baz:modified_name_baz",
+ "//one/foo",
+ "@three_pip_deps//pypi__threeboto3",
+ ],
+)
diff --git a/gazelle/testdata/monorepo/three/__init__.py b/gazelle/testdata/monorepo/three/__init__.py
new file mode 100644
index 0000000..fe955f6
--- /dev/null
+++ b/gazelle/testdata/monorepo/three/__init__.py
@@ -0,0 +1,14 @@
+import os
+
+import bar.baz.hue as hue
+import boto3
+from bar import bar
+from bar.baz import baz
+from foo import foo
+
+_ = os
+_ = boto3
+_ = bar
+_ = baz
+_ = foo
+_ = hue
diff --git a/gazelle/testdata/monorepo/three/gazelle_python.yaml b/gazelle/testdata/monorepo/three/gazelle_python.yaml
new file mode 100644
index 0000000..572216c
--- /dev/null
+++ b/gazelle/testdata/monorepo/three/gazelle_python.yaml
@@ -0,0 +1,4 @@
+manifest:
+ modules_mapping:
+ boto3: threeboto3
+ pip_deps_repository_name: three_pip_deps
diff --git a/gazelle/testdata/monorepo/two/BUILD.in b/gazelle/testdata/monorepo/two/BUILD.in
new file mode 100644
index 0000000..31812e0
--- /dev/null
+++ b/gazelle/testdata/monorepo/two/BUILD.in
@@ -0,0 +1,3 @@
+# gazelle:python_extension enabled
+# gazelle:python_root
+# gazelle:resolve py foo //one/foo
diff --git a/gazelle/testdata/monorepo/two/BUILD.out b/gazelle/testdata/monorepo/two/BUILD.out
new file mode 100644
index 0000000..4b638ed
--- /dev/null
+++ b/gazelle/testdata/monorepo/two/BUILD.out
@@ -0,0 +1,15 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+# gazelle:python_extension enabled
+# gazelle:python_root
+# gazelle:resolve py foo //one/foo
+
+py_library(
+ name = "two",
+ srcs = ["__init__.py"],
+ visibility = ["//two:__subpackages__"],
+ deps = [
+ "//one/foo",
+ "@two_pip_deps//pypi__twoboto3",
+ ],
+)
diff --git a/gazelle/testdata/monorepo/two/__init__.py b/gazelle/testdata/monorepo/two/__init__.py
new file mode 100644
index 0000000..fb3e877
--- /dev/null
+++ b/gazelle/testdata/monorepo/two/__init__.py
@@ -0,0 +1,8 @@
+import os
+
+import boto3
+from foo import foo
+
+_ = os
+_ = boto3
+_ = foo
diff --git a/gazelle/testdata/monorepo/two/gazelle_python.yaml b/gazelle/testdata/monorepo/two/gazelle_python.yaml
new file mode 100644
index 0000000..3bc5939
--- /dev/null
+++ b/gazelle/testdata/monorepo/two/gazelle_python.yaml
@@ -0,0 +1,4 @@
+manifest:
+ modules_mapping:
+ boto3: twoboto3
+ pip_deps_repository_name: two_pip_deps
diff --git a/gazelle/testdata/monorepo/wont_generate/BUILD.in b/gazelle/testdata/monorepo/wont_generate/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/monorepo/wont_generate/BUILD.in
diff --git a/gazelle/testdata/monorepo/wont_generate/BUILD.out b/gazelle/testdata/monorepo/wont_generate/BUILD.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/monorepo/wont_generate/BUILD.out
diff --git a/gazelle/testdata/monorepo/wont_generate/__main__.py b/gazelle/testdata/monorepo/wont_generate/__main__.py
new file mode 100644
index 0000000..2d241cc
--- /dev/null
+++ b/gazelle/testdata/monorepo/wont_generate/__main__.py
@@ -0,0 +1,12 @@
+import os
+
+from bar import bar
+from bar.baz import baz
+from foo import foo
+
+if __name__ == "__main__":
+ INIT_FILENAME = "__init__.py"
+ dirname = os.path.dirname(os.path.abspath(__file__))
+ assert bar() == os.path.join(dirname, "bar", INIT_FILENAME)
+ assert baz() == os.path.join(dirname, "bar", "baz", INIT_FILENAME)
+ assert foo() == os.path.join(dirname, "foo", INIT_FILENAME)
diff --git a/gazelle/testdata/monorepo/wont_generate/bar/BUILD.in b/gazelle/testdata/monorepo/wont_generate/bar/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/monorepo/wont_generate/bar/BUILD.in
diff --git a/gazelle/testdata/monorepo/wont_generate/bar/BUILD.out b/gazelle/testdata/monorepo/wont_generate/bar/BUILD.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/monorepo/wont_generate/bar/BUILD.out
diff --git a/gazelle/testdata/monorepo/wont_generate/bar/__init__.py b/gazelle/testdata/monorepo/wont_generate/bar/__init__.py
new file mode 100644
index 0000000..e311ff1
--- /dev/null
+++ b/gazelle/testdata/monorepo/wont_generate/bar/__init__.py
@@ -0,0 +1,5 @@
+import os
+
+
+def bar():
+ return os.path.abspath(__file__)
diff --git a/gazelle/testdata/monorepo/wont_generate/bar/baz/BUILD.in b/gazelle/testdata/monorepo/wont_generate/bar/baz/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/monorepo/wont_generate/bar/baz/BUILD.in
diff --git a/gazelle/testdata/monorepo/wont_generate/bar/baz/BUILD.out b/gazelle/testdata/monorepo/wont_generate/bar/baz/BUILD.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/monorepo/wont_generate/bar/baz/BUILD.out
diff --git a/gazelle/testdata/monorepo/wont_generate/bar/baz/__init__.py b/gazelle/testdata/monorepo/wont_generate/bar/baz/__init__.py
new file mode 100644
index 0000000..e74f519
--- /dev/null
+++ b/gazelle/testdata/monorepo/wont_generate/bar/baz/__init__.py
@@ -0,0 +1,5 @@
+import os
+
+
+def baz():
+ return os.path.abspath(__file__)
diff --git a/gazelle/testdata/monorepo/wont_generate/foo/BUILD.in b/gazelle/testdata/monorepo/wont_generate/foo/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/monorepo/wont_generate/foo/BUILD.in
diff --git a/gazelle/testdata/monorepo/wont_generate/foo/BUILD.out b/gazelle/testdata/monorepo/wont_generate/foo/BUILD.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/monorepo/wont_generate/foo/BUILD.out
diff --git a/gazelle/testdata/monorepo/wont_generate/foo/__init__.py b/gazelle/testdata/monorepo/wont_generate/foo/__init__.py
new file mode 100644
index 0000000..8aeca3d
--- /dev/null
+++ b/gazelle/testdata/monorepo/wont_generate/foo/__init__.py
@@ -0,0 +1,5 @@
+import os
+
+
+def foo():
+ return os.path.abspath(__file__)
diff --git a/gazelle/testdata/naming_convention/BUILD.in b/gazelle/testdata/naming_convention/BUILD.in
new file mode 100644
index 0000000..7517848
--- /dev/null
+++ b/gazelle/testdata/naming_convention/BUILD.in
@@ -0,0 +1,3 @@
+# gazelle:python_library_naming_convention my_$package_name$_library
+# gazelle:python_binary_naming_convention my_$package_name$_binary
+# gazelle:python_test_naming_convention my_$package_name$_test
diff --git a/gazelle/testdata/naming_convention/BUILD.out b/gazelle/testdata/naming_convention/BUILD.out
new file mode 100644
index 0000000..e2f0674
--- /dev/null
+++ b/gazelle/testdata/naming_convention/BUILD.out
@@ -0,0 +1,26 @@
+load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test")
+
+# gazelle:python_library_naming_convention my_$package_name$_library
+# gazelle:python_binary_naming_convention my_$package_name$_binary
+# gazelle:python_test_naming_convention my_$package_name$_test
+
+py_library(
+ name = "my_naming_convention_library",
+ srcs = ["__init__.py"],
+ visibility = ["//:__subpackages__"],
+)
+
+py_binary(
+ name = "my_naming_convention_binary",
+ srcs = ["__main__.py"],
+ main = "__main__.py",
+ visibility = ["//:__subpackages__"],
+ deps = [":my_naming_convention_library"],
+)
+
+py_test(
+ name = "my_naming_convention_test",
+ srcs = ["__test__.py"],
+ main = "__test__.py",
+ deps = [":my_naming_convention_library"],
+)
diff --git a/gazelle/testdata/naming_convention/README.md b/gazelle/testdata/naming_convention/README.md
new file mode 100644
index 0000000..9dd88ec
--- /dev/null
+++ b/gazelle/testdata/naming_convention/README.md
@@ -0,0 +1,4 @@
+# Naming convention
+
+This test case asserts that py\_{library,binary,test} targets are generated
+correctly based on the directives that control their naming conventions.
diff --git a/gazelle/testdata/naming_convention/WORKSPACE b/gazelle/testdata/naming_convention/WORKSPACE
new file mode 100644
index 0000000..faff6af
--- /dev/null
+++ b/gazelle/testdata/naming_convention/WORKSPACE
@@ -0,0 +1 @@
+# This is a Bazel workspace for the Gazelle test data.
diff --git a/gazelle/testdata/naming_convention/__init__.py b/gazelle/testdata/naming_convention/__init__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/naming_convention/__init__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/naming_convention/__main__.py b/gazelle/testdata/naming_convention/__main__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/naming_convention/__main__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/naming_convention/__test__.py b/gazelle/testdata/naming_convention/__test__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/naming_convention/__test__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/naming_convention/dont_rename/BUILD.in b/gazelle/testdata/naming_convention/dont_rename/BUILD.in
new file mode 100644
index 0000000..8d2ae35
--- /dev/null
+++ b/gazelle/testdata/naming_convention/dont_rename/BUILD.in
@@ -0,0 +1,7 @@
+load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test")
+
+py_library(
+ name = "dont_rename",
+ srcs = ["__init__.py"],
+ visibility = ["//:__subpackages__"],
+)
diff --git a/gazelle/testdata/naming_convention/dont_rename/BUILD.out b/gazelle/testdata/naming_convention/dont_rename/BUILD.out
new file mode 100644
index 0000000..4d4ead8
--- /dev/null
+++ b/gazelle/testdata/naming_convention/dont_rename/BUILD.out
@@ -0,0 +1,25 @@
+load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test")
+
+py_library(
+ name = "dont_rename",
+ srcs = ["__init__.py"],
+ imports = [".."],
+ visibility = ["//:__subpackages__"],
+)
+
+py_binary(
+ name = "my_dont_rename_binary",
+ srcs = ["__main__.py"],
+ imports = [".."],
+ main = "__main__.py",
+ visibility = ["//:__subpackages__"],
+ deps = [":dont_rename"],
+)
+
+py_test(
+ name = "my_dont_rename_test",
+ srcs = ["__test__.py"],
+ imports = [".."],
+ main = "__test__.py",
+ deps = [":dont_rename"],
+)
diff --git a/gazelle/testdata/naming_convention/dont_rename/__init__.py b/gazelle/testdata/naming_convention/dont_rename/__init__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/naming_convention/dont_rename/__init__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/naming_convention/dont_rename/__main__.py b/gazelle/testdata/naming_convention/dont_rename/__main__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/naming_convention/dont_rename/__main__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/naming_convention/dont_rename/__test__.py b/gazelle/testdata/naming_convention/dont_rename/__test__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/naming_convention/dont_rename/__test__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/naming_convention/resolve_conflict/BUILD.in b/gazelle/testdata/naming_convention/resolve_conflict/BUILD.in
new file mode 100644
index 0000000..c81e735
--- /dev/null
+++ b/gazelle/testdata/naming_convention/resolve_conflict/BUILD.in
@@ -0,0 +1,5 @@
+go_library(name = "resolve_conflict")
+
+go_binary(name = "resolve_conflict_bin")
+
+go_test(name = "resolve_conflict_test")
diff --git a/gazelle/testdata/naming_convention/resolve_conflict/BUILD.out b/gazelle/testdata/naming_convention/resolve_conflict/BUILD.out
new file mode 100644
index 0000000..3fa5de2
--- /dev/null
+++ b/gazelle/testdata/naming_convention/resolve_conflict/BUILD.out
@@ -0,0 +1,31 @@
+load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test")
+
+go_library(name = "resolve_conflict")
+
+go_binary(name = "resolve_conflict_bin")
+
+go_test(name = "resolve_conflict_test")
+
+py_library(
+ name = "my_resolve_conflict_library",
+ srcs = ["__init__.py"],
+ imports = [".."],
+ visibility = ["//:__subpackages__"],
+)
+
+py_binary(
+ name = "my_resolve_conflict_binary",
+ srcs = ["__main__.py"],
+ imports = [".."],
+ main = "__main__.py",
+ visibility = ["//:__subpackages__"],
+ deps = [":my_resolve_conflict_library"],
+)
+
+py_test(
+ name = "my_resolve_conflict_test",
+ srcs = ["__test__.py"],
+ imports = [".."],
+ main = "__test__.py",
+ deps = [":my_resolve_conflict_library"],
+)
diff --git a/gazelle/testdata/naming_convention/resolve_conflict/__init__.py b/gazelle/testdata/naming_convention/resolve_conflict/__init__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/naming_convention/resolve_conflict/__init__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/naming_convention/resolve_conflict/__main__.py b/gazelle/testdata/naming_convention/resolve_conflict/__main__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/naming_convention/resolve_conflict/__main__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/naming_convention/resolve_conflict/__test__.py b/gazelle/testdata/naming_convention/resolve_conflict/__test__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/naming_convention/resolve_conflict/__test__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/naming_convention/test.yaml b/gazelle/testdata/naming_convention/test.yaml
new file mode 100644
index 0000000..ed97d53
--- /dev/null
+++ b/gazelle/testdata/naming_convention/test.yaml
@@ -0,0 +1 @@
+---
diff --git a/gazelle/testdata/naming_convention_binary_fail/BUILD.in b/gazelle/testdata/naming_convention_binary_fail/BUILD.in
new file mode 100644
index 0000000..fd4dc1c
--- /dev/null
+++ b/gazelle/testdata/naming_convention_binary_fail/BUILD.in
@@ -0,0 +1 @@
+go_binary(name = "naming_convention_binary_fail_bin")
diff --git a/gazelle/testdata/naming_convention_binary_fail/BUILD.out b/gazelle/testdata/naming_convention_binary_fail/BUILD.out
new file mode 100644
index 0000000..fd4dc1c
--- /dev/null
+++ b/gazelle/testdata/naming_convention_binary_fail/BUILD.out
@@ -0,0 +1 @@
+go_binary(name = "naming_convention_binary_fail_bin")
diff --git a/gazelle/testdata/naming_convention_binary_fail/README.md b/gazelle/testdata/naming_convention_binary_fail/README.md
new file mode 100644
index 0000000..a58bbe4
--- /dev/null
+++ b/gazelle/testdata/naming_convention_binary_fail/README.md
@@ -0,0 +1,4 @@
+# Naming convention py_binary fail
+
+This test case asserts that a py_binary is not generated due to a naming conflict
+with existing target.
diff --git a/gazelle/testdata/naming_convention_binary_fail/WORKSPACE b/gazelle/testdata/naming_convention_binary_fail/WORKSPACE
new file mode 100644
index 0000000..faff6af
--- /dev/null
+++ b/gazelle/testdata/naming_convention_binary_fail/WORKSPACE
@@ -0,0 +1 @@
+# This is a Bazel workspace for the Gazelle test data.
diff --git a/gazelle/testdata/naming_convention_binary_fail/__main__.py b/gazelle/testdata/naming_convention_binary_fail/__main__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/naming_convention_binary_fail/__main__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/naming_convention_binary_fail/test.yaml b/gazelle/testdata/naming_convention_binary_fail/test.yaml
new file mode 100644
index 0000000..bc30dd0
--- /dev/null
+++ b/gazelle/testdata/naming_convention_binary_fail/test.yaml
@@ -0,0 +1,7 @@
+---
+expect:
+ exit_code: 1
+ stderr: >
+ gazelle: ERROR: failed to generate target "//:naming_convention_binary_fail_bin" of kind "py_binary":
+ a target of kind "go_binary" with the same name already exists.
+ Use the '# gazelle:python_binary_naming_convention' directive to change the naming convention.
diff --git a/gazelle/testdata/naming_convention_library_fail/BUILD.in b/gazelle/testdata/naming_convention_library_fail/BUILD.in
new file mode 100644
index 0000000..a684084
--- /dev/null
+++ b/gazelle/testdata/naming_convention_library_fail/BUILD.in
@@ -0,0 +1 @@
+go_library(name = "naming_convention_library_fail")
diff --git a/gazelle/testdata/naming_convention_library_fail/BUILD.out b/gazelle/testdata/naming_convention_library_fail/BUILD.out
new file mode 100644
index 0000000..a684084
--- /dev/null
+++ b/gazelle/testdata/naming_convention_library_fail/BUILD.out
@@ -0,0 +1 @@
+go_library(name = "naming_convention_library_fail")
diff --git a/gazelle/testdata/naming_convention_library_fail/README.md b/gazelle/testdata/naming_convention_library_fail/README.md
new file mode 100644
index 0000000..cd36917
--- /dev/null
+++ b/gazelle/testdata/naming_convention_library_fail/README.md
@@ -0,0 +1,4 @@
+# Naming convention py_library fail
+
+This test case asserts that a py_library is not generated due to a naming conflict
+with existing target.
diff --git a/gazelle/testdata/naming_convention_library_fail/WORKSPACE b/gazelle/testdata/naming_convention_library_fail/WORKSPACE
new file mode 100644
index 0000000..faff6af
--- /dev/null
+++ b/gazelle/testdata/naming_convention_library_fail/WORKSPACE
@@ -0,0 +1 @@
+# This is a Bazel workspace for the Gazelle test data.
diff --git a/gazelle/testdata/naming_convention_library_fail/__init__.py b/gazelle/testdata/naming_convention_library_fail/__init__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/naming_convention_library_fail/__init__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/naming_convention_library_fail/test.yaml b/gazelle/testdata/naming_convention_library_fail/test.yaml
new file mode 100644
index 0000000..3743c32
--- /dev/null
+++ b/gazelle/testdata/naming_convention_library_fail/test.yaml
@@ -0,0 +1,7 @@
+---
+expect:
+ exit_code: 1
+ stderr: >
+ gazelle: ERROR: failed to generate target "//:naming_convention_library_fail" of kind "py_library":
+ a target of kind "go_library" with the same name already exists.
+ Use the '# gazelle:python_library_naming_convention' directive to change the naming convention.
diff --git a/gazelle/testdata/naming_convention_test_fail/BUILD.in b/gazelle/testdata/naming_convention_test_fail/BUILD.in
new file mode 100644
index 0000000..2091253
--- /dev/null
+++ b/gazelle/testdata/naming_convention_test_fail/BUILD.in
@@ -0,0 +1 @@
+go_test(name = "naming_convention_test_fail_test")
diff --git a/gazelle/testdata/naming_convention_test_fail/BUILD.out b/gazelle/testdata/naming_convention_test_fail/BUILD.out
new file mode 100644
index 0000000..2091253
--- /dev/null
+++ b/gazelle/testdata/naming_convention_test_fail/BUILD.out
@@ -0,0 +1 @@
+go_test(name = "naming_convention_test_fail_test")
diff --git a/gazelle/testdata/naming_convention_test_fail/README.md b/gazelle/testdata/naming_convention_test_fail/README.md
new file mode 100644
index 0000000..886c1e3
--- /dev/null
+++ b/gazelle/testdata/naming_convention_test_fail/README.md
@@ -0,0 +1,4 @@
+# Naming convention py_test fail
+
+This test case asserts that a py_test is not generated due to a naming conflict
+with existing target.
diff --git a/gazelle/testdata/naming_convention_test_fail/WORKSPACE b/gazelle/testdata/naming_convention_test_fail/WORKSPACE
new file mode 100644
index 0000000..faff6af
--- /dev/null
+++ b/gazelle/testdata/naming_convention_test_fail/WORKSPACE
@@ -0,0 +1 @@
+# This is a Bazel workspace for the Gazelle test data.
diff --git a/gazelle/testdata/naming_convention_test_fail/__test__.py b/gazelle/testdata/naming_convention_test_fail/__test__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/naming_convention_test_fail/__test__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/naming_convention_test_fail/test.yaml b/gazelle/testdata/naming_convention_test_fail/test.yaml
new file mode 100644
index 0000000..fc4e24e
--- /dev/null
+++ b/gazelle/testdata/naming_convention_test_fail/test.yaml
@@ -0,0 +1,7 @@
+---
+expect:
+ exit_code: 1
+ stderr: >
+ gazelle: ERROR: failed to generate target "//:naming_convention_test_fail_test" of kind "py_test":
+ a target of kind "go_test" with the same name already exists.
+ Use the '# gazelle:python_test_naming_convention' directive to change the naming convention.
diff --git a/gazelle/testdata/python_ignore_dependencies_directive/BUILD.in b/gazelle/testdata/python_ignore_dependencies_directive/BUILD.in
new file mode 100644
index 0000000..1ba277a
--- /dev/null
+++ b/gazelle/testdata/python_ignore_dependencies_directive/BUILD.in
@@ -0,0 +1,2 @@
+# gazelle:python_ignore_dependencies foo,bar, baz
+# gazelle:python_ignore_dependencies foo.bar.baz
diff --git a/gazelle/testdata/python_ignore_dependencies_directive/BUILD.out b/gazelle/testdata/python_ignore_dependencies_directive/BUILD.out
new file mode 100644
index 0000000..37ae4f9
--- /dev/null
+++ b/gazelle/testdata/python_ignore_dependencies_directive/BUILD.out
@@ -0,0 +1,11 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+# gazelle:python_ignore_dependencies foo,bar, baz
+# gazelle:python_ignore_dependencies foo.bar.baz
+
+py_library(
+ name = "python_ignore_dependencies_directive",
+ srcs = ["__init__.py"],
+ visibility = ["//:__subpackages__"],
+ deps = ["@gazelle_python_test//pypi__boto3"],
+)
diff --git a/gazelle/testdata/python_ignore_dependencies_directive/README.md b/gazelle/testdata/python_ignore_dependencies_directive/README.md
new file mode 100644
index 0000000..75f61e1
--- /dev/null
+++ b/gazelle/testdata/python_ignore_dependencies_directive/README.md
@@ -0,0 +1,4 @@
+# python_ignore_dependencies directive
+
+This test case asserts that the target is generated ignoring some of the
+dependencies.
diff --git a/gazelle/testdata/python_ignore_dependencies_directive/WORKSPACE b/gazelle/testdata/python_ignore_dependencies_directive/WORKSPACE
new file mode 100644
index 0000000..faff6af
--- /dev/null
+++ b/gazelle/testdata/python_ignore_dependencies_directive/WORKSPACE
@@ -0,0 +1 @@
+# This is a Bazel workspace for the Gazelle test data.
diff --git a/gazelle/testdata/python_ignore_dependencies_directive/__init__.py b/gazelle/testdata/python_ignore_dependencies_directive/__init__.py
new file mode 100644
index 0000000..79935a7
--- /dev/null
+++ b/gazelle/testdata/python_ignore_dependencies_directive/__init__.py
@@ -0,0 +1,11 @@
+import bar
+import boto3
+import foo
+import foo.bar.baz
+from baz import baz as bazfn
+
+_ = foo
+_ = bar
+_ = bazfn
+_ = baz
+_ = boto3
diff --git a/gazelle/testdata/python_ignore_dependencies_directive/gazelle_python.yaml b/gazelle/testdata/python_ignore_dependencies_directive/gazelle_python.yaml
new file mode 100644
index 0000000..7288b79
--- /dev/null
+++ b/gazelle/testdata/python_ignore_dependencies_directive/gazelle_python.yaml
@@ -0,0 +1,4 @@
+manifest:
+ modules_mapping:
+ boto3: boto3
+ pip_deps_repository_name: gazelle_python_test
diff --git a/gazelle/testdata/python_ignore_dependencies_directive/test.yaml b/gazelle/testdata/python_ignore_dependencies_directive/test.yaml
new file mode 100644
index 0000000..ed97d53
--- /dev/null
+++ b/gazelle/testdata/python_ignore_dependencies_directive/test.yaml
@@ -0,0 +1 @@
+---
diff --git a/gazelle/testdata/python_ignore_files_directive/BUILD.in b/gazelle/testdata/python_ignore_files_directive/BUILD.in
new file mode 100644
index 0000000..6277446
--- /dev/null
+++ b/gazelle/testdata/python_ignore_files_directive/BUILD.in
@@ -0,0 +1 @@
+# gazelle:python_ignore_files some_other.py
diff --git a/gazelle/testdata/python_ignore_files_directive/BUILD.out b/gazelle/testdata/python_ignore_files_directive/BUILD.out
new file mode 100644
index 0000000..1fe6030
--- /dev/null
+++ b/gazelle/testdata/python_ignore_files_directive/BUILD.out
@@ -0,0 +1,9 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+# gazelle:python_ignore_files some_other.py
+
+py_library(
+ name = "python_ignore_files_directive",
+ srcs = ["__init__.py"],
+ visibility = ["//:__subpackages__"],
+)
diff --git a/gazelle/testdata/python_ignore_files_directive/README.md b/gazelle/testdata/python_ignore_files_directive/README.md
new file mode 100644
index 0000000..710118d
--- /dev/null
+++ b/gazelle/testdata/python_ignore_files_directive/README.md
@@ -0,0 +1,3 @@
+# python_ignore_files directive
+
+This test case asserts that no targets are generated for ignored files.
diff --git a/gazelle/testdata/python_ignore_files_directive/WORKSPACE b/gazelle/testdata/python_ignore_files_directive/WORKSPACE
new file mode 100644
index 0000000..faff6af
--- /dev/null
+++ b/gazelle/testdata/python_ignore_files_directive/WORKSPACE
@@ -0,0 +1 @@
+# This is a Bazel workspace for the Gazelle test data.
diff --git a/gazelle/testdata/python_ignore_files_directive/__init__.py b/gazelle/testdata/python_ignore_files_directive/__init__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/python_ignore_files_directive/__init__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/python_ignore_files_directive/bar/BUILD.in b/gazelle/testdata/python_ignore_files_directive/bar/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/python_ignore_files_directive/bar/BUILD.in
diff --git a/gazelle/testdata/python_ignore_files_directive/bar/BUILD.out b/gazelle/testdata/python_ignore_files_directive/bar/BUILD.out
new file mode 100644
index 0000000..af3c398
--- /dev/null
+++ b/gazelle/testdata/python_ignore_files_directive/bar/BUILD.out
@@ -0,0 +1,8 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "bar",
+ srcs = ["baz.py"],
+ imports = [".."],
+ visibility = ["//:__subpackages__"],
+)
diff --git a/gazelle/testdata/python_ignore_files_directive/bar/baz.py b/gazelle/testdata/python_ignore_files_directive/bar/baz.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/python_ignore_files_directive/bar/baz.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/python_ignore_files_directive/bar/some_other.py b/gazelle/testdata/python_ignore_files_directive/bar/some_other.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/python_ignore_files_directive/bar/some_other.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/python_ignore_files_directive/foo/BUILD.in b/gazelle/testdata/python_ignore_files_directive/foo/BUILD.in
new file mode 100644
index 0000000..c3049ca
--- /dev/null
+++ b/gazelle/testdata/python_ignore_files_directive/foo/BUILD.in
@@ -0,0 +1 @@
+# gazelle:python_ignore_files baz.py
diff --git a/gazelle/testdata/python_ignore_files_directive/foo/BUILD.out b/gazelle/testdata/python_ignore_files_directive/foo/BUILD.out
new file mode 100644
index 0000000..c3049ca
--- /dev/null
+++ b/gazelle/testdata/python_ignore_files_directive/foo/BUILD.out
@@ -0,0 +1 @@
+# gazelle:python_ignore_files baz.py
diff --git a/gazelle/testdata/python_ignore_files_directive/foo/baz.py b/gazelle/testdata/python_ignore_files_directive/foo/baz.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/python_ignore_files_directive/foo/baz.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/python_ignore_files_directive/setup.py b/gazelle/testdata/python_ignore_files_directive/setup.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/python_ignore_files_directive/setup.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/python_ignore_files_directive/some_other.py b/gazelle/testdata/python_ignore_files_directive/some_other.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/python_ignore_files_directive/some_other.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/python_ignore_files_directive/test.yaml b/gazelle/testdata/python_ignore_files_directive/test.yaml
new file mode 100644
index 0000000..ed97d53
--- /dev/null
+++ b/gazelle/testdata/python_ignore_files_directive/test.yaml
@@ -0,0 +1 @@
+---
diff --git a/gazelle/testdata/python_target_with_test_in_name/BUILD.in b/gazelle/testdata/python_target_with_test_in_name/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/python_target_with_test_in_name/BUILD.in
diff --git a/gazelle/testdata/python_target_with_test_in_name/BUILD.out b/gazelle/testdata/python_target_with_test_in_name/BUILD.out
new file mode 100644
index 0000000..bdde605
--- /dev/null
+++ b/gazelle/testdata/python_target_with_test_in_name/BUILD.out
@@ -0,0 +1,12 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "python_target_with_test_in_name",
+ srcs = [
+ "__init__.py",
+ "not_a_real_test.py",
+ "test_not_a_real.py",
+ ],
+ visibility = ["//:__subpackages__"],
+ deps = ["@gazelle_python_test//pypi__boto3"],
+)
diff --git a/gazelle/testdata/python_target_with_test_in_name/README.md b/gazelle/testdata/python_target_with_test_in_name/README.md
new file mode 100644
index 0000000..8b592e1
--- /dev/null
+++ b/gazelle/testdata/python_target_with_test_in_name/README.md
@@ -0,0 +1,3 @@
+# Python target with test in name
+
+Cover the case where a python file either starts with `test_` or ends with `_test`, but is not an actual test.
diff --git a/gazelle/testdata/python_target_with_test_in_name/WORKSPACE b/gazelle/testdata/python_target_with_test_in_name/WORKSPACE
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/python_target_with_test_in_name/WORKSPACE
diff --git a/gazelle/testdata/python_target_with_test_in_name/__init__.py b/gazelle/testdata/python_target_with_test_in_name/__init__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/python_target_with_test_in_name/__init__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/python_target_with_test_in_name/gazelle_python.yaml b/gazelle/testdata/python_target_with_test_in_name/gazelle_python.yaml
new file mode 100644
index 0000000..7288b79
--- /dev/null
+++ b/gazelle/testdata/python_target_with_test_in_name/gazelle_python.yaml
@@ -0,0 +1,4 @@
+manifest:
+ modules_mapping:
+ boto3: boto3
+ pip_deps_repository_name: gazelle_python_test
diff --git a/gazelle/testdata/python_target_with_test_in_name/not_a_real_test.py b/gazelle/testdata/python_target_with_test_in_name/not_a_real_test.py
new file mode 100644
index 0000000..57c019d
--- /dev/null
+++ b/gazelle/testdata/python_target_with_test_in_name/not_a_real_test.py
@@ -0,0 +1,3 @@
+import boto3
+
+_ = boto3
diff --git a/gazelle/testdata/python_target_with_test_in_name/test.yaml b/gazelle/testdata/python_target_with_test_in_name/test.yaml
new file mode 100644
index 0000000..ed97d53
--- /dev/null
+++ b/gazelle/testdata/python_target_with_test_in_name/test.yaml
@@ -0,0 +1 @@
+---
diff --git a/gazelle/testdata/python_target_with_test_in_name/test_not_a_real.py b/gazelle/testdata/python_target_with_test_in_name/test_not_a_real.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/python_target_with_test_in_name/test_not_a_real.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/relative_imports/BUILD.in b/gazelle/testdata/relative_imports/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/relative_imports/BUILD.in
diff --git a/gazelle/testdata/relative_imports/BUILD.out b/gazelle/testdata/relative_imports/BUILD.out
new file mode 100644
index 0000000..2c08627
--- /dev/null
+++ b/gazelle/testdata/relative_imports/BUILD.out
@@ -0,0 +1,21 @@
+load("@rules_python//python:defs.bzl", "py_binary", "py_library")
+
+py_library(
+ name = "relative_imports",
+ srcs = [
+ "package1/module1.py",
+ "package1/module2.py",
+ ],
+ visibility = ["//:__subpackages__"],
+)
+
+py_binary(
+ name = "relative_imports_bin",
+ srcs = ["__main__.py"],
+ main = "__main__.py",
+ visibility = ["//:__subpackages__"],
+ deps = [
+ ":relative_imports",
+ "//package2",
+ ],
+)
diff --git a/gazelle/testdata/relative_imports/README.md b/gazelle/testdata/relative_imports/README.md
new file mode 100644
index 0000000..1937cbc
--- /dev/null
+++ b/gazelle/testdata/relative_imports/README.md
@@ -0,0 +1,4 @@
+# Relative imports
+
+This test case asserts that the generated targets handle relative imports in
+Python correctly.
diff --git a/gazelle/testdata/relative_imports/WORKSPACE b/gazelle/testdata/relative_imports/WORKSPACE
new file mode 100644
index 0000000..4959898
--- /dev/null
+++ b/gazelle/testdata/relative_imports/WORKSPACE
@@ -0,0 +1 @@
+# This is a test data Bazel workspace.
diff --git a/gazelle/testdata/relative_imports/__main__.py b/gazelle/testdata/relative_imports/__main__.py
new file mode 100644
index 0000000..4fb887a
--- /dev/null
+++ b/gazelle/testdata/relative_imports/__main__.py
@@ -0,0 +1,5 @@
+from package1.module1 import function1
+from package2.module3 import function3
+
+print(function1())
+print(function3())
diff --git a/gazelle/testdata/relative_imports/package1/module1.py b/gazelle/testdata/relative_imports/package1/module1.py
new file mode 100644
index 0000000..69cdde2
--- /dev/null
+++ b/gazelle/testdata/relative_imports/package1/module1.py
@@ -0,0 +1,5 @@
+from .module2 import function2
+
+
+def function1():
+ return "function1 " + function2()
diff --git a/gazelle/testdata/relative_imports/package1/module2.py b/gazelle/testdata/relative_imports/package1/module2.py
new file mode 100644
index 0000000..1e731b4
--- /dev/null
+++ b/gazelle/testdata/relative_imports/package1/module2.py
@@ -0,0 +1,2 @@
+def function2():
+ return "function2"
diff --git a/gazelle/testdata/relative_imports/package2/BUILD.in b/gazelle/testdata/relative_imports/package2/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/relative_imports/package2/BUILD.in
diff --git a/gazelle/testdata/relative_imports/package2/BUILD.out b/gazelle/testdata/relative_imports/package2/BUILD.out
new file mode 100644
index 0000000..bbbc9f8
--- /dev/null
+++ b/gazelle/testdata/relative_imports/package2/BUILD.out
@@ -0,0 +1,13 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "package2",
+ srcs = [
+ "__init__.py",
+ "module3.py",
+ "module4.py",
+ "subpackage1/module5.py",
+ ],
+ imports = [".."],
+ visibility = ["//:__subpackages__"],
+)
diff --git a/gazelle/testdata/relative_imports/package2/__init__.py b/gazelle/testdata/relative_imports/package2/__init__.py
new file mode 100644
index 0000000..fd0384b
--- /dev/null
+++ b/gazelle/testdata/relative_imports/package2/__init__.py
@@ -0,0 +1,3 @@
+class Class1:
+ def method1(self):
+ return "method1"
diff --git a/gazelle/testdata/relative_imports/package2/module3.py b/gazelle/testdata/relative_imports/package2/module3.py
new file mode 100644
index 0000000..a5102dd
--- /dev/null
+++ b/gazelle/testdata/relative_imports/package2/module3.py
@@ -0,0 +1,7 @@
+from . import Class1
+from .subpackage1.module5 import function5
+
+
+def function3():
+ c1 = Class1()
+ return "function3 " + c1.method1() + " " + function5()
diff --git a/gazelle/testdata/relative_imports/package2/module4.py b/gazelle/testdata/relative_imports/package2/module4.py
new file mode 100644
index 0000000..6e69699
--- /dev/null
+++ b/gazelle/testdata/relative_imports/package2/module4.py
@@ -0,0 +1,2 @@
+def function4():
+ return "function4"
diff --git a/gazelle/testdata/relative_imports/package2/subpackage1/module5.py b/gazelle/testdata/relative_imports/package2/subpackage1/module5.py
new file mode 100644
index 0000000..ac1f725
--- /dev/null
+++ b/gazelle/testdata/relative_imports/package2/subpackage1/module5.py
@@ -0,0 +1,5 @@
+from ..module4 import function4
+
+
+def function5():
+ return "function5 " + function4()
diff --git a/gazelle/testdata/relative_imports/test.yaml b/gazelle/testdata/relative_imports/test.yaml
new file mode 100644
index 0000000..ed97d53
--- /dev/null
+++ b/gazelle/testdata/relative_imports/test.yaml
@@ -0,0 +1 @@
+---
diff --git a/gazelle/testdata/simple_binary/BUILD.in b/gazelle/testdata/simple_binary/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/simple_binary/BUILD.in
diff --git a/gazelle/testdata/simple_binary/BUILD.out b/gazelle/testdata/simple_binary/BUILD.out
new file mode 100644
index 0000000..35aa708
--- /dev/null
+++ b/gazelle/testdata/simple_binary/BUILD.out
@@ -0,0 +1,8 @@
+load("@rules_python//python:defs.bzl", "py_binary")
+
+py_binary(
+ name = "simple_binary_bin",
+ srcs = ["__main__.py"],
+ main = "__main__.py",
+ visibility = ["//:__subpackages__"],
+)
diff --git a/gazelle/testdata/simple_binary/README.md b/gazelle/testdata/simple_binary/README.md
new file mode 100644
index 0000000..00c90dc
--- /dev/null
+++ b/gazelle/testdata/simple_binary/README.md
@@ -0,0 +1,3 @@
+# Simple binary
+
+This test case asserts that a simple `py_binary` is generated as expected.
diff --git a/gazelle/testdata/simple_binary/WORKSPACE b/gazelle/testdata/simple_binary/WORKSPACE
new file mode 100644
index 0000000..faff6af
--- /dev/null
+++ b/gazelle/testdata/simple_binary/WORKSPACE
@@ -0,0 +1 @@
+# This is a Bazel workspace for the Gazelle test data.
diff --git a/gazelle/testdata/simple_binary/__main__.py b/gazelle/testdata/simple_binary/__main__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/simple_binary/__main__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/simple_binary/test.yaml b/gazelle/testdata/simple_binary/test.yaml
new file mode 100644
index 0000000..ed97d53
--- /dev/null
+++ b/gazelle/testdata/simple_binary/test.yaml
@@ -0,0 +1 @@
+---
diff --git a/gazelle/testdata/simple_binary_with_library/BUILD.in b/gazelle/testdata/simple_binary_with_library/BUILD.in
new file mode 100644
index 0000000..b60e84f
--- /dev/null
+++ b/gazelle/testdata/simple_binary_with_library/BUILD.in
@@ -0,0 +1,18 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "simple_binary_with_library",
+ srcs = [
+ "__init__.py",
+ "bar.py",
+ "foo.py",
+ ],
+)
+
+# This target should be kept unmodified by Gazelle.
+py_library(
+ name = "custom",
+ srcs = [
+ "bar.py",
+ ],
+)
diff --git a/gazelle/testdata/simple_binary_with_library/BUILD.out b/gazelle/testdata/simple_binary_with_library/BUILD.out
new file mode 100644
index 0000000..eddc15c
--- /dev/null
+++ b/gazelle/testdata/simple_binary_with_library/BUILD.out
@@ -0,0 +1,27 @@
+load("@rules_python//python:defs.bzl", "py_binary", "py_library")
+
+py_library(
+ name = "simple_binary_with_library",
+ srcs = [
+ "__init__.py",
+ "bar.py",
+ "foo.py",
+ ],
+ visibility = ["//:__subpackages__"],
+)
+
+# This target should be kept unmodified by Gazelle.
+py_library(
+ name = "custom",
+ srcs = [
+ "bar.py",
+ ],
+)
+
+py_binary(
+ name = "simple_binary_with_library_bin",
+ srcs = ["__main__.py"],
+ main = "__main__.py",
+ visibility = ["//:__subpackages__"],
+ deps = [":simple_binary_with_library"],
+)
diff --git a/gazelle/testdata/simple_binary_with_library/README.md b/gazelle/testdata/simple_binary_with_library/README.md
new file mode 100644
index 0000000..cfc81a3
--- /dev/null
+++ b/gazelle/testdata/simple_binary_with_library/README.md
@@ -0,0 +1,4 @@
+# Simple binary with library
+
+This test case asserts that a simple `py_binary` is generated as expected
+referencing a `py_library`.
diff --git a/gazelle/testdata/simple_binary_with_library/WORKSPACE b/gazelle/testdata/simple_binary_with_library/WORKSPACE
new file mode 100644
index 0000000..faff6af
--- /dev/null
+++ b/gazelle/testdata/simple_binary_with_library/WORKSPACE
@@ -0,0 +1 @@
+# This is a Bazel workspace for the Gazelle test data.
diff --git a/gazelle/testdata/simple_binary_with_library/__init__.py b/gazelle/testdata/simple_binary_with_library/__init__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/simple_binary_with_library/__init__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/simple_binary_with_library/__main__.py b/gazelle/testdata/simple_binary_with_library/__main__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/simple_binary_with_library/__main__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/simple_binary_with_library/bar.py b/gazelle/testdata/simple_binary_with_library/bar.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/simple_binary_with_library/bar.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/simple_binary_with_library/foo.py b/gazelle/testdata/simple_binary_with_library/foo.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/simple_binary_with_library/foo.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/simple_binary_with_library/test.yaml b/gazelle/testdata/simple_binary_with_library/test.yaml
new file mode 100644
index 0000000..ed97d53
--- /dev/null
+++ b/gazelle/testdata/simple_binary_with_library/test.yaml
@@ -0,0 +1 @@
+---
diff --git a/gazelle/testdata/simple_library/BUILD.in b/gazelle/testdata/simple_library/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/simple_library/BUILD.in
diff --git a/gazelle/testdata/simple_library/BUILD.out b/gazelle/testdata/simple_library/BUILD.out
new file mode 100644
index 0000000..5793ac2
--- /dev/null
+++ b/gazelle/testdata/simple_library/BUILD.out
@@ -0,0 +1,7 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "simple_library",
+ srcs = ["__init__.py"],
+ visibility = ["//:__subpackages__"],
+)
diff --git a/gazelle/testdata/simple_library/README.md b/gazelle/testdata/simple_library/README.md
new file mode 100644
index 0000000..f88bda1
--- /dev/null
+++ b/gazelle/testdata/simple_library/README.md
@@ -0,0 +1,3 @@
+# Simple library
+
+This test case asserts that a simple `py_library` is generated as expected.
diff --git a/gazelle/testdata/simple_library/WORKSPACE b/gazelle/testdata/simple_library/WORKSPACE
new file mode 100644
index 0000000..faff6af
--- /dev/null
+++ b/gazelle/testdata/simple_library/WORKSPACE
@@ -0,0 +1 @@
+# This is a Bazel workspace for the Gazelle test data.
diff --git a/gazelle/testdata/simple_library/__init__.py b/gazelle/testdata/simple_library/__init__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/simple_library/__init__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/simple_library/test.yaml b/gazelle/testdata/simple_library/test.yaml
new file mode 100644
index 0000000..ed97d53
--- /dev/null
+++ b/gazelle/testdata/simple_library/test.yaml
@@ -0,0 +1 @@
+---
diff --git a/gazelle/testdata/simple_library_without_init/BUILD.in b/gazelle/testdata/simple_library_without_init/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/simple_library_without_init/BUILD.in
diff --git a/gazelle/testdata/simple_library_without_init/BUILD.out b/gazelle/testdata/simple_library_without_init/BUILD.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/simple_library_without_init/BUILD.out
diff --git a/gazelle/testdata/simple_library_without_init/README.md b/gazelle/testdata/simple_library_without_init/README.md
new file mode 100644
index 0000000..5c0a1ca
--- /dev/null
+++ b/gazelle/testdata/simple_library_without_init/README.md
@@ -0,0 +1,4 @@
+# Simple library without `__init__.py`
+
+This test case asserts that a simple `py_library` is generated as expected
+without an `__init__.py` but with a `BUILD` file marking it as a package.
diff --git a/gazelle/testdata/simple_library_without_init/WORKSPACE b/gazelle/testdata/simple_library_without_init/WORKSPACE
new file mode 100644
index 0000000..faff6af
--- /dev/null
+++ b/gazelle/testdata/simple_library_without_init/WORKSPACE
@@ -0,0 +1 @@
+# This is a Bazel workspace for the Gazelle test data.
diff --git a/gazelle/testdata/simple_library_without_init/foo/BUILD.in b/gazelle/testdata/simple_library_without_init/foo/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/simple_library_without_init/foo/BUILD.in
diff --git a/gazelle/testdata/simple_library_without_init/foo/BUILD.out b/gazelle/testdata/simple_library_without_init/foo/BUILD.out
new file mode 100644
index 0000000..2faa046
--- /dev/null
+++ b/gazelle/testdata/simple_library_without_init/foo/BUILD.out
@@ -0,0 +1,8 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "foo",
+ srcs = ["foo.py"],
+ imports = [".."],
+ visibility = ["//:__subpackages__"],
+)
diff --git a/gazelle/testdata/simple_library_without_init/foo/foo.py b/gazelle/testdata/simple_library_without_init/foo/foo.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/simple_library_without_init/foo/foo.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/simple_library_without_init/test.yaml b/gazelle/testdata/simple_library_without_init/test.yaml
new file mode 100644
index 0000000..ed97d53
--- /dev/null
+++ b/gazelle/testdata/simple_library_without_init/test.yaml
@@ -0,0 +1 @@
+---
diff --git a/gazelle/testdata/simple_test/BUILD.in b/gazelle/testdata/simple_test/BUILD.in
new file mode 100644
index 0000000..ffd20ea
--- /dev/null
+++ b/gazelle/testdata/simple_test/BUILD.in
@@ -0,0 +1,6 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "simple_test",
+ srcs = ["__init__.py"],
+)
diff --git a/gazelle/testdata/simple_test/BUILD.out b/gazelle/testdata/simple_test/BUILD.out
new file mode 100644
index 0000000..ae2f982
--- /dev/null
+++ b/gazelle/testdata/simple_test/BUILD.out
@@ -0,0 +1,17 @@
+load("@rules_python//python:defs.bzl", "py_library", "py_test")
+
+py_library(
+ name = "simple_test",
+ srcs = [
+ "__init__.py",
+ "foo.py",
+ ],
+ visibility = ["//:__subpackages__"],
+)
+
+py_test(
+ name = "simple_test_test",
+ srcs = ["__test__.py"],
+ main = "__test__.py",
+ deps = [":simple_test"],
+)
diff --git a/gazelle/testdata/simple_test/README.md b/gazelle/testdata/simple_test/README.md
new file mode 100644
index 0000000..0cfbbeb
--- /dev/null
+++ b/gazelle/testdata/simple_test/README.md
@@ -0,0 +1,3 @@
+# Simple test
+
+This test case asserts that a simple `py_test` is generated as expected.
diff --git a/gazelle/testdata/simple_test/WORKSPACE b/gazelle/testdata/simple_test/WORKSPACE
new file mode 100644
index 0000000..faff6af
--- /dev/null
+++ b/gazelle/testdata/simple_test/WORKSPACE
@@ -0,0 +1 @@
+# This is a Bazel workspace for the Gazelle test data.
diff --git a/gazelle/testdata/simple_test/__init__.py b/gazelle/testdata/simple_test/__init__.py
new file mode 100644
index 0000000..6a49193
--- /dev/null
+++ b/gazelle/testdata/simple_test/__init__.py
@@ -0,0 +1,3 @@
+from foo import foo
+
+_ = foo
diff --git a/gazelle/testdata/simple_test/__test__.py b/gazelle/testdata/simple_test/__test__.py
new file mode 100644
index 0000000..d6085a4
--- /dev/null
+++ b/gazelle/testdata/simple_test/__test__.py
@@ -0,0 +1,12 @@
+import unittest
+
+from __init__ import foo
+
+
+class FooTest(unittest.TestCase):
+ def test_foo(self):
+ self.assertEqual("foo", foo())
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/gazelle/testdata/simple_test/foo.py b/gazelle/testdata/simple_test/foo.py
new file mode 100644
index 0000000..a266b7c
--- /dev/null
+++ b/gazelle/testdata/simple_test/foo.py
@@ -0,0 +1,2 @@
+def foo():
+ return 'foo'
diff --git a/gazelle/testdata/simple_test/test.yaml b/gazelle/testdata/simple_test/test.yaml
new file mode 100644
index 0000000..36dd656
--- /dev/null
+++ b/gazelle/testdata/simple_test/test.yaml
@@ -0,0 +1,3 @@
+---
+expect:
+ exit_code: 0
diff --git a/gazelle/testdata/subdir_sources/BUILD.in b/gazelle/testdata/subdir_sources/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/BUILD.in
diff --git a/gazelle/testdata/subdir_sources/BUILD.out b/gazelle/testdata/subdir_sources/BUILD.out
new file mode 100644
index 0000000..d03a8f0
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/BUILD.out
@@ -0,0 +1,12 @@
+load("@rules_python//python:defs.bzl", "py_binary")
+
+py_binary(
+ name = "subdir_sources_bin",
+ srcs = ["__main__.py"],
+ main = "__main__.py",
+ visibility = ["//:__subpackages__"],
+ deps = [
+ "//foo",
+ "//one/two",
+ ],
+)
diff --git a/gazelle/testdata/subdir_sources/README.md b/gazelle/testdata/subdir_sources/README.md
new file mode 100644
index 0000000..79ca3a2
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/README.md
@@ -0,0 +1,5 @@
+# Subdir sources
+
+This test case asserts that `py_library` targets are generated with sources from
+subdirectories and that dependencies are added according to the target that the
+imported source file belongs to.
diff --git a/gazelle/testdata/subdir_sources/WORKSPACE b/gazelle/testdata/subdir_sources/WORKSPACE
new file mode 100644
index 0000000..faff6af
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/WORKSPACE
@@ -0,0 +1 @@
+# This is a Bazel workspace for the Gazelle test data.
diff --git a/gazelle/testdata/subdir_sources/__main__.py b/gazelle/testdata/subdir_sources/__main__.py
new file mode 100644
index 0000000..3cc8834
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/__main__.py
@@ -0,0 +1,7 @@
+import foo.bar.bar as bar
+import foo.baz.baz as baz
+import one.two.three as three
+
+_ = bar
+_ = baz
+_ = three
diff --git a/gazelle/testdata/subdir_sources/foo/BUILD.in b/gazelle/testdata/subdir_sources/foo/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/BUILD.in
diff --git a/gazelle/testdata/subdir_sources/foo/BUILD.out b/gazelle/testdata/subdir_sources/foo/BUILD.out
new file mode 100644
index 0000000..f99857d
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/BUILD.out
@@ -0,0 +1,13 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "foo",
+ srcs = [
+ "__init__.py",
+ "bar/bar.py",
+ "baz/baz.py",
+ "foo.py",
+ ],
+ imports = [".."],
+ visibility = ["//:__subpackages__"],
+)
diff --git a/gazelle/testdata/subdir_sources/foo/__init__.py b/gazelle/testdata/subdir_sources/foo/__init__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/__init__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/subdir_sources/foo/bar/bar.py b/gazelle/testdata/subdir_sources/foo/bar/bar.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/bar/bar.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/subdir_sources/foo/baz/baz.py b/gazelle/testdata/subdir_sources/foo/baz/baz.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/baz/baz.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/subdir_sources/foo/foo.py b/gazelle/testdata/subdir_sources/foo/foo.py
new file mode 100644
index 0000000..6752f22
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/foo.py
@@ -0,0 +1,3 @@
+import foo.bar.bar as bar
+
+_ = bar
diff --git a/gazelle/testdata/subdir_sources/foo/has_build/BUILD.in b/gazelle/testdata/subdir_sources/foo/has_build/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/has_build/BUILD.in
diff --git a/gazelle/testdata/subdir_sources/foo/has_build/BUILD.out b/gazelle/testdata/subdir_sources/foo/has_build/BUILD.out
new file mode 100644
index 0000000..0ef0cc1
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/has_build/BUILD.out
@@ -0,0 +1,8 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "has_build",
+ srcs = ["python/my_module.py"],
+ imports = ["../.."],
+ visibility = ["//:__subpackages__"],
+)
diff --git a/gazelle/testdata/subdir_sources/foo/has_build/python/my_module.py b/gazelle/testdata/subdir_sources/foo/has_build/python/my_module.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/has_build/python/my_module.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/subdir_sources/foo/has_build_bazel/BUILD.bazel.in b/gazelle/testdata/subdir_sources/foo/has_build_bazel/BUILD.bazel.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/has_build_bazel/BUILD.bazel.in
diff --git a/gazelle/testdata/subdir_sources/foo/has_build_bazel/BUILD.bazel.out b/gazelle/testdata/subdir_sources/foo/has_build_bazel/BUILD.bazel.out
new file mode 100644
index 0000000..79bd70a
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/has_build_bazel/BUILD.bazel.out
@@ -0,0 +1,8 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "has_build_bazel",
+ srcs = ["python/my_module.py"],
+ imports = ["../.."],
+ visibility = ["//:__subpackages__"],
+)
diff --git a/gazelle/testdata/subdir_sources/foo/has_build_bazel/python/my_module.py b/gazelle/testdata/subdir_sources/foo/has_build_bazel/python/my_module.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/has_build_bazel/python/my_module.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/subdir_sources/foo/has_init/BUILD.in b/gazelle/testdata/subdir_sources/foo/has_init/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/has_init/BUILD.in
diff --git a/gazelle/testdata/subdir_sources/foo/has_init/BUILD.out b/gazelle/testdata/subdir_sources/foo/has_init/BUILD.out
new file mode 100644
index 0000000..ce59ee2
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/has_init/BUILD.out
@@ -0,0 +1,11 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "has_init",
+ srcs = [
+ "__init__.py",
+ "python/my_module.py",
+ ],
+ imports = ["../.."],
+ visibility = ["//:__subpackages__"],
+)
diff --git a/gazelle/testdata/subdir_sources/foo/has_init/__init__.py b/gazelle/testdata/subdir_sources/foo/has_init/__init__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/has_init/__init__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/subdir_sources/foo/has_init/python/my_module.py b/gazelle/testdata/subdir_sources/foo/has_init/python/my_module.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/has_init/python/my_module.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/subdir_sources/foo/has_main/BUILD.in b/gazelle/testdata/subdir_sources/foo/has_main/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/has_main/BUILD.in
diff --git a/gazelle/testdata/subdir_sources/foo/has_main/BUILD.out b/gazelle/testdata/subdir_sources/foo/has_main/BUILD.out
new file mode 100644
index 0000000..265c08b
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/has_main/BUILD.out
@@ -0,0 +1,17 @@
+load("@rules_python//python:defs.bzl", "py_binary", "py_library")
+
+py_library(
+ name = "has_main",
+ srcs = ["python/my_module.py"],
+ imports = ["../.."],
+ visibility = ["//:__subpackages__"],
+)
+
+py_binary(
+ name = "has_main_bin",
+ srcs = ["__main__.py"],
+ imports = ["../.."],
+ main = "__main__.py",
+ visibility = ["//:__subpackages__"],
+ deps = [":has_main"],
+)
diff --git a/gazelle/testdata/subdir_sources/foo/has_main/__main__.py b/gazelle/testdata/subdir_sources/foo/has_main/__main__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/has_main/__main__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/subdir_sources/foo/has_main/python/my_module.py b/gazelle/testdata/subdir_sources/foo/has_main/python/my_module.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/has_main/python/my_module.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/subdir_sources/foo/has_test/BUILD.in b/gazelle/testdata/subdir_sources/foo/has_test/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/has_test/BUILD.in
diff --git a/gazelle/testdata/subdir_sources/foo/has_test/BUILD.out b/gazelle/testdata/subdir_sources/foo/has_test/BUILD.out
new file mode 100644
index 0000000..80739d9
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/has_test/BUILD.out
@@ -0,0 +1,16 @@
+load("@rules_python//python:defs.bzl", "py_library", "py_test")
+
+py_library(
+ name = "has_test",
+ srcs = ["python/my_module.py"],
+ imports = ["../.."],
+ visibility = ["//:__subpackages__"],
+)
+
+py_test(
+ name = "has_test_test",
+ srcs = ["__test__.py"],
+ imports = ["../.."],
+ main = "__test__.py",
+ deps = [":has_test"],
+)
diff --git a/gazelle/testdata/subdir_sources/foo/has_test/__test__.py b/gazelle/testdata/subdir_sources/foo/has_test/__test__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/has_test/__test__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/subdir_sources/foo/has_test/python/my_module.py b/gazelle/testdata/subdir_sources/foo/has_test/python/my_module.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/foo/has_test/python/my_module.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/subdir_sources/one/BUILD.in b/gazelle/testdata/subdir_sources/one/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/one/BUILD.in
diff --git a/gazelle/testdata/subdir_sources/one/BUILD.out b/gazelle/testdata/subdir_sources/one/BUILD.out
new file mode 100644
index 0000000..f2e5745
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/one/BUILD.out
@@ -0,0 +1,8 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "one",
+ srcs = ["__init__.py"],
+ imports = [".."],
+ visibility = ["//:__subpackages__"],
+)
diff --git a/gazelle/testdata/subdir_sources/one/__init__.py b/gazelle/testdata/subdir_sources/one/__init__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/one/__init__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/subdir_sources/one/two/BUILD.in b/gazelle/testdata/subdir_sources/one/two/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/one/two/BUILD.in
diff --git a/gazelle/testdata/subdir_sources/one/two/BUILD.out b/gazelle/testdata/subdir_sources/one/two/BUILD.out
new file mode 100644
index 0000000..f632eed
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/one/two/BUILD.out
@@ -0,0 +1,12 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "two",
+ srcs = [
+ "__init__.py",
+ "three.py",
+ ],
+ imports = ["../.."],
+ visibility = ["//:__subpackages__"],
+ deps = ["//foo"],
+)
diff --git a/gazelle/testdata/subdir_sources/one/two/__init__.py b/gazelle/testdata/subdir_sources/one/two/__init__.py
new file mode 100644
index 0000000..f6c7d2a
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/one/two/__init__.py
@@ -0,0 +1,3 @@
+import foo.baz.baz as baz
+
+_ = baz
diff --git a/gazelle/testdata/subdir_sources/one/two/three.py b/gazelle/testdata/subdir_sources/one/two/three.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/one/two/three.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/subdir_sources/test.yaml b/gazelle/testdata/subdir_sources/test.yaml
new file mode 100644
index 0000000..ed97d53
--- /dev/null
+++ b/gazelle/testdata/subdir_sources/test.yaml
@@ -0,0 +1 @@
+---
diff --git a/gazelle/testdata/with_nested_import_statements/BUILD.in b/gazelle/testdata/with_nested_import_statements/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/with_nested_import_statements/BUILD.in
diff --git a/gazelle/testdata/with_nested_import_statements/BUILD.out b/gazelle/testdata/with_nested_import_statements/BUILD.out
new file mode 100644
index 0000000..bb2f34d
--- /dev/null
+++ b/gazelle/testdata/with_nested_import_statements/BUILD.out
@@ -0,0 +1,8 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "with_nested_import_statements",
+ srcs = ["__init__.py"],
+ visibility = ["//:__subpackages__"],
+ deps = ["@gazelle_python_test//pypi__boto3"],
+)
diff --git a/gazelle/testdata/with_nested_import_statements/README.md b/gazelle/testdata/with_nested_import_statements/README.md
new file mode 100644
index 0000000..7213b34
--- /dev/null
+++ b/gazelle/testdata/with_nested_import_statements/README.md
@@ -0,0 +1,4 @@
+# With nested import statements
+
+This test case asserts that a `py_library` is generated with dependencies
+extracted from nested import statements from the Python source file.
diff --git a/gazelle/testdata/with_nested_import_statements/WORKSPACE b/gazelle/testdata/with_nested_import_statements/WORKSPACE
new file mode 100644
index 0000000..faff6af
--- /dev/null
+++ b/gazelle/testdata/with_nested_import_statements/WORKSPACE
@@ -0,0 +1 @@
+# This is a Bazel workspace for the Gazelle test data.
diff --git a/gazelle/testdata/with_nested_import_statements/__init__.py b/gazelle/testdata/with_nested_import_statements/__init__.py
new file mode 100644
index 0000000..6871953
--- /dev/null
+++ b/gazelle/testdata/with_nested_import_statements/__init__.py
@@ -0,0 +1,11 @@
+import os
+import sys
+
+_ = os
+_ = sys
+
+
+def main():
+ import boto3
+
+ _ = boto3
diff --git a/gazelle/testdata/with_nested_import_statements/gazelle_python.yaml b/gazelle/testdata/with_nested_import_statements/gazelle_python.yaml
new file mode 100644
index 0000000..7288b79
--- /dev/null
+++ b/gazelle/testdata/with_nested_import_statements/gazelle_python.yaml
@@ -0,0 +1,4 @@
+manifest:
+ modules_mapping:
+ boto3: boto3
+ pip_deps_repository_name: gazelle_python_test
diff --git a/gazelle/testdata/with_nested_import_statements/test.yaml b/gazelle/testdata/with_nested_import_statements/test.yaml
new file mode 100644
index 0000000..ed97d53
--- /dev/null
+++ b/gazelle/testdata/with_nested_import_statements/test.yaml
@@ -0,0 +1 @@
+---
diff --git a/gazelle/testdata/with_std_requirements/BUILD.in b/gazelle/testdata/with_std_requirements/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/with_std_requirements/BUILD.in
diff --git a/gazelle/testdata/with_std_requirements/BUILD.out b/gazelle/testdata/with_std_requirements/BUILD.out
new file mode 100644
index 0000000..a382ca8
--- /dev/null
+++ b/gazelle/testdata/with_std_requirements/BUILD.out
@@ -0,0 +1,7 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+py_library(
+ name = "with_std_requirements",
+ srcs = ["__init__.py"],
+ visibility = ["//:__subpackages__"],
+)
diff --git a/gazelle/testdata/with_std_requirements/README.md b/gazelle/testdata/with_std_requirements/README.md
new file mode 100644
index 0000000..4eaf1b0
--- /dev/null
+++ b/gazelle/testdata/with_std_requirements/README.md
@@ -0,0 +1,4 @@
+# With std requirements
+
+This test case asserts that a `py_library` is generated without any `deps` since
+it only imports Python standard library packages.
diff --git a/gazelle/testdata/with_std_requirements/WORKSPACE b/gazelle/testdata/with_std_requirements/WORKSPACE
new file mode 100644
index 0000000..faff6af
--- /dev/null
+++ b/gazelle/testdata/with_std_requirements/WORKSPACE
@@ -0,0 +1 @@
+# This is a Bazel workspace for the Gazelle test data.
diff --git a/gazelle/testdata/with_std_requirements/__init__.py b/gazelle/testdata/with_std_requirements/__init__.py
new file mode 100644
index 0000000..154689a
--- /dev/null
+++ b/gazelle/testdata/with_std_requirements/__init__.py
@@ -0,0 +1,5 @@
+import os
+import sys
+
+_ = os
+_ = sys
diff --git a/gazelle/testdata/with_std_requirements/test.yaml b/gazelle/testdata/with_std_requirements/test.yaml
new file mode 100644
index 0000000..ed97d53
--- /dev/null
+++ b/gazelle/testdata/with_std_requirements/test.yaml
@@ -0,0 +1 @@
+---
diff --git a/gazelle/testdata/with_third_party_requirements/BUILD.in b/gazelle/testdata/with_third_party_requirements/BUILD.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gazelle/testdata/with_third_party_requirements/BUILD.in
diff --git a/gazelle/testdata/with_third_party_requirements/BUILD.out b/gazelle/testdata/with_third_party_requirements/BUILD.out
new file mode 100644
index 0000000..9854730
--- /dev/null
+++ b/gazelle/testdata/with_third_party_requirements/BUILD.out
@@ -0,0 +1,27 @@
+load("@rules_python//python:defs.bzl", "py_binary", "py_library")
+
+py_library(
+ name = "with_third_party_requirements",
+ srcs = [
+ "__init__.py",
+ "bar.py",
+ "foo.py",
+ ],
+ visibility = ["//:__subpackages__"],
+ deps = [
+ "@gazelle_python_test//pypi__baz",
+ "@gazelle_python_test//pypi__boto3",
+ "@gazelle_python_test//pypi__djangorestframework",
+ ],
+)
+
+py_binary(
+ name = "with_third_party_requirements_bin",
+ srcs = ["__main__.py"],
+ main = "__main__.py",
+ visibility = ["//:__subpackages__"],
+ deps = [
+ ":with_third_party_requirements",
+ "@gazelle_python_test//pypi__baz",
+ ],
+)
diff --git a/gazelle/testdata/with_third_party_requirements/README.md b/gazelle/testdata/with_third_party_requirements/README.md
new file mode 100644
index 0000000..b47101c
--- /dev/null
+++ b/gazelle/testdata/with_third_party_requirements/README.md
@@ -0,0 +1,5 @@
+# With third-party requirements
+
+This test case asserts that a `py_library` is generated with dependencies
+extracted from its sources and a `py_binary` is generated embeding the
+`py_library` and inherits its dependencies, without specifying the `deps` again.
diff --git a/gazelle/testdata/with_third_party_requirements/WORKSPACE b/gazelle/testdata/with_third_party_requirements/WORKSPACE
new file mode 100644
index 0000000..faff6af
--- /dev/null
+++ b/gazelle/testdata/with_third_party_requirements/WORKSPACE
@@ -0,0 +1 @@
+# This is a Bazel workspace for the Gazelle test data.
diff --git a/gazelle/testdata/with_third_party_requirements/__init__.py b/gazelle/testdata/with_third_party_requirements/__init__.py
new file mode 100644
index 0000000..6b58ff3
--- /dev/null
+++ b/gazelle/testdata/with_third_party_requirements/__init__.py
@@ -0,0 +1 @@
+# For test purposes only.
diff --git a/gazelle/testdata/with_third_party_requirements/__main__.py b/gazelle/testdata/with_third_party_requirements/__main__.py
new file mode 100644
index 0000000..fe551aa
--- /dev/null
+++ b/gazelle/testdata/with_third_party_requirements/__main__.py
@@ -0,0 +1,5 @@
+import bar
+import foo
+
+_ = bar
+_ = foo
diff --git a/gazelle/testdata/with_third_party_requirements/bar.py b/gazelle/testdata/with_third_party_requirements/bar.py
new file mode 100644
index 0000000..19ddd97
--- /dev/null
+++ b/gazelle/testdata/with_third_party_requirements/bar.py
@@ -0,0 +1,11 @@
+import os
+
+import bar
+import boto3
+import rest_framework
+
+_ = os
+
+_ = bar
+_ = boto3
+_ = rest_framework
diff --git a/gazelle/testdata/with_third_party_requirements/foo.py b/gazelle/testdata/with_third_party_requirements/foo.py
new file mode 100644
index 0000000..29a1f3b
--- /dev/null
+++ b/gazelle/testdata/with_third_party_requirements/foo.py
@@ -0,0 +1,11 @@
+import sys
+
+import boto3
+import foo
+import rest_framework
+
+_ = sys
+
+_ = boto3
+_ = foo
+_ = rest_framework
diff --git a/gazelle/testdata/with_third_party_requirements/gazelle_python.yaml b/gazelle/testdata/with_third_party_requirements/gazelle_python.yaml
new file mode 100644
index 0000000..76bb8bf
--- /dev/null
+++ b/gazelle/testdata/with_third_party_requirements/gazelle_python.yaml
@@ -0,0 +1,7 @@
+manifest:
+ modules_mapping:
+ boto3: boto3
+ rest_framework: djangorestframework
+ foo: baz
+ bar: baz
+ pip_deps_repository_name: gazelle_python_test
diff --git a/gazelle/testdata/with_third_party_requirements/test.yaml b/gazelle/testdata/with_third_party_requirements/test.yaml
new file mode 100644
index 0000000..ed97d53
--- /dev/null
+++ b/gazelle/testdata/with_third_party_requirements/test.yaml
@@ -0,0 +1 @@
+---
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..25036da
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,14 @@
+module github.com/bazelbuild/rules_python
+
+go 1.16
+
+require (
+ github.com/bazelbuild/bazel-gazelle v0.23.0
+ github.com/bazelbuild/buildtools v0.0.0-20200718160251-b1667ff58f71
+ github.com/bazelbuild/rules_go v0.0.0-20190719190356-6dae44dc5cab
+ github.com/bmatcuk/doublestar v1.2.2
+ github.com/emirpasic/gods v1.12.0
+ github.com/ghodss/yaml v1.0.0
+ github.com/google/uuid v1.3.0
+ gopkg.in/yaml.v2 v2.2.2
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..2aceab6
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,47 @@
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/bazelbuild/bazel-gazelle v0.23.0 h1:Ks6YN+WkOv2lYWlvf7ksxUpLvrDbBHPBXXUrBFQ3BZM=
+github.com/bazelbuild/bazel-gazelle v0.23.0/go.mod h1:3mHi4TYn0QxwdMKPJfj3FKhZxYgWm46DjWQQPOg20BY=
+github.com/bazelbuild/buildtools v0.0.0-20200718160251-b1667ff58f71 h1:Et1IIXrXwhpDvR5wH9REPEZ0sUtzUoJSq19nfmBqzBY=
+github.com/bazelbuild/buildtools v0.0.0-20200718160251-b1667ff58f71/go.mod h1:5JP0TXzWDHXv8qvxRC4InIazwdyDseBDbzESUMKk1yU=
+github.com/bazelbuild/rules_go v0.0.0-20190719190356-6dae44dc5cab h1:wzbawlkLtl2ze9w/312NHZ84c7kpUCtlkD8HgFY27sw=
+github.com/bazelbuild/rules_go v0.0.0-20190719190356-6dae44dc5cab/go.mod h1:MC23Dc/wkXEyk3Wpq6lCqz0ZAYOZDw2DR5y3N1q2i7M=
+github.com/bmatcuk/doublestar v1.2.2 h1:oC24CykoSAB8zd7XgruHo33E0cHJf/WhQA/7BeXj+x0=
+github.com/bmatcuk/doublestar v1.2.2/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
+github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e h1:aZzprAO9/8oim3qStq3wc1Xuxx4QmAGriC4VU4ojemQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/internal_deps.bzl b/internal_deps.bzl
index de2226c..e3910a9 100644
--- a/internal_deps.bzl
+++ b/internal_deps.bzl
@@ -36,6 +36,29 @@
strip_prefix = "stardoc-0.4.0",
)
+ maybe(
+ http_archive,
+ name = "io_bazel_rules_go",
+ sha256 = "69de5c704a05ff37862f7e0f5534d4f479418afc21806c887db544a316f3cb6b",
+ urls = [
+ "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.27.0/rules_go-v0.27.0.tar.gz",
+ "https://github.com/bazelbuild/rules_go/releases/download/v0.27.0/rules_go-v0.27.0.tar.gz",
+ ],
+ )
+
+ maybe(
+ http_archive,
+ name = "bazel_gazelle",
+ patch_args = ["-p1"],
+ patches = ["@rules_python//gazelle:bazel_gazelle.pr1095.patch"],
+ sha256 = "0bb8056ab9ed4cbcab5b74348d8530c0e0b939987b0cfe36c1ab53d35a99e4de",
+ strip_prefix = "bazel-gazelle-2834ea44b3ec6371c924baaf28704730ec9d4559",
+ urls = [
+ # No release since March, and we need subsequent fixes
+ "https://github.com/bazelbuild/bazel-gazelle/archive/2834ea44b3ec6371c924baaf28704730ec9d4559.zip",
+ ],
+ )
+
# Test data for WHL tool testing.
maybe(
http_file,
diff --git a/internal_setup.bzl b/internal_setup.bzl
index 8609915..9523f75 100644
--- a/internal_setup.bzl
+++ b/internal_setup.bzl
@@ -1,6 +1,8 @@
"""Setup for rules_python tests and tools."""
+load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
load("@build_bazel_integration_testing//tools:repositories.bzl", "bazel_binaries")
+load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
# Requirements for building our piptool.
load(
@@ -8,6 +10,7 @@
_piptool_install = "pip_install",
)
load("//:version.bzl", "SUPPORTED_BAZEL_VERSIONS")
+load("//gazelle:deps.bzl", _go_repositories = "gazelle_deps")
load("//python/pip_install:repositories.bzl", "pip_install_dependencies")
def rules_python_internal_setup():
@@ -21,3 +24,12 @@
# Depend on the Bazel binaries for running bazel-in-bazel tests
bazel_binaries(versions = SUPPORTED_BAZEL_VERSIONS)
+
+ # gazelle:repository_macro gazelle/deps.bzl%gazelle_deps
+ _go_repositories()
+
+ go_rules_dependencies()
+
+ go_register_toolchains(version = "1.16")
+
+ gazelle_dependencies()