Skip to content

Commit

Permalink
Merge pull request #100 from saritasa-nest/feature/improve-item-class…
Browse files Browse the repository at this point in the history
…-getting

Improve item class getting
  • Loading branch information
M1troll authored Dec 18, 2024
2 parents c278c42 + 4de3067 commit 767be78
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 4 deletions.
5 changes: 5 additions & 0 deletions docs/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ Version history

We follow `Semantic Versions <https://semver.org/>`_.

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.
Expand Down
51 changes: 49 additions & 2 deletions pomcorn/component.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import typing
from inspect import isclass
from typing import (
Any,
Expand Down Expand Up @@ -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__()
Expand Down Expand Up @@ -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."""
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>",
Expand Down
33 changes: 32 additions & 1 deletion tests/list_component/test_item_class.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Generic, TypeVar
from typing import Generic, TypeAlias, TypeVar

import pytest

Expand Down Expand Up @@ -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

0 comments on commit 767be78

Please sign in to comment.