Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ tmp.py
htmlcov/
.coverage.*
*.py[cod]
.mypy_cache

# emacs
*~
Expand Down Expand Up @@ -33,7 +34,7 @@ pip-log.txt

# Unit test / coverage reports
.coverage
.tox
.tox/
nosetests.xml

# Translations
Expand Down
5 changes: 4 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@
zip_safe=False,
license=__license__,
platforms='any',
install_requires=['idna>=2.5'],
install_requires=[
'idna>=2.5',
'typing ; python_version<"3.5"',
],
classifiers=[
'Topic :: Utilities',
'Intended Audience :: Developers',
Expand Down
46 changes: 46 additions & 0 deletions src/hyperlink/_socket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
try:
from socket import inet_pton
except ImportError:
from typing import TYPE_CHECKING
if TYPE_CHECKING: # pragma: no cover
pass
else:
# based on https://gist.github.com/nnemkin/4966028
# this code only applies on Windows Python 2.7
import ctypes
import socket

class SockAddr(ctypes.Structure):
_fields_ = [
("sa_family", ctypes.c_short),
("__pad1", ctypes.c_ushort),
("ipv4_addr", ctypes.c_byte * 4),
("ipv6_addr", ctypes.c_byte * 16),
("__pad2", ctypes.c_ulong),
]

WSAStringToAddressA = ctypes.windll.ws2_32.WSAStringToAddressA
WSAAddressToStringA = ctypes.windll.ws2_32.WSAAddressToStringA

def inet_pton(address_family, ip_string):
# type: (int, str) -> bytes
addr = SockAddr()
ip_string_bytes = ip_string.encode('ascii')
addr.sa_family = address_family
addr_size = ctypes.c_int(ctypes.sizeof(addr))

try:
attribute, size = {
socket.AF_INET: ("ipv4_addr", 4),
socket.AF_INET6: ("ipv6_addr", 16),
}[address_family]
except KeyError:
raise socket.error("unknown address family")

if WSAStringToAddressA(
ip_string_bytes, address_family, None,
ctypes.byref(addr), ctypes.byref(addr_size)
) != 0:
raise socket.error(ctypes.FormatError())

return ctypes.string_at(getattr(addr, attribute), size)
46 changes: 5 additions & 41 deletions src/hyperlink/_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@
import sys
import string
import socket
from typing import Callable, Text, Type
from unicodedata import normalize
try:
from socket import inet_pton
except ImportError:
inet_pton = None # defined below
from ._socket import inet_pton
try:
from collections.abc import Mapping
except ImportError: # Python 2
Expand All @@ -32,47 +30,13 @@
from idna import encode as idna_encode, decode as idna_decode


if inet_pton is None:
# based on https://gist.github.com/nnemkin/4966028
# this code only applies on Windows Python 2.7
import ctypes

class SockAddr(ctypes.Structure):
_fields_ = [("sa_family", ctypes.c_short),
("__pad1", ctypes.c_ushort),
("ipv4_addr", ctypes.c_byte * 4),
("ipv6_addr", ctypes.c_byte * 16),
("__pad2", ctypes.c_ulong)]

WSAStringToAddressA = ctypes.windll.ws2_32.WSAStringToAddressA
WSAAddressToStringA = ctypes.windll.ws2_32.WSAAddressToStringA

def inet_pton(address_family, ip_string):
addr = SockAddr()
ip_string = ip_string.encode('ascii')
addr.sa_family = address_family
addr_size = ctypes.c_int(ctypes.sizeof(addr))

if WSAStringToAddressA(
ip_string, address_family, None,
ctypes.byref(addr), ctypes.byref(addr_size)
) != 0:
raise socket.error(ctypes.FormatError())

if address_family == socket.AF_INET:
return ctypes.string_at(addr.ipv4_addr, 4)
if address_family == socket.AF_INET6:
return ctypes.string_at(addr.ipv6_addr, 16)
raise socket.error('unknown address family')


PY2 = (sys.version_info[0] == 2)
unicode = type(u'')
try:
unichr
except NameError:
unichr = chr # py3
NoneType = type(None)
except NameError: # Py3
unichr = chr # type: Callable[[int], Text]
NoneType = type(None) # type: Type[None]


# from boltons.typeutils
Expand Down
13 changes: 10 additions & 3 deletions src/hyperlink/test/common.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
from typing import Any, Callable, Optional, Type
from unittest import TestCase


class HyperlinkTestCase(TestCase):
"""This type mostly exists to provide a backwards-compatible
assertRaises method for Python 2.6 testing.
"""
def assertRaises(self, excClass, callableObj=None, *args, **kwargs):
"""Fail unless an exception of class excClass is raised
def assertRaises( # type: ignore[override] Doesn't match superclass, meh
self, expected_exception, callableObj=None, *args, **kwargs
):
# type: (Type[BaseException], Optional[Callable], Any, Any) -> Any
"""Fail unless an exception of class expected_exception is raised
by callableObj when invoked with arguments args and keyword
arguments kwargs. If a different type of exception is
raised, it will not be caught, and the test case will be
Expand All @@ -28,7 +32,7 @@ def assertRaises(self, excClass, callableObj=None, *args, **kwargs):
the_exception = cm.exception
self.assertEqual(the_exception.error_code, 3)
"""
context = _AssertRaisesContext(excClass, self)
context = _AssertRaisesContext(expected_exception, self)
if callableObj is None:
return context
with context:
Expand All @@ -39,13 +43,16 @@ class _AssertRaisesContext(object):
"A context manager used to implement HyperlinkTestCase.assertRaises."

def __init__(self, expected, test_case):
# type: (Type[BaseException], TestCase) -> None
self.expected = expected
self.failureException = test_case.failureException

def __enter__(self):
# type: () -> "_AssertRaisesContext"
return self

def __exit__(self, exc_type, exc_value, tb):
# type: (Optional[Type[BaseException]], Any, Any) -> bool
if exc_type is None:
exc_name = self.expected.__name__
raise self.failureException("%s not raised" % (exc_name,))
Expand Down
17 changes: 15 additions & 2 deletions src/hyperlink/test/test_common.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Tests for hyperlink.test.common
"""
from typing import Any
from unittest import TestCase
from .common import HyperlinkTestCase

Expand All @@ -21,9 +22,11 @@ class TestHyperlink(TestCase):
"""Tests for HyperlinkTestCase"""

def setUp(self):
# type: () -> None
self.hyperlink_test = HyperlinkTestCase("run")

def test_assertRaisesWithCallable(self):
# type: () -> None
"""HyperlinkTestCase.assertRaises does not raise an AssertionError
when given a callable that, when called with the provided
arguments, raises the expected exception.
Expand All @@ -32,6 +35,7 @@ def test_assertRaisesWithCallable(self):
called_with = []

def raisesExpected(*args, **kwargs):
# type: (Any, Any) -> None
called_with.append((args, kwargs))
raise _ExpectedException

Expand All @@ -40,12 +44,14 @@ def raisesExpected(*args, **kwargs):
self.assertEqual(called_with, [((1,), {"keyword": True})])

def test_assertRaisesWithCallableUnexpectedException(self):
# type: () -> None
"""When given a callable that raises an unexpected exception,
HyperlinkTestCase.assertRaises raises that exception.

"""

def doesNotRaiseExpected(*args, **kwargs):
# type: (Any, Any) -> None
raise _UnexpectedException

try:
Expand All @@ -55,13 +61,15 @@ def doesNotRaiseExpected(*args, **kwargs):
pass

def test_assertRaisesWithCallableDoesNotRaise(self):
# type: () -> None
"""HyperlinkTestCase.assertRaises raises an AssertionError when given
a callable that, when called, does not raise any exception.

"""

def doesNotRaise(*args, **kwargs):
return True
# type: (Any, Any) -> None
pass

try:
self.hyperlink_test.assertRaises(_ExpectedException,
Expand All @@ -70,6 +78,7 @@ def doesNotRaise(*args, **kwargs):
pass

def test_assertRaisesContextManager(self):
# type: () -> None
"""HyperlinkTestCase.assertRaises does not raise an AssertionError
when used as a context manager with a suite that raises the
expected exception. The context manager stores the exception
Expand All @@ -79,9 +88,12 @@ def test_assertRaisesContextManager(self):
with self.hyperlink_test.assertRaises(_ExpectedException) as cm:
raise _ExpectedException

self.assertTrue(isinstance(cm.exception, _ExpectedException))
self.assertTrue( # type: ignore[misc] unreachable
isinstance(cm.exception, _ExpectedException)
)

def test_assertRaisesContextManagerUnexpectedException(self):
# type: () -> None
"""When used as a context manager with a block that raises an
unexpected exception, HyperlinkTestCase.assertRaises raises
that unexpected exception.
Expand All @@ -94,6 +106,7 @@ def test_assertRaisesContextManagerUnexpectedException(self):
pass

def test_assertRaisesContextManagerDoesNotRaise(self):
# type: () -> None
"""HyperlinkTestcase.assertRaises raises an AssertionError when used
as a context manager with a block that does not raise any
exception.
Expand Down
13 changes: 13 additions & 0 deletions src/hyperlink/test/test_decoded_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
class TestURL(HyperlinkTestCase):

def test_durl_basic(self):
# type: () -> None
bdurl = DecodedURL.from_text(BASIC_URL)
assert bdurl.scheme == 'http'
assert bdurl.host == 'example.com'
Expand All @@ -36,6 +37,8 @@ def test_durl_basic(self):
assert durl.userinfo == ('user', '\0\0\0\0')

def test_passthroughs(self):
# type: () -> None

# just basic tests for the methods that more or less pass straight
# through to the underlying URL

Expand Down Expand Up @@ -72,10 +75,12 @@ def test_passthroughs(self):
assert durl != 1

def test_repr(self):
# type: () -> None
durl = DecodedURL.from_text(TOTAL_URL)
assert repr(durl) == 'DecodedURL(url=' + repr(durl._url) + ')'

def test_query_manipulation(self):
# type: () -> None
durl = DecodedURL.from_text(TOTAL_URL)

assert durl.get('zot') == ['23%']
Expand Down Expand Up @@ -115,6 +120,7 @@ def test_query_manipulation(self):
)

def test_equality_and_hashability(self):
# type: () -> None
durl = DecodedURL.from_text(TOTAL_URL)
durl2 = DecodedURL.from_text(TOTAL_URL)
burl = DecodedURL.from_text(BASIC_URL)
Expand All @@ -141,6 +147,7 @@ def test_equality_and_hashability(self):
assert len(durl_map) == 3

def test_replace_roundtrip(self):
# type: () -> None
durl = DecodedURL.from_text(TOTAL_URL)

durl2 = durl.replace(scheme=durl.scheme,
Expand All @@ -156,12 +163,14 @@ def test_replace_roundtrip(self):
assert durl == durl2

def test_replace_userinfo(self):
# type: () -> None
durl = DecodedURL.from_text(TOTAL_URL)
with self.assertRaises(ValueError):
durl.replace(userinfo=['user', 'pw', 'thiswillcauseafailure'])
return

def test_twisted_compat(self):
# type: () -> None
durl = DecodedURL.from_text(TOTAL_URL)

assert durl == DecodedURL.fromText(TOTAL_URL)
Expand All @@ -170,9 +179,12 @@ def test_twisted_compat(self):
assert durl.to_text() == durl.asText()

def test_percent_decode_bytes(self):
# type: () -> None
assert _percent_decode('%00', subencoding=False) == b'\0'

def test_percent_decode_mixed(self):
# type: () -> None

# See https://github.com/python-hyper/hyperlink/pull/59 for a
# nice discussion of the possibilities
assert _percent_decode('abcdé%C3%A9éfg') == 'abcdéééfg'
Expand All @@ -191,6 +203,7 @@ def test_percent_decode_mixed(self):
assert _percent_decode('é%25é', subencoding='ascii') == 'é%25é'

def test_click_decoded_url(self):
# type: () -> None
durl = DecodedURL.from_text(TOTAL_URL)
durl_dest = DecodedURL.from_text('/tëst')

Expand Down
3 changes: 1 addition & 2 deletions src/hyperlink/test/test_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
class TestURL(HyperlinkTestCase):

def test_parse(self):
# type: () -> None
purl = parse(TOTAL_URL)
assert isinstance(purl, DecodedURL)
assert purl.user == 'user'
Expand All @@ -35,5 +36,3 @@ def test_parse(self):

with self.assertRaises(UnicodeDecodeError):
purl3.fragment

return
Loading