characteristic.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. from __future__ import absolute_import, division, print_function
  2. """
  3. Python attributes without boilerplate.
  4. """
  5. import sys
  6. import warnings
  7. __version__ = "14.1.0"
  8. __author__ = "Hynek Schlawack"
  9. __license__ = "MIT"
  10. __copyright__ = "Copyright 2014 Hynek Schlawack"
  11. __all__ = [
  12. "Attribute",
  13. "NOTHING",
  14. "attributes",
  15. "immutable",
  16. "strip_leading_underscores",
  17. "with_cmp",
  18. "with_init",
  19. "with_repr",
  20. ]
  21. class _Nothing(object):
  22. """
  23. Sentinel class to indicate the lack of a value when ``None`` is ambiguous.
  24. .. versionadded:: 14.0
  25. """
  26. def __repr__(self):
  27. return "NOTHING"
  28. NOTHING = _Nothing()
  29. """
  30. Sentinel to indicate the lack of a value when ``None`` is ambiguous.
  31. .. versionadded:: 14.0
  32. """
  33. def strip_leading_underscores(attribute_name):
  34. """
  35. Strip leading underscores from *attribute_name*.
  36. Used by default by the ``init_aliaser`` argument of :class:`Attribute`.
  37. :param attribute_name: The original attribute name to mangle.
  38. :type attribute_name: str
  39. :rtype: str
  40. """
  41. return attribute_name.lstrip("_")
  42. class Attribute(object):
  43. """
  44. A representation of an attribute.
  45. In the simplest case, it only consists of a name but more advanced
  46. properties like default values are possible too.
  47. All attributes on the Attribute class are *read-only*.
  48. :param name: Name of the attribute.
  49. :type name: str
  50. :param exclude_from_cmp: Ignore attribute in :func:`with_cmp`.
  51. :type exclude_from_cmp: bool
  52. :param exclude_from_init: Ignore attribute in :func:`with_init`.
  53. :type exclude_from_init: bool
  54. :param exclude_from_repr: Ignore attribute in :func:`with_repr`.
  55. :type exclude_from_repr: bool
  56. :param exclude_from_immutable: Ignore attribute in :func:`immutable`.
  57. :type exclude_from_immutable: bool
  58. :param default_value: A value that is used whenever this attribute isn't
  59. passed as an keyword argument to a class that is decorated using
  60. :func:`with_init` (or :func:`attributes` with
  61. ``apply_with_init=True``).
  62. Therefore, setting this makes an attribute *optional*.
  63. Since a default value of `None` would be ambiguous, a special sentinel
  64. :data:`NOTHING` is used. Passing it means the lack of a default value.
  65. :param default_factory: A factory that is used for generating default
  66. values whenever this attribute isn't passed as an keyword
  67. argument to a class that is decorated using :func:`with_init` (or
  68. :func:`attributes` with ``apply_with_init=True``).
  69. Therefore, setting this makes an attribute *optional*.
  70. :type default_factory: callable
  71. :param instance_of: If used together with :func:`with_init` (or
  72. :func:`attributes` with ``apply_with_init=True``), the passed value is
  73. checked whether it's an instance of the type passed here. The
  74. initializer then raises :exc:`TypeError` on mismatch.
  75. :type instance_of: type
  76. :param init_aliaser: A callable that is invoked with the name of the
  77. attribute and whose return value is used as the keyword argument name
  78. for the ``__init__`` created by :func:`with_init` (or
  79. :func:`attributes` with ``apply_with_init=True``). Uses
  80. :func:`strip_leading_underscores` by default to change ``_foo`` to
  81. ``foo``. Set to ``None`` to disable aliasing.
  82. :type init_aliaser: callable
  83. :raises ValueError: If both ``default_value`` and ``default_factory`` have
  84. been passed.
  85. .. versionadded:: 14.0
  86. """
  87. __slots__ = [
  88. "name", "exclude_from_cmp", "exclude_from_init", "exclude_from_repr",
  89. "exclude_from_immutable", "default_value", "default_factory",
  90. "instance_of", "init_aliaser", "_default", "_kw_name",
  91. ]
  92. def __init__(self,
  93. name,
  94. exclude_from_cmp=False,
  95. exclude_from_init=False,
  96. exclude_from_repr=False,
  97. exclude_from_immutable=False,
  98. default_value=NOTHING,
  99. default_factory=None,
  100. instance_of=None,
  101. init_aliaser=strip_leading_underscores):
  102. if (
  103. default_value is not NOTHING
  104. and default_factory is not None
  105. ):
  106. raise ValueError(
  107. "Passing both default_value and default_factory is "
  108. "ambiguous."
  109. )
  110. self.name = name
  111. self.exclude_from_cmp = exclude_from_cmp
  112. self.exclude_from_init = exclude_from_init
  113. self.exclude_from_repr = exclude_from_repr
  114. self.exclude_from_immutable = exclude_from_immutable
  115. self.default_value = default_value
  116. self.default_factory = default_factory
  117. if default_value is not NOTHING:
  118. self._default = default_value
  119. elif default_factory is None:
  120. self._default = NOTHING
  121. self.instance_of = instance_of
  122. self.init_aliaser = init_aliaser
  123. if init_aliaser is not None:
  124. self._kw_name = init_aliaser(name)
  125. else:
  126. self._kw_name = name
  127. def __getattr__(self, name):
  128. """
  129. If no value has been set to _default, we need to call a factory.
  130. """
  131. if name == "_default" and self.default_factory:
  132. return self.default_factory()
  133. else:
  134. raise AttributeError
  135. def __repr__(self):
  136. return (
  137. "<Attribute(name={name!r}, exclude_from_cmp={exclude_from_cmp!r}, "
  138. "exclude_from_init={exclude_from_init!r}, exclude_from_repr="
  139. "{exclude_from_repr!r}, exclude_from_immutable="
  140. "{exclude_from_immutable!r}, default_value={default_value!r}, "
  141. "default_factory={default_factory!r}, instance_of={instance_of!r},"
  142. " init_aliaser={init_aliaser!r})>"
  143. ).format(
  144. name=self.name, exclude_from_cmp=self.exclude_from_cmp,
  145. exclude_from_init=self.exclude_from_init,
  146. exclude_from_repr=self.exclude_from_repr,
  147. exclude_from_immutable=self.exclude_from_immutable,
  148. default_value=self.default_value,
  149. default_factory=self.default_factory, instance_of=self.instance_of,
  150. init_aliaser=self.init_aliaser,
  151. )
  152. def _ensure_attributes(attrs, defaults):
  153. """
  154. Return a list of :class:`Attribute` generated by creating new instances for
  155. all non-Attributes.
  156. """
  157. if defaults is not NOTHING:
  158. defaults = defaults or {}
  159. warnings.warn(
  160. "`defaults` has been deprecated in 14.0, please use the "
  161. "`Attribute` class instead.",
  162. DeprecationWarning,
  163. stacklevel=3,
  164. )
  165. else:
  166. defaults = {}
  167. rv = []
  168. for attr in attrs:
  169. if isinstance(attr, Attribute):
  170. if defaults != {}:
  171. raise ValueError(
  172. "Mixing of the 'defaults' keyword argument and passing "
  173. "instances of Attribute for 'attrs' is prohibited. "
  174. "Please don't use 'defaults' anymore, it has been "
  175. "deprecated in 14.0."
  176. )
  177. else:
  178. rv.append(attr)
  179. else:
  180. rv.append(
  181. Attribute(
  182. attr,
  183. init_aliaser=None,
  184. default_value=defaults.get(attr, NOTHING)
  185. )
  186. )
  187. return rv
  188. def with_cmp(attrs):
  189. """
  190. A class decorator that adds comparison methods based on *attrs*.
  191. For that, each class is treated like a ``tuple`` of the values of *attrs*.
  192. But only instances of *identical* classes are compared!
  193. :param attrs: Attributes to work with.
  194. :type attrs: :class:`list` of :class:`str` or :class:`Attribute`\ s.
  195. """
  196. def attrs_to_tuple(obj):
  197. """
  198. Create a tuple of all values of *obj*'s *attrs*.
  199. """
  200. return tuple(getattr(obj, a.name) for a in attrs)
  201. def eq(self, other):
  202. """
  203. Automatically created by characteristic.
  204. """
  205. if other.__class__ is self.__class__:
  206. return attrs_to_tuple(self) == attrs_to_tuple(other)
  207. else:
  208. return NotImplemented
  209. def ne(self, other):
  210. """
  211. Automatically created by characteristic.
  212. """
  213. result = eq(self, other)
  214. if result is NotImplemented:
  215. return NotImplemented
  216. else:
  217. return not result
  218. def lt(self, other):
  219. """
  220. Automatically created by characteristic.
  221. """
  222. if other.__class__ is self.__class__:
  223. return attrs_to_tuple(self) < attrs_to_tuple(other)
  224. else:
  225. return NotImplemented
  226. def le(self, other):
  227. """
  228. Automatically created by characteristic.
  229. """
  230. if other.__class__ is self.__class__:
  231. return attrs_to_tuple(self) <= attrs_to_tuple(other)
  232. else:
  233. return NotImplemented
  234. def gt(self, other):
  235. """
  236. Automatically created by characteristic.
  237. """
  238. if other.__class__ is self.__class__:
  239. return attrs_to_tuple(self) > attrs_to_tuple(other)
  240. else:
  241. return NotImplemented
  242. def ge(self, other):
  243. """
  244. Automatically created by characteristic.
  245. """
  246. if other.__class__ is self.__class__:
  247. return attrs_to_tuple(self) >= attrs_to_tuple(other)
  248. else:
  249. return NotImplemented
  250. def hash_(self):
  251. """
  252. Automatically created by characteristic.
  253. """
  254. return hash(attrs_to_tuple(self))
  255. def wrap(cl):
  256. cl.__eq__ = eq
  257. cl.__ne__ = ne
  258. cl.__lt__ = lt
  259. cl.__le__ = le
  260. cl.__gt__ = gt
  261. cl.__ge__ = ge
  262. cl.__hash__ = hash_
  263. return cl
  264. attrs = [a
  265. for a in _ensure_attributes(attrs, NOTHING)
  266. if a.exclude_from_cmp is False]
  267. return wrap
  268. def with_repr(attrs):
  269. """
  270. A class decorator that adds a human readable ``__repr__`` method to your
  271. class using *attrs*.
  272. :param attrs: Attributes to work with.
  273. :type attrs: ``list`` of :class:`str` or :class:`Attribute`\ s.
  274. """
  275. def repr_(self):
  276. """
  277. Automatically created by characteristic.
  278. """
  279. return "<{0}({1})>".format(
  280. self.__class__.__name__,
  281. ", ".join(a.name + "=" + repr(getattr(self, a.name))
  282. for a in attrs)
  283. )
  284. def wrap(cl):
  285. cl.__repr__ = repr_
  286. return cl
  287. attrs = [a
  288. for a in _ensure_attributes(attrs, NOTHING)
  289. if a.exclude_from_repr is False]
  290. return wrap
  291. def with_init(attrs, **kw):
  292. """
  293. A class decorator that wraps the ``__init__`` method of a class and sets
  294. *attrs* using passed *keyword arguments* before calling the original
  295. ``__init__``.
  296. Those keyword arguments that are used, are removed from the `kwargs` that
  297. is passed into your original ``__init__``. Optionally, a dictionary of
  298. default values for some of *attrs* can be passed too.
  299. Attributes that are defined using :class:`Attribute` and start with
  300. underscores will get them stripped for the initializer arguments by default
  301. (this behavior is changeable on per-attribute basis when instantiating
  302. :class:`Attribute`.
  303. :param attrs: Attributes to work with.
  304. :type attrs: ``list`` of :class:`str` or :class:`Attribute`\ s.
  305. :raises ValueError: If the value for a non-optional attribute hasn't been
  306. passed as a keyword argument.
  307. :raises ValueError: If both *defaults* and an instance of
  308. :class:`Attribute` has been passed.
  309. .. deprecated:: 14.0
  310. Use :class:`Attribute` instead of ``defaults``.
  311. :param defaults: Default values if attributes are omitted on instantiation.
  312. :type defaults: ``dict`` or ``None``
  313. """
  314. def characteristic_init(self, *args, **kw):
  315. """
  316. Attribute initializer automatically created by characteristic.
  317. The original `__init__` method is renamed to `__original_init__` and
  318. is called at the end with the initialized attributes removed from the
  319. keyword arguments.
  320. """
  321. for a in attrs:
  322. v = kw.pop(a._kw_name, NOTHING)
  323. if v is NOTHING:
  324. # Since ``a._default`` could be a property that calls
  325. # a factory, we make this a separate step.
  326. v = a._default
  327. if v is NOTHING:
  328. raise ValueError(
  329. "Missing keyword value for '{0}'.".format(a._kw_name)
  330. )
  331. if (
  332. a.instance_of is not None
  333. and not isinstance(v, a.instance_of)
  334. ):
  335. raise TypeError(
  336. "Attribute '{0}' must be an instance of '{1}'."
  337. .format(a.name, a.instance_of.__name__)
  338. )
  339. self.__characteristic_setattr__(a.name, v)
  340. self.__original_init__(*args, **kw)
  341. def wrap(cl):
  342. cl.__original_init__ = cl.__init__
  343. cl.__init__ = characteristic_init
  344. # Sidestep immutability sentry completely if possible..
  345. cl.__characteristic_setattr__ = getattr(
  346. cl, "__original_setattr__", cl.__setattr__
  347. )
  348. return cl
  349. attrs = [attr
  350. for attr in _ensure_attributes(attrs,
  351. defaults=kw.get("defaults",
  352. NOTHING))
  353. if attr.exclude_from_init is False]
  354. return wrap
  355. _VALID_INITS = frozenset(["characteristic_init", "__init__"])
  356. def immutable(attrs):
  357. """
  358. Class decorator that makes *attrs* of a class immutable.
  359. That means that *attrs* can only be set from an initializer. If anyone
  360. else tries to set one of them, an :exc:`AttributeError` is raised.
  361. .. versionadded:: 14.0
  362. """
  363. # In this case, we just want to compare (native) strings.
  364. attrs = frozenset(attr.name if isinstance(attr, Attribute) else attr
  365. for attr in _ensure_attributes(attrs, NOTHING)
  366. if attr.exclude_from_immutable is False)
  367. def characteristic_immutability_sentry(self, attr, value):
  368. """
  369. Immutability sentry automatically created by characteristic.
  370. If an attribute is attempted to be set from any other place than an
  371. initializer, a TypeError is raised. Else the original __setattr__ is
  372. called.
  373. """
  374. prev = sys._getframe().f_back
  375. if (
  376. attr not in attrs
  377. or
  378. prev is not None and prev.f_code.co_name in _VALID_INITS
  379. ):
  380. self.__original_setattr__(attr, value)
  381. else:
  382. raise AttributeError(
  383. "Attribute '{0}' of class '{1}' is immutable."
  384. .format(attr, self.__class__.__name__)
  385. )
  386. def wrap(cl):
  387. cl.__original_setattr__ = cl.__setattr__
  388. cl.__setattr__ = characteristic_immutability_sentry
  389. return cl
  390. return wrap
  391. def attributes(attrs, apply_with_cmp=True, apply_with_init=True,
  392. apply_with_repr=True, apply_immutable=False, **kw):
  393. """
  394. A convenience class decorator that allows to *selectively* apply
  395. :func:`with_cmp`, :func:`with_repr`, :func:`with_init`, and
  396. :func:`immutable` to avoid code duplication.
  397. :param attrs: Attributes to work with.
  398. :type attrs: ``list`` of :class:`str` or :class:`Attribute`\ s.
  399. :param apply_with_cmp: Apply :func:`with_cmp`.
  400. :type apply_with_cmp: bool
  401. :param apply_with_init: Apply :func:`with_init`.
  402. :type apply_with_init: bool
  403. :param apply_with_repr: Apply :func:`with_repr`.
  404. :type apply_with_repr: bool
  405. :param apply_immutable: Apply :func:`immutable`. The only one that is off
  406. by default.
  407. :type apply_immutable: bool
  408. :raises ValueError: If both *defaults* and an instance of
  409. :class:`Attribute` has been passed.
  410. .. versionadded:: 14.0
  411. Added possibility to pass instances of :class:`Attribute` in ``attrs``.
  412. .. versionadded:: 14.0
  413. Added ``apply_*``.
  414. .. deprecated:: 14.0
  415. Use :class:`Attribute` instead of ``defaults``.
  416. :param defaults: Default values if attributes are omitted on instantiation.
  417. :type defaults: ``dict`` or ``None``
  418. .. deprecated:: 14.0
  419. Use ``apply_with_init`` instead of ``create_init``. Until removal, if
  420. *either* if `False`, ``with_init`` is not applied.
  421. :param create_init: Apply :func:`with_init`.
  422. :type create_init: bool
  423. """
  424. if "create_init" in kw:
  425. apply_with_init = kw["create_init"]
  426. warnings.warn(
  427. "`create_init` has been deprecated in 14.0, please use "
  428. "`apply_with_init`.", DeprecationWarning,
  429. stacklevel=2,
  430. )
  431. attrs = _ensure_attributes(attrs, defaults=kw.get("defaults", NOTHING))
  432. def wrap(cl):
  433. if apply_with_repr is True:
  434. cl = with_repr(attrs)(cl)
  435. if apply_with_cmp is True:
  436. cl = with_cmp(attrs)(cl)
  437. # Order matters here because with_init can optimize and side-step
  438. # immutable's sentry function.
  439. if apply_immutable is True:
  440. cl = immutable(attrs)(cl)
  441. if apply_with_init is True:
  442. cl = with_init(attrs)(cl)
  443. return cl
  444. return wrap