ckparser 0.3.2__tar.gz → 0.3.4__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: ckparser
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Parse Paradox Jomini data files to Python/JSON and revert them back experimentally.
5
5
  Author: Marc Debureaux (debnet)
6
6
  License-Expression: MIT
@@ -284,6 +284,29 @@ from ckparser import walk
284
284
  for value, path in walk(data):
285
285
  print(path, value)
286
286
  ```
287
+
288
+ ### `objectify`
289
+
290
+ Transform a dictionary datastructure into an object-like dictionary which allows a better inspection.
291
+
292
+ ```python
293
+ from ckparser import objectify
294
+
295
+ data = {
296
+ "name": "Louis",
297
+ "traits": [
298
+ "brave",
299
+ "diligent"
300
+ ],
301
+ "father": {
302
+ "name": "Charles"
303
+ }
304
+ }
305
+
306
+ obj = objectify(data)
307
+ print(obj.traits[1])
308
+ print(obj.father.name)
309
+ ```
287
310
 
288
311
  ### `parse_all_locales`
289
312
 
@@ -518,9 +541,9 @@ Paradox scripting languages include many practical exceptions, formatting varian
518
541
 
519
542
  ### Parsing
520
543
 
521
- * `parse_text(text, return_text_on_error=False, comments=False, dates=False, filename=None, is_global=False)`
522
- * `parse_file(path, output_dir=None, encoding="utf_8_sig", base_dir=None, save=False, comments=False, dates=False, is_global=False, patch=None)`
523
- * `parse_all_files(path, output_dir=None, encoding="utf_8_sig", keep_data=False, save=False, comments=False, dates=False, variables_first=True)`
544
+ * `parse_text(text, return_text_on_error=False, comments=False, dates=False, as_object=False, filename=None, is_global=False)`
545
+ * `parse_file(path, output_dir=None, encoding="utf_8_sig", base_dir=None, save=False, comments=False, dates=False, as_object=False, is_global=False, patch=None)`
546
+ * `parse_all_files(path, output_dir=None, encoding="utf_8_sig", keep_data=False, save=False, comments=False, dates=False, as_object=False, variables_first=True)`
524
547
 
525
548
  ### Reversion
526
549
 
@@ -533,6 +556,7 @@ Paradox scripting languages include many practical exceptions, formatting varian
533
556
  * `convert_date(date, key=None)`
534
557
  * `read_file(path, encoding="utf_8_sig")`
535
558
  * `walk(obj, *from_keys)`
559
+ * `objectify(item, name="Object", default=None)`
536
560
  * `parse_all_locales(path, encoding="utf_8_sig", language="english", save=False)`
537
561
  * `load_variables(filepath="_variables.json")`
538
562
  * `save_variables(filepath="_variables.json")`
@@ -253,6 +253,29 @@ from ckparser import walk
253
253
  for value, path in walk(data):
254
254
  print(path, value)
255
255
  ```
256
+
257
+ ### `objectify`
258
+
259
+ Transform a dictionary datastructure into an object-like dictionary which allows a better inspection.
260
+
261
+ ```python
262
+ from ckparser import objectify
263
+
264
+ data = {
265
+ "name": "Louis",
266
+ "traits": [
267
+ "brave",
268
+ "diligent"
269
+ ],
270
+ "father": {
271
+ "name": "Charles"
272
+ }
273
+ }
274
+
275
+ obj = objectify(data)
276
+ print(obj.traits[1])
277
+ print(obj.father.name)
278
+ ```
256
279
 
257
280
  ### `parse_all_locales`
258
281
 
@@ -487,9 +510,9 @@ Paradox scripting languages include many practical exceptions, formatting varian
487
510
 
488
511
  ### Parsing
489
512
 
490
- * `parse_text(text, return_text_on_error=False, comments=False, dates=False, filename=None, is_global=False)`
491
- * `parse_file(path, output_dir=None, encoding="utf_8_sig", base_dir=None, save=False, comments=False, dates=False, is_global=False, patch=None)`
492
- * `parse_all_files(path, output_dir=None, encoding="utf_8_sig", keep_data=False, save=False, comments=False, dates=False, variables_first=True)`
513
+ * `parse_text(text, return_text_on_error=False, comments=False, dates=False, as_object=False, filename=None, is_global=False)`
514
+ * `parse_file(path, output_dir=None, encoding="utf_8_sig", base_dir=None, save=False, comments=False, dates=False, as_object=False, is_global=False, patch=None)`
515
+ * `parse_all_files(path, output_dir=None, encoding="utf_8_sig", keep_data=False, save=False, comments=False, dates=False, as_object=False, variables_first=True)`
493
516
 
494
517
  ### Reversion
495
518
 
@@ -502,6 +525,7 @@ Paradox scripting languages include many practical exceptions, formatting varian
502
525
  * `convert_date(date, key=None)`
503
526
  * `read_file(path, encoding="utf_8_sig")`
504
527
  * `walk(obj, *from_keys)`
528
+ * `objectify(item, name="Object", default=None)`
505
529
  * `parse_all_locales(path, encoding="utf_8_sig", language="english", save=False)`
506
530
  * `load_variables(filepath="_variables.json")`
507
531
  * `save_variables(filepath="_variables.json")`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ckparser
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Parse Paradox Jomini data files to Python/JSON and revert them back experimentally.
5
5
  Author: Marc Debureaux (debnet)
6
6
  License-Expression: MIT
@@ -284,6 +284,29 @@ from ckparser import walk
284
284
  for value, path in walk(data):
285
285
  print(path, value)
286
286
  ```
287
+
288
+ ### `objectify`
289
+
290
+ Transform a dictionary datastructure into an object-like dictionary which allows a better inspection.
291
+
292
+ ```python
293
+ from ckparser import objectify
294
+
295
+ data = {
296
+ "name": "Louis",
297
+ "traits": [
298
+ "brave",
299
+ "diligent"
300
+ ],
301
+ "father": {
302
+ "name": "Charles"
303
+ }
304
+ }
305
+
306
+ obj = objectify(data)
307
+ print(obj.traits[1])
308
+ print(obj.father.name)
309
+ ```
287
310
 
288
311
  ### `parse_all_locales`
289
312
 
@@ -518,9 +541,9 @@ Paradox scripting languages include many practical exceptions, formatting varian
518
541
 
519
542
  ### Parsing
520
543
 
521
- * `parse_text(text, return_text_on_error=False, comments=False, dates=False, filename=None, is_global=False)`
522
- * `parse_file(path, output_dir=None, encoding="utf_8_sig", base_dir=None, save=False, comments=False, dates=False, is_global=False, patch=None)`
523
- * `parse_all_files(path, output_dir=None, encoding="utf_8_sig", keep_data=False, save=False, comments=False, dates=False, variables_first=True)`
544
+ * `parse_text(text, return_text_on_error=False, comments=False, dates=False, as_object=False, filename=None, is_global=False)`
545
+ * `parse_file(path, output_dir=None, encoding="utf_8_sig", base_dir=None, save=False, comments=False, dates=False, as_object=False, is_global=False, patch=None)`
546
+ * `parse_all_files(path, output_dir=None, encoding="utf_8_sig", keep_data=False, save=False, comments=False, dates=False, as_object=False, variables_first=True)`
524
547
 
525
548
  ### Reversion
526
549
 
@@ -533,6 +556,7 @@ Paradox scripting languages include many practical exceptions, formatting varian
533
556
  * `convert_date(date, key=None)`
534
557
  * `read_file(path, encoding="utf_8_sig")`
535
558
  * `walk(obj, *from_keys)`
559
+ * `objectify(item, name="Object", default=None)`
536
560
  * `parse_all_locales(path, encoding="utf_8_sig", language="english", save=False)`
537
561
  * `load_variables(filepath="_variables.json")`
538
562
  * `save_variables(filepath="_variables.json")`
@@ -41,7 +41,7 @@ import re
41
41
  import time
42
42
 
43
43
  # Script version
44
- __version__ = "0.3.2"
44
+ __version__ = "0.3.4"
45
45
 
46
46
  # Logger (because logging is awesome)
47
47
  logger = logging.getLogger(__name__)
@@ -120,7 +120,7 @@ regex_line = re.compile(r"\"?(?P<key>[^\s\"]+)\"?\s*(?P<operator>[?!<=>]+)\s*(li
120
120
  # Regex to parse independent items in a list
121
121
  regex_item = re.compile(r"(\"[^\"]+\"|[\d\.]+|[^\s]+)")
122
122
  # Regex to parse dates
123
- regex_date = re.compile(r"^(?:\d{1,4})\.(?:[1-9]|1[0-2])\.(?:[1-9]|[12]\d|3[01])$")
123
+ regex_date = re.compile(r"^(?P<year>\d{1,4})\.(?P<month>[1-9]|1[0-2])\.(?P<day>[1-9]|[12]\d|3[01])$")
124
124
  # Regex to remove empty lines
125
125
  regex_empty = re.compile(r"(\n\s*\n)+", re.MULTILINE)
126
126
  # Regex to parse locale files
@@ -192,14 +192,23 @@ def read_file(path, encoding="utf_8_sig"):
192
192
  return file.read()
193
193
 
194
194
 
195
- def parse_text(text, return_text_on_error=False, comments=False, dates=False, filename=None, is_global=False):
195
+ def parse_text(
196
+ text,
197
+ return_text_on_error=False,
198
+ comments=False,
199
+ dates=False,
200
+ as_object=False,
201
+ filename=None,
202
+ is_global=False,
203
+ ):
196
204
  """
197
205
  Parse raw text
198
206
  :param text: Text to parse
199
207
  :param return_text_on_error: (default false) Return working text document if parsing fails
200
208
  :param comments: (default false) Include comments?
201
209
  :param dates: (default false) Include dates?
202
- :param filename: (default none) Filename (only for debugging)
210
+ :param as_object: (default false) Return data as an object-like instead of dict
211
+ :param filename: (default none) file which is parsed (only for debugging)
203
212
  :param is_global: (default false) Are variables global?
204
213
  :return: Parsed data as dictionary
205
214
  """
@@ -285,7 +294,7 @@ def parse_text(text, return_text_on_error=False, comments=False, dates=False, fi
285
294
  if not isinstance(node[key], list):
286
295
  node[key] = [node[key]]
287
296
  if operator != "=":
288
- node[key].append({"@operator": operator, "@value": item})
297
+ node[key].append({"@type": "operator", "@operator": operator, "@value": item})
289
298
  else:
290
299
  node[key].append(item)
291
300
  # If this block name must be forced as list
@@ -294,7 +303,7 @@ def parse_text(text, return_text_on_error=False, comments=False, dates=False, fi
294
303
  elif isinstance(node, list): # Only for on_actions...
295
304
  node.append(item)
296
305
  elif operator != "=":
297
- node[key] = {"@operator": operator, "@value": item}
306
+ node[key] = {"@type": "operator", "@operator": operator, "@value": item}
298
307
  else:
299
308
  node[key] = item
300
309
  # Change current node for next lines
@@ -331,7 +340,7 @@ def parse_text(text, return_text_on_error=False, comments=False, dates=False, fi
331
340
  else:
332
341
  # If operator is not equal
333
342
  if operator != "=":
334
- node[key] = {"@operator": operator, "@value": value}
343
+ node[key] = {"@type": "operator", "@operator": operator, "@value": value}
335
344
  if isinstance(value, str):
336
345
  if value == "":
337
346
  node[key]["@value"] = item = {}
@@ -503,7 +512,7 @@ def parse_text(text, return_text_on_error=False, comments=False, dates=False, fi
503
512
  logger.error(f"Parse error: {error}")
504
513
  logger.debug("Exception:", exc_info=True)
505
514
  return regex_clean.sub("", text) if return_text_on_error else None
506
- return root
515
+ return objectify(root) if as_object else root
507
516
 
508
517
 
509
518
  def parse_file(
@@ -514,6 +523,7 @@ def parse_file(
514
523
  save=False,
515
524
  comments=False,
516
525
  dates=False,
526
+ as_object=False,
517
527
  is_global=False,
518
528
  patch=None,
519
529
  ):
@@ -524,12 +534,16 @@ def parse_file(
524
534
  :param encoding: Encoding used to read file
525
535
  :param base_dir: Base directory (for debug)
526
536
  :param save: (default false) Save parsed file in output directory
527
- :param comments: Include comments?
528
- :param dates: Include dates?
537
+ :param comments: (default false) Include comments?
538
+ :param dates: (default false) Include dates?
539
+ :param as_object: (default false) Return data as an object-like instead of dict
529
540
  :param is_global: (default false) Are file's variables global?
530
541
  :param patch: String replacement patterns
531
542
  :return: Parsed data as dictionary or text if parsing fails
532
543
  """
544
+ if save and as_object:
545
+ logger.warning("`as_object` disabled bacause only dicts can be converted to JSON.")
546
+ as_object = False
533
547
  start_time = time.monotonic()
534
548
  if base_dir:
535
549
  base_dir = os.sep.join(str(base_dir).rstrip(os.sep).split(os.sep)[:-1]) + os.sep
@@ -543,7 +557,13 @@ def parse_file(
543
557
  filename = os.path.join(base_dir, os.path.basename(path))
544
558
  logger.debug(f"Parsing {filename}")
545
559
  data = parse_text(
546
- text, return_text_on_error=True, comments=comments, dates=dates, filename=filename, is_global=is_global
560
+ text,
561
+ return_text_on_error=True,
562
+ comments=comments,
563
+ dates=dates,
564
+ as_object=as_object,
565
+ filename=filename,
566
+ is_global=is_global,
547
567
  )
548
568
  if save:
549
569
  filename, _ = os.path.splitext(os.path.basename(path))
@@ -573,6 +593,7 @@ def parse_all_files(
573
593
  save=False,
574
594
  comments=False,
575
595
  dates=False,
596
+ as_object=False,
576
597
  variables_first=True,
577
598
  _variables_only=False,
578
599
  ):
@@ -583,8 +604,9 @@ def parse_all_files(
583
604
  :param encoding: Encoding used to read files
584
605
  :param keep_data: (default false) Return parsed data of all files in a dictionary
585
606
  :param save: (default false) Save every parsed data in output directory
586
- :param comments: Include comments?
587
- :param dates: Include dates?
607
+ :param comments: (default false) Include comments?
608
+ :param dates: (default false) Include dates?
609
+ :param as_object: (default false) Return data as an object-like instead of dict
588
610
  :param variables_first: Try to parse variables first
589
611
  :return: Dictionary (key: file, value: parsed data if keep_data=True)
590
612
  """
@@ -600,6 +622,7 @@ def parse_all_files(
600
622
  save=save,
601
623
  comments=comments,
602
624
  dates=dates,
625
+ as_object=as_object,
603
626
  variables_first=False,
604
627
  _variables_only=True,
605
628
  )
@@ -620,6 +643,7 @@ def parse_all_files(
620
643
  save=save,
621
644
  comments=comments,
622
645
  dates=dates,
646
+ as_object=as_object,
623
647
  is_global=_variables_only,
624
648
  )
625
649
  if isinstance(data, str):
@@ -691,6 +715,62 @@ def walk(obj, *from_keys):
691
715
  yield obj, from_keys
692
716
 
693
717
 
718
+ def objectify(data, name="Object", default=None):
719
+ """
720
+ Convert a dictionary to an object-like dictionary
721
+ :param data: dictionary
722
+ :param name: name of the object
723
+ :param default: default value for unknown attributes
724
+ :return: object-like dictionary
725
+ """
726
+
727
+ def clean(item):
728
+ if isinstance(item, dict):
729
+ return {key: clean(value) for key, value in item.items() if not key.startswith("_")}
730
+ elif isinstance(item, list):
731
+ return [clean(value) for value in item]
732
+ else:
733
+ return item
734
+
735
+ def _getattr(s, k):
736
+ try:
737
+ object.__getattribute__(s, k)
738
+ except AttributeError:
739
+ return s.get(k, default)
740
+
741
+ if isinstance(data, list):
742
+ return [
743
+ objectify(item)
744
+ for item in data
745
+ if not (isinstance(item, str) and item.startswith("#") and item.endswith("#"))
746
+ ]
747
+ elif isinstance(data, dict):
748
+ name = (data.get("@type") or name).title()
749
+ slots = set()
750
+ attrs = dict(
751
+ __getattr__=lambda s, k: _getattr(s, k),
752
+ __slots__=slots,
753
+ __str__=lambda s: str(clean(s)),
754
+ __repr__=lambda s: repr(clean(s)),
755
+ )
756
+ subdata = {}
757
+ for key, value in data.items():
758
+ if key.startswith("#"):
759
+ continue
760
+ skey = key.replace("@", "_").replace(":", "_")
761
+ if match := regex_date.match(skey):
762
+ skey = "_".join(match.groups())
763
+ if not skey.startswith("_"):
764
+ skey = f"_{skey}" if key != skey or key[0].isdigit() else skey
765
+ slots.add(skey)
766
+ if isinstance(value, (list, dict)):
767
+ subdata[key] = subdata[skey] = objectify(value)
768
+ continue
769
+ subdata[key] = subdata[skey] = value
770
+ return type(name, (dict,), attrs)(subdata)
771
+ return data
772
+
773
+
694
774
  # Tags which are always a list when reverting
695
775
  list_keys_rules = [
696
776
  # Colors
@@ -737,6 +817,8 @@ def revert(obj, from_key=None, prev_key=None, depth=-1, sep="\t", sort=False):
737
817
  elif depth > 0:
738
818
  lines.append(f"{tabs}{{")
739
819
  for key, value in sorted(obj.items()) if sort else obj.items():
820
+ if key.startswith("_"):
821
+ continue
740
822
  lines.extend(revert(value, from_key=key, prev_key=from_key, depth=depth + 1, sep=sep, sort=sort))
741
823
  if from_key or depth > 0:
742
824
  lines.append(f"{tabs}}}")
@@ -884,12 +966,28 @@ def save_variables(filepath="_variables.json"):
884
966
  json_dump(global_variables, file, sort_keys=True)
885
967
 
886
968
 
969
+ def enable_logging(debug=False, file=False):
970
+ logger.setLevel(logging.DEBUG)
971
+ formatter = logging.Formatter("[%(asctime)s] %(levelname)s: %(message)s")
972
+ console_handler = logging.StreamHandler()
973
+ console_handler.setLevel(logging.INFO)
974
+ console_handler.setFormatter(formatter)
975
+ logger.addHandler(console_handler)
976
+ if debug:
977
+ console_handler.setLevel(logging.DEBUG)
978
+ if file:
979
+ file_handler = logging.FileHandler("ckparser.log")
980
+ file_handler.setLevel(logging.INFO)
981
+ file_handler.setFormatter(formatter)
982
+ logger.addHandler(file_handler)
983
+
984
+
887
985
  def main():
888
986
  """
889
987
  Command-line main entrypoint
890
988
  """
891
989
  parser = argparse.ArgumentParser(
892
- description="Parse data from Paradox files in JSON or revert JSON files to Paradox format"
990
+ description=f"Parse data from Paradox files in JSON or revert JSON files to Paradox format ({__version__})"
893
991
  )
894
992
  parser.add_argument("path", type=str, help="path to a file or a directory to parse/revert")
895
993
  parser.add_argument("--encoding", type=str, help="encoding for reading/writing files")
@@ -900,18 +998,7 @@ def main():
900
998
  parser.add_argument("--debug", action="store_true", help="debug mode?")
901
999
  args = parser.parse_args()
902
1000
 
903
- logger.setLevel(logging.DEBUG)
904
- formatter = logging.Formatter("[%(asctime)s] %(levelname)s: %(message)s")
905
- console_handler = logging.StreamHandler()
906
- console_handler.setLevel(logging.INFO)
907
- console_handler.setFormatter(formatter)
908
- logger.addHandler(console_handler)
909
- if args.debug:
910
- console_handler.setLevel(logging.DEBUG)
911
- file_handler = logging.FileHandler("ckparser.log")
912
- file_handler.setLevel(logging.INFO)
913
- file_handler.setFormatter(formatter)
914
- logger.addHandler(file_handler)
1001
+ enable_logging(debug=args.debug, file=True)
915
1002
 
916
1003
  if args.revert:
917
1004
  if os.path.isdir(args.path):
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ckparser"
7
- version = "0.3.2"
7
+ version = "0.3.4"
8
8
  description = "Parse Paradox Jomini data files to Python/JSON and revert them back experimentally."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
File without changes
File without changes
File without changes