diff --git a/.gitignore b/.gitignore index 481d9da..96fd286 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,6 @@ docs/_build docs/_static docs/templates .coverage -.pytest_cache/ # changes during CD/CI aliases/*/pyproject.toml diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cccb6b..acd67c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,5 @@ # Changelog -## 0.14.0 - -* Added `ast` module -* Deprecate `dei_args()` for problems with the typing system. The function is not going away tho -* Module `sqlalchemy`: added `email_column()` - -## 0.13.1 and 0.12.7 - -+ Typing fixes - ## 0.13.0 "Laconic Letters" + Added module `argparse` with class `LetterSubparsers()`, which allows pacman-style args by preprocessing them diff --git a/src/suou/__init__.py b/src/suou/__init__.py index d7d1720..200f22b 100644 --- a/src/suou/__init__.py +++ b/src/suou/__init__.py @@ -41,7 +41,7 @@ from .color import OKLabColor, chalk, WebColor, RGBColor, LinearRGBColor, \ from .mat import Matrix from .argparse import LetterSubparsers -__version__ = "0.14.0a1" +__version__ = "0.13.0" __all__ = ( 'ColorFormatter', diff --git a/src/suou/ast.py b/src/suou/ast.py deleted file mode 100644 index c6b744d..0000000 --- a/src/suou/ast.py +++ /dev/null @@ -1,116 +0,0 @@ -""" -Experimental AST module - ---- - -Copyright (c) 2026 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 __future__ import annotations -from ast import TypeVar -from typing import Any, Collection - -_T = TypeVar('_T') - -class Node: - __slots__ = ('_children', ) - - _count: int - _children: list[Node] - _types: Collection[type] - - def __repr__(self): - return f"{self.__class__.__name__}({', '.join(repr(x) for x in self._children)})" - - def __init__(self, *args): - if self._count != len(args): - raise TypeError(f'{self.__class__.__name__} must be instanced with {self._count} arguments, got {len(args)}') - - for i, (a, t) in enumerate(zip(args, self._types)): - if t != Any and not isinstance(a, t): - raise TypeError(f"argument {i} must be of type {t.__name__!r}, got {a.__class__.__name__!r}") - - self._children = list(args) - - def eval(self, ctx: dict): - raise TypeError(f'{self.__class__.__name__} cannot be eval()ed') - - def __getitem__(self, key: int): - if key >= self._count: - raise IndexError('child node index out of range') - return self._children[key] - - def __len__(self): - return self._count - - -class ZeroOp(Node): - _count = 0 - _types = () - - -class UnaryOp(Node): - _count = 1 - _types = (Any,) - - -class BinaryOp(Node): - _count = 2 - _types = (Any, Any) - - -class TernaryOp(Node): - _count = 3 - _types = (Any, Any, Any) - - -class MultiOp(Node): - _count = 1 - _types = (list,) - - def __getitem__(self, key: int): - return self._children[0][key] - - def __iter__(self): - return iter(self._children[0]) - - def __len__(self): - return len(self._children[0]) - - - -class Literal(UnaryOp): - """A literal evals to the enclosed value.""" - def eval(self, ctx: dict): - return self[0] - - -## SUOU provides some ready-made literals, for the sake of ease-of-use. - -class IntLiteral(Literal): - _types = (int,) - -class FloatLiteral(Literal): - """ - WARNING: may be subject to loss of precision. - """ - _types = (float,) - -class StringLiteral(Literal): - _types = (str,) - - -# This module is experimental and therefore not re-exported into __init__ -__all__ = ( - 'Node', 'ZeroOp', 'UnaryOp', 'BinaryOp', 'TernaryOp', - 'MultiOp', 'Literal', 'IntLiteral', 'FloatLiteral', - 'StringLiteral' -) \ No newline at end of file diff --git a/src/suou/dei.py b/src/suou/dei.py index a49a398..8d0f326 100644 --- a/src/suou/dei.py +++ b/src/suou/dei.py @@ -21,8 +21,6 @@ from __future__ import annotations from functools import wraps from typing import Callable, Collection, TypeVar, Any -from . import deprecated - _T = TypeVar('_T') _U = TypeVar('_U') @@ -117,7 +115,6 @@ class Pronoun(int): -@deprecated('breaks the typing system somewhat; won\'t be removed tho') def dei_args(**renames: dict[str, str]): """ Allow for aliases in the keyword argument names, in form alias='real_name'. diff --git a/src/suou/itertools.py b/src/suou/itertools.py index 9ec1e68..881e30a 100644 --- a/src/suou/itertools.py +++ b/src/suou/itertools.py @@ -22,23 +22,7 @@ from suou.classtools import MISSING _T = TypeVar('_T') -def _makelist_callable(l: Callable) -> Callable[..., list]: - @wraps(l) - def wrapper(*a, **k): - return _makelist_nowrap(l(*a, **k)) - return wrapper - -def _makelist_nowrap(l: Any) -> list: - if isinstance(l, (str, bytes, bytearray)): - return [l] - elif isinstance(l, Iterable): - return list(l) - elif l in (None, NotImplemented, Ellipsis, MISSING): - return [] - else: - return [l] - -def makelist(l: Any, wrap: bool = True) -> list | Callable[..., list]: +def makelist(l: Any, wrap: bool = True) -> list | Callable[Any, list]: ''' Make a list out of an iterable or a single value. @@ -48,11 +32,17 @@ def makelist(l: Any, wrap: bool = True) -> list | Callable[..., list]: *Changed in 0.11.0*: ``wrap`` argument is now no more keyword only. ''' if callable(l) and wrap: - return _makelist_callable(l) + return wraps(l)(lambda *a, **k: makelist(l(*a, **k), wrap=False)) + if isinstance(l, (str, bytes, bytearray)): + return [l] + elif isinstance(l, Iterable): + return list(l) + elif l in (None, NotImplemented, Ellipsis, MISSING): + return [] else: - return _makelist_nowrap(l) + return [l] -def ltuple(seq: Iterable[_T], size: int, /, pad = None) -> tuple[_T, ...]: +def ltuple(seq: Iterable[_T], size: int, /, pad = None) -> tuple: """ Truncate an iterable into a fixed size tuple, if necessary padding it. """ @@ -61,7 +51,7 @@ def ltuple(seq: Iterable[_T], size: int, /, pad = None) -> tuple[_T, ...]: seq = seq + (pad,) * (size - len(seq)) return seq -def rtuple(seq: Iterable[_T], size: int, /, pad = None) -> tuple[_T, ...]: +def rtuple(seq: Iterable[_T], size: int, /, pad = None) -> tuple: """ Same as rtuple() but the padding and truncation is made right to left. """ @@ -91,7 +81,7 @@ def kwargs_prefix(it: dict[str, Any], prefix: str, *, remove = True, keep_prefix it.pop(k) return ka -def additem(obj: MutableMapping, /, name: str | None = None): +def additem(obj: MutableMapping, /, name: str = None): """ Syntax sugar for adding a function to a mapping, immediately. """ @@ -103,7 +93,7 @@ def additem(obj: MutableMapping, /, name: str | None = None): return func return decorator -def addattr(obj: Any, /, name: str | None = None): +def addattr(obj: Any, /, name: str = None): """ Same as additem() but setting as attribute instead. """ diff --git a/src/suou/lex.py b/src/suou/lex.py index 6ad77a5..5655eea 100644 --- a/src/suou/lex.py +++ b/src/suou/lex.py @@ -62,6 +62,8 @@ def symbol_table(*args: Iterable[tuple | TokenSym], whitespace: str | None = Non yield TokenSym('[' + re.escape(whitespace) + ']+', '', discard=True) +symbol_table: Callable[..., list] + def ilex(text: str, table: Iterable[TokenSym], *, whitespace = False): """ Return a text as a list of tokens, given a token table (iterable of TokenSym). diff --git a/src/suou/luck.py b/src/suou/luck.py index 4c808fa..c4ec49e 100644 --- a/src/suou/luck.py +++ b/src/suou/luck.py @@ -35,7 +35,7 @@ def lucky(validators: Iterable[Callable[[_U], bool]] = ()): *New in 0.7.0* """ - def decorator(func: Callable[..., _U]) -> Callable[..., _U]: + def decorator(func: Callable[_T, _U]) -> Callable[_T, _U]: @wraps(func) def wrapper(*args, **kwargs) -> _U: try: diff --git a/src/suou/markdown.py b/src/suou/markdown.py index 09cac11..acd4ab5 100644 --- a/src/suou/markdown.py +++ b/src/suou/markdown.py @@ -86,5 +86,4 @@ class PingExtension(markdown.extensions.Extension): md.inlinePatterns.register(MentionPattern(re.escape(at) + r'(' + self.CHARACTERS + ')', url_prefix), 'ping_mention', 14) -# Optional dependency: do not import into __init__.py __all__ = ('PingExtension', 'SpoilerExtension', 'StrikethroughExtension') diff --git a/src/suou/signing.py b/src/suou/signing.py index 55a950d..c2011ce 100644 --- a/src/suou/signing.py +++ b/src/suou/signing.py @@ -22,8 +22,11 @@ from itsdangerous import TimestampSigner from itsdangerous import Signer as _Signer from itsdangerous.encoding import int_to_bytes as _int_to_bytes -from .itertools import rtuple -from .codecs import jsondecode, jsonencode, want_bytes, want_str, b64decode, b64encode +from suou.dei import dei_args +from suou.itertools import rtuple + +from .functools import not_implemented +from .codecs import jsondecode, jsonencode, rb64decode, want_bytes, want_str, b64decode, b64encode from .iding import Siq from .classtools import MISSING @@ -32,6 +35,7 @@ class UserSigner(TimestampSigner): itsdangerous.TimestampSigner() instanced from a user ID, with token generation and validation capabilities. """ user_id: int + @dei_args(primary_secret='master_secret') def __init__(self, master_secret: bytes, user_id: int, user_secret: bytes, **kwargs): super().__init__(master_secret + user_secret, salt=Siq(user_id).to_bytes(), **kwargs) self.user_id = user_id diff --git a/src/suou/sqlalchemy/asyncio.py b/src/suou/sqlalchemy/asyncio.py index 4061c34..b28f305 100644 --- a/src/suou/sqlalchemy/asyncio.py +++ b/src/suou/sqlalchemy/asyncio.py @@ -22,8 +22,8 @@ from functools import wraps from contextvars import ContextVar, Token from typing import Callable, TypeVar -from sqlalchemy import Select, Table, select -from sqlalchemy.orm import DeclarativeBase +from sqlalchemy import Select, Table, func, select +from sqlalchemy.orm import DeclarativeBase, lazyload from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine try: @@ -31,7 +31,8 @@ try: except ImportError: AsyncSelectPagination = None -from ..exceptions import NotFoundError +from suou.exceptions import NotFoundError +from suou.glue import glue _T = TypeVar('_T') _U = TypeVar('_U') @@ -125,11 +126,9 @@ class SQLAlchemy: self.engine, checkfirst=checkfirst ) - +# XXX NOT public API! DO NOT USE current_session: ContextVar[AsyncSession] = ContextVar('current_session') -""" -XXX NOT public API! DO NOT USE -""" + def async_query(db: SQLAlchemy, multi: False): """ diff --git a/src/suou/sqlalchemy/orm.py b/src/suou/sqlalchemy/orm.py index bfd6be1..ba022e7 100644 --- a/src/suou/sqlalchemy/orm.py +++ b/src/suou/sqlalchemy/orm.py @@ -18,6 +18,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +from binascii import Incomplete import os import re from typing import Any, Callable, TypeVar @@ -26,8 +27,10 @@ from sqlalchemy import BigInteger, Boolean, CheckConstraint, Column, Date, Forei from sqlalchemy.orm import DeclarativeBase, InstrumentedAttribute, Relationship, declarative_base as _declarative_base, relationship from sqlalchemy.types import TypeEngine from sqlalchemy.ext.hybrid import Comparator +from suou.functools import future from suou.classtools import Wanted, Incomplete from suou.codecs import StringCase +from suou.dei import dei_args from suou.iding import Siq, SiqCache, SiqGen, SiqType from suou.itertools import kwargs_prefix from suou.snowflake import SnowflakeGen @@ -128,23 +131,6 @@ def username_column( return match_column(length, regex, case=case, nullable=nullable, unique=True, *args, **kwargs) -EMAIL_RE_USERNAME = r"[a-z0-9-]+(\.[a-z0-9-]+)*" -EMAIL_RE_DOMAIN = r"([a-z0-9-]+\.)+[a-z0-9-]{2,15}" -EMAIL_RE_YESALIASES = EMAIL_RE_USERNAME + r"(\+" + EMAIL_RE_USERNAME + ")?@" + EMAIL_RE_DOMAIN -EMAIL_RE_NOALIASES = EMAIL_RE_USERNAME + r"@" + EMAIL_RE_DOMAIN - -def email_column( - length: int = 256, *args, allow_aliases: bool = True, nullable: bool = False, unique: bool = True, **kwargs -): - """ - Construct a column containing a email address. - - *New in 0.14.0* - """ - return match_column(length, EMAIL_RE_YESALIASES if allow_aliases else EMAIL_RE_NOALIASES, case = StringCase.FORCE_LOWER, - unique = unique, nullable = nullable, *args, **kwargs) - - def bool_column(value: bool = False, nullable: bool = False, **kwargs) -> Column[bool]: """ Column for a single boolean value. @@ -155,6 +141,7 @@ def bool_column(value: bool = False, nullable: bool = False, **kwargs) -> Column return Column(Boolean, server_default=def_val, nullable=nullable, **kwargs) +@dei_args(primary_secret='master_secret') def declarative_base(domain_name: str, master_secret: bytes, metadata: dict | None = None, **kwargs) -> type[DeclarativeBase]: """ Drop-in replacement for sqlalchemy.orm.declarative_base() diff --git a/src/suou/validators.py b/src/suou/validators.py index 72daf57..349172a 100644 --- a/src/suou/validators.py +++ b/src/suou/validators.py @@ -74,7 +74,7 @@ def yesno(x: str | int | bool | None) -> bool: if isinstance(x, str): return x.lower() not in ('', '0', 'off', 'n', 'no', 'false', 'f') return True - + __all__ = ('matches', 'must_be', 'not_greater_than', 'not_less_than', 'yesno') diff --git a/src/suou/waiter.py b/src/suou/waiter.py index 7c55999..9a5e3bd 100644 --- a/src/suou/waiter.py +++ b/src/suou/waiter.py @@ -28,7 +28,7 @@ from suou.functools import future @future() class Waiter(): - _cached_app: Starlette | None = None + _cached_app: Callable | None = None def __init__(self): self.routes: list[Route] = [] @@ -60,7 +60,7 @@ class Waiter(): def patch(self, endpoint: str, *a, **k): return self._route('PATCH', endpoint, *a, **k) - def _route(self, methods: str | list[str], endpoint: str, **kwargs): + def _route(self, methods: list[str], endpoint: str, **kwargs): def decorator(func): self.routes.append(Route(endpoint, func, methods=makelist(methods, False), **kwargs)) return func