Metadata-Version: 2.4
Name: lzconfig
Version: 0.1.3
Summary: Zero-boilerplate configuration library — import and go
Author-email: Lyu <shlv@qq.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/SihanLv/lzconfig
Keywords: config,configuration,yaml,json,toml,ini,dotenv,xml,zero-boilerplate
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Utilities
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pyyaml>=6.0
Requires-Dist: tomli>=2.0
Requires-Dist: python-dotenv>=1.0
Dynamic: license-file

# lzconfig — Lazy Configuration without instantiation

> lz: Too lazy to type `lazy`

[中文文档](README.zh.md)

**Import and go.** No init, no loader calls, no boilerplate.

```python
import lzconfig as lz

# Attribute-style access
print(lz.database.host)                # → "localhost"
print(lz.database.credentials.user)    # → "admin"

# Dict-style access
print(lz['database']['host'])          # → "localhost"

# Mixed access
print(lz.database['port'])             # → 5432

# Safe access with defaults
print(lz.get('debug', False))          # → True
```

## How It Works

At import time, `lzconfig` discovers your config file, parses it, interpolates
environment variables, and replaces itself in `sys.modules` with the loaded
configuration object.  From then on, `lz` **is** your config.

Command-line `--key value` arguments are merged over the file, so the command
line always wins.  And if command-line arguments are present, no config file is
needed at all.

## Config File Resolution

1. If `LZCONFIG_FILE` is set (and non-empty), that file is used.  Relative
   paths are resolved against the current working directory.
2. Otherwise, the current directory is scanned for the first match:

   | Priority | Filename |
   |----------|----------|
   | 1 | `lzconfig.yaml` |
   | 2 | `lzconfig.yml` |
   | 3 | `lzconfig.json` |
   | 4 | `lzconfig.toml` |
   | 5 | `lzconfig.ini` |
   | 6 | `lzconfig.cfg` |
   | 7 | `lzconfig.env` |
   | 8 | `lzconfig.xml` |

3. If nothing is found and there are no `--key value` command-line arguments,
   `ConfigNotFoundError` is raised at import time.

## Supported Formats

| Format | Extensions |
|--------|-----------|
| YAML   | `.yaml` `.yml` |
| JSON   | `.json` |
| TOML   | `.toml` |
| INI    | `.ini` `.cfg` |
| .env   | `.env` |
| XML    | `.xml` |

All formats work out of the box — no extras needed.

## Environment Variable Interpolation

`$VAR` and `${VAR}` references in config values are expanded at load time
using `os.path.expandvars`:

```yaml
# lzconfig.yaml
database:
  host: $DB_HOST
  password: ${DB_PASSWORD}
```

Undefined variables are left as literal text — no error is raised.

## Command-Line Arguments

`--key value` and `--key=value` arguments are merged over the file-based
configuration — the command line is the highest-priority source:

```bash
python app.py --database.host example.com --port 8080 --debug true
```

```python
import lzconfig as lz

lz.database.host  # "example.com" — overrides the config file
lz.port           # 8080 — an int, not a string
lz.debug          # True — a bool
```

Rules:

- Dotted keys build nested structure: `--database.host x` sets `lz.database.host`.
- Values are parsed with JSON when possible (`--port 5432` → int, `--debug true`
  → bool, `--tags '["a","b"]'` → list); otherwise they stay strings
  (`--zip 01234` → `"01234"`).
- A bare `--key` (no value) is treated as `True`.
- Only `--` options are consumed; everything else is left in `sys.argv`
  untouched, and a bare `--` ends option parsing.
- If command-line arguments are present, no config file is necessary, the config will be built from the command line alone.
- If neither a config file nor command-line arguments exist, `ConfigNotFoundError` is raised.

## Access Patterns

| Pattern | Example |
|---------|---------|
| Attribute chain | `lz.a.b.c` |
| Dict chain | `lz['a']['b']['c']` |
| Mixed | `lz.a['b'].c` |
| List index | `lz['items'][0].name` |
| Membership | `'key' in lz` |
| Safe get | `lz.get('key', default)` |
| Keys | `lz.keys()` |
| Length | `len(lz)` |
| Iteration | `for k in lz: ...` |
| Attribute write | `lz.debug = True` |
| Dict write | `lz['database']['port'] = 8080` |
| Delete key | `del lz.debug` / `del lz['debug']` |

The config object is mutable — attribute and dict-style writes update the
underlying data in place, and nested values share the same storage
(`lz.database.host = '...'` is visible through `lz['database']['host']`).

## Key / Method Collision

If a config key happens to have the same name as a ConfigObject method
(e.g., `get`, `items`, `keys`):

- **Attribute access** (`lz.items`) returns the **method**.
- **Dict access** (`lz['items']`) always returns the **config value**.

This is a deliberate trade-off.  When in doubt, use `[]` — it always works.
The same rule applies to writes: `lz.items = [...]` still stores the config
value, but reading `lz.items` returns the method.

## Error Handling

| Exception | When |
|-----------|------|
| `ConfigNotFoundError` | No config file found and no `--key value` arguments present |
| `ConfigParseError` | File found but cannot be parsed |
| `ConfigKeyError` | Accessing a non-existent key |

All exceptions inherit from `ConfigError` and can be imported directly:

```python
from lzconfig import ConfigError, ConfigNotFoundError, ConfigKeyError
```

## License

MIT
