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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quick_ini
3
- Version: 4.1
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 `backup`. Use `QuickIni.sync_template("config.ini")` to sync without loading.
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 `backup`. Use `QuickIni.sync_template("config.ini")` to sync without loading.
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.1"
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.1
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 `backup`. Use `QuickIni.sync_template("config.ini")` to sync without loading.
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
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ quick-ini = quick_ini.__main__:main
@@ -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