diff --git a/.bazelrc b/.bazelrc index 6aee0a663d..1508cde027 100644 --- a/.bazelrc +++ b/.bazelrc @@ -7,3 +7,11 @@ build --deleted_packages=examples/legacy_pip_import/boto,examples/legacy_pip_imp 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 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/.bazelversion b/.bazelversion index bea438e9ad..ee74734aa2 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -3.3.1 +4.1.0 diff --git a/.gitignore b/.gitignore index cc8decd9a1..bc2bd94176 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,8 @@ bazel-testlogs # 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 ad7569b447..8b163d3808 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 @@ -50,3 +52,18 @@ filegroup( ], 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", + ], + command = "update-repos", +) diff --git a/WORKSPACE b/WORKSPACE index 97c67eb532..c1c58ecc17 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -24,3 +24,8 @@ rules_python_internal_deps() 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 0000000000..0c89fc927e --- /dev/null +++ b/examples/build_file_generation/.bazelversion @@ -0,0 +1 @@ +4.0.0 \ No newline at end of file diff --git a/examples/build_file_generation/BUILD b/examples/build_file_generation/BUILD new file mode 100644 index 0000000000..576ff01aae --- /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_mapping//: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 0000000000..9b2fe1a7be --- /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 0000000000..c9ae5595dc --- /dev/null +++ b/examples/build_file_generation/WORKSPACE @@ -0,0 +1,74 @@ +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 + +http_archive( + name = "rules_python", + sha256 = "934c9ceb552e84577b0faf1e5a2f0450314985b4d8712b2b70717dc679fdc01b", + url = "https://github.com/bazelbuild/rules_python/releases/download/0.3.0/rules_python-0.3.0.tar.gz", +) + +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 0000000000..ce47b771f4 --- /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 0000000000..39eacccf41 --- /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 0000000000..9d84d35885 --- /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 0000000000..7573a6f591 --- /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 0000000000..256b0a1f43 --- /dev/null +++ b/gazelle/BUILD.bazel @@ -0,0 +1,71 @@ +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_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 0000000000..b89dc52b67 --- /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_coarse_grained_generation`| `false` | +| Controls whether a single target should be created in this package, `glob`'ing sub-directories rather than fine-grained target generation. Can be "true" or "false" | | +| `# 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 0000000000..a417c94944 --- /dev/null +++ b/gazelle/bazel_gazelle.pr1095.patch @@ -0,0 +1,19 @@ +commit b1c61c0b77648f7345a7c42cce941e32d87c84bf +Author: Alex Eagle +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 0000000000..1918ffd689 --- /dev/null +++ b/gazelle/configure.go @@ -0,0 +1,155 @@ +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.CoarseGrainedGeneration, + 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 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.CoarseGrainedGeneration: + v, err := strconv.ParseBool(strings.TrimSpace(d.Value)) + if err != nil { + log.Fatal(err) + } + config.SetCoarseGrainedGeneration(v) + 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 +} \ No newline at end of file diff --git a/gazelle/def.bzl b/gazelle/def.bzl new file mode 100644 index 0000000000..a402fc74c4 --- /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 0000000000..284bf4274a --- /dev/null +++ b/gazelle/deps.bzl @@ -0,0 +1,169 @@ +"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", + 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 0000000000..c929929748 --- /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 0000000000..ac9eb42a3a --- /dev/null +++ b/gazelle/generate.go @@ -0,0 +1,364 @@ +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/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) { + baseName := filepath.Base(path) + f, _ := filepath.Rel(args.Dir, 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 + } +} \ No newline at end of file diff --git a/gazelle/kinds.go b/gazelle/kinds.go new file mode 100644 index 0000000000..5a72f97129 --- /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 of 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 0000000000..39ca6b3ab3 --- /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 0000000000..2e5b6b8e99 --- /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 0000000000..425d2a3fec --- /dev/null +++ b/gazelle/manifest/defs.bzl @@ -0,0 +1,70 @@ +"""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"], + ) + + 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 0000000000..29b9f15628 --- /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 0000000000..1ed91bf1ec --- /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 0000000000..4d432dae28 --- /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 0000000000..40a231f2bd --- /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 0000000000..f14845f756 --- /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 0000000000..4b24b51ae4 --- /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 0000000000..518fe06eb6 --- /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 0000000000..4dc1f2c545 --- /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 0000000000..9dd49a6ac0 --- /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 0000000000..4ce6a0001e --- /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 0000000000..352bfcb4bf --- /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 0000000000..0c918b230c --- /dev/null +++ b/gazelle/modules_mapping/def.bzl @@ -0,0 +1,322 @@ +"""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", + ], + 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 = { + "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, + ), + "_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 0000000000..44cfcf63fa --- /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 0000000000..bbc9e97251 --- /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 0000000000..64c505eaec --- /dev/null +++ b/gazelle/parser.go @@ -0,0 +1,272 @@ +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 + } + + // Check if the imported module is part of the standard library. + if isStd, err := isStdModule(m); err != nil { + return nil, err + } else if isStd { + 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 '. + 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 +} \ No newline at end of file diff --git a/gazelle/python_test.go b/gazelle/python_test.go new file mode 100644 index 0000000000..967ce453f4 --- /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 0000000000..6e881d3972 --- /dev/null +++ b/gazelle/pythonconfig/BUILD.bazel @@ -0,0 +1,12 @@ +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"], +) diff --git a/gazelle/pythonconfig/pythonconfig.go b/gazelle/pythonconfig/pythonconfig.go new file mode 100644 index 0000000000..5f1b341d33 --- /dev/null +++ b/gazelle/pythonconfig/pythonconfig.go @@ -0,0 +1,293 @@ +package pythonconfig + +import ( + "fmt" + "path/filepath" + "strings" + + "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" + // CoarseGrainedGeneration represents the directive that controls whether a + // single target should be created englobing sub-directories rather than the + // default fine-grained target generation. + CoarseGrainedGeneration = "python_coarse_grained_generation" + // 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" +) + +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 + + 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, + 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, + 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, + } +} + +// 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) +} \ No newline at end of file diff --git a/gazelle/pythonconfig/types.go b/gazelle/pythonconfig/types.go new file mode 100644 index 0000000000..bdb535bf6e --- /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 0000000000..8e261bf233 --- /dev/null +++ b/gazelle/resolve.go @@ -0,0 +1,279 @@ +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 { + 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 + } + } + 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 + } + 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} +} \ No newline at end of file diff --git a/gazelle/std_modules.go b/gazelle/std_modules.go new file mode 100644 index 0000000000..8c2cd35f3c --- /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 0000000000..2d44a0aca1 --- /dev/null +++ b/gazelle/std_modules.py @@ -0,0 +1,51 @@ +# 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 distutils.sysconfig as sysconfig +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(site_packages, module): + try: + import_obj = __import__(module, globals(), locals(), [], 0) + if not hasattr(import_obj, "__file__"): + return True + if not import_obj.__file__.startswith(sysconfig.PREFIX): + return False + # pip is by default bundled with Python 2 >= 2.7.9 or Python 3 >= 3.4. + if module == "pip": + return True + for pkg in site_packages: + if import_obj.__file__.startswith(pkg): + return False + return True + except Exception: + return False + + +def main(stdin, stdout): + site_packages = site.getsitepackages() + 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(site_packages, module) else "false", + end="\n", + file=stdout, + ) + stdout.flush() + + +if __name__ == "__main__": + exit(main(sys.stdin, sys.stdout)) \ No newline at end of file diff --git a/gazelle/target.go b/gazelle/target.go new file mode 100644 index 0000000000..60abd0c237 --- /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 0000000000..6c25d4894c --- /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 0000000000..71a5c5adda --- /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 0000000000..2ba2c84c9a --- /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 0000000000..75ceb0b1b5 --- /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 0000000000..4959898cdd --- /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 0000000000..f2a1c081ad --- /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 0000000000..e69de29bb2 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 0000000000..da9915ddbe --- /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 0000000000..76c3313f0e --- /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 0000000000..e69de29bb2 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 0000000000..749fd3d490 --- /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 0000000000..76c3313f0e --- /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 0000000000..e69de29bb2 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 0000000000..4404d30461 --- /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 0000000000..76c3313f0e --- /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 0000000000..7e911bf29b --- /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 0000000000..e69de29bb2 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 0000000000..a0d421b8dc --- /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 0000000000..76c3313f0e --- /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 0000000000..ed97d539c0 --- /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 0000000000..741aff66ed --- /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 0000000000..964db6d484 --- /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 0000000000..a80fffec5e --- /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 0000000000..faff6af87a --- /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 0000000000..88eba74539 --- /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 0000000000..36dd656b39 --- /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 0000000000..33e8ec25cb --- /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 0000000000..62772e30b5 --- /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 0000000000..19f9d6637a --- /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 0000000000..faff6af87a --- /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 0000000000..e69de29bb2 diff --git a/gazelle/testdata/dont_rename_target/test.yaml b/gazelle/testdata/dont_rename_target/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /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 0000000000..e69de29bb2 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 0000000000..fd6c48559d --- /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 0000000000..591adc1c27 --- /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 0000000000..faff6af87a --- /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 0000000000..6b58ff30a8 --- /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 0000000000..63e6966941 --- /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 0000000000..9bede69c55 --- /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 0000000000..ed97d539c0 --- /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 0000000000..e69de29bb2 diff --git a/gazelle/testdata/first_party_dependencies/BUILD.out b/gazelle/testdata/first_party_dependencies/BUILD.out new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/testdata/first_party_dependencies/README.md b/gazelle/testdata/first_party_dependencies/README.md new file mode 100644 index 0000000000..f57e255fa7 --- /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 0000000000..4959898cdd --- /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 0000000000..6948b47b10 --- /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 0000000000..c96a56106d --- /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 0000000000..2d241cc41e --- /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 0000000000..7fe1f496d1 --- /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 0000000000..470bf82ce9 --- /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 0000000000..e311ff122a --- /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 0000000000..886a89cc3d --- /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 0000000000..a0172452e1 --- /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 0000000000..e74f519643 --- /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 0000000000..0ee9a303bf --- /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 0000000000..464fabb684 --- /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 0000000000..8aeca3de74 --- /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 0000000000..ed97d539c0 --- /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 0000000000..6948b47b10 --- /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 0000000000..ccfb3e0c08 --- /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 0000000000..41bec88fd3 --- /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 0000000000..6948b47b10 --- /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 0000000000..182db08f0e --- /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 0000000000..a0bb5c8715 --- /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 0000000000..fb90e4cbde --- /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 0000000000..264205b964 --- /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 0000000000..2a173b4305 --- /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 0000000000..faff6af87a --- /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 0000000000..6aca4f07ca --- /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 0000000000..cc299250bd --- /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 0000000000..81d3ef1be5 --- /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 0000000000..e69de29bb2 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 0000000000..3decd902e0 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..4b6419fa35 --- /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 0000000000..e69de29bb2 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 0000000000..7063141808 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..ce53b8788a --- /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 0000000000..ed97d539c0 --- /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 0000000000..6948b47b10 --- /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 0000000000..6948b47b10 --- /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 0000000000..c7d0e48a57 --- /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 0000000000..c7d0e48a57 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..668c700c56 --- /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 0000000000..06616fb1ae --- /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 0000000000..48df0688a6 --- /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 0000000000..69f8415999 --- /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 0000000000..faff6af87a --- /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 0000000000..6a49193fe4 --- /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 0000000000..a266b7c7c0 --- /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 0000000000..ed97d539c0 --- /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 0000000000..e69de29bb2 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 0000000000..3cd47a6fe0 --- /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 0000000000..55dcc9bf7b --- /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 0000000000..faff6af87a --- /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 0000000000..4301453aec --- /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 0000000000..54b3148810 --- /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 0000000000..36dd656b39 --- /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 0000000000..e69de29bb2 diff --git a/gazelle/testdata/invalid_imported_module/BUILD.out b/gazelle/testdata/invalid_imported_module/BUILD.out new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/testdata/invalid_imported_module/README.md b/gazelle/testdata/invalid_imported_module/README.md new file mode 100644 index 0000000000..85e6f45954 --- /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 0000000000..faff6af87a --- /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 0000000000..c100931cc4 --- /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 0000000000..f12c36b505 --- /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 0000000000..adc9e83069 --- /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 0000000000..adc9e83069 --- /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 0000000000..b3ac3d27bd --- /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 0000000000..4959898cdd --- /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 0000000000..585870c73f --- /dev/null +++ b/gazelle/testdata/monorepo/coarse_grained/BUILD.in @@ -0,0 +1,10 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_extension enabled +# gazelle:python_root +# gazelle:python_coarse_grained_generation true + +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 0000000000..ef5ebf4db8 --- /dev/null +++ b/gazelle/testdata/monorepo/coarse_grained/BUILD.out @@ -0,0 +1,18 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_extension enabled +# gazelle:python_root +# gazelle:python_coarse_grained_generation true + +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 0000000000..2b5b044257 --- /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 0000000000..3299820777 --- /dev/null +++ b/gazelle/testdata/monorepo/coarse_grained/_boundary/BUILD.in @@ -0,0 +1 @@ +# gazelle:python_coarse_grained_generation false 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 0000000000..605dcf1651 --- /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_coarse_grained_generation false + +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 0000000000..0e67695af3 --- /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 0000000000..e69de29bb2 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 0000000000..f6ec21462a --- /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 0000000000..e74f519643 --- /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/hue.py b/gazelle/testdata/monorepo/coarse_grained/bar/baz/hue.py new file mode 100644 index 0000000000..6b58ff30a8 --- /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/foo/__init__.py b/gazelle/testdata/monorepo/coarse_grained/foo/__init__.py new file mode 100644 index 0000000000..8aeca3de74 --- /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 0000000000..fe89518481 --- /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 0000000000..527b6ea884 --- /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 0000000000..b11b373468 --- /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 0000000000..a957227a9a --- /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 0000000000..f08f5e8009 --- /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 0000000000..7fe1f496d1 --- /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 0000000000..0e85623394 --- /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 0000000000..f6ec21462a --- /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 0000000000..00ba8ed974 --- /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 0000000000..1eb52fcf88 --- /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 0000000000..e74f519643 --- /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 0000000000..0ee9a303bf --- /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 0000000000..464fabb684 --- /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 0000000000..8aeca3de74 --- /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 0000000000..67c53451b4 --- /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 0000000000..ed97d539c0 --- /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 0000000000..79bb63fa49 --- /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 0000000000..bbb03b1d4b --- /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 0000000000..fe955f6e96 --- /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 0000000000..572216c14b --- /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 0000000000..31812e0535 --- /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 0000000000..4b638edea2 --- /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 0000000000..fb3e877fe5 --- /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 0000000000..3bc5939e58 --- /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 0000000000..e69de29bb2 diff --git a/gazelle/testdata/monorepo/wont_generate/BUILD.out b/gazelle/testdata/monorepo/wont_generate/BUILD.out new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/testdata/monorepo/wont_generate/__main__.py b/gazelle/testdata/monorepo/wont_generate/__main__.py new file mode 100644 index 0000000000..2d241cc41e --- /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 0000000000..e69de29bb2 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 0000000000..e69de29bb2 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 0000000000..e311ff122a --- /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 0000000000..e69de29bb2 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 0000000000..e69de29bb2 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 0000000000..e74f519643 --- /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 0000000000..e69de29bb2 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 0000000000..e69de29bb2 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 0000000000..8aeca3de74 --- /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 0000000000..7517848a92 --- /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 0000000000..e2f067489c --- /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 0000000000..9dd88ecd24 --- /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 0000000000..faff6af87a --- /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 0000000000..6b58ff30a8 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..8d2ae35fd4 --- /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 0000000000..4d4ead86b4 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..c81e735d7e --- /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 0000000000..3fa5de2b79 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..ed97d539c0 --- /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 0000000000..fd4dc1c5b7 --- /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 0000000000..fd4dc1c5b7 --- /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 0000000000..a58bbe45dd --- /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 0000000000..faff6af87a --- /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 0000000000..6b58ff30a8 --- /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 0000000000..bc30dd0858 --- /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 0000000000..a6840843c1 --- /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 0000000000..a6840843c1 --- /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 0000000000..cd36917251 --- /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 0000000000..faff6af87a --- /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 0000000000..6b58ff30a8 --- /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 0000000000..3743c324df --- /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 0000000000..2091253114 --- /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 0000000000..2091253114 --- /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 0000000000..886c1e368c --- /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 0000000000..faff6af87a --- /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 0000000000..6b58ff30a8 --- /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 0000000000..fc4e24e830 --- /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 0000000000..1ba277afbb --- /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 0000000000..37ae4f9aa1 --- /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 0000000000..75f61e1baf --- /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 0000000000..faff6af87a --- /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 0000000000..79935a70c4 --- /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 0000000000..7288b798e1 --- /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 0000000000..ed97d539c0 --- /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 0000000000..6277446576 --- /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 0000000000..1fe6030053 --- /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 0000000000..710118d6a4 --- /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 0000000000..faff6af87a --- /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 0000000000..6b58ff30a8 --- /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 0000000000..e69de29bb2 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 0000000000..af3c3983db --- /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 0000000000..6b58ff30a8 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..c3049cabf5 --- /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 0000000000..c3049cabf5 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..ed97d539c0 --- /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 0000000000..e69de29bb2 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 0000000000..bdde605c09 --- /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 0000000000..8b592e10a7 --- /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 0000000000..e69de29bb2 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 0000000000..6b58ff30a8 --- /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 0000000000..7288b798e1 --- /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 0000000000..57c019daab --- /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 0000000000..ed97d539c0 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..e69de29bb2 diff --git a/gazelle/testdata/relative_imports/BUILD.out b/gazelle/testdata/relative_imports/BUILD.out new file mode 100644 index 0000000000..2c0862748b --- /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 0000000000..1937cbcf4a --- /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 0000000000..4959898cdd --- /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 0000000000..4fb887a803 --- /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 0000000000..69cdde2633 --- /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 0000000000..1e731b4ec1 --- /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 0000000000..e69de29bb2 diff --git a/gazelle/testdata/relative_imports/package2/BUILD.out b/gazelle/testdata/relative_imports/package2/BUILD.out new file mode 100644 index 0000000000..bbbc9f8e95 --- /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 0000000000..fd0384ba7e --- /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 0000000000..a5102dd8bd --- /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 0000000000..6e69699985 --- /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 0000000000..ac1f7257df --- /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 0000000000..ed97d539c0 --- /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 0000000000..e69de29bb2 diff --git a/gazelle/testdata/simple_binary/BUILD.out b/gazelle/testdata/simple_binary/BUILD.out new file mode 100644 index 0000000000..35aa7089ec --- /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 0000000000..00c90dcf65 --- /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 0000000000..faff6af87a --- /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 0000000000..6b58ff30a8 --- /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 0000000000..ed97d539c0 --- /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 0000000000..b60e84f17e --- /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 0000000000..eddc15cacd --- /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 0000000000..cfc81a3581 --- /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 0000000000..faff6af87a --- /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 0000000000..6b58ff30a8 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..ed97d539c0 --- /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 0000000000..e69de29bb2 diff --git a/gazelle/testdata/simple_library/BUILD.out b/gazelle/testdata/simple_library/BUILD.out new file mode 100644 index 0000000000..5793ac2066 --- /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 0000000000..f88bda1ba1 --- /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 0000000000..faff6af87a --- /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 0000000000..6b58ff30a8 --- /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 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/testdata/simple_library/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 0000000000..ffd20ea85d --- /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 0000000000..ae2f982032 --- /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 0000000000..0cfbbebc02 --- /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 0000000000..faff6af87a --- /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 0000000000..6a49193fe4 --- /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 0000000000..d6085a41b4 --- /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 0000000000..a266b7c7c0 --- /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 0000000000..36dd656b39 --- /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 0000000000..e69de29bb2 diff --git a/gazelle/testdata/subdir_sources/BUILD.out b/gazelle/testdata/subdir_sources/BUILD.out new file mode 100644 index 0000000000..d03a8f05ac --- /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 0000000000..79ca3a2c20 --- /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 0000000000..faff6af87a --- /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 0000000000..3cc8834990 --- /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 0000000000..e69de29bb2 diff --git a/gazelle/testdata/subdir_sources/foo/BUILD.out b/gazelle/testdata/subdir_sources/foo/BUILD.out new file mode 100644 index 0000000000..f99857dc52 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..6752f22f90 --- /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 0000000000..e69de29bb2 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 0000000000..0ef0cc12e6 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..e69de29bb2 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 0000000000..79bd70a258 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..e69de29bb2 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 0000000000..ce59ee263e --- /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 0000000000..6b58ff30a8 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..e69de29bb2 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 0000000000..265c08bd57 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..e69de29bb2 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 0000000000..80739d9a3f --- /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 0000000000..6b58ff30a8 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..e69de29bb2 diff --git a/gazelle/testdata/subdir_sources/one/BUILD.out b/gazelle/testdata/subdir_sources/one/BUILD.out new file mode 100644 index 0000000000..f2e57456ca --- /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 0000000000..6b58ff30a8 --- /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 0000000000..e69de29bb2 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 0000000000..f632eedcf3 --- /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 0000000000..f6c7d2a988 --- /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 0000000000..6b58ff30a8 --- /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 0000000000..ed97d539c0 --- /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 0000000000..e69de29bb2 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 0000000000..bb2f34db55 --- /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 0000000000..7213b34565 --- /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 0000000000..faff6af87a --- /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 0000000000..6871953f88 --- /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 0000000000..7288b798e1 --- /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 0000000000..ed97d539c0 --- /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 0000000000..e69de29bb2 diff --git a/gazelle/testdata/with_std_requirements/BUILD.out b/gazelle/testdata/with_std_requirements/BUILD.out new file mode 100644 index 0000000000..a382ca88c2 --- /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 0000000000..4eaf1b04c2 --- /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 0000000000..faff6af87a --- /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 0000000000..154689a5f4 --- /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 0000000000..ed97d539c0 --- /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 0000000000..e69de29bb2 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 0000000000..9854730a2e --- /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 0000000000..b47101c8f8 --- /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 0000000000..faff6af87a --- /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 0000000000..6b58ff30a8 --- /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 0000000000..fe551aa423 --- /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 0000000000..19ddd97a87 --- /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 0000000000..29a1f3b612 --- /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 0000000000..76bb8bfa7b --- /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 0000000000..ed97d539c0 --- /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 0000000000..ec4e78e081 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +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/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 0000000000..96af7e53a0 --- /dev/null +++ b/go.sum @@ -0,0 +1,59 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +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 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +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 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= +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 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +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 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.4.1 h1:Kvvh58BN8Y9/lBi7hTekvtMpm07eUZ0ck5pRHpsMWrY= +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 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +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 h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= +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 h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +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 f8b880a90b..72b0f94810 100644 --- a/internal_deps.bzl +++ b/internal_deps.bzl @@ -35,6 +35,29 @@ def rules_python_internal_deps(): 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 8609915485..9523f75d46 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 @@ load( _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 @@ def rules_python_internal_setup(): # 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()