Metadata-Version: 2.4
Name: PersistentObjects
Version: 0.2.3
Summary: JSON-backed attribute-persistent object with namespace support
Author: Tornado300
License-Expression: MIT
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# PersistentObjects

A simple Python library that provides the `PersistentObject` class, which automatically saves all its attributes to a JSON file. Attributes are saved as soon as they are set.

> [!IMPORTANT]
> In-place mutations like `.append()`, `.insert()`, `.extend()`, etc. will **not** automatically save!
> Create a temp variable, mutate it, then reassign it back:
> ```python
> temp = pobject.my_list
> temp.append("new item")
> pobject.my_list = temp  # triggers save
> ```

## Installation

```bash
pip install PersistentObjects
```

## Usage

```python
from PersistentObjects import PersistentObject

pobject = PersistentObject("save.json")

# attributes are saved to the json file immediately
pobject.first_value = 10
pobject.second_value = "foo"

# namespaces create sub-dicts in the json file
pobject.namespace("test_namespace")
pobject.test_namespace.third_value = [10, 5]

# or save the namespace to a variable
namespace = pobject.namespace("other_namespace")
namespace.value = 42

# suffix with '_' to make an attribute non-persistent (normal python attribute)
pobject.temp_ = True

# delete a persistent attribute
del pobject.first_value
```

## Namespaces

Use `namespace()` to create a named section within the JSON file. After calling `namespace()`, you can access it via dot notation. Namespaces can be nested arbitrarily deep.

```python
# create and access via dot notation
pobject.namespace("ui")
pobject.ui.theme = "dark"
pobject.ui.font_size = 14

# nested namespaces
pobject.namespace("ui").namespace("colors")
pobject.ui.colors.primary = "#ff0000"
pobject.ui.colors.accent = "#0000ff"

# or chain it
colors = pobject.namespace("ui").namespace("colors")
colors.primary = "#ff0000"
```

> [!NOTE]
> You must call `.namespace()` at least once before using dot-access. This is by design — without it, accessing a nonexistent attribute like `pobject.typo` would silently create an empty namespace instead of raising an `AttributeError`.

## Supported types

Beyond the standard JSON types (`str`, `int`, `float`, `bool`, `list`, `dict`, `None`), the following Python types are automatically encoded and decoded:

| Type | JSON representation |
|---|---|
| `tuple` | `{"__type__": "tuple", "__value__": [...]}` |
| `set` | `{"__type__": "set", "__value__": [...]}` |
| `frozenset` | `{"__type__": "frozenset", "__value__": [...]}` |
| `bytes` | `{"__type__": "bytes", "__value__": "<base64>"}` |
| `datetime` | `{"__type__": "datetime", "__value__": "<isoformat>"}` |
| `date` | `{"__type__": "date", "__value__": "<isoformat>"}` |
| `time` | `{"__type__": "time", "__value__": "<isoformat>"}` |

Nested structures work too (e.g. a list of tuples, a set of tuples).

```python
from datetime import datetime

pobject.my_set = {1, 2, 3}
pobject.my_tuple = ("a", "b", "c")
pobject.my_bytes = b"hello"
pobject.my_datetime = datetime(2025, 6, 15, 12, 30)
```

## Type enforcement

Use `settype()` to register a type constraint on an attribute. Assigning a value of the wrong type raises a `TypeError`.

```python
from typing import Any

pobject.settype("count", int)
pobject.count = 5       # ok
pobject.count = "five"  # TypeError

# union types (Python 3.10+)
pobject.settype("value", int | None)
pobject.value = None    # ok

# tuple of types
pobject.settype("multi", (int, str))

# Any removes the constraint
pobject.settype("count", Any)

# also works on namespaces
namespace.settype("volume", int | float)
```

## Defaults

Use `setdefault()` to set a value only if the attribute doesn't already exist. It also registers the value for `reset()`. An optional `type` parameter combines setting a default with type enforcement.

```python
pobject.setdefault("name", "default_name")
pobject.setdefault("count", 0, type=int)  # sets default and enforces type

pobject.count = 99
pobject.reset("count")   # back to 0
pobject.reset()          # resets all attributes that have a registered default
```
