diff --git a/docs/packaging.md b/docs/packaging.md index 091e1a00fc..0e8e110ef5 100755 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -59,8 +59,8 @@ This also has the advantage that stamping information is included in the wheel's
 py_wheel_rule(name, abi, author, author_email, classifiers, console_scripts, deps,
               description_content_type, description_file, distribution, entry_points,
-              extra_distinfo_files, extra_requires, homepage, license, platform, python_requires,
-              python_tag, requires, stamp, strip_path_prefixes, summary, version)
+              extra_distinfo_files, extra_requires, homepage, license, platform, project_urls,
+              python_requires, python_tag, requires, stamp, strip_path_prefixes, summary, version)
 
Internal rule used by the [py_wheel macro](/docs/packaging.md#py_wheel). @@ -91,6 +91,7 @@ in the way they expect. | homepage | A string specifying the URL for the package homepage. | String | optional | "" | | license | A string specifying the license of the package. | String | optional | "" | | platform | Supported platform. Use 'any' for pure-Python wheel.

If you have included platform-specific data, such as a .pyd or .so extension module, you will need to specify the platform in standard pip format. If you support multiple platforms, you can define platform constraints, then use a select() to specify the appropriate specifier, eg:

platform = select({ "//platforms:windows_x86_64": "win_amd64", "//platforms:macos_x86_64": "macosx_10_7_x86_64", "//platforms:linux_x86_64": "manylinux2014_x86_64", }) | String | optional | "any" | +| project_urls | A string dict specifying additional browsable URLs for the project and corresponding labels, where label is the key and url is the value. e.g {{"Bug Tracker": "http://bitbucket.org/tarek/distribute/issues/"}} | Dictionary: String -> String | optional | {} | | python_requires | Python versions required by this distribution, e.g. '>=3.5,<3.7' | String | optional | "" | | python_tag | Supported Python version(s), eg py3, cp35.cp36, etc | String | optional | "py3" | | requires | List of requirements for this package. See the section on [Declaring required dependency](https://setuptools.readthedocs.io/en/latest/userguide/dependency_management.html#declaring-dependencies) for details and examples of the format of this argument. | List of strings | optional | [] | diff --git a/examples/wheel/BUILD.bazel b/examples/wheel/BUILD.bazel index 72cc3d4e8d..f56a41b370 100644 --- a/examples/wheel/BUILD.bazel +++ b/examples/wheel/BUILD.bazel @@ -157,6 +157,10 @@ py_wheel( }, homepage = "www.example.com", license = "Apache 2.0", + project_urls = { + "Bug Tracker": "www.example.com/issues", + "Documentation": "www.example.com/docs", + }, python_tag = "py3", # Requirements embedded into the wheel metadata. requires = ["pytest"], diff --git a/examples/wheel/wheel_test.py b/examples/wheel/wheel_test.py index 6869e77be2..f51a0ecedc 100644 --- a/examples/wheel/wheel_test.py +++ b/examples/wheel/wheel_test.py @@ -99,7 +99,7 @@ def test_customized_wheel(self): record_contents, # The entries are guaranteed to be sorted. b"""\ -example_customized-0.0.1.dist-info/METADATA,sha256=vRiyyV45PC5fzK_40nSTtIn3yYzDdsbBAbUvkZiRyc8,461 +example_customized-0.0.1.dist-info/METADATA,sha256=QYQcDJFQSIqan8eiXqL67bqsUfgEAwf2hoK_Lgi1S-0,559 example_customized-0.0.1.dist-info/NOTICE,sha256=Xpdw-FXET1IRgZ_wTkx1YQfo1-alET0FVf6V1LXO4js,76 example_customized-0.0.1.dist-info/README,sha256=WmOFwZ3Jga1bHG3JiGRsUheb4UbLffUxyTdHczS27-o,40 example_customized-0.0.1.dist-info/RECORD,, @@ -131,6 +131,8 @@ def test_customized_wheel(self): License: Apache 2.0 Description-Content-Type: text/markdown Summary: A one-line summary of this test package +Project-URL: Bug Tracker, www.example.com/issues +Project-URL: Documentation, www.example.com/docs Classifier: License :: OSI Approved :: Apache Software License Classifier: Intended Audience :: Developers Requires-Dist: pytest diff --git a/python/private/py_wheel.bzl b/python/private/py_wheel.bzl index edafa3e402..d8bceabcb8 100644 --- a/python/private/py_wheel.bzl +++ b/python/private/py_wheel.bzl @@ -176,6 +176,11 @@ _other_attrs = { doc = "A string specifying the license of the package.", default = "", ), + "project_urls": attr.string_dict( + doc = ("A string dict specifying additional browsable URLs for the project and corresponding labels, " + + "where label is the key and url is the value. " + + 'e.g `{{"Bug Tracker": "http://bitbucket.org/tarek/distribute/issues/"}}`'), + ), "python_requires": attr.string( doc = ( "Python versions required by this distribution, e.g. '>=3.5,<3.7'" @@ -191,6 +196,7 @@ _other_attrs = { ), } +_PROJECT_URL_LABEL_LENGTH_LIMIT = 32 _DESCRIPTION_FILE_EXTENSION_TO_TYPE = { "md": "text/markdown", "rst": "text/x-rst", @@ -301,6 +307,11 @@ def _py_wheel_impl(ctx): if ctx.attr.summary: metadata_contents.append("Summary: %s" % ctx.attr.summary) + for label, url in sorted(ctx.attr.project_urls.items()): + if len(label) > _PROJECT_URL_LABEL_LENGTH_LIMIT: + fail("`label` {} in `project_urls` is too long. It is limited to {} characters.".format(len(label), _PROJECT_URL_LABEL_LENGTH_LIMIT)) + metadata_contents.append("Project-URL: %s, %s" % (label, url)) + for c in ctx.attr.classifiers: metadata_contents.append("Classifier: %s" % c) diff --git a/tools/build_defs/python/tests/py_wheel/BUILD.bazel b/tools/build_defs/python/tests/py_wheel/BUILD.bazel new file mode 100644 index 0000000000..d925bb9801 --- /dev/null +++ b/tools/build_defs/python/tests/py_wheel/BUILD.bazel @@ -0,0 +1,18 @@ +# Copyright 2023 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. +"""Tests for py_wheel.""" + +load(":py_wheel_tests.bzl", "py_wheel_test_suite") + +py_wheel_test_suite(name = "py_wheel_tests") diff --git a/tools/build_defs/python/tests/py_wheel/py_wheel_tests.bzl b/tools/build_defs/python/tests/py_wheel/py_wheel_tests.bzl new file mode 100644 index 0000000000..4408592d32 --- /dev/null +++ b/tools/build_defs/python/tests/py_wheel/py_wheel_tests.bzl @@ -0,0 +1,39 @@ +"""Test for py_wheel.""" + +load("@rules_testing//lib:analysis_test.bzl", "analysis_test") +load("@rules_testing//lib:truth.bzl", "matching") +load("@rules_testing//lib:util.bzl", rt_util = "util") +load("//python:packaging.bzl", "py_wheel") +load("//tools/build_defs/python/tests:util.bzl", pt_util = "util") + +_tests = [] + +def _test_too_long_project_url_label(name, config): + rt_util.helper_target( + config.rule, + name = name + "_wheel", + distribution = name + "_wheel", + python_tag = "py3", + version = "0.0.1", + project_urls = {"This is a label whose length is above the limit!": "www.example.com"}, + ) + analysis_test( + name = name, + target = name + "_wheel", + impl = _test_too_long_project_url_label_impl, + expect_failure = True, + ) + +def _test_too_long_project_url_label_impl(env, target): + env.expect.that_target(target).failures().contains_predicate( + matching.str_matches("in `project_urls` is too long"), + ) + +_tests.append(_test_too_long_project_url_label) + +def py_wheel_test_suite(name): + config = struct(rule = py_wheel, base_test_rule = py_wheel) + native.test_suite( + name = name, + tests = pt_util.create_tests(_tests, config = config), + )