Skip to content
This repository was archived by the owner on Dec 26, 2025. It is now read-only.

Commit 4f5c274

Browse files
committed
Accept missing comment line in ADI files (issue #3)
Detailed some type hints, fixed doc
1 parent 4739dba commit 4f5c274

File tree

3 files changed

+43
-18
lines changed

3 files changed

+43
-18
lines changed

src/adif_file/adi.py

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# PyADIF-File (c) 2023-2025 by Andreas Schawo is licensed under CC BY-SA 4.0.
2+
# To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/
3+
14
"""Convert ADIF ADI content to dictionary and vice versa"""
25

36
import re
@@ -32,7 +35,7 @@ class IllegalDataTypeException(Exception):
3235
REGEX_PARAM = re.compile(r'[a-zA-Z][a-zA-Z_0-9]*')
3336

3437

35-
def unpack(data: str) -> dict:
38+
def unpack(data: str) -> dict[str, str]:
3639
"""Unpack header or record part to dictionary
3740
The parameters are converted to uppercase"""
3841

@@ -77,7 +80,7 @@ def unpack(data: str) -> dict:
7780
return unpacked
7881

7982

80-
def loadi(adi: str, skip: int = 0) -> Iterator[dict]:
83+
def loadi(adi: str, skip: int = 0) -> Iterator[dict[str, str]]:
8184
"""Turn ADI formated string to header/records as an iterator over dict
8285
The skip option is useful if you want to watch a file for new records only. This saves processing time.
8386
@@ -86,30 +89,27 @@ def loadi(adi: str, skip: int = 0) -> Iterator[dict]:
8689
:return: an iterator of records (first record is the header even if not available)
8790
"""
8891

89-
record_data = adi
90-
if not adi.startswith('<'): # If a header is available
91-
hr_list = re.split(r'<[eE][oO][hH]>', adi)
92-
if len(hr_list) > 2:
93-
raise TooMuchHeadersException()
94-
92+
hr_list = re.split(r'<[eE][oO][hH]>', adi)
93+
if len(hr_list) == 1: # Header is missing
94+
yield {}
95+
record_data = hr_list[0]
96+
elif len(hr_list) > 2: # More than one header
97+
raise TooMuchHeadersException()
98+
else: # One header and the records
9599
yield unpack(hr_list[0])
96100
record_data = hr_list[1]
97-
else: # Empty record for missing header
98-
yield {}
99101

100-
i = 0
101-
for rec in re.finditer(r'(.*?)<[eE][oO][rR]>', record_data, re.S):
102+
for i, rec in enumerate(re.finditer(r'(.*?)<[eE][oO][rR]>', record_data, re.S)):
102103
if i >= skip:
103104
yield unpack(rec.groups()[0])
104-
i += 1
105105

106106

107107
def loads(adi: str, skip: int = 0) -> dict:
108108
"""Turn ADI formated string to dictionary
109109
The parameters are converted to uppercase
110110
111111
{
112-
'HEADER': None,
112+
'HEADER': {},
113113
'RECORDS': [list of records]
114114
}
115115
@@ -121,7 +121,7 @@ def loads(adi: str, skip: int = 0) -> dict:
121121
:return: the ADI as a dict
122122
"""
123123

124-
doc = {'HEADER': None,
124+
doc = {'HEADER': {},
125125
'RECORDS': []
126126
}
127127

@@ -141,7 +141,7 @@ def load(file_name: str, skip: int = 0, encoding=None) -> dict:
141141
The parameters are converted to uppercase
142142
143143
{
144-
'HEADER': None,
144+
'HEADER': {},
145145
'RECORDS': [list of records]
146146
}
147147
@@ -164,7 +164,7 @@ def pack(param: str, value: str, dtype: str = None) -> str:
164164
"""Generates ADI tag if value is not empty
165165
Does not generate tags for *_INTL types as required by specification.
166166
167-
:param param: the tag parameter (converte to uppercase)
167+
:param param: the tag parameter (converted to uppercase)
168168
:param value: the tag value (or tag definition if param is a USERDEF field)
169169
:param dtype: the optional datatype (mainly used for USERDEFx in header)
170170
:return: <param:length>value
@@ -297,7 +297,6 @@ def dump(file_name: str, data_dict: dict, comment: str = 'ADIF export by ' + __p
297297
first = False
298298
else:
299299
af.write('\n\n' if linebreaks else '\n')
300-
301300
af.write(chunk)
302301

303302

test/test_loadadi.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ def test_50_goodfile(self):
5454
self.assertEqual(3, len(adi_dict['HEADER']))
5555
self.assertEqual(5, len(adi_dict['RECORDS']))
5656

57+
def test_51_goodfile_missing_comment(self):
58+
adi_dict = adif_file.adi.load(get_file_path('testdata/goodfile_missing hcomment.txt'))
59+
60+
self.assertIn('HEADER', adi_dict)
61+
self.assertIn('RECORDS', adi_dict)
62+
self.assertEqual(3, len(adi_dict['HEADER']))
63+
self.assertEqual(3, len(adi_dict['RECORDS']))
64+
5765
def test_52_no_header(self):
5866
adi_dict = adif_file.adi.load(get_file_path('testdata/goodfile_no_h.txt'))
5967

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<ADIF_VER:5>3.1.4 <PROGRAMID:8>Testprog <PROGRAMVERSION:4>v0.2
2+
<eoh>
3+
4+
<QSO_DATE:8>20231008 <TIME_ON:4>1145 <CALL:6>dl4bdf <NAME:6>Walter <QTH:8>Dortmund <GRIDSQUARE:8>Jo30Uj45
5+
<BAND:3>80M <MODE:2>AM <FREQ:5>4.000
6+
<STATION_CALLSIGN:6>XX1XXX <MY_GRIDSQUARE:8>JO35uj27 <DISTANCE:1>2
7+
<NOTES:5>Test1
8+
<eor>
9+
10+
<QSO_DATE:8>20231008 <TIME_ON:4>1146 <CALL:6>DL5HJK <NAME:5>Peter <QTH:13>Welschneudorf <GRIDSQUARE:8>Jo30uj12
11+
<RST_SENT:2>59 <RST_RCVD:2>47 <BAND:4>630M <MODE:2>AM <TX_PWR:3>4.0
12+
<STATION_CALLSIGN:6>XX1XXX <MY_GRIDSQUARE:8>JO35uj27 <DISTANCE:1>2
13+
<eor>
14+
15+
<QSO_DATE:8>20231008 <TIME_ON:4>1340
16+
<RST_SENT:2>59 <RST_RCVD:2>59 <BAND:4>630M <MODE:2>AM <FREQ:5>0.472 <TX_PWR:3>4.0
17+
<MY_GRIDSQUARE:8>JO35uj27
18+
<eor>

0 commit comments

Comments
 (0)