Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#!/usr/bin/env python 

2# cardinal_pythonlib/enumlike.py 

3 

4""" 

5=============================================================================== 

6 

7 Original code copyright (C) 2009-2021 Rudolf Cardinal (rudolf@pobox.com). 

8 

9 This file is part of cardinal_pythonlib. 

10 

11 Licensed under the Apache License, Version 2.0 (the "License"); 

12 you may not use this file except in compliance with the License. 

13 You may obtain a copy of the License at 

14 

15 https://www.apache.org/licenses/LICENSE-2.0 

16 

17 Unless required by applicable law or agreed to in writing, software 

18 distributed under the License is distributed on an "AS IS" BASIS, 

19 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

20 See the License for the specific language governing permissions and 

21 limitations under the License. 

22 

23=============================================================================== 

24 

25**Enum-based classes** 

26 

27See https://docs.python.org/3/library/enum.html. 

28 

29The good things about enums are: 

30 

31- they are immutable 

32- they are "class-like", not "instance-like" 

33- they can be accessed via attribute (like an object) or item (like a dict): 

34- you can add a ``@unique`` decorator to ensure no two have the same value 

35- IDEs know about them 

36 

37``AttrDict``'s disadvantages are: 

38 

39- more typing / DRY 

40- IDEs don't know about them 

41 

42Plain old objects: 

43 

44- not immutable 

45- no dictionary access -- though can use ``getattr()`` 

46- but otherwise simpler than enums 

47 

48LowerCaseAutoStringObject: 

49 

50- IDEs don't understand their values, so get types wrong 

51 

52.. code-block:: python 

53 

54 from enum import Enum 

55 

56 class Colour(Enum): 

57 red = 1 

58 green = 2 

59 blue = 3 

60 

61 Colour.red # <Colour.red: 1> 

62 Colour.red.name # 'red' 

63 Colour.red.value # 1 

64 Colour['red'] # <Colour.red: 1> 

65 

66 Colour.red = 4 # AttributeError: Cannot reassign members. 

67 

68Then, for fancier things below, note that: 

69 

70.. code-block:: none 

71 

72 metaclass 

73 __prepare__(mcs, name, bases) 

74 ... prepares (creates) the class namespace 

75 ... use if you don't want the namespace to be a plain dict() 

76 ... https://docs.python.org/3/reference/datamodel.html 

77 ... returns the (empty) namespace 

78 __new__(mcs, name, bases, namespace) 

79 ... called with the populated namespace 

80 ... makes and returns the class object, cls 

81 

82 class 

83 __new__(cls) 

84 ... controls the creation of a new instance; static classmethod 

85 ... makes self 

86 

87 __init__(self) 

88 ... controls the initialization of an instance 

89 

90 

91""" 

92 

93import collections 

94from collections import OrderedDict 

95# noinspection PyProtectedMember 

96from enum import EnumMeta, Enum, _EnumDict 

97import itertools 

98from typing import Any, List, Optional, Tuple, Type 

99 

100from cardinal_pythonlib.logs import get_brace_style_log_with_null_handler 

101from cardinal_pythonlib.reprfunc import ordered_repr 

102 

103log = get_brace_style_log_with_null_handler(__name__) 

104 

105 

106# ============================================================================= 

107# Enum-based classes 

108# ============================================================================= 

109 

110STR_ENUM_FWD_REF = "StrEnum" 

111# class name forward reference for type checker: 

112# http://mypy.readthedocs.io/en/latest/kinds_of_types.html 

113# ... but also: a variable (rather than a string literal) stops PyCharm giving 

114# the curious error "PEP 8: no newline at end of file" and pointing to the 

115# type hint string literal. 

116 

117 

118class StrEnum(Enum): 

119 """ 

120 StrEnum: 

121 

122 - makes ``str(myenum.x)`` give ``str(myenum.x.value)`` 

123 - adds a lookup function (from a string literal) 

124 - adds ordering by value 

125 

126 """ 

127 def __str__(self) -> str: 

128 return str(self.value) 

129 

130 @classmethod 

131 def lookup(cls, 

132 value: Any, 

133 allow_none: bool = False) -> Optional[STR_ENUM_FWD_REF]: 

134 for item in cls: 

135 if value == item.value: 

136 return item 

137 if not value and allow_none: 

138 return None 

139 raise ValueError( 

140 f"Value {value!r} not found in enum class {cls.__name__}") 

141 

142 def __lt__(self, other: STR_ENUM_FWD_REF) -> bool: 

143 return str(self) < str(other) 

144 

145 

146# ----------------------------------------------------------------------------- 

147# AutoStrEnum 

148# ----------------------------------------------------------------------------- 

149 

150class AutoStrEnumMeta(EnumMeta): 

151 # noinspection PyInitNewSignature 

152 def __new__(mcs, cls, bases, oldclassdict): 

153 """ 

154 Scan through ``oldclassdict`` and convert any value that is a plain 

155 tuple into a ``str`` of the name instead. 

156 """ 

157 newclassdict = _EnumDict() 

158 for k, v in oldclassdict.items(): 

159 if v == (): 

160 v = k 

161 newclassdict[k] = v 

162 return super().__new__(mcs, cls, bases, newclassdict) 

163 

164 

165class AutoStrEnum(str, 

166 StrEnum, # was Enum, 

167 metaclass=AutoStrEnumMeta): 

168 """ 

169 Base class for ``name=value`` ``str`` enums. 

170  

171 Example: 

172  

173 .. code-block:: python 

174  

175 class Animal(AutoStrEnum): 

176 horse = () 

177 dog = () 

178 whale = () 

179  

180 print(Animal.horse) 

181 print(Animal.horse == 'horse') 

182 print(Animal.horse.name, Animal.horse.value) 

183 

184 See 

185 https://stackoverflow.com/questions/32214614/automatically-setting-an-enum-members-value-to-its-name/32215467 

186 and then inherit from :class:`StrEnum` rather than :class:`Enum`. 

187 

188 """ # noqa 

189 pass 

190 

191 

192# ----------------------------------------------------------------------------- 

193# LowerCaseAutoStrEnumMeta 

194# ----------------------------------------------------------------------------- 

195 

196class LowerCaseAutoStrEnumMeta(EnumMeta): 

197 # noinspection PyInitNewSignature 

198 def __new__(mcs, cls, bases, oldclassdict): 

199 """ 

200 Scan through ``oldclassdict`` and convert any value that is a plain 

201 tuple into a ``str`` of the name instead. 

202 """ 

203 newclassdict = _EnumDict() 

204 for k, v in oldclassdict.items(): 

205 if v == (): 

206 v = k.lower() 

207 if v in newclassdict.keys(): 

208 raise ValueError(f"Duplicate value caused by key {k}") 

209 newclassdict[k] = v 

210 return super().__new__(mcs, cls, bases, newclassdict) 

211 

212 

213class LowerCaseAutoStrEnum(str, StrEnum, metaclass=LowerCaseAutoStrEnumMeta): 

214 """ 

215 Base class for ``name=value`` ``str`` enums, forcing lower-case values. 

216 

217 Example: 

218 

219 .. code-block:: python 

220 

221 class AnimalLC(LowerCaseAutoStrEnum): 

222 Horse = () 

223 Dog = () 

224 Whale = () 

225 

226 print(AnimalLC.Horse) 

227 print(AnimalLC.Horse == 'horse') 

228 print(AnimalLC.Horse.name, AnimalLC.Horse.value) 

229 

230 """ 

231 pass 

232 

233 

234# ----------------------------------------------------------------------------- 

235# AutoNumberEnum 

236# ----------------------------------------------------------------------------- 

237 

238class AutoNumberEnum(Enum): 

239 """ 

240 As per https://docs.python.org/3/library/enum.html (in which, called 

241 AutoNumber). 

242 

243 Usage: 

244 

245 .. code-block:: python 

246 

247 class Color(AutoNumberEnum): 

248 red = () 

249 green = () 

250 blue = () 

251 

252 Color.green.value == 2 # True 

253 """ 

254 def __new__(cls): 

255 value = len(cls.__members__) + 1 # will be numbered from 1 

256 obj = object.__new__(cls) 

257 obj._value_ = value 

258 return obj 

259 

260 

261# ----------------------------------------------------------------------------- 

262# AutoNumberObject 

263# ----------------------------------------------------------------------------- 

264 

265class AutoNumberObjectMetaClass(type): 

266 @classmethod 

267 def __prepare__(mcs, name, bases): # mcs: was metaclass 

268 """ 

269 Called when AutoEnum (below) is defined, prior to ``__new__``, with: 

270 

271 .. code-block:: python 

272 

273 name = 'AutoEnum' 

274 bases = () 

275 """ 

276 # print("__prepare__: name={}, bases={}".format( 

277 # repr(name), repr(bases))) 

278 return collections.defaultdict(itertools.count().__next__) 

279 

280 # noinspection PyInitNewSignature 

281 def __new__(mcs, name, bases, classdict): # mcs: was cls 

282 """ 

283 Called when AutoEnum (below) is defined, with: 

284 

285 .. code-block:: python 

286  

287 name = 'AutoEnum' 

288 bases = () 

289 classdict = defaultdict(<method-wrapper '__next__' of itertools.count 

290 object at 0x7f7d8fc5f648>, 

291 { 

292 '__doc__': '... a docstring... ', 

293 '__qualname__': 'AutoEnum', 

294 '__name__': 0, 

295 '__module__': 0 

296 } 

297 ) 

298 """ # noqa 

299 # print("__new__: name={}, bases={}, classdict={}".format( 

300 # repr(name), repr(bases), repr(classdict))) 

301 cls = type.__new__(mcs, name, bases, dict(classdict)) 

302 return cls # cls: was result 

303 

304 

305class AutoNumberObject(metaclass=AutoNumberObjectMetaClass): 

306 """ 

307 From comment by Duncan Booth at 

308 http://www.acooke.org/cute/Pythonssad0.html, with trivial rename. 

309 

310 Usage: 

311 

312 .. code-block:: python 

313 

314 class MyThing(AutoNumberObject): 

315 a 

316 b 

317 

318 MyThing.a 

319 # 1 

320 MyThing.b 

321 # 2 

322 """ 

323 pass 

324 

325 

326# ----------------------------------------------------------------------------- 

327# LowerCaseAutoStringObject 

328# ----------------------------------------------------------------------------- 

329# RNC. We need a defaultdict that does the job... 

330# Or similar. But the defaultdict argument function receives no parameters, 

331# so it can't read the key. Therefore: 

332 

333class LowerCaseAutoStringObjectMetaClass(type): 

334 @classmethod 

335 def __prepare__(mcs, name, bases): 

336 return collections.defaultdict(int) # start with all values as 0 

337 

338 # noinspection PyInitNewSignature 

339 def __new__(mcs, name, bases, classdict): 

340 for k in classdict.keys(): 

341 if k.startswith('__'): # e.g. __qualname__, __name__, __module__ 

342 continue 

343 value = k.lower() 

344 if value in classdict.values(): 

345 raise ValueError(f"Duplicate value from key: {k}") 

346 classdict[k] = value 

347 cls = type.__new__(mcs, name, bases, dict(classdict)) 

348 return cls 

349 

350 

351class LowerCaseAutoStringObject(metaclass=LowerCaseAutoStringObjectMetaClass): 

352 """ 

353 Usage: 

354 

355 .. code-block:: python 

356 

357 class Wacky(LowerCaseAutoStringObject): 

358 Thing # or can use Thing = () to avoid IDE complaints 

359 OtherThing = () 

360 

361 Wacky.Thing # 'thing' 

362 Wacky.OtherThing # 'otherthing' 

363 """ 

364 pass 

365 

366 

367# ----------------------------------------------------------------------------- 

368# AutoStringObject 

369# ----------------------------------------------------------------------------- 

370# RNC. We need a defaultdict that does the job... 

371# Or similar. But the defaultdict argument function receives no parameters, 

372# so it can't read the key. Therefore: 

373 

374class AutoStringObjectMetaClass(type): 

375 @classmethod 

376 def __prepare__(mcs, name, bases): 

377 return collections.defaultdict(int) 

378 

379 # noinspection PyInitNewSignature 

380 def __new__(mcs, name, bases, classdict): 

381 for k in classdict.keys(): 

382 if k.startswith('__'): # e.g. __qualname__, __name__, __module__ 

383 continue 

384 classdict[k] = k 

385 cls = type.__new__(mcs, name, bases, dict(classdict)) 

386 return cls 

387 

388 

389class AutoStringObject(metaclass=AutoStringObjectMetaClass): 

390 """ 

391 Usage: 

392 

393 .. code-block:: python 

394 

395 class Fish(AutoStringObject): 

396 Thing 

397 Blah 

398 

399 Fish.Thing # 'Thing' 

400 """ 

401 pass 

402 

403 

404# ============================================================================= 

405# enum: TOO OLD; NAME CLASH; DEPRECATED/REMOVED 

406# ============================================================================= 

407 

408# def enum(**enums: Any) -> Enum: 

409# """Enum support, as at https://stackoverflow.com/questions/36932""" 

410# return type('Enum', (), enums) 

411 

412 

413# ============================================================================= 

414# AttrDict: DEPRECATED 

415# ============================================================================= 

416 

417class AttrDict(dict): 

418 """ 

419 Dictionary with attribute access; see 

420 https://stackoverflow.com/questions/4984647 

421 """ 

422 def __init__(self, *args, **kwargs) -> None: 

423 super(AttrDict, self).__init__(*args, **kwargs) 

424 self.__dict__ = self 

425 

426 

427# ============================================================================= 

428# OrderedNamespace 

429# ============================================================================= 

430# for attrdict itself: use the attrdict package 

431 

432class OrderedNamespace(object): 

433 """ 

434 As per https://stackoverflow.com/questions/455059, modified for 

435 ``__init__``. 

436 """ 

437 def __init__(self, *args): 

438 super().__setattr__('_odict', OrderedDict(*args)) 

439 

440 def __getattr__(self, key): 

441 odict = super().__getattribute__('_odict') 

442 if key in odict: 

443 return odict[key] 

444 return super().__getattribute__(key) 

445 

446 def __setattr__(self, key, val): 

447 self._odict[key] = val 

448 

449 @property 

450 def __dict__(self): 

451 return self._odict 

452 

453 def __setstate__(self, state): # Support copy.copy 

454 super().__setattr__('_odict', OrderedDict()) 

455 self._odict.update(state) 

456 

457 def __eq__(self, other): 

458 return self.__dict__ == other.__dict__ 

459 

460 def __ne__(self, other): 

461 return not self.__eq__(other) 

462 

463 # Plus more (RNC): 

464 def items(self): 

465 return self.__dict__.items() 

466 

467 def __repr__(self): 

468 return ordered_repr(self, self.__dict__.keys()) 

469 

470 

471# ============================================================================= 

472# keys_descriptions_from_enum 

473# ============================================================================= 

474 

475def keys_descriptions_from_enum( 

476 enum: Type[Enum], 

477 sort_keys: bool = False, 

478 keys_to_lower: bool = False, 

479 keys_to_upper: bool = False, 

480 key_to_description: str = ": ", 

481 joiner: str = " // ") -> Tuple[List[str], str]: 

482 """ 

483 From an Enum subclass, return (keys, descriptions_as_formatted_string). 

484 This is a convenience function used to provide command-line help for 

485 options involving a choice of enums from an Enum class. 

486 """ 

487 assert not (keys_to_lower and keys_to_upper) 

488 keys = [e.name for e in enum] 

489 if keys_to_lower: 

490 keys = [k.lower() for k in keys] 

491 elif keys_to_upper: 

492 keys = [k.upper() for k in keys] 

493 if sort_keys: 

494 keys.sort() 

495 descriptions = [ 

496 f"{k}{key_to_description}{enum[k].value}" 

497 for k in keys 

498 ] 

499 description_str = joiner.join(descriptions) 

500 return keys, description_str 

501 

502 

503# ============================================================================= 

504# EnumLower 

505# ============================================================================= 

506 

507class CaseInsensitiveEnumMeta(EnumMeta): 

508 """ 

509 An Enum that permits lookup by a lower-case version of its keys. 

510  

511 https://stackoverflow.com/questions/42658609/how-to-construct-a-case-insensitive-enum 

512  

513 Example: 

514  

515 .. code-block:: python 

516 

517 from enum import Enum  

518 from cardinal_pythonlib.enumlike import CaseInsensitiveEnumMeta 

519  

520 class TestEnum(Enum, metaclass=CaseInsensitiveEnumMeta): 

521 REDAPPLE = 1 

522 greenapple = 2 

523 PineApple = 3 

524  

525 >>> TestEnum["REDAPPLE"] 

526 <TestEnum.REDAPPLE: 1> 

527 >>> TestEnum["redapple"] 

528 <TestEnum.REDAPPLE: 1> 

529 >>> TestEnum["greenapple"] 

530 <TestEnum.greenapple: 2> 

531 >>> TestEnum["greenappLE"] 

532 <TestEnum.greenapple: 2> 

533 >>> TestEnum["PineApple"] 

534 <TestEnum.PineApple: 3> 

535 >>> TestEnum["PineApplE"] 

536 <TestEnum.PineApple: 3> 

537 

538 """ # noqa 

539 def __getitem__(self, item: Any) -> Any: 

540 if isinstance(item, str): 

541 item_lower = item.lower() 

542 for member in self: 

543 if member.name.lower() == item_lower: 

544 return member 

545 return super().__getitem__(item)