Skip to content

Commit 876d998

Browse files
committed
Add global read-only protocol registry for default Multiaddr protocols with new support for custom user-managed registries
1 parent 1878efa commit 876d998

File tree

5 files changed

+263
-111
lines changed

5 files changed

+263
-111
lines changed

multiaddr/exceptions.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,21 @@ def __init__(self, message, binary, protocol, original=None):
5959
super().__init__(message)
6060

6161

62-
class ProtocolManagerError(Error):
62+
class ProtocolRegistryError(Error):
6363
pass
6464

6565

66-
class ProtocolExistsError(ProtocolManagerError):
67-
"""
68-
Protocol with the given name or code already exists
69-
"""
66+
ProtocolManagerError = ProtocolRegistryError
67+
68+
69+
class ProtocolRegistryLocked(Error):
70+
"""Protocol registry was locked and doesn't allow any further additions"""
71+
def __init__(self):
72+
super().__init__("Protocol registry is locked and does not accept any new values")
73+
74+
75+
class ProtocolExistsError(ProtocolRegistryError):
76+
"""Protocol with the given name or code already exists"""
7077
def __init__(self, proto, kind="name"):
7178
self.proto = proto
7279
self.kind = kind
@@ -76,10 +83,8 @@ def __init__(self, proto, kind="name"):
7683
)
7784

7885

79-
class ProtocolNotFoundError(ProtocolManagerError):
80-
"""
81-
No protocol with the given name or code found
82-
"""
86+
class ProtocolNotFoundError(ProtocolRegistryError):
87+
"""No protocol with the given name or code found"""
8388
def __init__(self, value, kind="name"):
8489
self.value = value
8590
self.kind = kind

multiaddr/multiaddr.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
class MultiAddrKeys(collections.abc.KeysView, collections.abc.Sequence):
1616
def __contains__(self, proto):
17-
proto = protocols.protocol_with_any(proto)
17+
proto = self._mapping.registry.find(proto)
1818
return collections.abc.Sequence.__contains__(self, proto)
1919

2020
def __getitem__(self, idx):
@@ -35,7 +35,7 @@ def __iter__(self):
3535
class MultiAddrItems(collections.abc.ItemsView, collections.abc.Sequence):
3636
def __contains__(self, item):
3737
proto, value = item
38-
proto = protocols.protocol_with_any(proto)
38+
proto = self._mapping.registry.find(proto)
3939
return collections.abc.Sequence.__contains__(self, (proto, value))
4040

4141
def __getitem__(self, idx):
@@ -98,15 +98,16 @@ class Multiaddr(collections.abc.Mapping):
9898
return new objects rather than modify internal state.
9999
"""
100100

101-
__slots__ = ("_bytes",)
101+
__slots__ = ("_bytes", "registry")
102102

103-
def __init__(self, addr):
103+
def __init__(self, addr, *, registry=protocols.REGISTRY):
104104
"""Instantiate a new Multiaddr.
105105
106106
Args:
107107
addr : A string-encoded or a byte-encoded Multiaddr
108108
109109
"""
110+
self.registry = registry
110111
if isinstance(addr, str):
111112
self._bytes = string_to_bytes(addr)
112113
elif isinstance(addr, bytes):
@@ -228,7 +229,7 @@ def value_for_protocol(self, proto):
228229
~multiaddr.exceptions.ProtocolLookupError
229230
MultiAddr does not contain any instance of this protocol
230231
"""
231-
proto = protocols.protocol_with_any(proto)
232+
proto = self.registry.find(proto)
232233
for proto2, value in self.items():
233234
if proto2 is proto or proto2 == proto:
234235
return value

multiaddr/protocols.py

Lines changed: 160 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from . import exceptions
44
from .codecs import codec_by_name
55

6+
__all__ = ("Protocol", "PROTOCOLS", "REGISTRY")
7+
68

79
# source of protocols https://github.com/multiformats/multicodec/blob/master/table.csv#L382
810
# replicating table here to:
@@ -88,7 +90,7 @@ def __repr__(self):
8890
)
8991

9092

91-
# Protocols is the list of multiaddr protocols supported by this module.
93+
# List of multiaddr protocols supported by this module by default
9294
PROTOCOLS = [
9395
Protocol(P_IP4, 'ip4', 'ip4'),
9496
Protocol(P_TCP, 'tcp', 'uint16be'),
@@ -118,56 +120,180 @@ def __repr__(self):
118120
Protocol(P_UNIX, 'unix', 'fspath'),
119121
]
120122

121-
_names_to_protocols = {proto.name: proto for proto in PROTOCOLS}
122-
_codes_to_protocols = {proto.code: proto for proto in PROTOCOLS}
123123

124+
class ProtocolRegistry:
125+
"""A collection of individual Multiaddr protocols indexed for fast lookup"""
126+
__slots__ = ("_codes_to_protocols", "_locked", "_names_to_protocols")
127+
128+
def __init__(self, protocols=()):
129+
self._locked = False
130+
self._codes_to_protocols = {proto.code: proto for proto in protocols}
131+
self._names_to_protocols = {proto.name: proto for proto in protocols}
132+
133+
def add(self, proto):
134+
"""Add the given protocol description to this registry
135+
136+
Raises
137+
------
138+
~multiaddr.exceptions.ProtocolRegistryLocked
139+
Protocol registry is locked and does not accept any new entries.
140+
141+
You can use `.copy(unlock=True)` to copy an existing locked registry
142+
and unlock it.
143+
~multiaddr.exceptions.ProtocolExistsError
144+
A protocol with the given name or code already exists.
145+
"""
146+
if self._locked:
147+
raise exceptions.ProtocolRegistryLocked()
148+
149+
if proto.name in self._names_to_protocols:
150+
raise exceptions.ProtocolExistsError(proto, "name")
151+
152+
if proto.code in self._codes_to_protocols:
153+
raise exceptions.ProtocolExistsError(proto, "code")
154+
155+
self._names_to_protocols[proto.name] = proto
156+
self._codes_to_protocols[proto.code] = proto
157+
return proto
158+
159+
def add_alias_name(self, proto, alias_name):
160+
"""Add an alternate name for an existing protocol description to the registry
161+
162+
Raises
163+
------
164+
~multiaddr.exceptions.ProtocolRegistryLocked
165+
Protocol registry is locked and does not accept any new entries.
166+
167+
You can use `.copy(unlock=True)` to copy an existing locked registry
168+
and unlock it.
169+
~multiaddr.exceptions.ProtocolExistsError
170+
A protocol with the given name already exists.
171+
~multiaddr.exceptions.ProtocolNotFoundError
172+
No protocol matching *proto* could be found.
173+
"""
174+
if self._locked:
175+
raise exceptions.ProtocolRegistryLocked()
176+
177+
proto = self.find(proto)
178+
assert self._names_to_protocols.get(proto.name) is proto, \
179+
"Protocol to alias must have already been added to the registry"
180+
181+
if alias_name in self._names_to_protocols:
182+
raise exceptions.ProtocolExistsError(self._names_to_protocols[alias_name], "name")
183+
184+
self._names_to_protocols[alias_name] = proto
185+
186+
def add_alias_code(self, proto, alias_code):
187+
"""Add an alternate code for an existing protocol description to the registry
188+
189+
Raises
190+
------
191+
~multiaddr.exceptions.ProtocolRegistryLocked
192+
Protocol registry is locked and does not accept any new entries.
124193
125-
def add_protocol(proto):
126-
if proto.name in _names_to_protocols:
127-
raise exceptions.ProtocolExistsError(proto, "name")
194+
You can use `.copy(unlock=True)` to copy an existing locked registry
195+
and unlock it.
196+
~multiaddr.exceptions.ProtocolExistsError
197+
A protocol with the given code already exists.
198+
~multiaddr.exceptions.ProtocolNotFoundError
199+
No protocol matching *proto* could be found.
200+
"""
201+
if self._locked:
202+
raise exceptions.ProtocolRegistryLocked()
128203

129-
if proto.code in _codes_to_protocols:
130-
raise exceptions.ProtocolExistsError(proto, "code")
204+
proto = self.find(proto)
205+
assert self._codes_to_protocols.get(proto.code) is proto, \
206+
"Protocol to alias must have already been added to the registry"
131207

132-
PROTOCOLS.append(proto)
133-
_names_to_protocols[proto.name] = proto
134-
_codes_to_protocols[proto.code] = proto
135-
return None
208+
if alias_code in self._codes_to_protocols:
209+
raise exceptions.ProtocolExistsError(self._codes_to_protocols[alias_code], "name")
210+
211+
self._codes_to_protocols[alias_code] = proto
212+
213+
def lock(self):
214+
"""Lock this registry instance to deny any further changes"""
215+
self._locked = True
216+
217+
@property
218+
def locked(self):
219+
return self._locked
220+
221+
def copy(self, *, unlock=False):
222+
"""Create a copy of this protocol registry
223+
224+
Arguments
225+
---------
226+
unlock
227+
Create the copied registry unlocked even if the current one is locked?
228+
"""
229+
registry = ProtocolRegistry()
230+
registry._locked = self._locked and not unlock
231+
registry._codes_to_protocols = self._codes_to_protocols.copy()
232+
registry._names_to_protocols = self._names_to_protocols.copy()
233+
return registry
234+
235+
__copy__ = copy
236+
237+
def find_by_name(self, name):
238+
"""Look up a protocol by its human-readable name
239+
240+
Raises
241+
------
242+
~multiaddr.exceptions.ProtocolNotFoundError
243+
"""
244+
if name not in self._names_to_protocols:
245+
raise exceptions.ProtocolNotFoundError(name, "name")
246+
return self._names_to_protocols[name]
247+
248+
def find_by_code(self, code):
249+
"""Look up a protocol by its binary representation code
250+
251+
Raises
252+
------
253+
~multiaddr.exceptions.ProtocolNotFoundError
254+
"""
255+
if code not in self._codes_to_protocols:
256+
raise exceptions.ProtocolNotFoundError(code, "code")
257+
return self._codes_to_protocols[code]
258+
259+
def find(self, proto):
260+
"""Look up a protocol by its name or code, return existing protocol objects unchanged
261+
262+
Raises
263+
------
264+
~multiaddr.exceptions.ProtocolNotFoundError
265+
"""
266+
if isinstance(proto, Protocol):
267+
return proto
268+
elif isinstance(proto, str):
269+
return self.find_by_name(proto)
270+
elif isinstance(proto, int):
271+
return self.find_by_code(proto)
272+
else:
273+
raise TypeError("Protocol object, name or code expected, got {0!r}".format(proto))
274+
275+
276+
REGISTRY = ProtocolRegistry(PROTOCOLS)
277+
REGISTRY.add_alias_name("p2p", "ipfs")
278+
REGISTRY.lock()
136279

137280

138281
def protocol_with_name(name):
139-
if name not in _names_to_protocols:
140-
raise exceptions.ProtocolNotFoundError(name, "name")
141-
return _names_to_protocols[name]
282+
return REGISTRY.find_by_name(name)
142283

143284

144285
def protocol_with_code(code):
145-
if code not in _codes_to_protocols:
146-
raise exceptions.ProtocolNotFoundError(code, "code")
147-
return _codes_to_protocols[code]
286+
return REGISTRY.find_by_code(code)
148287

149288

150289
def protocol_with_any(proto):
151-
if isinstance(proto, Protocol):
152-
return proto
153-
elif isinstance(proto, int):
154-
return protocol_with_code(proto)
155-
elif isinstance(proto, str):
156-
return protocol_with_name(proto)
157-
else:
158-
raise TypeError("Protocol object, name or code expected, got {0!r}".format(proto))
290+
return REGISTRY.find(proto)
159291

160292

161293
def protocols_with_string(string):
162294
"""Return a list of protocols matching given string."""
163-
# Normalize string
164-
while "//" in string:
165-
string = string.replace("//", "/")
166-
string = string.strip("/")
167-
if not string:
168-
return []
169-
170295
ret = []
171296
for name in string.split("/"):
172-
ret.append(protocol_with_name(name))
297+
if len(name) > 0:
298+
ret.append(protocol_with_name(name))
173299
return ret

0 commit comments

Comments
 (0)