diff --git a/.github/workflows/pip.yml b/.github/workflows/pip.yml index c50f06f94..4291592ea 100644 --- a/.github/workflows/pip.yml +++ b/.github/workflows/pip.yml @@ -103,7 +103,7 @@ jobs: - uses: actions/download-artifact@v4 - name: Generate artifact attestation for sdist and wheel - uses: actions/attest-build-provenance@c4fbc648846ca6f503a13a2281a5e7b98aa57202 # v2.0.1 + uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0 with: subject-path: "*/pybind11*" diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 3e15d8d0c..853165a49 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -1366,6 +1366,31 @@ object object_or_cast(T &&o) { return pybind11::cast(std::forward(o)); } +// Declared in pytypes.h: +// Implemented here so that make_caster can be used. +template +template +str_attr_accessor object_api::attr_with_type_hint(const char *key) const { +#if !defined(__cpp_inline_variables) + static_assert(always_false::value, + "C++17 feature __cpp_inline_variables not available: " + "https://en.cppreference.com/w/cpp/language/static#Static_data_members"); +#endif + object ann = annotations(); + if (ann.contains(key)) { + throw std::runtime_error("__annotations__[\"" + std::string(key) + "\"] was set already."); + } + ann[key] = make_caster::name.text; + return {derived(), key}; +} + +template +template +obj_attr_accessor object_api::attr_with_type_hint(handle key) const { + (void) attr_with_type_hint(key.cast().c_str()); + return {derived(), reinterpret_borrow(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/conduit/pybind11_platform_abi_id.h b/include/pybind11/conduit/pybind11_platform_abi_id.h index a7733bcf5..d21fdc56d 100644 --- a/include/pybind11/conduit/pybind11_platform_abi_id.h +++ b/include/pybind11/conduit/pybind11_platform_abi_id.h @@ -12,51 +12,32 @@ #define PYBIND11_PLATFORM_ABI_ID_STRINGIFY(x) #x #define PYBIND11_PLATFORM_ABI_ID_TOSTRING(x) PYBIND11_PLATFORM_ABI_ID_STRINGIFY(x) -// On MSVC, debug and release builds are not ABI-compatible! -#if defined(_MSC_VER) && defined(_DEBUG) -# define PYBIND11_BUILD_TYPE "_debug" +#ifdef PYBIND11_COMPILER_TYPE +// // To maintain backward compatibility (see PR #5439). +# define PYBIND11_COMPILER_TYPE_LEADING_UNDERSCORE "" #else -# define PYBIND11_BUILD_TYPE "" -#endif - -// Let's assume that different compilers are ABI-incompatible. -// A user can manually set this string if they know their -// compiler is compatible. -#ifndef PYBIND11_COMPILER_TYPE -# if defined(_MSC_VER) -# define PYBIND11_COMPILER_TYPE "_msvc" -# elif defined(__INTEL_COMPILER) -# define PYBIND11_COMPILER_TYPE "_icc" -# elif defined(__clang__) -# define PYBIND11_COMPILER_TYPE "_clang" -# elif defined(__PGI) -# define PYBIND11_COMPILER_TYPE "_pgi" -# elif defined(__MINGW32__) -# define PYBIND11_COMPILER_TYPE "_mingw" +# define PYBIND11_COMPILER_TYPE_LEADING_UNDERSCORE "_" +# if defined(__MINGW32__) +# define PYBIND11_COMPILER_TYPE "mingw" # elif defined(__CYGWIN__) -# define PYBIND11_COMPILER_TYPE "_gcc_cygwin" -# elif defined(__GNUC__) -# define PYBIND11_COMPILER_TYPE "_gcc" +# define PYBIND11_COMPILER_TYPE "gcc_cygwin" +# elif defined(_MSC_VER) +# define PYBIND11_COMPILER_TYPE "msvc" +# elif defined(__clang__) || defined(__GNUC__) +# define PYBIND11_COMPILER_TYPE "system" // Assumed compatible with system compiler. # else -# define PYBIND11_COMPILER_TYPE "_unknown" +# error "Unknown PYBIND11_COMPILER_TYPE: PLEASE REVISE THIS CODE." # endif #endif -// Also standard libs +// PR #5439 made this macro obsolete. However, there are many manipulations of this macro in the +// wild. Therefore, to maintain backward compatibility, it is kept around. #ifndef PYBIND11_STDLIB -# if defined(_LIBCPP_VERSION) -# define PYBIND11_STDLIB "_libcpp" -# elif defined(__GLIBCXX__) || defined(__GLIBCPP__) -# define PYBIND11_STDLIB "_libstdcpp" -# else -# define PYBIND11_STDLIB "" -# endif +# define PYBIND11_STDLIB "" #endif #ifndef PYBIND11_BUILD_ABI -# if defined(__GXX_ABI_VERSION) // Linux/OSX. -# define PYBIND11_BUILD_ABI "_cxxabi" PYBIND11_PLATFORM_ABI_ID_TOSTRING(__GXX_ABI_VERSION) -# elif defined(_MSC_VER) // See PR #4953. +# if defined(_MSC_VER) // See PR #4953. # if defined(_MT) && defined(_DLL) // Corresponding to CL command line options /MD or /MDd. # if (_MSC_VER) / 100 == 19 # define PYBIND11_BUILD_ABI "_md_mscver19" @@ -72,17 +53,35 @@ # error "Unknown major version for MSC_VER: PLEASE REVISE THIS CODE." # endif # endif -# elif defined(__NVCOMPILER) // NVHPC (PGI-based). -# define PYBIND11_BUILD_ABI "" // TODO: What should be here, to prevent UB? +# elif defined(_LIBCPP_ABI_VERSION) // https://libcxx.llvm.org/DesignDocs/ABIVersioning.html +# define PYBIND11_BUILD_ABI \ + "_libcpp_abi" PYBIND11_PLATFORM_ABI_ID_TOSTRING(_LIBCPP_ABI_VERSION) +# elif defined(_GLIBCXX_USE_CXX11_ABI) // See PR #5439. +# if defined(__NVCOMPILER) +// // Assume that NVHPC is in the 1xxx ABI family. +// // THIS ASSUMPTION IS NOT FUTURE PROOF but apparently the best we can do. +// // Please let us know if there is a way to validate the assumption here. +# elif !defined(__GXX_ABI_VERSION) +# error \ + "Unknown platform or compiler (_GLIBCXX_USE_CXX11_ABI): PLEASE REVISE THIS CODE." +# endif +# if defined(__GXX_ABI_VERSION) && __GXX_ABI_VERSION < 1002 || __GXX_ABI_VERSION >= 2000 +# error "Unknown platform or compiler (__GXX_ABI_VERSION): PLEASE REVISE THIS CODE." +# endif +# define PYBIND11_BUILD_ABI \ + "_libstdcpp_gxx_abi_1xxx_use_cxx11_abi_" PYBIND11_PLATFORM_ABI_ID_TOSTRING( \ + _GLIBCXX_USE_CXX11_ABI) # else # error "Unknown platform or compiler: PLEASE REVISE THIS CODE." # endif #endif -#ifndef PYBIND11_INTERNALS_KIND -# define PYBIND11_INTERNALS_KIND "" +// On MSVC, debug and release builds are not ABI-compatible! +#if defined(_MSC_VER) && defined(_DEBUG) +# define PYBIND11_BUILD_TYPE "_debug" +#else +# define PYBIND11_BUILD_TYPE "" #endif #define PYBIND11_PLATFORM_ABI_ID \ - PYBIND11_INTERNALS_KIND PYBIND11_COMPILER_TYPE PYBIND11_STDLIB PYBIND11_BUILD_ABI \ - PYBIND11_BUILD_TYPE + PYBIND11_COMPILER_TYPE PYBIND11_STDLIB PYBIND11_BUILD_ABI PYBIND11_BUILD_TYPE diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index 78173cad3..c05db0e7d 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -627,6 +627,14 @@ struct instance { static_assert(std::is_standard_layout::value, "Internal error: `pybind11::detail::instance` is not standard layout!"); +// Some older compilers (e.g. gcc 9.4.0) require +// static_assert(always_false::value, "..."); +// instead of +// static_assert(false, "..."); +// to trigger the static_assert() in a template only if it is actually instantiated. +template +struct always_false : std::false_type {}; + /// from __cpp_future__ import (convenient aliases from C++14/17) #if defined(PYBIND11_CPP14) using std::conditional_t; diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index 278f35bba..5fcaf9b9c 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -272,11 +272,11 @@ struct type_info { #define PYBIND11_INTERNALS_ID \ "__pybind11_internals_v" PYBIND11_TOSTRING(PYBIND11_INTERNALS_VERSION) \ - PYBIND11_PLATFORM_ABI_ID "__" + PYBIND11_COMPILER_TYPE_LEADING_UNDERSCORE PYBIND11_PLATFORM_ABI_ID "__" #define PYBIND11_MODULE_LOCAL_ID \ "__pybind11_module_local_v" PYBIND11_TOSTRING(PYBIND11_INTERNALS_VERSION) \ - PYBIND11_PLATFORM_ABI_ID "__" + PYBIND11_COMPILER_TYPE_LEADING_UNDERSCORE PYBIND11_PLATFORM_ABI_ID "__" /// Each module locally stores a pointer to the `internals` data. The data /// itself is shared among modules with the same `PYBIND11_INTERNALS_ID`. diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 60d51fdcf..92e0a81f4 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -113,6 +113,17 @@ public: /// See above (the only difference is that the key is provided as a string literal) str_attr_accessor attr(const char *key) const; + /** \rst + Similar to the above attr functions with the difference that the templated Type + is used to set the `__annotations__` dict value to the corresponding key. Worth noting + that attr_with_type_hint is implemented in cast.h. + \endrst */ + template + obj_attr_accessor attr_with_type_hint(handle key) const; + /// See above (the only difference is that the key is provided as a string literal) + template + str_attr_accessor attr_with_type_hint(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 +193,9 @@ public: /// Get or set the object's docstring, i.e. ``obj.__doc__``. str_attr_accessor doc() const; + /// Get or set the object's annotations, i.e. ``obj.__annotations__``. + object annotations() const; + /// Return the object's current reference count ssize_t ref_count() const { #ifdef PYPY_VERSION @@ -2558,6 +2572,19 @@ str_attr_accessor object_api::doc() const { return attr("__doc__"); } +template +object object_api::annotations() const { +#if PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION <= 9 + // https://docs.python.org/3/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older + if (!hasattr(derived(), "__annotations__")) { + setattr(derived(), "__annotations__", dict()); + } + return attr("__annotations__"); +#else + return getattr(derived(), "__annotations__", dict()); +#endif +} + 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 405ff8714..005279058 100644 --- a/include/pybind11/typing.h +++ b/include/pybind11/typing.h @@ -82,6 +82,18 @@ class Optional : public object { using object::object; }; +template +class Final : public object { + PYBIND11_OBJECT_DEFAULT(Final, object, PyObject_Type) + using object::object; +}; + +template +class ClassVar : public object { + PYBIND11_OBJECT_DEFAULT(ClassVar, object, PyObject_Type) + using object::object; +}; + template class TypeGuard : public bool_ { using bool_::bool_; @@ -251,6 +263,16 @@ struct handle_type_name> { = const_name("Optional[") + as_return_type>::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("ClassVar[") + make_caster::name + const_name("]"); +}; + // TypeGuard and TypeIs use as_return_type to use the return type if available, which is usually // the narrower type. diff --git a/tests/test_pytypes.cpp b/tests/test_pytypes.cpp index 1764ccda0..b4fa99192 100644 --- a/tests/test_pytypes.cpp +++ b/tests/test_pytypes.cpp @@ -1037,6 +1037,38 @@ TEST_SUBMODULE(pytypes, m) { #else m.attr("defined_PYBIND11_TEST_PYTYPES_HAS_RANGES") = false; #endif + +#if defined(__cpp_inline_variables) + // Exercises const char* overload: + m.attr_with_type_hint>("list_int") = py::list(); + // Exercises py::handle overload: + m.attr_with_type_hint>(py::str("set_str")) = py::set(); + + struct Empty {}; + py::class_(m, "EmptyAnnotationClass"); + + struct Static {}; + auto static_class = py::class_(m, "Static"); + static_class.def(py::init()); + static_class.attr_with_type_hint>("x") = 1.0; + static_class.attr_with_type_hint>>( + "dict_str_int") + = py::dict(); + + struct Instance {}; + auto instance = py::class_(m, "Instance", py::dynamic_attr()); + instance.def(py::init()); + instance.attr_with_type_hint("y"); + + m.def("attr_with_type_hint_float_x", + [](py::handle obj) { obj.attr_with_type_hint("x"); }); + + m.attr_with_type_hint>("CONST_INT") = 3; + + m.attr("defined___cpp_inline_variables") = true; +#else + m.attr("defined___cpp_inline_variables") = false; +#endif m.def("half_of_number", [](const RealNumber &x) { return RealNumber{x.value / 2}; }); // std::vector m.def("half_of_number_vector", [](const std::vector &x) { diff --git a/tests/test_pytypes.py b/tests/test_pytypes.py index b6e64b9bf..448bfa6a8 100644 --- a/tests/test_pytypes.py +++ b/tests/test_pytypes.py @@ -1103,6 +1103,96 @@ def test_dict_ranges(tested_dict, expected): assert m.transform_dict_plus_one(tested_dict) == expected +# https://docs.python.org/3/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older +def get_annotations_helper(o): + if isinstance(o, type): + return o.__dict__.get("__annotations__", None) + return getattr(o, "__annotations__", None) + + +@pytest.mark.skipif( + not m.defined___cpp_inline_variables, + reason="C++17 feature __cpp_inline_variables not available.", +) +def test_module_attribute_types() -> None: + module_annotations = get_annotations_helper(m) + + assert module_annotations["list_int"] == "list[int]" + assert module_annotations["set_str"] == "set[str]" + + +@pytest.mark.skipif( + not m.defined___cpp_inline_variables, + reason="C++17 feature __cpp_inline_variables not available.", +) +@pytest.mark.skipif( + sys.version_info < (3, 10), + reason="get_annotations function does not exist until Python3.10", +) +def test_get_annotations_compliance() -> None: + from inspect import get_annotations + + module_annotations = get_annotations(m) + + assert module_annotations["list_int"] == "list[int]" + assert module_annotations["set_str"] == "set[str]" + + +@pytest.mark.skipif( + not m.defined___cpp_inline_variables, + reason="C++17 feature __cpp_inline_variables not available.", +) +def test_class_attribute_types() -> None: + empty_annotations = get_annotations_helper(m.EmptyAnnotationClass) + static_annotations = get_annotations_helper(m.Static) + instance_annotations = get_annotations_helper(m.Instance) + + assert empty_annotations is None + assert static_annotations["x"] == "ClassVar[float]" + assert static_annotations["dict_str_int"] == "ClassVar[dict[str, int]]" + + assert m.Static.x == 1.0 + + m.Static.x = 3.0 + static = m.Static() + assert static.x == 3.0 + + static.dict_str_int["hi"] = 3 + assert m.Static().dict_str_int == {"hi": 3} + + assert instance_annotations["y"] == "float" + instance1 = m.Instance() + instance1.y = 4.0 + + instance2 = m.Instance() + instance2.y = 5.0 + + assert instance1.y != instance2.y + + +@pytest.mark.skipif( + not m.defined___cpp_inline_variables, + reason="C++17 feature __cpp_inline_variables not available.", +) +def test_redeclaration_attr_with_type_hint() -> None: + obj = m.Instance() + m.attr_with_type_hint_float_x(obj) + assert get_annotations_helper(obj)["x"] == "float" + with pytest.raises( + RuntimeError, match=r'^__annotations__\["x"\] was set already\.$' + ): + m.attr_with_type_hint_float_x(obj) + + +@pytest.mark.skipif( + not m.defined___cpp_inline_variables, + reason="C++17 feature __cpp_inline_variables not available.", +) +def test_final_annotation() -> None: + module_annotations = get_annotations_helper(m) + assert module_annotations["CONST_INT"] == "Final[int]" + + def test_arg_return_type_hints(doc): assert doc(m.half_of_number) == "half_of_number(arg0: Union[float, int]) -> float" assert m.half_of_number(2.0) == 1.0