Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(types) C++ int and bool Literals in Python Literal Type #5463

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions include/pybind11/detail/descr.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,19 @@ constexpr enable_if_t<!B, T2> const_name(const T1 &, const T2 &d) {
return d;
}

#if defined(PYBIND11_CPP17)

template <auto Bool,
typename std::enable_if<std::is_same<decltype(Bool), bool>::value, int>::type = 0>
auto constexpr const_name() {
return const_name<Bool>("True", "False");
}

template <auto Size,
typename std::enable_if<!std::is_same<decltype(Size), bool>::value, int>::type = 0>
#else
template <size_t Size>
#endif
auto constexpr const_name() -> remove_cv_t<decltype(int_to_str<Size / 10, Size % 10>::digits)> {
return int_to_str<Size / 10, Size % 10>::digits;
}
Expand Down Expand Up @@ -126,10 +138,23 @@ constexpr enable_if_t<!B, T2> _(const T1 &d1, const T2 &d2) {
return const_name<B, T1, T2>(d1, d2);
}

# if defined(PYBIND11_CPP17)

template <auto Bool,
typename std::enable_if<std::is_same<decltype(Bool), bool>::value, int>::type = 0>
auto constexpr _() {
return const_name<Bool>();
}

template <auto Size,
typename std::enable_if<!std::is_same<decltype(Size), bool>::value, int>::type = 0>
# else
template <size_t Size>
# endif
auto constexpr _() -> remove_cv_t<decltype(int_to_str<Size / 10, Size % 10>::digits)> {
return const_name<Size>();
}

template <typename Type>
constexpr descr<1, Type> _() {
return const_name<Type>();
Expand Down
35 changes: 24 additions & 11 deletions include/pybind11/typing.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,17 +102,14 @@ class Never : public none {

#if defined(__cpp_nontype_template_args) && __cpp_nontype_template_args >= 201911L
# define PYBIND11_TYPING_H_HAS_STRING_LITERAL

// Used for TypeVars and Strings, bytes, Enums, None for Literal object
template <size_t N>
struct StringLiteral {
constexpr StringLiteral(const char (&str)[N]) { std::copy_n(str, N, name); }
char name[N];
};

template <StringLiteral... StrLits>
class Literal : public object {
PYBIND11_OBJECT_DEFAULT(Literal, object, PyObject_Type)
};

// Example syntax for creating a TypeVar.
// typedef typing::TypeVar<"T"> TypeVarT;
template <StringLiteral>
Expand All @@ -122,6 +119,13 @@ class TypeVar : public object {
};
#endif

#if defined(PYBIND11_CPP17)
template <auto... Literals>
class Literal : public object {
PYBIND11_OBJECT_DEFAULT(Literal, object, PyObject_Type)
};
#endif

PYBIND11_NAMESPACE_END(typing)

PYBIND11_NAMESPACE_BEGIN(detail)
Expand Down Expand Up @@ -277,16 +281,25 @@ struct handle_type_name<typing::Never> {
};

#if defined(PYBIND11_TYPING_H_HAS_STRING_LITERAL)
template <typing::StringLiteral... Literals>
template <auto StrLit,
typename std::enable_if<!std::is_integral<decltype(StrLit)>::value, int>::type = 0>
auto constexpr const_name() {
return const_name(StrLit.name);
}

template <typing::StringLiteral StrLit>
struct handle_type_name<typing::TypeVar<StrLit>> {
static constexpr auto name = const_name<StrLit>();
};
#endif

#if defined(PYBIND11_CPP17)
template <auto... Literals>
struct handle_type_name<typing::Literal<Literals...>> {
static constexpr auto name = const_name("Literal[")
+ pybind11::detail::concat(const_name(Literals.name)...)
+ pybind11::detail::concat(const_name<Literals>()...)
+ const_name("]");
};
template <typing::StringLiteral StrLit>
struct handle_type_name<typing::TypeVar<StrLit>> {
static constexpr auto name = const_name(StrLit.name);
};
#endif

PYBIND11_NAMESPACE_END(detail)
Expand Down
28 changes: 19 additions & 9 deletions tests/test_pytypes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,14 @@ void m_defs(py::module_ &m) {
namespace literals {
enum Color { RED = 0, BLUE = 1 };

typedef py::typing::Literal<"26",
"0x1A",
"\"hello world\"",
"b\"hello world\"",
"u\"hello world\"",
"True",
"Color.RED",
"None">
typedef py::typing::Literal<26,
0x14,
py::typing::StringLiteral("\"hello world\""),
py::typing::StringLiteral("b\"hello world\""),
py::typing::StringLiteral("u\"hello world\""),
true,
py::typing::StringLiteral("Color.RED"),
py::typing::StringLiteral("None")>
Comment on lines +125 to +132
Copy link
Contributor

Choose a reason for hiding this comment

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

Just skimming over this, but would a simple string literal than have to use:
py::typing::Literal<py::typing::StringLiteral("hello")>?
Would be nice if strings could be used directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What could be done I believe is only allow each Literal of the same type. Then I don't think you would need the py::typing::Literal<py::typing::StringLiteral("hello")> however, you wouldn't be able to mix non-type templates inside Literals and would need to use Unions.

Copy link
Contributor Author

@InvincibleRMC InvincibleRMC Dec 18, 2024

Choose a reason for hiding this comment

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

After messing around with various solutions the only one I could get working is make different named literals.

py::typing::LiteralString"Hi">
py::typing::LiteralInt<1, 2, 3>
py::typing::LiteralBool<true>

To avoid confusion if we wanted to move forward with this the current typing::StringLiteral would be change to typing::fixed_string.

LiteralFoo;
} // namespace literals
namespace typevar {
Expand Down Expand Up @@ -969,7 +969,8 @@ TEST_SUBMODULE(pytypes, m) {
.value("RED", literals::Color::RED)
.value("BLUE", literals::Color::BLUE);

m.def("annotate_literal", [](literals::LiteralFoo &o) -> py::object { return o; });
m.def("annotate_complete_literal", [](literals::LiteralFoo &o) -> py::object { return o; });

m.def("annotate_generic_containers",
[](const py::typing::List<typevar::TypeVarT> &l) -> py::typing::List<typevar::TypeVarV> {
return l;
Expand All @@ -983,6 +984,15 @@ TEST_SUBMODULE(pytypes, m) {
m.attr("defined_PYBIND11_TYPING_H_HAS_STRING_LITERAL") = false;
#endif

#if defined(PYBIND11_CPP17)
m.def("annotate_literal",
[](py::typing::Literal<3, 6, 1, 0, true, false, 0x14> &o) -> py::object { return o; });

m.attr("PYBIND11_CPP17") = true;
#else
m.attr("PYBIND11_CPP17") = false;
#endif

#if defined(PYBIND11_TEST_PYTYPES_HAS_RANGES)

// test_tuple_ranges
Expand Down
17 changes: 14 additions & 3 deletions tests/test_pytypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1036,13 +1036,24 @@ def test_optional_object_annotations(doc):


@pytest.mark.skipif(
not m.defined_PYBIND11_TYPING_H_HAS_STRING_LITERAL,
reason="C++20 non-type template args feature not available.",
not m.PYBIND11_CPP17,
reason="C++17 auto template args feature not available.",
)
def test_literal(doc):
assert (
doc(m.annotate_literal)
== 'annotate_literal(arg0: Literal[26, 0x1A, "hello world", b"hello world", u"hello world", True, Color.RED, None]) -> object'
== "annotate_literal(arg0: Literal[3, 6, 1, 0, True, False, 20]) -> object"
)


@pytest.mark.skipif(
not m.defined_PYBIND11_TYPING_H_HAS_STRING_LITERAL,
reason="C++20 non-type template args feature not available.",
)
def test_complete_literal(doc):
assert (
doc(m.annotate_complete_literal)
== 'annotate_complete_literal(arg0: Literal[26, 20, "hello world", b"hello world", u"hello world", True, Color.RED, None]) -> object'
)


Expand Down
Loading