Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve item class getting #100

Merged
merged 2 commits into from
Dec 18, 2024
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
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