pyemaps 2.0.0__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.
- pyemaps/COPYING +798 -0
- pyemaps/CifFile/CifFile_module.py +3117 -0
- pyemaps/CifFile/StarFile.py +2496 -0
- pyemaps/CifFile/TypeContentsParser.py +82 -0
- pyemaps/CifFile/YappsStarParser_1_0.py +289 -0
- pyemaps/CifFile/YappsStarParser_1_1.py +288 -0
- pyemaps/CifFile/YappsStarParser_2_0.py +369 -0
- pyemaps/CifFile/YappsStarParser_STAR2.py +382 -0
- pyemaps/CifFile/__init__.py +7 -0
- pyemaps/CifFile/drel/__init__.py +1 -0
- pyemaps/CifFile/drel/drel_ast_yacc.py +582 -0
- pyemaps/CifFile/drel/drel_lex.py +228 -0
- pyemaps/CifFile/drel/drel_runtime.py +126 -0
- pyemaps/CifFile/drel/parsetab.py +185 -0
- pyemaps/CifFile/drel/py_from_ast.py +628 -0
- pyemaps/CifFile/parsetab.py +185 -0
- pyemaps/CifFile/yapps3_compiled_rt.py +390 -0
- pyemaps/README.md +201 -0
- pyemaps/__config__.py +91 -0
- pyemaps/__init__.py +233 -0
- pyemaps/__main__.py +219 -0
- pyemaps/__version__.py +1 -0
- pyemaps/cdata/Aluminium.xtl +4 -0
- pyemaps/cdata/AluminiumOxide.xtl +5 -0
- pyemaps/cdata/Aluminium_FCC.xtl +4 -0
- pyemaps/cdata/BariumTitanate_180k.xtl +6 -0
- pyemaps/cdata/BariumTitanate_270k.xtl +7 -0
- pyemaps/cdata/BariumTitanate_Tetra.xtl +7 -0
- pyemaps/cdata/BiMnO3.xtl +27 -0
- pyemaps/cdata/Boron_Tetra.xtl +8 -0
- pyemaps/cdata/CadmiumSelenide_Hex.xtl +5 -0
- pyemaps/cdata/CadmiumSulfide_Cubic.xtl +5 -0
- pyemaps/cdata/CadmiumSulfide_Hex.xtl +5 -0
- pyemaps/cdata/Chromium_BCC.xtl +4 -0
- pyemaps/cdata/CoSb3_Skutterudite.xtl +5 -0
- pyemaps/cdata/CopperOxide.xtl +5 -0
- pyemaps/cdata/Copper_FCC.xtl +4 -0
- pyemaps/cdata/Cu2O_Cuprite.xtl +5 -0
- pyemaps/cdata/Diamond.xtl +4 -0
- pyemaps/cdata/ErbiumPyrogermanate.xtl +9 -0
- pyemaps/cdata/FePd_Tetra.xtl +5 -0
- pyemaps/cdata/FeS2_Pyrite.xtl +15 -0
- pyemaps/cdata/GalliumAntimonide.xtl +5 -0
- pyemaps/cdata/GalliumArsenide.xtl +5 -0
- pyemaps/cdata/GalliumNitride.xtl +5 -0
- pyemaps/cdata/Germanium.xtl +4 -0
- pyemaps/cdata/Gold_FCC.xtl +4 -0
- pyemaps/cdata/IndiumArsenide.xtl +5 -0
- pyemaps/cdata/LaMnO3.xtl +7 -0
- pyemaps/cdata/LeadZirconateTitanate.xtl +8 -0
- pyemaps/cdata/Li2MnO3.xtl +13 -0
- pyemaps/cdata/NaFeO2.xtl +6 -0
- pyemaps/cdata/Nb3Sn.xtl +5 -0
- pyemaps/cdata/Pentacene.xtl +39 -0
- pyemaps/cdata/Silicon.xtl +4 -0
- pyemaps/cdata/StrontiumTitanate.xtl +6 -0
- pyemaps/cdata/TelluriumDioxide.xtl +5 -0
- pyemaps/cdata/TinDioxide_RT.xtl +5 -0
- pyemaps/cdata/TitaniumDioxide_Anatase.xtl +5 -0
- pyemaps/cdata/TitaniumDioxide_Rutile.xtl +5 -0
- pyemaps/cdata/TungstenDiselenide.xtl +5 -0
- pyemaps/cdata/VanadiumDioxide_RT.xtl +6 -0
- pyemaps/cdata/ZincOxide.xtl +5 -0
- pyemaps/cdata/Zinc_HCP.xtl +4 -0
- pyemaps/cdata/ZirconiumNitride.xtl +5 -0
- pyemaps/cdata/limno2.xtl +6 -0
- pyemaps/crystals.py +1415 -0
- pyemaps/ddiffs.py +147 -0
- pyemaps/diffract/__init__.py +70 -0
- pyemaps/diffract/bloch_dec.py +779 -0
- pyemaps/diffract/csf_dec.py +242 -0
- pyemaps/diffract/dif_dec.py +519 -0
- pyemaps/diffract/dpgen_dec.py +175 -0
- pyemaps/diffract/mxtal_dec.py +201 -0
- pyemaps/diffract/powder_dec.py +137 -0
- pyemaps/diffract/stereo_dec.py +111 -0
- pyemaps/display.py +791 -0
- pyemaps/emcontrols.py +1017 -0
- pyemaps/errors.py +266 -0
- pyemaps/fileutils.py +1001 -0
- pyemaps/kdiffs.py +1221 -0
- pyemaps/license.txt +42 -0
- pyemaps/samples/adf.py +137 -0
- pyemaps/samples/al.img +0 -0
- pyemaps/samples/al_dpgen.py +57 -0
- pyemaps/samples/al_ediom.py +94 -0
- pyemaps/samples/convert_image.py +88 -0
- pyemaps/samples/dm_diff.py +225 -0
- pyemaps/samples/mask.im3 +0 -0
- pyemaps/samples/powder.py +51 -0
- pyemaps/samples/raw.img +0 -0
- pyemaps/samples/si_bloch.py +120 -0
- pyemaps/samples/si_constructor.py +51 -0
- pyemaps/samples/si_csf.py +59 -0
- pyemaps/samples/si_dif.py +134 -0
- pyemaps/samples/si_lacbed.py +59 -0
- pyemaps/samples/si_pyemaps.py +80 -0
- pyemaps/samples/si_scm.py +156 -0
- pyemaps/samples/si_stereo.py +116 -0
- pyemaps/scattering/__init__.py +26 -0
- pyemaps/scattering/sct_dec.py +31 -0
- pyemaps/spg/__init__.py +25 -0
- pyemaps/spg/spg_dec.py +60 -0
- pyemaps/stackimg.py +1031 -0
- pyemaps/xtal.py +222 -0
- pyemaps-2.0.0.dist-info/METADATA +231 -0
- pyemaps-2.0.0.dist-info/RECORD +112 -0
- pyemaps-2.0.0.dist-info/WHEEL +5 -0
- pyemaps-2.0.0.dist-info/entry_points.txt +2 -0
- pyemaps-2.0.0.dist-info/licenses/COPYING +798 -0
- pyemaps-2.0.0.dist-info/licenses/license.txt +42 -0
- pyemaps-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,2496 @@
|
|
|
1
|
+
# To maximize python3/python2 compatibility
|
|
2
|
+
from __future__ import print_function
|
|
3
|
+
from __future__ import unicode_literals
|
|
4
|
+
from __future__ import division
|
|
5
|
+
from __future__ import absolute_import
|
|
6
|
+
|
|
7
|
+
__copyright = """
|
|
8
|
+
PYCIFRW License Agreement (Python License, Version 2)
|
|
9
|
+
-----------------------------------------------------
|
|
10
|
+
|
|
11
|
+
1. This LICENSE AGREEMENT is between the Australian Nuclear Science
|
|
12
|
+
and Technology Organisation ("ANSTO"), and the Individual or
|
|
13
|
+
Organization ("Licensee") accessing and otherwise using this software
|
|
14
|
+
("PyCIFRW") in source or binary form and its associated documentation.
|
|
15
|
+
|
|
16
|
+
2. Subject to the terms and conditions of this License Agreement,
|
|
17
|
+
ANSTO hereby grants Licensee a nonexclusive, royalty-free, world-wide
|
|
18
|
+
license to reproduce, analyze, test, perform and/or display publicly,
|
|
19
|
+
prepare derivative works, distribute, and otherwise use PyCIFRW alone
|
|
20
|
+
or in any derivative version, provided, however, that this License
|
|
21
|
+
Agreement and ANSTO's notice of copyright, i.e., "Copyright (c)
|
|
22
|
+
2001-2014 ANSTO; All Rights Reserved" are retained in PyCIFRW alone or
|
|
23
|
+
in any derivative version prepared by Licensee.
|
|
24
|
+
|
|
25
|
+
3. In the event Licensee prepares a derivative work that is based on
|
|
26
|
+
or incorporates PyCIFRW or any part thereof, and wants to make the
|
|
27
|
+
derivative work available to others as provided herein, then Licensee
|
|
28
|
+
hereby agrees to include in any such work a brief summary of the
|
|
29
|
+
changes made to PyCIFRW.
|
|
30
|
+
|
|
31
|
+
4. ANSTO is making PyCIFRW available to Licensee on an "AS IS"
|
|
32
|
+
basis. ANSTO MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
|
33
|
+
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, ANSTO MAKES NO AND
|
|
34
|
+
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
|
35
|
+
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYCIFRW WILL NOT
|
|
36
|
+
INFRINGE ANY THIRD PARTY RIGHTS.
|
|
37
|
+
|
|
38
|
+
5. ANSTO SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYCIFRW
|
|
39
|
+
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A
|
|
40
|
+
RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYCIFRW, OR ANY
|
|
41
|
+
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
|
42
|
+
|
|
43
|
+
6. This License Agreement will automatically terminate upon a material
|
|
44
|
+
breach of its terms and conditions.
|
|
45
|
+
|
|
46
|
+
7. Nothing in this License Agreement shall be deemed to create any
|
|
47
|
+
relationship of agency, partnership, or joint venture between ANSTO
|
|
48
|
+
and Licensee. This License Agreement does not grant permission to use
|
|
49
|
+
ANSTO trademarks or trade name in a trademark sense to endorse or
|
|
50
|
+
promote products or services of Licensee, or any third party.
|
|
51
|
+
|
|
52
|
+
8. By copying, installing or otherwise using PyCIFRW, Licensee agrees
|
|
53
|
+
to be bound by the terms and conditions of this License Agreement.
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
import sys
|
|
59
|
+
|
|
60
|
+
# Python 2,3 compatibility
|
|
61
|
+
try:
|
|
62
|
+
from urllib import urlopen # for arbitrary opening
|
|
63
|
+
from urlparse import urlparse, urlunparse
|
|
64
|
+
except:
|
|
65
|
+
from urllib.request import urlopen
|
|
66
|
+
from urllib.parse import urlparse,urlunparse
|
|
67
|
+
import re,os
|
|
68
|
+
import textwrap
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
from StringIO import StringIO #not cStringIO as we cannot subclass
|
|
72
|
+
except ImportError:
|
|
73
|
+
from io import StringIO
|
|
74
|
+
|
|
75
|
+
if isinstance(u"abc",str): #Python 3
|
|
76
|
+
unicode = str
|
|
77
|
+
long = int
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
import numpy
|
|
81
|
+
have_numpy = True
|
|
82
|
+
except ImportError:
|
|
83
|
+
have_numpy = False
|
|
84
|
+
|
|
85
|
+
# Windows paths require special handling. pathlib is available from
|
|
86
|
+
# Python 3.4. For earlier Python versions dictionary import will not work
|
|
87
|
+
# on Windows
|
|
88
|
+
try:
|
|
89
|
+
from pathlib import Path
|
|
90
|
+
have_pathlib = True
|
|
91
|
+
except:
|
|
92
|
+
have_pathlib = False
|
|
93
|
+
|
|
94
|
+
class StarList(list):
|
|
95
|
+
def __getitem__(self,args):
|
|
96
|
+
if isinstance(args,(int,slice)):
|
|
97
|
+
return super(StarList,self).__getitem__(args)
|
|
98
|
+
elif isinstance(args,tuple) and len(args)>1: #extended comma notation
|
|
99
|
+
return super(StarList,self).__getitem__(args[0]).__getitem__(args[1:])
|
|
100
|
+
else:
|
|
101
|
+
return super(StarList,self).__getitem__(args[0])
|
|
102
|
+
|
|
103
|
+
def __str__(self):
|
|
104
|
+
return "SL("+super(StarList,self).__str__() + ")"
|
|
105
|
+
|
|
106
|
+
class StarDict(dict):
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class LoopBlock(object):
|
|
111
|
+
def __init__(self,parent_block,dataname):
|
|
112
|
+
self.loop_no = parent_block.FindLoop(dataname)
|
|
113
|
+
if self.loop_no < 0:
|
|
114
|
+
raise KeyError('%s is not in a loop structure' % dataname)
|
|
115
|
+
self.parent_block = parent_block
|
|
116
|
+
|
|
117
|
+
def keys(self):
|
|
118
|
+
return self.parent_block.loops[self.loop_no]
|
|
119
|
+
|
|
120
|
+
def values(self):
|
|
121
|
+
return [self.parent_block[a] for a in self.keys()]
|
|
122
|
+
|
|
123
|
+
#Avoid iterator even though that is Python3-esque
|
|
124
|
+
def items(self):
|
|
125
|
+
return list(zip(self.keys(),self.values()))
|
|
126
|
+
|
|
127
|
+
def __getitem__(self,dataname):
|
|
128
|
+
if isinstance(dataname,int): #a packet request
|
|
129
|
+
return self.GetPacket(dataname)
|
|
130
|
+
if dataname in self.keys():
|
|
131
|
+
return self.parent_block[dataname]
|
|
132
|
+
else:
|
|
133
|
+
raise KeyError('%s not in loop block' % dataname)
|
|
134
|
+
|
|
135
|
+
def __setitem__(self,dataname,value):
|
|
136
|
+
self.parent_block[dataname] = value
|
|
137
|
+
self.parent_block.AddLoopName(self.keys()[0],dataname)
|
|
138
|
+
|
|
139
|
+
def __contains__(self,key):
|
|
140
|
+
return key in self.parent_block.loops[self.loop_no]
|
|
141
|
+
|
|
142
|
+
def has_key(self,key):
|
|
143
|
+
return key in self
|
|
144
|
+
|
|
145
|
+
def __iter__(self):
|
|
146
|
+
packet_list = zip(*self.values())
|
|
147
|
+
names = self.keys()
|
|
148
|
+
for p in packet_list:
|
|
149
|
+
r = StarPacket(p)
|
|
150
|
+
for n in range(len(names)):
|
|
151
|
+
setattr(r,names[n].lower(),r[n])
|
|
152
|
+
yield r
|
|
153
|
+
|
|
154
|
+
# for compatibility
|
|
155
|
+
def __getattr__(self,attname):
|
|
156
|
+
return getattr(self.parent_block,attname)
|
|
157
|
+
|
|
158
|
+
def load_iter(self,coords=[]):
|
|
159
|
+
count = 0 #to create packet index
|
|
160
|
+
while not self.popout:
|
|
161
|
+
# ok, we have a new packet: append a list to our subloops
|
|
162
|
+
for aloop in self.loops:
|
|
163
|
+
aloop.new_enclosing_packet()
|
|
164
|
+
for iname in self.item_order:
|
|
165
|
+
if isinstance(iname,LoopBlock): #into a nested loop
|
|
166
|
+
for subitems in iname.load_iter(coords=coords+[count]):
|
|
167
|
+
# print 'Yielding %s' % `subitems`
|
|
168
|
+
yield subitems
|
|
169
|
+
# print 'End of internal loop'
|
|
170
|
+
else:
|
|
171
|
+
if self.dimension == 0:
|
|
172
|
+
# print 'Yielding %s' % `self[iname]`
|
|
173
|
+
yield self,self[iname]
|
|
174
|
+
else:
|
|
175
|
+
backval = self.block[iname]
|
|
176
|
+
for i in range(len(coords)):
|
|
177
|
+
# print 'backval, coords: %s, %s' % (`backval`,`coords`)
|
|
178
|
+
backval = backval[coords[i]]
|
|
179
|
+
yield self,backval
|
|
180
|
+
count = count + 1 # count packets
|
|
181
|
+
self.popout = False # reinitialise
|
|
182
|
+
# print 'Finished iterating'
|
|
183
|
+
yield self,'###Blank###' #this value should never be used
|
|
184
|
+
|
|
185
|
+
# an experimental fast iterator for level-1 loops (ie CIF)
|
|
186
|
+
def fast_load_iter(self):
|
|
187
|
+
targets = map(lambda a:self.block[a],self.item_order)
|
|
188
|
+
while targets:
|
|
189
|
+
for target in targets:
|
|
190
|
+
yield self,target
|
|
191
|
+
|
|
192
|
+
# Add another list of the required shape to take into account a new outer packet
|
|
193
|
+
def new_enclosing_packet(self):
|
|
194
|
+
if self.dimension > 1: #otherwise have a top-level list
|
|
195
|
+
for iname in self.keys(): #includes lower levels
|
|
196
|
+
target_list = self[iname]
|
|
197
|
+
for i in range(3,self.dimension): #dim 2 upwards are lists of lists of...
|
|
198
|
+
target_list = target_list[-1]
|
|
199
|
+
target_list.append([])
|
|
200
|
+
# print '%s now %s' % (iname,`self[iname]`)
|
|
201
|
+
|
|
202
|
+
def recursive_iter(self,dict_so_far={},coord=[]):
|
|
203
|
+
# print "Recursive iter: coord %s, keys %s, dim %d" % (`coord`,`self.block.keys()`,self.dimension)
|
|
204
|
+
my_length = 0
|
|
205
|
+
top_items = self.block.items()
|
|
206
|
+
top_values = self.block.values() #same order as items
|
|
207
|
+
drill_values = self.block.values()
|
|
208
|
+
for dimup in range(0,self.dimension): #look higher in the tree
|
|
209
|
+
if len(drill_values)>0: #this block has values
|
|
210
|
+
drill_values=drill_values[0] #drill in
|
|
211
|
+
else:
|
|
212
|
+
raise StarError("Malformed loop packet %s" % repr( top_items[0] ))
|
|
213
|
+
my_length = len(drill_values[0]) #length of 'string' entry
|
|
214
|
+
if self.dimension == 0: #top level
|
|
215
|
+
for aloop in self.loops:
|
|
216
|
+
for apacket in aloop.recursive_iter():
|
|
217
|
+
# print "Recursive yielding %s" % repr( dict(top_items + apacket.items()) )
|
|
218
|
+
prep_yield = StarPacket(top_values+apacket.values()) #straight list
|
|
219
|
+
for name,value in top_items + apacket.items():
|
|
220
|
+
setattr(prep_yield,name,value)
|
|
221
|
+
yield prep_yield
|
|
222
|
+
else: #in some loop
|
|
223
|
+
for i in range(my_length):
|
|
224
|
+
kvpairs = map(lambda a:(a,self.coord_to_group(a,coord)[i]),self.block.keys())
|
|
225
|
+
kvvals = map(lambda a:a[1],kvpairs) #just values
|
|
226
|
+
# print "Recursive kvpairs at %d: %s" % (i,repr( kvpairs ))
|
|
227
|
+
if self.loops:
|
|
228
|
+
for aloop in self.loops:
|
|
229
|
+
for apacket in aloop.recursive_iter(coord=coord+[i]):
|
|
230
|
+
# print "Recursive yielding %s" % repr( dict(kvpairs + apacket.items()) )
|
|
231
|
+
prep_yield = StarPacket(kvvals+apacket.values())
|
|
232
|
+
for name,value in kvpairs + apacket.items():
|
|
233
|
+
setattr(prep_yield,name,value)
|
|
234
|
+
yield prep_yield
|
|
235
|
+
else: # we're at the bottom of the tree
|
|
236
|
+
# print "Recursive yielding %s" % repr( dict(kvpairs) )
|
|
237
|
+
prep_yield = StarPacket(kvvals)
|
|
238
|
+
for name,value in kvpairs:
|
|
239
|
+
setattr(prep_yield,name,value)
|
|
240
|
+
yield prep_yield
|
|
241
|
+
|
|
242
|
+
# small function to use the coordinates.
|
|
243
|
+
def coord_to_group(self,dataname,coords):
|
|
244
|
+
if not isinstance(dataname,unicode):
|
|
245
|
+
return dataname # flag inner loop processing
|
|
246
|
+
newm = self[dataname] # newm must be a list or tuple
|
|
247
|
+
for c in coords:
|
|
248
|
+
# print "Coord_to_group: %s ->" % (repr( newm )),
|
|
249
|
+
newm = newm[c]
|
|
250
|
+
# print repr( newm )
|
|
251
|
+
return newm
|
|
252
|
+
|
|
253
|
+
def flat_iterator(self):
|
|
254
|
+
my_length = 0
|
|
255
|
+
top_keys = self.block.keys()
|
|
256
|
+
if len(top_keys)>0:
|
|
257
|
+
my_length = len(self.block[top_keys[0]])
|
|
258
|
+
for pack_no in range(my_length):
|
|
259
|
+
yield(self.collapse(pack_no))
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def RemoveItem(self,itemname):
|
|
263
|
+
"""Remove `itemname` from the block."""
|
|
264
|
+
# first check any loops
|
|
265
|
+
loop_no = self.FindLoop(itemname)
|
|
266
|
+
testkey = itemname.lower()
|
|
267
|
+
if testkey in self:
|
|
268
|
+
del self.block[testkey]
|
|
269
|
+
del self.true_case[testkey]
|
|
270
|
+
# now remove from loop
|
|
271
|
+
if loop_no >= 0:
|
|
272
|
+
self.loops[loop_no].remove(testkey)
|
|
273
|
+
if len(self.loops[loop_no])==0:
|
|
274
|
+
del self.loops[loop_no]
|
|
275
|
+
self.item_order.remove(loop_no)
|
|
276
|
+
else: #will appear in order list
|
|
277
|
+
self.item_order.remove(testkey)
|
|
278
|
+
|
|
279
|
+
def RemoveLoopItem(self,itemname):
|
|
280
|
+
"""*Deprecated*. Use `RemoveItem` instead"""
|
|
281
|
+
self.RemoveItem(itemname)
|
|
282
|
+
|
|
283
|
+
def GetLoop(self,keyname):
|
|
284
|
+
"""Return a `StarFile.LoopBlock` object constructed from the loop containing `keyname`.
|
|
285
|
+
`keyname` is only significant as a way to specify the loop."""
|
|
286
|
+
return LoopBlock(self,keyname)
|
|
287
|
+
|
|
288
|
+
def GetPacket(self,index):
|
|
289
|
+
thispack = StarPacket([])
|
|
290
|
+
for myitem in self.parent_block.loops[self.loop_no]:
|
|
291
|
+
thispack.append(self[myitem][index])
|
|
292
|
+
setattr(thispack,myitem,thispack[-1])
|
|
293
|
+
return thispack
|
|
294
|
+
|
|
295
|
+
def AddPacket(self,packet):
|
|
296
|
+
for myitem in self.parent_block.loops[self.loop_no]:
|
|
297
|
+
old_values = self.parent_block[myitem]
|
|
298
|
+
old_values.append(packet.__getattribute__(myitem))
|
|
299
|
+
self.parent_block[myitem] = old_values
|
|
300
|
+
|
|
301
|
+
def GetItemOrder(self):
|
|
302
|
+
"""Return a list of datanames in this `LoopBlock` in the order that they will be
|
|
303
|
+
printed"""
|
|
304
|
+
return self.parent_block.loops[self.loop_no][:]
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def ChangeItemOrder(self,itemname,newpos):
|
|
308
|
+
"""Change the position at which `itemname` appears when printing out to `newpos`."""
|
|
309
|
+
self.parent_block.loops[self.loop_no].remove(itemname.lower())
|
|
310
|
+
self.parent_block.loops[self.loop_no].insert(newpos,itemname.lower())
|
|
311
|
+
|
|
312
|
+
def GetItemPosition(self,itemname):
|
|
313
|
+
"""A utility function to get the numerical order in the printout
|
|
314
|
+
of `itemname`. An item has coordinate `(loop_no,pos)` with
|
|
315
|
+
the top level having a `loop_no` of -1. If an integer is passed to
|
|
316
|
+
the routine then it will return the position of the loop
|
|
317
|
+
referenced by that number."""
|
|
318
|
+
if isinstance(itemname,int):
|
|
319
|
+
# return loop position
|
|
320
|
+
return (-1, self.item_order.index(itemname))
|
|
321
|
+
if not itemname in self:
|
|
322
|
+
raise ValueError('No such dataname %s' % itemname)
|
|
323
|
+
testname = itemname.lower()
|
|
324
|
+
if testname in self.item_order:
|
|
325
|
+
return (-1,self.item_order.index(testname))
|
|
326
|
+
loop_no = self.FindLoop(testname)
|
|
327
|
+
loop_pos = self.loops[loop_no].index(testname)
|
|
328
|
+
return loop_no,loop_pos
|
|
329
|
+
|
|
330
|
+
def GetLoopNames(self,keyname):
|
|
331
|
+
if keyname in self:
|
|
332
|
+
return self.keys()
|
|
333
|
+
for aloop in self.loops:
|
|
334
|
+
try:
|
|
335
|
+
return aloop.GetLoopNames(keyname)
|
|
336
|
+
except KeyError:
|
|
337
|
+
pass
|
|
338
|
+
raise KeyError('Item does not exist')
|
|
339
|
+
|
|
340
|
+
def GetLoopNames(self,keyname):
|
|
341
|
+
"""Return all datanames appearing together with `keyname`"""
|
|
342
|
+
loop_no = self.FindLoop(keyname)
|
|
343
|
+
if loop_no >= 0:
|
|
344
|
+
return self.loops[loop_no]
|
|
345
|
+
else:
|
|
346
|
+
raise KeyError('%s is not in any loop' % keyname)
|
|
347
|
+
|
|
348
|
+
def AddToLoop(self,dataname,loopdata):
|
|
349
|
+
thisloop = self.GetLoop(dataname)
|
|
350
|
+
for itemname,itemvalue in loopdata.items():
|
|
351
|
+
thisloop[itemname] = itemvalue
|
|
352
|
+
|
|
353
|
+
def AddToLoop(self,dataname,loopdata):
|
|
354
|
+
"""*Deprecated*. Use `AddItem` followed by calls to `AddLoopName`.
|
|
355
|
+
|
|
356
|
+
Add multiple columns to the loop containing `dataname`. `loopdata` is a
|
|
357
|
+
collection of (key,value) pairs, where `key` is the new dataname and `value`
|
|
358
|
+
is a list of values for that dataname"""
|
|
359
|
+
self.update(loopdata)
|
|
360
|
+
for one_name in loopdata:
|
|
361
|
+
self.AddLoopName(dataname,one_name)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class StarBlock(object):
|
|
365
|
+
def __init__(self,data = (), maxoutlength=2048, wraplength=80, overwrite=True,
|
|
366
|
+
characterset='ascii',maxnamelength=-1):
|
|
367
|
+
self.block = {} #the actual data storage (lower case keys)
|
|
368
|
+
self.loops = {} #each loop is indexed by a number and contains a list of datanames
|
|
369
|
+
self.item_order = [] #lower case, loops referenced by integer
|
|
370
|
+
self.formatting_hints = {}
|
|
371
|
+
self.true_case = {} #transform lower case to supplied case
|
|
372
|
+
self.provide_value = False #prefer string version always
|
|
373
|
+
self.dictionary = None #DDLm dictionary
|
|
374
|
+
self.popout = False #used during load iteration
|
|
375
|
+
self.curitem = -1 #used during iteration
|
|
376
|
+
self.cache_vals = True #store all calculated values
|
|
377
|
+
self.maxoutlength = maxoutlength
|
|
378
|
+
self.setmaxnamelength(maxnamelength) #to enforce CIF limit of 75 characters
|
|
379
|
+
self.set_characterset(characterset) #to check input names
|
|
380
|
+
self.wraplength = wraplength
|
|
381
|
+
self.overwrite = overwrite
|
|
382
|
+
self.string_delimiters = ["'",'"',"\n;"] #universal CIF set
|
|
383
|
+
self.list_delimiter = " " #CIF2 default
|
|
384
|
+
self.wrapper = textwrap.TextWrapper()
|
|
385
|
+
if isinstance(data,(tuple,list)):
|
|
386
|
+
for item in data:
|
|
387
|
+
self.AddLoopItem(item)
|
|
388
|
+
elif isinstance(data,StarBlock):
|
|
389
|
+
self.block = data.block.copy()
|
|
390
|
+
self.item_order = data.item_order[:]
|
|
391
|
+
self.true_case = data.true_case.copy()
|
|
392
|
+
# loops as well
|
|
393
|
+
self.loops = data.loops.copy()
|
|
394
|
+
|
|
395
|
+
def setmaxnamelength(self,maxlength):
|
|
396
|
+
"""Set the maximum allowable dataname length (-1 for no check)"""
|
|
397
|
+
self.maxnamelength = maxlength
|
|
398
|
+
if maxlength > 0:
|
|
399
|
+
bad_names = [a for a in self.keys() if len(a)>self.maxnamelength]
|
|
400
|
+
if len(bad_names)>0:
|
|
401
|
+
raise StarError('Datanames too long: ' + repr( bad_names ))
|
|
402
|
+
|
|
403
|
+
def set_characterset(self,characterset):
|
|
404
|
+
"""Set the characterset for checking datanames: may be `ascii` or `unicode`"""
|
|
405
|
+
self.characterset = characterset
|
|
406
|
+
if characterset == 'ascii':
|
|
407
|
+
self.char_check = re.compile("[][ \n\r\t!%&\\(\\)*+,./:<=>?@0-9A-Za-z\\\\^`{}\\|~\"#$';_-]+",re.M)
|
|
408
|
+
elif characterset == 'unicode':
|
|
409
|
+
if sys.maxunicode < 1114111:
|
|
410
|
+
self.char_check = re.compile(u"[][ \n\r\t!%&\\(\\)*+,./:<=>?@0-9A-Za-z\\\\^`{}\\|~\"#$';_\u00A0-\uD7FF\uE000-\uFDCF\uFDF0-\uFFFD-]+",re.M)
|
|
411
|
+
else:
|
|
412
|
+
self.char_check = re.compile(u"[][ \n\r\t!%&\\(\\)*+,./:<=>?@0-9A-Za-z\\\\^`{}\\|~\"#$';_\u00A0-\uD7FF\uE000-\uFDCF\uFDF0-\uFFFD\U00010000-\U0010FFFD-]+",re.M)
|
|
413
|
+
|
|
414
|
+
def __str__(self):
|
|
415
|
+
return self.printsection()
|
|
416
|
+
|
|
417
|
+
def __setitem__(self,key,value):
|
|
418
|
+
if key == "saves":
|
|
419
|
+
raise StarError("""Setting the saves key is deprecated. Add the save block to
|
|
420
|
+
an enclosing block collection (e.g. CIF or STAR file) with this block as child""")
|
|
421
|
+
self.AddItem(key,value)
|
|
422
|
+
|
|
423
|
+
def __getitem__(self,key):
|
|
424
|
+
if key == "saves":
|
|
425
|
+
raise StarError("""The saves key is deprecated. Access the save block from
|
|
426
|
+
the enclosing block collection (e.g. CIF or STAR file object)""")
|
|
427
|
+
try:
|
|
428
|
+
rawitem,is_value = self.GetFullItemValue(key)
|
|
429
|
+
except KeyError:
|
|
430
|
+
if self.dictionary:
|
|
431
|
+
# send the dictionary the required key and a pointer to us
|
|
432
|
+
try:
|
|
433
|
+
new_value = self.dictionary.derive_item(key,self,store_value=self.cache_vals,allow_defaults=False)
|
|
434
|
+
except StarDerivationFailure: #try now with defaults included
|
|
435
|
+
try:
|
|
436
|
+
new_value = self.dictionary.derive_item(key,self,store_value=self.cache_vals,allow_defaults=True)
|
|
437
|
+
except StarDerivationFailure as s:
|
|
438
|
+
print("In StarBlock.__getitem__, " + repr(s))
|
|
439
|
+
raise KeyError('No such item: %s' % key)
|
|
440
|
+
print('Set %s to derived value %s' % (key, repr(new_value)))
|
|
441
|
+
return new_value
|
|
442
|
+
else:
|
|
443
|
+
raise KeyError('No such item: %s' % key)
|
|
444
|
+
# we now have an item, we can try to convert it to a number if that is appropriate
|
|
445
|
+
# note numpy values are never stored but are converted to lists
|
|
446
|
+
if not self.dictionary or not key in self.dictionary: return rawitem
|
|
447
|
+
print('%s: is_value %s provide_value %s value %s' % (key,repr( is_value ),repr( self.provide_value ),repr( rawitem )))
|
|
448
|
+
if is_value:
|
|
449
|
+
if self.provide_value: return rawitem
|
|
450
|
+
else:
|
|
451
|
+
print('Turning %s into string' % repr( rawitem ))
|
|
452
|
+
return self.convert_to_string(key)
|
|
453
|
+
else: # a string
|
|
454
|
+
if self.provide_value and ((not isinstance(rawitem,list) and rawitem != '?' and rawitem != ".") or \
|
|
455
|
+
(isinstance(rawitem,list) and '?' not in rawitem and '.' not in rawitem)):
|
|
456
|
+
return self.dictionary.change_type(key,rawitem)
|
|
457
|
+
elif self.provide_value: # catch the question marks
|
|
458
|
+
do_calculate = False
|
|
459
|
+
if isinstance(rawitem,(list,tuple)):
|
|
460
|
+
known = [a for a in rawitem if a != '?']
|
|
461
|
+
if len(known) == 0: #all questions
|
|
462
|
+
do_calculate = True
|
|
463
|
+
elif rawitem == '?':
|
|
464
|
+
do_calculate = True
|
|
465
|
+
if do_calculate:
|
|
466
|
+
# remove old value
|
|
467
|
+
del self[key]
|
|
468
|
+
try:
|
|
469
|
+
new_value = self.dictionary.derive_item(key,self,store_value=True,allow_defaults=False)
|
|
470
|
+
except StarDerivationFailure as s:
|
|
471
|
+
try:
|
|
472
|
+
new_value = self.dictionary.derive_item(key,self,store_value=True,allow_defaults=True)
|
|
473
|
+
except StarDerivationFailure as s:
|
|
474
|
+
|
|
475
|
+
print("Could not turn %s into a value:" + repr(s))
|
|
476
|
+
return rawitem
|
|
477
|
+
else:
|
|
478
|
+
print('Set %s to derived value %s' % (key, repr( new_value )))
|
|
479
|
+
return new_value
|
|
480
|
+
return rawitem #can't do anything
|
|
481
|
+
|
|
482
|
+
def __delitem__(self,key):
|
|
483
|
+
self.RemoveItem(key)
|
|
484
|
+
|
|
485
|
+
def __len__(self):
|
|
486
|
+
blen = len(self.block)
|
|
487
|
+
return blen
|
|
488
|
+
|
|
489
|
+
def __nonzero__(self):
|
|
490
|
+
if self.__len__() > 0: return 1
|
|
491
|
+
return 0
|
|
492
|
+
|
|
493
|
+
# keys returns all internal keys
|
|
494
|
+
def keys(self):
|
|
495
|
+
return list(self.block.keys()) #always lower case
|
|
496
|
+
|
|
497
|
+
def values(self):
|
|
498
|
+
return [self[a] for a in self.keys()]
|
|
499
|
+
|
|
500
|
+
def items(self):
|
|
501
|
+
return list(zip(self.keys(),self.values()))
|
|
502
|
+
|
|
503
|
+
def __contains__(self,key):
|
|
504
|
+
if isinstance(key,(unicode,str)) and key.lower() in self.keys():
|
|
505
|
+
return True
|
|
506
|
+
return False
|
|
507
|
+
|
|
508
|
+
def has_key(self,key):
|
|
509
|
+
return key in self
|
|
510
|
+
|
|
511
|
+
def has_key_or_alias(self,key):
|
|
512
|
+
"""Check if a dataname or alias is available in the block"""
|
|
513
|
+
initial_test = key in self
|
|
514
|
+
if initial_test: return True
|
|
515
|
+
elif self.dictionary:
|
|
516
|
+
aliases = [k for k in self.dictionary.alias_table.get(key,[]) if self.has_key(k)]
|
|
517
|
+
if len(aliases)>0:
|
|
518
|
+
return True
|
|
519
|
+
return False
|
|
520
|
+
|
|
521
|
+
def get(self,key,default=None):
|
|
522
|
+
if key in self:
|
|
523
|
+
retval = self.__getitem__(key)
|
|
524
|
+
else:
|
|
525
|
+
retval = default
|
|
526
|
+
return retval
|
|
527
|
+
|
|
528
|
+
def clear(self):
|
|
529
|
+
self.block = {}
|
|
530
|
+
self.loops = {}
|
|
531
|
+
self.item_order = []
|
|
532
|
+
self.true_case = {}
|
|
533
|
+
|
|
534
|
+
# doesn't appear to work
|
|
535
|
+
def copy(self):
|
|
536
|
+
newcopy = StarBlock()
|
|
537
|
+
newcopy.block = self.block.copy()
|
|
538
|
+
newcopy.loops = []
|
|
539
|
+
newcopy.item_order = self.item_order[:]
|
|
540
|
+
newcopy.true_case = self.true_case.copy()
|
|
541
|
+
newcopy.loops = self.loops.copy()
|
|
542
|
+
# return self.copy.im_class(newcopy) #catch inheritance
|
|
543
|
+
return newcopy
|
|
544
|
+
|
|
545
|
+
def update(self,adict):
|
|
546
|
+
for key in adict.keys():
|
|
547
|
+
self.AddItem(key,adict[key])
|
|
548
|
+
|
|
549
|
+
def GetItemPosition(self,itemname):
|
|
550
|
+
"""A utility function to get the numerical order in the printout
|
|
551
|
+
of `itemname`. An item has coordinate `(loop_no,pos)` with
|
|
552
|
+
the top level having a `loop_no` of -1. If an integer is passed to
|
|
553
|
+
the routine then it will return the position of the loop
|
|
554
|
+
referenced by that number."""
|
|
555
|
+
if isinstance(itemname,int):
|
|
556
|
+
# return loop position
|
|
557
|
+
return (-1, self.item_order.index(itemname))
|
|
558
|
+
if not itemname in self:
|
|
559
|
+
raise ValueError('No such dataname %s' % itemname)
|
|
560
|
+
testname = itemname.lower()
|
|
561
|
+
if testname in self.item_order:
|
|
562
|
+
return (-1,self.item_order.index(testname))
|
|
563
|
+
loop_no = self.FindLoop(testname)
|
|
564
|
+
loop_pos = self.loops[loop_no].index(testname)
|
|
565
|
+
return loop_no,loop_pos
|
|
566
|
+
|
|
567
|
+
def ChangeItemOrder(self,itemname,newpos):
|
|
568
|
+
"""Move the printout order of `itemname` to `newpos`. If `itemname` is
|
|
569
|
+
in a loop, `newpos` refers to the order within the loop."""
|
|
570
|
+
if isinstance(itemname,(unicode,str)):
|
|
571
|
+
true_name = itemname.lower()
|
|
572
|
+
else:
|
|
573
|
+
true_name = itemname
|
|
574
|
+
loopno = self.FindLoop(true_name)
|
|
575
|
+
if loopno < 0: #top level
|
|
576
|
+
self.item_order.remove(true_name)
|
|
577
|
+
self.item_order.insert(newpos,true_name)
|
|
578
|
+
else:
|
|
579
|
+
self.loops[loopno].remove(true_name)
|
|
580
|
+
self.loops[loopno].insert(newpos,true_name)
|
|
581
|
+
|
|
582
|
+
def GetItemOrder(self):
|
|
583
|
+
"""Return a list of datanames in the order in which they will be printed. Loops are
|
|
584
|
+
referred to by numerical index"""
|
|
585
|
+
return self.item_order[:]
|
|
586
|
+
|
|
587
|
+
def AddItem(self,key,value,precheck=False):
|
|
588
|
+
"""Add dataname `key` to block with value `value`. `value` may be
|
|
589
|
+
a single value, a list or a tuple. If `precheck` is False (the default),
|
|
590
|
+
all values will be checked and converted to unicode strings as necessary. If
|
|
591
|
+
`precheck` is True, this checking is bypassed. No checking is necessary
|
|
592
|
+
when values are read from a CIF file as they are already in correct form."""
|
|
593
|
+
if not isinstance(key,(unicode,str)):
|
|
594
|
+
raise TypeError('Star datanames are strings only (got %s)' % repr( key ))
|
|
595
|
+
key = unicode(key) #everything is unicode internally
|
|
596
|
+
if not precheck:
|
|
597
|
+
self.check_data_name(key,self.maxnamelength) # make sure no nasty characters
|
|
598
|
+
# check for overwriting
|
|
599
|
+
if key in self:
|
|
600
|
+
if not self.overwrite:
|
|
601
|
+
raise StarError( 'Attempt to insert duplicate item name %s' % key)
|
|
602
|
+
if not precheck: #need to sanitise
|
|
603
|
+
regval,empty_val = self.regularise_data(value)
|
|
604
|
+
pure_string = check_stringiness(regval)
|
|
605
|
+
self.check_item_value(regval)
|
|
606
|
+
else:
|
|
607
|
+
regval,empty_val = value,None
|
|
608
|
+
pure_string = True
|
|
609
|
+
# update ancillary information first
|
|
610
|
+
lower_key = key.lower()
|
|
611
|
+
if not lower_key in self and self.FindLoop(lower_key)<0: #need to add to order
|
|
612
|
+
self.item_order.append(lower_key)
|
|
613
|
+
# always remove from our case table in case the case is different
|
|
614
|
+
try:
|
|
615
|
+
del self.true_case[lower_key]
|
|
616
|
+
except KeyError:
|
|
617
|
+
pass
|
|
618
|
+
self.true_case[lower_key] = key
|
|
619
|
+
if pure_string:
|
|
620
|
+
self.block.update({lower_key:[regval,empty_val]})
|
|
621
|
+
else:
|
|
622
|
+
self.block.update({lower_key:[empty_val,regval]})
|
|
623
|
+
|
|
624
|
+
def AddLoopItem(self,incomingdata,precheck=False,maxlength=-1):
|
|
625
|
+
"""*Deprecated*. Use `AddItem` followed by `CreateLoop` if
|
|
626
|
+
necessary."""
|
|
627
|
+
# print "Received data %s" % `incomingdata`
|
|
628
|
+
# we accept tuples, strings, lists and dicts!!
|
|
629
|
+
# Direct insertion: we have a string-valued key, with an array
|
|
630
|
+
# of values -> single-item into our loop
|
|
631
|
+
if isinstance(incomingdata[0],(tuple,list)):
|
|
632
|
+
# a whole loop
|
|
633
|
+
keyvallist = zip(incomingdata[0],incomingdata[1])
|
|
634
|
+
for key,value in keyvallist:
|
|
635
|
+
self.AddItem(key,value)
|
|
636
|
+
self.CreateLoop(incomingdata[0])
|
|
637
|
+
elif not isinstance(incomingdata[0],(unicode,str)):
|
|
638
|
+
raise TypeError('Star datanames are strings only (got %s)' % repr( incomingdata[0] ))
|
|
639
|
+
else:
|
|
640
|
+
self.AddItem(incomingdata[0],incomingdata[1])
|
|
641
|
+
|
|
642
|
+
def check_data_name(self,dataname,maxlength=-1):
|
|
643
|
+
if maxlength > 0:
|
|
644
|
+
self.check_name_length(dataname,maxlength)
|
|
645
|
+
if dataname[0]!='_':
|
|
646
|
+
raise StarError( 'Dataname ' + dataname + ' does not begin with _')
|
|
647
|
+
if self.characterset=='ascii':
|
|
648
|
+
if len ([a for a in dataname if ord(a) < 33 or ord(a) > 126]) > 0:
|
|
649
|
+
raise StarError( 'Dataname ' + dataname + ' contains forbidden characters')
|
|
650
|
+
else:
|
|
651
|
+
# print 'Checking %s for unicode characterset conformance' % dataname
|
|
652
|
+
if len ([a for a in dataname if ord(a) < 33]) > 0:
|
|
653
|
+
raise StarError( 'Dataname ' + dataname + ' contains forbidden characters (below code point 33)')
|
|
654
|
+
if len ([a for a in dataname if ord(a) > 126 and ord(a) < 160]) > 0:
|
|
655
|
+
raise StarError( 'Dataname ' + dataname + ' contains forbidden characters (between code point 127-159)')
|
|
656
|
+
if len ([a for a in dataname if ord(a) > 0xD7FF and ord(a) < 0xE000]) > 0:
|
|
657
|
+
raise StarError( 'Dataname ' + dataname + ' contains unsupported characters (between U+D800 and U+E000)')
|
|
658
|
+
if len ([a for a in dataname if ord(a) > 0xFDCF and ord(a) < 0xFDF0]) > 0:
|
|
659
|
+
raise StarError( 'Dataname ' + dataname + ' contains unsupported characters (between U+FDD0 and U+FDEF)')
|
|
660
|
+
if len ([a for a in dataname if ord(a) == 0xFFFE or ord(a) == 0xFFFF]) > 0:
|
|
661
|
+
raise StarError( 'Dataname ' + dataname + ' contains unsupported characters (U+FFFE and/or U+FFFF)')
|
|
662
|
+
if len ([a for a in dataname if ord(a) > 0x10000 and (ord(a) & 0xE == 0xE)]) > 0:
|
|
663
|
+
print('%s fails' % dataname)
|
|
664
|
+
for a in dataname: print('%x' % ord(a),end="")
|
|
665
|
+
print()
|
|
666
|
+
raise StarError( u'Dataname ' + dataname + u' contains unsupported characters (U+xFFFE and/or U+xFFFF)')
|
|
667
|
+
|
|
668
|
+
def check_name_length(self,dataname,maxlength):
|
|
669
|
+
if len(dataname)>maxlength:
|
|
670
|
+
raise StarError( 'Dataname %s exceeds maximum length %d' % (dataname,maxlength))
|
|
671
|
+
return
|
|
672
|
+
|
|
673
|
+
def check_item_value(self,item):
|
|
674
|
+
test_item = item
|
|
675
|
+
if not isinstance(item,(list,dict,tuple)):
|
|
676
|
+
test_item = [item] #single item list
|
|
677
|
+
def check_one (it):
|
|
678
|
+
if isinstance(it,unicode):
|
|
679
|
+
if it=='': return
|
|
680
|
+
me = self.char_check.match(it)
|
|
681
|
+
if not me:
|
|
682
|
+
print("Fail value check: %s" % it)
|
|
683
|
+
raise StarError('Bad character in %s' % it)
|
|
684
|
+
else:
|
|
685
|
+
if me.span() != (0,len(it)):
|
|
686
|
+
print("Fail value check, match only %d-%d in string %s" % (me.span()[0],me.span()[1],repr( it )))
|
|
687
|
+
raise StarError('Data item "' + repr( it ) + u'"... contains forbidden characters')
|
|
688
|
+
[check_one(a) for a in test_item]
|
|
689
|
+
|
|
690
|
+
def regularise_data(self,dataitem):
|
|
691
|
+
"""Place dataitem into a list if necessary"""
|
|
692
|
+
from numbers import Number
|
|
693
|
+
if isinstance(dataitem,str):
|
|
694
|
+
return unicode(dataitem),None
|
|
695
|
+
if isinstance(dataitem,(Number,unicode,StarList,StarDict)):
|
|
696
|
+
return dataitem,None #assume StarList/StarDict contain unicode if necessary
|
|
697
|
+
if isinstance(dataitem,(tuple,list)):
|
|
698
|
+
v,s = zip(*list([self.regularise_data(a) for a in dataitem]))
|
|
699
|
+
return list(v),list(s)
|
|
700
|
+
#return dataitem,[None]*len(dataitem)
|
|
701
|
+
# so try to make into a list
|
|
702
|
+
try:
|
|
703
|
+
regval = list(dataitem)
|
|
704
|
+
except TypeError as value:
|
|
705
|
+
raise StarError( str(dataitem) + ' is wrong type for data value\n' )
|
|
706
|
+
v,s = zip(*list([self.regularise_data(a) for a in regval]))
|
|
707
|
+
return list(v),list(s)
|
|
708
|
+
|
|
709
|
+
def RemoveItem(self,itemname):
|
|
710
|
+
"""Remove `itemname` from the block."""
|
|
711
|
+
# first check any loops
|
|
712
|
+
loop_no = self.FindLoop(itemname)
|
|
713
|
+
testkey = itemname.lower()
|
|
714
|
+
if testkey in self:
|
|
715
|
+
del self.block[testkey]
|
|
716
|
+
del self.true_case[testkey]
|
|
717
|
+
# now remove from loop
|
|
718
|
+
if loop_no >= 0:
|
|
719
|
+
self.loops[loop_no].remove(testkey)
|
|
720
|
+
if len(self.loops[loop_no])==0:
|
|
721
|
+
del self.loops[loop_no]
|
|
722
|
+
self.item_order.remove(loop_no)
|
|
723
|
+
else: #will appear in order list
|
|
724
|
+
self.item_order.remove(testkey)
|
|
725
|
+
|
|
726
|
+
def RemoveLoopItem(self,itemname):
|
|
727
|
+
"""*Deprecated*. Use `RemoveItem` instead"""
|
|
728
|
+
self.RemoveItem(itemname)
|
|
729
|
+
|
|
730
|
+
def GetItemValue(self,itemname):
|
|
731
|
+
"""Return value of `itemname`. If `itemname` is looped, a list
|
|
732
|
+
of all values will be returned."""
|
|
733
|
+
return self.GetFullItemValue(itemname)[0]
|
|
734
|
+
|
|
735
|
+
def GetFullItemValue(self,itemname):
|
|
736
|
+
"""Return the value associated with `itemname`, and a boolean flagging whether
|
|
737
|
+
(True) or not (False) it is in a form suitable for calculation. False is
|
|
738
|
+
always returned for strings and `StarList` objects."""
|
|
739
|
+
try:
|
|
740
|
+
s,v = self.block[itemname.lower()]
|
|
741
|
+
except KeyError:
|
|
742
|
+
raise KeyError('Itemname %s not in datablock' % itemname)
|
|
743
|
+
# prefer string value unless all are None
|
|
744
|
+
# are we a looped value?
|
|
745
|
+
if not isinstance(s,(tuple,list)) or isinstance(s,StarList):
|
|
746
|
+
if not_none(s):
|
|
747
|
+
return s,False #a string value
|
|
748
|
+
else:
|
|
749
|
+
return v,not isinstance(v,StarList) #a StarList is not calculation-ready
|
|
750
|
+
elif not_none(s):
|
|
751
|
+
return s,False #a list of string values
|
|
752
|
+
else:
|
|
753
|
+
if len(v)>0:
|
|
754
|
+
return v,not isinstance(v[0],StarList)
|
|
755
|
+
return v,True
|
|
756
|
+
|
|
757
|
+
def CreateLoop(self,datanames,order=-1,length_check=True):
|
|
758
|
+
"""Create a loop in the datablock. `datanames` is a list of datanames that
|
|
759
|
+
together form a loop. If length_check is True, they should have been initialised in the block
|
|
760
|
+
to have the same number of elements (possibly 0). If `order` is given,
|
|
761
|
+
the loop will appear at this position in the block when printing
|
|
762
|
+
out. A single-row loop will be created if the provided datanames are all
|
|
763
|
+
non-lists. A loop counts as a single position."""
|
|
764
|
+
|
|
765
|
+
if length_check:
|
|
766
|
+
# check lengths: these datanames should exist
|
|
767
|
+
listed_values = [a for a in datanames if isinstance(self[a],list) and not isinstance(self[a],StarList)]
|
|
768
|
+
if len(listed_values) == len(datanames):
|
|
769
|
+
len_set = set([len(self[a]) for a in datanames])
|
|
770
|
+
if len(len_set)>1:
|
|
771
|
+
raise ValueError('Request to loop datanames %s with different lengths: %s' % (repr( datanames ),repr( len_set )))
|
|
772
|
+
elif len(listed_values) != 0:
|
|
773
|
+
raise ValueError('Request to loop datanames where some are single values and some are not')
|
|
774
|
+
else: #all are unlisted, turn into lists
|
|
775
|
+
for d in datanames:
|
|
776
|
+
self[d] = [self[d]]
|
|
777
|
+
# store as lower case
|
|
778
|
+
lc_datanames = [d.lower() for d in datanames]
|
|
779
|
+
# remove these datanames from all other loops
|
|
780
|
+
[self.loops[a].remove(b) for a in self.loops for b in lc_datanames if b in self.loops[a]]
|
|
781
|
+
# remove empty loops
|
|
782
|
+
empty_loops = [a for a in self.loops.keys() if len(self.loops[a])==0]
|
|
783
|
+
for a in empty_loops:
|
|
784
|
+
self.item_order.remove(a)
|
|
785
|
+
del self.loops[a]
|
|
786
|
+
if len(self.loops)>0:
|
|
787
|
+
loopno = max(self.loops.keys()) + 1
|
|
788
|
+
else:
|
|
789
|
+
loopno = 1
|
|
790
|
+
self.loops[loopno] = list(lc_datanames)
|
|
791
|
+
if order >= 0:
|
|
792
|
+
self.item_order.insert(order,loopno)
|
|
793
|
+
else:
|
|
794
|
+
self.item_order.append(loopno)
|
|
795
|
+
# remove these datanames from item ordering
|
|
796
|
+
self.item_order = [a for a in self.item_order if a not in lc_datanames]
|
|
797
|
+
|
|
798
|
+
def AddLoopName(self,oldname, newname):
|
|
799
|
+
"""Add `newname` to the loop containing `oldname`. If it is already in the new loop, no
|
|
800
|
+
error is raised. If `newname` is in a different loop, it is removed from that loop.
|
|
801
|
+
The number of values associated with `newname` must match the number of values associated
|
|
802
|
+
with all other columns of the new loop or a `ValueError` will be raised."""
|
|
803
|
+
lower_newname = newname.lower()
|
|
804
|
+
loop_no = self.FindLoop(oldname)
|
|
805
|
+
if loop_no < 0:
|
|
806
|
+
raise KeyError('%s not in loop' % oldname)
|
|
807
|
+
if lower_newname in self.loops[loop_no]:
|
|
808
|
+
return
|
|
809
|
+
# check length
|
|
810
|
+
old_provides = self.provide_value
|
|
811
|
+
self.provide_value = False
|
|
812
|
+
loop_len = len(self[oldname])
|
|
813
|
+
self.provide_value = old_provides
|
|
814
|
+
if len(self[newname]) != loop_len:
|
|
815
|
+
raise StarLengthError('Mismatch of loop column lengths for %s: should be %d' % (newname,loop_len))
|
|
816
|
+
# remove from any other loops
|
|
817
|
+
[self.loops[a].remove(lower_newname) for a in self.loops if lower_newname in self.loops[a]]
|
|
818
|
+
# and add to this loop
|
|
819
|
+
self.loops[loop_no].append(lower_newname)
|
|
820
|
+
# remove from item_order if present
|
|
821
|
+
try:
|
|
822
|
+
self.item_order.remove(lower_newname)
|
|
823
|
+
except ValueError:
|
|
824
|
+
pass
|
|
825
|
+
|
|
826
|
+
def FindLoop(self,keyname):
|
|
827
|
+
"""Find the loop that contains `keyname` and return its numerical index or
|
|
828
|
+
-1 if not present. The numerical index can be used to refer to the loop in
|
|
829
|
+
other routines."""
|
|
830
|
+
loop_no = [a for a in self.loops.keys() if keyname.lower() in self.loops[a]]
|
|
831
|
+
if len(loop_no)>0:
|
|
832
|
+
return loop_no[0]
|
|
833
|
+
else:
|
|
834
|
+
return -1
|
|
835
|
+
|
|
836
|
+
def GetLoop(self,keyname):
|
|
837
|
+
"""Return a `StarFile.LoopBlock` object constructed from the loop containing `keyname`.
|
|
838
|
+
`keyname` is only significant as a way to specify the loop."""
|
|
839
|
+
return LoopBlock(self,keyname)
|
|
840
|
+
|
|
841
|
+
def GetLoopNames(self,keyname):
|
|
842
|
+
if keyname in self:
|
|
843
|
+
return self.keys()
|
|
844
|
+
for aloop in self.loops:
|
|
845
|
+
try:
|
|
846
|
+
return aloop.GetLoopNames(keyname)
|
|
847
|
+
except KeyError:
|
|
848
|
+
pass
|
|
849
|
+
raise KeyError('Item does not exist')
|
|
850
|
+
|
|
851
|
+
def GetLoopNames(self,keyname):
|
|
852
|
+
"""Return all datanames appearing together with `keyname`"""
|
|
853
|
+
loop_no = self.FindLoop(keyname)
|
|
854
|
+
if loop_no >= 0:
|
|
855
|
+
return self.loops[loop_no]
|
|
856
|
+
else:
|
|
857
|
+
raise KeyError('%s is not in any loop' % keyname)
|
|
858
|
+
|
|
859
|
+
def AddLoopName(self,oldname, newname):
|
|
860
|
+
"""Add `newname` to the loop containing `oldname`. If it is already in the new loop, no
|
|
861
|
+
error is raised. If `newname` is in a different loop, it is removed from that loop.
|
|
862
|
+
The number of values associated with `newname` must match the number of values associated
|
|
863
|
+
with all other columns of the new loop or a `ValueError` will be raised."""
|
|
864
|
+
lower_newname = newname.lower()
|
|
865
|
+
loop_no = self.FindLoop(oldname)
|
|
866
|
+
if loop_no < 0:
|
|
867
|
+
raise KeyError('%s not in loop' % oldname)
|
|
868
|
+
if lower_newname in self.loops[loop_no]:
|
|
869
|
+
return
|
|
870
|
+
# check length
|
|
871
|
+
old_provides = self.provide_value
|
|
872
|
+
self.provide_value = False
|
|
873
|
+
loop_len = len(self[oldname])
|
|
874
|
+
self.provide_value = old_provides
|
|
875
|
+
if len(self[newname]) != loop_len:
|
|
876
|
+
raise StarLengthError('Mismatch of loop column lengths for %s: should be %d' % (newname,loop_len))
|
|
877
|
+
# remove from any other loops
|
|
878
|
+
[self.loops[a].remove(lower_newname) for a in self.loops if lower_newname in self.loops[a]]
|
|
879
|
+
# and add to this loop
|
|
880
|
+
self.loops[loop_no].append(lower_newname)
|
|
881
|
+
# remove from item_order if present
|
|
882
|
+
try:
|
|
883
|
+
self.item_order.remove(lower_newname)
|
|
884
|
+
except ValueError:
|
|
885
|
+
pass
|
|
886
|
+
|
|
887
|
+
def AddToLoop(self,dataname,loopdata):
|
|
888
|
+
thisloop = self.GetLoop(dataname)
|
|
889
|
+
for itemname,itemvalue in loopdata.items():
|
|
890
|
+
thisloop[itemname] = itemvalue
|
|
891
|
+
|
|
892
|
+
def AddToLoop(self,dataname,loopdata):
|
|
893
|
+
"""*Deprecated*. Use `AddItem` followed by calls to `AddLoopName`.
|
|
894
|
+
|
|
895
|
+
Add multiple columns to the loop containing `dataname`. `loopdata` is a
|
|
896
|
+
collection of (key,value) pairs, where `key` is the new dataname and `value`
|
|
897
|
+
is a list of values for that dataname"""
|
|
898
|
+
self.update(loopdata)
|
|
899
|
+
for one_name in loopdata:
|
|
900
|
+
self.AddLoopName(dataname,one_name)
|
|
901
|
+
|
|
902
|
+
def RemoveKeyedPacket(self,keyname,keyvalue):
|
|
903
|
+
"""Remove the packet for which dataname `keyname` takes
|
|
904
|
+
value `keyvalue`. Only the first such occurrence is
|
|
905
|
+
removed."""
|
|
906
|
+
packet_coord = list(self[keyname]).index(keyvalue)
|
|
907
|
+
loopnames = self.GetLoopNames(keyname)
|
|
908
|
+
for dataname in loopnames:
|
|
909
|
+
self.block[dataname][0] = list(self.block[dataname][0])
|
|
910
|
+
del self.block[dataname][0][packet_coord]
|
|
911
|
+
self.block[dataname][1] = list(self.block[dataname][1])
|
|
912
|
+
del self.block[dataname][1][packet_coord]
|
|
913
|
+
|
|
914
|
+
def GetKeyedPacket(self,keyname,keyvalue,no_case=False):
|
|
915
|
+
"""Return the loop packet (a `StarPacket` object) where `keyname` has value
|
|
916
|
+
`keyvalue`. Ignore case in `keyvalue` if `no_case` is True. `ValueError`
|
|
917
|
+
is raised if no packet is found or more than one packet is found."""
|
|
918
|
+
my_loop = self.GetLoop(keyname)
|
|
919
|
+
#print("Looking for %s in %s" % (keyvalue, my_loop.parent_block))
|
|
920
|
+
#print('Packet check on:' + keyname)
|
|
921
|
+
#[print(repr(getattr(a,keyname))) for a in my_loop]
|
|
922
|
+
if no_case:
|
|
923
|
+
one_pack= [a for a in my_loop if getattr(a,keyname).lower()==keyvalue.lower()]
|
|
924
|
+
else:
|
|
925
|
+
one_pack= [a for a in my_loop if getattr(a,keyname)==keyvalue]
|
|
926
|
+
if len(one_pack)!=1:
|
|
927
|
+
raise ValueError("Bad packet key %s = %s: returned %d packets" % (keyname,keyvalue,len(one_pack)))
|
|
928
|
+
print("Keyed packet: %s" % one_pack[0])
|
|
929
|
+
return one_pack[0]
|
|
930
|
+
|
|
931
|
+
def GetCompoundKeyedPacket(self,keydict):
|
|
932
|
+
"""Return the loop packet (a `StarPacket` object) where the `{key:(value,caseless)}` pairs
|
|
933
|
+
in `keydict` take the appropriate values. Ignore case for a given `key` if `caseless` is
|
|
934
|
+
True. `ValueError` is raised if no packet is found or more than one packet is found."""
|
|
935
|
+
#print "Looking for %s in %s" % (keyvalue, self.parent_block[keyname])
|
|
936
|
+
keynames = list(keydict.keys())
|
|
937
|
+
my_loop = self.GetLoop(keynames[0])
|
|
938
|
+
for one_key in keynames:
|
|
939
|
+
keyval,no_case = keydict[one_key]
|
|
940
|
+
if no_case:
|
|
941
|
+
my_loop = list([a for a in my_loop if str(getattr(a,one_key)).lower()==str(keyval).lower()])
|
|
942
|
+
else:
|
|
943
|
+
my_loop = list([a for a in my_loop if getattr(a,one_key)==keyval])
|
|
944
|
+
if len(my_loop)!=1:
|
|
945
|
+
raise ValueError("Bad packet keys %s: returned %d packets" % (repr(keydict),len(my_loop)))
|
|
946
|
+
print("Compound keyed packet: %s" % my_loop[0])
|
|
947
|
+
return my_loop[0]
|
|
948
|
+
|
|
949
|
+
def GetKeyedSemanticPacket(self,keyvalue,cat_id):
|
|
950
|
+
"""Return a complete packet for category `cat_id` where the
|
|
951
|
+
category key for the category equals `keyvalue`. This routine
|
|
952
|
+
will understand any joined loops, so if separate loops in the
|
|
953
|
+
datafile belong to the
|
|
954
|
+
same category hierarchy (e.g. `_atom_site` and `_atom_site_aniso`),
|
|
955
|
+
the returned `StarPacket` object will contain datanames from
|
|
956
|
+
both categories."""
|
|
957
|
+
target_keys = self.dictionary.cat_key_table[cat_id]
|
|
958
|
+
target_keys = [k[0] for k in target_keys] #one only in each list
|
|
959
|
+
p = StarPacket()
|
|
960
|
+
# set case-sensitivity flag
|
|
961
|
+
lcase = False
|
|
962
|
+
if self.dictionary[target_keys[0]]['_type.contents'] in ['Code','Tag','Name']:
|
|
963
|
+
lcase = True
|
|
964
|
+
for cat_key in target_keys:
|
|
965
|
+
try:
|
|
966
|
+
extra_packet = self.GetKeyedPacket(cat_key,keyvalue,no_case=lcase)
|
|
967
|
+
except KeyError: #missing key
|
|
968
|
+
try:
|
|
969
|
+
test_key = self[cat_key] #generate key if possible
|
|
970
|
+
print('Test key is %s' % repr( test_key ))
|
|
971
|
+
if test_key is not None and\
|
|
972
|
+
not (isinstance(test_key,list) and (None in test_key or len(test_key)==0)):
|
|
973
|
+
print('Getting packet for key %s' % repr( keyvalue ))
|
|
974
|
+
extra_packet = self.GetKeyedPacket(cat_key,keyvalue,no_case=lcase)
|
|
975
|
+
except: #cannot be generated
|
|
976
|
+
continue
|
|
977
|
+
except ValueError: #none/more than one, assume none
|
|
978
|
+
continue
|
|
979
|
+
#extra_packet = self.dictionary.generate_default_packet(cat_id,cat_key,keyvalue)
|
|
980
|
+
p.merge_packet(extra_packet)
|
|
981
|
+
# the following attributes used to calculate missing values
|
|
982
|
+
for keyname in target_keys:
|
|
983
|
+
if hasattr(p,keyname):
|
|
984
|
+
p.key = [keyname]
|
|
985
|
+
break
|
|
986
|
+
if not hasattr(p,"key"):
|
|
987
|
+
raise ValueError("No key found for %s, packet is %s" % (cat_id,str(p)))
|
|
988
|
+
p.cif_dictionary = self.dictionary
|
|
989
|
+
p.fulldata = self
|
|
990
|
+
return p
|
|
991
|
+
|
|
992
|
+
def GetMultiKeyedSemanticPacket(self,keydict,cat_id):
|
|
993
|
+
"""Return a complete packet for category `cat_id` where the keyvalues are
|
|
994
|
+
provided as a dictionary of key:(value,caseless) pairs
|
|
995
|
+
This routine
|
|
996
|
+
will understand any joined loops, so if separate loops in the
|
|
997
|
+
datafile belong to the
|
|
998
|
+
same category hierarchy (e.g. `_atom_site` and `_atom_site_aniso`),
|
|
999
|
+
the returned `StarPacket` object will contain datanames from
|
|
1000
|
+
the requested category and any children."""
|
|
1001
|
+
#if len(keyvalues)==1: #simplification
|
|
1002
|
+
# return self.GetKeyedSemanticPacket(keydict[1][0],cat_id)
|
|
1003
|
+
target_keys = self.dictionary.cat_key_table[cat_id]
|
|
1004
|
+
# update the dictionary passed to us with all equivalents, for
|
|
1005
|
+
# simplicity.
|
|
1006
|
+
parallel_keys = list(zip(*target_keys)) #transpose
|
|
1007
|
+
print('Parallel keys:' + repr(parallel_keys))
|
|
1008
|
+
print('Keydict:' + repr(keydict))
|
|
1009
|
+
start_keys = list(keydict.keys())
|
|
1010
|
+
for one_name in start_keys:
|
|
1011
|
+
key_set = [a for a in parallel_keys if one_name in a]
|
|
1012
|
+
for one_key in key_set:
|
|
1013
|
+
keydict[one_key] = keydict[one_name]
|
|
1014
|
+
# target_keys is a list of lists, each of which is a compound key
|
|
1015
|
+
p = StarPacket()
|
|
1016
|
+
# a little function to return the dataname for a key
|
|
1017
|
+
def find_key(key):
|
|
1018
|
+
for one_key in self.dictionary.key_equivs.get(key,[])+[key]:
|
|
1019
|
+
if self.has_key(one_key):
|
|
1020
|
+
return one_key
|
|
1021
|
+
return None
|
|
1022
|
+
for one_set in target_keys: #loop down the categories
|
|
1023
|
+
true_keys = [find_key(k) for k in one_set]
|
|
1024
|
+
true_keys = [k for k in true_keys if k is not None]
|
|
1025
|
+
if len(true_keys)==len(one_set):
|
|
1026
|
+
truekeydict = dict([(t,keydict[k]) for t,k in zip(true_keys,one_set)])
|
|
1027
|
+
try:
|
|
1028
|
+
extra_packet = self.GetCompoundKeyedPacket(truekeydict)
|
|
1029
|
+
except KeyError: #one or more are missing
|
|
1030
|
+
continue #should try harder?
|
|
1031
|
+
except ValueError:
|
|
1032
|
+
continue
|
|
1033
|
+
else:
|
|
1034
|
+
continue
|
|
1035
|
+
print('Merging packet for keys ' + repr(one_set))
|
|
1036
|
+
p.merge_packet(extra_packet)
|
|
1037
|
+
# the following attributes used to calculate missing values
|
|
1038
|
+
p.key = true_keys
|
|
1039
|
+
p.cif_dictionary = self.dictionary
|
|
1040
|
+
p.fulldata = self
|
|
1041
|
+
return p
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
def set_grammar(self,new_grammar):
|
|
1045
|
+
self.string_delimiters = ["'",'"',"\n;",None]
|
|
1046
|
+
if new_grammar in ['STAR2','2.0']:
|
|
1047
|
+
self.string_delimiters += ['"""',"'''"]
|
|
1048
|
+
if new_grammar == '2.0':
|
|
1049
|
+
self.list_delimiter = " "
|
|
1050
|
+
elif new_grammar == 'STAR2':
|
|
1051
|
+
self.list_delimiter = ", "
|
|
1052
|
+
elif new_grammar not in ['1.0','1.1']:
|
|
1053
|
+
raise StarError('Request to set unknown grammar %s' % new_grammar)
|
|
1054
|
+
|
|
1055
|
+
def SetOutputLength(self,wraplength=80,maxoutlength=2048):
|
|
1056
|
+
"""Set the maximum output line length (`maxoutlength`) and the line length to
|
|
1057
|
+
wrap at (`wraplength`). The wrap length is a target only and may not always be
|
|
1058
|
+
possible."""
|
|
1059
|
+
if wraplength > maxoutlength:
|
|
1060
|
+
raise StarError("Wrap length (requested %d) must be <= Maximum line length (requested %d)" % (wraplength,maxoutlength))
|
|
1061
|
+
self.wraplength = wraplength
|
|
1062
|
+
self.maxoutlength = maxoutlength
|
|
1063
|
+
|
|
1064
|
+
def printsection(self,instring='',blockstart="",blockend="",indent=0,finish_at='',start_from=''):
|
|
1065
|
+
self.provide_value = False
|
|
1066
|
+
# first make an ordering
|
|
1067
|
+
self.create_ordering(finish_at,start_from) #create self.output_order
|
|
1068
|
+
# now do it...
|
|
1069
|
+
if not instring:
|
|
1070
|
+
outstring = CIFStringIO(target_width=80) # the returned string
|
|
1071
|
+
else:
|
|
1072
|
+
outstring = instring
|
|
1073
|
+
# print block delimiter
|
|
1074
|
+
outstring.write(blockstart,canbreak=True)
|
|
1075
|
+
while len(self.output_order)>0:
|
|
1076
|
+
#print "Remaining to output " + `self.output_order`
|
|
1077
|
+
itemname = self.output_order.pop(0)
|
|
1078
|
+
if not isinstance(itemname,int): #no loop
|
|
1079
|
+
item_spec = [i for i in self.formatting_hints if i['dataname'].lower()==itemname.lower()]
|
|
1080
|
+
if len(item_spec)>0:
|
|
1081
|
+
item_spec = item_spec[0]
|
|
1082
|
+
col_pos = item_spec.get('column',-1)
|
|
1083
|
+
name_pos = item_spec.get('name_pos',-1)
|
|
1084
|
+
else:
|
|
1085
|
+
col_pos = -1
|
|
1086
|
+
item_spec = {}
|
|
1087
|
+
name_pos = -1
|
|
1088
|
+
if col_pos < 0: col_pos = 40
|
|
1089
|
+
outstring.set_tab(col_pos)
|
|
1090
|
+
itemvalue = self[itemname]
|
|
1091
|
+
outstring.write(self.true_case[itemname],mustbreak=True,do_tab=False,startcol=name_pos)
|
|
1092
|
+
outstring.write(' ',canbreak=True,do_tab=False,delimiter=True) #space after itemname
|
|
1093
|
+
self.format_value(itemvalue,outstring,hints=item_spec)
|
|
1094
|
+
else:# we are asked to print a loop block
|
|
1095
|
+
outstring.set_tab(10) #guess this is OK?
|
|
1096
|
+
loop_spec = [i['name_pos'] for i in self.formatting_hints if i["dataname"]=='loop']
|
|
1097
|
+
if loop_spec:
|
|
1098
|
+
loop_indent = max(loop_spec[0],0)
|
|
1099
|
+
else:
|
|
1100
|
+
loop_indent = indent
|
|
1101
|
+
outstring.write('loop_\n',mustbreak=True,do_tab=False,startcol=loop_indent)
|
|
1102
|
+
self.format_names(outstring,indent+2,loop_no=itemname)
|
|
1103
|
+
self.format_packets(outstring,indent+2,loop_no=itemname)
|
|
1104
|
+
else:
|
|
1105
|
+
returnstring = outstring.getvalue()
|
|
1106
|
+
outstring.close()
|
|
1107
|
+
return returnstring
|
|
1108
|
+
|
|
1109
|
+
def format_names(self,outstring,indent=0,loop_no=-1):
|
|
1110
|
+
"""Print datanames from `loop_no` one per line"""
|
|
1111
|
+
temp_order = self.loops[loop_no][:] #copy
|
|
1112
|
+
format_hints = dict([(i['dataname'],i) for i in self.formatting_hints if i['dataname'] in temp_order])
|
|
1113
|
+
while len(temp_order)>0:
|
|
1114
|
+
itemname = temp_order.pop(0)
|
|
1115
|
+
req_indent = format_hints.get(itemname,{}).get('name_pos',indent)
|
|
1116
|
+
outstring.write(' ' * req_indent,do_tab=False)
|
|
1117
|
+
outstring.write(self.true_case[itemname],do_tab=False)
|
|
1118
|
+
outstring.write("\n",do_tab=False)
|
|
1119
|
+
|
|
1120
|
+
def format_packets(self,outstring,indent=0,loop_no=-1):
|
|
1121
|
+
alldata = [self[a] for a in self.loops[loop_no]]
|
|
1122
|
+
loopnames = self.loops[loop_no]
|
|
1123
|
+
#print 'Alldata: %s' % `alldata`
|
|
1124
|
+
packet_data = list(zip(*alldata))
|
|
1125
|
+
#print 'Packet data: %s' % `packet_data`
|
|
1126
|
+
#create a dictionary for quick lookup of formatting requirements
|
|
1127
|
+
format_hints = dict([(i['dataname'],i) for i in self.formatting_hints if i['dataname'] in loopnames])
|
|
1128
|
+
for position in range(len(packet_data)):
|
|
1129
|
+
if position > 0:
|
|
1130
|
+
outstring.write("\n") #new line each packet except first
|
|
1131
|
+
for point in range(len(packet_data[position])):
|
|
1132
|
+
datapoint = packet_data[position][point]
|
|
1133
|
+
format_hint = format_hints.get(loopnames[point],{})
|
|
1134
|
+
packstring = self.format_packet_item(datapoint,indent,outstring,format_hint)
|
|
1135
|
+
outstring.write(' ',canbreak=True,do_tab=False,delimiter=True)
|
|
1136
|
+
|
|
1137
|
+
def format_packet_item(self,pack_item,indent,outstring,format_hint):
|
|
1138
|
+
# print 'Formatting %s' % `pack_item`
|
|
1139
|
+
# temporary check for any non-unicode items
|
|
1140
|
+
if isinstance(pack_item,str) and not isinstance(pack_item,unicode):
|
|
1141
|
+
raise StarError("Item {0!r} is not unicode".format(pack_item))
|
|
1142
|
+
if isinstance(pack_item,unicode):
|
|
1143
|
+
delimiter = format_hint.get('delimiter',None)
|
|
1144
|
+
startcol = format_hint.get('column',-1)
|
|
1145
|
+
outstring.write(self._formatstring(pack_item,delimiter=delimiter),startcol=startcol)
|
|
1146
|
+
else:
|
|
1147
|
+
self.format_value(pack_item,outstring,hints = format_hint)
|
|
1148
|
+
|
|
1149
|
+
def _formatstring(self,instring,delimiter=None,standard='CIF1',indent=0,hints={}):
|
|
1150
|
+
if hints.get("reformat",False) and "\n" in instring:
|
|
1151
|
+
instring = "\n"+self.do_wrapping(instring,hints["reformat_indent"])
|
|
1152
|
+
allowed_delimiters = set(self.string_delimiters)
|
|
1153
|
+
if len(instring)==0: allowed_delimiters.difference_update([None])
|
|
1154
|
+
if len(instring) > (self.maxoutlength-2) or '\n' in instring:
|
|
1155
|
+
allowed_delimiters.intersection_update(["\n;","'''",'"""'])
|
|
1156
|
+
[allowed_delimiters.difference_update([None]) for k in '[]{}\v\t ,' if k in instring]
|
|
1157
|
+
if len(instring)>0 and instring[0] in '_$#;(':
|
|
1158
|
+
allowed_delimiters.difference_update([None])
|
|
1159
|
+
if len(instring)>3 and (instring[:4].lower()=='data' or instring[:4].lower()=='save'):
|
|
1160
|
+
allowed_delimiters.difference_update([None])
|
|
1161
|
+
if len(instring)>5 and instring[:6].lower()=='global':
|
|
1162
|
+
allowed_delimiters.difference_update([None])
|
|
1163
|
+
if '"' in instring: allowed_delimiters.difference_update(['"',None])
|
|
1164
|
+
if "'" in instring: allowed_delimiters.difference_update(["'",None])
|
|
1165
|
+
out_delimiter = "\n;" #default (most conservative)
|
|
1166
|
+
if delimiter in allowed_delimiters:
|
|
1167
|
+
out_delimiter = delimiter
|
|
1168
|
+
elif "'" in allowed_delimiters: out_delimiter = "'"
|
|
1169
|
+
elif '"' in allowed_delimiters: out_delimiter = '"'
|
|
1170
|
+
if out_delimiter in ['"',"'",'"""',"'''"]: return out_delimiter + instring + out_delimiter
|
|
1171
|
+
elif out_delimiter is None: return instring
|
|
1172
|
+
# we are left with semicolon strings
|
|
1173
|
+
# use our protocols:
|
|
1174
|
+
maxlinelength = max([len(a) for a in instring.split('\n')])
|
|
1175
|
+
if maxlinelength > self.maxoutlength:
|
|
1176
|
+
protocol_string = apply_line_folding(instring)
|
|
1177
|
+
else:
|
|
1178
|
+
protocol_string = instring
|
|
1179
|
+
# now check for embedded delimiters
|
|
1180
|
+
if "\n;" in protocol_string:
|
|
1181
|
+
prefix = "CIF:"
|
|
1182
|
+
while prefix in protocol_string: prefix = prefix + ":"
|
|
1183
|
+
protocol_string = apply_line_prefix(protocol_string,prefix+"> ")
|
|
1184
|
+
return "\n;" + protocol_string + "\n;"
|
|
1185
|
+
|
|
1186
|
+
def format_value(self,itemvalue,stringsink,compound=False,hints={}):
|
|
1187
|
+
"""Format a Star data value"""
|
|
1188
|
+
global have_numpy
|
|
1189
|
+
delimiter = hints.get('delimiter',None)
|
|
1190
|
+
startcol = hints.get('column',-1)
|
|
1191
|
+
if isinstance(itemvalue,str) and not isinstance(itemvalue,unicode): #not allowed
|
|
1192
|
+
raise StarError("Non-unicode value {0} found in block".format(itemvalue))
|
|
1193
|
+
if isinstance(itemvalue,unicode): #need to sanitize
|
|
1194
|
+
stringsink.write(self._formatstring(itemvalue,delimiter=delimiter,hints=hints),canbreak = True,startcol=startcol)
|
|
1195
|
+
elif isinstance(itemvalue,(list)) or (hasattr(itemvalue,'dtype') and hasattr(itemvalue,'__iter__')): #numpy
|
|
1196
|
+
stringsink.set_tab(0)
|
|
1197
|
+
stringsink.write('[',canbreak=True,newindent=True,mustbreak=compound,startcol=startcol)
|
|
1198
|
+
if len(itemvalue)>0:
|
|
1199
|
+
self.format_value(itemvalue[0],stringsink)
|
|
1200
|
+
for listval in itemvalue[1:]:
|
|
1201
|
+
# print 'Formatting %s' % `listval`
|
|
1202
|
+
stringsink.write(self.list_delimiter,do_tab=False)
|
|
1203
|
+
self.format_value(listval,stringsink,compound=True)
|
|
1204
|
+
stringsink.write(']',unindent=True)
|
|
1205
|
+
elif isinstance(itemvalue,dict):
|
|
1206
|
+
stringsink.set_tab(0)
|
|
1207
|
+
stringsink.write('{',newindent=True,mustbreak=compound,startcol=startcol) #start a new line inside
|
|
1208
|
+
items = list(itemvalue.items())
|
|
1209
|
+
if len(items)>0:
|
|
1210
|
+
stringsink.write("'"+items[0][0]+"'"+':',canbreak=True)
|
|
1211
|
+
self.format_value(items[0][1],stringsink)
|
|
1212
|
+
for key,value in items[1:]:
|
|
1213
|
+
stringsink.write(self.list_delimiter)
|
|
1214
|
+
stringsink.write("'"+key+"'"+":",canbreak=True)
|
|
1215
|
+
self.format_value(value,stringsink) #never break between key and value
|
|
1216
|
+
stringsink.write('}',unindent=True)
|
|
1217
|
+
elif isinstance(itemvalue,(float,int,long)) or \
|
|
1218
|
+
(have_numpy and isinstance(itemvalue,(numpy.number))): #TODO - handle uncertainties
|
|
1219
|
+
stringsink.write(str(itemvalue),canbreak=True,startcol=startcol) #numbers
|
|
1220
|
+
else:
|
|
1221
|
+
raise ValueError('Value in unexpected format for output: %s' % repr( itemvalue ))
|
|
1222
|
+
|
|
1223
|
+
def create_ordering(self,finish_at,start_from):
|
|
1224
|
+
"""Create a canonical ordering that includes loops using our formatting hints dictionary"""
|
|
1225
|
+
requested_order = list([i['dataname'] for i in self.formatting_hints if i['dataname']!='loop'])
|
|
1226
|
+
new_order = []
|
|
1227
|
+
for item in requested_order:
|
|
1228
|
+
if isinstance(item,unicode) and item.lower() in self.item_order:
|
|
1229
|
+
new_order.append(item.lower())
|
|
1230
|
+
elif item in self: #in a loop somewhere
|
|
1231
|
+
target_loop = self.FindLoop(item)
|
|
1232
|
+
if target_loop not in new_order:
|
|
1233
|
+
new_order.append(target_loop)
|
|
1234
|
+
# adjust loop name order
|
|
1235
|
+
loopnames = self.loops[target_loop]
|
|
1236
|
+
loop_order = [i for i in requested_order if i in loopnames]
|
|
1237
|
+
unordered = [i for i in loopnames if i not in loop_order]
|
|
1238
|
+
self.loops[target_loop] = loop_order + unordered
|
|
1239
|
+
extras = list([i for i in self.item_order if i not in new_order])
|
|
1240
|
+
self.output_order = new_order + extras
|
|
1241
|
+
# now handle partial output
|
|
1242
|
+
if start_from != '':
|
|
1243
|
+
if start_from in requested_order:
|
|
1244
|
+
sfi = requested_order.index(start_from)
|
|
1245
|
+
loop_order = [self.FindLoop(k) for k in requested_order[sfi:] if self.FindLoop(k)>0]
|
|
1246
|
+
candidates = list([k for k in self.output_order if k in requested_order[sfi:]])
|
|
1247
|
+
cand_pos = len(new_order)
|
|
1248
|
+
if len(candidates)>0:
|
|
1249
|
+
cand_pos = self.output_order.index(candidates[0])
|
|
1250
|
+
if len(loop_order)>0:
|
|
1251
|
+
cand_pos = min(cand_pos,self.output_order.index(loop_order[0]))
|
|
1252
|
+
if cand_pos < len(self.output_order):
|
|
1253
|
+
print('Output starts from %s, requested %s' % (self.output_order[cand_pos],start_from))
|
|
1254
|
+
self.output_order = self.output_order[cand_pos:]
|
|
1255
|
+
else:
|
|
1256
|
+
print('Start is beyond end of output list')
|
|
1257
|
+
self.output_order = []
|
|
1258
|
+
elif start_from in extras:
|
|
1259
|
+
self.output_order = self.output_order[self.output_order.index(start_from):]
|
|
1260
|
+
else:
|
|
1261
|
+
self.output_order = []
|
|
1262
|
+
if finish_at != '':
|
|
1263
|
+
if finish_at in requested_order:
|
|
1264
|
+
fai = requested_order.index(finish_at)
|
|
1265
|
+
loop_order = list([self.FindLoop(k) for k in requested_order[fai:] if self.FindLoop(k)>0])
|
|
1266
|
+
candidates = list([k for k in self.output_order if k in requested_order[fai:]])
|
|
1267
|
+
cand_pos = len(new_order)
|
|
1268
|
+
if len(candidates)>0:
|
|
1269
|
+
cand_pos = self.output_order.index(candidates[0])
|
|
1270
|
+
if len(loop_order)>0:
|
|
1271
|
+
cand_pos = min(cand_pos,self.output_order.index(loop_order[0]))
|
|
1272
|
+
if cand_pos < len(self.output_order):
|
|
1273
|
+
print('Output finishes before %s, requested before %s' % (self.output_order[cand_pos],finish_at))
|
|
1274
|
+
self.output_order = self.output_order[:cand_pos]
|
|
1275
|
+
else:
|
|
1276
|
+
print('All of block output')
|
|
1277
|
+
elif finish_at in extras:
|
|
1278
|
+
self.output_order = self.output_order[:self.output_order.index(finish_at)]
|
|
1279
|
+
#print('Final order: ' + repr(self.output_order))
|
|
1280
|
+
|
|
1281
|
+
def convert_to_string(self,dataname):
|
|
1282
|
+
"""Convert values held in dataname value fork to string version"""
|
|
1283
|
+
v,is_value = self.GetFullItemValue(dataname)
|
|
1284
|
+
if not is_value:
|
|
1285
|
+
return v
|
|
1286
|
+
if check_stringiness(v): return v #already strings
|
|
1287
|
+
# TODO...something else
|
|
1288
|
+
return v
|
|
1289
|
+
|
|
1290
|
+
def do_wrapping(self,instring,indent=3):
|
|
1291
|
+
"""Wrap the provided string"""
|
|
1292
|
+
if " " in instring: #already formatted
|
|
1293
|
+
return instring
|
|
1294
|
+
self.wrapper.initial_indent = ' '*indent
|
|
1295
|
+
self.wrapper.subsequent_indent = ' '*indent
|
|
1296
|
+
# remove leading and trailing space
|
|
1297
|
+
instring = instring.strip()
|
|
1298
|
+
# split into paragraphs
|
|
1299
|
+
paras = instring.split("\n\n")
|
|
1300
|
+
wrapped_paras = [self.wrapper.fill(p) for p in paras]
|
|
1301
|
+
return "\n".join(wrapped_paras)
|
|
1302
|
+
|
|
1303
|
+
|
|
1304
|
+
def merge(self,new_block,mode="strict",match_att=[],match_function=None,
|
|
1305
|
+
rel_keys = []):
|
|
1306
|
+
if mode == 'strict':
|
|
1307
|
+
for key in new_block.keys():
|
|
1308
|
+
if key in self and key not in match_att:
|
|
1309
|
+
raise StarError( "Identical keys %s in strict merge mode" % key)
|
|
1310
|
+
elif key not in match_att: #a new dataname
|
|
1311
|
+
self[key] = new_block[key]
|
|
1312
|
+
# we get here if there are no keys in common, so we can now copy
|
|
1313
|
+
# the loops and not worry about overlaps
|
|
1314
|
+
for one_loop in new_block.loops.values():
|
|
1315
|
+
self.CreateLoop(one_loop)
|
|
1316
|
+
# we have lost case information
|
|
1317
|
+
self.true_case.update(new_block.true_case)
|
|
1318
|
+
elif mode == 'replace':
|
|
1319
|
+
newkeys = list(new_block.keys())
|
|
1320
|
+
for ma in match_att:
|
|
1321
|
+
try:
|
|
1322
|
+
newkeys.remove(ma) #don't touch the special ones
|
|
1323
|
+
except ValueError:
|
|
1324
|
+
pass
|
|
1325
|
+
for key in new_block.keys():
|
|
1326
|
+
if isinstance(key,unicode):
|
|
1327
|
+
self[key] = new_block[key]
|
|
1328
|
+
# creating the loop will remove items from other loops
|
|
1329
|
+
for one_loop in new_block.loops.values():
|
|
1330
|
+
self.CreateLoop(one_loop)
|
|
1331
|
+
# we have lost case information
|
|
1332
|
+
self.true_case.update(new_block.true_case)
|
|
1333
|
+
elif mode == 'overlay':
|
|
1334
|
+
print('Overlay mode, current overwrite is %s' % self.overwrite)
|
|
1335
|
+
raise StarError('Overlay block merge mode not implemented')
|
|
1336
|
+
save_overwrite = self.overwrite
|
|
1337
|
+
self.overwrite = True
|
|
1338
|
+
for attribute in new_block.keys():
|
|
1339
|
+
if attribute in match_att: continue #ignore this one
|
|
1340
|
+
new_value = new_block[attribute]
|
|
1341
|
+
#non-looped items
|
|
1342
|
+
if new_block.FindLoop(attribute)<0: #not looped
|
|
1343
|
+
self[attribute] = new_value
|
|
1344
|
+
my_loops = self.loops.values()
|
|
1345
|
+
perfect_overlaps = [a for a in new_block.loops if a in my_loops]
|
|
1346
|
+
for po in perfect_overlaps:
|
|
1347
|
+
loop_keys = [a for a in po if a in rel_keys] #do we have a key?
|
|
1348
|
+
try:
|
|
1349
|
+
newkeypos = map(lambda a:newkeys.index(a),loop_keys)
|
|
1350
|
+
newkeypos = newkeypos[0] #one key per loop for now
|
|
1351
|
+
loop_keys = loop_keys[0]
|
|
1352
|
+
except (ValueError,IndexError):
|
|
1353
|
+
newkeypos = []
|
|
1354
|
+
overlap_data = map(lambda a:listify(self[a]),overlaps) #old packet data
|
|
1355
|
+
new_data = map(lambda a:new_block[a],overlaps) #new packet data
|
|
1356
|
+
packet_data = transpose(overlap_data)
|
|
1357
|
+
new_p_data = transpose(new_data)
|
|
1358
|
+
# remove any packets for which the keys match between old and new; we
|
|
1359
|
+
# make the arbitrary choice that the old data stays
|
|
1360
|
+
if newkeypos:
|
|
1361
|
+
# get matching values in new list
|
|
1362
|
+
print("Old, new data:\n%s\n%s" % (repr(overlap_data[newkeypos]),repr(new_data[newkeypos])))
|
|
1363
|
+
key_matches = filter(lambda a:a in overlap_data[newkeypos],new_data[newkeypos])
|
|
1364
|
+
# filter out any new data with these key values
|
|
1365
|
+
new_p_data = filter(lambda a:a[newkeypos] not in key_matches,new_p_data)
|
|
1366
|
+
if new_p_data:
|
|
1367
|
+
new_data = transpose(new_p_data)
|
|
1368
|
+
else: new_data = []
|
|
1369
|
+
# wipe out the old data and enter the new stuff
|
|
1370
|
+
byebyeloop = self.GetLoop(overlaps[0])
|
|
1371
|
+
# print("Removing '%r' with overlaps '%r'" % (byebyeloop, overlaps))
|
|
1372
|
+
# Note that if, in the original dictionary, overlaps are not
|
|
1373
|
+
# looped, GetLoop will return the block itself. So we check
|
|
1374
|
+
# for this case...
|
|
1375
|
+
if byebyeloop != self:
|
|
1376
|
+
self.remove_loop(byebyeloop)
|
|
1377
|
+
self.AddLoopItem((overlaps,overlap_data)) #adding old packets
|
|
1378
|
+
for pd in new_p_data: #adding new packets
|
|
1379
|
+
if pd not in packet_data:
|
|
1380
|
+
for i in range(len(overlaps)):
|
|
1381
|
+
#don't do this at home; we are appending
|
|
1382
|
+
#to something in place
|
|
1383
|
+
self[overlaps[i]].append(pd[i])
|
|
1384
|
+
self.overwrite = save_overwrite
|
|
1385
|
+
|
|
1386
|
+
def assign_dictionary(self,dic):
|
|
1387
|
+
if not dic.diclang=="DDLm":
|
|
1388
|
+
print("Warning: ignoring dictionary %s" % dic.my_uri)
|
|
1389
|
+
return
|
|
1390
|
+
self.dictionary = dic
|
|
1391
|
+
|
|
1392
|
+
def unassign_dictionary(self):
|
|
1393
|
+
"""Remove dictionary-dependent behaviour"""
|
|
1394
|
+
self.dictionary = None
|
|
1395
|
+
|
|
1396
|
+
|
|
1397
|
+
|
|
1398
|
+
class StarPacket(list):
|
|
1399
|
+
def merge_packet(self,incoming):
|
|
1400
|
+
"""Merge contents of incoming packet with this packet"""
|
|
1401
|
+
new_attrs = [a for a in dir(incoming) if a[0] == '_' and a[1] != "_"]
|
|
1402
|
+
self.extend(incoming)
|
|
1403
|
+
for na in new_attrs:
|
|
1404
|
+
setattr(self,na,getattr(incoming,na))
|
|
1405
|
+
|
|
1406
|
+
def __getattr__(self,att_name):
|
|
1407
|
+
"""Derive a missing attribute"""
|
|
1408
|
+
if att_name.lower() in self.__dict__:
|
|
1409
|
+
return getattr(self,att_name.lower())
|
|
1410
|
+
if att_name in ('cif_dictionary','fulldata','key'):
|
|
1411
|
+
raise AttributeError('Programming error: can only assign value of %s' % att_name)
|
|
1412
|
+
d = self.cif_dictionary
|
|
1413
|
+
c = self.fulldata
|
|
1414
|
+
k = self.key
|
|
1415
|
+
assert isinstance(k,list)
|
|
1416
|
+
d.derive_item(att_name,c,store_value=True)
|
|
1417
|
+
#
|
|
1418
|
+
# now pick out the new value
|
|
1419
|
+
# self.key is a list of the key values
|
|
1420
|
+
keydict = dict([(v,(getattr(self,v),True)) for v in k])
|
|
1421
|
+
full_pack = c.GetCompoundKeyedPacket(keydict)
|
|
1422
|
+
return getattr(full_pack,att_name)
|
|
1423
|
+
|
|
1424
|
+
class BlockCollection(object):
|
|
1425
|
+
"""A container for StarBlock objects. The constructor takes
|
|
1426
|
+
one non-keyword argument `datasource` to set the initial data. If
|
|
1427
|
+
`datasource` is a Python dictionary, the values must be `StarBlock`
|
|
1428
|
+
objects and the keys will be blocknames in the new object. Keyword
|
|
1429
|
+
arguments:
|
|
1430
|
+
|
|
1431
|
+
standard:
|
|
1432
|
+
`CIF` or `Dic`. `CIF` enforces 75-character blocknames, and will
|
|
1433
|
+
print block contents before that block's save frame.
|
|
1434
|
+
|
|
1435
|
+
blocktype:
|
|
1436
|
+
The type of blocks held in this container. Normally `StarBlock`
|
|
1437
|
+
or `CifBlock`.
|
|
1438
|
+
|
|
1439
|
+
characterset:
|
|
1440
|
+
`ascii` or `unicode`. Blocknames and datanames appearing within
|
|
1441
|
+
blocks are restricted to the appropriate characterset. Note that
|
|
1442
|
+
only characters in the basic multilingual plane are accepted. This
|
|
1443
|
+
restriction will be lifted when PyCIFRW is ported to Python3.
|
|
1444
|
+
|
|
1445
|
+
scoping:
|
|
1446
|
+
`instance` or `dictionary`: `instance` implies that save frames are
|
|
1447
|
+
hidden from save frames lower in the hierarchy or in sibling
|
|
1448
|
+
hierarchies. `dictionary` makes all save frames visible everywhere
|
|
1449
|
+
within a data block. This setting is only relevant for STAR2 dictionaries and
|
|
1450
|
+
STAR2 data files, as save frames are currently not used in plain CIF data
|
|
1451
|
+
files.
|
|
1452
|
+
|
|
1453
|
+
"""
|
|
1454
|
+
def __init__(self,datasource=None,standard='CIF',blocktype = StarBlock,
|
|
1455
|
+
characterset='ascii',scoping='instance',**kwargs):
|
|
1456
|
+
import collections
|
|
1457
|
+
self.dictionary = {}
|
|
1458
|
+
self.standard = standard
|
|
1459
|
+
self.lower_keys = set() # short_cuts
|
|
1460
|
+
self.renamed = {}
|
|
1461
|
+
self.PC = collections.namedtuple('PC',['block_id','parent'])
|
|
1462
|
+
self.child_table = {}
|
|
1463
|
+
self.visible_keys = [] # for efficiency
|
|
1464
|
+
self.block_input_order = [] # to output in same order
|
|
1465
|
+
self.scoping = scoping #will trigger setting of child table
|
|
1466
|
+
self.blocktype = blocktype
|
|
1467
|
+
self.master_template = {} #for outputting
|
|
1468
|
+
self.set_grammar('2.0')
|
|
1469
|
+
self.set_characterset(characterset)
|
|
1470
|
+
if isinstance(datasource,BlockCollection):
|
|
1471
|
+
self.merge_fast(datasource)
|
|
1472
|
+
self.scoping = scoping #reset visibility
|
|
1473
|
+
elif isinstance(datasource,dict):
|
|
1474
|
+
for key,value in datasource.items():
|
|
1475
|
+
self[key]= value
|
|
1476
|
+
self.header_comment = ''
|
|
1477
|
+
|
|
1478
|
+
def set_grammar(self,new_grammar):
|
|
1479
|
+
"""Set the syntax and grammar for output to `new_grammar`"""
|
|
1480
|
+
if new_grammar not in ['1.1','1.0','2.0','STAR2']:
|
|
1481
|
+
raise StarError('Unrecognised output grammar %s' % new_grammar)
|
|
1482
|
+
self.grammar = new_grammar
|
|
1483
|
+
|
|
1484
|
+
def set_characterset(self,characterset):
|
|
1485
|
+
"""Set the allowed characters for datanames and datablocks: may be `ascii` or `unicode`. If datanames
|
|
1486
|
+
have already been added to any datablocks, they are not checked."""
|
|
1487
|
+
self.characterset = characterset
|
|
1488
|
+
for one_block in self.lower_keys:
|
|
1489
|
+
self[one_block].set_characterset(characterset)
|
|
1490
|
+
|
|
1491
|
+
def unlock(self):
|
|
1492
|
+
"""Allow overwriting of all blocks in this collection"""
|
|
1493
|
+
for a in self.lower_keys:
|
|
1494
|
+
self[a].overwrite=True
|
|
1495
|
+
|
|
1496
|
+
def lock(self):
|
|
1497
|
+
"""Disallow overwriting for all blocks in this collection"""
|
|
1498
|
+
for a in self.lower_keys:
|
|
1499
|
+
self[a].overwrite = False
|
|
1500
|
+
|
|
1501
|
+
def __str__(self):
|
|
1502
|
+
return self.WriteOut()
|
|
1503
|
+
|
|
1504
|
+
def __setitem__(self,key,value):
|
|
1505
|
+
self.NewBlock(key,value,parent=None)
|
|
1506
|
+
|
|
1507
|
+
def __getitem__(self,key):
|
|
1508
|
+
if isinstance(key,(unicode,str)):
|
|
1509
|
+
lowerkey = key.lower()
|
|
1510
|
+
if lowerkey in self.lower_keys:
|
|
1511
|
+
return self.dictionary[lowerkey]
|
|
1512
|
+
#print 'Visible keys:' + `self.visible_keys`
|
|
1513
|
+
#print 'All keys' + `self.lower_keys`
|
|
1514
|
+
#print 'Child table' + `self.child_table`
|
|
1515
|
+
raise KeyError('No such item %s' % key)
|
|
1516
|
+
|
|
1517
|
+
# we have to get an ordered list of the current keys,
|
|
1518
|
+
# as we'll have to delete one of them anyway.
|
|
1519
|
+
# Deletion will delete any key regardless of visibility
|
|
1520
|
+
|
|
1521
|
+
def __delitem__(self,key):
|
|
1522
|
+
dummy = self[key] #raise error if not present
|
|
1523
|
+
lowerkey = key.lower()
|
|
1524
|
+
# get rid of all children recursively as well
|
|
1525
|
+
children = [a[0] for a in self.child_table.items() if a[1].parent == lowerkey]
|
|
1526
|
+
for child in children:
|
|
1527
|
+
del self[child] #recursive call
|
|
1528
|
+
del self.dictionary[lowerkey]
|
|
1529
|
+
del self.child_table[lowerkey]
|
|
1530
|
+
try:
|
|
1531
|
+
self.visible_keys.remove(lowerkey)
|
|
1532
|
+
except KeyError:
|
|
1533
|
+
pass
|
|
1534
|
+
self.lower_keys.remove(lowerkey)
|
|
1535
|
+
self.block_input_order.remove(lowerkey)
|
|
1536
|
+
|
|
1537
|
+
def __len__(self):
|
|
1538
|
+
return len(self.visible_keys)
|
|
1539
|
+
|
|
1540
|
+
def __contains__(self,item):
|
|
1541
|
+
"""Support the 'in' operator"""
|
|
1542
|
+
if not isinstance(item,(unicode,str)): return False
|
|
1543
|
+
if item.lower() in self.visible_keys:
|
|
1544
|
+
return True
|
|
1545
|
+
return False
|
|
1546
|
+
|
|
1547
|
+
# We iterate over all visible
|
|
1548
|
+
def __iter__(self):
|
|
1549
|
+
for one_block in self.keys():
|
|
1550
|
+
yield self[one_block]
|
|
1551
|
+
|
|
1552
|
+
# TODO: handle different case
|
|
1553
|
+
def keys(self):
|
|
1554
|
+
return self.visible_keys
|
|
1555
|
+
|
|
1556
|
+
# Note that has_key does not exist in 3.5
|
|
1557
|
+
def has_key(self,key):
|
|
1558
|
+
return key in self
|
|
1559
|
+
|
|
1560
|
+
def get(self,key,default=None):
|
|
1561
|
+
if key in self: # take account of case
|
|
1562
|
+
return self.__getitem__(key)
|
|
1563
|
+
else:
|
|
1564
|
+
return default
|
|
1565
|
+
|
|
1566
|
+
def clear(self):
|
|
1567
|
+
self.dictionary.clear()
|
|
1568
|
+
self.lower_keys = set()
|
|
1569
|
+
self.child_table = {}
|
|
1570
|
+
self.visible_keys = []
|
|
1571
|
+
self.block_input_order = []
|
|
1572
|
+
|
|
1573
|
+
def copy(self):
|
|
1574
|
+
newcopy = self.dictionary.copy() #all blocks
|
|
1575
|
+
for k,v in self.dictionary.items():
|
|
1576
|
+
newcopy[k] = v.copy()
|
|
1577
|
+
newcopy = BlockCollection(newcopy)
|
|
1578
|
+
newcopy.child_table = self.child_table.copy()
|
|
1579
|
+
newcopy.lower_keys = self.lower_keys.copy()
|
|
1580
|
+
newcopy.block_input_order = self.block_input_order.copy()
|
|
1581
|
+
newcopy.characterset = self.characterset
|
|
1582
|
+
newcopy.SetTemplate(self.master_template.copy())
|
|
1583
|
+
newcopy.scoping = self.scoping #this sets visible keys
|
|
1584
|
+
return newcopy
|
|
1585
|
+
|
|
1586
|
+
def update(self,adict):
|
|
1587
|
+
for key in adict.keys():
|
|
1588
|
+
self[key] = adict[key]
|
|
1589
|
+
|
|
1590
|
+
def items(self):
|
|
1591
|
+
return [(a,self[a]) for a in self.keys()]
|
|
1592
|
+
|
|
1593
|
+
def first_block(self):
|
|
1594
|
+
"""Return the 'first' block. This is not necessarily the first block in the file."""
|
|
1595
|
+
if self.keys():
|
|
1596
|
+
return self[self.keys()[0]]
|
|
1597
|
+
|
|
1598
|
+
def NewBlock(self,blockname,blockcontents=None,fix=True,parent=None):
|
|
1599
|
+
"""Add a new block named `blockname` with contents `blockcontents`. If `fix`
|
|
1600
|
+
is True, `blockname` will have spaces and tabs replaced by underscores. `parent`
|
|
1601
|
+
allows a parent block to be set so that block hierarchies can be created. Depending on
|
|
1602
|
+
the output standard, these blocks will be printed out as nested save frames or
|
|
1603
|
+
ignored."""
|
|
1604
|
+
if blockcontents is None:
|
|
1605
|
+
blockcontents = self.blocktype()
|
|
1606
|
+
if self.standard == "CIF":
|
|
1607
|
+
blockcontents.setmaxnamelength(75)
|
|
1608
|
+
if len(blockname)>75:
|
|
1609
|
+
raise StarError('Blockname %s is longer than 75 characters' % blockname)
|
|
1610
|
+
if fix:
|
|
1611
|
+
newblockname = re.sub('[ \t]','_',blockname)
|
|
1612
|
+
else: newblockname = blockname
|
|
1613
|
+
new_lowerbn = newblockname.lower()
|
|
1614
|
+
if new_lowerbn in self.lower_keys: #already there
|
|
1615
|
+
if self.standard is not None:
|
|
1616
|
+
toplevelnames = [a[0] for a in self.child_table.items() if a[1].parent==None]
|
|
1617
|
+
if parent is None and new_lowerbn not in toplevelnames: #can give a new key to this one
|
|
1618
|
+
while new_lowerbn in self.lower_keys: new_lowerbn = new_lowerbn + '+'
|
|
1619
|
+
elif parent is not None and new_lowerbn in toplevelnames: #can fix a different one
|
|
1620
|
+
replace_name = new_lowerbn
|
|
1621
|
+
while replace_name in self.lower_keys: replace_name = replace_name + '+'
|
|
1622
|
+
self._rekey(new_lowerbn,replace_name)
|
|
1623
|
+
# now continue on to add in the new block
|
|
1624
|
+
if parent.lower() == new_lowerbn: #the new block's requested parent just got renamed!!
|
|
1625
|
+
parent = replace_name
|
|
1626
|
+
else:
|
|
1627
|
+
raise StarError( "Attempt to replace existing block " + blockname)
|
|
1628
|
+
else:
|
|
1629
|
+
del self[new_lowerbn]
|
|
1630
|
+
self.dictionary.update({new_lowerbn:blockcontents})
|
|
1631
|
+
self.lower_keys.add(new_lowerbn)
|
|
1632
|
+
self.block_input_order.append(new_lowerbn)
|
|
1633
|
+
if parent is None:
|
|
1634
|
+
self.child_table[new_lowerbn]=self.PC(newblockname,None)
|
|
1635
|
+
self.visible_keys.append(new_lowerbn)
|
|
1636
|
+
else:
|
|
1637
|
+
if parent.lower() in self.lower_keys:
|
|
1638
|
+
if self.scoping == 'instance':
|
|
1639
|
+
self.child_table[new_lowerbn]=self.PC(newblockname,parent.lower())
|
|
1640
|
+
else:
|
|
1641
|
+
self.child_table[new_lowerbn]=self.PC(newblockname,parent.lower())
|
|
1642
|
+
self.visible_keys.append(new_lowerbn)
|
|
1643
|
+
else:
|
|
1644
|
+
print('Warning:Parent block %s does not exist for child %s' % (parent,newblockname))
|
|
1645
|
+
self[new_lowerbn].set_grammar(self.grammar)
|
|
1646
|
+
self[new_lowerbn].set_characterset(self.characterset)
|
|
1647
|
+
self[new_lowerbn].formatting_hints = self.master_template
|
|
1648
|
+
return new_lowerbn #in case calling routine wants to know
|
|
1649
|
+
|
|
1650
|
+
def _rekey(self,oldname,newname,block_id=''):
|
|
1651
|
+
"""The block with key [[oldname]] gets [[newname]] as a new key, but the printed name
|
|
1652
|
+
does not change unless [[block_id]] is given. Prefer [[rename]] for a safe version."""
|
|
1653
|
+
move_block = self[oldname] #old block
|
|
1654
|
+
is_visible = oldname in self.visible_keys
|
|
1655
|
+
move_block_info = self.child_table[oldname] #old info
|
|
1656
|
+
move_block_children = [a for a in self.child_table.items() if a[1].parent==oldname]
|
|
1657
|
+
# now rewrite the necessary bits
|
|
1658
|
+
self.child_table.update(dict([(a[0],self.PC(a[1].block_id,newname)) for a in move_block_children]))
|
|
1659
|
+
oldpos = self.block_input_order.index(oldname)
|
|
1660
|
+
del self[oldname] #do this after updating child table so we don't delete children
|
|
1661
|
+
self.dictionary.update({newname:move_block})
|
|
1662
|
+
self.lower_keys.add(newname)
|
|
1663
|
+
#print 'Block input order was: ' + `self.block_input_order`
|
|
1664
|
+
self.block_input_order[oldpos:oldpos]=[newname]
|
|
1665
|
+
if block_id == '':
|
|
1666
|
+
self.child_table.update({newname:move_block_info})
|
|
1667
|
+
else:
|
|
1668
|
+
self.child_table.update({newname:self.PC(block_id,move_block_info.parent)})
|
|
1669
|
+
if is_visible: self.visible_keys += [newname]
|
|
1670
|
+
|
|
1671
|
+
def rename(self,oldname,newname):
|
|
1672
|
+
"""Rename datablock from [[oldname]] to [[newname]]. Both key and printed name are changed. No
|
|
1673
|
+
conformance checks are conducted."""
|
|
1674
|
+
realoldname = oldname.lower()
|
|
1675
|
+
realnewname = newname.lower()
|
|
1676
|
+
if realnewname in self.lower_keys:
|
|
1677
|
+
raise StarError('Cannot change blockname %s to %s as %s already present' % (oldname,newname,newname))
|
|
1678
|
+
if realoldname not in self.lower_keys:
|
|
1679
|
+
raise KeyError('Cannot find old block %s' % realoldname)
|
|
1680
|
+
self._rekey(realoldname,realnewname,block_id=newname)
|
|
1681
|
+
|
|
1682
|
+
def makebc(self,namelist,scoping='dictionary'):
|
|
1683
|
+
"""Make a block collection from a list of block names"""
|
|
1684
|
+
newbc = BlockCollection()
|
|
1685
|
+
block_lower = [n.lower() for n in namelist]
|
|
1686
|
+
proto_child_table = [a for a in self.child_table.items() if a[0] in block_lower]
|
|
1687
|
+
newbc.child_table = dict(proto_child_table)
|
|
1688
|
+
new_top_level = [(a[0],self.PC(a[1].block_id,None)) for a in newbc.child_table.items() if a[1].parent not in block_lower]
|
|
1689
|
+
newbc.child_table.update(dict(new_top_level))
|
|
1690
|
+
newbc.lower_keys = set([a[0] for a in proto_child_table])
|
|
1691
|
+
newbc.dictionary = dict((a[0],self.dictionary[a[0]]) for a in proto_child_table)
|
|
1692
|
+
newbc.scoping = scoping
|
|
1693
|
+
newbc.block_input_order = block_lower
|
|
1694
|
+
return newbc
|
|
1695
|
+
|
|
1696
|
+
|
|
1697
|
+
def merge_fast(self,new_bc,parent=None):
|
|
1698
|
+
"""Do a fast merge. WARNING: this may change one or more of its frame headers in order to
|
|
1699
|
+
remove duplicate frames. Please keep a handle to the block object instead of the text of
|
|
1700
|
+
the header."""
|
|
1701
|
+
if self.standard is None:
|
|
1702
|
+
mode = 'replace'
|
|
1703
|
+
else:
|
|
1704
|
+
mode = 'strict'
|
|
1705
|
+
overlap_flag = not self.lower_keys.isdisjoint(new_bc.lower_keys)
|
|
1706
|
+
if parent is not None:
|
|
1707
|
+
parent_name = [a[0] for a in self.dictionary.items() if a[1] == parent]
|
|
1708
|
+
if len(parent_name)==0 or len(parent_name)>1:
|
|
1709
|
+
raise StarError("Unable to find unique parent block name: have %s" % str(parent_name))
|
|
1710
|
+
parent_name = parent_name[0]
|
|
1711
|
+
else:
|
|
1712
|
+
parent_name = None #an error will be thrown if we treat as a string
|
|
1713
|
+
if overlap_flag and mode != 'replace':
|
|
1714
|
+
double_keys = self.lower_keys.intersection(new_bc.lower_keys)
|
|
1715
|
+
for dup_key in double_keys:
|
|
1716
|
+
our_parent = self.child_table[dup_key].parent
|
|
1717
|
+
their_parent = new_bc.child_table[dup_key].parent
|
|
1718
|
+
if (our_parent is None and their_parent is not None and parent is None) or\
|
|
1719
|
+
parent is not None: #rename our block
|
|
1720
|
+
start_key = dup_key
|
|
1721
|
+
while start_key in self.lower_keys: start_key = start_key+'+'
|
|
1722
|
+
self._rekey(dup_key,start_key)
|
|
1723
|
+
if parent_name.lower() == dup_key: #we just renamed the prospective parent!
|
|
1724
|
+
parent_name = start_key
|
|
1725
|
+
elif our_parent is not None and their_parent is None and parent is None:
|
|
1726
|
+
start_key = dup_key
|
|
1727
|
+
while start_key in new_bc.lower_keys: start_key = start_key+'+'
|
|
1728
|
+
new_bc._rekey(dup_key,start_key)
|
|
1729
|
+
else:
|
|
1730
|
+
raise StarError("In strict merge mode:duplicate keys %s" % dup_key)
|
|
1731
|
+
self.dictionary.update(new_bc.dictionary)
|
|
1732
|
+
self.lower_keys.update(new_bc.lower_keys)
|
|
1733
|
+
self.visible_keys += (list(new_bc.lower_keys))
|
|
1734
|
+
self.block_input_order += new_bc.block_input_order
|
|
1735
|
+
#print('Block input order now:' + repr(self.block_input_order))
|
|
1736
|
+
self.child_table.update(new_bc.child_table)
|
|
1737
|
+
if parent_name is not None: #redo the child_table entries
|
|
1738
|
+
reparent_list = [(a[0],a[1].block_id) for a in new_bc.child_table.items() if a[1].parent==None]
|
|
1739
|
+
reparent_dict = [(a[0],self.PC(a[1],parent_name.lower())) for a in reparent_list]
|
|
1740
|
+
self.child_table.update(dict(reparent_dict))
|
|
1741
|
+
|
|
1742
|
+
def merge(self,new_bc,mode=None,parent=None,single_block=[],
|
|
1743
|
+
idblock="",match_att=[],match_function=None):
|
|
1744
|
+
if mode is None:
|
|
1745
|
+
if self.standard is None:
|
|
1746
|
+
mode = 'replace'
|
|
1747
|
+
else:
|
|
1748
|
+
mode = 'strict'
|
|
1749
|
+
if single_block:
|
|
1750
|
+
self[single_block[0]].merge(new_bc[single_block[1]],mode,
|
|
1751
|
+
match_att=match_att,
|
|
1752
|
+
match_function=match_function)
|
|
1753
|
+
return None
|
|
1754
|
+
base_keys = [a[1].block_id for a in self.child_table.items()]
|
|
1755
|
+
block_to_item = base_keys #default
|
|
1756
|
+
new_keys = [a[1].block_id for a in new_bc.child_table.items()] #get list of incoming blocks
|
|
1757
|
+
if match_att:
|
|
1758
|
+
#make a blockname -> item name map
|
|
1759
|
+
if match_function:
|
|
1760
|
+
block_to_item = [match_function(self[a]) for a in self.keys()]
|
|
1761
|
+
else:
|
|
1762
|
+
block_to_item = [self[a].get(match_att[0],None) for a in self.keys()]
|
|
1763
|
+
#print `block_to_item`
|
|
1764
|
+
for key in new_keys: #run over incoming blocknames
|
|
1765
|
+
if key == idblock: continue #skip dictionary id
|
|
1766
|
+
basekey = key #default value
|
|
1767
|
+
if len(match_att)>0:
|
|
1768
|
+
attval = new_bc[key].get(match_att[0],0) #0 if ignoring matching
|
|
1769
|
+
else:
|
|
1770
|
+
attval = 0
|
|
1771
|
+
for ii in range(len(block_to_item)): #do this way to get looped names
|
|
1772
|
+
thisatt = block_to_item[ii] #keyname in old block
|
|
1773
|
+
#print "Looking for %s in %s" % (attval,thisatt)
|
|
1774
|
+
if attval == thisatt or \
|
|
1775
|
+
(isinstance(thisatt,list) and attval in thisatt):
|
|
1776
|
+
basekey = base_keys.pop(ii)
|
|
1777
|
+
block_to_item.remove(thisatt)
|
|
1778
|
+
break
|
|
1779
|
+
if not basekey in self or mode=="replace":
|
|
1780
|
+
new_parent = new_bc.get_parent(key)
|
|
1781
|
+
if parent is not None and new_parent is None:
|
|
1782
|
+
new_parent = parent
|
|
1783
|
+
self.NewBlock(basekey,new_bc[key],parent=new_parent) #add the block
|
|
1784
|
+
else:
|
|
1785
|
+
if mode=="strict":
|
|
1786
|
+
raise StarError( "In strict merge mode: block %s in old and block %s in new files" % (basekey,key))
|
|
1787
|
+
elif mode=="overlay":
|
|
1788
|
+
# print "Merging block %s with %s" % (basekey,key)
|
|
1789
|
+
self[basekey].merge(new_bc[key],mode,match_att=match_att)
|
|
1790
|
+
else:
|
|
1791
|
+
raise StarError( "Merge called with unknown mode %s" % mode)
|
|
1792
|
+
|
|
1793
|
+
def checknamelengths(self,target_block,maxlength=-1):
|
|
1794
|
+
if maxlength < 0:
|
|
1795
|
+
return
|
|
1796
|
+
else:
|
|
1797
|
+
toolong = [a for a in target_block.keys() if len(a)>maxlength]
|
|
1798
|
+
outstring = ""
|
|
1799
|
+
if toolong:
|
|
1800
|
+
outstring = "\n".join(toolong)
|
|
1801
|
+
raise StarError( 'Following data names too long:' + outstring)
|
|
1802
|
+
|
|
1803
|
+
def get_all(self,item_name):
|
|
1804
|
+
raw_values = [self[a].get(item_name) for a in self.keys()]
|
|
1805
|
+
raw_values = [a for a in raw_values if a != None]
|
|
1806
|
+
ret_vals = []
|
|
1807
|
+
for rv in raw_values:
|
|
1808
|
+
if isinstance(rv,list):
|
|
1809
|
+
for rvv in rv:
|
|
1810
|
+
if rvv not in ret_vals: ret_vals.append(rvv)
|
|
1811
|
+
else:
|
|
1812
|
+
if rv not in ret_vals: ret_vals.append(rv)
|
|
1813
|
+
return ret_vals
|
|
1814
|
+
|
|
1815
|
+
def __setattr__(self,attr_name,newval):
|
|
1816
|
+
if attr_name == 'scoping':
|
|
1817
|
+
if newval not in ('dictionary','instance'):
|
|
1818
|
+
raise StarError("Star file may only have 'dictionary' or 'instance' scoping, not %s" % newval)
|
|
1819
|
+
if newval == 'dictionary':
|
|
1820
|
+
self.visible_keys = [a for a in self.lower_keys]
|
|
1821
|
+
else:
|
|
1822
|
+
#only top-level datablocks visible
|
|
1823
|
+
self.visible_keys = [a[0] for a in self.child_table.items() if a[1].parent==None]
|
|
1824
|
+
object.__setattr__(self,attr_name,newval)
|
|
1825
|
+
|
|
1826
|
+
def get_parent(self,blockname):
|
|
1827
|
+
"""Return the name of the block enclosing [[blockname]] in canonical form (lower case)"""
|
|
1828
|
+
possibles = (a for a in self.child_table.items() if a[0] == blockname.lower())
|
|
1829
|
+
try:
|
|
1830
|
+
first = next(possibles) #get first one
|
|
1831
|
+
except:
|
|
1832
|
+
raise StarError('no parent for %s' % blockname)
|
|
1833
|
+
try:
|
|
1834
|
+
second = next(possibles)
|
|
1835
|
+
except StopIteration:
|
|
1836
|
+
return first[1].parent
|
|
1837
|
+
raise StarError('More than one parent for %s' % blockname)
|
|
1838
|
+
|
|
1839
|
+
def get_roots(self):
|
|
1840
|
+
"""Get the top-level blocks"""
|
|
1841
|
+
return [a for a in self.child_table.items() if a[1].parent==None]
|
|
1842
|
+
|
|
1843
|
+
def get_children(self,blockname,include_parent=False,scoping='dictionary'):
|
|
1844
|
+
"""Get all children of [[blockname]] as a block collection. If [[include_parent]] is
|
|
1845
|
+
True, the parent block will also be included in the block collection as the root."""
|
|
1846
|
+
newbc = BlockCollection()
|
|
1847
|
+
block_lower = blockname.lower()
|
|
1848
|
+
proto_child_table = [a for a in self.child_table.items() if self.is_child_of_parent(block_lower,a[1].block_id)]
|
|
1849
|
+
newbc.child_table = dict(proto_child_table)
|
|
1850
|
+
if not include_parent:
|
|
1851
|
+
newbc.child_table.update(dict([(a[0],self.PC(a[1].block_id,None)) for a in proto_child_table if a[1].parent == block_lower]))
|
|
1852
|
+
newbc.lower_keys = set([a[0] for a in proto_child_table])
|
|
1853
|
+
newbc.dictionary = dict((a[0],self.dictionary[a[0]]) for a in proto_child_table)
|
|
1854
|
+
if include_parent:
|
|
1855
|
+
newbc.child_table.update({block_lower:self.PC(self.child_table[block_lower].block_id,None)})
|
|
1856
|
+
newbc.lower_keys.add(block_lower)
|
|
1857
|
+
newbc.dictionary.update({block_lower:self.dictionary[block_lower]})
|
|
1858
|
+
newbc.scoping = scoping
|
|
1859
|
+
return newbc
|
|
1860
|
+
|
|
1861
|
+
def get_immediate_children(self,parentname):
|
|
1862
|
+
"""Get the next level of children of the given block as a list, without nested levels"""
|
|
1863
|
+
child_handles = [a for a in self.child_table.items() if a[1].parent == parentname.lower()]
|
|
1864
|
+
return child_handles
|
|
1865
|
+
|
|
1866
|
+
# This takes time
|
|
1867
|
+
def get_child_list(self,parentname):
|
|
1868
|
+
"""Get a list of all child categories in alphabetical order"""
|
|
1869
|
+
child_handles = [a[0] for a in self.child_table.items() if self.is_child_of_parent(parentname.lower(),a[0])]
|
|
1870
|
+
child_handles.sort()
|
|
1871
|
+
return child_handles
|
|
1872
|
+
|
|
1873
|
+
def is_child_of_parent(self,parentname,blockname):
|
|
1874
|
+
"""Return `True` if `blockname` is a child of `parentname`"""
|
|
1875
|
+
checkname = parentname.lower()
|
|
1876
|
+
more_children = [a[0] for a in self.child_table.items() if a[1].parent == checkname]
|
|
1877
|
+
if blockname.lower() in more_children:
|
|
1878
|
+
return True
|
|
1879
|
+
else:
|
|
1880
|
+
for one_child in more_children:
|
|
1881
|
+
if self.is_child_of_parent(one_child,blockname): return True
|
|
1882
|
+
return False
|
|
1883
|
+
|
|
1884
|
+
def set_parent(self,parentname,childname):
|
|
1885
|
+
"""Set the parent block"""
|
|
1886
|
+
# first check that both blocks exist
|
|
1887
|
+
if parentname.lower() not in self.lower_keys:
|
|
1888
|
+
raise KeyError('Parent block %s does not exist' % parentname)
|
|
1889
|
+
if childname.lower() not in self.lower_keys:
|
|
1890
|
+
raise KeyError('Child block %s does not exist' % childname)
|
|
1891
|
+
old_entry = self.child_table[childname.lower()]
|
|
1892
|
+
self.child_table[childname.lower()]=self.PC(old_entry.block_id,
|
|
1893
|
+
parentname.lower())
|
|
1894
|
+
self.scoping = self.scoping #reset visibility
|
|
1895
|
+
|
|
1896
|
+
def SetTemplate(self,template_file):
|
|
1897
|
+
"""Use `template_file` as a template for all block output"""
|
|
1898
|
+
self.master_template = process_template(template_file)
|
|
1899
|
+
for b in self.dictionary.values():
|
|
1900
|
+
b.formatting_hints = self.master_template
|
|
1901
|
+
|
|
1902
|
+
def WriteOut(self,comment='',wraplength=80,maxoutlength=0,blockorder=None,saves_after=None):
|
|
1903
|
+
"""Return the contents of this file as a string, wrapping if possible at `wraplength`
|
|
1904
|
+
characters and restricting maximum line length to `maxoutlength`. Delimiters and
|
|
1905
|
+
save frame nesting are controlled by `self.grammar`. If `blockorder` is
|
|
1906
|
+
provided, blocks are output in this order unless nested save frames have been
|
|
1907
|
+
requested (STAR2). The default block order is the order in which blocks were input.
|
|
1908
|
+
`saves_after` inserts all save frames after the given dataname,
|
|
1909
|
+
which allows less important items to appear later. Useful in conjunction with a
|
|
1910
|
+
template for dictionary files."""
|
|
1911
|
+
if maxoutlength != 0:
|
|
1912
|
+
self.SetOutputLength(maxoutlength)
|
|
1913
|
+
if not comment:
|
|
1914
|
+
comment = self.header_comment
|
|
1915
|
+
outstring = StringIO()
|
|
1916
|
+
if self.grammar == "2.0" and comment[0:10] != r"#\#CIF_2.0":
|
|
1917
|
+
outstring.write(r"#\#CIF_2.0" + "\n")
|
|
1918
|
+
outstring.write(comment)
|
|
1919
|
+
# prepare all blocks
|
|
1920
|
+
for b in self.dictionary.values():
|
|
1921
|
+
b.set_grammar(self.grammar)
|
|
1922
|
+
b.formatting_hints = self.master_template
|
|
1923
|
+
b.SetOutputLength(wraplength,self.maxoutlength)
|
|
1924
|
+
# loop over top-level
|
|
1925
|
+
# monitor output
|
|
1926
|
+
all_names = list(self.child_table.keys()) #i.e. lower case
|
|
1927
|
+
if blockorder is None:
|
|
1928
|
+
blockorder = self.block_input_order
|
|
1929
|
+
top_block_names = [(a,self.child_table[a].block_id) for a in blockorder if self.child_table[a].parent is None]
|
|
1930
|
+
for blockref,blockname in top_block_names:
|
|
1931
|
+
print('Writing %s, ' % blockname + repr(self[blockref]))
|
|
1932
|
+
outstring.write('\n' + 'data_' +blockname+'\n')
|
|
1933
|
+
all_names.remove(blockref)
|
|
1934
|
+
if self.standard == 'Dic': #put contents before save frames
|
|
1935
|
+
outstring.write(self[blockref].printsection(finish_at='_dictionary_valid.application'))
|
|
1936
|
+
if self.grammar == 'STAR2': #nested save frames
|
|
1937
|
+
child_refs = self.get_immediate_children(blockref)
|
|
1938
|
+
for child_ref,child_info in child_refs:
|
|
1939
|
+
child_name = child_info.block_id
|
|
1940
|
+
outstring.write('\n\n' + 'save_' + child_name + '\n')
|
|
1941
|
+
self.block_to_string_nested(child_ref,child_name,outstring,4)
|
|
1942
|
+
outstring.write('\n' + 'save_'+ '\n')
|
|
1943
|
+
elif self.grammar in ('1.0','1.1','2.0'): #non-nested save frames
|
|
1944
|
+
child_refs = [a for a in blockorder if self.is_child_of_parent(blockref,a)]
|
|
1945
|
+
for child_ref in child_refs:
|
|
1946
|
+
child_name = self.child_table[child_ref].block_id
|
|
1947
|
+
outstring.write('\n\n' + 'save_' + child_name + '\n')
|
|
1948
|
+
outstring.write(str(self[child_ref]))
|
|
1949
|
+
outstring.write('\n\n' + 'save_' + '\n')
|
|
1950
|
+
all_names.remove(child_ref.lower())
|
|
1951
|
+
else:
|
|
1952
|
+
raise StarError('Grammar %s is not recognised for output' % self.grammar)
|
|
1953
|
+
if self.standard != 'Dic': #put contents after save frames
|
|
1954
|
+
outstring.write(str(self[blockref]))
|
|
1955
|
+
else:
|
|
1956
|
+
outstring.write(self[blockref].printsection(start_from='_dictionary_valid.application'))
|
|
1957
|
+
returnstring = outstring.getvalue()
|
|
1958
|
+
outstring.close()
|
|
1959
|
+
if len(all_names)>0:
|
|
1960
|
+
print('WARNING: following blocks not output: %s' % repr(all_names))
|
|
1961
|
+
else:
|
|
1962
|
+
print('All blocks output.')
|
|
1963
|
+
return returnstring
|
|
1964
|
+
|
|
1965
|
+
def block_to_string_nested(self,block_ref,block_id,outstring,indentlevel=0):
|
|
1966
|
+
"""Output a complete datablock indexed by [[block_ref]] and named [[block_id]], including children,
|
|
1967
|
+
and syntactically nesting save frames"""
|
|
1968
|
+
child_refs = self.get_immediate_children(block_ref)
|
|
1969
|
+
self[block_ref].set_grammar(self.grammar)
|
|
1970
|
+
if self.standard == 'Dic':
|
|
1971
|
+
outstring.write(str(self[block_ref]))
|
|
1972
|
+
for child_ref,child_info in child_refs:
|
|
1973
|
+
child_name = child_info.block_id
|
|
1974
|
+
outstring.write('\n' + 'save_' + child_name + '\n')
|
|
1975
|
+
self.block_to_string_nested(child_ref,child_name,outstring,indentlevel)
|
|
1976
|
+
outstring.write('\n' + ' '*indentlevel + 'save_' + '\n')
|
|
1977
|
+
if self.standard != 'Dic':
|
|
1978
|
+
outstring.write(str(self[block_ref]))
|
|
1979
|
+
|
|
1980
|
+
|
|
1981
|
+
class StarFile(BlockCollection):
|
|
1982
|
+
def __init__(self,datasource=None,maxinlength=-1,maxoutlength=0,
|
|
1983
|
+
scoping='instance',grammar='1.1',scantype='standard',
|
|
1984
|
+
permissive=False,**kwargs):
|
|
1985
|
+
super(StarFile,self).__init__(datasource=datasource,**kwargs)
|
|
1986
|
+
self.my_uri = getattr(datasource,'my_uri','')
|
|
1987
|
+
if maxoutlength == 0:
|
|
1988
|
+
self.maxoutlength = 2048
|
|
1989
|
+
else:
|
|
1990
|
+
self.maxoutlength = maxoutlength
|
|
1991
|
+
self.scoping = scoping
|
|
1992
|
+
if isinstance(datasource,(unicode,str)) or hasattr(datasource,"read"):
|
|
1993
|
+
ReadStar(datasource,prepared=self,grammar=grammar,scantype=scantype,
|
|
1994
|
+
maxlength = maxinlength,permissive=permissive)
|
|
1995
|
+
self.header_comment = \
|
|
1996
|
+
"""#\\#STAR
|
|
1997
|
+
##########################################################################
|
|
1998
|
+
# STAR Format file
|
|
1999
|
+
# Produced by PySTARRW module
|
|
2000
|
+
#
|
|
2001
|
+
# This is a STAR file. STAR is a superset of the CIF file type. For
|
|
2002
|
+
# more information, please refer to International Tables for Crystallography,
|
|
2003
|
+
# Volume G, Chapter 2.1
|
|
2004
|
+
#
|
|
2005
|
+
##########################################################################
|
|
2006
|
+
"""
|
|
2007
|
+
def set_uri(self,my_uri): self.my_uri = my_uri
|
|
2008
|
+
|
|
2009
|
+
|
|
2010
|
+
class CIFStringIO(StringIO):
|
|
2011
|
+
def __init__(self,target_width=80,**kwargs):
|
|
2012
|
+
StringIO.__init__(self,**kwargs)
|
|
2013
|
+
self.currentpos = 0
|
|
2014
|
+
self.target_width = target_width
|
|
2015
|
+
self.tabwidth = -1
|
|
2016
|
+
self.indentlist = [0]
|
|
2017
|
+
self.last_char = ""
|
|
2018
|
+
|
|
2019
|
+
def write(self,outstring,canbreak=False,mustbreak=False,do_tab=True,newindent=False,unindent=False,
|
|
2020
|
+
delimiter=False,startcol=-1):
|
|
2021
|
+
"""Write a string with correct linebreak, tabs and indents"""
|
|
2022
|
+
# do we need to break?
|
|
2023
|
+
if delimiter:
|
|
2024
|
+
if len(outstring)>1:
|
|
2025
|
+
raise ValueError('Delimiter %s is longer than one character' % repr( outstring ))
|
|
2026
|
+
output_delimiter = True
|
|
2027
|
+
if mustbreak: #insert a new line and indent
|
|
2028
|
+
temp_string = '\n' + ' ' * self.indentlist[-1]
|
|
2029
|
+
StringIO.write(self,temp_string)
|
|
2030
|
+
self.currentpos = self.indentlist[-1]
|
|
2031
|
+
self.last_char = temp_string[-1]
|
|
2032
|
+
if self.currentpos+len(outstring)>self.target_width: #try to break
|
|
2033
|
+
if not delimiter and outstring[0]!='\n': #ie <cr>;
|
|
2034
|
+
if canbreak:
|
|
2035
|
+
temp_string = '\n' + ' ' * self.indentlist[-1]
|
|
2036
|
+
StringIO.write(self,temp_string)
|
|
2037
|
+
self.currentpos = self.indentlist[-1]
|
|
2038
|
+
self.last_char = temp_string[-1]
|
|
2039
|
+
else: #assume a break will be forced on next value
|
|
2040
|
+
output_delimiter = False #the line break becomes the delimiter
|
|
2041
|
+
#try to match requested column
|
|
2042
|
+
if startcol > 0:
|
|
2043
|
+
if self.currentpos < startcol:
|
|
2044
|
+
StringIO.write(self,(startcol - self.currentpos)* ' ')
|
|
2045
|
+
self.currentpos = startcol
|
|
2046
|
+
self.last_char = ' '
|
|
2047
|
+
else:
|
|
2048
|
+
print('Could not format %s at column %d as already at %d' % (outstring,startcol,self.currentpos))
|
|
2049
|
+
startcol = -1 #so that tabbing works as a backup
|
|
2050
|
+
#handle tabs
|
|
2051
|
+
if self.tabwidth >0 and do_tab and startcol < 0:
|
|
2052
|
+
next_stop = ((self.currentpos//self.tabwidth)+1)*self.tabwidth
|
|
2053
|
+
#print 'Currentpos %d: Next tab stop at %d' % (self.currentpos,next_stop)
|
|
2054
|
+
if self.currentpos < next_stop:
|
|
2055
|
+
StringIO.write(self,(next_stop-self.currentpos)*' ')
|
|
2056
|
+
self.currentpos = next_stop
|
|
2057
|
+
self.last_char = ' '
|
|
2058
|
+
#calculate indentation after tabs and col setting applied
|
|
2059
|
+
if newindent: #indent by current amount
|
|
2060
|
+
if self.indentlist[-1] == 0: #first time
|
|
2061
|
+
self.indentlist.append(self.currentpos)
|
|
2062
|
+
# print 'Indentlist: ' + `self.indentlist`
|
|
2063
|
+
else:
|
|
2064
|
+
self.indentlist.append(self.indentlist[-1]+2)
|
|
2065
|
+
elif unindent:
|
|
2066
|
+
if len(self.indentlist)>1:
|
|
2067
|
+
self.indentlist.pop()
|
|
2068
|
+
else:
|
|
2069
|
+
print('Warning: cannot unindent any further')
|
|
2070
|
+
#check that we still need a delimiter
|
|
2071
|
+
if self.last_char in [' ','\n','\t']:
|
|
2072
|
+
output_delimiter = False
|
|
2073
|
+
#now output the string - every invocation comes through here
|
|
2074
|
+
if (delimiter and output_delimiter) or not delimiter:
|
|
2075
|
+
StringIO.write(self,outstring)
|
|
2076
|
+
last_line_break = outstring.rfind('\n')
|
|
2077
|
+
if last_line_break >=0:
|
|
2078
|
+
self.currentpos = len(outstring)-last_line_break
|
|
2079
|
+
else:
|
|
2080
|
+
self.currentpos = self.currentpos + len(outstring)
|
|
2081
|
+
#remember the last character
|
|
2082
|
+
if len(outstring)>0:
|
|
2083
|
+
self.last_char = outstring[-1]
|
|
2084
|
+
|
|
2085
|
+
def set_tab(self,tabwidth):
|
|
2086
|
+
"""Set the tab stop position"""
|
|
2087
|
+
self.tabwidth = tabwidth
|
|
2088
|
+
|
|
2089
|
+
class StarError(Exception):
|
|
2090
|
+
def __init__(self,value):
|
|
2091
|
+
self.value = value
|
|
2092
|
+
def __str__(self):
|
|
2093
|
+
return '\nStar Format error: '+ self.value
|
|
2094
|
+
|
|
2095
|
+
class StarLengthError(Exception):
|
|
2096
|
+
def __init__(self,value):
|
|
2097
|
+
self.value = value
|
|
2098
|
+
def __str__(self):
|
|
2099
|
+
return '\nStar length error: ' + self.value
|
|
2100
|
+
|
|
2101
|
+
class StarDerivationError(Exception):
|
|
2102
|
+
def __init__(self,fail_name):
|
|
2103
|
+
self.fail_name = fail_name
|
|
2104
|
+
def __str__(self):
|
|
2105
|
+
return "Derivation of %s failed, None returned" % self.fail_name
|
|
2106
|
+
|
|
2107
|
+
#
|
|
2108
|
+
# This is subclassed from AttributeError in order to allow hasattr
|
|
2109
|
+
# to work.
|
|
2110
|
+
#
|
|
2111
|
+
class StarDerivationFailure(AttributeError):
|
|
2112
|
+
def __init__(self,fail_name):
|
|
2113
|
+
self.fail_name = fail_name
|
|
2114
|
+
def __str__(self):
|
|
2115
|
+
return "Derivation of %s failed" % self.fail_name
|
|
2116
|
+
|
|
2117
|
+
def ReadStar(filename,prepared = None, maxlength=-1,
|
|
2118
|
+
scantype='standard',grammar='STAR2',CBF=False, permissive=False):
|
|
2119
|
+
|
|
2120
|
+
""" Read in a STAR file, returning the contents in the `prepared` object.
|
|
2121
|
+
|
|
2122
|
+
* `filename` may be a URL, a file
|
|
2123
|
+
path on the local system, or any object with a `read` method.
|
|
2124
|
+
|
|
2125
|
+
* `prepared` provides a `StarFile` or `CifFile` object that the contents of `filename`
|
|
2126
|
+
will be added to.
|
|
2127
|
+
|
|
2128
|
+
* `maxlength` is the maximum allowable line length in the input file. This has been set at
|
|
2129
|
+
2048 characters for CIF but is unlimited (-1) for STAR files.
|
|
2130
|
+
|
|
2131
|
+
* `grammar` chooses the STAR grammar variant. `1.0` is the original 1992 CIF/STAR grammar and `1.1`
|
|
2132
|
+
is identical except for the exclusion of square brackets as the first characters in
|
|
2133
|
+
undelimited datanames. `2.0` will read files in the CIF2.0 standard, and `STAR2` will
|
|
2134
|
+
read files according to the STAR2 publication. If grammar is `None` or `auto`, autodetection
|
|
2135
|
+
will be attempted in the order `2.0`, `1.1` and `1.0`. This will always succeed for conformant CIF2.0 files.
|
|
2136
|
+
Note that (nested) save frames are read in all grammar variations and then flagged afterwards if
|
|
2137
|
+
they do not match the requested grammar.
|
|
2138
|
+
|
|
2139
|
+
* `scantype` can be `standard` or `flex`. `standard` provides pure Python parsing at the
|
|
2140
|
+
cost of a factor of 10 or so in speed. `flex` will tokenise the input CIF file using
|
|
2141
|
+
fast C routines. Note that running PyCIFRW in Jython uses native Java regular expressions
|
|
2142
|
+
to provide a speedup regardless of this argument.
|
|
2143
|
+
|
|
2144
|
+
* `CBF` flags that the input file is in Crystallographic Binary File format. The binary block is
|
|
2145
|
+
excised from the input data stream before parsing and is not available in the returned object.
|
|
2146
|
+
|
|
2147
|
+
* `permissive` allows non UTF8 encodings (currently only latin1) in the input file. These are a
|
|
2148
|
+
violation of the standard.
|
|
2149
|
+
|
|
2150
|
+
"""
|
|
2151
|
+
|
|
2152
|
+
# save desired scoping
|
|
2153
|
+
save_scoping = prepared.scoping
|
|
2154
|
+
from . import YappsStarParser_1_1 as Y11
|
|
2155
|
+
from . import YappsStarParser_1_0 as Y10
|
|
2156
|
+
from . import YappsStarParser_2_0 as Y20
|
|
2157
|
+
from . import YappsStarParser_STAR2 as YST
|
|
2158
|
+
if prepared is None:
|
|
2159
|
+
prepared = StarFile()
|
|
2160
|
+
if grammar == "auto" or grammar is None:
|
|
2161
|
+
try_list = [('2.0',Y20),('1.1',Y11),('1.0',Y10)]
|
|
2162
|
+
elif grammar == '1.0':
|
|
2163
|
+
try_list = [('1.0',Y10)]
|
|
2164
|
+
elif grammar == '1.1':
|
|
2165
|
+
try_list = [('1.1',Y11)]
|
|
2166
|
+
elif grammar == '2.0':
|
|
2167
|
+
try_list = [('2.0',Y20)]
|
|
2168
|
+
elif grammar == 'STAR2':
|
|
2169
|
+
try_list = [('STAR2',YST)]
|
|
2170
|
+
else:
|
|
2171
|
+
raise AttributeError('Unknown STAR/CIF grammar requested, %s' % repr( grammar ))
|
|
2172
|
+
if isinstance(filename,(unicode,str)):
|
|
2173
|
+
# create an absolute URL
|
|
2174
|
+
relpath = urlparse(filename)
|
|
2175
|
+
if len(relpath.scheme) <= 1:
|
|
2176
|
+
if not os.path.isabs(filename):
|
|
2177
|
+
fullpath = os.path.join(os.getcwd(),filename)
|
|
2178
|
+
else:
|
|
2179
|
+
fullpath = filename
|
|
2180
|
+
if have_pathlib: # Python > 3.4
|
|
2181
|
+
my_uri = Path(fullpath).as_uri()
|
|
2182
|
+
else: # works on Linux/Mac only
|
|
2183
|
+
newrel = list(relpath)
|
|
2184
|
+
newrel[0] = "file"
|
|
2185
|
+
newrel[2] = fullpath
|
|
2186
|
+
my_uri = urlunparse(newrel)
|
|
2187
|
+
else:
|
|
2188
|
+
my_uri = urlunparse(relpath)
|
|
2189
|
+
# print("Full URL is: " + my_uri)
|
|
2190
|
+
filestream = urlopen(my_uri)
|
|
2191
|
+
try:
|
|
2192
|
+
text = filestream.read().decode('utf-8-sig')
|
|
2193
|
+
except UnicodeDecodeError:
|
|
2194
|
+
if permissive:
|
|
2195
|
+
text = filestream.read().decode('latin1')
|
|
2196
|
+
print("WARNING: %s violates standard (latin1 encoding instead of UTF8)." % filename)
|
|
2197
|
+
else:
|
|
2198
|
+
raise StarError("%s: bad encoding (must be utf8 or ascii)" % filename)
|
|
2199
|
+
filestream.close()
|
|
2200
|
+
else:
|
|
2201
|
+
filestream = filename #already opened for us
|
|
2202
|
+
text = filestream.read()
|
|
2203
|
+
if not isinstance(text,unicode):
|
|
2204
|
+
try:
|
|
2205
|
+
text = text.decode('utf-8-sig') #CIF is always ascii/utf8
|
|
2206
|
+
except UnicodeDecodeError:
|
|
2207
|
+
if permissive:
|
|
2208
|
+
text = filestream.read().decode('latin1')
|
|
2209
|
+
print("WARNING: text violates CIF standard (latin1 encoding instead of UTF8)")
|
|
2210
|
+
else:
|
|
2211
|
+
raise StarError("Bad input encoding (must be utf8 or ascii)")
|
|
2212
|
+
my_uri = ""
|
|
2213
|
+
if not text: # empty file, return empty block
|
|
2214
|
+
return prepared.set_uri(my_uri)
|
|
2215
|
+
# filter out non-ASCII characters in CBF files if required. We assume
|
|
2216
|
+
# that the binary is enclosed in a fixed string that occurs
|
|
2217
|
+
# nowhere else.
|
|
2218
|
+
if CBF:
|
|
2219
|
+
text_bits = text.split("-BINARY-FORMAT-SECTION-")
|
|
2220
|
+
text = text_bits[0]
|
|
2221
|
+
for section in range(2,len(text_bits),2):
|
|
2222
|
+
text = text+" (binary omitted)"+text_bits[section]
|
|
2223
|
+
# we recognise ctrl-Z as end of file
|
|
2224
|
+
endoffile = text.find(chr(26))
|
|
2225
|
+
if endoffile >= 0:
|
|
2226
|
+
text = text[:endoffile]
|
|
2227
|
+
split = text.split('\n')
|
|
2228
|
+
if maxlength > 0:
|
|
2229
|
+
toolong = [a for a in split if len(a)>maxlength]
|
|
2230
|
+
if toolong:
|
|
2231
|
+
pos = split.index(toolong[0])
|
|
2232
|
+
raise StarError( 'Line %d contains more than %d characters' % (pos+1,maxlength))
|
|
2233
|
+
# honour the header string
|
|
2234
|
+
if text[:10] != r"#\#CIF_2.0" and ('2.0',Y20) in try_list:
|
|
2235
|
+
try_list.remove(('2.0',Y20),)
|
|
2236
|
+
if not try_list:
|
|
2237
|
+
raise StarError('File %s missing CIF2.0 header' % (filename))
|
|
2238
|
+
for grammar_name,Y in try_list:
|
|
2239
|
+
if scantype == 'standard' or grammar_name in ['2.0','STAR2']:
|
|
2240
|
+
parser = Y.StarParser(Y.StarParserScanner(text))
|
|
2241
|
+
else:
|
|
2242
|
+
parser = Y.StarParser(Y.yappsrt.Scanner(None,[],text,scantype='flex'))
|
|
2243
|
+
# handle encoding switch
|
|
2244
|
+
if grammar_name in ['2.0','STAR2']:
|
|
2245
|
+
prepared.set_characterset('unicode')
|
|
2246
|
+
else:
|
|
2247
|
+
prepared.set_characterset('ascii')
|
|
2248
|
+
proto_star = None
|
|
2249
|
+
try:
|
|
2250
|
+
proto_star = getattr(parser,"input")(prepared)
|
|
2251
|
+
except Y.yappsrt.YappsSyntaxError as e:
|
|
2252
|
+
input = parser._scanner.input
|
|
2253
|
+
Y.yappsrt.print_error(input, e, parser._scanner)
|
|
2254
|
+
except Y.yappsrt.NoMoreTokens:
|
|
2255
|
+
print('Could not complete parsing; stopped around here:',file=sys.stderr)
|
|
2256
|
+
print(parser._scanner,file=sys.stderr)
|
|
2257
|
+
except ValueError:
|
|
2258
|
+
print('Unexpected error:')
|
|
2259
|
+
import traceback
|
|
2260
|
+
traceback.print_exc()
|
|
2261
|
+
if proto_star is not None:
|
|
2262
|
+
proto_star.set_grammar(grammar_name) #remember for output
|
|
2263
|
+
break
|
|
2264
|
+
if proto_star is None:
|
|
2265
|
+
errorstring = 'Syntax error in input file: last value parsed was %s' % Y.lastval
|
|
2266
|
+
errorstring = errorstring + '\nParser status: %s' % repr( parser._scanner )
|
|
2267
|
+
raise StarError( errorstring)
|
|
2268
|
+
# set visibility correctly
|
|
2269
|
+
proto_star.scoping = 'dictionary'
|
|
2270
|
+
proto_star.set_uri(my_uri)
|
|
2271
|
+
proto_star.scoping = save_scoping
|
|
2272
|
+
return proto_star
|
|
2273
|
+
|
|
2274
|
+
def get_dim(dataitem,current=0,packlen=0):
|
|
2275
|
+
zerotypes = [int, float, str]
|
|
2276
|
+
if type(dataitem) in zerotypes:
|
|
2277
|
+
return current, packlen
|
|
2278
|
+
if not dataitem.__class__ == ().__class__ and \
|
|
2279
|
+
not dataitem.__class__ == [].__class__:
|
|
2280
|
+
return current, packlen
|
|
2281
|
+
elif len(dataitem)>0:
|
|
2282
|
+
# print "Get_dim: %d: %s" % (current,`dataitem`)
|
|
2283
|
+
return get_dim(dataitem[0],current+1,len(dataitem))
|
|
2284
|
+
else: return current+1,0
|
|
2285
|
+
|
|
2286
|
+
def apply_line_folding(instring,minwraplength=60,maxwraplength=80):
|
|
2287
|
+
"""Insert line folding characters into instring between min/max wraplength"""
|
|
2288
|
+
# first check that we need to do this
|
|
2289
|
+
lines = instring.split('\n')
|
|
2290
|
+
line_len = [len(l) for l in lines]
|
|
2291
|
+
if max(line_len) < maxwraplength and re.match("\\[ \v\t\f]*\n",instring) is None:
|
|
2292
|
+
return instring
|
|
2293
|
+
outstring = "\\\n" #header
|
|
2294
|
+
for l in lines:
|
|
2295
|
+
if len(l) < maxwraplength:
|
|
2296
|
+
outstring = outstring + l
|
|
2297
|
+
if len(l) > 0 and l[-1]=='\\': #who'da thunk it? A line ending with a backslash
|
|
2298
|
+
outstring = outstring + "\\\n" #
|
|
2299
|
+
outstring = outstring + "\n" # put back the split character
|
|
2300
|
+
else:
|
|
2301
|
+
current_bit = l
|
|
2302
|
+
while len(current_bit) > maxwraplength:
|
|
2303
|
+
space_pos = re.search('[ \v\f\t]+',current_bit[minwraplength:])
|
|
2304
|
+
if space_pos is not None and space_pos.start()<maxwraplength-1:
|
|
2305
|
+
outstring = outstring + current_bit[:minwraplength+space_pos.start()] + "\\\n"
|
|
2306
|
+
current_bit = current_bit[minwraplength+space_pos.start():]
|
|
2307
|
+
else: #just blindly insert
|
|
2308
|
+
outstring = outstring + current_bit[:maxwraplength-1] + "\\\n"
|
|
2309
|
+
current_bit = current_bit[maxwraplength-1:]
|
|
2310
|
+
outstring = outstring + current_bit
|
|
2311
|
+
if current_bit[-1] == '\\': #a backslash just happens to be here
|
|
2312
|
+
outstring = outstring + "\\\n"
|
|
2313
|
+
outstring = outstring + '\n'
|
|
2314
|
+
outstring = outstring[:-1] #remove final newline
|
|
2315
|
+
return outstring
|
|
2316
|
+
|
|
2317
|
+
def remove_line_folding(instring):
|
|
2318
|
+
"""Remove line folding from instring"""
|
|
2319
|
+
if re.match(r"\\[ \v\t\f]*" +"\n",instring) is not None:
|
|
2320
|
+
return re.sub(r"\\[ \v\t\f]*$" + "\n?","",instring,flags=re.M)
|
|
2321
|
+
else:
|
|
2322
|
+
return instring
|
|
2323
|
+
|
|
2324
|
+
def apply_line_prefix(instring,prefix):
|
|
2325
|
+
"""Prefix every line in instring with prefix"""
|
|
2326
|
+
if prefix[0] != ";" and "\\" not in prefix:
|
|
2327
|
+
header = re.match(r"(\\[ \v\t\f]*" +"\n)",instring)
|
|
2328
|
+
if header is not None:
|
|
2329
|
+
print('Found line folded string for prefixing...')
|
|
2330
|
+
not_header = instring[header.end():]
|
|
2331
|
+
outstring = prefix + "\\\\\n" + prefix
|
|
2332
|
+
else:
|
|
2333
|
+
print('No folding in input string...')
|
|
2334
|
+
not_header = instring
|
|
2335
|
+
outstring = prefix + "\\\n" + prefix
|
|
2336
|
+
outstring = outstring + not_header.replace("\n","\n"+prefix)
|
|
2337
|
+
return outstring
|
|
2338
|
+
raise StarError("Requested prefix starts with semicolon or contains a backslash: " + prefix)
|
|
2339
|
+
|
|
2340
|
+
def remove_line_prefix(instring):
|
|
2341
|
+
"""Remove prefix from every line if present"""
|
|
2342
|
+
prefix_match = re.match("(?P<prefix>[^;\\\n][^\n\\\\]+)(?P<folding>\\\\{1,2}[ \t\v\f]*\n)",instring)
|
|
2343
|
+
if prefix_match is not None:
|
|
2344
|
+
prefix_text = prefix_match.group('prefix')
|
|
2345
|
+
print('Found prefix %s' % prefix_text)
|
|
2346
|
+
prefix_end = prefix_match.end('folding')
|
|
2347
|
+
# keep any line folding instructions
|
|
2348
|
+
if prefix_match.group('folding')[:2]=='\\\\': #two backslashes
|
|
2349
|
+
outstring = instring[prefix_match.end('folding')-1:].replace("\n"+prefix_text,"\n")
|
|
2350
|
+
return "\\" + outstring #keep line folding first line
|
|
2351
|
+
else:
|
|
2352
|
+
outstring = instring[prefix_match.end('folding')-1:].replace("\n"+prefix_text,"\n")
|
|
2353
|
+
return outstring[1:] #drop first line ending, no longer necessary
|
|
2354
|
+
else:
|
|
2355
|
+
return instring
|
|
2356
|
+
|
|
2357
|
+
|
|
2358
|
+
def listify(item):
|
|
2359
|
+
if isinstance(item,unicode): return [item]
|
|
2360
|
+
else: return item
|
|
2361
|
+
|
|
2362
|
+
#Transpose the list of lists passed to us
|
|
2363
|
+
def transpose(base_list):
|
|
2364
|
+
new_lofl = []
|
|
2365
|
+
full_length = len(base_list)
|
|
2366
|
+
opt_range = range(full_length)
|
|
2367
|
+
for i in range(len(base_list[0])):
|
|
2368
|
+
new_packet = []
|
|
2369
|
+
for j in opt_range:
|
|
2370
|
+
new_packet.append(base_list[j][i])
|
|
2371
|
+
new_lofl.append(new_packet)
|
|
2372
|
+
return new_lofl
|
|
2373
|
+
|
|
2374
|
+
# This routine optimised to return as quickly as possible
|
|
2375
|
+
# as it is called a lot.
|
|
2376
|
+
def not_none(itemlist):
|
|
2377
|
+
"""Return true only if no values of None are present"""
|
|
2378
|
+
if itemlist is None:
|
|
2379
|
+
return False
|
|
2380
|
+
if not isinstance(itemlist,(tuple,list)):
|
|
2381
|
+
return True
|
|
2382
|
+
for x in itemlist:
|
|
2383
|
+
if not not_none(x): return False
|
|
2384
|
+
return True
|
|
2385
|
+
|
|
2386
|
+
|
|
2387
|
+
def check_stringiness(data):
|
|
2388
|
+
"""Check that the contents of data are all strings"""
|
|
2389
|
+
if not hasattr(data,'dtype'): #so not Numpy
|
|
2390
|
+
from numbers import Number
|
|
2391
|
+
if isinstance(data,Number): return False
|
|
2392
|
+
elif isinstance(data,(unicode,str)): return True
|
|
2393
|
+
elif data is None:return False #should be data are None :)
|
|
2394
|
+
else:
|
|
2395
|
+
for one_item in data:
|
|
2396
|
+
if not check_stringiness(one_item): return False
|
|
2397
|
+
return True #all must be strings
|
|
2398
|
+
else: #numerical python
|
|
2399
|
+
import numpy
|
|
2400
|
+
if data.ndim == 0: #a bare value
|
|
2401
|
+
if data.dtype.kind in ['S','U']: return True
|
|
2402
|
+
else: return False
|
|
2403
|
+
else:
|
|
2404
|
+
for one_item in numpy.nditer(data):
|
|
2405
|
+
print('numpy data: ' + repr( one_item ))
|
|
2406
|
+
if not check_stringiness(one_item): return False
|
|
2407
|
+
return True
|
|
2408
|
+
|
|
2409
|
+
def process_template(template_file):
|
|
2410
|
+
"""Process a template datafile to formatting instructions"""
|
|
2411
|
+
template_as_cif = StarFile(template_file,grammar="2.0").first_block()
|
|
2412
|
+
if isinstance(template_file,(unicode,str)):
|
|
2413
|
+
template_string = open(template_file).read()
|
|
2414
|
+
else: #a StringIO object
|
|
2415
|
+
template_file.seek(0) #reset
|
|
2416
|
+
template_string = template_file.read()
|
|
2417
|
+
#template_as_lines = template_string.split("\n")
|
|
2418
|
+
#template_as_lines = [l for l in template_as_lines if len(l)>0 and l[0]!='#']
|
|
2419
|
+
#template_as_lines = [l for l in template_as_lines if l.split()[0] != 'loop_']
|
|
2420
|
+
#template_full_lines = dict([(l.split()[0],l) for l in template_as_lines if len(l.split())>0])
|
|
2421
|
+
form_hints = [] #ordered array of hint dictionaries
|
|
2422
|
+
find_indent = "^ +"
|
|
2423
|
+
for item in template_as_cif.item_order: #order of input
|
|
2424
|
+
if not isinstance(item,int): #not nested
|
|
2425
|
+
hint_dict = {"dataname":item}
|
|
2426
|
+
# find the line in the file
|
|
2427
|
+
start_pos = re.search("(^[ \t]*(?P<name>" + item + ")[ \t\n]+)(?P<spec>([\\S]+)|(^;))",template_string,re.I|re.M)
|
|
2428
|
+
if start_pos.group("spec") != None:
|
|
2429
|
+
spec_pos = start_pos.start("spec")-start_pos.start(0)
|
|
2430
|
+
spec_char = template_string[start_pos.start("spec"):start_pos.start("spec")+3]
|
|
2431
|
+
if spec_char[0] in '\'";':
|
|
2432
|
+
hint_dict.update({"delimiter":spec_char[0]})
|
|
2433
|
+
if spec_char == '"""' or spec_char == "'''":
|
|
2434
|
+
hint_dict.update({"delimiter":spec_char})
|
|
2435
|
+
if spec_char[0] != ";": #so we need to work out the column number
|
|
2436
|
+
hint_dict.update({"column":spec_pos})
|
|
2437
|
+
else: #need to put in the carriage return
|
|
2438
|
+
hint_dict.update({"delimiter":"\n;"})
|
|
2439
|
+
# can we format the text?
|
|
2440
|
+
text_val = template_as_cif[item]
|
|
2441
|
+
hint_dict["reformat"] = "\n\t" in text_val or "\n " in text_val
|
|
2442
|
+
if hint_dict["reformat"]: #find the indentation
|
|
2443
|
+
p = re.search(find_indent,text_val,re.M)
|
|
2444
|
+
if p.group() is not None:
|
|
2445
|
+
hint_dict["reformat_indent"]=p.end() - p.start()
|
|
2446
|
+
if start_pos.group('name') != None:
|
|
2447
|
+
name_pos = start_pos.start('name') - start_pos.start(0)
|
|
2448
|
+
hint_dict.update({"name_pos":name_pos})
|
|
2449
|
+
#print '%s: %s' % (item,`hint_dict`)
|
|
2450
|
+
form_hints.append(hint_dict)
|
|
2451
|
+
else: #loop block
|
|
2452
|
+
testnames = template_as_cif.loops[item]
|
|
2453
|
+
total_items = len(template_as_cif.loops[item])
|
|
2454
|
+
testname = testnames[0]
|
|
2455
|
+
#find the loop spec line in the file
|
|
2456
|
+
loop_regex = "(^[ \t]*(?P<loop>loop_)[ \t\n\r]+(?P<name>" + testname + ")([ \t\n\r]+_[\\S]+){%d}[ \t]*$(?P<packet>(.(?!_loop|_[\\S]+))*))" % (total_items - 1)
|
|
2457
|
+
loop_line = re.search(loop_regex,template_string,re.I|re.M|re.S)
|
|
2458
|
+
loop_so_far = loop_line.end()
|
|
2459
|
+
packet_text = loop_line.group('packet')
|
|
2460
|
+
loop_indent = loop_line.start('loop') - loop_line.start(0)
|
|
2461
|
+
form_hints.append({"dataname":'loop','name_pos':loop_indent})
|
|
2462
|
+
packet_regex = "[ \t]*(?P<all>(?P<sqqq>'''([^\n\r\f']*)''')|(?P<sq>'([^\n\r\f']*)'+)|(?P<dq>\"([^\n\r\"]*)\"+)|(?P<none>[^\\s]+))"
|
|
2463
|
+
packet_pos = re.finditer(packet_regex,packet_text)
|
|
2464
|
+
line_end_pos = re.finditer("^",packet_text,re.M)
|
|
2465
|
+
next_end = next(line_end_pos).end()
|
|
2466
|
+
last_end = next_end
|
|
2467
|
+
for loopname in testnames:
|
|
2468
|
+
#find the name in the file for name pos
|
|
2469
|
+
name_regex = "(^[ \t]*(?P<name>" + loopname + "))"
|
|
2470
|
+
name_match = re.search(name_regex,template_string,re.I|re.M|re.S)
|
|
2471
|
+
loop_name_indent = name_match.start('name')-name_match.start(0)
|
|
2472
|
+
hint_dict = {"dataname":loopname,"name_pos":loop_name_indent}
|
|
2473
|
+
#find the value
|
|
2474
|
+
thismatch = next(packet_pos)
|
|
2475
|
+
while thismatch.start('all') > next_end:
|
|
2476
|
+
try:
|
|
2477
|
+
last_end = next_end
|
|
2478
|
+
next_end = next(line_end_pos).start()
|
|
2479
|
+
print('next end %d' % next_end)
|
|
2480
|
+
except StopIteration:
|
|
2481
|
+
break
|
|
2482
|
+
print('Start %d, last_end %d' % (thismatch.start('all'),last_end))
|
|
2483
|
+
col_pos = thismatch.start('all') - last_end + 1
|
|
2484
|
+
if thismatch.group('none') is None:
|
|
2485
|
+
if thismatch.group('sqqq') is not None:
|
|
2486
|
+
hint_dict.update({'delimiter':"'''"})
|
|
2487
|
+
else:
|
|
2488
|
+
hint_dict.update({'delimiter':thismatch.groups()[0][0]})
|
|
2489
|
+
hint_dict.update({'column':col_pos})
|
|
2490
|
+
print('%s: %s' % (loopname,repr( hint_dict )))
|
|
2491
|
+
form_hints.append(hint_dict)
|
|
2492
|
+
return form_hints
|
|
2493
|
+
|
|
2494
|
+
|
|
2495
|
+
#No documentation flags
|
|
2496
|
+
|