Skip to content

Commit

Permalink
FIX: Condition.as_not + ConditionMismatch._to_raise* to work on failures
Browse files Browse the repository at this point in the history
TODOs left (even from previous commit):
- finalize testing error messages in present & co conditions tests
  • Loading branch information
yashaka committed Jun 9, 2024
1 parent 3104191 commit 180469e
Show file tree
Hide file tree
Showing 14 changed files with 494 additions and 105 deletions.
2 changes: 1 addition & 1 deletion .run/lint_pylint.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/bin/bash

touch __init__.py
pylint $(pwd) --disable="$(cat .pylint-disabled-rules)" --ignore-patterns=.venv
pylint $(pwd) --disable="$(cat .pylint-disabled-rules)" --ignore-patterns=.venv -r n
rm __init__.py
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,18 +110,24 @@ TODOs:

### TODO: document subclass based custom conditions

## 2.0.0rc10: «copy&paste, frames, shadow & texts_like» (to be released on DD.05.2024)
### TODO: should we help users do not shoot their legs when using browser.all(selector) in for loops? #534

### TODO: not_ as callable object?

### TODO: should we help users do not shoot their legs when using browser.all(selector) in for loops? #534
## 2.0.0rc10: «copy&paste, frames, shadow & texts_like» (to be released on DD.05.2024)

### TODO: finalize pylint update to 3.2.2. (clean all warnings, especially for pyproject.toml)

### TODO: rename all conditions inside match.py so match can be fully used instead be + have #530

### TODO: ENSURE ALL Condition.as_not USAGES ARE NOW CORRECT

...

#### TODO: finalize error messages tests for present, visible, hidden

#### TODO: decide on present vs present_in_dom (same for absent)

### TODO: ENSURE composed conditions work as expected (or/and, etc.)

...
Expand Down
40 changes: 31 additions & 9 deletions selene/common/_typing_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,18 @@
# SOFTWARE.
from __future__ import annotations

import functools
import inspect
import re

from typing_extensions import TypeVar, Callable, Generic, Optional, overload
from typing_extensions import (
TypeVar,
Callable,
Generic,
Optional,
overload,
Type,
Iterable,
)

from selene.common.fp import thread_last

Expand Down Expand Up @@ -108,26 +115,41 @@ def full_description_for(callable_: Optional[Callable]) -> str | None:

@staticmethod
@overload
def _inverted(predicate: Predicate[E]) -> Predicate[E]: ...
def _inverted(
predicate: Predicate[E],
_truthy_exceptions: Iterable[Type[Exception]] = (),
) -> Predicate[E]: ...

@staticmethod
@overload
def _inverted(predicate: Query[E, bool]) -> Query[E, bool]: ...
def _inverted(
predicate: Query[E, bool],
_truthy_exceptions: Iterable[Type[Exception]] = (),
) -> Query[E, bool]: ...

@staticmethod
def _inverted(
predicate: Predicate[E] | Query[E, bool]
predicate: Predicate[E] | Query[E, bool],
_truthy_exceptions: Iterable[Type[Exception]] = (),
) -> Predicate[E] | Query[E, bool]:
# TODO: ensure it works correctly:) e.g. unit test it

def not_predicate(entity: E) -> bool:
try:
return not predicate(entity)
except Exception as reason:
if any(
isinstance(reason, exception) for exception in _truthy_exceptions
):
return True
raise reason

if isinstance(predicate, Query):
return Query(
f'not {predicate}',
lambda entity: not predicate(entity),
not_predicate,
)

def not_predicate(entity: E) -> bool:
return not predicate(entity)

not_predicate.__module__ = predicate.__module__
not_predicate.__annotations__ = predicate.__annotations__

Expand Down
96 changes: 77 additions & 19 deletions selene/core/condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,10 +431,12 @@ def is_more_than(limit):
"""
from __future__ import annotations

import functools
import sys
import typing
import warnings

from selenium.common import WebDriverException
from typing_extensions import (
List,
TypeVar,
Expand Down Expand Up @@ -726,6 +728,11 @@ def as_not( # TODO: ENSURE ALL USAGES ARE NOW CORRECT
) -> Condition[E]:
# TODO: how will it work composed conditions?

# TODO: should we bother? – about "negated inversion via Condition.as_not"
# will "swallow" the reason of failure...
# because we invert the predicate or test itself, ignoring exceptions
# so then when we "recover original exception failure" on negation
# we can just recover it to "false" not to "raise reason error"
if description:
return (
cls(
Expand All @@ -736,11 +743,23 @@ def as_not( # TODO: ENSURE ALL USAGES ARE NOW CORRECT
# thus, no need to mark condition for further inversion:
_inverted=False,
)
if not condition.__by
if condition.__by is None
else cls(
description,
actual=condition.__actual,
by=Query._inverted(condition.__by),
# # We have to skip the actual here (re-building it into by below),
# # because can't "truthify" its Exceptions when raised on inverted
# # TODO: or can we?
# actual=condition.__actual,
by=Query._inverted(
functools.wraps(condition.__by)(
lambda entity: condition.__by( # type: ignore
condition.__actual(entity)
if condition.__actual
else entity
)
),
_truthy_exceptions=(AssertionError, WebDriverException),
),
_inverted=False,
)
)
Expand Down Expand Up @@ -826,9 +845,15 @@ def __init__(
ConditionMismatch._to_raise_if_actual(
self.__actual,
self.__by,
# TODO: should we DI? – remove this tight coupling to WebDriverException?
# here and elsewhere
_falsy_exceptions=(AssertionError, WebDriverException),
)
if self.__actual
else ConditionMismatch._to_raise_if(self.__by)
else ConditionMismatch._to_raise_if(
self.__by,
_falsy_exceptions=(AssertionError, WebDriverException),
)
)
return

Expand All @@ -854,7 +879,7 @@ def as_inverted(entity: E) -> None:
return

raise ValueError(
'either test or by with optional actual should be provided, ' 'not nothing'
'either test or by with optional actual should be provided, not nothing'
)

# TODO: rethink not_ naming...
Expand Down Expand Up @@ -913,6 +938,8 @@ def __describe(self) -> str:
def __describe_inverted(self) -> str:
condition_words = self.__describe().split(' ')
is_or_have = condition_words[0]
if is_or_have not in ('is', 'has', 'have'):
return f'not ({self.__describe()})'
name = ' '.join(condition_words[1:])
no_or_not = 'not' if is_or_have == 'is' else 'no'
return f'{is_or_have} {no_or_not} ({name})'
Expand Down Expand Up @@ -1312,24 +1339,57 @@ def __init__x(self, *args, **kwargs):
'or custom __str__ implementation '
'(like lambda wrapped in Query object)'
)
description = (
(str(actual_desc) + ' ')
if (actual_desc := Query.full_description_for(actual)) is not None
else ''
) + str(
actual_desc = Query.full_description_for(actual)
description = ((str(actual_desc) + ' ') if actual_desc else '') + str(
by_description
) # noqa
super().__init__(description, actual=actual, by=by)
return

raise ValueError('invalid arguments to Match initializer')

@overload
def __init__(
self,
description: str | Callable[[], str],
actual: Lambda[E, R],
*,
by: Predicate[R],
_inverted=False,
): ...

@overload
def __init__(
self,
description: str | Callable[[], str],
*,
by: Predicate[E],
_inverted=False,
): ...

@overload
def __init__(
self,
*,
actual: Lambda[E, R],
by: Predicate[R],
_inverted=False,
): ...

@overload
def __init__(
self,
*,
by: Predicate[E],
_inverted=False,
): ...

def __init__(
self,
description: str | Callable[[], str] | None = None,
actual: Lambda[E, R] | None = None,
*,
by: Predicate[R],
by: Predicate[E] | Predicate[R],
_inverted=False,
):
"""
Expand All @@ -1345,20 +1405,18 @@ def __init__(
"""
if not description and not (by_description := Query.full_description_for(by)):
raise ValueError(
'either provide description or ensure that at least by predicate'
'has __qualname__ (defined as regular named function)'
'either provide description or ensure that at least by predicate '
'has __qualname__ (defined as regular named function) '
'or custom __str__ implementation '
'(like lambda wrapped in Query object)'
)
actual_desc = Query.full_description_for(actual)
description = description or (
(
(str(actual_desc) + ' ')
if (actual_desc := Query.full_description_for(actual)) is not None
else ''
)
((str(actual_desc) + ' ') if actual_desc else '')
+ str(by_description) # noqa
)
super().__init__(
# TODO: fix "cannot infer type of argument 1 of __init__" or ignore
super().__init__( # type: ignore
description=description,
actual=actual,
by=by,
Expand Down
Loading

0 comments on commit 180469e

Please sign in to comment.