from __future__ import annotations
from typing import TYPE_CHECKING, NoReturn, overload
from pomcorn import locators
if TYPE_CHECKING:
from pomcorn import WebView, XPathElement
[docs]
class Element:
"""Descriptor for init `PomcornElement` as attribute by locator.
.. code-block:: python
# Example
from pomcorn import Page, Element
class MainPage(Page):
title_element = Element(locators.ClassLocator("page-title"))
"""
cache_attribute_name = "cached_elements"
@overload
def __init__(
self,
locator: locators.XPathLocator | None = None,
) -> None: ...
@overload
def __init__(
self,
*,
relative_locator: locators.XPathLocator | None = None,
) -> None: ...
[docs]
def __init__(
self,
locator: locators.XPathLocator | None = None,
*,
relative_locator: locators.XPathLocator | None = None,
) -> None:
"""Initialize descriptor.
Use `relative_locator` if you need to include `base_locator` of
instance, otherwise use `locator`.
If descriptor is used for instance of ``Page``, then
``relative_locator`` is not needed, since element will be searched
across the entire page, not within some component.
"""
self.locator = locator
self.relative_locator = relative_locator
def __set_name__(self, _owner: type, name: str) -> None:
"""Save attribute name for which descriptor is created."""
self.attribute_name = name
def __get__(
self,
instance: WebView | None,
_type: type[WebView],
) -> XPathElement:
"""Get element with stored locator."""
if not instance:
raise AttributeError("This descriptor is for instances only!")
return self.prepare_element(instance)
[docs]
def prepare_element(self, instance: WebView) -> XPathElement:
"""Init and cache element in instance.
Initiate element only once, and then store it in an instance and
return it each subsequent time. This is to avoid calling
`wait_until_visible` multiple times in the init of component.
If the instance doesn't already have an attribute to store cache, it
will be set.
If descriptor is used for ``Component`` and
``self.is_relative_locator=True``, element will be found by sum of
``base_locator`` of that component and passed locator of descriptor.
"""
if not getattr(instance, self.cache_attribute_name, None):
setattr(instance, self.cache_attribute_name, {})
cache = getattr(instance, self.cache_attribute_name, {})
if cached_element := cache.get(self.attribute_name):
return cached_element
element = instance.init_element(
locator=self._prepare_locator(instance),
)
cache[self.attribute_name] = element
return element
def _prepare_locator(self, instance: WebView) -> locators.XPathLocator:
"""Prepare a locator by arguments.
Check that only one locator argument is passed, or none.
If only `relative_locator` was passed, `base_locator` of instance will
be added to specified in descriptor arguments. If only `locator` was
passed, it will return only specified one.
Raises:
ValueError: If both arguments were passed or neither or
``relative_locator`` used not in ``Component``.
"""
if self.relative_locator and self.locator:
raise ValueError(
"You need to pass only one of the arguments: "
"`locator` or `relative_locator`.",
)
if not self.relative_locator:
if not self.locator:
raise ValueError(
"You need to pass one of the arguments: "
"`locator` or `relative_locator`.",
)
return self.locator
from pomcorn import Component
if self.relative_locator and isinstance(instance, Component):
return instance.base_locator // self.relative_locator
raise ValueError(
f"`relative_locator` should be used only if descriptor used in "
f"component. `{instance}` is not a component.",
)
def __set__(self, *args, **kwargs) -> NoReturn:
raise ValueError("You can't reset an element attribute value!")