From 4b502c5f3f145ed77fa57a8ee11843e149587706 Mon Sep 17 00:00:00 2001 From: Wellington Almeida Date: Fri, 17 Jan 2025 13:15:18 -0300 Subject: [PATCH 1/8] MNT: Add type annotations and improve function signatures. >> - Enhanced type annotations across botcity/core/bot.py and botcity/core/cv2find.py for better type safety and developer experience. >> - Added type hints to parameters and return types in key methods like find, find_all, find_until, and _load_cv2. >> - Ensured proper usage of Optional, List, Tuple, and Generator in function definitions. >> - Standardized method signatures for consistency, explicitly defining default values and expected data types. >> - Updated imports to include necessary types like Union, Optional, List, Tuple, Dict, and Generator. --- botcity/core/bot.py | 150 ++++++++++++++++++++++------------------ botcity/core/cv2find.py | 15 +++- 2 files changed, 96 insertions(+), 69 deletions(-) diff --git a/botcity/core/bot.py b/botcity/core/bot.py index af77767..ad21d42 100644 --- a/botcity/core/bot.py +++ b/botcity/core/bot.py @@ -6,8 +6,8 @@ import subprocess import time import webbrowser -from typing import Union, Tuple, Optional, List - +from typing import Union, Tuple, Optional, List, Dict, Box, Generator, Any +from numpy import ndarray import pyperclip from botcity.base import BaseBot, State @@ -121,7 +121,7 @@ def app(self, app: Union["Application", "WindowSpecification"]): # Display ########## - def add_image(self, label, path): + def add_image(self, label: str, path: str) -> None: """ Add an image into the state image map. @@ -131,7 +131,7 @@ def add_image(self, label, path): """ self.state.map_images[label] = path - def get_image_from_map(self, label): + def get_image_from_map(self, label: str) -> None: """ Return an image from teh state image map. @@ -149,25 +149,25 @@ def get_image_from_map(self, label): def find_multiple( self, - labels, - x=None, - y=None, - width=None, - height=None, + labels: List, + x: int = 0, + y: int = 0, + width: Optional[int] = None, + height: Optional[int] = None, *, - threshold=None, - matching=0.9, - waiting_time=10000, - best=True, - grayscale=False, - ): + threshold: Optional[int] = None, + matching: float = 0.9, + waiting_time: int = 10000, + best: bool = True, + grayscale: bool = False, + ) -> Dict: """ Find multiple elements defined by label on screen until a timeout happens. Args: labels (list): A list of image identifiers - x (int, optional): Search region start position x. Defaults to 0. - y (int, optional): Search region start position y. Defaults to 0. + x (int): Search region start position x. Defaults to 0. + y (int): Search region start position y. Defaults to 0. width (int, optional): Search region width. Defaults to screen width. height (int, optional): Search region height. Defaults to screen height. threshold (int, optional): The threshold to be applied when doing grayscale search. @@ -190,8 +190,6 @@ def _to_dict(lbs, elems): return {k: v for k, v in zip(lbs, elems)} screen_w, screen_h = self._fix_display_size() - x = x or 0 - y = y or 0 w = width or screen_w h = height or screen_h @@ -253,7 +251,14 @@ def _fix_display_size(self) -> Tuple[int, int]: return int(width * 2), int(height * 2) - def _find_multiple_helper(self, haystack, region, confidence, grayscale, needle): + def _find_multiple_helper( + self, + haystack: Image.Image, + region: Tuple[int, int, int, int], + confidence: float, + grayscale: bool, + needle: Union[Image.Image, ndarray, str] + ) -> Union[Box, None]: ele = cv2find.locate_all_opencv( needle, haystack, region=region, confidence=confidence, grayscale=grayscale ) @@ -265,18 +270,18 @@ def _find_multiple_helper(self, haystack, region, confidence, grayscale, needle) def find( self, - label, - x=None, - y=None, - width=None, - height=None, + label: str, + x: Optional[int] = None, + y: Optional[int] = None, + width: Optional[int] = None, + height: Optional[int] = None, *, - threshold=None, - matching=0.9, - waiting_time=10000, - best=True, - grayscale=False, - ): + threshold: Optional[int] = None, + matching: float = 0.9, + waiting_time: int = 10000, + best: bool = True, + grayscale: bool = False, + ) -> Tuple[int, int, int, int] | None: """ Find an element defined by label on screen until a timeout happens. @@ -315,18 +320,18 @@ def find( def find_until( self, - label, - x=None, - y=None, - width=None, - height=None, + label: str, + x: Optional[int] = None, + y: Optional[int] = None, + width: Optional[int] = None, + height: Optional[int] = None, *, - threshold=None, - matching=0.9, - waiting_time=10000, - best=True, - grayscale=False, - ): + threshold: Optional[int] = None, + matching: float = 0.9, + waiting_time: int = 10000, + best: bool = True, + grayscale: bool = False, + ) -> Tuple[int, int, int, int] | None: """ Find an element defined by label on screen until a timeout happens. @@ -399,17 +404,17 @@ def find_until( def find_all( self, - label, - x=None, - y=None, - width=None, - height=None, + label: str, + x: Optional[int] = None, + y: Optional[int] = None, + width: Optional[int] = None, + height: Optional[int] = None, *, - threshold=None, - matching=0.9, - waiting_time=10000, - grayscale=False, - ): + threshold: Optional[int] = None, + matching: float = 0.9, + waiting_time: int = 10000, + grayscale: bool = False, + ) -> Generator[Box, Any, None]: """ Find all elements defined by label on screen until a timeout happens. @@ -504,16 +509,16 @@ def find_same(item, items): def find_text( self, - label, - x=None, - y=None, - width=None, - height=None, + label: str, + x: Optional[int] = None, + y: Optional[int] = None, + width: Optional[int] = None, + height: Optional[int] = None, *, - threshold=None, - matching=0.9, - waiting_time=10000, - best=True, + threshold: Optional[int] = None, + matching: float = 0.9, + waiting_time: int = 10000, + best: bool = True, ): """ Find an element defined by label on screen until a timeout happens. @@ -570,7 +575,7 @@ def find_process(self, name: str = None, pid: str = None) -> Process: pass return None - def terminate_process(self, process: Process): + def terminate_process(self, process: Process) -> None: """ Terminate the process via the received Process object. @@ -582,7 +587,7 @@ def terminate_process(self, process: Process): if process.is_running(): raise Exception("Terminate process failed") - def get_last_element(self): + def get_last_element(self) -> Tuple[int, int, int, int]: """ Return the last element found. @@ -681,8 +686,14 @@ def save_screenshot(self, path: str) -> None: self.screenshot(path) def get_element_coords( - self, label, x=None, y=None, width=None, height=None, matching=0.9, best=True - ): + self, label: str, + x: Optional[int] = None, + y: Optional[int] = None, + width: Optional[int] = None, + height: Optional[int] = None, + matching: float = 0.9, + best: bool = True + ) -> Tuple[int, int] | Tuple[None, None]: """ Find an element defined by label on screen and returns its coordinates. @@ -736,7 +747,14 @@ def get_element_coords( return ele.left, ele.top def get_element_coords_centered( - self, label, x=None, y=None, width=None, height=None, matching=0.9, best=True + self, + label: str, + x: Optional[int] = None, + y: Optional[int] = None, + width: Optional[int] = None, + height: Optional[int] = None, + matching: float = 0.9, + best: bool = True ): """ Find an element defined by label on screen and returns its centered coordinates. diff --git a/botcity/core/cv2find.py b/botcity/core/cv2find.py index bbb6867..8d567f5 100644 --- a/botcity/core/cv2find.py +++ b/botcity/core/cv2find.py @@ -33,6 +33,8 @@ import collections import cv2 import numpy +from PIL.Image import Image +from typing import Union, Tuple, Optional, Generator, Any RUNNING_CV_2 = cv2.__version__[0] < '3' @@ -46,7 +48,7 @@ LOAD_GRAYSCALE = cv2.IMREAD_GRAYSCALE -def _load_cv2(img, grayscale=False): +def _load_cv2(img: Union[Image, numpy.ndarray, str], grayscale: bool = False) -> numpy.ndarray: """ TODO """ @@ -86,8 +88,15 @@ def _load_cv2(img, grayscale=False): return img_cv -def locate_all_opencv(needle_image, haystack_image, grayscale=False, limit=10000, region=None, step=1, - confidence=0.999): +def locate_all_opencv( + needle_image: Union[Image, numpy.ndarray, str], + haystack_image: Union[Image, numpy.ndarray, str], + grayscale: bool = False, + limit: int = 10000, + region: Optional[Tuple[int, int, int, int]] = None, + step: int = 1, + confidence: float = 0.999 +) -> Generator[Box, Any, None]: """ TODO - rewrite this faster but more memory-intensive than pure python From f093008fc1b88d752a4471dbf7fb057376027f88 Mon Sep 17 00:00:00 2001 From: welli7ngton Date: Sat, 18 Jan 2025 16:46:05 -0300 Subject: [PATCH 2/8] fix: start using the cv2find.Box type instead of trying to import from the typing module --- botcity/core/bot.py | 27 ++++++++++++++--------- botcity/core/cv2find.py | 47 +++++++++++++++++++++++++---------------- 2 files changed, 46 insertions(+), 28 deletions(-) diff --git a/botcity/core/bot.py b/botcity/core/bot.py index ad21d42..d93348a 100644 --- a/botcity/core/bot.py +++ b/botcity/core/bot.py @@ -6,7 +6,9 @@ import subprocess import time import webbrowser -from typing import Union, Tuple, Optional, List, Dict, Box, Generator, Any +from typing import Union, Tuple, Optional, List, Dict, Generator, Any +from collections import namedtuple + from numpy import ndarray import pyperclip @@ -131,7 +133,7 @@ def add_image(self, label: str, path: str) -> None: """ self.state.map_images[label] = path - def get_image_from_map(self, label: str) -> None: + def get_image_from_map(self, label: str) -> Image.Image: """ Return an image from teh state image map. @@ -257,8 +259,8 @@ def _find_multiple_helper( region: Tuple[int, int, int, int], confidence: float, grayscale: bool, - needle: Union[Image.Image, ndarray, str] - ) -> Union[Box, None]: + needle: Union[Image.Image, ndarray, str], + ) -> Union[cv2find.Box, None]: ele = cv2find.locate_all_opencv( needle, haystack, region=region, confidence=confidence, grayscale=grayscale ) @@ -414,7 +416,7 @@ def find_all( matching: float = 0.9, waiting_time: int = 10000, grayscale: bool = False, - ) -> Generator[Box, Any, None]: + ) -> Generator[cv2find.Box, Any, None]: """ Find all elements defined by label on screen until a timeout happens. @@ -438,7 +440,9 @@ def find_all( None if not found. """ - def deduplicate(elems): + def deduplicate( + elems: list[Generator[cv2find.Box, Any, None]] + ) -> list[Generator[cv2find.Box, Any, None]]: def find_same(item, items): x_start = item.left x_end = item.left + item.width @@ -554,7 +558,9 @@ def find_text( grayscale=True, ) - def find_process(self, name: str = None, pid: str = None) -> Process: + def find_process( + self, name: Optional[str] = None, pid: Optional[str] = None + ) -> Union[Process, None]: """ Find a process by name or PID @@ -686,13 +692,14 @@ def save_screenshot(self, path: str) -> None: self.screenshot(path) def get_element_coords( - self, label: str, + self, + label: str, x: Optional[int] = None, y: Optional[int] = None, width: Optional[int] = None, height: Optional[int] = None, matching: float = 0.9, - best: bool = True + best: bool = True, ) -> Tuple[int, int] | Tuple[None, None]: """ Find an element defined by label on screen and returns its coordinates. @@ -754,7 +761,7 @@ def get_element_coords_centered( width: Optional[int] = None, height: Optional[int] = None, matching: float = 0.9, - best: bool = True + best: bool = True, ): """ Find an element defined by label on screen and returns its centered coordinates. diff --git a/botcity/core/cv2find.py b/botcity/core/cv2find.py index 8d567f5..b922c74 100644 --- a/botcity/core/cv2find.py +++ b/botcity/core/cv2find.py @@ -30,15 +30,16 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ + import collections import cv2 import numpy from PIL.Image import Image from typing import Union, Tuple, Optional, Generator, Any -RUNNING_CV_2 = cv2.__version__[0] < '3' +RUNNING_CV_2 = cv2.__version__[0] < "3" -Box = collections.namedtuple('Box', 'left top width height') +Box = collections.namedtuple("Box", "left top width height") if RUNNING_CV_2: LOAD_COLOR = cv2.CV_LOAD_IMAGE_COLOR @@ -48,7 +49,9 @@ LOAD_GRAYSCALE = cv2.IMREAD_GRAYSCALE -def _load_cv2(img: Union[Image, numpy.ndarray, str], grayscale: bool = False) -> numpy.ndarray: +def _load_cv2( + img: Union[Image, numpy.ndarray, str], grayscale: bool = False +) -> numpy.ndarray: """ TODO """ @@ -68,23 +71,25 @@ def _load_cv2(img: Union[Image, numpy.ndarray, str], grayscale: bool = False) -> else: img_cv = cv2.imread(img, LOAD_COLOR) if img_cv is None: - raise IOError("Failed to read %s because file is missing, " - "has improper permissions, or is an " - "unsupported or invalid format" % img) + raise IOError( + "Failed to read %s because file is missing, " + "has improper permissions, or is an " + "unsupported or invalid format" % img + ) elif isinstance(img, numpy.ndarray): # don't try to convert an already-gray image to gray if grayscale and len(img.shape) == 3: # and img.shape[2] == 3: img_cv = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) else: img_cv = img - elif hasattr(img, 'convert'): + elif hasattr(img, "convert"): # assume its a PIL.Image, convert to cv format - img_array = numpy.array(img.convert('RGB')) + img_array = numpy.array(img.convert("RGB")) img_cv = img_array[:, :, ::-1].copy() # -1 does RGB -> BGR if grayscale: img_cv = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY) else: - raise TypeError('expected an image filename, OpenCV numpy array, or PIL image') + raise TypeError("expected an image filename, OpenCV numpy array, or PIL image") return img_cv @@ -95,7 +100,7 @@ def locate_all_opencv( limit: int = 10000, region: Optional[Tuple[int, int, int, int]] = None, step: int = 1, - confidence: float = 0.999 + confidence: float = 0.999, ) -> Generator[Box, Any, None]: """ TODO - rewrite this @@ -116,15 +121,19 @@ def locate_all_opencv( if region: haystack_image = haystack_image[ - region[1]:region[1] + region[3], - region[0]:region[0] + region[2] - ] + region[1] : region[1] + region[3], region[0] : region[0] + region[2] + ] else: - region = (0, 0) # full image; these values used in the yield statement - if (haystack_image.shape[0] < needle_image.shape[0] or - haystack_image.shape[1] < needle_image.shape[1]): + region = (0, 0, 0, 0) # full image; these values used in the yield statement + + if ( + haystack_image.shape[0] < needle_image.shape[0] + or haystack_image.shape[1] < needle_image.shape[1] + ): # avoid semi-cryptic OpenCV error below if bad size - raise ValueError('needle dimension(s) exceed the haystack image or region dimensions') + raise ValueError( + "needle dimension(s) exceed the haystack image or region dimensions" + ) if step == 2: confidence *= 0.95 @@ -147,6 +156,8 @@ def locate_all_opencv( matchy = matches[0] * step + region[1] # Order results before sending back - ordered = sorted(zip(matchx, matchy), key=lambda p: result[p[1]][p[0]], reverse=True) + ordered = sorted( + zip(matchx, matchy), key=lambda p: result[p[1]][p[0]], reverse=True + ) for x, y in ordered: yield Box(x, y, needle_width, needle_height) From ec24a3328f48156d32fea6f3db459cd5efb808dd Mon Sep 17 00:00:00 2001 From: welli7ngton Date: Sat, 18 Jan 2025 17:04:58 -0300 Subject: [PATCH 3/8] fix: fix the E203 flake8 error and remove unsuded module --- botcity/core/bot.py | 1 - botcity/core/cv2find.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/botcity/core/bot.py b/botcity/core/bot.py index d93348a..fa4a1d7 100644 --- a/botcity/core/bot.py +++ b/botcity/core/bot.py @@ -7,7 +7,6 @@ import time import webbrowser from typing import Union, Tuple, Optional, List, Dict, Generator, Any -from collections import namedtuple from numpy import ndarray diff --git a/botcity/core/cv2find.py b/botcity/core/cv2find.py index b922c74..39f6f98 100644 --- a/botcity/core/cv2find.py +++ b/botcity/core/cv2find.py @@ -121,7 +121,7 @@ def locate_all_opencv( if region: haystack_image = haystack_image[ - region[1] : region[1] + region[3], region[0] : region[0] + region[2] + region[1]: region[1] + region[3], region[0]: region[0] + region[2] ] else: region = (0, 0, 0, 0) # full image; these values used in the yield statement From c732c557631ccd4a4ac572906a3d6181d3447032 Mon Sep 17 00:00:00 2001 From: welli7ngton Date: Sat, 18 Jan 2025 17:16:08 -0300 Subject: [PATCH 4/8] fix: TypeError: unsupported operand type(s) for |: '_GenericAlias' and 'NoneType' --- botcity/core/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/botcity/core/bot.py b/botcity/core/bot.py index fa4a1d7..68f9648 100644 --- a/botcity/core/bot.py +++ b/botcity/core/bot.py @@ -282,7 +282,7 @@ def find( waiting_time: int = 10000, best: bool = True, grayscale: bool = False, - ) -> Tuple[int, int, int, int] | None: + ) -> Union[Tuple[int, int, int, int], None]: """ Find an element defined by label on screen until a timeout happens. From 2dbef9395de41988251fe46edfaa4ef95b20fff6 Mon Sep 17 00:00:00 2001 From: welli7ngton Date: Sat, 18 Jan 2025 17:22:27 -0300 Subject: [PATCH 5/8] fix: typeerror --- botcity/core/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/botcity/core/bot.py b/botcity/core/bot.py index 68f9648..21154dc 100644 --- a/botcity/core/bot.py +++ b/botcity/core/bot.py @@ -332,7 +332,7 @@ def find_until( waiting_time: int = 10000, best: bool = True, grayscale: bool = False, - ) -> Tuple[int, int, int, int] | None: + ) -> Union[Tuple[int, int, int, int], None]: """ Find an element defined by label on screen until a timeout happens. From 11efdc5778ada022fbe1b2f72866fa86df0a0479 Mon Sep 17 00:00:00 2001 From: welli7ngton Date: Sat, 18 Jan 2025 17:24:21 -0300 Subject: [PATCH 6/8] fix: type error --- botcity/core/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/botcity/core/bot.py b/botcity/core/bot.py index 21154dc..5272d63 100644 --- a/botcity/core/bot.py +++ b/botcity/core/bot.py @@ -699,7 +699,7 @@ def get_element_coords( height: Optional[int] = None, matching: float = 0.9, best: bool = True, - ) -> Tuple[int, int] | Tuple[None, None]: + ) -> Union[Tuple[int, int], Tuple[None, None]]: """ Find an element defined by label on screen and returns its coordinates. From a3ae7173db23000d45af1774098af7d04dc56dc2 Mon Sep 17 00:00:00 2001 From: welli7ngton Date: Fri, 24 Jan 2025 20:52:17 -0300 Subject: [PATCH 7/8] typing: add return annotation to find_text --- botcity/core/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/botcity/core/bot.py b/botcity/core/bot.py index 5272d63..ce4f904 100644 --- a/botcity/core/bot.py +++ b/botcity/core/bot.py @@ -522,7 +522,7 @@ def find_text( matching: float = 0.9, waiting_time: int = 10000, best: bool = True, - ): + ) -> Union[Tuple[int, int, int, int], None]: """ Find an element defined by label on screen until a timeout happens. From 16efc50c7e6cbdecb5b791cbf54a3085c0f18ddf Mon Sep 17 00:00:00 2001 From: welli7ngton Date: Fri, 24 Jan 2025 21:03:13 -0300 Subject: [PATCH 8/8] refactor: change the return annotation in methods find, find_until and get_last_element to cv2find.Box to improve code readability --- botcity/core/bot.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/botcity/core/bot.py b/botcity/core/bot.py index ce4f904..dbcdfda 100644 --- a/botcity/core/bot.py +++ b/botcity/core/bot.py @@ -282,7 +282,7 @@ def find( waiting_time: int = 10000, best: bool = True, grayscale: bool = False, - ) -> Union[Tuple[int, int, int, int], None]: + ) -> Union[cv2find.Box, None]: """ Find an element defined by label on screen until a timeout happens. @@ -332,7 +332,7 @@ def find_until( waiting_time: int = 10000, best: bool = True, grayscale: bool = False, - ) -> Union[Tuple[int, int, int, int], None]: + ) -> Union[cv2find.Box, None]: """ Find an element defined by label on screen until a timeout happens. @@ -522,7 +522,7 @@ def find_text( matching: float = 0.9, waiting_time: int = 10000, best: bool = True, - ) -> Union[Tuple[int, int, int, int], None]: + ) -> Union[cv2find.Box, None]: """ Find an element defined by label on screen until a timeout happens. @@ -592,7 +592,7 @@ def terminate_process(self, process: Process) -> None: if process.is_running(): raise Exception("Terminate process failed") - def get_last_element(self) -> Tuple[int, int, int, int]: + def get_last_element(self) -> cv2find.Box: """ Return the last element found.