From 2ee6ac8ee88fee1dee1a0e61fd8a22596ff5e24a Mon Sep 17 00:00:00 2001 From: David Winterbottom Date: Tue, 13 Apr 2021 10:02:32 +0100 Subject: [PATCH 1/4] Update direct dependencies to latest versions --- requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index f3fbf13..f6e8d03 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ # Packaging -pip==19.0.3 -setuptools==40.8.0 -wheel==0.33.1 +pip==20.3.4 # max version compatible with Python 2.7 +setuptools==56.0.0 +wheel==0.36.2 # Testing pytest -tox==3.7.0 +tox==3.23.0 From 17f909e5b4622ce92cae815ff6a0b3c1564d673d Mon Sep 17 00:00:00 2001 From: David Winterbottom Date: Sat, 15 May 2021 22:42:36 +0100 Subject: [PATCH 2/4] Switch to packaging with flit --- makefile | 11 +++-------- purl/__init__.py | 10 ++++++---- pyproject.toml | 13 +++++++++++++ requirements.txt | 9 ++++----- setup.py | 33 --------------------------------- 5 files changed, 26 insertions(+), 50 deletions(-) create mode 100644 pyproject.toml delete mode 100755 setup.py diff --git a/makefile b/makefile index e234a64..bbef07e 100644 --- a/makefile +++ b/makefile @@ -1,20 +1,15 @@ install: pip install -r requirements.txt - python setup.py develop + flit install --symlink test: pytest package: clean - # Test these packages in a fresh virtualenvs: - # $ pip install --no-index dist/purl-0.8.tar.gz - # $ pip install --use-wheel --no-index --find-links dist purl - ./setup.py sdist - ./setup.py bdist_wheel + flit build release: - ./setup.py sdist upload - ./setup.py bdist_wheel upload + flit publish git push --tags clean: diff --git a/purl/__init__.py b/purl/__init__.py index 044639e..b1e0cfc 100644 --- a/purl/__init__.py +++ b/purl/__init__.py @@ -1,6 +1,8 @@ -from .url import URL # noqa -from .template import expand, Template # noqa +""""An immutable URL class for easy URL-building and manipulation""" +# flake8: noqa +from .url import URL +from .template import expand, Template -__version__ = '1.5' +__version__ = "1.5" -__all__ = ['URL', 'expand', 'Template'] +__all__ = ["URL", "expand", "Template"] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7ad2d1b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[build-system] +requires = ["flit_core >=2,<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.metadata] +module = "purl" +author = "David Winterbottom" +author-email = "david.winterbottom@gmail.com" +home-page = "https://github.com/codeinthehole/purl" +classifiers = [ "License :: OSI Approved :: MIT License",] +description-file = "README.rst" +requires-python = ">=3.6" + diff --git a/requirements.txt b/requirements.txt index f6e8d03..61cd134 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,7 @@ # Packaging -pip==20.3.4 # max version compatible with Python 2.7 -setuptools==56.0.0 -wheel==0.36.2 +pip==21.1.1 +flit==3.2.0 # Testing -pytest -tox==3.23.0 +pytest==6.2.4 +tox==3.23.1 diff --git a/setup.py b/setup.py deleted file mode 100755 index b06e58f..0000000 --- a/setup.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python -from setuptools import setup, find_packages - -setup( - name='purl', - version='1.5', - description=( - "An immutable URL class for easy URL-building and manipulation"), - long_description=open('README.rst').read(), - url='https://github.com/codeinthehole/purl', - license='MIT', - author="David Winterbottom", - author_email="david.winterbottom@gmail.com", - packages=find_packages(exclude=['tests']), - install_requires=['six'], - include_package_data=True, - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: Implementation :: PyPy', - 'Topic :: Software Development :: Libraries :: Python Modules', - ], -) From cc2c34296159921f981c647474d889d7215e5f1b Mon Sep 17 00:00:00 2001 From: David Winterbottom Date: Sat, 15 May 2021 22:14:31 +0100 Subject: [PATCH 3/4] Drop support for Python 2.7 And factor out all the awkward string processing. --- .travis.yml | 1 - README.rst | 2 +- purl/template.py | 12 ++-- purl/url.py | 37 +++------- tests/test_url.py | 168 +++++++++++++++++++++++--------------------- tests/test_utils.py | 4 +- tox.ini | 2 +- 7 files changed, 106 insertions(+), 120 deletions(-) diff --git a/.travis.yml b/.travis.yml index 70e3961..49a091f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python python: - - 2.7 - 3.6 - 3.7 - 3.8 diff --git a/README.rst b/README.rst index 7cd270b..8e9108b 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ purl - A simple Python URL class ================================ A simple, immutable URL class with a clean API for interrogation and -manipulation. Supports Pythons 2.7, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8 and pypy. +manipulation. Supports Pythons 3.3, 3.4, 3.5, 3.6, 3.7, 3.8 and pypy. Also supports template URLs as per `RFC 6570`_ diff --git a/purl/template.py b/purl/template.py index b9a2f6b..85d926d 100644 --- a/purl/template.py +++ b/purl/template.py @@ -1,11 +1,7 @@ import re import functools -try: - from urllib.parse import quote -except ImportError: - # Python 2 - from urllib import quote +from urllib.parse import quote from . import url @@ -123,8 +119,8 @@ def _format_default(explode, separator, escape, key, value): return escaped_value -# Modifer functions -# ----------------- +# Modifier functions +# ------------------ # These are responsible for modifying the variable before formatting _identity = lambda x: x @@ -209,6 +205,8 @@ def _replace(variables, match): replacement = format_fn( explode, separator_char, escape_fn, key, variable) replacements.append(replacement) + if not replacements: return '' + return prefix_char + separator_char.join(replacements) diff --git a/purl/url.py b/purl/url.py index 46d3e6a..117764b 100644 --- a/purl/url.py +++ b/purl/url.py @@ -1,13 +1,8 @@ from __future__ import unicode_literals -try: - from urllib.parse import parse_qs, urlencode, urlparse, quote, unquote -except ImportError: - from urllib import urlencode, quote, unquote - from urlparse import parse_qs, urlparse +from urllib.parse import parse_qs, urlencode, urlparse, quote, unquote from collections import namedtuple -import six # To minimise memory consumption, we use a namedtuple to store all instance @@ -23,13 +18,11 @@ def to_unicode(string): """ Ensure a passed string is unicode """ - if isinstance(string, six.binary_type): - return string.decode('utf8') - if isinstance(string, six.text_type): - return string - if six.PY2: - return unicode(string) - return str(string) + if isinstance(string, bytes): + string = string.decode("utf8") + else: + string = str(string) + return string def to_utf8(string): @@ -37,11 +30,9 @@ def to_utf8(string): Encode a string as a UTF8 bytestring. This function could be passed a bytestring or unicode string so must distinguish between the two. """ - if isinstance(string, six.text_type): - return string.encode('utf8') - if isinstance(string, six.binary_type): - return string - return str(string) + if not isinstance(string, bytes): + string = str(string).encode("utf8") + return string def dict_to_unicode(raw_dict): @@ -72,9 +63,7 @@ def unicode_quote_path_segment(string): def unicode_unquote(string): if string is None: return None - if six.PY3: - return unquote(string) - return to_unicode(unquote(to_utf8(string))) + return unquote(string) def unicode_urlencode(query, doseq=True): @@ -485,12 +474,6 @@ def query_params(self, value=None): return URL._mutate(self, query=unicode_urlencode(value, doseq=True)) query = '' if self._tuple.query is None else self._tuple.query - # In Python 2.6, urlparse needs a bytestring so we encode and then - # decode the result. - if not six.PY3: - result = parse_qs(to_utf8(query), True) - return dict_to_unicode(result) - return parse_qs(query, True) def remove_query_param(self, key, value=None): diff --git a/tests/test_url.py b/tests/test_url.py index d3c3df1..d485f0f 100644 --- a/tests/test_url.py +++ b/tests/test_url.py @@ -65,22 +65,22 @@ def test_extracting_query_param(self): class TestFactory: url_str = 'http://www.google.com/search/?q=testing#fragment' - url = URL.from_string(url_str) def test_scheme(self): - assert 'http' == self.url.scheme() - - def test_fragment(self): - assert 'fragment' == self.url.fragment() + url = URL.from_string(self.url_str) + assert 'http' == url.scheme() def test_path(self): - assert '/search/' == self.url.path() + url = URL.from_string(self.url_str) + assert '/search/' == url.path() def test_host(self): - assert 'www.google.com' == self.url.host() + url = URL.from_string(self.url_str) + assert 'www.google.com' == url.host() def test_string_version(self): - assert self.url_str == str(self.url) + url = URL.from_string(self.url_str) + assert self.url_str == str(url) class TestEdgeCaseExtraction: @@ -138,81 +138,84 @@ def test_port_for_https_url(self): class TestSimpleExtraction: - url = URL.from_string('http://www.google.com/blog/article/1?q=testing') - def test_has_actual_param(self): - assert self.url.has_query_param('q') is True + @pytest.fixture + def url(self): + return URL.from_string('http://www.google.com/blog/article/1?q=testing') + + def test_has_actual_param(self, url): + assert url.has_query_param('q') is True - def test_remove_query_param(self): - new_url = self.url.remove_query_param('q') + def test_remove_query_param(self, url): + new_url = url.remove_query_param('q') assert 'http://www.google.com/blog/article/1' == new_url.as_string() - def test_has_query_params(self): - assert self.url.has_query_params(['q']) is True + def test_has_query_params(self, url): + assert url.has_query_params(['q']) is True - def test_has_query_params_negative(self): - assert self.url.has_query_params(['q', 'r']) is False + def test_has_query_params_negative(self, url): + assert url.has_query_params(['q', 'r']) is False - def test_netloc(self): - assert 'www.google.com' == self.url.netloc() + def test_netloc(self, url): + assert 'www.google.com' == url.netloc() - def test_path_extraction(self): - assert '1' == self.url.path_segment(2) + def test_path_extraction(self, url): + assert '1' == url.path_segment(2) - def test_port_defaults_to_none(self): - assert self.url.port() is None + def test_port_defaults_to_none(self, url): + assert url.port() is None - def test_scheme(self): - assert 'http' == self.url.scheme() + def test_scheme(self, url): + assert 'http' == url.scheme() - def test_host(self): - assert 'www.google.com' == self.url.host() + def test_host(self, url): + assert 'www.google.com' == url.host() - def test_domain(self): - assert 'www.google.com' == self.url.domain() + def test_domain(self, url): + assert 'www.google.com' == url.domain() - def test_subdomains(self): - assert ['www' == 'google', 'com'], self.url.subdomains() + def test_subdomains(self, url): + assert ['www' == 'google', 'com'], url.subdomains() - def test_subdomain(self): - assert 'www' == self.url.subdomain(0) + def test_subdomain(self, url): + assert 'www' == url.subdomain(0) - def test_invalid_subdomain_raises_indexerror(self): + def test_invalid_subdomain_raises_indexerror(self, url): with pytest.raises(IndexError): - self.url.subdomain(10) + url.subdomain(10) - def test_path(self): - assert '/blog/article/1' == self.url.path() + def test_path(self, url): + assert '/blog/article/1' == url.path() - def test_query(self): - assert 'q=testing' == self.url.query() + def test_query(self, url): + assert 'q=testing' == url.query() - def test_query_param_as_list(self): - assert ['testing'] == self.url.query_param('q', as_list=True) + def test_query_param_as_list(self, url): + assert ['testing'] == url.query_param('q', as_list=True) - def test_query_params(self): - assert {'q': ['testing']} == self.url.query_params() + def test_query_params(self, url): + assert {'q': ['testing']} == url.query_params() - def test_path_extraction_returns_none_if_index_too_large(self): - assert self.url.path_segment(14) is None + def test_path_extraction_returns_none_if_index_too_large(self, url): + assert url.path_segment(14) is None - def test_path_extraction_can_take_default_value(self): - assert 'hello' == self.url.path_segment(3, default='hello') + def test_path_extraction_can_take_default_value(self, url): + assert 'hello' == url.path_segment(3, default='hello') - def test_parameter_extraction(self): - assert 'testing' == self.url.query_param('q') + def test_parameter_extraction(self, url): + assert 'testing' == url.query_param('q') - def test_parameter_extraction_with_default(self): - assert 'eggs' == self.url.query_param('p', default='eggs') + def test_parameter_extraction_with_default(self, url): + assert 'eggs' == url.query_param('p', default='eggs') - def test_parameter_extraction_is_none_if_not_found(self): - assert self.url.query_param('p') is None + def test_parameter_extraction_is_none_if_not_found(self, url): + assert url.query_param('p') is None - def test_path_segments(self): - assert ('blog', 'article', '1') == self.url.path_segments() + def test_path_segments(self, url): + assert ('blog', 'article', '1') == url.path_segments() - def test_relative(self): - assert '/blog/article/1?q=testing' == str(self.url.relative()) + def test_relative(self, url): + assert '/blog/article/1?q=testing' == str(url.relative()) class TestNoTrailingSlash: @@ -395,52 +398,55 @@ def test_get_query_param_unicode_url(self): class TestUnicode: - base = URL('http://127.0.0.1/') - text = u'ć' + text = 'ć' bytes = text.encode('utf8') - def test_set_unicode_query_param_value(self): - url = self.base.query_param('q', self.text) + @pytest.fixture() + def url(self): + return URL('http://127.0.0.1/') + + def test_set_unicode_query_param_value(self, url): + url = url.query_param('q', self.text) assert self.text == url.query_param('q') - def test_set_bytestring_query_param_value(self): - url = self.base.query_param('q', self.bytes) + def test_set_bytestring_query_param_value(self, url): + url = url.query_param('q', self.bytes) assert self.text == url.query_param('q') - def test_set_unicode_query_param_key(self): - url = self.base.query_param(self.text, 'value') + def test_set_unicode_query_param_key(self, url): + url = url.query_param(self.text, 'value') assert 'value' == url.query_param(self.text) - def test_set_bytestring_query_param_key(self): - url = self.base.query_param(self.bytes, 'value') + def test_set_bytestring_query_param_key(self, url): + url = url.query_param(self.bytes, 'value') assert 'value' == url.query_param(self.text) - def test_append_unicode_query_param(self): - url = self.base.append_query_param('q', self.text) + def test_append_unicode_query_param(self, url): + url = url.append_query_param('q', self.text) assert self.text == url.query_param('q') - def test_append_bytestring_query_param(self): - url = self.base.append_query_param('q', self.bytes) + def test_append_bytestring_query_param(self, url): + url = url.append_query_param('q', self.bytes) assert self.text == url.query_param('q') - def test_set_unicode_query_params(self): - url = self.base.query_params({'q': self.text}) + def test_set_unicode_query_params(self, url): + url = url.query_params({'q': self.text}) assert self.text == url.query_param('q') - def test_set_bytestring_query_params(self): - url = self.base.query_params({'q': self.bytes}) + def test_set_bytestring_query_params(self, url): + url = url.query_params({'q': self.bytes}) assert self.text == url.query_param('q') - def test_add_unicode_path_segment(self): - url = self.base.add_path_segment(self.text) + def test_add_unicode_path_segment(self, url): + url = url.add_path_segment(self.text) assert self.text == url.path_segment(0) - def test_add_bytestring_path_segment(self): - url = self.base.add_path_segment(self.bytes) + def test_add_bytestring_path_segment(self, url): + url = url.add_path_segment(self.bytes) assert self.text == url.path_segment(0) - def test_add_unicode_fragment(self): - url = self.base.fragment(self.text) + def test_add_unicode_fragment(self, url): + url = url.fragment(self.text) assert self.text == url.fragment() diff --git a/tests/test_utils.py b/tests/test_utils.py index 9aafef8..fc148bb 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -4,7 +4,7 @@ class TestUnicodeHelper: def test_convert_int_to_bytes(self): - assert '1024' == to_utf8(1024) + assert b'1024' == to_utf8(1024) def test_convert_int_to_unicode(self): - assert u'1024' == to_unicode(1024) + assert '1024' == to_unicode(1024) diff --git a/tox.ini b/tox.ini index 763285a..a161723 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py26, py27, py36, py37, py38, pypy, pypy3 +envlist = py36, py37, py38, pypy, pypy3 [testenv] commands = pytest From 21ac3511e405f477c5d7e4797bba6bed42e6efe7 Mon Sep 17 00:00:00 2001 From: David Winterbottom Date: Fri, 18 Jun 2021 09:47:34 +0100 Subject: [PATCH 4/4] Add `tags` to `.gitignore` --- .gitignore | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 55c5775..130b320 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,13 @@ +# Packaging *.egg-info/ *.pyc -.tox -__pycache__/ dist/* docs/_build/* build + +# Testing +.tox +__pycache__/ + +# Ctags +tags