Skip to content

feat: add Identifier type for Python bindings#373

Merged
wu-vincent merged 6 commits into
developfrom
feat/py-identifier
May 28, 2026
Merged

feat: add Identifier type for Python bindings#373
wu-vincent merged 6 commits into
developfrom
feat/py-identifier

Conversation

@wu-vincent

@wu-vincent wu-vincent commented Apr 9, 2026

Copy link
Copy Markdown
Member

Motivation

Identifier<T> is used throughout the API (dimensions, block types, actor types, enchantments, etc.) but was exposed to Python as a plain str. This had two problems:

  1. No structured access — plugin developers had to manually parse "namespace:key" strings to extract components.
  2. Weak type safety — type checkers couldn't distinguish between a DimensionId and an ActorTypeId since both were just str. You could pass Dimension.OVERWORLD to spawn_actor(type=...) without any type error.

With Identifier[T], type checkers can now catch misuse:

# Type checker ERROR: Expected Identifier[ActorType], got Identifier[Dimension]
dimension.spawn_actor(location, Dimension.OVERWORLD)

# Type checker OK:
dimension.spawn_actor(location, ActorType.ZOMBIE)

Before

dim_id = dimension.id          # "minecraft:overworld" (str)
type(dim_id)                   # <class 'str'>
# To get namespace/key, you had to parse manually:
ns, key = dim_id.split(":")    # "minecraft", "overworld"

After

dim_id = dimension.id          # Identifier(minecraft:overworld)
type(dim_id)                   # <class 'Identifier'>
dim_id.namespace               # "minecraft"
dim_id.key                     # "overworld"

# Backward compatible — still compares with str:
dim_id == "minecraft:overworld"  # True
str(dim_id)                      # "minecraft:overworld"

# Accepts str input everywhere:
level.get_dimension("overworld")              # still works
level.get_dimension(Dimension.OVERWORLD)      # also works

Summary

  • Add PyIdentifier struct to expose Identifier<T> as a proper Python object with namespace and key properties
  • Update Identifier<T> type caster to return Identifier objects instead of str, while still accepting both str and Identifier as input
  • Fix __str__ methods on BlockType, Enchantment, and ItemType to return str (not Identifier)
  • Add Identifier generic facade in stubs and stubgen for Identifier[T] type hints
  • Validate that namespace and key are non-empty in PyIdentifier constructors

Test plan

  • Build and verify compilation
  • Run Python tests (pytest tests/endstone_test)
  • Verify Dimension.OVERWORLD.namespace == "minecraft" and .key == "overworld"
  • Verify Dimension.OVERWORLD == "minecraft:overworld" backward compat
  • Run stubgen and verify generated stubs

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.
Change PyRegistry::get, getOrThrow, and contains to accept
PyIdentifier instead of raw std::string. Update Registry stubs
to accept Identifier[_T] | str.
Without this, passing str to PyRegistry methods (get, contains, etc.)
would raise TypeError since pybind11 doesn't auto-convert.
@wu-vincent wu-vincent force-pushed the feat/py-identifier branch from cc9e725 to c351a9b Compare May 28, 2026 10:30
- Use pybind11's io_name so Identifier<T> casts to endstone.Identifier[T]
  on return but accepts endstone.Identifier[T] | str on input, reflecting
  the runtime behaviour without forcing every call site through the
  Identifier constructor.
- Mirror __str__ in __repr__ on PyIdentifier. Users never construct
  Identifier directly (it's a facade for typed ids surfaced by the
  registry API), so the eval(repr(x)) convention buys nothing and the
  shorter form reads better in logs.
- Add __eq__/__hash__/string-comparison overloads on BlockType and the
  missing __hash__ on ItemType so all four registry types
  (ActorType, BlockType, Enchantment, ItemType) are consistently
  hashable and string-comparable.
- Declare endstone-stubgen (from the EndstoneMC/stubgen GitHub source,
  which handles std::function callable annotations and equality
  overloads cleanly) and ruff as inline PEP 723 dependencies so
  `uv run scripts/stubgen.py` resolves them automatically.
- Strip the bogus `from . import endstone` that Pybind11ImportFix
  injects alongside the absolute import.
- Retype `NAME = Identifier(...)` class constants to
  `NAME: Identifier[<owner>] = "<key>"`, tracking the enclosing class
  by indentation so the generic type parameter survives and the bare
  string remains visible as documentation.
- Rewrite `endstone.Identifier` references in submodules to bare
  `Identifier` plus a `from endstone import Identifier` so the
  constant blocks read cleanly.
- Deduplicate exports so Identifier/Registry don't appear twice in
  `__all__`.
- Class constants like Dimension.OVERWORLD, ActorType.ZOMBIE,
  Enchantment.PROTECTION are now typed as `Identifier[Owner]` rather
  than plain `str`. Static type checkers reject passing
  `Dimension.OVERWORLD` where `Identifier[ActorType]` is expected.
- Registry lookups and `Identifier<T>`-typed parameters accept either
  an Identifier or a str at runtime; stubs show this as
  `Identifier[T] | str` via the io_name split.
- Expose Identifier, Registry, and BlockType through the lazy_loader
  forwarding in the relevant package __init__.py files so the runtime
  matches the stubs.
@wu-vincent wu-vincent marked this pull request as ready for review May 28, 2026 12:31
@wu-vincent wu-vincent merged commit af79942 into develop May 28, 2026
4 of 6 checks passed
@wu-vincent wu-vincent deleted the feat/py-identifier branch May 28, 2026 12:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant