Skip to content

Commit 5138437

Browse files
ZviBaratzclaude
andauthored
feat!: remove redundant 'index' field from parsed CSA tags (#15) (#36)
BREAKING CHANGE: The 'index' field has been removed from tag dictionaries returned by CsaHeader.read(). Tags now contain only 'VR', 'VM', and 'value'. The 'index' field was redundant with Python's guaranteed dict ordering (Python 3.7+). Since minimum supported version is Python 3.9, dict insertion order is language-guaranteed. Migration: Use enumerate(parsed.items(), 1) to get explicit indices. - Remove 'index' from parse_tag method - Add comprehensive docstring to read() method - Update tests to verify no 'index' field - Update README examples - Add migration guide to CHANGELOG - Bump version to 2.0.0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent c0f186d commit 5138437

File tree

6 files changed

+80
-33
lines changed

6 files changed

+80
-33
lines changed

CHANGELOG.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [2.0.0] - 2025-11-01
11+
12+
### 🔥 Breaking Changes
13+
14+
#### Removed redundant 'index' field from parsed CSA tags
15+
16+
**What changed:**
17+
- The `'index'` field has been removed from each tag dictionary returned by `CsaHeader.read()`
18+
- Tags now contain only: `'VR'` (Value Representation), `'VM'` (Value Multiplicity), and `'value'`
19+
20+
**Why:**
21+
- The `'index'` field was redundant with Python's guaranteed dict ordering (Python 3.7+)
22+
- Since the minimum supported Python version is 3.9, dict insertion order is guaranteed by the language
23+
- This simplifies the API and follows the DRY (Don't Repeat Yourself) principle
24+
- Resolves issue #15
25+
26+
**Migration guide:**
27+
28+
Before (v1.x):
29+
```python
30+
parsed = CsaHeader(raw_csa).read()
31+
for tag_name, tag_data in parsed.items():
32+
idx = tag_data['index'] # Direct access to index field
33+
print(f"Tag {idx}: {tag_name}")
34+
```
35+
36+
After (v2.0):
37+
```python
38+
parsed = CsaHeader(raw_csa).read()
39+
for idx, (tag_name, tag_data) in enumerate(parsed.items(), 1):
40+
print(f"Tag {idx}: {tag_name}")
41+
```
42+
43+
**Note:** Tag ordering is preserved via Python's dict insertion order (guaranteed since Python 3.7). Tags appear in the same sequential order as they do in the CSA header.
44+
45+
### Added
46+
- Comprehensive docstring for `CsaHeader.read()` method explaining dict ordering guarantees
47+
- Migration examples in CHANGELOG showing how to enumerate tags if explicit indices are needed
48+
49+
### Changed
50+
- Simplified tag structure from `{'index': int, 'VR': str, 'VM': int, 'value': Any}` to `{'VR': str, 'VM': int, 'value': Any}`
51+
- Updated README examples to reflect new tag structure without 'index' field
52+
- Updated test suite to verify tags no longer contain 'index' field
53+
1054
## [1.0.1] - 2025-10-28
1155

1256
### Fixed

README.md

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -146,20 +146,18 @@ Parse the contents of the CSA header with the `CsaHeader` class:
146146
>>> parsed_csa = CsaHeader(raw_csa).read()
147147
>>> parsed_csa
148148
{
149-
'NumberOfPrescans': {'index': 1, 'VR': 'IS', 'VM': 1, 'value': 0},
150-
'TransmitterCalibration': {'index': 2, 'VR': 'DS', 'VM': 1, 'value': 247.102},
151-
'PhaseGradientAmplitude': {'index': 3, 'VR': 'DS', 'VM': 1, 'value': 0.0},
152-
'ReadoutGradientAmplitude': {'index': 4, 'VR': 'DS', 'VM': 1, 'value': 0.0},
153-
'SelectionGradientAmplitude': {'index': 5, 'VR': 'DS', 'VM': 1, 'value': 0.0},
154-
'GradientDelayTime': {'index': 6,
155-
'VR': 'DS',
149+
'NumberOfPrescans': {'VR': 'IS', 'VM': 1, 'value': 0},
150+
'TransmitterCalibration': {'VR': 'DS', 'VM': 1, 'value': 247.102},
151+
'PhaseGradientAmplitude': {'VR': 'DS', 'VM': 1, 'value': 0.0},
152+
'ReadoutGradientAmplitude': {'VR': 'DS', 'VM': 1, 'value': 0.0},
153+
'SelectionGradientAmplitude': {'VR': 'DS', 'VM': 1, 'value': 0.0},
154+
'GradientDelayTime': {'VR': 'DS',
156155
'VM': 3,
157156
'value': [36.0, 35.0, 31.0]},
158-
'RfWatchdogMask': {'index': 7, 'VR': 'IS', 'VM': 1, 'value': 0},
159-
'RfPowerErrorIndicator': {'index': 8, 'VR': 'DS', 'VM': 1, 'value': None},
160-
'SarWholeBody': {'index': 9, 'VR': 'DS', 'VM': 3, 'value': None},
161-
'Sed': {'index': 10,
162-
'VR': 'DS',
157+
'RfWatchdogMask': {'VR': 'IS', 'VM': 1, 'value': 0},
158+
'RfPowerErrorIndicator': {'VR': 'DS', 'VM': 1, 'value': None},
159+
'SarWholeBody': {'VR': 'DS', 'VM': 3, 'value': None},
160+
'Sed': {'VR': 'DS',
163161
'VM': 3,
164162
'value': [1000000.0, 324.74800987, 324.74800832]}
165163
...

csa_header/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# SPDX-FileCopyrightText: 2023-present Zvi Baratz <z.baratz@gmail.com>
22
#
33
# SPDX-License-Identifier: MIT
4-
__version__ = "1.0.2"
4+
__version__ = "2.0.0"

csa_header/header.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,6 @@ def parse_tag(self, unpacker: Unpacker, i_tag: int) -> dict[str, Any]:
262262
vr = decode_latin1(vr_result)
263263
tag: dict[str, Any] = {
264264
"name": name,
265-
"index": i_tag,
266265
"VR": vr,
267266
"VM": vm,
268267
}
@@ -275,6 +274,31 @@ def parse_tag(self, unpacker: Unpacker, i_tag: int) -> dict[str, Any]:
275274
return tag
276275

277276
def read(self) -> dict[str, dict[str, Any]]:
277+
"""
278+
Parse the CSA header and return tag information as a dictionary.
279+
280+
Returns
281+
-------
282+
dict[str, dict[str, Any]]
283+
Dictionary mapping tag names to tag information. Keys are ordered
284+
by tag appearance in the CSA header (Python 3.7+ dict ordering
285+
guarantee). Each tag dictionary contains:
286+
287+
- 'VR' : str
288+
Value Representation (DICOM standard)
289+
- 'VM' : int
290+
Value Multiplicity (DICOM standard)
291+
- 'value' : Any
292+
Parsed tag value (type depends on VR)
293+
294+
Notes
295+
-----
296+
Tag ordering is preserved via Python's dict insertion order guarantee
297+
(Python 3.7+). To enumerate tags with explicit indices:
298+
299+
>>> for idx, (name, tag) in enumerate(parsed.items(), 1):
300+
... print(f"Tag {idx}: {name}")
301+
"""
278302
unpacker = Unpacker(self.raw, endian=self.ENDIAN)
279303
self.skip_prefix(unpacker)
280304
n_tags, _ = unpacker.unpack(self.PREFIX_FORMAT)

tests/test_documentation_examples.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,6 @@ def test_basic_csa_header_workflow(self):
184184
for tag_name, tag_data in parsed.items():
185185
self.assertIsInstance(tag_name, str)
186186
self.assertIsInstance(tag_data, dict)
187-
self.assertIn("index", tag_data)
188187
self.assertIn("VR", tag_data)
189188
self.assertIn("VM", tag_data)
190189
self.assertIn("value", tag_data)

tests/test_header.py

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -447,23 +447,6 @@ def test_parse_tag_extracts_vr_and_vm(self):
447447
self.assertEqual(tag["VR"], "DS")
448448
self.assertEqual(tag["VM"], 3)
449449

450-
def test_parse_tag_includes_index(self):
451-
"""Test that parse_tag includes the tag index."""
452-
tag_name = b"Tag" + b"\x00" * 61
453-
tag_data = (
454-
tag_name + struct.pack("<i", 1) + b"IS\x00\x00" + struct.pack("<3i", 0, 1, 205) # Use valid check bit 205
455-
)
456-
item_data = struct.pack("<4i", 3, 3, 0, 0) + b"99\x00"
457-
raw = b"SV10\x04\x03\x02\x01" + struct.pack("<2I", 1, 0) + tag_data + item_data
458-
459-
csa = CsaHeader(raw)
460-
unpacker = Unpacker(raw, endian="<", pointer=8)
461-
unpacker.unpack("2I")
462-
463-
tag = csa.parse_tag(unpacker, i_tag=42)
464-
465-
self.assertEqual(tag["index"], 42)
466-
467450
def test_parse_tag_validates_check_bit(self):
468451
"""Test that parse_tag validates check bit and raises on invalid value."""
469452
tag_name = b"BadTag" + b"\x00" * 58
@@ -545,7 +528,6 @@ def test_read_tag_structure(self):
545528
tag = result[first_tag_name]
546529

547530
# Tag should have these keys
548-
self.assertIn("index", tag)
549531
self.assertIn("VR", tag)
550532
self.assertIn("VM", tag)
551533
self.assertIn("value", tag)

0 commit comments

Comments
 (0)