Upgrade python/uritemplates to 3.0.1 am: eab8cbc70c am: 5492dc4462 am: 8bbe7cd57d

Change-Id: I96fe751f9f4633adfdf4610a60536a5f2105ab92
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 0000000..6f9a0eb
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,2 @@
+Ian Stapleton Cordasco <graffatcolmingov@gmail.com> Ian Cordasco <graffatcolmingov@gmail.com>
+Ian Stapleton Cordasco <graffatcolmingov@gmail.com> Ian Cordasco <sigmavirus24@users.noreply.github.com>
diff --git a/.travis.yml b/.travis.yml
index b7c9b48..ec4291e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,16 +3,18 @@
 
 matrix:
   include:
-    - python: 2.6
-      env: TOXENV=py26
     - python: 2.7
       env: TOXENV=py27
-    - python: 3.3
-      env: TOXENV=py33
     - python: 3.4
       env: TOXENV=py34
     - python: 3.5
       env: TOXENV=py35
+    - python: 3.6
+      env: TOXENV=py36
+    - python: 3.7
+      env: TOXENV=py37
+    - python: 3.8
+      env: TOXENV=py38
     - env: TOXENV=pep8
 
 install:
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 55848f0..2581f4d 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -1,4 +1,15 @@
 Development Lead
 ----------------
 
-- Ian Cordasco <graffatcolmingov@gmail.com>
+- Ian Stapleton Cordasco <graffatcolmingov@gmail.com>
+
+Contributors
+------------
+
+- Brett Cannon
+- Daniel Imhoff
+- Eugene Eeo
+- Jeff Potter
+- Philippe Ombredanne
+- Thierry Bastian
+- Thomas Grainger
diff --git a/HISTORY.rst b/HISTORY.rst
index f04a309..3a84017 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -1,6 +1,19 @@
 Changelog - uritemplate
 =======================
 
+3.0.1 - 2019-12-19
+------------------
+
+- Update to Python 3.6, 3.7, and 3.8
+- Drop support for Python 2.6, 3.2, and 3.3
+- Ignore ``None`` in list argument expansion
+- Handle a list with an empty string appropriately
+
+3.0.0 - 2016-08-29
+------------------
+
+- Match major version number of uritemplate.py
+
 2.0.0 - 2016-08-29
 ------------------
 
@@ -42,7 +55,7 @@
 0.3.0 - 2013-10-22
 ------------------
 
-- Add ``#partial`` to partially expand templates and return new instances of 
+- Add ``#partial`` to partially expand templates and return new instances of
   ``URITemplate``.
 
 0.2.0 - 2013-07-26
diff --git a/LICENSE b/LICENSE
index 26e92ad..41c87e7 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,3 +1,3 @@
 This software is made available under the terms of *either* of the licenses
-found in LICENSE.APACHE or LICENSE.BSD. Contributions to uritemplate.py are
+found in LICENSE.APACHE or LICENSE.BSD. Contributions to uritemplate are
 made under the terms of *both* these licenses.
diff --git a/METADATA b/METADATA
index b2e2d6c..b41e13f 100644
--- a/METADATA
+++ b/METADATA
@@ -1,7 +1,5 @@
 name: "uritemplate"
-description:
-    "Simple python library to deal with URI Templates."
-
+description: "Simple python library to deal with URI Templates."
 third_party {
   url {
     type: HOMEPAGE
@@ -11,6 +9,10 @@
     type: GIT
     value: "https://github.com/python-hyper/uritemplate"
   }
-  version: "3.0.0"
-  last_upgrade_date { year: 2018 month: 6 day: 4 }
+  version: "3.0.1"
+  last_upgrade_date {
+    year: 2019
+    month: 12
+    day: 23
+  }
 }
diff --git a/README.rst b/README.rst
index 2780d7b..2c9d98d 100644
--- a/README.rst
+++ b/README.rst
@@ -1,7 +1,7 @@
 uritemplate
 ===========
 
-Documentation_ -- GitHub_ -- BitBucket_ -- Travis-CI_
+Documentation_ -- GitHub_ -- Travis-CI_
 
 Simple python library to deal with `URI Templates`_. The API looks like
 
@@ -48,7 +48,7 @@
 
 ::
 
-    pip install uritemplate.py
+    pip install uritemplate
 
 License
 -------
@@ -56,9 +56,8 @@
 Modified BSD license_
 
 
-.. _Documentation: http://uritemplate.rtfd.org/
-.. _GitHub: https://github.com/sigmavirus24/uritemplate
-.. _BitBucket: https://bitbucket.org/icordasc/uritemplate
-.. _Travis-CI: https://travis-ci.org/sigmavirus24/uritemplate
+.. _Documentation: https://uritemplate.readthedocs.io/
+.. _GitHub: https://github.com/python-hyper/uritemplate
+.. _Travis-CI: https://travis-ci.org/python-hyper/uritemplate
 .. _URI Templates: http://tools.ietf.org/html/rfc6570
-.. _license: https://github.com/sigmavirus24/uritemplate/blob/master/LICENSE
+.. _license: https://github.com/python-hyper/uritemplate/blob/master/LICENSE
diff --git a/old/uritemplate.py/uritemplatepy-setup.py b/old/uritemplate.py/uritemplatepy-setup.py
index 430d519..ebececb 100644
--- a/old/uritemplate.py/uritemplatepy-setup.py
+++ b/old/uritemplate.py/uritemplatepy-setup.py
@@ -11,6 +11,7 @@
     author_email="graffatcolmingov@gmail.com",
     url="https://uritemplate.readthedocs.org",
     install_requires=["uritemplate>=2.0"],
+    python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
     classifiers=[
         'Development Status :: 5 - Production/Stable',
         'License :: OSI Approved',
@@ -19,13 +20,11 @@
         'Intended Audience :: Developers',
         'Programming Language :: Python',
         'Programming Language :: Python :: 2',
-        'Programming Language :: Python :: 2.6',
         'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.2',
-        'Programming Language :: Python :: 3.3',
         'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: Implementation :: CPython',
     ],
 )
diff --git a/setup.py b/setup.py
index 59f03ba..7a09f5e 100644
--- a/setup.py
+++ b/setup.py
@@ -1,3 +1,5 @@
+from io import open
+
 from setuptools import setup
 
 from uritemplate import __version__
@@ -6,19 +8,27 @@
     'uritemplate'
 ]
 
+with open("README.rst", encoding="utf-8") as file:
+    readme = file.read()
+
+with open("HISTORY.rst", encoding="utf-8") as file:
+    history = file.read()
+
 setup(
     name="uritemplate",
     version=__version__,
     description='URI templates',
-    long_description="\n\n".join([open("README.rst").read(),
-                                  open("HISTORY.rst").read()]),
+    long_description="\n\n".join([readme, history]),
+    long_description_content_type="text/x-rst",
     license="BSD 3-Clause License or Apache License, Version 2.0",
-    author="Ian Cordasco",
+    author="Ian Stapleton Cordasco",
     author_email="graffatcolmingov@gmail.com",
     url="https://uritemplate.readthedocs.org",
     packages=packages,
-    package_data={'': ['LICENSE', 'AUTHORS.rst']},
+    package_data={'': ['LICENSE', 'LICENSE.APACHE', 'LICENSE.BSD',
+                       'AUTHORS.rst']},
     include_package_data=True,
+    python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
     classifiers=[
         'Development Status :: 5 - Production/Stable',
         'License :: OSI Approved',
@@ -27,13 +37,13 @@
         'Intended Audience :: Developers',
         'Programming Language :: Python',
         'Programming Language :: Python :: 2',
-        'Programming Language :: Python :: 2.6',
         'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.2',
-        'Programming Language :: Python :: 3.3',
         'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: 3.7',
+        'Programming Language :: Python :: 3.8',
         'Programming Language :: Python :: Implementation :: CPython',
     ],
 )
diff --git a/tests/fixtures/extended-tests.json b/tests/fixtures/extended-tests.json
index fd69744..ad43ad6 100644
--- a/tests/fixtures/extended-tests.json
+++ b/tests/fixtures/extended-tests.json
@@ -11,7 +11,7 @@
             "lang"         : "en",
             "geocode"      : ["37.76","-122.427"],
             "first_name"   : "John",
-            "last.name"    : "Doe", 
+            "last.name"    : "Doe",
             "Some%20Thing" : "foo",
             "number"       : 6,
             "long"         : 37.76,
@@ -28,7 +28,7 @@
         "testcases":[
 
             [ "{/id*}" , "/person" ],
-            [ "{/id*}{?fields,first_name,last.name,token}" , [ 
+            [ "{/id*}{?fields,first_name,last.name,token}" , [
             	"/person?fields=id,name,picture&first_name=John&last.name=Doe&token=12345",
             	"/person?fields=id,picture,name&first_name=John&last.name=Doe&token=12345",
             	"/person?fields=picture,name,id&first_name=John&last.name=Doe&token=12345",
@@ -68,7 +68,7 @@
         "testcases":[
 
             [ "{/id*}" , ["/person/albums","/albums/person"] ],
-            [ "{/id*}{?fields,token}" , [ 
+            [ "{/id*}{?fields,token}" , [
             	"/person/albums?fields=id,name,picture&token=12345",
             	"/person/albums?fields=id,picture,name&token=12345",
             	"/person/albums?fields=picture,name,id&token=12345",
diff --git a/tests/test_from_fixtures.py b/tests/test_from_fixtures.py
index 70d6553..d6c9810 100644
--- a/tests/test_from_fixtures.py
+++ b/tests/test_from_fixtures.py
@@ -1,3 +1,4 @@
+import io
 import json
 import os.path
 
@@ -12,7 +13,7 @@
 
 def load_examples(filename):
     path = fixture_file_path(filename)
-    with open(path, 'r') as examples_file:
+    with io.open(path, 'r', encoding="utf-8") as examples_file:
         examples = json.load(examples_file)
     return examples
 
@@ -20,7 +21,7 @@
 def expected_set(expected):
     if isinstance(expected, list):
         return set(expected)
-    return set([expected])
+    return {expected}
 
 
 class FixtureMixin(object):
diff --git a/tests/test_uritemplate.py b/tests/test_uritemplate.py
index b1abc96..f67ad26 100644
--- a/tests/test_uritemplate.py
+++ b/tests/test_uritemplate.py
@@ -464,6 +464,52 @@
             None
         )
 
+    def test_label_path_expansion_explode_slash(self):
+        t = URITemplate('{/foo*}')
+        self.assertEqual(t.variables[0]._label_path_expansion(
+            'foo', [], True, '/'), None
+        )
+        self.assertEqual(t.variables[0]._label_path_expansion(
+            'foo', [None], True, '/'), None
+        )
+        self.assertEqual(t.variables[0]._label_path_expansion(
+            'foo', [None, None], True, '/'), None
+        )
+        self.assertEqual(t.variables[0]._label_path_expansion(
+            'foo', ['one'], True, '/'), 'one'
+        )
+        self.assertEqual(t.variables[0]._label_path_expansion(
+            'foo', ['one', 'two'], True, '/'), 'one/two'
+        )
+        self.assertEqual(t.variables[0]._label_path_expansion(
+            'foo', ['one', None, 'two'], True, '/'), 'one/two'
+        )
+        self.assertEqual(t.variables[0]._label_path_expansion(
+            'foo', [''], True, '/'), ''
+        )
+        self.assertEqual(t.variables[0]._label_path_expansion(
+            'foo', ['', ''], True, '/'), '/'
+        )
+
+        self.assertEqual(t.variables[0]._label_path_expansion(
+            'foo', {}, True, '/'), None
+        )
+        self.assertEqual(t.variables[0]._label_path_expansion(
+            'foo', {'one': ''}, True, '/'), 'one='
+        )
+        self.assertEqual(t.variables[0]._label_path_expansion(
+            'foo', {'one': '', 'two': ''}, True, '/'), 'one=/two='
+        )
+        self.assertEqual(t.variables[0]._label_path_expansion(
+            'foo', {'one': None}, True, '/'), None
+        )
+        self.assertEqual(t.variables[0]._label_path_expansion(
+            'foo', {'one': None, 'two': 'two'}, True, '/'), 'two=two'
+        )
+        self.assertEqual(t.variables[0]._label_path_expansion(
+            'foo', {'one': None, 'two': None}, True, '/'), None
+        )
+
     def test_semi_path_expansion(self):
         t = URITemplate('{foo}')
         v = t.variables[0]
@@ -544,24 +590,24 @@
 
 class TestVariableModule(TestCase):
     def test_is_list_of_tuples(self):
-        l = [(1, 2), (3, 4)]
-        self.assertEqual(variable.is_list_of_tuples(l), (True, l))
+        a_list = [(1, 2), (3, 4)]
+        self.assertEqual(variable.is_list_of_tuples(a_list), (True, a_list))
 
-        l = [1, 2, 3, 4]
-        self.assertEqual(variable.is_list_of_tuples(l), (False, None))
+        a_list = [1, 2, 3, 4]
+        self.assertEqual(variable.is_list_of_tuples(a_list), (False, None))
 
     def test_list_test(self):
-        l = [1, 2, 3, 4]
-        self.assertEqual(variable.list_test(l), True)
+        a_list = [1, 2, 3, 4]
+        self.assertEqual(variable.list_test(a_list), True)
 
-        l = str([1, 2, 3, 4])
-        self.assertEqual(variable.list_test(l), False)
+        a_list = str([1, 2, 3, 4])
+        self.assertEqual(variable.list_test(a_list), False)
 
     def test_list_of_tuples_test(self):
-        l = [(1, 2), (3, 4)]
-        self.assertEqual(variable.dict_test(l), False)
+        a_list = [(1, 2), (3, 4)]
+        self.assertEqual(variable.dict_test(a_list), False)
 
-        d = dict(l)
+        d = dict(a_list)
         self.assertEqual(variable.dict_test(d), True)
 
 
diff --git a/tox.ini b/tox.ini
index 9d86da0..ed27494 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,17 +1,17 @@
 [tox]
 envlist =
-    py26,
     py27,
-    py32,
-    py33,
     py34,
     py35,
+    py36,
+    py37,
+    py38,
     pep8,
 
 [testenv]
 deps =
     pytest
-commands = py.test {posargs}
+commands = pytest {posargs}
 
 [testenv:pep8]
 deps =
diff --git a/uritemplate/__init__.py b/uritemplate/__init__.py
index 40c0320..5352520 100644
--- a/uritemplate/__init__.py
+++ b/uritemplate/__init__.py
@@ -16,7 +16,7 @@
 __author__ = 'Ian Cordasco'
 __license__ = 'Modified BSD or Apache License, Version 2.0'
 __copyright__ = 'Copyright 2013 Ian Cordasco'
-__version__ = '3.0.0'
+__version__ = '3.0.1'
 __version_info__ = tuple(int(i) for i in __version__.split('.') if i.isdigit())
 
 from uritemplate.api import (
diff --git a/uritemplate/api.py b/uritemplate/api.py
index 37c7c45..5ad2815 100644
--- a/uritemplate/api.py
+++ b/uritemplate/api.py
@@ -6,6 +6,8 @@
 This module contains the very simple API provided by uritemplate.
 
 """
+
+from uritemplate.orderedset import OrderedSet
 from uritemplate.template import URITemplate
 
 
@@ -68,4 +70,4 @@
         # => {'username', 'repository'}
 
     """
-    return set(URITemplate(uri).variable_names)
+    return OrderedSet(URITemplate(uri).variable_names)
diff --git a/uritemplate/orderedset.py b/uritemplate/orderedset.py
new file mode 100644
index 0000000..f21c9cb
--- /dev/null
+++ b/uritemplate/orderedset.py
@@ -0,0 +1,90 @@
+# From: https://github.com/ActiveState/code/blob/master/recipes/Python/576696_OrderedSet_with_Weakrefs/  # noqa
+
+from weakref import proxy
+
+try:
+    import collections.abc as collections_abc
+except ImportError:
+    import collections as collections_abc
+
+
+class Link(object):
+    __slots__ = 'prev', 'next', 'key', '__weakref__'
+
+
+class OrderedSet(collections_abc.MutableSet):
+    'Set the remembers the order elements were added'
+    # Big-O running times for all methods are the same as for regular sets.
+    # The internal self.__map dictionary maps keys to links in a doubly linked
+    # list. The circular doubly linked list starts and ends with a sentinel
+    # element. The sentinel element never gets deleted (this simplifies the
+    # algorithm). The prev/next links are weakref proxies (to prevent circular
+    # references). Individual links are kept alive by the hard reference in
+    # self.__map. Those hard references disappear when a key is deleted from
+    # an OrderedSet.
+
+    def __init__(self, iterable=None):
+        self.__root = root = Link()  # sentinel node for doubly linked list
+        root.prev = root.next = root
+        self.__map = {}  # key --> link
+        if iterable is not None:
+            self |= iterable
+
+    def __len__(self):
+        return len(self.__map)
+
+    def __contains__(self, key):
+        return key in self.__map
+
+    def add(self, key):
+        # Store new key in a new link at the end of the linked list
+        if key not in self.__map:
+            self.__map[key] = link = Link()
+            root = self.__root
+            last = root.prev
+            link.prev, link.next, link.key = last, root, key
+            last.next = root.prev = proxy(link)
+
+    def discard(self, key):
+        # Remove an existing item using self.__map to find the link which is
+        # then removed by updating the links in the predecessor and successors.
+        if key in self.__map:
+            link = self.__map.pop(key)
+            link.prev.next = link.next
+            link.next.prev = link.prev
+
+    def __iter__(self):
+        # Traverse the linked list in order.
+        root = self.__root
+        curr = root.next
+        while curr is not root:
+            yield curr.key
+            curr = curr.next
+
+    def __reversed__(self):
+        # Traverse the linked list in reverse order.
+        root = self.__root
+        curr = root.prev
+        while curr is not root:
+            yield curr.key
+            curr = curr.prev
+
+    def pop(self, last=True):
+        if not self:
+            raise KeyError('set is empty')
+        key = next(reversed(self)) if last else next(iter(self))
+        self.discard(key)
+        return key
+
+    def __repr__(self):
+        if not self:
+            return '%s()' % (self.__class__.__name__,)
+        return '%s(%r)' % (self.__class__.__name__, list(self))
+
+    def __str__(self):
+        return self.__repr__()
+
+    def __eq__(self, other):
+        if isinstance(other, OrderedSet):
+            return len(self) == len(other) and list(self) == list(other)
+        return not self.isdisjoint(other)
diff --git a/uritemplate/template.py b/uritemplate/template.py
index c9d7c7e..0df0da6 100644
--- a/uritemplate/template.py
+++ b/uritemplate/template.py
@@ -16,9 +16,10 @@
 """
 
 import re
+from uritemplate.orderedset import OrderedSet
 from uritemplate.variable import URIVariable
 
-template_re = re.compile('{([^\}]+)}')
+template_re = re.compile('{([^}]+)}')
 
 
 def _merge(var_dict, overrides):
@@ -71,9 +72,10 @@
             URIVariable(m.groups()[0]) for m in template_re.finditer(self.uri)
         ]
         #: A set of variable names in the URI.
-        self.variable_names = set()
+        self.variable_names = OrderedSet()
         for variable in self.variables:
-            self.variable_names.update(variable.variable_names)
+            for name in variable.variable_names:
+                self.variable_names.add(name)
 
     def __repr__(self):
         return 'URITemplate("%s")' % self
diff --git a/uritemplate/variable.py b/uritemplate/variable.py
index 1842830..ce3f652 100644
--- a/uritemplate/variable.py
+++ b/uritemplate/variable.py
@@ -15,12 +15,16 @@
 
 """
 
-import collections
 import sys
 
-if (2, 6) <= sys.version_info < (2, 8):
+try:
+    import collections.abc as collections_abc
+except ImportError:
+    import collections as collections_abc
+
+if sys.version_info.major == 2:
     import urllib
-elif (3, 3) <= sys.version_info < (4, 0):
+elif sys.version_info.major == 3:
     import urllib.parse as urllib
 
 
@@ -149,11 +153,11 @@
                 return None
             if explode:
                 return self.join_str.join(
-                    '%s=%s' % (name, quote(v, safe)) for v in value
+                    '{}={}'.format(name, quote(v, safe)) for v in value
                 )
             else:
                 value = ','.join(quote(v, safe) for v in value)
-                return '%s=%s' % (name, value)
+                return '{}={}'.format(name, value)
 
         if dict_test(value) or tuples:
             if not value:
@@ -161,21 +165,21 @@
             items = items or sorted(value.items())
             if explode:
                 return self.join_str.join(
-                    '%s=%s' % (
+                    '{}={}'.format(
                         quote(k, safe), quote(v, safe)
                     ) for k, v in items
                 )
             else:
                 value = ','.join(
-                    '%s,%s' % (
+                    '{},{}'.format(
                         quote(k, safe), quote(v, safe)
                     ) for k, v in items
                 )
-                return '%s=%s' % (name, value)
+                return '{}={}'.format(name, value)
 
         if value:
             value = value[:prefix] if prefix else value
-            return '%s=%s' % (name, quote(value, safe))
+            return '{}={}'.format(name, quote(value, safe))
         return name + '='
 
     def _label_path_expansion(self, name, value, explode, prefix):
@@ -196,10 +200,8 @@
             if not explode:
                 join_str = ','
 
-            expanded = join_str.join(
-                quote(v, safe) for v in value if value is not None
-            )
-            return expanded if expanded else None
+            fragments = [quote(v, safe) for v in value if v is not None]
+            return join_str.join(fragments) if fragments else None
 
         if dict_test(value) or tuples:
             items = items or sorted(value.items())
@@ -234,35 +236,35 @@
         if list_test(value) and not tuples:
             if explode:
                 expanded = join_str.join(
-                    '%s=%s' % (
+                    '{}={}'.format(
                         name, quote(v, safe)
                     ) for v in value if v is not None
                 )
                 return expanded if expanded else None
             else:
                 value = ','.join(quote(v, safe) for v in value)
-                return '%s=%s' % (name, value)
+                return '{}={}'.format(name, value)
 
         if dict_test(value) or tuples:
             items = items or sorted(value.items())
 
             if explode:
                 return join_str.join(
-                    '%s=%s' % (
+                    '{}={}'.format(
                         quote(k, safe), quote(v, safe)
                     ) for k, v in items if v is not None
                 )
             else:
                 expanded = ','.join(
-                    '%s,%s' % (
+                    '{},{}'.format(
                         quote(k, safe), quote(v, safe)
                     ) for k, v in items if v is not None
                 )
-                return '%s=%s' % (name, expanded)
+                return '{}={}'.format(name, expanded)
 
         value = value[:prefix] if prefix else value
         if value:
-            return '%s=%s' % (name, quote(value, safe))
+            return '{}={}'.format(name, quote(value, safe))
 
         return name
 
@@ -360,7 +362,7 @@
 
 
 def dict_test(value):
-    return isinstance(value, (dict, collections.MutableMapping))
+    return isinstance(value, (dict, collections_abc.MutableMapping))
 
 
 try: