Compare commits

..

No commits in common. "master" and "v0.13.0" have entirely different histories.

14 changed files with 36 additions and 185 deletions

1
.gitignore vendored
View file

@ -30,7 +30,6 @@ docs/_build
docs/_static
docs/templates
.coverage
.pytest_cache/
# changes during CD/CI
aliases/*/pyproject.toml

View file

@ -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

View file

@ -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',

View file

@ -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'
)

View file

@ -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'.

View file

@ -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.
"""

View file

@ -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).

View file

@ -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:

View file

@ -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')

View file

@ -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

View file

@ -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):
"""

View file

@ -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()

View file

@ -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