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

414 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-25 22:22 +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 

38# from inspect import signature, Parameter 

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(f"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(f"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(f"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 annotations: Dict[str, Any] = members.get("__annotations__", {}) 

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

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

612 cls = inheritedSlottedFields[fieldName] 

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

614 

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

616 # * copy field and initial value to classFields dictionary 

617 # * remove field from members 

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

619 classFields[fieldName] = members[fieldName] 

620 del members[fieldName] 

621 

622 # If an annotated field has an initial value 

623 # * copy field and initial value to objectFields dictionary 

624 # * remove field from members 

625 elif fieldName in members: 

626 slottedFields.append(fieldName) 

627 objectFields[fieldName] = members[fieldName] 

628 del members[fieldName] 

629 else: 

630 slottedFields.append(fieldName) 

631 

632 mixinSlots = self._aggregateMixinSlots(className, baseClasses) 

633 else: 

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

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

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

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

638 # * copy field and initial value to classFields dictionary 

639 # * remove field from members 

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

641 classFields[fieldName] = members[fieldName] 

642 del members[fieldName] 

643 

644 # FIXME: search for fields without annotation 

645 if mixin: 

646 mixinSlots.extend(slottedFields) 

647 members["__slotted__"] = True 

648 members["__slots__"] = tuple() 

649 members["__isMixin__"] = True 

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

651 elif slots: 

652 slottedFields.extend(mixinSlots) 

653 members["__slotted__"] = True 

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

655 members["__isMixin__"] = False 

656 members["__mixinSlots__"] = tuple() 

657 else: 

658 members["__slotted__"] = False 

659 # NO __slots__ 

660 members["__isMixin__"] = False 

661 members["__mixinSlots__"] = tuple() 

662 return classFields, objectFields 

663 

664 @classmethod 

665 def _aggregateMixinSlots(self, className, baseClasses): 

666 mixinSlots = [] 

667 if len(baseClasses) > 0: 

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

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

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

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

672 for typePath in inheritancePaths[1:]: 

673 for t in typePath: 

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

675 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}'.") 

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

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

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

679 raise ex 

680 

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

682 # Ensure all base-classes are either constructed 

683 # * by meta-class ExtendedType, or 

684 # * use no slots, or 

685 # * are typing.Generic 

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

687 for baseClass in baseClasses: # type: ExtendedType 

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

689 pass 

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

691 mixinSlots.extend(baseClass.__mixinSlots__) 

692 elif hasattr(baseClass, "__mixinSlots__"): 

693 mixinSlots.extend(baseClass.__mixinSlots__) 

694 

695 return mixinSlots 

696 

697 @classmethod 

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

699 if len(baseClasses) == 0: 

700 return 

701 

702 visited: Set[type] = set() 

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

704 

705 for baseClass in baseClasses: 

706 yield baseClass 

707 visited.add(baseClass) 

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

709 

710 while True: 

711 try: 

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

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

714 yield base 

715 if len(base.__bases__) > 0: 

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

717 else: 

718 continue 

719 

720 except StopIteration: 

721 iteratorStack.pop() 

722 

723 if len(iteratorStack) == 0: 

724 break 

725 

726 @classmethod 

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

728 """ 

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

730 

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

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

733 

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

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

736 """ 

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

738 return 

739 

740 typeStack: List[type] = list() 

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

742 

743 for baseClass in baseClasses: 

744 typeStack.append(baseClass) 

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

746 

747 while True: 

748 try: 

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

750 typeStack.append(base) 

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

752 yield tuple(typeStack) 

753 typeStack.pop() 

754 else: 

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

756 

757 except StopIteration: 

758 typeStack.pop() 

759 iteratorStack.pop() 

760 

761 if len(typeStack) == 0: 

762 break 

763 

764 @classmethod 

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

766 """ 

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

768 

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

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

771 

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

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

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

775 """ 

776 abstractMethods = {} 

777 if baseClasses: 

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

779 for baseClass in baseClasses: 

780 if hasattr(baseClass, "__abstractMethods__"): 

781 abstractMethods.update(baseClass.__abstractMethods__) 

782 

783 for base in baseClasses: 

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

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

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

787 def outer(method): 

788 @wraps(method) 

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

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

791 

792 return inner 

793 

794 members[key] = outer(value) 

795 

796 # Check if methods are marked: 

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

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

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

800 if callable(member): 

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

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

803 abstractMethods[memberName] = member 

804 elif memberName in abstractMethods: 

805 del abstractMethods[memberName] 

806 

807 return abstractMethods, members 

808 

809 @classmethod 

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

811 """ 

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

813 

814 Only the first object creation initializes the object. 

815 

816 This implementation is threadsafe. 

817 

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

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

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

821 """ 

822 if hasattr(newClass, "__isSingleton__"): 

823 singleton = newClass.__isSingleton__ 

824 

825 if singleton: 

826 oldnew = newClass.__new__ 

827 if hasattr(oldnew, "__singleton_wrapper__"): 

828 oldnew = oldnew.__wrapped__ 

829 

830 oldinit = newClass.__init__ 

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

832 oldinit = oldinit.__wrapped__ 

833 

834 @wraps(oldnew) 

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

836 with cls.__singletonInstanceCond__: 

837 if cls.__singletonInstanceCache__ is None: 

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

839 cls.__singletonInstanceCache__ = obj 

840 else: 

841 obj = cls.__singletonInstanceCache__ 

842 

843 return obj 

844 

845 @wraps(oldinit) 

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

847 cls = self.__class__ 

848 cv = cls.__singletonInstanceCond__ 

849 with cv: 

850 if cls.__singletonInstanceInit__: 

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

852 cls.__singletonInstanceInit__ = False 

853 cv.notify_all() 

854 elif args or kwargs: 

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

856 else: 

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

858 cv.wait() 

859 

860 singleton_new.__singleton_wrapper__ = True 

861 singleton_init.__singleton_wrapper__ = True 

862 

863 newClass.__new__ = singleton_new 

864 newClass.__init__ = singleton_init 

865 newClass.__singletonInstanceCond__ = Condition() 

866 newClass.__singletonInstanceInit__ = True 

867 newClass.__singletonInstanceCache__ = None 

868 return True 

869 

870 return False 

871 

872 @classmethod 

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

874 """ 

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

876 

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

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

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

880 """ 

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

882 if len(newClass.__abstractMethods__) > 0: 

883 oldnew = newClass.__new__ 

884 if hasattr(oldnew, "__raises_abstract_class_error__"): 

885 oldnew = oldnew.__wrapped__ 

886 

887 @wraps(oldnew) 

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

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

890 

891 abstract_new.__raises_abstract_class_error__ = True 

892 

893 newClass.__new__ = abstract_new 

894 return True 

895 

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

897 else: 

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

899 try: 

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

901 origNew = newClass.__new__.__wrapped__ 

902 

903 # WORKAROUND: __new__ checks tp_new and implements different behavior 

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

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

906 @wraps(object.__new__) 

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

908 return object.__new__(inst) 

909 

910 newClass.__new__ = wrapped_new 

911 else: 

912 newClass.__new__ = origNew 

913 elif newClass.__new__.__isSingleton__: 

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

915 except AttributeError as ex: 

916 # WORKAROUND: 

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

918 try: 

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

920 raise ex 

921 except AttributeError: 

922 if "__raises_abstract_class_error__" not in str(ex): 922 ↛ 923line 922 didn't jump to line 923 because the condition on line 922 was never true

923 raise ex 

924 

925 return False 

926 

927 # Additional properties and methods on a class 

928 @property 

929 def HasClassAttributes(self) -> bool: 

930 """ 

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

932 

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

934 """ 

935 try: 

936 return len(self.__pyattr__) > 0 

937 except AttributeError: 

938 return False 

939 

940 @property 

941 def HasMethodAttributes(self) -> bool: 

942 """ 

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

944 

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

946 """ 

947 try: 

948 return len(self.__methodsWithAttributes__) > 0 

949 except AttributeError: 

950 return False 

951 

952 

953@export 

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

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