stdfast 0.1.2__tar.gz → 0.2.0__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,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+
4
+ ## 0.2.0
5
+
6
+ - New append mode for StdfWriter
7
+
3
8
  ## 0.1.2
4
9
 
5
10
  - Bump version for new build files
@@ -1922,7 +1922,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
1922
1922
 
1923
1923
  [[package]]
1924
1924
  name = "stdfast"
1925
- version = "0.1.2"
1925
+ version = "0.2.0"
1926
1926
  dependencies = [
1927
1927
  "clap",
1928
1928
  "itertools",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "stdfast"
3
- version = "0.1.2"
3
+ version = "0.2.0"
4
4
  edition = "2024"
5
5
  description = "Parsing of STDF file format to DataFrame with Python bindings"
6
6
  license = "BSD-2"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stdfast
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Requires-Dist: polars
5
5
  Requires-Dist: pydantic>=2.12.5
6
6
  License-File: LICENSE
@@ -107,7 +107,8 @@ sf.write_stdf("output.stdf", records)
107
107
  import stdfast as sf
108
108
  from stdfast.records import FAR, MIR, MRR, PIR, PTR, PRR
109
109
 
110
- with sf.StdfWriter("output.stdf") as w:
110
+ # Creates new file or overwrites content if it exists
111
+ with sf.StdfWriter("output.stdf", append=False) as w:
111
112
  w.write_record(FAR(cpu_type=2, stdf_ver=4))
112
113
  w.write_record(MIR(lot_id="LOT001", part_typ="MYPART", job_nam="MYJOB"))
113
114
  for i, result in enumerate(my_results):
@@ -115,6 +116,11 @@ with sf.StdfWriter("output.stdf") as w:
115
116
  w.write_record(PTR(test_num=1000 + i, head_num=1, site_num=1, result=result, test_txt=f"test_{i}"))
116
117
  w.write_record(PRR(head_num=1, site_num=1, hard_bin=1, soft_bin=1, num_test=1))
117
118
  w.write_record(MRR())
119
+
120
+ # Creates new file or appends to content if it exists
121
+ with sf.StdfWriter("output.stdf", append=True) as w:
122
+ ...
123
+
118
124
  ```
119
125
 
120
126
  `StdfWriter` is preferable when the number of records is large or unknown upfront, since it never holds more than one record in memory at a time.
@@ -92,7 +92,8 @@ sf.write_stdf("output.stdf", records)
92
92
  import stdfast as sf
93
93
  from stdfast.records import FAR, MIR, MRR, PIR, PTR, PRR
94
94
 
95
- with sf.StdfWriter("output.stdf") as w:
95
+ # Creates new file or overwrites content if it exists
96
+ with sf.StdfWriter("output.stdf", append=False) as w:
96
97
  w.write_record(FAR(cpu_type=2, stdf_ver=4))
97
98
  w.write_record(MIR(lot_id="LOT001", part_typ="MYPART", job_nam="MYJOB"))
98
99
  for i, result in enumerate(my_results):
@@ -100,6 +101,11 @@ with sf.StdfWriter("output.stdf") as w:
100
101
  w.write_record(PTR(test_num=1000 + i, head_num=1, site_num=1, result=result, test_txt=f"test_{i}"))
101
102
  w.write_record(PRR(head_num=1, site_num=1, hard_bin=1, soft_bin=1, num_test=1))
102
103
  w.write_record(MRR())
104
+
105
+ # Creates new file or appends to content if it exists
106
+ with sf.StdfWriter("output.stdf", append=True) as w:
107
+ ...
108
+
103
109
  ```
104
110
 
105
111
  `StdfWriter` is preferable when the number of records is large or unknown upfront, since it never holds more than one record in memory at a time.
@@ -20,6 +20,7 @@
20
20
  //! ```
21
21
 
22
22
  use std::fs::File;
23
+ use std::fs::OpenOptions;
23
24
  use std::io::{BufWriter, Write};
24
25
  use std::sync::Mutex;
25
26
 
@@ -576,8 +577,9 @@ pub struct StdfWriter {
576
577
  #[pymethods]
577
578
  impl StdfWriter {
578
579
  #[new]
579
- pub fn open(fname: &str) -> PyResult<Self> {
580
- let file = File::create(fname)
580
+ #[pyo3(signature = (fname, append = false))]
581
+ pub fn open(fname: &str, append: bool) -> PyResult<Self> {
582
+ let file = OpenOptions::new().write(true).create(true).truncate(!append).append(append).open(fname)
581
583
  .map_err(|e| pyo3::exceptions::PyIOError::new_err(e.to_string()))?;
582
584
  Ok(Self {
583
585
  inner: Mutex::new(Some(BufWriter::new(file))),
@@ -114,12 +114,19 @@ class StdfWriter:
114
114
 
115
115
  Can be used as a context manager::
116
116
 
117
- with sf.StdfWriter("out.stdf") as w:
117
+ # Create file or overwrites content if it exists
118
+ with sf.StdfWriter("out.stdf", append=False) as w:
118
119
  w.write_record(FAR(cpu_type=2, stdf_ver=4))
119
120
  w.write_record(MRR())
121
+
122
+ # Create file or appends to content if it exists
123
+ with sf.StdfWriter("out.stdf", append=True) as w:
124
+ w.write_record(FAR(cpu_type=2, stdf_ver=4))
125
+ w.write_record(MRR())
126
+
120
127
  """
121
128
 
122
- def __init__(self, fname: str) -> None: ...
129
+ def __init__(self, fname: str, append: bool = False) -> None: ...
123
130
  def write_record(self, record) -> None: ...
124
131
  def close(self) -> None: ...
125
132
  def __enter__(self) -> "StdfWriter": ...
@@ -3,6 +3,7 @@ Roundtrip tests: Python records → write_stdf → binary file → parse_stdf
3
3
  """
4
4
 
5
5
  import math
6
+ import os
6
7
 
7
8
  import pytest
8
9
 
@@ -248,6 +249,217 @@ class TestStdfWriterRoundtrip:
248
249
  assert row["units"][0] == "mA"
249
250
 
250
251
 
252
+ # ---------------------------------------------------------------------------
253
+ # StdfWriter append mode
254
+ # ---------------------------------------------------------------------------
255
+
256
+
257
+ def _make_first_part():
258
+ """FAR + MIR + TSR + first PIR/PTR/PRR block (no MRR yet).
259
+
260
+ The TSR must precede PIR so that test_num 1000 is registered before the
261
+ second parse pass processes PTRs.
262
+ """
263
+ return [
264
+ FAR(cpu_type=2, stdf_ver=4),
265
+ MIR(lot_id="LOT_APPEND", part_typ="APPEND_PART", job_nam="APPEND_JOB"),
266
+ TSR(
267
+ test_num=1000,
268
+ head_num=1,
269
+ site_num=1,
270
+ test_typ="P",
271
+ test_nam="vdd",
272
+ exec_cnt=2,
273
+ ),
274
+ PIR(head_num=1, site_num=1),
275
+ PTR(
276
+ test_num=1000,
277
+ head_num=1,
278
+ site_num=1,
279
+ result=1.0,
280
+ test_txt="vdd",
281
+ lo_limit=0.9,
282
+ hi_limit=1.2,
283
+ units="V",
284
+ ),
285
+ PRR(
286
+ head_num=1,
287
+ site_num=1,
288
+ hard_bin=1,
289
+ soft_bin=1,
290
+ num_test=1,
291
+ part_id="PART_001",
292
+ ),
293
+ ]
294
+
295
+
296
+ def _make_second_part():
297
+ """Second PIR/PTR/PRR block + MRR to close the file."""
298
+ return [
299
+ PIR(head_num=1, site_num=1),
300
+ PTR(
301
+ test_num=1000,
302
+ head_num=1,
303
+ site_num=1,
304
+ result=1.1,
305
+ test_txt="vdd",
306
+ lo_limit=0.9,
307
+ hi_limit=1.2,
308
+ units="V",
309
+ ),
310
+ PRR(
311
+ head_num=1,
312
+ site_num=1,
313
+ hard_bin=1,
314
+ soft_bin=1,
315
+ num_test=1,
316
+ part_id="PART_002",
317
+ ),
318
+ MRR(),
319
+ ]
320
+
321
+
322
+ class TestStdfWriterAppend:
323
+ """Tests for StdfWriter append=True mode."""
324
+
325
+ def test_append_extends_raw_record_count(self, tmp_path):
326
+ """Appending records to an existing file increases the total record count."""
327
+ path = str(tmp_path / "append_count.stdf")
328
+ first = _make_first_part()
329
+ second = _make_second_part()
330
+
331
+ with sf.StdfWriter(path) as w:
332
+ for r in first:
333
+ w.write_record(r)
334
+
335
+ with sf.StdfWriter(path, append=True) as w:
336
+ for r in second:
337
+ w.write_record(r)
338
+
339
+ raw = sf.get_raw_records(path)
340
+ assert len(raw) == len(first) + len(second)
341
+
342
+ def test_append_record_order(self, tmp_path):
343
+ """Records appear in the file in exactly the order they were written."""
344
+ path = str(tmp_path / "append_order.stdf")
345
+ first = _make_first_part() # FAR, MIR, PIR, PTR, PRR
346
+ second = _make_second_part() # PIR, PTR, PRR, MRR
347
+
348
+ with sf.StdfWriter(path) as w:
349
+ for r in first:
350
+ w.write_record(r)
351
+
352
+ with sf.StdfWriter(path, append=True) as w:
353
+ for r in second:
354
+ w.write_record(r)
355
+
356
+ types = [r["record_type"] for r in sf.get_raw_records(path)]
357
+ assert types == ["FAR", "MIR", "PIR", "PTR", "PRR", "PIR", "PTR", "PRR", "MRR"]
358
+
359
+ def test_append_parses_both_parts(self, tmp_path):
360
+ """parse_stdf on a split-written STDF yields both parts as data rows."""
361
+ path = str(tmp_path / "append_parse.stdf")
362
+
363
+ with sf.StdfWriter(path) as w:
364
+ for r in _make_first_part():
365
+ w.write_record(r)
366
+
367
+ with sf.StdfWriter(path, append=True) as w:
368
+ for r in _make_second_part():
369
+ w.write_record(r)
370
+
371
+ result = sf.parse_stdf(path)
372
+ assert len(result["data"]) == 2
373
+
374
+ def test_append_part_ids(self, tmp_path):
375
+ """Both part IDs survive the split-write roundtrip."""
376
+ path = str(tmp_path / "append_part_ids.stdf")
377
+
378
+ with sf.StdfWriter(path) as w:
379
+ for r in _make_first_part():
380
+ w.write_record(r)
381
+
382
+ with sf.StdfWriter(path, append=True) as w:
383
+ for r in _make_second_part():
384
+ w.write_record(r)
385
+
386
+ part_ids = sf.parse_stdf(path)["data"]["part_id"].to_list()
387
+ assert "PART_001" in part_ids
388
+ assert "PART_002" in part_ids
389
+
390
+ def test_no_append_truncates_existing_file(self, tmp_path):
391
+ """Opening StdfWriter without append=True discards the original content."""
392
+ path = str(tmp_path / "no_append.stdf")
393
+
394
+ with sf.StdfWriter(path) as w:
395
+ for r in _make_first_part() + _make_second_part():
396
+ w.write_record(r)
397
+
398
+ # Overwrite with just a single FAR record
399
+ with sf.StdfWriter(path) as w:
400
+ w.write_record(FAR(cpu_type=2, stdf_ver=4))
401
+
402
+ raw = sf.get_raw_records(path)
403
+ assert len(raw) == 1
404
+ assert raw[0]["record_type"] == "FAR"
405
+
406
+ def test_append_preserves_original_bytes(self, tmp_path):
407
+ """Bytes written in the first session are not modified by an append."""
408
+ path = str(tmp_path / "bytes_preserved.stdf")
409
+
410
+ with sf.StdfWriter(path) as w:
411
+ for r in _make_first_part():
412
+ w.write_record(r)
413
+
414
+ original_bytes = open(path, "rb").read()
415
+
416
+ with sf.StdfWriter(path, append=True) as w:
417
+ for r in _make_second_part():
418
+ w.write_record(r)
419
+
420
+ all_bytes = open(path, "rb").read()
421
+ assert all_bytes[: len(original_bytes)] == original_bytes
422
+
423
+ def test_append_bytes_equal_manual_concat(self, tmp_path):
424
+ """Content of the appended file equals the manual concatenation of both parts."""
425
+ file_a = str(tmp_path / "part_a.stdf")
426
+ file_b = str(tmp_path / "part_b.stdf")
427
+ file_ab = str(tmp_path / "appended.stdf")
428
+
429
+ with sf.StdfWriter(file_a) as w:
430
+ for r in _make_first_part():
431
+ w.write_record(r)
432
+
433
+ with sf.StdfWriter(file_b) as w:
434
+ for r in _make_second_part():
435
+ w.write_record(r)
436
+
437
+ with sf.StdfWriter(file_ab) as w:
438
+ for r in _make_first_part():
439
+ w.write_record(r)
440
+
441
+ with sf.StdfWriter(file_ab, append=True) as w:
442
+ for r in _make_second_part():
443
+ w.write_record(r)
444
+
445
+ concat = open(file_a, "rb").read() + open(file_b, "rb").read()
446
+ appended = open(file_ab, "rb").read()
447
+ assert appended == concat
448
+
449
+ def test_append_creates_new_file(self, tmp_path):
450
+ """append=True on a nonexistent path creates the file."""
451
+ path = str(tmp_path / "new_via_append.stdf")
452
+ assert not os.path.exists(path)
453
+
454
+ records = _make_first_part() + _make_second_part()
455
+ with sf.StdfWriter(path, append=True) as w:
456
+ for r in records:
457
+ w.write_record(r)
458
+
459
+ assert os.path.exists(path)
460
+ assert len(sf.get_raw_records(path)) == len(records)
461
+
462
+
251
463
  class TestStdfWriterBytesMatchBatch:
252
464
  """StdfWriter must produce bit-for-bit identical output to write_stdf."""
253
465
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes