Skip to content

Commit a596285

Browse files
committed
Update file updating logic
1 parent 694afb1 commit a596285

File tree

1 file changed

+94
-109
lines changed

1 file changed

+94
-109
lines changed

src/foamlib/_files/files.py

Lines changed: 94 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -480,21 +480,6 @@ def getall(
480480
FoamFile.SubDict(self, keywords) if v is ... else deepcopy(v) for v in ret
481481
]
482482

483-
def _normalize_and_validate_keywords(
484-
self, keywords: str | tuple[str, ...] | None
485-
) -> tuple[str, ...]:
486-
"""Normalize keywords to tuple format and validate them."""
487-
if keywords is None:
488-
keywords = ()
489-
elif not isinstance(keywords, tuple):
490-
keywords = (keywords,)
491-
492-
if keywords and not isinstance(normalize(keywords[-1], bool_ok=False), str):
493-
msg = f"Invalid keyword type: {keywords[-1]} (type {type(keywords[-1])})"
494-
raise TypeError(msg)
495-
496-
return keywords
497-
498483
def _write_header_if_needed(self, keywords: tuple[str, ...]) -> None:
499484
"""Write FoamFile header if needed."""
500485
try:
@@ -517,10 +502,10 @@ def _write_header_if_needed(self, keywords: tuple[str, ...]) -> None:
517502
def _update_class_for_field_if_needed(
518503
self,
519504
keywords: tuple[str, ...],
520-
data: DataLike | StandaloneDataLike | SubDictLike,
505+
data: Data | StandaloneData | SubDict,
521506
) -> None:
522507
"""Update class field to appropriate field type if this is a field entry."""
523-
if (
508+
if isinstance(data, (float, int, np.ndarray)) and (
524509
keywords == ("internalField",)
525510
or (
526511
len(keywords) == 3
@@ -532,26 +517,24 @@ def _update_class_for_field_if_needed(
532517
)
533518
)
534519
) and self.class_ == "dictionary":
535-
try:
536-
tensor_kind = _tensor_kind_for_field(data) # ty: ignore[invalid-argument-type]
537-
except ValueError:
538-
pass
539-
else:
540-
self.class_ = "vol" + tensor_kind[0].upper() + tensor_kind[1:] + "Field"
520+
tensor_kind = _tensor_kind_for_field(data)
521+
self.class_ = "vol" + tensor_kind[0].upper() + tensor_kind[1:] + "Field"
541522

542523
def _calculate_spacing(
543524
self,
544525
keywords: tuple[str, ...],
545526
start: int,
546527
end: int,
547-
operation: Literal["put", "add"],
528+
/,
529+
*,
530+
add: bool,
548531
) -> tuple[bytes, bytes]:
549532
"""Calculate before/after spacing for entry operations."""
550533
parsed = self._get_parsed(missing_ok=True)
551534

552535
# For setitem operations, check if this is an update to an existing entry
553536
# and preserve existing spacing for sub-dictionary entries
554-
if operation == "put":
537+
if not add:
555538
is_update = keywords in parsed
556539
if is_update and len(keywords) > 1:
557540
# For existing sub-dictionary entries, preserve existing formatting
@@ -588,105 +571,99 @@ def _calculate_spacing(
588571

589572
def _perform_entry_operation(
590573
self,
591-
keywords: str | tuple[str, ...] | None,
574+
keywords: tuple[str, ...],
592575
data: DataLike | StandaloneDataLike | SubDictLike,
593-
operation: Literal["put", "add"],
576+
/,
577+
*,
578+
add: bool,
594579
) -> None:
595580
"""Shared method for performing entry operations (setitem and add)."""
596-
keywords = self._normalize_and_validate_keywords(keywords)
581+
if keywords:
582+
keyword = normalize(keywords[-1], bool_ok=False)
583+
584+
if not isinstance(keyword, str):
585+
msg = (
586+
f"Invalid keyword type: {keywords[-1]} (type {type(keywords[-1])})"
587+
)
588+
raise TypeError(msg)
589+
590+
if keyword != keywords[-1]:
591+
msg = f"Invalid keyword: {keywords[-1]}"
592+
raise ValueError(msg)
593+
594+
data = normalize(data, keywords=keywords)
595+
596+
indentation = b" " * (len(keywords) - 1)
597597

598598
with self:
599599
self._write_header_if_needed(keywords)
600600
self._update_class_for_field_if_needed(keywords, data)
601601

602602
parsed = self._get_parsed(missing_ok=True)
603-
start, end = parsed.entry_location(keywords, add=(operation == "add"))
604-
before, after = self._calculate_spacing(keywords, start, end, operation)
605-
self._process_data_entry(keywords, data, before, after, operation)
603+
start, end = parsed.entry_location(keywords, add=add)
604+
before, after = self._calculate_spacing(keywords, start, end, add=add)
606605

607-
def _process_data_entry(
608-
self,
609-
keywords: tuple[str, ...],
610-
data: DataLike | StandaloneDataLike | SubDictLike,
611-
before: bytes,
612-
after: bytes,
613-
operation: Literal["put", "add"],
614-
) -> None:
615-
"""Process and write a data entry using either put or add operation."""
616-
parsed = self._get_parsed(missing_ok=True)
617-
indentation = b" " * (len(keywords) - 1)
606+
try:
607+
header = self["FoamFile"]
608+
assert isinstance(header, FoamFile.SubDict)
609+
except (KeyError, FileNotFoundError):
610+
header = None
618611

619-
if isinstance(data, Mapping):
620-
if not keywords:
621-
msg = "Cannot set a mapping at the root level of a FoamFile\nUse update(), extend(), or merge() instead."
622-
raise ValueError(msg)
612+
if isinstance(data, Mapping):
613+
if not keywords:
614+
msg = "Cannot set a mapping at the root level of a FoamFile\nUse update(), extend(), or merge() instead."
615+
raise ValueError(msg)
623616

624-
keyword = normalize(keywords[-1], bool_ok=False)
617+
if keyword.startswith("#"):
618+
msg = f"Cannot set a directive as the keyword for a dictionary: {keyword}"
619+
raise ValueError(msg)
625620

626-
if not isinstance(keyword, str):
627-
msg = (
628-
f"Invalid keyword type: {keywords[-1]} (type {type(keywords[-1])})"
629-
)
630-
raise TypeError(msg)
621+
if add and keywords in parsed:
622+
raise KeyError(keywords)
631623

632-
if keyword.startswith("#"):
633-
msg = (
634-
f"Cannot set a directive as the keyword for a dictionary: {keyword}"
624+
empty_dict_content = (
625+
before
626+
+ indentation
627+
+ dumps(keyword)
628+
+ b"\n"
629+
+ indentation
630+
+ b"{\n"
631+
+ indentation
632+
+ b"}"
633+
+ after
635634
)
636-
raise ValueError(msg)
635+
parsed.put(keywords, ..., empty_dict_content)
637636

638-
data = normalize(data, keywords=keywords)
639-
640-
content = (
641-
before
642-
+ indentation
643-
+ dumps(keyword)
644-
+ b"\n"
645-
+ indentation
646-
+ b"{\n"
647-
+ indentation
648-
+ b"}"
649-
+ after
650-
)
637+
for k, v in data.items():
638+
self[(*keywords, k)] = v
651639

652-
if operation == "add" and keywords in parsed:
653-
raise KeyError(keywords)
654-
655-
parsed.put(keywords, ..., content)
656-
for k, v in data.items(): # ty: ignore[possibly-missing-attribute]
657-
self[(*keywords, k)] = v
658-
659-
elif keywords:
660-
header = self.get("FoamFile", None)
661-
assert header is None or isinstance(header, FoamFile.SubDict)
662-
val = dumps(data, keywords=keywords, header=header)
663-
664-
content = (
665-
before
666-
+ indentation
667-
+ dumps(normalize(keywords[-1], bool_ok=False))
668-
+ ((b" " + val) if val else b"")
669-
+ (b";" if not keywords[-1].startswith("#") else b"")
670-
+ after
671-
)
640+
elif keywords:
641+
val = dumps(data, keywords=keywords, header=header)
672642

673-
if operation == "put":
674-
parsed.put(keywords, normalize(data, keywords=keywords), content)
675-
else: # operation == "add"
676-
if keywords in parsed and not keywords[-1].startswith("#"):
677-
raise KeyError(keywords)
678-
parsed.add(keywords, normalize(data, keywords=keywords), content)
643+
content = (
644+
before
645+
+ indentation
646+
+ dumps(keyword)
647+
+ ((b" " + val) if val else b"")
648+
+ (b";" if not keywords[-1].startswith("#") else b"")
649+
+ after
650+
)
679651

680-
else:
681-
if operation == "add" and () in parsed:
682-
raise KeyError(())
652+
if add:
653+
if keywords in parsed and not keywords[-1].startswith("#"):
654+
raise KeyError(keywords)
655+
656+
parsed.add(keywords, data, content)
657+
else:
658+
parsed.put(keywords, data, content)
683659

684-
header = self.get("FoamFile", None)
685-
assert header is None or isinstance(header, FoamFile.SubDict)
660+
else:
661+
if add and () in parsed:
662+
raise KeyError(None)
686663

687-
content = before + dumps(data, keywords=(), header=header) + after
664+
content = before + dumps(data, keywords=(), header=header) + after
688665

689-
parsed.put((), normalize(data, keywords=keywords), content)
666+
parsed.put((), data, content)
690667

691668
@overload
692669
def __setitem__(
@@ -709,15 +686,25 @@ def __setitem__( # ty: ignore[invalid-method-override]
709686
keywords: str | tuple[str, ...] | None,
710687
data: DataLike | StandaloneDataLike | SubDictLike,
711688
) -> None:
712-
self._perform_entry_operation(keywords, data, "put")
689+
if keywords is None:
690+
keywords = ()
691+
elif not isinstance(keywords, tuple):
692+
keywords = (keywords,)
693+
694+
self._perform_entry_operation(keywords, data, add=False)
713695

714696
@override
715697
def add(
716698
self,
717699
keywords: str | tuple[str, ...] | None,
718700
data: DataLike | StandaloneDataLike | SubDictLike,
719701
) -> None:
720-
self._perform_entry_operation(keywords, data, "add")
702+
if keywords is None:
703+
keywords = ()
704+
elif not isinstance(keywords, tuple):
705+
keywords = (keywords,)
706+
707+
self._perform_entry_operation(keywords, data, add=True)
721708

722709
@with_default
723710
@override
@@ -969,11 +956,9 @@ def dumps(
969956
if header is None and ensure_header:
970957
class_ = "dictionary"
971958
if isinstance(file, Mapping) and "internalField" in file:
972-
try:
973-
tensor_kind = _tensor_kind_for_field(file["internalField"])
974-
except (ValueError, TypeError):
975-
pass
976-
else:
959+
internal_field = file["internalField"]
960+
if isinstance(internal_field, (float, int, np.ndarray)):
961+
tensor_kind = _tensor_kind_for_field(internal_field)
977962
class_ = "vol" + tensor_kind[0].upper() + tensor_kind[1:] + "Field"
978963

979964
header = {"version": 2.0, "format": "ascii", "class": class_}

0 commit comments

Comments
 (0)