Multinut/README.md
Dominik Krenn 12c078cd72 feat: add Explainable class for AI-powered code explanations and examples in README.md
chore: update version to 0.3.1 and add OpenAI dependency in setup.py
2025-09-24 11:06:22 +02:00

335 lines
8.9 KiB
Markdown

# `multinut`
![PyPI - Version](https://img.shields.io/pypi/v/multinut)
![Downloads - Monthly](https://static.pepy.tech/personalized-badge/multinut?period=month&units=international_system&left_color=black&right_color=orange)
![Python Versions](https://img.shields.io/pypi/pyversions/multinut)
![Wheel](https://img.shields.io/pypi/wheel/multinut)
The multitool nobody asked for. Includes stuff and so.
---
## `multinut.env`
A simple but flexible environment loader.
Supports `.env` file parsing with optional mode suffixes (e.g. `.env.production`, `.env.testing`, etc.), lazy loading, and dynamic access.
### Features
* Mode-based config support (`.env.production`, `.env.testing`, etc.)
* Access via:
* `env["KEY"]`
* `env.get("KEY", ...)`
* `env.KEY`
* Optional type casting (`str`, `bool`, `list`, `dict`, etc.)
* Sane default handling
* Does **not** mutate `os.environ`
---
### Use cases
* Loading `.env` files in mode-aware Python projects
* Separating secrets and configs by deployment context
* Dynamically reading values like `env.DB_URL`, `env.get("DEBUG", default=False, cast=cast_bool)`
* Avoiding `os.environ` pollution
---
### Basic Example
```python
from multinut.env import Environment, Modes
env = Environment(env_file_name=".env", mode=Modes.DEVELOPMENT)
print(env.get("DEBUG", default=False))
print(env["API_KEY"])
print(env.DB_URL)
```
Given a `.env.development` file:
```env
DEBUG=true
API_KEY=secret
DB_URL=https://example.com
```
---
### Smart Casts Example
```python
from multinut.env import (
Environment, cast_bool, cast_int, cast_float,
cast_list, cast_tuple, cast_dict, cast_none_or_str
)
env = Environment()
print("INT:", env.get("PORT", cast=cast_int)) # -> int
print("FLOAT:", env.get("PI", cast=cast_float)) # -> float
print("BOOL:", env.get("ENABLED", cast=cast_bool)) # -> bool
print("LIST:", env.get("NUMBERS", cast=cast_list)) # -> list[str]
print("TUPLE:", env.get("WORDS", cast=cast_tuple)) # -> tuple[str]
print("DICT:", env.get("CONFIG", cast=cast_dict)) # -> dict
print("NONE_OR_STR:", env.get("OPTIONAL", cast=cast_none_or_str)) # -> None or str
```
Example `.env`:
```env
PORT=8080
PI=3.1415
ENABLED=yes
NUMBERS=1,2,3
WORDS=hello,world,test
CONFIG={"timeout": 30, "debug": true}
OPTIONAL=null
```
### Included Cast Helpers
All built-in cast functions handle common edge cases:
| Cast Function | Description |
| ------------------ | ------------------------------------------- |
| `cast_str` | Ensures string |
| `cast_int` | Converts to integer |
| `cast_float` | Converts to float |
| `cast_bool` | Accepts `1`, `true`, `yes`, `on`, etc. |
| `cast_list` | Comma-split list |
| `cast_tuple` | Comma-split, converted to tuple |
| `cast_dict` | Parses JSON string into dictionary |
| `cast_none_or_str` | Returns `None` if value is `null` or `None` |
---
### EnviromentKeyMissing
if a key is get from `env.get` and it has no default given it will raise
EnviromentKeyMissing(f"Environment variable '{key}' not found.")
---
## `multinut.funky`
A type-aware method overloading system for Python classes.
Provides a decorator-based approach to method overloading that dispatches based on argument types, allowing multiple implementations of the same method with different type signatures.
### Features
* Type-based method dispatch using Python type annotations
* Support for both exact type matching (`type(value) is ann`) and inheritance-based matching (`isinstance(value, ann)`)
* Automatic signature binding and validation
* Clean decorator syntax with `@overload`
* Descriptor protocol support for seamless integration with class methods
---
### Use cases
* Creating polymorphic methods that behave differently based on argument types
* Building APIs with type-specific implementations
* Implementing mathematical operations that work with different numeric types
* Creating flexible data processing methods that handle various input formats
---
### Basic Example
```python
from multinut.funky import Dispatcher, overload
class Calculator:
add = Dispatcher("add")
@overload(add)
def add(self, x: int, y: int) -> int:
return x + y
@overload(add)
def add(self, x: str, y: str) -> str:
return x + y
@overload(add)
def add(self, x: list, y: list) -> list:
return x + y
calc = Calculator()
print(calc.add(1, 2)) # -> 3 (int)
print(calc.add("a", "b")) # -> "ab" (str)
print(calc.add([1], [2])) # -> [1, 2] (list)
```
---
### Complex Type Dispatch Example
```python
from multinut.funky import Dispatcher, overload
from typing import Union
class DataProcessor:
process = Dispatcher("process")
@overload(process)
def process(self, data: str) -> str:
return f"Processing string: {data.upper()}"
@overload(process)
def process(self, data: int) -> str:
return f"Processing number: {data * 2}"
@overload(process)
def process(self, data: list) -> str:
return f"Processing list of {len(data)} items"
@overload(process)
def process(self, data: dict) -> str:
return f"Processing dict with keys: {list(data.keys())}"
processor = DataProcessor()
print(processor.process("hello")) # -> "Processing string: HELLO"
print(processor.process(42)) # -> "Processing number: 84"
print(processor.process([1, 2, 3])) # -> "Processing list of 3 items"
print(processor.process({"a": 1, "b": 2})) # -> "Processing dict with keys: ['a', 'b']"
```
### Error Handling
When no matching overload is found, a `TypeError` is raised with details about the failed dispatch:
```python
# This will raise: TypeError: No matching overload for add(1.5, 2.5)
calc.add(1.5, 2.5) # No overload for float arguments
```
---
## `multinut.explain`
An AI-powered code explanation system that adds `.explain()` methods to classes and functions.
Provides automatic code documentation by leveraging OpenAI's GPT models to generate human-readable explanations of Python code. Classes can inherit from `Explainable` to automatically gain explanation capabilities.
### Features
* Automatic `.explain()` method injection for classes and their methods
* AI-powered code analysis using OpenAI's GPT-4
* Environment-based API key management via `.env` files
* Source code introspection and intelligent explanation generation
* Method-level and class-level explanations
* Graceful error handling for missing API keys or source code issues
---
### Use cases
* Automatically generating documentation for complex classes
* Understanding legacy code or third-party implementations
* Creating educational materials and code walkthroughs
* Quick code review and comprehension assistance
* Onboarding new developers with self-documenting code
---
### Basic Example
```python
from multinut.explain import Explainable
# Set up API key from environment
Explainable.use_env(".env") # Looks for OPENAPI_KEY in .env file
class Calculator(Explainable):
def add(self, x: int, y: int) -> int:
return x + y
def multiply(self, x: int, y: int) -> int:
return x * y
# Explain the entire class
print(Calculator.explain())
# Explain individual methods
calc = Calculator()
print(calc.add.explain())
print(calc.multiply.explain())
```
### Environment Setup
Create a `.env` file with your OpenAI API key:
```env
OPENAPI_KEY=your-openai-api-key-here
```
Or set the API key directly:
```python
from multinut.explain import Explainable
Explainable.API_KEY = "your-openai-api-key-here"
```
---
### Complex Example
```python
from multinut.explain import Explainable
class DataAnalyzer(Explainable):
def __init__(self, dataset):
self.dataset = dataset
self.processed_data = []
def clean_data(self, remove_nulls=True):
cleaned = [item for item in self.dataset if item is not None]
if remove_nulls:
cleaned = [item for item in cleaned if item != ""]
return cleaned
def calculate_stats(self, data):
if not data:
return {"mean": 0, "count": 0}
return {
"mean": sum(data) / len(data),
"count": len(data),
"max": max(data),
"min": min(data)
}
# Get AI explanations
analyzer = DataAnalyzer([1, 2, 3, None, 4])
print("Class explanation:")
print(DataAnalyzer.explain())
print("\nMethod explanation:")
print(analyzer.clean_data.explain())
```
### Error Handling
The system handles various error conditions gracefully:
```python
# Missing API key
try:
Calculator.explain()
except ApiKeyMissingError as e:
print(f"Error: {e}")
# Invalid source code or API issues
print(some_method.explain()) # Returns: "<could not get function explanation: ...>"
```
---