ckparser 0.1__tar.gz → 0.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ckparser
3
- Version: 0.1
3
+ Version: 0.2.1
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
@@ -132,10 +132,10 @@ Parse a directory recursively and write JSON output:
132
132
  python -m ckparser my_mod/common --output output
133
133
  ```
134
134
 
135
- Include comments in the parsed output:
135
+ Include comments and/or dates in the parsed output:
136
136
 
137
137
  ```bash
138
- python -m ckparser my_mod/common --comments
138
+ python -m ckparser my_mod/common --comments --dates
139
139
  ```
140
140
 
141
141
  Revert a JSON file back to Jomini format:
@@ -412,6 +412,14 @@ With `comments=True` or `--comments`, the parser attempts to preserve them in an
412
412
 
413
413
  Comment preservation should be considered practical rather than perfectly lossless.
414
414
 
415
+ ### Dates
416
+
417
+ By default, dates are not converted in Python `datetime.date` objects in parsing.
418
+
419
+ While convenient, Jomini date format (`Y[YYY].M[M].D[D]`) does not enforce validation and can be invalid.
420
+
421
+ With `dates=True` or `--dates`, the parser attempts to convert these dates if possible and keeps invalid dates as text.
422
+
415
423
  ### Localization parsing
416
424
 
417
425
  `ckparser` also includes a dedicated parser for Paradox localization files:
@@ -510,9 +518,9 @@ Paradox scripting languages include many practical exceptions, formatting varian
510
518
 
511
519
  ### Parsing
512
520
 
513
- * `parse_text(text, return_text_on_error=False, comments=False, filename=None, is_global=False)`
514
- * `parse_file(path, output_dir=None, encoding="utf_8_sig", base_dir=None, save=False, comments=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, variables_first=True)`
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)`
516
524
 
517
525
  ### Reversion
518
526
 
@@ -101,10 +101,10 @@ Parse a directory recursively and write JSON output:
101
101
  python -m ckparser my_mod/common --output output
102
102
  ```
103
103
 
104
- Include comments in the parsed output:
104
+ Include comments and/or dates in the parsed output:
105
105
 
106
106
  ```bash
107
- python -m ckparser my_mod/common --comments
107
+ python -m ckparser my_mod/common --comments --dates
108
108
  ```
109
109
 
110
110
  Revert a JSON file back to Jomini format:
@@ -381,6 +381,14 @@ With `comments=True` or `--comments`, the parser attempts to preserve them in an
381
381
 
382
382
  Comment preservation should be considered practical rather than perfectly lossless.
383
383
 
384
+ ### Dates
385
+
386
+ By default, dates are not converted in Python `datetime.date` objects in parsing.
387
+
388
+ While convenient, Jomini date format (`Y[YYY].M[M].D[D]`) does not enforce validation and can be invalid.
389
+
390
+ With `dates=True` or `--dates`, the parser attempts to convert these dates if possible and keeps invalid dates as text.
391
+
384
392
  ### Localization parsing
385
393
 
386
394
  `ckparser` also includes a dedicated parser for Paradox localization files:
@@ -479,9 +487,9 @@ Paradox scripting languages include many practical exceptions, formatting varian
479
487
 
480
488
  ### Parsing
481
489
 
482
- * `parse_text(text, return_text_on_error=False, comments=False, filename=None, is_global=False)`
483
- * `parse_file(path, output_dir=None, encoding="utf_8_sig", base_dir=None, save=False, comments=False, is_global=False, patch=None)`
484
- * `parse_all_files(path, output_dir=None, encoding="utf_8_sig", keep_data=False, save=False, comments=False, variables_first=True)`
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)`
485
493
 
486
494
  ### Reversion
487
495
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ckparser
3
- Version: 0.1
3
+ Version: 0.2.1
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
@@ -132,10 +132,10 @@ Parse a directory recursively and write JSON output:
132
132
  python -m ckparser my_mod/common --output output
133
133
  ```
134
134
 
135
- Include comments in the parsed output:
135
+ Include comments and/or dates in the parsed output:
136
136
 
137
137
  ```bash
138
- python -m ckparser my_mod/common --comments
138
+ python -m ckparser my_mod/common --comments --dates
139
139
  ```
140
140
 
141
141
  Revert a JSON file back to Jomini format:
@@ -412,6 +412,14 @@ With `comments=True` or `--comments`, the parser attempts to preserve them in an
412
412
 
413
413
  Comment preservation should be considered practical rather than perfectly lossless.
414
414
 
415
+ ### Dates
416
+
417
+ By default, dates are not converted in Python `datetime.date` objects in parsing.
418
+
419
+ While convenient, Jomini date format (`Y[YYY].M[M].D[D]`) does not enforce validation and can be invalid.
420
+
421
+ With `dates=True` or `--dates`, the parser attempts to convert these dates if possible and keeps invalid dates as text.
422
+
415
423
  ### Localization parsing
416
424
 
417
425
  `ckparser` also includes a dedicated parser for Paradox localization files:
@@ -510,9 +518,9 @@ Paradox scripting languages include many practical exceptions, formatting varian
510
518
 
511
519
  ### Parsing
512
520
 
513
- * `parse_text(text, return_text_on_error=False, comments=False, filename=None, is_global=False)`
514
- * `parse_file(path, output_dir=None, encoding="utf_8_sig", base_dir=None, save=False, comments=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, variables_first=True)`
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)`
516
524
 
517
525
  ### Reversion
518
526
 
@@ -28,6 +28,7 @@ Author: Marc Debureaux (debnet)
28
28
  Project: https://github.com/debnet/ckparser
29
29
  License: MIT
30
30
  """
31
+
31
32
  import argparse
32
33
  import ast
33
34
  import colorsys
@@ -40,7 +41,7 @@ import re
40
41
  import time
41
42
 
42
43
  # Script version
43
- __version__ = "0.1"
44
+ __version__ = "0.2.1"
44
45
 
45
46
  # Logger (because logging is awesome)
46
47
  logger = logging.getLogger(__name__)
@@ -52,6 +53,38 @@ except ImportError:
52
53
  detect = None
53
54
  logger.warning("chardet not installed, encoding detection is disabled!")
54
55
 
56
+
57
+ class JominiJSONEncoder(json.JSONEncoder):
58
+ # JSON specific encoder for dates
59
+ def default(self, obj):
60
+ if isinstance(obj, datetime.date):
61
+ return f"{obj.year}.{obj.month}.{obj.day}"
62
+ return super().default(obj)
63
+
64
+
65
+ def jomini_object_hook(obj):
66
+ # JSON specific decoder for dates
67
+ if isinstance(obj, dict):
68
+ for key, value in obj.items():
69
+ if isinstance(value, str) and regex_date.fullmatch(value):
70
+ obj[key] = convert_date(value) or value
71
+ elif isinstance(value, list):
72
+ obj[key] = jomini_object_hook(value)
73
+ elif isinstance(obj, list):
74
+ for index in range(len(obj)):
75
+ item = obj[index]
76
+ if isinstance(item, str) and regex_date.fullmatch(item):
77
+ obj[index] = convert_date(item) or item
78
+ elif isinstance(item, list):
79
+ obj[index] = jomini_object_hook(item)
80
+ return obj
81
+
82
+
83
+ json_load = functools.partial(json.load, object_hook=jomini_object_hook)
84
+ json_loads = functools.partial(json.loads, object_hook=jomini_object_hook)
85
+ json_dump = functools.partial(json.dump, cls=JominiJSONEncoder, indent=4, ensure_ascii=False)
86
+ json_dumps = functools.partial(json.dumps, cls=JominiJSONEncoder, indent=4, ensure_ascii=False)
87
+
55
88
  # Boolean transformation
56
89
  booleans = {"yes": True, "no": False}
57
90
  # Tags which must be aggregate as a list in JSON (don't hesitate to add more if needed)
@@ -84,6 +117,8 @@ regex_values = re.compile(r"(([?!<=>]+)\s*\n+)|(\n+\s*([?!<=>]+))")
84
117
  regex_line = re.compile(r"\"?(?P<key>[^\s\"]+)\"?\s*(?P<operator>[?!<=>]+)\s*(list\s*)?(?P<value>.*)")
85
118
  # Regex to parse independent items in a list
86
119
  regex_item = re.compile(r"(\"[^\"]+\"|[\d\.]+|[^\s]+)")
120
+ # Regex to parse dates
121
+ regex_date = re.compile(r"^(?:\d{1,4})\.(?:[1-9]|1[0-2])\.(?:[1-9]|[12]\d|3[01])$")
87
122
  # Regex to remove empty lines
88
123
  regex_empty = re.compile(r"(\n\s*\n)+", re.MULTILINE)
89
124
  # Regex to parse locale files
@@ -151,12 +186,13 @@ def read_file(path, encoding="utf_8_sig"):
151
186
  return file.read()
152
187
 
153
188
 
154
- def parse_text(text, return_text_on_error=False, comments=False, filename=None, is_global=False):
189
+ def parse_text(text, return_text_on_error=False, comments=False, dates=False, filename=None, is_global=False):
155
190
  """
156
191
  Parse raw text
157
192
  :param text: Text to parse
158
193
  :param return_text_on_error: (default false) Return working text document if parsing fails
159
194
  :param comments: (default false) Include comments?
195
+ :param dates: (default false) Include dates?
160
196
  :param filename: (default none) Filename (only for debugging)
161
197
  :param is_global: (default false) Are variables global?
162
198
  :return: Parsed data as dictionary
@@ -251,10 +287,13 @@ def parse_text(text, return_text_on_error=False, comments=False, filename=None,
251
287
  value = val
252
288
  elif value:
253
289
  # Try to convert value to Python value
254
- try:
255
- value = ast.literal_eval(value)
256
- except: # noqa
257
- pass
290
+ if dates and regex_date.fullmatch(value):
291
+ value = convert_date(value) or value
292
+ else:
293
+ try:
294
+ value = ast.literal_eval(value)
295
+ except: # noqa
296
+ pass
258
297
  # If key is duplicate with direct value
259
298
  if key in node:
260
299
  if node[key] != value: # Avoid single duplicates
@@ -331,7 +370,7 @@ def parse_text(text, return_text_on_error=False, comments=False, filename=None,
331
370
  node[key] = {"@type": "variable", "@value": value, "@result": result}
332
371
  if result is not None and (key.startswith("@") or len(nodes) == 1):
333
372
  set_variable(key, result)
334
- elif result := variables.get(value):
373
+ elif (result := variables.get(value)) is not None:
335
374
  if not isinstance(result, str):
336
375
  node[key] = {"@type": "variable", "@value": value, "@result": result}
337
376
  if result is not None and (key.startswith("@") or len(nodes) == 1):
@@ -426,11 +465,15 @@ def parse_text(text, return_text_on_error=False, comments=False, filename=None,
426
465
  logger.warning(f"Filename: {filename}")
427
466
  logger.warning(f'Value for "{value}" cannot be found (line: {line_number})')
428
467
  node.append({"@type": "variable", "@value": item, "@result": result})
468
+ elif dates and regex_date.fullmatch(item):
469
+ item = convert_date(item) or item
470
+ node.append(item)
429
471
  else:
430
472
  try:
431
- node.append(ast.literal_eval(item))
473
+ item = ast.literal_eval(item)
432
474
  except: # noqa
433
- node.append(item)
475
+ pass
476
+ node.append(item)
434
477
  except Exception as error:
435
478
  if filename:
436
479
  logger.error(f"Filename: {filename}")
@@ -448,6 +491,7 @@ def parse_file(
448
491
  base_dir=None,
449
492
  save=False,
450
493
  comments=False,
494
+ dates=False,
451
495
  is_global=False,
452
496
  patch=None,
453
497
  ):
@@ -459,6 +503,7 @@ def parse_file(
459
503
  :param base_dir: Base directory (for debug)
460
504
  :param save: (default false) Save parsed file in output directory
461
505
  :param comments: Include comments?
506
+ :param dates: Include dates?
462
507
  :param is_global: (default false) Are file's variables global?
463
508
  :param patch: String replacement patterns
464
509
  :return: Parsed data as dictionary or text if parsing fails
@@ -475,7 +520,9 @@ def parse_file(
475
520
  text = re.sub(pattern, replacement, text)
476
521
  filename = os.path.join(base_dir, os.path.basename(path)).replace(os.sep, "/")
477
522
  logger.debug(f"Parsing {filename}")
478
- data = parse_text(text, return_text_on_error=True, comments=comments, filename=filename, is_global=is_global)
523
+ data = parse_text(
524
+ text, return_text_on_error=True, comments=comments, dates=dates, filename=filename, is_global=is_global
525
+ )
479
526
  if save:
480
527
  filename, _ = os.path.splitext(os.path.basename(path))
481
528
  directory = os.path.join(output_dir or "output", *base_dir.split("/")).replace(os.sep, "/")
@@ -490,7 +537,7 @@ def parse_file(
490
537
  else:
491
538
  filename = os.path.join(directory, filename + ".json")
492
539
  with open(filename, "w") as file:
493
- json.dump(data, file, indent=4)
540
+ json_dump(data, file)
494
541
  total_time = time.monotonic() - start_time
495
542
  logger.debug(f"Elapsed time: {total_time:0.3f}s!")
496
543
  return data
@@ -503,6 +550,7 @@ def parse_all_files(
503
550
  keep_data=False,
504
551
  save=False,
505
552
  comments=False,
553
+ dates=False,
506
554
  variables_first=True,
507
555
  _variables_only=False,
508
556
  ):
@@ -514,6 +562,7 @@ def parse_all_files(
514
562
  :param keep_data: (default false) Return parsed data of all files in a dictionary
515
563
  :param save: (default false) Save every parsed data in output directory
516
564
  :param comments: Include comments?
565
+ :param dates: Include dates?
517
566
  :param variables_first: Try to parse variables first
518
567
  :return: Dictionary (key: file, value: parsed data if keep_data=True)
519
568
  """
@@ -522,12 +571,13 @@ def parse_all_files(
522
571
  if variables_first and not _variables_only:
523
572
  success.update(
524
573
  parse_all_files(
525
- path,
526
- output_dir,
527
- encoding,
528
- keep_data,
529
- save,
530
- comments,
574
+ path=path,
575
+ output_dir=output_dir,
576
+ encoding=encoding,
577
+ keep_data=keep_data,
578
+ save=save,
579
+ comments=comments,
580
+ dates=dates,
531
581
  variables_first=False,
532
582
  _variables_only=True,
533
583
  )
@@ -547,6 +597,7 @@ def parse_all_files(
547
597
  base_dir=path,
548
598
  save=save,
549
599
  comments=comments,
600
+ dates=dates,
550
601
  is_global=_variables_only,
551
602
  )
552
603
  if isinstance(data, str):
@@ -561,13 +612,14 @@ def parse_all_files(
561
612
  return success
562
613
 
563
614
 
564
- def parse_all_locales(path, encoding="utf_8_sig", language="english", save=False):
615
+ def parse_all_locales(path, encoding="utf_8_sig", language="english", save=False, output="_locales.json"):
565
616
  """
566
617
  Parse all locales strings
567
618
  :param path: Path where to find locale files
568
619
  :param encoding: Encoding for reading files
569
620
  :param language: Target language
570
621
  :param save: (default false) save locales in file
622
+ :param output: locales output file location
571
623
  :return: Locales in dictionary
572
624
  """
573
625
  locales = {}
@@ -595,8 +647,8 @@ def parse_all_locales(path, encoding="utf_8_sig", language="english", save=False
595
647
  key, _, value = match.groups()
596
648
  locales[key] = value
597
649
  if save:
598
- with open("_locales.json", "w") as file:
599
- json.dump(locales, file, indent=4, sort_keys=True)
650
+ with open(output, "w") as file:
651
+ json_dump(locales, file, sort_keys=True)
600
652
  return locales
601
653
 
602
654
 
@@ -729,6 +781,8 @@ def revert_value(value, from_key=None, prev_key=None, **kwargs):
729
781
  return f'"{value}"'
730
782
  elif isinstance(value, dict):
731
783
  value = revert(value, from_key=from_key, prev_key=prev_key, depth=0, **kwargs)
784
+ elif isinstance(value, datetime.date):
785
+ value = f"{value.year}.{value.month}.{value.day}"
732
786
  return value
733
787
 
734
788
 
@@ -803,7 +857,7 @@ def save_variables(filepath="_variables.json"):
803
857
  if not global_variables:
804
858
  return
805
859
  with open(filepath, "w") as file:
806
- json.dump(global_variables, file, indent=4, sort_keys=True)
860
+ json_dump(global_variables, file, sort_keys=True)
807
861
 
808
862
 
809
863
  def main():
@@ -818,6 +872,7 @@ def main():
818
872
  parser.add_argument("--output", type=str, help="output directory for parsing results")
819
873
  parser.add_argument("--revert", action="store_true", help="revert JSON files?")
820
874
  parser.add_argument("--comments", action="store_true", help="include comments?")
875
+ parser.add_argument("--dates", action="store_true", help="include dates?")
821
876
  parser.add_argument("--debug", action="store_true", help="debug mode?")
822
877
  args = parser.parse_args()
823
878
 
@@ -847,6 +902,7 @@ def main():
847
902
  encoding=args.encoding,
848
903
  output_dir=args.output,
849
904
  comments=args.comments,
905
+ dates=args.dates,
850
906
  save=True,
851
907
  )
852
908
  else:
@@ -855,6 +911,7 @@ def main():
855
911
  encoding=args.encoding,
856
912
  output_dir=args.output,
857
913
  comments=args.comments,
914
+ dates=args.dates,
858
915
  save=True,
859
916
  )
860
917
  save_variables()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ckparser"
7
- version = "0.1"
7
+ version = "0.2.1"
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