Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagesmc
Path: blob/master/src/sage/misc/attached_files.py
8814 views
1
"""
2
Keep track of attached files
3
4
TESTS::
5
6
sage: attach('http://wstein.org/loadtest.py')
7
Traceback (most recent call last):
8
...
9
NotImplementedError: you can't attach a URL
10
11
Check that no file clutter is produced::
12
13
sage: dir = tmp_dir()
14
sage: src = os.path.join(dir, 'foobar.sage')
15
sage: with open(src, 'w') as f:
16
....: f.write('print "<output from attached file>"\n')
17
sage: attach(src)
18
<output from attached file>
19
sage: os.listdir(dir)
20
['foobar.sage']
21
sage: detach(src)
22
23
In debug mode backtraces contain code snippets. We need to manually
24
print the traceback because the python doctest module has special
25
support for exceptions and does not match them
26
character-by-character::
27
28
sage: import traceback
29
sage: with open(src, 'w') as f:
30
....: f.write('# first line\n')
31
....: f.write('# second line\n')
32
....: f.write('raise ValueError("third") # this should appear in the source snippet\n')
33
....: f.write('# fourth line\n')
34
35
sage: load_attach_mode(attach_debug=False)
36
sage: try:
37
....: attach(src)
38
....: except Exception:
39
....: traceback.print_exc()
40
Traceback (most recent call last):
41
...
42
exec preparse_file(open(fpath).read()) + "\n" in globals
43
File "<string>", line 3, in <module>
44
ValueError: third
45
sage: detach(src)
46
47
sage: load_attach_mode(attach_debug=True)
48
sage: try:
49
....: attach(src)
50
....: except Exception:
51
....: traceback.print_exc()
52
Traceback (most recent call last):
53
...
54
execfile(preparse_file_named(fpath), globals)
55
File ".../foobar.sage....py", line ..., in <module>
56
raise ValueError("third") # this should appear in the source snippet
57
ValueError: third
58
sage: detach(src)
59
"""
60
61
###########################################################################
62
# Copyright (C) 2013 Volker Braun <[email protected]>
63
#
64
# Distributed under the terms of the GNU General Public License (GPL)
65
# http://www.gnu.org/licenses/
66
###########################################################################
67
68
import os
69
import time
70
from sage.misc.preparser import load, load_wrap
71
import sage.env
72
73
# The attached files as a dict of {filename:mtime}
74
attached = {}
75
76
77
load_debug_mode = False
78
attach_debug_mode = True
79
80
def load_attach_mode(load_debug=None, attach_debug=None):
81
"""
82
Get or modify the current debug mode for the behavior of
83
:func:`load` and :func:`attach` on ``.sage`` files.
84
85
In debug mode, loaded or attached ``.sage`` files are preparsed
86
through a file to make their tracebacks more informative. If not
87
in debug mode, then ``.sage`` files are preparsed in memory only
88
for performance.
89
90
At startup, debug mode is ``True`` for attaching and ``False``
91
for loading.
92
93
.. NOTE::
94
95
This function should really be deprecated and code executed
96
from memory should raise proper tracebacks.
97
98
INPUT:
99
100
- ``load_debug`` -- boolean or ``None`` (default); if not
101
``None``, then set a new value for the debug mode for loading
102
files.
103
104
- ``attach_debug`` -- boolean or ``None`` (default); same as
105
``load_debug``, but for attaching files.
106
107
OUTPUT:
108
109
If all input values are ``None``, returns a tuple giving the
110
current modes for loading and attaching.
111
112
EXAMPLES::
113
114
sage: load_attach_mode()
115
(False, True)
116
sage: load_attach_mode(attach_debug=False)
117
sage: load_attach_mode()
118
(False, False)
119
sage: load_attach_mode(load_debug=True)
120
sage: load_attach_mode()
121
(True, False)
122
sage: load_attach_mode(load_debug=False, attach_debug=True)
123
"""
124
global load_debug_mode, attach_debug_mode
125
if load_debug is None and attach_debug is None:
126
return (load_debug_mode, attach_debug_mode)
127
if not load_debug is None:
128
load_debug_mode = load_debug
129
if not attach_debug is None:
130
attach_debug_mode = attach_debug
131
132
133
search_paths = []
134
135
def load_attach_path(path=None, replace=False):
136
"""
137
Get or modify the current search path for :func:`load` and
138
:func:`attach`.
139
140
INPUT:
141
142
- ``path`` -- string or list of strings (default: ``None``);
143
path(s) to append to or replace the current path.
144
145
- ``replace`` -- boolean (default: ``False``); if ``path`` is not
146
``None``, whether to *replace* the search path instead of
147
*appending* to it.
148
149
OUTPUT:
150
151
``None`` or a *reference* to the current search paths.
152
153
EXAMPLES:
154
155
First, we extend the example given in :func:`load`'s docstring::
156
157
sage: sage.misc.attached_files.reset(); reset_load_attach_path()
158
sage: load_attach_path()
159
['.']
160
sage: t_dir = tmp_dir()
161
sage: fullpath = os.path.join(t_dir, 'test.py')
162
sage: open(fullpath, 'w').write("print 37 * 3")
163
sage: attach('test.py')
164
Traceback (most recent call last):
165
...
166
IOError: did not find file 'test.py' in load / attach search path
167
sage: load_attach_path(t_dir)
168
sage: attach('test.py')
169
111
170
sage: attached_files() == [fullpath]
171
True
172
sage: sage.misc.attached_files.reset(); reset_load_attach_path()
173
sage: load_attach_path() == ['.']
174
True
175
sage: load('test.py')
176
Traceback (most recent call last):
177
...
178
IOError: did not find file 'test.py' in load / attach search path
179
180
The function returns a reference to the path list::
181
182
sage: reset_load_attach_path(); load_attach_path()
183
['.']
184
sage: load_attach_path('/path/to/my/sage/scripts'); load_attach_path()
185
['.', '/path/to/my/sage/scripts']
186
sage: load_attach_path(['good', 'bad', 'ugly'], replace=True)
187
sage: load_attach_path()
188
['good', 'bad', 'ugly']
189
sage: p = load_attach_path(); p.pop()
190
'ugly'
191
sage: p[0] = 'weird'; load_attach_path()
192
['weird', 'bad']
193
sage: reset_load_attach_path(); load_attach_path()
194
['.']
195
"""
196
global search_paths
197
if path is None:
198
return search_paths
199
else:
200
if isinstance(path, basestring):
201
path = [path]
202
if replace:
203
search_paths = path
204
else:
205
for p in path:
206
if not p:
207
continue
208
if p not in search_paths:
209
search_paths.append(p)
210
211
212
def reset_load_attach_path():
213
"""
214
Resets the current search path for :func:`load` and
215
:func:`attach`.
216
217
The default path is ``'.'`` plus any paths specified in the
218
environment variable ``SAGE_LOAD_ATTACH_PATH``.
219
220
EXAMPLES::
221
222
sage: load_attach_path()
223
['.']
224
sage: t_dir = tmp_dir()
225
sage: load_attach_path(t_dir)
226
sage: t_dir in load_attach_path()
227
True
228
sage: reset_load_attach_path(); load_attach_path()
229
['.']
230
231
At startup, Sage adds colon-separated paths in the environment
232
variable ``SAGE_LOAD_ATTACH_PATH``::
233
234
sage: reset_load_attach_path(); load_attach_path()
235
['.']
236
sage: os.environ['SAGE_LOAD_ATTACH_PATH'] = '/veni/vidi:vici:'
237
sage: reload(sage.misc.attached_files) # Simulate startup
238
<module 'sage.misc.attached_files' from '...'>
239
sage: load_attach_path()
240
['.', '/veni/vidi', 'vici']
241
sage: del os.environ['SAGE_LOAD_ATTACH_PATH']
242
sage: reload(sage.misc.preparser) # Simulate startup
243
<module 'sage.misc.preparser' from '...'>
244
sage: reset_load_attach_path(); load_attach_path()
245
['.']
246
"""
247
global search_paths
248
search_paths = ['.']
249
for path in os.environ.get('SAGE_LOAD_ATTACH_PATH', '').split(':'):
250
load_attach_path(path=path)
251
252
# Set up the initial search path for loading and attaching files. A
253
# user can modify the path with the function load_attach_path.
254
reset_load_attach_path()
255
256
257
def attach(*files):
258
"""
259
Attach a file or files to a running instance of Sage and also load
260
that file.
261
262
USAGE:
263
264
``attach file1 ...`` - space-separated list of ``.py``, ``.pyx``,
265
and ``.sage`` files, or ``attach('file1', 'file2')`` - filenames as
266
strings, given as arguments to :func:`attach`.
267
268
:meth:`~sage.misc.preparser.load` is the same as :func:`attach`, but
269
doesn't automatically reload a file when it changes.
270
271
.. NOTE::
272
273
On the Sage prompt you can also just type ``attach "foo.sage"``
274
as a short-hand for ``attach('foo.sage')``. However this
275
alternate form is not part of the Python language and does not
276
work in Python scripts.
277
278
EFFECT:
279
280
Each file is read in and added to an internal list of watched files.
281
The meaning of reading in a file depends on the file type:
282
283
- ``.py`` files are read in with no preparsing (so, e.g., ``2^3`` is 2
284
bit-xor 3);
285
286
- ``.sage`` files are preparsed, then the result is read in;
287
288
- ``.pyx`` files are *not* preparsed, but rather are compiled to a
289
module ``m`` and then ``from m import *`` is executed.
290
291
The contents of the file are then loaded, which means they are read
292
into the running Sage session. For example, if ``foo.sage`` contains
293
``x=5``, after attaching ``foo.sage`` the variable ``x`` will be set
294
to 5. Moreover, any time you change ``foo.sage``, before you execute
295
a command, the attached file will be re-read automatically (with no
296
intervention on your part).
297
298
EXAMPLES:
299
300
You attach a file, e.g., ``foo.sage`` or ``foo.py`` or
301
``foo.pyx``, to a running Sage session by typing::
302
303
sage: attach foo.sage # or foo.py or foo.pyx or even a URL to such a file (not tested)
304
305
or::
306
307
sage: attach('foo.sage') # not tested
308
309
Here we test attaching multiple files at once::
310
311
sage: sage.misc.attached_files.reset()
312
sage: t1 = tmp_filename(ext='.py')
313
sage: open(t1,'w').write("print 'hello world'")
314
sage: t2 = tmp_filename(ext='.py')
315
sage: open(t2,'w').write("print 'hi there xxx'")
316
sage: attach(t1, t2)
317
hello world
318
hi there xxx
319
sage: set(attached_files()) == set([t1,t2])
320
True
321
322
.. SEEALSO::
323
324
- :meth:`attached_files` returns a list of
325
all currently attached files.
326
327
- :meth:`detach` instructs Sage to remove a
328
file from the internal list of watched files.
329
330
- :meth:`load_attach_path` allows you to
331
get or modify the current search path for loading and attaching
332
files.
333
"""
334
try:
335
ipy = get_ipython()
336
except NameError:
337
ipy = None
338
global attached
339
for filename in files:
340
if ipy:
341
code = load_wrap(filename, attach=True)
342
ipy.run_cell(code)
343
else:
344
load(filename, globals(), attach=True)
345
346
347
def add_attached_file(filename):
348
"""
349
Add to the list of attached files
350
351
This is a callback to be used from
352
:func:`~sage.misc.preparse.load` after evaluating the attached
353
file the first time.
354
355
INPUT:
356
357
- ``filename`` -- string, the fully qualified file name.
358
359
EXAMPLES::
360
361
sage: import sage.misc.attached_files as af
362
sage: af.reset()
363
sage: t = tmp_filename(ext='.py')
364
sage: af.add_attached_file(t)
365
sage: af.attached_files()
366
['/.../tmp_....py']
367
sage: af.detach(t)
368
sage: af.attached_files()
369
[]
370
"""
371
fpath = os.path.abspath(filename)
372
attached[fpath] = os.path.getmtime(fpath)
373
374
375
def attached_files():
376
"""
377
Returns a list of all files attached to the current session with
378
:meth:`attach`.
379
380
OUTPUT:
381
382
The filenames in a sorted list of strings.
383
384
EXAMPLES::
385
386
sage: sage.misc.attached_files.reset()
387
sage: t = tmp_filename(ext='.py')
388
sage: open(t,'w').write("print 'hello world'")
389
sage: attach(t)
390
hello world
391
sage: attached_files()
392
['/....py']
393
sage: attached_files() == [t]
394
True
395
"""
396
global attached
397
return list(sorted(attached.keys()))
398
399
400
def detach(filename):
401
"""
402
Detach a file.
403
404
This is the counterpart to :meth:`attach`.
405
406
INPUT:
407
408
- ``filename`` -- a string, or a list of strings, or a tuple of strings.
409
410
EXAMPLES::
411
412
sage: sage.misc.attached_files.reset()
413
sage: t = tmp_filename(ext='.py')
414
sage: open(t,'w').write("print 'hello world'")
415
sage: attach(t)
416
hello world
417
sage: attached_files() == [t]
418
True
419
sage: detach(t)
420
sage: attached_files()
421
[]
422
423
sage: sage.misc.attached_files.reset(); reset_load_attach_path()
424
sage: load_attach_path()
425
['.']
426
sage: t_dir = tmp_dir()
427
sage: fullpath = os.path.join(t_dir, 'test.py')
428
sage: open(fullpath, 'w').write("print 37 * 3")
429
sage: load_attach_path(t_dir)
430
sage: attach('test.py')
431
111
432
sage: attached_files() == [os.path.normpath(fullpath)]
433
True
434
sage: detach('test.py')
435
sage: attached_files()
436
[]
437
sage: attach('test.py')
438
111
439
sage: fullpath = os.path.join(t_dir, 'test2.py')
440
sage: open(fullpath, 'w').write("print 3")
441
sage: attach('test2.py')
442
3
443
sage: detach(attached_files())
444
sage: attached_files()
445
[]
446
447
TESTS::
448
449
sage: detach('/dev/null/foobar.sage')
450
Traceback (most recent call last):
451
...
452
ValueError: file '/dev/null/foobar.sage' is not attached, see attached_files()
453
"""
454
if isinstance(filename, basestring):
455
filelist = [filename]
456
else:
457
filelist = [str(x) for x in filename]
458
459
global attached
460
for filename in filelist:
461
fpath = os.path.expanduser(filename)
462
if not os.path.isabs(fpath):
463
for path in load_attach_path():
464
epath = os.path.expanduser(path)
465
fpath = os.path.join(epath, filename)
466
fpath = os.path.abspath(fpath)
467
if fpath in attached:
468
break
469
if fpath in attached:
470
attached.pop(fpath)
471
else:
472
raise ValueError("file '{0}' is not attached, see attached_files()".format(filename))
473
474
def reset():
475
"""
476
Remove all the attached files from the list of attached files.
477
478
EXAMPLES::
479
480
sage: sage.misc.attached_files.reset()
481
sage: t = tmp_filename(ext='.py')
482
sage: open(t,'w').write("print 'hello world'")
483
sage: attach(t)
484
hello world
485
sage: attached_files() == [t]
486
True
487
sage: sage.misc.attached_files.reset()
488
sage: attached_files()
489
[]
490
"""
491
global attached
492
attached = {}
493
494
495
def modified_file_iterator():
496
"""
497
Iterate over the changed files
498
499
As a side effect the stored time stamps are updated with the
500
actual time stamps. So if you iterate over the attached files in
501
order to reload them and you hit an error then the subsequent
502
files are not marked as read.
503
504
Files that are in the process of being saved are excluded.
505
506
EXAMPLES::
507
508
sage: sage.misc.attached_files.reset()
509
sage: t = tmp_filename(ext='.py')
510
sage: attach(t)
511
sage: from sage.misc.attached_files import modified_file_iterator
512
sage: list(modified_file_iterator())
513
[]
514
sage: sleep(1) # filesystem mtime granularity
515
sage: open(t, 'w').write('1')
516
sage: list(modified_file_iterator())
517
[('/.../tmp_....py', time.struct_time(...))]
518
"""
519
global attached
520
modified = dict()
521
for filename in attached.keys():
522
old_tm = attached[filename]
523
if not os.path.exists(filename):
524
print '### detaching file {0} because it does not exist (deleted?) ###'.format(filename)
525
detach(filename)
526
continue
527
new_tm = os.path.getmtime(filename)
528
if new_tm > old_tm:
529
modified[filename] = new_tm
530
531
if not modified:
532
return
533
time.sleep(0.1) # sleep 100ms to give the editor time to finish saving
534
535
for filename in modified.keys():
536
old_tm = modified[filename]
537
new_tm = os.path.getmtime(filename)
538
if new_tm == old_tm:
539
# file was modified but did not change in the last 100ms
540
attached[filename] = new_tm
541
yield filename, time.gmtime(new_tm)
542
543
544
def reload_attached_files_if_modified():
545
"""
546
Reload attached files that have been modified
547
548
This is the internal implementation of the attach mechanism.
549
550
EXAMPLES::
551
552
sage: sage.misc.attached_files.reset()
553
sage: from sage.misc.interpreter import get_test_shell
554
sage: shell = get_test_shell()
555
sage: tmp = tmp_filename(ext='.py')
556
sage: open(tmp, 'w').write('a = 2\n')
557
sage: shell.run_cell('attach({0})'.format(repr(tmp)))
558
sage: shell.run_cell('a')
559
2
560
sage: sleep(1) # filesystem mtime granularity
561
sage: open(tmp, 'w').write('a = 3\n')
562
563
Note that the doctests are never really at the command prompt
564
where the automatic reload is triggered. So we have to do it
565
manually::
566
567
sage: shell.run_cell('from sage.misc.attached_files import reload_attached_files_if_modified')
568
sage: shell.run_cell('reload_attached_files_if_modified()')
569
### reloading attached file tmp_....py modified at ... ###
570
571
sage: shell.run_cell('a')
572
3
573
sage: shell.run_cell('detach({0})'.format(repr(tmp)))
574
sage: shell.run_cell('attached_files()')
575
[]
576
"""
577
for filename, mtime in modified_file_iterator():
578
basename = os.path.basename(filename)
579
timestr = time.strftime('%T', mtime)
580
from sage.libs.readline import interleaved_output
581
with interleaved_output():
582
print '### reloading attached file {0} modified at {1} ###'.format(basename, timestr)
583
code = load_wrap(filename, attach=True)
584
get_ipython().run_cell(code)
585
586