From a378f751abdee91cf7138f451faef84b184d7efb Mon Sep 17 00:00:00 2001 From: Anton Oboleninov Date: Fri, 29 Nov 2024 17:33:37 +0700 Subject: [PATCH 1/2] Improve item class getting After the previous pomcorn 0.8.1 update, we encountered two issues: * TypeAlias passed in first generic parametter don't pass `is_valid_item_class` check * We could not use `ListComponent` as a parameterized type (e.g., `list_component = List[ItemClass, Page]`). In this commit, we improved getting `_item_class` for a parameterized `ListComponent` and added support for TypeAlias in `is_valid_item_class`. We also added tests for these cases. --- pomcorn/component.py | 51 ++++++++++++++++++++++++- tests/list_component/test_item_class.py | 33 +++++++++++++++- 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/pomcorn/component.py b/pomcorn/component.py index b1376ad..834dcda 100644 --- a/pomcorn/component.py +++ b/pomcorn/component.py @@ -1,3 +1,4 @@ +import typing from inspect import isclass from typing import ( Any, @@ -207,6 +208,43 @@ class ListComponent(Generic[ListItemType, TPage], Component[TPage]): item_locator: locators.XPathLocator | None = None relative_item_locator: locators.XPathLocator | None = None + def __class_getitem__(cls, item: tuple[type, ...]) -> Any: + """Create parameterized versions of generic classes. + + This method is called when the class is used as a parameterized type, + such as MyGeneric[int] or MyGeneric[List[str]]. + + We override this method to store values passed in generic parameters. + + Args: + cls - The generic class itself. + item - The type used for parameterization. + + Returns: + type: A parameterized version of the class with the specified type. + + """ + list_cls = super().__class_getitem__(item) # type: ignore + cls.__parameters__ = item # type: ignore + return list_cls + + def __init__( + self, + page: TPage, + base_locator: locators.XPathLocator | None = None, + wait_until_visible: bool = True, + ) -> None: + # If `_item_class` was not specified in `__init_subclass__`, this means + # that `ListComponent` is used as a parameterized type + # (e.g., `List[ItemClass, Page]`). + if isinstance(self._item_class, _EmptyValue): + # In this way we check the stored generic parameters and, if first + # from them is valid, set it as `_item_class` + first_generic_param = self.__parameters__[0] + if self.is_valid_item_class(first_generic_param): + self._item_class = first_generic_param + super().__init__(page, base_locator, wait_until_visible) + def __init_subclass__(cls) -> None: """Run logic for getting/overriding item_class attr for subclasses.""" super().__init_subclass__() @@ -297,10 +335,19 @@ def get_list_item_class(cls) -> type[ListItemType] | None: def is_valid_item_class(cls, item_class: Any) -> bool: """Check that specified ``item_class`` is valid. - Valid `item_class` should be a class and subclass of ``Component``. + Valid ``item_class`` should be + * a class and subclass of ``Component`` + * or TypeAlias based on ``Component`` """ - return isclass(item_class) and issubclass(item_class, Component) + if isclass(item_class) and issubclass(item_class, Component): + return True + + if isinstance(item_class, typing._GenericAlias): # type: ignore + type_alias = item_class.__origin__ # type: ignore + return isclass(type_alias) and issubclass(type_alias, Component) + + return False def get_item_by_text(self, text: str) -> ListItemType: """Get list item by text.""" diff --git a/tests/list_component/test_item_class.py b/tests/list_component/test_item_class.py index d7bde11..b98380e 100644 --- a/tests/list_component/test_item_class.py +++ b/tests/list_component/test_item_class.py @@ -1,4 +1,4 @@ -from typing import Generic, TypeVar +from typing import Generic, TypeAlias, TypeVar import pytest @@ -105,3 +105,34 @@ class List(BaseList[Param]): # Ensure that `List.item_class` has correct type list_cls = List(fake_page) assert list_cls._item_class is ItemClass + + +def test_set_item_class_without_inheritance(fake_page: Page) -> None: + """Check that item_class will be correct in not inherited class.""" + + class BaseList(Generic[TItem, TPage], ListComponent[TItem, TPage]): + """Base list component without specified Generic variables.""" + + base_locator = locators.XPathLocator("html") # required + relative_item_locator = locators.XPathLocator("body") # required + wait_until_visible = lambda _: True # to not wait anything + + # Prepare base list component without specified Generic variables + list_cls = BaseList[ItemClass, Page](fake_page) + # Ensure that `InheritedList.item_class` has correct type + assert list_cls._item_class is ItemClass + + +# Type alias for check that ItemClass can be also a TypeAlias +TypeAliasItemClass: TypeAlias = Component[Page] + + +def test_item_class_can_be_type_alias(fake_page: Page) -> None: + """Check that item_class can be a type alias based on ``Component``.""" + + class List(ListComponent[TypeAliasItemClass, Page]): + base_locator = locators.XPathLocator("html") # required + wait_until_visible = lambda _: True # to not wait anything + + list_cls = List(fake_page) + assert list_cls._item_class is TypeAliasItemClass From 4de3067ee63e7305372e0281bcc2c203e8c05a8a Mon Sep 17 00:00:00 2001 From: Anton Oboleninov Date: Fri, 29 Nov 2024 17:38:02 +0700 Subject: [PATCH 2/2] Update version and changelog --- docs/CHANGELOG.rst | 5 +++++ pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.rst b/docs/CHANGELOG.rst index 2a3e56e..036f29f 100644 --- a/docs/CHANGELOG.rst +++ b/docs/CHANGELOG.rst @@ -3,6 +3,11 @@ Version history We follow `Semantic Versions `_. +0.8.2 (29.11.24) +******************************************************************************* +- Add ability to specify ``TypeAlias`` as ``_item class`` and use + ``ListComponent`` as a parameterized type + 0.8.1 (25.11.24) ******************************************************************************* - Improve getting ``item class`` from first ``ListComponent`` generic variable. diff --git a/pyproject.toml b/pyproject.toml index e9936d6..ba70148 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pomcorn" -version = "0.8.1" +version = "0.8.2" description = "Base implementation of Page Object Model" authors = [ "Saritasa ",