Skip to content

Commit 666078f

Browse files
authored
fix: correct type annotations for Plugin and event_handler (#369)
* fix: correct type annotations for Plugin class vars and event_handler decorator Plugin class variables were typed as bare `None`, causing mypy to reject string assignments in subclasses (e.g. `prefix = "MyPlugin"`). Add proper `str | None`, `dict | None`, etc. annotations. event_handler decorator stub returned `None` instead of a callable, causing mypy func-returns-value and untyped-decorator errors. Add @overload signatures for both `@event_handler` and `@event_handler(priority=...)` usage patterns. * fix: update type annotations in chart modules * fix: add missing initialization checks * fix: add missing checks across modules * fix: add server UUID check and correct type ignore annotations in metrics config * fix: update type annotations in stubs
1 parent a98d70a commit 666078f

32 files changed

Lines changed: 212 additions & 269 deletions

endstone/actor/__init__.pyi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,8 +346,8 @@ class ActorType:
346346
def __str__(self) -> str: ...
347347
def __repr__(self) -> str: ...
348348
def __hash__(self) -> int: ...
349-
def __eq__(self, arg0: ActorType) -> bool: ...
350-
def __ne__(self, arg0: ActorType) -> bool: ...
349+
def __eq__(self, other: object) -> bool: ...
350+
def __ne__(self, other: object) -> bool: ...
351351

352352
class Item(Actor):
353353
"""

endstone/asyncio/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def _lazy_init(self) -> None:
3434
loop.set_debug(self._debug)
3535
self._loop = loop
3636

37-
def _thread_main():
37+
def _thread_main() -> None:
3838
asyncio.set_event_loop(loop)
3939
self._started_evt.set()
4040
loop.run_forever()
@@ -53,7 +53,7 @@ def shutdown(self, *, timeout: Optional[float] = None) -> None:
5353
loop = self._loop
5454
thread = self._thread
5555

56-
async def _shutdown():
56+
async def _shutdown() -> None:
5757
current = asyncio.current_task()
5858
to_cancel = [t for t in asyncio.all_tasks() if t is not current]
5959
for t in to_cancel:
@@ -86,6 +86,8 @@ def submit(self, coro: typing.Awaitable[_T]) -> concurrent.futures.Future[_T]:
8686
if not asyncio.iscoroutine(coro):
8787
raise TypeError(f"a coroutine object is required, got {coro!r}")
8888
self._lazy_init()
89+
if self._loop is None:
90+
raise RuntimeError("Event loop is not initialized")
8991
return asyncio.run_coroutine_threadsafe(coro, self._loop)
9092

9193
def get_loop(self) -> asyncio.AbstractEventLoop:

endstone/cli/__init__.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
import platform
44
import sys
55
import time
6+
from typing import Any, Callable, TypeVar, cast
67

78
import click
89
import colorlog
910

1011
from endstone._version import __version__
12+
from endstone.cli.base import Bootstrap
1113

1214
handler = colorlog.StreamHandler()
1315
handler.setFormatter(
@@ -29,19 +31,21 @@
2931

3032
__all__ = ["main"]
3133

34+
_F = TypeVar("_F", bound=Callable[..., Any])
3235

33-
def catch_exceptions(func):
36+
37+
def catch_exceptions(func: _F) -> _F:
3438
"""Decorator to catch and log exceptions."""
3539

3640
@functools.wraps(func)
37-
def wrapper(*args, **kwargs):
41+
def wrapper(*args, **kwargs): # type: ignore[no-untyped-def]
3842
try:
3943
return func(*args, **kwargs)
4044
except Exception as e:
4145
logger.exception(e)
4246
sys.exit(-1)
4347

44-
return wrapper
48+
return cast(_F, wrapper)
4549

4650

4751
@click.group(invoke_without_command=True, help="Starts an endstone server.")
@@ -75,10 +79,11 @@ def wrapper(*args, **kwargs):
7579
@click.version_option(__version__)
7680
@click.pass_context
7781
@catch_exceptions
78-
def main(ctx, server_folder: str, no_confirm: bool, remote: str, interactive: bool) -> None:
82+
def main(ctx: click.Context, server_folder: str, no_confirm: bool, remote: str, interactive: bool) -> None:
7983
if ctx.invoked_subcommand is not None:
8084
return
8185

86+
cls: type[Bootstrap]
8287
system = platform.system()
8388
if system == "Windows":
8489
from .windows import WindowsBootstrap

endstone/cli/base.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import click
1616
import importlib_resources
1717
import requests
18-
import sentry_crashpad
18+
import sentry_crashpad # type: ignore[import-untyped]
1919
import tomlkit
2020
from packaging.version import Version
2121
from rich.progress import BarColumn, DownloadColumn, Progress, TextColumn, TimeRemainingColumn
@@ -29,7 +29,7 @@ def __init__(self, server_folder: str, no_confirm: bool, remote: str, interactiv
2929
self._no_confirm = no_confirm
3030
self._remote = remote
3131
self._logger = logging.getLogger(self.name)
32-
self._process: subprocess.Popen
32+
self._process: subprocess.Popen[str]
3333
self._interactive = interactive
3434

3535
@property
@@ -76,7 +76,7 @@ def _validate(self) -> None:
7676
if not self._endstone_runtime_path.exists():
7777
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), str(self._endstone_runtime_path))
7878

79-
def _download(self, dst: Union[str, os.PathLike]) -> None:
79+
def _download(self, dst: Union[str, os.PathLike[str]]) -> None:
8080
dst = Path(dst)
8181

8282
self._logger.info("Loading index from the remote server...")
@@ -140,8 +140,8 @@ def _download(self, dst: Union[str, os.PathLike]) -> None:
140140

141141
if should_modify_server_properties:
142142
properties = dst / "server.properties"
143-
with properties.open("r", encoding="utf-8") as file:
144-
in_lines = file.readlines()
143+
with properties.open("r", encoding="utf-8") as f:
144+
in_lines = f.readlines()
145145

146146
out_lines = []
147147
for line in in_lines:
@@ -152,12 +152,12 @@ def _download(self, dst: Union[str, os.PathLike]) -> None:
152152
else:
153153
out_lines.append(line)
154154

155-
with properties.open("w", encoding="utf-8") as file:
156-
file.writelines(out_lines)
155+
with properties.open("w", encoding="utf-8") as f:
156+
f.writelines(out_lines)
157157

158158
version_file = dst / "version.txt"
159-
with version_file.open("w", encoding="utf-8") as file:
160-
file.writelines(str(self.minecraft_version))
159+
with version_file.open("w", encoding="utf-8") as f:
160+
f.writelines(str(self.minecraft_version))
161161

162162
def _prepare(self) -> None:
163163
# ensure the plugin folder exists
@@ -200,7 +200,7 @@ def migrate_config(from_doc: tomlkit.TOMLDocument, to_doc: tomlkit.TOMLDocument)
200200
else:
201201
# if both are tables, dive deeper
202202
if isinstance(val, tomlkit.TOMLDocument) and isinstance(to_doc[key], tomlkit.TOMLDocument):
203-
migrate_config(val, to_doc[key])
203+
migrate_config(val, to_doc[key]) # type: ignore[arg-type]
204204

205205
migrate_config(default_config, config)
206206
with open(self.config_path, "w", encoding="utf-8") as f:
@@ -291,7 +291,7 @@ def _endstone_runtime_env(self) -> dict[str, str]:
291291
env["ENDSTONE_USE_INTERACTIVE_CONSOLE"] = "1"
292292
return env
293293

294-
def _run(self, *args, **kwargs) -> int:
294+
def _run(self, *args, **kwargs) -> int: # type: ignore[no-untyped-def]
295295
"""
296296
Runs the server and returns its exit code.
297297

endstone/cli/linux.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ def _prepare(self) -> None:
3636
os.chmod(self.executable_path, st.st_mode | stat.S_IEXEC)
3737
os.chmod(self.server_path / "crashpad_handler", st.st_mode | stat.S_IEXEC)
3838

39-
def _run(self, *args, **kwargs) -> int:
40-
process = subprocess.Popen(
39+
def _run(self, *args, **kwargs) -> int: # type: ignore[no-untyped-def]
40+
process = subprocess.Popen( # type: ignore[call-overload]
4141
[str(self.executable_path.absolute())],
4242
text=True,
4343
encoding="utf-8",
@@ -46,4 +46,4 @@ def _run(self, *args, **kwargs) -> int:
4646
*args,
4747
**kwargs,
4848
)
49-
return process.wait()
49+
return int(process.wait())

endstone/cli/windows.py

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@
44
import subprocess
55
import sys
66
import warnings
7-
from subprocess import STARTUPINFO, Handle, list2cmdline
7+
from subprocess import STARTUPINFO, Handle, list2cmdline # type: ignore[attr-defined]
88

9-
from endstone import _detours
9+
from endstone import _detours # type: ignore[attr-defined]
1010

1111
from .base import Bootstrap
1212

1313

14-
class PopenWithDll(subprocess.Popen):
15-
def __init__(self, *args, **kwargs):
14+
class PopenWithDll(subprocess.Popen[str]):
15+
def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
1616
self.dll_names = kwargs.pop("dll_names", None)
1717
super().__init__(*args, **kwargs)
1818

19-
def _execute_child(
19+
def _execute_child( # type: ignore[no-untyped-def]
2020
self,
2121
args,
2222
executable,
@@ -34,16 +34,9 @@ def _execute_child(
3434
c2pwrite,
3535
errread,
3636
errwrite,
37-
# unused_restore_signals,
38-
# unused_gid,
39-
# unused_gids,
40-
# unused_uid,
41-
# unused_umask,
42-
# unused_start_new_session,
43-
# unused_process_group,
4437
*unused_args,
4538
**unused_kwargs,
46-
):
39+
) -> None:
4740
"""Execute program (MS Windows version)"""
4841

4942
assert not pass_fds, "pass_fds not supported on Windows."
@@ -88,7 +81,7 @@ def _execute_child(
8881
if use_std_handles:
8982
handle_list += [int(p2cread), int(c2pwrite), int(errwrite)]
9083

91-
handle_list[:] = self._filter_handle_list(handle_list)
84+
handle_list[:] = self._filter_handle_list(handle_list) # type: ignore[attr-defined]
9285

9386
if handle_list:
9487
if not close_fds:
@@ -120,7 +113,7 @@ def _execute_child(
120113
dll_name=self.dll_names,
121114
)
122115
finally:
123-
self._close_pipe_fds(p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite)
116+
self._close_pipe_fds(p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite) # type: ignore[attr-defined]
124117

125118
# Retain the process handle, but close the thread handle
126119
self._child_created = True
@@ -178,7 +171,7 @@ def _check_python_distribution(self) -> None:
178171
"from https://www.python.org/downloads/ ."
179172
)
180173

181-
def _run(self, *args, **kwargs) -> int:
174+
def _run(self, *args, **kwargs) -> int: # type: ignore[no-untyped-def]
182175
try:
183176
self._add_loopback_exemption()
184177
except Exception as e:

endstone/command/__init__.pyi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ class CommandSenderWrapper(CommandSender):
6363
def __init__(
6464
self,
6565
sender: CommandSender,
66-
on_message: collections.abc.Callable[[str | Translatable], None] = None,
67-
on_error: collections.abc.Callable[[str | Translatable], None] = None,
66+
on_message: collections.abc.Callable[[str | Translatable], None] | None = None,
67+
on_error: collections.abc.Callable[[str | Translatable], None] | None = None,
6868
) -> None: ...
6969

7070
class ConsoleCommandSender(CommandSender):

endstone/enchantments/__init__.pyi

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
Classes relating to the specialized enhancements to ItemStacks.
33
"""
44

5-
import typing
6-
75
from endstone.inventory import ItemStack
86

97
__all__ = ["Enchantment"]
@@ -94,11 +92,5 @@ class Enchantment:
9492
...
9593
def __str__(self) -> str: ...
9694
def __hash__(self) -> int: ...
97-
@typing.overload
98-
def __eq__(self, arg0: Enchantment) -> bool: ...
99-
@typing.overload
100-
def __eq__(self, arg0: str) -> bool: ...
101-
@typing.overload
102-
def __ne__(self, arg0: Enchantment) -> bool: ...
103-
@typing.overload
104-
def __ne__(self, arg0: str) -> bool: ...
95+
def __eq__(self, other: object) -> bool: ...
96+
def __ne__(self, other: object) -> bool: ...

endstone/event/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
1+
from collections.abc import Callable
2+
from typing import TypeVar, overload
3+
14
import lazy_loader as lazy
25

36
from endstone._python.event import EventPriority
47

8+
_F = TypeVar("_F", bound=Callable[..., None])
9+
10+
11+
@overload
12+
def event_handler(__func: _F) -> _F: ...
13+
14+
15+
@overload
16+
def event_handler(*, priority: EventPriority = ..., ignore_cancelled: bool = ...) -> Callable[[_F], _F]: ...
17+
518

619
def event_handler(func=None, *, priority: EventPriority = EventPriority.NORMAL, ignore_cancelled: bool = False):
720
def decorator(f):

endstone/event/__init__.pyi

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ Classes relating to handling triggered code executions.
33
"""
44

55
import enum
6+
import typing
67

78
from endstone import GameMode, Player, Skin
89
from endstone.actor import Actor, Item, Mob
@@ -89,6 +90,8 @@ __all__ = [
8990
"event_handler",
9091
]
9192

93+
_F = typing.TypeVar("_F", bound=(typing.Callable[..., None]))
94+
9295
class EventPriority(enum.IntEnum):
9396
"""
9497
Listeners are called in following order: LOWEST -> LOW -> NORMAL -> HIGH -> HIGHEST -> MONITOR
@@ -1151,6 +1154,7 @@ class WeatherChangeEvent(WeatherEvent, Cancellable):
11511154
"""
11521155
...
11531156

1154-
def event_handler(
1155-
func=None, *, priority: EventPriority = EventPriority.NORMAL, ignore_cancelled: bool = False
1156-
) -> None: ...
1157+
@typing.overload
1158+
def event_handler(__func: _F) -> _F: ...
1159+
@typing.overload
1160+
def event_handler(*, priority: EventPriority = ..., ignore_cancelled: bool = ...) -> typing.Callable[[_F], _F]: ...

0 commit comments

Comments
 (0)