foamlib 0.6.10__py3-none-any.whl → 0.6.12__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.
foamlib/_files/_files.py CHANGED
@@ -1,5 +1,8 @@
1
+ from __future__ import annotations
2
+
1
3
  import sys
2
- from typing import TYPE_CHECKING, Any, Optional, Tuple, Union, cast
4
+ from copy import deepcopy
5
+ from typing import Any, Optional, Tuple, Union, cast
3
6
 
4
7
  if sys.version_info >= (3, 8):
5
8
  from typing import Literal
@@ -16,15 +19,12 @@ from ._io import FoamFileIO
16
19
  from ._serialization import Kind, dumps
17
20
  from ._util import is_sequence
18
21
 
19
- if TYPE_CHECKING:
20
- import numpy as np
21
-
22
22
 
23
23
  class FoamFile(
24
24
  FoamFileBase,
25
25
  MutableMapping[
26
26
  Optional[Union[str, Tuple[str, ...]]],
27
- Union["FoamFile.Data", "FoamFile.SubDict"],
27
+ FoamFileBase._MutableData,
28
28
  ],
29
29
  FoamFileIO,
30
30
  ):
@@ -37,23 +37,23 @@ class FoamFile(
37
37
  """
38
38
 
39
39
  class SubDict(
40
- MutableMapping[str, Union["FoamFile.Data", "FoamFile.SubDict"]],
40
+ MutableMapping[str, FoamFileBase._MutableData],
41
41
  ):
42
42
  """An OpenFOAM dictionary within a file as a mutable mapping."""
43
43
 
44
- def __init__(self, _file: "FoamFile", _keywords: Tuple[str, ...]) -> None:
44
+ def __init__(self, _file: FoamFile, _keywords: tuple[str, ...]) -> None:
45
45
  self._file = _file
46
46
  self._keywords = _keywords
47
47
 
48
48
  def __getitem__(
49
49
  self, keyword: str
50
- ) -> Union["FoamFile.Data", "FoamFile.SubDict"]:
50
+ ) -> FoamFileBase._DataEntry | FoamFile.SubDict:
51
51
  return self._file[(*self._keywords, keyword)]
52
52
 
53
53
  def __setitem__(
54
54
  self,
55
55
  keyword: str,
56
- data: "FoamFile._SetData",
56
+ data: FoamFileBase.Data,
57
57
  ) -> None:
58
58
  self._file[(*self._keywords, keyword)] = data
59
59
 
@@ -98,8 +98,9 @@ class FoamFile(
98
98
  def version(self) -> float:
99
99
  """Alias of `self["FoamFile", "version"]`."""
100
100
  ret = self["FoamFile", "version"]
101
- if not isinstance(ret, float):
102
- raise TypeError("version is not a float")
101
+ if not isinstance(ret, (int, float)):
102
+ msg = "version is not a number"
103
+ raise TypeError(msg)
103
104
  return ret
104
105
 
105
106
  @version.setter
@@ -111,9 +112,11 @@ class FoamFile(
111
112
  """Alias of `self["FoamFile", "format"]`."""
112
113
  ret = self["FoamFile", "format"]
113
114
  if not isinstance(ret, str):
114
- raise TypeError("format is not a string")
115
+ msg = "format is not a string"
116
+ raise TypeError(msg)
115
117
  if ret not in ("ascii", "binary"):
116
- raise ValueError("format is not 'ascii' or 'binary'")
118
+ msg = "format is not 'ascii' or 'binary'"
119
+ raise ValueError(msg)
117
120
  return cast(Literal["ascii", "binary"], ret)
118
121
 
119
122
  @format.setter
@@ -125,7 +128,8 @@ class FoamFile(
125
128
  """Alias of `self["FoamFile", "class"]`."""
126
129
  ret = self["FoamFile", "class"]
127
130
  if not isinstance(ret, str):
128
- raise TypeError("class is not a string")
131
+ msg = "class is not a string"
132
+ raise TypeError(msg)
129
133
  return ret
130
134
 
131
135
  @class_.setter
@@ -137,7 +141,8 @@ class FoamFile(
137
141
  """Alias of `self["FoamFile", "location"]`."""
138
142
  ret = self["FoamFile", "location"]
139
143
  if not isinstance(ret, str):
140
- raise TypeError("location is not a string")
144
+ msg = "location is not a string"
145
+ raise TypeError(msg)
141
146
  return ret
142
147
 
143
148
  @location.setter
@@ -149,7 +154,8 @@ class FoamFile(
149
154
  """Alias of `self["FoamFile", "object"]`."""
150
155
  ret = self["FoamFile", "object"]
151
156
  if not isinstance(ret, str):
152
- raise TypeError("object is not a string")
157
+ msg = "object is not a string"
158
+ raise TypeError(msg)
153
159
  return ret
154
160
 
155
161
  @object_.setter
@@ -157,30 +163,32 @@ class FoamFile(
157
163
  self["FoamFile", "object"] = value
158
164
 
159
165
  def __getitem__(
160
- self, keywords: Optional[Union[str, Tuple[str, ...]]]
161
- ) -> Union["FoamFile.Data", "FoamFile.SubDict"]:
166
+ self, keywords: str | tuple[str, ...] | None
167
+ ) -> FoamFileBase._DataEntry | FoamFile.SubDict:
162
168
  if not keywords:
163
169
  keywords = ()
164
170
  elif not isinstance(keywords, tuple):
165
171
  keywords = (keywords,)
166
172
 
167
- _, parsed = self._read()
173
+ parsed = self._get_parsed()
168
174
 
169
175
  value = parsed[keywords]
170
176
 
177
+ assert not isinstance(value, Mapping)
178
+
171
179
  if value is ...:
172
180
  return FoamFile.SubDict(self, keywords)
173
- return value
181
+ return deepcopy(value)
174
182
 
175
183
  def __setitem__(
176
- self, keywords: Optional[Union[str, Tuple[str, ...]]], data: "FoamFile._SetData"
184
+ self, keywords: str | tuple[str, ...] | None, data: FoamFileBase.Data
177
185
  ) -> None:
178
- with self:
179
- if not keywords:
180
- keywords = ()
181
- elif not isinstance(keywords, tuple):
182
- keywords = (keywords,)
186
+ if not keywords:
187
+ keywords = ()
188
+ elif not isinstance(keywords, tuple):
189
+ keywords = (keywords,)
183
190
 
191
+ with self:
184
192
  try:
185
193
  write_header = (
186
194
  not self and "FoamFile" not in self and keywords != ("FoamFile",)
@@ -213,91 +221,104 @@ class FoamFile(
213
221
  kind = Kind.DIMENSIONS
214
222
 
215
223
  if (
216
- kind == Kind.FIELD or kind == Kind.BINARY_FIELD
224
+ kind in (Kind.FIELD, Kind.BINARY_FIELD)
217
225
  ) and self.class_ == "dictionary":
218
- if not is_sequence(data):
219
- class_ = "volScalarField"
220
- elif (len(data) == 3 and not is_sequence(data[0])) or len(data[0]) == 3:
221
- class_ = "volVectorField"
222
- elif (len(data) == 6 and not is_sequence(data[0])) or len(data[0]) == 6:
223
- class_ = "volSymmTensorField"
224
- elif (len(data) == 9 and not is_sequence(data[0])) or len(data[0]) == 9:
225
- class_ = "volTensorField"
226
+ if isinstance(data, (int, float)):
227
+ self.class_ = "volScalarField"
228
+
229
+ elif is_sequence(data) and data:
230
+ if isinstance(data[0], (int, float)):
231
+ if len(data) == 3:
232
+ self.class_ = "volVectorField"
233
+ elif len(data) == 6:
234
+ self.class_ = "volSymmTensorField"
235
+ elif len(data) == 9:
236
+ self.class_ = "volTensorField"
237
+ elif (
238
+ is_sequence(data[0])
239
+ and data[0]
240
+ and isinstance(data[0][0], (int, float))
241
+ ):
242
+ if len(data[0]) == 3:
243
+ self.class_ = "volVectorField"
244
+ elif len(data[0]) == 6:
245
+ self.class_ = "volSymmTensorField"
246
+ elif len(data[0]) == 9:
247
+ self.class_ = "volTensorField"
248
+
249
+ parsed = self._get_parsed(missing_ok=True)
250
+
251
+ start, end = parsed.entry_location(keywords, missing_ok=True)
252
+
253
+ if start and not parsed.contents[:start].endswith(b"\n\n"):
254
+ if parsed.contents[:start].endswith(b"\n"):
255
+ before = b"\n" if len(keywords) <= 1 else b""
226
256
  else:
227
- class_ = "volScalarField"
228
-
229
- self.class_ = class_
230
- self[keywords] = data
257
+ before = b"\n\n" if len(keywords) <= 1 else b"\n"
258
+ else:
259
+ before = b""
231
260
 
261
+ if not parsed.contents[end:].strip() or parsed.contents[end:].startswith(
262
+ b"}"
263
+ ):
264
+ after = b"\n" + b" " * (len(keywords) - 2)
232
265
  else:
233
- contents, parsed = self._read(missing_ok=True)
234
-
235
- start, end = parsed.entry_location(keywords, missing_ok=True)
236
-
237
- before = contents[:start].rstrip() + b"\n"
238
- if len(keywords) <= 1:
239
- before += b"\n"
240
-
241
- after = contents[end:]
242
- if after.startswith(b"}"):
243
- after = b" " * (len(keywords) - 2) + after
244
- if not after or after[:1] != b"\n":
245
- after = b"\n" + after
246
- if len(keywords) <= 1 and len(after) > 1 and after[:2] != b"\n\n":
247
- after = b"\n" + after
248
-
249
- indentation = b" " * (len(keywords) - 1)
250
-
251
- if isinstance(data, Mapping):
252
- if isinstance(data, (FoamFile, FoamFile.SubDict)):
253
- data = data.as_dict()
254
-
255
- self._write(
256
- before
257
- + indentation
258
- + dumps(keywords[-1])
259
- + b"\n"
260
- + indentation
261
- + b"{\n"
262
- + indentation
263
- + b"}"
264
- + after
265
- )
266
-
267
- for k, v in data.items():
268
- self[(*keywords, k)] = v
269
-
270
- elif keywords:
271
- self._write(
272
- before
273
- + indentation
274
- + dumps(keywords[-1])
275
- + b" "
276
- + dumps(data, kind=kind)
277
- + b";"
278
- + after
279
- )
266
+ after = b""
267
+
268
+ indentation = b" " * (len(keywords) - 1)
269
+
270
+ if isinstance(data, Mapping):
271
+ if isinstance(data, (FoamFile, FoamFile.SubDict)):
272
+ data = data.as_dict()
273
+
274
+ parsed.put(
275
+ keywords,
276
+ ...,
277
+ before
278
+ + indentation
279
+ + dumps(keywords[-1])
280
+ + b"\n"
281
+ + indentation
282
+ + b"{\n"
283
+ + indentation
284
+ + b"}"
285
+ + after,
286
+ )
280
287
 
281
- else:
282
- self._write(before + dumps(data, kind=kind) + after)
288
+ for k, v in data.items():
289
+ self[(*keywords, k)] = v
290
+
291
+ elif keywords:
292
+ parsed.put(
293
+ keywords,
294
+ deepcopy(data),
295
+ before
296
+ + indentation
297
+ + dumps(keywords[-1])
298
+ + b" "
299
+ + dumps(data, kind=kind)
300
+ + b";"
301
+ + after,
302
+ )
303
+
304
+ else:
305
+ parsed.put((), deepcopy(data), before + dumps(data, kind=kind) + after)
283
306
 
284
- def __delitem__(self, keywords: Optional[Union[str, Tuple[str, ...]]]) -> None:
307
+ def __delitem__(self, keywords: str | tuple[str, ...] | None) -> None:
285
308
  if not keywords:
286
309
  keywords = ()
287
310
  elif not isinstance(keywords, tuple):
288
311
  keywords = (keywords,)
289
312
 
290
- contents, parsed = self._read()
291
-
292
- start, end = parsed.entry_location(keywords)
293
-
294
- self._write(contents[:start] + contents[end:])
313
+ with self:
314
+ del self._get_parsed()[keywords]
295
315
 
296
- def _iter(self, keywords: Tuple[str, ...] = ()) -> Iterator[Optional[str]]:
297
- _, parsed = self._read()
298
- yield from (k[-1] if k else None for k in parsed if k[:-1] == keywords)
316
+ def _iter(self, keywords: tuple[str, ...] = ()) -> Iterator[str | None]:
317
+ yield from (
318
+ k[-1] if k else None for k in self._get_parsed() if k[:-1] == keywords
319
+ )
299
320
 
300
- def __iter__(self) -> Iterator[Optional[str]]:
321
+ def __iter__(self) -> Iterator[str | None]:
301
322
  yield from (k for k in self._iter() if k != "FoamFile")
302
323
 
303
324
  def __contains__(self, keywords: object) -> bool:
@@ -306,9 +327,7 @@ class FoamFile(
306
327
  elif not isinstance(keywords, tuple):
307
328
  keywords = (keywords,)
308
329
 
309
- _, parsed = self._read()
310
-
311
- return keywords in parsed
330
+ return keywords in self._get_parsed()
312
331
 
313
332
  def __len__(self) -> int:
314
333
  return len(list(iter(self)))
@@ -330,22 +349,22 @@ class FoamFile(
330
349
 
331
350
  :param include_header: Whether to include the "FoamFile" header in the output.
332
351
  """
333
- _, parsed = self._read()
334
- d = parsed.as_dict()
352
+ d = self._get_parsed().as_dict()
335
353
  if not include_header:
336
354
  d.pop("FoamFile", None)
337
- return d
355
+ return deepcopy(d)
338
356
 
339
357
 
340
358
  class FoamFieldFile(FoamFile):
341
359
  """An OpenFOAM dictionary file representing a field as a mutable mapping."""
342
360
 
343
361
  class BoundariesSubDict(FoamFile.SubDict):
344
- def __getitem__(self, keyword: str) -> "FoamFieldFile.BoundarySubDict":
362
+ def __getitem__(self, keyword: str) -> FoamFieldFile.BoundarySubDict:
345
363
  value = super().__getitem__(keyword)
346
364
  if not isinstance(value, FoamFieldFile.BoundarySubDict):
347
365
  assert not isinstance(value, FoamFile.SubDict)
348
- raise TypeError(f"boundary {keyword} is not a dictionary")
366
+ msg = f"boundary {keyword} is not a dictionary"
367
+ raise TypeError(msg)
349
368
  return value
350
369
 
351
370
  class BoundarySubDict(FoamFile.SubDict):
@@ -356,7 +375,8 @@ class FoamFieldFile(FoamFile):
356
375
  """Alias of `self["type"]`."""
357
376
  ret = self["type"]
358
377
  if not isinstance(ret, str):
359
- raise TypeError("type is not a string")
378
+ msg = "type is not a string"
379
+ raise TypeError(msg)
360
380
  return ret
361
381
 
362
382
  @type.setter
@@ -366,36 +386,17 @@ class FoamFieldFile(FoamFile):
366
386
  @property
367
387
  def value(
368
388
  self,
369
- ) -> Union[
370
- int,
371
- float,
372
- Sequence[Union[int, float, Sequence[Union[int, float]]]],
373
- "np.ndarray[Tuple[()], np.dtype[np.generic]]",
374
- "np.ndarray[Tuple[int], np.dtype[np.generic]]",
375
- "np.ndarray[Tuple[int, int], np.dtype[np.generic]]",
376
- ]:
389
+ ) -> FoamFile._Field:
377
390
  """Alias of `self["value"]`."""
378
- ret = self["value"]
379
- if not isinstance(ret, (int, float, Sequence)):
380
- raise TypeError("value is not a field")
381
391
  return cast(
382
- Union[
383
- int, float, Sequence[Union[int, float, Sequence[Union[int, float]]]]
384
- ],
385
- ret,
392
+ FoamFile._Field,
393
+ self["value"],
386
394
  )
387
395
 
388
396
  @value.setter
389
397
  def value(
390
398
  self,
391
- value: Union[
392
- int,
393
- float,
394
- Sequence[Union[int, float, Sequence[Union[int, float]]]],
395
- "np.ndarray[Tuple[()], np.dtype[np.generic]]",
396
- "np.ndarray[Tuple[int], np.dtype[np.generic]]",
397
- "np.ndarray[Tuple[int, int], np.dtype[np.generic]]",
398
- ],
399
+ value: FoamFile._Field,
399
400
  ) -> None:
400
401
  self["value"] = value
401
402
 
@@ -404,8 +405,8 @@ class FoamFieldFile(FoamFile):
404
405
  del self["value"]
405
406
 
406
407
  def __getitem__(
407
- self, keywords: Optional[Union[str, Tuple[str, ...]]]
408
- ) -> Union[FoamFile.Data, FoamFile.SubDict]:
408
+ self, keywords: str | tuple[str, ...] | None
409
+ ) -> FoamFileBase._DataEntry | FoamFile.SubDict:
409
410
  if not keywords:
410
411
  keywords = ()
411
412
  elif not isinstance(keywords, tuple):
@@ -420,57 +421,40 @@ class FoamFieldFile(FoamFile):
420
421
  return ret
421
422
 
422
423
  @property
423
- def dimensions(self) -> Union[FoamFile.DimensionSet, Sequence[Union[int, float]]]:
424
+ def dimensions(self) -> FoamFile.DimensionSet | Sequence[float]:
424
425
  """Alias of `self["dimensions"]`."""
425
426
  ret = self["dimensions"]
426
427
  if not isinstance(ret, FoamFile.DimensionSet):
427
- raise TypeError("dimensions is not a DimensionSet")
428
+ msg = "dimensions is not a DimensionSet"
429
+ raise TypeError(msg)
428
430
  return ret
429
431
 
430
432
  @dimensions.setter
431
- def dimensions(
432
- self, value: Union[FoamFile.DimensionSet, Sequence[Union[int, float]]]
433
- ) -> None:
433
+ def dimensions(self, value: FoamFile.DimensionSet | Sequence[float]) -> None:
434
434
  self["dimensions"] = value
435
435
 
436
436
  @property
437
437
  def internal_field(
438
438
  self,
439
- ) -> Union[
440
- int,
441
- float,
442
- Sequence[Union[int, float, Sequence[Union[int, float]]]],
443
- "np.ndarray[Tuple[()], np.dtype[np.generic]]",
444
- "np.ndarray[Tuple[int], np.dtype[np.generic]]",
445
- "np.ndarray[Tuple[int, int], np.dtype[np.generic]]",
446
- ]:
439
+ ) -> FoamFile._Field:
447
440
  """Alias of `self["internalField"]`."""
448
- ret = self["internalField"]
449
- if not isinstance(ret, (int, float, Sequence)):
450
- raise TypeError("internalField is not a field")
451
- return cast(Union[int, float, Sequence[Union[int, float]]], ret)
441
+ return cast(FoamFile._Field, self["internalField"])
452
442
 
453
443
  @internal_field.setter
454
444
  def internal_field(
455
445
  self,
456
- value: Union[
457
- int,
458
- float,
459
- Sequence[Union[int, float, Sequence[Union[int, float]]]],
460
- "np.ndarray[Tuple[()], np.dtype[np.generic]]",
461
- "np.ndarray[Tuple[int], np.dtype[np.generic]]",
462
- "np.ndarray[Tuple[int, int], np.dtype[np.generic]]",
463
- ],
446
+ value: FoamFile._Field,
464
447
  ) -> None:
465
448
  self["internalField"] = value
466
449
 
467
450
  @property
468
- def boundary_field(self) -> "FoamFieldFile.BoundariesSubDict":
451
+ def boundary_field(self) -> FoamFieldFile.BoundariesSubDict:
469
452
  """Alias of `self["boundaryField"]`."""
470
453
  ret = self["boundaryField"]
471
454
  if not isinstance(ret, FoamFieldFile.BoundariesSubDict):
472
455
  assert not isinstance(ret, FoamFile.SubDict)
473
- raise TypeError("boundaryField is not a dictionary")
456
+ msg = "boundaryField is not a dictionary"
457
+ raise TypeError(msg)
474
458
  return ret
475
459
 
476
460
  @boundary_field.setter
foamlib/_files/_io.py CHANGED
@@ -1,14 +1,10 @@
1
+ from __future__ import annotations
2
+
1
3
  import gzip
2
4
  import sys
3
- from copy import deepcopy
4
5
  from pathlib import Path
5
- from types import TracebackType
6
6
  from typing import (
7
7
  TYPE_CHECKING,
8
- Optional,
9
- Tuple,
10
- Type,
11
- Union,
12
8
  )
13
9
 
14
10
  if sys.version_info >= (3, 11):
@@ -20,71 +16,64 @@ from ._parsing import Parsed
20
16
 
21
17
  if TYPE_CHECKING:
22
18
  import os
19
+ from types import TracebackType
23
20
 
24
21
 
25
22
  class FoamFileIO:
26
- def __init__(self, path: Union["os.PathLike[str]", str]) -> None:
23
+ def __init__(self, path: os.PathLike[str] | str) -> None:
27
24
  self.path = Path(path).absolute()
28
25
 
29
- self.__contents: Optional[bytes] = None
30
- self.__parsed: Optional[Parsed] = None
26
+ self.__parsed: Parsed | None = None
27
+ self.__missing: bool | None = None
31
28
  self.__defer_io = 0
32
- self.__dirty = False
33
29
 
34
30
  def __enter__(self) -> Self:
35
31
  if self.__defer_io == 0:
36
- self._read(missing_ok=True)
32
+ self._get_parsed(missing_ok=True)
37
33
  self.__defer_io += 1
38
34
  return self
39
35
 
40
36
  def __exit__(
41
37
  self,
42
- exc_type: Optional[Type[BaseException]],
43
- exc_val: Optional[BaseException],
44
- exc_tb: Optional[TracebackType],
38
+ exc_type: type[BaseException] | None,
39
+ exc_val: BaseException | None,
40
+ exc_tb: TracebackType | None,
45
41
  ) -> None:
46
42
  self.__defer_io -= 1
47
- if self.__defer_io == 0 and self.__dirty:
48
- assert self.__contents is not None
49
- self._write(self.__contents)
43
+ if self.__defer_io == 0:
44
+ assert self.__parsed is not None
45
+ if self.__parsed.modified:
46
+ contents = self.__parsed.contents
47
+
48
+ if self.path.suffix == ".gz":
49
+ contents = gzip.compress(contents)
50
+
51
+ self.path.write_bytes(contents)
52
+ self.__parsed.modified = False
53
+ self.__missing = False
50
54
 
51
- def _read(self, *, missing_ok: bool = False) -> Tuple[bytes, Parsed]:
55
+ def _get_parsed(self, *, missing_ok: bool = False) -> Parsed:
52
56
  if not self.__defer_io:
53
57
  try:
54
58
  contents = self.path.read_bytes()
55
59
  except FileNotFoundError:
56
- contents = None
60
+ self.__missing = True
61
+ contents = b""
57
62
  else:
58
- assert isinstance(contents, bytes)
63
+ self.__missing = False
59
64
  if self.path.suffix == ".gz":
60
65
  contents = gzip.decompress(contents)
61
66
 
62
- if contents != self.__contents:
63
- self.__contents = contents
64
- self.__parsed = None
67
+ if self.__parsed is None or self.__parsed.contents != contents:
68
+ self.__parsed = Parsed(contents)
65
69
 
66
- if self.__contents is None:
67
- if missing_ok:
68
- return b"", Parsed(b"")
69
- raise FileNotFoundError(self.path)
70
-
71
- if self.__parsed is None:
72
- parsed = Parsed(self.__contents)
73
- self.__parsed = parsed
74
-
75
- return self.__contents, deepcopy(self.__parsed)
70
+ assert self.__parsed is not None
71
+ assert self.__missing is not None
76
72
 
77
- def _write(self, contents: bytes) -> None:
78
- self.__contents = contents
79
- self.__parsed = None
80
- if not self.__defer_io:
81
- if self.path.suffix == ".gz":
82
- contents = gzip.compress(contents)
73
+ if self.__missing and not self.__parsed.modified and not missing_ok:
74
+ raise FileNotFoundError(self.path)
83
75
 
84
- self.path.write_bytes(contents)
85
- self.__dirty = False
86
- else:
87
- self.__dirty = True
76
+ return self.__parsed
88
77
 
89
78
  def __repr__(self) -> str:
90
79
  return f"{type(self).__qualname__}('{self.path}')"