diff --git a/CHANGELOG.md b/CHANGELOG.md index 37975e7..80f1123 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. diff --git a/src/suou/__init__.py b/src/suou/__init__.py index 3c0efd3..9f80088 100644 --- a/src/suou/__init__.py +++ b/src/suou/__init__.py @@ -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', diff --git a/src/suou/exceptions.py b/src/suou/exceptions.py index 7952bc7..74ea7ec 100644 --- a/src/suou/exceptions.py +++ b/src/suou/exceptions.py @@ -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' ) \ No newline at end of file diff --git a/src/suou/lex.py b/src/suou/lex.py index 15791c3..5655eea 100644 --- a/src/suou/lex.py +++ b/src/suou/lex.py @@ -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 diff --git a/src/suou/luck.py b/src/suou/luck.py new file mode 100644 index 0000000..c22b552 --- /dev/null +++ b/src/suou/luck.py @@ -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 + + + + \ No newline at end of file