From 2fa7650c5549f07ac90aee03b2a2c60e1ea92733 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 10 Apr 2026 00:10:38 +0100 Subject: [PATCH 1/3] feat: add PyIdentifier type for Identifier Python bindings Replace transparent str conversion with a proper Identifier Python object that exposes namespace and key properties. The type caster accepts both str and Identifier inputs and returns Identifier objects. --- endstone/__init__.pyi | 25 +++++++++++ scripts/stubgen.py | 8 ++-- src/endstone/python/block.cpp | 2 +- src/endstone/python/enchantments.cpp | 2 +- src/endstone/python/endstone_python.cpp | 39 ++++++++++++++++ src/endstone/python/inventory.cpp | 2 +- src/endstone/python/registry.h | 40 +++++++++++++++++ src/endstone/python/type_caster.h | 45 +++++++++++++------ .../src/endstone_test/tests/test_registry.py | 3 +- 9 files changed, 143 insertions(+), 23 deletions(-) diff --git a/endstone/__init__.pyi b/endstone/__init__.pyi index 5c0a1fb8ba..c58e38b349 100644 --- a/endstone/__init__.pyi +++ b/endstone/__init__.pyi @@ -50,6 +50,7 @@ __all__ = [ "__version__", "ColorFormat", "GameMode", + "Identifier", "Logger", "OfflinePlayer", "Player", @@ -780,6 +781,30 @@ class Skin: __minecraft_version__ = "26.12" +class Identifier(typing.Generic[_T]): + """ + Represents a namespaced identifier consisting of a namespace and a key. + """ + def __init__(self, id: str) -> None: ... + @property + def namespace(self) -> str: + """ + The namespace component of this identifier. + """ + ... + @property + def key(self) -> str: + """ + The key component of this identifier. + """ + ... + def __str__(self) -> str: ... + def __repr__(self) -> str: ... + def __hash__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + + class Registry(typing.Generic[_T]): """ Presents a registry diff --git a/scripts/stubgen.py b/scripts/stubgen.py index 76104b73db..339e570e28 100644 --- a/scripts/stubgen.py +++ b/scripts/stubgen.py @@ -19,16 +19,14 @@ def main() -> None: module = load(module_name) package = module.package + # endstone module.members = {"_T": package.get_member("_T"), **module.members} - for member in list(module.members.keys()): - if member.endswith("Registry"): - module.members.pop(member) - module.exports.remove(member) module.set_member("Registry", package.get_member("Registry")) module.exports.append("Registry") + module.set_member("Identifier", package.get_member("Identifier")) + module.exports.append("Identifier") module.exports = sorted(module.exports) module["Server.get_registry"] = package["Server.get_registry"] - module.set_member("__version__", package.get_member("__version__")) module.imports.setdefault("__version__", package.imports.get("__version__")) module.exports = ["__version__"] + module.exports diff --git a/src/endstone/python/block.cpp b/src/endstone/python/block.cpp index 03e6e51af7..685ad24716 100644 --- a/src/endstone/python/block.cpp +++ b/src/endstone/python/block.cpp @@ -50,7 +50,7 @@ void init_block(py::module_ &m, py::class_ &block) "Creates a new BlockData instance for this block type, with all properties initialized to defaults.") .def_static("get", &BlockType::get, py::arg("name"), "Attempts to get the BlockType with the given name.", py::return_value_policy::reference) - .def("__str__", &BlockType::getId) + .def("__str__", [](const BlockType &self) { return std::string(self.getId()); }) .def("__repr__", [](const BlockType &self) { return fmt::format("BlockType({})", self.getId()); }); py::class_(m, "BlockState", diff --git a/src/endstone/python/enchantments.cpp b/src/endstone/python/enchantments.cpp index bf154b61d8..57d005b45b 100644 --- a/src/endstone/python/enchantments.cpp +++ b/src/endstone/python/enchantments.cpp @@ -84,7 +84,7 @@ void init_enchantments(py::module_ &m) "with any enchantments already applied to the item.") .def_static("get", &Enchantment::get, py::arg("name"), "Attempts to get the Enchantment with the given name.", py::return_value_policy::reference) - .def("__str__", &Enchantment::getId) + .def("__str__", [](const Enchantment &self) { return std::string(self.getId()); }) .def("__hash__", [](const Enchantment &self) { return std::hash{}(self.getId()); }) .def(py::self == py::self) .def(py::self != py::self) diff --git a/src/endstone/python/endstone_python.cpp b/src/endstone/python/endstone_python.cpp index aaa2f85139..ba12863def 100644 --- a/src/endstone/python/endstone_python.cpp +++ b/src/endstone/python/endstone_python.cpp @@ -58,6 +58,45 @@ PYBIND11_MODULE(_python, m) // NOLINT(*-use-anonymous-namespace) py::options options; options.disable_enum_members_docstring(); + // Identifier (registered early, before classes that use it via type caster) + py::class_(m, "Identifier", + "Represents a namespaced identifier consisting of a namespace and a key.") + .def(py::init(), py::arg("id"), "Create an Identifier from a string like 'namespace:key'.") + .def(py::init(), py::arg("namespace_"), py::arg("key"), + "Create an Identifier from separate namespace and key.") + .def_property_readonly( + "namespace", [](const PyIdentifier &self) { return self.namespace_; }, + "The namespace component of this identifier.") + .def_property_readonly( + "key", [](const PyIdentifier &self) { return self.key_; }, "The key component of this identifier.") + .def("__str__", &PyIdentifier::str) + .def("__repr__", + [](const PyIdentifier &self) { return "Identifier(" + self.str() + ")"; }) + .def("__hash__", + [](const PyIdentifier &self) { return py::hash(py::str(self.str())); }) + .def("__eq__", + [](const PyIdentifier &self, const py::object &other) { + if (py::isinstance(other)) { + return self == other.cast(); + } + if (py::isinstance(other)) { + return self.str() == other.cast(); + } + return false; + }) + .def("__ne__", + [](const PyIdentifier &self, const py::object &other) { + if (py::isinstance(other)) { + return !(self == other.cast()); + } + if (py::isinstance(other)) { + return self.str() != other.cast(); + } + return true; + }) + .def_static( + "__class_getitem__", [](const py::object &) { return py::type::of(); }, py::arg("item")); + // Submodules auto m_actor = m.def_submodule("actor", "Classes relating to actors (entities) that can exist in a world, including all " diff --git a/src/endstone/python/inventory.cpp b/src/endstone/python/inventory.cpp index 92e66caa2f..5237d4daff 100644 --- a/src/endstone/python/inventory.cpp +++ b/src/endstone/python/inventory.cpp @@ -44,7 +44,7 @@ void init_inventory(py::module_ &m, py::class_ &item_stack) "Constructs a new ItemStack with this item type.") .def_static("get", &ItemType::get, py::arg("name"), "Attempts to get the ItemType with the given name.", py::return_value_policy::reference) - .def("__str__", &ItemType::getId) + .def("__str__", [](const ItemType &self) { return std::string(self.getId()); }) .def(py::self == py::self) .def(py::self != py::self) .def(py::self == std::string_view()) diff --git a/src/endstone/python/registry.h b/src/endstone/python/registry.h index 49c2d52152..b4b5225926 100644 --- a/src/endstone/python/registry.h +++ b/src/endstone/python/registry.h @@ -24,6 +24,46 @@ namespace py = pybind11; namespace endstone::python { +struct PyIdentifier { + std::string namespace_; + std::string key_; + + PyIdentifier() = default; + + PyIdentifier(std::string ns, std::string key) : namespace_(std::move(ns)), key_(std::move(key)) + { + if (namespace_.empty() || key_.empty()) { + throw std::invalid_argument("Identifier namespace and key must not be empty."); + } + } + + explicit PyIdentifier(const std::string &full) + { + if (full.empty()) { + throw std::invalid_argument("Identifier string must not be empty."); + } + const auto pos = full.rfind(':'); + if (pos == std::string::npos) { + namespace_ = "minecraft"; + key_ = full; + } + else { + namespace_ = full.substr(0, pos); + key_ = full.substr(pos + 1); + } + if (namespace_.empty() || key_.empty()) { + throw std::invalid_argument("Identifier namespace and key must not be empty."); + } + } + + [[nodiscard]] std::string str() const { return namespace_ + ":" + key_; } + + bool operator==(const PyIdentifier &other) const + { + return namespace_ == other.namespace_ && key_ == other.key_; + } +}; + class PyRegistry { public: explicit PyRegistry(const IRegistry ®istry) : registry_(registry) {} diff --git a/src/endstone/python/type_caster.h b/src/endstone/python/type_caster.h index 9efc056a01..85116a03db 100644 --- a/src/endstone/python/type_caster.h +++ b/src/endstone/python/type_caster.h @@ -18,6 +18,7 @@ #include #include "endstone/endstone.hpp" +#include "registry.h" namespace pybind11::detail { template <> @@ -266,32 +267,48 @@ class type_caster { template class type_caster> { public: + using value_conv = make_caster; explicit type_caster() : value("") {} // Python -> C++ bool load(handle src, bool convert) { - make_caster str_caster; - if (!str_caster.load(src, convert)) { - return false; - } - try { - value = static_cast(str_caster); + // Accept PyIdentifier objects + if (isinstance(src)) { + auto &py_id = src.cast(); + storage_ = py_id.str(); + value = endstone::Identifier(std::string_view(storage_)); return true; } - catch (const std::exception &e) { - PyErr_SetString(PyExc_ValueError, e.what()); - return false; + // Accept strings + make_caster str_caster; + if (str_caster.load(src, convert)) { + try { + storage_ = cast_op(std::move(str_caster)); + value = endstone::Identifier(std::string_view(storage_)); + return true; + } + catch (const std::exception &e) { + PyErr_SetString(PyExc_ValueError, e.what()); + return false; + } } + return false; } - // C++ -> Python - static handle cast(endstone::Identifier src, return_value_policy policy, handle parent) + // C++ -> Python: return PyIdentifier object + static handle cast(endstone::Identifier src, return_value_policy /*policy*/, handle /*parent*/) { - make_caster str_caster; - return str_caster.cast(src, policy, parent); + endstone::python::PyIdentifier id(std::string(src.getNamespace()), std::string(src.getKey())); + return pybind11::cast(std::move(id)).release(); } - PYBIND11_TYPE_CASTER(endstone::Identifier, const_name(PYBIND11_STRING_NAME)); + // PYBIND11_TYPE_CASTER(endstone::Identifier, const_name("@Identifier[") + value_conv::name + + // const_name("] | str@Identifier[") + value_conv::name + + // const_name("]@")); + PYBIND11_TYPE_CASTER(endstone::Identifier, const_name("endstone.Identifier[") + value_conv::name + const_name("]")); + +private: + std::string storage_; }; template <> diff --git a/tests/endstone_test/src/endstone_test/tests/test_registry.py b/tests/endstone_test/src/endstone_test/tests/test_registry.py index 6fb26d5f55..a7ad0370b7 100644 --- a/tests/endstone_test/src/endstone_test/tests/test_registry.py +++ b/tests/endstone_test/src/endstone_test/tests/test_registry.py @@ -1,5 +1,6 @@ import pytest from endstone import Server +from endstone._python import Identifier from endstone.actor import ActorType from endstone.enchantments import Enchantment from endstone.inventory import ItemType @@ -13,7 +14,7 @@ def _get_enum_constants(cls): return { name: getattr(cls, name) for name in dir(cls) - if not name.startswith("_") and isinstance(getattr(cls, name), str) + if not name.startswith("_") and isinstance(getattr(cls, name), Identifier) } From eface9a758488d5164a5c1a55acbb1c14243a331 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 10 Apr 2026 00:20:47 +0100 Subject: [PATCH 2/3] feat: accept Identifier in PyRegistry methods Change PyRegistry::get, getOrThrow, and contains to accept PyIdentifier instead of raw std::string. Update Registry stubs to accept Identifier[_T] | str. --- endstone/__init__.pyi | 9 ++++----- src/endstone/python/registry.h | 12 ++++++------ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/endstone/__init__.pyi b/endstone/__init__.pyi index c58e38b349..fdd1e04192 100644 --- a/endstone/__init__.pyi +++ b/endstone/__init__.pyi @@ -804,22 +804,21 @@ class Identifier(typing.Generic[_T]): def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... - class Registry(typing.Generic[_T]): """ Presents a registry """ - def get(self, key: str) -> _T | None: + def get(self, id: Identifier[_T] | str) -> _T | None: """ Get the object by its key. """ ... - def get_or_throw(self, key: str) -> _T: + def get_or_throw(self, id: Identifier[_T] | str) -> _T: """ Get the object by its key or throw if missing. """ ... - def __getitem__(self, key: str) -> _T: ... + def __getitem__(self, id: Identifier[_T] | str) -> _T: ... def __iter__(self) -> list: ... - def __contains__(self, key: str) -> bool: ... + def __contains__(self, id: Identifier[_T] | str) -> bool: ... def __len__(self) -> int: ... diff --git a/src/endstone/python/registry.h b/src/endstone/python/registry.h index b4b5225926..7788cc20f3 100644 --- a/src/endstone/python/registry.h +++ b/src/endstone/python/registry.h @@ -68,20 +68,20 @@ class PyRegistry { public: explicit PyRegistry(const IRegistry ®istry) : registry_(registry) {} - [[nodiscard]] py::object get(const std::string &id) const + [[nodiscard]] py::object get(const PyIdentifier &id) const { - if (const auto *p = registry_.get0(id)) { + if (const auto *p = registry_.get0(id.str())) { return cast(p); } return py::none(); } - [[nodiscard]] py::object getOrThrow(const std::string &id) const + [[nodiscard]] py::object getOrThrow(const PyIdentifier &id) const { - if (const auto *p = registry_.get0(id)) { + if (const auto *p = registry_.get0(id.str())) { return cast(p); } - throw py::key_error(fmt::format("No registry entry found for identifier: {}", id)); + throw py::key_error(fmt::format("No registry entry found for identifier: {}", id.str())); } [[nodiscard]] py::iterator iter() const @@ -94,7 +94,7 @@ class PyRegistry { return py::iter(items); } - [[nodiscard]] bool contains(const std::string &id) const { return registry_.get0(id) != nullptr; } + [[nodiscard]] bool contains(const PyIdentifier &id) const { return registry_.get0(id.str()) != nullptr; } [[nodiscard]] std::size_t size() const { return registry_.size(); } From cc9e725ee3016ba06ca1a20853a25d62f94e4dfc Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 10 Apr 2026 00:25:15 +0100 Subject: [PATCH 3/3] fix: register implicit conversion from str to PyIdentifier Without this, passing str to PyRegistry methods (get, contains, etc.) would raise TypeError since pybind11 doesn't auto-convert. --- src/endstone/python/endstone_python.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/endstone/python/endstone_python.cpp b/src/endstone/python/endstone_python.cpp index ba12863def..a468e62526 100644 --- a/src/endstone/python/endstone_python.cpp +++ b/src/endstone/python/endstone_python.cpp @@ -96,6 +96,7 @@ PYBIND11_MODULE(_python, m) // NOLINT(*-use-anonymous-namespace) }) .def_static( "__class_getitem__", [](const py::object &) { return py::type::of(); }, py::arg("item")); + py::implicitly_convertible(); // Submodules auto m_actor =