feat: wheel publishing (#1015)

feat: add a .publish target to py_wheel macro
diff --git a/docs/packaging.md b/docs/packaging.md
index a7a65ab..b244b42 100755
--- a/docs/packaging.md
+++ b/docs/packaging.md
@@ -121,7 +121,7 @@
 ## py_wheel
 
 <pre>
-py_wheel(<a href="#py_wheel-name">name</a>, <a href="#py_wheel-kwargs">kwargs</a>)
+py_wheel(<a href="#py_wheel-name">name</a>, <a href="#py_wheel-twine">twine</a>, <a href="#py_wheel-kwargs">kwargs</a>)
 </pre>
 
 Builds a Python Wheel.
@@ -168,6 +168,31 @@
 )
 ```
 
+To publish the wheel to Pypi, the twine package is required.
+rules_python doesn't provide twine itself, see https://github.com/bazelbuild/rules_python/issues/1016
+However you can install it with pip_parse, just like we do in the WORKSPACE file in rules_python.
+
+Once you've installed twine, you can pass its label to the `twine` attribute of this macro,
+to get a "[name].publish" target.
+
+Example:
+
+```python
+py_wheel(
+    name = "my_wheel",
+    twine = "@publish_deps_twine//:pkg",
+    ...
+)
+```
+
+Now you can run a command like the following, which publishes to https://test.pypi.org/
+
+```sh
+% TWINE_USERNAME=__token__ TWINE_PASSWORD=pypi-*** \
+    bazel run --stamp --embed_label=1.2.4 -- \
+    //path/to:my_wheel.publish --repository testpypi
+```
+
 
 **PARAMETERS**
 
@@ -175,6 +200,7 @@
 | Name  | Description | Default Value |
 | :------------- | :------------- | :------------- |
 | <a id="py_wheel-name"></a>name |  A unique name for this target.   |  none |
+| <a id="py_wheel-twine"></a>twine |  A label of the external location of the py_library target for twine   |  <code>None</code> |
 | <a id="py_wheel-kwargs"></a>kwargs |  other named parameters passed to the underlying [py_wheel rule](#py_wheel_rule)   |  none |
 
 
diff --git a/python/packaging.bzl b/python/packaging.bzl
index 9274579..1984eb7 100644
--- a/python/packaging.bzl
+++ b/python/packaging.bzl
@@ -68,7 +68,7 @@
     },
 )
 
-def py_wheel(name, **kwargs):
+def py_wheel(name, twine = None, **kwargs):
     """Builds a Python Wheel.
 
     Wheels are Python distribution format defined in https://www.python.org/dev/peps/pep-0427/.
@@ -113,16 +113,63 @@
     )
     ```
 
+    To publish the wheel to Pypi, the twine package is required.
+    rules_python doesn't provide twine itself, see https://github.com/bazelbuild/rules_python/issues/1016
+    However you can install it with pip_parse, just like we do in the WORKSPACE file in rules_python.
+
+    Once you've installed twine, you can pass its label to the `twine` attribute of this macro,
+    to get a "[name].publish" target.
+
+    Example:
+
+    ```python
+    py_wheel(
+        name = "my_wheel",
+        twine = "@publish_deps_twine//:pkg",
+        ...
+    )
+    ```
+
+    Now you can run a command like the following, which publishes to https://test.pypi.org/
+
+    ```sh
+    % TWINE_USERNAME=__token__ TWINE_PASSWORD=pypi-*** \\
+        bazel run --stamp --embed_label=1.2.4 -- \\
+        //path/to:my_wheel.publish --repository testpypi
+    ```
+
     Args:
         name:  A unique name for this target.
+        twine: A label of the external location of the py_library target for twine
         **kwargs: other named parameters passed to the underlying [py_wheel rule](#py_wheel_rule)
     """
+    _dist_target = "{}.dist".format(name)
     py_wheel_dist(
-        name = "{}.dist".format(name),
+        name = _dist_target,
         wheel = name,
         out = kwargs.pop("dist_folder", "{}_dist".format(name)),
     )
 
     _py_wheel(name = name, **kwargs)
 
+    if twine:
+        if not twine.endswith(":pkg"):
+            fail("twine label should look like @my_twine_repo//:pkg")
+        twine_main = twine.replace(":pkg", ":rules_python_wheel_entry_point_twine.py")
+
+        # TODO: use py_binary from //python:defs.bzl after our stardoc setup is less brittle
+        # buildifier: disable=native-py
+        native.py_binary(
+            name = "{}.publish".format(name),
+            srcs = [twine_main],
+            args = [
+                "upload",
+                "$(rootpath :{})/*".format(_dist_target),
+            ],
+            data = [_dist_target],
+            imports = ["."],
+            main = twine_main,
+            deps = [twine],
+        )
+
 py_wheel_rule = _py_wheel
diff --git a/python/runfiles/BUILD.bazel b/python/runfiles/BUILD.bazel
index 19d7804..3a93d40 100644
--- a/python/runfiles/BUILD.bazel
+++ b/python/runfiles/BUILD.bazel
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-load("//python:defs.bzl", "py_binary", "py_library")
+load("//python:defs.bzl", "py_library")
 load("//python:packaging.bzl", "py_wheel")
 
 filegroup(
@@ -45,26 +45,9 @@
     distribution = "bazel_runfiles",
     homepage = "https://github.com/bazelbuild/rules_python",
     strip_path_prefixes = ["python"],
+    twine = "@publish_deps_twine//:pkg",
     # this can be replaced by building with --stamp --embed_label=1.2.3
     version = "{BUILD_EMBED_LABEL}",
     visibility = ["//visibility:public"],
     deps = [":runfiles"],
 )
-
-# TODO(alexeagle): carry forward #1015 to make this part of the py_wheel macro
-# Typical command-line to run this:
-# TWINE_USERNAME=__token__ TWINE_PASSWORD=pypi-*** \
-#    bazel run --stamp --embed_label=1.2.4 -- \
-#    //python/runfiles:wheel.publish --repository testpypi
-py_binary(
-    name = "wheel.publish",
-    srcs = ["@publish_deps_twine//:rules_python_wheel_entry_point_twine.py"],
-    args = [
-        "upload",
-        "$(rootpath :wheel.dist)/*",
-    ],
-    data = [":wheel.dist"],
-    imports = ["."],
-    main = "@publish_deps_twine//:rules_python_wheel_entry_point_twine.py",
-    deps = ["@publish_deps_twine//:pkg"],
-)