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

451 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-31 07:24 +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-2026 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:: 

36 

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

38""" 

39from functools import wraps 

40from itertools import chain 

41from sys import version_info 

42from threading import Condition 

43from types import BuiltinFunctionType, FunctionType, MethodType 

44from typing import Any, Tuple, List, Dict, Callable, Generator, Set, Iterator, Iterable, Union, NoReturn, Self 

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

46 

47from pyTooling.Exceptions import ToolingException 

48from pyTooling.Decorators import export, readonly 

49from pyTooling.Warning import Warning, WarningCollector 

50 

51 

52__all__ = ["M"] 

53 

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

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

56 

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

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

59iterable of those.""" 

60 

61 

62@export 

63class ExtendedTypeError(ToolingException): 

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

65 

66 

67@export 

68class BaseClassWithoutSlotsError(ExtendedTypeError): 

69 """ 

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

71 

72 .. seealso:: 

73 

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

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

76 """ 

77 

78 

79@export 

80class BaseClassWithNonEmptySlotsError(ExtendedTypeError): 

81 """ 

82 This exception is raised when a mixin-class uses slots, but Python prohibits slots. 

83 

84 .. important:: 

85 

86 To fulfill Python's requirements on slots, pyTooling uses slots only on the prinmary inheritance line. 

87 Mixin-classes collect slots, which get materialized when the mixin-class (secondary inheritance lines) gets merged 

88 into the primary inheritance line. 

89 """ 

90 

91 

92@export 

93class BaseClassIsNotAMixinError(ExtendedTypeError): 

94 pass 

95 

96 

97@export 

98class DuplicateFieldInSlotsError(ExtendedTypeError): 

99 """ 

100 This exception is raised when a slot name is used multiple times within the inheritance hierarchy. 

101 """ 

102 

103 

104@export 

105class UnannotatedFieldWarning(Warning): 

106 """ 

107 A class declares a field that was assigned in the class body without a type annotation. 

108 

109 An object field is annotated with its type, a class variable with :class:`~typing.ClassVar`. Without an annotation, 

110 :class:`ExtendedType` can't tell the two apart, and the field never becomes a slot. 

111 """ 

112 

113 

114@export 

115class IncompatibleMetaClassError(ExtendedTypeError): 

116 """ 

117 This exception is raised when a class decorated with :deco:`slotted`, :deco:`mixin` or :deco:`singleton` uses a 

118 meta-class that is neither :class:`type` nor derived from :class:`~pyTooling.MetaClasses.ExtendedType`. 

119 """ 

120 

121 

122@export 

123class AbstractClassError(ExtendedTypeError): 

124 """ 

125 This exception is raised, when a class contains methods marked with *abstractmethod* or *must-override*. 

126 

127 .. seealso:: 

128 

129 :deco:`~pyTooling.MetaClasses.abstractmethod` 

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

131 :deco:`~pyTooling.MetaClasses.mustoverride` 

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

133 :exc:`~MustOverrideClassError` 

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

135 """ 

136 

137 

138@export 

139class MustOverrideClassError(AbstractClassError): 

140 """ 

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

142 

143 .. seealso:: 

144 

145 :deco:`~pyTooling.MetaClasses.abstractmethod` 

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

147 :deco:`~pyTooling.MetaClasses.mustoverride` 

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

149 :exc:`~AbstractClassError` 

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

151 """ 

152 

153 

154# """ 

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

156# 

157# .. seealso: 

158# 

159# `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>`__ 

160# """ 

161 

162 

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

164 

165 

166def _recreateClass(cls: type, decoratorName: str, **options: bool) -> type: 

167 """ 

168 Recreate a class with :class:`ExtendedType` (or the class' own compatible meta-class) applying the given options. 

169 

170 :param cls: Class to recreate. 

171 :param decoratorName: Name of the calling decorator. It's used in the error message. 

172 :param options: Meta-class options like ``slots``, ``mixin`` or ``singleton``. 

173 :returns: The recreated class. 

174 :raises IncompatibleMetaClassError: If the class' meta-class is neither :class:`type`, nor derived from 

175 :class:`ExtendedType`. 

176 """ 

177 if cls.__class__ is type: 

178 metacls = ExtendedType 

179 elif issubclass(cls.__class__, ExtendedType): 

180 metacls = cls.__class__ 

181 for method in cls.__methods__: 

182 delattr(method, "__classobj__") 

183 else: 

184 metaClass = cls.__class__ 

185 ex = IncompatibleMetaClassError(f"Class '{cls.__name__}' decorated with '@{decoratorName}' uses an incompatible meta-class.") 

186 ex.add_note(f"Meta-class is '{metaClass.__module__}.{metaClass.__name__}'.") 

187 ex.add_note(f"A decorated class must use 'type' or a meta-class derived from 'pyTooling.MetaClasses.ExtendedType'.") 

188 raise ex 

189 

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

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

192 members = { 

193 "__qualname__": cls.__qualname__ 

194 } 

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

196 if key not in slots: 

197 members[key] = value 

198 

199 return metacls(cls.__name__, bases, members, **options) 

200 

201 

202@export 

203def slotted(cls): 

204 return _recreateClass(cls, "slotted", slots=True) 

205 

206 

207@export 

208def mixin(cls): 

209 return _recreateClass(cls, "mixin", mixin=True) 

210 

211 

212@export 

213def singleton(cls): 

214 return _recreateClass(cls, "singleton", singleton=True) 

215 

216 

217@export 

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

219 """ 

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

221 

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

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

224 

225 .. warning:: 

226 

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

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

229 instantiation. 

230 

231 .. admonition:: ``example.py`` 

232 

233 .. code-block:: python 

234 

235 class Data(mataclass=ExtendedType): 

236 @abstractmethod 

237 def method(self) -> bool: 

238 '''This method needs to be implemented''' 

239 

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

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

242 

243 .. seealso:: 

244 

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

246 * :deco:`~pyTooling.MetaClasses.mustoverride` 

247 * :deco:`~pyTooling.Decorators.notimplemented` 

248 """ 

249 @wraps(method) 

250 def func(self) -> NoReturn: 

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

252 

253 func.__abstract__ = True 

254 return func 

255 

256 

257@export 

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

259 """ 

260 Mark a method as *must-override*. 

261 

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

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

264 overridden by an inheriting class. 

265 

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

267 

268 .. warning:: 

269 

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

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

272 instantiation. 

273 

274 .. admonition:: ``example.py`` 

275 

276 .. code-block:: python 

277 

278 class Data(mataclass=ExtendedType): 

279 @mustoverride 

280 def method(self): 

281 '''This is a very basic implementation''' 

282 

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

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

285 

286 .. seealso:: 

287 

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

289 * :deco:`~pyTooling.MetaClasses.abstractmethod` 

290 * :deco:`~pyTooling.Decorators.notimplemented` 

291 """ 

292 method.__mustOverride__ = True 

293 return method 

294 

295 

296# @export 

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

298# method.__overloadable__ = True 

299# return method 

300 

301 

302# @export 

303# class DispatchableMethod: 

304# """Represents a single multimethod.""" 

305# 

306# _methods: Dict[Tuple, Callable] 

307# __name__: str 

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

309# 

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

311# self.__name__ = name 

312# self._methods = {} 

313# 

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

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

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

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

318# if meth: 

319# return meth(*args) 

320# else: 

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

322# 

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

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

325# if instance is not None: 

326# return MethodType(self, instance) 

327# else: 

328# return self 

329# 

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

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

332# 

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

334# sig = signature(method) 

335# types: List[Type] = [] 

336# 

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

338# if name == "self": 

339# continue 

340# 

341# if parameter.annotation is Parameter.empty: 

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

343# 

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

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

346# 

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

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

349# 

350# types.append(parameter.annotation) 

351# 

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

353 

354 

355# @export 

356# class DispatchDictionary(dict): 

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

358# 

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

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

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

362# currentValue = self[key] 

363# if isinstance(currentValue, DispatchableMethod): 

364# currentValue.register(value) 

365# else: 

366# dispatchable = DispatchableMethod(key) 

367# dispatchable.register(currentValue) 

368# dispatchable.register(value) 

369# 

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

371# else: 

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

373 

374 

375@export 

376class ExtendedType(type): 

377 """ 

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

379 

380 .. todo:: META::ExtendedType Needs documentation. 

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

382 

383 .. rubric:: Features: 

384 

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

386 

387 * Implement ``__slots__`` only on primary inheritance line. 

388 * Collect class variables on secondary inheritance lines (mixin-classes) and defer implementation as ``__slots__``. 

389 * Handle object state exporting and importing for slots (:mod:`pickle` support) via ``__getstate__``/``__setstate__``. 

390 

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

392 Further instantiations will return the previously create instance (identical object). 

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

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

395 

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

397 

398 .. rubric:: Added class fields: 

399 

400 :__slotted__: True, if class uses `__slots__`. 

401 :__allSlots__: Set of class fields stored in slots for all classes in the inheritance hierarchy. 

402 :__slots__: Tuple of class fields stored in slots for current class in the inheritance hierarchy. |br| 

403 See :pep:`253` for details. 

404 :__isMixin__: True, if class is a mixin-class 

405 :__mixinSlots__: List of collected slots from secondary inheritance hierarchy (mixin hierarchy). 

406 :__methods__: List of methods. 

407 :__methodsWithAttributes__: List of methods with pyTooling attributes. 

408 :__abstractMethods__: List of abstract methods, which need to be implemented in the next class hierarchy levels. 

409 :__isAbstract__: True, if class is abstract. 

410 :__isSingleton__: True, if class is a singleton 

411 :__singletonInstanceCond__: Condition variable to protect the singleton creation. 

412 :__singletonInstanceInit__: Singleton is initialized. 

413 :__singletonInstanceCache__: The singleton object, once created. 

414 :__pyattr__: List of class attributes. 

415 

416 .. rubric:: Added class properties: 

417 

418 :HasClassAttributes: Read-only property to check if the class has Attributes. 

419 :HasMethodAttributes: Read-only property to check if the class has methods with Attributes. 

420 

421 .. rubric:: Added methods: 

422 

423 If slots are used, the following methods are added to support :mod:`pickle`: 

424 

425 :__getstate__: Export an object's state for serialization. |br| 

426 See :pep:`307` for details. 

427 :__setstate__: Import an object's state for deserialization. |br| 

428 See :pep:`307` for details. 

429 

430 .. rubric:: Modified ``__new__`` method: 

431 

432 If class is a singleton, ``__new__`` will be replaced by a wrapper method. This wrapper is marked with ``__singleton_wrapper__``. 

433 

434 If class is abstract, ``__new__`` will be replaced by a method raising an exception. This replacement is marked with ``__raises_abstract_class_error__``. 

435 

436 .. rubric:: Modified ``__init__`` method: 

437 

438 If class is a singleton, ``__init__`` will be replaced by a wrapper method. This wrapper is marked by ``__singleton_wrapper__``. 

439 

440 .. rubric:: Modified abstract methods: 

441 

442 If a method is abstract, its marked with ``__abstract__``. |br| 

443 If a method is must override, its marked with ``__mustOverride__``. 

444 """ 

445 

446 # @classmethod 

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

448 # return DispatchDictionary() 

449 

450 def __new__( 

451 self, 

452 className: str, 

453 baseClasses: Tuple[type], 

454 members: Dict[str, Any], 

455 slots: bool = False, 

456 mixin: bool = False, 

457 singleton: bool = False 

458 ) -> Self: 

459 """ 

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

461 

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

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

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

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

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

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

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

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

470 :returns: The new class. 

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

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

473 """ 

474 from pyTooling.Attributes import ATTRIBUTES_MEMBER_NAME, AttributeScope 

475 

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

477 if len(baseClasses) > 0: 

478 primaryBaseClass = baseClasses[0] 

479 if isinstance(primaryBaseClass, self): 

480 slots = primaryBaseClass.__slotted__ 

481 

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

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

484 

485 # Compute abstract methods 

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

487 

488 # Create a new class 

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

490 

491 # Apply class fields 

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

493 setattr(newClass, fieldName, typeAnnotation) 

494 

495 # Search in inheritance tree for abstract methods 

496 newClass.__abstractMethods__ = abstractMethods 

497 newClass.__isAbstract__ = self._wrapNewMethodIfAbstract(newClass) 

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

499 

500 if slots: 

501 # If slots are used, implement __getstate__/__setstate__ API to support serialization using pickle. 

502 if "__getstate__" not in members: 

503 def __getstate__(self) -> Dict[str, Any]: 

504 try: 

505 return {slotName: getattr(self, slotName) for slotName in self.__allSlots__} 

506 except AttributeError as ex: 

507 raise ExtendedTypeError(f"Unassigned field '{ex.name}' in object '{self}' of type '{self.__class__.__name__}'.") from ex 

508 

509 newClass.__getstate__ = __getstate__ 

510 

511 if "__setstate__" not in members: 

512 def __setstate__(self, state: Dict[str, Any]) -> None: 

513 if self.__allSlots__ != (slots := set(state.keys())): 

514 if len(diff := self.__allSlots__.difference(slots)) > 0: 

515 raise ExtendedTypeError(f"""Missing fields in parameter 'state': '{"', '".join(diff)}'""") # WORKAROUND: Python <3.12 

516 else: 

517 diff = slots.difference(self.__allSlots__) 

518 raise ExtendedTypeError(f"""Unexpected fields in parameter 'state': '{"', '".join(diff)}'""") # WORKAROUND: Python <3.12 

519 

520 for slotName, value in state.items(): 

521 setattr(self, slotName, value) 

522 

523 newClass.__setstate__ = __setstate__ 

524 

525 # Check for inherited class attributes 

526 attributes = [] 

527 setattr(newClass, ATTRIBUTES_MEMBER_NAME, attributes) 

528 for base in baseClasses: 

529 if hasattr(base, ATTRIBUTES_MEMBER_NAME): 

530 pyAttr = getattr(base, ATTRIBUTES_MEMBER_NAME) 

531 for att in pyAttr: 

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

533 attributes.append(att) 

534 att.__class__._classes.append(newClass) 

535 

536 # Check methods for attributes 

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

538 

539 # Add new fields for found methods 

540 newClass.__methods__ = tuple(methods) 

541 newClass.__methodsWithAttributes__ = tuple(methodsWithAttributes) 

542 

543 # Additional methods on a class 

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

545 """ 

546 

547 :param predicate: An attribute class, an iterable of attribute classes, or ``None`` to accept every attribute. 

548 :returns: Dictionary of methods and the matching attributes attached to them. 

549 :raises ValueError: If an element of parameter 'predicate' is not a sub-class of :class:`~pyTooling.Attributes.Attribute`. 

550 :raises ValueError: If parameter 'predicate' is neither an attribute class nor an iterable of those. 

551 """ 

552 from pyTooling.Attributes import Attribute 

553 

554 if predicate is None: 

555 predicate = Attribute 

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

557 for attribute in predicate: 

558 if not issubclass(attribute, Attribute): 

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

560 

561 predicate = tuple(predicate) 

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

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

564 

565 methodAttributePairs = {} 

566 for method in newClass.__methodsWithAttributes__: 

567 matchingAttributes = [] 

568 for attribute in method.__pyattr__: 

569 if isinstance(attribute, predicate): 

570 matchingAttributes.append(attribute) 

571 

572 if len(matchingAttributes) > 0: 

573 methodAttributePairs[method] = tuple(matchingAttributes) 

574 

575 return methodAttributePairs 

576 

577 newClass.GetMethodsWithAttributes = classmethod(GetMethodsWithAttributes) 

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

579 

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

581 # GetClassAtrributes -> list[attributes] / generator 

582 # MethodHasAttributes(predicate) -> bool 

583 # GetAttribute 

584 

585 return newClass 

586 

587 @classmethod 

588 def _findMethods( 

589 self, 

590 newClass: "ExtendedType", 

591 baseClasses: Tuple[type], 

592 members: Dict[str, Any] 

593 ) -> Tuple[List[MethodType], List[MethodType]]: 

594 """ 

595 Find methods and methods with :mod:`pyTooling.Attributes`. 

596 

597 .. todo:: 

598 

599 Describe algorithm. 

600 

601 :param newClass: Newly created class instance. 

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

603 :param members: Members of the new class. 

604 :returns: A 2-tuple of all methods and those methods carrying at least one attribute. 

605 """ 

606 from pyTooling.Attributes import Attribute 

607 

608 # Embedded bind function due to circular dependencies. 

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

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

611 methodName = func.__name__ 

612 

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

614 setattr(instance, methodName, boundMethod) 

615 

616 return boundMethod 

617 

618 methods = [] 

619 methodsWithAttributes = [] 

620 attributeIndex = {} 

621 

622 for base in baseClasses: 

623 if hasattr(base, "__methodsWithAttributes__"): 

624 methodsWithAttributes.extend(base.__methodsWithAttributes__) 

625 

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

627 if isinstance(member, FunctionType): 

628 method = newClass.__dict__[memberName] 

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

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

631 else: 

632 setattr(method, "__classobj__", newClass) 

633 

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

635 results = [] 

636 try: 

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

638 if isinstance(attribute, predicate): 

639 results.append(attribute) 

640 return tuple(results) 

641 except AttributeError: 

642 return tuple() 

643 

644 method.GetAttributes = bind(method, GetAttributes) 

645 methods.append(method) 

646 

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

648 # print(f" {member}") 

649 if "__pyattr__" in member.__dict__: 

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

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

652 methodsWithAttributes.append(member) 

653 for attribute in attributes: 

654 attribute._functions.remove(method) 

655 attribute._methods.append(method) 

656 

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

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

659 attributeIndex[attribute] = [member] 

660 else: 

661 attributeIndex[attribute].append(member) 

662 # else: 

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

664 # else: 

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

666 return methods, methodsWithAttributes 

667 

668 @classmethod 

669 def _getAnnotations(metacls, members: Dict[str, Any]) -> Dict[str, Any]: 

670 """ 

671 Return the type annotations declared in a class body. 

672 

673 .. important:: 

674 

675 Python 3.14 (:pep:`649`) no longer fills ``__annotations__`` while the class body is executed, but installs an 

676 ``__annotate_func__`` instead. The :mod:`annotationlib` module needed to evaluate that function doesn't exist on 

677 older Python versions, therefore both mechanisms are supported. 

678 

679 :param members: Dictionary of class members. 

680 :returns: Dictionary of annotated field names and their type annotations. Empty, if the class body declared 

681 no annotations. 

682 """ 

683 if "__annotations__" in members: 

684 # WORKAROUND: LEGACY SUPPORT Python <= 3.13 

685 # Accessing annotations was changed in Python 3.14. 

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

687 return members["__annotations__"] 

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

689 from annotationlib import Format 

690 return annotate(Format.VALUE) 

691 else: 

692 return {} 

693 

694 @classmethod 

695 def _isClassVariable(metacls, typeAnnotation: Any) -> bool: 

696 """ 

697 Check if a type annotation declares a class variable. 

698 

699 :param typeAnnotation: The type annotation to check. 

700 :returns: ``True``, if the annotation is a :class:`~typing.ClassVar`. 

701 """ 

702 return isinstance(typeAnnotation, _GenericAlias) and typeAnnotation.__origin__ is ClassVar 

703 

704 @classmethod 

705 def _isField(metacls, member: Any) -> bool: 

706 """ 

707 Check if a class member is a field, so a type annotation is expected for it. 

708 

709 Methods, nested classes, properties and any other descriptor carry their type information in their signature or 

710 in their own declaration, therefore they aren't fields. 

711 

712 :param member: The class member to check. 

713 :returns: ``True``, if the member is a field. 

714 """ 

715 if isinstance(member, (FunctionType, MethodType, BuiltinFunctionType, classmethod, staticmethod, property, type)): 

716 return False 

717 

718 # Any descriptor (e.g. a custom property implementation) declares its own type information. 

719 return not (hasattr(member, "__get__") or hasattr(member, "__set__")) 

720 

721 @classmethod 

722 def _checkForUnannotatedFields(metacls, className: str, members: Dict[str, Any], annotations: Dict[str, Any]) -> None: 

723 """ 

724 Report fields that were assigned in the class body without a type annotation. 

725 

726 Every field should carry type information: an object field is annotated with its type, a class variable with 

727 ``ClassVar[...]``. Without an annotation, :class:`ExtendedType` can't tell the two apart - an un-annotated 

728 assignment never becomes a slot and silently stays a class attribute. 

729 

730 .. important:: 

731 

732 This is reported as a :class:`~pyTooling.Warning.Warning`, so it needs a 

733 :class:`~pyTooling.Warning.WarningCollector` somewhere up the call-hierarchy to be observed. Importing a module 

734 with un-annotated fields doesn't fail. 

735 

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

737 :param members: Dictionary of class members. 

738 :param annotations: Dictionary of annotated field names and their type annotations. 

739 """ 

740 unannotatedFields = [ 

741 fieldName 

742 for fieldName, member in members.items() 

743 if not (fieldName.startswith("__") and fieldName.endswith("__")) 

744 and fieldName not in annotations 

745 and metacls._isField(member) 

746 ] 

747 

748 if len(unannotatedFields) > 0: 

749 fieldNames = "', '".join(unannotatedFields) 

750 WarningCollector.Raise( 

751 UnannotatedFieldWarning(f"Class '{className}' declares {len(unannotatedFields)} field(s) without a type annotation."), 

752 notes=( 

753 f"Field(s) without a type annotation: '{fieldNames}'.", 

754 f"Annotate a class variable as 'ClassVar[...]' or an object field with its type.", 

755 ) 

756 ) 

757 

758 @classmethod 

759 def _computeSlots( 

760 self, 

761 className: str, 

762 baseClasses: Tuple[type], 

763 members: Dict[str, Any], 

764 slots: bool, 

765 mixin: bool 

766 ) -> Tuple[Dict[str, Any], Dict[str, Any]]: 

767 """ 

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

769 

770 .. todo:: 

771 

772 Describe algorithm. 

773 

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

775 :param baseClasses: Tuple of base-classes. 

776 :param members: Dictionary of class members. 

777 :param slots: True, if the class should setup ``__slots__``. 

778 :param mixin: True, if the class should behave as a mixin-class. 

779 :returns: A 2-tuple with a dictionary of class members and object members. 

780 """ 

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

782 slottedFields = [] 

783 classFields = {} 

784 objectFields = {} 

785 annotations: Dict[str, Any] = self._getAnnotations(members) 

786 if slots or mixin: 

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

788 for baseClass in self._iterateBaseClasses(baseClasses): 

789 # Exclude object as a special case 

790 if baseClass is object or baseClass is Generic: 

791 continue 

792 

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

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

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

796 raise ex 

797 

798 # Non-empty __slots__ on secondary base-classes are rejected by _aggregateMixinSlots below. 

799 

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

801 inheritedSlottedFields = {} 

802 if len(baseClasses) > 0: 

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

804 # Exclude object as a special case 

805 if base is object or base is Generic: 

806 continue 

807 

808 for annotation in base.__slots__: 

809 inheritedSlottedFields[annotation] = base 

810 

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

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

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

814 cls = inheritedSlottedFields[fieldName] 

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

816 

817 # A ClassVar is never a slot, with or without an initial value. 

818 # * If it has an initial value, copy field and initial value to classFields dictionary and remove field from members. 

819 # * Otherwise it's a forward declaration and derived classes assign the actual value. 

820 isClassVariable = self._isClassVariable(typeAnnotation) 

821 hasInitialValue = fieldName in members 

822 if isClassVariable: 

823 if hasInitialValue: 

824 classFields[fieldName] = members[fieldName] 

825 del members[fieldName] 

826 

827 # If an annotated field has an initial value 

828 # * copy field and initial value to objectFields dictionary 

829 # * remove field from members 

830 elif hasInitialValue: 

831 slottedFields.append(fieldName) 

832 objectFields[fieldName] = members[fieldName] 

833 del members[fieldName] 

834 else: 

835 slottedFields.append(fieldName) 

836 

837 mixinSlots = self._aggregateMixinSlots(className, baseClasses) 

838 

839 # A member assigned in the class body without a type annotation stays a class attribute. If it carries the name 

840 # of a slot, that class attribute shadows the slot's descriptor and the field becomes read-only on instances. 

841 # Report it here instead of letting the first assignment fail with a bare AttributeError. 

842 shadowedSlots = {**inheritedSlottedFields, **{fieldName: None for fieldName in mixinSlots}} 

843 for fieldName in shadowedSlots.keys() & members.keys(): 

844 ex = DuplicateFieldInSlotsError(f"Slot '{fieldName}' is shadowed by a class member in class '{className}'.") 

845 if (baseClass := shadowedSlots[fieldName]) is not None: 

846 ex.add_note(f"Slot '{fieldName}' is declared in base-class '{baseClass.__module__}.{baseClass.__name__}'.") 

847 ex.add_note(f"An assignment without a type annotation creates a class attribute, which hides the slot's descriptor.") 

848 ex.add_note(f"Reading the field works, but assigning it on an instance raises an AttributeError.") 

849 else: 

850 ex.add_note(f"Slot '{fieldName}' is contributed by a mixin-class and materialized in this class' '__slots__'.") 

851 ex.add_note(f"Python doesn't allow a name to be listed in '__slots__' and assigned in the class body.") 

852 ex.add_note(f"Annotate it as 'ClassVar[...]' to declare a class variable, or remove the assignment.") 

853 raise ex 

854 else: 

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

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

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

858 # * copy field and initial value to classFields dictionary 

859 # * remove field from members 

860 if self._isClassVariable(typeAnnotation) and fieldName in members: 

861 classFields[fieldName] = members[fieldName] 

862 del members[fieldName] 

863 

864 self._checkForUnannotatedFields(className, members, annotations) 

865 

866 if mixin: 

867 mixinSlots.extend(slottedFields) 

868 members["__slotted__"] = True 

869 members["__slots__"] = tuple() 

870 members["__allSlots__"] = set() 

871 members["__isMixin__"] = True 

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

873 elif slots: 

874 slottedFields.extend(mixinSlots) 

875 members["__slotted__"] = True 

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

877 members["__allSlots__"] = set(chain(slottedFields, inheritedSlottedFields.keys())) 

878 members["__isMixin__"] = False 

879 members["__mixinSlots__"] = tuple() 

880 else: 

881 members["__slotted__"] = False 

882 # NO __slots__ 

883 # members["__allSlots__"] = set() 

884 members["__isMixin__"] = False 

885 members["__mixinSlots__"] = tuple() 

886 return classFields, objectFields 

887 

888 @classmethod 

889 def _aggregateMixinSlots(self, className: str, baseClasses: Tuple[type]) -> List[str]: 

890 """ 

891 Aggregate slot names requested by mixin-base-classes. 

892 

893 .. todo:: 

894 

895 Describe algorithm. 

896 

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

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

899 :returns: A list of slot names. 

900 """ 

901 mixinSlots = [] 

902 if len(baseClasses) > 0: 

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

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

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

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

907 for typePath in inheritancePaths[1:]: 

908 for t in typePath: 

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

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

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

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

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

914 raise ex 

915 

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

917 # Ensure all base-classes are either constructed 

918 # * by meta-class ExtendedType, or 

919 # * use no slots, or 

920 # * are typing.Generic 

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

922 for baseClass in baseClasses: # type: ExtendedType 

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

924 pass 

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

926 mixinSlots.extend(baseClass.__mixinSlots__) 

927 elif hasattr(baseClass, "__mixinSlots__"): 

928 mixinSlots.extend(baseClass.__mixinSlots__) 

929 

930 return mixinSlots 

931 

932 @classmethod 

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

934 """ 

935 Return a generator to iterate (visit) all base-classes ... 

936 

937 .. todo:: 

938 

939 Describe iteration order. 

940 

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

942 :returns: Generator to iterate all base-classes. 

943 """ 

944 if len(baseClasses) == 0: 

945 return 

946 

947 visited: Set[type] = set() 

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

949 

950 for baseClass in baseClasses: 

951 yield baseClass 

952 visited.add(baseClass) 

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

954 

955 while True: 

956 try: 

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

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

959 yield base 

960 if len(base.__bases__) > 0: 

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

962 else: 

963 continue 

964 

965 except StopIteration: 

966 iteratorStack.pop() 

967 

968 if len(iteratorStack) == 0: 

969 break 

970 

971 @classmethod 

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

973 """ 

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

975 

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

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

978 

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

980 :returns: Generator to iterate all inheritance paths. |br| 

981 An inheritance path is a tuple of types (base-classes). 

982 """ 

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

984 return 

985 

986 typeStack: List[type] = list() 

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

988 

989 for baseClass in baseClasses: 

990 typeStack.append(baseClass) 

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

992 

993 while True: 

994 try: 

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

996 typeStack.append(base) 

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

998 yield tuple(typeStack) 

999 typeStack.pop() 

1000 else: 

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

1002 

1003 except StopIteration: 

1004 typeStack.pop() 

1005 iteratorStack.pop() 

1006 

1007 if len(typeStack) == 0: 

1008 break 

1009 

1010 @classmethod 

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

1012 """ 

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

1014 

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

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

1017 

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

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

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

1021 """ 

1022 abstractMethods = {} 

1023 if baseClasses: 

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

1025 for baseClass in baseClasses: 

1026 if hasattr(baseClass, "__abstractMethods__"): 

1027 abstractMethods.update(baseClass.__abstractMethods__) 

1028 

1029 for base in baseClasses: 

1030 for memberName, member in base.__dict__.items(): 

1031 if (memberName in abstractMethods and isinstance(member, FunctionType) and 

1032 not (hasattr(member, "__abstract__") or hasattr(member, "__mustOverride__"))): 

1033 def outer(method): 

1034 @wraps(method) 

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

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

1037 

1038 return inner 

1039 

1040 members[memberName] = outer(member) 

1041 

1042 # Check if methods are marked: 

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

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

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

1046 if callable(member): 

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

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

1049 abstractMethods[memberName] = member 

1050 elif memberName in abstractMethods: 

1051 del abstractMethods[memberName] 

1052 

1053 return abstractMethods, members 

1054 

1055 @classmethod 

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

1057 """ 

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

1059 

1060 Only the first object creation initializes the object. 

1061 

1062 This implementation is threadsafe. 

1063 

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

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

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

1067 """ 

1068 if hasattr(newClass, "__isSingleton__"): 

1069 singleton = newClass.__isSingleton__ 

1070 

1071 if singleton: 

1072 oldnew = newClass.__new__ 

1073 if hasattr(oldnew, "__singleton_wrapper__"): 

1074 oldnew = oldnew.__wrapped__ 

1075 

1076 oldinit = newClass.__init__ 

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

1078 oldinit = oldinit.__wrapped__ 

1079 

1080 @wraps(oldnew) 

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

1082 with cls.__singletonInstanceCond__: 

1083 if cls.__singletonInstanceCache__ is None: 

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

1085 cls.__singletonInstanceCache__ = obj 

1086 else: 

1087 obj = cls.__singletonInstanceCache__ 

1088 

1089 return obj 

1090 

1091 @wraps(oldinit) 

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

1093 cls = self.__class__ 

1094 cv = cls.__singletonInstanceCond__ 

1095 with cv: 

1096 if cls.__singletonInstanceInit__: 

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

1098 cls.__singletonInstanceInit__ = False 

1099 cv.notify_all() 

1100 elif args or kwargs: 

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

1102 else: 

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

1104 cv.wait() 

1105 

1106 singleton_new.__singleton_wrapper__ = True 

1107 singleton_init.__singleton_wrapper__ = True 

1108 

1109 newClass.__new__ = singleton_new 

1110 newClass.__init__ = singleton_init 

1111 newClass.__singletonInstanceCond__ = Condition() 

1112 newClass.__singletonInstanceInit__ = True 

1113 newClass.__singletonInstanceCache__ = None 

1114 return True 

1115 

1116 return False 

1117 

1118 @classmethod 

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

1120 """ 

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

1122 

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

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

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

1126 """ 

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

1128 if len(newClass.__abstractMethods__) > 0: 

1129 oldnew = newClass.__new__ 

1130 if hasattr(oldnew, "__raises_abstract_class_error__"): 

1131 oldnew = oldnew.__wrapped__ 

1132 

1133 @wraps(oldnew) 

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

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

1136 

1137 abstract_new.__raises_abstract_class_error__ = True 

1138 

1139 newClass.__new__ = abstract_new 

1140 return True 

1141 

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

1143 else: 

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

1145 try: 

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

1147 origNew = newClass.__new__.__wrapped__ 

1148 

1149 # WORKAROUND: __new__ checks tp_new and implements different behavior 

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

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

1152 @wraps(object.__new__) 

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

1154 return object.__new__(inst) 

1155 

1156 newClass.__new__ = wrapped_new 

1157 else: 

1158 newClass.__new__ = origNew 

1159 elif newClass.__new__.__isSingleton__: 

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

1161 except AttributeError as ex: 

1162 # WORKAROUND: 

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

1164 try: 

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

1166 raise ex 

1167 except AttributeError: 

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

1169 raise ex 

1170 

1171 return False 

1172 

1173 # Additional properties and methods on a class 

1174 @readonly 

1175 def HasClassAttributes(self) -> bool: 

1176 """ 

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

1178 

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

1180 """ 

1181 try: 

1182 return len(self.__pyattr__) > 0 

1183 except AttributeError: 

1184 return False 

1185 

1186 @readonly 

1187 def HasMethodAttributes(self) -> bool: 

1188 """ 

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

1190 

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

1192 """ 

1193 try: 

1194 return len(self.__methodsWithAttributes__) > 0 

1195 except AttributeError: 

1196 return False 

1197 

1198 

1199@export 

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

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