Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
cf93851
240048 -> 230046
VladimirPodolian Jan 26, 2025
f4441e5
240048 -> 10002
VladimirPodolian Jan 26, 2025
79cee07
Fixes
VladimirPodolian Jan 26, 2025
30ee169
get_static used for validation
VladimirPodolian Jan 26, 2025
50d4e6c
Split get_static into 2 methods
VladimirPodolian Jan 26, 2025
af82fe6
Fixes
VladimirPodolian Jan 26, 2025
58cfab9
Fixes: setattr & page
VladimirPodolian Jan 26, 2025
2f50ef5
_modify_sub_elements reworked for Element
VladimirPodolian Jan 26, 2025
c120fe9
NotInitializedException moved to CoreElement/PlayElement
VladimirPodolian Jan 26, 2025
7132da4
Static test fixes
VladimirPodolian Jan 26, 2025
683b1e0
Static test fixes
VladimirPodolian Jan 26, 2025
c48efd9
Static test fixes
VladimirPodolian Jan 26, 2025
e0baedc
Static test fixes
VladimirPodolian Jan 26, 2025
9087d2d
Small update
VladimirPodolian Jan 30, 2025
e754221
get_all_sub_elements added
VladimirPodolian Jan 31, 2025
760235b
Final speed up fixes. Static tests need to be fixed
VladimirPodolian Feb 1, 2025
2c53d04
Version updated
VladimirPodolian Feb 1, 2025
e8e690b
Element._element_cls added
VladimirPodolian Feb 2, 2025
5fcb9c0
Locator setup fixes
VladimirPodolian Feb 2, 2025
dbebfed
Fixes and improvements
VladimirPodolian Feb 3, 2025
cb85548
Merge remote-tracking branch 'origin/master' into performance-improve…
VladimirPodolian Jan 7, 2026
f4911d5
Post merge fixes
VladimirPodolian Jan 7, 2026
765942e
Claude optimisation
VladimirPodolian Mar 12, 2026
7d76e3f
Fix ShadowDriverWrapper init
VladimirPodolian Mar 12, 2026
bb58b3d
Fixes
VladimirPodolian Mar 12, 2026
d3c3710
Rollback some changes
VladimirPodolian Mar 12, 2026
4d783d3
Fix unexpected work with inheritance
VladimirPodolian Mar 12, 2026
c177b71
Fix missed parent attribute from some elements
VladimirPodolian Mar 24, 2026
94e60d7
Merge remote-tracking branch 'origin/master' into performance-improve…
VladimirPodolian Mar 26, 2026
c5237e9
Test fixes
VladimirPodolian Mar 26, 2026
9b313b7
Test fixes
VladimirPodolian Mar 26, 2026
8261974
Error message improvement
VladimirPodolian Mar 26, 2026
9cc48a0
Changelog & final fixes
VladimirPodolian Mar 26, 2026
e8f875a
Final fixes
VladimirPodolian Mar 26, 2026
47a164c
Rollback performance changes
VladimirPodolian Mar 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/static_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: [ "3.9", "3.11", "3.12", "3.13"]

steps:
- name: Checkout code
Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,34 @@

<br>

## v3.4.0 (Performance improvement)
*Release date: 2026-03-26*

### Breaking Changes
- **`Group` subclasses**: `parent` is now correctly set on sub-elements defined after `super().__init__()` —
previously such elements did not receive `parent` argument

### Added
- `Element.sub_elements` dict — collected once and reused instead of rescanning on every access
- `ElementMeta` metaclass — triggers `_modify_sub_elements` automatically after `__init__` of the final class
- `get_static_attributes` / `get_all_static_attributes` with `lru_cache` — replaces repeated attribute scanning
- `get_driver_instance` with `lru_cache` — caches `isinstance` results for driver type checks
- `_driver_is_instance` method on `InternalMixin` — single cached entry point for driver type detection

### Changed
- `all_tags` converted to `frozenset` for O(1) membership checks
- `initialize_objects` no longer recurses manually — delegates to `_modify_sub_elements` on each child
- `set_parent_for_attr` uses `sub_elements` dict instead of rescanning object attributes
- `get_child_elements_with_names` / `safe_getattribute` removed, replaced by `extract_named_objects` / `extract_all_named_objects`
- `locator`, `locator_type`, `log_locator` on `Element` converted to lazy properties — resolved on first access
- `__copy__` added to `Element` for explicit shallow copy control
- `__getattribute__` override removed from `Element` — initialization guard moved to `CoreElement`/`PlayElement`

### Fixed
- Error messages for unsupported driver type now include the actual driver class name and list expected types

---

## v3.3.2
*Release date: 2026-03-24*

Expand All @@ -16,6 +44,8 @@
### Changed
- `safe_call` exceptions list

---

## v3.3.0
*Release date: 2026-01-05*

Expand Down
2 changes: 1 addition & 1 deletion mops/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
__version__ = '3.3.2'
__version__ = '3.4.0'
__project_name__ = 'mops'
38 changes: 34 additions & 4 deletions mops/abstraction/element_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,32 @@

class ElementABC(MixinABC, ABC):

locator: Union[Locator, str]
name: str = ''
parent: Union[Any, bool, None] = None
wait: Optional[bool] = None
name: str
parent: Union[Any, bool, None]
wait: Optional[bool]

_locator: Union[str, Locator]
_locator_type: Union[str, None] = None

@property
def locator(self) -> str:
raise NotImplementedError()

@locator.setter
def locator(self, value: Union[Locator, str]) -> None:
raise NotImplementedError()

@property
def locator_type(self) -> str:
raise NotImplementedError()

@locator_type.setter
def locator_type(self, value: str) -> None:
raise NotImplementedError()

@property
def log_locator(self) -> str:
raise NotImplementedError()

@property
def element(self) -> Union[SeleniumWebElement, AppiumWebElement, PlayWebElement]:
Expand Down Expand Up @@ -925,3 +947,11 @@ def _get_all_elements(self, sources: Union[tuple, list]) -> List[Element]:
:return: A list of wrapped :class:`Element` objects.
"""
raise NotImplementedError()

def _set_locator(self) -> None:
"""
Set locator for current object

:return: :obj:`None`
"""
raise NotImplementedError()
10 changes: 7 additions & 3 deletions mops/base/driver_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from mops.selenium.driver.web_driver import WebDriver
from mops.exceptions import DriverWrapperException
from mops.mixins.internal_mixin import InternalMixin
from mops.utils.internal_utils import get_attributes_from_object, get_child_elements_with_names
from mops.utils.internal_utils import extract_named_objects, get_attributes_from_object
from mops.utils.logs import Logging, LogLevel


Expand Down Expand Up @@ -130,7 +130,7 @@ def __new__(cls, *args, **kwargs):
else:
cls = super().__new__(type(f'ShadowDriverWrapper', (cls, ), get_attributes_from_object(cls))) # noqa

for name, _ in get_child_elements_with_names(cls, bool).items():
for name, _ in extract_named_objects(cls, bool).items():
setattr(cls, name, False)

return cls
Expand Down Expand Up @@ -348,7 +348,11 @@ def __init_base_class__(self) -> None:
self.is_selenium = True
self._base_cls = WebDriver
else:
raise DriverWrapperException(f'Cant specify {self.__class__.__name__}')
raise DriverWrapperException(
f'Cannot initialize {self.__class__.__name__}: '
f'unsupported driver type "{type(source_driver).__name__}". '
f'Expected Playwright, Appium or Selenium driver instance'
)

self._set_static(self._base_cls)
self._base_cls.__init__(self, driver_container=self.__driver_container)
Expand Down
144 changes: 97 additions & 47 deletions mops/base/element.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from __future__ import annotations

import functools
import time
from abc import ABCMeta
from copy import copy
from functools import cached_property
from typing import Union, List, Type, Tuple, Optional, TYPE_CHECKING

from PIL.Image import Image
Expand Down Expand Up @@ -33,10 +36,8 @@
WAIT_EL,
is_target_on_screen,
initialize_objects,
get_child_elements_with_names,
safe_getattribute,
extract_named_objects,
set_parent_for_attr,
is_page,
QUARTER_WAIT_EL,
)
from mops.utils.decorators import wait_condition, wait_continuous
Expand All @@ -45,7 +46,22 @@
from mops.base.group import Group


class Element(DriverMixin, InternalMixin, Logging, ElementABC):
class ElementMeta(ABCMeta):
def __new__(mcs, name, bases, namespace, **kwargs):
cls = super().__new__(mcs, name, bases, namespace, **kwargs)
orig_init = cls.__init__

@functools.wraps(orig_init)
def wrapped_init(self, *args, **kw):
orig_init(self, *args, **kw)
if type(self) is cls and getattr(self, '_initialized', False):
self._modify_sub_elements()

cls.__init__ = wrapped_init
return cls


class Element(DriverMixin, InternalMixin, Logging, ElementABC, metaclass=ElementMeta):
"""
Represents a UI element that serves as a central component for interaction.

Expand All @@ -54,34 +70,30 @@ class Element(DriverMixin, InternalMixin, Logging, ElementABC):
It dynamically adapts to different driver types (Playwright, Appium, Selenium)
and provides a unified interface for UI interactions.
"""
_object: str = 'element'
_initialized: bool = False
_is_locator_configured: bool = False
_base_cls: Type[PlayElement, MobileElement, WebElement]

source_locator: Union[Locator, str]

_object = 'element'
_base_cls: Type[PlayElement, MobileElement, WebElement]
driver_wrapper: DriverWrapper

def __new__(cls, *args, **kwargs):
instance = super(Element, cls).__new__(cls)
set_instance_frame(instance)
return instance

def __copy__(self):
new = object.__new__(self.__class__)
new.__dict__.update(self.__dict__)
return new

def __repr__(self):
return self._repr_builder()

def __call__(self, driver_wrapper: DriverWrapper = None):
self.__full_init__(driver_wrapper=get_driver_wrapper_from_object(driver_wrapper))
return self

def __getattribute__(self, item):
if 'element' in item and not safe_getattribute(self, '_initialized'):
raise NotInitializedException(
f'{repr(self)} object is not initialized. '
'Try to initialize base object first or call it directly as a method'
)

return safe_getattribute(self, item)

def __init__(
self,
locator: Union[Locator, str],
Expand All @@ -108,36 +120,26 @@ def __init__(
an object containing it to be used for this element.
:type driver_wrapper: typing.Union[DriverWrapper, typing.Any]
"""
self._validate_inheritance()

if parent:
if not isinstance(parent, (bool, Element)):
error = (f'The given "parent" arg of "{self.name}" should take an Element/Group '
f'object or False for skip. Get {parent}')
raise ValueError(error)
self.driver_wrapper = get_driver_wrapper_from_object(driver_wrapper)

self.locator = locator
self.source_locator = locator
self.name = name if name else locator
self.locator = locator
self.name = name or locator
self.parent = parent
self.wait = wait
self.driver_wrapper = get_driver_wrapper_from_object(driver_wrapper)

self._init_locals = getattr(self, '_init_locals', locals())
self._safe_setter('__base_obj_id', id(self))
self._initialized = False

if self.driver_wrapper:
self.__full_init__(driver_wrapper)

def __full_init__(self, driver_wrapper: Any = None):
self._driver_wrapper_given = bool(driver_wrapper)

if self._driver_wrapper_given and driver_wrapper != self.driver_wrapper:
if driver_wrapper and driver_wrapper != self.driver_wrapper:
self.driver_wrapper = get_driver_wrapper_from_object(driver_wrapper)

self._modify_object()
self._modify_children()

if not self._initialized:
self.__init_base_class__()
Expand All @@ -148,19 +150,57 @@ def __init_base_class__(self) -> None:

:return: None
"""
if isinstance(self.driver, PlaywrightDriver):
if self._driver_is_instance(PlaywrightDriver):
self._base_cls = PlayElement
elif isinstance(self.driver, AppiumDriver):
elif self._driver_is_instance(AppiumDriver):
self._base_cls = MobileElement
elif isinstance(self.driver, SeleniumDriver):
elif self._driver_is_instance(SeleniumDriver):
self._base_cls = WebElement
else:
raise DriverWrapperException(f'Cant specify {self.__class__.__name__}')
raise DriverWrapperException(
f'Cannot initialize {self.__class__.__name__}: '
f'unsupported driver type "{type(self.driver).__name__}". '
f'Expected Playwright, Appium or Selenium driver instance'
)

self._set_static(self._base_cls)
self._base_cls.__init__(self)
self._initialized = True

@property
def locator(self) -> str:
if not self._is_locator_configured:
self._set_locator()

return self._locator

@locator.setter
def locator(self, value: Union[Locator, str]) -> None:
self._log_locator = value
self._locator = value

@property
def locator_type(self) -> str:
if not self._is_locator_configured:
self._set_locator()

return self._locator_type

@locator_type.setter
def locator_type(self, value: str) -> None:
self._locator_type = value

@property
def log_locator(self) -> str:
if not self._is_locator_configured:
self._set_locator()

return self._log_locator

@log_locator.setter
def log_locator(self, value: str) -> None:
self._log_locator = value

# Following methods works same for both Selenium/Appium and Playwright APIs using internal methods

# Elements interaction
Expand Down Expand Up @@ -955,31 +995,41 @@ def _get_all_elements(self, sources: Union[tuple, list]) -> List[Any]:
wrapped_object: Any = copy(self)
wrapped_object.element = element
wrapped_object._wrapped = True
set_parent_for_attr(wrapped_object, Element, with_copy=True)
wrapped_object.sub_elements = dict(self.sub_elements)
set_parent_for_attr(wrapped_object, with_copy=True)
wrapped_elements.append(wrapped_object)

return wrapped_elements

def _modify_children(self):
def _modify_sub_elements(self) -> None:
"""
Initializing of attributes with type == Element.
Initializing of attributes with type == Element.
Required for classes with base == Element.

:return: :obj:`None`
"""
initialize_objects(self, get_child_elements_with_names(self, Element), Element)
self.sub_elements = {}

if type(self) is not self._element_cls:
self.sub_elements = extract_named_objects(self, Element)
initialize_objects(self, self.sub_elements)

def _modify_object(self):
def _modify_object(self) -> None:
"""
Modify current object if driver_wrapper is not given. Required for Page that placed into functions:
- sets driver from previous object

:return: :obj:`None`
"""
if not self._driver_wrapper_given:
PreviousObjectDriver().set_driver_from_previous_object(self)

def _validate_inheritance(self):
cls = self.__class__
mro = cls.__mro__
@cached_property
def _element_cls(self) -> Type[Element]:
"""
Returns the `Element` class.
This can be overridden for performance optimizations.

for item in mro:
if is_page(item):
raise TypeError(
f"You cannot make an inheritance for {cls.__name__} from both Element/Group and Page objects")
:return: :obj:`typing.Type` [:class:`Element`]
"""
return Element
Loading
Loading