kain
Project description
kain
Minimalist Python utility library. Zero runtime dependencies.
kain is a set of helper tools: introspection, descriptors, dynamic imports, monkey-patching, signal handling. Everything is built on typing and the standard library — no external packages.
Installation
Via uv:
uv add kain
Contents
Feature overview
| Module | Purpose |
|---|---|
kain.Is |
Type predicates: Is.Class, Is.callable, Is.collection, Is.mapping, Is.subclass, ... |
kain.Who |
Object name formatting: Who.Name, Who.Addr, Who.Is, Who.Module, Who.Args |
kain.importer |
Dynamic imports: required(), optional(), add_path(), get_path() |
kain.properties |
Descriptors: class_property, mixed_property, pin, cached variants |
kain.signals |
Graceful shutdown: on_quit, quit_at |
kain.classes |
Sentinels: Missing, Nothing, Singleton |
kain.internals |
Utilities: to_ascii, to_bytes, unique, iter_inheritance |
How to use
Is — type predicates
Is is a namespace module of predicates. All members are callables returning bool (or a type for Is.classOf).
from kain import Is
Is.Class(str) # True
Is.callable(print) # True
Is.collection([1, 2]) # True
Is.mapping({"a": 1}) # True
Is.primitive(42) # True
Is.subclass(int, object) # True
# Special
Is.tty # True if stdin/stdout/stderr are TTYs
Is.Builtin # alias for is_from_builtin
Who — introspection and names
Who builds human-readable strings for objects — fully-qualified names, memory addresses, call arguments.
from kain import Who
Who.Name(str) # 'builtins.str'
Who.Addr(str) # 'builtins.str#7f8b2c4d1e00' # with hex id
Who.Is(str) # 'builtins.str (type)'
Who.Module(str) # 'builtins'
# Call arguments
Who.Args(1, "hello", flag=True) # "1, 'hello', flag=True"
# Short name
Who.Name(str, full=False) # 'str'
required / optional — dynamic imports
Import by string name with different strictness levels.
from kain import required, optional
# Strict — raises ImportError if not found
os_path = required("os.path")
join = required("os.path.join")
# Optional — falls back to default if package is missing
natsorted = optional("natsort.natsorted", default=sorted)
# If natsort is not installed, returns built-in sorted
Adding paths to sys.path:
from kain import add_path
add_path("../src") # Path("../src").resolve() is added to sys.path
class_property / mixed_property — descriptors
Analogues of property, but for the class (class_property) and both levels (mixed_property).
from kain import class_property, mixed_property
class Config:
_name = "default"
@class_property
def name(cls):
return cls._name
@mixed_property
def greet(cls_or_self):
who = "class" if isinstance(cls_or_self, type) else "instance"
return f"hello from {who}"
Config.name # 'default'
Config().greet # 'hello from instance'
Config.greet # 'hello from class'
pin — cached properties
pin is the unified entry point for all cached descriptors. It exposes aliases for different variants:
from kain import pin
class Data:
@pin
def expensive(self):
return sum(range(10_000_000))
@pin.cls
def class_level(cls):
return f"cache on {cls.__name__}"
@pin.any
def mixed(cls_or_self):
return "works on both"
d = Data()
d.expensive # computed once, then read from __dict__
Data.class_level # cache on class
Under the hood, pin uses the kain.properties.cached descriptor family:
| Descriptor | Cache on | Access level |
|---|---|---|
cached_property |
instance | instance only |
class_cached_property |
class | class only |
mixed_cached_property |
class | class + instance |
pre_cached_property |
class | instance (value stored on class) |
post_cached_property |
class | instance (lazy evaluation) |
All support inheritance via with_parent:
from kain.properties import class_property
class Base:
@class_property
def label(cls):
return "base"
class Derived(Base):
@class_property.with_parent
def label(cls, parent_value):
return f"derived:{parent_value}"
Derived.label # 'derived:base'
proxy_to — attribute proxying
Class decorator that forwards method calls to a pivot attribute.
from kain import proxy_to
@proxy_to("_inner", "read", "write", "close")
class Wrapper:
def __init__(self, inner):
self._inner = inner
# Wrapper.read / Wrapper.write / Wrapper.close now proxy
# to _inner.read / _inner.write / _inner.close
Monkey — monkey-patch
from kain import Monkey
# Replace an attribute on a module or object
Monkey.patch((os.path, "join"), lambda *a: "patched!")
# Wrap an existing callable
original = Monkey.wrap(len, lambda orig, x: orig(x) * 2)
# Bind a new method to a class
Monkey.bind(SomeClass, "new_method", lambda self: 42)
# Decorator: suppress specified exceptions
class Parser:
@Monkey.expect(ValueError)
def parse_int(cls, s):
return int(s)
Parser.parse_int("not-a-number") # None, exception swallowed
on_quit / quit_at — signals
from kain import on_quit, quit_at
# Register a callback for graceful shutdown
@on_quit().schedule
def cleanup():
print("Cleaning up before exit...")
# Auto-reload on file change (dev mode)
checker = quit_at()
while checker.sleep(2.5):
pass # main loop; exits when script mtime changes
on_quit is a singleton that intercepts SIGINT, SIGTERM, SIGQUIT, and atexit. All registered callbacks are executed exactly once.
Utilities
from kain import to_ascii, to_bytes, unique
# Encoding / decoding with default charset
to_bytes("hello") # b'hello'
to_ascii(b"hello") # 'hello'
# Unique elements with key and filtering
items = [1, 2, 2, 3, 1]
list(unique(items)) # [1, 2, 3]
# MRO iteration with filters
from kain.internals import iter_inheritance
list(iter_inheritance("hello", exclude_stdlib=True))
License
BSD License. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file kain-1.1.4.tar.gz.
File metadata
- Download URL: kain-1.1.4.tar.gz
- Upload date:
- Size: 86.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.3 {"installer":{"name":"uv","version":"0.11.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e1fe97f9c1d5644f3d1314e0b2f2518aae038527751df36e854e9bd6af761889
|
|
| MD5 |
85473cad9162a856e8bd33014c46cb46
|
|
| BLAKE2b-256 |
051283a46876d916a3c3f0364dbd1a23b4299dd881bc5641886ceb0c8fff6ca7
|
File details
Details for the file kain-1.1.4-py3-none-any.whl.
File metadata
- Download URL: kain-1.1.4-py3-none-any.whl
- Upload date:
- Size: 46.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.3 {"installer":{"name":"uv","version":"0.11.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
989254d23d691dd7bb1ed8d81a602fc48e2444c3bc3b5065bc8156057a3d8eee
|
|
| MD5 |
acce5d0f66a907bf4c222b449d650c0e
|
|
| BLAKE2b-256 |
e5399622c5ab68eb9889dd9d5590f54131c925ca1449a7aa7f9081fe27104443
|