Add py_import rule (#174)

This change adds adds a `py_import` rule to import Python eggs like
`java_import` imports Java jars.

py_import.egg generated using `zipper`[1]:

```console
$ third_party/ijar/zipper Cc examples/py_import/py_import.egg examples/py_import/helloworld.py=examples/helloworld/helloworld.py examples/__init__.py= examples/py_import/__init__.py=
```

Partially addresses bazelbuild/bazel#7312.
Addresses #222.

[1]: https://github.com/bazelbuild/bazel/tree/master/third_party/ijar

Testing Done:
```console
$ bazelisk test --override_repository=rules_python=$PWD/../.. ...
//:py_import_test                                                        PASSED in 0.6s
```
diff --git a/.bazelrc b/.bazelrc
index ddba1f3..6aee0a6 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -3,7 +3,7 @@
 # This lets us glob() up all the files inside the examples to make them inputs to tests
 # (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it)
 # To update these lines, run tools/bazel_integration_test/update_deleted_packages.sh
-build --deleted_packages=examples/legacy_pip_import/boto,examples/legacy_pip_import/extras,examples/legacy_pip_import/helloworld,examples/pip_install,examples/pip_parse
-query --deleted_packages=examples/legacy_pip_import/boto,examples/legacy_pip_import/extras,examples/legacy_pip_import/helloworld,examples/pip_install,examples/pip_parse
+build --deleted_packages=examples/legacy_pip_import/boto,examples/legacy_pip_import/extras,examples/legacy_pip_import/helloworld,examples/pip_install,examples/pip_parse,examples/py_import
+query --deleted_packages=examples/legacy_pip_import/boto,examples/legacy_pip_import/extras,examples/legacy_pip_import/helloworld,examples/pip_install,examples/pip_parse,examples/py_import
 
 test --test_output=errors
diff --git a/examples/BUILD b/examples/BUILD
index 5b798d5..e263c07 100644
--- a/examples/BUILD
+++ b/examples/BUILD
@@ -31,3 +31,8 @@
     name = "pip_parse_example",
     timeout = "long",
 )
+
+bazel_integration_test(
+    name = "py_import_example",
+    timeout = "long",
+)
diff --git a/examples/py_import/BUILD b/examples/py_import/BUILD
new file mode 100644
index 0000000..30ab584
--- /dev/null
+++ b/examples/py_import/BUILD
@@ -0,0 +1,40 @@
+# Copyright 2020 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.
+
+load("@pip//:requirements.bzl", "requirement")
+load("@rules_python//python:defs.bzl", "py_import", "py_test")
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])  # Apache 2.0
+
+# Adapt helloworld.egg so it can be depended on as if it were a
+# py_library target
+py_import(
+    name = "py_import",
+    # Note: this .egg file can be regenerated using zipper:
+    # $ third_party/ijar/zipper Cc \
+    #   examples/py_import/helloworld.egg \
+    #   examples/py_import/helloworld.py=examples/legacy_pip_import/helloworld/helloworld.py \
+    #   examples/__init__.py= \
+    #   examples/py_import/__init__.py=
+    srcs = ["helloworld.egg"],
+    deps = [requirement("futures")],
+)
+
+py_test(
+    name = "py_import_test",
+    srcs = ["py_import_test.py"],
+    deps = [":py_import"],
+)
diff --git a/examples/py_import/WORKSPACE b/examples/py_import/WORKSPACE
new file mode 100644
index 0000000..78aba2e
--- /dev/null
+++ b/examples/py_import/WORKSPACE
@@ -0,0 +1,15 @@
+workspace(name = "py_import")
+
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+# Note, this relies on a pre-release of 0.3.0, and doesn't actually work with 0.2.0
+# Use --override_repository=rules_python=$HOME/Projects/rules_python to test for now
+http_archive(
+    name = "rules_python",
+    sha256 = "778197e26c5fbeb07ac2a2c5ae405b30f6cb7ad1f5510ea6fdac03bded96cc6f",
+    url = "https://github.com/bazelbuild/rules_python/releases/download/0.2.0/rules_python-0.2.0.tar.gz",
+)
+
+load("@rules_python//python:pip.bzl", "pip_install")
+
+pip_install(requirements = "//:requirements.txt")
diff --git a/examples/py_import/helloworld.egg b/examples/py_import/helloworld.egg
new file mode 100644
index 0000000..f41f396
--- /dev/null
+++ b/examples/py_import/helloworld.egg
Binary files differ
diff --git a/examples/py_import/py_import_test.py b/examples/py_import/py_import_test.py
new file mode 100644
index 0000000..1d7212d
--- /dev/null
+++ b/examples/py_import/py_import_test.py
@@ -0,0 +1,41 @@
+# Copyright 2017-2019 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.
+
+import unittest
+
+from examples.py_import import helloworld
+
+
+class HelloWorldTest(unittest.TestCase):
+
+  def test_helloworld(self):
+    hw = helloworld.HelloWorld()
+    hw.SayHello()
+
+  def test_helloworld_async(self):
+    hw = helloworld.HelloWorld()
+    hw.SayHelloAsync()
+    hw.Stop()
+
+  def test_helloworld_multiple(self):
+    hw = helloworld.HelloWorld()
+    hw.SayHelloAsync()
+    hw.SayHelloAsync()
+    hw.SayHelloAsync()
+    hw.SayHelloAsync()
+    hw.Stop()
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/examples/py_import/requirements.txt b/examples/py_import/requirements.txt
new file mode 100644
index 0000000..372420d
--- /dev/null
+++ b/examples/py_import/requirements.txt
@@ -0,0 +1 @@
+futures>=3.1
diff --git a/examples/py_import/some_library.egg b/examples/py_import/some_library.egg
new file mode 100644
index 0000000..f41f396
--- /dev/null
+++ b/examples/py_import/some_library.egg
Binary files differ
diff --git a/python/defs.bzl b/python/defs.bzl
index 890c221..28073d4 100644
--- a/python/defs.bzl
+++ b/python/defs.bzl
@@ -71,6 +71,56 @@
     # buildifier: disable=native-python
     native.py_test(**_add_tags(attrs))
 
+def _py_import_impl(ctx):
+    # See https://github.com/bazelbuild/bazel/blob/0.24.0/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonSemantics.java#L104 .
+    import_paths = [
+        "/".join([ctx.workspace_name, x.short_path])
+        for x in ctx.files.srcs
+    ]
+
+    return [
+        DefaultInfo(
+            default_runfiles = ctx.runfiles(ctx.files.srcs, collect_default = True),
+        ),
+        PyInfo(
+            transitive_sources = depset(transitive = [
+                dep[PyInfo].transitive_sources
+                for dep in ctx.attr.deps
+            ]),
+            imports = depset(direct = import_paths, transitive = [
+                dep[PyInfo].imports
+                for dep in ctx.attr.deps
+            ]),
+        ),
+    ]
+
+py_import = rule(
+    doc = """This rule allows the use of Python packages as dependencies.
+
+    It imports the given `.egg` file(s), which might be checked in source files,
+    fetched externally as with `http_file`, or produced as outputs of other rules.
+
+    It may be used like a `py_library`, in the `deps` of other Python rules.
+
+    This is similar to [java_import](https://docs.bazel.build/versions/master/be/java.html#java_import).
+    """,
+    implementation = _py_import_impl,
+    attrs = {
+        "deps": attr.label_list(
+            doc = "The list of other libraries to be linked in to the " +
+                  "binary target.",
+            providers = [PyInfo],
+        ),
+        "srcs": attr.label_list(
+            doc = "The list of Python package files provided to Python targets " +
+                  "that depend on this target. Note that currently only the .egg " +
+                  "format is accepted. For .whl files, try the whl_library rule. " +
+                  "We accept contributions to extend py_import to handle .whl.",
+            allow_files = [".egg"],
+        ),
+    },
+)
+
 def py_runtime(**attrs):
     """See the Bazel core [py_runtime](https://docs.bazel.build/versions/master/be/python.html#py_runtime) documentation.
 
diff --git a/tools/bazel_integration_test/bazel_integration_test.bzl b/tools/bazel_integration_test/bazel_integration_test.bzl
index caac0d9..7fcace6 100644
--- a/tools/bazel_integration_test/bazel_integration_test.bzl
+++ b/tools/bazel_integration_test/bazel_integration_test.bzl
@@ -13,8 +13,8 @@
 It is assumed by the test runner that the bazel binary is found at label_workspace/bazel (wksp/bazel.exe on Windows)""",
     ),
     "bazel_commands": attr.string_list(
-        default = ["info", "test ..."],
-        doc = """The list of bazel commands to run. Defaults to `["info", "test ..."]`.
+        default = ["info", "test --test_output=errors ..."],
+        doc = """The list of bazel commands to run.
 
 Note that if a command contains a bare `--` argument, the --test_arg passed to Bazel will appear before it.
 """,