From b0318d4710870276373e8c9d25feb8ee8c48a36e Mon Sep 17 00:00:00 2001 From: lexter0705 Date: Thu, 25 Jun 2026 21:48:40 +0300 Subject: [PATCH 1/5] 1) Fixed GetText global coordinates and OCR tests 2) Fixed PressKeysCombination key handling and validation tests 3) Fixed Undetected event naming in docs and tests 4) Fixed UniqueAlgorithm attributes mutation 5) Cleaned up docstrings, error messages and documentation text 6) Updated version to 1.1.2 --- README.md | 4 +-- apparser/core/ui/coordinates.py | 4 +-- apparser/cv/events/undetected.py | 2 +- apparser/exceptions/instruction_not_found.py | 2 +- apparser/exceptions/text_not_found.py | 6 ++--- apparser/instructions/default/click.py | 4 +-- apparser/instructions/default/press.py | 6 +++++ apparser/instructions/ocr/click_on_text.py | 2 +- apparser/instructions/ocr/move_to_text.py | 10 +++---- apparser/instructions/ocr/plot_text.py | 6 ++--- apparser/instructions/ocr/print_all_text.py | 2 +- apparser/instructions/ocr/text_getter.py | 27 ++++++++++--------- apparser/instructions/ui/algorithms/unique.py | 4 +-- apparser/instructions/ui/click.py | 4 +-- apparser/movers/default.py | 6 ++--- apparser/text_readers/readers/compound.py | 6 ++--- docs/api/cv/events/index.rst | 2 +- docs/index.rst | 2 +- docs/info/about.rst | 2 +- pyproject.toml | 4 +-- tests/apparser/cv/events/test_undetected.py | 2 +- .../exceptions/test_text_not_found.py | 7 ++--- .../instructions/default/test_press.py | 12 +++++++++ .../instructions/ocr/test_text_getter.py | 4 +-- .../instructions/ui/algorithms/test_unique.py | 8 ++++++ tests/apparser/instructions/ui/test_click.py | 2 +- 26 files changed, 83 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index f3d6b0d..8a42147 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Apparser is a Python library for automating desktop applications and interacting # Installation ```bash -# Base Apparser package with base ocr model +# Base Apparser package with base OCR model pip install apparser # Apparser with text recognition support @@ -78,7 +78,7 @@ Full documentation is available PyPI # Donation -If you'd like to financially support the developers for their work: +If you'd like to financially support the developers' work: Donation link diff --git a/apparser/core/ui/coordinates.py b/apparser/core/ui/coordinates.py index a9a3c30..47aaf9c 100644 --- a/apparser/core/ui/coordinates.py +++ b/apparser/core/ui/coordinates.py @@ -30,10 +30,10 @@ def __init__( raise TypeError('from_ui must be BaseUi') if not isinstance(point_one, (Point, RelativelyPoint)): - raise TypeError('point1 must be Point or RelativelyPoint') + raise TypeError('point_one must be Point or RelativelyPoint') elif not isinstance(point_two, (Point, RelativelyPoint)): - raise TypeError('point2 must be Point, RelativelyPoint') + raise TypeError('point_two must be Point or RelativelyPoint') self.__from_ui = from_ui self.__point_one = point_one diff --git a/apparser/cv/events/undetected.py b/apparser/cv/events/undetected.py index ca0d1cb..f248209 100644 --- a/apparser/cv/events/undetected.py +++ b/apparser/cv/events/undetected.py @@ -10,4 +10,4 @@ def __str__(self) -> str: :return: Undetected event name. :rtype: str """ - return "UnDetected" + return "Undetected" diff --git a/apparser/exceptions/instruction_not_found.py b/apparser/exceptions/instruction_not_found.py index bfd38ee..45dc73e 100644 --- a/apparser/exceptions/instruction_not_found.py +++ b/apparser/exceptions/instruction_not_found.py @@ -1,7 +1,7 @@ class InstructionNotFoundException(Exception): """Represent a failure to resolve an instruction.""" - def __init__(self, text: str): + def __init__(self, text: str | None): """Initialize an instruction lookup exception. :param text: Error message text. diff --git a/apparser/exceptions/text_not_found.py b/apparser/exceptions/text_not_found.py index 87d7813..4ed712d 100644 --- a/apparser/exceptions/text_not_found.py +++ b/apparser/exceptions/text_not_found.py @@ -5,12 +5,12 @@ def __init__(self, min_similarity: float): """Initialize a text lookup exception. :param min_similarity: Minimum accepted similarity value. - :type min_similarity: float + :type min_similarity: float | int :raises TypeError: If ``min_similarity`` has an invalid type. :raises ValueError: If ``min_similarity`` is outside the inclusive range from 0 to 1. """ - if not isinstance(min_similarity, float): - raise TypeError("min_similarity must be float") + if not isinstance(min_similarity, (float, int)): + raise TypeError("min_similarity must be a number") if min_similarity < 0 or min_similarity > 1: raise ValueError("min_similarity must be between 0 and 1") diff --git a/apparser/instructions/default/click.py b/apparser/instructions/default/click.py index e3d2e99..b982545 100644 --- a/apparser/instructions/default/click.py +++ b/apparser/instructions/default/click.py @@ -35,7 +35,7 @@ def __init__(self, click_type: BaseKeyCode = LeftClick()): :param click_type: Mouse button to press down. :type click_type: BaseKeyCode - :raises TypeError: If ``click_type`` is neither :class:`BaseKeyCode`. + :raises TypeError: If ``click_type`` is not a :class:`BaseKeyCode`. """ if not isinstance(click_type, BaseKeyCode): raise TypeError('click_type must be BaseKeyCode') @@ -58,7 +58,7 @@ def __init__(self, click_type: BaseKeyCode = LeftClick()): :param click_type: Mouse button to release. :type click_type: BaseKeyCode - :raises TypeError: If ``click_type`` is neither :class:`BaseKeyCode`. + :raises TypeError: If ``click_type`` is not a :class:`BaseKeyCode`. """ if not isinstance(click_type, BaseKeyCode): raise TypeError('click_type must be BaseKeyCode') diff --git a/apparser/instructions/default/press.py b/apparser/instructions/default/press.py index 025fb45..a361842 100644 --- a/apparser/instructions/default/press.py +++ b/apparser/instructions/default/press.py @@ -35,7 +35,13 @@ def __init__(self, keys: list[BaseKeyCode | str] | str): :param keys: Keys to press together. :type keys: list[BaseKeyCode | str] | str + :raises TypeError: If ``keys`` or any key has an invalid type. """ + if isinstance(keys, str): + keys = [keys] + elif not isinstance(keys, list): + raise TypeError('keys must be list or str') + self.__keys = keys self.__validate() diff --git a/apparser/instructions/ocr/click_on_text.py b/apparser/instructions/ocr/click_on_text.py index 62c1a0b..dfb9292 100644 --- a/apparser/instructions/ocr/click_on_text.py +++ b/apparser/instructions/ocr/click_on_text.py @@ -29,7 +29,7 @@ def __init__(self, text: str, :type min_similarity: float :param offset: Offset relative to the detected text center. :type offset: Point | RelativelyPoint - :param text_getter: Instruction used to extract text from the screen. If None use GetText() + :param text_getter: Instruction used to extract text from the screen. If None, use GetText(). :type text_getter: GetText | None :param sleep_time_after_move: Delay before the click is performed. :type sleep_time_after_move: float diff --git a/apparser/instructions/ocr/move_to_text.py b/apparser/instructions/ocr/move_to_text.py index a909fc2..b98580f 100644 --- a/apparser/instructions/ocr/move_to_text.py +++ b/apparser/instructions/ocr/move_to_text.py @@ -26,7 +26,7 @@ def __init__(self, text: str, :type min_similarity: float :param offset: Offset relative to the detected text center. :type offset: Point | RelativelyPoint - :param text_getter: Instruction used to extract text from the screen. If None use GetText() + :param text_getter: Instruction used to extract text from the screen. If None, use GetText(). :type text_getter: GetText | None """ if text_getter is None: @@ -58,11 +58,11 @@ def perform(self, ui: BaseUi, text_reader: BaseTextReader, *args, **kwargs): needed_data, rating = self.find_text(self.__text_getter.local_answer) if self.__min_similarity > rating: raise TextNotFoundException(self.__min_similarity) - y_cords = [needed_data.coordinates.right_top.y, needed_data.coordinates.right_bottom.y] - x_cords = [needed_data.coordinates.left_top.x, needed_data.coordinates.right_top.x] + y_coords = [needed_data.coordinates.right_top.y, needed_data.coordinates.right_bottom.y] + x_coords = [needed_data.coordinates.left_top.x, needed_data.coordinates.right_top.x] offset_point = self.__get_local_offset(ui) - x_center = round((x_cords[0] - x_cords[1]) / 2 + x_cords[1]) + offset_point.x - y_center = round((y_cords[0] - y_cords[1]) / 2 + y_cords[1]) + offset_point.y + x_center = round((x_coords[0] - x_coords[1]) / 2 + x_coords[1]) + offset_point.x + y_center = round((y_coords[0] - y_coords[1]) / 2 + y_coords[1]) + offset_point.y MouseMove(Point(x_center, y_center)).perform(ui) @property diff --git a/apparser/instructions/ocr/plot_text.py b/apparser/instructions/ocr/plot_text.py index 21af8d0..4d7dc5a 100644 --- a/apparser/instructions/ocr/plot_text.py +++ b/apparser/instructions/ocr/plot_text.py @@ -29,14 +29,14 @@ def __init__(self, draw: ImageDraw.Draw, color: Tuple[int, int, int, int], def draw(self, bboxes: list[TextData]): for data in bboxes: - self.__paint_cords(data) + self.__paint_coords(data) self.__paint_lines(data) def __paint_lines(self, data: TextData): shape = [(data.coordinates.left_top.x, data.coordinates.left_top.y), (data.coordinates.right_bottom.x, data.coordinates.right_bottom.y)] self.__draw.rectangle(shape, outline=self.__color, width=1) - def __paint_cords(self, data: TextData): + def __paint_coords(self, data: TextData): y = data.coordinates.left_top.y + self.__text_move.y if y < 0: y = data.coordinates.right_bottom.y - self.__text_move.y @@ -54,7 +54,7 @@ def __init__(self, text_getter: GetText | None = None, text_move: Point = Point(0, 20)): """Initialize an OCR plotting instruction. - :param text_getter: Instruction used to extract text from the screen. If None use GetText() + :param text_getter: Instruction used to extract text from the screen. If None, use GetText(). :type text_getter: GetText | None :param color_rgba: RGBA color used for the rendered overlays. :type color_rgba: tuple[int, int, int, int] diff --git a/apparser/instructions/ocr/print_all_text.py b/apparser/instructions/ocr/print_all_text.py index 9c78abb..a554803 100644 --- a/apparser/instructions/ocr/print_all_text.py +++ b/apparser/instructions/ocr/print_all_text.py @@ -12,7 +12,7 @@ class PrintAllText(OCRInstruction): def __init__(self, text_getter: GetText | None = None): """Initialize an OCR text printing instruction. - :param text_getter: Instruction used to extract text from the screen. If None use GetText() + :param text_getter: Instruction used to extract text from the screen. If None, use GetText(). :type text_getter: GetText | None """ if text_getter is None: diff --git a/apparser/instructions/ocr/text_getter.py b/apparser/instructions/ocr/text_getter.py index 8e2ab2c..5defacc 100644 --- a/apparser/instructions/ocr/text_getter.py +++ b/apparser/instructions/ocr/text_getter.py @@ -30,42 +30,43 @@ def __init__(self, self.__reload_every_try = reload_every_try self.__local_answer = [] self.__global_answer = [] - self.__left_top_point_global = left_top_point + self.__left_top_point_local = left_top_point self.__screenshot = None @property def id(self) -> int: return 2000 - def __text_coordinates_to_local(self, text: TextData) -> TextData: + @staticmethod + def __shift_text_coordinates(text: TextData, offset: Point) -> TextData: new_coordinates = QuadPoints( - text.coordinates.left_top + self.__left_top_point_global, - text.coordinates.right_top + self.__left_top_point_global, - text.coordinates.right_bottom + self.__left_top_point_global, - text.coordinates.left_bottom + self.__left_top_point_global, + text.coordinates.left_top + offset, + text.coordinates.right_top + offset, + text.coordinates.right_bottom + offset, + text.coordinates.left_bottom + offset, ) return TextData(text.text, new_coordinates) - def __texts_coordinates_to_local(self, texts: list[TextData]) -> list[TextData]: + def __shift_texts_coordinates(self, texts: list[TextData], offset: Point) -> list[TextData]: returned_data = [] for text in texts: - returned_data.append(self.__text_coordinates_to_local(text)) + returned_data.append(self.__shift_text_coordinates(text, offset)) return returned_data def perform(self, ui: BaseUi, text_reader: BaseTextReader, *args, **kwargs): if len(self.__local_answer) != 0 and not self.__reload_every_try: return right_bottom_point = ui.point_to_local(ui.point_to_global(self.__right_bottom_point)) - self.__left_top_point_global = ui.point_to_local(ui.point_to_global(self.__left_top_point)) + self.__left_top_point_local = ui.point_to_local(ui.point_to_global(self.__left_top_point)) + left_top_point_global = ui.point_to_global(self.__left_top_point_local) screen = Image.fromarray(ui.get_screenshot()) - screen = screen.crop((self.__left_top_point_global.x, self.__left_top_point_global.y, right_bottom_point.x, + screen = screen.crop((self.__left_top_point_local.x, self.__left_top_point_local.y, right_bottom_point.x, right_bottom_point.y)) screen = numpy.array(screen) self.__screenshot = screen ai_answer = text_reader.read_image(screen) - self.__global_answer = ai_answer.copy() - ai_answer = self.__texts_coordinates_to_local(ai_answer) - self.__local_answer = ai_answer + self.__global_answer = self.__shift_texts_coordinates(ai_answer, left_top_point_global) + self.__local_answer = self.__shift_texts_coordinates(ai_answer, self.__left_top_point_local) @property def local_answer(self) -> list[TextData]: diff --git a/apparser/instructions/ui/algorithms/unique.py b/apparser/instructions/ui/algorithms/unique.py index 2021a58..be5a62e 100644 --- a/apparser/instructions/ui/algorithms/unique.py +++ b/apparser/instructions/ui/algorithms/unique.py @@ -56,10 +56,8 @@ def __init__(self, elif debugger == False: debugger = None - attributes.reverse() - self.__instructions = instructions - self.__attributes = attributes + self.__attributes = list(reversed(attributes)) self.__debugger = debugger def __form_args(self, instruction: BaseInstruction) -> dict[str, Any]: diff --git a/apparser/instructions/ui/click.py b/apparser/instructions/ui/click.py index 6356cb1..a121f53 100644 --- a/apparser/instructions/ui/click.py +++ b/apparser/instructions/ui/click.py @@ -23,11 +23,11 @@ def __init__(self, coordinates: Point | RelativelyPoint, :type click_type: RightClick | LeftClick :param mover: Mouse movement strategy used before the click. :type mover: BaseMover - :raises ValueError: If ``coordinates`` has an invalid type. + :raises TypeError: If ``coordinates`` has an invalid type. """ if (not isinstance(coordinates, Point) and not isinstance(coordinates, RelativelyPoint)): - raise ValueError('coordinates must be Point or RelativelyPoint') + raise TypeError('coordinates must be Point or RelativelyPoint') self.__click = MouseClick(click_type) self.__move = MouseMove(coordinates, mover=mover) diff --git a/apparser/movers/default.py b/apparser/movers/default.py index 3c65f10..45b4590 100644 --- a/apparser/movers/default.py +++ b/apparser/movers/default.py @@ -17,10 +17,10 @@ def __init__(self, :raises ValueError: If ``duration`` is negative. """ if not (isinstance(duration, float) or isinstance(duration, int)): - raise TypeError("Duration must be a number") + raise TypeError("duration must be a number") if duration < 0: - raise ValueError("Duration must be >= 0") + raise ValueError("duration must be >= 0") self.__duration = duration @@ -31,4 +31,4 @@ def move(self, position: Point): :type position: Point """ pyautogui.moveTo(position.x, position.y, - duration=self.__duration) + duration=self.__duration) diff --git a/apparser/text_readers/readers/compound.py b/apparser/text_readers/readers/compound.py index 9efdded..1314ddb 100644 --- a/apparser/text_readers/readers/compound.py +++ b/apparser/text_readers/readers/compound.py @@ -34,7 +34,7 @@ def _cut_by_coordinates(image: numpy.ndarray, coordinates: QuadPoints) -> numpy. class CompoundReader(BaseTextReader): """Detect text regions and scan each detected image fragment.""" - def __init__(self, detector: BaseTextDetector, scanner: BaseTextScanner ): + def __init__(self, detector: BaseTextDetector, scanner: BaseTextScanner): """Initialize a compound text reader. :param detector: Detector used to find text regions in an image. @@ -62,7 +62,7 @@ def read_image(self, image: numpy.ndarray) -> list[TextData]: """ result = [] for coordinates in self.__detector.read_image(image): - cuted_image = _cut_by_coordinates(image, coordinates) - text = self.__scanner.read_image(cuted_image) + cropped_image = _cut_by_coordinates(image, coordinates) + text = self.__scanner.read_image(cropped_image) result.append(TextData(text=text, coordinates=coordinates)) return result diff --git a/docs/api/cv/events/index.rst b/docs/api/cv/events/index.rst index 3a73949..b8663a4 100644 --- a/docs/api/cv/events/index.rst +++ b/docs/api/cv/events/index.rst @@ -16,7 +16,7 @@ events :show-inheritance: :member-order: bysource -.. autoclass:: UnDetected +.. autoclass:: Undetected :members: :undoc-members: :show-inheritance: diff --git a/docs/index.rst b/docs/index.rst index c6d746e..62030a8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,7 +26,7 @@ Repository on `GitHub `__ Donation =========== -If you'd like to financially support the developers for their work: +If you'd like to financially support the developers' work: .. raw:: html diff --git a/docs/info/about.rst b/docs/info/about.rst index 2826f6e..3fd71be 100644 --- a/docs/info/about.rst +++ b/docs/info/about.rst @@ -27,7 +27,7 @@ Repository on `GitHub `__ Donation ---------- -If you'd like to financially support the developers for their work: +If you'd like to financially support the developers' work: .. raw:: html diff --git a/pyproject.toml b/pyproject.toml index 99d3528..d009d10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,11 +10,11 @@ dependencies = [ "onnxruntime>=1.20.1" ] name = "apparser" -version = "1.1.1" +version = "1.1.2" authors = [ { name = "Terochkin A.S", email = "apparser.development@gmail.com" }, ] -description = "Apparser is a Python library designed for automating desktop applications and managing UI interfaces using artificial intelligence, such as OCR or object detection models. " +description = "Apparser is a Python library for automating desktop applications and interacting with UIs using AI-powered tools such as OCR and object detection models." readme = "README.md" requires-python = ">=3.10" classifiers = [ diff --git a/tests/apparser/cv/events/test_undetected.py b/tests/apparser/cv/events/test_undetected.py index 52cbb37..b90286b 100644 --- a/tests/apparser/cv/events/test_undetected.py +++ b/tests/apparser/cv/events/test_undetected.py @@ -4,4 +4,4 @@ def test_undetected_string_representation() -> None: - assert str(Undetected()) == "UnDetected" + assert str(Undetected()) == "Undetected" diff --git a/tests/apparser/exceptions/test_text_not_found.py b/tests/apparser/exceptions/test_text_not_found.py index 931c684..c5af75d 100644 --- a/tests/apparser/exceptions/test_text_not_found.py +++ b/tests/apparser/exceptions/test_text_not_found.py @@ -7,13 +7,14 @@ from apparser.exceptions.text_not_found import TextNotFoundException -def test_text_not_found_accepts_valid_similarity() -> None: - error = TextNotFoundException(0.5) +@pytest.mark.parametrize("value", [0, 0.5, 1]) +def test_text_not_found_accepts_valid_similarity(value: float | int) -> None: + error = TextNotFoundException(value) assert isinstance(error, Exception) -@pytest.mark.parametrize("value", [0, "0.5", None, object()]) +@pytest.mark.parametrize("value", ["0.5", None, object()]) def test_text_not_found_rejects_invalid_similarity_type(value: Any) -> None: with pytest.raises(TypeError): TextNotFoundException(value) diff --git a/tests/apparser/instructions/default/test_press.py b/tests/apparser/instructions/default/test_press.py index f9b496a..23692af 100644 --- a/tests/apparser/instructions/default/test_press.py +++ b/tests/apparser/instructions/default/test_press.py @@ -32,6 +32,18 @@ def test_press_keys_combination_presses_and_releases_keys() -> None: assert instruction.id == 3 +def test_press_keys_combination_treats_string_as_one_key() -> None: + instruction = PressKeysCombination("ctrl") + instruction.perform() + assert pyautogui_stub.press_calls == ["ctrl"] + assert pyautogui_stub.release_calls == ["ctrl"] + + +def test_press_keys_combination_rejects_invalid_keys_type() -> None: + with pytest.raises(TypeError): + PressKeysCombination(object()) + + def test_press_keys_combination_rejects_invalid_key_on_perform() -> None: with pytest.raises(TypeError): PressKeysCombination([object()]) diff --git a/tests/apparser/instructions/ocr/test_text_getter.py b/tests/apparser/instructions/ocr/test_text_getter.py index d16ba0c..06ee4c6 100644 --- a/tests/apparser/instructions/ocr/test_text_getter.py +++ b/tests/apparser/instructions/ocr/test_text_getter.py @@ -20,12 +20,12 @@ def test_text_getter_reads_and_converts_coordinates() -> None: ] ) instruction = GetText(Point(1, 1), Point(4, 3)) - ui = FakeUi(screenshot=screenshot) + ui = FakeUi(offset=Point(10, 20), screenshot=screenshot) instruction.perform(ui, reader) assert reader.images[0].shape == (2, 3, 3) - assert instruction.global_answer[0].coordinates.left_top == Point(0, 0) + assert instruction.global_answer[0].coordinates.left_top == Point(11, 21) assert instruction.local_answer[0].coordinates.left_top == Point(1, 1) assert instruction.screenshot.shape == (2, 3, 3) diff --git a/tests/apparser/instructions/ui/algorithms/test_unique.py b/tests/apparser/instructions/ui/algorithms/test_unique.py index 83b960d..be86d15 100644 --- a/tests/apparser/instructions/ui/algorithms/test_unique.py +++ b/tests/apparser/instructions/ui/algorithms/test_unique.py @@ -29,6 +29,14 @@ def test_unique_algorithm_injects_attributes_by_type() -> None: assert second.calls == [{"args": (ui,), "kwargs": {"name": "alex"}}] +def test_unique_algorithm_does_not_mutate_attributes() -> None: + attributes = [10, "alex"] + + UniqueAlgorithm([], attributes=attributes, debugger=False) + + assert attributes == [10, "alex"] + + def test_unique_algorithm_uses_debugger() -> None: debugger = FakeDebugger(call_inner=False) instruction = FakeIntAttributeInstruction(1) diff --git a/tests/apparser/instructions/ui/test_click.py b/tests/apparser/instructions/ui/test_click.py index d1c75fb..e0740c8 100644 --- a/tests/apparser/instructions/ui/test_click.py +++ b/tests/apparser/instructions/ui/test_click.py @@ -10,7 +10,7 @@ def test_mouse_click_to_rejects_invalid_coordinates() -> None: - with pytest.raises(ValueError): + with pytest.raises(TypeError): MouseClickTo(object()) From dc668b83bbe2313d2eb8f8b18b7249c5545ece30 Mon Sep 17 00:00:00 2001 From: lexter0705 Date: Thu, 25 Jun 2026 22:09:21 +0300 Subject: [PATCH 2/5] 1) Fixed App error raise 2) The PressKeysCombination class no longer accepts a string --- apparser/core/app.py | 3 ++- apparser/instructions/default/press.py | 12 ++++-------- apparser/instructions/ocr/plot_text.py | 2 +- tests/apparser/instructions/default/test_press.py | 2 +- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/apparser/core/app.py b/apparser/core/app.py index eeee884..c003c75 100644 --- a/apparser/core/app.py +++ b/apparser/core/app.py @@ -77,7 +77,8 @@ def start_app(self): self.__find_window_by_process_id(i.get_process_id()) if self.__ui is not None: return - self.__find_window_by_title() + if self.__window_title_name is not None: + self.__find_window_by_title() if self.__ui is None: raise WindowDoesNotValidException() diff --git a/apparser/instructions/default/press.py b/apparser/instructions/default/press.py index a361842..4970983 100644 --- a/apparser/instructions/default/press.py +++ b/apparser/instructions/default/press.py @@ -30,23 +30,20 @@ def perform(self, *args, **kwargs): class PressKeysCombination(BaseInstruction): """Send a keyboard shortcut as a pressed combination.""" - def __init__(self, keys: list[BaseKeyCode | str] | str): + def __init__(self, keys: list[BaseKeyCode | str]): """Initialize a key combination instruction. :param keys: Keys to press together. - :type keys: list[BaseKeyCode | str] | str + :type keys: list[BaseKeyCode | str] :raises TypeError: If ``keys`` or any key has an invalid type. """ - if isinstance(keys, str): - keys = [keys] - elif not isinstance(keys, list): - raise TypeError('keys must be list or str') + if not isinstance(keys, list): + raise TypeError('keys must be list') self.__keys = keys self.__validate() def __validate(self): - for key in self.__keys: if not (isinstance(key, BaseKeyCode) or isinstance(key, str)): raise TypeError('key_code must be BaseKeyCode or str') @@ -58,7 +55,6 @@ def id(self) -> int: def perform(self, *args, **kwargs): for key in self.__keys: pyautogui.keyDown(str(key)) - for key in self.__keys: pyautogui.keyUp(str(key)) diff --git a/apparser/instructions/ocr/plot_text.py b/apparser/instructions/ocr/plot_text.py index 4d7dc5a..a309fe2 100644 --- a/apparser/instructions/ocr/plot_text.py +++ b/apparser/instructions/ocr/plot_text.py @@ -72,7 +72,7 @@ def id(self) -> int: def perform(self, ui: BaseUi, text_reader: BaseTextReader, *args, **kwargs): self.__text_getter.perform(ui, text_reader) - texts = self.__text_getter.global_answer + texts = self.__text_getter.local_answer image = self.__text_getter.screenshot image = Image.fromarray(image) draw = ImageDraw.Draw(image) diff --git a/tests/apparser/instructions/default/test_press.py b/tests/apparser/instructions/default/test_press.py index 23692af..db9f9e6 100644 --- a/tests/apparser/instructions/default/test_press.py +++ b/tests/apparser/instructions/default/test_press.py @@ -33,7 +33,7 @@ def test_press_keys_combination_presses_and_releases_keys() -> None: def test_press_keys_combination_treats_string_as_one_key() -> None: - instruction = PressKeysCombination("ctrl") + instruction = PressKeysCombination(["ctrl"]) instruction.perform() assert pyautogui_stub.press_calls == ["ctrl"] assert pyautogui_stub.release_calls == ["ctrl"] From 799f7818c11382f1378696e4d801fe3c09706f5b Mon Sep 17 00:00:00 2001 From: lexter0705 Date: Thu, 25 Jun 2026 22:09:51 +0300 Subject: [PATCH 3/5] 1) Fixed tests to new API --- tests/apparser/instructions/ocr/test_plot_text.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/apparser/instructions/ocr/test_plot_text.py b/tests/apparser/instructions/ocr/test_plot_text.py index 9cea6b6..f4b2d9d 100644 --- a/tests/apparser/instructions/ocr/test_plot_text.py +++ b/tests/apparser/instructions/ocr/test_plot_text.py @@ -37,6 +37,6 @@ def test_plot_all_text_draws_and_shows_image(monkeypatch: pytest.MonkeyPatch) -> instruction.perform(FakeUi(), FakeTextReader()) - assert painter_calls == [getter.global_answer] + assert painter_calls == [getter.local_answer] assert shown == [True] assert instruction.id == 2004 From a239d32a87b3caf413c4aa6df7ba866632e01c66 Mon Sep 17 00:00:00 2001 From: lexter0705 Date: Wed, 1 Jul 2026 22:03:38 +0300 Subject: [PATCH 4/5] 1) Updated .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 725e98b..3c5145b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ apparser.egg-info dist __pycache__ _build*/ +_tmp*/ +.pypirc From e3c89b9fdbc2c5c13c5aeeb8c0343185288ab212 Mon Sep 17 00:00:00 2001 From: lexter0705 Date: Wed, 1 Jul 2026 22:12:55 +0300 Subject: [PATCH 5/5] 1) Fixed IdsAlgorithm and NamesAlgorithm instructions validation 2) Fixed RelativelyPoint error messages 3) Fixed instructions.Sleep docstring and annotation --- apparser/geometry/relatively_point.py | 4 ++-- apparser/instructions/default/sleep.py | 4 ++-- apparser/instructions/ui/algorithms/ids.py | 9 ++++++--- apparser/instructions/ui/algorithms/names.py | 9 ++++++--- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/apparser/geometry/relatively_point.py b/apparser/geometry/relatively_point.py index d325391..6665b36 100644 --- a/apparser/geometry/relatively_point.py +++ b/apparser/geometry/relatively_point.py @@ -18,10 +18,10 @@ def __init__(self, x_percent: float, y_percent: float): raise TypeError('y_percent must be number') if x_percent < -1 or x_percent > 1: - raise ValueError('x must be between -1 and 1') + raise ValueError('x_percent must be between -1 and 1') if y_percent < -1 or y_percent > 1: - raise ValueError('y must be between -1 and 1') + raise ValueError('y_percent must be between -1 and 1') self.__x_percent = x_percent self.__y_percent = y_percent diff --git a/apparser/instructions/default/sleep.py b/apparser/instructions/default/sleep.py index bd22f52..be5da03 100644 --- a/apparser/instructions/default/sleep.py +++ b/apparser/instructions/default/sleep.py @@ -6,11 +6,11 @@ class Sleep(BaseInstruction): """Pause execution for a fixed amount of time.""" - def __init__(self, sleep_time: float): + def __init__(self, sleep_time: float | int): """Initialize a sleep instruction. :param sleep_time: Delay duration in seconds. - :type sleep_time: float + :type sleep_time: float | int :raises ValueError: If ``sleep_time`` is not greater than zero. :raises TypeError: If ``sleep_time`` is not a number. """ diff --git a/apparser/instructions/ui/algorithms/ids.py b/apparser/instructions/ui/algorithms/ids.py index 605294e..4004bd7 100644 --- a/apparser/instructions/ui/algorithms/ids.py +++ b/apparser/instructions/ui/algorithms/ids.py @@ -11,14 +11,17 @@ def _check_instruction(instruction: tuple[int, list[Any]]) -> tuple[int, list[Any]]: if not isinstance(instruction, tuple): - raise TypeError(f"{instruction} must be tuple") + raise TypeError(f"instruction must be tuple") + + if len(instruction) < 2: + raise TypeError(f"instruction must have at least 2 arguments") instruction_id, instruction_args = instruction if not isinstance(instruction_id, int): - raise TypeError(f"{instruction_id} must be int") + raise TypeError(f"instruction_id must be int") if not isinstance(instruction_args, list): - raise TypeError(f"{instruction_args} must be list") + raise TypeError(f"instruction_args must be list") return instruction_id, instruction_args diff --git a/apparser/instructions/ui/algorithms/names.py b/apparser/instructions/ui/algorithms/names.py index 4bdae59..44ca2d1 100644 --- a/apparser/instructions/ui/algorithms/names.py +++ b/apparser/instructions/ui/algorithms/names.py @@ -12,14 +12,17 @@ def _check_instruction(instruction: tuple[str, list[Any]]) -> tuple[str, list[Any]]: if not isinstance(instruction, tuple): - raise TypeError(f"{instruction} must be tuple") + raise TypeError(f"instruction must be tuple") + + if len(instruction) < 2: + raise TypeError(f"instruction must have at least 2 arguments") instruction_name, instruction_args = instruction if not isinstance(instruction_name, str): - raise TypeError(f"{instruction_name} must be str") + raise TypeError(f"instruction_name must be str") if not isinstance(instruction_args, list): - raise TypeError(f"{instruction_args} must be list") + raise TypeError(f"instruction_args must be list") return instruction_name, instruction_args