diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 2ae25c2eb..ebc1d1596 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -1329,6 +1329,15 @@ object object_or_cast(T &&o) { return pybind11::cast(std::forward(o)); } +// Declared in pytypes.h: +// Written here so make_caster can be used +template +template +str_attr_accessor object_api::attr_with_type(const char *key) const { + annotations()[key] = make_caster::name.text; + return {derived(), key}; +}; + // Placeholder type for the unneeded (and dead code) static variable in the // PYBIND11_OVERRIDE_OVERRIDE macro struct override_unused {}; diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 60d51fdcf..93c703326 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -113,6 +113,10 @@ public: /// See above (the only difference is that the key is provided as a string literal) str_attr_accessor attr(const char *key) const; + // attr_with_type is implemented in cast.h: + template + str_attr_accessor attr_with_type(const char *key) const; + /** \rst Matches * unpacking in Python, e.g. to unpack arguments out of a ``tuple`` or ``list`` for a function call. Applying another * to the result yields @@ -182,6 +186,9 @@ public: /// Get or set the object's docstring, i.e. ``obj.__doc__``. str_attr_accessor doc() const; + // TODO: Make read only? + str_attr_accessor annotations() const; + /// Return the object's current reference count ssize_t ref_count() const { #ifdef PYPY_VERSION @@ -2558,6 +2565,16 @@ str_attr_accessor object_api::doc() const { return attr("__doc__"); } +template +str_attr_accessor object_api::annotations() const { + str_attr_accessor annotations_dict = attr("__annotations__"); + // Create dict automatically + if (!isinstance(annotations_dict)){ + annotations_dict = dict(); + } + return annotations_dict; +} + template handle object_api::get_type() const { return type::handle_of(derived()); diff --git a/include/pybind11/typing.h b/include/pybind11/typing.h index 84aaf9f70..d8ed68d23 100644 --- a/include/pybind11/typing.h +++ b/include/pybind11/typing.h @@ -82,6 +82,12 @@ class Optional : public object { using object::object; }; +template +class Final : public object { + PYBIND11_OBJECT_DEFAULT(Final, object, PyObject_Type) + using object::object; +}; + template class TypeGuard : public bool_ { using bool_::bool_; @@ -205,6 +211,11 @@ struct handle_type_name> { static constexpr auto name = const_name("Optional[") + make_caster::name + const_name("]"); }; +template +struct handle_type_name> { + static constexpr auto name = const_name("Final[") + make_caster::name + const_name("]"); +}; + template struct handle_type_name> { static constexpr auto name = const_name("TypeGuard[") + make_caster::name + const_name("]"); diff --git a/tests/test_pytypes.cpp b/tests/test_pytypes.cpp index 8df4cdd3f..18d327f87 100644 --- a/tests/test_pytypes.cpp +++ b/tests/test_pytypes.cpp @@ -998,4 +998,22 @@ TEST_SUBMODULE(pytypes, m) { #else m.attr("defined_PYBIND11_TEST_PYTYPES_HAS_RANGES") = false; #endif + + m.attr_with_type>("list_int") = py::list(); + m.attr_with_type>("set_str") = py::set(); + + + struct Empty {}; + py::class_(m, "EmptyAnnotationClass"); + + struct Point { + float x; + py::dict dict_str_int; + }; + auto point = py::class_(m, "Point"); + point.attr_with_type("x"); + point.attr_with_type>("dict_str_int") = py::dict(); + + m.attr_with_type>("CONST_INT") = 3; + } diff --git a/tests/test_pytypes.py b/tests/test_pytypes.py index 9fd24b34f..741a34570 100644 --- a/tests/test_pytypes.py +++ b/tests/test_pytypes.py @@ -1101,3 +1101,23 @@ def test_list_ranges(tested_list, expected): def test_dict_ranges(tested_dict, expected): assert m.dict_iterator_default_initialization() assert m.transform_dict_plus_one(tested_dict) == expected + + +def test_module_attribute_types() -> None: + module_annotations = m.__annotations__ + + assert module_annotations['list_int'] == 'list[int]' + assert module_annotations['set_str'] == 'set[str]' + + +def test_class_attribute_types() -> None: + empty_annotations = m.EmptyAnnotationClass.__annotations__ + annotations = m.Point.__annotations__ + + assert empty_annotations == {} + assert annotations['x'] == 'float' + assert annotations['dict_str_int'] == 'dict[str, int]' + +def test_final_annotation() -> None: + module_annotations = m.__annotations__ + assert module_annotations['CONST_INT'] == 'Final[int]'