##// END OF EJS Templates
Merge pull request #12644 from deep-jkl/fix-pathlib-in-tests
Matthias Bussonnier -
r26187:4967ec2a merge
parent child Browse files
Show More
@@ -6,7 +6,7
6 6
7 7 import atexit
8 8 import datetime
9 import os
9 from pathlib import Path
10 10 import re
11 11 import sqlite3
12 12 import threading
@@ -16,8 +16,17 from decorator import decorator
16 16 from IPython.utils.decorators import undoc
17 17 from IPython.paths import locate_profile
18 18 from traitlets import (
19 Any, Bool, Dict, Instance, Integer, List, Unicode, TraitError,
20 default, observe,
19 Any,
20 Bool,
21 Dict,
22 Instance,
23 Integer,
24 List,
25 Unicode,
26 Union,
27 TraitError,
28 default,
29 observe,
21 30 )
22 31
23 32 #-----------------------------------------------------------------------------
@@ -27,17 +36,17 from traitlets import (
27 36 @undoc
28 37 class DummyDB(object):
29 38 """Dummy DB that will act as a black hole for history.
30
39
31 40 Only used in the absence of sqlite"""
32 41 def execute(*args, **kwargs):
33 42 return []
34
43
35 44 def commit(self, *args, **kwargs):
36 45 pass
37
46
38 47 def __enter__(self, *args, **kwargs):
39 48 pass
40
49
41 50 def __exit__(self, *args, **kwargs):
42 51 pass
43 52
@@ -73,17 +82,18 def catch_corrupt_db(f, self, *a, **kw):
73 82 if self._corrupt_db_counter > self._corrupt_db_limit:
74 83 self.hist_file = ':memory:'
75 84 self.log.error("Failed to load history too many times, history will not be saved.")
76 elif os.path.isfile(self.hist_file):
85 elif self.hist_file.is_file():
77 86 # move the file out of the way
78 base, ext = os.path.splitext(self.hist_file)
79 size = os.stat(self.hist_file).st_size
87 base = str(self.hist_file.parent / self.hist_file.stem)
88 ext = self.hist_file.suffix
89 size = self.hist_file.stat().st_size
80 90 if size >= _SAVE_DB_SIZE:
81 91 # if there's significant content, avoid clobbering
82 92 now = datetime.datetime.now().isoformat().replace(':', '.')
83 93 newpath = base + '-corrupt-' + now + ext
84 94 # don't clobber previous corrupt backups
85 95 for i in range(100):
86 if not os.path.isfile(newpath):
96 if not Path(newpath).exists():
87 97 break
88 98 else:
89 99 newpath = base + '-corrupt-' + now + (u'-%i' % i) + ext
@@ -91,14 +101,15 def catch_corrupt_db(f, self, *a, **kw):
91 101 # not much content, possibly empty; don't worry about clobbering
92 102 # maybe we should just delete it?
93 103 newpath = base + '-corrupt' + ext
94 os.rename(self.hist_file, newpath)
104 self.hist_file.rename(newpath)
95 105 self.log.error("History file was moved to %s and a new file created.", newpath)
96 106 self.init_db()
97 107 return []
98 108 else:
99 109 # Failed with :memory:, something serious is wrong
100 110 raise
101
111
112
102 113 class HistoryAccessorBase(LoggingConfigurable):
103 114 """An abstract class for History Accessors """
104 115
@@ -118,7 +129,7 class HistoryAccessorBase(LoggingConfigurable):
118 129
119 130 class HistoryAccessor(HistoryAccessorBase):
120 131 """Access the history database without adding to it.
121
132
122 133 This is intended for use by standalone history tools. IPython shells use
123 134 HistoryManager, below, which is a subclass of this."""
124 135
@@ -128,37 +139,39 class HistoryAccessor(HistoryAccessorBase):
128 139 _corrupt_db_limit = 2
129 140
130 141 # String holding the path to the history file
131 hist_file = Unicode(
142 hist_file = Union(
143 [Instance(Path), Unicode()],
132 144 help="""Path to file to use for SQLite history database.
133
145
134 146 By default, IPython will put the history database in the IPython
135 147 profile directory. If you would rather share one history among
136 148 profiles, you can set this value in each, so that they are consistent.
137
149
138 150 Due to an issue with fcntl, SQLite is known to misbehave on some NFS
139 151 mounts. If you see IPython hanging, try setting this to something on a
140 152 local disk, e.g::
141
153
142 154 ipython --HistoryManager.hist_file=/tmp/ipython_hist.sqlite
143 155
144 156 you can also use the specific value `:memory:` (including the colon
145 157 at both end but not the back ticks), to avoid creating an history file.
146
147 """).tag(config=True)
148
158
159 """,
160 ).tag(config=True)
161
149 162 enabled = Bool(True,
150 163 help="""enable the SQLite history
151
164
152 165 set enabled=False to disable the SQLite history,
153 166 in which case there will be no stored history, no SQLite connection,
154 167 and no background saving thread. This may be necessary in some
155 168 threaded environments where IPython is embedded.
156 169 """
157 170 ).tag(config=True)
158
171
159 172 connection_options = Dict(
160 173 help="""Options for configuring the SQLite connection
161
174
162 175 These options are passed as keyword args to sqlite3.connect
163 176 when establishing database connections.
164 177 """
@@ -175,10 +188,10 class HistoryAccessor(HistoryAccessorBase):
175 188 msg = "%s.db must be sqlite3 Connection or DummyDB, not %r" % \
176 189 (self.__class__.__name__, new)
177 190 raise TraitError(msg)
178
179 def __init__(self, profile='default', hist_file=u'', **traits):
191
192 def __init__(self, profile="default", hist_file="", **traits):
180 193 """Create a new history accessor.
181
194
182 195 Parameters
183 196 ----------
184 197 profile : str
@@ -196,37 +209,39 class HistoryAccessor(HistoryAccessorBase):
196 209 # set by config
197 210 if hist_file:
198 211 self.hist_file = hist_file
199
200 if self.hist_file == u'':
212
213 try:
214 self.hist_file
215 except TraitError:
201 216 # No one has set the hist_file, yet.
202 217 self.hist_file = self._get_hist_file_name(profile)
203
218
204 219 self.init_db()
205
220
206 221 def _get_hist_file_name(self, profile='default'):
207 222 """Find the history file for the given profile name.
208
223
209 224 This is overridden by the HistoryManager subclass, to use the shell's
210 225 active profile.
211
226
212 227 Parameters
213 228 ----------
214 229 profile : str
215 230 The name of a profile which has a history file.
216 231 """
217 return os.path.join(locate_profile(profile), 'history.sqlite')
218
232 return Path(locate_profile(profile)) / "history.sqlite"
233
219 234 @catch_corrupt_db
220 235 def init_db(self):
221 236 """Connect to the database, and create tables if necessary."""
222 237 if not self.enabled:
223 238 self.db = DummyDB()
224 239 return
225
240
226 241 # use detect_types so that timestamps return datetime objects
227 242 kwargs = dict(detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
228 243 kwargs.update(self.connection_options)
229 self.db = sqlite3.connect(self.hist_file, **kwargs)
244 self.db = sqlite3.connect(str(self.hist_file), **kwargs)
230 245 self.db.execute("""CREATE TABLE IF NOT EXISTS sessions (session integer
231 246 primary key autoincrement, start timestamp,
232 247 end timestamp, num_cmds integer, remark text)""")
@@ -290,7 +305,7 class HistoryAccessor(HistoryAccessorBase):
290 305
291 306 Returns
292 307 -------
293
308
294 309 session_id : int
295 310 Session ID number
296 311 start : datetime
@@ -308,7 +323,7 class HistoryAccessor(HistoryAccessorBase):
308 323 @catch_corrupt_db
309 324 def get_last_session_id(self):
310 325 """Get the last session ID currently in the database.
311
326
312 327 Within IPython, this should be the same as the value stored in
313 328 :attr:`HistoryManager.session_number`.
314 329 """
@@ -384,7 +399,7 class HistoryAccessor(HistoryAccessorBase):
384 399 if n is not None:
385 400 return reversed(list(cur))
386 401 return cur
387
402
388 403 @catch_corrupt_db
389 404 def get_range(self, session, start=1, stop=None, raw=True,output=False):
390 405 """Retrieve input by session.
@@ -461,7 +476,7 class HistoryManager(HistoryAccessor):
461 476 @default('dir_hist')
462 477 def _dir_hist_default(self):
463 478 try:
464 return [os.getcwd()]
479 return [Path.cwd()]
465 480 except OSError:
466 481 return []
467 482
@@ -473,7 +488,7 class HistoryManager(HistoryAccessor):
473 488
474 489 # The number of the current session in the history database
475 490 session_number = Integer()
476
491
477 492 db_log_output = Bool(False,
478 493 help="Should the history database include output? (default: no)"
479 494 ).tag(config=True)
@@ -484,12 +499,12 class HistoryManager(HistoryAccessor):
484 499 # The input and output caches
485 500 db_input_cache = List()
486 501 db_output_cache = List()
487
502
488 503 # History saving in separate thread
489 504 save_thread = Instance('IPython.core.history.HistorySavingThread',
490 505 allow_none=True)
491 506 save_flag = Instance(threading.Event, allow_none=True)
492
507
493 508 # Private interface
494 509 # Variables used to store the three last inputs from the user. On each new
495 510 # history update, we populate the user's namespace with these, shifted as
@@ -513,37 +528,37 class HistoryManager(HistoryAccessor):
513 528 self.save_flag = threading.Event()
514 529 self.db_input_cache_lock = threading.Lock()
515 530 self.db_output_cache_lock = threading.Lock()
516
531
517 532 try:
518 533 self.new_session()
519 534 except sqlite3.OperationalError:
520 535 self.log.error("Failed to create history session in %s. History will not be saved.",
521 536 self.hist_file, exc_info=True)
522 537 self.hist_file = ':memory:'
523
538
524 539 if self.enabled and self.hist_file != ':memory:':
525 540 self.save_thread = HistorySavingThread(self)
526 541 self.save_thread.start()
527 542
528 543 def _get_hist_file_name(self, profile=None):
529 544 """Get default history file name based on the Shell's profile.
530
545
531 546 The profile parameter is ignored, but must exist for compatibility with
532 547 the parent class."""
533 548 profile_dir = self.shell.profile_dir.location
534 return os.path.join(profile_dir, 'history.sqlite')
535
549 return Path(profile_dir) / "history.sqlite"
550
536 551 @only_when_enabled
537 552 def new_session(self, conn=None):
538 553 """Get a new session number."""
539 554 if conn is None:
540 555 conn = self.db
541
556
542 557 with conn:
543 558 cur = conn.execute("""INSERT INTO sessions VALUES (NULL, ?, NULL,
544 559 NULL, "") """, (datetime.datetime.now(),))
545 560 self.session_number = cur.lastrowid
546
561
547 562 def end_session(self):
548 563 """Close the database session, filling in the end time and line count."""
549 564 self.writeout_cache()
@@ -552,20 +567,20 class HistoryManager(HistoryAccessor):
552 567 session==?""", (datetime.datetime.now(),
553 568 len(self.input_hist_parsed)-1, self.session_number))
554 569 self.session_number = 0
555
570
556 571 def name_session(self, name):
557 572 """Give the current session a name in the history database."""
558 573 with self.db:
559 574 self.db.execute("UPDATE sessions SET remark=? WHERE session==?",
560 575 (name, self.session_number))
561
576
562 577 def reset(self, new_session=True):
563 578 """Clear the session history, releasing all object references, and
564 579 optionally open a new session."""
565 580 self.output_hist.clear()
566 581 # The directory history can't be completely empty
567 self.dir_hist[:] = [os.getcwd()]
568
582 self.dir_hist[:] = [Path.cwd()]
583
569 584 if new_session:
570 585 if self.session_number:
571 586 self.end_session()
@@ -588,7 +603,7 class HistoryManager(HistoryAccessor):
588 603
589 604 Returns
590 605 -------
591
606
592 607 session_id : int
593 608 Session ID number
594 609 start : datetime
@@ -609,7 +624,7 class HistoryManager(HistoryAccessor):
609 624 """Get input and output history from the current session. Called by
610 625 get_range, and takes similar parameters."""
611 626 input_hist = self.input_hist_raw if raw else self.input_hist_parsed
612
627
613 628 n = len(input_hist)
614 629 if start < 0:
615 630 start += n
@@ -617,17 +632,17 class HistoryManager(HistoryAccessor):
617 632 stop = n
618 633 elif stop < 0:
619 634 stop += n
620
635
621 636 for i in range(start, stop):
622 637 if output:
623 638 line = (input_hist[i], self.output_hist_reprs.get(i))
624 639 else:
625 640 line = input_hist[i]
626 641 yield (0, i, line)
627
642
628 643 def get_range(self, session=0, start=1, stop=None, raw=True,output=False):
629 644 """Retrieve input by session.
630
645
631 646 Parameters
632 647 ----------
633 648 session : int
@@ -645,7 +660,7 class HistoryManager(HistoryAccessor):
645 660 objects for the current session, or text reprs from previous
646 661 sessions if db_log_output was enabled at the time. Where no output
647 662 is found, None is used.
648
663
649 664 Returns
650 665 -------
651 666 entries
@@ -709,7 +724,7 class HistoryManager(HistoryAccessor):
709 724 '_ii': self._ii,
710 725 '_iii': self._iii,
711 726 new_i : self._i00 }
712
727
713 728 if self.shell is not None:
714 729 self.shell.push(to_main, interactive=False)
715 730
@@ -797,8 +812,9 class HistorySavingThread(threading.Thread):
797 812 def run(self):
798 813 # We need a separate db connection per thread:
799 814 try:
800 self.db = sqlite3.connect(self.history_manager.hist_file,
801 **self.history_manager.connection_options
815 self.db = sqlite3.connect(
816 str(self.history_manager.hist_file),
817 **self.history_manager.connection_options
802 818 )
803 819 while True:
804 820 self.history_manager.save_flag.wait()
@@ -7,7 +7,7
7 7
8 8 # stdlib
9 9 import io
10 import os
10 from pathlib import Path
11 11 import sys
12 12 import tempfile
13 13 from datetime import datetime
@@ -29,8 +29,9 def test_proper_default_encoding():
29 29 def test_history():
30 30 ip = get_ipython()
31 31 with TemporaryDirectory() as tmpdir:
32 tmp_path = Path(tmpdir)
32 33 hist_manager_ori = ip.history_manager
33 hist_file = os.path.join(tmpdir, 'history.sqlite')
34 hist_file = tmp_path / "history.sqlite"
34 35 try:
35 36 ip.history_manager = HistoryManager(shell=ip, hist_file=hist_file)
36 37 hist = [u'a=1', u'def f():\n test = 1\n return test', u"b='€Æ¾÷ß'"]
@@ -55,10 +56,10 def test_history():
55 56 ip.magic('%hist 2-500')
56 57
57 58 # Check that we can write non-ascii characters to a file
58 ip.magic("%%hist -f %s" % os.path.join(tmpdir, "test1"))
59 ip.magic("%%hist -pf %s" % os.path.join(tmpdir, "test2"))
60 ip.magic("%%hist -nf %s" % os.path.join(tmpdir, "test3"))
61 ip.magic("%%save %s 1-10" % os.path.join(tmpdir, "test4"))
59 ip.magic("%%hist -f %s" % (tmp_path / "test1"))
60 ip.magic("%%hist -pf %s" % (tmp_path / "test2"))
61 ip.magic("%%hist -nf %s" % (tmp_path / "test3"))
62 ip.magic("%%save %s 1-10" % (tmp_path / "test4"))
62 63
63 64 # New session
64 65 ip.history_manager.reset()
@@ -126,8 +127,8 def test_history():
126 127 nt.assert_equal(list(gothist), [(1,3,(hist[2],"spam"))] )
127 128
128 129 # Cross testing: check that magic %save can get previous session.
129 testfilename = os.path.realpath(os.path.join(tmpdir, "test.py"))
130 ip.magic("save " + testfilename + " ~1/1-3")
130 testfilename = (tmp_path / "test.py").resolve()
131 ip.magic("save " + str(testfilename) + " ~1/1-3")
131 132 with io.open(testfilename, encoding='utf-8') as testfile:
132 133 nt.assert_equal(testfile.read(),
133 134 u"# coding: utf-8\n" + u"\n".join(hist)+u"\n")
@@ -176,13 +177,13 def test_timestamp_type():
176 177 def test_hist_file_config():
177 178 cfg = Config()
178 179 tfile = tempfile.NamedTemporaryFile(delete=False)
179 cfg.HistoryManager.hist_file = tfile.name
180 cfg.HistoryManager.hist_file = Path(tfile.name)
180 181 try:
181 182 hm = HistoryManager(shell=get_ipython(), config=cfg)
182 183 nt.assert_equal(hm.hist_file, cfg.HistoryManager.hist_file)
183 184 finally:
184 185 try:
185 os.remove(tfile.name)
186 Path(tfile.name).unlink()
186 187 except OSError:
187 188 # same catch as in testing.tools.TempFileMixin
188 189 # On Windows, even though we close the file, we still can't
@@ -197,7 +198,7 def test_histmanager_disabled():
197 198 ip = get_ipython()
198 199 with TemporaryDirectory() as tmpdir:
199 200 hist_manager_ori = ip.history_manager
200 hist_file = os.path.join(tmpdir, 'history.sqlite')
201 hist_file = Path(tmpdir) / "history.sqlite"
201 202 cfg.HistoryManager.hist_file = hist_file
202 203 try:
203 204 ip.history_manager = HistoryManager(shell=ip, config=cfg)
@@ -211,4 +212,4 def test_histmanager_disabled():
211 212 ip.history_manager = hist_manager_ori
212 213
213 214 # hist_file should not be created
214 nt.assert_false(os.path.exists(hist_file))
215 nt.assert_false(hist_file.exists())
@@ -10,6 +10,7 Authors
10 10 # Distributed under the terms of the Modified BSD License.
11 11
12 12 import os
13 from pathlib import Path
13 14 import re
14 15 import sys
15 16 import tempfile
@@ -142,7 +143,7 def default_config():
142 143 config.TerminalTerminalInteractiveShell.term_title = False,
143 144 config.TerminalInteractiveShell.autocall = 0
144 145 f = tempfile.NamedTemporaryFile(suffix=u'test_hist.sqlite', delete=False)
145 config.HistoryManager.hist_file = f.name
146 config.HistoryManager.hist_file = Path(f.name)
146 147 f.close()
147 148 config.HistoryManager.db_cache_size = 10000
148 149 return config
General Comments 0
You need to be logged in to leave comments. Login now