Escape characters in the wheel filename. (#518)

Fixes #517.
diff --git a/examples/wheel/BUILD b/examples/wheel/BUILD
index a404540..e60fd11 100644
--- a/examples/wheel/BUILD
+++ b/examples/wheel/BUILD
@@ -189,6 +189,17 @@
     version = "0.0.1",
 )
 
+py_wheel(
+    name = "filename_escaping",
+    # Per https://www.python.org/dev/peps/pep-0427/#escaping-and-unicode
+    # runs of non-alphanumeric, non-digit symbols should be replaced with a single underscore.
+    # Unicode non-ascii letters should *not* be replaced with underscore.
+    distribution = "file~~name-escaping",
+    python_tag = "py3",
+    version = "0.0.1-r7",
+    deps = [":example_pkg"],
+)
+
 py_test(
     name = "wheel_test",
     srcs = ["wheel_test.py"],
@@ -197,6 +208,7 @@
         ":custom_package_root_multi_prefix",
         ":custom_package_root_multi_prefix_reverse_order",
         ":customized",
+        ":filename_escaping",
         ":minimal_with_py_library",
         ":minimal_with_py_package",
         ":python_abi3_binary_wheel",
diff --git a/examples/wheel/wheel_test.py b/examples/wheel/wheel_test.py
index 074cac9..e1d7b18 100644
--- a/examples/wheel/wheel_test.py
+++ b/examples/wheel/wheel_test.py
@@ -112,6 +112,34 @@
 first = first.main:f
 second = second.main:s""")
 
+    def test_filename_escaping(self):
+        filename = os.path.join(os.environ['TEST_SRCDIR'],
+                                'rules_python',
+                                'examples', 'wheel',
+                                'file_name_escaping-0.0.1_r7-py3-none-any.whl')
+        with zipfile.ZipFile(filename) as zf:
+            self.assertEquals(
+                zf.namelist(),
+                ['examples/wheel/lib/data.txt',
+                 'examples/wheel/lib/module_with_data.py',
+                 'examples/wheel/lib/simple_module.py',
+                 'examples/wheel/main.py',
+                 # PEP calls for replacing only in the archive filename.
+                 # Alas setuptools also escapes in the dist-info directory
+                 # name, so let's be compatible.
+                 'file_name_escaping-0.0.1_r7.dist-info/WHEEL',
+                 'file_name_escaping-0.0.1_r7.dist-info/METADATA',
+                 'file_name_escaping-0.0.1_r7.dist-info/RECORD'])
+            metadata_contents = zf.read(
+                'file_name_escaping-0.0.1_r7.dist-info/METADATA')
+            self.assertEquals(metadata_contents, b"""\
+Metadata-Version: 2.1
+Name: file~~name-escaping
+Version: 0.0.1-r7
+
+UNKNOWN
+""")
+
     def test_custom_package_root_wheel(self):
         filename = os.path.join(os.environ['TEST_SRCDIR'],
                                 'rules_python',
diff --git a/python/packaging.bzl b/python/packaging.bzl
index bfb6d90..5eac83a 100644
--- a/python/packaging.bzl
+++ b/python/packaging.bzl
@@ -83,13 +83,31 @@
     },
 )
 
+def _escape_filename_segment(segment):
+    """Escape a segment of the wheel filename.
+
+    See https://www.python.org/dev/peps/pep-0427/#escaping-and-unicode
+    """
+
+    # TODO: this is wrong, isalnum replaces non-ascii letters, while we should
+    # not replace them.
+    # TODO: replace this with a regexp once starlark supports them.
+    escaped = ""
+    for character in segment.elems():
+        # isalnum doesn't handle unicode characters properly.
+        if character.isalnum() or character == ".":
+            escaped += character
+        elif not escaped.endswith("_"):
+            escaped += "_"
+    return escaped
+
 def _py_wheel_impl(ctx):
     outfile = ctx.actions.declare_file("-".join([
-        ctx.attr.distribution,
-        ctx.attr.version,
-        ctx.attr.python_tag,
-        ctx.attr.abi,
-        ctx.attr.platform,
+        _escape_filename_segment(ctx.attr.distribution),
+        _escape_filename_segment(ctx.attr.version),
+        _escape_filename_segment(ctx.attr.python_tag),
+        _escape_filename_segment(ctx.attr.abi),
+        _escape_filename_segment(ctx.attr.platform),
     ]) + ".whl")
 
     inputs_to_package = depset(
diff --git a/tools/wheelmaker.py b/tools/wheelmaker.py
index 418dfdb..050599f 100644
--- a/tools/wheelmaker.py
+++ b/tools/wheelmaker.py
@@ -18,6 +18,7 @@
 import hashlib
 import os
 import os.path
+import re
 import sys
 import zipfile
 
@@ -31,6 +32,11 @@
     return os.path.sep.join(ret)
 
 
+def escape_filename_segment(segment):
+    """Escapes a filename segment per https://www.python.org/dev/peps/pep-0427/#escaping-and-unicode"""
+    return re.sub(r"[^\w\d.]+", "_", segment, re.UNICODE)
+
+
 class WheelMaker(object):
     def __init__(self, name, version, build_tag, python_tag, abi, platform,
                  outfile=None, strip_path_prefixes=None):
@@ -43,6 +49,9 @@
         self._outfile = outfile
         self._strip_path_prefixes = strip_path_prefixes if strip_path_prefixes is not None else []
 
+        self._distinfo_dir = (escape_filename_segment(self._name) + '-' +
+                              escape_filename_segment(self._version) +
+                              '.dist-info/')
         self._zipfile = None
         self._record = []
 
@@ -64,14 +73,11 @@
         components += [self._python_tag, self._abi, self._platform]
         return '-'.join(components) + '.whl'
 
-    def distname(self):
-        return self._name + '-' + self._version
-
     def disttags(self):
         return ['-'.join([self._python_tag, self._abi, self._platform])]
 
     def distinfo_path(self, basename):
-        return self.distname() + '.dist-info/' + basename
+        return self._distinfo_dir + basename
 
     def _serialize_digest(self, hash):
         # https://www.python.org/dev/peps/pep-0376/#record