Make stl.h list|set|map_caster more user friendly. (#4686)

* Add `test_pass_std_vector_int()`, `test_pass_std_set_int()` in test_stl

* Change `list_caster` to also accept generator objects (`PyGen_Check(src.ptr()`).

Note for completeness: This is a more conservative change than https://github.com/google/pywrapcc/pull/30042

* Drop in (currently unpublished) PyCLIF code, use in `list_caster`, adjust tests.

* Use `PyObjectTypeIsConvertibleToStdSet()` in `set_caster`, adjust tests.

* Use `PyObjectTypeIsConvertibleToStdMap()` in `map_caster`, add tests.

* Simplify `list_caster` `load()` implementation, push str/bytes check into `PyObjectTypeIsConvertibleToStdVector()`.

* clang-tidy cleanup with a few extra `(... != 0)` to be more consistent.

* Also use `PyObjectTypeIsConvertibleToStdVector()` in `array_caster`.

* Update comment pointing to clif/python/runtime.cc (code is unchanged).

* Comprehensive test coverage, enhanced set_caster load implementation.

* Resolve clang-tidy eror.

* Add a long C++ comment explaining what led to the `PyObjectTypeIsConvertibleTo*()` implementations.

* Minor function name change in test.

* strcmp -> std::strcmp (thanks @Skylion007 for catching this)

* Add `PyCallable_Check(items)` in `PyObjectTypeIsConvertibleToStdMap()`

* Resolve clang-tidy error

* Use `PyMapping_Items()` instead of `src.attr("items")()`, to be internally consistent with `PyMapping_Check()`

* Update link to PyCLIF sources.

* Fix typo (thanks @wangxf123456 for catching this)

* Add `test_pass_std_vector_int()`, `test_pass_std_set_int()` in test_stl

* Change `list_caster` to also accept generator objects (`PyGen_Check(src.ptr()`).

Note for completeness: This is a more conservative change than https://github.com/google/pywrapcc/pull/30042

* Drop in (currently unpublished) PyCLIF code, use in `list_caster`, adjust tests.

* Use `PyObjectTypeIsConvertibleToStdSet()` in `set_caster`, adjust tests.

* Use `PyObjectTypeIsConvertibleToStdMap()` in `map_caster`, add tests.

* Simplify `list_caster` `load()` implementation, push str/bytes check into `PyObjectTypeIsConvertibleToStdVector()`.

* clang-tidy cleanup with a few extra `(... != 0)` to be more consistent.

* Also use `PyObjectTypeIsConvertibleToStdVector()` in `array_caster`.

* Update comment pointing to clif/python/runtime.cc (code is unchanged).

* Comprehensive test coverage, enhanced set_caster load implementation.

* Resolve clang-tidy eror.

* Add a long C++ comment explaining what led to the `PyObjectTypeIsConvertibleTo*()` implementations.

* Minor function name change in test.

* strcmp -> std::strcmp (thanks @Skylion007 for catching this)

* Add `PyCallable_Check(items)` in `PyObjectTypeIsConvertibleToStdMap()`

* Resolve clang-tidy error

* Use `PyMapping_Items()` instead of `src.attr("items")()`, to be internally consistent with `PyMapping_Check()`

* Update link to PyCLIF sources.

* Fix typo (thanks @wangxf123456 for catching this)

* Fix typo discovered by new version of codespell.
This commit is contained in:
Ralf W. Grosse-Kunstleve
2024-08-14 01:42:51 +07:00
committed by GitHub
parent 4a06eca591
commit 0d44d720cb
3 changed files with 338 additions and 34 deletions

View File

@@ -381,3 +381,129 @@ def test_return_vector_bool_raw_ptr():
v = m.return_vector_bool_raw_ptr()
assert isinstance(v, list)
assert len(v) == 4513
@pytest.mark.parametrize(
("fn", "offset"), [(m.pass_std_vector_int, 0), (m.pass_std_array_int_2, 1)]
)
def test_pass_std_vector_int(fn, offset):
assert fn([7, 13]) == 140 + offset
assert fn({6, 2}) == 116 + offset
assert fn({"x": 8, "y": 11}.values()) == 138 + offset
assert fn({3: None, 9: None}.keys()) == 124 + offset
assert fn(i for i in [4, 17]) == 142 + offset
assert fn(map(lambda i: i * 3, [8, 7])) == 190 + offset # noqa: C417
with pytest.raises(TypeError):
fn({"x": 0, "y": 1})
with pytest.raises(TypeError):
fn({})
def test_pass_std_vector_pair_int():
fn = m.pass_std_vector_pair_int
assert fn({1: 2, 3: 4}.items()) == 406
assert fn(zip([5, 17], [13, 9])) == 2222
def test_list_caster_fully_consumes_generator_object():
def gen_invalid():
yield from [1, 2.0, 3]
gen_obj = gen_invalid()
with pytest.raises(TypeError):
m.pass_std_vector_int(gen_obj)
assert not tuple(gen_obj)
def test_pass_std_set_int():
fn = m.pass_std_set_int
assert fn({3, 15}) == 254
assert fn({5: None, 12: None}.keys()) == 251
with pytest.raises(TypeError):
fn([])
with pytest.raises(TypeError):
fn({})
with pytest.raises(TypeError):
fn({}.values())
with pytest.raises(TypeError):
fn(i for i in [])
def test_set_caster_dict_keys_failure():
dict_keys = {1: None, 2.0: None, 3: None}.keys()
# The asserts does not really exercise anything in pybind11, but if one of
# them fails in some future version of Python, the set_caster load
# implementation may need to be revisited.
assert tuple(dict_keys) == (1, 2.0, 3)
assert tuple(dict_keys) == (1, 2.0, 3)
with pytest.raises(TypeError):
m.pass_std_set_int(dict_keys)
assert tuple(dict_keys) == (1, 2.0, 3)
class FakePyMappingMissingItems:
def __getitem__(self, _):
raise RuntimeError("Not expected to be called.")
class FakePyMappingWithItems(FakePyMappingMissingItems):
def items(self):
return ((1, 3), (2, 4))
class FakePyMappingBadItems(FakePyMappingMissingItems):
def items(self):
return ((1, 2), (3, "x"))
class FakePyMappingItemsNotCallable(FakePyMappingMissingItems):
@property
def items(self):
return ((1, 2), (3, 4))
class FakePyMappingItemsWithArg(FakePyMappingMissingItems):
def items(self, _):
return ((1, 2), (3, 4))
class FakePyMappingGenObj(FakePyMappingMissingItems):
def __init__(self, gen_obj):
super().__init__()
self.gen_obj = gen_obj
def items(self):
yield from self.gen_obj
def test_pass_std_map_int():
fn = m.pass_std_map_int
assert fn({1: 2, 3: 4}) == 4506
with pytest.raises(TypeError):
fn([])
assert fn(FakePyMappingWithItems()) == 3507
with pytest.raises(TypeError):
fn(FakePyMappingMissingItems())
with pytest.raises(TypeError):
fn(FakePyMappingBadItems())
with pytest.raises(TypeError):
fn(FakePyMappingItemsNotCallable())
with pytest.raises(TypeError):
fn(FakePyMappingItemsWithArg())
@pytest.mark.parametrize(
("items", "expected_exception"),
[
(((1, 2), (3, "x"), (4, 5)), TypeError),
(((1, 2), (3, 4, 5), (6, 7)), ValueError),
],
)
def test_map_caster_fully_consumes_generator_object(items, expected_exception):
def gen_invalid():
yield from items
gen_obj = gen_invalid()
with pytest.raises(expected_exception):
m.pass_std_map_int(FakePyMappingGenObj(gen_obj))
assert not tuple(gen_obj)