Coverage for pyTooling/MetaClasses/__init__.py: 84%

420 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-19 06:41 +0000

1# ==================================================================================================================== # 

2# _____ _ _ __ __ _ ____ _ # 

3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ | \/ | ___| |_ __ _ / ___| | __ _ ___ ___ ___ ___ # 

4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |\/| |/ _ \ __/ _` | | | |/ _` / __/ __|/ _ \/ __| # 

5# | |_) | |_| || | (_) | (_) | | | | | | (_| |_| | | | __/ || (_| | |___| | (_| \__ \__ \ __/\__ \ # 

6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| |_|\___|\__\__,_|\____|_|\__,_|___/___/\___||___/ # 

7# |_| |___/ |___/ # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# Sven Köhler # 

12# # 

13# License: # 

14# ==================================================================================================================== # 

15# Copyright 2017-2025 Patrick Lehmann - Bötzingen, Germany # 

16# # 

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

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

19# You may obtain a copy of the License at # 

20# # 

21# http://www.apache.org/licenses/LICENSE-2.0 # 

22# # 

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

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

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

26# See the License for the specific language governing permissions and # 

27# limitations under the License. # 

28# # 

29# SPDX-License-Identifier: Apache-2.0 # 

30# ==================================================================================================================== # 

31# 

32""" 

33The MetaClasses package implements Python meta-classes (classes to construct other classes in Python). 

34 

35.. hint:: See :ref:`high-level help <META>` for explanations and usage examples. 

36""" 

37from functools import wraps 

38from sys import version_info 

39from threading import Condition 

40from types import FunctionType #, MethodType 

41from typing import Any, Tuple, List, Dict, Callable, Generator, Set, Iterator, Iterable, Union 

42from typing import Type, TypeVar, Generic, _GenericAlias, ClassVar, Optional as Nullable 

43 

44try: 

45 from pyTooling.Exceptions import ToolingException 

46 from pyTooling.Decorators import export, readonly 

47except (ImportError, ModuleNotFoundError): # pragma: no cover 

48 print("[pyTooling.MetaClasses] Could not import from 'pyTooling.*'!") 

49 

50 try: 

51 from Exceptions import ToolingException 

52 from Decorators import export, readonly 

53 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover 

54 print("[pyTooling.MetaClasses] Could not import directly!") 

55 raise ex 

56 

57 

58__all__ = ["M"] 

59 

60TAttr = TypeVar("TAttr") # , bound='Attribute') 

61"""A type variable for :class:`~pyTooling.Attributes.Attribute`.""" 

62 

63TAttributeFilter = Union[TAttr, Iterable[TAttr], None] 

64"""A type hint for a predicate parameter that accepts either a single :class:`~pyTooling.Attributes.Attribute` or an 

65iterable of those.""" 

66 

67 

68@export 

69class ExtendedTypeError(ToolingException): 

70 """The exception is raised by the meta-class :class:`~pyTooling.Metaclasses.ExtendedType`.""" 

71 

72 

73@export 

74class BaseClassWithoutSlotsError(ExtendedTypeError): 

75 """ 

76 The exception is raised when a class using ``__slots__`` inherits from at-least one base-class not using ``__slots__``. 

77 

78 .. seealso:: 

79 

80 * :ref:`Python data model for slots <slots>` 

81 * :term:`Glossary entry __slots__ <__slots__>` 

82 """ 

83 

84 

85@export 

86class BaseClassWithNonEmptySlotsError(ExtendedTypeError): 

87 pass 

88 

89 

90@export 

91class BaseClassIsNotAMixinError(ExtendedTypeError): 

92 pass 

93 

94 

95@export 

96class DuplicateFieldInSlotsError(ExtendedTypeError): 

97 pass 

98 

99 

100@export 

101class AbstractClassError(ExtendedTypeError): 

102 """ 

103 The exception is raised, when a class contains methods marked with *abstractmethod* or *mustoverride*. 

104 

105 .. seealso:: 

106 

107 :func:`@abstractmethod <pyTooling.MetaClasses.abstractmethod>` 

108 |rarr| Mark a method as *abstract*. 

109 :func:`@mustoverride <pyTooling.MetaClasses.mustoverride>` 

110 |rarr| Mark a method as *must overrride*. 

111 :exc:`~MustOverrideClassError` 

112 |rarr| Exception raised, if a method is marked as *must-override*. 

113 """ 

114 

115 

116@export 

117class MustOverrideClassError(AbstractClassError): 

118 """ 

119 The exception is raised, when a class contains methods marked with *must-override*. 

120 

121 .. seealso:: 

122 

123 :func:`@abstractmethod <pyTooling.MetaClasses.abstractmethod>` 

124 |rarr| Mark a method as *abstract*. 

125 :func:`@mustoverride <pyTooling.MetaClasses.mustoverride>` 

126 |rarr| Mark a method as *must overrride*. 

127 :exc:`~AbstractClassError` 

128 |rarr| Exception raised, if a method is marked as *abstract*. 

129 """ 

130 

131 

132# """ 

133# Metaclass that allows multiple dispatch of methods based on method signatures. 

134# 

135# .. seealso: 

136# 

137# `Python Cookbook - Multiple dispatch with function annotations <https://GitHub.com/dabeaz/python-cookbook/blob/master/src/9/multiple_dispatch_with_function_annotations/example1.py?ts=2>`__ 

138# """ 

139 

140 

141M = TypeVar("M", bound=Callable) #: A type variable for methods. 

142 

143 

144@export 

145def slotted(cls): 

146 if cls.__class__ is type: 

147 metacls = ExtendedType 

148 elif issubclass(cls.__class__, ExtendedType): 

149 metacls = cls.__class__ 

150 for method in cls.__methods__: 

151 delattr(method, "__classobj__") 

152 else: 

153 raise ExtendedTypeError("Class uses an incompatible meta-class.") # FIXME: create exception for it? 

154 

155 bases = tuple(base for base in cls.__bases__ if base is not object) 

156 slots = cls.__dict__["__slots__"] if "__slots__" in cls.__dict__ else tuple() 

157 members = { 

158 "__qualname__": cls.__qualname__ 

159 } 

160 for key, value in cls.__dict__.items(): 

161 if key not in slots: 

162 members[key] = value 

163 

164 return metacls(cls.__name__, bases, members, slots=True) 

165 

166 

167@export 

168def mixin(cls): 

169 if cls.__class__ is type: 

170 metacls = ExtendedType 

171 elif issubclass(cls.__class__, ExtendedType): 171 ↛ 176line 171 didn't jump to line 176 because the condition on line 171 was always true

172 metacls = cls.__class__ 

173 for method in cls.__methods__: 

174 delattr(method, "__classobj__") 

175 else: 

176 raise ExtendedTypeError("Class uses an incompatible meta-class.") # FIXME: create exception for it? 

177 

178 bases = tuple(base for base in cls.__bases__ if base is not object) 

179 slots = cls.__dict__["__slots__"] if "__slots__" in cls.__dict__ else tuple() 

180 members = { 

181 "__qualname__": cls.__qualname__ 

182 } 

183 for key, value in cls.__dict__.items(): 

184 if key not in slots: 

185 members[key] = value 

186 

187 return metacls(cls.__name__, bases, members, mixin=True) 

188 

189 

190@export 

191def singleton(cls): 

192 if cls.__class__ is type: 

193 metacls = ExtendedType 

194 elif issubclass(cls.__class__, ExtendedType): 

195 metacls = cls.__class__ 

196 for method in cls.__methods__: 

197 delattr(method, "__classobj__") 

198 else: 

199 raise ExtendedTypeError("Class uses an incompatible meta-class.") # FIXME: create exception for it? 

200 

201 bases = tuple(base for base in cls.__bases__ if base is not object) 

202 slots = cls.__dict__["__slots__"] if "__slots__" in cls.__dict__ else tuple() 

203 members = { 

204 "__qualname__": cls.__qualname__ 

205 } 

206 for key, value in cls.__dict__.items(): 

207 if key not in slots: 

208 members[key] = value 

209 

210 return metacls(cls.__name__, bases, members, singleton=True) 

211 

212 

213@export 

214def abstractmethod(method: M) -> M: 

215 """ 

216 Mark a method as *abstract* and replace the implementation with a new method raising a :exc:`NotImplementedError`. 

217 

218 The original method is stored in ``<method>.__wrapped__`` and it's doc-string is copied to the replacing method. In 

219 additional field ``<method>.__abstract__`` is added. 

220 

221 .. warning:: 

222 

223 This decorator should be used in combination with meta-class :class:`~pyTooling.Metaclasses.ExtendedType`. 

224 Otherwise, an abstract class itself doesn't throw a :exc:`~pyTooling.Exceptions.AbstractClassError` at 

225 instantiation. 

226 

227 .. admonition:: ``example.py`` 

228 

229 .. code-block:: python 

230 

231 class Data(mataclass=ExtendedType): 

232 @abstractmethod 

233 def method(self) -> bool: 

234 '''This method needs to be implemented''' 

235 

236 :param method: Method that is marked as *abstract*. 

237 :returns: Replacement method, which raises a :exc:`NotImplementedError`. 

238 

239 .. seealso:: 

240 

241 * :exc:`~pyTooling.Exceptions.AbstractClassError` 

242 * :func:`~pyTooling.Metaclasses.mustoverride` 

243 * :func:`~pyTooling.Metaclasses.notimplemented` 

244 """ 

245 @wraps(method) 

246 def func(self): 

247 raise NotImplementedError(f"Method '{method.__name__}' is abstract and needs to be overridden in a derived class.") 

248 

249 func.__abstract__ = True 

250 return func 

251 

252 

253@export 

254def mustoverride(method: M) -> M: 

255 """ 

256 Mark a method as *must-override*. 

257 

258 The returned function is the original function, but with an additional field ``<method>.____mustOverride__``, so a 

259 meta-class can identify a *must-override* method and raise an error. Such an error is not raised if the method is 

260 overridden by an inheriting class. 

261 

262 A *must-override* methods can offer a partial implementation, which is called via ``super()...``. 

263 

264 .. warning:: 

265 

266 This decorator needs to be used in combination with meta-class :class:`~pyTooling.Metaclasses.ExtendedType`. 

267 Otherwise, an abstract class itself doesn't throw a :exc:`~pyTooling.Exceptions.MustOverrideClassError` at 

268 instantiation. 

269 

270 .. admonition:: ``example.py`` 

271 

272 .. code-block:: python 

273 

274 class Data(mataclass=ExtendedType): 

275 @mustoverride 

276 def method(self): 

277 '''This is a very basic implementation''' 

278 

279 :param method: Method that is marked as *must-override*. 

280 :returns: Same method, but with additional ``<method>.__mustOverride__`` field. 

281 

282 .. seealso:: 

283 

284 * :exc:`~pyTooling.Exceptions.MustOverrideClassError` 

285 * :func:`~pyTooling.Metaclasses.abstractmethod` 

286 * :func:`~pyTooling.Metaclasses.notimplemented` 

287 """ 

288 method.__mustOverride__ = True 

289 return method 

290 

291 

292# @export 

293# def overloadable(method: M) -> M: 

294# method.__overloadable__ = True 

295# return method 

296 

297 

298# @export 

299# class DispatchableMethod: 

300# """Represents a single multimethod.""" 

301# 

302# _methods: Dict[Tuple, Callable] 

303# __name__: str 

304# __slots__ = ("_methods", "__name__") 

305# 

306# def __init__(self, name: str) -> None: 

307# self.__name__ = name 

308# self._methods = {} 

309# 

310# def __call__(self, *args: Any): 

311# """Call a method based on type signature of the arguments.""" 

312# types = tuple(type(arg) for arg in args[1:]) 

313# meth = self._methods.get(types, None) 

314# if meth: 

315# return meth(*args) 

316# else: 

317# raise TypeError(f"No matching method for types {types}.") 

318# 

319# def __get__(self, instance, cls): # Starting with Python 3.11+, use typing.Self as return type 

320# """Descriptor method needed to make calls work in a class.""" 

321# if instance is not None: 

322# return MethodType(self, instance) 

323# else: 

324# return self 

325# 

326# def register(self, method: Callable) -> None: 

327# """Register a new method as a dispatchable.""" 

328# 

329# # Build a signature from the method's type annotations 

330# sig = signature(method) 

331# types: List[Type] = [] 

332# 

333# for name, parameter in sig.parameters.items(): 

334# if name == "self": 

335# continue 

336# 

337# if parameter.annotation is Parameter.empty: 

338# raise TypeError(f"Parameter '{name}' in method '{method.__name__}' must be annotated with a type.") 

339# 

340# if not isinstance(parameter.annotation, type): 

341# raise TypeError(f"Parameter '{name}' in method '{method.__name__}' annotation must be a type.") 

342# 

343# if parameter.default is not Parameter.empty: 

344# self._methods[tuple(types)] = method 

345# 

346# types.append(parameter.annotation) 

347# 

348# self._methods[tuple(types)] = method 

349 

350 

351# @export 

352# class DispatchDictionary(dict): 

353# """Special dictionary to build dispatchable methods in a metaclass.""" 

354# 

355# def __setitem__(self, key: str, value: Any): 

356# if callable(value) and key in self: 

357# # If key already exists, it must be a dispatchable method or callable 

358# currentValue = self[key] 

359# if isinstance(currentValue, DispatchableMethod): 

360# currentValue.register(value) 

361# else: 

362# dispatchable = DispatchableMethod(key) 

363# dispatchable.register(currentValue) 

364# dispatchable.register(value) 

365# 

366# super().__setitem__(key, dispatchable) 

367# else: 

368# super().__setitem__(key, value) 

369 

370 

371@export 

372class ExtendedType(type): 

373 """ 

374 An updates meta-class to construct new classes with an extended feature set. 

375 

376 .. todo:: META::ExtendedType Needs documentation. 

377 .. todo:: META::ExtendedType allow __dict__ and __weakref__ if slotted is enabled 

378 

379 Features: 

380 

381 * Store object members more efficiently in ``__slots__`` instead of ``_dict__``. 

382 * Allow only a single instance to be created (:term:`singleton`). 

383 * Define methods as :term:`abstract <abstract method>` or :term:`must-override <mustoverride method>` and prohibit 

384 instantiation of :term:`abstract classes <abstract class>`. 

385 

386 .. #* Allow method overloading and dispatch overloads based on argument signatures. 

387 """ 

388 

389 # @classmethod 

390 # def __prepare__(cls, className, baseClasses, slots: bool = False, mixin: bool = False, singleton: bool = False): 

391 # return DispatchDictionary() 

392 

393 def __new__(self, className: str, baseClasses: Tuple[type], members: Dict[str, Any], 

394 slots: bool = False, mixin: bool = False, singleton: bool = False) -> "ExtendedType": 

395 """ 

396 Construct a new class using this :term:`meta-class`. 

397 

398 :param className: The name of the class to construct. 

399 :param baseClasses: The tuple of :term:`base-classes <base-class>` the class is derived from. 

400 :param members: The dictionary of members for the constructed class. 

401 :param slots: If true, store object attributes in :term:`__slots__ <slots>` instead of ``__dict__``. 

402 :param mixin: If true, make the class a :term:`Mixin-Class`. 

403 If false, create slots if ``slots`` is true. 

404 If none, preserve behavior of primary base-class. 

405 :param singleton: If true, make the class a :term:`Singleton`. 

406 :returns: The new class. 

407 :raises AttributeError: If base-class has no '__slots__' attribute. 

408 :raises AttributeError: If slot already exists in base-class. 

409 """ 

410 try: 

411 from pyTooling.Attributes import ATTRIBUTES_MEMBER_NAME, AttributeScope 

412 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover 

413 from Attributes import ATTRIBUTES_MEMBER_NAME, AttributeScope 

414 

415 # Inherit 'slots' feature from primary base-class 

416 if len(baseClasses) > 0: 

417 primaryBaseClass = baseClasses[0] 

418 if isinstance(primaryBaseClass, self): 

419 slots = primaryBaseClass.__slotted__ 

420 

421 # Compute slots and mixin-slots from annotated fields as well as class- and object-fields with initial values. 

422 classFields, objectFields = self._computeSlots(className, baseClasses, members, slots, mixin) 

423 

424 # Compute abstract methods 

425 abstractMethods, members = self._checkForAbstractMethods(baseClasses, members) 

426 

427 # Create a new class 

428 newClass = type.__new__(self, className, baseClasses, members) 

429 

430 # Apply class fields 

431 for fieldName, typeAnnotation in classFields.items(): 

432 setattr(newClass, fieldName, typeAnnotation) 

433 

434 # Search in inheritance tree for abstract methods 

435 newClass.__abstractMethods__ = abstractMethods 

436 newClass.__isAbstract__ = self._wrapNewMethodIfAbstract(newClass) 

437 newClass.__isSingleton__ = self._wrapNewMethodIfSingleton(newClass, singleton) 

438 

439 # Check for inherited class attributes 

440 attributes = [] 

441 setattr(newClass, ATTRIBUTES_MEMBER_NAME, attributes) 

442 for base in baseClasses: 

443 if hasattr(base, ATTRIBUTES_MEMBER_NAME): 

444 pyAttr = getattr(base, ATTRIBUTES_MEMBER_NAME) 

445 for att in pyAttr: 

446 if AttributeScope.Class in att.Scope: 446 ↛ 445line 446 didn't jump to line 445 because the condition on line 446 was always true

447 attributes.append(att) 

448 att.__class__._classes.append(newClass) 

449 

450 # Check methods for attributes 

451 methods, methodsWithAttributes = self._findMethods(newClass, baseClasses, members) 

452 

453 # Add new fields for found methods 

454 newClass.__methods__ = tuple(methods) 

455 newClass.__methodsWithAttributes__ = tuple(methodsWithAttributes) 

456 

457 # Additional methods on a class 

458 def GetMethodsWithAttributes(self, predicate: Nullable[TAttributeFilter[TAttr]] = None) -> Dict[Callable, Tuple["Attribute", ...]]: 

459 """ 

460 

461 :param predicate: 

462 :return: 

463 :raises ValueError: 

464 :raises ValueError: 

465 """ 

466 try: 

467 from ..Attributes import Attribute 

468 except (ImportError, ModuleNotFoundError): # pragma: no cover 

469 try: 

470 from Attributes import Attribute 

471 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover 

472 raise ex 

473 

474 if predicate is None: 

475 predicate = Attribute 

476 elif isinstance(predicate, Iterable): 476 ↛ 477line 476 didn't jump to line 477 because the condition on line 476 was never true

477 for attribute in predicate: 

478 if not issubclass(attribute, Attribute): 

479 raise ValueError(f"Parameter 'predicate' contains an element which is not a sub-class of 'Attribute'.") 

480 

481 predicate = tuple(predicate) 

482 elif not issubclass(predicate, Attribute): 482 ↛ 483line 482 didn't jump to line 483 because the condition on line 482 was never true

483 raise ValueError(f"Parameter 'predicate' is not a sub-class of 'Attribute'.") 

484 

485 methodAttributePairs = {} 

486 for method in newClass.__methodsWithAttributes__: 

487 matchingAttributes = [] 

488 for attribute in method.__pyattr__: 

489 if isinstance(attribute, predicate): 

490 matchingAttributes.append(attribute) 

491 

492 if len(matchingAttributes) > 0: 

493 methodAttributePairs[method] = tuple(matchingAttributes) 

494 

495 return methodAttributePairs 

496 

497 newClass.GetMethodsWithAttributes = classmethod(GetMethodsWithAttributes) 

498 GetMethodsWithAttributes.__qualname__ = f"{className}.{GetMethodsWithAttributes.__name__}" 

499 

500 # GetMethods(predicate) -> dict[method, list[attribute]] / generator 

501 # GetClassAtrributes -> list[attributes] / generator 

502 # MethodHasAttributes(predicate) -> bool 

503 # GetAttribute 

504 

505 return newClass 

506 

507 @classmethod 

508 def _findMethods(self, newClass: "ExtendedType", baseClasses: Tuple[type], members: Dict[str, Any]): 

509 try: 

510 from ..Attributes import Attribute 

511 except (ImportError, ModuleNotFoundError): # pragma: no cover 

512 try: 

513 from Attributes import Attribute 

514 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover 

515 raise ex 

516 

517 # Embedded bind function due to circular dependencies. 

518 def bind(instance: object, func: FunctionType, methodName: Nullable[str] = None): 

519 if methodName is None: 519 ↛ 522line 519 didn't jump to line 522 because the condition on line 519 was always true

520 methodName = func.__name__ 

521 

522 boundMethod = func.__get__(instance, instance.__class__) 

523 setattr(instance, methodName, boundMethod) 

524 

525 return boundMethod 

526 

527 methods = [] 

528 methodsWithAttributes = [] 

529 attributeIndex = {} 

530 

531 for base in baseClasses: 

532 if hasattr(base, "__methodsWithAttributes__"): 

533 methodsWithAttributes.extend(base.__methodsWithAttributes__) 

534 

535 for memberName, member in members.items(): 

536 if isinstance(member, FunctionType): 

537 method = newClass.__dict__[memberName] 

538 if hasattr(method, "__classobj__") and getattr(method, "__classobj__") is not newClass: 538 ↛ 539line 538 didn't jump to line 539 because the condition on line 538 was never true

539 raise TypeError(f"Method '{memberName}' is used by multiple classes: {method.__classobj__} and {newClass}.") 

540 else: 

541 setattr(method, "__classobj__", newClass) 

542 

543 def GetAttributes(inst: Any, predicate: Nullable[Type[Attribute]] = None) -> Tuple[Attribute, ...]: 

544 results = [] 

545 try: 

546 for attribute in inst.__pyattr__: # type: Attribute 

547 if isinstance(attribute, predicate): 

548 results.append(attribute) 

549 return tuple(results) 

550 except AttributeError: 

551 return tuple() 

552 

553 method.GetAttributes = bind(method, GetAttributes) 

554 methods.append(method) 

555 

556 # print(f" convert function: '{memberName}' to method") 

557 # print(f" {member}") 

558 if "__pyattr__" in member.__dict__: 

559 attributes = member.__pyattr__ # type: List[Attribute] 

560 if isinstance(attributes, list) and len(attributes) > 0: 560 ↛ 535line 560 didn't jump to line 535 because the condition on line 560 was always true

561 methodsWithAttributes.append(member) 

562 for attribute in attributes: 

563 attribute._functions.remove(method) 

564 attribute._methods.append(method) 

565 

566 # print(f" attributes: {attribute.__class__.__name__}") 

567 if attribute not in attributeIndex: 567 ↛ 570line 567 didn't jump to line 570 because the condition on line 567 was always true

568 attributeIndex[attribute] = [member] 

569 else: 

570 attributeIndex[attribute].append(member) 

571 # else: 

572 # print(f" But has no attributes.") 

573 # else: 

574 # print(f" ?? {memberName}") 

575 return methods, methodsWithAttributes 

576 

577 @classmethod 

578 def _computeSlots(self, className, baseClasses, members, slots, mixin): 

579 # Compute which field are listed in __slots__ and which need to be initialized in an instance or class. 

580 slottedFields = [] 

581 objectFields = {} 

582 classFields = {} 

583 if slots or mixin: 

584 # If slots are used, all base classes must use __slots__. 

585 for baseClass in self._iterateBaseClasses(baseClasses): 

586 # Exclude object as a special case 

587 if baseClass is object or baseClass is Generic: 

588 continue 

589 

590 if not hasattr(baseClass, "__slots__"): 

591 ex = BaseClassWithoutSlotsError(f"Base-classes '{baseClass.__name__}' doesn't use '__slots__'.") 

592 ex.add_note(f"All base-classes of a class using '__slots__' must use '__slots__' itself.") 

593 raise ex 

594 

595 # FIXME: should have a check for non-empty slots on secondary base-classes too 

596 

597 # Copy all field names from primary base-class' __slots__, which are later needed for error checking. 

598 inheritedSlottedFields = {} 

599 if len(baseClasses) > 0: 

600 for base in reversed(baseClasses[0].mro()): 

601 # Exclude object as a special case 

602 if base is object or base is Generic: 

603 continue 

604 

605 for annotation in base.__slots__: 

606 inheritedSlottedFields[annotation] = base 

607 

608 # When adding annotated fields to slottedFields, check if name was not used in inheritance hierarchy. 

609 if "__annotations__" in members: 

610 # WORKAROUND: LEGACY SUPPORT Python <= 3.13 

611 # Accessing annotations was changed in Python 3.14. 

612 # The necessary 'annotationlib' is not available for older Python versions. 

613 annotations: Dict[str, Any] = members.get("__annotations__", {}) 

614 elif version_info >= (3, 14) and (annotate := members.get("__annotate_func__", None)) is not None: 

615 from annotationlib import Format 

616 annotations: Dict[str, Any] = annotate(Format.VALUE) 

617 else: 

618 annotations = {} 

619 

620 for fieldName, typeAnnotation in annotations.items(): 

621 if fieldName in inheritedSlottedFields: 621 ↛ 622line 621 didn't jump to line 622 because the condition on line 621 was never true

622 cls = inheritedSlottedFields[fieldName] 

623 raise AttributeError(f"Slot '{fieldName}' already exists in base-class '{cls.__module__}.{cls.__name__}'.") 

624 

625 # If annotated field is a ClassVar, and it has an initial value 

626 # * copy field and initial value to classFields dictionary 

627 # * remove field from members 

628 if isinstance(typeAnnotation, _GenericAlias) and typeAnnotation.__origin__ is ClassVar and fieldName in members: 

629 classFields[fieldName] = members[fieldName] 

630 del members[fieldName] 

631 

632 # If an annotated field has an initial value 

633 # * copy field and initial value to objectFields dictionary 

634 # * remove field from members 

635 elif fieldName in members: 

636 slottedFields.append(fieldName) 

637 objectFields[fieldName] = members[fieldName] 

638 del members[fieldName] 

639 else: 

640 slottedFields.append(fieldName) 

641 

642 mixinSlots = self._aggregateMixinSlots(className, baseClasses) 

643 else: 

644 # When adding annotated fields to slottedFields, check if name was not used in inheritance hierarchy. 

645 annotations: Dict[str, Any] = members.get("__annotations__", {}) 

646 for fieldName, typeAnnotation in annotations.items(): 

647 # If annotated field is a ClassVar, and it has an initial value 

648 # * copy field and initial value to classFields dictionary 

649 # * remove field from members 

650 if isinstance(typeAnnotation, _GenericAlias) and typeAnnotation.__origin__ is ClassVar and fieldName in members: 

651 classFields[fieldName] = members[fieldName] 

652 del members[fieldName] 

653 

654 # FIXME: search for fields without annotation 

655 if mixin: 

656 mixinSlots.extend(slottedFields) 

657 members["__slotted__"] = True 

658 members["__slots__"] = tuple() 

659 members["__isMixin__"] = True 

660 members["__mixinSlots__"] = tuple(mixinSlots) 

661 elif slots: 

662 slottedFields.extend(mixinSlots) 

663 members["__slotted__"] = True 

664 members["__slots__"] = tuple(slottedFields) 

665 members["__isMixin__"] = False 

666 members["__mixinSlots__"] = tuple() 

667 else: 

668 members["__slotted__"] = False 

669 # NO __slots__ 

670 members["__isMixin__"] = False 

671 members["__mixinSlots__"] = tuple() 

672 return classFields, objectFields 

673 

674 @classmethod 

675 def _aggregateMixinSlots(self, className, baseClasses): 

676 mixinSlots = [] 

677 if len(baseClasses) > 0: 

678 # If class has base-classes ensure only the primary inheritance path uses slots and all secondary inheritance 

679 # paths have an empty slots tuple. Otherwise, raise a BaseClassWithNonEmptySlotsError. 

680 inheritancePaths = [path for path in self._iterateBaseClassPaths(baseClasses)] 

681 primaryInharitancePath: Set[type] = set(inheritancePaths[0]) 

682 for typePath in inheritancePaths[1:]: 

683 for t in typePath: 

684 if hasattr(t, "__slots__") and len(t.__slots__) != 0 and t not in primaryInharitancePath: 

685 ex = BaseClassWithNonEmptySlotsError(f"Base-class '{t.__name__}' has non-empty __slots__ and can't be used as a direct or indirect base-class for '{className}'.") 

686 ex.add_note(f"In Python, only one inheritance branch can use non-empty __slots__.") 

687 # ex.add_note(f"With ExtendedType, only the primary base-class can use non-empty __slots__.") 

688 # ex.add_note(f"Secondary base-classes should be marked as mixin-classes.") 

689 raise ex 

690 

691 # If current class is set to be a mixin, then aggregate all mixinSlots in a list. 

692 # Ensure all base-classes are either constructed 

693 # * by meta-class ExtendedType, or 

694 # * use no slots, or 

695 # * are typing.Generic 

696 # If it was constructed by ExtendedType, then ensure this class itself is a mixin-class. 

697 for baseClass in baseClasses: # type: ExtendedType 

698 if isinstance(baseClass, _GenericAlias) and baseClass.__origin__ is Generic: 698 ↛ 699line 698 didn't jump to line 699 because the condition on line 698 was never true

699 pass 

700 elif baseClass.__class__ is self and baseClass.__isMixin__: 

701 mixinSlots.extend(baseClass.__mixinSlots__) 

702 elif hasattr(baseClass, "__mixinSlots__"): 

703 mixinSlots.extend(baseClass.__mixinSlots__) 

704 

705 return mixinSlots 

706 

707 @classmethod 

708 def _iterateBaseClasses(metacls, baseClasses: Tuple[type]) -> Generator[type, None, None]: 

709 if len(baseClasses) == 0: 

710 return 

711 

712 visited: Set[type] = set() 

713 iteratorStack: List[Iterator[type]] = list() 

714 

715 for baseClass in baseClasses: 

716 yield baseClass 

717 visited.add(baseClass) 

718 iteratorStack.append(iter(baseClass.__bases__)) 

719 

720 while True: 

721 try: 

722 base = next(iteratorStack[-1]) # type: type 

723 if base not in visited: 723 ↛ 728line 723 didn't jump to line 728 because the condition on line 723 was always true

724 yield base 

725 if len(base.__bases__) > 0: 

726 iteratorStack.append(iter(base.__bases__)) 

727 else: 

728 continue 

729 

730 except StopIteration: 

731 iteratorStack.pop() 

732 

733 if len(iteratorStack) == 0: 

734 break 

735 

736 @classmethod 

737 def _iterateBaseClassPaths(metacls, baseClasses: Tuple[type]) -> Generator[Tuple[type, ...], None, None]: 

738 """ 

739 Return a generator to iterate all possible inheritance paths for a given list of base-classes. 

740 

741 An inheritance path is expressed as a tuple of base-classes from current base-class (left-most item) to 

742 :class:`object` (right-most item). 

743 

744 :param baseClasses: List (tuple) of base-classes. 

745 :returns: Generator to iterate all inheritance paths. An inheritance path is a tuple of types (base-classes). 

746 """ 

747 if len(baseClasses) == 0: 747 ↛ 748line 747 didn't jump to line 748 because the condition on line 747 was never true

748 return 

749 

750 typeStack: List[type] = list() 

751 iteratorStack: List[Iterator[type]] = list() 

752 

753 for baseClass in baseClasses: 

754 typeStack.append(baseClass) 

755 iteratorStack.append(iter(baseClass.__bases__)) 

756 

757 while True: 

758 try: 

759 base = next(iteratorStack[-1]) # type: type 

760 typeStack.append(base) 

761 if len(base.__bases__) == 0: 

762 yield tuple(typeStack) 

763 typeStack.pop() 

764 else: 

765 iteratorStack.append(iter(base.__bases__)) 

766 

767 except StopIteration: 

768 typeStack.pop() 

769 iteratorStack.pop() 

770 

771 if len(typeStack) == 0: 

772 break 

773 

774 @classmethod 

775 def _checkForAbstractMethods(metacls, baseClasses: Tuple[type], members: Dict[str, Any]) -> Tuple[Dict[str, Callable], Dict[str, Any]]: 

776 """ 

777 Check if the current class contains abstract methods and return a tuple of them. 

778 

779 These abstract methods might be inherited from any base-class. If there are inherited abstract methods, check if 

780 they are now implemented (overridden) by the current class that's right now constructed. 

781 

782 :param baseClasses: The tuple of :term:`base-classes <base-class>` the class is derived from. 

783 :param members: The dictionary of members for the constructed class. 

784 :returns: A tuple of abstract method's names. 

785 """ 

786 abstractMethods = {} 

787 if baseClasses: 

788 # Aggregate all abstract methods from all base-classes. 

789 for baseClass in baseClasses: 

790 if hasattr(baseClass, "__abstractMethods__"): 

791 abstractMethods.update(baseClass.__abstractMethods__) 

792 

793 for base in baseClasses: 

794 for key, value in base.__dict__.items(): 

795 if (key in abstractMethods and isinstance(value, FunctionType) and 

796 not (hasattr(value, "__abstract__") or hasattr(value, "__mustOverride__"))): 

797 def outer(method): 

798 @wraps(method) 

799 def inner(cls, *args: Any, **kwargs: Any): 

800 return method(cls, *args, **kwargs) 

801 

802 return inner 

803 

804 members[key] = outer(value) 

805 

806 # Check if methods are marked: 

807 # * If so, add them to list of abstract methods 

808 # * If not, method is now implemented and removed from list 

809 for memberName, member in members.items(): 

810 if callable(member): 

811 if ((hasattr(member, "__abstract__") and member.__abstract__) or 

812 (hasattr(member, "__mustOverride__") and member.__mustOverride__)): 

813 abstractMethods[memberName] = member 

814 elif memberName in abstractMethods: 

815 del abstractMethods[memberName] 

816 

817 return abstractMethods, members 

818 

819 @classmethod 

820 def _wrapNewMethodIfSingleton(metacls, newClass, singleton: bool) -> bool: 

821 """ 

822 If a class is a singleton, wrap the ``_new__`` method, so it returns a cached object, if a first object was created. 

823 

824 Only the first object creation initializes the object. 

825 

826 This implementation is threadsafe. 

827 

828 :param newClass: The newly constructed class for further modifications. 

829 :param singleton: If ``True``, the class allows only a single instance to exist. 

830 :returns: ``True``, if the class is a singleton. 

831 """ 

832 if hasattr(newClass, "__isSingleton__"): 

833 singleton = newClass.__isSingleton__ 

834 

835 if singleton: 

836 oldnew = newClass.__new__ 

837 if hasattr(oldnew, "__singleton_wrapper__"): 

838 oldnew = oldnew.__wrapped__ 

839 

840 oldinit = newClass.__init__ 

841 if hasattr(oldinit, "__singleton_wrapper__"): 841 ↛ 842line 841 didn't jump to line 842 because the condition on line 841 was never true

842 oldinit = oldinit.__wrapped__ 

843 

844 @wraps(oldnew) 

845 def singleton_new(cls, *args: Any, **kwargs: Any): 

846 with cls.__singletonInstanceCond__: 

847 if cls.__singletonInstanceCache__ is None: 

848 obj = oldnew(cls, *args, **kwargs) 

849 cls.__singletonInstanceCache__ = obj 

850 else: 

851 obj = cls.__singletonInstanceCache__ 

852 

853 return obj 

854 

855 @wraps(oldinit) 

856 def singleton_init(self, *args: Any, **kwargs: Any): 

857 cls = self.__class__ 

858 cv = cls.__singletonInstanceCond__ 

859 with cv: 

860 if cls.__singletonInstanceInit__: 

861 oldinit(self, *args, **kwargs) 

862 cls.__singletonInstanceInit__ = False 

863 cv.notify_all() 

864 elif args or kwargs: 

865 raise ValueError(f"A further instance of a singleton can't be reinitialized with parameters.") 

866 else: 

867 while cls.__singletonInstanceInit__: 867 ↛ 868line 867 didn't jump to line 868 because the condition on line 867 was never true

868 cv.wait() 

869 

870 singleton_new.__singleton_wrapper__ = True 

871 singleton_init.__singleton_wrapper__ = True 

872 

873 newClass.__new__ = singleton_new 

874 newClass.__init__ = singleton_init 

875 newClass.__singletonInstanceCond__ = Condition() 

876 newClass.__singletonInstanceInit__ = True 

877 newClass.__singletonInstanceCache__ = None 

878 return True 

879 

880 return False 

881 

882 @classmethod 

883 def _wrapNewMethodIfAbstract(metacls, newClass) -> bool: 

884 """ 

885 If the class has abstract methods, replace the ``_new__`` method, so it raises an exception. 

886 

887 :param newClass: The newly constructed class for further modifications. 

888 :returns: ``True``, if the class is abstract. 

889 :raises AbstractClassError: If the class is abstract and can't be instantiated. 

890 """ 

891 # Replace '__new__' by a variant to throw an error on not overridden methods 

892 if len(newClass.__abstractMethods__) > 0: 

893 oldnew = newClass.__new__ 

894 if hasattr(oldnew, "__raises_abstract_class_error__"): 

895 oldnew = oldnew.__wrapped__ 

896 

897 @wraps(oldnew) 

898 def abstract_new(cls, *_, **__): 

899 raise AbstractClassError(f"""Class '{cls.__name__}' is abstract. The following methods: '{"', '".join(newClass.__abstractMethods__)}' need to be overridden in a derived class.""") 

900 

901 abstract_new.__raises_abstract_class_error__ = True 

902 

903 newClass.__new__ = abstract_new 

904 return True 

905 

906 # Handle classes which are not abstract, especially derived classes, if not abstract anymore 

907 else: 

908 # skip intermediate 'new' function if class isn't abstract anymore 

909 try: 

910 if newClass.__new__.__raises_abstract_class_error__: 910 ↛ 923line 910 didn't jump to line 923 because the condition on line 910 was always true

911 origNew = newClass.__new__.__wrapped__ 

912 

913 # WORKAROUND: __new__ checks tp_new and implements different behavior 

914 # Bugreport: https://github.com/python/cpython/issues/105888 

915 if origNew is object.__new__: 915 ↛ 922line 915 didn't jump to line 922 because the condition on line 915 was always true

916 @wraps(object.__new__) 

917 def wrapped_new(inst, *_, **__): 

918 return object.__new__(inst) 

919 

920 newClass.__new__ = wrapped_new 

921 else: 

922 newClass.__new__ = origNew 

923 elif newClass.__new__.__isSingleton__: 

924 raise Exception(f"Found a singleton wrapper around an AbstractError raising method. This case is not handled yet.") 

925 except AttributeError as ex: 

926 # WORKAROUND: 

927 # AttributeError.name was added in Python 3.10. For version <3.10 use a string contains operation. 

928 try: 

929 if ex.name != "__raises_abstract_class_error__": 929 ↛ 930line 929 didn't jump to line 930 because the condition on line 929 was never true

930 raise ex 

931 except AttributeError: 

932 if "__raises_abstract_class_error__" not in str(ex): 

933 raise ex 

934 

935 return False 

936 

937 # Additional properties and methods on a class 

938 @property 

939 def HasClassAttributes(self) -> bool: 

940 """ 

941 Read-only property to check if the class has Attributes (:attr:`__pyattr__`). 

942 

943 :returns: ``True``, if the class has Attributes. 

944 """ 

945 try: 

946 return len(self.__pyattr__) > 0 

947 except AttributeError: 

948 return False 

949 

950 @property 

951 def HasMethodAttributes(self) -> bool: 

952 """ 

953 Read-only property to check if the class has methods with Attributes (:attr:`__methodsWithAttributes__`). 

954 

955 :returns: ``True``, if the class has any method with Attributes. 

956 """ 

957 try: 

958 return len(self.__methodsWithAttributes__) > 0 

959 except AttributeError: 

960 return False 

961 

962 

963@export 

964class SlottedObject(metaclass=ExtendedType, slots=True): 

965 """Classes derived from this class will store all members in ``__slots__``."""