kiarina-utils-common 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,39 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.so
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .ruff_cache/
9
+ .mypy_cache/
10
+ .pytest_cache/
11
+ .coverage
12
+ coverage.xml
13
+ htmlcov/
14
+
15
+ # uv
16
+ .uv_cache/
17
+
18
+ # Virtual environments & config
19
+ .venv/
20
+ .env
21
+
22
+ # IDE
23
+ .vscode/
24
+ *.code-workspace
25
+
26
+ # OS
27
+ .DS_Store
28
+
29
+ # Project specific
30
+ *.log
31
+ tmp/
32
+ ai.yaml
33
+
34
+ # Test data
35
+ tests/data/large/
36
+
37
+ # mise tasks (always include)
38
+ !mise-tasks/
39
+ !mise-tasks/**
@@ -0,0 +1,39 @@
1
+ # Changelog
2
+
3
+ All notable changes to the kiarina-utils-common package will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [1.0.0] - 2025-09-09
11
+
12
+ ### Added
13
+ - Comprehensive README.md with usage examples and API documentation
14
+ - Enhanced pyproject.toml with proper metadata, classifiers, and project URLs
15
+ - CHANGELOG.md for tracking version changes
16
+
17
+ ### Changed
18
+ - Improved package documentation and metadata
19
+
20
+ ## [0.1.0] - 2025-01-09
21
+
22
+ ### Added
23
+ - Initial release of kiarina-utils-common
24
+ - `parse_config_string` function for parsing configuration strings
25
+ - Support for nested keys using dot notation
26
+ - Support for array indices in configuration strings
27
+ - Automatic type conversion (bool, int, float, str)
28
+ - Flag support (keys without values)
29
+ - Customizable separators for different parsing needs
30
+ - Comprehensive test suite with pytest
31
+ - Type hints and py.typed marker for full typing support
32
+
33
+ ### Features
34
+ - Parse configuration strings like `"cache.enabled:true,db.port:5432"`
35
+ - Support for nested structures: `{"cache": {"enabled": True}, "db": {"port": 5432}}`
36
+ - Array index support: `"items.0:first,items.1:second"` → `{"items": ["first", "second"]}`
37
+ - Flag functionality: `"debug,verbose"` → `{"debug": None, "verbose": None}`
38
+ - Custom separators: configurable item, key-value, and nested separators
39
+ - Automatic type detection and conversion for common data types
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: kiarina-utils-common
3
+ Version: 1.0.0
4
+ Summary: Common utility functions for the kiarina namespace packages
5
+ Project-URL: Homepage, https://github.com/kiarina/kiarina-python
6
+ Project-URL: Repository, https://github.com/kiarina/kiarina-python
7
+ Project-URL: Issues, https://github.com/kiarina/kiarina-python/issues
8
+ Project-URL: Changelog, https://github.com/kiarina/kiarina-python/blob/main/packages/kiarina-utils-common/CHANGELOG.md
9
+ Project-URL: Documentation, https://github.com/kiarina/kiarina-python/tree/main/packages/kiarina-utils-common#readme
10
+ Author-email: kiarina <kiarinadawa@gmail.com>
11
+ Maintainer-email: kiarina <kiarinadawa@gmail.com>
12
+ License: MIT
13
+ Keywords: common,config,parser,utilities
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Topic :: Utilities
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.12
25
+ Description-Content-Type: text/markdown
26
+
27
+ # kiarina-utils-common
28
+
29
+ [![PyPI version](https://badge.fury.io/py/kiarina-utils-common.svg)](https://badge.fury.io/py/kiarina-utils-common)
30
+ [![Python](https://img.shields.io/pypi/pyversions/kiarina-utils-common.svg)](https://pypi.org/project/kiarina-utils-common/)
31
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
32
+
33
+ Common utility functions for the kiarina namespace packages.
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install kiarina-utils-common
39
+ ```
40
+
41
+ ## Features
42
+
43
+ ### Configuration String Parser
44
+
45
+ Parse configuration strings into nested dictionaries with automatic type conversion.
46
+
47
+ ```python
48
+ from kiarina.utils.common import parse_config_string
49
+
50
+ # Basic usage
51
+ config = parse_config_string("cache.enabled:true,db.port:5432")
52
+ # Result: {"cache": {"enabled": True}, "db": {"port": 5432}}
53
+
54
+ # Flag support (no value)
55
+ config = parse_config_string("debug,verbose,cache.enabled:true")
56
+ # Result: {"debug": None, "verbose": None, "cache": {"enabled": True}}
57
+
58
+ # Array indices support
59
+ config = parse_config_string("items.0:first,items.1:second")
60
+ # Result: {"items": ["first", "second"]}
61
+
62
+ # Custom separators
63
+ config = parse_config_string(
64
+ "key1=val1;key2.sub=42",
65
+ separator=";",
66
+ key_value_separator="="
67
+ )
68
+ # Result: {"key1": "val1", "key2": {"sub": 42}}
69
+ ```
70
+
71
+ #### Type Conversion
72
+
73
+ Values are automatically converted to appropriate types:
74
+
75
+ - `"true"`, `"True"` → `bool(True)`
76
+ - `"false"`, `"False"` → `bool(False)`
77
+ - Numeric strings (`"1"`, `"0"`, `"-5"`, `"3.14"`) → `int` or `float`
78
+ - Other strings → `str`
79
+
80
+ #### Nested Keys
81
+
82
+ Use dot notation for nested structures:
83
+
84
+ ```python
85
+ config = parse_config_string("database.host:localhost,database.port:5432")
86
+ # Result: {"database": {"host": "localhost", "port": 5432}}
87
+ ```
88
+
89
+ #### Array Indices
90
+
91
+ Use numeric keys for array structures:
92
+
93
+ ```python
94
+ config = parse_config_string("users.0.name:Alice,users.0.age:30,users.1.name:Bob")
95
+ # Result: {"users": [{"name": "Alice", "age": 30}, {"name": "Bob"}]}
96
+ ```
97
+
98
+ ## API Reference
99
+
100
+ ### `parse_config_string(config_str, *, separator=",", key_value_separator=":", nested_separator=".")`
101
+
102
+ Parse configuration string into nested dictionary.
103
+
104
+ **Parameters:**
105
+ - `config_str` (str): Configuration string to parse
106
+ - `separator` (str, optional): Item separator. Default: `","`
107
+ - `key_value_separator` (str, optional): Key-value separator. Default: `":"`
108
+ - `nested_separator` (str, optional): Nested key separator. Default: `"."`
109
+
110
+ **Returns:**
111
+ - `dict[str, Any]`: Parsed configuration dictionary
112
+
113
+ **Examples:**
114
+
115
+ ```python
116
+ # Basic usage
117
+ parse_config_string("key1:value1,key2:value2")
118
+ # {"key1": "value1", "key2": "value2"}
119
+
120
+ # Nested keys
121
+ parse_config_string("cache.enabled:true,db.port:5432")
122
+ # {"cache": {"enabled": True}, "db": {"port": 5432}}
123
+
124
+ # Flags (no value)
125
+ parse_config_string("debug,verbose")
126
+ # {"debug": None, "verbose": None}
127
+
128
+ # Custom separators
129
+ parse_config_string("a=1;b=2", separator=";", key_value_separator="=")
130
+ # {"a": 1, "b": 2}
131
+ ```
132
+
133
+ ## License
134
+
135
+ This project is licensed under the MIT License - see the [LICENSE](../../LICENSE) file for details.
136
+
137
+ ## Contributing
138
+
139
+ This is a personal project by kiarina. While issues and pull requests are welcome, please note that this is primarily developed for personal use.
140
+
141
+ ## Related Packages
142
+
143
+ - [kiarina-utils-file](../kiarina-utils-file/): File operation utilities
144
+ - [kiarina-llm](../kiarina-llm/): LLM-related utilities
@@ -0,0 +1,118 @@
1
+ # kiarina-utils-common
2
+
3
+ [![PyPI version](https://badge.fury.io/py/kiarina-utils-common.svg)](https://badge.fury.io/py/kiarina-utils-common)
4
+ [![Python](https://img.shields.io/pypi/pyversions/kiarina-utils-common.svg)](https://pypi.org/project/kiarina-utils-common/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ Common utility functions for the kiarina namespace packages.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install kiarina-utils-common
13
+ ```
14
+
15
+ ## Features
16
+
17
+ ### Configuration String Parser
18
+
19
+ Parse configuration strings into nested dictionaries with automatic type conversion.
20
+
21
+ ```python
22
+ from kiarina.utils.common import parse_config_string
23
+
24
+ # Basic usage
25
+ config = parse_config_string("cache.enabled:true,db.port:5432")
26
+ # Result: {"cache": {"enabled": True}, "db": {"port": 5432}}
27
+
28
+ # Flag support (no value)
29
+ config = parse_config_string("debug,verbose,cache.enabled:true")
30
+ # Result: {"debug": None, "verbose": None, "cache": {"enabled": True}}
31
+
32
+ # Array indices support
33
+ config = parse_config_string("items.0:first,items.1:second")
34
+ # Result: {"items": ["first", "second"]}
35
+
36
+ # Custom separators
37
+ config = parse_config_string(
38
+ "key1=val1;key2.sub=42",
39
+ separator=";",
40
+ key_value_separator="="
41
+ )
42
+ # Result: {"key1": "val1", "key2": {"sub": 42}}
43
+ ```
44
+
45
+ #### Type Conversion
46
+
47
+ Values are automatically converted to appropriate types:
48
+
49
+ - `"true"`, `"True"` → `bool(True)`
50
+ - `"false"`, `"False"` → `bool(False)`
51
+ - Numeric strings (`"1"`, `"0"`, `"-5"`, `"3.14"`) → `int` or `float`
52
+ - Other strings → `str`
53
+
54
+ #### Nested Keys
55
+
56
+ Use dot notation for nested structures:
57
+
58
+ ```python
59
+ config = parse_config_string("database.host:localhost,database.port:5432")
60
+ # Result: {"database": {"host": "localhost", "port": 5432}}
61
+ ```
62
+
63
+ #### Array Indices
64
+
65
+ Use numeric keys for array structures:
66
+
67
+ ```python
68
+ config = parse_config_string("users.0.name:Alice,users.0.age:30,users.1.name:Bob")
69
+ # Result: {"users": [{"name": "Alice", "age": 30}, {"name": "Bob"}]}
70
+ ```
71
+
72
+ ## API Reference
73
+
74
+ ### `parse_config_string(config_str, *, separator=",", key_value_separator=":", nested_separator=".")`
75
+
76
+ Parse configuration string into nested dictionary.
77
+
78
+ **Parameters:**
79
+ - `config_str` (str): Configuration string to parse
80
+ - `separator` (str, optional): Item separator. Default: `","`
81
+ - `key_value_separator` (str, optional): Key-value separator. Default: `":"`
82
+ - `nested_separator` (str, optional): Nested key separator. Default: `"."`
83
+
84
+ **Returns:**
85
+ - `dict[str, Any]`: Parsed configuration dictionary
86
+
87
+ **Examples:**
88
+
89
+ ```python
90
+ # Basic usage
91
+ parse_config_string("key1:value1,key2:value2")
92
+ # {"key1": "value1", "key2": "value2"}
93
+
94
+ # Nested keys
95
+ parse_config_string("cache.enabled:true,db.port:5432")
96
+ # {"cache": {"enabled": True}, "db": {"port": 5432}}
97
+
98
+ # Flags (no value)
99
+ parse_config_string("debug,verbose")
100
+ # {"debug": None, "verbose": None}
101
+
102
+ # Custom separators
103
+ parse_config_string("a=1;b=2", separator=";", key_value_separator="=")
104
+ # {"a": 1, "b": 2}
105
+ ```
106
+
107
+ ## License
108
+
109
+ This project is licensed under the MIT License - see the [LICENSE](../../LICENSE) file for details.
110
+
111
+ ## Contributing
112
+
113
+ This is a personal project by kiarina. While issues and pull requests are welcome, please note that this is primarily developed for personal use.
114
+
115
+ ## Related Packages
116
+
117
+ - [kiarina-utils-file](../kiarina-utils-file/): File operation utilities
118
+ - [kiarina-llm](../kiarina-llm/): LLM-related utilities
@@ -0,0 +1,41 @@
1
+ [project]
2
+ name = "kiarina-utils-common"
3
+ version = "1.0.0"
4
+ description = "Common utility functions for the kiarina namespace packages"
5
+ readme = "README.md"
6
+ license = { text = "MIT" }
7
+ authors = [
8
+ { name = "kiarina", email = "kiarinadawa@gmail.com" }
9
+ ]
10
+ maintainers = [
11
+ { name = "kiarina", email = "kiarinadawa@gmail.com" }
12
+ ]
13
+ keywords = ["utilities", "config", "parser", "common"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: Software Development :: Libraries :: Python Modules",
23
+ "Topic :: Utilities",
24
+ "Typing :: Typed",
25
+ ]
26
+ requires-python = ">=3.12"
27
+ dependencies = []
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/kiarina/kiarina-python"
31
+ Repository = "https://github.com/kiarina/kiarina-python"
32
+ Issues = "https://github.com/kiarina/kiarina-python/issues"
33
+ Changelog = "https://github.com/kiarina/kiarina-python/blob/main/packages/kiarina-utils-common/CHANGELOG.md"
34
+ Documentation = "https://github.com/kiarina/kiarina-python/tree/main/packages/kiarina-utils-common#readme"
35
+
36
+ [build-system]
37
+ requires = ["hatchling"]
38
+ build-backend = "hatchling.build"
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["src/kiarina"]
@@ -0,0 +1,10 @@
1
+ # pip install kiarina-utils-common
2
+ from importlib.metadata import version
3
+
4
+ from ._parse_config_string import parse_config_string
5
+
6
+ __version__ = version("kiarina-utils-common")
7
+
8
+ __all__ = [
9
+ "parse_config_string",
10
+ ]
@@ -0,0 +1,216 @@
1
+ from typing import Any
2
+
3
+
4
+ def parse_config_string(
5
+ config_str: str,
6
+ *,
7
+ separator: str = ",",
8
+ key_value_separator: str = ":",
9
+ nested_separator: str = ".",
10
+ ) -> dict[str, Any]:
11
+ """
12
+ Parse configuration string into nested dictionary
13
+
14
+ When key contains nested_separator, it will be treated as nested keys:
15
+ - hoge.fuga:30 → {"hoge": {"fuga": 30}}
16
+
17
+ When key_value_separator is not found, the key is treated as a flag with None value:
18
+ - debug,enabled:true → {"debug": None, "enabled": True}
19
+
20
+ Values are automatically converted to appropriate types:
21
+ - "true", "True" → bool(True)
22
+ - "false", "False" → bool(False)
23
+ - Numeric strings ("1", "0", "-5", etc.) → int/float
24
+ - Others → str
25
+
26
+ Args:
27
+ config_str: Configuration string to parse
28
+ separator: Item separator (default: ",")
29
+ key_value_separator: Key-value separator (default: ":")
30
+ nested_separator: Nested key separator (default: ".")
31
+
32
+ Returns:
33
+ Parsed configuration dictionary
34
+
35
+ Examples:
36
+ >>> parse_config_string("cache.enabled:true,db.port:5432")
37
+ {"cache": {"enabled": True}, "db": {"port": 5432}}
38
+
39
+ >>> parse_config_string("debug,verbose,cache.enabled:true")
40
+ {"debug": None, "verbose": None, "cache": {"enabled": True}}
41
+
42
+ >>> parse_config_string("key1=val1;key2.sub=42", separator=";", key_value_separator="=")
43
+ {"key1": "val1", "key2": {"sub": 42}}
44
+ """
45
+ if not config_str:
46
+ return {}
47
+
48
+ result: dict[str, Any] = {}
49
+
50
+ for option in config_str.split(separator):
51
+ option = option.strip()
52
+
53
+ if not option:
54
+ continue
55
+
56
+ if key_value_separator in option:
57
+ key, value = option.split(key_value_separator, 1)
58
+ key = key.strip()
59
+ value = value.strip()
60
+
61
+ converted_value = _convert_value(value)
62
+
63
+ # Handle nested keys
64
+ _set_nested_value(result, key, converted_value, nested_separator)
65
+ else:
66
+ # No key_value_separator found, treat as flag with None value
67
+ _set_nested_value(result, option, None, nested_separator)
68
+
69
+ return result
70
+
71
+
72
+ def _convert_value(value: str) -> Any:
73
+ """
74
+ Convert string value to appropriate type
75
+
76
+ Args:
77
+ value: String value to convert
78
+
79
+ Returns:
80
+ Converted value
81
+ """
82
+ try:
83
+ # Boolean values (only true and false, 1 and 0 are treated as numbers)
84
+ if value.lower() == "true":
85
+ return True
86
+
87
+ elif value.lower() == "false":
88
+ return False
89
+
90
+ # Integer case
91
+ elif value.lstrip("-").isdigit():
92
+ return int(value)
93
+
94
+ # Floating point case
95
+ else:
96
+ try:
97
+ float_value = float(value)
98
+
99
+ # Don't convert to integer if it can be represented as integer (respect original value)
100
+ if "." in value:
101
+ return float_value
102
+ else:
103
+ # If integer format but not caught above, treat as string
104
+ return value
105
+
106
+ except ValueError:
107
+ # String case
108
+ return value
109
+
110
+ except (ValueError, TypeError):
111
+ # If conversion fails, save as string
112
+ return value
113
+
114
+
115
+ def _set_nested_value(
116
+ target: dict[str, Any], key: str, value: Any, separator: str = "."
117
+ ) -> None:
118
+ """
119
+ Set value with nested keys (supports array indices)
120
+
121
+ When a key is a numeric string, it's treated as an array index:
122
+ - items.0:foo → {"items": ["foo"]}
123
+ - users.0.name:Alice → {"users": [{"name": "Alice"}]}
124
+
125
+ Args:
126
+ target: Target dictionary to set the value
127
+ key: Key (supports dot notation and array indices)
128
+ value: Value to set
129
+ separator: Nested key separator (default: ".")
130
+ """
131
+ keys = key.split(separator)
132
+
133
+ current: dict[str, Any] | list[Any] = target
134
+
135
+ # Navigate/create nested structure up to the last key
136
+ for i, k in enumerate(keys[:-1]):
137
+ next_key = keys[i + 1]
138
+
139
+ if _is_array_index(k):
140
+ index = int(k)
141
+
142
+ if not isinstance(current, list):
143
+ raise ValueError(
144
+ f"Cannot access array index {index} on non-array value"
145
+ )
146
+
147
+ while len(current) <= index:
148
+ current.append(None)
149
+
150
+ if _is_array_index(next_key):
151
+ if current[index] is None:
152
+ current[index] = []
153
+ elif not isinstance(current[index], list):
154
+ current[index] = []
155
+
156
+ else:
157
+ if current[index] is None:
158
+ current[index] = {}
159
+ elif not isinstance(current[index], dict):
160
+ current[index] = {}
161
+
162
+ current = current[index]
163
+
164
+ else:
165
+ if not isinstance(current, dict):
166
+ raise ValueError(f"Cannot access key '{k}' on non-dict value")
167
+
168
+ if _is_array_index(next_key):
169
+ if k not in current:
170
+ current[k] = []
171
+ elif not isinstance(current[k], list):
172
+ current[k] = []
173
+
174
+ current = current[k]
175
+
176
+ else:
177
+ if k not in current:
178
+ current[k] = {}
179
+ elif not isinstance(current[k], dict):
180
+ current[k] = {}
181
+
182
+ current = current[k]
183
+
184
+ # Set the value for the last key
185
+ last_key = keys[-1]
186
+
187
+ if _is_array_index(last_key):
188
+ index = int(last_key)
189
+
190
+ if not isinstance(current, list):
191
+ raise ValueError(f"Cannot set array index {index} on non-array value")
192
+
193
+ # Extend list if necessary (fill with None)
194
+ while len(current) <= index:
195
+ current.append(None)
196
+
197
+ current[index] = value
198
+
199
+ else:
200
+ if not isinstance(current, dict):
201
+ raise ValueError(f"Cannot set key '{last_key}' on non-dict value")
202
+
203
+ current[last_key] = value
204
+
205
+
206
+ def _is_array_index(key: str) -> bool:
207
+ """
208
+ Check if a key represents an array index
209
+
210
+ Args:
211
+ key: Key to check
212
+
213
+ Returns:
214
+ True if key is a non-negative integer string
215
+ """
216
+ return key.isdigit()
@@ -0,0 +1,296 @@
1
+ import pytest
2
+
3
+ from kiarina.utils.common import parse_config_string
4
+
5
+
6
+ def test_parse_config_string_empty():
7
+ """Test empty configuration string"""
8
+ result = parse_config_string("")
9
+ assert result == {}
10
+
11
+
12
+ @pytest.mark.parametrize(
13
+ "config_str,expected",
14
+ [
15
+ # Basic key-value pairs
16
+ ("key1:value1,key2:value2", {"key1": "value1", "key2": "value2"}),
17
+ ("single:option", {"single": "option"}),
18
+ # Flag functionality (no key_value_separator)
19
+ ("debug", {"debug": None}),
20
+ ("debug,verbose", {"debug": None, "verbose": None}),
21
+ ("debug,enabled:true", {"debug": None, "enabled": True}),
22
+ (
23
+ "debug,verbose,cache.enabled:true",
24
+ {"debug": None, "verbose": None, "cache": {"enabled": True}},
25
+ ),
26
+ # Type conversion
27
+ ("bool_true:true", {"bool_true": True}),
28
+ ("bool_false:false", {"bool_false": False}),
29
+ ("bool_True:True", {"bool_True": True}),
30
+ ("bool_False:False", {"bool_False": False}),
31
+ ("int_pos:123", {"int_pos": 123}),
32
+ ("int_neg:-456", {"int_neg": -456}),
33
+ ("int_zero:0", {"int_zero": 0}),
34
+ ("float_pos:3.14", {"float_pos": 3.14}),
35
+ ("float_neg:-2.5", {"float_neg": -2.5}),
36
+ ("str_val:hello", {"str_val": "hello"}),
37
+ # Mixed types
38
+ (
39
+ "bool:true,int:42,float:3.14,str:hello",
40
+ {"bool": True, "int": 42, "float": 3.14, "str": "hello"},
41
+ ),
42
+ ],
43
+ )
44
+ def test_parse_config_string_basic(config_str, expected):
45
+ """Test basic configuration string parsing and type conversion"""
46
+ result = parse_config_string(config_str)
47
+ assert result == expected
48
+
49
+
50
+ @pytest.mark.parametrize(
51
+ "config_str,expected",
52
+ [
53
+ # Simple nesting
54
+ ("cache.enabled:true", {"cache": {"enabled": True}}),
55
+ ("db.port:5432", {"db": {"port": 5432}}),
56
+ # Nested flags
57
+ ("app.debug", {"app": {"debug": None}}),
58
+ ("app.debug,app.verbose", {"app": {"debug": None, "verbose": None}}),
59
+ ("app.debug,app.port:8080", {"app": {"debug": None, "port": 8080}}),
60
+ # Multiple nested keys
61
+ (
62
+ "cache.enabled:true,cache.ttl:3600",
63
+ {"cache": {"enabled": True, "ttl": 3600}},
64
+ ),
65
+ # Different sections
66
+ (
67
+ "cache.enabled:true,db.host:localhost,db.port:5432",
68
+ {"cache": {"enabled": True}, "db": {"host": "localhost", "port": 5432}},
69
+ ),
70
+ # Deep nesting
71
+ ("a.b.c.d:value1", {"a": {"b": {"c": {"d": "value1"}}}}),
72
+ # Mixed depth nesting with flags
73
+ (
74
+ "a.b.c.d:value1,a.b.e:value2,a.f:value3,a.b.flag",
75
+ {
76
+ "a": {
77
+ "b": {"c": {"d": "value1"}, "e": "value2", "flag": None},
78
+ "f": "value3",
79
+ }
80
+ },
81
+ ),
82
+ ],
83
+ )
84
+ def test_parse_config_string_nested(config_str, expected):
85
+ """Test nested key parsing with dot notation"""
86
+ result = parse_config_string(config_str)
87
+ assert result == expected
88
+
89
+
90
+ @pytest.mark.parametrize(
91
+ "config_str,expected",
92
+ [
93
+ # Whitespace around options
94
+ (" key1:value1 , key2:value2 ", {"key1": "value1", "key2": "value2"}),
95
+ # Whitespace around key-value pairs
96
+ ("key1 : value1", {"key1": "value1"}),
97
+ # Whitespace with nested keys
98
+ (" key1.nested : 42 ", {"key1": {"nested": 42}}),
99
+ # Mixed whitespace
100
+ (
101
+ " cache.enabled : true , db.port : 5432 ",
102
+ {"cache": {"enabled": True}, "db": {"port": 5432}},
103
+ ),
104
+ ],
105
+ )
106
+ def test_parse_config_string_whitespace(config_str, expected):
107
+ """Test whitespace handling in keys and values"""
108
+ result = parse_config_string(config_str)
109
+ assert result == expected
110
+
111
+
112
+ @pytest.mark.parametrize(
113
+ "config_str,separator,key_value_separator,nested_separator,expected",
114
+ [
115
+ # Custom item separator
116
+ (
117
+ "key1=value1;key2=value2",
118
+ ";",
119
+ "=",
120
+ ".",
121
+ {"key1": "value1", "key2": "value2"},
122
+ ),
123
+ # Custom key-value separator with nesting
124
+ (
125
+ "key1=value1;key2.sub=42",
126
+ ";",
127
+ "=",
128
+ ".",
129
+ {"key1": "value1", "key2": {"sub": 42}},
130
+ ),
131
+ # Custom nested separator
132
+ (
133
+ "key1/sub:value1,key2/nested/deep:42",
134
+ ",",
135
+ ":",
136
+ "/",
137
+ {"key1": {"sub": "value1"}, "key2": {"nested": {"deep": 42}}},
138
+ ),
139
+ # All custom separators
140
+ (
141
+ "app|debug=true;app|port=8080",
142
+ ";",
143
+ "=",
144
+ "|",
145
+ {"app": {"debug": True, "port": 8080}},
146
+ ),
147
+ ],
148
+ )
149
+ def test_parse_config_string_custom_separators(
150
+ config_str, separator, key_value_separator, nested_separator, expected
151
+ ):
152
+ """Test custom separators"""
153
+ result = parse_config_string(
154
+ config_str,
155
+ separator=separator,
156
+ key_value_separator=key_value_separator,
157
+ nested_separator=nested_separator,
158
+ )
159
+ assert result == expected
160
+
161
+
162
+ @pytest.mark.parametrize(
163
+ "config_str,expected",
164
+ [
165
+ # Options without key-value separator are now treated as flags
166
+ ("debug,valid:value", {"debug": None, "valid": "value"}),
167
+ ("flag1,key1:value1,flag2", {"flag1": None, "key1": "value1", "flag2": None}),
168
+ # Empty options (should be ignored)
169
+ ("key1:value1,,key2:value2", {"key1": "value1", "key2": "value2"}),
170
+ # Mixed flags and values with empty options
171
+ ("debug,,verbose,port:8080,", {"debug": None, "verbose": None, "port": 8080}),
172
+ # Whitespace handling with flags
173
+ (
174
+ " debug , verbose , port : 8080 ",
175
+ {"debug": None, "verbose": None, "port": 8080},
176
+ ),
177
+ ],
178
+ )
179
+ def test_parse_config_string_edge_cases(config_str, expected):
180
+ """Test edge cases and error handling"""
181
+ result = parse_config_string(config_str)
182
+ assert result == expected
183
+
184
+
185
+ @pytest.mark.parametrize(
186
+ "config_str,expected",
187
+ [
188
+ # Simple array
189
+ ("items.0:foo,items.1:bar,items.2:baz", {"items": ["foo", "bar", "baz"]}),
190
+ ("tags.0:python,tags.1:rust", {"tags": ["python", "rust"]}),
191
+ # Array with type conversion
192
+ ("numbers.0:1,numbers.1:2,numbers.2:3", {"numbers": [1, 2, 3]}),
193
+ ("mixed.0:1,mixed.1:true,mixed.2:hello", {"mixed": [1, True, "hello"]}),
194
+ # Sparse array (with None gaps)
195
+ ("sparse.0:first,sparse.2:third", {"sparse": ["first", None, "third"]}),
196
+ # Nested structure with arrays
197
+ (
198
+ "users.0.name:Alice,users.0.age:30,users.1.name:Bob,users.1.age:25",
199
+ {"users": [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]},
200
+ ),
201
+ # Mixed arrays and regular keys
202
+ (
203
+ "app.name:myapp,app.tags.0:web,app.tags.1:api,app.debug:true",
204
+ {"app": {"name": "myapp", "tags": ["web", "api"], "debug": True}},
205
+ ),
206
+ # Array in nested structure
207
+ (
208
+ "db.servers.0:localhost,db.servers.1:192.168.1.1,db.port:5432",
209
+ {"db": {"servers": ["localhost", "192.168.1.1"], "port": 5432}},
210
+ ),
211
+ # Deep nesting with arrays
212
+ (
213
+ "config.db.hosts.0:primary,config.db.hosts.1:secondary",
214
+ {"config": {"db": {"hosts": ["primary", "secondary"]}}},
215
+ ),
216
+ ],
217
+ )
218
+ def test_parse_config_string_arrays(config_str, expected):
219
+ """Test array support with index notation"""
220
+ result = parse_config_string(config_str)
221
+ assert result == expected
222
+
223
+
224
+ @pytest.mark.parametrize(
225
+ "config_str,expected",
226
+ [
227
+ # Array flags (no values)
228
+ ("items.0,items.1:value", {"items": [None, "value"]}),
229
+ ("flags.0,flags.1,flags.2", {"flags": [None, None, None]}),
230
+ # Mixed flags and arrays
231
+ (
232
+ "debug,items.0:first,verbose,items.1:second",
233
+ {"debug": None, "items": ["first", "second"], "verbose": None},
234
+ ),
235
+ ],
236
+ )
237
+ def test_parse_config_string_array_flags(config_str, expected):
238
+ """Test array support with flag notation"""
239
+ result = parse_config_string(config_str)
240
+ assert result == expected
241
+
242
+
243
+ def test_parse_config_string_array_edge_cases():
244
+ """Test edge cases for array functionality"""
245
+ # Single element array
246
+ result = parse_config_string("items.0:single")
247
+ assert result == {"items": ["single"]}
248
+
249
+ # Large index (creates sparse array)
250
+ result = parse_config_string("items.10:value")
251
+ expected_items = [None] * 10 + ["value"]
252
+ assert result == {"items": expected_items}
253
+
254
+ # Zero index
255
+ result = parse_config_string("items.0:zero")
256
+ assert result == {"items": ["zero"]}
257
+
258
+
259
+ def test_parse_config_string_array_type_conflicts():
260
+ """Test handling of type conflicts with arrays"""
261
+ # When dict key comes first, then array index - dict gets converted to array
262
+ result = parse_config_string("items.key:value,items.0:first")
263
+ # The dict gets converted to array, losing the original key
264
+ assert result == {"items": ["first"]}
265
+
266
+ # When array index comes first, then dict key - array gets converted to dict
267
+ result = parse_config_string("items.0:first,items.key:value")
268
+ # The array gets converted to dict, preserving both values
269
+ assert result == {"items": {"key": "value"}}
270
+
271
+
272
+ def test_parse_config_string_real_world_example():
273
+ """Test real-world application configuration example"""
274
+ config_str = "app.debug:true,app.port:8080,app.name:myapp,db.host:localhost,db.port:5432,db.timeout:30.5,cache.enabled:true,cache.ttl:3600"
275
+
276
+ expected = {
277
+ "app": {"debug": True, "port": 8080, "name": "myapp"},
278
+ "db": {"host": "localhost", "port": 5432, "timeout": 30.5},
279
+ "cache": {"enabled": True, "ttl": 3600},
280
+ }
281
+
282
+ result = parse_config_string(config_str)
283
+ assert result == expected
284
+
285
+
286
+ def test_parse_config_string_real_world_with_arrays():
287
+ """Test real-world configuration with arrays"""
288
+ config_str = "app.name:myapp,app.tags.0:web,app.tags.1:api,db.servers.0:primary,db.servers.1:secondary,db.ports.0:5432,db.ports.1:5433"
289
+
290
+ expected = {
291
+ "app": {"name": "myapp", "tags": ["web", "api"]},
292
+ "db": {"servers": ["primary", "secondary"], "ports": [5432, 5433]},
293
+ }
294
+
295
+ result = parse_config_string(config_str)
296
+ assert result == expected