Skip to content

Commit 53fd748

Browse files
authored
Merge pull request #42 from alexander255/master
Miscellaneous improvements
2 parents b72ec6f + bb3d9b4 commit 53fd748

File tree

8 files changed

+182
-50
lines changed

8 files changed

+182
-50
lines changed

multiaddr/codecs/_util.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
def packed_net_bytes_to_int(b):
33
"""Convert the given big-endian byte-string to an int."""
44
return int.from_bytes(b, byteorder='big')
5-
else: # PY2
5+
else: # pragma: no cover (PY2)
66
def packed_net_bytes_to_int(b):
77
"""Convert the given big-endian byte-string to an int."""
88
return int(b.encode('hex'), 16)

multiaddr/codecs/fspath.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,16 @@
1313
if hasattr(os, "fsencode") and hasattr(os, "fsdecode"):
1414
fsencode = os.fsencode
1515
fsdecode = os.fsdecode
16-
else: # PY2
16+
else: # pragma: no cover (PY2)
1717
import sys
1818

1919
def fsencode(path):
20-
if not isinstance(path, six.binary_type): # pragma: no cover
20+
if not isinstance(path, six.binary_type):
2121
path = path.encode(sys.getfilesystemencoding())
2222
return path
2323

2424
def fsdecode(path):
25-
if not isinstance(path, six.text_type): # pragma: no cover
25+
if not isinstance(path, six.text_type):
2626
path = path.decode(sys.getfilesystemencoding())
2727
return path
2828

multiaddr/codecs/p2p.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
def to_bytes(proto, string):
1414
# the address is a base58-encoded string
15-
if six.PY2 and isinstance(string, unicode):
15+
if six.PY2 and isinstance(string, unicode): # pragma: no cover (PY2)
1616
string = string.encode("ascii")
1717
mm = base58.b58decode(string)
1818
if len(mm) < 5:

multiaddr/multiaddr.py

Lines changed: 123 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# -*- coding: utf-8 -*-
2-
from copy import copy
2+
try:
3+
import collections.abc
4+
except ImportError: # pragma: no cover (PY2)
5+
import collections
6+
collections.abc = collections
37

48
import six
59

@@ -10,7 +14,76 @@
1014
from .transforms import bytes_to_string
1115

1216

13-
class Multiaddr(object):
17+
__all__ = ("Multiaddr",)
18+
19+
20+
21+
class MultiAddrKeys(collections.abc.KeysView, collections.abc.Sequence):
22+
def __contains__(self, proto):
23+
proto = protocols.protocol_with_any(proto)
24+
return collections.abc.Sequence.__contains__(self, proto)
25+
26+
def __getitem__(self, idx):
27+
if idx < 0:
28+
idx = len(self)+idx
29+
for idx2, proto in enumerate(self):
30+
if idx2 == idx:
31+
return proto
32+
raise IndexError("Protocol list index out of range")
33+
34+
__hash__ = collections.abc.KeysView._hash
35+
36+
def __iter__(self):
37+
for proto, _, _ in bytes_iter(self._mapping.to_bytes()):
38+
yield proto
39+
40+
41+
class MultiAddrItems(collections.abc.ItemsView, collections.abc.Sequence):
42+
def __contains__(self, item):
43+
proto, value = item
44+
proto = protocols.protocol_with_any(proto)
45+
return collections.abc.Sequence.__contains__(self, (proto, value))
46+
47+
def __getitem__(self, idx):
48+
if idx < 0:
49+
idx = len(self)+idx
50+
for idx2, item in enumerate(self):
51+
if idx2 == idx:
52+
return item
53+
raise IndexError("Protocol item list index out of range")
54+
55+
def __iter__(self):
56+
for proto, codec, part in bytes_iter(self._mapping.to_bytes()):
57+
if codec.SIZE != 0:
58+
try:
59+
# If we have an address, return it
60+
yield proto, codec.to_string(proto, part)
61+
except Exception as exc:
62+
six.raise_from(exceptions.BinaryParseError(str(exc), self._mapping.to_bytes(), proto.name, exc), exc)
63+
else:
64+
# We were given something like '/utp', which doesn't have
65+
# an address, so return None
66+
yield proto, None
67+
68+
69+
class MultiAddrValues(collections.abc.ValuesView, collections.abc.Sequence):
70+
__contains__ = collections.abc.Sequence.__contains__
71+
72+
def __getitem__(self, idx):
73+
if idx < 0:
74+
idx = len(self)+idx
75+
for idx2, proto in enumerate(self):
76+
if idx2 == idx:
77+
return proto
78+
raise IndexError("Protocol value list index out of range")
79+
80+
def __iter__(self):
81+
for _, value in MultiAddrItems(self._mapping):
82+
yield value
83+
84+
85+
86+
class Multiaddr(collections.abc.Mapping):
1487
"""Multiaddr is a representation of multiple nested internet addresses.
1588
1689
Multiaddr is a cross-protocol, cross-platform format for representing
@@ -39,34 +112,42 @@ def __init__(self, addr):
39112
# On Python 2 text string will often be binary anyways so detect the
40113
# obvious case of a “binary-encoded” multiaddr starting with a slash
41114
# and decode it into text
42-
if six.PY2 and isinstance(addr, str) and addr.startswith("/"):
115+
if six.PY2 and isinstance(addr, str) and addr.startswith("/"): # pragma: no cover (PY2)
43116
addr = addr.decode("utf-8")
44117

45118
if isinstance(addr, six.text_type):
46119
self._bytes = string_to_bytes(addr)
47120
elif isinstance(addr, six.binary_type):
48121
self._bytes = addr
122+
elif isinstance(addr, Multiaddr):
123+
self._bytes = addr.to_bytes()
49124
else:
50-
raise TypeError("MultiAddr must be bytes or str")
125+
raise TypeError("MultiAddr must be bytes, str or another MultiAddr instance")
51126

52127
def __eq__(self, other):
53128
"""Checks if two Multiaddr objects are exactly equal."""
54129
return self._bytes == other._bytes
55130

56-
def __ne__(self, other):
57-
return not (self == other)
58-
59131
def __str__(self):
60132
"""Return the string representation of this Multiaddr.
61133
62134
May raise a :class:`~multiaddr.exceptions.BinaryParseError` if the
63135
stored MultiAddr binary representation is invalid."""
64136
return bytes_to_string(self._bytes)
65137

138+
def __contains__(self, proto):
139+
return proto in MultiAddrKeys(self)
140+
141+
def __iter__(self):
142+
return iter(MultiAddrKeys(self))
143+
144+
def __len__(self):
145+
return sum((1 for _ in bytes_iter(self.to_bytes())))
146+
66147
# On Python 2 __str__ needs to return binary text, so expose the original
67148
# function as __unicode__ and transparently encode its returned text based
68149
# on the current locale
69-
if six.PY2:
150+
if six.PY2: # pragma: no cover (PY2)
70151
__unicode__ = __str__
71152

72153
def __str__(self):
@@ -82,7 +163,15 @@ def to_bytes(self):
82163

83164
def protocols(self):
84165
"""Returns a list of Protocols this Multiaddr includes."""
85-
return list(proto for proto, _, _ in bytes_iter(self.to_bytes()))
166+
return MultiAddrKeys(self)
167+
168+
keys = protocols
169+
170+
def items(self):
171+
return MultiAddrItems(self)
172+
173+
def values(self):
174+
return MultiAddrValues(self)
86175

87176
def encapsulate(self, other):
88177
"""Wrap this Multiaddr around another.
@@ -91,7 +180,7 @@ def encapsulate(self, other):
91180
/ip4/1.2.3.4 encapsulate /tcp/80 = /ip4/1.2.3.4/tcp/80
92181
"""
93182
mb = self.to_bytes()
94-
ob = other.to_bytes()
183+
ob = Multiaddr(other).to_bytes()
95184
return Multiaddr(b''.join([mb, ob]))
96185

97186
def decapsulate(self, other):
@@ -100,35 +189,35 @@ def decapsulate(self, other):
100189
For example:
101190
/ip4/1.2.3.4/tcp/80 decapsulate /ip4/1.2.3.4 = /tcp/80
102191
"""
103-
s1 = str(self)
104-
s2 = str(other)
192+
s1 = self.to_bytes()
193+
s2 = Multiaddr(other).to_bytes()
105194
try:
106195
idx = s1.rindex(s2)
107196
except ValueError:
108197
# if multiaddr not contained, returns a copy
109-
return copy(self)
198+
return Multiaddr(self)
110199
return Multiaddr(s1[:idx])
111200

112201
def value_for_protocol(self, proto):
113-
"""Return the value (if any) following the specified protocol."""
114-
if not isinstance(proto, protocols.Protocol):
115-
if isinstance(proto, int):
116-
proto = protocols.protocol_with_code(proto)
117-
elif isinstance(proto, six.string_types):
118-
proto = protocols.protocol_with_name(proto)
119-
else:
120-
raise TypeError("Protocol object, name or code expected, got {0!r}".format(proto))
121-
122-
for proto2, codec, part in bytes_iter(self.to_bytes()):
123-
if proto2 == proto:
124-
if codec.SIZE != 0:
125-
try:
126-
# If we have an address, return it
127-
return codec.to_string(proto2, part)
128-
except Exception as exc:
129-
six.raise_from(exceptions.BinaryParseError(str(exc), self.to_bytes(), proto2.name, exc), exc)
130-
else:
131-
# We were given something like '/utp', which doesn't have
132-
# an address, so return ''
133-
return ''
202+
"""Return the value (if any) following the specified protocol
203+
204+
Returns
205+
-------
206+
union[object, NoneType]
207+
The parsed protocol value for the given protocol code or ``None``
208+
if the given protocol does not require any value
209+
210+
Raises
211+
------
212+
~multiaddr.exceptions.BinaryParseError
213+
The stored MultiAddr binary representation is invalid
214+
~multiaddr.exceptions.ProtocolLookupError
215+
MultiAddr does not contain any instance of this protocol
216+
"""
217+
proto = protocols.protocol_with_any(proto)
218+
for proto2, value in self.items():
219+
if proto2 is proto or proto2 == proto:
220+
return value
134221
raise exceptions.ProtocolLookupError(proto, str(self))
222+
223+
__getitem__ = value_for_protocol

multiaddr/protocols.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,16 @@ def vcode(self):
100100
return varint.encode(self.code)
101101

102102
def __eq__(self, other):
103+
if not isinstance(other, Protocol):
104+
return NotImplemented
105+
103106
return all((self.code == other.code,
104107
self.name == other.name,
105108
self.codec == other.codec,
106109
self.path == other.path))
107110

108-
def __ne__(self, other):
109-
return not self == other
111+
def __hash__(self):
112+
return self.code
110113

111114
def __repr__(self):
112115
return "Protocol(code={code!r}, name={name!r}, codec={codec!r})".format(
@@ -123,7 +126,7 @@ def _uvarint(buf):
123126
for i, b_str in enumerate(buf):
124127
if six.PY3:
125128
b = b_str
126-
else:
129+
else: # pragma: no cover (PY2)
127130
b = int(binascii.b2a_hex(b_str), 16)
128131
if b < 0x80:
129132
if i > 9 or (i == 9 and b > 1):
@@ -200,6 +203,17 @@ def protocol_with_code(code):
200203
return _codes_to_protocols[code]
201204

202205

206+
def protocol_with_any(proto):
207+
if isinstance(proto, Protocol):
208+
return proto
209+
elif isinstance(proto, int):
210+
return protocol_with_code(proto)
211+
elif isinstance(proto, six.string_types):
212+
return protocol_with_name(proto)
213+
else:
214+
raise TypeError("Protocol object, name or code expected, got {0!r}".format(proto))
215+
216+
203217
def protocols_with_string(string):
204218
"""Return a list of protocols matching given string."""
205219
# Normalize string

multiaddr/transforms.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@
1414

1515

1616
def string_to_bytes(string):
17-
if not string:
18-
return b''
19-
2017
bs = []
2118
for proto, codec, value in string_iter(string):
2219
bs.append(varint.encode(proto.code))
@@ -78,7 +75,7 @@ def string_iter(string):
7875
value = "/" + "/".join(sp)
7976
if not six.PY2:
8077
sp.clear()
81-
else:
78+
else: # pragma: no cover (PY2)
8279
sp = []
8380
else:
8481
value = sp.pop(0)

tests/test_multiaddr.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,17 +190,17 @@ def test_get_value():
190190
"p2p/QmbHVEEepCi7rn7VL7Exxpd2Ci9NNB6ifvqwhsrbRMgQFP")
191191

192192
assert_value_for_proto(ma, P_IP4, "127.0.0.1")
193-
assert_value_for_proto(ma, P_UTP, "")
193+
assert_value_for_proto(ma, P_UTP, None)
194194
assert_value_for_proto(ma, P_TCP, "5555")
195195
assert_value_for_proto(ma, P_UDP, "1234")
196196
assert_value_for_proto(
197197
ma, P_P2P, "QmbHVEEepCi7rn7VL7Exxpd2Ci9NNB6ifvqwhsrbRMgQFP")
198198
assert_value_for_proto(ma, "ip4", "127.0.0.1")
199-
assert_value_for_proto(ma, "utp", "")
199+
assert_value_for_proto(ma, "utp", None)
200200
assert_value_for_proto(ma, "tcp", "5555")
201201
assert_value_for_proto(ma, "udp", "1234")
202202
assert_value_for_proto(ma, protocol_with_name("ip4"), "127.0.0.1")
203-
assert_value_for_proto(ma, protocol_with_name("utp"), "")
203+
assert_value_for_proto(ma, protocol_with_name("utp"), None)
204204
assert_value_for_proto(ma, protocol_with_name("tcp"), "5555")
205205
assert_value_for_proto(ma, protocol_with_name("udp"), "1234")
206206

@@ -224,7 +224,7 @@ def test_get_value():
224224
a = Multiaddr("/ip4/0.0.0.0/udp/12345/utp") # ending in a no-value one.
225225
assert_value_for_proto(a, P_IP4, "0.0.0.0")
226226
assert_value_for_proto(a, P_UDP, "12345")
227-
assert_value_for_proto(a, P_UTP, "")
227+
assert_value_for_proto(a, P_UTP, None)
228228

229229
a = Multiaddr("/ip4/0.0.0.0/unix/a/b/c/d") # ending in a path one.
230230
assert_value_for_proto(a, P_IP4, "0.0.0.0")
@@ -234,6 +234,34 @@ def test_get_value():
234234
assert_value_for_proto(a, P_UNIX, "/studio") # only a path.
235235

236236

237+
def test_views():
238+
ma = Multiaddr(
239+
"/ip4/127.0.0.1/utp/tcp/5555/udp/1234/utp/"
240+
"p2p/QmbHVEEepCi7rn7VL7Exxpd2Ci9NNB6ifvqwhsrbRMgQFP")
241+
242+
for idx, (proto1, proto2, item, value) in enumerate(zip(ma, ma.keys(), ma.items(), ma.values())):
243+
assert (proto1, value) == (proto2, value) == item
244+
assert proto1 in ma
245+
assert proto2 in ma.keys()
246+
assert item in ma.items()
247+
assert value in ma.values()
248+
assert ma.keys()[idx] == ma.keys()[idx-len(ma)] == proto1 == proto2
249+
assert ma.items()[idx] == ma.items()[idx-len(ma)] == item
250+
assert ma.values()[idx] == ma.values()[idx-len(ma)] == ma[proto1] == value
251+
252+
assert len(ma.keys()) == len(ma.items()) == len(ma.values()) == len(ma)
253+
assert len(list(ma.keys())) == len(ma.keys())
254+
assert len(list(ma.items())) == len(ma.items())
255+
assert len(list(ma.values())) == len(ma.values())
256+
257+
with pytest.raises(IndexError):
258+
ma.keys()[len(ma)]
259+
with pytest.raises(IndexError):
260+
ma.items()[len(ma)]
261+
with pytest.raises(IndexError):
262+
ma.values()[len(ma)]
263+
264+
237265
def test_bad_initialization_no_params():
238266
with pytest.raises(TypeError):
239267
Multiaddr()

0 commit comments

Comments
 (0)