You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

290 lines
9.5 KiB

4 years ago
  1. from __future__ import absolute_import, division, print_function
  2. import copy
  3. from ._compat import iteritems
  4. from ._make import NOTHING, _obj_setattr, fields
  5. from .exceptions import AttrsAttributeNotFoundError
  6. def asdict(
  7. inst,
  8. recurse=True,
  9. filter=None,
  10. dict_factory=dict,
  11. retain_collection_types=False,
  12. ):
  13. """
  14. Return the ``attrs`` attribute values of *inst* as a dict.
  15. Optionally recurse into other ``attrs``-decorated classes.
  16. :param inst: Instance of an ``attrs``-decorated class.
  17. :param bool recurse: Recurse into classes that are also
  18. ``attrs``-decorated.
  19. :param callable filter: A callable whose return code determines whether an
  20. attribute or element is included (``True``) or dropped (``False``). Is
  21. called with the :class:`attr.Attribute` as the first argument and the
  22. value as the second argument.
  23. :param callable dict_factory: A callable to produce dictionaries from. For
  24. example, to produce ordered dictionaries instead of normal Python
  25. dictionaries, pass in ``collections.OrderedDict``.
  26. :param bool retain_collection_types: Do not convert to ``list`` when
  27. encountering an attribute whose type is ``tuple`` or ``set``. Only
  28. meaningful if ``recurse`` is ``True``.
  29. :rtype: return type of *dict_factory*
  30. :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
  31. class.
  32. .. versionadded:: 16.0.0 *dict_factory*
  33. .. versionadded:: 16.1.0 *retain_collection_types*
  34. """
  35. attrs = fields(inst.__class__)
  36. rv = dict_factory()
  37. for a in attrs:
  38. v = getattr(inst, a.name)
  39. if filter is not None and not filter(a, v):
  40. continue
  41. if recurse is True:
  42. if has(v.__class__):
  43. rv[a.name] = asdict(
  44. v, True, filter, dict_factory, retain_collection_types
  45. )
  46. elif isinstance(v, (tuple, list, set)):
  47. cf = v.__class__ if retain_collection_types is True else list
  48. rv[a.name] = cf(
  49. [
  50. _asdict_anything(
  51. i, filter, dict_factory, retain_collection_types
  52. )
  53. for i in v
  54. ]
  55. )
  56. elif isinstance(v, dict):
  57. df = dict_factory
  58. rv[a.name] = df(
  59. (
  60. _asdict_anything(
  61. kk, filter, df, retain_collection_types
  62. ),
  63. _asdict_anything(
  64. vv, filter, df, retain_collection_types
  65. ),
  66. )
  67. for kk, vv in iteritems(v)
  68. )
  69. else:
  70. rv[a.name] = v
  71. else:
  72. rv[a.name] = v
  73. return rv
  74. def _asdict_anything(val, filter, dict_factory, retain_collection_types):
  75. """
  76. ``asdict`` only works on attrs instances, this works on anything.
  77. """
  78. if getattr(val.__class__, "__attrs_attrs__", None) is not None:
  79. # Attrs class.
  80. rv = asdict(val, True, filter, dict_factory, retain_collection_types)
  81. elif isinstance(val, (tuple, list, set)):
  82. cf = val.__class__ if retain_collection_types is True else list
  83. rv = cf(
  84. [
  85. _asdict_anything(
  86. i, filter, dict_factory, retain_collection_types
  87. )
  88. for i in val
  89. ]
  90. )
  91. elif isinstance(val, dict):
  92. df = dict_factory
  93. rv = df(
  94. (
  95. _asdict_anything(kk, filter, df, retain_collection_types),
  96. _asdict_anything(vv, filter, df, retain_collection_types),
  97. )
  98. for kk, vv in iteritems(val)
  99. )
  100. else:
  101. rv = val
  102. return rv
  103. def astuple(
  104. inst,
  105. recurse=True,
  106. filter=None,
  107. tuple_factory=tuple,
  108. retain_collection_types=False,
  109. ):
  110. """
  111. Return the ``attrs`` attribute values of *inst* as a tuple.
  112. Optionally recurse into other ``attrs``-decorated classes.
  113. :param inst: Instance of an ``attrs``-decorated class.
  114. :param bool recurse: Recurse into classes that are also
  115. ``attrs``-decorated.
  116. :param callable filter: A callable whose return code determines whether an
  117. attribute or element is included (``True``) or dropped (``False``). Is
  118. called with the :class:`attr.Attribute` as the first argument and the
  119. value as the second argument.
  120. :param callable tuple_factory: A callable to produce tuples from. For
  121. example, to produce lists instead of tuples.
  122. :param bool retain_collection_types: Do not convert to ``list``
  123. or ``dict`` when encountering an attribute which type is
  124. ``tuple``, ``dict`` or ``set``. Only meaningful if ``recurse`` is
  125. ``True``.
  126. :rtype: return type of *tuple_factory*
  127. :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
  128. class.
  129. .. versionadded:: 16.2.0
  130. """
  131. attrs = fields(inst.__class__)
  132. rv = []
  133. retain = retain_collection_types # Very long. :/
  134. for a in attrs:
  135. v = getattr(inst, a.name)
  136. if filter is not None and not filter(a, v):
  137. continue
  138. if recurse is True:
  139. if has(v.__class__):
  140. rv.append(
  141. astuple(
  142. v,
  143. recurse=True,
  144. filter=filter,
  145. tuple_factory=tuple_factory,
  146. retain_collection_types=retain,
  147. )
  148. )
  149. elif isinstance(v, (tuple, list, set)):
  150. cf = v.__class__ if retain is True else list
  151. rv.append(
  152. cf(
  153. [
  154. astuple(
  155. j,
  156. recurse=True,
  157. filter=filter,
  158. tuple_factory=tuple_factory,
  159. retain_collection_types=retain,
  160. )
  161. if has(j.__class__)
  162. else j
  163. for j in v
  164. ]
  165. )
  166. )
  167. elif isinstance(v, dict):
  168. df = v.__class__ if retain is True else dict
  169. rv.append(
  170. df(
  171. (
  172. astuple(
  173. kk,
  174. tuple_factory=tuple_factory,
  175. retain_collection_types=retain,
  176. )
  177. if has(kk.__class__)
  178. else kk,
  179. astuple(
  180. vv,
  181. tuple_factory=tuple_factory,
  182. retain_collection_types=retain,
  183. )
  184. if has(vv.__class__)
  185. else vv,
  186. )
  187. for kk, vv in iteritems(v)
  188. )
  189. )
  190. else:
  191. rv.append(v)
  192. else:
  193. rv.append(v)
  194. return rv if tuple_factory is list else tuple_factory(rv)
  195. def has(cls):
  196. """
  197. Check whether *cls* is a class with ``attrs`` attributes.
  198. :param type cls: Class to introspect.
  199. :raise TypeError: If *cls* is not a class.
  200. :rtype: :class:`bool`
  201. """
  202. return getattr(cls, "__attrs_attrs__", None) is not None
  203. def assoc(inst, **changes):
  204. """
  205. Copy *inst* and apply *changes*.
  206. :param inst: Instance of a class with ``attrs`` attributes.
  207. :param changes: Keyword changes in the new copy.
  208. :return: A copy of inst with *changes* incorporated.
  209. :raise attr.exceptions.AttrsAttributeNotFoundError: If *attr_name* couldn't
  210. be found on *cls*.
  211. :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
  212. class.
  213. .. deprecated:: 17.1.0
  214. Use :func:`evolve` instead.
  215. """
  216. import warnings
  217. warnings.warn(
  218. "assoc is deprecated and will be removed after 2018/01.",
  219. DeprecationWarning,
  220. stacklevel=2,
  221. )
  222. new = copy.copy(inst)
  223. attrs = fields(inst.__class__)
  224. for k, v in iteritems(changes):
  225. a = getattr(attrs, k, NOTHING)
  226. if a is NOTHING:
  227. raise AttrsAttributeNotFoundError(
  228. "{k} is not an attrs attribute on {cl}.".format(
  229. k=k, cl=new.__class__
  230. )
  231. )
  232. _obj_setattr(new, k, v)
  233. return new
  234. def evolve(inst, **changes):
  235. """
  236. Create a new instance, based on *inst* with *changes* applied.
  237. :param inst: Instance of a class with ``attrs`` attributes.
  238. :param changes: Keyword changes in the new copy.
  239. :return: A copy of inst with *changes* incorporated.
  240. :raise TypeError: If *attr_name* couldn't be found in the class
  241. ``__init__``.
  242. :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
  243. class.
  244. .. versionadded:: 17.1.0
  245. """
  246. cls = inst.__class__
  247. attrs = fields(cls)
  248. for a in attrs:
  249. if not a.init:
  250. continue
  251. attr_name = a.name # To deal with private attributes.
  252. init_name = attr_name if attr_name[0] != "_" else attr_name[1:]
  253. if init_name not in changes:
  254. changes[init_name] = getattr(inst, attr_name)
  255. return cls(**changes)