Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,20 @@

<br>

## v3.4.1
## v3.4.2
*Release date: 2026-03-28*

### Fixed
- `ShadowDriverWrapper` now correctly receives static methods of its driver type — previously methods from the first session's driver were inherited and not overridden
- `get_driver_instance` cache key changed from driver instance to driver type — prevents cache misses on every new driver object

### Changed
- `_set_static` guard stores the configured class instead of `True` — allows re-configuration when driver type changes
- `_set_static` uses `_framework_attrs` snapshot instead of full MRO scan — protects only original framework methods, not previously set driver-specific ones

---

## v3.4.1
*Release date: 2026-03-27*

### Fixed
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.4.1'
__version__ = '3.4.2'
__project_name__ = 'mops'
4 changes: 3 additions & 1 deletion mops/base/driver_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,9 @@ def __new__(cls, *args, **kwargs):
if cls.session.sessions_count() == 0:
cls = super().__new__(cls)
else:
cls = super().__new__(type(f'ShadowDriverWrapper', (cls, ), get_attributes_from_object(cls))) # noqa
attrs = get_attributes_from_object(cls)
attrs.pop('_configured', None)
cls = super().__new__(type(f'ShadowDriverWrapper', (cls, ), attrs)) # noqa

for name, _ in extract_named_objects(cls, bool).items():
setattr(cls, name, False)
Expand Down
18 changes: 14 additions & 4 deletions mops/mixins/internal_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from mops.utils.internal_utils import (
extract_named_objects,
extract_all_named_objects,
is_driver_wrapper,
)


Expand Down Expand Up @@ -49,6 +50,15 @@ def _safe_setter(self, var: str, value: Any):
if not hasattr(self, var):
setattr(self, var, value)

def _get_protected_attrs(self: Any, current_obj_cls) -> set:
if not is_driver_wrapper(self):
return set(get_all_static_attributes(current_obj_cls))

if '_framework_attrs' not in current_obj_cls.__dict__:
current_obj_cls._framework_attrs = set(get_all_static_attributes(current_obj_cls))

return current_obj_cls.__dict__['_framework_attrs']

def _set_static(self: Any, cls) -> None:
"""
Set static from base cls (Web/Mobile/Play Element/Page etc.)
Expand All @@ -57,16 +67,16 @@ def _set_static(self: Any, cls) -> None:
"""
current_obj_cls = self.__class__

if current_obj_cls.__dict__.get('_configured'):
if current_obj_cls.__dict__.get('_configured') is cls:
return

existing_attrs = set(get_all_static_attributes(current_obj_cls))
protected = self._get_protected_attrs(current_obj_cls)

for name, value in get_static_attributes(cls).items():
if name not in existing_attrs:
if name not in protected:
setattr(current_obj_cls, name, value)

current_obj_cls._configured = True
current_obj_cls._configured = cls

def _repr_builder(self: Any):
class_name = self.__class__.__name__
Expand Down
12 changes: 12 additions & 0 deletions tests/static_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,18 @@ def base_teardown():
CoreDriver.driver = None
DriverWrapperSessions.all_sessions = []

if '_framework_attrs' in MockedDriverWrapper.__dict__:
framework_attrs = MockedDriverWrapper.__dict__['_framework_attrs']
for attr in list(MockedDriverWrapper.__dict__.keys()):
if not attr.startswith('_') and attr not in framework_attrs:
try:
delattr(MockedDriverWrapper, attr)
except AttributeError:
pass
del MockedDriverWrapper._framework_attrs
if '_configured' in MockedDriverWrapper.__dict__:
del MockedDriverWrapper._configured


mobile_drivers = [mocked_ios_driver.__name__, mocked_android_driver.__name__]
mobile_ids = ['appium ios', 'appium android']
Expand Down
65 changes: 65 additions & 0 deletions tests/static_tests/integration/test_driver_attrs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import inspect

from mops.base.driver_wrapper import DriverWrapper
from mops.playwright.play_driver import PlayDriver
from mops.selenium.core.core_driver import CoreDriver
from mops.selenium.driver.mobile_driver import MobileDriver
from mops.selenium.driver.web_driver import WebDriver
from mops.utils.internal_utils import get_attributes_from_object


def _own_methods(cls):
return {
name for name, val in get_attributes_from_object(cls).items()
if not name.startswith('_') and callable(val)
}


def _assert_from(dw, methods, source_cls):
for name in methods:
raw = inspect.getattr_static(dw, name, None)
assert raw is not None, f"'{name}' not found on {type(dw).__name__}"
func = raw.__func__ if isinstance(raw, (classmethod, staticmethod)) else raw
qualname = getattr(func, '__qualname__', '')
assert source_cls.__name__ in qualname, (
f"'{name}' on {type(dw).__name__} expected from {source_cls.__name__}, "
f"got {qualname!r}"
)


_WRAPPER = _own_methods(DriverWrapper)
_WEB = _own_methods(WebDriver)
_MOBILE = _own_methods(MobileDriver)
_CORE = _own_methods(CoreDriver)
_PLAY = _own_methods(PlayDriver)


def _assert_android_attrs(dw):
_assert_from(dw, _MOBILE, MobileDriver)
_assert_from(dw, _CORE - _MOBILE - _WRAPPER, CoreDriver)
_assert_from(dw, _WRAPPER, DriverWrapper)


def _assert_selenium_attrs(dw):
_assert_from(dw, _WEB, WebDriver)
_assert_from(dw, _CORE - _WEB - _WRAPPER, CoreDriver)
_assert_from(dw, _WRAPPER, DriverWrapper)


def _assert_playwright_attrs(dw):
_assert_from(dw, _PLAY - _WRAPPER, PlayDriver)
_assert_from(dw, _WRAPPER, DriverWrapper)


def test_android_and_selenium_attrs(mocked_android_driver, mocked_selenium_driver):
assert 'Shadow' not in type(mocked_android_driver).__name__
assert type(mocked_selenium_driver).__name__ == 'ShadowDriverWrapper'
_assert_android_attrs(mocked_android_driver)
_assert_selenium_attrs(mocked_selenium_driver)


def test_android_and_playwright_attrs(mocked_android_driver, mocked_play_driver):
assert 'Shadow' not in type(mocked_android_driver).__name__
assert type(mocked_play_driver).__name__ == 'ShadowDriverWrapper'
_assert_android_attrs(mocked_android_driver)
_assert_playwright_attrs(mocked_play_driver)
Loading