-
Notifications
You must be signed in to change notification settings - Fork 1.6k
add reusable Selector support and flexible argument passing #1167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,7 +22,23 @@ def test_exists(app: u2.Device): | |
| assert app(text='Addition').exists(timeout=.1) | ||
| assert not app(text='should-not-exists').exists | ||
| assert not app(text='should-not-exists').exists(timeout=.1) | ||
|
|
||
|
|
||
|
|
||
| def test_exists2(app: u2.Device): | ||
| # New writing style | ||
| addition_label = Selector(text="Addition") | ||
| # Use selector as positional argument | ||
| assert app(addition_label).exists | ||
| # Use selector as keyword argument | ||
| assert app(selector=addition_label).exists(timeout=.1) | ||
|
|
||
| not_exist_label = Selector(text="should-not-exists") | ||
| assert not app(not_exist_label).exists | ||
| assert not app(selector=not_exist_label).exists(timeout=.1) | ||
|
|
||
| # Use selector as parameter for other methods | ||
| assert app(addition_label).right(not_exist_label) is None | ||
|
|
||
|
Comment on lines
+28
to
+42
|
||
|
|
||
| def test_selector_info(app: u2.Device): | ||
| _info = app(text="Addition").info | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -463,8 +463,8 @@ def serial(self) -> str: | |||||||||||||||||||||||||||||||||||
| return self._serial | ||||||||||||||||||||||||||||||||||||
| return self.shell(['getprop', 'ro.serialno']).output.strip() | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| def __call__(self, **kwargs) -> 'UiObject': | ||||||||||||||||||||||||||||||||||||
| return UiObject(self, Selector(**kwargs)) | ||||||||||||||||||||||||||||||||||||
| def __call__(self, *args, **kwargs) -> 'UiObject': | ||||||||||||||||||||||||||||||||||||
| return UiObject(self, Selector(*args, **kwargs)) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
| return UiObject(self, Selector(*args, **kwargs)) | |
| """ | |
| Create a UiObject selector. | |
| Use keyword arguments, for example: | |
| d(text="Clock") | |
| Positional arguments are not supported and will raise TypeError | |
| to avoid silently creating an unintended empty selector. | |
| """ | |
| if args: | |
| raise TypeError( | |
| "Positional arguments are not supported in Device.__call__. " | |
| "Use keyword arguments, e.g. d(text='Clock')." | |
| ) | |
| return UiObject(self, Selector(**kwargs)) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,12 +43,44 @@ class Selector(dict): | |
| } | ||
| __mask, __childOrSibling, __childOrSiblingSelector = "mask", "childOrSibling", "childOrSiblingSelector" | ||
|
|
||
| def __init__(self, **kwargs): | ||
| super(Selector, self).__setitem__(self.__mask, 0) | ||
| super(Selector, self).__setitem__(self.__childOrSibling, []) | ||
| super(Selector, self).__setitem__(self.__childOrSiblingSelector, []) | ||
| for k in kwargs: | ||
| self[k] = kwargs[k] | ||
| def __init__(self, *args, **kwargs): | ||
| super().__init__() | ||
|
|
||
| # Initialize internal fields (bypass __setitem__ to avoid mask update) | ||
| dict.__setitem__(self, self.__mask, 0) | ||
| dict.__setitem__(self, self.__childOrSibling, []) | ||
| dict.__setitem__(self, self.__childOrSiblingSelector, []) | ||
|
|
||
| # Extract source selector (support both positional and kwarg) | ||
| selector = None | ||
| if args and len(args) == 1: | ||
| selector = args[0] | ||
| if "selector" in kwargs: | ||
| selector = kwargs.pop("selector") | ||
|
|
||
| if selector and isinstance(selector, Selector): | ||
| self._copy_from(selector) | ||
|
|
||
| # Apply remaining kwargs (triggers __setitem__ + mask update) | ||
| for k, v in kwargs.items(): | ||
| self[k] = v | ||
|
||
|
|
||
| def _copy_from(self, other): | ||
| """Internal: deep copy from another Selector (shared by __init__ and clone).""" | ||
| # Copy mask directly | ||
| dict.__setitem__(self, self.__mask, other[self.__mask]) | ||
|
|
||
| # Shallow copy of child/sibling list | ||
| dict.__setitem__(self, self.__childOrSibling, other[self.__childOrSibling][:]) | ||
|
|
||
| # Deep copy of nested selectors | ||
| dict.__setitem__(self, self.__childOrSiblingSelector, | ||
| [s.clone() for s in other[self.__childOrSiblingSelector]]) | ||
|
|
||
| # Copy normal fields (triggers __setitem__ to update mask) | ||
| for key, value in other.items(): | ||
| if key not in {self.__mask, self.__childOrSibling, self.__childOrSiblingSelector}: | ||
| self[key] = value | ||
|
|
||
| def __str__(self): | ||
| """ remove useless part for easily debugger """ | ||
|
|
@@ -78,25 +110,24 @@ def __delitem__(self, k): | |
| self).__setitem__(self.__mask, | ||
| self[self.__mask] & ~self.__fields[k][0]) | ||
|
|
||
| def __deepcopy__(self, memo): | ||
| """Support copy.deepcopy() - delegates to our clean clone logic.""" | ||
| return self.clone() | ||
|
|
||
| def clone(self): | ||
| kwargs = dict((k, self[k]) for k in self if k not in [ | ||
| self.__mask, self.__childOrSibling, self.__childOrSiblingSelector | ||
| ]) | ||
| selector = Selector(**kwargs) | ||
| for v in self[self.__childOrSibling]: | ||
| selector[self.__childOrSibling].append(v) | ||
| for s in self[self.__childOrSiblingSelector]: | ||
| selector[self.__childOrSiblingSelector].append(s.clone()) | ||
| return selector | ||
|
|
||
| def child(self, **kwargs): | ||
| """Return a deep clone of this Selector.""" | ||
| new_selector = Selector() | ||
| new_selector._copy_from(self) | ||
| return new_selector | ||
|
|
||
| def child(self, *args, **kwargs): | ||
| self[self.__childOrSibling].append("child") | ||
| self[self.__childOrSiblingSelector].append(Selector(**kwargs)) | ||
| self[self.__childOrSiblingSelector].append(Selector(*args, **kwargs)) | ||
| return self | ||
|
|
||
| def sibling(self, **kwargs): | ||
| def sibling(self, *args, **kwargs): | ||
| self[self.__childOrSibling].append("sibling") | ||
| self[self.__childOrSiblingSelector].append(Selector(**kwargs)) | ||
| self[self.__childOrSiblingSelector].append(Selector(*args, **kwargs)) | ||
| return self | ||
|
|
||
| def update_instance(self, i): | ||
|
|
@@ -217,7 +248,7 @@ def long_click(self, duration: float = 0.5, timeout=None): | |
| x, y = self.center() | ||
| return self.session.long_click(x, y, duration) | ||
|
|
||
| def drag_to(self, *args, **kwargs): | ||
| def drag_to(self, *args, **kwargs) -> bool: | ||
| duration = kwargs.pop('duration', 0.5) | ||
| timeout = kwargs.pop('timeout', None) | ||
| self.must_wait(timeout=timeout) | ||
|
|
@@ -231,7 +262,9 @@ def drag2xy(x, y): | |
| return self.jsonrpc.dragTo(self.selector, x, y, steps) | ||
|
|
||
| return drag2xy(*args, **kwargs) | ||
| return self.jsonrpc.dragTo(self.selector, Selector(**kwargs), steps) | ||
| elif len(args) == 1 and isinstance(args[0], Selector): | ||
| return self.jsonrpc.dragTo(self.selector, args[0], steps) | ||
| return self.jsonrpc.dragTo(self.selector, Selector(*args, **kwargs), steps) | ||
|
|
||
| def swipe(self, direction, steps=10): | ||
| """ | ||
|
|
@@ -357,11 +390,11 @@ def clear_text(self, timeout=None): | |
| self.must_wait(timeout=timeout) | ||
| return self.set_text(None) | ||
|
|
||
| def child(self, **kwargs): | ||
| return UiObject(self.session, self.selector.clone().child(**kwargs)) | ||
| def child(self, *args, **kwargs) -> 'UiObject': | ||
| return UiObject(self.session, self.selector.clone().child(*args, **kwargs)) | ||
|
|
||
| def sibling(self, **kwargs): | ||
| return UiObject(self.session, self.selector.clone().sibling(**kwargs)) | ||
| def sibling(self, *args, **kwargs) -> 'UiObject': | ||
| return UiObject(self.session, self.selector.clone().sibling(*args, **kwargs)) | ||
|
|
||
| child_selector, from_parent = child, sibling | ||
|
|
||
|
|
@@ -446,38 +479,38 @@ def next(self): | |
|
|
||
| return Iter() | ||
|
|
||
| def right(self, **kwargs): | ||
| def right(self, *args, **kwargs) -> 'UiObject': | ||
| def onrightof(rect1, rect2): | ||
| left, top, right, bottom = intersect(rect1, rect2) | ||
| return rect2["left"] - rect1["right"] if top < bottom else -1 | ||
|
|
||
| return self.__view_beside(onrightof, **kwargs) | ||
| return self.__view_beside(onrightof, *args, **kwargs) | ||
|
|
||
| def left(self, **kwargs): | ||
| def left(self, *args, **kwargs) -> 'UiObject': | ||
| def onleftof(rect1, rect2): | ||
| left, top, right, bottom = intersect(rect1, rect2) | ||
| return rect1["left"] - rect2["right"] if top < bottom else -1 | ||
|
|
||
| return self.__view_beside(onleftof, **kwargs) | ||
| return self.__view_beside(onleftof, *args, **kwargs) | ||
|
|
||
| def up(self, **kwargs): | ||
| def up(self, *args, **kwargs) -> 'UiObject': | ||
| def above(rect1, rect2): | ||
| left, top, right, bottom = intersect(rect1, rect2) | ||
| return rect1["top"] - rect2["bottom"] if left < right else -1 | ||
|
|
||
| return self.__view_beside(above, **kwargs) | ||
| return self.__view_beside(above, *args, **kwargs) | ||
|
|
||
| def down(self, **kwargs): | ||
| def down(self, *args, **kwargs) -> 'UiObject': | ||
| def under(rect1, rect2): | ||
| left, top, right, bottom = intersect(rect1, rect2) | ||
| return rect2["top"] - rect1["bottom"] if left < right else -1 | ||
|
|
||
| return self.__view_beside(under, **kwargs) | ||
| return self.__view_beside(under, *args, **kwargs) | ||
|
|
||
| def __view_beside(self, onsideof, **kwargs): | ||
| def __view_beside(self, onsideof, *args, **kwargs): | ||
| bounds = self.info["bounds"] | ||
| min_dist, found = -1, None | ||
| for ui in UiObject(self.session, Selector(**kwargs)): | ||
| for ui in UiObject(self.session, Selector(*args, **kwargs)): | ||
| dist = onsideof(bounds, ui.info["bounds"]) | ||
| if dist >= 0 and (min_dist < 0 or dist < min_dist): | ||
| min_dist, found = dist, ui | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
README example creates
clock_icon = Selector(...)but the surrounding Quick Start examples only importuiautomator2 as u2(noSelectorsymbol in scope). This snippet will raise NameError as written. Please update the example to either referenceu2.Selector(...)or add an explicitfrom uiautomator2 import Selectorin the snippet/context.