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()