Metadata-Version: 2.4
Name: crossconfig
Version: 0.0.5
Summary: Simple cross-platform settings/configuration package.
Project-URL: Homepage, https://pycelium.com
Project-URL: Repository, https://github.com/k98kurz/crossconfig
Project-URL: Bug Tracker, https://github.com/k98kurz/crossconfig/issues
Author-email: k98kurz <k98kurz@gmail.com>
License: Copyright (c) 2026 Jonathan Voss (k98kurz)
        
        Permission to use, copy, modify, and/or distribute this software
        for any purpose with or without fee is hereby granted, provided
        that the above copyright notice and this permission notice appear in
        all copies.
        
        THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
        WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
        WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
        AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
        CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
        OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
        NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
        CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: ISC License (ISCL)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Other/Nonlisted Topic
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# CrossConfig

This package provides a simple cross-platform configuration system for storing,
loading, and updating settings for an application or project. This package uses
only standard library modules, and it encodes settings in JSON.

## Installation

```bash
pip install crossconfig
```

## Status

Issues can be tracked [here](https://github.com/k98kurz/crossconfig/issues).
Changelog can be found
[here](https://github.com/k98kurz/crossconfig/blob/master/changelog.md)

## Usage

Usage is simple: import the `get_config` function, and call it with the name of
your application. This will return a config object that you can use to load,
save, and get settings. If you want to use a portable config, pass
`portable=True` to the function, and the settings file will be stored in the
current working directory instead of the user's home directory.

Full documentation generated by [autodox](https://pypi.org/project/autodox) can
be found [here](https://github.com/k98kurz/crossconfig/blob/master/docs.md).

<details>
<summary>Example</summary>

```python
from crossconfig import get_config

# choose whether to load the config for the current user or a portable config
portable = True

# load the config
config = get_config("my_app_name", portable=portable)
config.load()

# get a path for a subdirectory
subdir_path = config.path("subdir")

# set a setting
config.set("my_setting", "my_value")

# set a nested setting
config.set(["section", "key"], "nested_value")

# save and reload the config
config.save()
[config.unset(key) for key in config.list()]
assert len(config.list()) == 0
config.load()

# get a setting
assert config.get("my_setting") == "my_value"

# unset a setting
config.unset("my_setting")

# save the config
config.save()
```
</details>

### Nested Access with List Keys

For hierarchical settings, use a list of key parts instead of a string:

```python
from crossconfig import get_config

config = get_config("my_app")
config.load()

# Set nested values (creates intermediate dicts automatically)
config.set(["database", "host"], "localhost")
config.set(["database", "port"], 5432)
config.set(["ui", "theme", "dark"], True)

# Get nested values
assert config.get(["database", "host"]) == "localhost"
assert config.get(["ui", "theme"]) == {"dark": True}

# Missing paths return None (or your default)
assert config.get(["missing", "path"], "default") == "default"
```

Values can be any JSON-compatible type: `bool`, `str`, `int`, `float`, `list`, or `dict`.

### Event Notifications

The config object supports a publish/subscribe event system for reacting to
configuration changes. Automatic events include the following: `('set', *key)`,
`('unset', *key)`, `'save'`, and `'load'`.

<details>
<summary>Basic Event Subscription</summary>

You can subscribe to automatic events that fire when settings are set, unset,
saved, or loaded:

```python
from crossconfig import get_config

config = get_config("my_app")

# Define a listener function
def on_setting_change(event, data):
    print(f"Event: {event}, Data: {data}")

# Subscribe to a specific key being set/unset
config.subscribe(("set", "theme"), on_setting_change)
config.subscribe(("unset", "theme"), on_setting_change)

# Set/unset a setting - this will trigger the listener
config.set("theme", "dark")  # Prints: Event: ('set', 'theme'), Data: dark
config.unset("theme")  # Prints: Event: ('unset', 'theme'), Data: None

# Subscribe to file operation events
config.subscribe("load", on_setting_change)
config.subscribe("save", on_setting_change)

# Save/load config - triggers the listener
config.save()  # Event: save, Data: None
config.load()  # Event: load, Data: {...settings...}

# Unsubscribe from an event
config.unsubscribe(("set", "theme"), on_setting_change)
```
</details>

<details>
<summary>Wildcard Subscriptions</summary>

Wildcards let you subscribe to multiple events at once:

```python
config = get_config("my_app")

# Subscribe to ALL set/unset events
config.subscribe(("set", "*"), lambda e, d: print(f"Setting changed: {e}"))
config.subscribe(("unset", "*"), lambda e, d: print(f"Setting removed: {e}"))

# Subscribe to ANY event on a specific key (set or unset)
config.subscribe(("*", "theme"), lambda e, d: print(f"Theme changed: {e}, {d}"))

# Subscribe to ALL events (wildcard of wildcards)
config.subscribe("*", lambda e, d: print(f"Any event: {e}"))

config.set("theme", "dark")
config.set("language", "en")
config.unset("theme")
```
</details>

<details>
<summary>Hierarchical Event Bubbling</summary>

Nested events bubble to all parent listeners:

```python
config = get_config("my_app")

# Listen to all changes under "database" section
config.subscribe(("*", "database"), lambda e, d: print(f"DB: {e}"))

# This fires the listener (nested under "database")
config.set(["database", "host"], "localhost")  # DB: ('set', 'database', 'host')
config.set(["database", "port"], 5432)  # DB: ('set', 'database', 'port')

# This also fires the listener (direct change to "database")
config.set("database", {"host": "localhost"})  # DB: ('set', 'database')
```
</details>

<details>
<summary>Custom Event Publishing & Unsubscribing</summary>

You can publish custom events and remove subscriptions:

```python
config = get_config("my_app")

listener = lambda e, d: print(f"Event: {e}, Data: {d}")

# Subscribe to a custom event or event family
config.subscribe("custom_event", listener)
config.subscribe(("custom_event",), listener)
config.subscribe(("*", "scope"), listener)

# Publish a custom event
config.publish("custom_event", {"message": "hello"})
# Prints: Event: custom_event, Data: {'message': 'hello'}

# Hiearchical events work with the ("custom_event",) subscription
config.publish(("custom_event", "something"), "secret message")
# Prints: Event: ('custom_event', 'something'), Data: secret message

# Wildstar ("*", "scope") listener also works hierarchically
config.publish(("verb", "scope", "thing"), 123)
# Prints: Event: ('verb', 'scope', 'thing'), Data: 123

# Unsubscribe the listener
config.unsubscribe("custom_event", listener)

# Publishing now won't trigger the listener
config.publish("custom_event", {"message": "hello again"})
```
</details>

### Notes

- The `load` method will return a JSON decode error if the config file is not
  valid JSON. If it loads successfully, it will return `None`.
- The `load` method publishes a `'load'` event with the loaded settings or a
  `JSONDecodeError` on failure.
- The `save` method publishes a `'save'` event after writing to file.
- There is no lock for multi-threaded access to the config object or file.
  Calling `save` or `load` in a multi-threaded environment may result in a race
  condition.
- If a setting is set in one instance of the config object, it will be
  reflected in all other instances of the config object retrieved with the same
  call to `get_config` within the same process.
- This may not follow the Windows- or MacOS-specific conventions for the current
  year. The goal is to make something that works regardless. However, if the
  behavior of `os.expanduser` or `os.getcwd` changes, this may break in the
  future.

## More Resources

Check out the [Pycelium discord server](https://discord.gg/b2QFEJDX69). If you
experience a problem, please discuss it on the Discord server. All suggestions
for improvement are also welcome, and the best place for that is also Discord.
If you do not use Discord, open an issue or discussion thread on Github.

## Testing

To test, clone the repo and then execute the test files.

On Windows:

```cmd
python tests\test_base.py
python tests\test_windows.py
```

On civilized OSes:

```bash
find tests/ -name test_*.py -print -exec python {} \;
```

Testing suites are platform-specific, but the tests that should not run on a
given platform will be skipped if their files are run.

There are a total of 42 tests: 32 tests of the base class methods; 5 tests for
POSIX systems; and 5 tests for Windows.

(Platform-dependent test suites only run on the appropriate platforms.)

## License

Copyright (c) 2026 Jonathan Voss (k98kurz)

Permission to use, copy, modify, and/or distribute this software
for any purpose with or without fee is hereby granted, provided
that the above copyright notice and this permission notice appear in
all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
