Coverage for pyfields/autofields_.py: 82%

205 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-11-06 16:35 +0000

1# Authors: Sylvain MARIE <sylvain.marie@se.com> 

2# + All contributors to <https://github.com/smarie/python-pyfields> 

3# 

4# License: 3-clause BSD, <https://github.com/smarie/python-pyfields/blob/master/LICENSE> 

5import sys 

6from copy import deepcopy 

7from inspect import isdatadescriptor, ismethoddescriptor 

8 

9try: 

10 from typing import Union, Callable, Type, Any, TypeVar, Tuple, Iterable 

11 DecoratedClass = TypeVar("DecoratedClass", bound=Type[Any]) 

12except ImportError: 

13 pass 

14 

15 

16from .core import Field, field 

17from .init_makers import make_init as mkinit 

18from .helpers import copy_value, get_fields 

19 

20 

21PY36 = sys.version_info >= (3, 6) 

22DEFAULT_EXCLUDED = ('_abc_impl',) 

23 

24 

25def _make_init(cls): 

26 """Utility method used in autofields and autoclass to create the constructor based on the class fields""" 

27 if "__init__" not in cls.__dict__: 

28 new_init = mkinit() 

29 cls.__init__ = new_init 

30 # attach explicitly to the class so that the descriptor is correctly completed. 

31 new_init.__set_name__(cls, '__init__') 

32 

33 

34def autofields(check_types=False, # type: Union[bool, DecoratedClass] 

35 include_upper=False, # type: bool 

36 include_dunder=False, # type: bool 

37 exclude=DEFAULT_EXCLUDED, # type: Iterable[str] 

38 make_init=True, # type: bool 

39 ): 

40 # type: (...) -> Union[Callable[[DecoratedClass], DecoratedClass], DecoratedClass] 

41 """ 

42 Decorator to automatically create fields and constructor on a class. 

43 

44 When a class is decorated with `@autofields`, all of its members are automatically transformed to fields. 

45 More precisely: members that only contain a type annotation become mandatory fields, while members that contain a 

46 value (with or without type annotation) become optional fields with a `copy_value` default_factory. 

47 

48 By default, the following members are NOT transformed into fields: 

49 

50 * members with upper-case names. This is because this kind of name formatting usually denotes class constants. They 

51 can be transformed to fields by setting `include_upper=True`. 

52 * members with dunder-like names. They can be included using `include_dunder=True`. Note that reserved python 

53 dunder names such as `__name__`, `__setattr__`, etc. can not be transformed to fields, even when 

54 `include_dunder=True`. 

55 * members that are classes or methods defined in the class (that is, where their `.__name__` is the same name than 

56 the member name). 

57 * members that are already fields. Therefore you can continue to use `field()` on certain members explicitly if 

58 you need to add custom validators, converters, etc. 

59 

60 All created fields have their `type_hint` filled with the type hint associated with the member, and have 

61 `check_type=False` by default. This can be changed by setting `check_types=True`. 

62 

63 Finally, in addition, an init method (constructor) is generated for the class, using `make_init()`. This may be 

64 disabled by setting `make_init=False`.. 

65 

66 >>> import sys, pytest 

67 >>> if sys.version_info < (3, 6): pytest.skip("doctest skipped for python < 3.6") 

68 ... 

69 >>> @autofields 

70 ... class Pocket: 

71 ... SENTENCE = "hello world" # uppercase: not a field 

72 ... size: int # mandatory field 

73 ... items = [] # optional - default value will be a factory 

74 ... 

75 >>> p = Pocket(size=10) 

76 >>> p.items 

77 [] 

78 >>> Pocket(size=10, SENTENCE="hello") 

79 Traceback (most recent call last): 

80 ... 

81 TypeError: __init__() got an unexpected keyword argument 'SENTENCE' 

82 

83 

84 :param check_types: boolean flag (default: `False`) indicating the value of `check_type` for created fields. Note 

85 that the type hint of each created field is copied from the type hint of the member it originates from. 

86 :param include_upper: boolean flag (default: `False`) indicating whether upper-case class members should be also 

87 transformed to fields (usually such names are reserved for class constants, not for fields). 

88 :param include_dunder: boolean flag (default: `False`) indicating whether dunder-named class members should be also 

89 transformed to fields. Note that even if you set this to True, members with reserved python dunder names will 

90 not be transformed. See `is_reserved_dunder` for the list of reserved names. 

91 :param exclude: a tuple of field names that should be excluded from automatic creation. By default this is set to 

92 `DEFAULT_EXCLUDED`, which eliminates fields created by `ABC`. 

93 :param make_init: boolean flag (default: `True`) indicating whether a constructor should be created for the class if 

94 no `__init__` method is present. Such constructor will be created using `__init__ = make_init()`. 

95 :return: 

96 """ 

97 def _autofields(cls): 

98 NO_DEFAULT = object() 

99 

100 try: 

101 # Are type hints present ? 

102 # note: since this attribute can be inherited, we get the own attribute only 

103 # cls_annotations = cls.__annotations__ 

104 cls_annotations = getownattr(cls, "__annotations__") 

105 except AttributeError: 

106 # No type hints: shortcut. note: do not return a generator since we'll modify __dict__ in the loop after 

107 members_defs = tuple((k, None, v) for k, v in cls.__dict__.items()) 

108 else: 

109 # Fill the list of potential fields definitions 

110 members_defs = [] 

111 cls_dict = cls.__dict__ 

112 

113 if not PY36: 113 ↛ 115line 113 didn't jump to line 115, because the condition on line 113 was never true

114 # Is this even possible ? does not seem so. Raising an error until this is reported 

115 raise ValueError("Unsupported case: `__annotations__` is present while python is < 3.6 - please report") 

116 # # dont care about the order, it is not preserved 

117 # # -- fields with type hint 

118 # for member_name, type_hint in cls_annotations.items(): 

119 # members_defs.append((member_name, type_hint, cls_dict.get(member_name, NO_DEFAULT))) 

120 # 

121 # # -- fields without type hint 

122 # members_with_type = set(cls_annotations.keys()) 

123 # for member_name, default_value in cls_dict.items(): 

124 # if member_name not in members_with_type: 

125 # members_defs.append((member_name, None, default_value)) 

126 # 

127 else: 

128 # create a list of members with consistent order 

129 members_with_type_and_value = set(cls_annotations.keys()).intersection(cls_dict.keys()) 

130 

131 in_types = [name for name in cls_annotations if name in members_with_type_and_value] 

132 in_values = [name for name in cls_dict if name in members_with_type_and_value] 

133 assert in_types == in_values 

134 

135 def t_gen(): 

136 """ generator used to fill the definitions for members only in annotations dict """ 

137 next_stop_name = yield 

138 for _name, _type_hint in cls_annotations.items(): 

139 if _name != next_stop_name: 

140 members_defs.append((_name, _type_hint, NO_DEFAULT)) 

141 else: 

142 next_stop_name = yield 

143 

144 def v_gen(): 

145 """ generator used to fill the definitions for members only in the values dict """ 

146 next_stop_name, next_stop_type_hint = yield 

147 for _name, _default_value in cls_dict.items(): 

148 if _name != next_stop_name: 

149 members_defs.append((_name, None, _default_value)) 

150 else: 

151 members_defs.append((_name, next_stop_type_hint, _default_value)) 

152 next_stop_name, next_stop_type_hint = yield 

153 

154 types_gen = t_gen() 

155 types_gen.send(None) 

156 values_gen = v_gen() 

157 values_gen.send(None) 

158 for common_name in in_types: 

159 types_gen.send(common_name) 

160 values_gen.send((common_name, cls_annotations[common_name])) 

161 # last one 

162 try: 

163 types_gen.send(None) 

164 except StopIteration: 

165 pass 

166 try: 

167 values_gen.send((None, None)) 

168 except StopIteration: 

169 pass 

170 

171 # Main loop : for each member, possibly create a field() 

172 for member_name, type_hint, default_value in members_defs: 

173 if member_name in exclude: 

174 # excluded explicitly 

175 continue 

176 elif not include_upper and member_name == member_name.upper(): 

177 # excluded uppercase 

178 continue 

179 elif (include_dunder and is_reserved_dunder(member_name)) \ 

180 or is_dunder(member_name): 

181 # excluded dunder 

182 continue 

183 elif isinstance(default_value, Field): 

184 # already a field, no need to create 

185 # but in order to preserve relative order with generated fields, detach and attach again 

186 try: 

187 delattr(cls, member_name) 

188 except AttributeError: 

189 pass 

190 setattr(cls, member_name, default_value) 

191 continue 

192 elif isinstance(default_value, property) or isdatadescriptor(default_value) \ 

193 or ismethoddescriptor(default_value): 

194 # a property or a data or non-data descriptor > exclude 

195 continue 

196 elif (isinstance(default_value, type) or callable(default_value)) \ 

197 and getattr(default_value, '__name__', None) == member_name: 

198 # a function/class defined in the class > exclude 

199 continue 

200 else: 

201 # Create a field !! 

202 need_to_check_type = check_types and (type_hint is not None) 

203 if default_value is NO_DEFAULT: 

204 # mandatory field 

205 new_field = field(check_type=need_to_check_type) 

206 else: 

207 # optional field : copy the default value by default 

208 try: 

209 # autocheck: make sure that we will be able to create copies later 

210 deepcopy(default_value) 

211 except Exception as e: 

212 raise ValueError("The provided default value for field %r=%r can not be deep-copied: " 

213 "caught error %r" % (member_name, default_value, e)) 

214 new_field = field(check_type=need_to_check_type, 

215 default_factory=copy_value(default_value, autocheck=False)) 

216 

217 # Attach the newly created field to the class. Delete attr first so that order is preserved 

218 # even if one of them had only an annotation. 

219 try: 

220 delattr(cls, member_name) 

221 except AttributeError: 

222 pass 

223 setattr(cls, member_name, new_field) 

224 new_field.set_as_cls_member(cls, member_name, type_hint=type_hint) 

225 

226 # Finally, make init if not already explicitly present 

227 if make_init: 

228 _make_init(cls) 

229 

230 return cls 

231 # end of _autofields(cls) 

232 

233 # Main logic of autofield(**kwargs) 

234 if check_types is not True and check_types is not False and isinstance(check_types, type): 

235 # called without arguments @autofields: check_types is the decorated class 

236 assert include_upper is False 

237 assert include_dunder is False 

238 # use the parameter and use the correct check_types default value now 

239 _cls = check_types 

240 check_types = False # <-- important: variable is in the local context of _autofields 

241 return _autofields(cls=_cls) 

242 else: 

243 # called with arguments @autofields(...): return the decorator 

244 return _autofields 

245 

246 

247def is_dunder(name): 

248 return len(name) >= 4 and name.startswith('__') and name.endswith('__') 

249 

250 

251def is_reserved_dunder(name): 

252 return name in ('__doc__', '__name__', '__qualname__', '__module__', '__code__', '__globals__', 

253 '__dict__', '__closure__', '__annotations__') # '__defaults__', '__kwdefaults__') 

254 

255 

256_dict, _hash = dict, hash 

257"""Aliases for autoclass body""" 

258 

259 

260def autoclass( 

261 # --- autofields 

262 fields=True, # type: Union[bool, DecoratedClass] 

263 typecheck=False, # type: bool 

264 # --- constructor 

265 init=True, # type: bool 

266 # --- class methods 

267 dict=True, # type: bool 

268 dict_public_only=True, # type: bool 

269 repr=True, # type: bool 

270 repr_curly_mode=False, # type: bool 

271 repr_public_only=True, # type: bool 

272 eq=True, # type: bool 

273 eq_public_only=False, # type: bool 

274 hash=True, # type: bool 

275 hash_public_only=False, # type: bool 

276 # --- advanced 

277 af_include_upper=False, # type: bool 

278 af_include_dunder=False, # type: bool 

279 af_exclude=DEFAULT_EXCLUDED, # type: Iterable[str] 

280 ac_include=None, # type: Union[str, Tuple[str]] 

281 ac_exclude=None, # type: Union[str, Tuple[str]] 

282): 

283 """ 

284 A decorator to automate many things at once for your class. 

285 

286 First if `fields=True` (default) it executes `@autofields` to generate fields from attribute defined at class 

287 level. 

288 

289 - you can include attributes with dunder names or uppercase names with `af_include_dunder` and 

290 `af_include_upper` respectively 

291 - you can enable type checking on all fields at once by setting `check_types=True` 

292 - the constructor is not generated at this stage 

293 

294 Then it generates methods for the class: 

295 

296 - if `init=True` (default) it generates the constructor based on all fields present, using `make_init()`. 

297 - if `dict=True` (default) it generates `to_dict` and `from_dict` methods. Only public fields are represented in 

298 `to_dict`, you can change this with `dict_public_only=False`. 

299 - if `repr=True` (default) it generates a `__repr__` method. Only public fields are represented, you can change 

300 this with `repr_public_only=False`. 

301 - if `eq=True` (default) it generates an `__eq__` method, so that instances can be compared to other instances and 

302 to dicts. All fields are compared by default, you can change this with `eq_public_only=True`. 

303 - if `hash=True` (default) it generates an `__hash__` method, so that instances can be inserted in sets or dict 

304 keys. All fields are hashed by default, you can change this with `hash_public_only=True`. 

305 

306 You can specify an explicit list of fields to include or exclude in the dict/repr/eq/hash methods with the 

307 `ac_include` and `ac_exclude` parameters. 

308 

309 Note that this decorator is similar to the [autoclass library](https://smarie.github.io/python-autoclass/) but is 

310 reimplemented here. In particular the parameter names and dictionary behaviour are different. 

311 

312 :param fields: boolean flag (default: True) indicating whether to create fields automatically. See `@autofields` 

313 for details 

314 :param typecheck: boolean flag (default: False) used when fields=True indicating the value of `check_type` 

315 for created fields. Note that the type hint of each created field is copied from the type hint of the member it 

316 originates from. 

317 :param init: boolean flag (default: True) indicating whether a constructor should be created for the class if 

318 no `__init__` method is already present. Such constructor will be created using `__init__ = make_init()`. 

319 This is the same behaviour than `make_init` in `@autofields`. Note that this is *not* automatically disabled if 

320 you set `fields=False`. 

321 :param dict: a boolean to automatically create `cls.from_dict(dct)` and `obj.to_dict()` methods on the class 

322 (default: True). 

323 :param dict_public_only: a boolean (default: True) to indicate if only public fields should be 

324 exposed in the dictionary view created by `to_dict` when `dict=True`. 

325 :param repr: a boolean (default: True) to indicate if `__repr__` and `__str__` should be created for the class if 

326 not explicitly present. 

327 :param repr_curly_mode: a boolean (default: False) to turn on an alternate string representation when `repr=True`, 

328 using curly braces. 

329 :param repr_public_only: a boolean (default: True) to indicate if only public fields should be 

330 exposed in the string representation when `repr=True`. 

331 :param eq: a boolean (default: True) to indicate if `__eq__` should be created for the class if not explicitly 

332 present. 

333 :param eq_public_only: a boolean (default: False) to indicate if only public fields should be 

334 compared in the equality method created when `eq=True`. 

335 :param hash: a boolean (default: True) to indicate if `__hash__` should be created for the class if not explicitly 

336 present. 

337 :param hash_public_only: a boolean (default: False) to indicate if only public fields should be 

338 hashed in the hash method created when `hash=True`. 

339 :param af_include_upper: boolean flag (default: False) used when autofields=True indicating whether 

340 upper-case class members should be also transformed to fields (usually such names are reserved for class 

341 constants, not for fields). 

342 :param af_include_dunder: boolean flag (default: False) used when autofields=True indicating whether 

343 dunder-named class members should be also transformed to fields. Note that even if you set this to True, 

344 members with reserved python dunder names will not be transformed. See `is_reserved_dunder` for the list of 

345 reserved names. 

346 :param af_exclude: a tuple of explicit attribute names to exclude from automatic fields creation. See 

347 `@autofields(exclude=...)` for details. 

348 :param ac_include: a tuple of explicit attribute names to include in dict/repr/eq/hash (None means all) 

349 :param ac_exclude: a tuple of explicit attribute names to exclude in dict/repr/eq/hash. In such case, 

350 include should be None. 

351 :return: 

352 """ 

353 if not fields and (af_include_dunder or af_include_upper or typecheck): 353 ↛ 354line 353 didn't jump to line 354, because the condition on line 353 was never true

354 raise ValueError("Not able to set af_include_dunder or af_include_upper or typecheck when fields=False") 

355 

356 # switch between args and actual symbols for readability 

357 dict_on = dict 

358 dict = _dict 

359 hash_on = hash 

360 hash = _hash 

361 

362 # Create the decorator function 

363 def _apply_decorator(cls): 

364 

365 # create fields automatically 

366 if fields: 366 ↛ 371line 366 didn't jump to line 371, because the condition on line 366 was never false

367 cls = autofields(check_types=typecheck, include_upper=af_include_upper, 

368 exclude=af_exclude, include_dunder=af_include_dunder, make_init=False)(cls) 

369 

370 # make init if not already explicitly present 

371 if init: 371 ↛ 375line 371 didn't jump to line 375, because the condition on line 371 was never false

372 _make_init(cls) 

373 

374 # list all fields 

375 all_pyfields = get_fields(cls) 

376 if len(all_pyfields) == 0: 376 ↛ 377line 376 didn't jump to line 377, because the condition on line 376 was never true

377 raise ValueError("No fields detected on class %s (including inherited ones)" % cls) 

378 

379 # filter selected 

380 all_names = tuple(f.name for f in all_pyfields) 

381 selected_names = filter_names(all_names, include=ac_include, exclude=ac_exclude, caller="@autoclass") 

382 public_selected_names = tuple(n for n in selected_names if not n.startswith('_')) 

383 

384 # to/from dict 

385 if dict_on: 

386 dict_names = public_selected_names if dict_public_only else selected_names 

387 if "to_dict" not in cls.__dict__: 387 ↛ 394line 387 didn't jump to line 394, because the condition on line 387 was never false

388 

389 def to_dict(self): 

390 """ Generated by @pyfields.autoclass based on the class fields """ 

391 return {n: getattr(self, n) for n in dict_names} 

392 

393 cls.to_dict = to_dict 

394 if "from_dict" not in cls.__dict__: 394 ↛ 403line 394 didn't jump to line 403, because the condition on line 394 was never false

395 

396 def from_dict(cls, dct): 

397 """ Generated by @pyfields.autoclass """ 

398 return cls(**dct) 

399 

400 cls.from_dict = classmethod(from_dict) 

401 

402 # __str__ and __repr__ 

403 if repr: 403 ↛ 423line 403 didn't jump to line 423, because the condition on line 403 was never false

404 repr_names = public_selected_names if repr_public_only else selected_names 

405 if not repr_curly_mode: # default 405 ↛ 412line 405 didn't jump to line 412, because the condition on line 405 was never false

406 

407 def __repr__(self): 

408 """ Generated by @pyfields.autoclass based on the class fields """ 

409 return '%s(%s)' % (self.__class__.__name__, 

410 ', '.join('%s=%r' % (k, getattr(self, k)) for k in repr_names)) 

411 else: 

412 def __repr__(self): 

413 """ Generated by @pyfields.autoclass based on the class fields """ 

414 return '%s(**{%s})' % (self.__class__.__name__, 

415 ', '.join('%r: %r' % (k, getattr(self, k)) for k in repr_names)) 

416 

417 if "__repr__" not in cls.__dict__: 417 ↛ 419line 417 didn't jump to line 419, because the condition on line 417 was never false

418 cls.__repr__ = __repr__ 

419 if "__str__" not in cls.__dict__: 419 ↛ 423line 419 didn't jump to line 423, because the condition on line 419 was never false

420 cls.__str__ = __repr__ 

421 

422 # __eq__ 

423 if eq: 423 ↛ 457line 423 didn't jump to line 457, because the condition on line 423 was never false

424 eq_names = public_selected_names if eq_public_only else selected_names 

425 

426 def __eq__(self, other): 

427 """ Generated by @pyfields.autoclass based on the class fields """ 

428 if isinstance(other, dict): 

429 # comparison with dicts only when a to_dict method is available 

430 try: 

431 _self_to_dict = self.to_dict 

432 except AttributeError: 

433 return False 

434 else: 

435 return _self_to_dict() == other 

436 elif isinstance(self, other.__class__): 436 ↛ 446line 436 didn't jump to line 446, because the condition on line 436 was never false

437 # comparison with objects of the same class or a parent 

438 try: 

439 for att_name in eq_names: 

440 if getattr(self, att_name) != getattr(other, att_name): 

441 return False 

442 except AttributeError: 

443 return False 

444 else: 

445 return True 

446 elif isinstance(other, self.__class__): 

447 # other is a subtype: call method on other 

448 return other.__eq__(self) # same as NotImplemented ? 

449 else: 

450 # classes are not related: False 

451 return False 

452 

453 if "__eq__" not in cls.__dict__: 453 ↛ 457line 453 didn't jump to line 457, because the condition on line 453 was never false

454 cls.__eq__ = __eq__ 

455 

456 # __hash__ 

457 if hash_on: 457 ↛ 474line 457 didn't jump to line 474, because the condition on line 457 was never false

458 hash_names = public_selected_names if hash_public_only else selected_names 

459 

460 def __hash__(self): 

461 """ Generated by @autoclass. Implements the __hash__ method by hashing a tuple of field values """ 

462 

463 # note: Should we prepend a unique hash for the class as `attrs` does ? 

464 # return hash(tuple([type(self)] + [getattr(self, att_name) for att_name in added])) 

465 # > No, it seems more intuitive to not do that. 

466 # Warning: the consequence is that instances of subtypes will have the same hash has instance of their 

467 # parent class if they have all the same attribute values 

468 

469 return hash(tuple(getattr(self, att_name) for att_name in hash_names)) 

470 

471 if "__hash__" not in cls.__dict__: 471 ↛ 474line 471 didn't jump to line 474, because the condition on line 471 was never false

472 cls.__hash__ = __hash__ 

473 

474 return cls 

475 

476 # Apply: Decorator vs decorator factory logic 

477 if isinstance(fields, type): 

478 # called without parenthesis: directly apply decorator on first argument 

479 cls = fields 

480 fields = True # set it back to its default value 

481 return _apply_decorator(cls) 

482 else: 

483 # called with parenthesis: return a decorator function 

484 return _apply_decorator 

485 

486 

487def filter_names(all_names, 

488 include=None, # type: Union[str, Tuple[str]] 

489 exclude=None, # type: Union[str, Tuple[str]] 

490 caller="" # type: str 

491 ): 

492 # type: (...) -> Iterable[str] 

493 """ 

494 Common validator for include and exclude arguments 

495 

496 :param all_names: 

497 :param include: 

498 :param exclude: 

499 :param caller: 

500 :return: 

501 """ 

502 if include is not None and exclude is not None: 502 ↛ 503line 502 didn't jump to line 503, because the condition on line 502 was never true

503 raise ValueError("Only one of 'include' or 'exclude' argument should be provided.") 

504 

505 # check that include/exclude don't contain names that are incorrect 

506 selected_names = all_names 

507 if include is not None: 507 ↛ 508line 507 didn't jump to line 508, because the condition on line 507 was never true

508 if exclude is not None: 

509 raise ValueError('Only one of \'include\' or \'exclude\' argument should be provided.') 

510 

511 # get the selected names and check that all names in 'include' are actually valid names 

512 included = (include,) if isinstance(include, str) else tuple(include) 

513 incorrect = set(included) - set(all_names) 

514 if len(incorrect) > 0: 

515 raise ValueError("`%s` definition exception: `include` contains %r that is/are " 

516 "not part of %r" % (caller, incorrect, all_names)) 

517 selected_names = included 

518 

519 elif exclude is not None: 519 ↛ 520line 519 didn't jump to line 520, because the condition on line 519 was never true

520 excluded_set = {exclude} if isinstance(exclude, str) else set(exclude) 

521 incorrect = excluded_set - set(all_names) 

522 if len(incorrect) > 0: 

523 raise ValueError("`%s` definition exception: exclude contains %r that is/are " 

524 "not part of %r" % (caller, incorrect, all_names)) 

525 selected_names = tuple(n for n in all_names if n not in excluded_set) 

526 

527 return selected_names 

528 

529 

530# def method_already_there(cls, 

531# method_name, # type: str 

532# this_class_only=False # type: bool 

533# ): 

534# # type: (...) -> bool 

535# """ 

536# Returns True if method `method_name` is already implemented by object_type, that is, its implementation differs 

537# from the one in `object`. 

538# 

539# :param cls: 

540# :param method_name: 

541# :param this_class_only: 

542# :return: 

543# """ 

544# if this_class_only: 

545# return method_name in cls.__dict__ # or vars(cls) 

546# else: 

547# method = getattr(cls, method_name, None) 

548# return method is not None and method is not getattr(object, method_name, None) 

549 

550 

551def getownattr(cls, attrib_name): 

552 """ 

553 Return the value of `cls.<attrib_name>` if it is defined in the class (and not inherited). 

554 If the attribute is not present or is inherited, an `AttributeError` is raised. 

555 

556 >>> class A(object): 

557 ... a = 1 

558 >>> 

559 >>> class B(A): 

560 ... pass 

561 >>> 

562 >>> getownattr(A, 'a') 

563 1 

564 >>> getownattr(A, 'unknown') 

565 Traceback (most recent call last): 

566 ... 

567 AttributeError: type object 'A' has no attribute 'unknown' 

568 >>> getownattr(B, 'a') 

569 Traceback (most recent call last): 

570 ... 

571 AttributeError: type object 'B' has no directly defined attribute 'a' 

572 

573 """ 

574 attr = getattr(cls, attrib_name) 

575 

576 for base_cls in cls.__mro__[1:]: 

577 a = getattr(base_cls, attrib_name, None) 

578 if attr is a: 

579 raise AttributeError("type object %r has no directly defined attribute %r" % (cls.__name__, attrib_name)) 

580 

581 return attr