Skip to content

fix(types): updated STL casters and py::buffer to use collections.abc - #5566

Merged
rwgk merged 16 commits into
pybind:masterfrom
timohl:stl-typing
Apr 14, 2025
Merged

fix(types): updated STL casters and py::buffer to use collections.abc#5566
rwgk merged 16 commits into
pybind:masterfrom
timohl:stl-typing

Conversation

@timohl

@timohltimohl commented Mar 16, 2025

Copy link
Copy Markdown
Contributor

Description

This updates type hints for the set, map, list and array STL casters, as well as for py::buffer, to use the more generic collections.abc types in convertible arguments.
The casters have been changed to allow types derived from those abstract base classes.

casterconvert-argreturn/noconvert-arg
set_castercollections.abc.Setset
map_castercollections.abc.Mappingdict
list_castercollections.abc.Sequencelist
array_castercollections.abc.Sequencelist

Old description:

For map_caster this is exactly how the caster works.
Unfortunately, list_caster, set_caster and array_caster work a bit different regarding noconvert:
For args list_caster and array_caster always allow sequence and set_caster always allows anyset, but all three allow iterable in convert mode.

The current system only differs between input and output type (io_name) and falls back to the output type in noconvert args.
These casters' behavior would require three different type hints: arg-convert, arg-noconvert, return.
Therefore, I currently see no way to improve these type hints further without deeper changes.
So for now, I think this should be a good compromise for most use cases.

During testing I found that map_caster and set_caster did not always allow for types derived from collections.abc.Mapping and collections.abc.Set since the checks contained additional restictions (like having an items() method for mappings), while the list caster already allowed types derived from collections.abc.Sequence.
This PR changes the behavior of those casters to allow types derived from those base classes.

Additionally, the array_caster was updated to match the typing.Annotated style of numpy/eigen type hints.

As suggested by @InvincibleRMC, the type hint Buffer was changed to collections.abc.Buffer since Buffer does not exist in the typing module.

Resolves#5498

Suggested changelog entry:

Added support for collections.abc in type hints and convertible checks of STL casters and py::buffer

@timohl

Copy link
Copy Markdown
ContributorAuthor

The failing check is unrelated, I think (maybe rerun is enough).

@timohl

Copy link
Copy Markdown
ContributorAuthor

After seeing #5498 and digging deeper into the caster code, I noticed that I have to think more about this.

These three functions restrict the casters further than I thought:

inlineboolPyObjectTypeIsConvertibleToStdVector(PyObject *obj) {
if (PySequence_Check(obj) != 0) {
return !PyUnicode_Check(obj) && !PyBytes_Check(obj);
}
return (PyGen_Check(obj) != 0) || (PyAnySet_Check(obj) != 0)
|| PyObjectIsInstanceWithOneOfTpNames(
obj, {"dict_keys", "dict_values", "dict_items", "map", "zip"});
}
inlineboolPyObjectTypeIsConvertibleToStdSet(PyObject *obj) {
return (PyAnySet_Check(obj) != 0) || PyObjectIsInstanceWithOneOfTpNames(obj, {"dict_keys"});
}
inlineboolPyObjectTypeIsConvertibleToStdMap(PyObject *obj) {
if (PyDict_Check(obj)) {
returntrue;
}
// Implicit requirement in the conditions below:
// A type with `.__getitem__()` & `.items()` methods must implement these
// to be compatible with https://docs.python.org/3/c-api/mapping.html
if (PyMapping_Check(obj) == 0) {
returnfalse;
}
PyObject *items = PyObject_GetAttrString(obj, "items");
if (items == nullptr) {
PyErr_Clear();
returnfalse;
}
bool is_convertible = (PyCallable_Check(items) != 0);
Py_DECREF(items);
return is_convertible;
}

For example, it requires the mapping to be of type or subtype of set or frozenset or have dict_keys if I understand correctly.
The caster itself uses the Mapping protocol though and could easily be changed to fully allow it.
I will add some tests to better map out what is allowed and what not and how this relates to the type hints.

Git blame directed me to #4686, which seems to have more insight.
@rwgk if you remember this PR, I would love to hear your view.
If not, I will dig into this PR on the weekend and summarize my findings here.

@rwgk

rwgk commented Mar 18, 2025

Copy link
Copy Markdown
Collaborator

@timohl Did you see these already?

These three functions restrict the casters further than I thought:

I'm fine if you want to work on those functions. I'm thinking it's best to keep the current logic, which is super fast, but where we're currently returning false, add additional sophisticated conditions as needed.

@timohl
timohl marked this pull request as draft March 18, 2025 15:25
Comment threadinclude/pybind11/stl.h Outdated

@rwgkrwgk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only glanced through very quickly. Is this still in draft mode intentionally?

Comment threadinclude/pybind11/stl.h Outdated
@timohl

Copy link
Copy Markdown
ContributorAuthor

I only glanced through very quickly. Is this still in draft mode intentionally?

I would like to improve the comments before finalizing and being ready to merge.
Unfortunately, I was pretty busy the last couple of days and could not find enough time.
I can probably get back to it tomorrow.

Also, your comment about the function names sounds good. I will change that.

@rwgk

rwgk commented Mar 25, 2025

Copy link
Copy Markdown
Collaborator

No rush, at all, from my end. I just wanted to be sure you're not waiting for my feedback.

@InvincibleRMC

Copy link
Copy Markdown
Contributor

Would it also be possible to update Buffer type to collections.abc.Buffer? More info here.

@timohl

Copy link
Copy Markdown
ContributorAuthor

Would it also be possible to update Buffer type to collections.abc.Buffer? More info here.

Going through the code here:

template <>
structhandle_type_name<object> {
staticconstexprauto name = const_name("object");
};
template <>
structhandle_type_name<list> {
staticconstexprauto name = const_name("list");
};
template <>
structhandle_type_name<dict> {
staticconstexprauto name = const_name("dict");
};
template <>
structhandle_type_name<anyset> {
staticconstexprauto name = const_name("Union[set, frozenset]");
};
template <>
structhandle_type_name<set> {
staticconstexprauto name = const_name("set");
};
template <>
structhandle_type_name<frozenset> {
staticconstexprauto name = const_name("frozenset");
};
template <>
structhandle_type_name<str> {
staticconstexprauto name = const_name("str");
};
template <>
structhandle_type_name<tuple> {
staticconstexprauto name = const_name("tuple");
};
template <>
structhandle_type_name<bool_> {
staticconstexprauto name = const_name("bool");
};
template <>
structhandle_type_name<bytes> {
staticconstexprauto name = const_name(PYBIND11_BYTES_NAME);
};
template <>
structhandle_type_name<buffer> {
staticconstexprauto name = const_name("Buffer");
};
template <>
structhandle_type_name<int_> {
staticconstexprauto name = io_name("typing.SupportsInt", "int");
};
template <>
structhandle_type_name<iterable> {
staticconstexprauto name = const_name("Iterable");
};
template <>
structhandle_type_name<iterator> {
staticconstexprauto name = const_name("Iterator");
};
template <>
structhandle_type_name<float_> {
staticconstexprauto name = io_name("typing.SupportsFloat", "float");
};
template <>
structhandle_type_name<function> {
staticconstexprauto name = const_name("Callable");
};
template <>
structhandle_type_name<handle> {
staticconstexprauto name = handle_type_name<object>::name;
};
template <>
structhandle_type_name<none> {
staticconstexprauto name = const_name("None");
};
template <>
structhandle_type_name<sequence> {
staticconstexprauto name = const_name("Sequence");
};
template <>
structhandle_type_name<bytearray> {
staticconstexprauto name = const_name("bytearray");
};
template <>
structhandle_type_name<memoryview> {
staticconstexprauto name = const_name("memoryview");
};
template <>
structhandle_type_name<slice> {
staticconstexprauto name = const_name("slice");
};
template <>
structhandle_type_name<type> {
staticconstexprauto name = const_name("type");
};
template <>
structhandle_type_name<capsule> {
staticconstexprauto name = const_name("types.CapsuleType");
};
template <>
structhandle_type_name<ellipsis> {
staticconstexprauto name = const_name("ellipsis");
};
template <>
structhandle_type_name<weakref> {
staticconstexprauto name = const_name("weakref");
};

There are a bunch of other types that could be changed:

  • Union[set, frozenset] -> typing.Union[set, frozenset] or maybe better set | frozenset
  • Buffer -> collections.abc.Buffer
  • Iterable -> collections.abc.Iterable
  • Iterator -> collections.abc.Iterator
  • Callable -> collections.abc.Callable
  • Sequence -> collections.abc.Sequence
  • ellipsis -> types.EllipsisType
    @InvincibleRMC Would you agree with those? Am I missing some?

@InvincibleRMC

InvincibleRMC commented Mar 26, 2025

Copy link
Copy Markdown
Contributor

Currently stub generators typically know that Iterable and the other types are available in the typing module. However, this doesn't apply to Buffer since it does not exist in the typing module. If we determine it is better to make all the types explicit (in the form of foo.bar.Baz) we should also update all the types found in typing.h.

@timohl

Copy link
Copy Markdown
ContributorAuthor

I have changed Buffer to collections.abc.Buffer.
If all deprecated types from typing (or without any module reference) should be changed to collections.abc.*, this should probably go into a separate PR.

The convertible check functions now contain some comments to make it more obvious what is allowed and what not.
Those functions have also been renamed to object_is_*.

I removed explicit checks for methods required by the collections.abc base classes (e.g., __getitem__, since they already check for those methods on instantiation (see quick test in interactive Python):

classFakeSeq(collections.abc.Sequence): ...
a=FakeSeq()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class FakeSeq with abstract methods __getitem__, __len__

@timohl
timohl marked this pull request as ready for review April 4, 2025 22:26
@timohl
timohl requested a review from rwgkApril 4, 2025 22:53
@timohl

Copy link
Copy Markdown
ContributorAuthor

I have just updated the PR description as well.

@timohltimohl changed the title Updated STL type hints to use collections.abcUpdated STL casters and py::buffer to use collections.abcApr 4, 2025
@rwgkrwgk mentioned this pull request Apr 10, 2025

@rwgkrwgk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The production code is great as-is! I only have suggestions for simple changes to the test code.

Here is another small suggestion generated by ChatGPT:

The PR includes comments explaining the rationale behind the changes, particularly the shift to using collections.abc. These comments are clear and provide valuable context. However, ensuring consistency in terminology (e.g., consistently referring to collections.abc.Set rather than alternating with collections.Set) would enhance clarity.

Comment threadtests/test_stl.py Outdated
Comment threadtests/test_stl.cpp
Comment threadtests/test_stl.py Outdated
"a": 1,
"b": 2,
"c": 3,
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be nice to keep this more compact via

 a1b2c3 = {"a": 1, "b": 2, "c": 3}

and then reuse three times.

@timohltimohlApr 13, 2025

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in 6fc2ab3

I just made this change for the mapping test.
In the sequence test, there is now:

assertm.roundtrip_std_vector_int_noconvert(FormalSequenceLike(1, 2, 3)) == [
1,
2,
3,
]

Should I change this to use a variable as well (like list123)?

@timohl

Copy link
Copy Markdown
ContributorAuthor

The production code is great as-is! I only have suggestions for simple changes to the test code.

Thanks for the review. I added some commits to address your comments.

Here is another small suggestion generated by ChatGPT:

* https://chatgpt.com/share/67fb2d28-21d4-8008-bfea-597507977bfb

The PR includes comments explaining the rationale behind the changes, particularly the shift to using collections.abc. These comments are clear and provide valuable context. However, ensuring consistency in terminology (e.g., consistently referring to collections.abc.Set rather than alternating with collections.Set) would enhance clarity.

I am not sure though what chatgpt means here.
Searching for the term "collections" in the entire code base, I could not find any inconsistency in terminology.
Either collections.abc is used correctly or the term is used in another context as far as I can see.

@rwgkrwgk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure though what chatgpt means here.
Searching for the term "collections" in the entire code base, I could not find any inconsistency in terminology.
Either collections.abc is used correctly or the term is used in another context as far as I can see.

Sorry, I didn't double-check the ChatGPT finding; must be a hallucination then. Thanks for checking!

@rwgk
rwgk merged commit ee04df0 into pybind:masterApr 14, 2025
@github-actionsgithub-actionsBot added the needs changelog Possibly needs a changelog entry label Apr 14, 2025
@gentlegiantJGC

Copy link
Copy Markdown
Contributor

@timohl@InvincibleRMC@rwgk
collections.abc.Buffer was only added in Python 3.12 according to the documentation.
https://docs.python.org/3/library/collections.abc.html#collections.abc.Buffer
From my understanding pybind11 supports versions older than that.
Is it possible to use collections.abc.Buffer for 3.12+ and typing_extensions.Buffer for versions older than that?
I don't know how this information could be accessed at compile time.

@timohl

timohl commented May 12, 2025

Copy link
Copy Markdown
ContributorAuthor

collections.abc.Buffer is only used in type annotations.
With PEP 563 Postponed evaluation of annotations (from __future__ import annotations) this should not raise any error at runtime.

Tested in Python 3.8:

>>>importcollections.abc>>>defb(x: collections.abc.Buffer) ->float:
... return4
... Traceback (mostrecentcalllast):
File"<stdin>", line1, in<module>AttributeError: module'collections.abc'hasnoattribute'Buffer'>>>from __future__ importannotations>>>defb(x: collections.abc.Buffer) ->float:
... return4
... >>>

Stubs generated by pybind11-stubgen contain from __future__ import annotations, so those should work with older Python versions as well.

Do you have a use case or tool where this creates a problem?
Otherwise, I would prefer not adding any additional version checks (especially since typing.Annotated is used a lot and was introduced in Python 3.9 while pybind11 still supports 3.8)

@gentlegiantJGC

Copy link
Copy Markdown
Contributor

You are correct that there are no runtime issues but a static type checker in 3.11 would error because collections.abc.Buffer does not exist in that version.
Is there a way to check which python version is being compiled against and switch the behaviour based on that?

@timohl

Copy link
Copy Markdown
ContributorAuthor

Ok, I see. Pylance and mypy are complaining, that is true.

nanobind uses a central place to define typing types depending on the Python version:
https://github.com/wjakob/nanobind/blob/62fc996018d9ea4d51af9c86cf008c2562b4eeab/include/nanobind/nb_defs.h#L95-L127

Maybe something similar would be good instead of having version checks all over the place.

@gentlegiantJGC

Copy link
Copy Markdown
Contributor

My personal preference would be to keep it where it gets used unless it is used in multiple places but I am not a developer here.
I think this issue applies to a number of other type hints as well.

@henryiiihenryiii changed the title Updated STL casters and py::buffer to use collections.abcfix(types): updated STL casters and py::buffer to use collections.abcMay 17, 2025
@henryiiihenryiii removed the needs changelog Possibly needs a changelog entry label May 17, 2025
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: STL casters should probably type cast from collections.abc

5 participants

@timohl@rwgk@InvincibleRMC@gentlegiantJGC@henryiii