File: test_class_sh_property.py

package info (click to toggle)
pybind11 3.0.1-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 4,448 kB
  • sloc: cpp: 27,239; python: 13,512; ansic: 4,244; makefile: 204; sh: 36
file content (166 lines) | stat: -rw-r--r-- 6,046 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# The compact 4-character naming scheme (e.g. mptr, cptr, shcp) is explained at the top of
# test_class_sh_property.cpp.
from __future__ import annotations

import pytest

import env  # noqa: F401
from pybind11_tests import class_sh_property as m


@pytest.mark.skipif(
    "env.PYPY or env.GRAALPY", reason="gc after `del field` is apparently deferred"
)
@pytest.mark.parametrize("m_attr", ["m_valu_readonly", "m_valu_readwrite"])
def test_valu_getter(m_attr):
    # Reduced from PyCLIF test:
    # https://github.com/google/clif/blob/c371a6d4b28d25d53a16e6d2a6d97305fb1be25a/clif/testing/python/nested_fields_test.py#L56
    outer = m.Outer()
    field = getattr(outer, m_attr)
    assert field.num == -99
    with pytest.raises(ValueError) as excinfo:
        m.DisownOuter(outer)
    assert str(excinfo.value) == "Cannot disown use_count != 1 (load_as_unique_ptr)."
    del field
    m.DisownOuter(outer)
    with pytest.raises(ValueError, match="Python instance was disowned") as excinfo:
        getattr(outer, m_attr)


def test_valu_setter():
    outer = m.Outer()
    assert outer.m_valu_readonly.num == -99
    assert outer.m_valu_readwrite.num == -99
    field = m.Field()
    field.num = 35
    outer.m_valu_readwrite = field
    assert outer.m_valu_readonly.num == 35
    assert outer.m_valu_readwrite.num == 35


@pytest.mark.parametrize("m_attr", ["m_shmp", "m_shcp"])
def test_shp(m_attr):
    m_attr_readonly = m_attr + "_readonly"
    m_attr_readwrite = m_attr + "_readwrite"
    outer = m.Outer()
    assert getattr(outer, m_attr_readonly) is None
    assert getattr(outer, m_attr_readwrite) is None
    field = m.Field()
    field.num = 43
    setattr(outer, m_attr_readwrite, field)
    assert getattr(outer, m_attr_readonly).num == 43
    assert getattr(outer, m_attr_readwrite).num == 43
    getattr(outer, m_attr_readonly).num = 57
    getattr(outer, m_attr_readwrite).num = 57
    assert field.num == 57
    del field
    assert getattr(outer, m_attr_readonly).num == 57
    assert getattr(outer, m_attr_readwrite).num == 57


@pytest.mark.parametrize(
    ("field_type", "num_default", "outer_type"),
    [
        (m.ClassicField, -88, m.ClassicOuter),
        (m.Field, -99, m.Outer),
    ],
)
@pytest.mark.parametrize("m_attr", ["m_mptr", "m_cptr"])
@pytest.mark.parametrize("r_kind", ["_readonly", "_readwrite"])
def test_ptr(field_type, num_default, outer_type, m_attr, r_kind):
    m_attr_r_kind = m_attr + r_kind
    outer = outer_type()
    assert getattr(outer, m_attr_r_kind) is None
    field = field_type()
    assert field.num == num_default
    setattr(outer, m_attr + "_readwrite", field)
    assert getattr(outer, m_attr_r_kind).num == num_default
    field.num = 76
    assert getattr(outer, m_attr_r_kind).num == 76
    # Change to -88 or -99 to demonstrate Undefined Behavior (dangling pointer).
    if num_default == 88 and m_attr == "m_mptr":
        del field
    assert getattr(outer, m_attr_r_kind).num == 76


@pytest.mark.parametrize("m_attr_readwrite", ["m_uqmp_readwrite", "m_uqcp_readwrite"])
def test_uqp(m_attr_readwrite):
    outer = m.Outer()
    assert getattr(outer, m_attr_readwrite) is None
    field_orig = m.Field()
    field_orig.num = 39
    setattr(outer, m_attr_readwrite, field_orig)
    with pytest.raises(ValueError, match="Python instance was disowned"):
        _ = field_orig.num
    field_retr1 = getattr(outer, m_attr_readwrite)
    assert getattr(outer, m_attr_readwrite) is None
    assert field_retr1.num == 39
    field_retr1.num = 93
    setattr(outer, m_attr_readwrite, field_retr1)
    with pytest.raises(ValueError):
        _ = field_retr1.num
    field_retr2 = getattr(outer, m_attr_readwrite)
    assert field_retr2.num == 93


# Proof-of-concept (POC) for safe & intuitive Python access to unique_ptr members.
# The C++ member unique_ptr is disowned to a temporary Python object for accessing
# an attribute of the member. After the attribute was accessed, the Python object
# is disowned back to the C++ member unique_ptr.
# Productizing this POC is left for a future separate PR, as needed.
class unique_ptr_field_proxy_poc:
    def __init__(self, obj, field_name):
        object.__setattr__(self, "__obj", obj)
        object.__setattr__(self, "__field_name", field_name)

    def __getattr__(self, *args, **kwargs):
        return _proxy_dereference(self, getattr, *args, **kwargs)

    def __setattr__(self, *args, **kwargs):
        return _proxy_dereference(self, setattr, *args, **kwargs)

    def __delattr__(self, *args, **kwargs):
        return _proxy_dereference(self, delattr, *args, **kwargs)


def _proxy_dereference(proxy, xxxattr, *args, **kwargs):
    obj = object.__getattribute__(proxy, "__obj")
    field_name = object.__getattribute__(proxy, "__field_name")
    field = getattr(obj, field_name)  # Disowns the C++ unique_ptr member.
    assert field is not None
    try:
        return xxxattr(field, *args, **kwargs)
    finally:
        setattr(obj, field_name, field)  # Disowns the temporary Python object (field).


@pytest.mark.parametrize("m_attr", ["m_uqmp", "m_uqcp"])
def test_unique_ptr_field_proxy_poc(m_attr):
    m_attr_readwrite = m_attr + "_readwrite"
    outer = m.Outer()
    field_orig = m.Field()
    field_orig.num = 45
    setattr(outer, m_attr_readwrite, field_orig)
    field_proxy = unique_ptr_field_proxy_poc(outer, m_attr_readwrite)
    assert field_proxy.num == 45
    assert field_proxy.num == 45
    with pytest.raises(AttributeError):
        _ = field_proxy.xyz
    assert field_proxy.num == 45
    field_proxy.num = 82
    assert field_proxy.num == 82
    field_proxy = unique_ptr_field_proxy_poc(outer, m_attr_readwrite)
    assert field_proxy.num == 82
    with pytest.raises(AttributeError):
        del field_proxy.num
    assert field_proxy.num == 82


def test_readonly_char6_member():
    obj = m.WithCharArrayMember()
    assert obj.char6_member == "Char6"


def test_readonly_const_char_ptr_member():
    obj = m.WithConstCharPtrMember()
    assert obj.const_char_ptr_member == "ConstChar*"