Skip to content

Commit 16503d4

Browse files
authored
Merge pull request #138 from BCDA-APS/spec_newfile_128
BUG: SPEC filewriter scan numbering and newfile() when file exists fixes #125 and #128
2 parents 145e8e5 + edd547b commit 16503d4

File tree

8 files changed

+317
-34
lines changed

8 files changed

+317
-34
lines changed

apstools/filewriters.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ def descriptor(self, doc):
338338
doc_hints_names = []
339339
for k, d in doc["hints"].items():
340340
doc_hints_names.append(k)
341-
doc_hints_names += doc["hints"][k]["fields"]
341+
doc_hints_names += d["fields"]
342342

343343
# independent variable(s) first
344344
# assumes start["motors"] was defined
@@ -528,22 +528,29 @@ def newfile(self, filename=None, scan_id=None, RE=None):
528528
self.clear()
529529
filename = filename or self.make_default_filename()
530530
if os.path.exists(filename):
531-
ValueError(f"file {filename} exists")
531+
from spec2nexus.spec import SpecDataFile
532+
sdf = SpecDataFile(filename)
533+
scan_list = sdf.getScanNumbers()
534+
l = len(scan_list)
535+
m = max(map(float, scan_list))
536+
highest = int(max(l, m) + 0.9999) # solves issue #128
537+
scan_id = max(scan_id or 0, highest)
532538
self.spec_filename = filename
533539
self.spec_epoch = int(time.time()) # ! no roundup here!!!
534540
self.spec_host = socket.gethostname() or 'localhost'
535541
self.spec_user = getpass.getuser() or 'BlueSkyUser'
536542
self.write_file_header = True # don't write the file yet
537543

538544
# backwards-compatibility
539-
if scan_id == True:
540-
scan_id = SCAN_ID_RESET_VALUE
541-
elif scan_id == False:
542-
scan_id = None
545+
if isinstance(scan_id, bool):
546+
# True means reset the scan ID to default
547+
# False means do not modify it
548+
scan_id = {True: SCAN_ID_RESET_VALUE, False: None}[scan_id]
543549
if scan_id is not None and RE is not None:
544-
# assume isinstance(RE, bluesky.run_engine.RunEngine)
550+
# RE is an instance of bluesky.run_engine.RunEngine
551+
# (or duck type for testing)
545552
RE.md["scan_id"] = scan_id
546-
print(f"scan ID set to {scan_id}")
553+
self.scan_id = scan_id
547554
return self.spec_filename
548555

549556
def usefile(self, filename):

conda-recipe/meta.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ requirements:
4343
- pyRestTable
4444
- pandas
4545
- xlrd
46+
- spec2nexus
4647

4748
test:
4849
imports:

packaging.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Packaging Hints
2+
3+
## PyPI upload
4+
5+
Preceed the wildcard with tag text (`apstools-1.1.1*`)::
6+
7+
python setup.py sdist bdist_wheel
8+
twine upload dist/*
9+
10+
## Conda upload
11+
12+
In the upload command below, use the text reported
13+
at (near) the end of a successful conda build.
14+
15+
conda build ./conda-recipe/
16+
anaconda upload /home/mintadmin/Apps/anaconda/conda-bld/noarch/apstools-1.1.1-py_0.tar.bz2

pypi.txt

Lines changed: 0 additions & 26 deletions
This file was deleted.

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ ophyd
55
pandas
66
pyRestTable
77
xlrd
8+
spec2nexus

tests/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
def suite(*args, **kw):
1414

1515
import test_simple
16+
import test_filewriter
1617
# import test_excel
1718
test_list = [
1819
test_simple,
20+
test_filewriter,
1921
# test_excel
2022
]
2123

tests/test_filewriter.py

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
2+
"""
3+
unit tests for the SPEC filewriter
4+
"""
5+
6+
import json
7+
import os
8+
import shutil
9+
import sys
10+
import tempfile
11+
import unittest
12+
import zipfile
13+
14+
_test_path = os.path.dirname(__file__)
15+
_path = os.path.join(_test_path, '..')
16+
if _path not in sys.path:
17+
sys.path.insert(0, _path)
18+
19+
from apstools.filewriters import SpecWriterCallback
20+
21+
22+
ZIP_FILE = os.path.join(_test_path, "usaxs_docs.json.zip")
23+
JSON_FILE = "usaxs_docs.json.txt"
24+
25+
26+
def write_stream(specwriter, stream):
27+
"""write the doc stream to the file"""
28+
for document in stream:
29+
tag, doc = document
30+
specwriter.receiver(tag, doc)
31+
32+
33+
def get_test_data():
34+
"""get document streams as dict from zip file"""
35+
with zipfile.ZipFile(ZIP_FILE, "r") as fp:
36+
buf = fp.read(JSON_FILE).decode("utf-8")
37+
return json.loads(buf)
38+
39+
40+
class Test_Data_is_Readable(unittest.TestCase):
41+
42+
def test_00_testdata_exist(self):
43+
self.assertTrue(
44+
os.path.exists(ZIP_FILE),
45+
"zip file with test data")
46+
with zipfile.ZipFile(ZIP_FILE, "r") as fp:
47+
self.assertIn(JSON_FILE, fp.namelist(), "JSON test data")
48+
49+
def test_testfile_content(self):
50+
# get our test document stream
51+
datasets = get_test_data()
52+
53+
census = {}
54+
for document in datasets["tune_mr"]:
55+
tag, _doc = document
56+
if tag not in census:
57+
census[tag] = 0
58+
census[tag] += 1
59+
60+
# test that tune_mr content arrived intact
61+
keys = dict(start=1, descriptor=2, event=33, stop=1)
62+
self.assertEqual(
63+
len(census.keys()),
64+
len(keys),
65+
"four document types")
66+
for k, v in keys.items():
67+
self.assertIn(k, census, f"{k} document exists")
68+
self.assertEqual(
69+
census[k],
70+
v,
71+
f"expected {v} '{k}' document(s)")
72+
73+
74+
class Test_SpecWriterCallback(unittest.TestCase):
75+
76+
def setUp(self):
77+
self.tempdir = tempfile.mkdtemp()
78+
self.db = get_test_data()
79+
80+
def tearDown(self):
81+
if os.path.exists(self.tempdir):
82+
shutil.rmtree(self.tempdir, ignore_errors=True)
83+
84+
def test_writer_default_name(self):
85+
specwriter = SpecWriterCallback()
86+
path = os.path.abspath(
87+
os.path.dirname(
88+
specwriter.spec_filename))
89+
self.assertNotEqual(
90+
path,
91+
self.tempdir,
92+
"default file not in tempdir")
93+
self.assertEqual(
94+
path,
95+
os.path.abspath(os.getcwd()),
96+
"default file to go in pwd")
97+
98+
# change the directory
99+
specwriter.spec_filename = os.path.join(
100+
self.tempdir,
101+
specwriter.spec_filename)
102+
103+
self.assertFalse(
104+
os.path.exists(specwriter.spec_filename),
105+
"data file not created yet")
106+
write_stream(specwriter, self.db["tune_mr"])
107+
self.assertTrue(
108+
os.path.exists(specwriter.spec_filename),
109+
"data file created")
110+
111+
def test_writer_filename(self):
112+
self.assertTrue(len(self.db) > 0, "test data ready")
113+
114+
testfile = os.path.join(self.tempdir, "tune_mr.dat")
115+
if os.path.exists(testfile):
116+
os.remove(testfile)
117+
specwriter = SpecWriterCallback(filename=testfile)
118+
119+
self.assertIsInstance(
120+
specwriter, SpecWriterCallback,
121+
"specwriter object")
122+
self.assertEqual(
123+
specwriter.spec_filename,
124+
testfile,
125+
"output data file")
126+
127+
self.assertFalse(
128+
os.path.exists(testfile),
129+
"data file not created yet")
130+
write_stream(specwriter, self.db["tune_mr"])
131+
self.assertTrue(os.path.exists(testfile), "data file created")
132+
133+
def test_newfile_exists(self):
134+
testfile = os.path.join(self.tempdir, "tune_mr.dat")
135+
if os.path.exists(testfile):
136+
os.remove(testfile)
137+
specwriter = SpecWriterCallback(filename=testfile)
138+
139+
from apstools.filewriters import SCAN_ID_RESET_VALUE
140+
self.assertEqual(SCAN_ID_RESET_VALUE, 0, "default reset scan id")
141+
142+
write_stream(specwriter, self.db["tune_mr"])
143+
self.assertTrue(os.path.exists(testfile), "data file created")
144+
145+
try:
146+
specwriter.newfile(filename=testfile)
147+
raised = False
148+
except ValueError:
149+
raised = True
150+
finally:
151+
self.assertFalse(raised, "file exists")
152+
self.assertEqual(specwriter.reset_scan_id, 0, "check scan id")
153+
154+
class my_RunEngine:
155+
# dick type for testing _here_
156+
md = dict(scan_id=SCAN_ID_RESET_VALUE)
157+
RE = my_RunEngine()
158+
159+
specwriter.scan_id = -5 # an unusual value for testing only
160+
RE.md["scan_id"] = -10 # an unusual value for testing only
161+
specwriter.newfile(filename=testfile, scan_id=None, RE=RE)
162+
self.assertEqual(specwriter.scan_id, 108, "scan_id unchanged")
163+
self.assertEqual(RE.md["scan_id"], 108, "RE.md['scan_id'] unchanged")
164+
165+
specwriter.scan_id = -5 # an unusual value for testing only
166+
RE.md["scan_id"] = -10 # an unusual value for testing only
167+
specwriter.newfile(filename=testfile, scan_id=False, RE=RE)
168+
self.assertEqual(specwriter.scan_id, 108, "scan_id unchanged")
169+
self.assertEqual(RE.md["scan_id"], 108, "RE.md['scan_id'] unchanged")
170+
171+
specwriter.scan_id = -5 # an unusual value for testing only
172+
RE.md["scan_id"] = -10 # an unusual value for testing only
173+
specwriter.newfile(filename=testfile, scan_id=True, RE=RE)
174+
self.assertEqual(specwriter.scan_id, 108, "scan_id reset")
175+
self.assertEqual(RE.md["scan_id"], 108, "RE.md['scan_id'] reset")
176+
177+
for n, s in {'0': 108, '108': 108, '110': 110}.items():
178+
specwriter.scan_id = -5 # an unusual value for testing only
179+
RE.md["scan_id"] = -10 # an unusual value for testing only
180+
specwriter.newfile(filename=testfile, scan_id=int(n), RE=RE)
181+
self.assertEqual(specwriter.scan_id, s, f"scan_id set to {n}, actually {s}")
182+
self.assertEqual(RE.md["scan_id"], s, f"RE.md['scan_id'] set to {n}, actually {s}")
183+
184+
def test__rebuild_scan_command(self):
185+
from apstools.filewriters import _rebuild_scan_command
186+
187+
self.assertTrue(len(self.db) > 0, "test data ready")
188+
189+
start_docs = []
190+
for header in self.db["tune_mr"]:
191+
tag, doc = header
192+
if tag == "start":
193+
start_docs.append(doc)
194+
self.assertEqual(len(start_docs), 1, "unique start doc found")
195+
196+
doc = start_docs[0]
197+
expected = "108 tune_mr()"
198+
result = _rebuild_scan_command(doc)
199+
self.assertEqual(result, expected, "rebuilt #S line")
200+
201+
def test_spec_comment(self):
202+
from apstools.filewriters import spec_comment
203+
204+
# spec_comment(comment, doc=None, writer=None)
205+
testfile = os.path.join(self.tempdir, "spec_comment.dat")
206+
if os.path.exists(testfile):
207+
os.remove(testfile)
208+
specwriter = SpecWriterCallback(filename=testfile)
209+
210+
for category in "buffered_comments comments".split():
211+
for k in "start stop descriptor event".split():
212+
o = getattr(specwriter, category)
213+
self.assertEqual(len(o[k]), 0, f"no '{k}' {category}")
214+
215+
# insert comments with every document
216+
spec_comment(
217+
"TESTING: Should appear within start doc",
218+
doc=None,
219+
writer=specwriter)
220+
221+
for idx, document in enumerate(self.db["tune_mr"]):
222+
tag, doc = document
223+
msg = f"TESTING: document {idx+1}: '{tag}' %s specwriter.receiver"
224+
spec_comment(
225+
msg % "before",
226+
doc=tag,
227+
writer=specwriter)
228+
specwriter.receiver(tag, doc)
229+
if tag == "stop":
230+
# since stop doc was received, this appears in the next scan
231+
spec_comment(
232+
str(msg % "before") + " (appears at END of next scan)",
233+
doc=tag,
234+
writer=specwriter)
235+
else:
236+
spec_comment(
237+
msg % "after",
238+
doc=tag,
239+
writer=specwriter)
240+
241+
self.assertEqual(
242+
len(specwriter.buffered_comments['stop']),
243+
1,
244+
"last 'stop' comment buffered")
245+
246+
# since stop doc was received, this appears in the next scan
247+
spec_comment(
248+
"TESTING: Appears at END of next scan",
249+
doc="stop",
250+
writer=specwriter)
251+
252+
self.assertEqual(
253+
len(specwriter.buffered_comments['stop']),
254+
2,
255+
"last end of scan comment buffered")
256+
write_stream(specwriter, self.db["tune_ar"])
257+
258+
for k in "start descriptor event".split():
259+
o = specwriter.buffered_comments
260+
self.assertEqual(len(o[k]), 0, f"no '{k}' {category}")
261+
expected = dict(start=2, stop=5, event=0, descriptor=0)
262+
for k, v in expected.items():
263+
self.assertEqual(
264+
len(specwriter.comments[k]),
265+
v,
266+
f"'{k}' comments")
267+
268+
269+
def suite(*args, **kw):
270+
test_list = [
271+
Test_Data_is_Readable,
272+
Test_SpecWriterCallback,
273+
]
274+
test_suite = unittest.TestSuite()
275+
for test_case in test_list:
276+
test_suite.addTest(unittest.makeSuite(test_case))
277+
return test_suite
278+
279+
280+
if __name__ == "__main__":
281+
runner=unittest.TextTestRunner()
282+
runner.run(suite())

tests/usaxs_docs.json.zip

275 KB
Binary file not shown.

0 commit comments

Comments
 (0)