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/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) diff --git a/mops/mixins/internal_mixin.py b/mops/mixins/internal_mixin.py index f06b602e..2254785b 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, ) @@ -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.) @@ -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__ 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)