pyseext 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyseext-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Martyn West
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ include LICENSE
pyseext-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyseext
3
+ Version: 0.1.0
4
+ Summary: A short description
5
+ Home-page: https://github.com/westy/pyseext
6
+ Author: Martyn West
7
+ Author-email: 657393+westy@users.noreply.github.com
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: selenium
12
+ Dynamic: author
13
+ Dynamic: author-email
14
+ Dynamic: home-page
15
+ Dynamic: license-file
16
+
17
+ # PySeExt Module Repository
18
+
19
+ This project contains a package to aid the testing of ExtJS applications from Python using Selenium.
20
+
21
+ ---
22
+ ## Naming Standards
23
+
24
+ Full details [here](https://namingconvention.org/python/).
25
+
26
+ ### TL;DR
27
+ **Type** | **Public** | **Internal**
28
+ --- | --- | ---
29
+ Packages | `lower_with_under` |
30
+ Modules | `lower_with_under` | `_lower_with_under`
31
+ Classes | `CapWords` | `_CapWords`
32
+ Exceptions | `CapWords` |
33
+ Functions | `lower_with_under()` | `_lower_with_under()`
34
+ Global/Class Constants | `CAPS_WITH_UNDER` | `_CAPS_WITH_UNDER`
35
+ Global/Class Variables | `lower_with_under` | `_lower_with_under`
36
+ Instance Variables | `lower_with_under` | `_lower_with_under`
37
+ Method Names | `lower_with_under()` | `_lower_with_under()`
38
+ Function/Method Parameters | `lower_with_under` |
39
+ Local Variables | `lower_with_under` |
40
+
41
+ ### Additional Notes
42
+ I have also settled on a standard of having a single class per source file (that I now understand are called modules), although inner classes are allowed.
43
+ This is pretty much standard practice in other languages, and makes source control and managing conflicts far easier.
@@ -0,0 +1,27 @@
1
+ # PySeExt Module Repository
2
+
3
+ This project contains a package to aid the testing of ExtJS applications from Python using Selenium.
4
+
5
+ ---
6
+ ## Naming Standards
7
+
8
+ Full details [here](https://namingconvention.org/python/).
9
+
10
+ ### TL;DR
11
+ **Type** | **Public** | **Internal**
12
+ --- | --- | ---
13
+ Packages | `lower_with_under` |
14
+ Modules | `lower_with_under` | `_lower_with_under`
15
+ Classes | `CapWords` | `_CapWords`
16
+ Exceptions | `CapWords` |
17
+ Functions | `lower_with_under()` | `_lower_with_under()`
18
+ Global/Class Constants | `CAPS_WITH_UNDER` | `_CAPS_WITH_UNDER`
19
+ Global/Class Variables | `lower_with_under` | `_lower_with_under`
20
+ Instance Variables | `lower_with_under` | `_lower_with_under`
21
+ Method Names | `lower_with_under()` | `_lower_with_under()`
22
+ Function/Method Parameters | `lower_with_under` |
23
+ Local Variables | `lower_with_under` |
24
+
25
+ ### Additional Notes
26
+ I have also settled on a standard of having a single class per source file (that I now understand are called modules), although inner classes are allowed.
27
+ This is pretty much standard practice in other languages, and makes source control and managing conflicts far easier.
@@ -0,0 +1,16 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyseext"
7
+ version = "0.1.0"
8
+ description = "A short description"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ dependencies = [
12
+ "selenium",
13
+ ]
14
+
15
+ [tool.setuptools.packages.find]
16
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
pyseext-0.1.0/setup.py ADDED
@@ -0,0 +1,25 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Learn more: https://github.com/kennethreitz/setup.py
4
+
5
+ from setuptools import setup, find_packages
6
+
7
+
8
+ with open('README.md', encoding = 'utf-8') as f:
9
+ readme = f.read()
10
+
11
+ with open('LICENSE', encoding = 'utf-8') as f:
12
+ license_text = f.read()
13
+
14
+ setup(
15
+ name='pyseext',
16
+ version='1.4.1',
17
+ description='Python Selenium ExtJS - package for helping interact with an ExtJS application from Python using Selenium',
18
+ long_description=readme,
19
+ author='Martyn West',
20
+ author_email='657393+westy@users.noreply.github.com',
21
+ url='https://github.com/westy/pyseext',
22
+ license=license_text,
23
+ packages=find_packages(exclude=('tests', 'docs')),
24
+ package_data={'pyseext': ['js/*.js']}
25
+ )
@@ -0,0 +1,9 @@
1
+ """
2
+ A package to aid the testing of ExtJS applications from Python using Selenium.
3
+
4
+ See: https://github.com/westy/pyseext
5
+ """
6
+ import logging
7
+
8
+ logger = logging.getLogger(__name__)
9
+ logger.info("Imported package '%s' from %s",__name__, __path__)
@@ -0,0 +1,115 @@
1
+ """
2
+ Module that contains our ButtonHelper class.
3
+ """
4
+
5
+ import logging
6
+ from typing import Union
7
+
8
+ from selenium.webdriver.common.action_chains import ActionChains
9
+ from selenium.webdriver.remote.webdriver import WebDriver
10
+
11
+ from pyseext.component_query import ComponentQuery
12
+
13
+ class ButtonHelper:
14
+ """A class to help with interacting with Ext buttons"""
15
+
16
+ # Class variables
17
+ _ENABLED_BUTTON_TEMPLATE: str = 'button[text="{text}"][disabled=false]'
18
+ """The component query template to use to find an enabled button.
19
+ Requires the inserts: {text}"""
20
+
21
+ _DISABLED_BUTTON_TEMPLATE: str = 'button[text="{text}"][disabled=true]'
22
+ """The component query template to use to find a disabled button.
23
+ Requires the inserts: {text}"""
24
+
25
+ _MESSAGEBOX_BUTTON_TEMPLATE: str = 'messagebox{{isVisible(true)}} button[text="{text}"]'
26
+ """The component query template to use to find a button on a visible message box.
27
+ Requires the inserts: {text}"""
28
+
29
+ def __init__(self, driver: WebDriver):
30
+ """Initialises an instance of this class
31
+
32
+ Args:
33
+ driver (WebDriver): The webdriver to use
34
+ """
35
+ # Instance variables
36
+ self._logger = logging.getLogger(__name__)
37
+ """The Logger instance for this class instance"""
38
+
39
+ self._cq = ComponentQuery(driver)
40
+ """The `ComponentQuery` instance for this class instance"""
41
+
42
+ self._action_chains = ActionChains(driver)
43
+ """The ActionChains instance for this class instance"""
44
+
45
+ def click_button(self, cq: str, root_id: Union[str, None] = None):
46
+ """Finds a button using the supplied component query and clicks it.
47
+
48
+ Args:
49
+ cq (str): The component query to find the button.
50
+ root_id (str, optional): The id of the container within which to perform the query.
51
+ If omitted, all components within the document are included in the search.
52
+ """
53
+ button = self._cq.wait_for_single_query_visible(cq, root_id)
54
+
55
+ # Rather than call click, move mouse to button and click...
56
+ self._logger.info("Clicking button with CQ '%s'", cq)
57
+
58
+ self._action_chains.move_to_element(button)
59
+ self._action_chains.click()
60
+ self._action_chains.perform()
61
+
62
+ def click_button_by_text(self, text: str, root_id: Union[str, None] = None):
63
+ """Finds a visible, enabled button with the specified text and clicks it.
64
+
65
+ Args:
66
+ text (str): The text on the button
67
+ root_id (str, optional): The id of the container within which to perform the query.
68
+ If omitted, all components within the document are included in the search.
69
+ """
70
+ self.click_button(self._ENABLED_BUTTON_TEMPLATE.format(text=text), root_id)
71
+
72
+ def check_button_enabled(self, text: str, root_id: Union[str, None] = None):
73
+ """Checks that we can find an enabled button with the specified text.
74
+
75
+ Args:
76
+ text (str): The text on the button
77
+ root_id (str, optional): The id of the container within which to perform the query.
78
+ If omitted, all components within the document are included in the search.
79
+ """
80
+ self._cq.wait_for_single_query(self._ENABLED_BUTTON_TEMPLATE.format(text=text), root_id)
81
+
82
+ def check_button_disabled(self, text: str, root_id: Union[str, None] = None):
83
+ """Checks that we can find a disabled button with the specified text.
84
+
85
+ Args:
86
+ text (str): The text on the button
87
+ root_id (str, optional): The id of the container within which to perform the query.
88
+ If omitted, all components within the document are included in the search.
89
+ """
90
+ self._cq.wait_for_single_query(self._DISABLED_BUTTON_TEMPLATE.format(text=text), root_id)
91
+
92
+ def click_button_on_messagebox(self, text: str = 'OK'):
93
+ """Clicks a button on a messagebox.
94
+
95
+ The messagebox must be visible.
96
+
97
+ Args:
98
+ text (str, optional): The text of the button to click. Defaults to 'OK'.
99
+ """
100
+ self.click_button(self._MESSAGEBOX_BUTTON_TEMPLATE.format(text=text))
101
+
102
+ def click_button_arrow(self, button_text: str, root_id: Union[str, None] = None, css_selector: str = ".x-btn-arrow-el"):
103
+ """Clicks the dropdown arrow of a split button to open its menu.
104
+
105
+ Args:
106
+ button_text (str): The text on the button
107
+ root_id (str, optional): The id of the container within which to perform the query.
108
+ css_selector (str, optional): The CSS selector to find the arrow element within the button. Defaults to ".x-btn-arrow-el".
109
+ """
110
+ arrow = self._cq.wait_for_single_query_visible(cq = button_text, root_id = root_id, css_selector = css_selector)
111
+
112
+ # Move to the button and click on the arrow area
113
+ self._action_chains.move_to_element(arrow)
114
+ self._action_chains.click()
115
+ self._action_chains.perform()
@@ -0,0 +1,259 @@
1
+ """
2
+ Module that contains our ComponentQuery class.
3
+ """
4
+ import logging
5
+ from typing import Union
6
+
7
+ from selenium.common.exceptions import TimeoutException
8
+ from selenium.webdriver.remote.webdriver import WebDriver
9
+ from selenium.webdriver.remote.webelement import WebElement
10
+ from selenium.webdriver.support.wait import WebDriverWait
11
+
12
+ from pyseext.has_referenced_javascript import HasReferencedJavaScript
13
+
14
+
15
+ class ComponentQuery(HasReferencedJavaScript):
16
+ """A class to help with using Ext.ComponentQuery"""
17
+
18
+ # Class variables
19
+ _QUERY_TEMPLATE: str = "return globalThis.PySeExt.ComponentQuery.query('{cq}')"
20
+ """The script template to use to execute a component query.
21
+ Requires the inserts: {cq}"""
22
+
23
+ _QUERY_TEMPLATE_WITH_ROOT: str = "return globalThis.PySeExt.ComponentQuery.query('{cq}', '{root_id}')"
24
+ """The script template to use to execute a component query under a specified root.
25
+ Requires the inserts: {cq}, {root_id}"""
26
+
27
+ _QUERY_TEMPLATE_WITH_CSS_SELECTOR: str = "return globalThis.PySeExt.ComponentQuery.query('{cq}', undefined, '{css_selector}')"
28
+ """The script template to use to execute a component query, and then execute a CSS selector query against each matched element.
29
+ Requires the inserts: {cq}, {css_selector}"""
30
+
31
+ _QUERY_TEMPLATE_WITH_ROOT_AND_CSS_SELECTOR: str = "return globalThis.PySeExt.ComponentQuery.query('{cq}', '{root_id}', '{css_selector}')"
32
+ """The script template to use to execute a component query under a specified root, and then execute a CSS selector query against each matched element.
33
+ Requires the inserts: {cq}, {root_id}, {css_selector}"""
34
+
35
+ _IS_COMPONENT_INSTANCE_OF_CLASS_TEMPLATE: str = "return globalThis.PySeExt.ComponentQuery.isComponentInstanceOf('{class_name}', '{cq}')"
36
+ """The script template to use to determine whether a component query matches a component of the specified class.
37
+ Requires the inserts: {class_name}, {cq}"""
38
+
39
+ _IS_COMPONENT_INSTANCE_OF_CLASS_TEMPLATE_WITH_ROOT: str = "return globalThis.PySeExt.ComponentQuery.isComponentInstanceOf('{class_name}', '{cq}', '{root_id}')"
40
+ """The script template to use to determine whether a component query matches a component of the specified class.
41
+ Requires the inserts: {class_name}, {cq}, {root_id}"""
42
+
43
+ def __init__(self, driver: WebDriver):
44
+ """Initialises an instance of this class
45
+
46
+ Args:
47
+ driver (WebDriver): The webdriver to use
48
+ """
49
+ # Instance variables
50
+ self._logger = logging.getLogger(__name__)
51
+ """The logger instance for this class instance"""
52
+
53
+ self._driver = driver
54
+ """The WebDriver instance for this class instance"""
55
+
56
+ # Initialise our base class
57
+ super().__init__(driver, self._logger)
58
+
59
+ def query(self, cq: str, root_id: Union[str, None] = None, css_selector: Union[str, None] = None) -> list[WebElement]:
60
+ """Executes a ComponentQuery and returns the result
61
+
62
+ Args:
63
+ cq (str): The query to execute
64
+ root_id (str, optional): The id of the container within which to perform the query.
65
+ If omitted, all components within the document are included in the search.
66
+ css_selector (str, optional): An optional CSS selector that can be used to get child elements of a found component,
67
+ e.g. a clear trigger on a field would be '.x-form-clear-trigger'.
68
+ Returns:
69
+ list[WebElement]: An array of DOM elements that match the query or an empty array if not found
70
+ """
71
+ if root_id is None and css_selector is None:
72
+ self._logger.debug("Executing CQ '%s'", cq)
73
+ script = self._QUERY_TEMPLATE.format(cq=cq)
74
+ elif css_selector is None:
75
+ self._logger.debug("Executing CQ '%s' under root '%s'", cq, root_id)
76
+ script = self._QUERY_TEMPLATE_WITH_ROOT.format(cq=cq, root_id=root_id)
77
+ elif root_id is None:
78
+ self._logger.debug("Executing CQ '%s' with CSS selector '%s'", cq, css_selector)
79
+ script = self._QUERY_TEMPLATE_WITH_CSS_SELECTOR.format(cq=cq, css_selector=css_selector)
80
+ else:
81
+ self._logger.debug("Executing CQ '%s' under root '%s' with CSS selector '%s'", cq, root_id, css_selector)
82
+ script = self._QUERY_TEMPLATE_WITH_ROOT_AND_CSS_SELECTOR.format(cq=cq, root_id=root_id, css_selector=css_selector)
83
+
84
+ self.ensure_javascript_loaded()
85
+ query_result = self._driver.execute_script(script)
86
+
87
+ self._logger.debug("CQ '%s' gave results: %s", cq, query_result)
88
+
89
+ return query_result
90
+
91
+ def wait_for_query(self, cq: str, root_id: Union[str, None] = None, timeout: float = 10, throw_if_not_found: bool = True, css_selector: Union[str, None] = None) -> list[WebElement]:
92
+ """Method that waits for the specified CQ to match something
93
+
94
+ Args:
95
+ cq (str): The query to execute
96
+ root_id (str, optional): The id of the container within which to perform the query.
97
+ If omitted, all components within the document are included in the search.
98
+ timeout (float): Number of seconds before timing out (default 10)
99
+ throw_if_not_found (bool): Indicates whether to throw an exception if not found (default True).
100
+ css_selector (str, optional): An optional CSS selector that can be used to get child elements of a found component,
101
+ e.g. a clear trigger on a field would be '.x-form-clear-trigger'.
102
+
103
+ Returns:
104
+ list[WebElement]: An array of DOM elements that match the query or an empty array if not found (and not configured to throw)
105
+ """
106
+ try:
107
+ WebDriverWait(self._driver, timeout).until(ComponentQuery.ComponentQueryFoundExpectation(cq))
108
+ return self.query(cq, root_id, css_selector)
109
+ except TimeoutException as exc:
110
+ if throw_if_not_found:
111
+ raise ComponentQuery.QueryNotFoundException(cq, timeout, root_id) from exc
112
+
113
+ return []
114
+
115
+ def wait_for_single_query(self, cq: str, root_id: Union[str, None] = None, timeout: float = 10, css_selector: Union[str, None] = None) -> WebElement:
116
+ """Method that waits for the specified CQ to match a single result.
117
+ If there are multiple matches then an error is thrown.
118
+
119
+ Args:
120
+ cq (str): The query to execute
121
+ root_id (str, optional): The id of the container within which to perform the query.
122
+ If omitted, all components within the document are included in the search.
123
+ timeout (float): Number of seconds before timing out (default 10)
124
+ css_selector (str, optional): An optional CSS selector that can be used to get child elements of a found component,
125
+ e.g. a clear trigger on a field would be '.x-form-clear-trigger'.
126
+
127
+ Returns:
128
+ WebElement: The DOM element that matches the query
129
+ """
130
+ results = self.wait_for_query(cq, root_id, timeout, True, css_selector)
131
+ if len(results) > 1:
132
+ raise ComponentQuery.QueryMatchedMultipleElementsException(cq, len(results))
133
+
134
+ return results[0]
135
+
136
+ def wait_for_single_query_visible(self, cq: str, root_id: Union[str, None] = None, timeout: float = 10, css_selector: Union[str, None] = None) -> WebElement:
137
+ """Method that waits for the specified CQ to match a single visible result.
138
+ If there are multiple matches then an error is thrown.
139
+
140
+ Args:
141
+ cq (str): The query to execute
142
+ root_id (str, optional): The id of the container within which to perform the query.
143
+ If omitted, all components within the document are included in the search.
144
+ timeout (float): Number of seconds before timing out (default 10)
145
+ css_selector (str, optional): An optional CSS selector that can be used to get child elements of a found component,
146
+ e.g. a clear trigger on a field would be '.x-form-clear-trigger'.
147
+
148
+ Returns:
149
+ WebElement: The DOM element that matches the query
150
+ """
151
+ if not cq.endswith('{isVisible(true)}'):
152
+ cq = cq + '{isVisible(true)}'
153
+
154
+ return self.wait_for_single_query(cq, root_id, timeout, css_selector)
155
+
156
+ def is_component_instance_of_class(self, class_name: str, cq: str, root_id: Union[str, None] = None, timeout: float = 1) -> bool:
157
+ """Determines whether the component for the specified CQ is an instance of the specified class name.
158
+
159
+ Note, will return True if the component is a subclass of the type too.
160
+
161
+ If the component is not found then an error is thrown.
162
+
163
+ Args:
164
+ class_name (str): The class name to test for, e.g. 'Ext.container.Container'.
165
+ cq (str): The query to find the component.
166
+ root_id (str, optional): The id of the container within which to perform the query.
167
+ If omitted, all components within the document are included in the search.
168
+ timeout (float): Number of seconds before timing out (default 1)
169
+
170
+ Returns:
171
+ bool: True if the component is an instance of the specified class (including a subclass). False otherwise.
172
+ """
173
+ if root_id is None:
174
+ script = self._IS_COMPONENT_INSTANCE_OF_CLASS_TEMPLATE.format(class_name=class_name, cq=cq)
175
+ else:
176
+ script = self._IS_COMPONENT_INSTANCE_OF_CLASS_TEMPLATE_WITH_ROOT.format(class_name=class_name, cq=cq, root_id=root_id)
177
+
178
+ self.ensure_javascript_loaded()
179
+ result = self._driver.execute_script(script)
180
+
181
+ if result is None:
182
+ raise ComponentQuery.QueryNotFoundException(cq, timeout, root_id)
183
+
184
+ return result
185
+
186
+ class ComponentQueryFoundExpectation:
187
+ """ An expectation for checking that an Ext.ComponentQuery is found"""
188
+
189
+ def __init__(self, cq: str):
190
+ """Initialises an instance of this class.
191
+ """
192
+ self._cq = cq
193
+
194
+ def __call__(self, driver):
195
+ """Method that determines whether a CQ is found
196
+ """
197
+ results = ComponentQuery(driver).query(self._cq)
198
+ return results is not None and len(results) > 0
199
+
200
+ class QueryMatchedMultipleElementsException(Exception):
201
+ """Exception class thrown when expecting a single component query match and get multiple"""
202
+
203
+ def __init__(self, cq: str, count: int, message: str = "Expected a single match from ComponentQuery '{cq}' but got {count}."):
204
+ """Initialises an instance of this exception
205
+
206
+ Args:
207
+ cq (str): The component query that has been executed
208
+ timeout (float): Number of seconds before timing out
209
+ count (int): The number of results that we got
210
+ message (str, optional): The message for the exception. Must contain a 'cq' and 'count' format inserts.
211
+ Defaults to "Expected a single match from ComponentQuery '{cq}' but got '{count}'".
212
+ """
213
+ self.message = message
214
+ self._cq = cq
215
+ self._count = count
216
+
217
+ super().__init__(self.message)
218
+
219
+ def __str__(self):
220
+ """Returns a string representation of this exception"""
221
+ return self.message.format(cq=self._cq, count=self._count)
222
+
223
+ class QueryNotFoundException(Exception):
224
+ """Exception class thrown when a component query could not be found"""
225
+
226
+ def __init__(self,
227
+ cq: str,
228
+ timeout: float,
229
+ root_id: Union[str, None] = None,
230
+ message_without_root: str = "Waiting for component query '{cq}' timed out after {timeout} seconds",
231
+ message_with_root: str = "Waiting for component query '{cq}' under root '{root_id}' timed out after {timeout} seconds"):
232
+ """Initialises an instance of this exception
233
+
234
+ Args:
235
+ cq (str): The component query that has been executed
236
+ timeout (float): Number of seconds waited
237
+ root_id (str, optional): The id of the container within which the query was performed.
238
+ message_without_root (str, optional): The message for the exception when there is no root. Must contain a 'cq' and 'timeout' format inserts.
239
+ Defaults to "Waiting for component query '{cq}' timed out after {timeout} seconds".
240
+ message_with_root (str, optional): The message for the exception when there is a root. Must contain a 'cq', 'root_id' and 'timeout' format inserts.
241
+ Defaults to "Waiting for component query '{cq}' under root '{root_id}' timed out after {timeout} seconds".
242
+ """
243
+ self._cq = cq
244
+ self._timeout = timeout
245
+ self._root_id = root_id
246
+
247
+ if root_id is None:
248
+ self.message = message_without_root
249
+ else:
250
+ self.message = message_with_root
251
+
252
+ super().__init__(self.message)
253
+
254
+ def __str__(self):
255
+ """Returns a string representation of this exception"""
256
+ if self._root_id is None:
257
+ return self.message.format(cq=self._cq, timeout=self._timeout)
258
+ else:
259
+ return self.message.format(cq=self._cq, timeout=self._timeout, root_id=self._root_id)