Skip to content

Commit

Permalink
Merge branch 'master' into sh_merge_master
Browse files Browse the repository at this point in the history
  • Loading branch information
rwgk committed Dec 22, 2024
2 parents 58042d4 + cf020a1 commit 3c85ffc
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 0 deletions.
25 changes: 25 additions & 0 deletions include/pybind11/cast.h
Original file line number Diff line number Diff line change
Expand Up @@ -1649,6 +1649,31 @@ object object_or_cast(T &&o) {
return pybind11::cast(std::forward<T>(o));
}

// Declared in pytypes.h:
// Implemented here so that make_caster<T> can be used.
template <typename D>
template <typename T>
str_attr_accessor object_api<D>::attr_with_type_hint(const char *key) const {
#if !defined(__cpp_inline_variables)
static_assert(always_false<T>::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<T>::name.text;
return {derived(), key};
}

template <typename D>
template <typename T>
obj_attr_accessor object_api<D>::attr_with_type_hint(handle key) const {
(void) attr_with_type_hint<T>(key.cast<std::string>().c_str());
return {derived(), reinterpret_borrow<object>(key)};
}

// Placeholder type for the unneeded (and dead code) static variable in the
// PYBIND11_OVERRIDE_OVERRIDE macro
struct override_unused {};
Expand Down
8 changes: 8 additions & 0 deletions include/pybind11/detail/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,14 @@ struct instance {
static_assert(std::is_standard_layout<instance>::value,
"Internal error: `pybind11::detail::instance` is not standard layout!");

// Some older compilers (e.g. gcc 9.4.0) require
// static_assert(always_false<T>::value, "...");
// instead of
// static_assert(false, "...");
// to trigger the static_assert() in a template only if it is actually instantiated.
template <typename>
struct always_false : std::false_type {};

/// from __cpp_future__ import (convenient aliases from C++14/17)
#if defined(PYBIND11_CPP14)
using std::conditional_t;
Expand Down
27 changes: 27 additions & 0 deletions include/pybind11/pytypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,17 @@ class object_api : public pyobject_tag {
/// 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 <typename T>
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 <typename T>
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
Expand Down Expand Up @@ -182,6 +193,9 @@ class object_api : public pyobject_tag {
/// 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
Expand Down Expand Up @@ -2558,6 +2572,19 @@ str_attr_accessor object_api<D>::doc() const {
return attr("__doc__");
}

template <typename D>
object object_api<D>::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 <typename D>
handle object_api<D>::get_type() const {
return type::handle_of(derived());
Expand Down
22 changes: 22 additions & 0 deletions include/pybind11/typing.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ class Optional : public object {
using object::object;
};

template <typename T>
class Final : public object {
PYBIND11_OBJECT_DEFAULT(Final, object, PyObject_Type)
using object::object;
};

template <typename T>
class ClassVar : public object {
PYBIND11_OBJECT_DEFAULT(ClassVar, object, PyObject_Type)
using object::object;
};

template <typename T>
class TypeGuard : public bool_ {
using bool_::bool_;
Expand Down Expand Up @@ -251,6 +263,16 @@ struct handle_type_name<typing::Optional<T>> {
= const_name("Optional[") + as_return_type<make_caster<T>>::name + const_name("]");
};

template <typename T>
struct handle_type_name<typing::Final<T>> {
static constexpr auto name = const_name("Final[") + make_caster<T>::name + const_name("]");
};

template <typename T>
struct handle_type_name<typing::ClassVar<T>> {
static constexpr auto name = const_name("ClassVar[") + make_caster<T>::name + const_name("]");
};

// TypeGuard and TypeIs use as_return_type to use the return type if available, which is usually
// the narrower type.

Expand Down
32 changes: 32 additions & 0 deletions tests/test_pytypes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<py::typing::List<int>>("list_int") = py::list();
// Exercises py::handle overload:
m.attr_with_type_hint<py::typing::Set<py::str>>(py::str("set_str")) = py::set();

struct Empty {};
py::class_<Empty>(m, "EmptyAnnotationClass");

struct Static {};
auto static_class = py::class_<Static>(m, "Static");
static_class.def(py::init());
static_class.attr_with_type_hint<py::typing::ClassVar<float>>("x") = 1.0;
static_class.attr_with_type_hint<py::typing::ClassVar<py::typing::Dict<py::str, int>>>(
"dict_str_int")
= py::dict();

struct Instance {};
auto instance = py::class_<Instance>(m, "Instance", py::dynamic_attr());
instance.def(py::init());
instance.attr_with_type_hint<float>("y");

m.def("attr_with_type_hint_float_x",
[](py::handle obj) { obj.attr_with_type_hint<float>("x"); });

m.attr_with_type_hint<py::typing::Final<int>>("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<T>
m.def("half_of_number_vector", [](const std::vector<RealNumber> &x) {
Expand Down
90 changes: 90 additions & 0 deletions tests/test_pytypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 3c85ffc

Please sign in to comment.