Coverage for aiocoap/util/__init__.py: 81%
69 statements
« prev ^ index » next coverage.py v7.6.8, created at 2024-11-28 12:34 +0000
« prev ^ index » next coverage.py v7.6.8, created at 2024-11-28 12:34 +0000
1# SPDX-FileCopyrightText: Christian Amsüss and the aiocoap contributors
2#
3# SPDX-License-Identifier: MIT
5"""Tools not directly related with CoAP that are needed to provide the API
7These are only part of the stable API to the extent they are used by other APIs
8-- for example, you can use the type constructor of :class:`ExtensibleEnumMeta`
9when creating an :class:`aiocoap.numbers.optionnumbers.OptionNumber`, but don't
10expect it to be usable in a stable way for own extensions.
12Most functions are available in submodules; some of them may only have
13components that are exclusively used internally and never part of the public
14API even in the limited fashion stated above.
16.. toctree::
17 :glob:
19 aiocoap.util.*
20"""
22import urllib.parse
23from warnings import warn
24import enum
25import sys
28class ExtensibleEnumMeta(enum.EnumMeta):
29 """Metaclass that provides a workaround for
30 https://github.com/python/cpython/issues/118650 (_repr_html_ is not
31 allowed on enum) for versions before that is fixed"""
33 if sys.version_info < (3, 13, 0, "beta", 1):
35 @classmethod
36 def __prepare__(metacls, cls, bases, **kwd):
37 enum_dict = super().__prepare__(cls, bases, **kwd)
39 class PermissiveEnumDict(type(enum_dict)):
40 def __setitem__(self, key, value):
41 if key == "_repr_html_":
42 # Bypass _EnumDict, go directly for the regular dict
43 # behavior it falls back to for regular items
44 dict.__setitem__(self, key, value)
45 else:
46 super().__setitem__(key, value)
48 permissive_dict = PermissiveEnumDict()
49 dict.update(permissive_dict, enum_dict.items())
50 vars(permissive_dict).update(vars(enum_dict).items())
51 return permissive_dict
54class ExtensibleIntEnum(enum.IntEnum, metaclass=ExtensibleEnumMeta):
55 """Similar to Python's enum.IntEnum, this type can be used for named
56 numbers which are not comprehensively known, like CoAP option numbers."""
58 def __repr__(self):
59 return "<%s %d%s>" % (
60 type(self).__name__,
61 self,
62 ' "%s"' % self.name if hasattr(self, "name") else "",
63 )
65 def __str__(self):
66 return self.name if hasattr(self, "name") else int.__str__(self)
68 def _repr_html_(self):
69 import html
71 if hasattr(self, "name"):
72 return f'<abbr title="{html.escape(type(self).__name__)} {int(self)}">{html.escape(self.name)}</abbr>'
73 else:
74 return f'<abbr title="Unknown {html.escape(type(self).__name__)}">{int(self)}</abbr>'
76 @classmethod
77 def _missing_(cls, value):
78 """Construct a member, sidestepping the lookup (because we know the
79 lookup already failed, and there is no singleton instance to return)"""
80 new_member = int.__new__(cls, value)
81 new_member._value_ = value
82 cls._value2member_map_[value] = new_member
83 return new_member
85 if sys.version_info < (3, 11, 5):
86 # backport of https://github.com/python/cpython/pull/106666
87 #
88 # Without this, Python versions up to 3.11.4 (eg. 3.11.2 in Debian
89 # Bookworm) fail the copy used to modify messages without mutating them
90 # when attempting to access the _name_ of an unknown option.
91 def __copy__(self):
92 return self
94 def __deepcopy__(self, memo):
95 return self
98def hostportjoin(host, port=None):
99 """Join a host and optionally port into a hostinfo-style host:port
100 string
102 >>> hostportjoin('example.com')
103 'example.com'
104 >>> hostportjoin('example.com', 1234)
105 'example.com:1234'
106 >>> hostportjoin('127.0.0.1', 1234)
107 '127.0.0.1:1234'
109 This is lax with respect to whether host is an IPv6 literal in brackets or
110 not, and accepts either form; IP-future literals that do not contain a
111 colon must be already presented in their bracketed form:
113 >>> hostportjoin('2001:db8::1')
114 '[2001:db8::1]'
115 >>> hostportjoin('2001:db8::1', 1234)
116 '[2001:db8::1]:1234'
117 >>> hostportjoin('[2001:db8::1]', 1234)
118 '[2001:db8::1]:1234'
119 """
120 if ":" in host and not (host.startswith("[") and host.endswith("]")):
121 host = "[%s]" % host
123 if port is None:
124 hostinfo = host
125 else:
126 hostinfo = "%s:%d" % (host, port)
127 return hostinfo
130def hostportsplit(hostport):
131 """Like urllib.parse.splitport, but return port as int, and as None if not
132 given. Also, it allows giving IPv6 addresses like a netloc:
134 >>> hostportsplit('foo')
135 ('foo', None)
136 >>> hostportsplit('foo:5683')
137 ('foo', 5683)
138 >>> hostportsplit('[::1%eth0]:56830')
139 ('::1%eth0', 56830)
140 """
142 pseudoparsed = urllib.parse.SplitResult(None, hostport, None, None, None)
143 try:
144 return pseudoparsed.hostname, pseudoparsed.port
145 except ValueError:
146 if "[" not in hostport and hostport.count(":") > 1:
147 raise ValueError(
148 "Could not parse network location. "
149 "Beware that when IPv6 literals are expressed in URIs, they "
150 "need to be put in square brackets to distinguish them from "
151 "port numbers."
152 )
153 raise
156def quote_nonascii(s):
157 """Like urllib.parse.quote, but explicitly only escaping non-ascii characters.
159 This function is deprecated due to it use of the irrelevant "being an ASCII
160 character" property (when instead RFC3986 productions like "unreserved"
161 should be used), and due for removal when aiocoap's URI processing is
162 overhauled the next time.
163 """
165 return "".join(chr(c) if c <= 127 else "%%%02X" % c for c in s.encode("utf8"))
168class Sentinel:
169 """Class for sentinel that can only be compared for identity. No efforts
170 are taken to make these singletons; it is up to the users to always refer
171 to the same instance, which is typically defined on module level.
173 This value can intentionally not be serialized im CBOR or JSON:
175 >>> import json # Not using CBOR as that may not be present for tests
176 >>> FIXME = Sentinel("FIXME")
177 >>> json.dumps([1, FIXME, 3])
178 Traceback (most recent call last):
179 TypeError: Object of type Sentinel is not JSON serializable
180 """
182 def __init__(self, label):
183 self._label = label
185 def __repr__(self):
186 return "<%s>" % self._label
189def deprecation_getattr(_deprecated_aliases: dict, _globals: dict):
190 """Factory for a module-level ``__getattr__`` function
192 This creates deprecation warnings whenever a module level item by one of
193 the keys of the alias dict is accessed by its old name rather than by its
194 new name (which is in the values):
196 >>> FOOBAR = 42
197 >>>
198 >>> __getattr__ = deprecation_getattr({'FOOBRA': 'FOOBAR'}, globals())
199 """
201 def __getattr__(name):
202 if name in _deprecated_aliases:
203 modern = _deprecated_aliases[name]
204 warn(
205 f"{name} is deprecated, use {modern} instead",
206 DeprecationWarning,
207 stacklevel=2,
208 )
209 return _globals[modern]
210 raise AttributeError(f"module {__name__} has no attribute {name}")
212 return __getattr__