Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
martinxsliu committed Nov 16, 2020
0 parents commit 962c937
Show file tree
Hide file tree
Showing 35 changed files with 1,272 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
bazel-*

.venv
*.egg-info

# Poetry extracts vendored tarballs into this directory.
tests/multi/app/vendor/unpacked
Empty file added BUILD.bazel
Empty file.
154 changes: 154 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# rules_python_poetry

Bazel rules to install Python dependencies from a [Poetry](https://python-poetry.org/) project.
Works with native Python rules for Bazel.

## Getting started

Add the following to your `WORKSPACE` file:

```py
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

http_archive(
name = "rules_python",
url = "https://github.com/bazelbuild/rules_python/releases/download/0.1.0/rules_python-0.1.0.tar.gz",
sha256 = "b6d46438523a3ec0f3cead544190ee13223a52f6a6765a29eae7b7cc24cc83a0",
)

http_archive(
name = "rules_python_poetry",
url = "TODO",
sha256 = "TODO",
)

load("@rules_python_poetry//:defs.bzl", "poetry_install_toolchain", "poetry_install")

# Optional, if you want to use a specific version of Poetry (1.0.10 is the default).
poetry_install_toolchain(poetry_version = "1.1.4")

poetry_install(
name = "my_deps",
pyproject_toml = "//path/to:pyproject.toml",
poetry_lock = "//path/to:poetry.lock",
dev = True, # Optional
)
```

Under the hood, `poetry_install` uses Poetry to export a `requirements.txt` file which is then passed to [`rule_python`'s `pip_install` repository rule](https://github.com/bazelbuild/rules_python#importing-pip-dependencies).
You can consume dependencies the same way as you would with `pip_install`, e.g.:

```py
load("@my_deps//:requirements.bzl", "requirement")

py_library(
name = "my_lib",
srcs = ["my_lib.py"],
deps = [
":my_other_lib",
requirement("some_pip_dep"),
requirement("another_pip_dep[some_extra]"),
],
)
```

## Poetry dependencies

Poetry allows you to specify dependencies from different types of sources that are not automatically fetched and installed by the `poetry_install` rule. You will have to manually declare these dependencies.

See [`tests/multi/app`](tests/multi/app) for examples.

### Local directory dependency

A dependency on a local directory, for example if you have multiple projects within a monorepo that depend on each other.

```toml
[tool.poetry.dependencies]
foo = {path = "../libs/foo"}
```

If the local dependency has a `py_library` target, you can include it in the `deps` attribute.

```py
py_library(
name = "my_lib",
srcs = ["my_lib.py"],
deps = [
"//path/to/libs:foo",
],
)
```

### Local file dependency

A dependency on a local tarball, for example if you have vendored packages.

```toml
[tool.poetry.dependencies]
foo = {path = "../vendor/foo-1.2.3.tar.gz"}
```

There are some options available.
The first is to extract the archive and vendor the extracted files. Then add a `py_library` that can be included as a `deps`, like the local directory dependency.

The second is to use the `py_archive` repository rule to declare the archive as an external repository in your `WORKSPACE` file, e.g.:

```py
load("@rules_python_poetry//:defs.bzl", "py_archive")

py_archive(
name = "foo",
archive = "//path/to/vendor:foo-1.2.3.tar.gz",
strip_prefix = "foo-1.2.3",
)
```

The `py_archive` rule defines a target named `:py_library` that can be referenced like so:

```py
py_library(
name = "my_lib",
srcs = ["my_lib.py"],
deps = [
"@foo//:py_library",
],
)
```

### URL dependency

A dependency on a remote archive.

```toml
[tool.poetry.dependencies]
foo = {url = "https://example.com/packages/foo-1.2.3.tar.gz"}
```

You can use the `py_archive` repository rule to declare the remote archive as an external repository in your `WORKSPACE` file, e.g.:

```py
load("@rules_python_poetry//:defs.bzl", "py_archive")

py_archive(
name = "foo",
url = "https://example.com/packages/foo-1.2.3.tar.gz",
sha256 = "...",
strip_prefix = "foo-1.2.3",
)
```

The `py_archive` rule defines a target named `:py_library` that can be referenced like so:

```py
py_library(
name = "my_lib",
srcs = ["my_lib.py"],
deps = [
"@foo//:py_library",
],
)
```

### Git dependency

Git dependencies are not currently supported. You can work around this by using a URL dependency instead of a git key.
55 changes: 55 additions & 0 deletions WORKSPACE
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
workspace(name = "rules_python_poetry")

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

http_archive(
name = "rules_python",
url = "https://github.com/bazelbuild/rules_python/releases/download/0.1.0/rules_python-0.1.0.tar.gz",
sha256 = "b6d46438523a3ec0f3cead544190ee13223a52f6a6765a29eae7b7cc24cc83a0",
)

load("//:defs.bzl", "poetry_install_toolchain", "poetry_install", "py_archive")

poetry_install_toolchain(poetry_version = "1.0.10")

# Simple test

poetry_install(
name = "simple_deps",
pyproject_toml = "//tests/simple:pyproject.toml",
poetry_lock = "//tests/simple:poetry.lock",
dev = True,
)

# Multi test

py_archive(
name = "multi_app_requests",
archive = "//tests/multi/app/vendor:requests-2.25.0.tar.gz",
strip_prefix = "requests-2.25.0",
)

py_archive(
name = "multi_app_responses",
url = "https://files.pythonhosted.org/packages/88/98/bf9e777a482ac076a6d75fad7d62b064f535244bf1771c3b2a7d41fd5920/responses-0.12.1.tar.gz",
sha256 = "2e5764325c6b624e42b428688f2111fea166af46623cb0127c05f6afb14d3457",
strip_prefix = "responses-0.12.1",
)

poetry_install(
name = "multi_app_deps",
pyproject_toml = "//tests/multi/app:pyproject.toml",
poetry_lock = "//tests/multi/app:poetry.lock",
)

poetry_install(
name = "multi_liba_deps",
pyproject_toml = "//tests/multi/liba:pyproject.toml",
poetry_lock = "//tests/multi/liba:poetry.lock",
)

poetry_install(
name = "multi_libb_deps",
pyproject_toml = "//tests/multi/libb:pyproject.toml",
poetry_lock = "//tests/multi/libb:poetry.lock",
)
9 changes: 9 additions & 0 deletions defs.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
load("//internal/toolchain:toolchain.bzl", _poetry_install_toolchain = "poetry_install_toolchain")
load("//internal:export.bzl", _poetry_export = "poetry_export")
load("//internal:install.bzl", _poetry_install = "poetry_install")
load("//internal:py_archive.bzl", _py_archive = "py_archive")

poetry_install_toolchain = _poetry_install_toolchain
poetry_export = _poetry_export
poetry_install = _poetry_install
py_archive = _py_archive
Empty file added internal/BUILD.bazel
Empty file.
63 changes: 63 additions & 0 deletions internal/export.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
def _poetry_export_impl(repository_ctx):
poetry_runner_py = repository_ctx.path(Label("@poetry_toolchain//:poetry_runner.py"))
strip_dependencies_py = repository_ctx.path(Label("@poetry_toolchain//:strip_dependencies.py"))

repository_ctx.symlink(repository_ctx.attr.pyproject_toml, repository_ctx.path("pyproject.toml.in"))
repository_ctx.symlink(repository_ctx.attr.poetry_lock, repository_ctx.path("poetry.lock.in"))

for format in ["pyproject.toml", "poetry.lock"]:
result = repository_ctx.execute([
"python",
strip_dependencies_py,
"--file",
repository_ctx.path(format + ".in"),
"--output",
repository_ctx.path(format),
"--format",
format,
])
if result.return_code:
fail("Poetry strip dependencies failed:\n%s\n%s" % (result.stdout, result.stderr))

args = [
"python",
poetry_runner_py,
"export",
"--without-hashes",
"--format",
repository_ctx.attr.format,
"--output",
repository_ctx.path(repository_ctx.attr.format),
]
if repository_ctx.attr.dev:
args.append("--dev")

result = repository_ctx.execute(args)
if result.return_code:
fail("Poetry export to requirements.txt failed:\n%s\n%s" % (result.stdout, result.stderr))

repository_ctx.file("BUILD.bazel")

poetry_export = repository_rule(
implementation = _poetry_export_impl,
attrs = {
"pyproject_toml": attr.label(
mandatory = True,
allow_single_file = True,
doc = "Label of the project's pyproject.toml file",
),
"poetry_lock": attr.label(
mandatory = True,
allow_single_file = True,
doc = "Label of the project's poetry.lock file",
),
"dev": attr.bool(
default = False,
doc = "Include development dependencies",
),
"format": attr.string(
default = "requirements.txt",
doc = "Format to export to. Currently, only requirements.txt is supported.",
),
},
)
21 changes: 21 additions & 0 deletions internal/install.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:pip.bzl", "pip_install")
load("//internal/toolchain:toolchain.bzl", "poetry_install_toolchain")
load("//internal:export.bzl", "poetry_export")

def poetry_install(name, pyproject_toml, poetry_lock, dev = False, **kwargs):
if "poetry_toolchain" not in native.existing_rules().keys():
poetry_install_toolchain()

export_name = name + "_export"
poetry_export(
name = export_name,
pyproject_toml = pyproject_toml,
poetry_lock = poetry_lock,
dev = dev,
)

pip_install(
name = name,
requirements = "@{}//:requirements.txt".format(export_name),
**kwargs,
)
47 changes: 47 additions & 0 deletions internal/py_archive.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
BUILD_TMPL = """\
package(default_visibility = ["//visibility:public"])
load("@rules_python//python:defs.bzl", "py_library")
py_library(
name = "py_library",
srcs = glob(["**/*.py"]),
data = glob(["**/*"], exclude=["**/*.py", "**/* *"]),
)
"""

def _py_archive_impl(repository_ctx):
if repository_ctx.attr.url:
repository_ctx.download_and_extract(
url = repository_ctx.attr.url,
sha256 = repository_ctx.attr.sha256,
stripPrefix = repository_ctx.attr.strip_prefix,
)
elif repository_ctx.attr.archive:
repository_ctx.extract(
archive = repository_ctx.attr.archive,
stripPrefix = repository_ctx.attr.strip_prefix,
)
else:
fail("Either 'url' or 'archive' must be provided.")

repository_ctx.file("BUILD.bazel", BUILD_TMPL)

py_archive = repository_rule(
implementation = _py_archive_impl,
attrs = {
"url": attr.string(
doc = "URL of the Python archive.",
),
"sha256": attr.string(
doc = "SHA256 hash of the downloaded archive file.",
),
"archive": attr.label(
allow_single_file = True,
doc = "Label of the Python archive. Either 'url' or 'archive' must be provided.",
),
"strip_prefix": attr.string(
doc = "A directory prefix to strip from the extracted files.",
),
},
)
Empty file added internal/toolchain/BUILD.bazel
Empty file.
18 changes: 18 additions & 0 deletions internal/toolchain/poetry_runner.py.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Adapted from https://github.com/python-poetry/poetry/blob/master/get-poetry.py

import sys
import os

lib = os.path.normpath(os.path.join(os.path.realpath(__file__), "..", "src"))
vendors = os.path.join(lib, "poetry", "_vendor")
current_vendors = os.path.join(
vendors, "py{}".format(".".join(str(v) for v in sys.version_info[:2]))
)

sys.path.insert(0, lib)
sys.path.insert(0, current_vendors)

if __name__ == "__main__":
from poetry.console import main
main()
Loading

0 comments on commit 962c937

Please sign in to comment.