def __getattribute__(self, attr_name):
"""
Attribute access for this class
If we already have criteria for both dialog and control then
resolve the control and return the requested attribute.
If we have only criteria for the dialog but the attribute
requested is an attribute of DialogWrapper then resolve the
dialog and return the requested attribute.
Otherwise delegate functionality to :func:`__getitem__` - which
sets the appropriate criteria for the control.
"""
allow_magic_lookup = object.__getattribute__(self, "allow_magic_lookup") # Beware of recursions here!
if not allow_magic_lookup:
try:
return object.__getattribute__(self, attr_name)
except AttributeError:
wrapper_object = self.wrapper_object() # 这里是重点,需要看对应函数的实现代码
try:
return getattr(wrapper_object, attr_name)
except AttributeError:
message = (
'Attribute "%s" exists neither on %s object nor on'
'targeted %s element wrapper (typo? or set allow_magic_lookup to True?)' %
(attr_name, self.__class__, wrapper_object.__class__))
raise AttributeError(message)
if attr_name in ['__dict__', '__members__', '__methods__', '__class__', '__name__']:
return object.__getattribute__(self, attr_name)
if attr_name in dir(self.__class__):
return object.__getattribute__(self, attr_name)
if attr_name in self.__dict__:
return self.__dict__[attr_name]
# if we already have 2 levels of criteria (dlg, control)
# this third must be an attribute so resolve and get the
# attribute and return it
if len(self.criteria) >= 2: # FIXME - this is surprising
ctrls = self.__resolve_control(self.criteria)
try:
return getattr(ctrls[-1], attr_name)
except AttributeError:
return self.child_window(best_match=attr_name)
else:
# FIXME - I don't get this part at all, why is it win32-specific and why not keep the same logic as above?
# if we have been asked for an attribute of the dialog
# then resolve the window and return the attribute
desktop_wrapper = self.backend.generic_wrapper_class(self.backend.element_info_class())
need_to_resolve = (len(self.criteria) == 1 and hasattr(desktop_wrapper, attr_name))
if hasattr(self.backend, 'dialog_class'):
need_to_resolve = need_to_resolve and hasattr(self.backend.dialog_class, attr_name)
# Probably there is no DialogWrapper for another backend
if need_to_resolve:
ctrls = self.__resolve_control(self.criteria)
return getattr(ctrls[-1], attr_name)
# It is a dialog/control criterion so let getitem
# deal with it
return self[attr_name]
def __get_ctrl(self, criteria_):
"""Get a control based on the various criteria"""
# make a copy of the criteria
criteria = [crit.copy() for crit in criteria_]
# find the dialog
if 'backend' not in criteria[0]:
criteria[0]['backend'] = self.backend.name
if self.app is not None:
# find_elements(...) accepts only "process" argument
criteria[0]['process'] = self.app.process
del criteria[0]['app']
dialog = self.backend.generic_wrapper_class(findwindows.find_element(**criteria[0]))
ctrls = []
# if there is only criteria for a dialog then return it
if len(criteria) > 1:
# so there was criteria for a control, add the extra criteria
# that are required for child controls
previous_parent = dialog.element_info
for ctrl_criteria in criteria[1:]:
ctrl_criteria["top_level_only"] = False
if "parent" not in ctrl_criteria:
ctrl_criteria["parent"] = previous_parent
if isinstance(ctrl_criteria["parent"], WindowSpecification):
ctrl_criteria["parent"] = ctrl_criteria["parent"].wrapper_object()
# resolve the control and return it
if 'backend' not in ctrl_criteria:
ctrl_criteria['backend'] = self.backend.name
ctrl = self.backend.generic_wrapper_class(findwindows.find_element(**ctrl_criteria))
previous_parent = ctrl.element_info
ctrls.append(ctrl)
if ctrls:
return (dialog, ) + tuple(ctrls)
else:
return (dialog, )
def __resolve_control(self, criteria, timeout=None, retry_interval=None):
"""
Find a control using criteria
* **criteria** - a list that contains 1 or 2 dictionaries
1st element is search criteria for the dialog
2nd element is search criteria for a control of the dialog
* **timeout** - maximum length of time to try to find the controls (default 5)
* **retry_interval** - how long to wait between each retry (default .2)
"""
if timeout is None:
timeout = Timings.window_find_timeout
if retry_interval is None:
retry_interval = Timings.window_find_retry
try:
ctrl = wait_until_passes(
timeout,
retry_interval,
self.__get_ctrl, # 传入了一个函数接口
(findwindows.ElementNotFoundError,
findbestmatch.MatchError,
controls.InvalidWindowHandle,
controls.InvalidElement),
criteria)
except TimeoutError as e:
raise e.original_exception
return ctrl
def wrapper_object(self):
"""Allow the calling code to get the HwndWrapper object"""
ctrls = self.__resolve_control(self.criteria)
return ctrls[-1]
#=========================================================================
def wait_until_passes(timeout,
retry_interval,
func,
exceptions=(Exception),
*args, **kwargs):
"""
Wait until ``func(*args, **kwargs)`` does not raise one of the exceptions
* **timeout** how long the function will try the function
* **retry_interval** how long to wait between retries
* **func** the function that will be executed
* **exceptions** list of exceptions to test against (default: Exception)
* **args** optional arguments to be passed to func when called
* **kwargs** optional keyword arguments to be passed to func when called
Returns the return value of the function
If the operation times out then the original exception raised is in
the 'original_exception' attribute of the raised exception.
e.g. ::
try:
# wait a maximum of 10.5 seconds for the
# window to be found in increments of .5 of a second.
# P.int a message and re-raise the original exception if never found.
wait_until_passes(10.5, .5, self.Exists, (ElementNotFoundError))
except TimeoutError as e:
print("timed out")
raise e.
"""
start = timestamp()
# keep trying until the timeout is passed
while True:
try:
# Call the function with any arguments
func_val = func(*args, **kwargs)
# if no exception is raised then we are finished
break
# An exception was raised - so wait and try again
except exceptions as e:
# find out how much of the time is left
time_left = timeout - (timestamp() - start)
# if we have to wait some more
if time_left > 0:
# wait either the retry_interval or else the amount of
# time until the timeout expires (whichever is less)
time.sleep(min(retry_interval, time_left))
else:
# Raise a TimeoutError - and put the original exception
# inside it
err = TimeoutError()
err.original_exception = e
raise err
# return the function value
return func_val
#=========================================================================
def find_element(**kwargs):
"""
Call find_elements and ensure that only one element is returned
Calls find_elements with exactly the same arguments as it is called with
so please see :py:func:`find_elements` for the full parameters description.
"""
elements = find_elements(**kwargs)
if not elements:
raise ElementNotFoundError(kwargs)
if len(elements) > 1:
exception = ElementAmbiguousError(
"There are {0} elements that match the criteria {1}".format(
len(elements),
six.text_type(kwargs),
)
)
exception.elements = elements
raise exception
return elements[0]
以下是我的理解:
1. 通过child_window()无条件的返回了一个pywinauto.application.WindowSpecification对象,这个对象只是封装了对应的查找条件,拿着这个返回对象还不能知道是否成功查找到窗口;
2. WindowSpecification对象获取其wapper_object()时才开始真正查找。wrapper_object() -> _resolve_control() -> wait_until_passes() -> _get_ctrl() -> findwindows.find_element()
3. wait_until_passes()中使用了总超时时间和重试间隔,当总时间超时时会抛出Timeout异常。如果__get_ctrl()返回了正常值就立即返回;
4.wait_until_passes会捕获_resolve_control()中指定的(findwindows.ElementNotFoundError,
findbestmatch.MatchError,
controls.InvalidWindowHandle,
controls.InvalidElement)几种类型的异常,当出现这些异常时会继续进行重试。