##// END OF EJS Templates
montone: fix addition to list by using .append() instead of '+'...
Pulkit Goyal -
r41644:210a999b default
parent child Browse files
Show More
@@ -1,373 +1,373
1 # monotone.py - monotone support for the convert extension
1 # monotone.py - monotone support for the convert extension
2 #
2 #
3 # Copyright 2008, 2009 Mikkel Fahnoe Jorgensen <mikkel@dvide.com> and
3 # Copyright 2008, 2009 Mikkel Fahnoe Jorgensen <mikkel@dvide.com> and
4 # others
4 # others
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import os
10 import os
11 import re
11 import re
12
12
13 from mercurial.i18n import _
13 from mercurial.i18n import _
14 from mercurial import (
14 from mercurial import (
15 error,
15 error,
16 pycompat,
16 pycompat,
17 )
17 )
18 from mercurial.utils import dateutil
18 from mercurial.utils import dateutil
19
19
20 from . import common
20 from . import common
21
21
22 class monotone_source(common.converter_source, common.commandline):
22 class monotone_source(common.converter_source, common.commandline):
23 def __init__(self, ui, repotype, path=None, revs=None):
23 def __init__(self, ui, repotype, path=None, revs=None):
24 common.converter_source.__init__(self, ui, repotype, path, revs)
24 common.converter_source.__init__(self, ui, repotype, path, revs)
25 if revs and len(revs) > 1:
25 if revs and len(revs) > 1:
26 raise error.Abort(_('monotone source does not support specifying '
26 raise error.Abort(_('monotone source does not support specifying '
27 'multiple revs'))
27 'multiple revs'))
28 common.commandline.__init__(self, ui, 'mtn')
28 common.commandline.__init__(self, ui, 'mtn')
29
29
30 self.ui = ui
30 self.ui = ui
31 self.path = path
31 self.path = path
32 self.automatestdio = False
32 self.automatestdio = False
33 self.revs = revs
33 self.revs = revs
34
34
35 norepo = common.NoRepo(_("%s does not look like a monotone repository")
35 norepo = common.NoRepo(_("%s does not look like a monotone repository")
36 % path)
36 % path)
37 if not os.path.exists(os.path.join(path, '_MTN')):
37 if not os.path.exists(os.path.join(path, '_MTN')):
38 # Could be a monotone repository (SQLite db file)
38 # Could be a monotone repository (SQLite db file)
39 try:
39 try:
40 f = open(path, 'rb')
40 f = open(path, 'rb')
41 header = f.read(16)
41 header = f.read(16)
42 f.close()
42 f.close()
43 except IOError:
43 except IOError:
44 header = ''
44 header = ''
45 if header != 'SQLite format 3\x00':
45 if header != 'SQLite format 3\x00':
46 raise norepo
46 raise norepo
47
47
48 # regular expressions for parsing monotone output
48 # regular expressions for parsing monotone output
49 space = br'\s*'
49 space = br'\s*'
50 name = br'\s+"((?:\\"|[^"])*)"\s*'
50 name = br'\s+"((?:\\"|[^"])*)"\s*'
51 value = name
51 value = name
52 revision = br'\s+\[(\w+)\]\s*'
52 revision = br'\s+\[(\w+)\]\s*'
53 lines = br'(?:.|\n)+'
53 lines = br'(?:.|\n)+'
54
54
55 self.dir_re = re.compile(space + "dir" + name)
55 self.dir_re = re.compile(space + "dir" + name)
56 self.file_re = re.compile(space + "file" + name +
56 self.file_re = re.compile(space + "file" + name +
57 "content" + revision)
57 "content" + revision)
58 self.add_file_re = re.compile(space + "add_file" + name +
58 self.add_file_re = re.compile(space + "add_file" + name +
59 "content" + revision)
59 "content" + revision)
60 self.patch_re = re.compile(space + "patch" + name +
60 self.patch_re = re.compile(space + "patch" + name +
61 "from" + revision + "to" + revision)
61 "from" + revision + "to" + revision)
62 self.rename_re = re.compile(space + "rename" + name + "to" + name)
62 self.rename_re = re.compile(space + "rename" + name + "to" + name)
63 self.delete_re = re.compile(space + "delete" + name)
63 self.delete_re = re.compile(space + "delete" + name)
64 self.tag_re = re.compile(space + "tag" + name + "revision" +
64 self.tag_re = re.compile(space + "tag" + name + "revision" +
65 revision)
65 revision)
66 self.cert_re = re.compile(lines + space + "name" + name +
66 self.cert_re = re.compile(lines + space + "name" + name +
67 "value" + value)
67 "value" + value)
68
68
69 attr = space + "file" + lines + space + "attr" + space
69 attr = space + "file" + lines + space + "attr" + space
70 self.attr_execute_re = re.compile(attr + '"mtn:execute"' +
70 self.attr_execute_re = re.compile(attr + '"mtn:execute"' +
71 space + '"true"')
71 space + '"true"')
72
72
73 # cached data
73 # cached data
74 self.manifest_rev = None
74 self.manifest_rev = None
75 self.manifest = None
75 self.manifest = None
76 self.files = None
76 self.files = None
77 self.dirs = None
77 self.dirs = None
78
78
79 common.checktool('mtn', abort=False)
79 common.checktool('mtn', abort=False)
80
80
81 def mtnrun(self, *args, **kwargs):
81 def mtnrun(self, *args, **kwargs):
82 if self.automatestdio:
82 if self.automatestdio:
83 return self.mtnrunstdio(*args, **kwargs)
83 return self.mtnrunstdio(*args, **kwargs)
84 else:
84 else:
85 return self.mtnrunsingle(*args, **kwargs)
85 return self.mtnrunsingle(*args, **kwargs)
86
86
87 def mtnrunsingle(self, *args, **kwargs):
87 def mtnrunsingle(self, *args, **kwargs):
88 kwargs[r'd'] = self.path
88 kwargs[r'd'] = self.path
89 return self.run0('automate', *args, **kwargs)
89 return self.run0('automate', *args, **kwargs)
90
90
91 def mtnrunstdio(self, *args, **kwargs):
91 def mtnrunstdio(self, *args, **kwargs):
92 # Prepare the command in automate stdio format
92 # Prepare the command in automate stdio format
93 kwargs = pycompat.byteskwargs(kwargs)
93 kwargs = pycompat.byteskwargs(kwargs)
94 command = []
94 command = []
95 for k, v in kwargs.iteritems():
95 for k, v in kwargs.iteritems():
96 command.append("%d:%s" % (len(k), k))
96 command.append("%d:%s" % (len(k), k))
97 if v:
97 if v:
98 command.append("%d:%s" % (len(v), v))
98 command.append("%d:%s" % (len(v), v))
99 if command:
99 if command:
100 command.insert(0, 'o')
100 command.insert(0, 'o')
101 command.append('e')
101 command.append('e')
102
102
103 command.append('l')
103 command.append('l')
104 for arg in args:
104 for arg in args:
105 command += "%d:%s" % (len(arg), arg)
105 command.append("%d:%s" % (len(arg), arg))
106 command.append('e')
106 command.append('e')
107 command = ''.join(command)
107 command = ''.join(command)
108
108
109 self.ui.debug("mtn: sending '%s'\n" % command)
109 self.ui.debug("mtn: sending '%s'\n" % command)
110 self.mtnwritefp.write(command)
110 self.mtnwritefp.write(command)
111 self.mtnwritefp.flush()
111 self.mtnwritefp.flush()
112
112
113 return self.mtnstdioreadcommandoutput(command)
113 return self.mtnstdioreadcommandoutput(command)
114
114
115 def mtnstdioreadpacket(self):
115 def mtnstdioreadpacket(self):
116 read = None
116 read = None
117 commandnbr = ''
117 commandnbr = ''
118 while read != ':':
118 while read != ':':
119 read = self.mtnreadfp.read(1)
119 read = self.mtnreadfp.read(1)
120 if not read:
120 if not read:
121 raise error.Abort(_('bad mtn packet - no end of commandnbr'))
121 raise error.Abort(_('bad mtn packet - no end of commandnbr'))
122 commandnbr += read
122 commandnbr += read
123 commandnbr = commandnbr[:-1]
123 commandnbr = commandnbr[:-1]
124
124
125 stream = self.mtnreadfp.read(1)
125 stream = self.mtnreadfp.read(1)
126 if stream not in 'mewptl':
126 if stream not in 'mewptl':
127 raise error.Abort(_('bad mtn packet - bad stream type %s') % stream)
127 raise error.Abort(_('bad mtn packet - bad stream type %s') % stream)
128
128
129 read = self.mtnreadfp.read(1)
129 read = self.mtnreadfp.read(1)
130 if read != ':':
130 if read != ':':
131 raise error.Abort(_('bad mtn packet - no divider before size'))
131 raise error.Abort(_('bad mtn packet - no divider before size'))
132
132
133 read = None
133 read = None
134 lengthstr = ''
134 lengthstr = ''
135 while read != ':':
135 while read != ':':
136 read = self.mtnreadfp.read(1)
136 read = self.mtnreadfp.read(1)
137 if not read:
137 if not read:
138 raise error.Abort(_('bad mtn packet - no end of packet size'))
138 raise error.Abort(_('bad mtn packet - no end of packet size'))
139 lengthstr += read
139 lengthstr += read
140 try:
140 try:
141 length = long(lengthstr[:-1])
141 length = long(lengthstr[:-1])
142 except TypeError:
142 except TypeError:
143 raise error.Abort(_('bad mtn packet - bad packet size %s')
143 raise error.Abort(_('bad mtn packet - bad packet size %s')
144 % lengthstr)
144 % lengthstr)
145
145
146 read = self.mtnreadfp.read(length)
146 read = self.mtnreadfp.read(length)
147 if len(read) != length:
147 if len(read) != length:
148 raise error.Abort(_("bad mtn packet - unable to read full packet "
148 raise error.Abort(_("bad mtn packet - unable to read full packet "
149 "read %s of %s") % (len(read), length))
149 "read %s of %s") % (len(read), length))
150
150
151 return (commandnbr, stream, length, read)
151 return (commandnbr, stream, length, read)
152
152
153 def mtnstdioreadcommandoutput(self, command):
153 def mtnstdioreadcommandoutput(self, command):
154 retval = []
154 retval = []
155 while True:
155 while True:
156 commandnbr, stream, length, output = self.mtnstdioreadpacket()
156 commandnbr, stream, length, output = self.mtnstdioreadpacket()
157 self.ui.debug('mtn: read packet %s:%s:%s\n' %
157 self.ui.debug('mtn: read packet %s:%s:%s\n' %
158 (commandnbr, stream, length))
158 (commandnbr, stream, length))
159
159
160 if stream == 'l':
160 if stream == 'l':
161 # End of command
161 # End of command
162 if output != '0':
162 if output != '0':
163 raise error.Abort(_("mtn command '%s' returned %s") %
163 raise error.Abort(_("mtn command '%s' returned %s") %
164 (command, output))
164 (command, output))
165 break
165 break
166 elif stream in 'ew':
166 elif stream in 'ew':
167 # Error, warning output
167 # Error, warning output
168 self.ui.warn(_('%s error:\n') % self.command)
168 self.ui.warn(_('%s error:\n') % self.command)
169 self.ui.warn(output)
169 self.ui.warn(output)
170 elif stream == 'p':
170 elif stream == 'p':
171 # Progress messages
171 # Progress messages
172 self.ui.debug('mtn: ' + output)
172 self.ui.debug('mtn: ' + output)
173 elif stream == 'm':
173 elif stream == 'm':
174 # Main stream - command output
174 # Main stream - command output
175 retval.append(output)
175 retval.append(output)
176
176
177 return ''.join(retval)
177 return ''.join(retval)
178
178
179 def mtnloadmanifest(self, rev):
179 def mtnloadmanifest(self, rev):
180 if self.manifest_rev == rev:
180 if self.manifest_rev == rev:
181 return
181 return
182 self.manifest = self.mtnrun("get_manifest_of", rev).split("\n\n")
182 self.manifest = self.mtnrun("get_manifest_of", rev).split("\n\n")
183 self.manifest_rev = rev
183 self.manifest_rev = rev
184 self.files = {}
184 self.files = {}
185 self.dirs = {}
185 self.dirs = {}
186
186
187 for e in self.manifest:
187 for e in self.manifest:
188 m = self.file_re.match(e)
188 m = self.file_re.match(e)
189 if m:
189 if m:
190 attr = ""
190 attr = ""
191 name = m.group(1)
191 name = m.group(1)
192 node = m.group(2)
192 node = m.group(2)
193 if self.attr_execute_re.match(e):
193 if self.attr_execute_re.match(e):
194 attr += "x"
194 attr += "x"
195 self.files[name] = (node, attr)
195 self.files[name] = (node, attr)
196 m = self.dir_re.match(e)
196 m = self.dir_re.match(e)
197 if m:
197 if m:
198 self.dirs[m.group(1)] = True
198 self.dirs[m.group(1)] = True
199
199
200 def mtnisfile(self, name, rev):
200 def mtnisfile(self, name, rev):
201 # a non-file could be a directory or a deleted or renamed file
201 # a non-file could be a directory or a deleted or renamed file
202 self.mtnloadmanifest(rev)
202 self.mtnloadmanifest(rev)
203 return name in self.files
203 return name in self.files
204
204
205 def mtnisdir(self, name, rev):
205 def mtnisdir(self, name, rev):
206 self.mtnloadmanifest(rev)
206 self.mtnloadmanifest(rev)
207 return name in self.dirs
207 return name in self.dirs
208
208
209 def mtngetcerts(self, rev):
209 def mtngetcerts(self, rev):
210 certs = {"author":"<missing>", "date":"<missing>",
210 certs = {"author":"<missing>", "date":"<missing>",
211 "changelog":"<missing>", "branch":"<missing>"}
211 "changelog":"<missing>", "branch":"<missing>"}
212 certlist = self.mtnrun("certs", rev)
212 certlist = self.mtnrun("certs", rev)
213 # mtn < 0.45:
213 # mtn < 0.45:
214 # key "test@selenic.com"
214 # key "test@selenic.com"
215 # mtn >= 0.45:
215 # mtn >= 0.45:
216 # key [ff58a7ffb771907c4ff68995eada1c4da068d328]
216 # key [ff58a7ffb771907c4ff68995eada1c4da068d328]
217 certlist = re.split('\n\n key ["\[]', certlist)
217 certlist = re.split('\n\n key ["\[]', certlist)
218 for e in certlist:
218 for e in certlist:
219 m = self.cert_re.match(e)
219 m = self.cert_re.match(e)
220 if m:
220 if m:
221 name, value = m.groups()
221 name, value = m.groups()
222 value = value.replace(r'\"', '"')
222 value = value.replace(r'\"', '"')
223 value = value.replace(r'\\', '\\')
223 value = value.replace(r'\\', '\\')
224 certs[name] = value
224 certs[name] = value
225 # Monotone may have subsecond dates: 2005-02-05T09:39:12.364306
225 # Monotone may have subsecond dates: 2005-02-05T09:39:12.364306
226 # and all times are stored in UTC
226 # and all times are stored in UTC
227 certs["date"] = certs["date"].split('.')[0] + " UTC"
227 certs["date"] = certs["date"].split('.')[0] + " UTC"
228 return certs
228 return certs
229
229
230 # implement the converter_source interface:
230 # implement the converter_source interface:
231
231
232 def getheads(self):
232 def getheads(self):
233 if not self.revs:
233 if not self.revs:
234 return self.mtnrun("leaves").splitlines()
234 return self.mtnrun("leaves").splitlines()
235 else:
235 else:
236 return self.revs
236 return self.revs
237
237
238 def getchanges(self, rev, full):
238 def getchanges(self, rev, full):
239 if full:
239 if full:
240 raise error.Abort(_("convert from monotone does not support "
240 raise error.Abort(_("convert from monotone does not support "
241 "--full"))
241 "--full"))
242 revision = self.mtnrun("get_revision", rev).split("\n\n")
242 revision = self.mtnrun("get_revision", rev).split("\n\n")
243 files = {}
243 files = {}
244 ignoremove = {}
244 ignoremove = {}
245 renameddirs = []
245 renameddirs = []
246 copies = {}
246 copies = {}
247 for e in revision:
247 for e in revision:
248 m = self.add_file_re.match(e)
248 m = self.add_file_re.match(e)
249 if m:
249 if m:
250 files[m.group(1)] = rev
250 files[m.group(1)] = rev
251 ignoremove[m.group(1)] = rev
251 ignoremove[m.group(1)] = rev
252 m = self.patch_re.match(e)
252 m = self.patch_re.match(e)
253 if m:
253 if m:
254 files[m.group(1)] = rev
254 files[m.group(1)] = rev
255 # Delete/rename is handled later when the convert engine
255 # Delete/rename is handled later when the convert engine
256 # discovers an IOError exception from getfile,
256 # discovers an IOError exception from getfile,
257 # but only if we add the "from" file to the list of changes.
257 # but only if we add the "from" file to the list of changes.
258 m = self.delete_re.match(e)
258 m = self.delete_re.match(e)
259 if m:
259 if m:
260 files[m.group(1)] = rev
260 files[m.group(1)] = rev
261 m = self.rename_re.match(e)
261 m = self.rename_re.match(e)
262 if m:
262 if m:
263 toname = m.group(2)
263 toname = m.group(2)
264 fromname = m.group(1)
264 fromname = m.group(1)
265 if self.mtnisfile(toname, rev):
265 if self.mtnisfile(toname, rev):
266 ignoremove[toname] = 1
266 ignoremove[toname] = 1
267 copies[toname] = fromname
267 copies[toname] = fromname
268 files[toname] = rev
268 files[toname] = rev
269 files[fromname] = rev
269 files[fromname] = rev
270 elif self.mtnisdir(toname, rev):
270 elif self.mtnisdir(toname, rev):
271 renameddirs.append((fromname, toname))
271 renameddirs.append((fromname, toname))
272
272
273 # Directory renames can be handled only once we have recorded
273 # Directory renames can be handled only once we have recorded
274 # all new files
274 # all new files
275 for fromdir, todir in renameddirs:
275 for fromdir, todir in renameddirs:
276 renamed = {}
276 renamed = {}
277 for tofile in self.files:
277 for tofile in self.files:
278 if tofile in ignoremove:
278 if tofile in ignoremove:
279 continue
279 continue
280 if tofile.startswith(todir + '/'):
280 if tofile.startswith(todir + '/'):
281 renamed[tofile] = fromdir + tofile[len(todir):]
281 renamed[tofile] = fromdir + tofile[len(todir):]
282 # Avoid chained moves like:
282 # Avoid chained moves like:
283 # d1(/a) => d3/d1(/a)
283 # d1(/a) => d3/d1(/a)
284 # d2 => d3
284 # d2 => d3
285 ignoremove[tofile] = 1
285 ignoremove[tofile] = 1
286 for tofile, fromfile in renamed.items():
286 for tofile, fromfile in renamed.items():
287 self.ui.debug (_("copying file in renamed directory "
287 self.ui.debug (_("copying file in renamed directory "
288 "from '%s' to '%s'")
288 "from '%s' to '%s'")
289 % (fromfile, tofile), '\n')
289 % (fromfile, tofile), '\n')
290 files[tofile] = rev
290 files[tofile] = rev
291 copies[tofile] = fromfile
291 copies[tofile] = fromfile
292 for fromfile in renamed.values():
292 for fromfile in renamed.values():
293 files[fromfile] = rev
293 files[fromfile] = rev
294
294
295 return (files.items(), copies, set())
295 return (files.items(), copies, set())
296
296
297 def getfile(self, name, rev):
297 def getfile(self, name, rev):
298 if not self.mtnisfile(name, rev):
298 if not self.mtnisfile(name, rev):
299 return None, None
299 return None, None
300 try:
300 try:
301 data = self.mtnrun("get_file_of", name, r=rev)
301 data = self.mtnrun("get_file_of", name, r=rev)
302 except Exception:
302 except Exception:
303 return None, None
303 return None, None
304 self.mtnloadmanifest(rev)
304 self.mtnloadmanifest(rev)
305 node, attr = self.files.get(name, (None, ""))
305 node, attr = self.files.get(name, (None, ""))
306 return data, attr
306 return data, attr
307
307
308 def getcommit(self, rev):
308 def getcommit(self, rev):
309 extra = {}
309 extra = {}
310 certs = self.mtngetcerts(rev)
310 certs = self.mtngetcerts(rev)
311 if certs.get('suspend') == certs["branch"]:
311 if certs.get('suspend') == certs["branch"]:
312 extra['close'] = 1
312 extra['close'] = 1
313 dateformat = "%Y-%m-%dT%H:%M:%S"
313 dateformat = "%Y-%m-%dT%H:%M:%S"
314 return common.commit(
314 return common.commit(
315 author=certs["author"],
315 author=certs["author"],
316 date=dateutil.datestr(dateutil.strdate(certs["date"], dateformat)),
316 date=dateutil.datestr(dateutil.strdate(certs["date"], dateformat)),
317 desc=certs["changelog"],
317 desc=certs["changelog"],
318 rev=rev,
318 rev=rev,
319 parents=self.mtnrun("parents", rev).splitlines(),
319 parents=self.mtnrun("parents", rev).splitlines(),
320 branch=certs["branch"],
320 branch=certs["branch"],
321 extra=extra)
321 extra=extra)
322
322
323 def gettags(self):
323 def gettags(self):
324 tags = {}
324 tags = {}
325 for e in self.mtnrun("tags").split("\n\n"):
325 for e in self.mtnrun("tags").split("\n\n"):
326 m = self.tag_re.match(e)
326 m = self.tag_re.match(e)
327 if m:
327 if m:
328 tags[m.group(1)] = m.group(2)
328 tags[m.group(1)] = m.group(2)
329 return tags
329 return tags
330
330
331 def getchangedfiles(self, rev, i):
331 def getchangedfiles(self, rev, i):
332 # This function is only needed to support --filemap
332 # This function is only needed to support --filemap
333 # ... and we don't support that
333 # ... and we don't support that
334 raise NotImplementedError
334 raise NotImplementedError
335
335
336 def before(self):
336 def before(self):
337 # Check if we have a new enough version to use automate stdio
337 # Check if we have a new enough version to use automate stdio
338 try:
338 try:
339 versionstr = self.mtnrunsingle("interface_version")
339 versionstr = self.mtnrunsingle("interface_version")
340 version = float(versionstr)
340 version = float(versionstr)
341 except Exception:
341 except Exception:
342 raise error.Abort(_("unable to determine mtn automate interface "
342 raise error.Abort(_("unable to determine mtn automate interface "
343 "version"))
343 "version"))
344
344
345 if version >= 12.0:
345 if version >= 12.0:
346 self.automatestdio = True
346 self.automatestdio = True
347 self.ui.debug("mtn automate version %f - using automate stdio\n" %
347 self.ui.debug("mtn automate version %f - using automate stdio\n" %
348 version)
348 version)
349
349
350 # launch the long-running automate stdio process
350 # launch the long-running automate stdio process
351 self.mtnwritefp, self.mtnreadfp = self._run2('automate', 'stdio',
351 self.mtnwritefp, self.mtnreadfp = self._run2('automate', 'stdio',
352 '-d', self.path)
352 '-d', self.path)
353 # read the headers
353 # read the headers
354 read = self.mtnreadfp.readline()
354 read = self.mtnreadfp.readline()
355 if read != 'format-version: 2\n':
355 if read != 'format-version: 2\n':
356 raise error.Abort(_('mtn automate stdio header unexpected: %s')
356 raise error.Abort(_('mtn automate stdio header unexpected: %s')
357 % read)
357 % read)
358 while read != '\n':
358 while read != '\n':
359 read = self.mtnreadfp.readline()
359 read = self.mtnreadfp.readline()
360 if not read:
360 if not read:
361 raise error.Abort(_("failed to reach end of mtn automate "
361 raise error.Abort(_("failed to reach end of mtn automate "
362 "stdio headers"))
362 "stdio headers"))
363 else:
363 else:
364 self.ui.debug("mtn automate version %s - not using automate stdio "
364 self.ui.debug("mtn automate version %s - not using automate stdio "
365 "(automate >= 12.0 - mtn >= 0.46 is needed)\n" % version)
365 "(automate >= 12.0 - mtn >= 0.46 is needed)\n" % version)
366
366
367 def after(self):
367 def after(self):
368 if self.automatestdio:
368 if self.automatestdio:
369 self.mtnwritefp.close()
369 self.mtnwritefp.close()
370 self.mtnwritefp = None
370 self.mtnwritefp = None
371 self.mtnreadfp.close()
371 self.mtnreadfp.close()
372 self.mtnreadfp = None
372 self.mtnreadfp = None
373
373
General Comments 0
You need to be logged in to leave comments. Login now