feat(py_wheel): Add support for specifying Project-URL in METADATA (#1276) `Project-URL` is a field available in core metadata since version 1.2, which allows specifying additional URLs and display as Project Links in PyPI package web page. https://packaging.python.org/en/latest/specifications/core-metadata/#project-url-multiple-use This change adds the support to specify that.
diff --git a/docs/packaging.md b/docs/packaging.md index 091e1a0..0e8e110 100755 --- a/docs/packaging.md +++ b/docs/packaging.md
@@ -59,8 +59,8 @@ <pre> py_wheel_rule(<a href="#py_wheel_rule-name">name</a>, <a href="#py_wheel_rule-abi">abi</a>, <a href="#py_wheel_rule-author">author</a>, <a href="#py_wheel_rule-author_email">author_email</a>, <a href="#py_wheel_rule-classifiers">classifiers</a>, <a href="#py_wheel_rule-console_scripts">console_scripts</a>, <a href="#py_wheel_rule-deps">deps</a>, <a href="#py_wheel_rule-description_content_type">description_content_type</a>, <a href="#py_wheel_rule-description_file">description_file</a>, <a href="#py_wheel_rule-distribution">distribution</a>, <a href="#py_wheel_rule-entry_points">entry_points</a>, - <a href="#py_wheel_rule-extra_distinfo_files">extra_distinfo_files</a>, <a href="#py_wheel_rule-extra_requires">extra_requires</a>, <a href="#py_wheel_rule-homepage">homepage</a>, <a href="#py_wheel_rule-license">license</a>, <a href="#py_wheel_rule-platform">platform</a>, <a href="#py_wheel_rule-python_requires">python_requires</a>, - <a href="#py_wheel_rule-python_tag">python_tag</a>, <a href="#py_wheel_rule-requires">requires</a>, <a href="#py_wheel_rule-stamp">stamp</a>, <a href="#py_wheel_rule-strip_path_prefixes">strip_path_prefixes</a>, <a href="#py_wheel_rule-summary">summary</a>, <a href="#py_wheel_rule-version">version</a>) + <a href="#py_wheel_rule-extra_distinfo_files">extra_distinfo_files</a>, <a href="#py_wheel_rule-extra_requires">extra_requires</a>, <a href="#py_wheel_rule-homepage">homepage</a>, <a href="#py_wheel_rule-license">license</a>, <a href="#py_wheel_rule-platform">platform</a>, <a href="#py_wheel_rule-project_urls">project_urls</a>, + <a href="#py_wheel_rule-python_requires">python_requires</a>, <a href="#py_wheel_rule-python_tag">python_tag</a>, <a href="#py_wheel_rule-requires">requires</a>, <a href="#py_wheel_rule-stamp">stamp</a>, <a href="#py_wheel_rule-strip_path_prefixes">strip_path_prefixes</a>, <a href="#py_wheel_rule-summary">summary</a>, <a href="#py_wheel_rule-version">version</a>) </pre> Internal rule used by the [py_wheel macro](/docs/packaging.md#py_wheel). @@ -91,6 +91,7 @@ | <a id="py_wheel_rule-homepage"></a>homepage | A string specifying the URL for the package homepage. | String | optional | <code>""</code> | | <a id="py_wheel_rule-license"></a>license | A string specifying the license of the package. | String | optional | <code>""</code> | | <a id="py_wheel_rule-platform"></a>platform | Supported platform. Use 'any' for pure-Python wheel.<br><br>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:<br><br><code> platform = select({ "//platforms:windows_x86_64": "win_amd64", "//platforms:macos_x86_64": "macosx_10_7_x86_64", "//platforms:linux_x86_64": "manylinux2014_x86_64", }) </code> | String | optional | <code>"any"</code> | +| <a id="py_wheel_rule-project_urls"></a>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 <code>{{"Bug Tracker": "http://bitbucket.org/tarek/distribute/issues/"}}</code> | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | optional | <code>{}</code> | | <a id="py_wheel_rule-python_requires"></a>python_requires | Python versions required by this distribution, e.g. '>=3.5,<3.7' | String | optional | <code>""</code> | | <a id="py_wheel_rule-python_tag"></a>python_tag | Supported Python version(s), eg <code>py3</code>, <code>cp35.cp36</code>, etc | String | optional | <code>"py3"</code> | | <a id="py_wheel_rule-requires"></a>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 | <code>[]</code> |
diff --git a/examples/wheel/BUILD.bazel b/examples/wheel/BUILD.bazel index 72cc3d4..f56a41b 100644 --- a/examples/wheel/BUILD.bazel +++ b/examples/wheel/BUILD.bazel
@@ -157,6 +157,10 @@ }, 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 6869e77..f51a0ec 100644 --- a/examples/wheel/wheel_test.py +++ b/examples/wheel/wheel_test.py
@@ -99,7 +99,7 @@ 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 @@ 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 edafa3e..d8bceab 100644 --- a/python/private/py_wheel.bzl +++ b/python/private/py_wheel.bzl
@@ -176,6 +176,11 @@ 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 @@ ), } +_PROJECT_URL_LABEL_LENGTH_LIMIT = 32 _DESCRIPTION_FILE_EXTENSION_TO_TYPE = { "md": "text/markdown", "rst": "text/x-rst", @@ -301,6 +307,11 @@ 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 0000000..d925bb9 --- /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 0000000..4408592 --- /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), + )