Compare commits

..

6 commits

23 changed files with 464 additions and 321 deletions

3
.gitignore vendored
View file

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

View file

@ -1,5 +1,16 @@
# 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

19
docs/color.rst Normal file
View file

@ -0,0 +1,19 @@
Color
=====
.. currentmodule:: suou.color
...
Web colors
----------
.. autoclass:: RGBColor
.. autoclass:: WebColor
.. autoclass:: XYZColor

View file

@ -1,4 +1,4 @@
suou.codecs
suou.codecs
===========
.. automodule:: suou.codecs
@ -16,6 +16,7 @@ suou.codecs
b64encode
cb32decode
cb32encode
cb32lencode
jsonencode
quote_css_string
rb64decode

View file

@ -1,4 +1,4 @@
suou.color
suou.color
==========
.. automodule:: suou.color
@ -9,5 +9,7 @@ suou.color
.. autosummary::
Chalk
RGBColor
SRGBColor
WebColor

View file

@ -1,18 +1,6 @@
suou.flask\_sqlalchemy
suou.flask\_sqlalchemy
======================
.. automodule:: suou.flask_sqlalchemy
.. rubric:: Functions
.. autosummary::
require_auth
.. rubric:: Classes
.. autosummary::
FlaskAuthSrc

View file

@ -1,6 +1,12 @@
suou.legal
suou.legal
==========
.. automodule:: suou.legal
.. rubric:: Classes
.. autosummary::
Lawyer

View file

@ -0,0 +1,23 @@
suou.peewee
===========
.. automodule:: suou.peewee
.. rubric:: Functions
.. autosummary::
connect_reconnect
.. rubric:: Classes
.. autosummary::
ConnectToDatabase
PeeweeConnectionState
ReconnectMysqlDatabase
RegexCharField
SiqField
SnowflakeField

View file

@ -1,4 +1,4 @@
suou.strtools
suou.strtools
=============
.. automodule:: suou.strtools
@ -9,4 +9,5 @@ suou.strtools
.. autosummary::
PrefixIdentifier
SpitText

View file

@ -1,4 +1,4 @@
suou.validators
suou.validators
===============
.. automodule:: suou.validators
@ -12,4 +12,5 @@ suou.validators
must_be
not_greater_than
not_less_than
yesno

View file

@ -16,4 +16,5 @@ ease programmer's QoL and write shorter and cleaner code that works.
sqlalchemy
iding
validators
color
api

View file

@ -35,17 +35,19 @@ 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 chalk, WebColor
from .color import OKLabColor, chalk, WebColor, RGBColor, SRGBColor, XYZColor, OKLabColor
from .mat import Matrix
__version__ = "0.11.1"
__version__ = "0.12.0a5"
__all__ = (
'ConfigOptions', 'ConfigParserConfigSource', 'ConfigSource', 'ConfigValue',
'DictConfigSource', 'EnvConfigSource', 'I18n', 'Incomplete', 'JsonI18n',
'MissingConfigError', 'MissingConfigWarning', 'PrefixIdentifier',
'Matrix', 'MissingConfigError', 'MissingConfigWarning', 'OKLabColor',
'PrefixIdentifier', 'RGBColor', 'SRGBColor',
'Siq', 'SiqCache', 'SiqGen', 'SiqType', 'Snowflake', 'SnowflakeGen',
'StringCase', 'TimedDict', 'TomlI18n', 'UserSigner', 'Wanted', 'WantsContentType',
'WebColor',
'WebColor', 'XYZColor',
'addattr', 'additem', 'age_and_days', 'alru_cache', 'b2048decode', 'b2048encode',
'b32ldecode', 'b32lencode', 'b64encode', 'b64decode', 'cb32encode',
'cb32decode', 'chalk', 'count_ones', 'dei_args', 'deprecated',

View file

@ -21,6 +21,9 @@ from __future__ import annotations
from collections import namedtuple
from functools import lru_cache
import math
from suou.mat import Matrix
class Chalk:
@ -93,11 +96,14 @@ chalk = Chalk()
## Utilities for web colors
class WebColor(namedtuple('_WebColor', 'red green blue')):
class RGBColor(namedtuple('_WebColor', 'red green blue')):
"""
Representation of a color in the TrueColor space (aka rgb).
Representation of a color in the RGB TrueColor space.
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):
"""
@ -126,21 +132,176 @@ class WebColor(namedtuple('_WebColor', 'red green blue')):
"""
return self.darken(factor=factor) + self.lighten(factor=factor)
def blend_with(self, other: WebColor):
def blend_with(self, other: RGBColor):
"""
Mix two colors, returning the average.
"""
return WebColor (
return RGBColor (
(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())
__all__ = ('chalk', 'WebColor')
def to_oklch(self):
return self.to_xyz().to_oklch()
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')

View file

@ -1,7 +1,7 @@
"""
Utilities for Flask-SQLAlchemy binding.
This module is deprecated and will be REMOVED in 0.14.0.
This module has been emptied in 0.12.0 following deprecation removals.
---
@ -16,50 +16,6 @@ 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__ = ()

View file

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

View file

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

View file

@ -249,13 +249,20 @@ 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!
@ -305,12 +312,10 @@ 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}'

View file

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

143
src/suou/mat.py Normal file
View file

@ -0,0 +1,143 @@
"""
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', )

View file

@ -135,4 +135,4 @@ class UlidSiqMigrator(SiqMigrator):
__all__ = (
'SnowflakeSiqMigrator', 'UlidSiqMigrator'
)
)

View file

@ -1,239 +0,0 @@
"""
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'
)

47
tests/test_mat.py Normal file
View file

@ -0,0 +1,47 @@
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))

View file

@ -1,7 +1,8 @@
import unittest
from suou.validators import yesno
from suou.calendar import not_greater_than
from suou.validators import not_less_than, yesno
class TestValidators(unittest.TestCase):
def setUp(self):
@ -21,4 +22,17 @@ class TestValidators(unittest.TestCase):
self.assertTrue(yesno('2'))
self.assertTrue(yesno('o'))
self.assertFalse(yesno('oFF'))
self.assertFalse(yesno('no'))
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))