diff --git a/.gitignore b/.gitignore index 96fd286..5f9c2fc 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,6 @@ aliases/*/src docs/_build docs/_static docs/templates -.coverage # changes during CD/CI -aliases/*/pyproject.toml +aliases/*/pyproject.toml \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bb633f..cd0115b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,20 +1,5 @@ # Changelog -## 0.12.0 "The Color Update" - -* All `AuthSrc()` derivatives, deprecated and never used, have been removed. -* New module `mat` adds a shallow reimplementation of `Matrix()` in order to implement matrix multiplication -* Removed obsolete `configparse` implementation that has been around since 0.3 and shelved since 0.4. -* `color`: added support for conversion from RGB to sRGB, XYZ, OKLab and OKLCH. - -## 0.11.2 - -+ increase test coverage of `validators` - -## 0.11.1 - -+ make `yesno()` accept boolean types - ## 0.11.0 + **Breaking**: sessions returned by `SQLAlchemy()` are now wrapped by default. Restore original behavior by passing `wrap=False` to the constructor or to `begin()` diff --git a/docs/color.rst b/docs/color.rst deleted file mode 100644 index 189a063..0000000 --- a/docs/color.rst +++ /dev/null @@ -1,19 +0,0 @@ - -Color -===== - -.. currentmodule:: suou.color - -... - -Web colors ----------- - -.. autoclass:: RGBColor - - -.. autoclass:: WebColor - - -.. autoclass:: XYZColor - diff --git a/docs/generated/suou.codecs.rst b/docs/generated/suou.codecs.rst index 0e76eff..0112a23 100644 --- a/docs/generated/suou.codecs.rst +++ b/docs/generated/suou.codecs.rst @@ -1,4 +1,4 @@ -suou.codecs +suou.codecs =========== .. automodule:: suou.codecs @@ -16,7 +16,6 @@ b64encode cb32decode cb32encode - cb32lencode jsonencode quote_css_string rb64decode diff --git a/docs/generated/suou.color.rst b/docs/generated/suou.color.rst index 339e4ed..03365c4 100644 --- a/docs/generated/suou.color.rst +++ b/docs/generated/suou.color.rst @@ -1,4 +1,4 @@ -suou.color +suou.color ========== .. automodule:: suou.color @@ -9,7 +9,5 @@ .. autosummary:: Chalk - RGBColor - SRGBColor WebColor \ No newline at end of file diff --git a/docs/generated/suou.flask_sqlalchemy.rst b/docs/generated/suou.flask_sqlalchemy.rst index dd6a455..458fa6f 100644 --- a/docs/generated/suou.flask_sqlalchemy.rst +++ b/docs/generated/suou.flask_sqlalchemy.rst @@ -1,6 +1,18 @@ -suou.flask\_sqlalchemy +suou.flask\_sqlalchemy ====================== .. automodule:: suou.flask_sqlalchemy + + .. rubric:: Functions + + .. autosummary:: + + require_auth + + .. rubric:: Classes + + .. autosummary:: + + FlaskAuthSrc \ No newline at end of file diff --git a/docs/generated/suou.legal.rst b/docs/generated/suou.legal.rst index 2598387..f19f6f0 100644 --- a/docs/generated/suou.legal.rst +++ b/docs/generated/suou.legal.rst @@ -1,12 +1,6 @@ -suou.legal +suou.legal ========== .. automodule:: suou.legal - - .. rubric:: Classes - - .. autosummary:: - - Lawyer \ No newline at end of file diff --git a/docs/generated/suou.peewee.rst b/docs/generated/suou.peewee.rst deleted file mode 100644 index f2c339e..0000000 --- a/docs/generated/suou.peewee.rst +++ /dev/null @@ -1,23 +0,0 @@ -suou.peewee -=========== - -.. automodule:: suou.peewee - - - .. rubric:: Functions - - .. autosummary:: - - connect_reconnect - - .. rubric:: Classes - - .. autosummary:: - - ConnectToDatabase - PeeweeConnectionState - ReconnectMysqlDatabase - RegexCharField - SiqField - SnowflakeField - \ No newline at end of file diff --git a/docs/generated/suou.strtools.rst b/docs/generated/suou.strtools.rst index 5ae2742..1bc81a1 100644 --- a/docs/generated/suou.strtools.rst +++ b/docs/generated/suou.strtools.rst @@ -1,4 +1,4 @@ -suou.strtools +suou.strtools ============= .. automodule:: suou.strtools @@ -9,5 +9,4 @@ .. autosummary:: PrefixIdentifier - SpitText \ No newline at end of file diff --git a/docs/generated/suou.validators.rst b/docs/generated/suou.validators.rst index 4c10573..b7974a0 100644 --- a/docs/generated/suou.validators.rst +++ b/docs/generated/suou.validators.rst @@ -1,4 +1,4 @@ -suou.validators +suou.validators =============== .. automodule:: suou.validators @@ -12,5 +12,4 @@ must_be not_greater_than not_less_than - yesno \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 69ab767..9c3d855 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,5 +16,4 @@ ease programmer's QoL and write shorter and cleaner code that works. sqlalchemy iding validators - color api \ No newline at end of file diff --git a/src/suou/__init__.py b/src/suou/__init__.py index ce85766..63b6d18 100644 --- a/src/suou/__init__.py +++ b/src/suou/__init__.py @@ -35,19 +35,17 @@ from .strtools import PrefixIdentifier from .validators import matches, not_less_than, not_greater_than, yesno from .redact import redact_url_password from .http import WantsContentType -from .color import OKLabColor, chalk, WebColor, RGBColor, SRGBColor, XYZColor, OKLabColor -from .mat import Matrix +from .color import chalk, WebColor -__version__ = "0.12.0a5" +__version__ = "0.11.0" __all__ = ( 'ConfigOptions', 'ConfigParserConfigSource', 'ConfigSource', 'ConfigValue', 'DictConfigSource', 'EnvConfigSource', 'I18n', 'Incomplete', 'JsonI18n', - 'Matrix', 'MissingConfigError', 'MissingConfigWarning', 'OKLabColor', - 'PrefixIdentifier', 'RGBColor', 'SRGBColor', + 'MissingConfigError', 'MissingConfigWarning', 'PrefixIdentifier', 'Siq', 'SiqCache', 'SiqGen', 'SiqType', 'Snowflake', 'SnowflakeGen', 'StringCase', 'TimedDict', 'TomlI18n', 'UserSigner', 'Wanted', 'WantsContentType', - 'WebColor', 'XYZColor', + 'WebColor', 'addattr', 'additem', 'age_and_days', 'alru_cache', 'b2048decode', 'b2048encode', 'b32ldecode', 'b32lencode', 'b64encode', 'b64decode', 'cb32encode', 'cb32decode', 'chalk', 'count_ones', 'dei_args', 'deprecated', diff --git a/src/suou/color.py b/src/suou/color.py index 058fcc7..5a8b899 100644 --- a/src/suou/color.py +++ b/src/suou/color.py @@ -21,9 +21,6 @@ from __future__ import annotations from collections import namedtuple from functools import lru_cache -import math - -from suou.mat import Matrix class Chalk: @@ -96,14 +93,11 @@ chalk = Chalk() ## Utilities for web colors -class RGBColor(namedtuple('_WebColor', 'red green blue')): +class WebColor(namedtuple('_WebColor', 'red green blue')): """ - Representation of a color in the RGB TrueColor space. + Representation of a color in the TrueColor space (aka rgb). Useful for theming. - - *Changed in 0.12.0*: name is now RGBColor, with WebColor being an alias. - Added conversions to and from OKLCH, OKLab, sRGB, and XYZ. """ def lighten(self, *, factor = .75): """ @@ -132,176 +126,21 @@ class RGBColor(namedtuple('_WebColor', 'red green blue')): """ return self.darken(factor=factor) + self.lighten(factor=factor) - def blend_with(self, other: RGBColor): + def blend_with(self, other: WebColor): """ Mix two colors, returning the average. """ - return RGBColor ( + return WebColor ( (self.red + other.red) // 2, (self.green + other.green) // 2, (self.blue + other.blue) // 2 ) - def to_srgb(self): - """ - Convert to sRGB space. - - *New in 0.12.0* - """ - return SRGBColor(*( - (i / 12.92 if abs(i) <= 0.04045 else - (-1 if i < 0 else 1) * (((abs(i) + 0.55)) / 1.055) ** 2.4) for i in self - )) - - def to_oklab(self): - return self.to_xyz().to_oklab() - __add__ = blend_with def __str__(self): return f"rgb({self.red}, {self.green}, {self.blue})" - RGB_TO_XYZ = Matrix([ - [0.41239079926595934, 0.357584339383878, 0.1804807884018343], - [0.21263900587151027, 0.715168678767756, 0.07219231536073371], - [0.01933081871559182, 0.11919477979462598, 0.9505321522496607] - ]) - - def to_xyz(self): - return XYZColor(*(self.RGB_TO_XYZ @ Matrix.as_column(self)).get_column()) - def to_oklch(self): - return self.to_xyz().to_oklch() +__all__ = ('chalk', 'WebColor') - def to_oklab(self): - return self.to_xyz().to_oklab() - -WebColor = RGBColor - -## The following have been adapted from -## https://gist.github.com/dkaraush/65d19d61396f5f3cd8ba7d1b4b3c9432 - -class SRGBColor(namedtuple('_SRGBColor', 'red green blue')): - """ - Represent a color in the sRGB space. - - *New in 0.12.0* - """ - red: float - green: float - blue: float - - def __str__(self): - return f"srgb({self.red}, {self.green}, {self.blue})" - - def to_rgb(self): - return RGBColor(*( - ((-1 if i < 0 else 1) * (1.055 * (abs(i) ** (1/2.4)) - 0.055) - if abs(i) > 0.0031308 else 12.92 * i) for i in self)) - - def to_xyz(self): - return self.to_rgb().to_xyz() - - def to_oklab(self): - return self.to_rgb().to_oklab() - - -class XYZColor(namedtuple('_XYZColor', 'x y z')): - """ - Represent a color in the XYZ color space. - - *New in 0.12.0* - """ - - XYZ_TO_RGB = Matrix([ - [ 3.2409699419045226, -1.537383177570094, -0.4986107602930034], - [-0.9692436362808796, 1.8759675015077202, 0.04155505740717559], - [ 0.05563007969699366, -0.20397695888897652, 1.0569715142428786] - ]) - - XYZ_TO_LMS = Matrix([ - [0.8190224379967030, 0.3619062600528904, -0.1288737815209879], - [0.0329836539323885, 0.9292868615863434, 0.0361446663506424], - [0.0481771893596242, 0.2642395317527308, 0.6335478284694309] - ]) - - LMSG_TO_OKLAB = Matrix([ - [0.2104542683093140, 0.7936177747023054, -0.0040720430116193], - [1.9779985324311684, -2.4285922420485799, 0.4505937096174110], - [0.0259040424655478, 0.7827717124575296, -0.8086757549230774] - ]) - - def to_rgb(self): - return RGBColor(*(self.XYZ_TO_RGB @ Matrix.as_column(self)).get_column()) - - def to_oklab(self): - lms = (self.XYZ_TO_LMS @ Matrix.as_column(self)).get_column() - lmsg = [math.cbrt(i) for i in lms] - oklab = (self.LMSG_TO_OKLAB @ Matrix.as_column(self)).get_column() - return OKLabColor(*oklab) - - def to_oklch(self): - return self.to_oklab().to_oklch() - - -class OKLabColor(namedtuple('_OKLabColor', 'l a b')): - """ - Represent a color in the OKLab color space. - - *New in 0.12.0* - """ - - OKLAB_TO_LMSG = Matrix([ - [1., 0.3963377773761749, 0.2158037573099136], - [1., -0.1055613458156586, -0.0638541728258133], - [1., -0.0894841775298119, -1.2914855480194092] - ]) - - LMS_TO_XYZ = Matrix([ - [ 1.2268798758459243, -0.5578149944602171, 0.2813910456659647], - [-0.0405757452148008, 1.1122868032803170, -0.0717110580655164], - [-0.0763729366746601, -0.4214933324022432, 1.5869240198367816] - ]) - - def to_xyz(self): - lmsg = (self.OKLAB_TO_LMSG @ Matrix.as_column(self)).get_column() - lms = [i ** 3 for i in lmsg] - xyz = (self.LMS_TO_XYZ @ Matrix.as_column(lms)).get_column() - return XYZColor(*xyz) - - def to_oklch(self): - return OKLCHColor( - self.l, - math.sqrt(self.a ** 2 + self.b ** 2), - 0 if abs(self.a) < .0002 and abs(self.b) < .0002 else (((math.atan2(self.b, self.a) * 180) / math.pi % 360) + 360) % 360 - ) - - def to_rgb(self): - return self.to_xyz().to_rgb() - -class OKLCHColor(namedtuple('_OKLCHColor', 'l c h')): - """ - Represent a color in the OKLCH color space. - - *Warning*: conversion to RGB is not bound checked yet! - - *New in 0.12.0* - """ - - def __str__(self): - l, c, h = round(self.l, 4), round(self.c, 4), round(self.h, 4) - - return f'oklch({l}, {c}, {h})' - - - def to_oklab(self): - return OKLabColor( - self.l, - self.c * math.cos(self.h * math.pi / 180), - self.h * math.cos(self.h * math.pi / 180) - ) - - def to_rgb(self): - return self.to_oklab().to_rgb() - -__all__ = ('chalk', 'WebColor', "RGBColor", 'SRGBColor', 'XYZColor', 'OKLabColor') diff --git a/src/suou/flask_sqlalchemy.py b/src/suou/flask_sqlalchemy.py index 3f1caf0..88122d2 100644 --- a/src/suou/flask_sqlalchemy.py +++ b/src/suou/flask_sqlalchemy.py @@ -1,7 +1,7 @@ """ Utilities for Flask-SQLAlchemy binding. -This module has been emptied in 0.12.0 following deprecation removals. +This module is deprecated and will be REMOVED in 0.14.0. --- @@ -16,6 +16,50 @@ This software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. """ +from functools import partial +from typing import Any, Callable, Never + +from flask import abort, request +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy.orm import DeclarativeBase, Session +from .functools import deprecated + +from .codecs import want_bytes +from .sqlalchemy import AuthSrc, require_auth_base + +@deprecated('inherits from deprecated and unused class') +class FlaskAuthSrc(AuthSrc): + ''' + + ''' + db: SQLAlchemy + def __init__(self, db: SQLAlchemy): + super().__init__() + self.db = db + def get_session(self) -> Session: + return self.db.session + def get_token(self): + if request.authorization: + return request.authorization.token + def get_signature(self) -> bytes: + sig = request.headers.get('authorization-signature', None) + return want_bytes(sig) if sig else None + def invalid_exc(self, msg: str = 'validation failed') -> Never: + abort(400, msg) + def required_exc(self): + abort(401, 'Login required') + +@deprecated('not intuitive to use') +def require_auth(cls: type[DeclarativeBase], db: SQLAlchemy) -> Callable[Any, Callable]: + """ + + """ + def auth_required(**kwargs): + return require_auth_base(cls=cls, src=FlaskAuthSrc(db), **kwargs) + + auth_required.__doc__ = require_auth_base.__doc__ + + return auth_required # Optional dependency: do not import into __init__.py __all__ = () diff --git a/src/suou/functools.py b/src/suou/functools.py index e72b689..c68c6b6 100644 --- a/src/suou/functools.py +++ b/src/suou/functools.py @@ -341,4 +341,4 @@ def none_pass(func: Callable[_T, _U], *args, **kwargs) -> Callable[_T, _U]: __all__ = ( 'deprecated', 'not_implemented', 'timed_cache', 'none_pass', 'alru_cache' -) +) \ No newline at end of file diff --git a/src/suou/glue.py b/src/suou/glue.py index 2ead20b..6368deb 100644 --- a/src/suou/glue.py +++ b/src/suou/glue.py @@ -66,4 +66,4 @@ def glue(*modules): return decorator # This module is experimental and therefore not re-exported into __init__ -__all__ = ('glue', 'FakeModule') +__all__ = ('glue', 'FakeModule') \ No newline at end of file diff --git a/src/suou/iding.py b/src/suou/iding.py index 7997da3..a2e0c37 100644 --- a/src/suou/iding.py +++ b/src/suou/iding.py @@ -249,20 +249,13 @@ class Siq(int): def to_base64(self, length: int = 15, *, strip: bool = True) -> str: return b64encode(self.to_bytes(length), strip=strip) - def to_cb32(self) -> str: return cb32encode(self.to_bytes(15, 'big')).lstrip('0') to_crockford = to_cb32 - @classmethod - def from_cb32(cls, val: str | bytes): - return cls.from_bytes(cb32decode(want_str(val).zfill(24))) - def to_hex(self) -> str: return f'{self:x}' - def to_oct(self) -> str: return f'{self:o}' - def to_b32l(self) -> str: """ This is NOT the URI serializer! @@ -312,10 +305,12 @@ class Siq(int): raise ValueError('checksum mismatch') return cls(int.from_bytes(b, 'big')) + @classmethod + def from_cb32(cls, val: str | bytes): + return cls.from_bytes(cb32decode(want_str(val).zfill(24))) def to_mastodon(self, /, domain: str | None = None): return f'@{self:u}{"@" if domain else ""}{domain}' - def to_matrix(self, /, domain: str): return f'@{self:u}:{domain}' diff --git a/src/suou/legal.py b/src/suou/legal.py index 91d9c8b..8046435 100644 --- a/src/suou/legal.py +++ b/src/suou/legal.py @@ -95,4 +95,4 @@ class Lawyer(SpitText): return self.format(COMPLETENESS, 'app_name') # This module is experimental and therefore not re-exported into __init__ -__all__ = ('Lawyer',) +__all__ = ('Lawyer',) \ No newline at end of file diff --git a/src/suou/mat.py b/src/suou/mat.py deleted file mode 100644 index 8bcbb01..0000000 --- a/src/suou/mat.py +++ /dev/null @@ -1,143 +0,0 @@ -""" -Matrix (not the movie...) - -*New in 0.12.0* - ---- - -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 __future__ import annotations -from typing import Collection, Iterable, TypeVar -from .functools import deprecated - -_T = TypeVar('_T') - -class Matrix(Collection[_T]): - """ - Minimalist reimplementation of matrices in pure Python. - - This to avoid adding numpy as a dependency. - - *New in 0.12.0* - """ - _shape: tuple[int, int] - _elements: list[_T] - - def shape(self): - return self._shape - - def __init__(self, iterable: Iterable[_T] | Iterable[Collection[_T]], shape: tuple[int, int] | None = None): - elements = [] - boundary_x = boundary_y = 0 - for row in iterable: - if isinstance(row, Collection): - if not boundary_y: - boundary_y = len(row) - elements.extend(row) - boundary_x += 1 - elif boundary_y != len(row): - raise ValueError('row length mismatch') - else: - elements.extend(row) - boundary_x += 1 - elif shape: - if not boundary_x: - boundary_x, boundary_y = shape - elements.append(row) - self._shape = boundary_x, boundary_y - self._elements = elements - assert len(self._elements) == boundary_x * boundary_y - - def __getitem__(self, key: tuple[int, int]) -> _T: - (x, y), (_, sy) = key, self.shape() - - return self._elements[x * sy + y] - - @property - def T(self): - sx, sy = self.shape() - return Matrix( - [ - [ - self[j, i] for j in range(sx) - ] for i in range(sy) - ] - ) - - def __matmul__(self, other: Matrix) -> Matrix: - (ax, ay), (bx, by) = self.shape(), other.shape() - - if ay != bx: - raise ValueError('cannot multiply matrices with incompatible shape') - - return Matrix([ - [ - sum(self[i, k] * other[k, j] for k in range(ay)) for j in range(by) - ] for i in range(ax) - ]) - - def __eq__(self, other: Matrix): - try: - return self._elements == other._elements and self._shape == other._shape - except Exception: - return False - - def __len__(self): - ax, ay = self.shape() - return ax * ay - - @deprecated('please use .rows() or .columns() instead') - def __iter__(self): - return iter(self._elements) - - def __contains__(self, x: object, /) -> bool: - return x in self._elements - - def __repr__(self): - return f'{self.__class__.__name__}({list(self.rows())})' - - def rows(self): - sx, sy = self.shape() - return ( - [self[j, i] for j in range(sy)] for i in range(sx) - ) - - def columns(self): - sx, sy = self.shape() - return ( - [self[j, i] for j in range(sx)] for i in range(sy) - ) - - @classmethod - def as_row(cls, iterable: Iterable): - return cls([[*iterable]]) - - @classmethod - def as_column(cls, iterable: Iterable): - return cls([[x] for x in iterable]) - - def get_column(self, idx = 0): - sx, _ = self.shape() - return [ - self[j, idx] for j in range(sx) - ] - - def get_row(self, idx = 0): - _, sy = self.shape() - return [ - self[idx, j] for j in range(sy) - ] - -__all__ = ('Matrix', ) - - diff --git a/src/suou/migrate.py b/src/suou/migrate.py index 5cb199c..357dc03 100644 --- a/src/suou/migrate.py +++ b/src/suou/migrate.py @@ -135,4 +135,4 @@ class UlidSiqMigrator(SiqMigrator): __all__ = ( 'SnowflakeSiqMigrator', 'UlidSiqMigrator' -) +) \ No newline at end of file diff --git a/src/suou/obsolete/configparsev0_3.py b/src/suou/obsolete/configparsev0_3.py new file mode 100644 index 0000000..1563813 --- /dev/null +++ b/src/suou/obsolete/configparsev0_3.py @@ -0,0 +1,239 @@ +""" +Utilities for parsing config variables. + +BREAKING older, non-generalized version, kept for backwards compability +in case 0.4+ version happens to break. + +WILL BE removed in 0.5.0. + +--- + +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 __future__ import annotations +from ast import TypeVar +from collections.abc import Mapping +from configparser import ConfigParser as _ConfigParser +import os +from typing import Any, Callable, Iterator +from collections import OrderedDict +import warnings + +from ..functools import deprecated +from ..exceptions import MissingConfigError, MissingConfigWarning + +warnings.warn('This module will be removed in 0.5.0 and is kept only in case new implementation breaks!\n'\ + 'Do not use unless you know what you are doing.', DeprecationWarning) + +MISSING = object() +_T = TypeVar('T') + + +@deprecated('use configparse') +class ConfigSource(Mapping): + ''' + Abstract config source. + ''' + __slots__ = () + +@deprecated('use configparse') +class EnvConfigSource(ConfigSource): + ''' + Config source from os.environ aka .env + ''' + def __getitem__(self, key: str, /) -> str: + return os.environ[key] + def get(self, key: str, fallback = None, /): + return os.getenv(key, fallback) + def __contains__(self, key: str, /) -> bool: + return key in os.environ + def __iter__(self) -> Iterator[str]: + yield from os.environ + def __len__(self) -> int: + return len(os.environ) + +@deprecated('use configparse') +class ConfigParserConfigSource(ConfigSource): + ''' + Config source from ConfigParser + ''' + __slots__ = ('_cfp', ) + _cfp: _ConfigParser + + def __init__(self, cfp: _ConfigParser): + if not isinstance(cfp, _ConfigParser): + raise TypeError(f'a ConfigParser object is required (got {cfp.__class__.__name__!r})') + self._cfp = cfp + def __getitem__(self, key: str, /) -> str: + k1, _, k2 = key.partition('.') + return self._cfp.get(k1, k2) + def get(self, key: str, fallback = None, /): + k1, _, k2 = key.partition('.') + return self._cfp.get(k1, k2, fallback=fallback) + def __contains__(self, key: str, /) -> bool: + k1, _, k2 = key.partition('.') + return self._cfp.has_option(k1, k2) + def __iter__(self) -> Iterator[str]: + for k1, v1 in self._cfp.items(): + for k2 in v1: + yield f'{k1}.{k2}' + def __len__(self) -> int: + ## XXX might be incorrect but who cares + return sum(len(x) for x in self._cfp) + +@deprecated('use configparse') +class DictConfigSource(ConfigSource): + ''' + Config source from Python mappings. Useful with JSON/TOML config + ''' + __slots__ = ('_d',) + + _d: dict[str, Any] + + def __init__(self, mapping: dict[str, Any]): + self._d = mapping + def __getitem__(self, key: str, /) -> str: + return self._d[key] + def get(self, key: str, fallback: _T = None, /): + return self._d.get(key, fallback) + def __contains__(self, key: str, /) -> bool: + return key in self._d + def __iter__(self) -> Iterator[str]: + yield from self._d + def __len__(self) -> int: + return len(self._d) + +@deprecated('use configparse') +class ConfigValue: + """ + A single config property. + + By default, it is sourced from os.environ — i.e. environment variables, + and property name is upper cased. + + You can specify further sources, if the parent ConfigOptions class + supports them. + + Arguments: + - public: mark value as public, making it available across the app (e.g. in Jinja2 templates). + - prefix: src but for the lazy + - preserve_case: if True, src is not CAPITALIZED. Useful for parsing from Python dictionaries or ConfigParser's + - required: throw an error if empty or not supplied + """ + # XXX disabled per https://stackoverflow.com/questions/45864273/slots-conflicts-with-a-class-variable-in-a-generic-class + #__slots__ = ('_srcs', '_val', '_default', '_cast', '_required', '_preserve_case') + + _srcs: dict[str, str] | None + _preserve_case: bool = False + _val: Any | MISSING = MISSING + _default: Any | None + _cast: Callable | None + _required: bool + _pub_name: str | bool = False + def __init__(self, /, + src: str | None = None, *, default = None, cast: Callable | None = None, + required: bool = False, preserve_case: bool = False, prefix: str | None = None, + public: str | bool = False, **kwargs): + self._srcs = dict() + self._preserve_case = preserve_case + if src: + self._srcs['default'] = src if preserve_case else src.upper() + elif prefix: + self._srcs['default'] = f'{prefix if preserve_case else prefix.upper}?' + self._default = default + self._cast = cast + self._required = required + self._pub_name = public + for k, v in kwargs.items(): + if k.endswith('_src'): + self._srcs[k[:-4]] = v + else: + raise TypeError(f'unknown keyword argument {k!r}') + def __set_name__(self, owner, name: str): + if 'default' not in self._srcs: + self._srcs['default'] = name if self._preserve_case else name.upper() + elif self._srcs['default'].endswith('?'): + self._srcs['default'] = self._srcs['default'].rstrip('?') + (name if self._preserve_case else name.upper() ) + + if self._pub_name is True: + self._pub_name = name + if self._pub_name and isinstance(owner, ConfigOptions): + owner.expose(self._pub_name, name) + def __get__(self, obj: ConfigOptions, owner=False): + if self._val is MISSING: + v = MISSING + for srckey, src in obj._srcs.items(): + if srckey in self._srcs: + v = src.get(self._srcs[srckey], v) + if self._required and (not v or v is MISSING): + raise MissingConfigError(f'required config {self._srcs['default']} not set!') + if v is MISSING: + v = self._default + if callable(self._cast): + v = self._cast(v) if v is not None else self._cast() + self._val = v + return self._val + + @property + def source(self, /): + return self._srcs['default'] + +@deprecated('use configparse') +class ConfigOptions: + """ + Base class for loading config values. + + It is intended to get subclassed; config values must be defined as + ConfigValue() properties. + + Further config sources can be added with .add_source() + """ + + __slots__ = ('_srcs', '_pub') + + _srcs: OrderedDict[str, ConfigSource] + _pub: dict[str, str] + + def __init__(self, /): + self._srcs = OrderedDict( + default = EnvConfigSource() + ) + self._pub = dict() + + def add_source(self, key: str, csrc: ConfigSource, /, *, first: bool = False): + self._srcs[key] = csrc + if first: + self._srcs.move_to_end(key, False) + + add_config_source = deprecated_alias(add_source) + + def expose(self, public_name: str, attr_name: str | None = None) -> None: + ''' + Mark a config value as public. + + Called automatically by ConfigValue.__set_name__(). + ''' + attr_name = attr_name or public_name + self._pub[public_name] = attr_name + + def to_dict(self, /): + d = dict() + for k, v in self._pub.items(): + d[k] = getattr(self, v) + return d + + +__all__ = ( + 'MissingConfigError', 'MissingConfigWarning', 'ConfigOptions', 'EnvConfigSource', 'ConfigParserConfigSource', 'DictConfigSource', 'ConfigSource', 'ConfigValue' +) + + diff --git a/src/suou/validators.py b/src/suou/validators.py index 349172a..e8b366f 100644 --- a/src/suou/validators.py +++ b/src/suou/validators.py @@ -20,6 +20,8 @@ from typing import Any, Iterable, TypeVar from suou.classtools import MISSING +from .functools import future + _T = TypeVar('_T') def matches(regex: str | int, /, length: int = 0, *, flags=0): @@ -57,24 +59,13 @@ def not_less_than(y): """ return lambda x: x >= y -def yesno(x: str | int | bool | None) -> bool: +def yesno(x: str) -> bool: """ Returns False if x.lower() is in '0', '', 'no', 'n', 'false' or 'off'. *New in 0.9.0* - - *Changed in 0.11.1*: now accepts None and bool. """ - if x in (None, MISSING): - return False - if isinstance(x, bool): - return x - if isinstance(x, int): - return x != 0 - if isinstance(x, str): - return x.lower() not in ('', '0', 'off', 'n', 'no', 'false', 'f') - return True - + return x not in (None, MISSING) and x.lower() not in ('', '0', 'off', 'n', 'no', 'false', 'f') __all__ = ('matches', 'must_be', 'not_greater_than', 'not_less_than', 'yesno') diff --git a/tests/test_mat.py b/tests/test_mat.py deleted file mode 100644 index ac1e00c..0000000 --- a/tests/test_mat.py +++ /dev/null @@ -1,47 +0,0 @@ - - -import unittest - -from suou.mat import Matrix - - -class TestMat(unittest.TestCase): - def setUp(self): - self.m_a = Matrix([ - [2, 2], - [1, 3] - ]) - self.m_b = Matrix([ - [1], [-4] - ]) - def tearDown(self) -> None: - ... - def test_transpose(self): - self.assertEqual( - self.m_a.T, - Matrix([ - [2, 1], - [2, 3] - ]) - ) - self.assertEqual( - self.m_b.T, - Matrix([[1, -4]]) - ) - def test_mul(self): - self.assertEqual( - self.m_b.T @ self.m_a, - Matrix([ - [-2, -10] - ]) - ) - self.assertEqual( - self.m_a @ self.m_b, - Matrix([ - [-6], [-11] - ]) - ) - def test_shape(self): - self.assertEqual(self.m_a.shape(), (2, 2)) - self.assertEqual(self.m_b.shape(), (2, 1)) - self.assertEqual(self.m_b.T.shape(), (1, 2)) \ No newline at end of file diff --git a/tests/test_validators.py b/tests/test_validators.py index 2d3cc89..0064128 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -1,8 +1,7 @@ import unittest -from suou.calendar import not_greater_than -from suou.validators import not_less_than, yesno +from suou.validators import yesno class TestValidators(unittest.TestCase): def setUp(self): @@ -22,17 +21,4 @@ class TestValidators(unittest.TestCase): self.assertTrue(yesno('2')) self.assertTrue(yesno('o')) self.assertFalse(yesno('oFF')) - self.assertFalse(yesno('no')) - self.assertFalse(yesno(False)) - self.assertTrue(yesno(True)) - self.assertFalse(yesno('')) - - def test_not_greater_than(self): - self.assertTrue(not_greater_than(5)(5)) - self.assertTrue(not_greater_than(5)(3)) - self.assertFalse(not_greater_than(3)(8)) - - def test_not_less_than(self): - self.assertTrue(not_less_than(5)(5)) - self.assertFalse(not_less_than(5)(3)) - self.assertTrue(not_less_than(3)(8)) \ No newline at end of file + self.assertFalse(yesno('no')) \ No newline at end of file