Coverage for aiocoap/util/__init__.py: 81%

69 statements  

« prev     ^ index     » next       coverage.py v7.5.4, created at 2024-07-10 11:47 +0000

1# SPDX-FileCopyrightText: Christian Amsüss and the aiocoap contributors 

2# 

3# SPDX-License-Identifier: MIT 

4 

5"""Tools not directly related with CoAP that are needed to provide the API 

6 

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. 

11 

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. 

15 

16.. toctree:: 

17 :glob: 

18 

19 aiocoap.util.* 

20""" 

21 

22import urllib.parse 

23from warnings import warn 

24import enum 

25import sys 

26 

27 

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""" 

32 

33 if sys.version_info < (3, 13, 0, "beta", 1): 

34 

35 @classmethod 

36 def __prepare__(metacls, cls, bases, **kwd): 

37 enum_dict = super().__prepare__(cls, bases, **kwd) 

38 

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) 

47 

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 

52 

53 

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.""" 

57 

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 ) 

64 

65 def __str__(self): 

66 return self.name if hasattr(self, "name") else int.__str__(self) 

67 

68 def _repr_html_(self): 

69 import html 

70 

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>' 

75 

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 

84 

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 

93 

94 def __deepcopy__(self, memo): 

95 return self 

96 

97 

98def hostportjoin(host, port=None): 

99 """Join a host and optionally port into a hostinfo-style host:port 

100 string 

101 

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' 

108 

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: 

112 

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 

122 

123 if port is None: 

124 hostinfo = host 

125 else: 

126 hostinfo = "%s:%d" % (host, port) 

127 return hostinfo 

128 

129 

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: 

133 

134 >>> hostportsplit('foo') 

135 ('foo', None) 

136 >>> hostportsplit('foo:5683') 

137 ('foo', 5683) 

138 >>> hostportsplit('[::1%eth0]:56830') 

139 ('::1%eth0', 56830) 

140 """ 

141 

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 

154 

155 

156def quote_nonascii(s): 

157 """Like urllib.parse.quote, but explicitly only escaping non-ascii characters. 

158 

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 """ 

164 

165 return "".join(chr(c) if c <= 127 else "%%%02X" % c for c in s.encode("utf8")) 

166 

167 

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.""" 

172 

173 def __init__(self, label): 

174 self._label = label 

175 

176 def __repr__(self): 

177 return "<%s>" % self._label 

178 

179 

180def deprecation_getattr(_deprecated_aliases: dict, _globals: dict): 

181 """Factory for a module-level ``__getattr__`` function 

182 

183 This creates deprecation warnings whenever a module level item by one of 

184 the keys of the alias dict is accessed by its old name rather than by its 

185 new name (which is in the values): 

186 

187 >>> FOOBAR = 42 

188 >>> 

189 >>> __getattr__ = deprecation_getattr({'FOOBRA': 'FOOBAR'}, globals()) 

190 """ 

191 

192 def __getattr__(name): 

193 if name in _deprecated_aliases: 

194 modern = _deprecated_aliases[name] 

195 warn( 

196 f"{name} is deprecated, use {modern} instead", 

197 DeprecationWarning, 

198 stacklevel=2, 

199 ) 

200 return _globals[modern] 

201 raise AttributeError(f"module {__name__} has no attribute {name}") 

202 

203 return __getattr__