From bc0a2128699dd847fc48f5d7fb543cb41c5c99cc Mon Sep 17 00:00:00 2001 From: hmasdev Date: Sun, 16 Nov 2025 11:35:50 +0900 Subject: [PATCH 1/4] Add title and instructions properties to TypingGame class --- simple_typing_application/typing_game.py | 39 +++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/simple_typing_application/typing_game.py b/simple_typing_application/typing_game.py index f8beed7..2be8df2 100644 --- a/simple_typing_application/typing_game.py +++ b/simple_typing_application/typing_game.py @@ -5,7 +5,6 @@ from logging import getLogger, Logger import os - from .const.color import EColor from .const.keys import EMetaKey from .key_monitor.base import BaseKeyMonitor @@ -93,6 +92,39 @@ def __init__( os.makedirs(self._record_direc, exist_ok=True) + self._ui.system_anounce(self.title) + self._ui.system_anounce(self.instructions) + + @property + def title(self) -> str: + from . import __version__ + + title_line = f" Simple Typing Application {__version__} " + return "\n".join( + [ + "=" * len(title_line), + "", + title_line, + "", + "=" * len(title_line), + ] + ) + + @property + def instructions(self) -> str: + return """Type the displayed characters as quickly and accurately as possible! + +- How to Play: + 1. A typing target will be displayed on the screen. + 2. Type the characters exactly as shown. + 3. Your input will be displayed in real-time, with correct inputs shown in green and incorrect inputs in red. + 4. Once you complete the typing target, a new one will be generated automatically. + +Key Commands: + - Press 'Esc' to exit the game at any time. + - Press 'Tab' to skip the current typing target. +""" + def start(self): asyncio.run(self._main_loop()) @@ -110,6 +142,7 @@ async def _main_loop(self): typing_target, _ = await asyncio.gather(task1, task2) def _show_typing_target(self, typing_target: TypingTargetModel): + self._ui.system_anounce("") self._ui.show_typing_target( typing_target.text, title="Typing Target", @@ -149,9 +182,7 @@ async def _typing_step(self, typing_target: TypingTargetModel): output.records = sorted(list(self.__current_records), key=lambda x: x.timestamp) # noqa self._logger.debug(f"The following data has been saved to {output_path}: {output.model_dump(mode='json')}") # noqa with open(output_path, "a", encoding="utf-8") as f: - json.dump( - output.model_dump(mode="json"), f, indent=4, ensure_ascii=False - ) # noqa + json.dump(output.model_dump(mode="json"), f, indent=4, ensure_ascii=False) # noqa # clean up self.__clean_up_typing_step() From a9bb45d32e0353cbf9d74299079e95488a6c2367 Mon Sep 17 00:00:00 2001 From: hmasdev Date: Sun, 16 Nov 2025 11:49:20 +0900 Subject: [PATCH 2/4] Fix assertions in typing game tests to use assert_called_with instead of assert_called_once_with --- tests/test_typing_game.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_typing_game.py b/tests/test_typing_game.py index bdce856..cc0e64c 100644 --- a/tests/test_typing_game.py +++ b/tests/test_typing_game.py @@ -297,7 +297,7 @@ def test_typing_game__skip_typing_step(typing_game_with_mocks: tuple[TypingGame, typing_game._TypingGame__skip_typing_step() # type: ignore # assert - typing_game._ui.system_anounce.assert_called_once_with( # type: ignore # noqa + typing_game._ui.system_anounce.assert_called_with( # type: ignore # noqa "SKIP!", color=typing_game._system_anounce_color, ) @@ -312,7 +312,7 @@ def test_typing_game__done_typing_step(typing_game_with_mocks: tuple[TypingGame, typing_game._TypingGame__done_typing_step() # type: ignore # assert - typing_game._ui.system_anounce.assert_called_once_with( # type: ignore # noqa + typing_game._ui.system_anounce.assert_called_with( # type: ignore # noqa "DONE!", color=typing_game._system_anounce_color, ) @@ -327,7 +327,7 @@ def test_typing_game__exit_typing_step(typing_game_with_mocks: tuple[TypingGame, typing_game._TypingGame__exit_typing_step() # type: ignore # assert - typing_game._ui.system_anounce.assert_called_once_with( # type: ignore + typing_game._ui.system_anounce.assert_called_with( # type: ignore "EXIT!", color=typing_game._system_anounce_color, ) From fd05b34a079c454d9f9f9043e740579090cf4c65 Mon Sep 17 00:00:00 2001 From: hmasdev Date: Sun, 16 Nov 2025 11:53:41 +0900 Subject: [PATCH 3/4] Refactor type annotations in factory functions and improve stopwatch decorator documentation --- .../key_monitor/factory.py | 5 +- .../sentence_generator/factory.py | 5 +- .../openai_sentence_generator.py | 4 +- simple_typing_application/ui/factory.py | 5 +- simple_typing_application/utils/stopwatch.py | 56 ++++++++++++++++++- 5 files changed, 65 insertions(+), 10 deletions(-) diff --git a/simple_typing_application/key_monitor/factory.py b/simple_typing_application/key_monitor/factory.py index aaceb18..f538c1d 100644 --- a/simple_typing_application/key_monitor/factory.py +++ b/simple_typing_application/key_monitor/factory.py @@ -37,10 +37,11 @@ def create_key_monitor( # create key monitor logger.debug(f"create {key_monitor_cls.__name__}") + key_monitor_config: BaseKeyMonitorConfigModel if isinstance(config, key_monitor_config_model): - key_monitor_config: BaseKeyMonitorConfigModel = config + key_monitor_config = config else: - key_monitor_config: BaseKeyMonitorConfigModel = key_monitor_config_model(**config.model_dump()) # noqa + key_monitor_config = key_monitor_config_model(**config.model_dump()) # noqa key_monitor: BaseKeyMonitor = key_monitor_cls(**key_monitor_config.model_dump()) # type: ignore # noqa return key_monitor diff --git a/simple_typing_application/sentence_generator/factory.py b/simple_typing_application/sentence_generator/factory.py index 1cc92f9..5215f62 100644 --- a/simple_typing_application/sentence_generator/factory.py +++ b/simple_typing_application/sentence_generator/factory.py @@ -50,10 +50,11 @@ def create_sentence_generator( # create sentence generator logger.debug(f"create {sentence_generator_cls.__name__}") + sentence_generator_config: BaseSentenceGeneratorConfigModel if isinstance(config, sentence_generator_config_model): - sentence_generator_config: BaseSentenceGeneratorConfigModel = config + sentence_generator_config = config else: - sentence_generator_config: BaseSentenceGeneratorConfigModel = sentence_generator_config_model(**config.model_dump()) # noqa + sentence_generator_config = sentence_generator_config_model(**config.model_dump()) # noqa sentence_generator: BaseSentenceGenerator = sentence_generator_cls(**sentence_generator_config.model_dump()) # type: ignore # noqa return sentence_generator diff --git a/simple_typing_application/sentence_generator/openai_sentence_generator.py b/simple_typing_application/sentence_generator/openai_sentence_generator.py index 24663fb..015b3ba 100644 --- a/simple_typing_application/sentence_generator/openai_sentence_generator.py +++ b/simple_typing_application/sentence_generator/openai_sentence_generator.py @@ -90,8 +90,8 @@ async def generate( self._logger.debug(f"agent input messages: {messages}") with stopwatch(level=DEBUG, logger=self._logger, prefix="OpenAI agent invocation"): ret: dict[str, Any] = await self._agent.ainvoke( - {"messages": messages}, - ) # type: ignore + {"messages": messages}, # type: ignore + ) self._logger.debug(f"agent response: {ret}") # store to memory diff --git a/simple_typing_application/ui/factory.py b/simple_typing_application/ui/factory.py index 3f937af..f01f46d 100644 --- a/simple_typing_application/ui/factory.py +++ b/simple_typing_application/ui/factory.py @@ -32,10 +32,11 @@ def create_user_interface( # create user interface logger.debug(f"create {user_interface_cls.__name__}") + user_interface_config: BaseUserInterfaceConfigModel if isinstance(config, user_interface_config_model): - user_interface_config: BaseUserInterfaceConfigModel = config + user_interface_config = config else: - user_interface_config: BaseUserInterfaceConfigModel = user_interface_config_model(**config.model_dump()) # noqa + user_interface_config = user_interface_config_model(**config.model_dump()) # noqa user_interface: BaseUserInterface = user_interface_cls(**user_interface_config.model_dump()) # type: ignore # noqa return user_interface diff --git a/simple_typing_application/utils/stopwatch.py b/simple_typing_application/utils/stopwatch.py index 2dee7f4..faf00e2 100644 --- a/simple_typing_application/utils/stopwatch.py +++ b/simple_typing_application/utils/stopwatch.py @@ -3,7 +3,7 @@ from contextlib import contextmanager from functools import wraps, partial from logging import Logger, getLogger -from typing import Callable, Generator, TypeVar, ParamSpec +from typing import Callable, Generator, TypeVar, ParamSpec, overload T = TypeVar("T") P = ParamSpec("P") @@ -67,8 +67,9 @@ def stopwatch( log_func(log_msg_fmt.format(elapsed_time=end_time - start_time)) +@overload def stopwatch_deco( - func: Callable[P, T] | None = None, + func: Callable[P, T], *, level: int = logging.INFO, prefix: str | None = None, @@ -77,6 +78,57 @@ def stopwatch_deco( ) -> Callable[P, T]: """Decorator to measure the execution time of a function. + Args: + func (Callable[P, T]): function to be decorated. + *, + level (int, optional): log level. Defaults to logging.INFO. + Must be one of logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL. + prefix (str | None, optional): prefix of the log message. Defaults to None. + postfix (str, optional): postfix of the log message. Defaults to "". + logger (Logger, optional): logger. Defaults to logger. + + Returns: + Callable[P, T]: decorated function. + """ # noqa + ... + + +@overload +def stopwatch_deco( + func: None = None, + *, + level: int = logging.INFO, + prefix: str | None = None, + postfix: str = "", + logger: Logger = logger, +) -> Callable[[Callable[P, T]], Callable[P, T]]: + """Decorator to measure the execution time of a function. + + Args: + func (Callable[P, T], optional): function to be decorated. Defaults to None. + *, + level (int, optional): log level. Defaults to logging.INFO. + Must be one of logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL. + prefix (str | None, optional): prefix of the log message. Defaults to None. + postfix (str, optional): postfix of the log message. Defaults to "". + logger (Logger, optional): logger. Defaults to logger. + + Returns: + Callable[[Callable[P, T]], Callable[P, T]]: decorator. + """ # noqa + ... + + +def stopwatch_deco( + func: Callable[P, T] | None = None, + *, + level: int = logging.INFO, + prefix: str | None = None, + postfix: str = "", + logger: Logger = logger, +) -> Callable[P, T] | Callable[[Callable[P, T]], Callable[P, T]]: + """Decorator to measure the execution time of a function. + Args: func (Callable[P, T], optional): function to be decorated. Defaults to None. *, From efc6e10bb20f638c168af343659223dc16defab8 Mon Sep 17 00:00:00 2001 From: hmasdev Date: Sun, 16 Nov 2025 12:03:34 +0900 Subject: [PATCH 4/4] Fix assertions in typing game tests to use assert_called_once_with for system announcements --- tests/test_typing_game.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_typing_game.py b/tests/test_typing_game.py index cc0e64c..c60f5e8 100644 --- a/tests/test_typing_game.py +++ b/tests/test_typing_game.py @@ -294,10 +294,11 @@ def test_typing_game__skip_typing_step(typing_game_with_mocks: tuple[TypingGame, typing_game, _ = typing_game_with_mocks # execute + typing_game._ui.system_anounce.reset_mock() # type: ignore # noqa typing_game._TypingGame__skip_typing_step() # type: ignore # assert - typing_game._ui.system_anounce.assert_called_with( # type: ignore # noqa + typing_game._ui.system_anounce.assert_called_once_with( # type: ignore # noqa "SKIP!", color=typing_game._system_anounce_color, ) @@ -309,10 +310,11 @@ def test_typing_game__done_typing_step(typing_game_with_mocks: tuple[TypingGame, typing_game, _ = typing_game_with_mocks # execute + typing_game._ui.system_anounce.reset_mock() # type: ignore # noqa typing_game._TypingGame__done_typing_step() # type: ignore # assert - typing_game._ui.system_anounce.assert_called_with( # type: ignore # noqa + typing_game._ui.system_anounce.assert_called_once_with( # type: ignore # noqa "DONE!", color=typing_game._system_anounce_color, ) @@ -324,10 +326,11 @@ def test_typing_game__exit_typing_step(typing_game_with_mocks: tuple[TypingGame, typing_game, mocks = typing_game_with_mocks # execute + typing_game._ui.system_anounce.reset_mock() # type: ignore # noqa typing_game._TypingGame__exit_typing_step() # type: ignore # assert - typing_game._ui.system_anounce.assert_called_with( # type: ignore + typing_game._ui.system_anounce.assert_called_once_with( # type: ignore "EXIT!", color=typing_game._system_anounce_color, )