From 15f7db196ff876acfd0526d1d6363e7307e51b9a Mon Sep 17 00:00:00 2001 From: Vladimir Podolyan Date: Sat, 28 Mar 2026 14:36:13 +0100 Subject: [PATCH 1/5] Fix second driver initialization --- mops/base/driver_wrapper.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mops/base/driver_wrapper.py b/mops/base/driver_wrapper.py index 84a7f535..829aec50 100644 --- a/mops/base/driver_wrapper.py +++ b/mops/base/driver_wrapper.py @@ -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) From d8ae7bfd2885e3d9437ef323afbc1875e0f26b2b Mon Sep 17 00:00:00 2001 From: Vladimir Podolyan Date: Sat, 28 Mar 2026 14:36:35 +0100 Subject: [PATCH 2/5] Fix second driver initialization --- mops/mixins/internal_mixin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mops/mixins/internal_mixin.py b/mops/mixins/internal_mixin.py index f06b602e..f9020dca 100644 --- a/mops/mixins/internal_mixin.py +++ b/mops/mixins/internal_mixin.py @@ -57,7 +57,7 @@ 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)) @@ -66,7 +66,7 @@ def _set_static(self: Any, cls) -> None: if name not in existing_attrs: 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__ From ba0443c655d347a8a9a94d2d8ce80fbdfc83a7a9 Mon Sep 17 00:00:00 2001 From: Vladimir Podolyan Date: Sat, 28 Mar 2026 14:52:53 +0100 Subject: [PATCH 3/5] Tests & bug fix --- mops/mixins/internal_mixin.py | 7 +- tests/static_tests/conftest.py | 12 ++++ .../integration/test_driver_attrs.py | 65 +++++++++++++++++++ 3 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 tests/static_tests/integration/test_driver_attrs.py diff --git a/mops/mixins/internal_mixin.py b/mops/mixins/internal_mixin.py index f9020dca..aa1b932b 100644 --- a/mops/mixins/internal_mixin.py +++ b/mops/mixins/internal_mixin.py @@ -60,10 +60,13 @@ def _set_static(self: Any, cls) -> None: if current_obj_cls.__dict__.get('_configured') is cls: return - existing_attrs = 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)) + + protected = current_obj_cls.__dict__['_framework_attrs'] 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 = cls diff --git a/tests/static_tests/conftest.py b/tests/static_tests/conftest.py index 9849473e..407adb65 100644 --- a/tests/static_tests/conftest.py +++ b/tests/static_tests/conftest.py @@ -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'] diff --git a/tests/static_tests/integration/test_driver_attrs.py b/tests/static_tests/integration/test_driver_attrs.py new file mode 100644 index 00000000..f02fb163 --- /dev/null +++ b/tests/static_tests/integration/test_driver_attrs.py @@ -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) From 476532678177c9c1b64945df01d26e8a934f4d4f Mon Sep 17 00:00:00 2001 From: Vladimir Podolyan Date: Sat, 28 Mar 2026 14:58:43 +0100 Subject: [PATCH 4/5] Performance issue fix --- CHANGELOG.md | 15 ++++++++++++++- mops/__init__.py | 2 +- mops/mixins/internal_mixin.py | 11 +++++++---- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e834a93c..694b420f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,20 @@
-## 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 diff --git a/mops/__init__.py b/mops/__init__.py index b1460d50..6282fef8 100644 --- a/mops/__init__.py +++ b/mops/__init__.py @@ -1,2 +1,2 @@ -__version__ = '3.4.1' +__version__ = '3.4.2' __project_name__ = 'mops' diff --git a/mops/mixins/internal_mixin.py b/mops/mixins/internal_mixin.py index aa1b932b..db11246f 100644 --- a/mops/mixins/internal_mixin.py +++ b/mops/mixins/internal_mixin.py @@ -6,6 +6,7 @@ from mops.utils.internal_utils import ( extract_named_objects, extract_all_named_objects, + is_driver_wrapper, ) @@ -60,10 +61,12 @@ def _set_static(self: Any, cls) -> None: if current_obj_cls.__dict__.get('_configured') is cls: return - if '_framework_attrs' not in current_obj_cls.__dict__: - current_obj_cls._framework_attrs = set(get_all_static_attributes(current_obj_cls)) - - protected = current_obj_cls.__dict__['_framework_attrs'] + if is_driver_wrapper(self): + if '_framework_attrs' not in current_obj_cls.__dict__: + current_obj_cls._framework_attrs = set(get_all_static_attributes(current_obj_cls)) + protected = current_obj_cls.__dict__['_framework_attrs'] + else: + protected = set(get_all_static_attributes(current_obj_cls)) for name, value in get_static_attributes(cls).items(): if name not in protected: From 830dd19d6a0618db4af2500392ccd71231491b57 Mon Sep 17 00:00:00 2001 From: Vladimir Podolyan Date: Sat, 28 Mar 2026 14:59:47 +0100 Subject: [PATCH 5/5] _get_protected_attrs added --- mops/mixins/internal_mixin.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/mops/mixins/internal_mixin.py b/mops/mixins/internal_mixin.py index db11246f..2254785b 100644 --- a/mops/mixins/internal_mixin.py +++ b/mops/mixins/internal_mixin.py @@ -50,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.) @@ -61,12 +70,7 @@ def _set_static(self: Any, cls) -> None: if current_obj_cls.__dict__.get('_configured') is cls: return - if is_driver_wrapper(self): - if '_framework_attrs' not in current_obj_cls.__dict__: - current_obj_cls._framework_attrs = set(get_all_static_attributes(current_obj_cls)) - protected = current_obj_cls.__dict__['_framework_attrs'] - else: - protected = 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 protected: