FourCIPP 1.2.0__py3-none-any.whl → 1.4.0__py3-none-any.whl

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.
fourcipp/fourc_input.py CHANGED
@@ -27,7 +27,7 @@ import copy
27
27
  import difflib
28
28
  import pathlib
29
29
  from collections.abc import Sequence
30
- from typing import Any
30
+ from typing import Any, Callable
31
31
 
32
32
  from loguru import logger
33
33
 
@@ -37,7 +37,10 @@ from fourcipp.legacy_io import (
37
37
  interpret_legacy_section,
38
38
  )
39
39
  from fourcipp.utils.converter import Converter
40
- from fourcipp.utils.dict_utils import compare_nested_dicts_or_lists
40
+ from fourcipp.utils.dict_utils import (
41
+ compare_nested_dicts_or_lists,
42
+ sort_by_key_order,
43
+ )
41
44
  from fourcipp.utils.not_set import NotSet, check_if_set
42
45
  from fourcipp.utils.typing import Path
43
46
  from fourcipp.utils.validation import ValidationError, validate_using_json_schema
@@ -66,6 +69,75 @@ def is_section_known(section_name: str, known_section_names: list[str]) -> bool:
66
69
  return section_name in known_section_names or section_name.startswith("FUNCT")
67
70
 
68
71
 
72
+ def _sort_by_section_names(data: dict) -> dict:
73
+ """Sort a dictionary by its 4C sections.
74
+
75
+ This sorts the dictionary in the following style:
76
+
77
+ 1. "TITLE" section
78
+ 2. Alphabetically sorted sections
79
+ 3. Alphabetically sorted legacy sections
80
+
81
+ Args:
82
+ data: Dictionary to sort.
83
+ section_names: List of all section names in the 4C style order.
84
+
85
+ Returns:
86
+ Dict sorted in 4C fashion
87
+ """
88
+
89
+ required_sections = CONFIG.fourc_json_schema["required"]
90
+ n_sections_splitter = len(CONFIG.sections.all_sections) * 1000
91
+
92
+ # collect typed sections + numeric FUNCT sections
93
+ typed_and_funct = sorted(
94
+ sorted(CONFIG.sections.typed_sections, key=str.lower)
95
+ + [s for s in data.keys() if s.startswith("FUNCT") and s[5:].isdigit()],
96
+ key=lambda s: (
97
+ s.lower() if not s.startswith("FUNCT") else f"funct{s[5:].zfill(10)}"
98
+ ),
99
+ )
100
+
101
+ def ordering_score(section: str) -> int:
102
+ """Get ordering score, small score comes first, larger comes later.
103
+
104
+ We offset the score by the number of sections multiplied by 1000. This way a score is guaranteed to never appear twice.
105
+
106
+ Args:
107
+ section: Section name to score
108
+
109
+ Returns:
110
+ ordering score
111
+ """
112
+ # Title sections
113
+ if section == CONFIG.fourc_metadata["metadata"]["description_section_name"]:
114
+ return 0
115
+ # Required sections
116
+ elif section in required_sections:
117
+ return 1 * n_sections_splitter + required_sections.index(section)
118
+ # Typed + FUNCT sections (alphabetical + case insensitive)
119
+ elif section in typed_and_funct:
120
+ return 2 * n_sections_splitter + typed_and_funct.index(section)
121
+ # Legacy sections
122
+ elif section in CONFIG.sections.legacy_sections:
123
+ return 3 * n_sections_splitter + CONFIG.sections.legacy_sections.index(
124
+ section
125
+ )
126
+ # Unknown section
127
+ else:
128
+ raise KeyError(f"Unknown section {section}")
129
+
130
+ unknown_sections = set(data.keys()) - set(CONFIG.sections.all_sections)
131
+
132
+ # Remove functions, these are a special case
133
+ if [section for section in unknown_sections if not section.startswith("FUNCT")]:
134
+ raise ValueError(
135
+ f"Sections {list(unknown_sections)} are not known in 'section_names'"
136
+ )
137
+
138
+ return sort_by_key_order(data, sorted(data.keys(), key=ordering_score))
139
+
140
+
69
141
  class FourCInput:
70
142
  """4C inout file object."""
71
143
 
@@ -420,20 +492,22 @@ class FourCInput:
420
492
  def dump(
421
493
  self,
422
494
  input_file_path: Path,
423
- sort_sections: bool = False,
424
495
  validate: bool = False,
425
496
  validate_sections_only: bool = False,
426
497
  convert_to_native_types: bool = True,
498
+ sort_function: Callable[[dict], dict] | None = _sort_by_section_names,
499
+ use_fourcipp_yaml_style: bool = True,
427
500
  ) -> None:
428
501
  """Dump object to yaml.
429
502
 
430
503
  Args:
431
504
  input_file_path: Path to dump the data to
432
- sort_sections: Sort the sections alphabetically
433
505
  validate: Validate input data before dumping
434
506
  validate_sections_only: Validate each section independently.
435
507
  Requiredness of the sections themselves is ignored.
436
508
  convert_to_native_types: Convert all sections to native Python types
509
+ sort_function: Function to sort the sections.
510
+ use_fourcipp_yaml_style: If FourCIPP yaml style is to be used
437
511
  """
438
512
 
439
513
  if validate or validate_sections_only:
@@ -448,7 +522,7 @@ class FourCInput:
448
522
  if convert_to_native_types:
449
523
  self.convert_to_native_types()
450
524
 
451
- dump_yaml(self.inlined, input_file_path, sort_sections)
525
+ dump_yaml(self.inlined, input_file_path, sort_function, use_fourcipp_yaml_style)
452
526
 
453
527
  def validate(
454
528
  self,
fourcipp/utils/cli.py CHANGED
@@ -34,9 +34,12 @@ from fourcipp.utils.configuration import (
34
34
  show_config,
35
35
  )
36
36
  from fourcipp.utils.typing import Path
37
+ from fourcipp.utils.yaml_io import dump_yaml, load_yaml
37
38
 
38
39
 
39
- def modify_input_with_defaults(input_path: Path, overwrite: bool) -> None:
40
+ def modify_input_with_defaults(
41
+ input_path: Path, overwrite: bool
42
+ ) -> None: # pragma: no cover
40
43
  """Apply user defaults to an input file located at input_path.
41
44
 
42
45
  Args:
@@ -62,6 +65,24 @@ def modify_input_with_defaults(input_path: Path, overwrite: bool) -> None:
62
65
  logger.info(f"Input file incl. user defaults is now '{output_filename}'.")
63
66
 
64
67
 
68
+ def format_file(
69
+ input_file: str, sort_sections: bool = False
70
+ ) -> None: # pragma: no cover
71
+ """Formatting file.
72
+
73
+ Args:
74
+ input_file: File to format
75
+ sort_sections: Sort sections
76
+ """
77
+ if sort_sections:
78
+ # Requires reading the config
79
+ fourc_input = FourCInput.from_4C_yaml(input_file)
80
+ fourc_input.dump(input_file, use_fourcipp_yaml_style=True)
81
+ else:
82
+ # No config required, is purely a style question
83
+ dump_yaml(load_yaml(input_file), input_file, use_fourcipp_yaml_style=True)
84
+
85
+
65
86
  def main() -> None:
66
87
  """Main CLI interface."""
67
88
  # Set up the logger
@@ -110,6 +131,23 @@ def main() -> None:
110
131
  type=str,
111
132
  )
112
133
 
134
+ # Format parser
135
+ format_parser = subparsers.add_parser(
136
+ "format",
137
+ help="Format the file in fourcipp style. This sorts the sections and uses the flow styles from FourCIPP",
138
+ )
139
+
140
+ format_parser.add_argument(
141
+ "input-file",
142
+ help=f"4C input file.",
143
+ type=str,
144
+ )
145
+
146
+ format_parser.add_argument(
147
+ "--sort-sections",
148
+ action="store_true",
149
+ help=f"Overwrite existing input file.",
150
+ )
113
151
  # Replace "-" with "_" for variable names
114
152
  kwargs: dict = {}
115
153
  for key, value in vars(main_parser.parse_args(sys.argv[1:])).items():
@@ -126,3 +164,5 @@ def main() -> None:
126
164
  input_path = pathlib.Path(kwargs.pop("input_file"))
127
165
  overwrite = kwargs.pop("overwrite")
128
166
  modify_input_with_defaults(input_path, overwrite)
167
+ case "format":
168
+ format_file(**kwargs)
@@ -45,7 +45,7 @@ class Sections:
45
45
  """
46
46
  self.legacy_sections: list[str] = legacy_sections
47
47
  self.typed_sections: list[str] = typed_sections
48
- self.all_sections: list[str] = legacy_sections + typed_sections
48
+ self.all_sections: list[str] = typed_sections + legacy_sections
49
49
 
50
50
  @classmethod
51
51
  def from_metadata(cls, fourc_metadata: dict) -> Sections:
@@ -58,9 +58,10 @@ class Sections:
58
58
  Sections: sections object
59
59
  """
60
60
  description_section = fourc_metadata["metadata"]["description_section_name"]
61
- sections = [
61
+ sections = [description_section] + [
62
62
  section["name"] for section in fourc_metadata["sections"]["specs"]
63
- ] + [description_section]
63
+ ]
64
+
64
65
  legacy_sections = list(fourc_metadata["legacy_string_sections"])
65
66
 
66
67
  return cls(legacy_sections, sections)
@@ -373,3 +373,35 @@ def rename_parameter(
373
373
 
374
374
  for entry, last_key in _split_off_last_key(nested_dict, keys):
375
375
  entry[new_name] = entry.pop(last_key)
376
+
377
+
378
+ def sort_by_key_order(data: dict, key_order: list[str]) -> dict:
379
+ """Sort a dictionary by a specific key order.
380
+
381
+ Args:
382
+ data: Dictionary to sort.
383
+ key_order: List of keys in the desired order.
384
+
385
+ Returns:
386
+ Sorted dictionary.
387
+ """
388
+ if set(key_order) != set(data.keys()):
389
+ raise ValueError("'key_order' must include all keys in the dictionary!")
390
+
391
+ return {key: data[key] for key in key_order if key in data}
392
+
393
+
394
+ def sort_alphabetically(
395
+ data: dict,
396
+ ) -> dict:
397
+ """Sort a dictionary alphabetically.
398
+
399
+ Args:
400
+ data: Dictionary to sort.
401
+
402
+ Returns:
403
+ Sorted dictionary.
404
+ """
405
+ return sort_by_key_order(
406
+ data, sorted(data.keys(), key=lambda s: (s.lower(), 0 if s.islower() else 1))
407
+ )
fourcipp/utils/yaml_io.py CHANGED
@@ -24,6 +24,7 @@
24
24
  import json
25
25
  import pathlib
26
26
  import re
27
+ from typing import Callable
27
28
 
28
29
  import ryml
29
30
 
@@ -61,26 +62,83 @@ def load_yaml(path_to_yaml_file: Path) -> dict:
61
62
  return data
62
63
 
63
64
 
64
- def dict_to_yaml_string(data: dict, sort_keys: bool = False) -> str:
65
+ def dict_to_yaml_string(
66
+ data: dict,
67
+ sort_function: Callable[[dict], dict] | None = None,
68
+ use_fourcipp_yaml_style: bool = True,
69
+ ) -> str:
65
70
  """Dump dict as yaml.
66
71
 
72
+ The FourCIPP yaml style sets flow
67
73
  Args:
68
74
  data: Data to dump.
69
- sort_keys: If true sort the sections by section name
75
+ sort_function: Function to sort the data.
76
+ use_fourcipp_yaml_style: If FourCIPP yaml style is to be used
77
+
78
+ Returns:
79
+ YAML string representation of the data
70
80
  """
71
81
 
72
- # Sort keys
73
- if sort_keys:
74
- data = {key: data[key] for key in sorted(data.keys())}
82
+ if sort_function is not None:
83
+ data = sort_function(data)
75
84
 
76
85
  # Convert dictionary into a ryml tree
77
86
  tree = ryml.parse_in_arena(bytearray(json.dumps(data).encode("utf8")))
78
87
 
79
- # remove all style bits to enable a YAML style output
88
+ def check_is_vector(tree: ryml.Tree, node_id: int) -> bool:
89
+ """Check if sequence is of ints, floats or sequence there of.
90
+
91
+ In 4C metadata terms, list of strings, bools, etc. could also be vectors.
92
+ For the sake of simplicity these are omitted.
93
+
94
+ Args:
95
+ tree (ryml.Tree): Tree to check
96
+ node_id (int): Node id
97
+
98
+ Returns:
99
+ returns if entry is a vector
100
+ """
101
+
102
+ for sub_node, _ in ryml.walk(tree, node_id):
103
+ # Ignore the root node
104
+ if sub_node == node_id:
105
+ continue
106
+
107
+ # If sequence contains a dict
108
+ if tree.is_map(sub_node):
109
+ return False
110
+
111
+ # If sequence contains a sequence
112
+ elif tree.is_seq(sub_node):
113
+ if not check_is_vector(tree, sub_node):
114
+ return False
115
+
116
+ # Else it's a value
117
+ else:
118
+ val = tree.val(sub_node).tobytes().decode("ascii")
119
+ is_not_numeric = (
120
+ tree.is_val_quoted(sub_node) # string
121
+ or tree.val_is_null(sub_node) # null
122
+ or val == "true" # bool
123
+ or val == "false" # bool
124
+ )
125
+ if is_not_numeric:
126
+ return False
127
+
128
+ return True
129
+
130
+ # Change style bits to avoid JSON output and format vectors correctly
80
131
  # see https://github.com/biojppm/rapidyaml/issues/520
81
- for node_id, _ in ryml.walk(tree):
82
- if tree.is_map(node_id) or tree.is_seq(node_id):
132
+ for node_id, depth in ryml.walk(tree):
133
+ if tree.is_map(node_id):
83
134
  tree.set_container_style(node_id, ryml.NOTYPE)
135
+ elif tree.is_seq(node_id):
136
+ if (
137
+ not use_fourcipp_yaml_style # do not do special formatting
138
+ or depth == 1 # is a section
139
+ or not check_is_vector(tree, node_id) # is not a vector
140
+ ):
141
+ tree.set_container_style(node_id, ryml.NOTYPE)
84
142
 
85
143
  if tree.has_key(node_id):
86
144
  tree.set_key_style(node_id, ryml.NOTYPE)
@@ -88,14 +146,21 @@ def dict_to_yaml_string(data: dict, sort_keys: bool = False) -> str:
88
146
  return ryml.emit_yaml(tree)
89
147
 
90
148
 
91
- def dump_yaml(data: dict, path_to_yaml_file: Path, sort_keys: bool = False) -> None:
149
+ def dump_yaml(
150
+ data: dict,
151
+ path_to_yaml_file: Path,
152
+ sort_function: Callable[[dict], dict] | None = None,
153
+ use_fourcipp_yaml_style: bool = True,
154
+ ) -> None:
92
155
  """Dump yaml to file.
93
156
 
94
157
  Args:
95
158
  data: Data to dump.
96
159
  path_to_yaml_file: Yaml file path
97
- sort_keys: If true sort the sections by section name
160
+ sort_function: Function to sort the data
161
+ use_fourcipp_yaml_style: If FourCIPP yaml style is to be used
98
162
  """
99
163
  pathlib.Path(path_to_yaml_file).write_text(
100
- dict_to_yaml_string(data, sort_keys), encoding="utf-8"
164
+ dict_to_yaml_string(data, sort_function, use_fourcipp_yaml_style),
165
+ encoding="utf-8",
101
166
  )
fourcipp/version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '1.2.0'
32
- __version_tuple__ = version_tuple = (1, 2, 0)
31
+ __version__ = version = '1.4.0'
32
+ __version_tuple__ = version_tuple = (1, 4, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: FourCIPP
3
- Version: 1.2.0
3
+ Version: 1.4.0
4
4
  Summary: A streamlined Python Parser for 4C input files
5
5
  Author: FourCIPP Authors
6
6
  License: The MIT License (MIT)
@@ -168,7 +168,7 @@ input_4C["PROBLEM SIZE"]["NODES"] = 10_000_000
168
168
  removed_section = input_4C.pop("PROBLEM SIZE")
169
169
 
170
170
  # Dump to file
171
- input_4C.dump(input_file_path, sort_sections=True, validate=True)
171
+ input_4C.dump(input_file_path, validate=True)
172
172
  ```
173
173
  <!--example, do not remove this comment-->
174
174
 
@@ -1,6 +1,6 @@
1
1
  fourcipp/__init__.py,sha256=4pz7DVXErSbUcLqPTaHnQfdJzKkpfZDivBqbHbTgpRE,1388
2
- fourcipp/fourc_input.py,sha256=4ZcaD5NQVqPqsUaUCap0UFLn80QboXSbrvWk1qd7yTU,21260
3
- fourcipp/version.py,sha256=-uLONazCO1SzFfcY-K6A1keL--LIVfTYccGX6ciADac,704
2
+ fourcipp/fourc_input.py,sha256=TXVP-N_FTadt4L8MPH3VZXeT7i5uy7Btz3H-N0pxMr0,23948
3
+ fourcipp/version.py,sha256=qX8_mGjWClM0QeMyVAByaVXEA4Pk87fxkFSMEQx9-KM,704
4
4
  fourcipp/config/4C_metadata.yaml,sha256=3n3Uj24iblvsfqUI-8H2LYMHOROjeHl68Nok0F22Unc,11868927
5
5
  fourcipp/config/4C_schema.json,sha256=67giZGZSKXkjO_NCbZzx-blVdGB6BFXRbHrXkmqB_T4,16412275
6
6
  fourcipp/config/config.yaml,sha256=n2c2a6E4HKfAdNWOQz1kLUuf5p4NLxIddaAi2t5eM38,460
@@ -11,18 +11,18 @@ fourcipp/legacy_io/node.py,sha256=e7m0W7dai_b1XgaqD37k3QS44ySCas3HetmyTLS9_78,35
11
11
  fourcipp/legacy_io/node_topology.py,sha256=hqGGzhyNhFX-BMW8SzztfzM8zoZLLltVScMv-p4hDH0,5394
12
12
  fourcipp/legacy_io/particle.py,sha256=0rxwdiMnHI1QUGZPqWgsxqyslrxB7aq7qxgNMb_y_rk,2166
13
13
  fourcipp/utils/__init__.py,sha256=ccdlf4taJ0mKLg_ru8ilXEa72PoO9N2UIxHNHDEtQmY,1151
14
- fourcipp/utils/cli.py,sha256=43Nmy9tnoQzwWMZwpktWVv3T3a3y4qDzfuDB1bHogbA,4658
15
- fourcipp/utils/configuration.py,sha256=mDrizczVvMQ1r5EancScbWzYn3g9bmQAdDACLbPLfq0,6460
14
+ fourcipp/utils/cli.py,sha256=I0N6mcJQGX7seLtXpfrPJA5EWrmhgOph_gHrrxEs6JE,5801
15
+ fourcipp/utils/configuration.py,sha256=HDrvHrH4K5mdienzVsAt_sh9j2Pv81bxWBqqbeNb1KE,6461
16
16
  fourcipp/utils/converter.py,sha256=D40YQ96WqPEEpB7fwAB5XC2v-jRki4v1sIJ-ngO08nU,5194
17
- fourcipp/utils/dict_utils.py,sha256=Ha9eHHcItLZFQgkqC_Ew_2h_2tmLNmzdJmpECcJ3LFQ,12126
17
+ fourcipp/utils/dict_utils.py,sha256=uC0uaBNP3Wh2v3kFy9JHnAYARMukAN4cl40pmiBT13Y,12891
18
18
  fourcipp/utils/metadata.py,sha256=98jz9Gm8qdvZ-b1jwYObNJTaiMQWwbknvS70Nv6Gwhk,1291
19
19
  fourcipp/utils/not_set.py,sha256=04C_3axe2cupBYgfpgDAcGs1zHVzG3I58UGO58TH05A,2017
20
20
  fourcipp/utils/typing.py,sha256=8iX9PuKe8B1WJ3vEjiM5ZfefvgYnaZDiSdB7Nx9SrVw,1625
21
21
  fourcipp/utils/validation.py,sha256=FejHOj1MddaU7gEpMN-f8Mz3rYjflakd1qcsKnctHqA,5292
22
- fourcipp/utils/yaml_io.py,sha256=fPJiIMVzVoHX97LFynGxmQ2PKQsUAb3GIBDWvkPfftI,3477
23
- fourcipp-1.2.0.dist-info/licenses/LICENSE,sha256=lkSOHdH9IZ8c3Vnz0fFjqls1cRlmLADBP8QEIwUlH3o,1082
24
- fourcipp-1.2.0.dist-info/METADATA,sha256=hTqbCDM3DU1mK3f_pGeYSvT0xTR3gBe4TbBSr2wZxVI,7967
25
- fourcipp-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
26
- fourcipp-1.2.0.dist-info/entry_points.txt,sha256=44XBG4bwhuq1EwOZ0U055HYP8_qN6_d30ecVsa5Igng,53
27
- fourcipp-1.2.0.dist-info/top_level.txt,sha256=oZ6jgFFmvi157VwGUEFuKT3D8oS5mOkpOVx8zZURZrQ,9
28
- fourcipp-1.2.0.dist-info/RECORD,,
22
+ fourcipp/utils/yaml_io.py,sha256=bs78Mc3fIz0kK5wgwwu89OhaDpLzECTRBQtIpnifTqk,5559
23
+ fourcipp-1.4.0.dist-info/licenses/LICENSE,sha256=lkSOHdH9IZ8c3Vnz0fFjqls1cRlmLADBP8QEIwUlH3o,1082
24
+ fourcipp-1.4.0.dist-info/METADATA,sha256=ldyK7yc2khoquz4mNXn5ibAG8EATRDaUlYiPxNnsGW4,7947
25
+ fourcipp-1.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
26
+ fourcipp-1.4.0.dist-info/entry_points.txt,sha256=44XBG4bwhuq1EwOZ0U055HYP8_qN6_d30ecVsa5Igng,53
27
+ fourcipp-1.4.0.dist-info/top_level.txt,sha256=oZ6jgFFmvi157VwGUEFuKT3D8oS5mOkpOVx8zZURZrQ,9
28
+ fourcipp-1.4.0.dist-info/RECORD,,