Skip to content

Commit

Permalink
feat(twine): support 'bzlmod' users out of the box (bazelbuild#1572)
Browse files Browse the repository at this point in the history
Implements a test that starts a [`pypiserver`] and checks
that the publishing with the new machinery still works.

Fixes bazelbuild#1369

[pypiserver]: https://github.com/pypiserver/pypiserver
  • Loading branch information
aignas committed Mar 27, 2024
1 parent c5c03b2 commit e8039db
Show file tree
Hide file tree
Showing 16 changed files with 305 additions and 37 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ A brief description of the categories of changes:
* (wheel) Add support for `data_files` attributes in py_wheel rule
([#1777](https://github.com/bazelbuild/rules_python/issues/1777))

* (py_wheel) `bzlmod` installations now provide a `twine` setup for the default
Python toolchain in `rules_python` for version 3.11.

[0.XX.0]: https://github.com/bazelbuild/rules_python/releases/tag/0.XX.0
[python_default_visibility]: gazelle/README.md#directive-python_default_visibility

Expand Down Expand Up @@ -269,7 +272,6 @@ A brief description of the categories of changes:
attribute for every target in the package. This is enabled through a separate
directive `python_generation_mode_per_file_include_init`.


## [0.27.0] - 2023-11-16

[0.27.0]: https://github.com/bazelbuild/rules_python/releases/tag/0.27.0
Expand Down
33 changes: 17 additions & 16 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,24 @@ python.toolchain(
is_default = True,
python_version = "3.11",
)
use_repo(python, "pythons_hub")
use_repo(python, "python_versions", "pythons_hub")

# This call registers the Python toolchains.
register_toolchains("@pythons_hub//:all")

#####################
# Install twine for our own runfiles wheel publishing and allow bzlmod users to use it.

pip = use_extension("//python/extensions:pip.bzl", "pip")
pip.parse(
hub_name = "rules_python_publish_deps",
python_version = "3.11",
requirements_darwin = "//tools/publish:requirements_darwin.txt",
requirements_lock = "//tools/publish:requirements.txt",
requirements_windows = "//tools/publish:requirements_windows.txt",
)
use_repo(pip, "rules_python_publish_deps")

# ===== DEV ONLY DEPS AND SETUP BELOW HERE =====
bazel_dep(name = "stardoc", version = "0.6.2", dev_dependency = True, repo_name = "io_bazel_stardoc")
bazel_dep(name = "rules_bazel_integration_test", version = "0.20.0", dev_dependency = True)
Expand Down Expand Up @@ -84,24 +97,12 @@ dev_pip.parse(
python_version = "3.11",
requirements_lock = "//docs/sphinx:requirements.txt",
)

#####################
# Install twine for our own runfiles wheel publishing.
# Eventually we might want to install twine automatically for users too, see:
# https://github.com/bazelbuild/rules_python/issues/1016.

dev_pip.parse(
hub_name = "publish_deps",
hub_name = "pypiserver",
python_version = "3.11",
requirements_darwin = "//tools/publish:requirements_darwin.txt",
requirements_lock = "//tools/publish:requirements.txt",
requirements_windows = "//tools/publish:requirements_windows.txt",
)
use_repo(
dev_pip,
"dev_pip",
publish_deps_twine = "publish_deps_311_twine",
requirements_lock = "//examples/wheel:requirements_server.txt",
)
use_repo(dev_pip, "dev_pip", "pypiserver")

# Bazel integration test setup below

Expand Down
14 changes: 12 additions & 2 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -94,17 +94,27 @@ load("@python//3.11.8:defs.bzl", "interpreter")
load("@rules_python//python:pip.bzl", "pip_parse")

pip_parse(
name = "publish_deps",
name = "rules_python_publish_deps",
python_interpreter_target = interpreter,
requirements_darwin = "//tools/publish:requirements_darwin.txt",
requirements_lock = "//tools/publish:requirements.txt",
requirements_windows = "//tools/publish:requirements_windows.txt",
)

load("@publish_deps//:requirements.bzl", "install_deps")
load("@rules_python_publish_deps//:requirements.bzl", "install_deps")

install_deps()

pip_parse(
name = "pypiserver",
python_interpreter_target = interpreter,
requirements_lock = "//examples/wheel:requirements_server.txt",
)

load("@pypiserver//:requirements.bzl", install_pypiserver = "install_deps")

install_pypiserver()

#####################
# Install sphinx for doc generation.

Expand Down
43 changes: 43 additions & 0 deletions examples/wheel/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ load("@bazel_skylib//rules:write_file.bzl", "write_file")
load("//examples/wheel/private:wheel_utils.bzl", "directory_writer", "make_variable_tags")
load("//python:defs.bzl", "py_library", "py_test")
load("//python:packaging.bzl", "py_package", "py_wheel")
load("//python:pip.bzl", "compile_pip_requirements")
load("//python:versions.bzl", "gen_python_config_settings")
load("//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary")
load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility

package(default_visibility = ["//visibility:public"])

Expand Down Expand Up @@ -56,6 +59,10 @@ py_wheel(
# Package data. We're building "example_minimal_library-0.0.1-py3-none-any.whl"
distribution = "example_minimal_library",
python_tag = "py3",
# NOTE: twine_binary = "//tools/publish:twine" does not work on non-bzlmod
# setups because the `//tools/publish:twine` produces multiple files and is
# unsuitable as the `src` to the underlying native_binary rule.
twine = None if BZLMOD_ENABLED else "@rules_python_publish_deps_twine//:pkg",
version = "0.0.1",
deps = [
"//examples/wheel/lib:module_with_data",
Expand Down Expand Up @@ -348,3 +355,39 @@ py_test(
"//python/runfiles",
],
)

# Test wheel publishing

compile_pip_requirements(
name = "requirements_server",
src = "requirements_server.in",
)

py_test(
name = "test_publish",
srcs = ["test_publish.py"],
data = [
":minimal_with_py_library",
":minimal_with_py_library.publish",
":pypiserver",
],
env = {
"PUBLISH_PATH": "$(location :minimal_with_py_library.publish)",
"SERVER_PATH": "$(location :pypiserver)",
"WHEEL_PATH": "$(rootpath :minimal_with_py_library)",
},
target_compatible_with = select({
"@platforms//os:linux": [],
"@platforms//os:macos": [],
"//conditions:default": ["@platforms//:incompatible"],
}),
deps = [
"@pypiserver//pypiserver",
],
)

py_console_script_binary(
name = "pypiserver",
pkg = "@pypiserver//pypiserver",
script = "pypi-server",
)
2 changes: 2 additions & 0 deletions examples/wheel/requirements_server.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# This is for running publishing tests
pypiserver
16 changes: 16 additions & 0 deletions examples/wheel/requirements_server.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# bazel run //examples/wheel:requirements_server.update
#
pypiserver==2.0.1 \
--hash=sha256:1dd98fb99d2da4199fb44c7284e57d69a9f7fda2c6c8dc01975c151c592677bf \
--hash=sha256:7b58fbd54468235f79e4de07c4f7a9ff829e7ac6869bef47ec11e0710138e162
# via -r examples/wheel/requirements_server.in

# The following packages are considered to be unsafe in a requirements file:
pip==24.0 \
--hash=sha256:ba0d021a166865d2265246961bec0152ff124de910c5cc39f1156ce3fa7c69dc \
--hash=sha256:ea9bd1a847e8c5774a5777bb398c19e80bcd4e2aa16a4b301b718fe6f593aba2
# via pypiserver
117 changes: 117 additions & 0 deletions examples/wheel/test_publish.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import os
import socket
import subprocess
import textwrap
import time
import unittest
from contextlib import closing
from pathlib import Path
from urllib.request import urlopen


def find_free_port():
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
s.bind(("", 0))
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
return s.getsockname()[1]


class TestTwineUpload(unittest.TestCase):
def setUp(self):
self.maxDiff = 1000
self.port = find_free_port()
self.url = f"http://localhost:{self.port}"
self.dir = Path(os.environ["TEST_TMPDIR"])

self.log_file = self.dir / "pypiserver-log.txt"
self.log_file.touch()
_storage_dir = self.dir / "data"
for d in [_storage_dir]:
d.mkdir(exist_ok=True)

print("Starting PyPI server...")
self._server = subprocess.Popen(
[
str(Path(os.environ["SERVER_PATH"])),
"run",
"--verbose",
"--log-file",
str(self.log_file),
"--host",
"localhost",
"--port",
str(self.port),
# Allow unauthenticated access
"--authenticate",
".",
"--passwords",
".",
str(_storage_dir),
],
)

line = "Hit Ctrl-C to quit"
interval = 0.1
wait_seconds = 40
for _ in range(int(wait_seconds / interval)): # 40 second timeout
current_logs = self.log_file.read_text()
if line in current_logs:
print(current_logs.strip())
print("...")
break

time.sleep(0.1)
else:
raise RuntimeError(
f"Could not get the server running fast enough, waited for {wait_seconds}s"
)

def tearDown(self):
self._server.terminate()
print(f"Stopped PyPI server, all logs:\n{self.log_file.read_text()}")

def test_upload_and_query_simple_api(self):
# Given
script_path = Path(os.environ["PUBLISH_PATH"])
whl = Path(os.environ["WHEEL_PATH"])

# When I publish a whl to a package registry
subprocess.check_output(
[
str(script_path),
"--no-color",
"upload",
str(whl),
"--verbose",
"--non-interactive",
"--disable-progress-bar",
],
env={
"TWINE_REPOSITORY_URL": self.url,
"TWINE_USERNAME": "dummy",
"TWINE_PASSWORD": "dummy",
},
)

# Then I should be able to get its contents
with urlopen(self.url + "/example-minimal-library/") as response:
got_content = response.read().decode("utf-8")
want_content = """
<!DOCTYPE html>
<html>
<head>
<title>Links for example-minimal-library</title>
</head>
<body>
<h1>Links for example-minimal-library</h1>
<a href="/packages/example_minimal_library-0.0.1-py3-none-any.whl#sha256=79a4e9c1838c0631d5d8fa49a26efd6e9a364f6b38d9597c0f6df112271a0e28">example_minimal_library-0.0.1-py3-none-any.whl</a><br>
</body>
</html>"""
self.assertEqual(
textwrap.dedent(want_content).strip(),
textwrap.dedent(got_content).strip(),
)


if __name__ == "__main__":
unittest.main()
2 changes: 2 additions & 0 deletions python/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,13 @@ bzl_library(
srcs = ["packaging.bzl"],
deps = [
":py_binary_bzl",
"//python/private:bzlmod_enabled_bzl",
"//python/private:py_package.bzl",
"//python/private:py_wheel_bzl",
"//python/private:py_wheel_normalize_pep440.bzl",
"//python/private:stamp_bzl",
"//python/private:util_bzl",
"@bazel_skylib//rules:native_binary",
],
)

Expand Down
Loading

0 comments on commit e8039db

Please sign in to comment.