quick-ini 4.1__tar.gz → 4.3__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.
- {quick_ini-4.1/quick_ini.egg-info → quick_ini-4.3}/PKG-INFO +14 -2
- {quick_ini-4.1 → quick_ini-4.3}/README.md +13 -1
- {quick_ini-4.1 → quick_ini-4.3}/pyproject.toml +4 -1
- quick_ini-4.3/quick_ini/__main__.py +67 -0
- {quick_ini-4.1 → quick_ini-4.3}/quick_ini/quick_ini.py +6 -4
- {quick_ini-4.1 → quick_ini-4.3/quick_ini.egg-info}/PKG-INFO +14 -2
- {quick_ini-4.1 → quick_ini-4.3}/quick_ini.egg-info/SOURCES.txt +2 -0
- quick_ini-4.3/quick_ini.egg-info/entry_points.txt +2 -0
- {quick_ini-4.1 → quick_ini-4.3}/tests/test_quickini.py +100 -0
- {quick_ini-4.1 → quick_ini-4.3}/LICENSE +0 -0
- {quick_ini-4.1 → quick_ini-4.3}/quick_ini/__init__.py +0 -0
- {quick_ini-4.1 → quick_ini-4.3}/quick_ini/py.typed +0 -0
- {quick_ini-4.1 → quick_ini-4.3}/quick_ini.egg-info/dependency_links.txt +0 -0
- {quick_ini-4.1 → quick_ini-4.3}/quick_ini.egg-info/top_level.txt +0 -0
- {quick_ini-4.1 → quick_ini-4.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quick_ini
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3
|
|
4
4
|
Summary: A library for reading from and writing to .ini formatted files
|
|
5
5
|
Author-email: OllieZ-Mods <socksinthewash@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -82,7 +82,19 @@ if report and report["added"]:
|
|
|
82
82
|
print("New settings added to config.ini:", ", ".join(report["added"]))
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
The report also has `created`, `changed`, `orphaned` (settings kept that aren't in the template) and `
|
|
85
|
+
The report also has `created`, `changed`, `orphaned` (settings kept that aren't in the template), `backup` and `text` (the synced file). Use `QuickIni.sync_template("config.ini")` to sync without loading, and `dry_run=True` to work out the changes without writing anything.
|
|
86
|
+
|
|
87
|
+
### From the command line
|
|
88
|
+
|
|
89
|
+
Installing QuickINI adds a `quick-ini` command (or run `python -m quick_ini`), so you can sync without starting your program:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
quick-ini sync # config.ini with config.ini.template
|
|
93
|
+
quick-ini sync --dry-run # show what would change as a diff, write nothing (--diff works too)
|
|
94
|
+
quick-ini sync --config app.ini --template defaults.ini
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`--config` defaults to `config.ini` and `--template` to `<config>.template`. Unlike `template=True`, a missing template is an error here. It prints the settings added and kept, and exits with 1 on any error.
|
|
86
98
|
|
|
87
99
|
## File Format
|
|
88
100
|
|
|
@@ -57,7 +57,19 @@ if report and report["added"]:
|
|
|
57
57
|
print("New settings added to config.ini:", ", ".join(report["added"]))
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
The report also has `created`, `changed`, `orphaned` (settings kept that aren't in the template) and `
|
|
60
|
+
The report also has `created`, `changed`, `orphaned` (settings kept that aren't in the template), `backup` and `text` (the synced file). Use `QuickIni.sync_template("config.ini")` to sync without loading, and `dry_run=True` to work out the changes without writing anything.
|
|
61
|
+
|
|
62
|
+
### From the command line
|
|
63
|
+
|
|
64
|
+
Installing QuickINI adds a `quick-ini` command (or run `python -m quick_ini`), so you can sync without starting your program:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
quick-ini sync # config.ini with config.ini.template
|
|
68
|
+
quick-ini sync --dry-run # show what would change as a diff, write nothing (--diff works too)
|
|
69
|
+
quick-ini sync --config app.ini --template defaults.ini
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`--config` defaults to `config.ini` and `--template` to `<config>.template`. Unlike `template=True`, a missing template is an error here. It prints the settings added and kept, and exits with 1 on any error.
|
|
61
73
|
|
|
62
74
|
## File Format
|
|
63
75
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "quick_ini"
|
|
7
|
-
version = "4.
|
|
7
|
+
version = "4.3"
|
|
8
8
|
description = "A library for reading from and writing to .ini formatted files"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -25,6 +25,9 @@ classifiers = [
|
|
|
25
25
|
"Typing :: Typed",
|
|
26
26
|
]
|
|
27
27
|
|
|
28
|
+
[project.scripts]
|
|
29
|
+
quick-ini = "quick_ini.__main__:main"
|
|
30
|
+
|
|
28
31
|
[project.urls]
|
|
29
32
|
Homepage = "https://github.com/olliez-mods/Quick-Ini"
|
|
30
33
|
"Bug Reports" = "https://github.com/olliez-mods/Quick-Ini/issues"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command line: quick-ini sync [--config config.ini] [--template <config>.template] [--dry-run | --diff]
|
|
3
|
+
\nAlso runs as "python -m quick_ini". Brings a config file up to date with its template, see QuickIni.sync_template().
|
|
4
|
+
"""
|
|
5
|
+
from .quick_ini import QuickIni
|
|
6
|
+
import argparse
|
|
7
|
+
import difflib
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
def sync(config:str, template:str | None, dry_run:bool) -> int:
|
|
11
|
+
# Unlike load_file(template=True), a missing template is an error here: on the command line it's a typo
|
|
12
|
+
template = template or f"{config}.template"
|
|
13
|
+
try:
|
|
14
|
+
with open(config, 'r', encoding='utf-8') as file:
|
|
15
|
+
old_text = file.read()
|
|
16
|
+
except FileNotFoundError:
|
|
17
|
+
old_text = ""
|
|
18
|
+
except IOError as e:
|
|
19
|
+
print(f"Error reading file: '{e}'", file=sys.stderr)
|
|
20
|
+
return 1
|
|
21
|
+
|
|
22
|
+
if(not QuickIni.sync_template(config, template, dry_run=dry_run)):
|
|
23
|
+
print(QuickIni.get_last_error(), file=sys.stderr)
|
|
24
|
+
return 1
|
|
25
|
+
report = QuickIni.get_sync_report()
|
|
26
|
+
|
|
27
|
+
if(dry_run and report["changed"]):
|
|
28
|
+
diff = difflib.unified_diff(old_text.splitlines(keepends=True), report["text"].splitlines(keepends=True),
|
|
29
|
+
config, f"{config} (synced)")
|
|
30
|
+
sys.stdout.writelines(diff)
|
|
31
|
+
print()
|
|
32
|
+
|
|
33
|
+
if(report["added"]):
|
|
34
|
+
print("Added from template:", ", ".join(report["added"]))
|
|
35
|
+
if(report["orphaned"]):
|
|
36
|
+
print("Kept at the end (not in template):", ", ".join(report["orphaned"]))
|
|
37
|
+
|
|
38
|
+
if(not report["changed"]):
|
|
39
|
+
print("Already in sync")
|
|
40
|
+
elif(dry_run):
|
|
41
|
+
print(f"Would create {config} from {template}" if report["created"] else "Dry run: nothing written")
|
|
42
|
+
elif(report["created"]):
|
|
43
|
+
print(f"Created {config} from {template}")
|
|
44
|
+
else:
|
|
45
|
+
print(f"Synced {config} (previous version: {report['backup']})")
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
def main(argv:list[str] | None = None) -> int:
|
|
49
|
+
parser = argparse.ArgumentParser(prog="quick-ini", description="Tools for .ini config files.")
|
|
50
|
+
commands = parser.add_subparsers(dest="command")
|
|
51
|
+
sync_parser = commands.add_parser("sync", help="bring a config file up to date with its template",
|
|
52
|
+
description="Bring a config file up to date with its template. Your values are "
|
|
53
|
+
"kept, new keys get the template's value, keys the template doesn't have are kept "
|
|
54
|
+
"at the end of their section. The previous version is saved as <config>.bak.")
|
|
55
|
+
sync_parser.add_argument("--config", default="config.ini", help="the config file (default: config.ini)")
|
|
56
|
+
sync_parser.add_argument("--template", help="the template (default: <config>.template)")
|
|
57
|
+
sync_parser.add_argument("--dry-run", "--diff", dest="dry_run", action="store_true",
|
|
58
|
+
help="show the changes as a diff, write nothing")
|
|
59
|
+
|
|
60
|
+
args = parser.parse_args(argv)
|
|
61
|
+
if(args.command == "sync"):
|
|
62
|
+
return sync(args.config, args.template, args.dry_run)
|
|
63
|
+
parser.print_help()
|
|
64
|
+
return 1
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
sys.exit(main())
|
|
@@ -160,7 +160,7 @@ class QuickIni:
|
|
|
160
160
|
raise
|
|
161
161
|
|
|
162
162
|
@staticmethod
|
|
163
|
-
def sync_template(file_location:str, template:bool | str = True) -> bool:
|
|
163
|
+
def sync_template(file_location:str, template:bool | str = True, dry_run:bool = False) -> bool:
|
|
164
164
|
"""
|
|
165
165
|
Brings a config file up to date with its template, keeping the values already set in the config.
|
|
166
166
|
\nThe template decides which keys exist, their order, sections and comments. Keys the config doesn't have yet get
|
|
@@ -171,6 +171,7 @@ class QuickIni:
|
|
|
171
171
|
file_location (str): The path of the config file.
|
|
172
172
|
template (bool | str): True to use "<config>.template", skipped quietly if it doesn't exist.
|
|
173
173
|
Or the path of the template file, which must exist.
|
|
174
|
+
dry_run (bool): Work out the synced file without writing it (or a backup). The report has it as "text".
|
|
174
175
|
Returns:
|
|
175
176
|
bool: True if successfull, False otherwise
|
|
176
177
|
Example:
|
|
@@ -266,7 +267,7 @@ class QuickIni:
|
|
|
266
267
|
changed = new_text != old_text
|
|
267
268
|
backup = None
|
|
268
269
|
|
|
269
|
-
if(changed):
|
|
270
|
+
if(changed and not dry_run):
|
|
270
271
|
try:
|
|
271
272
|
if(old_text is not None):
|
|
272
273
|
backup = f"{path}.bak"
|
|
@@ -282,6 +283,7 @@ class QuickIni:
|
|
|
282
283
|
"added": added if old_text is not None else [],
|
|
283
284
|
"orphaned": orphaned,
|
|
284
285
|
"backup": backup,
|
|
286
|
+
"text": new_text,
|
|
285
287
|
}
|
|
286
288
|
return True
|
|
287
289
|
|
|
@@ -292,12 +294,12 @@ class QuickIni:
|
|
|
292
294
|
Return:
|
|
293
295
|
dict[str, Any]: "created" (bool) the config was made from the template, "changed" (bool) the config was rewritten,
|
|
294
296
|
"added" (list) keys taken from the template, "orphaned" (list) keys kept that aren't in the template,
|
|
295
|
-
"backup" (str | None) the path of the backup of the previous config
|
|
297
|
+
"backup" (str | None) the path of the backup of the previous config, "text" (str) the synced file
|
|
296
298
|
Example:
|
|
297
299
|
>>> load_file("config.ini", template=True)
|
|
298
300
|
True
|
|
299
301
|
>>> get_sync_report()
|
|
300
|
-
{'created': False, 'changed': True, 'added': ['database.timeout'], 'orphaned': [], 'backup': '/app/config.ini.bak'}
|
|
302
|
+
{'created': False, 'changed': True, 'added': ['database.timeout'], 'orphaned': [], 'backup': '/app/config.ini.bak', 'text': '...'}
|
|
301
303
|
"""
|
|
302
304
|
if(QuickIni.sync_report is None):
|
|
303
305
|
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quick_ini
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3
|
|
4
4
|
Summary: A library for reading from and writing to .ini formatted files
|
|
5
5
|
Author-email: OllieZ-Mods <socksinthewash@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -82,7 +82,19 @@ if report and report["added"]:
|
|
|
82
82
|
print("New settings added to config.ini:", ", ".join(report["added"]))
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
The report also has `created`, `changed`, `orphaned` (settings kept that aren't in the template) and `
|
|
85
|
+
The report also has `created`, `changed`, `orphaned` (settings kept that aren't in the template), `backup` and `text` (the synced file). Use `QuickIni.sync_template("config.ini")` to sync without loading, and `dry_run=True` to work out the changes without writing anything.
|
|
86
|
+
|
|
87
|
+
### From the command line
|
|
88
|
+
|
|
89
|
+
Installing QuickINI adds a `quick-ini` command (or run `python -m quick_ini`), so you can sync without starting your program:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
quick-ini sync # config.ini with config.ini.template
|
|
93
|
+
quick-ini sync --dry-run # show what would change as a diff, write nothing (--diff works too)
|
|
94
|
+
quick-ini sync --config app.ini --template defaults.ini
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`--config` defaults to `config.ini` and `--template` to `<config>.template`. Unlike `template=True`, a missing template is an error here. It prints the settings added and kept, and exits with 1 on any error.
|
|
86
98
|
|
|
87
99
|
## File Format
|
|
88
100
|
|
|
@@ -2,10 +2,12 @@ LICENSE
|
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
4
|
quick_ini/__init__.py
|
|
5
|
+
quick_ini/__main__.py
|
|
5
6
|
quick_ini/py.typed
|
|
6
7
|
quick_ini/quick_ini.py
|
|
7
8
|
quick_ini.egg-info/PKG-INFO
|
|
8
9
|
quick_ini.egg-info/SOURCES.txt
|
|
9
10
|
quick_ini.egg-info/dependency_links.txt
|
|
11
|
+
quick_ini.egg-info/entry_points.txt
|
|
10
12
|
quick_ini.egg-info/top_level.txt
|
|
11
13
|
tests/test_quickini.py
|
|
@@ -8,6 +8,9 @@ import tempfile
|
|
|
8
8
|
import os
|
|
9
9
|
from unittest.mock import patch, mock_open
|
|
10
10
|
from quick_ini import QuickIni
|
|
11
|
+
from quick_ini import __main__ as cli
|
|
12
|
+
from contextlib import redirect_stdout, redirect_stderr
|
|
13
|
+
import io
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
class TestQuickIni(unittest.TestCase):
|
|
@@ -853,5 +856,102 @@ port = 6000
|
|
|
853
856
|
self.assertFalse(any(name.endswith(".tmp") for name in os.listdir(self.dir.name)))
|
|
854
857
|
|
|
855
858
|
|
|
859
|
+
|
|
860
|
+
class TestQuickIniCommandLine(unittest.TestCase):
|
|
861
|
+
"""Tests for "quick-ini sync" (quick_ini/__main__.py), run from inside a temp folder for the default paths."""
|
|
862
|
+
|
|
863
|
+
# The template tests' setup and helpers, without running those tests again
|
|
864
|
+
TEMPLATE = TestQuickIniTemplate.TEMPLATE
|
|
865
|
+
write = TestQuickIniTemplate.write
|
|
866
|
+
read = TestQuickIniTemplate.read
|
|
867
|
+
|
|
868
|
+
def setUp(self):
|
|
869
|
+
TestQuickIniTemplate.setUp(self)
|
|
870
|
+
self.cwd = os.getcwd()
|
|
871
|
+
os.chdir(self.dir.name)
|
|
872
|
+
|
|
873
|
+
def tearDown(self):
|
|
874
|
+
os.chdir(self.cwd)
|
|
875
|
+
TestQuickIniTemplate.tearDown(self)
|
|
876
|
+
|
|
877
|
+
def run_cli(self, *args):
|
|
878
|
+
out, err = io.StringIO(), io.StringIO()
|
|
879
|
+
with redirect_stdout(out), redirect_stderr(err):
|
|
880
|
+
code = cli.main(list(args))
|
|
881
|
+
return code, out.getvalue(), err.getvalue()
|
|
882
|
+
|
|
883
|
+
def test_default_config_and_template(self):
|
|
884
|
+
self.write("config.ini.template", self.TEMPLATE)
|
|
885
|
+
self.write("config.ini", "debug = true\nold = 1\n")
|
|
886
|
+
code, out, _ = self.run_cli("sync")
|
|
887
|
+
self.assertEqual(code, 0)
|
|
888
|
+
self.assertIn("debug = true", self.read(self.config))
|
|
889
|
+
self.assertIn("name = MyApp", self.read(self.config))
|
|
890
|
+
self.assertIn("Added from template: name, parts, database.host, database.port", out)
|
|
891
|
+
self.assertIn("Kept at the end (not in template): old", out)
|
|
892
|
+
self.assertIn("previous version:", out)
|
|
893
|
+
self.assertEqual(self.read(self.config + ".bak"), "debug = true\nold = 1\n")
|
|
894
|
+
|
|
895
|
+
def test_config_and_template_given(self):
|
|
896
|
+
self.write("defaults.ini", self.TEMPLATE)
|
|
897
|
+
self.write("app.ini", "debug = true\n")
|
|
898
|
+
code, _, _ = self.run_cli("sync", "--config", "app.ini", "--template", "defaults.ini")
|
|
899
|
+
self.assertEqual(code, 0)
|
|
900
|
+
self.assertIn("name = MyApp", self.read(os.path.join(self.dir.name, "app.ini")))
|
|
901
|
+
|
|
902
|
+
def test_template_defaults_to_given_config(self):
|
|
903
|
+
self.write("app.ini.template", self.TEMPLATE)
|
|
904
|
+
self.write("app.ini", "debug = true\n")
|
|
905
|
+
code, _, _ = self.run_cli("sync", "--config", "app.ini")
|
|
906
|
+
self.assertEqual(code, 0)
|
|
907
|
+
self.assertIn("name = MyApp", self.read(os.path.join(self.dir.name, "app.ini")))
|
|
908
|
+
|
|
909
|
+
def test_dry_run_shows_diff_and_writes_nothing(self):
|
|
910
|
+
for flag in ("--dry-run", "--diff"):
|
|
911
|
+
self.write("config.ini.template", self.TEMPLATE)
|
|
912
|
+
self.write("config.ini", "debug = true\n")
|
|
913
|
+
code, out, _ = self.run_cli("sync", flag)
|
|
914
|
+
self.assertEqual(code, 0)
|
|
915
|
+
self.assertEqual(self.read(self.config), "debug = true\n")
|
|
916
|
+
self.assertFalse(os.path.exists(self.config + ".bak"))
|
|
917
|
+
self.assertIn("+++ config.ini (synced)", out)
|
|
918
|
+
self.assertIn("+name = MyApp", out)
|
|
919
|
+
self.assertIn("Dry run: nothing written", out)
|
|
920
|
+
|
|
921
|
+
def test_dry_run_does_not_create_config(self):
|
|
922
|
+
self.write("config.ini.template", self.TEMPLATE)
|
|
923
|
+
code, out, _ = self.run_cli("sync", "--dry-run")
|
|
924
|
+
self.assertEqual(code, 0)
|
|
925
|
+
self.assertFalse(os.path.exists(self.config))
|
|
926
|
+
self.assertIn("Would create config.ini from config.ini.template", out)
|
|
927
|
+
|
|
928
|
+
def test_missing_config_is_created(self):
|
|
929
|
+
self.write("config.ini.template", self.TEMPLATE)
|
|
930
|
+
code, out, _ = self.run_cli("sync")
|
|
931
|
+
self.assertEqual(code, 0)
|
|
932
|
+
self.assertEqual(self.read(self.config), self.TEMPLATE)
|
|
933
|
+
self.assertIn("Created config.ini from config.ini.template", out)
|
|
934
|
+
|
|
935
|
+
def test_already_in_sync(self):
|
|
936
|
+
self.write("config.ini.template", self.TEMPLATE)
|
|
937
|
+
self.write("config.ini", self.TEMPLATE)
|
|
938
|
+
code, out, _ = self.run_cli("sync", "--dry-run")
|
|
939
|
+
self.assertEqual(code, 0)
|
|
940
|
+
self.assertEqual(out.strip(), "Already in sync")
|
|
941
|
+
|
|
942
|
+
def test_missing_template_is_an_error(self):
|
|
943
|
+
self.write("config.ini", "debug = true\n")
|
|
944
|
+
code, out, err = self.run_cli("sync")
|
|
945
|
+
self.assertEqual(code, 1)
|
|
946
|
+
self.assertIn("Could not find template", err)
|
|
947
|
+
self.assertEqual(out, "")
|
|
948
|
+
self.assertEqual(self.read(self.config), "debug = true\n")
|
|
949
|
+
|
|
950
|
+
def test_no_command_prints_help(self):
|
|
951
|
+
code, out, _ = self.run_cli()
|
|
952
|
+
self.assertEqual(code, 1)
|
|
953
|
+
self.assertIn("sync", out)
|
|
954
|
+
|
|
955
|
+
|
|
856
956
|
if __name__ == '__main__':
|
|
857
957
|
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|