1# SPDX-License-Identifier: MIT

2

3

4 import copy

5

6 from ._make import NOTHING, _obj_setattr, fields

7 from .exceptions import AttrsAttributeNotFoundError

8

9

10 def asdict(

11 inst,

12 recurse=True,

13 filter=None,

14 dict_factory=dict,

15 retain_collection_types=False,

16 value_serializer=None,

17):

18 """

19 Return the ``attrs`` attribute values of *inst* as a dict.

20

21 Optionally recurse into other ``attrs``-decorated classes.

22

23 :param inst: Instance of an ``attrs``-decorated class.

24 :param bool recurse: Recurse into classes that are also

25 ``attrs``-decorated.

26 :param callable filter: A callable whose return code determines whether an

27 attribute or element is included (``True``) or dropped (``False``). Is

28 called with the `attrs.Attribute` as the first argument and the

29 value as the second argument.

30 :param callable dict_factory: A callable to produce dictionaries from. For

31 example, to produce ordered dictionaries instead of normal Python

32 dictionaries, pass in ``collections.OrderedDict``.

33 :param bool retain_collection_types: Do not convert to ``list`` when

34 encountering an attribute whose type is ``tuple`` or ``set``. Only

35 meaningful if ``recurse`` is ``True``.

36 :param Optional[callable] value_serializer: A hook that is called for every

37 attribute or dict key/value. It receives the current instance, field

38 and value and must return the (updated) value. The hook is run *after*

39 the optional *filter* has been applied.

40

41 :rtype: return type of *dict_factory*

42

43 :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``

44 class.

45

46 .. versionadded:: 16.0.0 *dict_factory*

47 .. versionadded:: 16.1.0 *retain_collection_types*

48 .. versionadded:: 20.3.0 *value_serializer*

49 .. versionadded:: 21.3.0 If a dict has a collection for a key, it is

50 serialized as a tuple.

51 """

52 attrs = fields(inst.__class__)

53 rv = dict_factory()

54 for a in attrs:

55 v = getattr(inst, a.name)

56 if filter is not None and not filter(a, v):

57 continue

58

59 if value_serializer is not None:

60 v = value_serializer(inst, a, v)

61

62 if recurse is True:

63 if has(v.__class__):

64 rv[a.name] = asdict(

65 v,

66 recurse=True,

67 filter=filter,

68 dict_factory=dict_factory,

69 retain_collection_types=retain_collection_types,

70 value_serializer=value_serializer,

71 )

72 elif isinstance(v, (tuple, list, set, frozenset)):

73 cf = v.__class__ if retain_collection_types is True else list

74 rv[a.name] = cf(

75 [

76 _asdict_anything(

77 i,

78 is_key=False,

79 filter=filter,

80 dict_factory=dict_factory,

81 retain_collection_types=retain_collection_types,

82 value_serializer=value_serializer,

83 )

84 for i in v

85 ]

86 )

87 elif isinstance(v, dict):

88 df = dict_factory

89 rv[a.name] = df(

90 (

91 _asdict_anything(

92 kk,

93 is_key=True,

94 filter=filter,

95 dict_factory=df,

96 retain_collection_types=retain_collection_types,

97 value_serializer=value_serializer,

98 ),

99 _asdict_anything(

100 vv,

101 is_key=False,

102 filter=filter,

103 dict_factory=df,

104 retain_collection_types=retain_collection_types,

105 value_serializer=value_serializer,

106 ),

107 )

108 for kk, vv in v.items()

109 )

110 else:

111 rv[a.name] = v

112 else:

113 rv[a.name] = v

114 return rv

115

116

117 def _asdict_anything(

118 val,

119 is_key,

120 filter,

121 dict_factory,

122 retain_collection_types,

123 value_serializer,

124):

125 """

126 ``asdict`` only works on attrs instances, this works on anything.

127 """

128 if getattr(val.__class__, "__attrs_attrs__", None) is not None:

129 # Attrs class.

130 rv = asdict(

131 val,

132 recurse=True,

133 filter=filter,

134 dict_factory=dict_factory,

135 retain_collection_types=retain_collection_types,

136 value_serializer=value_serializer,

137 )

138 elif isinstance(val, (tuple, list, set, frozenset)):

139 if retain_collection_types is True:

140 cf = val.__class__

141 elif is_key:

142 cf = tuple

143 else:

144 cf = list

145

146 rv = cf(

147 [

148 _asdict_anything(

149 i,

150 is_key=False,

151 filter=filter,

152 dict_factory=dict_factory,

153 retain_collection_types=retain_collection_types,

154 value_serializer=value_serializer,

155 )

156 for i in val

157 ]

158 )

159 elif isinstance(val, dict):

160 df = dict_factory

161 rv = df(

162 (

163 _asdict_anything(

164 kk,

165 is_key=True,

166 filter=filter,

167 dict_factory=df,

168 retain_collection_types=retain_collection_types,

169 value_serializer=value_serializer,

170 ),

171 _asdict_anything(

172 vv,

173 is_key=False,

174 filter=filter,

175 dict_factory=df,

176 retain_collection_types=retain_collection_types,

177 value_serializer=value_serializer,

178 ),

179 )

180 for kk, vv in val.items()

181 )

182 else:

183 rv = val

184 if value_serializer is not None:

185 rv = value_serializer(None, None, rv)

186

187 return rv

188

189

190 def astuple(

191 inst,

192 recurse=True,

193 filter=None,

194 tuple_factory=tuple,

195 retain_collection_types=False,

196):

197 """

198 Return the ``attrs`` attribute values of *inst* as a tuple.

199

200 Optionally recurse into other ``attrs``-decorated classes.

201

202 :param inst: Instance of an ``attrs``-decorated class.

203 :param bool recurse: Recurse into classes that are also

204 ``attrs``-decorated.

205 :param callable filter: A callable whose return code determines whether an

206 attribute or element is included (``True``) or dropped (``False``). Is

207 called with the `attrs.Attribute` as the first argument and the

208 value as the second argument.

209 :param callable tuple_factory: A callable to produce tuples from. For

210 example, to produce lists instead of tuples.

211 :param bool retain_collection_types: Do not convert to ``list``

212 or ``dict`` when encountering an attribute which type is

213 ``tuple``, ``dict`` or ``set``. Only meaningful if ``recurse`` is

214 ``True``.

215

216 :rtype: return type of *tuple_factory*

217

218 :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``

219 class.

220

221 .. versionadded:: 16.2.0

222 """

223 attrs = fields(inst.__class__)

224 rv = []

225 retain = retain_collection_types # Very long. :/

226 for a in attrs:

227 v = getattr(inst, a.name)

228 if filter is not None and not filter(a, v):

229 continue

230 if recurse is True:

231 if has(v.__class__):

232 rv.append(

233 astuple(

234 v,

235 recurse=True,

236 filter=filter,

237 tuple_factory=tuple_factory,

238 retain_collection_types=retain,

239 )

240 )

241 elif isinstance(v, (tuple, list, set, frozenset)):

242 cf = v.__class__ if retain is True else list

243 rv.append(

244 cf(

245 [

246 astuple(

247 j,

248 recurse=True,

249 filter=filter,

250 tuple_factory=tuple_factory,

251 retain_collection_types=retain,

252 )

253 if has(j.__class__)

254 else j

255 for j in v

256 ]

257 )

258 )

259 elif isinstance(v, dict):

260 df = v.__class__ if retain is True else dict

261 rv.append(

262 df(

263 (

264 astuple(

265 kk,

266 tuple_factory=tuple_factory,

267 retain_collection_types=retain,

268 )

269 if has(kk.__class__)

270 else kk,

271 astuple(

272 vv,

273 tuple_factory=tuple_factory,

274 retain_collection_types=retain,

275 )

276 if has(vv.__class__)

277 else vv,

278 )

279 for kk, vv in v.items()

280 )

281 )

282 else:

283 rv.append(v)

284 else:

285 rv.append(v)

286

287 return rv if tuple_factory is list else tuple_factory(rv)

288

289

290 def has(cls):

291 """

292 Check whether *cls* is a class with ``attrs`` attributes.

293

294 :param type cls: Class to introspect.

295 :raise TypeError: If *cls* is not a class.

296

297 :rtype: bool

298 """

299 return getattr(cls, "__attrs_attrs__", None) is not None

300

301

302 def assoc(inst, **changes):

303 """

304 Copy *inst* and apply *changes*.

305

306 :param inst: Instance of a class with ``attrs`` attributes.

307 :param changes: Keyword changes in the new copy.

308

309 :return: A copy of inst with *changes* incorporated.

310

311 :raise attr.exceptions.AttrsAttributeNotFoundError: If *attr_name* couldn't

312 be found on *cls*.

313 :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``

314 class.

315

316 .. deprecated:: 17.1.0

317 Use `attrs.evolve` instead if you can.

318 This function will not be removed du to the slightly different approach

319 compared to `attrs.evolve`.

320 """

321 import warnings

322

323 warnings.warn(

324 "assoc is deprecated and will be removed after 2018/01.",

325 DeprecationWarning,

326 stacklevel=2,

327 )

328 new = copy.copy(inst)

329 attrs = fields(inst.__class__)

330 for k, v in changes.items():

331 a = getattr(attrs, k, NOTHING)

332 if a is NOTHING:

333 raise AttrsAttributeNotFoundError(

334 f"{k} is not an attrs attribute on {new.__class__}."

335 )

336 _obj_setattr(new, k, v)

337 return new

338

339

340 def evolve(inst, **changes):

341 """

342 Create a new instance, based on *inst* with *changes* applied.

343

344 :param inst: Instance of a class with ``attrs`` attributes.

345 :param changes: Keyword changes in the new copy.

346

347 :return: A copy of inst with *changes* incorporated.

348

349 :raise TypeError: If *attr_name* couldn't be found in the class

350 ``__init__``.

351 :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``

352 class.

353

354 .. versionadded:: 17.1.0

355 """

356 cls = inst.__class__

357 attrs = fields(cls)

358 for a in attrs:

359 if not a.init:

360 continue

361 attr_name = a.name # To deal with private attributes.

362 init_name = a.alias

363 if init_name not in changes:

364 changes[init_name] = getattr(inst, attr_name)

365

366 return cls(**changes)

367

368

369 def resolve_types(cls, globalns=None, localns=None, attribs=None):

370 """

371 Resolve any strings and forward annotations in type annotations.

372

373 This is only required if you need concrete types in `Attribute`'s *type*

374 field. In other words, you don't need to resolve your types if you only

375 use them for static type checking.

376

377 With no arguments, names will be looked up in the module in which the class

378 was created. If this is not what you want, e.g. if the name only exists

379 inside a method, you may pass *globalns* or *localns* to specify other

380 dictionaries in which to look up these names. See the docs of

381 `typing.get_type_hints` for more details.

382

383 :param type cls: Class to resolve.

384 :param Optional[dict] globalns: Dictionary containing global variables.

385 :param Optional[dict] localns: Dictionary containing local variables.

386 :param Optional[list] attribs: List of attribs for the given class.

387 This is necessary when calling from inside a ``field_transformer``

388 since *cls* is not an ``attrs`` class yet.

389

390 :raise TypeError: If *cls* is not a class.

391 :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``

392 class and you didn't pass any attribs.

393 :raise NameError: If types cannot be resolved because of missing variables.

394

395 :returns: *cls* so you can use this function also as a class decorator.

396 Please note that you have to apply it **after** `attrs.define`. That

397 means the decorator has to come in the line **before** `attrs.define`.

398

399 .. versionadded:: 20.1.0

400 .. versionadded:: 21.1.0 *attribs*

401

402 """

403 # Since calling get_type_hints is expensive we cache whether we've

404 # done it already.

405 if getattr(cls, "__attrs_types_resolved__", None) != cls:

406 import typing

407

408 hints = typing.get_type_hints(cls, globalns=globalns, localns=localns)

409 for field in fields(cls) if attribs is None else attribs:

410 if field.name in hints:

411 # Since fields have been frozen we must work around it.

412 _obj_setattr(field, "type", hints[field.name])

413 # We store the class we resolved so that subclasses know they haven't

414 # been resolved.

415 cls.__attrs_types_resolved__ = cls

416

417 # Return the class so you can use it as a decorator too.

418 return cls