test_factory_constructors.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. import pytest
  2. import re
  3. from pybind11_tests import factory_constructors as m
  4. from pybind11_tests.factory_constructors import tag
  5. from pybind11_tests import ConstructorStats
  6. def test_init_factory_basic():
  7. """Tests py::init_factory() wrapper around various ways of returning the object"""
  8. cstats = [ConstructorStats.get(c) for c in [m.TestFactory1, m.TestFactory2, m.TestFactory3]]
  9. cstats[0].alive() # force gc
  10. n_inst = ConstructorStats.detail_reg_inst()
  11. x1 = m.TestFactory1(tag.unique_ptr, 3)
  12. assert x1.value == "3"
  13. y1 = m.TestFactory1(tag.pointer)
  14. assert y1.value == "(empty)"
  15. z1 = m.TestFactory1("hi!")
  16. assert z1.value == "hi!"
  17. assert ConstructorStats.detail_reg_inst() == n_inst + 3
  18. x2 = m.TestFactory2(tag.move)
  19. assert x2.value == "(empty2)"
  20. y2 = m.TestFactory2(tag.pointer, 7)
  21. assert y2.value == "7"
  22. z2 = m.TestFactory2(tag.unique_ptr, "hi again")
  23. assert z2.value == "hi again"
  24. assert ConstructorStats.detail_reg_inst() == n_inst + 6
  25. x3 = m.TestFactory3(tag.shared_ptr)
  26. assert x3.value == "(empty3)"
  27. y3 = m.TestFactory3(tag.pointer, 42)
  28. assert y3.value == "42"
  29. z3 = m.TestFactory3("bye")
  30. assert z3.value == "bye"
  31. with pytest.raises(TypeError) as excinfo:
  32. m.TestFactory3(tag.null_ptr)
  33. assert str(excinfo.value) == "pybind11::init(): factory function returned nullptr"
  34. assert [i.alive() for i in cstats] == [3, 3, 3]
  35. assert ConstructorStats.detail_reg_inst() == n_inst + 9
  36. del x1, y2, y3, z3
  37. assert [i.alive() for i in cstats] == [2, 2, 1]
  38. assert ConstructorStats.detail_reg_inst() == n_inst + 5
  39. del x2, x3, y1, z1, z2
  40. assert [i.alive() for i in cstats] == [0, 0, 0]
  41. assert ConstructorStats.detail_reg_inst() == n_inst
  42. assert [i.values() for i in cstats] == [
  43. ["3", "hi!"],
  44. ["7", "hi again"],
  45. ["42", "bye"]
  46. ]
  47. assert [i.default_constructions for i in cstats] == [1, 1, 1]
  48. def test_init_factory_signature(msg):
  49. with pytest.raises(TypeError) as excinfo:
  50. m.TestFactory1("invalid", "constructor", "arguments")
  51. assert msg(excinfo.value) == """
  52. __init__(): incompatible constructor arguments. The following argument types are supported:
  53. 1. m.factory_constructors.TestFactory1(arg0: m.factory_constructors.tag.unique_ptr_tag, arg1: int)
  54. 2. m.factory_constructors.TestFactory1(arg0: str)
  55. 3. m.factory_constructors.TestFactory1(arg0: m.factory_constructors.tag.pointer_tag)
  56. 4. m.factory_constructors.TestFactory1(arg0: handle, arg1: int, arg2: handle)
  57. Invoked with: 'invalid', 'constructor', 'arguments'
  58. """ # noqa: E501 line too long
  59. assert msg(m.TestFactory1.__init__.__doc__) == """
  60. __init__(*args, **kwargs)
  61. Overloaded function.
  62. 1. __init__(self: m.factory_constructors.TestFactory1, arg0: m.factory_constructors.tag.unique_ptr_tag, arg1: int) -> None
  63. 2. __init__(self: m.factory_constructors.TestFactory1, arg0: str) -> None
  64. 3. __init__(self: m.factory_constructors.TestFactory1, arg0: m.factory_constructors.tag.pointer_tag) -> None
  65. 4. __init__(self: m.factory_constructors.TestFactory1, arg0: handle, arg1: int, arg2: handle) -> None
  66. """ # noqa: E501 line too long
  67. def test_init_factory_casting():
  68. """Tests py::init_factory() wrapper with various upcasting and downcasting returns"""
  69. cstats = [ConstructorStats.get(c) for c in [m.TestFactory3, m.TestFactory4, m.TestFactory5]]
  70. cstats[0].alive() # force gc
  71. n_inst = ConstructorStats.detail_reg_inst()
  72. # Construction from derived references:
  73. a = m.TestFactory3(tag.pointer, tag.TF4, 4)
  74. assert a.value == "4"
  75. b = m.TestFactory3(tag.shared_ptr, tag.TF4, 5)
  76. assert b.value == "5"
  77. c = m.TestFactory3(tag.pointer, tag.TF5, 6)
  78. assert c.value == "6"
  79. d = m.TestFactory3(tag.shared_ptr, tag.TF5, 7)
  80. assert d.value == "7"
  81. assert ConstructorStats.detail_reg_inst() == n_inst + 4
  82. # Shared a lambda with TF3:
  83. e = m.TestFactory4(tag.pointer, tag.TF4, 8)
  84. assert e.value == "8"
  85. assert ConstructorStats.detail_reg_inst() == n_inst + 5
  86. assert [i.alive() for i in cstats] == [5, 3, 2]
  87. del a
  88. assert [i.alive() for i in cstats] == [4, 2, 2]
  89. assert ConstructorStats.detail_reg_inst() == n_inst + 4
  90. del b, c, e
  91. assert [i.alive() for i in cstats] == [1, 0, 1]
  92. assert ConstructorStats.detail_reg_inst() == n_inst + 1
  93. del d
  94. assert [i.alive() for i in cstats] == [0, 0, 0]
  95. assert ConstructorStats.detail_reg_inst() == n_inst
  96. assert [i.values() for i in cstats] == [
  97. ["4", "5", "6", "7", "8"],
  98. ["4", "5", "8"],
  99. ["6", "7"]
  100. ]
  101. def test_init_factory_alias():
  102. """Tests py::init_factory() wrapper with value conversions and alias types"""
  103. cstats = [m.TestFactory6.get_cstats(), m.TestFactory6.get_alias_cstats()]
  104. cstats[0].alive() # force gc
  105. n_inst = ConstructorStats.detail_reg_inst()
  106. a = m.TestFactory6(tag.base, 1)
  107. assert a.get() == 1
  108. assert not a.has_alias()
  109. b = m.TestFactory6(tag.alias, "hi there")
  110. assert b.get() == 8
  111. assert b.has_alias()
  112. c = m.TestFactory6(tag.alias, 3)
  113. assert c.get() == 3
  114. assert c.has_alias()
  115. d = m.TestFactory6(tag.alias, tag.pointer, 4)
  116. assert d.get() == 4
  117. assert d.has_alias()
  118. e = m.TestFactory6(tag.base, tag.pointer, 5)
  119. assert e.get() == 5
  120. assert not e.has_alias()
  121. f = m.TestFactory6(tag.base, tag.alias, tag.pointer, 6)
  122. assert f.get() == 6
  123. assert f.has_alias()
  124. assert ConstructorStats.detail_reg_inst() == n_inst + 6
  125. assert [i.alive() for i in cstats] == [6, 4]
  126. del a, b, e
  127. assert [i.alive() for i in cstats] == [3, 3]
  128. assert ConstructorStats.detail_reg_inst() == n_inst + 3
  129. del f, c, d
  130. assert [i.alive() for i in cstats] == [0, 0]
  131. assert ConstructorStats.detail_reg_inst() == n_inst
  132. class MyTest(m.TestFactory6):
  133. def __init__(self, *args):
  134. m.TestFactory6.__init__(self, *args)
  135. def get(self):
  136. return -5 + m.TestFactory6.get(self)
  137. # Return Class by value, moved into new alias:
  138. z = MyTest(tag.base, 123)
  139. assert z.get() == 118
  140. assert z.has_alias()
  141. # Return alias by value, moved into new alias:
  142. y = MyTest(tag.alias, "why hello!")
  143. assert y.get() == 5
  144. assert y.has_alias()
  145. # Return Class by pointer, moved into new alias then original destroyed:
  146. x = MyTest(tag.base, tag.pointer, 47)
  147. assert x.get() == 42
  148. assert x.has_alias()
  149. assert ConstructorStats.detail_reg_inst() == n_inst + 3
  150. assert [i.alive() for i in cstats] == [3, 3]
  151. del x, y, z
  152. assert [i.alive() for i in cstats] == [0, 0]
  153. assert ConstructorStats.detail_reg_inst() == n_inst
  154. assert [i.values() for i in cstats] == [
  155. ["1", "8", "3", "4", "5", "6", "123", "10", "47"],
  156. ["hi there", "3", "4", "6", "move", "123", "why hello!", "move", "47"]
  157. ]
  158. def test_init_factory_dual():
  159. """Tests init factory functions with dual main/alias factory functions"""
  160. from pybind11_tests.factory_constructors import TestFactory7
  161. cstats = [TestFactory7.get_cstats(), TestFactory7.get_alias_cstats()]
  162. cstats[0].alive() # force gc
  163. n_inst = ConstructorStats.detail_reg_inst()
  164. class PythFactory7(TestFactory7):
  165. def get(self):
  166. return 100 + TestFactory7.get(self)
  167. a1 = TestFactory7(1)
  168. a2 = PythFactory7(2)
  169. assert a1.get() == 1
  170. assert a2.get() == 102
  171. assert not a1.has_alias()
  172. assert a2.has_alias()
  173. b1 = TestFactory7(tag.pointer, 3)
  174. b2 = PythFactory7(tag.pointer, 4)
  175. assert b1.get() == 3
  176. assert b2.get() == 104
  177. assert not b1.has_alias()
  178. assert b2.has_alias()
  179. c1 = TestFactory7(tag.mixed, 5)
  180. c2 = PythFactory7(tag.mixed, 6)
  181. assert c1.get() == 5
  182. assert c2.get() == 106
  183. assert not c1.has_alias()
  184. assert c2.has_alias()
  185. d1 = TestFactory7(tag.base, tag.pointer, 7)
  186. d2 = PythFactory7(tag.base, tag.pointer, 8)
  187. assert d1.get() == 7
  188. assert d2.get() == 108
  189. assert not d1.has_alias()
  190. assert d2.has_alias()
  191. # Both return an alias; the second multiplies the value by 10:
  192. e1 = TestFactory7(tag.alias, tag.pointer, 9)
  193. e2 = PythFactory7(tag.alias, tag.pointer, 10)
  194. assert e1.get() == 9
  195. assert e2.get() == 200
  196. assert e1.has_alias()
  197. assert e2.has_alias()
  198. f1 = TestFactory7(tag.shared_ptr, tag.base, 11)
  199. f2 = PythFactory7(tag.shared_ptr, tag.base, 12)
  200. assert f1.get() == 11
  201. assert f2.get() == 112
  202. assert not f1.has_alias()
  203. assert f2.has_alias()
  204. g1 = TestFactory7(tag.shared_ptr, tag.invalid_base, 13)
  205. assert g1.get() == 13
  206. assert not g1.has_alias()
  207. with pytest.raises(TypeError) as excinfo:
  208. PythFactory7(tag.shared_ptr, tag.invalid_base, 14)
  209. assert (str(excinfo.value) ==
  210. "pybind11::init(): construction failed: returned holder-wrapped instance is not an "
  211. "alias instance")
  212. assert [i.alive() for i in cstats] == [13, 7]
  213. assert ConstructorStats.detail_reg_inst() == n_inst + 13
  214. del a1, a2, b1, d1, e1, e2
  215. assert [i.alive() for i in cstats] == [7, 4]
  216. assert ConstructorStats.detail_reg_inst() == n_inst + 7
  217. del b2, c1, c2, d2, f1, f2, g1
  218. assert [i.alive() for i in cstats] == [0, 0]
  219. assert ConstructorStats.detail_reg_inst() == n_inst
  220. assert [i.values() for i in cstats] == [
  221. ["1", "2", "3", "4", "5", "6", "7", "8", "9", "100", "11", "12", "13", "14"],
  222. ["2", "4", "6", "8", "9", "100", "12"]
  223. ]
  224. def test_no_placement_new(capture):
  225. """Prior to 2.2, `py::init<...>` relied on the type supporting placement
  226. new; this tests a class without placement new support."""
  227. with capture:
  228. a = m.NoPlacementNew(123)
  229. found = re.search(r'^operator new called, returning (\d+)\n$', str(capture))
  230. assert found
  231. assert a.i == 123
  232. with capture:
  233. del a
  234. pytest.gc_collect()
  235. assert capture == "operator delete called on " + found.group(1)
  236. with capture:
  237. b = m.NoPlacementNew()
  238. found = re.search(r'^operator new called, returning (\d+)\n$', str(capture))
  239. assert found
  240. assert b.i == 100
  241. with capture:
  242. del b
  243. pytest.gc_collect()
  244. assert capture == "operator delete called on " + found.group(1)
  245. def test_multiple_inheritance():
  246. class MITest(m.TestFactory1, m.TestFactory2):
  247. def __init__(self):
  248. m.TestFactory1.__init__(self, tag.unique_ptr, 33)
  249. m.TestFactory2.__init__(self, tag.move)
  250. a = MITest()
  251. assert m.TestFactory1.value.fget(a) == "33"
  252. assert m.TestFactory2.value.fget(a) == "(empty2)"
  253. def create_and_destroy(*args):
  254. a = m.NoisyAlloc(*args)
  255. print("---")
  256. del a
  257. pytest.gc_collect()
  258. def strip_comments(s):
  259. return re.sub(r'\s+#.*', '', s)
  260. def test_reallocations(capture, msg):
  261. """When the constructor is overloaded, previous overloads can require a preallocated value.
  262. This test makes sure that such preallocated values only happen when they might be necessary,
  263. and that they are deallocated properly"""
  264. pytest.gc_collect()
  265. with capture:
  266. create_and_destroy(1)
  267. assert msg(capture) == """
  268. noisy new
  269. noisy placement new
  270. NoisyAlloc(int 1)
  271. ---
  272. ~NoisyAlloc()
  273. noisy delete
  274. """
  275. with capture:
  276. create_and_destroy(1.5)
  277. assert msg(capture) == strip_comments("""
  278. noisy new # allocation required to attempt first overload
  279. noisy delete # have to dealloc before considering factory init overload
  280. noisy new # pointer factory calling "new", part 1: allocation
  281. NoisyAlloc(double 1.5) # ... part two, invoking constructor
  282. ---
  283. ~NoisyAlloc() # Destructor
  284. noisy delete # operator delete
  285. """)
  286. with capture:
  287. create_and_destroy(2, 3)
  288. assert msg(capture) == strip_comments("""
  289. noisy new # pointer factory calling "new", allocation
  290. NoisyAlloc(int 2) # constructor
  291. ---
  292. ~NoisyAlloc() # Destructor
  293. noisy delete # operator delete
  294. """)
  295. with capture:
  296. create_and_destroy(2.5, 3)
  297. assert msg(capture) == strip_comments("""
  298. NoisyAlloc(double 2.5) # construction (local func variable: operator_new not called)
  299. noisy new # return-by-value "new" part 1: allocation
  300. ~NoisyAlloc() # moved-away local func variable destruction
  301. ---
  302. ~NoisyAlloc() # Destructor
  303. noisy delete # operator delete
  304. """)
  305. with capture:
  306. create_and_destroy(3.5, 4.5)
  307. assert msg(capture) == strip_comments("""
  308. noisy new # preallocation needed before invoking placement-new overload
  309. noisy placement new # Placement new
  310. NoisyAlloc(double 3.5) # construction
  311. ---
  312. ~NoisyAlloc() # Destructor
  313. noisy delete # operator delete
  314. """)
  315. with capture:
  316. create_and_destroy(4, 0.5)
  317. assert msg(capture) == strip_comments("""
  318. noisy new # preallocation needed before invoking placement-new overload
  319. noisy delete # deallocation of preallocated storage
  320. noisy new # Factory pointer allocation
  321. NoisyAlloc(int 4) # factory pointer construction
  322. ---
  323. ~NoisyAlloc() # Destructor
  324. noisy delete # operator delete
  325. """)
  326. with capture:
  327. create_and_destroy(5, "hi")
  328. assert msg(capture) == strip_comments("""
  329. noisy new # preallocation needed before invoking first placement new
  330. noisy delete # delete before considering new-style constructor
  331. noisy new # preallocation for second placement new
  332. noisy placement new # Placement new in the second placement new overload
  333. NoisyAlloc(int 5) # construction
  334. ---
  335. ~NoisyAlloc() # Destructor
  336. noisy delete # operator delete
  337. """)
  338. @pytest.unsupported_on_py2
  339. def test_invalid_self():
  340. """Tests invocation of the pybind-registered base class with an invalid `self` argument. You
  341. can only actually do this on Python 3: Python 2 raises an exception itself if you try."""
  342. class NotPybindDerived(object):
  343. pass
  344. # Attempts to initialize with an invalid type passed as `self`:
  345. class BrokenTF1(m.TestFactory1):
  346. def __init__(self, bad):
  347. if bad == 1:
  348. a = m.TestFactory2(tag.pointer, 1)
  349. m.TestFactory1.__init__(a, tag.pointer)
  350. elif bad == 2:
  351. a = NotPybindDerived()
  352. m.TestFactory1.__init__(a, tag.pointer)
  353. # Same as above, but for a class with an alias:
  354. class BrokenTF6(m.TestFactory6):
  355. def __init__(self, bad):
  356. if bad == 1:
  357. a = m.TestFactory2(tag.pointer, 1)
  358. m.TestFactory6.__init__(a, tag.base, 1)
  359. elif bad == 2:
  360. a = m.TestFactory2(tag.pointer, 1)
  361. m.TestFactory6.__init__(a, tag.alias, 1)
  362. elif bad == 3:
  363. m.TestFactory6.__init__(NotPybindDerived.__new__(NotPybindDerived), tag.base, 1)
  364. elif bad == 4:
  365. m.TestFactory6.__init__(NotPybindDerived.__new__(NotPybindDerived), tag.alias, 1)
  366. for arg in (1, 2):
  367. with pytest.raises(TypeError) as excinfo:
  368. BrokenTF1(arg)
  369. assert str(excinfo.value) == "__init__(self, ...) called with invalid `self` argument"
  370. for arg in (1, 2, 3, 4):
  371. with pytest.raises(TypeError) as excinfo:
  372. BrokenTF6(arg)
  373. assert str(excinfo.value) == "__init__(self, ...) called with invalid `self` argument"