annotate urllib3/_collections.py @ 16:dc2c003078e9 tip

planemo upload for repository https://toolrepo.galaxytrakr.org/view/jpayne/bioproject_to_srr_2/556cac4fb538
author jpayne
date Tue, 21 May 2024 01:09:25 -0400
parents 5eb2d5e3bf22
children
rev   line source
jpayne@7 1 from __future__ import annotations
jpayne@7 2
jpayne@7 3 import typing
jpayne@7 4 from collections import OrderedDict
jpayne@7 5 from enum import Enum, auto
jpayne@7 6 from threading import RLock
jpayne@7 7
jpayne@7 8 if typing.TYPE_CHECKING:
jpayne@7 9 # We can only import Protocol if TYPE_CHECKING because it's a development
jpayne@7 10 # dependency, and is not available at runtime.
jpayne@7 11 from typing import Protocol
jpayne@7 12
jpayne@7 13 from typing_extensions import Self
jpayne@7 14
jpayne@7 15 class HasGettableStringKeys(Protocol):
jpayne@7 16 def keys(self) -> typing.Iterator[str]:
jpayne@7 17 ...
jpayne@7 18
jpayne@7 19 def __getitem__(self, key: str) -> str:
jpayne@7 20 ...
jpayne@7 21
jpayne@7 22
jpayne@7 23 __all__ = ["RecentlyUsedContainer", "HTTPHeaderDict"]
jpayne@7 24
jpayne@7 25
jpayne@7 26 # Key type
jpayne@7 27 _KT = typing.TypeVar("_KT")
jpayne@7 28 # Value type
jpayne@7 29 _VT = typing.TypeVar("_VT")
jpayne@7 30 # Default type
jpayne@7 31 _DT = typing.TypeVar("_DT")
jpayne@7 32
jpayne@7 33 ValidHTTPHeaderSource = typing.Union[
jpayne@7 34 "HTTPHeaderDict",
jpayne@7 35 typing.Mapping[str, str],
jpayne@7 36 typing.Iterable[typing.Tuple[str, str]],
jpayne@7 37 "HasGettableStringKeys",
jpayne@7 38 ]
jpayne@7 39
jpayne@7 40
jpayne@7 41 class _Sentinel(Enum):
jpayne@7 42 not_passed = auto()
jpayne@7 43
jpayne@7 44
jpayne@7 45 def ensure_can_construct_http_header_dict(
jpayne@7 46 potential: object,
jpayne@7 47 ) -> ValidHTTPHeaderSource | None:
jpayne@7 48 if isinstance(potential, HTTPHeaderDict):
jpayne@7 49 return potential
jpayne@7 50 elif isinstance(potential, typing.Mapping):
jpayne@7 51 # Full runtime checking of the contents of a Mapping is expensive, so for the
jpayne@7 52 # purposes of typechecking, we assume that any Mapping is the right shape.
jpayne@7 53 return typing.cast(typing.Mapping[str, str], potential)
jpayne@7 54 elif isinstance(potential, typing.Iterable):
jpayne@7 55 # Similarly to Mapping, full runtime checking of the contents of an Iterable is
jpayne@7 56 # expensive, so for the purposes of typechecking, we assume that any Iterable
jpayne@7 57 # is the right shape.
jpayne@7 58 return typing.cast(typing.Iterable[typing.Tuple[str, str]], potential)
jpayne@7 59 elif hasattr(potential, "keys") and hasattr(potential, "__getitem__"):
jpayne@7 60 return typing.cast("HasGettableStringKeys", potential)
jpayne@7 61 else:
jpayne@7 62 return None
jpayne@7 63
jpayne@7 64
jpayne@7 65 class RecentlyUsedContainer(typing.Generic[_KT, _VT], typing.MutableMapping[_KT, _VT]):
jpayne@7 66 """
jpayne@7 67 Provides a thread-safe dict-like container which maintains up to
jpayne@7 68 ``maxsize`` keys while throwing away the least-recently-used keys beyond
jpayne@7 69 ``maxsize``.
jpayne@7 70
jpayne@7 71 :param maxsize:
jpayne@7 72 Maximum number of recent elements to retain.
jpayne@7 73
jpayne@7 74 :param dispose_func:
jpayne@7 75 Every time an item is evicted from the container,
jpayne@7 76 ``dispose_func(value)`` is called. Callback which will get called
jpayne@7 77 """
jpayne@7 78
jpayne@7 79 _container: typing.OrderedDict[_KT, _VT]
jpayne@7 80 _maxsize: int
jpayne@7 81 dispose_func: typing.Callable[[_VT], None] | None
jpayne@7 82 lock: RLock
jpayne@7 83
jpayne@7 84 def __init__(
jpayne@7 85 self,
jpayne@7 86 maxsize: int = 10,
jpayne@7 87 dispose_func: typing.Callable[[_VT], None] | None = None,
jpayne@7 88 ) -> None:
jpayne@7 89 super().__init__()
jpayne@7 90 self._maxsize = maxsize
jpayne@7 91 self.dispose_func = dispose_func
jpayne@7 92 self._container = OrderedDict()
jpayne@7 93 self.lock = RLock()
jpayne@7 94
jpayne@7 95 def __getitem__(self, key: _KT) -> _VT:
jpayne@7 96 # Re-insert the item, moving it to the end of the eviction line.
jpayne@7 97 with self.lock:
jpayne@7 98 item = self._container.pop(key)
jpayne@7 99 self._container[key] = item
jpayne@7 100 return item
jpayne@7 101
jpayne@7 102 def __setitem__(self, key: _KT, value: _VT) -> None:
jpayne@7 103 evicted_item = None
jpayne@7 104 with self.lock:
jpayne@7 105 # Possibly evict the existing value of 'key'
jpayne@7 106 try:
jpayne@7 107 # If the key exists, we'll overwrite it, which won't change the
jpayne@7 108 # size of the pool. Because accessing a key should move it to
jpayne@7 109 # the end of the eviction line, we pop it out first.
jpayne@7 110 evicted_item = key, self._container.pop(key)
jpayne@7 111 self._container[key] = value
jpayne@7 112 except KeyError:
jpayne@7 113 # When the key does not exist, we insert the value first so that
jpayne@7 114 # evicting works in all cases, including when self._maxsize is 0
jpayne@7 115 self._container[key] = value
jpayne@7 116 if len(self._container) > self._maxsize:
jpayne@7 117 # If we didn't evict an existing value, and we've hit our maximum
jpayne@7 118 # size, then we have to evict the least recently used item from
jpayne@7 119 # the beginning of the container.
jpayne@7 120 evicted_item = self._container.popitem(last=False)
jpayne@7 121
jpayne@7 122 # After releasing the lock on the pool, dispose of any evicted value.
jpayne@7 123 if evicted_item is not None and self.dispose_func:
jpayne@7 124 _, evicted_value = evicted_item
jpayne@7 125 self.dispose_func(evicted_value)
jpayne@7 126
jpayne@7 127 def __delitem__(self, key: _KT) -> None:
jpayne@7 128 with self.lock:
jpayne@7 129 value = self._container.pop(key)
jpayne@7 130
jpayne@7 131 if self.dispose_func:
jpayne@7 132 self.dispose_func(value)
jpayne@7 133
jpayne@7 134 def __len__(self) -> int:
jpayne@7 135 with self.lock:
jpayne@7 136 return len(self._container)
jpayne@7 137
jpayne@7 138 def __iter__(self) -> typing.NoReturn:
jpayne@7 139 raise NotImplementedError(
jpayne@7 140 "Iteration over this class is unlikely to be threadsafe."
jpayne@7 141 )
jpayne@7 142
jpayne@7 143 def clear(self) -> None:
jpayne@7 144 with self.lock:
jpayne@7 145 # Copy pointers to all values, then wipe the mapping
jpayne@7 146 values = list(self._container.values())
jpayne@7 147 self._container.clear()
jpayne@7 148
jpayne@7 149 if self.dispose_func:
jpayne@7 150 for value in values:
jpayne@7 151 self.dispose_func(value)
jpayne@7 152
jpayne@7 153 def keys(self) -> set[_KT]: # type: ignore[override]
jpayne@7 154 with self.lock:
jpayne@7 155 return set(self._container.keys())
jpayne@7 156
jpayne@7 157
jpayne@7 158 class HTTPHeaderDictItemView(typing.Set[typing.Tuple[str, str]]):
jpayne@7 159 """
jpayne@7 160 HTTPHeaderDict is unusual for a Mapping[str, str] in that it has two modes of
jpayne@7 161 address.
jpayne@7 162
jpayne@7 163 If we directly try to get an item with a particular name, we will get a string
jpayne@7 164 back that is the concatenated version of all the values:
jpayne@7 165
jpayne@7 166 >>> d['X-Header-Name']
jpayne@7 167 'Value1, Value2, Value3'
jpayne@7 168
jpayne@7 169 However, if we iterate over an HTTPHeaderDict's items, we will optionally combine
jpayne@7 170 these values based on whether combine=True was called when building up the dictionary
jpayne@7 171
jpayne@7 172 >>> d = HTTPHeaderDict({"A": "1", "B": "foo"})
jpayne@7 173 >>> d.add("A", "2", combine=True)
jpayne@7 174 >>> d.add("B", "bar")
jpayne@7 175 >>> list(d.items())
jpayne@7 176 [
jpayne@7 177 ('A', '1, 2'),
jpayne@7 178 ('B', 'foo'),
jpayne@7 179 ('B', 'bar'),
jpayne@7 180 ]
jpayne@7 181
jpayne@7 182 This class conforms to the interface required by the MutableMapping ABC while
jpayne@7 183 also giving us the nonstandard iteration behavior we want; items with duplicate
jpayne@7 184 keys, ordered by time of first insertion.
jpayne@7 185 """
jpayne@7 186
jpayne@7 187 _headers: HTTPHeaderDict
jpayne@7 188
jpayne@7 189 def __init__(self, headers: HTTPHeaderDict) -> None:
jpayne@7 190 self._headers = headers
jpayne@7 191
jpayne@7 192 def __len__(self) -> int:
jpayne@7 193 return len(list(self._headers.iteritems()))
jpayne@7 194
jpayne@7 195 def __iter__(self) -> typing.Iterator[tuple[str, str]]:
jpayne@7 196 return self._headers.iteritems()
jpayne@7 197
jpayne@7 198 def __contains__(self, item: object) -> bool:
jpayne@7 199 if isinstance(item, tuple) and len(item) == 2:
jpayne@7 200 passed_key, passed_val = item
jpayne@7 201 if isinstance(passed_key, str) and isinstance(passed_val, str):
jpayne@7 202 return self._headers._has_value_for_header(passed_key, passed_val)
jpayne@7 203 return False
jpayne@7 204
jpayne@7 205
jpayne@7 206 class HTTPHeaderDict(typing.MutableMapping[str, str]):
jpayne@7 207 """
jpayne@7 208 :param headers:
jpayne@7 209 An iterable of field-value pairs. Must not contain multiple field names
jpayne@7 210 when compared case-insensitively.
jpayne@7 211
jpayne@7 212 :param kwargs:
jpayne@7 213 Additional field-value pairs to pass in to ``dict.update``.
jpayne@7 214
jpayne@7 215 A ``dict`` like container for storing HTTP Headers.
jpayne@7 216
jpayne@7 217 Field names are stored and compared case-insensitively in compliance with
jpayne@7 218 RFC 7230. Iteration provides the first case-sensitive key seen for each
jpayne@7 219 case-insensitive pair.
jpayne@7 220
jpayne@7 221 Using ``__setitem__`` syntax overwrites fields that compare equal
jpayne@7 222 case-insensitively in order to maintain ``dict``'s api. For fields that
jpayne@7 223 compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add``
jpayne@7 224 in a loop.
jpayne@7 225
jpayne@7 226 If multiple fields that are equal case-insensitively are passed to the
jpayne@7 227 constructor or ``.update``, the behavior is undefined and some will be
jpayne@7 228 lost.
jpayne@7 229
jpayne@7 230 >>> headers = HTTPHeaderDict()
jpayne@7 231 >>> headers.add('Set-Cookie', 'foo=bar')
jpayne@7 232 >>> headers.add('set-cookie', 'baz=quxx')
jpayne@7 233 >>> headers['content-length'] = '7'
jpayne@7 234 >>> headers['SET-cookie']
jpayne@7 235 'foo=bar, baz=quxx'
jpayne@7 236 >>> headers['Content-Length']
jpayne@7 237 '7'
jpayne@7 238 """
jpayne@7 239
jpayne@7 240 _container: typing.MutableMapping[str, list[str]]
jpayne@7 241
jpayne@7 242 def __init__(self, headers: ValidHTTPHeaderSource | None = None, **kwargs: str):
jpayne@7 243 super().__init__()
jpayne@7 244 self._container = {} # 'dict' is insert-ordered
jpayne@7 245 if headers is not None:
jpayne@7 246 if isinstance(headers, HTTPHeaderDict):
jpayne@7 247 self._copy_from(headers)
jpayne@7 248 else:
jpayne@7 249 self.extend(headers)
jpayne@7 250 if kwargs:
jpayne@7 251 self.extend(kwargs)
jpayne@7 252
jpayne@7 253 def __setitem__(self, key: str, val: str) -> None:
jpayne@7 254 # avoid a bytes/str comparison by decoding before httplib
jpayne@7 255 if isinstance(key, bytes):
jpayne@7 256 key = key.decode("latin-1")
jpayne@7 257 self._container[key.lower()] = [key, val]
jpayne@7 258
jpayne@7 259 def __getitem__(self, key: str) -> str:
jpayne@7 260 val = self._container[key.lower()]
jpayne@7 261 return ", ".join(val[1:])
jpayne@7 262
jpayne@7 263 def __delitem__(self, key: str) -> None:
jpayne@7 264 del self._container[key.lower()]
jpayne@7 265
jpayne@7 266 def __contains__(self, key: object) -> bool:
jpayne@7 267 if isinstance(key, str):
jpayne@7 268 return key.lower() in self._container
jpayne@7 269 return False
jpayne@7 270
jpayne@7 271 def setdefault(self, key: str, default: str = "") -> str:
jpayne@7 272 return super().setdefault(key, default)
jpayne@7 273
jpayne@7 274 def __eq__(self, other: object) -> bool:
jpayne@7 275 maybe_constructable = ensure_can_construct_http_header_dict(other)
jpayne@7 276 if maybe_constructable is None:
jpayne@7 277 return False
jpayne@7 278 else:
jpayne@7 279 other_as_http_header_dict = type(self)(maybe_constructable)
jpayne@7 280
jpayne@7 281 return {k.lower(): v for k, v in self.itermerged()} == {
jpayne@7 282 k.lower(): v for k, v in other_as_http_header_dict.itermerged()
jpayne@7 283 }
jpayne@7 284
jpayne@7 285 def __ne__(self, other: object) -> bool:
jpayne@7 286 return not self.__eq__(other)
jpayne@7 287
jpayne@7 288 def __len__(self) -> int:
jpayne@7 289 return len(self._container)
jpayne@7 290
jpayne@7 291 def __iter__(self) -> typing.Iterator[str]:
jpayne@7 292 # Only provide the originally cased names
jpayne@7 293 for vals in self._container.values():
jpayne@7 294 yield vals[0]
jpayne@7 295
jpayne@7 296 def discard(self, key: str) -> None:
jpayne@7 297 try:
jpayne@7 298 del self[key]
jpayne@7 299 except KeyError:
jpayne@7 300 pass
jpayne@7 301
jpayne@7 302 def add(self, key: str, val: str, *, combine: bool = False) -> None:
jpayne@7 303 """Adds a (name, value) pair, doesn't overwrite the value if it already
jpayne@7 304 exists.
jpayne@7 305
jpayne@7 306 If this is called with combine=True, instead of adding a new header value
jpayne@7 307 as a distinct item during iteration, this will instead append the value to
jpayne@7 308 any existing header value with a comma. If no existing header value exists
jpayne@7 309 for the key, then the value will simply be added, ignoring the combine parameter.
jpayne@7 310
jpayne@7 311 >>> headers = HTTPHeaderDict(foo='bar')
jpayne@7 312 >>> headers.add('Foo', 'baz')
jpayne@7 313 >>> headers['foo']
jpayne@7 314 'bar, baz'
jpayne@7 315 >>> list(headers.items())
jpayne@7 316 [('foo', 'bar'), ('foo', 'baz')]
jpayne@7 317 >>> headers.add('foo', 'quz', combine=True)
jpayne@7 318 >>> list(headers.items())
jpayne@7 319 [('foo', 'bar, baz, quz')]
jpayne@7 320 """
jpayne@7 321 # avoid a bytes/str comparison by decoding before httplib
jpayne@7 322 if isinstance(key, bytes):
jpayne@7 323 key = key.decode("latin-1")
jpayne@7 324 key_lower = key.lower()
jpayne@7 325 new_vals = [key, val]
jpayne@7 326 # Keep the common case aka no item present as fast as possible
jpayne@7 327 vals = self._container.setdefault(key_lower, new_vals)
jpayne@7 328 if new_vals is not vals:
jpayne@7 329 # if there are values here, then there is at least the initial
jpayne@7 330 # key/value pair
jpayne@7 331 assert len(vals) >= 2
jpayne@7 332 if combine:
jpayne@7 333 vals[-1] = vals[-1] + ", " + val
jpayne@7 334 else:
jpayne@7 335 vals.append(val)
jpayne@7 336
jpayne@7 337 def extend(self, *args: ValidHTTPHeaderSource, **kwargs: str) -> None:
jpayne@7 338 """Generic import function for any type of header-like object.
jpayne@7 339 Adapted version of MutableMapping.update in order to insert items
jpayne@7 340 with self.add instead of self.__setitem__
jpayne@7 341 """
jpayne@7 342 if len(args) > 1:
jpayne@7 343 raise TypeError(
jpayne@7 344 f"extend() takes at most 1 positional arguments ({len(args)} given)"
jpayne@7 345 )
jpayne@7 346 other = args[0] if len(args) >= 1 else ()
jpayne@7 347
jpayne@7 348 if isinstance(other, HTTPHeaderDict):
jpayne@7 349 for key, val in other.iteritems():
jpayne@7 350 self.add(key, val)
jpayne@7 351 elif isinstance(other, typing.Mapping):
jpayne@7 352 for key, val in other.items():
jpayne@7 353 self.add(key, val)
jpayne@7 354 elif isinstance(other, typing.Iterable):
jpayne@7 355 other = typing.cast(typing.Iterable[typing.Tuple[str, str]], other)
jpayne@7 356 for key, value in other:
jpayne@7 357 self.add(key, value)
jpayne@7 358 elif hasattr(other, "keys") and hasattr(other, "__getitem__"):
jpayne@7 359 # THIS IS NOT A TYPESAFE BRANCH
jpayne@7 360 # In this branch, the object has a `keys` attr but is not a Mapping or any of
jpayne@7 361 # the other types indicated in the method signature. We do some stuff with
jpayne@7 362 # it as though it partially implements the Mapping interface, but we're not
jpayne@7 363 # doing that stuff safely AT ALL.
jpayne@7 364 for key in other.keys():
jpayne@7 365 self.add(key, other[key])
jpayne@7 366
jpayne@7 367 for key, value in kwargs.items():
jpayne@7 368 self.add(key, value)
jpayne@7 369
jpayne@7 370 @typing.overload
jpayne@7 371 def getlist(self, key: str) -> list[str]:
jpayne@7 372 ...
jpayne@7 373
jpayne@7 374 @typing.overload
jpayne@7 375 def getlist(self, key: str, default: _DT) -> list[str] | _DT:
jpayne@7 376 ...
jpayne@7 377
jpayne@7 378 def getlist(
jpayne@7 379 self, key: str, default: _Sentinel | _DT = _Sentinel.not_passed
jpayne@7 380 ) -> list[str] | _DT:
jpayne@7 381 """Returns a list of all the values for the named field. Returns an
jpayne@7 382 empty list if the key doesn't exist."""
jpayne@7 383 try:
jpayne@7 384 vals = self._container[key.lower()]
jpayne@7 385 except KeyError:
jpayne@7 386 if default is _Sentinel.not_passed:
jpayne@7 387 # _DT is unbound; empty list is instance of List[str]
jpayne@7 388 return []
jpayne@7 389 # _DT is bound; default is instance of _DT
jpayne@7 390 return default
jpayne@7 391 else:
jpayne@7 392 # _DT may or may not be bound; vals[1:] is instance of List[str], which
jpayne@7 393 # meets our external interface requirement of `Union[List[str], _DT]`.
jpayne@7 394 return vals[1:]
jpayne@7 395
jpayne@7 396 def _prepare_for_method_change(self) -> Self:
jpayne@7 397 """
jpayne@7 398 Remove content-specific header fields before changing the request
jpayne@7 399 method to GET or HEAD according to RFC 9110, Section 15.4.
jpayne@7 400 """
jpayne@7 401 content_specific_headers = [
jpayne@7 402 "Content-Encoding",
jpayne@7 403 "Content-Language",
jpayne@7 404 "Content-Location",
jpayne@7 405 "Content-Type",
jpayne@7 406 "Content-Length",
jpayne@7 407 "Digest",
jpayne@7 408 "Last-Modified",
jpayne@7 409 ]
jpayne@7 410 for header in content_specific_headers:
jpayne@7 411 self.discard(header)
jpayne@7 412 return self
jpayne@7 413
jpayne@7 414 # Backwards compatibility for httplib
jpayne@7 415 getheaders = getlist
jpayne@7 416 getallmatchingheaders = getlist
jpayne@7 417 iget = getlist
jpayne@7 418
jpayne@7 419 # Backwards compatibility for http.cookiejar
jpayne@7 420 get_all = getlist
jpayne@7 421
jpayne@7 422 def __repr__(self) -> str:
jpayne@7 423 return f"{type(self).__name__}({dict(self.itermerged())})"
jpayne@7 424
jpayne@7 425 def _copy_from(self, other: HTTPHeaderDict) -> None:
jpayne@7 426 for key in other:
jpayne@7 427 val = other.getlist(key)
jpayne@7 428 self._container[key.lower()] = [key, *val]
jpayne@7 429
jpayne@7 430 def copy(self) -> HTTPHeaderDict:
jpayne@7 431 clone = type(self)()
jpayne@7 432 clone._copy_from(self)
jpayne@7 433 return clone
jpayne@7 434
jpayne@7 435 def iteritems(self) -> typing.Iterator[tuple[str, str]]:
jpayne@7 436 """Iterate over all header lines, including duplicate ones."""
jpayne@7 437 for key in self:
jpayne@7 438 vals = self._container[key.lower()]
jpayne@7 439 for val in vals[1:]:
jpayne@7 440 yield vals[0], val
jpayne@7 441
jpayne@7 442 def itermerged(self) -> typing.Iterator[tuple[str, str]]:
jpayne@7 443 """Iterate over all headers, merging duplicate ones together."""
jpayne@7 444 for key in self:
jpayne@7 445 val = self._container[key.lower()]
jpayne@7 446 yield val[0], ", ".join(val[1:])
jpayne@7 447
jpayne@7 448 def items(self) -> HTTPHeaderDictItemView: # type: ignore[override]
jpayne@7 449 return HTTPHeaderDictItemView(self)
jpayne@7 450
jpayne@7 451 def _has_value_for_header(self, header_name: str, potential_value: str) -> bool:
jpayne@7 452 if header_name in self:
jpayne@7 453 return potential_value in self._container[header_name.lower()][1:]
jpayne@7 454 return False
jpayne@7 455
jpayne@7 456 def __ior__(self, other: object) -> HTTPHeaderDict:
jpayne@7 457 # Supports extending a header dict in-place using operator |=
jpayne@7 458 # combining items with add instead of __setitem__
jpayne@7 459 maybe_constructable = ensure_can_construct_http_header_dict(other)
jpayne@7 460 if maybe_constructable is None:
jpayne@7 461 return NotImplemented
jpayne@7 462 self.extend(maybe_constructable)
jpayne@7 463 return self
jpayne@7 464
jpayne@7 465 def __or__(self, other: object) -> HTTPHeaderDict:
jpayne@7 466 # Supports merging header dicts using operator |
jpayne@7 467 # combining items with add instead of __setitem__
jpayne@7 468 maybe_constructable = ensure_can_construct_http_header_dict(other)
jpayne@7 469 if maybe_constructable is None:
jpayne@7 470 return NotImplemented
jpayne@7 471 result = self.copy()
jpayne@7 472 result.extend(maybe_constructable)
jpayne@7 473 return result
jpayne@7 474
jpayne@7 475 def __ror__(self, other: object) -> HTTPHeaderDict:
jpayne@7 476 # Supports merging header dicts using operator | when other is on left side
jpayne@7 477 # combining items with add instead of __setitem__
jpayne@7 478 maybe_constructable = ensure_can_construct_http_header_dict(other)
jpayne@7 479 if maybe_constructable is None:
jpayne@7 480 return NotImplemented
jpayne@7 481 result = type(self)(maybe_constructable)
jpayne@7 482 result.extend(self)
jpayne@7 483 return result