Coverage for C:\leo.repo\leo-editor\leo\core\leoExternalFiles.py: 17%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

370 statements  

1# -*- coding: utf-8 -*- 

2#@+leo-ver=5-thin 

3#@+node:ekr.20160306114544.1: * @file leoExternalFiles.py 

4#@@first 

5import getpass 

6import os 

7import subprocess 

8import tempfile 

9from leo.core import leoGlobals as g 

10#@+others 

11#@+node:ekr.20160306110233.1: ** class ExternalFile 

12class ExternalFile: 

13 """A class holding all data about an external file.""" 

14 

15 def __init__(self, c, ext, p, path, time): 

16 """Ctor for ExternalFile class.""" 

17 self.c = c 

18 self.ext = ext 

19 self.p = p and p.copy() # The nearest @<file> node. 

20 self.path = path 

21 # Inhibit endless dialog loop. See efc.idle_check_open_with_file. 

22 self.time = time 

23 

24 def __repr__(self): 

25 return f"<ExternalFile: {self.time:20} {g.shortFilename(self.path)}>" 

26 

27 __str__ = __repr__ 

28 #@+others 

29 #@+node:ekr.20161011174757.1: *3* ef.shortFileName 

30 def shortFileName(self): 

31 return g.shortFilename(self.path) 

32 #@+node:ekr.20161011174800.1: *3* ef.exists 

33 def exists(self): 

34 """Return True if the external file still exists.""" 

35 return g.os_path_exists(self.path) 

36 #@-others 

37#@+node:ekr.20150405073203.1: ** class ExternalFilesController 

38class ExternalFilesController: 

39 """ 

40 A class tracking changes to external files: 

41 

42 - temp files created by open-with commands. 

43 - external files corresponding to @file nodes. 

44 

45 This class raises a dialog when a file changes outside of Leo. 

46 

47 **Naming conventions**: 

48 

49 - d is always a dict created by the @open-with logic. 

50 This dict describes *only* how to open the file. 

51 

52 - ef is always an ExternalFiles instance. 

53 """ 

54 #@+others 

55 #@+node:ekr.20150404083533.1: *3* efc.ctor 

56 def __init__(self, c=None): 

57 """Ctor for ExternalFiles class.""" 

58 self.checksum_d = {} 

59 # Keys are full paths, values are file checksums. 

60 self.enabled_d = {} 

61 # For efc.on_idle. 

62 # Keys are commanders. 

63 # Values are cached @bool check-for-changed-external-file settings. 

64 self.files = [] 

65 # List of ExternalFile instances created by self.open_with. 

66 self.has_changed_d = {} 

67 # Keys are commanders. Values are bools. 

68 # Used only to limit traces. 

69 self.unchecked_commanders = [] 

70 # Copy of g.app.commanders() 

71 self.unchecked_files = [] 

72 # Copy of self file. Only one files is checked at idle time. 

73 self._time_d = {} 

74 # Keys are full paths, values are modification times. 

75 # DO NOT alter directly, use set_time(path) and 

76 # get_time(path), see set_time() for notes. 

77 self.yesno_all_answer = None # answer, 'yes-all', or 'no-all' 

78 g.app.idleTimeManager.add_callback(self.on_idle) 

79 #@+node:ekr.20150405105938.1: *3* efc.entries 

80 #@+node:ekr.20150405194745.1: *4* efc.check_overwrite (called from c.checkTimeStamp) 

81 def check_overwrite(self, c, path): 

82 """ 

83 Implements c.checkTimeStamp. 

84 

85 Return True if the file given by fn has not been changed 

86 since Leo read it or if the user agrees to overwrite it. 

87 """ 

88 if c.sqlite_connection and c.mFileName == path: 

89 # sqlite database file is never actually overwriten by Leo 

90 # so no need to check its timestamp. It is modified through 

91 # sqlite methods. 

92 return True 

93 if self.has_changed(path): 

94 val = self.ask(c, path) 

95 return val in ('yes', 'yes-all') # #1888 

96 return True 

97 #@+node:ekr.20031218072017.2613: *4* efc.destroy_frame 

98 def destroy_frame(self, frame): 

99 """ 

100 Close all "Open With" files associated with frame. 

101 Called by g.app.destroyWindow. 

102 """ 

103 files = [ef for ef in self.files if ef.c.frame == frame] 

104 paths = [ef.path for ef in files] 

105 for ef in files: 

106 self.destroy_temp_file(ef) 

107 self.files = [z for z in self.files if z.path not in paths] 

108 #@+node:ekr.20150407141838.1: *4* efc.find_path_for_node (called from vim.py) 

109 def find_path_for_node(self, p): 

110 """ 

111 Find the path corresponding to node p. 

112 called from vim.py. 

113 """ 

114 for ef in self.files: 

115 if ef.p and ef.p.v == p.v: 

116 path = ef.path 

117 break 

118 else: 

119 path = None 

120 return path 

121 #@+node:ekr.20150330033306.1: *4* efc.on_idle & helpers 

122 on_idle_count = 0 

123 

124 def on_idle(self): 

125 """ 

126 Check for changed open-with files and all external files in commanders 

127 for which @bool check_for_changed_external_file is True. 

128 """ 

129 # 

130 # #1240: Note: The "asking" dialog prevents idle time. 

131 # 

132 if not g.app or g.app.killed or g.app.restarting: # #1240. 

133 return 

134 self.on_idle_count += 1 

135 # New in Leo 5.7: always handle delayed requests. 

136 if g.app.windowList: 

137 c = g.app.log and g.app.log.c 

138 if c: 

139 c.outerUpdate() 

140 # Fix #262: Improve performance when @bool check-for-changed-external-files is True. 

141 if self.unchecked_files: 

142 # Check all external files. 

143 while self.unchecked_files: 

144 ef = self.unchecked_files.pop() # #1959: ensure progress. 

145 self.idle_check_open_with_file(c, ef) 

146 elif self.unchecked_commanders: 

147 # Check the next commander for which 

148 # @bool check_for_changed_external_file is True. 

149 c = self.unchecked_commanders.pop() 

150 self.idle_check_commander(c) 

151 else: 

152 # Add all commanders for which 

153 # @bool check_for_changed_external_file is True. 

154 self.unchecked_commanders = [ 

155 z for z in g.app.commanders() if self.is_enabled(z) 

156 ] 

157 self.unchecked_files = [z for z in self.files if z.exists()] 

158 #@+node:ekr.20150404045115.1: *5* efc.idle_check_commander 

159 def idle_check_commander(self, c): 

160 """ 

161 Check all external files corresponding to @<file> nodes in c for 

162 changes. 

163 """ 

164 # #1240: Check the .leo file itself. 

165 self.idle_check_leo_file(c) 

166 # 

167 # #1100: always scan the entire file for @<file> nodes. 

168 # #1134: Nested @<file> nodes are no longer valid, but this will do no harm. 

169 state = 'no' 

170 for p in c.all_unique_positions(): 

171 if not p.isAnyAtFileNode(): 

172 continue 

173 path = g.fullPath(c, p) 

174 if not self.has_changed(path): 

175 continue 

176 # Prevent further checks for path. 

177 self.set_time(path) 

178 self.checksum_d[path] = self.checksum(path) 

179 # Check file. 

180 if p.isAtAsisFileNode() or p.isAtNoSentFileNode(): 

181 # #1081: issue a warning. 

182 self.warn(c, path, p=p) 

183 continue 

184 if state in ('yes', 'no'): 

185 state = self.ask(c, path, p=p) 

186 if state in ('yes', 'yes-all'): 

187 c.redraw(p=p) 

188 c.refreshFromDisk(p) 

189 c.redraw() 

190 #@+node:ekr.20201207055713.1: *5* efc.idle_check_leo_file 

191 def idle_check_leo_file(self, c): 

192 """Check c's .leo file for external changes.""" 

193 path = c.fileName() 

194 if not self.has_changed(path): 

195 return 

196 # Always update the path & time to prevent future warnings. 

197 self.set_time(path) 

198 self.checksum_d[path] = self.checksum(path) 

199 # #1888: 

200 val = self.ask(c, path) 

201 if val in ('yes', 'yes-all'): 

202 # Do a complete restart of Leo. 

203 g.es_print('restarting Leo...') 

204 c.restartLeo() 

205 #@+node:ekr.20150407124259.1: *5* efc.idle_check_open_with_file & helper 

206 def idle_check_open_with_file(self, c, ef): 

207 """Update the open-with node given by ef.""" 

208 assert isinstance(ef, ExternalFile), ef 

209 if not ef.path or not os.path.exists(ef.path): 

210 return 

211 time = self.get_mtime(ef.path) 

212 if not time or time == ef.time: 

213 return 

214 # Inhibit endless dialog loop. 

215 ef.time = time 

216 # #1888: Handle all possible user responses to self.ask. 

217 val = self.ask(c, ef.path, p=ef.p.copy()) 

218 if val == 'yes-all': 

219 for ef in self.unchecked_files: 

220 self.update_open_with_node(ef) 

221 self.unchecked_files = [] 

222 elif val == 'no-all': 

223 self.unchecked_files = [] 

224 elif val == 'yes': 

225 self.update_open_with_node(ef) 

226 elif val == 'no': 

227 pass 

228 #@+node:ekr.20150407205631.1: *6* efc.update_open_with_node 

229 def update_open_with_node(self, ef): 

230 """Update the body text of ef.p to the contents of ef.path.""" 

231 assert isinstance(ef, ExternalFile), ef 

232 c, p = ef.c, ef.p.copy() 

233 g.blue(f"updated {p.h}") 

234 s, e = g.readFileIntoString(ef.path) 

235 p.b = s 

236 if c.config.getBool('open-with-goto-node-on-update'): 

237 c.selectPosition(p) 

238 if c.config.getBool('open-with-save-on-update'): 

239 c.save() 

240 else: 

241 p.setDirty() 

242 c.setChanged() 

243 #@+node:ekr.20150404082344.1: *4* efc.open_with & helpers 

244 def open_with(self, c, d): 

245 """ 

246 Called by c.openWith to handle items in the Open With... menu. 

247 

248 'd' a dict created from an @openwith settings node with these keys: 

249 

250 'args': the command-line arguments to be used to open the file. 

251 'ext': the file extension. 

252 'kind': the method used to open the file, such as subprocess.Popen. 

253 'name': menu label (used only by the menu code). 

254 'p': the nearest @<file> node, or None. 

255 'shortcut': menu shortcut (used only by the menu code). 

256 """ 

257 try: 

258 ext = d.get('ext') 

259 if not g.doHook('openwith1', c=c, p=c.p, v=c.p.v, d=d): 

260 root = d.get('p') 

261 if root: 

262 # Open the external file itself. 

263 path = g.fullPath(c, root) # #1914. 

264 self.open_file_in_external_editor(c, d, path) 

265 else: 

266 # Open a temp file containing just the node. 

267 p = c.p 

268 ext = self.compute_ext(c, p, ext) 

269 path = self.compute_temp_file_path(c, p, ext) 

270 if path: 

271 self.remove_temp_file(p, path) 

272 self.create_temp_file(c, ext, p) 

273 self.open_file_in_external_editor(c, d, path) 

274 g.doHook('openwith2', c=c, p=c.p, v=c.p.v, d=d) 

275 except Exception: 

276 g.es('unexpected exception in c.openWith') 

277 g.es_exception() 

278 #@+node:ekr.20031218072017.2824: *5* efc.compute_ext 

279 def compute_ext(self, c, p, ext): 

280 """Return the file extension to be used in the temp file.""" 

281 if ext: 

282 for ch in ("'", '"'): 

283 if ext.startswith(ch): 

284 ext = ext.strip(ch) 

285 if not ext: 

286 # if node is part of @<file> tree, get ext from file name 

287 for p2 in p.self_and_parents(copy=False): 

288 if p2.isAnyAtFileNode(): 

289 fn = p2.h.split(None, 1)[1] 

290 ext = g.os_path_splitext(fn)[1] 

291 break 

292 if not ext: 

293 theDict = c.scanAllDirectives(c.p) 

294 language = theDict.get('language') 

295 ext = g.app.language_extension_dict.get(language) 

296 if not ext: 

297 ext = '.txt' 

298 if ext[0] != '.': 

299 ext = '.' + ext 

300 return ext 

301 #@+node:ekr.20031218072017.2832: *5* efc.compute_temp_file_path & helpers 

302 def compute_temp_file_path(self, c, p, ext): 

303 """Return the path to the temp file for p and ext.""" 

304 if c.config.getBool('open-with-clean-filenames'): 

305 path = self.clean_file_name(c, ext, p) 

306 else: 

307 path = self.legacy_file_name(c, ext, p) 

308 if not path: 

309 g.error('c.temp_file_path failed') 

310 return path 

311 #@+node:ekr.20150406055221.2: *6* efc.clean_file_name 

312 def clean_file_name(self, c, ext, p): 

313 """Compute the file name when subdirectories mirror the node's hierarchy in Leo.""" 

314 use_extentions = c.config.getBool('open-with-uses-derived-file-extensions') 

315 ancestors, found = [], False 

316 for p2 in p.self_and_parents(copy=False): 

317 h = p2.anyAtFileNodeName() 

318 if not h: 

319 h = p2.h # Not an @file node: use the entire header 

320 elif use_extentions and not found: 

321 # Found the nearest ancestor @<file> node. 

322 found = True 

323 base, ext2 = g.os_path_splitext(h) 

324 if p2 == p: 

325 h = base 

326 if ext2: 

327 ext = ext2 

328 ancestors.append(g.sanitize_filename(h)) 

329 # The base directory is <tempdir>/Leo<id(v)>. 

330 ancestors.append("Leo" + str(id(p.v))) 

331 # Build temporary directories. 

332 td = os.path.abspath(tempfile.gettempdir()) 

333 while len(ancestors) > 1: 

334 td = os.path.join(td, ancestors.pop()) 

335 if not os.path.exists(td): 

336 os.mkdir(td) 

337 # Compute the full path. 

338 name = ancestors.pop() + ext 

339 path = os.path.join(td, name) 

340 return path 

341 #@+node:ekr.20150406055221.3: *6* efc.legacy_file_name 

342 def legacy_file_name(self, c, ext, p): 

343 """Compute a legacy file name for unsupported operating systems.""" 

344 try: 

345 leoTempDir = getpass.getuser() + "_" + "Leo" 

346 except Exception: 

347 leoTempDir = "LeoTemp" 

348 g.es("Could not retrieve your user name.") 

349 g.es(f"Temporary files will be stored in: {leoTempDir}") 

350 td = os.path.join(os.path.abspath(tempfile.gettempdir()), leoTempDir) 

351 if not os.path.exists(td): 

352 os.mkdir(td) 

353 name = g.sanitize_filename(p.h) + '_' + str(id(p.v)) + ext 

354 path = os.path.join(td, name) 

355 return path 

356 #@+node:ekr.20100203050306.5937: *5* efc.create_temp_file 

357 def create_temp_file(self, c, ext, p): 

358 """ 

359 Create the file used by open-with if necessary. 

360 Add the corresponding ExternalFile instance to self.files 

361 """ 

362 path = self.compute_temp_file_path(c, p, ext) 

363 exists = g.os_path_exists(path) 

364 # Compute encoding and s. 

365 d2 = c.scanAllDirectives(p) 

366 encoding = d2.get('encoding', None) 

367 if encoding is None: 

368 encoding = c.config.default_derived_file_encoding 

369 s = g.toEncodedString(p.b, encoding, reportErrors=True) 

370 # Write the file *only* if it doesn't exist. 

371 # No need to read the file: recomputing s above suffices. 

372 if not exists: 

373 try: 

374 with open(path, 'wb') as f: 

375 f.write(s) 

376 f.flush() 

377 except IOError: 

378 g.error(f"exception creating temp file: {path}") 

379 g.es_exception() 

380 return None 

381 # Add or update the external file entry. 

382 time = self.get_mtime(path) 

383 self.files = [z for z in self.files if z.path != path] 

384 self.files.append(ExternalFile(c, ext, p, path, time)) 

385 return path 

386 #@+node:ekr.20031218072017.2829: *5* efc.open_file_in_external_editor 

387 def open_file_in_external_editor(self, c, d, fn, testing=False): 

388 """ 

389 Open a file fn in an external editor. 

390 

391 This will be an entire external file, or a temp file for a single node. 

392 

393 d is a dictionary created from an @openwith settings node. 

394 

395 'args': the command-line arguments to be used to open the file. 

396 'ext': the file extension. 

397 'kind': the method used to open the file, such as subprocess.Popen. 

398 'name': menu label (used only by the menu code). 

399 'p': the nearest @<file> node, or None. 

400 'shortcut': menu shortcut (used only by the menu code). 

401 """ 

402 testing = testing or g.unitTesting 

403 arg_tuple = d.get('args', []) 

404 arg = ' '.join(arg_tuple) 

405 kind = d.get('kind') 

406 try: 

407 # All of these must be supported because they 

408 # could exist in @open-with nodes. 

409 command = '<no command>' 

410 if kind in ('os.system', 'os.startfile'): 

411 # New in Leo 5.7: 

412 # Use subProcess.Popen(..., shell=True) 

413 c_arg = self.join(arg, fn) 

414 if not testing: 

415 try: 

416 subprocess.Popen(c_arg, shell=True) 

417 except OSError: 

418 g.es_print('c_arg', repr(c_arg)) 

419 g.es_exception() 

420 elif kind == 'exec': 

421 g.es_print('open-with exec no longer valid.') 

422 elif kind == 'os.spawnl': 

423 filename = g.os_path_basename(arg) 

424 command = f"os.spawnl({arg},{filename},{fn})" 

425 if not testing: 

426 os.spawnl(os.P_NOWAIT, arg, filename, fn) 

427 elif kind == 'os.spawnv': 

428 filename = os.path.basename(arg_tuple[0]) 

429 vtuple = arg_tuple[1:] 

430 vtuple.insert(0, filename) 

431 # add the name of the program as the first argument. 

432 # Change suggested by Jim Sizelove. 

433 vtuple.append(fn) 

434 command = f"os.spawnv({vtuple})" 

435 if not testing: 

436 os.spawnv(os.P_NOWAIT, arg[0], vtuple) #??? 

437 elif kind == 'subprocess.Popen': 

438 c_arg = self.join(arg, fn) 

439 command = f"subprocess.Popen({c_arg})" 

440 if not testing: 

441 try: 

442 subprocess.Popen(c_arg, shell=True) 

443 except OSError: 

444 g.es_print('c_arg', repr(c_arg)) 

445 g.es_exception() 

446 elif hasattr(kind, '__call__'): 

447 # Invoke openWith like this: 

448 # c.openWith(data=[func,None,None]) 

449 # func will be called with one arg, the filename 

450 command = f"{kind}({fn})" 

451 if not testing: 

452 kind(fn) 

453 else: 

454 command = 'bad command:' + str(kind) 

455 if not testing: 

456 g.trace(command) 

457 return command # for unit testing. 

458 except Exception: 

459 g.es('exception executing open-with command:', command) 

460 g.es_exception() 

461 return f"oops: {command}" 

462 #@+node:ekr.20190123051253.1: *5* efc.remove_temp_file 

463 def remove_temp_file(self, p, path): 

464 """ 

465 Remove any existing *temp* file for p and path, updating self.files. 

466 """ 

467 for ef in self.files: 

468 if path and path == ef.path and p.v == ef.p.v: 

469 self.destroy_temp_file(ef) 

470 self.files = [z for z in self.files if z != ef] 

471 return 

472 #@+node:ekr.20150404092538.1: *4* efc.shut_down 

473 def shut_down(self): 

474 """ 

475 Destroy all temporary open-with files. 

476 This may fail if the files are still open. 

477 

478 Called by g.app.finishQuit. 

479 """ 

480 # Dont call g.es or g.trace! The log stream no longer exists. 

481 for ef in self.files[:]: 

482 self.destroy_temp_file(ef) 

483 self.files = [] 

484 #@+node:ekr.20150405110219.1: *3* efc.utilities 

485 # pylint: disable=no-value-for-parameter 

486 #@+node:ekr.20150405200212.1: *4* efc.ask 

487 def ask(self, c, path, p=None): 

488 """ 

489 Ask user whether to overwrite an @<file> tree. 

490 

491 Return one of ('yes', 'no', 'yes-all', 'no-all') 

492 """ 

493 if g.unitTesting: 

494 return False 

495 if c not in g.app.commanders(): 

496 return False 

497 is_leo = path.endswith(('.leo', '.db')) 

498 is_external_file = not is_leo 

499 # 

500 # Create the message. 

501 message1 = f"{g.splitLongFileName(path)} has changed outside Leo.\n" 

502 if is_leo: 

503 message2 = 'Restart Leo?' 

504 elif p: 

505 message2 = f"Reload {p.h}?" 

506 else: 

507 for ef in self.files: 

508 if ef.path == path: 

509 message2 = f"Reload {ef.p.h}?" 

510 break 

511 else: 

512 message2 = f"Reload {path}?" 

513 # 

514 # #1240: Note: This dialog prevents idle time. 

515 result = g.app.gui.runAskYesNoDialog(c, 

516 'Overwrite the version in Leo?', 

517 message1 + message2, 

518 yes_all=is_external_file, 

519 no_all=is_external_file, 

520 ) 

521 # 

522 # #1961. Re-init the checksum to suppress concurent dialogs. 

523 self.checksum_d[path] = self.checksum(path) 

524 # 

525 # #1888: return one of ('yes', 'no', 'yes-all', 'no-all') 

526 return result.lower() if result else 'no' 

527 #@+node:ekr.20150404052819.1: *4* efc.checksum 

528 def checksum(self, path): 

529 """Return the checksum of the file at the given path.""" 

530 import hashlib 

531 # #1454: Explicitly close the file. 

532 with open(path, 'rb') as f: 

533 s = f.read() 

534 return hashlib.md5(s).hexdigest() 

535 #@+node:ekr.20031218072017.2614: *4* efc.destroy_temp_file 

536 def destroy_temp_file(self, ef): 

537 """Destroy the *temp* file corresponding to ef, an ExternalFile instance.""" 

538 # Do not use g.trace here. 

539 if ef.path and g.os_path_exists(ef.path): 

540 try: 

541 os.remove(ef.path) 

542 except Exception: 

543 pass 

544 #@+node:ekr.20150407204201.1: *4* efc.get_mtime 

545 def get_mtime(self, path): 

546 """Return the modification time for the path.""" 

547 return g.os_path_getmtime(g.os_path_realpath(path)) 

548 #@+node:ekr.20150405122428.1: *4* efc.get_time 

549 def get_time(self, path): 

550 """ 

551 return timestamp for path 

552 

553 see set_time() for notes 

554 """ 

555 return self._time_d.get(g.os_path_realpath(path)) 

556 #@+node:ekr.20150403045207.1: *4* efc.has_changed 

557 def has_changed(self, path): 

558 """Return True if the file at path has changed outside of Leo.""" 

559 if not path: 

560 return False 

561 if not g.os_path_exists(path): 

562 return False 

563 if g.os_path_isdir(path): 

564 return False 

565 # 

566 # First, check the modification times. 

567 old_time = self.get_time(path) 

568 new_time = self.get_mtime(path) 

569 if not old_time: 

570 # Initialize. 

571 self.set_time(path, new_time) 

572 self.checksum_d[path] = self.checksum(path) 

573 return False 

574 if old_time == new_time: 

575 return False 

576 # 

577 # Check the checksums *only* if the mod times don't match. 

578 old_sum = self.checksum_d.get(path) 

579 new_sum = self.checksum(path) 

580 if new_sum == old_sum: 

581 # The modtime changed, but it's contents didn't. 

582 # Update the time, so we don't keep checking the checksums. 

583 # Return False so we don't prompt the user for an update. 

584 self.set_time(path, new_time) 

585 return False 

586 # The file has really changed. 

587 assert old_time, path 

588 return True 

589 #@+node:ekr.20150405104340.1: *4* efc.is_enabled 

590 def is_enabled(self, c): 

591 """Return the cached @bool check_for_changed_external_file setting.""" 

592 d = self.enabled_d 

593 val = d.get(c) 

594 if val is None: 

595 val = c.config.getBool('check-for-changed-external-files', default=False) 

596 d[c] = val 

597 return val 

598 #@+node:ekr.20150404083049.1: *4* efc.join 

599 def join(self, s1, s2): 

600 """Return s1 + ' ' + s2""" 

601 return f"{s1} {s2}" 

602 #@+node:tbrown.20150904102518.1: *4* efc.set_time 

603 def set_time(self, path, new_time=None): 

604 """ 

605 Implements c.setTimeStamp. 

606 

607 Update the timestamp for path. 

608 

609 NOTE: file paths with symbolic links occur with and without those links 

610 resolved depending on the code call path. This inconsistency is 

611 probably not Leo's fault but an underlying Python issue. 

612 Hence the need to call realpath() here. 

613 """ 

614 t = new_time or self.get_mtime(path) 

615 self._time_d[g.os_path_realpath(path)] = t 

616 #@+node:ekr.20190218055230.1: *4* efc.warn 

617 def warn(self, c, path, p): 

618 """ 

619 Warn that an @asis or @nosent node has been changed externally. 

620 

621 There is *no way* to update the tree automatically. 

622 """ 

623 if g.unitTesting or c not in g.app.commanders(): 

624 return 

625 if not p: 

626 g.trace('NO P') 

627 return 

628 g.app.gui.runAskOkDialog( 

629 c=c, 

630 message='\n'.join([ 

631 f"{g.splitLongFileName(path)} has changed outside Leo.\n", 

632 'Leo can not update this file automatically.\n', 

633 f"This file was created from {p.h}.\n", 

634 'Warning: refresh-from-disk will destroy all children.' 

635 ]), 

636 title='External file changed', 

637 ) 

638 #@-others 

639#@-others 

640#@@language python 

641#@@tabwidth -4 

642#@@pagewidth 70 

643#@-leo