test_characteristic.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720
  1. from __future__ import absolute_import, division, print_function
  2. import sys
  3. import warnings
  4. import pytest
  5. from characteristic import (
  6. Attribute,
  7. NOTHING,
  8. _ensure_attributes,
  9. attributes,
  10. immutable,
  11. with_cmp,
  12. with_init,
  13. with_repr,
  14. )
  15. PY2 = sys.version_info[0] == 2
  16. warnings.simplefilter("always")
  17. class TestAttribute(object):
  18. def test_init_simple(self):
  19. """
  20. Instantiating with just the name initializes properly.
  21. """
  22. a = Attribute("foo")
  23. assert "foo" == a.name
  24. assert NOTHING is a._default
  25. def test_init_default_factory(self):
  26. """
  27. Instantiating with default_factory creates a proper descriptor for
  28. _default.
  29. """
  30. a = Attribute("foo", default_factory=list)
  31. assert list() == a._default
  32. def test_init_default_value(self):
  33. """
  34. Instantiating with default_value initializes default properly.
  35. """
  36. a = Attribute("foo", default_value="bar")
  37. assert "bar" == a._default
  38. def test_ambiguous_defaults(self):
  39. """
  40. Instantiating with both default_value and default_factory raises
  41. ValueError.
  42. """
  43. with pytest.raises(ValueError):
  44. Attribute(
  45. "foo",
  46. default_value="bar",
  47. default_factory=lambda: 42
  48. )
  49. def test_missing_attr(self):
  50. """
  51. Accessing inexistent attributes still raises an AttributeError.
  52. """
  53. a = Attribute("foo")
  54. with pytest.raises(AttributeError):
  55. a.bar
  56. def test_alias(self):
  57. """
  58. If an attribute with a leading _ is defined, the initializer keyword
  59. is stripped of it.
  60. """
  61. a = Attribute("_private")
  62. assert "private" == a._kw_name
  63. def test_non_alias(self):
  64. """
  65. The keyword name of a non-private
  66. """
  67. a = Attribute("public")
  68. assert "public" == a._kw_name
  69. def test_dunder(self):
  70. """
  71. Dunder gets all _ stripped.
  72. """
  73. a = Attribute("__very_private")
  74. assert "very_private" == a._kw_name
  75. def test_init_aliaser_none(self):
  76. """
  77. No aliasing if init_aliaser is None.
  78. """
  79. a = Attribute("_private", init_aliaser=None)
  80. assert a.name == a._kw_name
  81. def test_init_aliaser(self):
  82. """
  83. Any callable works for aliasing.
  84. """
  85. a = Attribute("a", init_aliaser=lambda _: "foo")
  86. assert "foo" == a._kw_name
  87. def test_repr(self):
  88. """
  89. repr returns the correct string.
  90. """
  91. a = Attribute(
  92. name="name",
  93. exclude_from_cmp=True,
  94. exclude_from_init=True,
  95. exclude_from_repr=True,
  96. exclude_from_immutable=True,
  97. default_value=42,
  98. instance_of=str,
  99. init_aliaser=None
  100. )
  101. assert (
  102. "<Attribute(name='name', exclude_from_cmp=True, "
  103. "exclude_from_init=True, exclude_from_repr=True, "
  104. "exclude_from_immutable=True, "
  105. "default_value=42, default_factory=None, instance_of=<{0} 'str'>,"
  106. " init_aliaser=None)>"
  107. ).format("type" if PY2 else "class") == repr(a)
  108. @with_cmp(["a", "b"])
  109. class CmpC(object):
  110. def __init__(self, a, b):
  111. self.a = a
  112. self.b = b
  113. class TestWithCmp(object):
  114. def test_equal(self):
  115. """
  116. Equal objects are detected as equal.
  117. """
  118. assert CmpC(1, 2) == CmpC(1, 2)
  119. assert not (CmpC(1, 2) != CmpC(1, 2))
  120. def test_unequal_same_class(self):
  121. """
  122. Unequal objects of correct type are detected as unequal.
  123. """
  124. assert CmpC(1, 2) != CmpC(2, 1)
  125. assert not (CmpC(1, 2) == CmpC(2, 1))
  126. def test_unequal_different_class(self):
  127. """
  128. Unequal objects of differnt type are detected even if their attributes
  129. match.
  130. """
  131. class NotCmpC(object):
  132. a = 1
  133. b = 2
  134. assert CmpC(1, 2) != NotCmpC()
  135. assert not (CmpC(1, 2) == NotCmpC())
  136. def test_lt(self):
  137. """
  138. __lt__ compares objects as tuples of attribute values.
  139. """
  140. for a, b in [
  141. ((1, 2), (2, 1)),
  142. ((1, 2), (1, 3)),
  143. (("a", "b"), ("b", "a")),
  144. ]:
  145. assert CmpC(*a) < CmpC(*b)
  146. def test_lt_unordable(self):
  147. """
  148. __lt__ returns NotImplemented if classes differ.
  149. """
  150. assert NotImplemented == (CmpC(1, 2).__lt__(42))
  151. def test_le(self):
  152. """
  153. __le__ compares objects as tuples of attribute values.
  154. """
  155. for a, b in [
  156. ((1, 2), (2, 1)),
  157. ((1, 2), (1, 3)),
  158. ((1, 1), (1, 1)),
  159. (("a", "b"), ("b", "a")),
  160. (("a", "b"), ("a", "b")),
  161. ]:
  162. assert CmpC(*a) <= CmpC(*b)
  163. def test_le_unordable(self):
  164. """
  165. __le__ returns NotImplemented if classes differ.
  166. """
  167. assert NotImplemented == (CmpC(1, 2).__le__(42))
  168. def test_gt(self):
  169. """
  170. __gt__ compares objects as tuples of attribute values.
  171. """
  172. for a, b in [
  173. ((2, 1), (1, 2)),
  174. ((1, 3), (1, 2)),
  175. (("b", "a"), ("a", "b")),
  176. ]:
  177. assert CmpC(*a) > CmpC(*b)
  178. def test_gt_unordable(self):
  179. """
  180. __gt__ returns NotImplemented if classes differ.
  181. """
  182. assert NotImplemented == (CmpC(1, 2).__gt__(42))
  183. def test_ge(self):
  184. """
  185. __ge__ compares objects as tuples of attribute values.
  186. """
  187. for a, b in [
  188. ((2, 1), (1, 2)),
  189. ((1, 3), (1, 2)),
  190. ((1, 1), (1, 1)),
  191. (("b", "a"), ("a", "b")),
  192. (("a", "b"), ("a", "b")),
  193. ]:
  194. assert CmpC(*a) >= CmpC(*b)
  195. def test_ge_unordable(self):
  196. """
  197. __ge__ returns NotImplemented if classes differ.
  198. """
  199. assert NotImplemented == (CmpC(1, 2).__ge__(42))
  200. def test_hash(self):
  201. """
  202. __hash__ returns different hashes for different values.
  203. """
  204. assert hash(CmpC(1, 2)) != hash(CmpC(1, 1))
  205. def test_Attribute_exclude_from_cmp(self):
  206. """
  207. Ignores attribute if exclude_from_cmp=True.
  208. """
  209. @with_cmp([Attribute("a", exclude_from_cmp=True), "b"])
  210. class C(object):
  211. def __init__(self, a, b):
  212. self.a = a
  213. self.b = b
  214. assert C(42, 1) == C(23, 1)
  215. @with_repr(["a", "b"])
  216. class ReprC(object):
  217. def __init__(self, a, b):
  218. self.a = a
  219. self.b = b
  220. class TestReprAttrs(object):
  221. def test_repr(self):
  222. """
  223. Test repr returns a sensible value.
  224. """
  225. assert "<ReprC(a=1, b=2)>" == repr(ReprC(1, 2))
  226. def test_Attribute_exclude_from_repr(self):
  227. """
  228. Ignores attribute if exclude_from_repr=True.
  229. """
  230. @with_repr([Attribute("a", exclude_from_repr=True), "b"])
  231. class C(object):
  232. def __init__(self, a, b):
  233. self.a = a
  234. self.b = b
  235. assert "<C(b=2)>" == repr(C(1, 2))
  236. @with_init([Attribute("a"), Attribute("b")])
  237. class InitC(object):
  238. def __init__(self):
  239. if self.a == self.b:
  240. raise ValueError
  241. class TestWithInit(object):
  242. def test_sets_attributes(self):
  243. """
  244. The attributes are initialized using the passed keywords.
  245. """
  246. obj = InitC(a=1, b=2)
  247. assert 1 == obj.a
  248. assert 2 == obj.b
  249. def test_custom_init(self):
  250. """
  251. The class initializer is called too.
  252. """
  253. with pytest.raises(ValueError):
  254. InitC(a=1, b=1)
  255. def test_passes_args(self):
  256. """
  257. All positional parameters are passed to the original initializer.
  258. """
  259. @with_init(["a"])
  260. class InitWithArg(object):
  261. def __init__(self, arg):
  262. self.arg = arg
  263. obj = InitWithArg(42, a=1)
  264. assert 42 == obj.arg
  265. assert 1 == obj.a
  266. def test_passes_remaining_kw(self):
  267. """
  268. Keyword arguments that aren't used for attributes are passed to the
  269. original initializer.
  270. """
  271. @with_init(["a"])
  272. class InitWithKWArg(object):
  273. def __init__(self, kw_arg=None):
  274. self.kw_arg = kw_arg
  275. obj = InitWithKWArg(a=1, kw_arg=42)
  276. assert 42 == obj.kw_arg
  277. assert 1 == obj.a
  278. def test_does_not_pass_attrs(self):
  279. """
  280. The attributes are removed from the keyword arguments before they are
  281. passed to the original initializer.
  282. """
  283. @with_init(["a"])
  284. class InitWithKWArgs(object):
  285. def __init__(self, **kw):
  286. assert "a" not in kw
  287. assert "b" in kw
  288. InitWithKWArgs(a=1, b=42)
  289. def test_defaults(self):
  290. """
  291. If defaults are passed, they are used as fallback.
  292. """
  293. @with_init(["a", "b"], defaults={"b": 2})
  294. class InitWithDefaults(object):
  295. pass
  296. obj = InitWithDefaults(a=1)
  297. assert 2 == obj.b
  298. def test_missing_arg(self):
  299. """
  300. Raises `ValueError` if a value isn't passed.
  301. """
  302. with pytest.raises(ValueError) as e:
  303. InitC(a=1)
  304. assert "Missing keyword value for 'b'." == e.value.args[0]
  305. def test_defaults_conflict(self):
  306. """
  307. Raises `ValueError` if both defaults and an Attribute are passed.
  308. """
  309. with pytest.raises(ValueError) as e:
  310. @with_init([Attribute("a")], defaults={"a": 42})
  311. class C(object):
  312. pass
  313. assert (
  314. "Mixing of the 'defaults' keyword argument and passing instances "
  315. "of Attribute for 'attrs' is prohibited. Please don't use "
  316. "'defaults' anymore, it has been deprecated in 14.0."
  317. == e.value.args[0]
  318. )
  319. def test_attribute(self):
  320. """
  321. String attributes are converted to Attributes and thus work.
  322. """
  323. @with_init(["a"])
  324. class C(object):
  325. pass
  326. o = C(a=1)
  327. assert 1 == o.a
  328. def test_default_factory(self):
  329. """
  330. The default factory is used for each instance of missing keyword
  331. argument.
  332. """
  333. @with_init([Attribute("a", default_factory=list)])
  334. class C(object):
  335. pass
  336. o1 = C()
  337. o2 = C()
  338. assert o1.a is not o2.a
  339. def test_optimizes(self):
  340. """
  341. Uses __original_setattr__ if possible.
  342. """
  343. @immutable(["a"])
  344. @with_init(["a"])
  345. class C(object):
  346. pass
  347. c = C(a=42)
  348. assert c.__original_setattr__ == c.__characteristic_setattr__
  349. def test_setattr(self):
  350. """
  351. Uses setattr by default.
  352. """
  353. @with_init(["a"])
  354. class C(object):
  355. pass
  356. c = C(a=42)
  357. assert c.__setattr__ == c.__characteristic_setattr__
  358. def test_underscores(self):
  359. """
  360. with_init takes keyword aliasing into account.
  361. """
  362. @with_init([Attribute("_a")])
  363. class C(object):
  364. pass
  365. c = C(a=1)
  366. assert 1 == c._a
  367. def test_plain_no_alias(self):
  368. """
  369. str-based attributes don't get aliased for backward-compatibility.
  370. """
  371. @with_init(["_a"])
  372. class C(object):
  373. pass
  374. c = C(_a=1)
  375. assert 1 == c._a
  376. def test_instance_of_fail(self):
  377. """
  378. Raise `TypeError` if an Attribute with an `instance_of` is is attempted
  379. to be set to a mismatched type.
  380. """
  381. @with_init([Attribute("a", instance_of=int)])
  382. class C(object):
  383. pass
  384. with pytest.raises(TypeError) as e:
  385. C(a="not an int!")
  386. assert (
  387. "Attribute 'a' must be an instance of 'int'."
  388. == e.value.args[0]
  389. )
  390. def test_instance_of_success(self):
  391. """
  392. Setting an attribute to a value that doesn't conflict with an
  393. `instance_of` declaration works.
  394. """
  395. @with_init([Attribute("a", instance_of=int)])
  396. class C(object):
  397. pass
  398. c = C(a=42)
  399. assert 42 == c.a
  400. def test_Attribute_exclude_from_init(self):
  401. """
  402. Ignores attribute if exclude_from_init=True.
  403. """
  404. @with_init([Attribute("a", exclude_from_init=True), "b"])
  405. class C(object):
  406. pass
  407. C(b=1)
  408. def test_deprecation_defaults(self):
  409. """
  410. Emits a DeprecationWarning if `defaults` is used.
  411. """
  412. with warnings.catch_warnings(record=True) as w:
  413. @with_init(["a"], defaults={"a": 42})
  414. class C(object):
  415. pass
  416. assert (
  417. '`defaults` has been deprecated in 14.0, please use the '
  418. '`Attribute` class instead.'
  419. ) == w[0].message.args[0]
  420. assert issubclass(w[0].category, DeprecationWarning)
  421. class TestAttributes(object):
  422. def test_leaves_init_alone(self):
  423. """
  424. If *apply_with_init* or *create_init* is `False`, leave __init__ alone.
  425. """
  426. @attributes(["a"], apply_with_init=False)
  427. class C(object):
  428. pass
  429. @attributes(["a"], create_init=False)
  430. class CDeprecated(object):
  431. pass
  432. obj1 = C()
  433. obj2 = CDeprecated()
  434. with pytest.raises(AttributeError):
  435. obj1.a
  436. with pytest.raises(AttributeError):
  437. obj2.a
  438. def test_wraps_init(self):
  439. """
  440. If *create_init* is `True`, build initializer.
  441. """
  442. @attributes(["a", "b"], apply_with_init=True)
  443. class C(object):
  444. pass
  445. obj = C(a=1, b=2)
  446. assert 1 == obj.a
  447. assert 2 == obj.b
  448. def test_immutable(self):
  449. """
  450. If *apply_immutable* is `True`, make class immutable.
  451. """
  452. @attributes(["a"], apply_immutable=True)
  453. class ImmuClass(object):
  454. pass
  455. obj = ImmuClass(a=42)
  456. with pytest.raises(AttributeError):
  457. obj.a = "23"
  458. def test_apply_with_cmp(self):
  459. """
  460. Don't add cmp methods if *apply_with_cmp* is `False`.
  461. """
  462. @attributes(["a"], apply_with_cmp=False)
  463. class C(object):
  464. pass
  465. obj = C(a=1)
  466. if PY2:
  467. assert None is getattr(obj, "__eq__", None)
  468. else:
  469. assert object.__eq__ == C.__eq__
  470. def test_apply_with_repr(self):
  471. """
  472. Don't add __repr__ if *apply_with_repr* is `False`.
  473. """
  474. @attributes(["a"], apply_with_repr=False)
  475. class C(object):
  476. pass
  477. assert repr(C(a=1)).startswith("<test_characteristic.")
  478. def test_optimizes(self):
  479. """
  480. Uses correct order such that with_init can us __original_setattr__.
  481. """
  482. @attributes(["a"], apply_immutable=True)
  483. class C(object):
  484. __slots__ = ["a"]
  485. c = C(a=42)
  486. assert c.__original_setattr__ == c.__characteristic_setattr__
  487. def test_private(self):
  488. """
  489. Integration test for name mangling/aliasing.
  490. """
  491. @attributes([Attribute("_a")])
  492. class C(object):
  493. pass
  494. c = C(a=42)
  495. assert 42 == c._a
  496. def test_private_no_alias(self):
  497. """
  498. Integration test for name mangling/aliasing.
  499. """
  500. @attributes([Attribute("_a", init_aliaser=None)])
  501. class C(object):
  502. pass
  503. c = C(_a=42)
  504. assert 42 == c._a
  505. def test_deprecation_create_init(self):
  506. """
  507. Emits a DeprecationWarning if `create_init` is used.
  508. """
  509. with warnings.catch_warnings(record=True) as w:
  510. @attributes(["a"], create_init=False)
  511. class C(object):
  512. pass
  513. assert (
  514. '`create_init` has been deprecated in 14.0, please use '
  515. '`apply_with_init`.'
  516. ) == w[0].message.args[0]
  517. assert issubclass(w[0].category, DeprecationWarning)
  518. def test_deprecation_defaults(self):
  519. """
  520. Emits a DeprecationWarning if `defaults` is used.
  521. """
  522. with warnings.catch_warnings(record=True) as w:
  523. @attributes(["a"], defaults={"a": 42})
  524. class C(object):
  525. pass
  526. assert (
  527. '`defaults` has been deprecated in 14.0, please use the '
  528. '`Attribute` class instead.'
  529. ) == w[0].message.args[0]
  530. assert issubclass(w[0].category, DeprecationWarning)
  531. class TestEnsureAttributes(object):
  532. def test_leaves_attribute_alone(self):
  533. """
  534. List items that are an Attribute stay an Attribute.
  535. """
  536. a = Attribute("a")
  537. assert a is _ensure_attributes([a], {})[0]
  538. def test_converts_rest(self):
  539. """
  540. Any other item will be transformed into an Attribute.
  541. """
  542. l = _ensure_attributes(["a"], {})
  543. assert isinstance(l[0], Attribute)
  544. assert "a" == l[0].name
  545. def test_defaults(self):
  546. """
  547. Legacy defaults are translated into default_value attributes.
  548. """
  549. l = _ensure_attributes(["a"], {"a": 42})
  550. assert 42 == l[0].default_value
  551. def test_defaults_Attribute(self):
  552. """
  553. Raises ValueError on defaults != {} and an Attribute within attrs.
  554. """
  555. with pytest.raises(ValueError):
  556. _ensure_attributes([Attribute("a")], defaults={"a": 42})
  557. class TestImmutable(object):
  558. def test_bare(self):
  559. """
  560. In an immutable class, setting an definition-time attribute raises an
  561. AttributeError.
  562. """
  563. @immutable(["foo"])
  564. class ImmuClass(object):
  565. foo = "bar"
  566. i = ImmuClass()
  567. with pytest.raises(AttributeError):
  568. i.foo = "not bar"
  569. def test_Attribute(self):
  570. """
  571. Mutation is caught if user passes an Attribute instance.
  572. """
  573. @immutable([Attribute("foo")])
  574. class ImmuClass(object):
  575. def __init__(self):
  576. self.foo = "bar"
  577. i = ImmuClass()
  578. with pytest.raises(AttributeError):
  579. i.foo = "not bar"
  580. def test_init(self):
  581. """
  582. Changes within __init__ are allowed.
  583. """
  584. @immutable(["foo"])
  585. class ImmuClass(object):
  586. def __init__(self):
  587. self.foo = "bar"
  588. i = ImmuClass()
  589. assert "bar" == i.foo
  590. def test_with_init(self):
  591. """
  592. Changes in with_init's initializer are allowed.
  593. """
  594. @immutable(["foo"])
  595. @with_init(["foo"])
  596. class ImmuClass(object):
  597. pass
  598. i = ImmuClass(foo="qux")
  599. assert "qux" == i.foo
  600. def test_Attribute_exclude_from_immutable(self):
  601. """
  602. Ignores attribute if exclude_from_immutable=True.
  603. """
  604. @immutable([Attribute("a", exclude_from_immutable=True), "b"])
  605. class C(object):
  606. def __init__(self, a, b):
  607. self.a = a
  608. self.b = b
  609. c = C(1, 2)
  610. c.a = 3
  611. with pytest.raises(AttributeError):
  612. c.b = 4
  613. def test_nothing():
  614. """
  615. ``NOTHING`` has a sensible repr.
  616. """
  617. assert "NOTHING" == repr(NOTHING)