def pipe(*functions):
    """
    pipes functions one by one in the provided order
    i.e. applies arg1, then arg2, then arg3, and so on
    if any arg is None, just skips it
    """
    return (
        functools.reduce(
            lambda f, g: lambda x: f(g(x)) if g else f(x),
            functions[::-1],
            lambda x: x,
        )
        if functions
        else None
    ) class Config:
    # todo: add hooks:
    #            hook_wait_command: Callable[[Command], None]
    #            hook_...
    #       consider impementing some hooks as generators...
    #       hook generally should allow to do some processing before and after e.g. command
    #       think on better name depending on hook type (generator vs standard fn)
    def __init__(
        self,
        driver: Optional[WebDriver] = None,
        timeout: int = 4,
        hook_wait_failure: Optional[
            Callable[[TimeoutException], Exception]
        ] = None,
        base_url: str = '',
        set_value_by_js: bool = False,
        type_by_js: bool = False,
        wait_for_no_overlap_found_by_js: bool = False,
        window_width: Optional[int] = None,
        window_height: Optional[int] = None,
        log_outer_html_on_failure: bool = False,
    ):

        self._driver = driver
        self._timeout = timeout
        self._hook_wait_failure = hook_wait_failure
        '''
        todo: why we name it as hook_* why not handle_* ?
              what would be proper style?
        '''
        self._base_url = base_url
        self._set_value_by_js = set_value_by_js
        '''
        todo: will it work on mobile? probably no! why then we have it here? o_O
        '''
        self._type_by_js = type_by_js
        self._wait_for_no_overlap_found_by_js = wait_for_no_overlap_found_by_js
        self._window_width = window_width
        self._window_height = window_height
        self._log_outer_html_on_failure = log_outer_html_on_failure
        '''
        todo: should we even keep it as part of core.configuration?
              taking into account that it does not work for mobile
              while we want the core of Selene to be versatile as much as psbl.
              probably we should move it to support.shared.config only
        '''

    def as_dict(self, skip_empty=True):
        return {
            _strip_first_underscore(k): v
            for k, v in self.__dict__.items()
            if not (skip_empty and v is None) and not k.startswith('__')
        }

    def with_(self, config: Config = None, **config_as_kwargs) -> Config:
        return self.__class__(
            **{
                **self.as_dict(),
                **(config.as_dict() if config else {}),
                **config_as_kwargs,
            }
        )

    @property
    def driver(self) -> Optional[WebDriver]:
        return self._driver

    @property
    def timeout(self) -> int:
        return self._timeout

    @property
    def hook_wait_failure(self) -> Callable[[TimeoutException], Exception]:
        return self._hook_wait_failure

    # def hook_wait_success(self)
    # def hook_wait(self)

    def wait(self, entity):
        return Wait(
            entity, at_most=self.timeout, or_fail_with=self.hook_wait_failure
        )

    @property
    def base_url(self) -> str:
        return self._base_url

    @property
    def set_value_by_js(self) -> bool:
        return self._set_value_by_js

    @property
    def type_by_js(self) -> bool:
        return self._type_by_js

    @property
    def wait_for_no_overlap_found_by_js(self) -> bool:
        return self._wait_for_no_overlap_found_by_js

    @property
    def window_width(self) -> Optional[int]:
        return self._window_width

    @property
    def window_height(self) -> Optional[int]:
        return self._window_height

    @property
    def log_outer_html_on_failure(self) -> bool:
        return self._log_outer_html_on_failure class Wait(Generic[E]):

    # todo: provide the smallest possible timeout default, something like 1ms
    def __init__(
        self,
        entity: E,
        at_most: int,
        or_fail_with: Optional[Callable[[TimeoutException], Exception]] = None,
    ):
        self._entity = entity
        self._timeout = at_most
        self._hook_failure = or_fail_with or identity

    def at_most(self, timeout: int) -> Wait[E]:
        return Wait(self._entity, timeout, self._hook_failure)

    def or_fail_with(
        self, hook_failure: Optional[Callable[[TimeoutException], Exception]]
    ) -> Wait[E]:

        return Wait(self._entity, self._timeout, hook_failure)

    @property
    def hook_failure(
        self,
    ) -> Optional[Callable[[TimeoutException], Exception]]:
        # todo: hook_failure or failure_hook?
        return self._hook_failure

    # todo: consider renaming to `def to(...)`, though will sound awkward when wait.to(condition)
    def for_(self, fn: Callable[[E], R]) -> R:
        finish_time = time.time() + self._timeout

        while True:
            try:
                return fn(self._entity)
            except Exception as reason:
                if time.time() > finish_time:

                    reason_message = str(reason)

                    reason_string = '{name}: {message}'.format(
                        name=reason.__class__.__name__, message=reason_message
                    )
                    # todo: think on how can we improve logging failures in selene, e.g. reverse msg and stacktrace
                    # stacktrace = getattr(reason, 'stacktrace', None)
                    timeout = self._timeout
                    entity = self._entity

                    failure = TimeoutException(
                        f'''

Timed out after {timeout}s, while waiting for:
{entity}.{fn}

Reason: {reason_string}'''
                    )

                    raise self._hook_failure(failure)

    def until(self, fn: Callable[[E], R]) -> bool:
        try:
            self.for_(fn)
            return True
        except TimeoutException:
            return False

    # todo: do we really need these aliases?
    def command(self, description: str, fn: Callable[[E], None]) -> None:
        self.for_(Command(description, fn))

    def query(self, description: str, fn: Callable[[E], R]) -> R:
        return self.for_(Query(description, fn)) class Command(Query[T, None]):
    pass class Query(Generic[T, R]):
    def __init__(self, description: str, fn: Callable[[T], R]):
        self._description = description
        self._fn = fn

    def __call__(self, entity: T) -> R:
        return self._fn(entity)

    def __str__(self):
        return self._description class Condition(Callable[[E], None]):
    @classmethod
    def by_and(cls, *conditions):
        def fn(entity):
            for condition in conditions:
                condition.call(entity)

        return cls(' and '.join(map(str, conditions)), fn)

    @classmethod
    def by_or(cls, *conditions):
        def fn(entity):
            errors: List[Exception] = []
            for condition in conditions:
                try:
                    condition.call(entity)
                    return
                except Exception as e:
                    errors.append(e)
            raise AssertionError('; '.join(map(str, errors)))

        return cls(' or '.join(map(str, conditions)), fn)

    @classmethod
    def as_not(
        cls, condition: Condition[E], description: str = None
    ) -> Condition[E]:
        # todo: how will it work composed conditions?
        condition_words = str(condition).split(' ')
        is_or_have = condition_words[0]
        name = ' '.join(condition_words[1:])
        no_or_not = 'not' if is_or_have == 'is' else 'no'
        new_description = description or f'{is_or_have} {no_or_not} {name}'

        def fn(entity):
            try:
                condition.call(entity)
            except Exception:
                return
            raise ConditionNotMatchedError()  # todo: try to handle printing actual values here too...

        return cls(new_description, fn)

    # function throwIfNot<E>(predicate: (entity: E) => Promise<boolean>): Lambda<E, void> {
    #     return async (entity: E) => {
    #         if (!await predicate(entity)) {
    #             throw new ConditionNotMatchedError();
    #         }
    #     };
    # }

    @classmethod
    def raise_if_not(
        cls, description: str, predicate: Predicate[E]
    ) -> Condition[E]:
        def fn(entity: E) -> None:
            if not predicate(entity):
                raise ConditionNotMatchedError()

        return cls(description, fn)

    @classmethod
    def raise_if_not_actual(
        cls, description: str, query: Lambda[E, R], predicate: Predicate[R]
    ) -> Condition[E]:
        def fn(entity: E) -> None:
            query_to_str = str(query)
            result = (
                query.__name__
                if query_to_str.startswith('<function')
                else query_to_str
            )
            actual = query(entity)
            if not predicate(actual):
                raise AssertionError(f'actual {result}: {actual}')

        return cls(description, fn)

    def __init__(self, description: str, fn: Lambda[E, None]):
        self._description = description
        self._fn = fn

    def call(self, entity: E) -> None:
        self._fn(entity)

    @property
    def predicate(self) -> Lambda[E, bool]:
        def fn(entity):
            try:
                self.call(entity)
                return True
            except Exception as e:
                return False

        return fn

    @property
    def not_(self) -> Condition[E]:  # todo: better name?
        return self.__class__.as_not(self)

    def __call__(self, *args, **kwargs):
        return self._fn(*args, **kwargs)

    def __str__(self):
        # todo: consider changing has to have on the fly for CollectionConditions
        # todo: or changing in collection locator rendering `all` to `collection`
        # todo: or changing in match.* names from collection_has_* to all_have_*
        return self._description

    def and_(self, condition: Condition[E]) -> Condition[E]:
        return Condition.by_and(self, condition)

    def or_(self, condition: Condition[E]) -> Condition[E]:
        return Condition.by_or(self, condition) @property
def not_(self) -> Condition[E]:  # todo: better name?
    return self.__class__.as_not(self) class Locator(Generic[T]):
    def __init__(self, description: str, locate: Callable[[], T]):
        self._description = description
        self._locate = locate

    def __call__(self) -> T:
        return self._locate()

    def __str__(self):
        return self._description def to_by(selector_or_by: Union[str, tuple]) -> Tuple[str, str]:
    # todo: will it work `if isinstance(css_selector_or_by, Tuple[str, str]):` ?
    if isinstance(selector_or_by, tuple):
        return selector_or_by
    if isinstance(selector_or_by, str):
        return (
            (By.XPATH, selector_or_by)
            if (
                selector_or_by.startswith('/')
                or selector_or_by.startswith('./')
                or selector_or_by.startswith('..')
                or selector_or_by.startswith('(')
            )
            else (By.CSS_SELECTOR, selector_or_by)
        )
    raise TypeError(
        'selector_or_by should be str with CSS selector or XPATH selector or Tuple[by:str, value:str]'
    ) def flatten(list_of_lists: List[list]) -> list:
    # todo: consider adding skip_none=False option
    return [
        item
        for sublist in list_of_lists
        for item in (sublist if isinstance(sublist, list) else [sublist])
    ] def is_absolute_url(relative_or_absolute_url: str) -> bool:
    url = relative_or_absolute_url.lower()
    return (
        url.startswith('http:')
        or url.startswith('https:')
        or url.startswith('file:')
        or url.startswith('about:')
        or url.startswith('data:')
    ) class TimeoutException(AssertionError):
    def __init__(self, msg=None):
        self.msg = msg

    def __str__(self):
        exception_msg = "Message: %s\n" % self.msg
        return exception_msg class _SeleneError(AssertionError):
    def __init__(self, message: Union[str, Callable[[], str]]):
        self._render_message: Callable[[], str] = (
            (lambda: message) if isinstance(message, str) else message
        )

    @property
    def args(self):
        return (self._render_message(),)

    def __str__(self):
        return self._render_message()

    def __repr__(self):
        return f"SeleneError: {self._render_message()}" class CollectionCondition(Condition[Collection]):
    pass class ElementCondition(Condition[Element]):
    pass class BrowserCondition(Condition[Browser]):
    pass
import warnings
from abc import abstractmethod, ABC
from typing import TypeVar, Union, List, Dict, Any, Callable, Tuple
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.remote.switch_to import SwitchTo
from selenium.webdriver.remote.webelement import WebElement
from selene.common.fp import pipe
from selene.core.configuration import Config
from selene.core.wait import Wait, Command, Query
from selene.core.condition import Condition, not_
from selene.core.locator import Locator
from selene.common.helpers import to_by, flatten, is_absolute_url
from selene.core.exceptions import TimeoutException, _SeleneError
            from selene.core import query
            from selene.core.match import element_is_present
        from selene.core import command
        from selene.core import command
        from selene.core import command
        from selene.core import query
        from selene.core import query
        from selene.core import query
        from selene.core import query
        from selene.core import query
        from selene.core import query
        from selene.core import query
        from selene.support.conditions import be
        from selene.support.conditions import be
        from selene.support.conditions import be
        from selene.core import query
        from selene.core import query
        from selene.core import query
        from selene.core import query
        from selene.core import query
        from selene.core import query
        from selene.core import query
        from selene.core import query
        from selene.core import query
            from selene.core import query
        from selene.core import query
        from selene.core import query
            from selene.core import query
from selene.core.conditions import (
    CollectionCondition,
    ElementCondition,
    BrowserCondition,
)
                .element('.result-url').click()

        OR with PageObject:

        THEN::

            Result(results.element_by_its(lambda it: Result(it).title, have.text(text)))\
                .url.click()

        Shortcut for::

            Result(results.element_by(lambda it: have.text(text)(Result(it).title)))\
                .url.click()

        WHERE::

            results = browser.all('.result')
            class Result:
                def __init__(self, element):
                    self.element = element
                    self.title = self.element.element('.result-title')
                    self.url = self.element.element('.result-url')
            # ...
        """
        # todo: main questions to answer before removing warning:
        #       - isn't it enough to allow Callable[[Element], bool] as condition?
        #           browser.all('.result').element_by(
        #               lambda it: it.element('.result-title').matching(have.text('browser tests in Python')))
        #               .element('.result-url').click()
        #       - how to improve error messages in case we pass lambda (not a fun with good name/str repr)?
        #       - what about accepting collection condition? should we allow it?
        warnings.warn(
            'element_by_its is experimental; might be renamed or removed in future',
            FutureWarning,
        )

        def find_in(parent: Element):
            if callable(selector_or_callable):
                return selector_or_callable(parent)
            else:
                return parent.element(selector_or_callable)

        return self.element_by(lambda it: condition(find_in(it)))

    # todo: consider adding ss alias
    def all(self, css_or_xpath_or_by: Union[str, tuple]) -> Collection:
        warnings.warn(
            'might be renamed or deprecated in future; '
            'all is actually a shortcut for collected(lambda element: element.all(selector)...'
            'but we also have all_first and...'
            'it is yet unclear what name would be best for all_first as addition to all... '
            'all_first might confuse with all(...).first... I mean: '
            'all_first(selector) is actually '
            'collected(lambda e: e.element(selector)) '
            'but it is not the same as '
            'all(selector).first '
            'that is collected(lambda e: e.all(selector)).first ... o_O ',
            FutureWarning,
        )
        by = to_by(css_or_xpath_or_by)

        # todo: consider implement it through calling self.collected
        #       because actually the impl is self.collected(lambda element: element.all(selector))

        return Collection(
            Locator(
                f'{self}.all({by})',
                lambda: flatten(
                    [webelement.find_elements(*by) for webelement in self()]
                ),
            ),
            self.config,
        )

    # todo: consider adding s alias
    def all_first(self, css_or_xpath_or_by: Union[str, tuple]) -> Collection:
        warnings.warn(
            'might be renamed or deprecated in future; '
            'it is yet unclear what name would be best... '
            'all_first might confuse with all(...).first... I mean: '
            'all_first(selector) is actually '
            'collected(lambda e: e.element(selector)) '
            'but it is not the same as '
            'all(selector).first '
            'that is collected(lambda e: e.all(selector)).first ... o_O ',
            FutureWarning,
        )
        by = to_by(css_or_xpath_or_by)

        # todo: consider implement it through calling self.collected
        #       because actually the impl is self.collected(lambda element: element.element(selector))

        return Collection(
            Locator(
                f'{self}.all_first({by})',
                lambda: [
                    webelement.find_element(*by) for webelement in self()
                ],
            ),
            self.config,
        )

    def collected(
        self, finder: Callable[[Element], Union[Element, Collection]]
    ) -> Collection:
        # todo: consider adding predefined queries to be able to write
        #         collected(query.element(selector))
        #       over
        #         collected(lambda element: element.element(selector))
        #       and
        #         collected(query.all(selector))
        #       over
        #         collected(lambda element: element.all(selector))
        #       consider also putting such element builders like to find.* module instead of query.* module
        #       because they are not supposed to be used in entity.get(*) context defined for other query.* fns

        return Collection(
            Locator(
                f'{self}.collected({finder})',
                # todo: consider skipping None while flattening

                lambda: flatten(