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