summaryrefslogtreecommitdiff
diff options
authorThomas Goirand <zigo@debian.org>2021-01-26 10:28:54 +0100
committergit-ubuntu importer <ubuntu-devel-discuss@lists.ubuntu.com>2021-01-26 16:41:50 +0000
commit8240341ee2c8ea08b0c587916eac0cf39afcf5b1 (patch)
tree6ef8c9fbe76bc046b5018845fb9001454c4692a0
parent5a74ac341317ced4661f46304077b80716a3374e (diff)
parent4d2e4a760dec31ee50d22ba68f7bec04a1c8c5d1 (diff)
Imported using git-ubuntu import.
-rw-r--r--.gitignore15
-rw-r--r--.gitmodules3
-rw-r--r--.mailmap2
-rw-r--r--.travis.yml42
-rw-r--r--AUTHORS.rst15
-rw-r--r--HISTORY.rst81
-rw-r--r--LICENSE3
-rw-r--r--LICENSE.APACHE202
-rw-r--r--LICENSE.BSD23
-rw-r--r--MAINTAINERS.rst14
-rw-r--r--MANIFEST.in11
-rw-r--r--README.rst86
-rw-r--r--debian/changelog10
-rw-r--r--debian/control9
-rw-r--r--debian/copyright2
-rwxr-xr-xdebian/rules2
-rw-r--r--docs/Makefile130
-rw-r--r--docs/conf.py240
-rw-r--r--docs/index.rst81
-rw-r--r--old/uritemplate.py/uritemplatepy-setup.py30
-rw-r--r--setup.cfg2
-rw-r--r--[-rwxr-xr-x]setup.py76
-rw-r--r--test/Makefile19
-rw-r--r--test/uritemplate_test.py37
-rw-r--r--test/variables_test.py83
-rw-r--r--tests/fixtures/README.md91
-rw-r--r--tests/fixtures/extended-tests.json118
-rw-r--r--tests/fixtures/json2xml.xslt201
-rw-r--r--tests/fixtures/negative-tests.json57
-rw-r--r--tests/fixtures/spec-examples-by-section.json439
-rw-r--r--tests/fixtures/spec-examples.json218
-rw-r--r--tests/fixtures/transform-json-tests.xslt51
-rw-r--r--tests/test_from_fixtures.py117
-rw-r--r--tests/test_uritemplate.py635
-rw-r--r--tox.ini31
-rw-r--r--[-rwxr-xr-x]uritemplate/__init__.py273
-rw-r--r--uritemplate/api.py73
-rw-r--r--uritemplate/orderedset.py90
-rw-r--r--uritemplate/template.py152
-rw-r--r--uritemplate/variable.py386
40 files changed, 3638 insertions, 512 deletions
diff --git a/.gitignore b/.gitignore
index 5b1b935..47b44f1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,12 @@
*.pyc
-build
-dist
-MANIFEST
-
+*.swp
+docs/_build
+bin/
+include/
+lib/
+lib64/
+dist/
+*.egg-info/
+.coverage
+htmlcov/
+.tox/
diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644
index 3258e94..0000000
--- a/.gitmodules
+++ /dev/null
@@ -1,3 +0,0 @@
-[submodule "test/cases"]
- path = test/cases
- url = git://github.com/uri-templates/uritemplate-test.git
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 520ac1b..ec4291e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,11 +1,33 @@
language: python
-python:
- - "2.5"
- - "2.6"
- - "2.7"
- - "3.3"
- - "pypy"
-# dependencies
-install: pip install simplejson --use-mirrors
-# command to run tests
-script: "cd test; make"
+sudo: false
+
+matrix:
+ include:
+ - python: 2.7
+ env: TOXENV=py27
+ - 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:
+ - pip install tox
+
+script:
+ - tox
+
+notifications:
+ on_success: change
+ on_failure: change
+ irc:
+ channels:
+ - "irc.freenode.org#github3.py"
+ user_notice: true
+ skip_join: true
diff --git a/AUTHORS.rst b/AUTHORS.rst
new file mode 100644
index 0000000..2581f4d
--- /dev/null
+++ b/AUTHORS.rst
@@ -0,0 +1,15 @@
+Development Lead
+----------------
+
+- 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
new file mode 100644
index 0000000..3a84017
--- /dev/null
+++ b/HISTORY.rst
@@ -0,0 +1,81 @@
+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
+------------------
+
+- Merge uritemplate.py into uritemplate
+
+
+Changelog - uritemplate.py
+==========================
+
+3.0.2 - 2015-08-30
+------------------
+
+- Fix meta-package requirements.
+
+3.0.1 - 2015-08-29
+------------------
+
+- Deprecate in favor of uritemplate. This package is now a metapackage that
+ depends on uritemplate.
+
+2.0.0 - 2016-08-20
+------------------
+
+- Relicense uritemplate.py as Apache 2 and BSD (See
+ https://github.com/sigmavirus24/uritemplate/pull/23)
+
+1.0.1 - 2016-08-18
+------------------
+
+- Fix some minor packaging problems.
+
+1.0.0 - 2016-08-17
+------------------
+
+- Fix handling of Unicode values on Python 2.6 and 2.7 for urllib.quote.
+
+- Confirm public stable API via version number.
+
+0.3.0 - 2013-10-22
+------------------
+
+- Add ``#partial`` to partially expand templates and return new instances of
+ ``URITemplate``.
+
+0.2.0 - 2013-07-26
+------------------
+
+- Refactor the library a bit and add more tests.
+
+- Backwards incompatible with 0.1.x if using ``URIVariable`` directly from
+ ``uritemplate.template``
+
+0.1.1 - 2013-05-19
+------------------
+
+- Add ability to get set of variable names in the current URI
+
+- If there is no value or default given, simply return an empty string
+
+- Fix sdist
+
+0.1.0 - 2013-05-14
+------------------
+
+- Initial Release
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..41c87e7
--- /dev/null
+++ b/LICENSE
@@ -0,0 +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 are
+made under the terms of *both* these licenses.
diff --git a/LICENSE.APACHE b/LICENSE.APACHE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE.APACHE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/LICENSE.BSD b/LICENSE.BSD
new file mode 100644
index 0000000..ee1eb05
--- /dev/null
+++ b/LICENSE.BSD
@@ -0,0 +1,23 @@
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+3. The name of the author may not be used to endorse or promote products
+derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/MAINTAINERS.rst b/MAINTAINERS.rst
deleted file mode 100644
index 4918f84..0000000
--- a/MAINTAINERS.rst
+++ /dev/null
@@ -1,14 +0,0 @@
-Instructions for Maintainers
-============================
-
-Release
--------
-
-To release a build:
-
-1. Push all changes, verify CI passes (see link in README.rst).
-2. Increment __version__ in __init__.py
-3. ``git tag -a uri-template-py-[version]``
-4. ``git push --tags origin master``
-5. ``python setup.py sdist upload``
-6. Brew coffee or tea.
diff --git a/MANIFEST.in b/MANIFEST.in
index 9561fb1..f545e0f 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1 +1,12 @@
include README.rst
+include HISTORY.rst
+include LICENSE
+include LICENSE.APACHE
+include LICENSE.BSD
+include AUTHORS.rst
+
+recursive-include docs *
+recursive-include tests *
+
+prune docs/_build
+global-exclude *.py[cdo] __pycache__ *.so *.pyd
diff --git a/README.rst b/README.rst
index 80129cb..2c9d98d 100644
--- a/README.rst
+++ b/README.rst
@@ -1,71 +1,63 @@
uritemplate
===========
-.. image:: https://secure.travis-ci.org/uri-templates/uritemplate-py.png?branch=master
- :alt: build status
- :target: http://travis-ci.org/uri-templates/uritemplate-py
+Documentation_ -- GitHub_ -- Travis-CI_
-This is a Python implementation of `RFC6570`_, URI Template, and can
-expand templates up to and including Level 4 in that specification.
-
-It exposes a method, *expand*. For example:
-
-.. code-block:: python
-
- >>> from uritemplate import expand
- >>> expand("http://www.{domain}/", {"domain": "foo.com"})
- 'http://www.foo.com/'
-
-It also exposes a method *variables* that returns all variables used in a
-uritemplate. For example:
+Simple python library to deal with `URI Templates`_. The API looks like
.. code-block:: python
- >>> from uritemplate import variables
- >>> variables('http:www{.domain*}{/top,next}{?q:20}')
- >>> set(['domain', 'next', 'q', 'top'])
-
-This function can be useful to determine what keywords are available to be
-expanded.
+ from uritemplate import URITemplate, expand
-.. _RFC6570: http://tools.ietf.org/html/rfc6570
+ # NOTE: URI params must be strings not integers
+ gist_uri = 'https://api.github.com/users/sigmavirus24/gists{/gist_id}'
+ t = URITemplate(gist_uri)
+ print(t.expand(gist_id='123456'))
+ # => https://api.github.com/users/sigmavirus24/gists/123456
-Requirements
-------------
+ # or
+ print(expand(gist_uri, gist_id='123456'))
-uritemplate works with Python 2.5+.
+ # also
+ t.expand({'gist_id': '123456'})
+ print(expand(gist_uri, {'gist_id': '123456'}))
-.. note:: You need to install `simplejson`_ module for Python 2.5.
-
-.. _simplejson: https://pypi.python.org/pypi/simplejson/
+Where it might be useful to have a class
+.. code-block:: python
-Install
--------
+ import requests
-The easiest way to install uritemplate is with pip::
+ class GitHubUser(object):
+ url = URITemplate('https://api.github.com/user{/login}')
+ def __init__(self, name):
+ self.api_url = url.expand(login=name)
+ response = requests.get(self.api_url)
+ if response.status_code == 200:
+ self.__dict__.update(response.json())
- $ pip install uritemplate
+When the module containing this class is loaded, ``GitHubUser.url`` is
+evaluated and so the template is created once. It's often hard to notice in
+Python, but object creation can consume a great deal of time and so can the
+``re`` module which uritemplate relies on. Constructing the object once should
+reduce the amount of time your code takes to run.
-See its `Python Package Index entry`_ for more.
+Installing
+----------
-.. _Python Package Index entry: http://pypi.python.org/pypi/uritemplate
+::
+ pip install uritemplate
License
-=======
-
-Copyright 2011-2013 Joe Gregorio
+-------
-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
+Modified BSD license_
-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.
+.. _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/python-hyper/uritemplate/blob/master/LICENSE
diff --git a/debian/changelog b/debian/changelog
index 2bf243b..9220546 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,13 @@
+python-uritemplate (3.0.1-1) unstable; urgency=medium
+
+ * New upstream release. (Closes: #974040)
+ - Fixes issue with Python 3.9 (Closes: #980779).
+ * Update upstream URLs to new location. (Closes: #945530)
+ * Move the package to the OpenStack team.
+ * Remove Jimmy from uploaders (he never contributed).
+
+ -- Thomas Goirand <zigo@debian.org> Tue, 26 Jan 2021 10:28:54 +0100
+
python-uritemplate (0.6-4) unstable; urgency=medium
* Using debhelper-compat (= 10).
diff --git a/debian/control b/debian/control
index faf4f6b..2aee857 100644
--- a/debian/control
+++ b/debian/control
@@ -1,9 +1,8 @@
Source: python-uritemplate
Section: python
Priority: optional
-Maintainer: PKG Cloud <team+cloud@tracker.debian.org>
+Maintainer: Debian OpenStack <team+openstack@tracker.debian.org>
Uploaders:
- Jimmy Kaplowitz <jimmy@debian.org>,
Thomas Goirand <zigo@debian.org>,
Build-Depends:
debhelper-compat (= 11),
@@ -12,9 +11,9 @@ Build-Depends:
python3-all,
python3-setuptools,
Standards-Version: 4.1.3
-Vcs-Browser: https://salsa.debian.org/cloud-team/python-uritemplate
-Vcs-Git: https://salsa.debian.org/cloud-team/python-uritemplate.git
-Homepage: http://github.com/uri-templates/uritemplate-py/
+Vcs-Browser: https://salsa.debian.org/openstack-team/python/python-uritemplate
+Vcs-Git: https://salsa.debian.org/openstack-team/python/python-uritemplate.git
+Homepage: https://github.com/python-hyper/uritemplate
Package: python3-uritemplate
Architecture: all
diff --git a/debian/copyright b/debian/copyright
index 6d45e05..f73d74f 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -1,6 +1,6 @@
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: uritemplate
-Source: http://github.com/uri-templates/uritemplate-py/
+Source: https://github.com/python-hyper/uritemplate
Files: *
Copyright: (c) 2013, Joe Gregorio <joe.gregorio@gmail.com>
diff --git a/debian/rules b/debian/rules
index 032c03f..b26b26e 100755
--- a/debian/rules
+++ b/debian/rules
@@ -1,6 +1,6 @@
#!/usr/bin/make -f
-UPSTREAM_GIT = git://github.com/uri-templates/uritemplate-py.git
+UPSTREAM_GIT = https://github.com/python-hyper/uritemplate
include /usr/share/openstack-pkg-tools/pkgos.make
%:
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..bf49b54
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,130 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ -rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Raclette.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Raclette.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/Raclette"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Raclette"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ make -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..86292ea
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,240 @@
+# -*- coding: utf-8 -*-
+#
+# Requests documentation build configuration file, created by
+# sphinx-quickstart on Sun Feb 13 23:54:25 2011.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os
+
+# This environment variable makes decorators not decorate functions, so their
+# signatures in the generated documentation are still correct
+os.environ['GENERATING_DOCUMENTATION'] = "uritemplate"
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+sys.path.insert(0, os.path.abspath('..'))
+import uritemplate
+
+# -- General configuration -----------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'uritemplate'
+copyright = u'2013 - Ian Cordasco'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = uritemplate.__version__
+# The full version, including alpha/beta/rc tags.
+release = version
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+# pygments_style = 'flask_theme_support.FlaskyStyle'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = 'nature'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+#html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = False
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = False
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'uritemplate_doc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+ ('index', 'uritemplate.tex', u'uritemplate Documentation',
+ u'Ian Cordasco', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'uritemplate', u'uritemplate Documentation',
+ [u'Ian Cordasco'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+# -- Options for Texinfo output ------------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'uritemplate', u'uritemplate Documentation', u'Ian Cordasco',
+ 'uritemplate', 'Library to expand RFC6570 templated URIs',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+texinfo_appendices = []
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..35776f0
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,81 @@
+uritemplate
+===========
+
+Release v\ |version|.
+
+Examples
+--------
+
+This first example shows how simple the API can be when using for a one-off
+item in a script or elsewhere.
+
+.. code-block:: python
+
+ from requests import get
+ from uritemplate import expand
+
+ uri = 'https://api.github.com{/user}'
+
+ user = get(expand(uri, user='sigmavirus24')).json()
+
+This second example shows how using the class will save you time for template
+parsing and object creation. Making the template once means the URI is parsed
+once which decreases the number of :class:`URITemplate
+<uritemplate.URITemplate>` objects created and usage of the ``re`` module.
+This means that as soon as the file is parsed, the ``User.github_url`` and
+``Repository.github_url`` variables are made once and only once. They're then
+usable in every instance of those classes.
+
+.. code-block:: python
+
+ from uritemplate import URITemplate
+
+ class User(object):
+ github_url = URITemplate('https://api.github.com{/user}')
+ def __init__(self, name):
+ self.uri = self.github_url.expand({'user': name})
+ self.name = name
+
+ class Repository(object):
+ github_url = URITemplate('https://api.github.com{/user}{/repo}')
+ def __init__(self, name):
+ self.uri = self.github_url.expand(
+ dict(zip(['user', 'repo'], name.split('/')))
+ )
+ self.name = name
+
+API
+---
+
+.. module:: uritemplate
+
+.. autofunction:: uritemplate.expand
+
+.. autofunction:: uritemplate.partial
+
+.. autofunction:: uritemplate.variables
+
+.. autoclass:: uritemplate.URITemplate
+ :members:
+
+Implementation Details
+----------------------
+
+Classes, their methods, and functions in this section are not part of the API
+and as such are not meant to be used by users of ``uritemplate.py``. These are
+documented here purely for reference as they are inadvertently exposed via the
+public API.
+
+For example::
+
+ t = URITemplate('https://api.github.com/users{/user}')
+ t.variables
+ # => [URIVariable(/user)]
+
+Users can interact with :class:`URIVariable` objects as they see fit, but
+their API may change and are not guaranteed to be consistent across versions.
+Code relying on methods defined on :class:`URIVariable` and other classes,
+methods, and functions in this section may break in future releases.
+
+.. autoclass:: uritemplate.template.URIVariable
+ :members: expand
diff --git a/old/uritemplate.py/uritemplatepy-setup.py b/old/uritemplate.py/uritemplatepy-setup.py
new file mode 100644
index 0000000..ebececb
--- /dev/null
+++ b/old/uritemplate.py/uritemplatepy-setup.py
@@ -0,0 +1,30 @@
+from setuptools import setup
+
+setup(
+ name="uritemplate.py",
+ version="3.0.2",
+ description="URI templates",
+ long_description="\n\n".join([open("README.rst").read(),
+ open("HISTORY.rst").read()]),
+ license="BSD 3-Clause License or Apache License, Version 2.0",
+ author="Ian Cordasco",
+ 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',
+ 'License :: OSI Approved :: BSD License',
+ 'License :: OSI Approved :: Apache Software License',
+ 'Intended Audience :: Developers',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 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.cfg b/setup.cfg
new file mode 100644
index 0000000..2a9acf1
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[bdist_wheel]
+universal = 1
diff --git a/setup.py b/setup.py
index 9b71aae..7a09f5e 100755..100644
--- a/setup.py
+++ b/setup.py
@@ -1,37 +1,49 @@
-#!/usr/bin/env python
+from io import open
-from distutils.core import setup
-import uritemplate
+from setuptools import setup
-base_url = "http://github.com/uri-templates/uritemplate-py/"
+from uritemplate import __version__
+
+packages = [
+ '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 = uritemplate.__version__,
- description = 'URI Templates',
- author = 'Joe Gregorio',
- author_email = 'joe@bitworking.org',
- url = base_url,
- download_url = \
- '%starball/uritemplate-py-%s' % (base_url, uritemplate.__version__),
- packages = ['uritemplate'],
- provides = ['uritemplate'],
- long_description=open("README.rst").read(),
- install_requires = ['simplejson >= 2.5.0'],
- classifiers = [
- 'Development Status :: 4 - Beta',
- 'Intended Audience :: Developers',
- 'License :: OSI Approved :: Apache Software License',
- 'Programming Language :: Python',
- 'Programming Language :: Python :: 2',
- 'Programming Language :: Python :: 2.5',
- 'Programming Language :: Python :: 2.6',
- 'Programming Language :: Python :: 2.7',
- 'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.3',
- 'Operating System :: POSIX',
- 'Topic :: Internet :: WWW/HTTP',
- 'Topic :: Software Development :: Libraries :: Python Modules',
- ]
+ name="uritemplate",
+ version=__version__,
+ description='URI templates',
+ 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 Stapleton Cordasco",
+ author_email="graffatcolmingov@gmail.com",
+ url="https://uritemplate.readthedocs.org",
+ packages=packages,
+ 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',
+ 'License :: OSI Approved :: BSD License',
+ 'License :: OSI Approved :: Apache Software License',
+ 'Intended Audience :: Developers',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 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/test/Makefile b/test/Makefile
deleted file mode 100644
index 85d5ee9..0000000
--- a/test/Makefile
+++ /dev/null
@@ -1,19 +0,0 @@
-
-LEVEL = 4
-
-test: spec-examples spec-examples-by-section extended variables
-
-spec-examples:
- PYTHONPATH=..:$(PYTHONPATH) python uritemplate_test.py cases/spec-examples.json $(LEVEL)
-
-spec-examples-by-section:
- PYTHONPATH=..:$(PYTHONPATH) python uritemplate_test.py cases/spec-examples-by-section.json $(LEVEL)
-
-extended:
- PYTHONPATH=..:$(PYTHONPATH) python uritemplate_test.py cases/extended-tests.json $(LEVEL)
-
-negative:
- PYTHONPATH=..:$(PYTHONPATH) python uritemplate_test.py cases/negative-tests.json $(LEVEL)
-
-variables:
- PYTHONPATH=..:$(PYTHONPATH) python variables_test.py
diff --git a/test/uritemplate_test.py b/test/uritemplate_test.py
deleted file mode 100644
index f691cd9..0000000
--- a/test/uritemplate_test.py
+++ /dev/null
@@ -1,37 +0,0 @@
-import uritemplate
-import simplejson
-import sys
-
-filename = sys.argv[1]
-print("Running", filename)
-f = open(filename)
-testdata = simplejson.load(f)
-
-try:
- desired_level = int(sys.argv[2])
-except IndexError:
- desired_level = 4
-
-for name, testsuite in testdata.items():
- vars = testsuite['variables']
- testcases = testsuite['testcases']
-
- level = testsuite.get('level', 4)
- if level > desired_level:
- continue
-
- print(name)
- for testcase in testcases:
- template = testcase[0]
- expected = testcase[1]
- actual = uritemplate.expand(template, vars)
- sys.stdout.write(".")
- if type(expected) == type([]):
- if actual not in expected:
- sys.stderr.write("%s didn't expand as expected, got %s instead\n" % (template, actual))
- assert 0
- else:
- if actual != expected:
- sys.stderr.write("%s expected to expand to %s, got %s instead\n" % (template, expected, actual))
- assert 0
- print()
diff --git a/test/variables_test.py b/test/variables_test.py
deleted file mode 100644
index 7a45d34..0000000
--- a/test/variables_test.py
+++ /dev/null
@@ -1,83 +0,0 @@
-'''Tests for the variables function'''
-
-import unittest
-
-import uritemplate
-
-
-class VariablesTests(unittest.TestCase):
-
- def test_simple(self):
- template = 'http://example.com/{x,y}'
- vars = uritemplate.variables(template)
- self.assertEquals(vars, set(['x', 'y']))
-
- def test_simple2(self):
- template = 'http://example.com/{x,y}/{z}'
- vars = uritemplate.variables(template)
- self.assertEquals(vars, set(['x', 'y', 'z']))
-
- def test_reserved(self):
- template = 'http://example.com/{+x,y}/{+z}'
- vars = uritemplate.variables(template)
- self.assertEquals(vars, set(['x', 'y', 'z']))
-
- def test_fragment(self):
- template = 'http://example.com/{#x,y},{#z}'
- vars = uritemplate.variables(template)
- self.assertEquals(vars, set(['x', 'y', 'z']))
-
- def test_label(self):
- template = 'http://{.x,y,z}'
- vars = uritemplate.variables(template)
- self.assertEquals(vars, set(['x', 'y', 'z']))
-
- def test_path_segment(self):
- template = 'http://example.com{/x,y}/w{/z}'
- vars = uritemplate.variables(template)
- self.assertEquals(vars, set(['x', 'y', 'z']))
-
- def test_parameter(self):
- template = 'http://example.com{;x,y}{;z}'
- vars = uritemplate.variables(template)
- self.assertEquals(vars, set(['x', 'y', 'z']))
-
- def test_query(self):
- template = 'http://example.com{?x,y,z}'
- vars = uritemplate.variables(template)
- self.assertEquals(vars, set(['x', 'y', 'z']))
-
- def test_query_continuation(self):
- template = 'http://example.com?a=1&b=2{&x,y}&r=13{&z}'
- vars = uritemplate.variables(template)
- self.assertEquals(vars, set(['x', 'y', 'z']))
-
- def test_prefix_modifier(self):
- template = 'http://example.com{/x:5,y:7}{/z:2}'
- vars = uritemplate.variables(template)
- self.assertEquals(vars, set(['x', 'y', 'z']))
-
- def test_explode_modifier(self):
- template = 'http://example.com{/x*,y*}/page{/z*}'
- vars = uritemplate.variables(template)
- self.assertEquals(vars, set(['x', 'y', 'z']))
-
- def test_mixed_expansion_types(self):
- template = 'http://{a,b}.com{;c,d}{/e,f}/page{?g,h}{&i,j}{#k,l}'
- vars = uritemplate.variables(template)
- expected_vars = set('abcdefghijkl')
- self.assertEquals(vars, expected_vars)
-
- def test_overlapping_expansion(self):
- template = 'http://{a,b}.com{;a,b}{/a,b}/page{?a,b}{&a,b}{#a,b}'
- vars = uritemplate.variables(template)
- self.assertEquals(vars, set(['a', 'b']))
-
- def test_partially_overlapping(self):
- template = 'http://{.a,b}{/b,c}/{c,d}'
- vars = uritemplate.variables(template)
- self.assertEquals(vars, set(['a', 'b', 'c', 'd']))
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md
new file mode 100644
index 0000000..11dcddb
--- /dev/null
+++ b/tests/fixtures/README.md
@@ -0,0 +1,91 @@
+Theses test are borrowed from https://github.com/uri-templates/uritemplate-test
+at commit: fdd5d611a849b922c2ff40fc3997fd265dd14c02
+URI Template Tests
+==================
+
+This is a set of tests for implementations of
+[RFC6570](http://tools.ietf.org/html/rfc6570) - URI Template. It is designed
+to be reused by any implementation, to improve interoperability and
+implementation quality.
+
+If your project uses Git for version control, you can make uritemplate-tests into a [submodule](http://help.github.com/submodules/).
+
+Test Format
+-----------
+
+Each test file is a [JSON](http://tools.ietf.org/html/RFC6627) document
+containing an object whose properties are groups of related tests.
+Alternatively, all tests are available in XML as well, with the XML files
+being generated by transform-json-tests.xslt which uses json2xml.xslt as a
+general-purpose JSON-to-XML parsing library.
+
+Each group, in turn, is an object with three children:
+
+* level - the level of the tests covered, as per the RFC (optional; if absent,
+ assume level 4).
+* variables - an object representing the variables that are available to the
+ tests in the suite
+* testcases - a list of testcases, where each case is a two-member list, the
+ first being the template, the second being the result of expanding the
+ template with the provided variables.
+
+Note that the result string can be a few different things:
+
+* string - if the second member is a string, the result of expansion is
+ expected to match it, character-for-character.
+* list - if the second member is a list of strings, the result of expansion
+ is expected to match one of them; this allows for templates that can
+ expand into different, equally-acceptable URIs.
+* false - if the second member is boolean false, expansion is expected to
+ fail (i.e., the template was invalid).
+
+For example:
+
+ {
+ "Level 1 Examples" :
+ {
+ "level": 1,
+ "variables": {
+ "var" : "value",
+ "hello" : "Hello World!"
+ },
+ "testcases" : [
+ ["{var}", "value"],
+ ["{hello}", "Hello%20World%21"]
+ ]
+ }
+ }
+
+
+Tests Included
+--------------
+
+The following test files are included:
+
+* spec-examples.json - The complete set of example templates from the RFC
+* spec-examples-by-section.json - The examples, section by section
+* extended-tests.json - more complex test cases
+* negative-tests.json - invalid templates
+
+For all these test files, XML versions with the names *.xml can be
+generated with the transform-json-tests.xslt XSLT stylesheet. The XSLT
+contains the names of the above test files as a parameter, and can be
+started with any XML as input (i.e., the XML input is ignored).
+
+License
+-------
+
+ Copyright 2011-2012 The Authors
+
+ 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.
+
diff --git a/tests/fixtures/extended-tests.json b/tests/fixtures/extended-tests.json
new file mode 100644
index 0000000..ad43ad6
--- /dev/null
+++ b/tests/fixtures/extended-tests.json
@@ -0,0 +1,118 @@
+{
+ "Additional Examples 1":{
+ "level":4,
+ "variables":{
+ "id" : "person",
+ "token" : "12345",
+ "fields" : ["id", "name", "picture"],
+ "format" : "json",
+ "q" : "URI Templates",
+ "page" : "5",
+ "lang" : "en",
+ "geocode" : ["37.76","-122.427"],
+ "first_name" : "John",
+ "last.name" : "Doe",
+ "Some%20Thing" : "foo",
+ "number" : 6,
+ "long" : 37.76,
+ "lat" : -122.427,
+ "group_id" : "12345",
+ "query" : "PREFIX dc: <http://purl.org/dc/elements/1.1/> SELECT ?book ?who WHERE { ?book dc:creator ?who }",
+ "uri" : "http://example.org/?uri=http%3A%2F%2Fexample.org%2F",
+ "word" : "drücken",
+ "Stra%C3%9Fe" : "Grüner Weg",
+ "random" : "šö䟜ñꀣ¥‡ÑÒÓÔÕÖרÙÚàáâãäåæçÿ",
+ "assoc_special_chars" :
+ { "šö䟜ñꀣ¥‡ÑÒÓÔÕ" : "ÖרÙÚàáâãäåæçÿ" }
+ },
+ "testcases":[
+
+ [ "{/id*}" , "/person" ],
+ [ "{/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",
+ "/person?fields=picture,id,name&first_name=John&last.name=Doe&token=12345",
+ "/person?fields=name,picture,id&first_name=John&last.name=Doe&token=12345",
+ "/person?fields=name,id,picture&first_name=John&last.name=Doe&token=12345"]
+ ],
+ ["/search.{format}{?q,geocode,lang,locale,page,result_type}",
+ [ "/search.json?q=URI%20Templates&geocode=37.76,-122.427&lang=en&page=5",
+ "/search.json?q=URI%20Templates&geocode=-122.427,37.76&lang=en&page=5"]
+ ],
+ ["/test{/Some%20Thing}", "/test/foo" ],
+ ["/set{?number}", "/set?number=6"],
+ ["/loc{?long,lat}" , "/loc?long=37.76&lat=-122.427"],
+ ["/base{/group_id,first_name}/pages{/page,lang}{?format,q}","/base/12345/John/pages/5/en?format=json&q=URI%20Templates"],
+ ["/sparql{?query}", "/sparql?query=PREFIX%20dc%3A%20%3Chttp%3A%2F%2Fpurl.org%2Fdc%2Felements%2F1.1%2F%3E%20SELECT%20%3Fbook%20%3Fwho%20WHERE%20%7B%20%3Fbook%20dc%3Acreator%20%3Fwho%20%7D"],
+ ["/go{?uri}", "/go?uri=http%3A%2F%2Fexample.org%2F%3Furi%3Dhttp%253A%252F%252Fexample.org%252F"],
+ ["/service{?word}", "/service?word=dr%C3%BCcken"],
+ ["/lookup{?Stra%C3%9Fe}", "/lookup?Stra%C3%9Fe=Gr%C3%BCner%20Weg"],
+ ["{random}" , "%C5%A1%C3%B6%C3%A4%C5%B8%C5%93%C3%B1%C3%AA%E2%82%AC%C2%A3%C2%A5%E2%80%A1%C3%91%C3%92%C3%93%C3%94%C3%95%C3%96%C3%97%C3%98%C3%99%C3%9A%C3%A0%C3%A1%C3%A2%C3%A3%C3%A4%C3%A5%C3%A6%C3%A7%C3%BF"],
+ ["{?assoc_special_chars*}", "?%C5%A1%C3%B6%C3%A4%C5%B8%C5%93%C3%B1%C3%AA%E2%82%AC%C2%A3%C2%A5%E2%80%A1%C3%91%C3%92%C3%93%C3%94%C3%95=%C3%96%C3%97%C3%98%C3%99%C3%9A%C3%A0%C3%A1%C3%A2%C3%A3%C3%A4%C3%A5%C3%A6%C3%A7%C3%BF"]
+ ]
+ },
+ "Additional Examples 2":{
+ "level":4,
+ "variables":{
+ "id" : ["person","albums"],
+ "token" : "12345",
+ "fields" : ["id", "name", "picture"],
+ "format" : "atom",
+ "q" : "URI Templates",
+ "page" : "10",
+ "start" : "5",
+ "lang" : "en",
+ "geocode" : ["37.76","-122.427"]
+ },
+ "testcases":[
+
+ [ "{/id*}" , ["/person/albums","/albums/person"] ],
+ [ "{/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",
+ "/person/albums?fields=picture,id,name&token=12345",
+ "/person/albums?fields=name,picture,id&token=12345",
+ "/person/albums?fields=name,id,picture&token=12345",
+ "/albums/person?fields=id,name,picture&token=12345",
+ "/albums/person?fields=id,picture,name&token=12345",
+ "/albums/person?fields=picture,name,id&token=12345",
+ "/albums/person?fields=picture,id,name&token=12345",
+ "/albums/person?fields=name,picture,id&token=12345",
+ "/albums/person?fields=name,id,picture&token=12345"]
+ ]
+ ]
+ },
+ "Additional Examples 3: Empty Variables":{
+ "variables" : {
+ "empty_list" : [],
+ "empty_assoc" : {}
+ },
+ "testcases":[
+ [ "{/empty_list}", [ "" ] ],
+ [ "{/empty_list*}", [ "" ] ],
+ [ "{?empty_list}", [ ""] ],
+ [ "{?empty_list*}", [ "" ] ],
+ [ "{?empty_assoc}", [ "" ] ],
+ [ "{?empty_assoc*}", [ "" ] ]
+ ]
+ },
+ "Additional Examples 4: Numeric Keys":{
+ "variables" : {
+ "42" : "The Answer to the Ultimate Question of Life, the Universe, and Everything",
+ "1337" : ["leet", "as","it", "can","be"],
+ "german" : {
+ "11": "elf",
+ "12": "zwölf"
+ }
+ },
+ "testcases":[
+ [ "{42}", "The%20Answer%20to%20the%20Ultimate%20Question%20of%20Life%2C%20the%20Universe%2C%20and%20Everything"],
+ [ "{?42}", "?42=The%20Answer%20to%20the%20Ultimate%20Question%20of%20Life%2C%20the%20Universe%2C%20and%20Everything"],
+ [ "{1337}", "leet,as,it,can,be"],
+ [ "{?1337*}", "?1337=leet&1337=as&1337=it&1337=can&1337=be"],
+ [ "{?german*}", [ "?11=elf&12=zw%C3%B6lf", "?12=zw%C3%B6lf&11=elf"] ]
+ ]
+ }
+}
diff --git a/tests/fixtures/json2xml.xslt b/tests/fixtures/json2xml.xslt
new file mode 100644
index 0000000..59b3548
--- /dev/null
+++ b/tests/fixtures/json2xml.xslt
@@ -0,0 +1,201 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Downloaded on 12/6/2012 from http://www.gerixsoft.com/blog/xslt/json2xml -->
+
+<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+ <xsl:template match="json">
+ <xsl:copy>
+ <xsl:copy-of select="@*"/>
+ <xsl:call-template name="json2xml">
+ <xsl:with-param name="text" select="."/>
+ </xsl:call-template>
+ </xsl:copy>
+ </xsl:template>
+
+ <xsl:template name="json2xml">
+ <xsl:param name="text"/>
+ <xsl:variable name="mode0">
+ <xsl:variable name="regexps" select="'//(.*?)\n', '/\*(.*?)\*/', '(''|&quot;)(([^\\]|\\[\\&quot;''/btnvfr])*?)\3', '(-?\d+(\.\d+([eE][+-]?\d+)?|[eE][+-]?\d+))', '(-?[1-9]\d*)', '(-?0[0-7]+)', '(-?0x[0-9a-fA-F]+)', '([:,\{\}\[\]])', '(true|false)', '(null)'"/>
+ <xsl:analyze-string select="$text" regex="{string-join($regexps,'|')}" flags="s">
+ <xsl:matching-substring>
+ <xsl:choose>
+ <!-- single line comment -->
+ <xsl:when test="regex-group(1)">
+ <xsl:comment>
+ <xsl:value-of select="regex-group(1)"/>
+ </xsl:comment>
+ <xsl:text>&#10;</xsl:text>
+ </xsl:when>
+ <!-- multi line comment -->
+ <xsl:when test="regex-group(2)">
+ <xsl:comment>
+ <xsl:value-of select="regex-group(2)"/>
+ </xsl:comment>
+ </xsl:when>
+ <!-- string -->
+ <xsl:when test="regex-group(3)">
+ <string>
+ <xsl:analyze-string select="regex-group(4)" regex="\\([\\&quot;'/btnvfr])" flags="s">
+ <xsl:matching-substring>
+ <xsl:variable name="s" select="regex-group(1)"/>
+ <xsl:choose>
+ <xsl:when test="$s=('\', '&quot;', '''', '/')">
+ <xsl:value-of select="regex-group(1)"/>
+ </xsl:when>
+ <xsl:when test="$s='b'">
+ <!--xsl:text>&#8;</xsl:text-->
+ <xsl:message select="'escape sequense \b is not supported by XML'"/>
+ <xsl:text>\b</xsl:text>
+ </xsl:when>
+ <xsl:when test="$s='t'">
+ <xsl:text>&#9;</xsl:text>
+ </xsl:when>
+ <xsl:when test="$s='n'">
+ <xsl:text>&#10;</xsl:text>
+ </xsl:when>
+ <xsl:when test="$s='v'">
+ <!--xsl:text>&#11;</xsl:text-->
+ <xsl:message select="'escape sequence \v is not supported by XML'"/>
+ <xsl:text>\v</xsl:text>
+ </xsl:when>
+ <xsl:when test="$s='f'">
+ <!--xsl:text>&#12;</xsl:text-->
+ <xsl:message select="'escape sequence \f is not supported by XML'"/>
+ <xsl:text>\f</xsl:text>
+ </xsl:when>
+ <xsl:when test="$s='r'">
+ <xsl:text>&#13;</xsl:text>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:message terminate="yes" select="'internal error'"/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:matching-substring>
+ <xsl:non-matching-substring>
+ <xsl:value-of select="."/>
+ </xsl:non-matching-substring>
+ </xsl:analyze-string>
+ </string>
+ </xsl:when>
+ <!-- double -->
+ <xsl:when test="regex-group(6)">
+ <double>
+ <xsl:value-of select="regex-group(6)"/>
+ </double>
+ </xsl:when>
+ <!-- integer -->
+ <xsl:when test="regex-group(9)">
+ <integer>
+ <xsl:value-of select="regex-group(9)"/>
+ </integer>
+ </xsl:when>
+ <!-- octal -->
+ <xsl:when test="regex-group(10)">
+ <integer>
+ <xsl:value-of xmlns:Integer="java:java.lang.Integer" select="Integer:parseInt(regex-group(10), 8)"/>
+ </integer>
+ </xsl:when>
+ <!-- hex -->
+ <xsl:when test="regex-group(11)">
+ <integer>
+ <xsl:value-of xmlns:Integer="java:java.lang.Integer" select="Integer:parseInt(replace(regex-group(11), '0x', ''), 16)"/>
+ </integer>
+ </xsl:when>
+ <!-- symbol -->
+ <xsl:when test="regex-group(12)">
+ <symbol>
+ <xsl:value-of select="regex-group(12)"/>
+ </symbol>
+ </xsl:when>
+ <!-- boolean -->
+ <xsl:when test="regex-group(13)">
+ <boolean>
+ <xsl:value-of select="regex-group(13)"/>
+ </boolean>
+ </xsl:when>
+ <!-- null -->
+ <xsl:when test="regex-group(14)">
+ <null />
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:message terminate="yes" select="'internal error'"/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:matching-substring>
+ <xsl:non-matching-substring>
+ <xsl:if test="normalize-space()!=''">
+ <xsl:message select="concat('unknown token: ', .)"/>
+ <xsl:value-of select="."/>
+ </xsl:if>
+ </xsl:non-matching-substring>
+ </xsl:analyze-string>
+ </xsl:variable>
+ <xsl:variable name="mode1">
+ <xsl:apply-templates mode="json2xml1" select="$mode0/node()[1]"/>
+ </xsl:variable>
+ <xsl:variable name="mode2">
+ <xsl:apply-templates mode="json2xml2" select="$mode1"/>
+ </xsl:variable>
+ <xsl:variable name="mode3">
+ <xsl:apply-templates mode="json2xml3" select="$mode2"/>
+ </xsl:variable>
+ <xsl:copy-of select="$mode3"/> <!-- change $mode3 to $mode[0-2] for easy debug -->
+ </xsl:template>
+
+ <!-- json2xml1 mode: group content between {} and [] into object and array elements -->
+
+ <xsl:template mode="json2xml1" match="node()" priority="-9">
+ <xsl:copy-of select="."/>
+ <xsl:apply-templates mode="json2xml1" select="following-sibling::node()[1]"/>
+ </xsl:template>
+
+ <xsl:template mode="json2xml1" match="symbol[.=('}',']')]"/>
+
+ <xsl:template mode="json2xml1" match="symbol[.=('{','[')]">
+ <xsl:element name="{if (.='{') then 'object' else 'array'}">
+ <xsl:apply-templates mode="json2xml1" select="following-sibling::node()[1]"/>
+ </xsl:element>
+ <xsl:variable name="level" select="count(preceding-sibling::symbol[.=('{','[')])-count(preceding-sibling::symbol[.=('}',']')])+1"/>
+ <xsl:variable name="ender"
+ select="following-sibling::symbol[.=('}',']') and count(preceding-sibling::symbol[.=('{','[')])-count(preceding-sibling::symbol[.=('}',']')])=$level][1]"/>
+ <xsl:apply-templates mode="json2xml1" select="$ender/following-sibling::node()[1]"/>
+ </xsl:template>
+
+ <!-- json2xml2 mode: group <string>:<string|integer|double|object|array> into field element -->
+
+ <xsl:template priority="-9" mode="json2xml2" match="@*|node()">
+ <xsl:copy>
+ <xsl:apply-templates mode="json2xml2" select="@*|node()"/>
+ </xsl:copy>
+ </xsl:template>
+
+ <xsl:template mode="json2xml2"
+ match="string[following-sibling::*[1]/self::symbol[.=':'] and following-sibling::*[2]/(self::string|self::integer|self::double|self::boolean|self::object|self::array|self::null)]"/>
+
+ <xsl:template mode="json2xml2"
+ match="symbol[.=':'][preceding-sibling::*[1]/self::string and following-sibling::*[1]/(self::string|self::integer|self::double|self::boolean|self::object|self::array|self::null)]">
+ <field name="{preceding-sibling::*[1]}">
+ <xsl:for-each select="following-sibling::*[1]">
+ <xsl:copy>
+ <xsl:apply-templates mode="json2xml2" select="@*|node()"/>
+ </xsl:copy>
+ </xsl:for-each>
+ </field>
+ </xsl:template>
+
+ <xsl:template mode="json2xml2"
+ match="*[self::string|self::integer|self::double|self::boolean|self::object|self::array|self::null][preceding-sibling::*[2]/self::string and preceding-sibling::*[1]/self::symbol[.=':']]"/>
+
+ <!-- json2xml3 mode: drop comma between consecutive field and object elements -->
+
+ <xsl:template priority="-9" mode="json2xml3" match="@*|node()">
+ <xsl:copy>
+ <xsl:apply-templates mode="json2xml3" select="@*|node()"/>
+ </xsl:copy>
+ </xsl:template>
+
+ <xsl:template mode="json2xml3" match="object/symbol[.=','][preceding-sibling::*[1]/self::field and following-sibling::*[1]/self::field]"/>
+
+ <xsl:template mode="json2xml3" match="array/symbol[.=','][preceding-sibling::*[1]/(self::string|self::integer|self::double|self::boolean|self::object|self::array|self::null) and following-sibling::*[1]/(self::string|self::integer|self::double|self::boolean|self::object|self::array|self::null)]"/>
+
+</xsl:stylesheet> \ No newline at end of file
diff --git a/tests/fixtures/negative-tests.json b/tests/fixtures/negative-tests.json
new file mode 100644
index 0000000..552a6bf
--- /dev/null
+++ b/tests/fixtures/negative-tests.json
@@ -0,0 +1,57 @@
+{
+ "Failure Tests":{
+ "level":4,
+ "variables":{
+ "id" : "thing",
+ "var" : "value",
+ "hello" : "Hello World!",
+ "with space" : "fail",
+ " leading_space" : "Hi!",
+ "trailing_space " : "Bye!",
+ "empty" : "",
+ "path" : "/foo/bar",
+ "x" : "1024",
+ "y" : "768",
+ "list" : ["red", "green", "blue"],
+ "keys" : { "semi" : ";", "dot" : ".", "comma" : ","},
+ "example" : "red",
+ "searchTerms" : "uri templates",
+ "~thing" : "some-user",
+ "default-graph-uri" : ["http://www.example/book/","http://www.example/papers/"],
+ "query" : "PREFIX dc: <http://purl.org/dc/elements/1.1/> SELECT ?book ?who WHERE { ?book dc:creator ?who }"
+
+ },
+ "testcases":[
+ [ "{/id*", false ],
+ [ "/id*}", false ],
+ [ "{/?id}", false ],
+ [ "{var:prefix}", false ],
+ [ "{hello:2*}", false ] ,
+ [ "{??hello}", false ] ,
+ [ "{!hello}", false ] ,
+ [ "{with space}", false],
+ [ "{ leading_space}", false],
+ [ "{trailing_space }", false],
+ [ "{=path}", false ] ,
+ [ "{$var}", false ],
+ [ "{|var*}", false ],
+ [ "{*keys?}", false ],
+ [ "{?empty=default,var}", false ],
+ [ "{var}{-prefix|/-/|var}" , false ],
+ [ "?q={searchTerms}&amp;c={example:color?}" , false ],
+ [ "x{?empty|foo=none}" , false ],
+ [ "/h{#hello+}" , false ],
+ [ "/h#{hello+}" , false ],
+ [ "{keys:1}", false ],
+ [ "{+keys:1}", false ],
+ [ "{;keys:1*}", false ],
+ [ "?{-join|&|var,list}" , false ],
+ [ "/people/{~thing}", false],
+ [ "/{default-graph-uri}", false ],
+ [ "/sparql{?query,default-graph-uri}", false ],
+ [ "/sparql{?query){&default-graph-uri*}", false ],
+ [ "/resolution{?x, y}" , false ]
+
+ ]
+ }
+} \ No newline at end of file
diff --git a/tests/fixtures/spec-examples-by-section.json b/tests/fixtures/spec-examples-by-section.json
new file mode 100644
index 0000000..5aef182
--- /dev/null
+++ b/tests/fixtures/spec-examples-by-section.json
@@ -0,0 +1,439 @@
+{
+ "3.2.1 Variable Expansion" :
+ {
+ "variables": {
+ "count" : ["one", "two", "three"],
+ "dom" : ["example", "com"],
+ "dub" : "me/too",
+ "hello" : "Hello World!",
+ "half" : "50%",
+ "var" : "value",
+ "who" : "fred",
+ "base" : "http://example.com/home/",
+ "path" : "/foo/bar",
+ "list" : ["red", "green", "blue"],
+ "keys" : { "semi" : ";", "dot" : ".", "comma" : ","},
+ "v" : "6",
+ "x" : "1024",
+ "y" : "768",
+ "empty" : "",
+ "empty_keys" : [],
+ "undef" : null
+ },
+ "testcases" : [
+ ["{count}", "one,two,three"],
+ ["{count*}", "one,two,three"],
+ ["{/count}", "/one,two,three"],
+ ["{/count*}", "/one/two/three"],
+ ["{;count}", ";count=one,two,three"],
+ ["{;count*}", ";count=one;count=two;count=three"],
+ ["{?count}", "?count=one,two,three"],
+ ["{?count*}", "?count=one&count=two&count=three"],
+ ["{&count*}", "&count=one&count=two&count=three"]
+ ]
+ },
+ "3.2.2 Simple String Expansion" :
+ {
+ "variables": {
+ "count" : ["one", "two", "three"],
+ "dom" : ["example", "com"],
+ "dub" : "me/too",
+ "hello" : "Hello World!",
+ "half" : "50%",
+ "var" : "value",
+ "who" : "fred",
+ "base" : "http://example.com/home/",
+ "path" : "/foo/bar",
+ "list" : ["red", "green", "blue"],
+ "keys" : { "semi" : ";", "dot" : ".", "comma" : ","},
+ "v" : "6",
+ "x" : "1024",
+ "y" : "768",
+ "empty" : "",
+ "empty_keys" : [],
+ "undef" : null
+ },
+ "testcases" : [
+ ["{var}", "value"],
+ ["{hello}", "Hello%20World%21"],
+ ["{half}", "50%25"],
+ ["O{empty}X", "OX"],
+ ["O{undef}X", "OX"],
+ ["{x,y}", "1024,768"],
+ ["{x,hello,y}", "1024,Hello%20World%21,768"],
+ ["?{x,empty}", "?1024,"],
+ ["?{x,undef}", "?1024"],
+ ["?{undef,y}", "?768"],
+ ["{var:3}", "val"],
+ ["{var:30}", "value"],
+ ["{list}", "red,green,blue"],
+ ["{list*}", "red,green,blue"],
+ ["{keys}", [
+ "comma,%2C,dot,.,semi,%3B",
+ "comma,%2C,semi,%3B,dot,.",
+ "dot,.,comma,%2C,semi,%3B",
+ "dot,.,semi,%3B,comma,%2C",
+ "semi,%3B,comma,%2C,dot,.",
+ "semi,%3B,dot,.,comma,%2C"
+ ]],
+ ["{keys*}", [
+ "comma=%2C,dot=.,semi=%3B",
+ "comma=%2C,semi=%3B,dot=.",
+ "dot=.,comma=%2C,semi=%3B",
+ "dot=.,semi=%3B,comma=%2C",
+ "semi=%3B,comma=%2C,dot=.",
+ "semi=%3B,dot=.,comma=%2C"
+ ]]
+ ]
+ },
+ "3.2.3 Reserved Expansion" :
+ {
+ "variables": {
+ "count" : ["one", "two", "three"],
+ "dom" : ["example", "com"],
+ "dub" : "me/too",
+ "hello" : "Hello World!",
+ "half" : "50%",
+ "var" : "value",
+ "who" : "fred",
+ "base" : "http://example.com/home/",
+ "path" : "/foo/bar",
+ "list" : ["red", "green", "blue"],
+ "keys" : { "semi" : ";", "dot" : ".", "comma" : ","},
+ "v" : "6",
+ "x" : "1024",
+ "y" : "768",
+ "empty" : "",
+ "empty_keys" : [],
+ "undef" : null
+ },
+ "testcases" : [
+ ["{+var}", "value"],
+ ["{/var,empty}", "/value/"],
+ ["{/var,undef}", "/value"],
+ ["{+hello}", "Hello%20World!"],
+ ["{+half}", "50%25"],
+ ["{base}index", "http%3A%2F%2Fexample.com%2Fhome%2Findex"],
+ ["{+base}index", "http://example.com/home/index"],
+ ["O{+empty}X", "OX"],
+ ["O{+undef}X", "OX"],
+ ["{+path}/here", "/foo/bar/here"],
+ ["{+path:6}/here", "/foo/b/here"],
+ ["here?ref={+path}", "here?ref=/foo/bar"],
+ ["up{+path}{var}/here", "up/foo/barvalue/here"],
+ ["{+x,hello,y}", "1024,Hello%20World!,768"],
+ ["{+path,x}/here", "/foo/bar,1024/here"],
+ ["{+list}", "red,green,blue"],
+ ["{+list*}", "red,green,blue"],
+ ["{+keys}", [
+ "comma,,,dot,.,semi,;",
+ "comma,,,semi,;,dot,.",
+ "dot,.,comma,,,semi,;",
+ "dot,.,semi,;,comma,,",
+ "semi,;,comma,,,dot,.",
+ "semi,;,dot,.,comma,,"
+ ]],
+ ["{+keys*}", [
+ "comma=,,dot=.,semi=;",
+ "comma=,,semi=;,dot=.",
+ "dot=.,comma=,,semi=;",
+ "dot=.,semi=;,comma=,",
+ "semi=;,comma=,,dot=.",
+ "semi=;,dot=.,comma=,"
+ ]]
+ ]
+ },
+ "3.2.4 Fragment Expansion" :
+ {
+ "variables": {
+ "count" : ["one", "two", "three"],
+ "dom" : ["example", "com"],
+ "dub" : "me/too",
+ "hello" : "Hello World!",
+ "half" : "50%",
+ "var" : "value",
+ "who" : "fred",
+ "base" : "http://example.com/home/",
+ "path" : "/foo/bar",
+ "list" : ["red", "green", "blue"],
+ "keys" : { "semi" : ";", "dot" : ".", "comma" : ","},
+ "v" : "6",
+ "x" : "1024",
+ "y" : "768",
+ "empty" : "",
+ "empty_keys" : [],
+ "undef" : null
+ },
+ "testcases" : [
+ ["{#var}", "#value"],
+ ["{#hello}", "#Hello%20World!"],
+ ["{#half}", "#50%25"],
+ ["foo{#empty}", "foo#"],
+ ["foo{#undef}", "foo"],
+ ["{#x,hello,y}", "#1024,Hello%20World!,768"],
+ ["{#path,x}/here", "#/foo/bar,1024/here"],
+ ["{#path:6}/here", "#/foo/b/here"],
+ ["{#list}", "#red,green,blue"],
+ ["{#list*}", "#red,green,blue"],
+ ["{#keys}", [
+ "#comma,,,dot,.,semi,;",
+ "#comma,,,semi,;,dot,.",
+ "#dot,.,comma,,,semi,;",
+ "#dot,.,semi,;,comma,,",
+ "#semi,;,comma,,,dot,.",
+ "#semi,;,dot,.,comma,,"
+ ]]
+ ]
+ },
+ "3.2.5 Label Expansion with Dot-Prefix" :
+ {
+ "variables": {
+ "count" : ["one", "two", "three"],
+ "dom" : ["example", "com"],
+ "dub" : "me/too",
+ "hello" : "Hello World!",
+ "half" : "50%",
+ "var" : "value",
+ "who" : "fred",
+ "base" : "http://example.com/home/",
+ "path" : "/foo/bar",
+ "list" : ["red", "green", "blue"],
+ "keys" : { "semi" : ";", "dot" : ".", "comma" : ","},
+ "v" : "6",
+ "x" : "1024",
+ "y" : "768",
+ "empty" : "",
+ "empty_keys" : [],
+ "undef" : null
+ },
+ "testcases" : [
+ ["{.who}", ".fred"],
+ ["{.who,who}", ".fred.fred"],
+ ["{.half,who}", ".50%25.fred"],
+ ["www{.dom*}", "www.example.com"],
+ ["X{.var}", "X.value"],
+ ["X{.var:3}", "X.val"],
+ ["X{.empty}", "X."],
+ ["X{.undef}", "X"],
+ ["X{.list}", "X.red,green,blue"],
+ ["X{.list*}", "X.red.green.blue"],
+ ["{#keys}", [
+ "#comma,,,dot,.,semi,;",
+ "#comma,,,semi,;,dot,.",
+ "#dot,.,comma,,,semi,;",
+ "#dot,.,semi,;,comma,,",
+ "#semi,;,comma,,,dot,.",
+ "#semi,;,dot,.,comma,,"
+ ]],
+ ["{#keys*}", [
+ "#comma=,,dot=.,semi=;",
+ "#comma=,,semi=;,dot=.",
+ "#dot=.,comma=,,semi=;",
+ "#dot=.,semi=;,comma=,",
+ "#semi=;,comma=,,dot=.",
+ "#semi=;,dot=.,comma=,"
+ ]],
+ ["X{.empty_keys}", "X"],
+ ["X{.empty_keys*}", "X"]
+ ]
+ },
+ "3.2.6 Path Segment Expansion" :
+ {
+ "variables": {
+ "count" : ["one", "two", "three"],
+ "dom" : ["example", "com"],
+ "dub" : "me/too",
+ "hello" : "Hello World!",
+ "half" : "50%",
+ "var" : "value",
+ "who" : "fred",
+ "base" : "http://example.com/home/",
+ "path" : "/foo/bar",
+ "list" : ["red", "green", "blue"],
+ "keys" : { "semi" : ";", "dot" : ".", "comma" : ","},
+ "v" : "6",
+ "x" : "1024",
+ "y" : "768",
+ "empty" : "",
+ "empty_keys" : [],
+ "undef" : null
+ },
+ "testcases" : [
+ ["{/who}", "/fred"],
+ ["{/who,who}", "/fred/fred"],
+ ["{/half,who}", "/50%25/fred"],
+ ["{/who,dub}", "/fred/me%2Ftoo"],
+ ["{/var}", "/value"],
+ ["{/var,empty}", "/value/"],
+ ["{/var,undef}", "/value"],
+ ["{/var,x}/here", "/value/1024/here"],
+ ["{/var:1,var}", "/v/value"],
+ ["{/list}", "/red,green,blue"],
+ ["{/list*}", "/red/green/blue"],
+ ["{/list*,path:4}", "/red/green/blue/%2Ffoo"],
+ ["{/keys}", [
+ "/comma,%2C,dot,.,semi,%3B",
+ "/comma,%2C,semi,%3B,dot,.",
+ "/dot,.,comma,%2C,semi,%3B",
+ "/dot,.,semi,%3B,comma,%2C",
+ "/semi,%3B,comma,%2C,dot,.",
+ "/semi,%3B,dot,.,comma,%2C"
+ ]],
+ ["{/keys*}", [
+ "/comma=%2C/dot=./semi=%3B",
+ "/comma=%2C/semi=%3B/dot=.",
+ "/dot=./comma=%2C/semi=%3B",
+ "/dot=./semi=%3B/comma=%2C",
+ "/semi=%3B/comma=%2C/dot=.",
+ "/semi=%3B/dot=./comma=%2C"
+ ]]
+ ]
+ },
+ "3.2.7 Path-Style Parameter Expansion" :
+ {
+ "variables": {
+ "count" : ["one", "two", "three"],
+ "dom" : ["example", "com"],
+ "dub" : "me/too",
+ "hello" : "Hello World!",
+ "half" : "50%",
+ "var" : "value",
+ "who" : "fred",
+ "base" : "http://example.com/home/",
+ "path" : "/foo/bar",
+ "list" : ["red", "green", "blue"],
+ "keys" : { "semi" : ";", "dot" : ".", "comma" : ","},
+ "v" : "6",
+ "x" : "1024",
+ "y" : "768",
+ "empty" : "",
+ "empty_keys" : [],
+ "undef" : null
+ },
+ "testcases" : [
+ ["{;who}", ";who=fred"],
+ ["{;half}", ";half=50%25"],
+ ["{;empty}", ";empty"],
+ ["{;hello:5}", ";hello=Hello"],
+ ["{;v,empty,who}", ";v=6;empty;who=fred"],
+ ["{;v,bar,who}", ";v=6;who=fred"],
+ ["{;x,y}", ";x=1024;y=768"],
+ ["{;x,y,empty}", ";x=1024;y=768;empty"],
+ ["{;x,y,undef}", ";x=1024;y=768"],
+ ["{;list}", ";list=red,green,blue"],
+ ["{;list*}", ";list=red;list=green;list=blue"],
+ ["{;keys}", [
+ ";keys=comma,%2C,dot,.,semi,%3B",
+ ";keys=comma,%2C,semi,%3B,dot,.",
+ ";keys=dot,.,comma,%2C,semi,%3B",
+ ";keys=dot,.,semi,%3B,comma,%2C",
+ ";keys=semi,%3B,comma,%2C,dot,.",
+ ";keys=semi,%3B,dot,.,comma,%2C"
+ ]],
+ ["{;keys*}", [
+ ";comma=%2C;dot=.;semi=%3B",
+ ";comma=%2C;semi=%3B;dot=.",
+ ";dot=.;comma=%2C;semi=%3B",
+ ";dot=.;semi=%3B;comma=%2C",
+ ";semi=%3B;comma=%2C;dot=.",
+ ";semi=%3B;dot=.;comma=%2C"
+ ]]
+ ]
+ },
+ "3.2.8 Form-Style Query Expansion" :
+ {
+ "variables": {
+ "count" : ["one", "two", "three"],
+ "dom" : ["example", "com"],
+ "dub" : "me/too",
+ "hello" : "Hello World!",
+ "half" : "50%",
+ "var" : "value",
+ "who" : "fred",
+ "base" : "http://example.com/home/",
+ "path" : "/foo/bar",
+ "list" : ["red", "green", "blue"],
+ "keys" : { "semi" : ";", "dot" : ".", "comma" : ","},
+ "v" : "6",
+ "x" : "1024",
+ "y" : "768",
+ "empty" : "",
+ "empty_keys" : [],
+ "undef" : null
+ },
+ "testcases" : [
+ ["{?who}", "?who=fred"],
+ ["{?half}", "?half=50%25"],
+ ["{?x,y}", "?x=1024&y=768"],
+ ["{?x,y,empty}", "?x=1024&y=768&empty="],
+ ["{?x,y,undef}", "?x=1024&y=768"],
+ ["{?var:3}", "?var=val"],
+ ["{?list}", "?list=red,green,blue"],
+ ["{?list*}", "?list=red&list=green&list=blue"],
+ ["{?keys}", [
+ "?keys=comma,%2C,dot,.,semi,%3B",
+ "?keys=comma,%2C,semi,%3B,dot,.",
+ "?keys=dot,.,comma,%2C,semi,%3B",
+ "?keys=dot,.,semi,%3B,comma,%2C",
+ "?keys=semi,%3B,comma,%2C,dot,.",
+ "?keys=semi,%3B,dot,.,comma,%2C"
+ ]],
+ ["{?keys*}", [
+ "?comma=%2C&dot=.&semi=%3B",
+ "?comma=%2C&semi=%3B&dot=.",
+ "?dot=.&comma=%2C&semi=%3B",
+ "?dot=.&semi=%3B&comma=%2C",
+ "?semi=%3B&comma=%2C&dot=.",
+ "?semi=%3B&dot=.&comma=%2C"
+ ]]
+ ]
+ },
+ "3.2.9 Form-Style Query Continuation" :
+ {
+ "variables": {
+ "count" : ["one", "two", "three"],
+ "dom" : ["example", "com"],
+ "dub" : "me/too",
+ "hello" : "Hello World!",
+ "half" : "50%",
+ "var" : "value",
+ "who" : "fred",
+ "base" : "http://example.com/home/",
+ "path" : "/foo/bar",
+ "list" : ["red", "green", "blue"],
+ "keys" : { "semi" : ";", "dot" : ".", "comma" : ","},
+ "v" : "6",
+ "x" : "1024",
+ "y" : "768",
+ "empty" : "",
+ "empty_keys" : [],
+ "undef" : null
+ },
+ "testcases" : [
+ ["{&who}", "&who=fred"],
+ ["{&half}", "&half=50%25"],
+ ["?fixed=yes{&x}", "?fixed=yes&x=1024"],
+ ["{&var:3}", "&var=val"],
+ ["{&x,y,empty}", "&x=1024&y=768&empty="],
+ ["{&x,y,undef}", "&x=1024&y=768"],
+ ["{&list}", "&list=red,green,blue"],
+ ["{&list*}", "&list=red&list=green&list=blue"],
+ ["{&keys}", [
+ "&keys=comma,%2C,dot,.,semi,%3B",
+ "&keys=comma,%2C,semi,%3B,dot,.",
+ "&keys=dot,.,comma,%2C,semi,%3B",
+ "&keys=dot,.,semi,%3B,comma,%2C",
+ "&keys=semi,%3B,comma,%2C,dot,.",
+ "&keys=semi,%3B,dot,.,comma,%2C"
+ ]],
+ ["{&keys*}", [
+ "&comma=%2C&dot=.&semi=%3B",
+ "&comma=%2C&semi=%3B&dot=.",
+ "&dot=.&comma=%2C&semi=%3B",
+ "&dot=.&semi=%3B&comma=%2C",
+ "&semi=%3B&comma=%2C&dot=.",
+ "&semi=%3B&dot=.&comma=%2C"
+ ]]
+ ]
+ }
+}
diff --git a/tests/fixtures/spec-examples.json b/tests/fixtures/spec-examples.json
new file mode 100644
index 0000000..2e8e942
--- /dev/null
+++ b/tests/fixtures/spec-examples.json
@@ -0,0 +1,218 @@
+{
+ "Level 1 Examples" :
+ {
+ "level": 1,
+ "variables": {
+ "var" : "value",
+ "hello" : "Hello World!"
+ },
+ "testcases" : [
+ ["{var}", "value"],
+ ["{hello}", "Hello%20World%21"]
+ ]
+ },
+ "Level 2 Examples" :
+ {
+ "level": 2,
+ "variables": {
+ "var" : "value",
+ "hello" : "Hello World!",
+ "path" : "/foo/bar"
+ },
+ "testcases" : [
+ ["{+var}", "value"],
+ ["{+hello}", "Hello%20World!"],
+ ["{+path}/here", "/foo/bar/here"],
+ ["here?ref={+path}", "here?ref=/foo/bar"]
+ ]
+ },
+ "Level 3 Examples" :
+ {
+ "level": 3,
+ "variables": {
+ "var" : "value",
+ "hello" : "Hello World!",
+ "empty" : "",
+ "path" : "/foo/bar",
+ "x" : "1024",
+ "y" : "768"
+ },
+ "testcases" : [
+ ["map?{x,y}", "map?1024,768"],
+ ["{x,hello,y}", "1024,Hello%20World%21,768"],
+ ["{+x,hello,y}", "1024,Hello%20World!,768"],
+ ["{+path,x}/here", "/foo/bar,1024/here"],
+ ["{#x,hello,y}", "#1024,Hello%20World!,768"],
+ ["{#path,x}/here", "#/foo/bar,1024/here"],
+ ["X{.var}", "X.value"],
+ ["X{.x,y}", "X.1024.768"],
+ ["{/var}", "/value"],
+ ["{/var,x}/here", "/value/1024/here"],
+ ["{;x,y}", ";x=1024;y=768"],
+ ["{;x,y,empty}", ";x=1024;y=768;empty"],
+ ["{?x,y}", "?x=1024&y=768"],
+ ["{?x,y,empty}", "?x=1024&y=768&empty="],
+ ["?fixed=yes{&x}", "?fixed=yes&x=1024"],
+ ["{&x,y,empty}", "&x=1024&y=768&empty="]
+ ]
+ },
+ "Level 4 Examples" :
+ {
+ "level": 4,
+ "variables": {
+ "var": "value",
+ "hello": "Hello World!",
+ "path": "/foo/bar",
+ "list": ["red", "green", "blue"],
+ "keys": {"semi": ";", "dot": ".", "comma":","}
+ },
+ "testcases": [
+ ["{var:3}", "val"],
+ ["{var:30}", "value"],
+ ["{list}", "red,green,blue"],
+ ["{list*}", "red,green,blue"],
+ ["{keys}", [
+ "comma,%2C,dot,.,semi,%3B",
+ "comma,%2C,semi,%3B,dot,.",
+ "dot,.,comma,%2C,semi,%3B",
+ "dot,.,semi,%3B,comma,%2C",
+ "semi,%3B,comma,%2C,dot,.",
+ "semi,%3B,dot,.,comma,%2C"
+ ]],
+ ["{keys*}", [
+ "comma=%2C,dot=.,semi=%3B",
+ "comma=%2C,semi=%3B,dot=.",
+ "dot=.,comma=%2C,semi=%3B",
+ "dot=.,semi=%3B,comma=%2C",
+ "semi=%3B,comma=%2C,dot=.",
+ "semi=%3B,dot=.,comma=%2C"
+ ]],
+ ["{+path:6}/here", "/foo/b/here"],
+ ["{+list}", "red,green,blue"],
+ ["{+list*}", "red,green,blue"],
+ ["{+keys}", [
+ "comma,,,dot,.,semi,;",
+ "comma,,,semi,;,dot,.",
+ "dot,.,comma,,,semi,;",
+ "dot,.,semi,;,comma,,",
+ "semi,;,comma,,,dot,.",
+ "semi,;,dot,.,comma,,"
+ ]],
+ ["{+keys*}", [
+ "comma=,,dot=.,semi=;",
+ "comma=,,semi=;,dot=.",
+ "dot=.,comma=,,semi=;",
+ "dot=.,semi=;,comma=,",
+ "semi=;,comma=,,dot=.",
+ "semi=;,dot=.,comma=,"
+ ]],
+ ["{#path:6}/here", "#/foo/b/here"],
+ ["{#list}", "#red,green,blue"],
+ ["{#list*}", "#red,green,blue"],
+ ["{#keys}", [
+ "#comma,,,dot,.,semi,;",
+ "#comma,,,semi,;,dot,.",
+ "#dot,.,comma,,,semi,;",
+ "#dot,.,semi,;,comma,,",
+ "#semi,;,comma,,,dot,.",
+ "#semi,;,dot,.,comma,,"
+ ]],
+ ["{#keys*}", [
+ "#comma=,,dot=.,semi=;",
+ "#comma=,,semi=;,dot=.",
+ "#dot=.,comma=,,semi=;",
+ "#dot=.,semi=;,comma=,",
+ "#semi=;,comma=,,dot=.",
+ "#semi=;,dot=.,comma=,"
+ ]],
+ ["X{.var:3}", "X.val"],
+ ["X{.list}", "X.red,green,blue"],
+ ["X{.list*}", "X.red.green.blue"],
+ ["X{.keys}", [
+ "X.comma,%2C,dot,.,semi,%3B",
+ "X.comma,%2C,semi,%3B,dot,.",
+ "X.dot,.,comma,%2C,semi,%3B",
+ "X.dot,.,semi,%3B,comma,%2C",
+ "X.semi,%3B,comma,%2C,dot,.",
+ "X.semi,%3B,dot,.,comma,%2C"
+ ]],
+ ["{/var:1,var}", "/v/value"],
+ ["{/list}", "/red,green,blue"],
+ ["{/list*}", "/red/green/blue"],
+ ["{/list*,path:4}", "/red/green/blue/%2Ffoo"],
+ ["{/keys}", [
+ "/comma,%2C,dot,.,semi,%3B",
+ "/comma,%2C,semi,%3B,dot,.",
+ "/dot,.,comma,%2C,semi,%3B",
+ "/dot,.,semi,%3B,comma,%2C",
+ "/semi,%3B,comma,%2C,dot,.",
+ "/semi,%3B,dot,.,comma,%2C"
+ ]],
+ ["{/keys*}", [
+ "/comma=%2C/dot=./semi=%3B",
+ "/comma=%2C/semi=%3B/dot=.",
+ "/dot=./comma=%2C/semi=%3B",
+ "/dot=./semi=%3B/comma=%2C",
+ "/semi=%3B/comma=%2C/dot=.",
+ "/semi=%3B/dot=./comma=%2C"
+ ]],
+ ["{;hello:5}", ";hello=Hello"],
+ ["{;list}", ";list=red,green,blue"],
+ ["{;list*}", ";list=red;list=green;list=blue"],
+ ["{;keys}", [
+ ";keys=comma,%2C,dot,.,semi,%3B",
+ ";keys=comma,%2C,semi,%3B,dot,.",
+ ";keys=dot,.,comma,%2C,semi,%3B",
+ ";keys=dot,.,semi,%3B,comma,%2C",
+ ";keys=semi,%3B,comma,%2C,dot,.",
+ ";keys=semi,%3B,dot,.,comma,%2C"
+ ]],
+ ["{;keys*}", [
+ ";comma=%2C;dot=.;semi=%3B",
+ ";comma=%2C;semi=%3B;dot=.",
+ ";dot=.;comma=%2C;semi=%3B",
+ ";dot=.;semi=%3B;comma=%2C",
+ ";semi=%3B;comma=%2C;dot=.",
+ ";semi=%3B;dot=.;comma=%2C"
+ ]],
+ ["{?var:3}", "?var=val"],
+ ["{?list}", "?list=red,green,blue"],
+ ["{?list*}", "?list=red&list=green&list=blue"],
+ ["{?keys}", [
+ "?keys=comma,%2C,dot,.,semi,%3B",
+ "?keys=comma,%2C,semi,%3B,dot,.",
+ "?keys=dot,.,comma,%2C,semi,%3B",
+ "?keys=dot,.,semi,%3B,comma,%2C",
+ "?keys=semi,%3B,comma,%2C,dot,.",
+ "?keys=semi,%3B,dot,.,comma,%2C"
+ ]],
+ ["{?keys*}", [
+ "?comma=%2C&dot=.&semi=%3B",
+ "?comma=%2C&semi=%3B&dot=.",
+ "?dot=.&comma=%2C&semi=%3B",
+ "?dot=.&semi=%3B&comma=%2C",
+ "?semi=%3B&comma=%2C&dot=.",
+ "?semi=%3B&dot=.&comma=%2C"
+ ]],
+ ["{&var:3}", "&var=val"],
+ ["{&list}", "&list=red,green,blue"],
+ ["{&list*}", "&list=red&list=green&list=blue"],
+ ["{&keys}", [
+ "&keys=comma,%2C,dot,.,semi,%3B",
+ "&keys=comma,%2C,semi,%3B,dot,.",
+ "&keys=dot,.,comma,%2C,semi,%3B",
+ "&keys=dot,.,semi,%3B,comma,%2C",
+ "&keys=semi,%3B,comma,%2C,dot,.",
+ "&keys=semi,%3B,dot,.,comma,%2C"
+ ]],
+ ["{&keys*}", [
+ "&comma=%2C&dot=.&semi=%3B",
+ "&comma=%2C&semi=%3B&dot=.",
+ "&dot=.&comma=%2C&semi=%3B",
+ "&dot=.&semi=%3B&comma=%2C",
+ "&semi=%3B&comma=%2C&dot=.",
+ "&semi=%3B&dot=.&comma=%2C"
+ ]]
+ ]
+ }
+}
diff --git a/tests/fixtures/transform-json-tests.xslt b/tests/fixtures/transform-json-tests.xslt
new file mode 100644
index 0000000..d956b6b
--- /dev/null
+++ b/tests/fixtures/transform-json-tests.xslt
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+ <xsl:include href="json2xml.xslt"/>
+
+ <!-- the input test files need to be supplied as a sequence of file names only (no extension) -->
+ <xsl:param name="tests" select="('extended-tests', 'negative-tests', 'spec-examples-by-section', 'spec-examples')"/>
+ <xsl:param name="json-ext" select="'json'"/>
+ <xsl:param name="xml-ext" select="'xml'"/>
+
+ <!-- run this stylesheet with any input to generate the XML test files at output. -->
+ <!-- (a popular way of doing this is to supply the XSLT to itself as the input file.) -->
+
+ <xsl:template match="/">
+ <xsl:for-each select="$tests">
+ <xsl:variable name="file" select="current()"/>
+ <xsl:variable name="json">
+ <json>
+ <xsl:value-of select="unparsed-text(concat($file, '.', $json-ext))"/>
+ </json>
+ </xsl:variable>
+ <xsl:variable name="xml">
+ <xsl:apply-templates select="$json/json"/>
+ </xsl:variable>
+ <xsl:result-document href="{$file}.{$xml-ext}" method="xml" indent="yes">
+ <tests>
+ <xsl:for-each select="$xml/json/object/field">
+ <test name="{@name}">
+ <xsl:if test="exists(object/field[1][@name eq 'level'])">
+ <xsl:attribute name="level" select="object/field[1][@name eq 'level']/integer/text()"/>
+ </xsl:if>
+ <variables>
+ <xsl:for-each select="object/field[@name eq 'variables']/object/field">
+ <variable name="{@name}">
+ <xsl:copy-of select="*"/>
+ </variable>
+ </xsl:for-each>
+ </variables>
+ <testcases>
+ <xsl:for-each select="object/field[@name eq 'testcases']/array/array">
+ <testcase template="{*[1]}" result="{*[2]}"/>
+ </xsl:for-each> </testcases>
+ </test>
+ </xsl:for-each>
+ </tests>
+ </xsl:result-document>
+ </xsl:for-each>
+ </xsl:template>
+
+</xsl:stylesheet> \ No newline at end of file
diff --git a/tests/test_from_fixtures.py b/tests/test_from_fixtures.py
new file mode 100644
index 0000000..d6c9810
--- /dev/null
+++ b/tests/test_from_fixtures.py
@@ -0,0 +1,117 @@
+import io
+import json
+import os.path
+
+import uritemplate
+
+
+def fixture_file_path(filename):
+ absolute_dir = os.path.abspath(os.path.dirname(__file__))
+ filename = filename + '.json'
+ return os.path.join(absolute_dir, 'fixtures', filename)
+
+
+def load_examples(filename):
+ path = fixture_file_path(filename)
+ with io.open(path, 'r', encoding="utf-8") as examples_file:
+ examples = json.load(examples_file)
+ return examples
+
+
+def expected_set(expected):
+ if isinstance(expected, list):
+ return set(expected)
+ return {expected}
+
+
+class FixtureMixin(object):
+ def _get_test(self, section):
+ test = self.examples.get(section, {})
+ return test.get('variables', {}), test.get('testcases', [])
+
+ def _test(self, testname):
+ variables, testcases = self._get_test(testname)
+ for template, expected in testcases:
+ expected = expected_set(expected)
+ expanded = uritemplate.expand(template, variables)
+ assert expanded in expected
+
+
+class TestSpecExamples(FixtureMixin):
+ examples = load_examples('spec-examples')
+
+ def test_level_1(self):
+ """Check that uritemplate.expand matches Level 1 expectations."""
+ self._test('Level 1 Examples')
+
+ def test_level_2(self):
+ """Check that uritemplate.expand matches Level 2 expectations."""
+ self._test('Level 2 Examples')
+
+ def test_level_3(self):
+ """Check that uritemplate.expand matches Level 3 expectations."""
+ self._test('Level 3 Examples')
+
+ def test_level_4(self):
+ """Check that uritemplate.expand matches Level 4 expectations."""
+ self._test('Level 4 Examples')
+
+
+class TestSpecExamplesByRFCSection(FixtureMixin):
+ examples = load_examples('spec-examples-by-section')
+
+ def test_variable_expansion(self):
+ """Check variable expansion."""
+ self._test('3.2.1 Variable Expansion')
+
+ def test_simple_string_expansion(self):
+ """Check simple string expansion."""
+ self._test('3.2.2 Simple String Expansion')
+
+ def test_reserved_expansion(self):
+ """Check reserved expansion."""
+ self._test('3.2.3 Reserved Expansion')
+
+ def test_fragment_expansion(self):
+ """Check fragment expansion."""
+ self._test('3.2.4 Fragment Expansion')
+
+ def test_dot_prefixed_label_expansion(self):
+ """Check label expansion with dot-prefix."""
+ self._test('3.2.5 Label Expansion with Dot-Prefix')
+
+ def test_path_segment_expansion(self):
+ """Check path segment expansion."""
+ self._test('3.2.6 Path Segment Expansion')
+
+ def test_path_style_parameter_expansion(self):
+ """Check path-style param expansion."""
+ self._test('3.2.7 Path-Style Parameter Expansion')
+
+ def test_form_style_query_expansion(self):
+ """Check form-style query expansion."""
+ self._test('3.2.8 Form-Style Query Expansion')
+
+ def test_form_style_query_cntinuation(self):
+ """Check form-style query continuation."""
+ self._test('3.2.9 Form-Style Query Continuation')
+
+
+class TestExtendedTests(FixtureMixin):
+ examples = load_examples('extended-tests')
+
+ def test_additional_examples_1(self):
+ """Check Additional Examples 1."""
+ self._test('Additional Examples 1')
+
+ def test_additional_examples_2(self):
+ """Check Additional Examples 2."""
+ self._test('Additional Examples 2')
+
+ def test_additional_examples_3(self):
+ """Check Additional Examples 3."""
+ self._test('Additional Examples 3: Empty Variables')
+
+ def test_additional_examples_4(self):
+ """Check Additional Examples 4."""
+ self._test('Additional Examples 4: Numeric Keys')
diff --git a/tests/test_uritemplate.py b/tests/test_uritemplate.py
new file mode 100644
index 0000000..f67ad26
--- /dev/null
+++ b/tests/test_uritemplate.py
@@ -0,0 +1,635 @@
+from unittest import TestCase, main
+from uritemplate import URITemplate, expand, partial, variables
+from uritemplate import variable
+
+
+def merge_dicts(*args):
+ d = {}
+ for arg in args:
+ d.update(arg)
+ return d
+
+
+class RFCTemplateExamples(type):
+ var = {'var': 'value'}
+ hello = {'hello': 'Hello World!'}
+ path = {'path': '/foo/bar'}
+ x = {'x': '1024'}
+ y = {'y': '768'}
+ empty = {'empty': ''}
+ merged_x_y = merge_dicts(x, y)
+ list_ex = {'list': ['red', 'green', 'blue']}
+ keys = {'keys': [('semi', ';'), ('dot', '.'), ('comma', ',')]}
+
+ # # Level 1
+ # Simple string expansion
+ level1_examples = {
+ '{var}': {
+ 'expansion': var,
+ 'expected': 'value',
+ },
+ '{hello}': {
+ 'expansion': hello,
+ 'expected': 'Hello%20World%21',
+ },
+ }
+
+ # # Level 2
+ # Reserved string expansion
+ level2_reserved_examples = {
+ '{+var}': {
+ 'expansion': var,
+ 'expected': 'value',
+ },
+ '{+hello}': {
+ 'expansion': hello,
+ 'expected': 'Hello%20World!',
+ },
+ '{+path}/here': {
+ 'expansion': path,
+ 'expected': '/foo/bar/here',
+ },
+ 'here?ref={+path}': {
+ 'expansion': path,
+ 'expected': 'here?ref=/foo/bar',
+ },
+ }
+
+ # Fragment expansion, crosshatch-prefixed
+ level2_fragment_examples = {
+ 'X{#var}': {
+ 'expansion': var,
+ 'expected': 'X#value',
+ },
+ 'X{#hello}': {
+ 'expansion': hello,
+ 'expected': 'X#Hello%20World!'
+ },
+ }
+
+ # # Level 3
+ # String expansion with multiple variables
+ level3_multiple_variable_examples = {
+ 'map?{x,y}': {
+ 'expansion': merged_x_y,
+ 'expected': 'map?1024,768',
+ },
+ '{x,hello,y}': {
+ 'expansion': merge_dicts(x, y, hello),
+ 'expected': '1024,Hello%20World%21,768',
+ },
+ }
+
+ # Reserved expansion with multiple variables
+ level3_reserved_examples = {
+ '{+x,hello,y}': {
+ 'expansion': merge_dicts(x, y, hello),
+ 'expected': '1024,Hello%20World!,768',
+ },
+ '{+path,x}/here': {
+ 'expansion': merge_dicts(path, x),
+ 'expected': '/foo/bar,1024/here',
+ },
+ }
+
+ # Fragment expansion with multiple variables
+ level3_fragment_examples = {
+ '{#x,hello,y}': {
+ 'expansion': merge_dicts(x, y, hello),
+ 'expected': '#1024,Hello%20World!,768',
+ },
+ '{#path,x}/here': {
+ 'expansion': merge_dicts(path, x),
+ 'expected': '#/foo/bar,1024/here'
+ },
+ }
+
+ # Label expansion, dot-prefixed
+ level3_label_examples = {
+ 'X{.var}': {
+ 'expansion': var,
+ 'expected': 'X.value',
+ },
+ 'X{.x,y}': {
+ 'expansion': merged_x_y,
+ 'expected': 'X.1024.768',
+ }
+ }
+
+ # Path segments, slash-prefixed
+ level3_path_segment_examples = {
+ '{/var}': {
+ 'expansion': var,
+ 'expected': '/value',
+ },
+ '{/var,x}/here': {
+ 'expansion': merge_dicts(var, x),
+ 'expected': '/value/1024/here',
+ },
+ }
+
+ # Path-style parameters, semicolon-prefixed
+ level3_path_semi_examples = {
+ '{;x,y}': {
+ 'expansion': merged_x_y,
+ 'expected': ';x=1024;y=768',
+ },
+ '{;x,y,empty}': {
+ 'expansion': merge_dicts(x, y, empty),
+ 'expected': ';x=1024;y=768;empty',
+ },
+ }
+
+ # Form-style query, ampersand-separated
+ level3_form_amp_examples = {
+ '{?x,y}': {
+ 'expansion': merged_x_y,
+ 'expected': '?x=1024&y=768',
+ },
+ '{?x,y,empty}': {
+ 'expansion': merge_dicts(x, y, empty),
+ 'expected': '?x=1024&y=768&empty=',
+ },
+ }
+
+ # Form-style query continuation
+ level3_form_cont_examples = {
+ '?fixed=yes{&x}': {
+ 'expansion': x,
+ 'expected': '?fixed=yes&x=1024',
+ },
+ '{&x,y,empty}': {
+ 'expansion': merge_dicts(x, y, empty),
+ 'expected': '&x=1024&y=768&empty=',
+ }
+ }
+
+ # # Level 4
+ # String expansion with value modifiers
+ level4_value_modifier_examples = {
+ '{var:3}': {
+ 'expansion': var,
+ 'expected': 'val',
+ },
+ '{var:30}': {
+ 'expansion': var,
+ 'expected': 'value',
+ },
+ '{list}': {
+ 'expansion': list_ex,
+ 'expected': 'red,green,blue',
+ },
+ '{list*}': {
+ 'expansion': list_ex,
+ 'expected': 'red,green,blue',
+ },
+ '{keys}': {
+ 'expansion': keys,
+ 'expected': 'semi,%3B,dot,.,comma,%2C',
+ },
+ '{keys*}': {
+ 'expansion': keys,
+ 'expected': 'semi=%3B,dot=.,comma=%2C',
+ },
+ }
+
+ # Reserved expansion with value modifiers
+ level4_reserved_examples = {
+ '{+path:6}/here': {
+ 'expansion': path,
+ 'expected': '/foo/b/here',
+ },
+ '{+list}': {
+ 'expansion': list_ex,
+ 'expected': 'red,green,blue',
+ },
+ '{+list*}': {
+ 'expansion': list_ex,
+ 'expected': 'red,green,blue',
+ },
+ '{+keys}': {
+ 'expansion': keys,
+ 'expected': 'semi,;,dot,.,comma,,',
+ },
+ '{+keys*}': {
+ 'expansion': keys,
+ 'expected': 'semi=;,dot=.,comma=,',
+ },
+ }
+
+ # Fragment expansion with value modifiers
+ level4_fragment_examples = {
+ '{#path:6}/here': {
+ 'expansion': path,
+ 'expected': '#/foo/b/here',
+ },
+ '{#list}': {
+ 'expansion': list_ex,
+ 'expected': '#red,green,blue',
+ },
+ '{#list*}': {
+ 'expansion': list_ex,
+ 'expected': '#red,green,blue',
+ },
+ '{#keys}': {
+ 'expansion': keys,
+ 'expected': '#semi,;,dot,.,comma,,'
+ },
+ '{#keys*}': {
+ 'expansion': keys,
+ 'expected': '#semi=;,dot=.,comma=,'
+ },
+ }
+
+ # Label expansion, dot-prefixed
+ level4_label_examples = {
+ 'X{.var:3}': {
+ 'expansion': var,
+ 'expected': 'X.val',
+ },
+ 'X{.list}': {
+ 'expansion': list_ex,
+ 'expected': 'X.red,green,blue',
+ },
+ 'X{.list*}': {
+ 'expansion': list_ex,
+ 'expected': 'X.red.green.blue',
+ },
+ 'X{.keys}': {
+ 'expansion': keys,
+ 'expected': 'X.semi,%3B,dot,.,comma,%2C',
+ },
+ 'X{.keys*}': {
+ 'expansion': keys,
+ 'expected': 'X.semi=%3B.dot=..comma=%2C',
+ },
+ }
+
+ # Path segments, slash-prefixed
+ level4_path_slash_examples = {
+ '{/var:1,var}': {
+ 'expansion': var,
+ 'expected': '/v/value',
+ },
+ '{/list}': {
+ 'expansion': list_ex,
+ 'expected': '/red,green,blue',
+ },
+ '{/list*}': {
+ 'expansion': list_ex,
+ 'expected': '/red/green/blue',
+ },
+ '{/list*,path:4}': {
+ 'expansion': merge_dicts(list_ex, path),
+ 'expected': '/red/green/blue/%2Ffoo',
+ },
+ '{/keys}': {
+ 'expansion': keys,
+ 'expected': '/semi,%3B,dot,.,comma,%2C',
+ },
+ '{/keys*}': {
+ 'expansion': keys,
+ 'expected': '/semi=%3B/dot=./comma=%2C',
+ },
+ }
+
+ # Path-style parameters, semicolon-prefixed
+ level4_path_semi_examples = {
+ '{;hello:5}': {
+ 'expansion': hello,
+ 'expected': ';hello=Hello',
+ },
+ '{;list}': {
+ 'expansion': list_ex,
+ 'expected': ';list=red,green,blue',
+ },
+ '{;list*}': {
+ 'expansion': list_ex,
+ 'expected': ';list=red;list=green;list=blue',
+ },
+ '{;keys}': {
+ 'expansion': keys,
+ 'expected': ';keys=semi,%3B,dot,.,comma,%2C',
+ },
+ '{;keys*}': {
+ 'expansion': keys,
+ 'expected': ';semi=%3B;dot=.;comma=%2C',
+ },
+ }
+
+ # Form-style query, ampersand-separated
+ level4_form_amp_examples = {
+ '{?var:3}': {
+ 'expansion': var,
+ 'expected': '?var=val',
+ },
+ '{?list}': {
+ 'expansion': list_ex,
+ 'expected': '?list=red,green,blue',
+ },
+ '{?list*}': {
+ 'expansion': list_ex,
+ 'expected': '?list=red&list=green&list=blue',
+ },
+ '{?keys}': {
+ 'expansion': keys,
+ 'expected': '?keys=semi,%3B,dot,.,comma,%2C',
+ },
+ '{?keys*}': {
+ 'expansion': keys,
+ 'expected': '?semi=%3B&dot=.&comma=%2C',
+ },
+ }
+
+ # Form-style query continuation
+ level4_form_query_examples = {
+ '{&var:3}': {
+ 'expansion': var,
+ 'expected': '&var=val',
+ },
+ '{&list}': {
+ 'expansion': list_ex,
+ 'expected': '&list=red,green,blue',
+ },
+ '{&list*}': {
+ 'expansion': list_ex,
+ 'expected': '&list=red&list=green&list=blue',
+ },
+ '{&keys}': {
+ 'expansion': keys,
+ 'expected': '&keys=semi,%3B,dot,.,comma,%2C',
+ },
+ '{&keys*}': {
+ 'expansion': keys,
+ 'expected': '&semi=%3B&dot=.&comma=%2C',
+ },
+ }
+
+ def __new__(cls, name, bases, attrs):
+ def make_test(d):
+ def _test_(self):
+ for k, v in d.items():
+ t = URITemplate(k)
+ self.assertEqual(t.expand(v['expansion']), v['expected'])
+ return _test_
+
+ examples = [
+ (
+ n, getattr(RFCTemplateExamples, n)
+ ) for n in dir(RFCTemplateExamples) if n.startswith('level')
+ ]
+
+ for name, value in examples:
+ testname = 'test_%s' % name
+ attrs[testname] = make_test(value)
+
+ return type.__new__(cls, name, bases, attrs)
+
+
+class TestURITemplate(RFCTemplateExamples('RFCMeta', (TestCase,), {})):
+ def test_no_variables_in_uri(self):
+ """
+ This test ensures that if there are no variables present, the
+ template evaluates to itself.
+ """
+ uri = 'https://api.github.com/users'
+ t = URITemplate(uri)
+ self.assertEqual(t.expand(), uri)
+ self.assertEqual(t.expand(users='foo'), uri)
+
+ def test_all_variables_parsed(self):
+ """
+ This test ensures that all variables are parsed.
+ """
+ uris = [
+ 'https://api.github.com',
+ 'https://api.github.com/users{/user}',
+ 'https://api.github.com/repos{/user}{/repo}',
+ 'https://api.github.com/repos{/user}{/repo}/issues{/issue}'
+ ]
+
+ for i, uri in enumerate(uris):
+ t = URITemplate(uri)
+ self.assertEqual(len(t.variables), i)
+
+ def test_expand(self):
+ """
+ This test ensures that expansion works as expected.
+ """
+ # Single
+ t = URITemplate('https://api.github.com/users{/user}')
+ expanded = 'https://api.github.com/users/sigmavirus24'
+ self.assertEqual(t.expand(user='sigmavirus24'), expanded)
+ v = t.variables[0]
+ self.assertEqual(v.expand({'user': None}), {'/user': ''})
+
+ # Multiple
+ t = URITemplate('https://api.github.com/users{/user}{/repo}')
+ expanded = 'https://api.github.com/users/sigmavirus24/github3.py'
+ self.assertEqual(
+ t.expand({'repo': 'github3.py'}, user='sigmavirus24'),
+ expanded
+ )
+
+ def test_str_repr(self):
+ uri = 'https://api.github.com{/endpoint}'
+ t = URITemplate(uri)
+ self.assertEqual(str(t), uri)
+ self.assertEqual(str(t.variables[0]), '/endpoint')
+ self.assertEqual(repr(t), 'URITemplate("%s")' % uri)
+ self.assertEqual(repr(t.variables[0]), 'URIVariable(/endpoint)')
+
+ def test_hash(self):
+ uri = 'https://api.github.com{/endpoint}'
+ self.assertEqual(hash(URITemplate(uri)), hash(uri))
+
+ def test_default_value(self):
+ uri = 'https://api.github.com/user{/user=sigmavirus24}'
+ t = URITemplate(uri)
+ self.assertEqual(t.expand(),
+ 'https://api.github.com/user/sigmavirus24')
+ self.assertEqual(t.expand(user='lukasa'),
+ 'https://api.github.com/user/lukasa')
+
+ def test_query_expansion(self):
+ t = URITemplate('{foo}')
+ self.assertEqual(
+ t.variables[0]._query_expansion('foo', None, False, False), None
+ )
+
+ def test_label_path_expansion(self):
+ t = URITemplate('{foo}')
+ self.assertEqual(
+ t.variables[0]._label_path_expansion('foo', None, False, False),
+ 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]
+ self.assertEqual(
+ v._semi_path_expansion('foo', None, False, False),
+ None
+ )
+ t.variables[0].operator = '?'
+ self.assertEqual(
+ v._semi_path_expansion('foo', ['bar', 'bogus'], True, False),
+ 'foo=bar&foo=bogus'
+ )
+
+ def test_string_expansion(self):
+ t = URITemplate('{foo}')
+ self.assertEqual(
+ t.variables[0]._string_expansion('foo', None, False, False),
+ None
+ )
+
+ def test_hashability(self):
+ t = URITemplate('{foo}')
+ u = URITemplate('{foo}')
+ d = {t: 1}
+ d[u] += 1
+ self.assertEqual(d, {t: 2})
+
+ def test_no_mutate(self):
+ args = {}
+ t = URITemplate('')
+ t.expand(args, key=1)
+ self.assertEqual(args, {})
+
+
+class TestURIVariable(TestCase):
+ def setUp(self):
+ self.v = variable.URIVariable('{foo}')
+
+ def test_post_parse(self):
+ v = self.v
+ self.assertEqual(v.join_str, ',')
+ self.assertEqual(v.operator, '')
+ self.assertEqual(v.safe, '')
+ self.assertEqual(v.start, '')
+
+ def test_post_parse_plus(self):
+ v = self.v
+ v.operator = '+'
+ v.post_parse()
+ self.assertEqual(v.join_str, ',')
+ self.assertEqual(v.safe, variable.URIVariable.reserved)
+ self.assertEqual(v.start, '')
+
+ def test_post_parse_octothorpe(self):
+ v = self.v
+ v.operator = '#'
+ v.post_parse()
+ self.assertEqual(v.join_str, ',')
+ self.assertEqual(v.safe, variable.URIVariable.reserved)
+ self.assertEqual(v.start, '#')
+
+ def test_post_parse_question(self):
+ v = self.v
+ v.operator = '?'
+ v.post_parse()
+ self.assertEqual(v.join_str, '&')
+ self.assertEqual(v.safe, '')
+ self.assertEqual(v.start, '?')
+
+ def test_post_parse_ampersand(self):
+ v = self.v
+ v.operator = '&'
+ v.post_parse()
+ self.assertEqual(v.join_str, '&')
+ self.assertEqual(v.safe, '')
+ self.assertEqual(v.start, '&')
+
+
+class TestVariableModule(TestCase):
+ def test_is_list_of_tuples(self):
+ a_list = [(1, 2), (3, 4)]
+ self.assertEqual(variable.is_list_of_tuples(a_list), (True, a_list))
+
+ a_list = [1, 2, 3, 4]
+ self.assertEqual(variable.is_list_of_tuples(a_list), (False, None))
+
+ def test_list_test(self):
+ a_list = [1, 2, 3, 4]
+ self.assertEqual(variable.list_test(a_list), True)
+
+ a_list = str([1, 2, 3, 4])
+ self.assertEqual(variable.list_test(a_list), False)
+
+ def test_list_of_tuples_test(self):
+ a_list = [(1, 2), (3, 4)]
+ self.assertEqual(variable.dict_test(a_list), False)
+
+ d = dict(a_list)
+ self.assertEqual(variable.dict_test(d), True)
+
+
+class TestAPI(TestCase):
+ uri = 'https://api.github.com{/endpoint}'
+
+ def test_expand(self):
+ self.assertEqual(expand(self.uri, {'endpoint': 'users'}),
+ 'https://api.github.com/users')
+
+ def test_partial(self):
+ self.assertEqual(partial(self.uri), URITemplate(self.uri))
+ uri = self.uri + '/sigmavirus24{/other}'
+ self.assertEqual(
+ partial(uri, endpoint='users'),
+ URITemplate('https://api.github.com/users/sigmavirus24{/other}')
+ )
+
+ def test_variables(self):
+ self.assertEqual(variables(self.uri),
+ URITemplate(self.uri).variable_names)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..ed27494
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,31 @@
+[tox]
+envlist =
+ py27,
+ py34,
+ py35,
+ py36,
+ py37,
+ py38,
+ pep8,
+
+[testenv]
+deps =
+ pytest
+commands = pytest {posargs}
+
+[testenv:pep8]
+deps =
+ flake8
+commands =
+ flake8 {posargs} uritemplate tests setup.py
+
+[testenv:release]
+deps =
+ wheel
+ twine>=1.8.0
+commands =
+ python setup.py -q sdist bdist_wheel
+ twine upload --skip-existing dist/*
+
+[flake8]
+exclude = docs/
diff --git a/uritemplate/__init__.py b/uritemplate/__init__.py
index 712405d..5352520 100755..100644
--- a/uritemplate/__init__.py
+++ b/uritemplate/__init__.py
@@ -1,265 +1,26 @@
-#!/usr/bin/env python
-
-"""
-URI Template (RFC6570) Processor
-"""
-
-__copyright__ = """\
-Copyright 2011-2013 Joe Gregorio
-
-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 re
-try:
- from urllib.parse import quote
-except ImportError:
- from urllib import quote
-
-
-
-__version__ = "0.6"
-
-RESERVED = ":/?#[]@!$&'()*+,;="
-OPERATOR = "+#./;?&|!@"
-MODIFIER = ":^"
-TEMPLATE = re.compile("{([^\}]+)}")
-
+uritemplate
+===========
-def variables(template):
- '''Returns the set of keywords in a uri template'''
- vars = set()
- for varlist in TEMPLATE.findall(template):
- if varlist[0] in OPERATOR:
- varlist = varlist[1:]
- varspecs = varlist.split(',')
- for var in varspecs:
- # handle prefix values
- var = var.split(':')[0]
- # handle composite values
- if var.endswith('*'):
- var = var[:-1]
- vars.add(var)
- return vars
+The URI templating library for humans.
+See http://uritemplate.rtfd.org/ for documentation
-def _quote(value, safe, prefix=None):
- if prefix is not None:
- return quote(str(value)[:prefix], safe)
- return quote(str(value), safe)
+:copyright: (c) 2013-2015 Ian Cordasco
+:license: Modified BSD, see LICENSE for more details
+"""
-def _tostring(varname, value, explode, prefix, operator, safe=""):
- if isinstance(value, list):
- return ",".join([_quote(x, safe) for x in value])
- if isinstance(value, dict):
- keys = sorted(value.keys())
- if explode:
- return ",".join([_quote(key, safe) + "=" + \
- _quote(value[key], safe) for key in keys])
- else:
- return ",".join([_quote(key, safe) + "," + \
- _quote(value[key], safe) for key in keys])
- elif value is None:
- return
- else:
- return _quote(value, safe, prefix)
-
-
-def _tostring_path(varname, value, explode, prefix, operator, safe=""):
- joiner = operator
- if isinstance(value, list):
- if explode:
- out = [_quote(x, safe) for x in value if value is not None]
- else:
- joiner = ","
- out = [_quote(x, safe) for x in value if value is not None]
- if out:
- return joiner.join(out)
- else:
- return
- elif isinstance(value, dict):
- keys = sorted(value.keys())
- if explode:
- out = [_quote(key, safe) + "=" + \
- _quote(value[key], safe) for key in keys \
- if value[key] is not None]
- else:
- joiner = ","
- out = [_quote(key, safe) + "," + \
- _quote(value[key], safe) \
- for key in keys if value[key] is not None]
- if out:
- return joiner.join(out)
- else:
- return
- elif value is None:
- return
- else:
- return _quote(value, safe, prefix)
-
-
-def _tostring_semi(varname, value, explode, prefix, operator, safe=""):
- joiner = operator
- if operator == "?":
- joiner = "&"
- if isinstance(value, list):
- if explode:
- out = [varname + "=" + _quote(x, safe) \
- for x in value if x is not None]
- if out:
- return joiner.join(out)
- else:
- return
- else:
- return varname + "=" + ",".join([_quote(x, safe) \
- for x in value])
- elif isinstance(value, dict):
- keys = sorted(value.keys())
- if explode:
- return joiner.join([_quote(key, safe) + "=" + \
- _quote(value[key], safe) \
- for key in keys if key is not None])
- else:
- return varname + "=" + ",".join([_quote(key, safe) + "," + \
- _quote(value[key], safe) for key in keys \
- if key is not None])
- else:
- if value is None:
- return
- elif value:
- return (varname + "=" + _quote(value, safe, prefix))
- else:
- return varname
-
-
-def _tostring_query(varname, value, explode, prefix, operator, safe=""):
- joiner = operator
- if operator in ["?", "&"]:
- joiner = "&"
- if isinstance(value, list):
- if 0 == len(value):
- return None
- if explode:
- return joiner.join([varname + "=" + _quote(x, safe) \
- for x in value])
- else:
- return (varname + "=" + ",".join([_quote(x, safe) \
- for x in value]))
- elif isinstance(value, dict):
- if 0 == len(value):
- return None
- keys = sorted(value.keys())
- if explode:
- return joiner.join([_quote(key, safe) + "=" + \
- _quote(value[key], safe) \
- for key in keys])
- else:
- return varname + "=" + \
- ",".join([_quote(key, safe) + "," + \
- _quote(value[key], safe) for key in keys])
- else:
- if value is None:
- return
- elif value:
- return (varname + "=" + _quote(value, safe, prefix))
- else:
- return (varname + "=")
-
-
-TOSTRING = {
- "" : _tostring,
- "+": _tostring,
- "#": _tostring,
- ";": _tostring_semi,
- "?": _tostring_query,
- "&": _tostring_query,
- "/": _tostring_path,
- ".": _tostring_path,
- }
-
-
-def expand(template, variables):
- """
- Expand template as a URI Template using variables.
- """
- def _sub(match):
- expression = match.group(1)
- operator = ""
- if expression[0] in OPERATOR:
- operator = expression[0]
- varlist = expression[1:]
- else:
- varlist = expression
-
- safe = ""
- if operator in ["+", "#"]:
- safe = RESERVED
- varspecs = varlist.split(",")
- varnames = []
- defaults = {}
- for varspec in varspecs:
- default = None
- explode = False
- prefix = None
- if "=" in varspec:
- varname, default = tuple(varspec.split("=", 1))
- else:
- varname = varspec
- if varname[-1] == "*":
- explode = True
- varname = varname[:-1]
- elif ":" in varname:
- try:
- prefix = int(varname[varname.index(":")+1:])
- except ValueError:
- raise ValueError("non-integer prefix '{0}'".format(
- varname[varname.index(":")+1:]))
- varname = varname[:varname.index(":")]
- if default:
- defaults[varname] = default
- varnames.append((varname, explode, prefix))
+__title__ = 'uritemplate'
+__author__ = 'Ian Cordasco'
+__license__ = 'Modified BSD or Apache License, Version 2.0'
+__copyright__ = 'Copyright 2013 Ian Cordasco'
+__version__ = '3.0.1'
+__version_info__ = tuple(int(i) for i in __version__.split('.') if i.isdigit())
- retval = []
- joiner = operator
- start = operator
- if operator == "+":
- start = ""
- joiner = ","
- if operator == "#":
- joiner = ","
- if operator == "?":
- joiner = "&"
- if operator == "&":
- start = "&"
- if operator == "":
- joiner = ","
- for varname, explode, prefix in varnames:
- if varname in variables:
- value = variables[varname]
- if not value and value != "" and varname in defaults:
- value = defaults[varname]
- elif varname in defaults:
- value = defaults[varname]
- else:
- continue
- expanded = TOSTRING[operator](
- varname, value, explode, prefix, operator, safe=safe)
- if expanded is not None:
- retval.append(expanded)
- if len(retval) > 0:
- return start + joiner.join(retval)
- else:
- return ""
+from uritemplate.api import (
+ URITemplate, expand, partial, variables # noqa: E402
+)
- return TEMPLATE.sub(_sub, template)
+__all__ = ('URITemplate', 'expand', 'partial', 'variables')
diff --git a/uritemplate/api.py b/uritemplate/api.py
new file mode 100644
index 0000000..5ad2815
--- /dev/null
+++ b/uritemplate/api.py
@@ -0,0 +1,73 @@
+"""
+
+uritemplate.api
+===============
+
+This module contains the very simple API provided by uritemplate.
+
+"""
+
+from uritemplate.orderedset import OrderedSet
+from uritemplate.template import URITemplate
+
+
+def expand(uri, var_dict=None, **kwargs):
+ """Expand the template with the given parameters.
+
+ :param str uri: The templated URI to expand
+ :param dict var_dict: Optional dictionary with variables and values
+ :param kwargs: Alternative way to pass arguments
+ :returns: str
+
+ Example::
+
+ expand('https://api.github.com{/end}', {'end': 'users'})
+ expand('https://api.github.com{/end}', end='gists')
+
+ .. note:: Passing values by both parts, may override values in
+ ``var_dict``. For example::
+
+ expand('https://{var}', {'var': 'val1'}, var='val2')
+
+ ``val2`` will be used instead of ``val1``.
+
+ """
+ return URITemplate(uri).expand(var_dict, **kwargs)
+
+
+def partial(uri, var_dict=None, **kwargs):
+ """Partially expand the template with the given parameters.
+
+ If all of the parameters for the template are not given, return a
+ partially expanded template.
+
+ :param dict var_dict: Optional dictionary with variables and values
+ :param kwargs: Alternative way to pass arguments
+ :returns: :class:`URITemplate`
+
+ Example::
+
+ t = URITemplate('https://api.github.com{/end}')
+ t.partial() # => URITemplate('https://api.github.com{/end}')
+
+ """
+ return URITemplate(uri).partial(var_dict, **kwargs)
+
+
+def variables(uri):
+ """Parse the variables of the template.
+
+ This returns all of the variable names in the URI Template.
+
+ :returns: Set of variable names
+ :rtype: set
+
+ Example::
+
+ variables('https://api.github.com{/end})
+ # => {'end'}
+ variables('https://api.github.com/repos{/username}{/repository}')
+ # => {'username', 'repository'}
+
+ """
+ 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
new file mode 100644
index 0000000..0df0da6
--- /dev/null
+++ b/uritemplate/template.py
@@ -0,0 +1,152 @@
+"""
+
+uritemplate.template
+====================
+
+This module contains the essential inner workings of uritemplate.
+
+What treasures await you:
+
+- URITemplate class
+
+You see a treasure chest of knowledge in front of you.
+What do you do?
+>
+
+"""
+
+import re
+from uritemplate.orderedset import OrderedSet
+from uritemplate.variable import URIVariable
+
+template_re = re.compile('{([^}]+)}')
+
+
+def _merge(var_dict, overrides):
+ if var_dict:
+ opts = var_dict.copy()
+ opts.update(overrides)
+ return opts
+ return overrides
+
+
+class URITemplate(object):
+
+ """This parses the template and will be used to expand it.
+
+ This is the most important object as the center of the API.
+
+ Example::
+
+ from uritemplate import URITemplate
+ import requests
+
+
+ t = URITemplate(
+ 'https://api.github.com/users/sigmavirus24/gists{/gist_id}'
+ )
+ uri = t.expand(gist_id=123456)
+ resp = requests.get(uri)
+ for gist in resp.json():
+ print(gist['html_url'])
+
+ Please note::
+
+ str(t)
+ # 'https://api.github.com/users/sigmavirus24/gists{/gistid}'
+ repr(t) # is equivalent to
+ # URITemplate(str(t))
+ # Where str(t) is interpreted as the URI string.
+
+ Also, ``URITemplates`` are hashable so they can be used as keys in
+ dictionaries.
+
+ """
+
+ def __init__(self, uri):
+ #: The original URI to be parsed.
+ self.uri = uri
+ #: A list of the variables in the URI. They are stored as
+ #: :class:`URIVariable`\ s
+ self.variables = [
+ URIVariable(m.groups()[0]) for m in template_re.finditer(self.uri)
+ ]
+ #: A set of variable names in the URI.
+ self.variable_names = OrderedSet()
+ for variable in self.variables:
+ for name in variable.variable_names:
+ self.variable_names.add(name)
+
+ def __repr__(self):
+ return 'URITemplate("%s")' % self
+
+ def __str__(self):
+ return self.uri
+
+ def __eq__(self, other):
+ return self.uri == other.uri
+
+ def __hash__(self):
+ return hash(self.uri)
+
+ def _expand(self, var_dict, replace):
+ if not self.variables:
+ return self.uri
+
+ expansion = var_dict
+ expanded = {}
+ for v in self.variables:
+ expanded.update(v.expand(expansion))
+
+ def replace_all(match):
+ return expanded.get(match.groups()[0], '')
+
+ def replace_partial(match):
+ match = match.groups()[0]
+ var = '{%s}' % match
+ return expanded.get(match) or var
+
+ replace = replace_partial if replace else replace_all
+
+ return template_re.sub(replace, self.uri)
+
+ def expand(self, var_dict=None, **kwargs):
+ """Expand the template with the given parameters.
+
+ :param dict var_dict: Optional dictionary with variables and values
+ :param kwargs: Alternative way to pass arguments
+ :returns: str
+
+ Example::
+
+ t = URITemplate('https://api.github.com{/end}')
+ t.expand({'end': 'users'})
+ t.expand(end='gists')
+
+ .. note:: Passing values by both parts, may override values in
+ ``var_dict``. For example::
+
+ expand('https://{var}', {'var': 'val1'}, var='val2')
+
+ ``val2`` will be used instead of ``val1``.
+
+ """
+ return self._expand(_merge(var_dict, kwargs), False)
+
+ def partial(self, var_dict=None, **kwargs):
+ """Partially expand the template with the given parameters.
+
+ If all of the parameters for the template are not given, return a
+ partially expanded template.
+
+ :param dict var_dict: Optional dictionary with variables and values
+ :param kwargs: Alternative way to pass arguments
+ :returns: :class:`URITemplate`
+
+ Example::
+
+ t = URITemplate('https://api.github.com{/end}')
+ t.partial() # => URITemplate('https://api.github.com{/end}')
+
+ """
+ return URITemplate(self._expand(_merge(var_dict, kwargs), True))
diff --git a/uritemplate/variable.py b/uritemplate/variable.py
new file mode 100644
index 0000000..ce3f652
--- /dev/null
+++ b/uritemplate/variable.py
@@ -0,0 +1,386 @@
+"""
+
+uritemplate.variable
+====================
+
+This module contains the URIVariable class which powers the URITemplate class.
+
+What treasures await you:
+
+- URIVariable class
+
+You see a hammer in front of you.
+What do you do?
+>
+
+"""
+
+import sys
+
+try:
+ import collections.abc as collections_abc
+except ImportError:
+ import collections as collections_abc
+
+if sys.version_info.major == 2:
+ import urllib
+elif sys.version_info.major == 3:
+ import urllib.parse as urllib
+
+
+class URIVariable(object):
+
+ """This object validates everything inside the URITemplate object.
+
+ It validates template expansions and will truncate length as decided by
+ the template.
+
+ Please note that just like the :class:`URITemplate <URITemplate>`, this
+ object's ``__str__`` and ``__repr__`` methods do not return the same
+ information. Calling ``str(var)`` will return the original variable.
+
+ This object does the majority of the heavy lifting. The ``URITemplate``
+ object finds the variables in the URI and then creates ``URIVariable``
+ objects. Expansions of the URI are handled by each ``URIVariable``
+ object. ``URIVariable.expand()`` returns a dictionary of the original
+ variable and the expanded value. Check that method's documentation for
+ more information.
+
+ """
+
+ operators = ('+', '#', '.', '/', ';', '?', '&', '|', '!', '@')
+ reserved = ":/?#[]@!$&'()*+,;="
+
+ def __init__(self, var):
+ #: The original string that comes through with the variable
+ self.original = var
+ #: The operator for the variable
+ self.operator = ''
+ #: List of safe characters when quoting the string
+ self.safe = ''
+ #: List of variables in this variable
+ self.variables = []
+ #: List of variable names
+ self.variable_names = []
+ #: List of defaults passed in
+ self.defaults = {}
+ # Parse the variable itself.
+ self.parse()
+ self.post_parse()
+
+ def __repr__(self):
+ return 'URIVariable(%s)' % self
+
+ def __str__(self):
+ return self.original
+
+ def parse(self):
+ """Parse the variable.
+
+ This finds the:
+ - operator,
+ - set of safe characters,
+ - variables, and
+ - defaults.
+
+ """
+ var_list = self.original
+ if self.original[0] in URIVariable.operators:
+ self.operator = self.original[0]
+ var_list = self.original[1:]
+
+ if self.operator in URIVariable.operators[:2]:
+ self.safe = URIVariable.reserved
+
+ var_list = var_list.split(',')
+
+ for var in var_list:
+ default_val = None
+ name = var
+ if '=' in var:
+ name, default_val = tuple(var.split('=', 1))
+
+ explode = False
+ if name.endswith('*'):
+ explode = True
+ name = name[:-1]
+
+ prefix = None
+ if ':' in name:
+ name, prefix = tuple(name.split(':', 1))
+ prefix = int(prefix)
+
+ if default_val:
+ self.defaults[name] = default_val
+
+ self.variables.append(
+ (name, {'explode': explode, 'prefix': prefix})
+ )
+
+ self.variable_names = [varname for (varname, _) in self.variables]
+
+ def post_parse(self):
+ """Set ``start``, ``join_str`` and ``safe`` attributes.
+
+ After parsing the variable, we need to set up these attributes and it
+ only makes sense to do it in a more easily testable way.
+ """
+ self.safe = ''
+ self.start = self.join_str = self.operator
+ if self.operator == '+':
+ self.start = ''
+ if self.operator in ('+', '#', ''):
+ self.join_str = ','
+ if self.operator == '#':
+ self.start = '#'
+ if self.operator == '?':
+ self.start = '?'
+ self.join_str = '&'
+
+ if self.operator in ('+', '#'):
+ self.safe = URIVariable.reserved
+
+ def _query_expansion(self, name, value, explode, prefix):
+ """Expansion method for the '?' and '&' operators."""
+ if value is None:
+ return None
+
+ tuples, items = is_list_of_tuples(value)
+
+ safe = self.safe
+ if list_test(value) and not tuples:
+ if not value:
+ return None
+ if explode:
+ return self.join_str.join(
+ '{}={}'.format(name, quote(v, safe)) for v in value
+ )
+ else:
+ value = ','.join(quote(v, safe) for v in value)
+ return '{}={}'.format(name, value)
+
+ if dict_test(value) or tuples:
+ if not value:
+ return None
+ items = items or sorted(value.items())
+ if explode:
+ return self.join_str.join(
+ '{}={}'.format(
+ quote(k, safe), quote(v, safe)
+ ) for k, v in items
+ )
+ else:
+ value = ','.join(
+ '{},{}'.format(
+ quote(k, safe), quote(v, safe)
+ ) for k, v in items
+ )
+ return '{}={}'.format(name, value)
+
+ if value:
+ value = value[:prefix] if prefix else value
+ return '{}={}'.format(name, quote(value, safe))
+ return name + '='
+
+ def _label_path_expansion(self, name, value, explode, prefix):
+ """Label and path expansion method.
+
+ Expands for operators: '/', '.'
+
+ """
+ join_str = self.join_str
+ safe = self.safe
+
+ if value is None or (len(value) == 0 and value != ''):
+ return None
+
+ tuples, items = is_list_of_tuples(value)
+
+ if list_test(value) and not tuples:
+ if not explode:
+ join_str = ','
+
+ 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())
+ format_str = '%s=%s'
+ if not explode:
+ format_str = '%s,%s'
+ join_str = ','
+
+ expanded = join_str.join(
+ format_str % (
+ quote(k, safe), quote(v, safe)
+ ) for k, v in items if v is not None
+ )
+ return expanded if expanded else None
+
+ value = value[:prefix] if prefix else value
+ return quote(value, safe)
+
+ def _semi_path_expansion(self, name, value, explode, prefix):
+ """Expansion method for ';' operator."""
+ join_str = self.join_str
+ safe = self.safe
+
+ if value is None:
+ return None
+
+ if self.operator == '?':
+ join_str = '&'
+
+ tuples, items = is_list_of_tuples(value)
+
+ if list_test(value) and not tuples:
+ if explode:
+ expanded = join_str.join(
+ '{}={}'.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 '{}={}'.format(name, value)
+
+ if dict_test(value) or tuples:
+ items = items or sorted(value.items())
+
+ if explode:
+ return join_str.join(
+ '{}={}'.format(
+ quote(k, safe), quote(v, safe)
+ ) for k, v in items if v is not None
+ )
+ else:
+ expanded = ','.join(
+ '{},{}'.format(
+ quote(k, safe), quote(v, safe)
+ ) for k, v in items if v is not None
+ )
+ return '{}={}'.format(name, expanded)
+
+ value = value[:prefix] if prefix else value
+ if value:
+ return '{}={}'.format(name, quote(value, safe))
+
+ return name
+
+ def _string_expansion(self, name, value, explode, prefix):
+ if value is None:
+ return None
+
+ tuples, items = is_list_of_tuples(value)
+
+ if list_test(value) and not tuples:
+ return ','.join(quote(v, self.safe) for v in value)
+
+ if dict_test(value) or tuples:
+ items = items or sorted(value.items())
+ format_str = '%s=%s' if explode else '%s,%s'
+
+ return ','.join(
+ format_str % (
+ quote(k, self.safe), quote(v, self.safe)
+ ) for k, v in items
+ )
+
+ value = value[:prefix] if prefix else value
+ return quote(value, self.safe)
+
+ def expand(self, var_dict=None):
+ """Expand the variable in question.
+
+ Using ``var_dict`` and the previously parsed defaults, expand this
+ variable and subvariables.
+
+ :param dict var_dict: dictionary of key-value pairs to be used during
+ expansion
+ :returns: dict(variable=value)
+
+ Examples::
+
+ # (1)
+ v = URIVariable('/var')
+ expansion = v.expand({'var': 'value'})
+ print(expansion)
+ # => {'/var': '/value'}
+
+ # (2)
+ v = URIVariable('?var,hello,x,y')
+ expansion = v.expand({'var': 'value', 'hello': 'Hello World!',
+ 'x': '1024', 'y': '768'})
+ print(expansion)
+ # => {'?var,hello,x,y':
+ # '?var=value&hello=Hello%20World%21&x=1024&y=768'}
+
+ """
+ return_values = []
+
+ for name, opts in self.variables:
+ value = var_dict.get(name, None)
+ if not value and value != '' and name in self.defaults:
+ value = self.defaults[name]
+
+ if value is None:
+ continue
+
+ expanded = None
+ if self.operator in ('/', '.'):
+ expansion = self._label_path_expansion
+ elif self.operator in ('?', '&'):
+ expansion = self._query_expansion
+ elif self.operator == ';':
+ expansion = self._semi_path_expansion
+ else:
+ expansion = self._string_expansion
+
+ expanded = expansion(name, value, opts['explode'], opts['prefix'])
+
+ if expanded is not None:
+ return_values.append(expanded)
+
+ value = ''
+ if return_values:
+ value = self.start + self.join_str.join(return_values)
+ return {self.original: value}
+
+
+def is_list_of_tuples(value):
+ if (not value or
+ not isinstance(value, (list, tuple)) or
+ not all(isinstance(t, tuple) and len(t) == 2 for t in value)):
+ return False, None
+
+ return True, value
+
+
+def list_test(value):
+ return isinstance(value, (list, tuple))
+
+
+def dict_test(value):
+ return isinstance(value, (dict, collections_abc.MutableMapping))
+
+
+try:
+ texttype = unicode
+except NameError: # Python 3
+ texttype = str
+
+stringlikes = (texttype, bytes)
+
+
+def _encode(value, encoding='utf-8'):
+ if (isinstance(value, texttype) and
+ getattr(value, 'encode', None) is not None):
+ return value.encode(encoding)
+ return value
+
+
+def quote(value, safe):
+ if not isinstance(value, stringlikes):
+ value = str(value)
+ return urllib.quote(_encode(value), safe)