jpcl 1.1.1__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.
jpcl-1.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Clove Twilight
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
jpcl-1.1.1/PKG-INFO ADDED
@@ -0,0 +1,323 @@
1
+ Metadata-Version: 2.4
2
+ Name: jpcl
3
+ Version: 1.1.1
4
+ Summary: A hybrid JSON/TOML configuration language (.jp files)
5
+ Keywords: config,configuration,parser,toml,json,jp,jpcl
6
+ Author: Clove Twilight
7
+ Author-email: Clove Twilight <admin@doughmination.win>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Topic :: File Formats
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Classifier: Topic :: Text Processing :: Markup
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.14
19
+ Project-URL: Homepage, https://github.com/jpcl-lang/jpcl-py
20
+ Project-URL: Repository, https://github.com/jpcl-lang/jpcl-py
21
+ Project-URL: Issues, https://github.com/jpcl-lang/jpcl-py/issues
22
+ Description-Content-Type: text/markdown
23
+
24
+ # jpcl-py
25
+
26
+ Python package for JPCL
27
+
28
+ [![CI](https://github.com/jpcl-lang/jpcl-py/actions/workflows/ci.yml/badge.svg)](https://github.com/jpcl-lang/jpcl-py/actions/workflows/ci.yml)
29
+ [![PyPI](https://img.shields.io/pypi/v/jpcl)](https://pypi.org/project/jpcl/)
30
+
31
+ **A configuration language that borrows TOML's sections and JSON's nesting.**
32
+
33
+ `.jp` files use `[SECTION]` headers at the top level and `{...}` / `[...]`
34
+ structures inside them. Keys need no quotes, `#` starts a comment, trailing
35
+ commas are fine, and a value is allowed to be *empty*.
36
+
37
+ ```jp
38
+ [SERVER_ID]
39
+ config: {
40
+ disabled_channels:,
41
+ disabled_users: [9892, 82082, 8209]
42
+ }
43
+
44
+ [SERVER_ID_2]
45
+ prefix: "!"
46
+ modules: {
47
+ moderation: true,
48
+ fun: false
49
+ }
50
+ ```
51
+
52
+ ```python
53
+ >>> import jpcl
54
+ >>> jpcl.load("data/servers.jp")
55
+ {'SERVER_ID': {'config': {'disabled_channels': None,
56
+ 'disabled_users': [9892, 82082, 8209]}},
57
+ 'SERVER_ID_2': {'prefix': '!', 'modules': {'moderation': True, 'fun': False}}}
58
+ ```
59
+
60
+ ---
61
+
62
+ ## Why another format
63
+
64
+ JSON has no comments, demands quotes on every key, and rejects a trailing
65
+ comma. TOML has comments and headers, but nesting anything non-trivial means
66
+ either deeply dotted keys or a table per level.
67
+
68
+ `.jp` takes the half of each that suits configuration files people edit by hand:
69
+
70
+ * **Sections for the top level.** `[SERVER_ID]` reads better than another brace.
71
+ * **JSON for everything below it.** Nest objects and arrays as deep as you like.
72
+ * **No ceremony.** Unquoted keys, comments anywhere, trailing commas ignored.
73
+ * **Empty values are legal.** `disabled_channels:,` means the key exists and has
74
+ no value yet — a real state in configs that JSON can only spell as `null`.
75
+
76
+ It is a small, fully specified format with a strict parser, precise error
77
+ messages, and a deterministic writer, so files stay stable when a program
78
+ rewrites them.
79
+
80
+ ```bash
81
+ pip install jpcl # or: uv add jpcl
82
+ ```
83
+
84
+ No runtime dependencies. Python 3.14+.
85
+
86
+ ---
87
+
88
+ ## The format
89
+
90
+ ### Sections
91
+
92
+ A `[NAME]` header opens a root key. Everything below it, until the next header,
93
+ belongs to that section.
94
+
95
+ ```jp
96
+ [SERVER_ID]
97
+ prefix: "!"
98
+ ```
99
+
100
+ Headers may be dotted to nest, and quoted when a name contains a dot:
101
+
102
+ ```jp
103
+ [guild.limits] # -> {"guild": {"limits": {...}}}
104
+ ["weird.name"] # -> {"weird.name": {...}}
105
+ ```
106
+
107
+ Key/value pairs written *before* the first header land at the document root:
108
+
109
+ ```jp
110
+ version: 2
111
+
112
+ [SERVER_ID]
113
+ prefix: "!"
114
+ ```
115
+
116
+ ### Entries
117
+
118
+ An entry is `key: value`. Keys need no quotes; a bare key may contain spaces but
119
+ not brackets, commas or quotes — quote it if it needs those.
120
+
121
+ Entries are separated by a line break, a comma, or both. Trailing and repeated
122
+ commas are accepted:
123
+
124
+ ```jp
125
+ [SERVER_ID]
126
+ a: 1
127
+ b: {x: 1, y: 2,}
128
+ c: [1, 2, 3,]
129
+ ```
130
+
131
+ ### Empty values
132
+
133
+ A key with nothing after the colon parses to `None`:
134
+
135
+ ```jp
136
+ config: {
137
+ disabled_channels:, # -> None
138
+ timeout: # -> None
139
+ }
140
+ ```
141
+
142
+ Because of this, **a value must start on the same line as its `:`**. An opening
143
+ `{` or `[` goes on the colon's line; its contents may then wrap freely.
144
+
145
+ ### Values
146
+
147
+ | Type | Examples |
148
+ | --- | --- |
149
+ | String | `"hello"`, `'hello'`, `hello world` (unquoted) |
150
+ | Integer | `42`, `-7`, `1_000`, `0xff`, `0o755`, `0b1010` |
151
+ | Float | `3.5`, `1e3`, `inf`, `-inf`, `nan` |
152
+ | Boolean | `true`, `false` (case-insensitive, so `True` works too) |
153
+ | Null | `null`, `none`, `nil`, or nothing at all |
154
+ | Object | `{a: 1, b: 2}` |
155
+ | Array | `[1, 2, 3]` |
156
+
157
+ Unquoted values are read as a keyword first, then a number, then a plain string.
158
+ Quote a value if it contains a `#`, a comma, a bracket, or leading/trailing
159
+ whitespace you want to keep.
160
+
161
+ Strings honour the usual escapes — `\n`, `\t`, `\\`, `\"`, `\uXXXX`,
162
+ `\U0001F600`, plus `\` at end of line to continue onto the next.
163
+
164
+ ### Comments
165
+
166
+ `#` runs to the end of the line and is allowed anywhere, including inside
167
+ objects and arrays.
168
+
169
+ ---
170
+
171
+ ## What you can do with it
172
+
173
+ ### Read and write files
174
+
175
+ ```python
176
+ import jpcl
177
+
178
+ data = jpcl.load("data/servers.jp") # -> dict
179
+ jpcl.dump(data, "data/servers.jp") # formatted, atomic write
180
+
181
+ text = jpcl.dumps(data) # -> str
182
+ data = jpcl.loads(text) # -> dict
183
+ ```
184
+
185
+ Writes are atomic by default: the file goes to a temporary neighbour and is
186
+ renamed into place, so a crash or a concurrent reader never sees half a config.
187
+
188
+ Options worth knowing:
189
+
190
+ ```python
191
+ jpcl.load("servers.jp", duplicate_keys="last") # "error" (default), "first", "last"
192
+ jpcl.dumps(data, indent=4, sort_keys=True) # also: width, ensure_ascii
193
+ jpcl.dumps(data, default=str) # convert datetimes and friends
194
+ ```
195
+
196
+ ### Edit a config in place
197
+
198
+ `JPConfig` is a `MutableMapping` that remembers the file it came from.
199
+
200
+ ```python
201
+ from jpcl import JPConfig
202
+
203
+ cfg = JPConfig.load("data/servers.jp", missing_ok=True)
204
+
205
+ cfg["SERVER_ID"]["prefix"] # plain dict access
206
+ cfg.get_path("SERVER_ID.config.disabled_users", []) # never raises
207
+ cfg.set_path("SERVER_ID.config.disabled_users", [9892]) # creates missing sections
208
+ cfg.has_path("SERVER_ID.prefix")
209
+ cfg.section("NEW_SERVER", create=True)["prefix"] = "?"
210
+ cfg.merge({"SERVER_ID": {"modules": {"fun": True}}}) # deep merge
211
+ cfg.save() # atomic, back to its own path
212
+ cfg.reload() # discard in-memory changes
213
+ cfg.to_dict() # deep copy as a plain dict
214
+ ```
215
+
216
+ `missing_ok=True` gives an empty config bound to the path, which is what you
217
+ want for a program that writes its config on first run. Formatting options given
218
+ to the constructor are remembered by `save()`:
219
+
220
+ ```python
221
+ cfg = JPConfig.load("data/servers.jp", indent=4, sort_keys=True)
222
+ ```
223
+
224
+ ### Load a whole folder
225
+
226
+ ```python
227
+ config = jpcl.load_dir("data") # {'servers': {...}, 'roles': {...}}
228
+ guilds = jpcl.load_dir("data/guilds") # {'1234567890': {...}, ...}
229
+ everything = jpcl.load_dir("data", recursive=True)
230
+ ```
231
+
232
+ Each file becomes one key, named after the file.
233
+
234
+ ### Find mistakes quickly
235
+
236
+ Every error derives from `jpcl.JPError`. `JPDecodeError` (a `ValueError`) points
237
+ at the exact character:
238
+
239
+ ```
240
+ data/servers.jp:2:8: expected ':' after key 'prefix', found '"'
241
+ prefix "!"
242
+ ^
243
+ ```
244
+
245
+ It carries `.line`, `.col`, `.pos`, `.filename` and `.raw_message` if you want to
246
+ render the failure yourself. `JPEncodeError` (a `TypeError`) explains what could
247
+ not be serialised — an unsupported type, a non-string key, a circular reference.
248
+
249
+ By default a repeated key is an error rather than a silent overwrite; pass
250
+ `duplicate_keys="first"` or `"last"` if you would rather it not be.
251
+
252
+ ### Work from the shell
253
+
254
+ ```bash
255
+ jpcl check data/*.jp # validate; non-zero exit on failure
256
+ jpcl fmt -w data/servers.jp # reformat in place
257
+ jpcl get data/servers.jp SERVER_ID.prefix # read one value
258
+ jpcl to-json data/servers.jp -o out.json
259
+ jpcl from-json out.json -o data/servers.jp
260
+ ```
261
+
262
+ `python -m jpcl ...` works identically, and `-` reads stdin.
263
+
264
+ ---
265
+
266
+ ## Round trips
267
+
268
+ `dumps` is deterministic, so a file rewritten twice is byte-identical:
269
+
270
+ * every top-level mapping becomes a `[SECTION]`, separated by a blank line;
271
+ * section entries sit one per line, with no separating commas;
272
+ * nested objects always expand across lines, `{}` being the only inline form;
273
+ * arrays stay inline while they fit inside `width` (default 88), then break one
274
+ element per line;
275
+ * `None` is written as an empty value inside mappings (`key:`) and as `null`
276
+ inside arrays, since an array element cannot be empty;
277
+ * insertion order is preserved unless `sort_keys=True`.
278
+
279
+ Two things do not survive a rewrite:
280
+
281
+ * **Comments are dropped.** Rewriting a hand-annotated file loses its notes.
282
+ * **Root-level scalars move above the first section**, because anything after a
283
+ header would be read back as part of that section.
284
+
285
+ ---
286
+
287
+ ## Organising your configs
288
+
289
+ Nothing is enforced, but this layout is what `load_dir` is built for:
290
+
291
+ ```
292
+ your-project/
293
+ ├─ data/
294
+ │ ├─ servers.jp # one file per concern
295
+ │ ├─ roles.jp
296
+ │ ├─ servers.example.jp # committed template, safe to publish
297
+ │ └─ guilds/ # optional: one file per entity
298
+ │ ├─ 1234567890.jp
299
+ │ └─ 9876543210.jp
300
+ └─ src/
301
+ ```
302
+
303
+ A few habits that save pain later:
304
+
305
+ 1. **One file per concern.** A parse error then takes out one feature, not
306
+ everything.
307
+ 2. **Keep live data out of git**, and commit a template instead:
308
+ ```gitignore
309
+ data/*.jp
310
+ !data/*.example.jp
311
+ ```
312
+ 3. **Use IDs as section names.** `[1234567890]` parses to the string key
313
+ `"1234567890"`, and integer keys are stringified on write, so
314
+ `{1234567890: {...}}` round-trips.
315
+ 4. **Write through `JPConfig.save()`** rather than by hand, so an interrupted
316
+ write cannot truncate a live config.
317
+ 5. **Validate in CI** with `jpcl check data/*.jp`.
318
+
319
+ ---
320
+
321
+ ## Licence
322
+
323
+ MIT. Contributing, tests and release process: [CONTRIBUTING.md](CONTRIBUTING.md).
jpcl-1.1.1/README.md ADDED
@@ -0,0 +1,300 @@
1
+ # jpcl-py
2
+
3
+ Python package for JPCL
4
+
5
+ [![CI](https://github.com/jpcl-lang/jpcl-py/actions/workflows/ci.yml/badge.svg)](https://github.com/jpcl-lang/jpcl-py/actions/workflows/ci.yml)
6
+ [![PyPI](https://img.shields.io/pypi/v/jpcl)](https://pypi.org/project/jpcl/)
7
+
8
+ **A configuration language that borrows TOML's sections and JSON's nesting.**
9
+
10
+ `.jp` files use `[SECTION]` headers at the top level and `{...}` / `[...]`
11
+ structures inside them. Keys need no quotes, `#` starts a comment, trailing
12
+ commas are fine, and a value is allowed to be *empty*.
13
+
14
+ ```jp
15
+ [SERVER_ID]
16
+ config: {
17
+ disabled_channels:,
18
+ disabled_users: [9892, 82082, 8209]
19
+ }
20
+
21
+ [SERVER_ID_2]
22
+ prefix: "!"
23
+ modules: {
24
+ moderation: true,
25
+ fun: false
26
+ }
27
+ ```
28
+
29
+ ```python
30
+ >>> import jpcl
31
+ >>> jpcl.load("data/servers.jp")
32
+ {'SERVER_ID': {'config': {'disabled_channels': None,
33
+ 'disabled_users': [9892, 82082, 8209]}},
34
+ 'SERVER_ID_2': {'prefix': '!', 'modules': {'moderation': True, 'fun': False}}}
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Why another format
40
+
41
+ JSON has no comments, demands quotes on every key, and rejects a trailing
42
+ comma. TOML has comments and headers, but nesting anything non-trivial means
43
+ either deeply dotted keys or a table per level.
44
+
45
+ `.jp` takes the half of each that suits configuration files people edit by hand:
46
+
47
+ * **Sections for the top level.** `[SERVER_ID]` reads better than another brace.
48
+ * **JSON for everything below it.** Nest objects and arrays as deep as you like.
49
+ * **No ceremony.** Unquoted keys, comments anywhere, trailing commas ignored.
50
+ * **Empty values are legal.** `disabled_channels:,` means the key exists and has
51
+ no value yet — a real state in configs that JSON can only spell as `null`.
52
+
53
+ It is a small, fully specified format with a strict parser, precise error
54
+ messages, and a deterministic writer, so files stay stable when a program
55
+ rewrites them.
56
+
57
+ ```bash
58
+ pip install jpcl # or: uv add jpcl
59
+ ```
60
+
61
+ No runtime dependencies. Python 3.14+.
62
+
63
+ ---
64
+
65
+ ## The format
66
+
67
+ ### Sections
68
+
69
+ A `[NAME]` header opens a root key. Everything below it, until the next header,
70
+ belongs to that section.
71
+
72
+ ```jp
73
+ [SERVER_ID]
74
+ prefix: "!"
75
+ ```
76
+
77
+ Headers may be dotted to nest, and quoted when a name contains a dot:
78
+
79
+ ```jp
80
+ [guild.limits] # -> {"guild": {"limits": {...}}}
81
+ ["weird.name"] # -> {"weird.name": {...}}
82
+ ```
83
+
84
+ Key/value pairs written *before* the first header land at the document root:
85
+
86
+ ```jp
87
+ version: 2
88
+
89
+ [SERVER_ID]
90
+ prefix: "!"
91
+ ```
92
+
93
+ ### Entries
94
+
95
+ An entry is `key: value`. Keys need no quotes; a bare key may contain spaces but
96
+ not brackets, commas or quotes — quote it if it needs those.
97
+
98
+ Entries are separated by a line break, a comma, or both. Trailing and repeated
99
+ commas are accepted:
100
+
101
+ ```jp
102
+ [SERVER_ID]
103
+ a: 1
104
+ b: {x: 1, y: 2,}
105
+ c: [1, 2, 3,]
106
+ ```
107
+
108
+ ### Empty values
109
+
110
+ A key with nothing after the colon parses to `None`:
111
+
112
+ ```jp
113
+ config: {
114
+ disabled_channels:, # -> None
115
+ timeout: # -> None
116
+ }
117
+ ```
118
+
119
+ Because of this, **a value must start on the same line as its `:`**. An opening
120
+ `{` or `[` goes on the colon's line; its contents may then wrap freely.
121
+
122
+ ### Values
123
+
124
+ | Type | Examples |
125
+ | --- | --- |
126
+ | String | `"hello"`, `'hello'`, `hello world` (unquoted) |
127
+ | Integer | `42`, `-7`, `1_000`, `0xff`, `0o755`, `0b1010` |
128
+ | Float | `3.5`, `1e3`, `inf`, `-inf`, `nan` |
129
+ | Boolean | `true`, `false` (case-insensitive, so `True` works too) |
130
+ | Null | `null`, `none`, `nil`, or nothing at all |
131
+ | Object | `{a: 1, b: 2}` |
132
+ | Array | `[1, 2, 3]` |
133
+
134
+ Unquoted values are read as a keyword first, then a number, then a plain string.
135
+ Quote a value if it contains a `#`, a comma, a bracket, or leading/trailing
136
+ whitespace you want to keep.
137
+
138
+ Strings honour the usual escapes — `\n`, `\t`, `\\`, `\"`, `\uXXXX`,
139
+ `\U0001F600`, plus `\` at end of line to continue onto the next.
140
+
141
+ ### Comments
142
+
143
+ `#` runs to the end of the line and is allowed anywhere, including inside
144
+ objects and arrays.
145
+
146
+ ---
147
+
148
+ ## What you can do with it
149
+
150
+ ### Read and write files
151
+
152
+ ```python
153
+ import jpcl
154
+
155
+ data = jpcl.load("data/servers.jp") # -> dict
156
+ jpcl.dump(data, "data/servers.jp") # formatted, atomic write
157
+
158
+ text = jpcl.dumps(data) # -> str
159
+ data = jpcl.loads(text) # -> dict
160
+ ```
161
+
162
+ Writes are atomic by default: the file goes to a temporary neighbour and is
163
+ renamed into place, so a crash or a concurrent reader never sees half a config.
164
+
165
+ Options worth knowing:
166
+
167
+ ```python
168
+ jpcl.load("servers.jp", duplicate_keys="last") # "error" (default), "first", "last"
169
+ jpcl.dumps(data, indent=4, sort_keys=True) # also: width, ensure_ascii
170
+ jpcl.dumps(data, default=str) # convert datetimes and friends
171
+ ```
172
+
173
+ ### Edit a config in place
174
+
175
+ `JPConfig` is a `MutableMapping` that remembers the file it came from.
176
+
177
+ ```python
178
+ from jpcl import JPConfig
179
+
180
+ cfg = JPConfig.load("data/servers.jp", missing_ok=True)
181
+
182
+ cfg["SERVER_ID"]["prefix"] # plain dict access
183
+ cfg.get_path("SERVER_ID.config.disabled_users", []) # never raises
184
+ cfg.set_path("SERVER_ID.config.disabled_users", [9892]) # creates missing sections
185
+ cfg.has_path("SERVER_ID.prefix")
186
+ cfg.section("NEW_SERVER", create=True)["prefix"] = "?"
187
+ cfg.merge({"SERVER_ID": {"modules": {"fun": True}}}) # deep merge
188
+ cfg.save() # atomic, back to its own path
189
+ cfg.reload() # discard in-memory changes
190
+ cfg.to_dict() # deep copy as a plain dict
191
+ ```
192
+
193
+ `missing_ok=True` gives an empty config bound to the path, which is what you
194
+ want for a program that writes its config on first run. Formatting options given
195
+ to the constructor are remembered by `save()`:
196
+
197
+ ```python
198
+ cfg = JPConfig.load("data/servers.jp", indent=4, sort_keys=True)
199
+ ```
200
+
201
+ ### Load a whole folder
202
+
203
+ ```python
204
+ config = jpcl.load_dir("data") # {'servers': {...}, 'roles': {...}}
205
+ guilds = jpcl.load_dir("data/guilds") # {'1234567890': {...}, ...}
206
+ everything = jpcl.load_dir("data", recursive=True)
207
+ ```
208
+
209
+ Each file becomes one key, named after the file.
210
+
211
+ ### Find mistakes quickly
212
+
213
+ Every error derives from `jpcl.JPError`. `JPDecodeError` (a `ValueError`) points
214
+ at the exact character:
215
+
216
+ ```
217
+ data/servers.jp:2:8: expected ':' after key 'prefix', found '"'
218
+ prefix "!"
219
+ ^
220
+ ```
221
+
222
+ It carries `.line`, `.col`, `.pos`, `.filename` and `.raw_message` if you want to
223
+ render the failure yourself. `JPEncodeError` (a `TypeError`) explains what could
224
+ not be serialised — an unsupported type, a non-string key, a circular reference.
225
+
226
+ By default a repeated key is an error rather than a silent overwrite; pass
227
+ `duplicate_keys="first"` or `"last"` if you would rather it not be.
228
+
229
+ ### Work from the shell
230
+
231
+ ```bash
232
+ jpcl check data/*.jp # validate; non-zero exit on failure
233
+ jpcl fmt -w data/servers.jp # reformat in place
234
+ jpcl get data/servers.jp SERVER_ID.prefix # read one value
235
+ jpcl to-json data/servers.jp -o out.json
236
+ jpcl from-json out.json -o data/servers.jp
237
+ ```
238
+
239
+ `python -m jpcl ...` works identically, and `-` reads stdin.
240
+
241
+ ---
242
+
243
+ ## Round trips
244
+
245
+ `dumps` is deterministic, so a file rewritten twice is byte-identical:
246
+
247
+ * every top-level mapping becomes a `[SECTION]`, separated by a blank line;
248
+ * section entries sit one per line, with no separating commas;
249
+ * nested objects always expand across lines, `{}` being the only inline form;
250
+ * arrays stay inline while they fit inside `width` (default 88), then break one
251
+ element per line;
252
+ * `None` is written as an empty value inside mappings (`key:`) and as `null`
253
+ inside arrays, since an array element cannot be empty;
254
+ * insertion order is preserved unless `sort_keys=True`.
255
+
256
+ Two things do not survive a rewrite:
257
+
258
+ * **Comments are dropped.** Rewriting a hand-annotated file loses its notes.
259
+ * **Root-level scalars move above the first section**, because anything after a
260
+ header would be read back as part of that section.
261
+
262
+ ---
263
+
264
+ ## Organising your configs
265
+
266
+ Nothing is enforced, but this layout is what `load_dir` is built for:
267
+
268
+ ```
269
+ your-project/
270
+ ├─ data/
271
+ │ ├─ servers.jp # one file per concern
272
+ │ ├─ roles.jp
273
+ │ ├─ servers.example.jp # committed template, safe to publish
274
+ │ └─ guilds/ # optional: one file per entity
275
+ │ ├─ 1234567890.jp
276
+ │ └─ 9876543210.jp
277
+ └─ src/
278
+ ```
279
+
280
+ A few habits that save pain later:
281
+
282
+ 1. **One file per concern.** A parse error then takes out one feature, not
283
+ everything.
284
+ 2. **Keep live data out of git**, and commit a template instead:
285
+ ```gitignore
286
+ data/*.jp
287
+ !data/*.example.jp
288
+ ```
289
+ 3. **Use IDs as section names.** `[1234567890]` parses to the string key
290
+ `"1234567890"`, and integer keys are stringified on write, so
291
+ `{1234567890: {...}}` round-trips.
292
+ 4. **Write through `JPConfig.save()`** rather than by hand, so an interrupted
293
+ write cannot truncate a live config.
294
+ 5. **Validate in CI** with `jpcl check data/*.jp`.
295
+
296
+ ---
297
+
298
+ ## Licence
299
+
300
+ MIT. Contributing, tests and release process: [CONTRIBUTING.md](CONTRIBUTING.md).
@@ -0,0 +1,47 @@
1
+ [project]
2
+ name = "jpcl"
3
+ version = "1.1.1"
4
+ description = "A hybrid JSON/TOML configuration language (.jp files)"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ dependencies = []
8
+ license = "MIT"
9
+ license-files = ["LICENSE"]
10
+ keywords = [
11
+ "config",
12
+ "configuration",
13
+ "parser",
14
+ "toml",
15
+ "json",
16
+ "jp",
17
+ "jpcl",
18
+ ]
19
+ classifiers = [
20
+ "Development Status :: 4 - Beta",
21
+ "Intended Audience :: Developers",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Topic :: File Formats",
25
+ "Topic :: Software Development :: Libraries :: Python Modules",
26
+ "Topic :: Text Processing :: Markup",
27
+ "Typing :: Typed",
28
+ ]
29
+
30
+ [[project.authors]]
31
+ name = "Clove Twilight"
32
+ email = "admin@doughmination.win"
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/jpcl-lang/jpcl-py"
36
+ Repository = "https://github.com/jpcl-lang/jpcl-py"
37
+ Issues = "https://github.com/jpcl-lang/jpcl-py/issues"
38
+
39
+ [project.scripts]
40
+ jpcl = "jpcl.cli:main"
41
+
42
+ [build-system]
43
+ requires = ["uv_build>=0.12.9,<0.13.0"]
44
+ build-backend = "uv_build"
45
+
46
+ [dependency-groups]
47
+ dev = ["pytest>=8"]