Skip to content

Commit ee2a293

Browse files
committed
ENH: Add parser for Siemens "ASCCONV" text format
1 parent 65d5fc6 commit ee2a293

File tree

3 files changed

+1189
-0
lines changed

3 files changed

+1189
-0
lines changed

nibabel/nicom/ascconv.py

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*-
2+
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
"""
4+
Parse the "ASCCONV" meta data format found in a variety of Siemens MR files.
5+
"""
6+
import re
7+
import ast
8+
from ..externals import OrderedDict
9+
10+
11+
ASCCONV_RE = re.compile(
12+
r'### ASCCONV BEGIN((?:\s*[^=\s]+=[^=\s]+)*) ###\n(.*?)\n### ASCCONV END ###',
13+
flags=re.M | re.S)
14+
15+
16+
class AscconvParseError(Exception):
17+
""" Error parsing ascconv file """
18+
19+
20+
class Atom(object):
21+
""" Object to hold operation, object type and object identifier
22+
23+
An atom represents an element in an expression. For example::
24+
25+
a.b[0].c
26+
27+
has four elements. We call these elements "atoms".
28+
29+
We represent objects (like ``a``) as dicts for convenience.
30+
31+
The last element (``.c``) is an ``op = ast.Attribute`` operation where the
32+
object type (`obj_type`) of ``c`` is not constrained (we can't tell from
33+
the operation what type it is). The `obj_id` is the name of the object --
34+
"c".
35+
36+
The second to last element ``[0]``, is ``op = ast.Subscript``, with object type
37+
dict (we know from the subsequent operation ``.c`` that this must be an
38+
object, we represent the object by a dict). The `obj_id` is the index 0.
39+
40+
Parameters
41+
----------
42+
op : {'name', 'attr', 'list'}
43+
Assignment type. Assignment to name (root namespace), attribute or
44+
list element.
45+
obj_type : {list, dict, other}
46+
Object type being assigned to.
47+
obj_id : str or int
48+
Key (``obj_type is dict``) or index (``obj_type is list``)
49+
"""
50+
51+
def __init__(self, op, obj_type, obj_id):
52+
self.op = op
53+
self.obj_type = obj_type
54+
self.obj_id = obj_id
55+
56+
57+
class NoValue(object):
58+
""" Signals no value present """
59+
60+
61+
def assign2atoms(assign_ast, default_class=int):
62+
""" Parse single assignment ast from ascconv line into atoms
63+
64+
Parameters
65+
----------
66+
assign_ast : assignment statement ast
67+
ast derived from single line of ascconv file.
68+
default_class : class, optional
69+
Class that will create an object where we cannot yet know the object
70+
type in the assignment.
71+
72+
Returns
73+
-------
74+
atoms : list
75+
List of :class:`atoms`. See docstring for :class:`atoms`. Defines
76+
left to right sequence of assignment in `line_ast`.
77+
"""
78+
if not len(assign_ast.targets) == 1:
79+
raise AscconvParseError('Too many targets in assign')
80+
target = assign_ast.targets[0]
81+
atoms = []
82+
prev_target_type = default_class # Placeholder for any scalar value
83+
while True:
84+
if isinstance(target, ast.Name):
85+
atoms.append(Atom(target, prev_target_type, target.id))
86+
break
87+
if isinstance(target, ast.Attribute):
88+
atoms.append(Atom(target, prev_target_type, target.attr))
89+
target = target.value
90+
prev_target_type = OrderedDict
91+
elif isinstance(target, ast.Subscript):
92+
index = target.slice.value.n
93+
atoms.append(Atom(target, prev_target_type, index))
94+
target = target.value
95+
prev_target_type = list
96+
else:
97+
raise AscconvParseError(
98+
'Unexpected LHS element {0}'.format(target))
99+
return reversed(atoms)
100+
101+
102+
def _create_obj_in(atom, root):
103+
""" Create object defined in `atom` in dict-like given by `root`
104+
105+
Return defined object.
106+
"""
107+
name = atom.obj_id
108+
obj = root.get(name, NoValue)
109+
if obj is not NoValue:
110+
return obj
111+
obj = atom.obj_type()
112+
root[name] = obj
113+
return obj
114+
115+
116+
def _create_subscript_in(atom, root):
117+
""" Create object defined in `atom` at index ``atom.obj_id`` in list `root`
118+
119+
Return defined object.
120+
"""
121+
curr_n = len(root)
122+
index = atom.obj_id
123+
if curr_n > index:
124+
return root[index]
125+
obj = atom.obj_type()
126+
root += [None] * (index - curr_n) + [obj]
127+
return obj
128+
129+
130+
def obj_from_atoms(atoms, namespace):
131+
""" Return object defined by list `atoms` in dict-like `namespace`
132+
133+
Parameters
134+
----------
135+
atoms : list
136+
List of :class:`atoms`
137+
namespace : dict-like
138+
Namespace in which object will be defined.
139+
140+
Returns
141+
-------
142+
obj_root : object
143+
Namespace such that we can set a desired value to the object defined in
144+
`atoms` with ``obj_root[obj_key] = value``.
145+
obj_key : str or int
146+
Index into list or key into dictionary for `obj_root`.
147+
"""
148+
root_obj = namespace
149+
for el in atoms:
150+
prev_root = root_obj
151+
if isinstance(el.op, (ast.Attribute, ast.Name)):
152+
root_obj = _create_obj_in(el, root_obj)
153+
else:
154+
root_obj = _create_subscript_in(el, root_obj)
155+
if not isinstance(root_obj, el.obj_type):
156+
raise AscconvParseError(
157+
'Unexpected type for {0} in {1}'.format(el.obj_id, prev_root))
158+
return prev_root, el.obj_id
159+
160+
161+
def _get_value(assign):
162+
value = assign.value
163+
if isinstance(value, ast.Num):
164+
return value.n
165+
if isinstance(value, ast.Str):
166+
return value.s
167+
if isinstance(value, ast.UnaryOp) and isinstance(value.op, ast.USub):
168+
return -value.operand.n
169+
raise AscconvParseError('Unexpected RHS of assignment: {0}'.format(value))
170+
171+
172+
def parse_ascconv(ascconv_str, str_delim='"'):
173+
'''Parse the 'ASCCONV' format from `input_str`.
174+
175+
Parameters
176+
----------
177+
ascconv_str : str
178+
The string we are parsing
179+
str_delim : str, optional
180+
String delimiter. Typically '"' or '""'
181+
182+
Returns
183+
-------
184+
prot_dict : OrderedDict
185+
Meta data pulled from the ASCCONV section.
186+
attrs : OrderedDict
187+
Any attributes stored in the 'ASCCONV BEGIN' line
188+
189+
Raises
190+
------
191+
AsconvParseError
192+
A line of the ASCCONV section could not be parsed.
193+
'''
194+
attrs, content = ASCCONV_RE.match(ascconv_str).groups()
195+
attrs = OrderedDict((tuple(x.split('=')) for x in attrs.split()))
196+
# Normalize string start / end markers to something Python understands
197+
content = content.replace(str_delim, '"""')
198+
# Use Python's own parser to parse modified ASCCONV assignments
199+
tree = ast.parse(content)
200+
201+
prot_dict = OrderedDict()
202+
for assign in tree.body:
203+
atoms = assign2atoms(assign)
204+
obj_to_index, key = obj_from_atoms(atoms, prot_dict)
205+
obj_to_index[key] = _get_value(assign)
206+
207+
return prot_dict, attrs

0 commit comments

Comments
 (0)