0.7.x: @lucky, @rng_overload and more exceptions

This commit is contained in:
Yusur 2025-09-19 13:34:51 +02:00
parent 3de5a3629d
commit a2fdc9166f
5 changed files with 169 additions and 2 deletions

View file

@ -1,5 +1,10 @@
# Changelog
## 0.7.0 "The Lucky Update"
+ Add RNG/random selection overloads such as `luck()`, `rng_overload()`.
+ Add 7 new throwable exceptions.
## 0.6.1
- First release on PyPI under the name `suou`.

View file

@ -36,7 +36,7 @@ from .validators import matches
from .redact import redact_url_password
from .http import WantsContentType
__version__ = "0.6.1"
__version__ = "0.7.0-dev37"
__all__ = (
'ConfigOptions', 'ConfigParserConfigSource', 'ConfigSource', 'ConfigValue',

View file

@ -1,5 +1,5 @@
"""
Exceptions and throwables for various purposes
Exceptions and throwables for all purposes!
---
@ -14,6 +14,17 @@ This software is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
class PoliticalError(Exception):
"""
Base class for anything that is refused to be executed for political reasons.
"""
class PoliticalWarning(PoliticalError, Warning):
"""
Base class for politically suspicious behaviors.
"""
class MissingConfigError(LookupError):
"""
Config variable not found.
@ -53,6 +64,35 @@ class BabelTowerError(NotFoundError):
The user requested a language that cannot be understood.
"""
class BadLuckError(Exception):
"""
Stuff did not go as expected.
Raised by @lucky decorator.
"""
class TerminalRequiredError(OSError):
"""
Raised by terminal_required() decorator when a function is called from a non-interactive environment.
"""
class BrokenStringsError(OSError):
"""
Issues related to audio happened, i.e. appropriate executables/libraries/drivers are not installed.
"""
class Fahrenheit451Error(PoliticalError):
"""
Base class for thought crimes related to arts (e.g. writing, visual arts, music)
"""
class FuckAroundFindOutError(PoliticalError):
"""
Raised when there is no actual grounds to raise an exception, but you did something in the past to deserve this outcome.
Ideal for permanent service bans or something.
"""
__all__ = (
'MissingConfigError', 'MissingConfigWarning', 'LexError', 'InconsistencyError', 'NotFoundError'
)

View file

@ -2,6 +2,16 @@
Utilities for tokenization of text.
---
Copyright (c) 2025 Sakuragasaki46.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
See LICENSE for the specific language governing permissions and
limitations under the License.
This software is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
from re import Match

112
src/suou/luck.py Normal file
View file

@ -0,0 +1,112 @@
"""
Fortune and esoterism helpers.
---
Copyright (c) 2025 Sakuragasaki46.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
See LICENSE for the specific language governing permissions and
limitations under the License.
This software is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
from typing import Callable, Generic, Iterable, TypeVar
import random
from suou.exceptions import BadLuckError
_T = TypeVar('_T')
_U = TypeVar('_U')
def lucky(validators: Iterable[Callable[[_U], bool]] = ()):
"""
Add one or more constraint on a function's return value.
Each validator must return a boolean. If false, the result is considered
unlucky and BadLuckError() is raised.
UNTESTED
NEW 0.7.0
"""
def decorator(func: Callable[_T, _U]):
@wraps(func)
def wrapper(*args, **kwargs) -> _U:
try:
result = func(*args, **kwargs)
except Exception as e:
raise BadLuckError(f'exception happened: {e}') from e
for v in validators:
try:
if not v(result):
raise BadLuckError(f'result not expected: {result!r}')
except BadLuckError:
raise
except Exception as e:
raise BadLuckError(f'cannot validate: {e}') from e
return result
return wrapper
return decorator
class RngCallable(Callable, Generic[_T, _U]):
"""
Overloaded ... randomly chosen callable.
UNTESTED
NEW 0.7.0
"""
def __init__(self, /, func: Callable[_T, _U] | None = None, weight: int = 1):
self._callables = []
self._max_weight = 0
if callable(func):
self.add_callable(func, weight)
def add_callable(self, func: Callable[_T, _U], weight: int = 1):
"""
"""
weight = int(weight)
if weight <= 0:
return
self._callables.append((func, weight))
self._max_weight += weight
def __call__(self, *a, **ka) -> _U:
choice = random.randrange(self._max_weight)
for w, c in self._callables:
if choice < w:
return c(*a, **ka)
elif choice < 0:
raise RuntimeError('inconsistent state')
else:
choice -= w
def rng_overload(prev_func: RngCallable[_T, _U] | int | None, /, *, weight: int = 1) -> RngCallable[_T, _U]:
"""
Decorate the first function with @rng_overload and the weight= parameter
(default 1, must be an integer) to create a "RNG" overloaded callable.
Each call chooses randomly one candidate (weight is taken in consideration)
, calls it, and returns the result.
UNTESTED
NEW 0.7.0
"""
if isinstance(prev_func, int) and weight == 1:
weight, prev_func = prev_func, None
def decorator(func: Callable[_T, _U]):
nonlocal prev_func
if prev_func is None:
prev_func = RngCallable(func, weight=weight)
else:
prev_func.add_callable(func, weight=weight)
return prev_func
return decorator