From 649a56d5f5286c18a0ec71717a814e1f6fd441fe Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:54:27 +0000 Subject: [PATCH 1/2] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.15.22 → v0.16.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.22...v0.16.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5555113..5a4da40 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.22 + rev: v0.16.0 hooks: - id: ruff args: From 92ce05c924cd922d9a2a210480e44d9fd347da11 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:59:21 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../basic-widgets/text_widget_basic.py | 2 +- docs/examples/browser_setup.py | 2 + docs/examples/conftest.py | 5 +- .../default_fill_strategy_examples.py | 6 +- .../strategy_inheritance_examples.py | 2 +- .../wait_fill_strategy_examples.py | 2 +- docs/examples/getting-started/first_script.py | 7 +- .../getting-started/test_installation.py | 4 +- docs/examples/iframe-handling/basic_iframe.py | 2 +- .../iframe-handling/context_isolation.py | 2 +- .../examples/iframe-handling/nested_iframe.py | 6 +- .../version-picking/version_picking.py | 6 +- docs/examples/views/basic_view.py | 2 +- docs/examples/views/batch_operations.py | 2 +- .../views/conditional_switchable_view.py | 2 +- .../views/nested_parametrized_view.py | 4 +- .../views/nested_view_attribute_assignment.py | 2 +- .../views/nested_view_inner_classes.py | 8 +- docs/examples/views/parametrized_view.py | 2 +- .../views/parametrized_view_iteration.py | 4 +- docs/examples/views/root_locator_scoping.py | 2 +- .../views/simple_conditional_widget.py | 2 +- docs/examples/views/view_lifecycle_hooks.py | 2 +- docs/examples/views/view_state_checking.py | 2 +- .../basic_windows_management.py | 2 + .../window-management/handling_popups.py | 4 +- src/widgetastic/browser.py | 162 ++++++++---------- src/widgetastic/exceptions.py | 28 +-- src/widgetastic/locator.py | 22 +-- src/widgetastic/log.py | 20 +-- src/widgetastic/ouia/__init__.py | 14 +- src/widgetastic/types.py | 22 +-- src/widgetastic/utils.py | 21 +-- src/widgetastic/widget/__init__.py | 15 +- src/widgetastic/widget/base.py | 79 ++++----- src/widgetastic/widget/checkbox.py | 3 +- src/widgetastic/widget/input.py | 3 +- src/widgetastic/widget/select.py | 8 +- src/widgetastic/widget/table.py | 89 +++------- src/widgetastic/xpath.py | 3 +- testing/conftest.py | 9 +- testing/ouia_widgets.py | 3 +- testing/test_basic_widgets.py | 29 ++-- testing/test_browser.py | 24 ++- testing/test_fillable.py | 11 +- testing/test_iframe.py | 7 +- testing/test_locator.py | 7 +- testing/test_log.py | 6 +- testing/test_ouia.py | 3 +- testing/test_table.py | 10 +- testing/test_utils.py | 28 +-- testing/test_version_pick.py | 11 +- testing/test_view.py | 39 ++--- testing/test_widget.py | 4 +- testing/test_window_manager.py | 11 +- testing/test_xpath.py | 3 +- 56 files changed, 330 insertions(+), 450 deletions(-) diff --git a/docs/examples/basic-widgets/text_widget_basic.py b/docs/examples/basic-widgets/text_widget_basic.py index 8a04332..06690a3 100644 --- a/docs/examples/basic-widgets/text_widget_basic.py +++ b/docs/examples/basic-widgets/text_widget_basic.py @@ -3,8 +3,8 @@ This example demonstrates how to use the Text widget to extract text content. """ -from widgetastic.widget import Text from widgetastic.exceptions import NoSuchElementException +from widgetastic.widget import Text # In-line Initialization of Text widget main_title = Text(parent=browser, locator=".//h1[@id='wt-core-title']") # noqa: F821 diff --git a/docs/examples/browser_setup.py b/docs/examples/browser_setup.py index 8b52040..554020d 100644 --- a/docs/examples/browser_setup.py +++ b/docs/examples/browser_setup.py @@ -1,7 +1,9 @@ import inspect import os from pathlib import Path + from playwright.sync_api import sync_playwright + from widgetastic.browser import Browser diff --git a/docs/examples/conftest.py b/docs/examples/conftest.py index 752e3ef..044b6c9 100644 --- a/docs/examples/conftest.py +++ b/docs/examples/conftest.py @@ -1,9 +1,10 @@ """Pytest configuration for documentation examples.""" import os -import pytest -from pathlib import Path import sys +from pathlib import Path + +import pytest EXCLUDED_NAMES = {"__init__.py", "conftest.py"} diff --git a/docs/examples/fill-strategies/default_fill_strategy_examples.py b/docs/examples/fill-strategies/default_fill_strategy_examples.py index 02ce7d3..9a2cce8 100644 --- a/docs/examples/fill-strategies/default_fill_strategy_examples.py +++ b/docs/examples/fill-strategies/default_fill_strategy_examples.py @@ -5,7 +5,7 @@ """ from widgetastic.utils import DefaultFillViewStrategy -from widgetastic.widget import View, TextInput, Checkbox, Widget +from widgetastic.widget import Checkbox, TextInput, View, Widget class BasicForm(View): @@ -39,7 +39,7 @@ class BasicForm(View): # End Example: Filtering None Values # Example: Handling Extra Keys -import logging # noqa: E402 +import logging logging.basicConfig(level=logging.WARNING) @@ -59,8 +59,6 @@ class BasicForm(View): class NoFillWidget(Widget): """Widget without fill method.""" - pass - class TestForm(View): input1 = TextInput(name="input1") diff --git a/docs/examples/fill-strategies/strategy_inheritance_examples.py b/docs/examples/fill-strategies/strategy_inheritance_examples.py index 1f92320..132db87 100644 --- a/docs/examples/fill-strategies/strategy_inheritance_examples.py +++ b/docs/examples/fill-strategies/strategy_inheritance_examples.py @@ -5,7 +5,7 @@ # Example: Without Inheritance from widgetastic.utils import WaitFillViewStrategy -from widgetastic.widget import View, TextInput +from widgetastic.widget import TextInput, View # Example: Without respect_parent (default behavior) diff --git a/docs/examples/fill-strategies/wait_fill_strategy_examples.py b/docs/examples/fill-strategies/wait_fill_strategy_examples.py index 78ca597..bcc76b0 100644 --- a/docs/examples/fill-strategies/wait_fill_strategy_examples.py +++ b/docs/examples/fill-strategies/wait_fill_strategy_examples.py @@ -5,7 +5,7 @@ """ from widgetastic.utils import WaitFillViewStrategy -from widgetastic.widget import View, TextInput, Checkbox +from widgetastic.widget import Checkbox, TextInput, View class DynamicForm(View): diff --git a/docs/examples/getting-started/first_script.py b/docs/examples/getting-started/first_script.py index 34095d6..58a784d 100644 --- a/docs/examples/getting-started/first_script.py +++ b/docs/examples/getting-started/first_script.py @@ -9,8 +9,9 @@ from pathlib import Path from playwright.sync_api import sync_playwright + from widgetastic.browser import Browser -from widgetastic.widget import View, Text, TextInput, Checkbox +from widgetastic.widget import Checkbox, Text, TextInput, View # Define your widgets and views i.e. Modeling of the testing page. @@ -21,13 +22,13 @@ class DemoFormView(View): email = TextInput(locator='.//input[@name="custemail"]') @View.nested - class pizza_size(View): # noqa + class pizza_size(View): small = Checkbox(locator=".//input[@value='small']") medium = Checkbox(locator=".//input[@value='medium']") large = Checkbox(locator=".//input[@value='large']") @View.nested - class pizza_toppings(View): # noqa + class pizza_toppings(View): bacon = Checkbox(locator=".//input[@value='bacon']") extra_cheese = Checkbox(locator=".//input[@value='cheese']") onion = Checkbox(locator=".//input[@value='onion']") diff --git a/docs/examples/getting-started/test_installation.py b/docs/examples/getting-started/test_installation.py index 5197e74..c4ded62 100644 --- a/docs/examples/getting-started/test_installation.py +++ b/docs/examples/getting-started/test_installation.py @@ -1,9 +1,11 @@ # test_installation.py import os + from playwright.sync_api import sync_playwright + from widgetastic.browser import Browser -from widgetastic.widget import View, Text +from widgetastic.widget import Text, View class TestView(View): diff --git a/docs/examples/iframe-handling/basic_iframe.py b/docs/examples/iframe-handling/basic_iframe.py index 847a154..316bf81 100644 --- a/docs/examples/iframe-handling/basic_iframe.py +++ b/docs/examples/iframe-handling/basic_iframe.py @@ -3,7 +3,7 @@ This example demonstrates accessing elements inside an iframe. """ -from widgetastic.widget import View, Text, Select +from widgetastic.widget import Select, Text, View class BasicIFrameView(View): diff --git a/docs/examples/iframe-handling/context_isolation.py b/docs/examples/iframe-handling/context_isolation.py index cde9ff1..95e4a05 100644 --- a/docs/examples/iframe-handling/context_isolation.py +++ b/docs/examples/iframe-handling/context_isolation.py @@ -3,7 +3,7 @@ This example demonstrates that iframe contexts are completely isolated. """ -from widgetastic.widget import View, Text, Select, Checkbox +from widgetastic.widget import Checkbox, Select, Text, View class MainPageView(View): diff --git a/docs/examples/iframe-handling/nested_iframe.py b/docs/examples/iframe-handling/nested_iframe.py index 3999a26..226d771 100644 --- a/docs/examples/iframe-handling/nested_iframe.py +++ b/docs/examples/iframe-handling/nested_iframe.py @@ -3,7 +3,7 @@ This example demonstrates handling nested iframes (iframe within iframe). """ -from widgetastic.widget import View, Text, Select, TextInput +from widgetastic.widget import Select, Text, TextInput, View class NestedIFrameView(View): @@ -13,14 +13,14 @@ class NestedIFrameView(View): # Nested iframe class (iframe within iframe) @View.nested - class nested_iframe(View): # noqa + class nested_iframe(View): FRAME = './/iframe[@name="another_iframe"]' nested_title = Text(".//h3") nested_select = Select(id="iframe_select3") # Deeply nested view within the nested iframe @View.nested - class deep_nested(View): # noqa + class deep_nested(View): ROOT = './/div[@id="nested_view"]' nested_input = TextInput(name="input222") diff --git a/docs/examples/version-picking/version_picking.py b/docs/examples/version-picking/version_picking.py index 713371a..1aaf871 100644 --- a/docs/examples/version-picking/version_picking.py +++ b/docs/examples/version-picking/version_picking.py @@ -7,10 +7,12 @@ import inspect import os from pathlib import Path + from playwright.sync_api import sync_playwright + from widgetastic.browser import Browser -from widgetastic.utils import VersionPick, Version -from widgetastic.widget import View, Text, TextInput +from widgetastic.utils import Version, VersionPick +from widgetastic.widget import Text, TextInput, View # Browser setup (from previous example) diff --git a/docs/examples/views/basic_view.py b/docs/examples/views/basic_view.py index 9f24013..24fde47 100644 --- a/docs/examples/views/basic_view.py +++ b/docs/examples/views/basic_view.py @@ -3,7 +3,7 @@ This example demonstrates creating and using a basic View. """ -from widgetastic.widget import View, Text +from widgetastic.widget import Text, View class TestingPageView(View): diff --git a/docs/examples/views/batch_operations.py b/docs/examples/views/batch_operations.py index 4caa165..2ad83c5 100644 --- a/docs/examples/views/batch_operations.py +++ b/docs/examples/views/batch_operations.py @@ -3,7 +3,7 @@ This example demonstrates batch fill and read operations on views. """ -from widgetastic.widget import View, TextInput, Checkbox +from widgetastic.widget import Checkbox, TextInput, View class NormalView(View): diff --git a/docs/examples/views/conditional_switchable_view.py b/docs/examples/views/conditional_switchable_view.py index 6556862..0d249a7 100644 --- a/docs/examples/views/conditional_switchable_view.py +++ b/docs/examples/views/conditional_switchable_view.py @@ -3,7 +3,7 @@ This example demonstrates using ConditionalSwitchableView to handle dynamic UI sections. """ -from widgetastic.widget import ConditionalSwitchableView, View, TextInput, Select, Checkbox +from widgetastic.widget import Checkbox, ConditionalSwitchableView, Select, TextInput, View class ConditionalSwitchableViewTesting(View): diff --git a/docs/examples/views/nested_parametrized_view.py b/docs/examples/views/nested_parametrized_view.py index ae6907b..9019e68 100644 --- a/docs/examples/views/nested_parametrized_view.py +++ b/docs/examples/views/nested_parametrized_view.py @@ -4,7 +4,7 @@ """ from widgetastic.utils import ParametrizedLocator, ParametrizedString -from widgetastic.widget import ParametrizedView, TextInput, Checkbox, View, Text +from widgetastic.widget import Checkbox, ParametrizedView, Text, TextInput, View class ParametrizedViewTesting(View): @@ -13,7 +13,7 @@ class ParametrizedViewTesting(View): ROOT = ".//div[contains(@class, 'parametrized-view')]" title = Text(locator=".//div[@class='widget-title']") - class thing_container_view(ParametrizedView): # noqa + class thing_container_view(ParametrizedView): # Defining one parameter PARAMETERS = ("thing_id",) # ParametrizedLocator coerces to a string upon access diff --git a/docs/examples/views/nested_view_attribute_assignment.py b/docs/examples/views/nested_view_attribute_assignment.py index 9d3e8bb..abe8bc5 100644 --- a/docs/examples/views/nested_view_attribute_assignment.py +++ b/docs/examples/views/nested_view_attribute_assignment.py @@ -3,7 +3,7 @@ This example demonstrates creating nested views using View.nested(). """ -from widgetastic.widget import View, Text, TextInput, Checkbox +from widgetastic.widget import Checkbox, Text, TextInput, View class NormalViewTesting(View): diff --git a/docs/examples/views/nested_view_inner_classes.py b/docs/examples/views/nested_view_inner_classes.py index 90e3337..db61789 100644 --- a/docs/examples/views/nested_view_inner_classes.py +++ b/docs/examples/views/nested_view_inner_classes.py @@ -3,12 +3,12 @@ This example demonstrates creating nested views using @View.nested decorator. """ -from widgetastic.widget import View, Text, TextInput, Checkbox +from widgetastic.widget import Checkbox, Text, TextInput, View class ViewTesting(View): @View.nested - class normal_view(View): # noqa + class normal_view(View): """Normal View under View testing.""" ROOT = ".//div[contains(@class, 'normal-view')]" @@ -19,7 +19,7 @@ class normal_view(View): # noqa submit = Text(locator=".//button[@id='normal_submit']") @View.nested - class parametrized_view(View): # noqa + class parametrized_view(View): """Parametrized View under View testing.""" ROOT = ".//div[contains(@class, 'parametrized-view')]" @@ -27,7 +27,7 @@ class parametrized_view(View): # noqa # Some other widgets @View.nested - class conditional_switchable_view(View): # noqa + class conditional_switchable_view(View): """Conditional Switchable View under View testing.""" ROOT = ".//div[contains(@class, 'conditional-switchable-view')]" diff --git a/docs/examples/views/parametrized_view.py b/docs/examples/views/parametrized_view.py index 68f67d3..2773d01 100644 --- a/docs/examples/views/parametrized_view.py +++ b/docs/examples/views/parametrized_view.py @@ -4,7 +4,7 @@ """ from widgetastic.utils import ParametrizedLocator, ParametrizedString -from widgetastic.widget import ParametrizedView, TextInput, Checkbox +from widgetastic.widget import Checkbox, ParametrizedView, TextInput class ThingContainerView(ParametrizedView): diff --git a/docs/examples/views/parametrized_view_iteration.py b/docs/examples/views/parametrized_view_iteration.py index ae55b2f..8e225cb 100644 --- a/docs/examples/views/parametrized_view_iteration.py +++ b/docs/examples/views/parametrized_view_iteration.py @@ -4,7 +4,7 @@ """ from widgetastic.utils import ParametrizedLocator, ParametrizedString -from widgetastic.widget import ParametrizedView, TextInput, Checkbox, View, Text +from widgetastic.widget import Checkbox, ParametrizedView, Text, TextInput, View class ParametrizedViewTesting(View): @@ -13,7 +13,7 @@ class ParametrizedViewTesting(View): ROOT = ".//div[contains(@class, 'parametrized-view')]" title = Text(locator=".//div[@class='widget-title']") - class thing_container_view(ParametrizedView): # noqa + class thing_container_view(ParametrizedView): # Defining one parameter PARAMETERS = ("thing_id",) # ParametrizedLocator coerces to a string upon access diff --git a/docs/examples/views/root_locator_scoping.py b/docs/examples/views/root_locator_scoping.py index 82fd659..38eb3a7 100644 --- a/docs/examples/views/root_locator_scoping.py +++ b/docs/examples/views/root_locator_scoping.py @@ -3,7 +3,7 @@ This example demonstrates how ROOT locators scope widget searches. """ -from widgetastic.widget import View, Text, TextInput +from widgetastic.widget import Text, TextInput, View class NormalViewTesting(View): diff --git a/docs/examples/views/simple_conditional_widget.py b/docs/examples/views/simple_conditional_widget.py index a1713c8..eb98161 100644 --- a/docs/examples/views/simple_conditional_widget.py +++ b/docs/examples/views/simple_conditional_widget.py @@ -3,7 +3,7 @@ This example demonstrates registering a simple widget directly with ConditionalSwitchableView. """ -from widgetastic.widget import ConditionalSwitchableView, View, TextInput, Select +from widgetastic.widget import ConditionalSwitchableView, Select, TextInput, View class SimpleConditionalWidgetView(View): diff --git a/docs/examples/views/view_lifecycle_hooks.py b/docs/examples/views/view_lifecycle_hooks.py index e026e7d..f962757 100644 --- a/docs/examples/views/view_lifecycle_hooks.py +++ b/docs/examples/views/view_lifecycle_hooks.py @@ -3,7 +3,7 @@ This example demonstrates using before_fill and after_fill hooks. """ -from widgetastic.widget import View, TextInput, Checkbox +from widgetastic.widget import Checkbox, TextInput, View class FormView(View): diff --git a/docs/examples/views/view_state_checking.py b/docs/examples/views/view_state_checking.py index 42e7bda..3464fc7 100644 --- a/docs/examples/views/view_state_checking.py +++ b/docs/examples/views/view_state_checking.py @@ -3,7 +3,7 @@ This example demonstrates how ROOT locators affect is_displayed behavior. """ -from widgetastic.widget import View, TextInput +from widgetastic.widget import TextInput, View # Example 1: Without ROOT locator diff --git a/docs/examples/window-management/basic_windows_management.py b/docs/examples/window-management/basic_windows_management.py index 00bcd56..e540e5b 100644 --- a/docs/examples/window-management/basic_windows_management.py +++ b/docs/examples/window-management/basic_windows_management.py @@ -6,7 +6,9 @@ import inspect import os from pathlib import Path + from playwright.sync_api import sync_playwright + from widgetastic.browser import Browser, WindowManager diff --git a/docs/examples/window-management/handling_popups.py b/docs/examples/window-management/handling_popups.py index a3c2e20..5a9897d 100644 --- a/docs/examples/window-management/handling_popups.py +++ b/docs/examples/window-management/handling_popups.py @@ -7,9 +7,11 @@ import inspect import os from pathlib import Path + from playwright.sync_api import sync_playwright + from widgetastic.browser import Browser, WindowManager -from widgetastic.widget import View, Text +from widgetastic.widget import Text, View def setup_window_manager(): diff --git a/src/widgetastic/browser.py b/src/widgetastic/browser.py index 1e83e94..e5a7420 100644 --- a/src/widgetastic/browser.py +++ b/src/widgetastic/browser.py @@ -15,46 +15,40 @@ """ import inspect +import warnings from logging import Logger from textwrap import dedent -from typing import Any -from typing import cast -from typing import Dict -from typing import List -from typing import NamedTuple -from typing import Optional -from typing import Set -from typing import Type -from typing import TYPE_CHECKING -from typing import Union -from typing import Literal -import warnings +from typing import ( + TYPE_CHECKING, + Any, + Literal, + NamedTuple, + cast, +) from cached_property import cached_property -from playwright.sync_api import BrowserContext -from playwright.sync_api import ElementHandle, FrameLocator +from playwright.sync_api import ( + BrowserContext, + ElementHandle, + FrameLocator, + Locator, + Page, + TimeoutError, +) from playwright.sync_api import Error as PlaywrightError -from playwright.sync_api import Locator -from playwright.sync_api import Page -from playwright.sync_api import TimeoutError - -from .locator import SmartLocator from wait_for import TimedOutError, wait_for -from .exceptions import LocatorNotImplemented -from .exceptions import NoSuchElementException -from .exceptions import WidgetOperationFailed -from .exceptions import FrameNotFoundError - - -from .log import create_widget_logger -from .log import null_logger -from .types import ElementParent -from .types import LocatorAlias -from .types import LocatorProtocol +from .exceptions import ( + FrameNotFoundError, + LocatorNotImplemented, + NoSuchElementException, + WidgetOperationFailed, +) +from .locator import SmartLocator +from .log import create_widget_logger, null_logger +from .types import ElementParent, LocatorAlias, LocatorProtocol from .xpath import normalize_space - if TYPE_CHECKING: from .widget.base import Widget @@ -82,7 +76,7 @@ def logger(self): """ return create_widget_logger(type(self).__name__, self.browser.logger) - def ensure_page_safe(self, timeout: Optional[Union[int, float]] = None) -> None: + def ensure_page_safe(self, timeout: float | None = None) -> None: """Waits for the page to be quiescent, replacing the old JS-based check. Args: @@ -94,31 +88,26 @@ def ensure_page_safe(self, timeout: Optional[Union[int, float]] = None) -> None: def after_click(self, element: Locator, locator: LocatorAlias) -> None: """Invoked after clicking on an element.""" - pass def after_click_safe_timeout(self, element: Locator, locator: LocatorAlias) -> None: """Invoked after clicking on an element and `ensure_page_safe` failing to wait.""" - pass def before_click(self, element: Locator, locator: LocatorAlias) -> None: """Invoked before clicking on an element.""" - pass - def after_keyboard_input(self, element: Locator, keyboard_input: Optional[str]) -> None: + def after_keyboard_input(self, element: Locator, keyboard_input: str | None) -> None: """Invoked after sending keys into an element. Args: keyboard_input: String if any text typed in, None if the element is cleared. """ - pass - def before_keyboard_input(self, element: Locator, keyboard_input: Optional[str]) -> None: + def before_keyboard_input(self, element: Locator, keyboard_input: str | None) -> None: """Invoked after sending keys into an element. Args: keyboard_input: String if any text typed in, None if the element is cleared. """ - pass def highlight_element( self, @@ -220,12 +209,12 @@ class Browser: def __init__( self, page: Page, - plugin_class: Optional[Type[DefaultPlugin]] = None, - logger: Optional[Logger] = None, - extra_objects: Optional[Dict[Any, Any]] = None, + plugin_class: type[DefaultPlugin] | None = None, + logger: Logger | None = None, + extra_objects: dict[Any, Any] | None = None, ) -> None: self.page = page - self.active_context: Union[Page, FrameLocator] = page + self.active_context: Page | FrameLocator = page plugin_class = plugin_class or DefaultPlugin self.plugin = plugin_class(self) self.logger = logger or null_logger @@ -256,9 +245,7 @@ def product_version(self): """ raise NotImplementedError("You have to implement product_version") - def goto( - self, address: str, *, wait_until: Optional[str] = "domcontentloaded", **kwargs - ) -> None: + def goto(self, address: str, *, wait_until: str | None = "domcontentloaded", **kwargs) -> None: """Navigate to the specified URL with config supported by playwright. Args: @@ -344,11 +331,11 @@ def browser_version(self) -> int: def screenshot( self, - path: Optional[str] = None, + path: str | None = None, *, full_page: bool = False, type: Literal["png", "jpeg"] = "png", - quality: Optional[int] = None, + quality: int | None = None, animations: Literal["disabled", "allow"] = "allow", scale: Literal["css", "device"] = "device", **kwargs, @@ -384,7 +371,7 @@ def screenshot( png_bytes = browser.screenshot() """ self.logger.debug("screenshot(path=%r, full_page=%s, type=%s)", path, full_page, type) - opts: Dict[str, Any] = { + opts: dict[str, Any] = { "full_page": full_page, "type": type, "animations": animations, @@ -432,7 +419,7 @@ def clear_cookies(self) -> None: self.logger.info("Clearing all browser cookies") self.page.context.clear_cookies() - def add_cookie(self, cookie: Dict[str, Any]) -> None: + def add_cookie(self, cookie: dict[str, Any]) -> None: """Add a cookie to the current browser context. Playwright requires either ``url`` or ``domain`` on every cookie. @@ -456,7 +443,7 @@ def add_cookie(self, cookie: Dict[str, Any]) -> None: self.logger.debug("add_cookie: %r", cookie.get("name")) self.page.context.add_cookies([cookie]) - def get_cookies(self, urls: Optional[List[str]] = None) -> List[Dict[str, Any]]: + def get_cookies(self, urls: list[str] | None = None) -> list[dict[str, Any]]: """Return cookies from the current browser context. Args: @@ -479,7 +466,7 @@ def get_cookies(self, urls: Optional[List[str]] = None) -> List[Dict[str, Any]]: # ======================= ELEMENT DISCOVERY & WAITING ======================= @staticmethod - def _process_locator(locator: LocatorAlias) -> Union[Locator, SmartLocator, None]: + def _process_locator(locator: LocatorAlias) -> Locator | SmartLocator | None: """Processes the locator so the :py:meth:`elements` gets exactly what it needs. Args: @@ -507,7 +494,7 @@ def _process_locator(locator: LocatorAlias) -> Union[Locator, SmartLocator, None ) from None @staticmethod - def _locator_force_visibility_check(locator: LocatorAlias) -> Optional[bool]: + def _locator_force_visibility_check(locator: LocatorAlias) -> bool | None: if hasattr(locator, "__locator__") and hasattr(locator, "CHECK_VISIBILITY"): return cast(LocatorProtocol, locator).CHECK_VISIBILITY else: @@ -516,13 +503,13 @@ def _locator_force_visibility_check(locator: LocatorAlias) -> Optional[bool]: def elements( self, locator: LocatorAlias, - parent: Optional[ElementParent] = None, + parent: ElementParent | None = None, check_visibility: bool = False, check_safe: bool = True, force_check_safe: bool = False, *args, **kwargs, - ) -> List[Locator]: + ) -> list[Locator]: """Find all elements matching the given locator. Locates all elements that match the provided locator, with optional parent scoping @@ -572,7 +559,7 @@ def elements( # Handle nonexistent iframe case - ensure consistent behavior across Playwright versions if "Failed to find frame" in str(e): # Raise a specific frame error with clear semantics - raise FrameNotFoundError(f"Failed to find frame: {str(e)}") from e + raise FrameNotFoundError(f"Failed to find frame: {e!s}") from e else: raise @@ -618,21 +605,21 @@ def element(self, locator: LocatorAlias, *args, **kwargs) -> Locator: # the correct visibility filtering logic. elements = self.elements(locator, *args, **kwargs) if not elements: - raise NoSuchElementException(f"Could not find an element {repr(locator)}") + raise NoSuchElementException(f"Could not find an element {locator!r}") return elements[0] except IndexError: - raise NoSuchElementException(f"Could not find an element {repr(locator)}") from None + raise NoSuchElementException(f"Could not find an element {locator!r}") from None def wait_for_element( self, locator: str, - parent: Optional[ElementParent] = None, + parent: ElementParent | None = None, visible: bool = False, - timeout: Union[float, int] = 5, + timeout: float = 5, exception: bool = True, ensure_page_safe: bool = False, - ) -> Optional[Locator]: + ) -> Locator | None: """Waits for an element matching the locator to appear, optionally checking for visibility. Args: @@ -657,7 +644,7 @@ def wait_for_element( if ensure_page_safe: self.plugin.ensure_page_safe() - root_element: Union[Page, Locator] = self.page + root_element: Page | Locator = self.page if parent: root_element = self.element(parent) @@ -851,7 +838,7 @@ def type(self, *args, **kwargs) -> str: """ return self.element(*args, **kwargs).evaluate("el=>el.type") - def classes(self, locator: LocatorAlias, *args, **kwargs) -> Set[str]: + def classes(self, locator: LocatorAlias, *args, **kwargs) -> set[str]: """Return a set of classes attached to the element. Args: @@ -865,7 +852,7 @@ def classes(self, locator: LocatorAlias, *args, **kwargs) -> Set[str]: class_string = self.get_attribute("class", locator, *args, **kwargs) return set(class_string.split()) if class_string else set() - def attributes(self, locator: LocatorAlias, *args, **kwargs) -> Dict: + def attributes(self, locator: LocatorAlias, *args, **kwargs) -> dict: """Return a dict of attributes attached to the element. This implementation uses Playwright's .evaluate() method to execute a @@ -891,7 +878,7 @@ def attributes(self, locator: LocatorAlias, *args, **kwargs) -> Dict: self.logger.debug("css attributes for %r => %r", locator, result) return result - def get_attribute(self, attr: str, *args, **kwargs) -> Optional[str]: + def get_attribute(self, attr: str, *args, **kwargs) -> str | None: """Returns the value of an element's attribute. Uses .input_value() for the 'value' attribute for better reliability. @@ -920,7 +907,7 @@ def set_attribute(self, attr: str, value: str, *args, **kwargs) -> None: def value_of_css_property( self, locator: LocatorAlias, property: str, *args, **kwargs - ) -> Optional[str]: + ) -> str | None: """Retrieves the value of specified computed style property of an element in the current browsing context. Args: @@ -997,12 +984,12 @@ def highlight(self, locator: LocatorAlias, *args, **kwargs) -> None: def click( self, locator: LocatorAlias, - button: Optional[Literal["left", "middle", "right"]] = "left", - click_count: Optional[int] = None, - delay: Optional[float] = None, - force: Optional[bool] = None, - no_wait_after: Optional[bool] = None, - timeout: Optional[float] = None, + button: Literal["left", "middle", "right"] | None = "left", + click_count: int | None = None, + delay: float | None = None, + force: bool | None = None, + no_wait_after: bool | None = None, + timeout: float | None = None, *args, **kwargs, ) -> None: @@ -1282,7 +1269,6 @@ def send_keys(self, text: str, locator: LocatorAlias, sensitive=False, *args, ** self.logger.warning( "Element detached after send_keys, skipping after_keyboard_input hook." ) - pass else: raise else: @@ -1478,8 +1464,8 @@ def drag_and_drop_by_offset(self, source: LocatorAlias, by_x: int, by_y: int) -> def drag_and_drop_to( self, source: LocatorAlias, - to_x: Optional[int] = None, - to_y: Optional[int] = None, + to_x: int | None = None, + to_y: int | None = None, ) -> None: """Drags an element to a target location specified by ``to_x`` and ``to_y``. @@ -1740,7 +1726,7 @@ def __init__(self, o: "Widget", browser: Browser) -> None: self._o = o self._browser = browser - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: if not isinstance(other, BrowserParentWrapper): return False return self._o == other._o and self._browser == other._browser @@ -1748,11 +1734,11 @@ def __eq__(self, other: Any) -> bool: def elements( self, locator: LocatorAlias, - parent: Optional[ElementParent] = None, + parent: ElementParent | None = None, check_visibility: bool = False, check_safe: bool = True, force_check_safe: bool = False, - ) -> List[Locator]: + ) -> list[Locator]: return self._browser.elements( locator, parent=parent or self._o, @@ -1884,7 +1870,7 @@ def __init__( self._context = context self._browser_class = browser_class self._browser_kwargs = browser_kwargs - self._browsers: Dict[Page, Browser] = {} + self._browsers: dict[Page, Browser] = {} self.current: Browser = self._wrap_page(initial_page) self._context.on("page", self._on_new_page) @@ -1951,9 +1937,9 @@ def __init__(self, window_manager: "WindowManager", timeout_ms: int) -> None: self.window_manager = window_manager self.context = window_manager._context self.timeout_ms = timeout_ms - self.page_info: Optional[Any] = None - self._browser: Optional[Browser] = None - self._initial_browsers: Optional[Set[Browser]] = None + self.page_info: Any | None = None + self._browser: Browser | None = None + self._initial_browsers: set[Browser] | None = None def __enter__(self) -> "_BrowserWaiter": # Capture initial browsers before setting up expectation @@ -1962,7 +1948,7 @@ def __enter__(self) -> "_BrowserWaiter": self.page_info.__enter__() return self - def __exit__(self, *args: Any) -> None: + def __exit__(self, *args: object) -> None: # Exit Playwright's context manager to get the Page/Browser if not self.page_info: # If page_info was never set, something went wrong in __enter__ @@ -2027,7 +2013,7 @@ def __getattr__(self, name: str) -> Any: ) return getattr(self._browser, name) - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """Compare by page identity for 'in' checks to work.""" if self._browser is None: return False @@ -2038,7 +2024,7 @@ def __eq__(self, other: Any) -> bool: return _BrowserWaiter(self, int(timeout * 1000)) @property - def all_browsers(self) -> List[Browser]: + def all_browsers(self) -> list[Browser]: """Get all managed Browser instances with automatic cleanup. Returns a list of all currently active widgetastic Browser instances. This property @@ -2140,7 +2126,7 @@ def _check_for_new_pages(): return active_browsers @property - def all_pages(self) -> List[Page]: + def all_pages(self) -> list[Page]: """Get all active Playwright Page objects from the browser context. Returns the raw Playwright Page instances managed by the browser context. @@ -2201,7 +2187,7 @@ def new_browser(self, url: str, focus: bool = True) -> Browser: self.switch_to(new_browser_instance) return new_browser_instance - def switch_to(self, browser_or_page: Union[Browser, Page]): + def switch_to(self, browser_or_page: Browser | Page): """Switch focus to a different browser tab/window. Changes the currently active browser to the specified Browser or Page instance. @@ -2249,7 +2235,7 @@ def switch_to(self, browser_or_page: Union[Browser, Page]): target_page.bring_to_front() self.current = self._browsers[target_page] - def close_browser(self, browser_or_page: Optional[Union[Browser, Page]] = None): + def close_browser(self, browser_or_page: Browser | Page | None = None): """Close a browser tab/window with automatic cleanup and focus management. Closes the specified browser tab/window or the current one if none specified. diff --git a/src/widgetastic/exceptions.py b/src/widgetastic/exceptions.py index c7cfbc6..356d773 100644 --- a/src/widgetastic/exceptions.py +++ b/src/widgetastic/exceptions.py @@ -7,20 +7,14 @@ class WidgetasticException(Exception): """A base exception for the widgetastic framework.""" - pass - class LocatorNotImplemented(NotImplementedError, WidgetasticException): """Raised when a widget does not have a locator defined.""" - pass - class WidgetOperationFailed(WidgetasticException): """Raised when an action on a widget does not result in the expected outcome.""" - pass - class DoNotReadThisWidget(WidgetasticException): """ @@ -28,40 +22,30 @@ class DoNotReadThisWidget(WidgetasticException): that it should not be included in the results of a view.read(). """ - pass - class RowNotFound(IndexError, WidgetasticException): """Raised when a table row cannot be found.""" - pass - class NoSuchElementException(WidgetasticException): """Raised when an element cannot be found.""" - pass - class NoAlertPresentException(WidgetasticException): """Raised when an action is attempted on an alert that is not present.""" - pass - class FrameNotFoundError(WidgetasticException): """Raised when trying to access elements in a nonexistent iframe or frame context.""" - pass - __all__ = [ - "WidgetasticException", - "LocatorNotImplemented", - "WidgetOperationFailed", "DoNotReadThisWidget", - "RowNotFound", - "NoSuchElementException", - "NoAlertPresentException", "FrameNotFoundError", + "LocatorNotImplemented", + "NoAlertPresentException", + "NoSuchElementException", + "RowNotFound", + "WidgetOperationFailed", + "WidgetasticException", ] diff --git a/src/widgetastic/locator.py b/src/widgetastic/locator.py index 5840465..2782244 100644 --- a/src/widgetastic/locator.py +++ b/src/widgetastic/locator.py @@ -64,20 +64,14 @@ def __locator__(self): import re from collections import namedtuple from typing import Any -from typing import Optional -from typing import Tuple -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Type class LocatorStrategy: """Abstract base class for all locator resolution strategies.""" - locator_class: "Type[SmartLocator]" # This will be injected to avoid circular imports + locator_class: "type[SmartLocator]" # This will be injected to avoid circular imports - def create_locator(self, value: Any) -> Optional[Tuple[str, str]]: + def create_locator(self, value: Any) -> tuple[str, str] | None: """ Tries to create a (by, locator) tuple from the given value. Returns the tuple if successful, otherwise None. @@ -90,7 +84,7 @@ class CSSStrategy(LocatorStrategy): CSS_SELECTOR_RE = re.compile(r"^(?:[a-zA-Z][a-zA-Z0-9-]*)?(?:[#.][a-zA-Z0-9_-]+)+$") - def create_locator(self, value: Any) -> Optional[Tuple[str, str]]: + def create_locator(self, value: Any) -> tuple[str, str] | None: if isinstance(value, str) and self.CSS_SELECTOR_RE.match(value): return "css", value return None @@ -99,7 +93,7 @@ def create_locator(self, value: Any) -> Optional[Tuple[str, str]]: class XPathStrategy(LocatorStrategy): """Handles simple XPath expressions.""" - def create_locator(self, value: Any) -> Optional[Tuple[str, str]]: + def create_locator(self, value: Any) -> tuple[str, str] | None: if isinstance(value, str) and value.strip().startswith(("/", "(", ".")): return "xpath", value return None @@ -120,7 +114,7 @@ class KwargsStrategy(LocatorStrategy): "css", } - def create_locator(self, value: Any) -> Optional[Tuple[str, str]]: + def create_locator(self, value: Any) -> tuple[str, str] | None: if isinstance(value, dict): if "by" in value and "locator" in value: engine, locator_value = value["by"], value["locator"] @@ -137,7 +131,7 @@ def create_locator(self, value: Any) -> Optional[Tuple[str, str]]: class LocatorObjectStrategy(LocatorStrategy): """Handles cases where a locatable object is passed in.""" - def create_locator(self, value: Any) -> Optional[Tuple[str, str]]: + def create_locator(self, value: Any) -> tuple[str, str] | None: if isinstance(value, self.locator_class): return value.by, value.locator if hasattr(value, "__locator__"): @@ -224,8 +218,8 @@ class SmartLocator(namedtuple("SmartLocator", ["by", "locator"])): ] def __new__(cls, *args: Any, **kwargs: Any): - by: Optional[str] = None - locator: Optional[str] = None + by: str | None = None + locator: str | None = None if args and len(args) == 2 and not kwargs: # Handle tuple-style: SmartLocator("xpath", "//h1") diff --git a/src/widgetastic/log.py b/src/widgetastic/log.py index 45322a7..eeda837 100644 --- a/src/widgetastic/log.py +++ b/src/widgetastic/log.py @@ -1,19 +1,11 @@ import functools import logging import time -from typing import Any -from typing import Callable -from typing import cast -from typing import Iterator -from typing import MutableMapping -from typing import Optional -from typing import Tuple -from typing import TypeVar -from typing import Union +from collections.abc import Callable, Iterator, MutableMapping +from typing import Any, TypeVar, cast from .exceptions import DoNotReadThisWidget - null_logger = logging.getLogger("widgetastic_null") null_logger.addHandler(logging.NullHandler()) @@ -42,7 +34,7 @@ class PrependParentsAdapter(logging.LoggerAdapter): def process( self, msg: str, kwargs: MutableMapping[str, Any] - ) -> Tuple[str, MutableMapping[str, Any]]: + ) -> tuple[str, MutableMapping[str, Any]]: assert self.extra is not None # python 3.10+ type check widget_path = cast(str, self.extra["widget_path"]) # Sanitizing %->%% for formatter working properly @@ -57,7 +49,7 @@ def __repr__(self) -> str: def create_widget_logger( - widget_path: str, logger: Optional[logging.Logger] = None + widget_path: str, logger: logging.Logger | None = None ) -> PrependParentsAdapter: """Create a logger that prepends the ``widget_path`` to the log records. @@ -96,9 +88,7 @@ def create_child_logger(parent_logger: logging.Logger, child_name: str) -> Prepe return _create_logger_appender(parent_logger, f"/{child_name}") -def create_item_logger( - parent_logger: logging.Logger, item: Union[str, int] -) -> PrependParentsAdapter: +def create_item_logger(parent_logger: logging.Logger, item: str | int) -> PrependParentsAdapter: """Creates a logger for a widget that is inside iteration - referred to by index or key. Args: diff --git a/src/widgetastic/ouia/__init__.py b/src/widgetastic/ouia/__init__.py index 9189bb4..cdab8e9 100644 --- a/src/widgetastic/ouia/__init__.py +++ b/src/widgetastic/ouia/__init__.py @@ -4,9 +4,7 @@ from widgetastic.browser import Browser from widgetastic.types import ViewParent from widgetastic.utils import ParametrizedLocator -from widgetastic.widget.base import ClickableMixin -from widgetastic.widget.base import View -from widgetastic.widget.base import Widget +from widgetastic.widget.base import ClickableMixin, View, Widget from widgetastic.xpath import quote @@ -67,16 +65,16 @@ class OUIAGenericView(OUIABase, View): """ OUIA_COMPONENT_TYPE: str - OUIA_ID: Optional[str] + OUIA_ID: str | None def __init__( self, parent: ViewParent, component_id: str = "", - logger: Optional[Logger] = None, + logger: Logger | None = None, **kwargs, ) -> None: - component_type: Optional[str] = kwargs.pop("component_type", None) + component_type: str | None = kwargs.pop("component_type", None) self._set_attrs( component_type=component_type or self.OUIA_COMPONENT_TYPE or type(self).__name__, component_id=getattr(self, "OUIA_ID", component_id), @@ -105,8 +103,8 @@ def __init__( self, parent: ViewParent, component_id: str = "", - logger: Optional[Logger] = None, - component_type: Optional[str] = None, + logger: Logger | None = None, + component_type: str | None = None, ) -> None: self._set_attrs( component_type=component_type or self.OUIA_COMPONENT_TYPE or type(self).__name__, diff --git a/src/widgetastic/types.py b/src/widgetastic/types.py index 92967ae..e2e0a4e 100644 --- a/src/widgetastic/types.py +++ b/src/widgetastic/types.py @@ -3,39 +3,31 @@ ================================== """ -from typing import Callable -from typing import Dict -from typing import List -from typing import Protocol -from typing import Tuple -from typing import TYPE_CHECKING -from typing import Union +from collections.abc import Callable +from typing import TYPE_CHECKING, Protocol, Union -from playwright.sync_api import ElementHandle -from playwright.sync_api import Locator +from playwright.sync_api import ElementHandle, Locator from .locator import SmartLocator if TYPE_CHECKING: from .browser import Browser from .utils import Version - from .widget.base import View - from .widget.base import Widget - from .widget.base import ClickableMixin + from .widget.base import ClickableMixin, View, Widget class LocatorProtocol(Protocol): CHECK_VISIBILITY: bool - def __locator__(self) -> Union[str, SmartLocator, Locator, ElementHandle]: ... + def __locator__(self) -> str | SmartLocator | Locator | ElementHandle: ... -LocatorAlias = Union[str, Dict[str, str], Locator, ElementHandle, LocatorProtocol, "Widget"] +LocatorAlias = Union[str, dict[str, str], Locator, ElementHandle, LocatorProtocol, "Widget"] ElementParent = Union[LocatorAlias, "Browser"] ViewParent = Union["Browser", "View"] -VString = Union[str, "Version", List[Union[int, str]], Tuple[Union[int, str]]] +VString = Union[str, "Version", list[int | str], tuple[int | str]] Handler = Union[str, "ClickableMixin", Callable] diff --git a/src/widgetastic/utils.py b/src/widgetastic/utils.py index 5e1108b..9ee5947 100644 --- a/src/widgetastic/utils.py +++ b/src/widgetastic/utils.py @@ -11,9 +11,8 @@ from cached_property import cached_property +from . import log, xpath from .locator import SmartLocator -from . import log -from . import xpath class Widgetable: @@ -163,7 +162,7 @@ def __str__(self): return self.vstring def __repr__(self): - return f"{type(self).__name__}({repr(self.vstring)})" + return f"{type(self).__name__}({self.vstring!r})" def __lt__(self, other): try: @@ -172,9 +171,7 @@ def __lt__(self, other): except Exception: raise ValueError(f"Cannot compare Version to {type(other).__name__}") - if self == other: - return False - elif self == self.latest() or other == self.lowest(): + if self == other or self == self.latest() or other == self.lowest(): return False elif self == self.lowest() or other == self.latest(): return True @@ -333,7 +330,7 @@ def __init__(self, version_dict): self.version_dict = version_dict def __repr__(self): - return f"{type(self).__name__}({repr(self.version_dict)})" + return f"{type(self).__name__}({self.version_dict!r})" @property def child_items(self): @@ -358,9 +355,7 @@ def pick(self, version): return v_dict.get(sorted_matching_versions[0]) else: raise ValueError( - "When trying to version pick {!r} in {!r}, matching version was not found".format( - version, versions - ) + f"When trying to version pick {version!r} in {versions!r}, matching version was not found" ) def __get__(self, o, type=None): @@ -777,9 +772,7 @@ def nested_getattr(o, steps): steps = steps.split(".") if not isinstance(steps, (list, tuple)): raise TypeError( - "nested_getattr only accepts strings, lists, or tuples!, You passed {}".format( - type(steps).__name__ - ) + f"nested_getattr only accepts strings, lists, or tuples!, You passed {type(steps).__name__}" ) steps = [step.strip() for step in steps if step.strip()] if not steps: @@ -851,7 +844,7 @@ def crop_string_middle(s, length=32, cropper="..."): return s[:half] + cropper + s[-half - 1 :] -class partial_match: # noqa +class partial_match: """Use this to wrap values to be selected using partial matching in various objects. It proxies all ``get`` operations to the underlying ``item``. diff --git a/src/widgetastic/widget/__init__.py b/src/widgetastic/widget/__init__.py index d3501fd..8a2764f 100644 --- a/src/widgetastic/widget/__init__.py +++ b/src/widgetastic/widget/__init__.py @@ -1,28 +1,23 @@ """This module contains the base classes that are used to implement the more specific behaviour.""" -from .base import * # noqa: F403 F401 +from .base import * from .checkbox import Checkbox from .image import Image -from .input import BaseInput -from .input import ColourInput -from .input import FileInput -from .input import TextInput +from .input import BaseInput, ColourInput, FileInput, TextInput from .select import Select -from .table import Table -from .table import TableColumn -from .table import TableRow +from .table import Table, TableColumn, TableRow from .text import Text __all__ = [ - "Image", "BaseInput", "Checkbox", "ColourInput", "FileInput", - "TextInput", + "Image", "Select", "Table", "TableColumn", "TableRow", "Text", + "TextInput", ] diff --git a/src/widgetastic/widget/base.py b/src/widgetastic/widget/base.py index 3327d62..cecb75a 100644 --- a/src/widgetastic/widget/base.py +++ b/src/widgetastic/widget/base.py @@ -3,32 +3,34 @@ import types from copy import copy -from playwright.sync_api import ElementHandle -from playwright.sync_api import Locator - - -from widgetastic.locator import SmartLocator +from playwright.sync_api import ElementHandle, Locator from wait_for import wait_for -from widgetastic.browser import Browser -from widgetastic.browser import BrowserParentWrapper -from widgetastic.exceptions import DoNotReadThisWidget -from widgetastic.exceptions import LocatorNotImplemented -from widgetastic.exceptions import NoSuchElementException -from widgetastic.log import call_sig -from widgetastic.log import create_child_logger -from widgetastic.log import create_widget_logger -from widgetastic.log import logged -from widgetastic.log import PrependParentsAdapter -from widgetastic.utils import ConstructorResolvable -from widgetastic.utils import DefaultFillViewStrategy -from widgetastic.utils import deflatten_dict -from widgetastic.utils import Fillable -from widgetastic.utils import FillContext -from widgetastic.utils import nested_getattr -from widgetastic.utils import ParametrizedLocator -from widgetastic.utils import ParametrizedString -from widgetastic.utils import Widgetable +from widgetastic.browser import Browser, BrowserParentWrapper +from widgetastic.exceptions import ( + DoNotReadThisWidget, + LocatorNotImplemented, + NoSuchElementException, +) +from widgetastic.locator import SmartLocator +from widgetastic.log import ( + PrependParentsAdapter, + call_sig, + create_child_logger, + create_widget_logger, + logged, +) +from widgetastic.utils import ( + ConstructorResolvable, + DefaultFillViewStrategy, + Fillable, + FillContext, + ParametrizedLocator, + ParametrizedString, + Widgetable, + deflatten_dict, + nested_getattr, +) def do_not_read_this_widget(): @@ -453,8 +455,7 @@ def locatable_parent(self): for locatable in list(reversed(self.hierarchy))[1:]: if hasattr(locatable, "__locator__") and not getattr(locatable, "INDIRECT", False): return locatable - else: - return None + return None @property def root_browser(self): @@ -576,7 +577,6 @@ def child_widget_accessed(self, widget): Args: widget: The widget being accessed or :py:class:`ParametrizedViewRequest`. """ - pass def fill(self, *args, **kwargs): """Interactive objects like inputs, selects, checkboxes, et cetera should implement fill. @@ -695,7 +695,7 @@ def __iter__(self): def _gen_locator_meth(loc): """Generate a __locator__ method for a widget.""" - def __locator__(self): # noqa + def __locator__(self): return loc return __locator__ @@ -704,7 +704,7 @@ def __locator__(self): # noqa def _gen_locator_root(): """Generate a __locator__ method for a widget that returns the ROOT locator.""" - def __locator__(self): # noqa + def __locator__(self): return self.ROOT return __locator__ @@ -838,9 +838,7 @@ def view_process(cls_or_descriptor): or (inspect.isclass(cls_or_descriptor) and issubclass(cls_or_descriptor, Widget)) ): raise TypeError( - "Unsupported object registered into the selector ({!r})".format( - cls_or_descriptor - ) + f"Unsupported object registered into the selector ({cls_or_descriptor!r})" ) self.registered_views.append((condition, cls_or_descriptor)) if default: @@ -848,7 +846,6 @@ def view_process(cls_or_descriptor): raise TypeError("Multiple default views specified") self.default_view = cls_or_descriptor # We explicitly return None - return None if widget is None: return view_process @@ -879,9 +876,7 @@ def __get__(self, o, t): condition_arg_cache[self.reference] = ref_value except AttributeError: raise TypeError( - "Wrong widget name specified as reference=: {}".format( - self.reference - ) + f"Wrong widget name specified as reference=: {self.reference}" ) except NoSuchElementException: if self.ignore_bad_reference: @@ -1087,7 +1082,6 @@ def before_fill(self, values): Args: values: The same values that are passed to :py:meth:`fill` """ - pass def after_fill(self, was_change): """A hook invoked after all the widgets were filled. @@ -1098,7 +1092,6 @@ def after_fill(self, was_change): Args: was_change: :py:class:`bool` signalizing whether the :py:meth:`fill` changed anything, """ - pass def child_widget_accessed(self, widget): """This hook is called when a child widget of current view is accessed. @@ -1111,7 +1104,7 @@ def child_widget_accessed(self, widget): self.browser.switch_to_main_frame() parents = [p for p in self.hierarchy if getattr(p, "FRAME", None)] for parent in parents: - self.browser.switch_to_frame(getattr(parent, "FRAME")) + self.browser.switch_to_frame(parent.FRAME) class ParametrizedView(View): @@ -1219,9 +1212,7 @@ def __call__(self, *args, **kwargs): for param in self.view_class.PARAMETERS: if param not in param_dict: raise TypeError( - "You did not pass the required parameter {} into {}".format( - param, self.view_class.__name__ - ) + f"You did not pass the required parameter {param} into {self.view_class.__name__}" ) new_kwargs = copy(self.kwargs) @@ -1267,8 +1258,8 @@ def __len__(self): def __getattr__(self, attr): raise AttributeError( - "This is not an instance of {}. You need to call this object and pass the required " - "parameters of the view.".format(self.view_class.__name__) + f"This is not an instance of {self.view_class.__name__}. You need to call this object and pass the required " + "parameters of the view." ) def read(self): diff --git a/src/widgetastic/widget/checkbox.py b/src/widgetastic/widget/checkbox.py index 242f28c..29bdca9 100644 --- a/src/widgetastic/widget/checkbox.py +++ b/src/widgetastic/widget/checkbox.py @@ -1,6 +1,7 @@ +from widgetastic.exceptions import WidgetOperationFailed + from .base import ClickableMixin from .input import BaseInput -from widgetastic.exceptions import WidgetOperationFailed class Checkbox(BaseInput, ClickableMixin): diff --git a/src/widgetastic/widget/input.py b/src/widgetastic/widget/input.py index 5879756..bd7e828 100644 --- a/src/widgetastic/widget/input.py +++ b/src/widgetastic/widget/input.py @@ -1,7 +1,8 @@ -from .base import Widget from widgetastic.exceptions import DoNotReadThisWidget from widgetastic.xpath import quote +from .base import Widget + class BaseInput(Widget): """This represents the bare minimum to interact with bogo-standard form inputs. diff --git a/src/widgetastic/widget/select.py b/src/widgetastic/widget/select.py index 21aa654..d40033c 100644 --- a/src/widgetastic/widget/select.py +++ b/src/widgetastic/widget/select.py @@ -3,11 +3,11 @@ from cached_property import cached_property -from .base import ClickableMixin -from .base import Widget from widgetastic.utils import normalize_space from widgetastic.xpath import quote +from .base import ClickableMixin, Widget + class Select(Widget, ClickableMixin): """Representation of the bogo-standard ``