"""
Module for operations wrappers,
operations that control behavior of the wrapped operation.
Classes
-------
- `SkipNone` - Skips the operation if the input is None.
- `Suppress` - Suppresses exceptions raised by the operation.
"""
from __future__ import annotations
from typing import Any, Type, Union
import soupsavvy.exceptions as exc
from soupsavvy.base import BaseOperation, OperationSearcherMixin, check_operation
# allowed exception types to suppress
ExceptionCategory = Union[Type[Exception], tuple[Type[Exception], ...]]
[docs]
class OperationWrapper(OperationSearcherMixin):
"""
A wrapper class for operations that extends the base operation functionality.
Acts as a higher order operation, which controls behavior of the wrapped operation.
"""
[docs]
def __init__(self, operation: BaseOperation) -> None:
"""
Initialize Operation Wrapper.
Parameters
----------
operation : BaseOperation
The operation to be wrapped.
Raises
------
NotOperationException
If provided object is not an instance of `BaseOperation`.
"""
self.operation = check_operation(operation)
def __eq__(self, x: Any) -> bool:
"""
Check if two `OperationWrapper` instances are equal.
They need to be of the same class and wrap the same operation.
Parameters
----------
x : Any
The object to compare with.
Returns
-------
bool
True if the instances are equal, False otherwise.
"""
if not isinstance(x, self.__class__):
return NotImplemented
return self.operation == x.operation
def __str__(self) -> str:
return f"{self.__class__.__name__}({self.operation})"
def __repr__(self) -> str:
return str(self)
[docs]
class SkipNone(OperationWrapper):
"""
A wrapper that skips the operation if the input is None.
Used to prevent exceptions where it's safe and expected to skip the operation.
Example
-------
>>> from soupsavvy.operations import Text
... from soupsavvy.models import SkipNone
... operation = SkipNone(Text())
... operation.execute(None)
None
When element was not found, which can be expected, skips operation
and returns None.
"""
def _execute(self, arg: Any) -> Any:
if arg is None:
return None
return self.operation.execute(arg)
[docs]
class Suppress(OperationWrapper):
"""
A wrapper that executes the operation and suppresses exceptions raised,
returning None instead. Used to catch exceptions where it's expected
this might happen.
Example
-------
>>> from soupsavvy.operations import Operation
... from soupsavvy.models import Suppress
... operation = Suppress(Operation(int))
... operation.execute("")
None
This can be used with `Default` operation to provide a default value
when the operation fails.
Example
-------
>>> from soupsavvy.operations import Operation
... from soupsavvy.models import Suppress
... operation = Default(Suppress(Operation(int)), 0)
... operation.execute("")
None
Operations in example can be used to try to convert string to integer
from text of element, that can potentially be empty.
In such case, if it's not required, default can be set to None or known value.
Suppress also accepts category of exceptions to suppress, by default it suppresses
all exceptions that inherit from Exception.
Example
-------
>>> from soupsavvy.operations import Operation
... from soupsavvy.models import Suppress
... operation = Suppress(Operation(int), category=ValueError)
... operation.execute("not an integer")
None
Category can be a tuple of exceptions as well, `issubclass` is used to check
if the cause of exception is subclass of provided category.
Example
-------
>>> from soupsavvy.operations import Operation
... from soupsavvy.models import Suppress
... operation = Suppress(Operation(int), category=(AttributeError, ValueError))
... operation.execute("not an integer")
FailedOperationExecution
"""
[docs]
def __init__(
self,
operation: BaseOperation,
category: ExceptionCategory = Exception,
) -> None:
"""
Initialize `Suppress` operation instance.
Parameters
----------
operation : BaseOperation
The operation to be wrapped.
category : Type[Exception] | tuple[Type[Exception], ...], optional
The exception type(s) to suppress. By default, suppresses all exceptions
that inherit from `Exception`.
Raises
------
NotOperationException
If provided object is not an instance of `BaseOperation`.
"""
super().__init__(operation)
self._category = category
def _execute(self, arg: Any) -> Any:
try:
return self.operation.execute(arg)
except exc.FailedOperationExecution as e:
cause = e.__cause__.__class__
if not issubclass(cause, self._category):
raise
return None