Coverage for C:\leo.repo\leo-editor\leo\core\leoGlobals.py : 42%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2#@+leo-ver=5-thin
3#@+node:ekr.20031218072017.3093: * @file leoGlobals.py
4#@@first
5"""
6Global constants, variables and utility functions used throughout Leo.
8Important: This module imports no other Leo module.
9"""
10#@+<< imports >>
11#@+node:ekr.20050208101229: ** << imports >> (leoGlobals)
12import binascii
13import codecs
14import fnmatch
15from functools import reduce
16import gc
17import gettext
18import glob
19import importlib
20import inspect
21import io
22import operator
23import os
24from pathlib import Path
26# import pdb # Do NOT import pdb here!
27 # We shall define pdb as a _function_ below.
28import re
29import shlex
30import string
31import sys
32import subprocess
33import tempfile
34import textwrap
35import time
36import traceback
37import types
38from typing import TYPE_CHECKING
39from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Sequence, Set, Tuple, Union
40import unittest
41import urllib
42import urllib.parse as urlparse
43import webbrowser
44#
45# Leo never imports any other Leo module.
46if TYPE_CHECKING: # Always False at runtime.
47 from leo.core.leoCommands import Commands as Cmdr
48 from leo.core.leoNodes import Position as Pos
49 from leo.core.leoNodes import VNode
50else:
51 Cmdr = Pos = VNode = Any
52#
53# Abbreviations...
54StringIO = io.StringIO
55#@-<< imports >>
56in_bridge = False
57 # Set to True in leoBridge.py just before importing leo.core.leoApp.
58 # This tells leoApp to load a null Gui.
59in_vs_code = False # #2098.
60minimum_python_version = '3.6' # #1215.
61isPython3 = sys.version_info >= (3, 0, 0)
62isMac = sys.platform.startswith('darwin')
63isWindows = sys.platform.startswith('win')
64#@+<< define g.globalDirectiveList >>
65#@+node:EKR.20040610094819: ** << define g.globalDirectiveList >>
66# Visible externally so plugins may add to the list of directives.
67# The atFile write logic uses this, but not the atFile read logic.
68globalDirectiveList = [
69 # Order does not matter.
70 'all',
71 'beautify',
72 'colorcache', 'code', 'color', 'comment', 'c',
73 'delims', 'doc',
74 'encoding',
75 # 'end_raw', # #2276.
76 'first', 'header', 'ignore',
77 'killbeautify', 'killcolor',
78 'language', 'last', 'lineending',
79 'markup',
80 'nobeautify',
81 'nocolor-node', 'nocolor', 'noheader', 'nowrap',
82 'nopyflakes', # Leo 6.1.
83 'nosearch', # Leo 5.3.
84 'others', 'pagewidth', 'path', 'quiet',
85 # 'raw', # #2276.
86 'section-delims', # Leo 6.6. #2276.
87 'silent',
88 'tabwidth', 'terse',
89 'unit', 'verbose', 'wrap',
90]
92directives_pat = None # Set below.
93#@-<< define g.globalDirectiveList >>
94#@+<< define global decorator dicts >>
95#@+node:ekr.20150510103918.1: ** << define global decorator dicts >> (leoGlobals.py)
96#@@nobeautify
97#@@language rest
98#@+at
99# The cmd_instance_dict supports per-class @cmd decorators. For example, the
100# following appears in leo.commands.
101#
102# def cmd(name: Any) -> Any:
103# """Command decorator for the abbrevCommands class."""
104# return g.new_cmd_decorator(name, ['c', 'abbrevCommands',])
105#
106# For commands based on functions, use the @g.command decorator.
107#@@c
108#@@language python
110global_commands_dict = {}
112cmd_instance_dict = {
113 # Keys are class names, values are attribute chains.
114 'AbbrevCommandsClass': ['c', 'abbrevCommands'],
115 'AtFile': ['c', 'atFileCommands'],
116 'AutoCompleterClass': ['c', 'k', 'autoCompleter'],
117 'ChapterController': ['c', 'chapterController'],
118 'Commands': ['c'],
119 'ControlCommandsClass': ['c', 'controlCommands'],
120 'DebugCommandsClass': ['c', 'debugCommands'],
121 'EditCommandsClass': ['c', 'editCommands'],
122 'EditFileCommandsClass': ['c', 'editFileCommands'],
123 'FileCommands': ['c', 'fileCommands'],
124 'HelpCommandsClass': ['c', 'helpCommands'],
125 'KeyHandlerClass': ['c', 'k'],
126 'KeyHandlerCommandsClass': ['c', 'keyHandlerCommands'],
127 'KillBufferCommandsClass': ['c', 'killBufferCommands'],
128 'LeoApp': ['g', 'app'],
129 'LeoFind': ['c', 'findCommands'],
130 'LeoImportCommands': ['c', 'importCommands'],
131 # 'MacroCommandsClass': ['c', 'macroCommands'],
132 'PrintingController': ['c', 'printingController'],
133 'RectangleCommandsClass': ['c', 'rectangleCommands'],
134 'RstCommands': ['c', 'rstCommands'],
135 'SpellCommandsClass': ['c', 'spellCommands'],
136 'Undoer': ['c', 'undoer'],
137 'VimCommands': ['c', 'vimCommands'],
138}
139#@-<< define global decorator dicts >>
140#@+<< define g.decorators >>
141#@+node:ekr.20150508165324.1: ** << define g.Decorators >>
142#@+others
143#@+node:ekr.20150510104148.1: *3* g.check_cmd_instance_dict
144def check_cmd_instance_dict(c: Cmdr, g: Any) -> None:
145 """
146 Check g.check_cmd_instance_dict.
147 This is a permanent unit test, called from c.finishCreate.
148 """
149 d = cmd_instance_dict
150 for key in d:
151 ivars = d.get(key)
152 obj = ivars2instance(c, g, ivars) # type:ignore
153 # Produces warnings.
154 if obj:
155 name = obj.__class__.__name__
156 if name != key:
157 g.trace('class mismatch', key, name)
158#@+node:ville.20090521164644.5924: *3* g.command (decorator)
159class Command:
160 """
161 A global decorator for creating commands.
163 This is the recommended way of defining all new commands, including
164 commands that could befined inside a class. The typical usage is:
166 @g.command('command-name')
167 def A_Command(event):
168 c = event.get('c')
169 ...
171 g can *not* be used anywhere in this class!
172 """
174 def __init__(self, name: str, **kwargs: Any) -> None:
175 """Ctor for command decorator class."""
176 self.name = name
178 def __call__(self, func: Callable) -> Callable:
179 """Register command for all future commanders."""
180 global_commands_dict[self.name] = func
181 if app:
182 for c in app.commanders():
183 c.k.registerCommand(self.name, func)
184 # Inject ivars for plugins_menu.py.
185 func.__func_name__ = func.__name__ # For leoInteg.
186 func.is_command = True
187 func.command_name = self.name
188 return func
190command = Command
191#@+node:ekr.20171124070654.1: *3* g.command_alias
192def command_alias(alias: str, func: Callable) -> None:
193 """Create an alias for the *already defined* method in the Commands class."""
194 from leo.core import leoCommands
195 assert hasattr(leoCommands.Commands, func.__name__)
196 funcToMethod(func, leoCommands.Commands, alias)
197#@+node:ekr.20171123095526.1: *3* g.commander_command (decorator)
198class CommanderCommand:
199 """
200 A global decorator for creating commander commands, that is, commands
201 that were formerly methods of the Commands class in leoCommands.py.
203 Usage:
205 @g.command('command-name')
206 def command_name(self, *args, **kwargs):
207 ...
209 The decorator injects command_name into the Commander class and calls
210 funcToMethod so the ivar will be injected in all future commanders.
212 g can *not* be used anywhere in this class!
213 """
215 def __init__(self, name: str, **kwargs: Any) -> None:
216 """Ctor for command decorator class."""
217 self.name = name
219 def __call__(self, func: Callable) -> Callable:
220 """Register command for all future commanders."""
222 def commander_command_wrapper(event: Any) -> None:
223 c = event.get('c')
224 method = getattr(c, func.__name__, None)
225 method(event=event)
227 # Inject ivars for plugins_menu.py.
228 commander_command_wrapper.__func_name__ = func.__name__ # For leoInteg.
229 commander_command_wrapper.__name__ = self.name
230 commander_command_wrapper.__doc__ = func.__doc__
231 global_commands_dict[self.name] = commander_command_wrapper
232 if app:
233 from leo.core import leoCommands
234 funcToMethod(func, leoCommands.Commands)
235 for c in app.commanders():
236 c.k.registerCommand(self.name, func)
237 # Inject ivars for plugins_menu.py.
238 func.is_command = True
239 func.command_name = self.name
240 return func
242commander_command = CommanderCommand
243#@+node:ekr.20150508164812.1: *3* g.ivars2instance
244def ivars2instance(c: Cmdr, g: Any, ivars: List[str]) -> Any:
245 """
246 Return the instance of c given by ivars.
247 ivars is a list of strings.
248 A special case: ivars may be 'g', indicating the leoGlobals module.
249 """
250 if not ivars:
251 g.trace('can not happen: no ivars')
252 return None
253 ivar = ivars[0]
254 if ivar not in ('c', 'g'):
255 g.trace('can not happen: unknown base', ivar)
256 return None
257 obj = c if ivar == 'c' else g
258 for ivar in ivars[1:]:
259 obj = getattr(obj, ivar, None)
260 if not obj:
261 g.trace('can not happen: unknown attribute', obj, ivar, ivars)
262 break
263 return obj
264#@+node:ekr.20150508134046.1: *3* g.new_cmd_decorator (decorator)
265def new_cmd_decorator(name: str, ivars: List[str]) -> Callable:
266 """
267 Return a new decorator for a command with the given name.
268 Compute the class *instance* using the ivar string or list.
270 Don't even think about removing the @cmd decorators!
271 See https://github.com/leo-editor/leo-editor/issues/325
272 """
274 def _decorator(func: Callable) -> Callable:
276 def new_cmd_wrapper(event: Any) -> None:
277 if isinstance(event, dict):
278 c = event.get('c')
279 else:
280 c = event.c
281 self = g.ivars2instance(c, g, ivars)
282 try:
283 func(self, event=event)
284 # Don't use a keyword for self.
285 # This allows the VimCommands class to use vc instead.
286 except Exception:
287 g.es_exception()
289 new_cmd_wrapper.__func_name__ = func.__name__ # For leoInteg.
290 new_cmd_wrapper.__name__ = name
291 new_cmd_wrapper.__doc__ = func.__doc__
292 global_commands_dict[name] = new_cmd_wrapper
293 # Put the *wrapper* into the global dict.
294 return func
295 # The decorator must return the func itself.
297 return _decorator
298#@-others
299#@-<< define g.decorators >>
300#@+<< define regex's >>
301#@+node:ekr.20200810093517.1: ** << define regex's >>
302# Regex used by this module, and in leoColorizer.py.
303g_language_pat = re.compile(r'^@language\s+(\w+)+', re.MULTILINE)
304#
305# Patterns used only in this module...
307# g_is_directive_pattern excludes @encoding.whatever and @encoding(whatever)
308# It must allow @language python, @nocolor-node, etc.
309g_is_directive_pattern = re.compile(r'^\s*@([\w-]+)\s*')
310g_noweb_root = re.compile('<' + '<' + '*' + '>' + '>' + '=', re.MULTILINE)
311g_tabwidth_pat = re.compile(r'(^@tabwidth)', re.MULTILINE)
312# #2267: Support for @section-delims.
313g_section_delims_pat = re.compile(r'^@section-delims[ \t]+([^ \w\n\t]+)[ \t]+([^ \w\n\t]+)[ \t]*$')
314#@-<< define regex's >>
315tree_popup_handlers: List[Callable] = [] # Set later.
316user_dict: Dict[Any, Any] = {}
317 # Non-persistent dictionary for free use by scripts and plugins.
318app: Any = None # The singleton app object. Set by runLeo.py.
319# Global status vars.
320inScript = False # A synonym for app.inScript
321unitTesting = False # A synonym for app.unitTesting.
322#@+others
323#@+node:ekr.20201211182722.1: ** g.Backup
324#@+node:ekr.20201211182659.1: *3* g.standard_timestamp
325def standard_timestamp() -> str:
326 """Return a reasonable timestamp."""
327 return time.strftime("%Y%m%d-%H%M%S")
328#@+node:ekr.20201211183100.1: *3* g.get_backup_directory
329def get_backup_path(sub_directory: str) -> Optional[str]:
330 """
331 Return the full path to the subdirectory of the main backup directory.
333 The main backup directory is computed as follows:
335 1. os.environ['LEO_BACKUP']
336 2. ~/Backup
337 """
338 # Compute the main backup directory.
339 # First, try the LEO_BACKUP directory.
340 backup = None
341 try:
342 backup = os.environ['LEO_BACKUP']
343 if not os.path.exists(backup):
344 backup = None
345 except KeyError:
346 pass
347 except Exception:
348 g.es_exception()
349 # Second, try ~/Backup.
350 if not backup:
351 backup = os.path.join(str(Path.home()), 'Backup')
352 if not os.path.exists(backup):
353 backup = None
354 if not backup:
355 return None
356 # Compute the path to backup/sub_directory
357 directory = os.path.join(backup, sub_directory)
358 return directory if os.path.exists(directory) else None
359#@+node:ekr.20140711071454.17644: ** g.Classes & class accessors
360#@+node:ekr.20120123115816.10209: *3* class g.BindingInfo & isBindingInfo
361class BindingInfo:
362 """
363 A class representing any kind of key binding line.
365 This includes other information besides just the KeyStroke.
366 """
367 # Important: The startup code uses this class,
368 # so it is convenient to define it in leoGlobals.py.
369 #@+others
370 #@+node:ekr.20120129040823.10254: *4* bi.__init__
371 def __init__(
372 self,
373 kind: str,
374 commandName: str='',
375 func: Any=None,
376 nextMode: Any=None,
377 pane: Any=None,
378 stroke: Any=None,
379 ) -> None:
380 if not g.isStrokeOrNone(stroke):
381 g.trace('***** (BindingInfo) oops', repr(stroke))
382 self.kind = kind
383 self.commandName = commandName
384 self.func = func
385 self.nextMode = nextMode
386 self.pane = pane
387 self.stroke = stroke
388 # The *caller* must canonicalize the shortcut.
389 #@+node:ekr.20120203153754.10031: *4* bi.__hash__
390 def __hash__(self) -> Any:
391 return self.stroke.__hash__() if self.stroke else 0
392 #@+node:ekr.20120125045244.10188: *4* bi.__repr__ & ___str_& dump
393 def __repr__(self) -> str:
394 return self.dump()
396 __str__ = __repr__
398 def dump(self) -> str:
399 result = [f"BindingInfo {self.kind:17}"]
400 # Print all existing ivars.
401 table = ('commandName', 'func', 'nextMode', 'pane', 'stroke')
402 for ivar in table:
403 if hasattr(self, ivar):
404 val = getattr(self, ivar)
405 if val not in (None, 'none', 'None', ''):
406 if ivar == 'func':
407 # pylint: disable=no-member
408 val = val.__name__
409 s = f"{ivar}: {val!r}"
410 result.append(s)
411 # Clearer w/o f-string.
412 return "[%s]" % ' '.join(result).strip()
413 #@+node:ekr.20120129040823.10226: *4* bi.isModeBinding
414 def isModeBinding(self) -> bool:
415 return self.kind.startswith('*mode')
416 #@-others
417def isBindingInfo(obj: Any) -> bool:
418 return isinstance(obj, BindingInfo)
419#@+node:ekr.20031218072017.3098: *3* class g.Bunch (Python Cookbook)
420class Bunch:
421 """
422 From The Python Cookbook:
424 Create a Bunch whenever you want to group a few variables:
426 point = Bunch(datum=y, squared=y*y, coord=x)
428 You can read/write the named attributes you just created, add others,
429 del some of them, etc::
431 if point.squared > threshold:
432 point.isok = True
433 """
435 def __init__(self, **keywords: Any) -> None:
436 self.__dict__.update(keywords)
438 def __repr__(self) -> str:
439 return self.toString()
441 def ivars(self) -> List:
442 return sorted(self.__dict__)
444 def keys(self) -> List:
445 return sorted(self.__dict__)
447 def toString(self) -> str:
448 tag = self.__dict__.get('tag')
449 entries = [
450 f"{key}: {str(self.__dict__.get(key)) or repr(self.__dict__.get(key))}"
451 for key in self.ivars() if key != 'tag'
452 ]
453 # Fail.
454 result = [f'g.Bunch({tag or ""})']
455 result.extend(entries)
456 return '\n '.join(result) + '\n'
458 # Used by new undo code.
460 def __setitem__(self, key: str, value: Any) -> Any:
461 """Support aBunch[key] = val"""
462 return operator.setitem(self.__dict__, key, value)
464 def __getitem__(self, key: str) -> Any:
465 """Support aBunch[key]"""
466 # g.pr('g.Bunch.__getitem__', key)
467 return operator.getitem(self.__dict__, key)
469 def get(self, key: str, theDefault: Any=None) -> Any:
470 return self.__dict__.get(key, theDefault)
472 def __contains__(self, key: str) -> bool: # New.
473 # g.pr('g.Bunch.__contains__', key in self.__dict__, key)
474 return key in self.__dict__
476bunch = Bunch
477#@+node:ekr.20120219154958.10492: *3* class g.EmergencyDialog
478class EmergencyDialog:
479 """A class that creates an tkinter dialog with a single OK button."""
480 #@+others
481 #@+node:ekr.20120219154958.10493: *4* emergencyDialog.__init__
482 def __init__(self, title: str, message: str) -> None:
483 """Constructor for the leoTkinterDialog class."""
484 self.answer = None # Value returned from run()
485 self.title = title
486 self.message = message
487 self.buttonsFrame = None # Frame to hold typical dialog buttons.
488 self.defaultButtonCommand = None
489 # Command to call when user closes the window
490 # by clicking the close box.
491 self.frame = None # The outermost frame.
492 self.root = None # Created in createTopFrame.
493 self.top = None # The toplevel Tk widget.
494 self.createTopFrame()
495 buttons = [{
496 "text": "OK",
497 "command": self.okButton,
498 "default": True,
499 }]
500 self.createButtons(buttons)
501 self.top.bind("<Key>", self.onKey)
502 #@+node:ekr.20120219154958.10494: *4* emergencyDialog.createButtons
503 def createButtons(self, buttons: List[Dict[str, Any]]) -> List[Any]:
504 """Create a row of buttons.
506 buttons is a list of dictionaries containing
507 the properties of each button.
508 """
509 import tkinter as Tk
510 assert self.frame
511 self.buttonsFrame = f = Tk.Frame(self.top)
512 f.pack(side="top", padx=30)
513 # Buttons is a list of dictionaries, with an empty dictionary
514 # at the end if there is only one entry.
515 buttonList = []
516 for d in buttons:
517 text = d.get("text", "<missing button name>")
518 isDefault = d.get("default", False)
519 underline = d.get("underline", 0)
520 command = d.get("command", None)
521 bd = 4 if isDefault else 2
522 b = Tk.Button(f, width=6, text=text, bd=bd,
523 underline=underline, command=command)
524 b.pack(side="left", padx=5, pady=10)
525 buttonList.append(b)
526 if isDefault and command:
527 self.defaultButtonCommand = command
528 return buttonList
529 #@+node:ekr.20120219154958.10495: *4* emergencyDialog.createTopFrame
530 def createTopFrame(self) -> None:
531 """Create the Tk.Toplevel widget for a leoTkinterDialog."""
532 import tkinter as Tk
533 self.root = Tk.Tk() # type:ignore
534 self.top = Tk.Toplevel(self.root) # type:ignore
535 self.top.title(self.title)
536 self.root.withdraw()
537 self.frame = Tk.Frame(self.top) # type:ignore
538 self.frame.pack(side="top", expand=1, fill="both")
539 label = Tk.Label(self.frame, text=self.message, bg='white')
540 label.pack(pady=10)
541 #@+node:ekr.20120219154958.10496: *4* emergencyDialog.okButton
542 def okButton(self) -> None:
543 """Do default click action in ok button."""
544 self.top.destroy()
545 self.top = None
546 #@+node:ekr.20120219154958.10497: *4* emergencyDialog.onKey
547 def onKey(self, event: Any) -> None:
548 """Handle Key events in askOk dialogs."""
549 self.okButton()
550 #@+node:ekr.20120219154958.10498: *4* emergencyDialog.run
551 def run(self) -> None:
552 """Run the modal emergency dialog."""
553 # Suppress f-stringify.
554 self.top.geometry("%dx%d%+d%+d" % (300, 200, 50, 50))
555 self.top.lift()
556 self.top.grab_set() # Make the dialog a modal dialog.
557 self.root.wait_window(self.top)
558 #@-others
559#@+node:ekr.20120123143207.10223: *3* class g.GeneralSetting
560# Important: The startup code uses this class,
561# so it is convenient to define it in leoGlobals.py.
564class GeneralSetting:
565 """A class representing any kind of setting except shortcuts."""
567 def __init__(
568 self,
569 kind: str,
570 encoding: str=None,
571 ivar: str=None,
572 setting: str=None,
573 val: Any=None,
574 path: str=None,
575 tag: str='setting',
576 unl: str=None,
577 ) -> None:
578 self.encoding = encoding
579 self.ivar = ivar
580 self.kind = kind
581 self.path = path
582 self.unl = unl
583 self.setting = setting
584 self.val = val
585 self.tag = tag
587 def __repr__(self) -> str:
588 # Better for g.printObj.
589 val = str(self.val).replace('\n', ' ')
590 return (
591 f"GS: {g.shortFileName(self.path):20} "
592 f"{self.kind:7} = {g.truncate(val, 50)}")
594 dump = __repr__
595 __str__ = __repr__
596#@+node:ekr.20120201164453.10090: *3* class g.KeyStroke & isStroke/OrNone
597class KeyStroke:
598 """
599 A class that represent any key stroke or binding.
601 stroke.s is the "canonicalized" stroke.
602 """
603 #@+others
604 #@+node:ekr.20180414195401.2: *4* ks.__init__
605 def __init__(self, binding: str) -> None:
607 if binding:
608 self.s = self.finalize_binding(binding)
609 else:
610 self.s = None # type:ignore
611 #@+node:ekr.20120203053243.10117: *4* ks.__eq__, etc
612 #@+at All these must be defined in order to say, for example:
613 # for key in sorted(d)
614 # where the keys of d are KeyStroke objects.
615 #@@c
617 def __eq__(self, other: Any) -> bool:
618 if not other:
619 return False
620 if hasattr(other, 's'):
621 return self.s == other.s
622 return self.s == other
624 def __lt__(self, other: Any) -> bool:
625 if not other:
626 return False
627 if hasattr(other, 's'):
628 return self.s < other.s
629 return self.s < other
631 def __le__(self, other: Any) -> bool:
632 return self.__lt__(other) or self.__eq__(other)
634 def __ne__(self, other: Any) -> bool:
635 return not self.__eq__(other)
637 def __gt__(self, other: Any) -> bool:
638 return not self.__lt__(other) and not self.__eq__(other)
640 def __ge__(self, other: Any) -> bool:
641 return not self.__lt__(other)
642 #@+node:ekr.20120203053243.10118: *4* ks.__hash__
643 # Allow KeyStroke objects to be keys in dictionaries.
645 def __hash__(self) -> Any:
646 return self.s.__hash__() if self.s else 0
647 #@+node:ekr.20120204061120.10067: *4* ks.__repr___ & __str__
648 def __repr__(self) -> str:
649 return f"<KeyStroke: {repr(self.s)}>"
651 def __str__(self) -> str:
652 return repr(self.s)
653 #@+node:ekr.20180417160703.1: *4* ks.dump
654 def dump(self) -> None:
655 """Show results of printable chars."""
656 for i in range(128):
657 s = chr(i)
658 stroke = g.KeyStroke(s)
659 if stroke.s != s:
660 print(f"{i:2} {s!r:10} {stroke.s!r}")
661 for ch in ('backspace', 'linefeed', 'return', 'tab'):
662 stroke = g.KeyStroke(ch)
663 print(f'{"":2} {ch!r:10} {stroke.s!r}')
664 #@+node:ekr.20180415082249.1: *4* ks.finalize_binding
665 def finalize_binding(self, binding: str) -> str:
667 trace = False and 'keys' in g.app.debug
668 # This trace is good for devs only.
669 self.mods = self.find_mods(binding)
670 s = self.strip_mods(binding)
671 s = self.finalize_char(s)
672 # May change self.mods.
673 mods = ''.join([f"{z.capitalize()}+" for z in self.mods])
674 if trace and 'meta' in self.mods:
675 g.trace(f"{binding:20}:{self.mods:>20} ==> {mods+s}")
676 return mods + s
677 #@+node:ekr.20180415083926.1: *4* ks.finalize_char & helper
678 def finalize_char(self, s: str) -> str:
679 """Perform very-last-minute translations on bindings."""
680 #
681 # Retain "bigger" spelling for gang-of-four bindings with modifiers.
682 shift_d = {
683 'bksp': 'BackSpace',
684 'backspace': 'BackSpace',
685 'backtab': 'Tab', # The shift mod will convert to 'Shift+Tab',
686 'linefeed': 'Return',
687 '\r': 'Return',
688 'return': 'Return',
689 'tab': 'Tab',
690 }
691 if self.mods and s.lower() in shift_d:
692 # Returning '' breaks existing code.
693 return shift_d.get(s.lower()) # type:ignore
694 #
695 # Make all other translations...
696 #
697 # This dict ensures proper capitalization.
698 # It also translates legacy Tk binding names to ascii chars.
699 translate_d = {
700 #
701 # The gang of four...
702 'bksp': 'BackSpace',
703 'backspace': 'BackSpace',
704 'backtab': 'Tab', # The shift mod will convert to 'Shift+Tab',
705 'linefeed': '\n',
706 '\r': '\n',
707 'return': '\n',
708 'tab': 'Tab',
709 #
710 # Special chars...
711 'delete': 'Delete',
712 'down': 'Down',
713 'end': 'End',
714 'enter': 'Enter',
715 'escape': 'Escape',
716 'home': 'Home',
717 'insert': 'Insert',
718 'left': 'Left',
719 'next': 'Next',
720 'prior': 'Prior',
721 'right': 'Right',
722 'up': 'Up',
723 #
724 # Qt key names...
725 'del': 'Delete',
726 'dnarrow': 'Down',
727 'esc': 'Escape',
728 'ins': 'Insert',
729 'ltarrow': 'Left',
730 'pagedn': 'Next',
731 'pageup': 'Prior',
732 'pgdown': 'Next',
733 'pgup': 'Prior',
734 'rtarrow': 'Right',
735 'uparrow': 'Up',
736 #
737 # Legacy Tk binding names...
738 "ampersand": "&",
739 "asciicircum": "^",
740 "asciitilde": "~",
741 "asterisk": "*",
742 "at": "@",
743 "backslash": "\\",
744 "bar": "|",
745 "braceleft": "{",
746 "braceright": "}",
747 "bracketleft": "[",
748 "bracketright": "]",
749 "colon": ":",
750 "comma": ",",
751 "dollar": "$",
752 "equal": "=",
753 "exclam": "!",
754 "greater": ">",
755 "less": "<",
756 "minus": "-",
757 "numbersign": "#",
758 "quotedbl": '"',
759 "quoteright": "'",
760 "parenleft": "(",
761 "parenright": ")",
762 "percent": "%",
763 "period": ".",
764 "plus": "+",
765 "question": "?",
766 "quoteleft": "`",
767 "semicolon": ";",
768 "slash": "/",
769 "space": " ",
770 "underscore": "_",
771 }
772 #
773 # pylint: disable=undefined-loop-variable
774 # Looks like a pylint bug.
775 if s in (None, 'none', 'None'):
776 return 'None'
777 if s.lower() in translate_d:
778 s = translate_d.get(s.lower())
779 return self.strip_shift(s) # type:ignore
780 if len(s) > 1 and s.find(' ') > -1:
781 # #917: not a pure, but should be ignored.
782 return ''
783 if s.isalpha():
784 if len(s) == 1:
785 if 'shift' in self.mods:
786 if len(self.mods) == 1:
787 self.mods.remove('shift')
788 s = s.upper()
789 else:
790 s = s.lower()
791 elif self.mods:
792 s = s.lower()
793 else:
794 # 917: Ignore multi-byte alphas not in the table.
795 s = ''
796 if 0:
797 # Make sure all special chars are in translate_d.
798 if g.app.gui: # It may not exist yet.
799 if s.capitalize() in g.app.gui.specialChars:
800 s = s.capitalize()
801 return s
802 #
803 # Translate shifted keys to their appropriate alternatives.
804 return self.strip_shift(s)
805 #@+node:ekr.20180502104829.1: *5* ks.strip_shift
806 def strip_shift(self, s: str) -> str:
807 """
808 Handle supposedly shifted keys.
810 User settings might specify an already-shifted key, which is not an error.
812 The legacy Tk binding names have already been translated,
813 so we don't have to worry about Shift-ampersand, etc.
814 """
815 #
816 # The second entry in each line handles shifting an already-shifted character.
817 # That's ok in user settings: the Shift modifier is just removed.
818 shift_d = {
819 # Top row of keyboard.
820 "`": "~", "~": "~",
821 "1": "!", "!": "!",
822 "2": "@", "@": "@",
823 "3": "#", "#": "#",
824 "4": "$", "$": "$",
825 "5": "%", "%": "%",
826 "6": "^", "^": "^",
827 "7": "&", "&": "&",
828 "8": "*", "*": "*",
829 "9": "(", "(": "(",
830 "0": ")", ")": ")",
831 "-": "_", "_": "_",
832 "=": "+", "+": "+",
833 # Second row of keyboard.
834 "[": "{", "{": "{",
835 "]": "}", "}": "}",
836 "\\": '|', "|": "|",
837 # Third row of keyboard.
838 ";": ":", ":": ":",
839 "'": '"', '"': '"',
840 # Fourth row of keyboard.
841 ".": "<", "<": "<",
842 ",": ">", ">": ">",
843 "//": "?", "?": "?",
844 }
845 if 'shift' in self.mods and s in shift_d:
846 self.mods.remove('shift')
847 s = shift_d.get(s) # type:ignore
848 return s
849 #@+node:ekr.20120203053243.10124: *4* ks.find, lower & startswith
850 # These may go away later, but for now they make conversion of string strokes easier.
852 def find(self, pattern: str) -> int:
853 return self.s.find(pattern)
855 def lower(self) -> str:
856 return self.s.lower()
858 def startswith(self, s: str) -> bool:
859 return self.s.startswith(s)
860 #@+node:ekr.20180415081209.2: *4* ks.find_mods
861 def find_mods(self, s: str) -> List[str]:
862 """Return the list of all modifiers seen in s."""
863 s = s.lower()
864 table = (
865 ['alt',],
866 ['command', 'cmd',],
867 ['ctrl', 'control',], # Use ctrl, not control.
868 ['meta',],
869 ['shift', 'shft',],
870 ['keypad', 'key_pad', 'numpad', 'num_pad'],
871 # 868: Allow alternative spellings.
872 )
873 result = []
874 for aList in table:
875 kind = aList[0]
876 for mod in aList:
877 for suffix in '+-':
878 if s.find(mod + suffix) > -1:
879 s = s.replace(mod + suffix, '')
880 result.append(kind)
881 break
882 return result
883 #@+node:ekr.20180417101435.1: *4* ks.isAltCtl
884 def isAltCtrl(self) -> bool:
885 """Return True if this is an Alt-Ctrl character."""
886 mods = self.find_mods(self.s)
887 return 'alt' in mods and 'ctrl' in mods
888 #@+node:ekr.20120203053243.10121: *4* ks.isFKey
889 def isFKey(self) -> bool:
890 return self.s in g.app.gui.FKeys
891 #@+node:ekr.20180417102341.1: *4* ks.isPlainKey (does not handle alt-ctrl chars)
892 def isPlainKey(self) -> bool:
893 """
894 Return True if self.s represents a plain key.
896 A plain key is a key that can be inserted into text.
898 **Note**: The caller is responsible for handling Alt-Ctrl keys.
899 """
900 s = self.s
901 if s in g.app.gui.ignoreChars:
902 # For unit tests.
903 return False
904 # #868:
905 if s.find('Keypad+') > -1:
906 # Enable bindings.
907 return False
908 if self.find_mods(s) or self.isFKey():
909 return False
910 if s in g.app.gui.specialChars:
911 return False
912 if s == 'BackSpace':
913 return False
914 return True
915 #@+node:ekr.20180511092713.1: *4* ks.isNumPadKey, ks.isPlainNumPad & ks.removeNumPadModifier
916 def isNumPadKey(self) -> bool:
917 return self.s.find('Keypad+') > -1
919 def isPlainNumPad(self) -> bool:
920 return (
921 self.isNumPadKey() and
922 len(self.s.replace('Keypad+', '')) == 1
923 )
925 def removeNumPadModifier(self) -> None:
926 self.s = self.s.replace('Keypad+', '')
927 #@+node:ekr.20180419170934.1: *4* ks.prettyPrint
928 def prettyPrint(self) -> str:
930 s = self.s
931 if not s:
932 return '<None>'
933 d = {' ': 'Space', '\t': 'Tab', '\n': 'Return', '\r': 'LineFeed'}
934 ch = s[-1]
935 return s[:-1] + d.get(ch, ch)
936 #@+node:ekr.20180415124853.1: *4* ks.strip_mods
937 def strip_mods(self, s: str) -> str:
938 """Remove all modifiers from s, without changing the case of s."""
939 table = (
940 'alt',
941 'cmd', 'command',
942 'control', 'ctrl',
943 'keypad', 'key_pad', # 868:
944 'meta',
945 'shift', 'shft',
946 )
947 for mod in table:
948 for suffix in '+-':
949 target = mod + suffix
950 i = s.lower().find(target)
951 if i > -1:
952 s = s[:i] + s[i + len(target) :]
953 break
954 return s
955 #@+node:ekr.20120203053243.10125: *4* ks.toGuiChar
956 def toGuiChar(self) -> str:
957 """Replace special chars by the actual gui char."""
958 s = self.s.lower()
959 if s in ('\n', 'return'):
960 s = '\n'
961 elif s in ('\t', 'tab'):
962 s = '\t'
963 elif s in ('\b', 'backspace'):
964 s = '\b'
965 elif s in ('.', 'period'):
966 s = '.'
967 return s
968 #@+node:ekr.20180417100834.1: *4* ks.toInsertableChar
969 def toInsertableChar(self) -> str:
970 """Convert self to an (insertable) char."""
971 # pylint: disable=len-as-condition
972 s = self.s
973 if not s or self.find_mods(s):
974 return ''
975 # Handle the "Gang of Four"
976 d = {
977 'BackSpace': '\b',
978 'LineFeed': '\n',
979 # 'Insert': '\n',
980 'Return': '\n',
981 'Tab': '\t',
982 }
983 if s in d:
984 return d.get(s) # type:ignore
985 return s if len(s) == 1 else ''
986 #@-others
988def isStroke(obj: Any) -> bool:
989 return isinstance(obj, KeyStroke)
991def isStrokeOrNone(obj: Any) -> bool:
992 return obj is None or isinstance(obj, KeyStroke)
993#@+node:ekr.20160119093947.1: *3* class g.MatchBrackets
994class MatchBrackets:
995 """
996 A class implementing the match-brackets command.
998 In the interest of speed, the code assumes that the user invokes the
999 match-bracket command ouside of any string, comment or (for perl or
1000 javascript) regex.
1001 """
1002 #@+others
1003 #@+node:ekr.20160119104510.1: *4* mb.ctor
1004 def __init__(self, c: Cmdr, p: Pos, language: str) -> None:
1005 """Ctor for MatchBrackets class."""
1006 self.c = c
1007 self.p = p.copy()
1008 self.language = language
1009 # Constants.
1010 self.close_brackets = ")]}>"
1011 self.open_brackets = "([{<"
1012 self.brackets = self.open_brackets + self.close_brackets
1013 self.matching_brackets = self.close_brackets + self.open_brackets
1014 # Language dependent.
1015 d1, d2, d3 = g.set_delims_from_language(language)
1016 self.single_comment, self.start_comment, self.end_comment = d1, d2, d3
1017 # to track expanding selection
1018 c.user_dict.setdefault('_match_brackets', {'count': 0, 'range': (0, 0)})
1019 #@+node:ekr.20160121164723.1: *4* mb.bi-directional helpers
1020 #@+node:ekr.20160121112812.1: *5* mb.is_regex
1021 def is_regex(self, s: str, i: int) -> bool:
1022 """Return true if there is another slash on the line."""
1023 if self.language in ('javascript', 'perl',):
1024 assert s[i] == '/'
1025 offset = 1 if self.forward else -1
1026 i += offset
1027 while 0 <= i < len(s) and s[i] != '\n':
1028 if s[i] == '/':
1029 return True
1030 i += offset
1031 return False
1032 return False
1033 #@+node:ekr.20160121112536.1: *5* mb.scan_regex
1034 def scan_regex(self, s: str, i: int) -> int:
1035 """Scan a regex (or regex substitution for perl)."""
1036 assert s[i] == '/'
1037 offset = 1 if self.forward else -1
1038 i1 = i
1039 i += offset
1040 found: Union[int, bool] = False
1041 while 0 <= i < len(s) and s[i] != '\n':
1042 ch = s[i]
1043 i2 = i - 1 # in case we have to look behind.
1044 i += offset
1045 if ch == '/':
1046 # Count the preceding backslashes.
1047 n = 0
1048 while 0 <= i2 < len(s) and s[i2] == '\\':
1049 n += 1
1050 i2 -= 1
1051 if (n % 2) == 0:
1052 if self.language == 'perl' and found is None:
1053 found = i
1054 else:
1055 found = i
1056 break
1057 if found is None:
1058 self.oops('unmatched regex delim')
1059 return i1 + offset
1060 return found
1061 #@+node:ekr.20160121112303.1: *5* mb.scan_string
1062 def scan_string(self, s: str, i: int) -> int:
1063 """
1064 Scan the string starting at s[i] (forward or backward).
1065 Return the index of the next character.
1066 """
1067 # i1 = i if self.forward else i + 1
1068 delim = s[i]
1069 assert delim in "'\"", repr(delim)
1070 offset = 1 if self.forward else -1
1071 i += offset
1072 while 0 <= i < len(s):
1073 ch = s[i]
1074 i2 = i - 1 # in case we have to look behind.
1075 i += offset
1076 if ch == delim:
1077 # Count the preceding backslashes.
1078 n = 0
1079 while 0 <= i2 < len(s) and s[i2] == '\\':
1080 n += 1
1081 i2 -= 1
1082 if (n % 2) == 0:
1083 return i
1084 # Annoying when matching brackets on the fly.
1085 # self.oops('unmatched string')
1086 return i + offset
1087 #@+node:tbrown.20180226113621.1: *4* mb.expand_range
1088 def expand_range(
1089 self,
1090 s: str,
1091 left: int,
1092 right: int,
1093 max_right: int,
1094 expand: bool=False,
1095 ) -> Tuple[Any, Any, Any, Any]:
1096 """
1097 Find the bracket nearest the cursor searching outwards left and right.
1099 Expand the range (left, right) in string s until either s[left] or
1100 s[right] is a bracket. right can not exceed max_right, and if expand is
1101 True, the new range must encompass the old range, in addition to s[left]
1102 or s[right] being a bracket.
1104 Returns
1105 new_left, new_right, bracket_char, index_of_bracket_char
1106 if expansion succeeds, otherwise
1107 None, None, None, None
1109 Note that only one of new_left and new_right will necessarily be a
1110 bracket, but index_of_bracket_char will definitely be a bracket.
1111 """
1112 expanded: Union[bool, str] = False
1113 left = max(0, min(left, len(s))) # #2240
1114 right = max(0, min(right, len(s))) # #2240
1115 orig_left = left
1116 orig_right = right
1117 while (
1118 (s[left] not in self.brackets or expand and not expanded)
1119 and (s[right] not in self.brackets or expand and not expanded)
1120 and (left > 0 or right < max_right)
1121 ):
1122 expanded = False
1123 if left > 0:
1124 left -= 1
1125 if s[left] in self.brackets:
1126 other = self.find_matching_bracket(s[left], s, left)
1127 if other is not None and other >= orig_right:
1128 expanded = 'left'
1129 if right < max_right:
1130 right += 1
1131 if s[right] in self.brackets:
1132 other = self.find_matching_bracket(s[right], s, right)
1133 if other is not None and other <= orig_left:
1134 expanded = 'right'
1135 if s[left] in self.brackets and (not expand or expanded == 'left'):
1136 return left, right, s[left], left
1137 if s[right] in self.brackets and (not expand or expanded == 'right'):
1138 return left, right, s[right], right
1139 return None, None, None, None
1140 #@+node:ekr.20061113221414: *4* mb.find_matching_bracket
1141 def find_matching_bracket(self, ch1: str, s: str, i: int) -> Any:
1142 """Find the bracket matching s[i] for self.language."""
1143 self.forward = ch1 in self.open_brackets
1144 # Find the character matching the initial bracket.
1145 for n in range(len(self.brackets)): # pylint: disable=consider-using-enumerate
1146 if ch1 == self.brackets[n]:
1147 target = self.matching_brackets[n]
1148 break
1149 else:
1150 return None
1151 f = self.scan if self.forward else self.scan_back
1152 return f(ch1, target, s, i)
1153 #@+node:ekr.20160121164556.1: *4* mb.scan & helpers
1154 def scan(self, ch1: str, target: str, s: str, i: int) -> Optional[int]:
1155 """Scan forward for target."""
1156 level = 0
1157 while 0 <= i < len(s):
1158 progress = i
1159 ch = s[i]
1160 if ch in '"\'':
1161 # Scan to the end/beginning of the string.
1162 i = self.scan_string(s, i)
1163 elif self.starts_comment(s, i):
1164 i = self.scan_comment(s, i) # type:ignore
1165 elif ch == '/' and self.is_regex(s, i):
1166 i = self.scan_regex(s, i)
1167 elif ch == ch1:
1168 level += 1
1169 i += 1
1170 elif ch == target:
1171 level -= 1
1172 if level <= 0:
1173 return i
1174 i += 1
1175 else:
1176 i += 1
1177 assert i > progress
1178 # Not found
1179 return None
1180 #@+node:ekr.20160119090634.1: *5* mb.scan_comment
1181 def scan_comment(self, s: str, i: int) -> Optional[int]:
1182 """Return the index of the character after a comment."""
1183 i1 = i
1184 start = self.start_comment if self.forward else self.end_comment
1185 end = self.end_comment if self.forward else self.start_comment
1186 offset = 1 if self.forward else -1
1187 if g.match(s, i, start):
1188 if not self.forward:
1189 i1 += len(end)
1190 i += offset
1191 while 0 <= i < len(s):
1192 if g.match(s, i, end):
1193 i = i + len(end) if self.forward else i - 1
1194 return i
1195 i += offset
1196 self.oops('unmatched multiline comment')
1197 elif self.forward:
1198 # Scan to the newline.
1199 target = '\n'
1200 while 0 <= i < len(s):
1201 if s[i] == '\n':
1202 i += 1
1203 return i
1204 i += 1
1205 else:
1206 # Careful: scan to the *first* target on the line
1207 target = self.single_comment
1208 found = None
1209 i -= 1
1210 while 0 <= i < len(s) and s[i] != '\n':
1211 if g.match(s, i, target):
1212 found = i
1213 i -= 1
1214 if found is None:
1215 self.oops('can not happen: unterminated single-line comment')
1216 found = 0
1217 return found
1218 return i
1219 #@+node:ekr.20160119101851.1: *5* mb.starts_comment
1220 def starts_comment(self, s: str, i: int) -> bool:
1221 """Return True if s[i] starts a comment."""
1222 assert 0 <= i < len(s)
1223 if self.forward:
1224 if self.single_comment and g.match(s, i, self.single_comment):
1225 return True
1226 return (
1227 self.start_comment and self.end_comment and
1228 g.match(s, i, self.start_comment)
1229 )
1230 if s[i] == '\n':
1231 if self.single_comment:
1232 # Scan backward for any single-comment delim.
1233 i -= 1
1234 while i >= 0 and s[i] != '\n':
1235 if g.match(s, i, self.single_comment):
1236 return True
1237 i -= 1
1238 return False
1239 return (
1240 self.start_comment and self.end_comment and
1241 g.match(s, i, self.end_comment)
1242 )
1243 #@+node:ekr.20160119230141.1: *4* mb.scan_back & helpers
1244 def scan_back(self, ch1: str, target: str, s: str, i: int) -> Optional[int]:
1245 """Scan backwards for delim."""
1246 level = 0
1247 while i >= 0:
1248 progress = i
1249 ch = s[i]
1250 if self.ends_comment(s, i):
1251 i = self.back_scan_comment(s, i)
1252 elif ch in '"\'':
1253 # Scan to the beginning of the string.
1254 i = self.scan_string(s, i)
1255 elif ch == '/' and self.is_regex(s, i):
1256 i = self.scan_regex(s, i)
1257 elif ch == ch1:
1258 level += 1
1259 i -= 1
1260 elif ch == target:
1261 level -= 1
1262 if level <= 0:
1263 return i
1264 i -= 1
1265 else:
1266 i -= 1
1267 assert i < progress
1268 # Not found
1269 return None
1270 #@+node:ekr.20160119230141.2: *5* mb.back_scan_comment
1271 def back_scan_comment(self, s: str, i: int) -> int:
1272 """Return the index of the character after a comment."""
1273 i1 = i
1274 if g.match(s, i, self.end_comment):
1275 i1 += len(self.end_comment) # For traces.
1276 i -= 1
1277 while i >= 0:
1278 if g.match(s, i, self.start_comment):
1279 i -= 1
1280 return i
1281 i -= 1
1282 self.oops('unmatched multiline comment')
1283 return i
1284 # Careful: scan to the *first* target on the line
1285 found = None
1286 i -= 1
1287 while i >= 0 and s[i] != '\n':
1288 if g.match(s, i, self.single_comment):
1289 found = i - 1
1290 i -= 1
1291 if found is None:
1292 self.oops('can not happen: unterminated single-line comment')
1293 found = 0
1294 return found
1295 #@+node:ekr.20160119230141.4: *5* mb.ends_comment
1296 def ends_comment(self, s: str, i: int) -> bool:
1297 """
1298 Return True if s[i] ends a comment. This is called while scanning
1299 backward, so this is a bit of a guess.
1300 """
1301 if s[i] == '\n':
1302 # This is the hard (dubious) case.
1303 # Let w, x, y and z stand for any strings not containg // or quotes.
1304 # Case 1: w"x//y"z Assume // is inside a string.
1305 # Case 2: x//y"z Assume " is inside the comment.
1306 # Case 3: w//x"y"z Assume both quotes are inside the comment.
1307 #
1308 # That is, we assume (perhaps wrongly) that a quote terminates a
1309 # string if and *only* if the string starts *and* ends on the line.
1310 if self.single_comment:
1311 # Scan backward for single-line comment delims or quotes.
1312 quote = None
1313 i -= 1
1314 while i >= 0 and s[i] != '\n':
1315 progress = i
1316 if quote and s[i] == quote:
1317 quote = None
1318 i -= 1
1319 elif s[i] in '"\'':
1320 if not quote:
1321 quote = s[i]
1322 i -= 1
1323 elif g.match(s, i, self.single_comment):
1324 # Assume that there is a comment only if the comment delim
1325 # isn't inside a string that begins and ends on *this* line.
1326 if quote:
1327 while i >= 0 and s[i] != 'n':
1328 if s[i] == quote:
1329 return False
1330 i -= 1
1331 return True
1332 else:
1333 i -= 1
1334 assert progress > i
1335 return False
1336 return (
1337 self.start_comment and
1338 self.end_comment and
1339 g.match(s, i, self.end_comment))
1340 #@+node:ekr.20160119104148.1: *4* mb.oops
1341 def oops(self, s: str) -> None:
1342 """Report an error in the match-brackets command."""
1343 g.es(s, color='red')
1344 #@+node:ekr.20160119094053.1: *4* mb.run
1345 #@@nobeautify
1347 def run(self) -> None:
1348 """The driver for the MatchBrackets class.
1350 With no selected range: find the nearest bracket and select from
1351 it to it's match, moving cursor to match.
1353 With selected range: the first time, move cursor back to other end of
1354 range. The second time, select enclosing range.
1355 """
1356 #
1357 # A partial fix for bug 127: Bracket matching is buggy.
1358 w = self.c.frame.body.wrapper
1359 s = w.getAllText()
1360 _mb = self.c.user_dict['_match_brackets']
1361 sel_range = w.getSelectionRange()
1362 if not w.hasSelection():
1363 _mb['count'] = 1
1364 if _mb['range'] == sel_range and _mb['count'] == 1:
1365 # haven't been to other end yet
1366 _mb['count'] += 1
1367 # move insert point to other end of selection
1368 insert = 1 if w.getInsertPoint() == sel_range[0] else 0
1369 w.setSelectionRange(
1370 sel_range[0], sel_range[1], insert=sel_range[insert])
1371 return
1373 # Find the bracket nearest the cursor.
1374 max_right = len(s) - 1 # insert point can be past last char.
1375 left = right = min(max_right, w.getInsertPoint())
1376 left, right, ch, index = self.expand_range(s, left, right, max_right)
1377 if left is None:
1378 g.es("Bracket not found")
1379 return
1380 index2 = self.find_matching_bracket(ch, s, index)
1381 if index2 is None:
1382 g.es("No matching bracket.") # #1447.
1383 return
1385 # If this is the first time we've selected the range index-index2, do
1386 # nothing extra. The second time, move cursor to other end (requires
1387 # no special action here), and the third time, try to expand the range
1388 # to any enclosing brackets
1389 minmax = (min(index, index2), max(index, index2)+1)
1390 # the range, +1 to match w.getSelectionRange()
1391 if _mb['range'] == minmax: # count how many times this has been the answer
1392 _mb['count'] += 1
1393 else:
1394 _mb['count'] = 1
1395 _mb['range'] = minmax
1396 if _mb['count'] >= 3: # try to expand range
1397 left, right, ch, index3 = self.expand_range(
1398 s,
1399 max(minmax[0], 0),
1400 min(minmax[1], max_right),
1401 max_right, expand=True
1402 )
1403 if index3 is not None: # found nearest bracket outside range
1404 index4 = self.find_matching_bracket(ch, s, index3)
1405 if index4 is not None: # found matching bracket, expand range
1406 index, index2 = index3, index4
1407 _mb['count'] = 1
1408 _mb['range'] = (min(index3, index4), max(index3, index4)+1)
1410 if index2 is not None:
1411 if index2 < index:
1412 w.setSelectionRange(index2, index + 1, insert=index2)
1413 else:
1414 w.setSelectionRange(
1415 index, index2 + 1, insert=min(len(s), index2 + 1))
1416 w.see(index2)
1417 else:
1418 g.es("unmatched", repr(ch))
1419 #@-others
1420#@+node:ekr.20090128083459.82: *3* class g.PosList (deprecated)
1421class PosList(list):
1422 #@+<< docstring for PosList >>
1423 #@+node:ekr.20090130114732.2: *4* << docstring for PosList >>
1424 """A subclass of list for creating and selecting lists of positions.
1426 This is deprecated, use leoNodes.PosList instead!
1428 aList = g.PosList(c)
1429 # Creates a PosList containing all positions in c.
1431 aList = g.PosList(c,aList2)
1432 # Creates a PosList from aList2.
1434 aList2 = aList.select(pattern,regex=False,removeClones=True)
1435 # Creates a PosList containing all positions p in aList
1436 # such that p.h matches the pattern.
1437 # The pattern is a regular expression if regex is True.
1438 # if removeClones is True, all positions p2 are removed
1439 # if a position p is already in the list and p2.v == p.v.
1441 aList.dump(sort=False,verbose=False)
1442 # Prints all positions in aList, sorted if sort is True.
1443 # Prints p.h, or repr(p) if verbose is True.
1444 """
1445 #@-<< docstring for PosList >>
1446 #@+others
1447 #@+node:ekr.20140531104908.17611: *4* PosList.ctor
1448 def __init__(self, c: Cmdr, aList: List[Cmdr]=None) -> None:
1449 self.c = c
1450 super().__init__()
1451 if aList is None:
1452 for p in c.all_positions():
1453 self.append(p.copy())
1454 else:
1455 for p in aList:
1456 self.append(p.copy())
1457 #@+node:ekr.20140531104908.17612: *4* PosList.dump
1458 def dump(self, sort: bool=False, verbose: bool=False) -> str:
1459 if verbose:
1460 return g.listToString(self, sort=sort)
1461 return g.listToString([p.h for p in self], sort=sort)
1462 #@+node:ekr.20140531104908.17613: *4* PosList.select
1463 def select(self, pat: str, regex: bool=False, removeClones: bool=True) -> "PosList":
1464 """
1465 Return a new PosList containing all positions
1466 in self that match the given pattern.
1467 """
1468 c = self.c
1470 aList = []
1471 if regex:
1472 for p in self:
1473 if re.match(pat, p.h):
1474 aList.append(p.copy())
1475 else:
1476 for p in self:
1477 if p.h.find(pat) != -1:
1478 aList.append(p.copy())
1479 if removeClones:
1480 aList = self.removeClones(aList)
1481 return PosList(c, aList)
1482 #@+node:ekr.20140531104908.17614: *4* PosList.removeClones
1483 def removeClones(self, aList: List[Pos]) -> List[Pos]:
1484 seen = {}
1485 aList2: List[Pos] = []
1486 for p in aList:
1487 if p.v not in seen:
1488 seen[p.v] = p.v
1489 aList2.append(p)
1490 return aList2
1491 #@-others
1492#@+node:EKR.20040612114220.4: *3* class g.ReadLinesClass
1493class ReadLinesClass:
1494 """A class whose next method provides a readline method for Python's tokenize module."""
1496 def __init__(self, s: str) -> None:
1497 self.lines = g.splitLines(s)
1498 self.i = 0
1500 def next(self) -> str:
1501 if self.i < len(self.lines):
1502 line = self.lines[self.i]
1503 self.i += 1
1504 else:
1505 line = ''
1506 return line
1508 __next__ = next
1509#@+node:ekr.20031218072017.3121: *3* class g.RedirectClass & convenience functions
1510class RedirectClass:
1511 """A class to redirect stdout and stderr to Leo's log pane."""
1512 #@+<< RedirectClass methods >>
1513 #@+node:ekr.20031218072017.1656: *4* << RedirectClass methods >>
1514 #@+others
1515 #@+node:ekr.20041012082437: *5* RedirectClass.__init__
1516 def __init__(self) -> None:
1517 self.old = None
1518 self.encoding = 'utf-8' # 2019/03/29 For pdb.
1519 #@+node:ekr.20041012082437.1: *5* isRedirected
1520 def isRedirected(self) -> bool:
1521 return self.old is not None
1522 #@+node:ekr.20041012082437.2: *5* flush
1523 # For LeoN: just for compatibility.
1525 def flush(self, *args: Any) -> None:
1526 return
1527 #@+node:ekr.20041012091252: *5* rawPrint
1528 def rawPrint(self, s: str) -> None:
1529 if self.old:
1530 self.old.write(s + '\n')
1531 else:
1532 g.pr(s)
1533 #@+node:ekr.20041012082437.3: *5* redirect
1534 def redirect(self, stdout: bool=True) -> None:
1535 if g.app.batchMode:
1536 # Redirection is futile in batch mode.
1537 return
1538 if not self.old:
1539 if stdout:
1540 self.old, sys.stdout = sys.stdout, self # type:ignore
1541 else:
1542 self.old, sys.stderr = sys.stderr, self # type:ignore
1543 #@+node:ekr.20041012082437.4: *5* undirect
1544 def undirect(self, stdout: bool=True) -> None:
1545 if self.old:
1546 if stdout:
1547 sys.stdout, self.old = self.old, None
1548 else:
1549 sys.stderr, self.old = self.old, None
1550 #@+node:ekr.20041012082437.5: *5* write
1551 def write(self, s: str) -> None:
1553 if self.old:
1554 if app.log:
1555 app.log.put(s, from_redirect=True)
1556 else:
1557 self.old.write(s + '\n')
1558 else:
1559 # Can happen when g.batchMode is True.
1560 g.pr(s)
1561 #@-others
1562 #@-<< RedirectClass methods >>
1564# Create two redirection objects, one for each stream.
1566redirectStdErrObj = RedirectClass()
1567redirectStdOutObj = RedirectClass()
1568#@+<< define convenience methods for redirecting streams >>
1569#@+node:ekr.20031218072017.3122: *4* << define convenience methods for redirecting streams >>
1570#@+others
1571#@+node:ekr.20041012090942: *5* redirectStderr & redirectStdout
1572# Redirect streams to the current log window.
1574def redirectStderr() -> None:
1575 global redirectStdErrObj
1576 redirectStdErrObj.redirect(stdout=False)
1578def redirectStdout() -> None:
1579 global redirectStdOutObj
1580 redirectStdOutObj.redirect()
1581#@+node:ekr.20041012090942.1: *5* restoreStderr & restoreStdout
1582# Restore standard streams.
1584def restoreStderr() -> None:
1585 global redirectStdErrObj
1586 redirectStdErrObj.undirect(stdout=False)
1588def restoreStdout() -> None:
1589 global redirectStdOutObj
1590 redirectStdOutObj.undirect()
1591#@+node:ekr.20041012090942.2: *5* stdErrIsRedirected & stdOutIsRedirected
1592def stdErrIsRedirected() -> bool:
1593 global redirectStdErrObj
1594 return redirectStdErrObj.isRedirected()
1596def stdOutIsRedirected() -> bool:
1597 global redirectStdOutObj
1598 return redirectStdOutObj.isRedirected()
1599#@+node:ekr.20041012090942.3: *5* rawPrint
1600# Send output to original stdout.
1602def rawPrint(s: str) -> None:
1603 global redirectStdOutObj
1604 redirectStdOutObj.rawPrint(s)
1605#@-others
1606#@-<< define convenience methods for redirecting streams >>
1607#@+node:ekr.20121128031949.12605: *3* class g.SherlockTracer
1608class SherlockTracer:
1609 """
1610 A stand-alone tracer class with many of Sherlock's features.
1612 This class should work in any environment containing the re, os and sys modules.
1614 The arguments in the pattern lists determine which functions get traced
1615 or which stats get printed. Each pattern starts with "+", "-", "+:" or
1616 "-:", followed by a regular expression::
1618 "+x" Enables tracing (or stats) for all functions/methods whose name
1619 matches the regular expression x.
1620 "-x" Disables tracing for functions/methods.
1621 "+:x" Enables tracing for all functions in the **file** whose name matches x.
1622 "-:x" Disables tracing for an entire file.
1624 Enabling and disabling depends on the order of arguments in the pattern
1625 list. Consider the arguments for the Rope trace::
1627 patterns=['+.*','+:.*',
1628 '-:.*\\lib\\.*','+:.*rope.*','-:.*leoGlobals.py',
1629 '-:.*worder.py','-:.*prefs.py','-:.*resources.py',])
1631 This enables tracing for everything, then disables tracing for all
1632 library modules, except for all rope modules. Finally, it disables the
1633 tracing for Rope's worder, prefs and resources modules. Btw, this is
1634 one of the best uses for regular expressions that I know of.
1636 Being able to zero in on the code of interest can be a big help in
1637 studying other people's code. This is a non-invasive method: no tracing
1638 code needs to be inserted anywhere.
1640 Usage:
1642 g.SherlockTracer(patterns).run()
1643 """
1644 #@+others
1645 #@+node:ekr.20121128031949.12602: *4* __init__
1646 def __init__(
1647 self,
1648 patterns: List[Any],
1649 dots: bool=True,
1650 show_args: bool=True,
1651 show_return: bool=True,
1652 verbose: bool=True,
1653 ) -> None:
1654 """SherlockTracer ctor."""
1655 self.bad_patterns: List[str] = [] # List of bad patterns.
1656 self.dots = dots # True: print level dots.
1657 self.contents_d: Dict[str, List] = {} # Keys are file names, values are file lines.
1658 self.n = 0 # The frame level on entry to run.
1659 self.stats: Dict[str, Dict] = {} # Keys are full file names, values are dicts.
1660 self.patterns: List[Any] = None # A list of regex patterns to match.
1661 self.pattern_stack: List[str] = []
1662 self.show_args = show_args # True: show args for each function call.
1663 self.show_return = show_return # True: show returns from each function.
1664 self.trace_lines = True # True: trace lines in enabled functions.
1665 self.verbose = verbose # True: print filename:func
1666 self.set_patterns(patterns)
1667 from leo.core.leoQt import QtCore
1668 if QtCore:
1669 # pylint: disable=no-member
1670 QtCore.pyqtRemoveInputHook()
1671 #@+node:ekr.20140326100337.16844: *4* __call__
1672 def __call__(self, frame: Any, event: Any, arg: Any) -> Any:
1673 """Exists so that self.dispatch can return self."""
1674 return self.dispatch(frame, event, arg)
1675 #@+node:ekr.20140326100337.16846: *4* sherlock.bad_pattern
1676 def bad_pattern(self, pattern: Any) -> None:
1677 """Report a bad Sherlock pattern."""
1678 if pattern not in self.bad_patterns:
1679 self.bad_patterns.append(pattern)
1680 print(f"\nignoring bad pattern: {pattern}\n")
1681 #@+node:ekr.20140326100337.16847: *4* sherlock.check_pattern
1682 def check_pattern(self, pattern: str) -> bool:
1683 """Give an error and return False for an invalid pattern."""
1684 try:
1685 for prefix in ('+:', '-:', '+', '-'):
1686 if pattern.startswith(prefix):
1687 re.match(pattern[len(prefix) :], 'xyzzy')
1688 return True
1689 self.bad_pattern(pattern)
1690 return False
1691 except Exception:
1692 self.bad_pattern(pattern)
1693 return False
1694 #@+node:ekr.20121128031949.12609: *4* sherlock.dispatch
1695 def dispatch(self, frame: Any, event: Any, arg: Any) -> Any:
1696 """The dispatch method."""
1697 if event == 'call':
1698 self.do_call(frame, arg)
1699 elif event == 'return' and self.show_return:
1700 self.do_return(frame, arg)
1701 elif event == 'line' and self.trace_lines:
1702 self.do_line(frame, arg)
1703 # Queue the SherlockTracer instance again.
1704 return self
1705 #@+node:ekr.20121128031949.12603: *4* sherlock.do_call & helper
1706 def do_call(self, frame: Any, unused_arg: Any) -> None:
1707 """Trace through a function call."""
1708 frame1 = frame
1709 code = frame.f_code
1710 file_name = code.co_filename
1711 locals_ = frame.f_locals
1712 function_name = code.co_name
1713 try:
1714 full_name = self.get_full_name(locals_, function_name)
1715 except Exception:
1716 full_name = function_name
1717 if not self.is_enabled(file_name, full_name, self.patterns):
1718 # 2020/09/09: Don't touch, for example, __ methods.
1719 return
1720 n = 0 # The number of callers of this def.
1721 while frame:
1722 frame = frame.f_back
1723 n += 1
1724 dots = '.' * max(0, n - self.n) if self.dots else ''
1725 path = f"{os.path.basename(file_name):>20}" if self.verbose else ''
1726 leadin = '+' if self.show_return else ''
1727 args = "(%s)" % self.get_args(frame1) if self.show_args else ''
1728 print(f"{path}:{dots}{leadin}{full_name}{args}")
1729 # Always update stats.
1730 d = self.stats.get(file_name, {})
1731 d[full_name] = 1 + d.get(full_name, 0)
1732 self.stats[file_name] = d
1733 #@+node:ekr.20130111185820.10194: *5* sherlock.get_args
1734 def get_args(self, frame: Any) -> str:
1735 """Return name=val for each arg in the function call."""
1736 code = frame.f_code
1737 locals_ = frame.f_locals
1738 name = code.co_name
1739 n = code.co_argcount
1740 if code.co_flags & 4:
1741 n = n + 1
1742 if code.co_flags & 8:
1743 n = n + 1
1744 result = []
1745 for i in range(n):
1746 name = code.co_varnames[i]
1747 if name != 'self':
1748 arg = locals_.get(name, '*undefined*')
1749 if arg:
1750 if isinstance(arg, (list, tuple)):
1751 # Clearer w/o f-string
1752 val = "[%s]" % ','.join(
1753 [self.show(z) for z in arg if self.show(z)])
1754 else:
1755 val = self.show(arg)
1756 if val:
1757 result.append(f"{name}={val}")
1758 return ','.join(result)
1759 #@+node:ekr.20140402060647.16845: *4* sherlock.do_line (not used)
1760 bad_fns: List[str] = []
1762 def do_line(self, frame: Any, arg: Any) -> None:
1763 """print each line of enabled functions."""
1764 if 1:
1765 return
1766 code = frame.f_code
1767 file_name = code.co_filename
1768 locals_ = frame.f_locals
1769 name = code.co_name
1770 full_name = self.get_full_name(locals_, name)
1771 if not self.is_enabled(file_name, full_name, self.patterns):
1772 return
1773 n = frame.f_lineno - 1 # Apparently, the first line is line 1.
1774 d = self.contents_d
1775 lines = d.get(file_name)
1776 if not lines:
1777 print(file_name)
1778 try:
1779 with open(file_name) as f:
1780 s = f.read()
1781 except Exception:
1782 if file_name not in self.bad_fns:
1783 self.bad_fns.append(file_name)
1784 print(f"open({file_name}) failed")
1785 return
1786 lines = g.splitLines(s)
1787 d[file_name] = lines
1788 line = lines[n].rstrip() if n < len(lines) else '<EOF>'
1789 if 0:
1790 print(f"{name:3} {line}")
1791 else:
1792 print(f"{g.shortFileName(file_name)} {n} {full_name} {line}")
1793 #@+node:ekr.20130109154743.10172: *4* sherlock.do_return & helper
1794 def do_return(self, frame: Any, arg: Any) -> None: # Arg *is* used below.
1795 """Trace a return statement."""
1796 code = frame.f_code
1797 fn = code.co_filename
1798 locals_ = frame.f_locals
1799 name = code.co_name
1800 full_name = self.get_full_name(locals_, name)
1801 if self.is_enabled(fn, full_name, self.patterns):
1802 n = 0
1803 while frame:
1804 frame = frame.f_back
1805 n += 1
1806 dots = '.' * max(0, n - self.n) if self.dots else ''
1807 path = f"{os.path.basename(fn):>20}" if self.verbose else ''
1808 if name and name == '__init__':
1809 try:
1810 ret1 = locals_ and locals_.get('self', None)
1811 ret = self.format_ret(ret1)
1812 except NameError:
1813 ret = f"<{ret1.__class__.__name__}>"
1814 else:
1815 ret = self.format_ret(arg)
1816 print(f"{path}{dots}-{full_name}{ret}")
1817 #@+node:ekr.20130111120935.10192: *5* sherlock.format_ret
1818 def format_ret(self, arg: Any) -> str:
1819 """Format arg, the value returned by a "return" statement."""
1820 try:
1821 if isinstance(arg, types.GeneratorType):
1822 ret = '<generator>'
1823 elif isinstance(arg, (tuple, list)):
1824 # Clearer w/o f-string.
1825 ret = "[%s]" % ','.join([self.show(z) for z in arg])
1826 if len(ret) > 40:
1827 # Clearer w/o f-string.
1828 ret = "[\n%s]" % ('\n,'.join([self.show(z) for z in arg]))
1829 elif arg:
1830 ret = self.show(arg)
1831 if len(ret) > 40:
1832 ret = f"\n {ret}"
1833 else:
1834 ret = '' if arg is None else repr(arg)
1835 except Exception:
1836 exctype, value = sys.exc_info()[:2]
1837 s = f"<**exception: {exctype.__name__}, {value} arg: {arg !r}**>"
1838 ret = f" ->\n {s}" if len(s) > 40 else f" -> {s}"
1839 return f" -> {ret}"
1840 #@+node:ekr.20121128111829.12185: *4* sherlock.fn_is_enabled (not used)
1841 def fn_is_enabled(self, func: Any, patterns: List[str]) -> bool:
1842 """Return True if tracing for the given function is enabled."""
1843 if func in self.ignored_functions:
1844 return False
1846 def ignore_function() -> None:
1847 if func not in self.ignored_functions:
1848 self.ignored_functions.append(func)
1849 print(f"Ignore function: {func}")
1850 #
1851 # New in Leo 6.3. Never trace dangerous functions.
1852 table = (
1853 '_deepcopy.*',
1854 # Unicode primitives.
1855 'encode\b', 'decode\b',
1856 # System functions
1857 '.*__next\b',
1858 '<frozen>', '<genexpr>', '<listcomp>',
1859 # '<decorator-gen-.*>',
1860 'get\b',
1861 # String primitives.
1862 'append\b', 'split\b', 'join\b',
1863 # File primitives...
1864 'access_check\b', 'expanduser\b', 'exists\b', 'find_spec\b',
1865 'abspath\b', 'normcase\b', 'normpath\b', 'splitdrive\b',
1866 )
1867 g.trace('=====', func)
1868 for z in table:
1869 if re.match(z, func):
1870 ignore_function()
1871 return False
1872 #
1873 # Legacy code.
1874 try:
1875 enabled, pattern = False, None
1876 for pattern in patterns:
1877 if pattern.startswith('+:'):
1878 if re.match(pattern[2:], func):
1879 enabled = True
1880 elif pattern.startswith('-:'):
1881 if re.match(pattern[2:], func):
1882 enabled = False
1883 return enabled
1884 except Exception:
1885 self.bad_pattern(pattern)
1886 return False
1887 #@+node:ekr.20130112093655.10195: *4* get_full_name
1888 def get_full_name(self, locals_: Any, name: str) -> str:
1889 """Return class_name::name if possible."""
1890 full_name = name
1891 try:
1892 user_self = locals_ and locals_.get('self', None)
1893 if user_self:
1894 full_name = user_self.__class__.__name__ + '::' + name
1895 except Exception:
1896 pass
1897 return full_name
1898 #@+node:ekr.20121128111829.12183: *4* sherlock.is_enabled
1899 ignored_files: List[str] = [] # List of files.
1900 ignored_functions: List[str] = [] # List of files.
1902 def is_enabled(
1903 self,
1904 file_name: str,
1905 function_name: str,
1906 patterns: List[str]=None,
1907 ) -> bool:
1908 """Return True if tracing for function_name in the given file is enabled."""
1909 #
1910 # New in Leo 6.3. Never trace through some files.
1911 if not os:
1912 return False # Shutting down.
1913 base_name = os.path.basename(file_name)
1914 if base_name in self.ignored_files:
1915 return False
1917 def ignore_file() -> None:
1918 if not base_name in self.ignored_files:
1919 self.ignored_files.append(base_name)
1921 def ignore_function() -> None:
1922 if function_name not in self.ignored_functions:
1923 self.ignored_functions.append(function_name)
1925 if f"{os.sep}lib{os.sep}" in file_name:
1926 ignore_file()
1927 return False
1928 if base_name.startswith('<') and base_name.endswith('>'):
1929 ignore_file()
1930 return False
1931 #
1932 # New in Leo 6.3. Never trace dangerous functions.
1933 table = (
1934 '_deepcopy.*',
1935 # Unicode primitives.
1936 'encode\b', 'decode\b',
1937 # System functions
1938 '.*__next\b',
1939 '<frozen>', '<genexpr>', '<listcomp>',
1940 # '<decorator-gen-.*>',
1941 'get\b',
1942 # String primitives.
1943 'append\b', 'split\b', 'join\b',
1944 # File primitives...
1945 'access_check\b', 'expanduser\b', 'exists\b', 'find_spec\b',
1946 'abspath\b', 'normcase\b', 'normpath\b', 'splitdrive\b',
1947 )
1948 for z in table:
1949 if re.match(z, function_name):
1950 ignore_function()
1951 return False
1952 #
1953 # Legacy code.
1954 enabled = False
1955 if patterns is None:
1956 patterns = self.patterns
1957 for pattern in patterns:
1958 try:
1959 if pattern.startswith('+:'):
1960 if re.match(pattern[2:], file_name):
1961 enabled = True
1962 elif pattern.startswith('-:'):
1963 if re.match(pattern[2:], file_name):
1964 enabled = False
1965 elif pattern.startswith('+'):
1966 if re.match(pattern[1:], function_name):
1967 enabled = True
1968 elif pattern.startswith('-'):
1969 if re.match(pattern[1:], function_name):
1970 enabled = False
1971 else:
1972 self.bad_pattern(pattern)
1973 except Exception:
1974 self.bad_pattern(pattern)
1975 return enabled
1976 #@+node:ekr.20121128111829.12182: *4* print_stats
1977 def print_stats(self, patterns: List[str]=None) -> None:
1978 """Print all accumulated statisitics."""
1979 print('\nSherlock statistics...')
1980 if not patterns:
1981 patterns = ['+.*', '+:.*',]
1982 for fn in sorted(self.stats.keys()):
1983 d = self.stats.get(fn)
1984 if self.fn_is_enabled(fn, patterns):
1985 result = sorted(d.keys()) # type:ignore
1986 else:
1987 result = [key for key in sorted(d.keys()) # type:ignore
1988 if self.is_enabled(fn, key, patterns)]
1989 if result:
1990 print('')
1991 fn = fn.replace('\\', '/')
1992 parts = fn.split('/')
1993 print('/'.join(parts[-2:]))
1994 for key in result:
1995 print(f"{d.get(key):4} {key}")
1996 #@+node:ekr.20121128031949.12614: *4* run
1997 # Modified from pdb.Pdb.set_trace.
1999 def run(self, frame: Any=None) -> None:
2000 """Trace from the given frame or the caller's frame."""
2001 print("SherlockTracer.run:patterns:\n%s" % '\n'.join(self.patterns))
2002 if frame is None:
2003 frame = sys._getframe().f_back
2004 # Compute self.n, the number of frames to ignore.
2005 self.n = 0
2006 while frame:
2007 frame = frame.f_back
2008 self.n += 1
2009 # Pass self to sys.settrace to give easy access to all methods.
2010 sys.settrace(self)
2011 #@+node:ekr.20140322090829.16834: *4* push & pop
2012 def push(self, patterns: List[str]) -> None:
2013 """Push the old patterns and set the new."""
2014 self.pattern_stack.append(self.patterns) # type:ignore
2015 self.set_patterns(patterns)
2016 print(f"SherlockTracer.push: {self.patterns}")
2018 def pop(self) -> None:
2019 """Restore the pushed patterns."""
2020 if self.pattern_stack:
2021 self.patterns = self.pattern_stack.pop() # type:ignore
2022 print(f"SherlockTracer.pop: {self.patterns}")
2023 else:
2024 print('SherlockTracer.pop: pattern stack underflow')
2025 #@+node:ekr.20140326100337.16845: *4* set_patterns
2026 def set_patterns(self, patterns: List[str]) -> None:
2027 """Set the patterns in effect."""
2028 self.patterns = [z for z in patterns if self.check_pattern(z)]
2029 #@+node:ekr.20140322090829.16831: *4* show
2030 def show(self, item: Any) -> str:
2031 """return the best representation of item."""
2032 if not item:
2033 return repr(item)
2034 if isinstance(item, dict):
2035 return 'dict'
2036 if isinstance(item, str):
2037 s = repr(item)
2038 if len(s) <= 20:
2039 return s
2040 return s[:17] + '...'
2041 return repr(item)
2042 #@+node:ekr.20121128093229.12616: *4* stop
2043 def stop(self) -> None:
2044 """Stop all tracing."""
2045 sys.settrace(None)
2046 #@-others
2047#@+node:ekr.20191013145307.1: *3* class g.TkIDDialog (EmergencyDialog)
2048class TkIDDialog(EmergencyDialog):
2049 """A class that creates an tkinter dialog to get the Leo ID."""
2051 message = (
2052 "leoID.txt not found\n\n"
2053 "Please enter an id that identifies you uniquely.\n"
2054 "Your git/cvs/bzr login name is a good choice.\n\n"
2055 "Leo uses this id to uniquely identify nodes.\n\n"
2056 "Your id should contain only letters and numbers\n"
2057 "and must be at least 3 characters in length.")
2059 title = 'Enter Leo id'
2061 def __init__(self) -> None:
2062 super().__init__(self.title, self.message)
2063 self.val = ''
2065 #@+others
2066 #@+node:ekr.20191013145710.1: *4* leo_id_dialog.onKey
2067 def onKey(self, event: Any) -> None:
2068 """Handle Key events in askOk dialogs."""
2069 if event.char in '\n\r':
2070 self.okButton()
2071 #@+node:ekr.20191013145757.1: *4* leo_id_dialog.createTopFrame
2072 def createTopFrame(self) -> None:
2073 """Create the Tk.Toplevel widget for a leoTkinterDialog."""
2074 import tkinter as Tk
2075 self.root = Tk.Tk() # type:ignore
2076 self.top = Tk.Toplevel(self.root) # type:ignore
2077 self.top.title(self.title)
2078 self.root.withdraw()
2079 self.frame = Tk.Frame(self.top) # type:ignore
2080 self.frame.pack(side="top", expand=1, fill="both")
2081 label = Tk.Label(self.frame, text=self.message, bg='white')
2082 label.pack(pady=10)
2083 self.entry = Tk.Entry(self.frame)
2084 self.entry.pack()
2085 self.entry.focus_set()
2086 #@+node:ekr.20191013150158.1: *4* leo_id_dialog.okButton
2087 def okButton(self) -> None:
2088 """Do default click action in ok button."""
2089 self.val = self.entry.get()
2090 # Return is not possible.
2091 self.top.destroy()
2092 self.top = None
2093 #@-others
2094#@+node:ekr.20080531075119.1: *3* class g.Tracer
2095class Tracer:
2096 """A "debugger" that computes a call graph.
2098 To trace a function and its callers, put the following at the function's start:
2100 g.startTracer()
2101 """
2102 #@+others
2103 #@+node:ekr.20080531075119.2: *4* __init__ (Tracer)
2104 def __init__(self, limit: int=0, trace: bool=False, verbose: bool=False) -> None:
2105 self.callDict: Dict[str, Any] = {}
2106 # Keys are function names.
2107 # Values are the number of times the function was called by the caller.
2108 self.calledDict: Dict[str, int] = {}
2109 # Keys are function names.
2110 # Values are the total number of times the function was called.
2111 self.count = 0
2112 self.inited = False
2113 self.limit = limit # 0: no limit, otherwise, limit trace to n entries deep.
2114 self.stack: List[str] = []
2115 self.trace = trace
2116 self.verbose = verbose # True: print returns as well as calls.
2117 #@+node:ekr.20080531075119.3: *4* computeName
2118 def computeName(self, frame: Any) -> str:
2119 if not frame:
2120 return ''
2121 code = frame.f_code
2122 result = []
2123 module = inspect.getmodule(code)
2124 if module:
2125 module_name = module.__name__
2126 if module_name == 'leo.core.leoGlobals':
2127 result.append('g')
2128 else:
2129 tag = 'leo.core.'
2130 if module_name.startswith(tag):
2131 module_name = module_name[len(tag) :]
2132 result.append(module_name)
2133 try:
2134 # This can fail during startup.
2135 self_obj = frame.f_locals.get('self')
2136 if self_obj:
2137 result.append(self_obj.__class__.__name__)
2138 except Exception:
2139 pass
2140 result.append(code.co_name)
2141 return '.'.join(result)
2142 #@+node:ekr.20080531075119.4: *4* report
2143 def report(self) -> None:
2144 if 0:
2145 g.pr('\nstack')
2146 for z in self.stack:
2147 g.pr(z)
2148 g.pr('\ncallDict...')
2149 for key in sorted(self.callDict):
2150 # Print the calling function.
2151 g.pr(f"{self.calledDict.get(key,0):d}", key)
2152 # Print the called functions.
2153 d = self.callDict.get(key)
2154 for key2 in sorted(d): # type:ignore
2155 g.pr(f"{d.get(key2):8d}", key2) # type:ignore
2156 #@+node:ekr.20080531075119.5: *4* stop
2157 def stop(self) -> None:
2158 sys.settrace(None)
2159 self.report()
2160 #@+node:ekr.20080531075119.6: *4* tracer
2161 def tracer(self, frame: Any, event: Any, arg: Any) -> Optional[Callable]:
2162 """A function to be passed to sys.settrace."""
2163 n = len(self.stack)
2164 if event == 'return':
2165 n = max(0, n - 1)
2166 pad = '.' * n
2167 if event == 'call':
2168 if not self.inited:
2169 # Add an extra stack element for the routine containing the call to startTracer.
2170 self.inited = True
2171 name = self.computeName(frame.f_back)
2172 self.updateStats(name)
2173 self.stack.append(name)
2174 name = self.computeName(frame)
2175 if self.trace and (self.limit == 0 or len(self.stack) < self.limit):
2176 g.trace(f"{pad}call", name)
2177 self.updateStats(name)
2178 self.stack.append(name)
2179 return self.tracer
2180 if event == 'return':
2181 if self.stack:
2182 name = self.stack.pop()
2183 if (
2184 self.trace and
2185 self.verbose and
2186 (self.limit == 0 or len(self.stack) < self.limit)
2187 ):
2188 g.trace(f"{pad}ret ", name)
2189 else:
2190 g.trace('return underflow')
2191 self.stop()
2192 return None
2193 if self.stack:
2194 return self.tracer
2195 self.stop()
2196 return None
2197 return self.tracer
2198 #@+node:ekr.20080531075119.7: *4* updateStats
2199 def updateStats(self, name: str) -> None:
2200 if not self.stack:
2201 return
2202 caller = self.stack[-1]
2203 # d is a dict reprenting the called functions.
2204 # Keys are called functions, values are counts.
2205 d: Dict[str, int] = self.callDict.get(caller, {})
2206 d[name] = 1 + d.get(name, 0)
2207 self.callDict[caller] = d
2208 # Update the total counts.
2209 self.calledDict[name] = 1 + self.calledDict.get(name, 0)
2210 #@-others
2212def startTracer(limit: int=0, trace: bool=False, verbose: bool=False) -> Callable:
2213 t = g.Tracer(limit=limit, trace=trace, verbose=verbose)
2214 sys.settrace(t.tracer)
2215 return t
2216#@+node:ekr.20031219074948.1: *3* class g.Tracing/NullObject & helpers
2217#@@nobeautify
2219tracing_tags: Dict[int, str] = {} # Keys are id's, values are tags.
2220tracing_vars: Dict[int, List] = {} # Keys are id's, values are names of ivars.
2221# Keys are signatures: '%s.%s:%s' % (tag, attr, callers). Values not important.
2222tracing_signatures: Dict[str, Any] = {}
2224class NullObject:
2225 """An object that does nothing, and does it very well."""
2226 def __init__(self, ivars: List[str]=None, *args: Any, **kwargs: Any) -> None:
2227 if isinstance(ivars, str):
2228 ivars = [ivars]
2229 tracing_vars [id(self)] = ivars or []
2230 def __call__(self, *args: Any, **keys: Any) -> "NullObject":
2231 return self
2232 def __repr__(self) -> str:
2233 return "NullObject"
2234 def __str__(self) -> str:
2235 return "NullObject"
2236 # Attribute access...
2237 def __delattr__(self, attr: str) -> None:
2238 return None
2239 def __getattr__(self, attr: str) -> Any:
2240 if attr in tracing_vars.get(id(self), []):
2241 return getattr(self, attr, None)
2242 return self # Required.
2243 def __setattr__(self, attr: str, val: Any) -> None:
2244 if attr in tracing_vars.get(id(self), []):
2245 object.__setattr__(self, attr, val)
2246 # Container methods..
2247 def __bool__(self) -> bool:
2248 return False
2249 def __contains__(self, item: Any) -> bool:
2250 return False
2251 def __getitem__(self, key: str) -> None:
2252 raise KeyError
2253 def __setitem__(self, key: str, val: Any) -> None:
2254 pass
2255 def __iter__(self) -> "NullObject":
2256 return self
2257 def __len__(self) -> int:
2258 return 0
2259 # Iteration methods:
2260 def __next__(self) -> None:
2261 raise StopIteration
2264class TracingNullObject:
2265 """Tracing NullObject."""
2266 def __init__(self, tag: str, ivars: List[Any]=None, *args: Any, **kwargs: Any) -> None:
2267 tracing_tags [id(self)] = tag
2268 if isinstance(ivars, str):
2269 ivars = [ivars]
2270 tracing_vars [id(self)] = ivars or []
2271 def __call__(self, *args: Any, **kwargs: Any) -> "TracingNullObject":
2272 return self
2273 def __repr__(self) -> str:
2274 return f'TracingNullObject: {tracing_tags.get(id(self), "<NO TAG>")}'
2275 def __str__(self) -> str:
2276 return f'TracingNullObject: {tracing_tags.get(id(self), "<NO TAG>")}'
2277 #
2278 # Attribute access...
2279 def __delattr__(self, attr: str) -> None:
2280 return None
2281 def __getattr__(self, attr: str) -> "TracingNullObject":
2282 null_object_print_attr(id(self), attr)
2283 if attr in tracing_vars.get(id(self), []):
2284 return getattr(self, attr, None)
2285 return self # Required.
2286 def __setattr__(self, attr: str, val: Any) -> None:
2287 g.null_object_print(id(self), '__setattr__', attr, val)
2288 if attr in tracing_vars.get(id(self), []):
2289 object.__setattr__(self, attr, val)
2290 #
2291 # All other methods...
2292 def __bool__(self) -> bool:
2293 if 0: # To do: print only once.
2294 suppress = ('getShortcut','on_idle', 'setItemText')
2295 callers = g.callers(2)
2296 if not callers.endswith(suppress):
2297 g.null_object_print(id(self), '__bool__')
2298 return False
2299 def __contains__(self, item: Any) -> bool:
2300 g.null_object_print(id(self), '__contains__')
2301 return False
2302 def __getitem__(self, key: str) -> None:
2303 g.null_object_print(id(self), '__getitem__')
2304 # pylint doesn't like trailing return None.
2305 def __iter__(self) -> "TracingNullObject":
2306 g.null_object_print(id(self), '__iter__')
2307 return self
2308 def __len__(self) -> int:
2309 # g.null_object_print(id(self), '__len__')
2310 return 0
2311 def __next__(self) -> None:
2312 g.null_object_print(id(self), '__next__')
2313 raise StopIteration
2314 def __setitem__(self, key: str, val: Any) -> None:
2315 g.null_object_print(id(self), '__setitem__')
2316 # pylint doesn't like trailing return None.
2317#@+node:ekr.20190330062625.1: *4* g.null_object_print_attr
2318def null_object_print_attr(id_: int, attr: str) -> None:
2319 suppress = True
2320 suppress_callers: List[str] = []
2321 suppress_attrs: List[str] = []
2322 if suppress:
2323 #@+<< define suppression lists >>
2324 #@+node:ekr.20190330072026.1: *5* << define suppression lists >>
2325 suppress_callers = [
2326 'drawNode', 'drawTopTree', 'drawTree',
2327 'contractItem', 'getCurrentItem',
2328 'declutter_node',
2329 'finishCreate',
2330 'initAfterLoad',
2331 'show_tips',
2332 'writeWaitingLog',
2333 # 'set_focus', 'show_tips',
2334 ]
2335 suppress_attrs = [
2336 # Leo...
2337 'c.frame.body.wrapper',
2338 'c.frame.getIconBar.add',
2339 'c.frame.log.createTab',
2340 'c.frame.log.enable',
2341 'c.frame.log.finishCreate',
2342 'c.frame.menu.createMenuBar',
2343 'c.frame.menu.finishCreate',
2344 # 'c.frame.menu.getMenu',
2345 'currentItem',
2346 'dw.leo_master.windowTitle',
2347 # Pyzo...
2348 'pyzo.keyMapper.connect',
2349 'pyzo.keyMapper.keyMappingChanged',
2350 'pyzo.keyMapper.setShortcut',
2351 ]
2352 #@-<< define suppression lists >>
2353 tag = tracing_tags.get(id_, "<NO TAG>")
2354 callers = g.callers(3).split(',')
2355 callers = ','.join(callers[:-1])
2356 in_callers = any(z in callers for z in suppress_callers)
2357 s = f"{tag}.{attr}"
2358 if suppress:
2359 # Filter traces.
2360 if not in_callers and s not in suppress_attrs:
2361 g.pr(f"{s:40} {callers}")
2362 else:
2363 # Print each signature once. No need to filter!
2364 signature = f"{tag}.{attr}:{callers}"
2365 if signature not in tracing_signatures:
2366 tracing_signatures[signature] = True
2367 g.pr(f"{s:40} {callers}")
2368#@+node:ekr.20190330072832.1: *4* g.null_object_print
2369def null_object_print(id_: int, kind: Any, *args: Any) -> None:
2370 tag = tracing_tags.get(id_, "<NO TAG>")
2371 callers = g.callers(3).split(',')
2372 callers = ','.join(callers[:-1])
2373 s = f"{kind}.{tag}"
2374 signature = f"{s}:{callers}"
2375 if 1:
2376 # Always print:
2377 if args:
2378 args_s = ', '.join([repr(z) for z in args])
2379 g.pr(f"{s:40} {callers}\n\t\t\targs: {args_s}")
2380 else:
2381 g.pr(f"{s:40} {callers}")
2382 elif signature not in tracing_signatures:
2383 # Print each signature once.
2384 tracing_signatures[signature] = True
2385 g.pr(f"{s:40} {callers}")
2386#@+node:ekr.20120129181245.10220: *3* class g.TypedDict
2387class TypedDict:
2388 """
2389 A class providing additional dictionary-related methods:
2391 __init__: Specifies types and the dict's name.
2392 __repr__: Compatible with g.printObj, based on g.objToString.
2393 __setitem__: Type checks its arguments.
2394 __str__: A concise summary of the inner dict.
2395 add_to_list: A convenience method that adds a value to its key's list.
2396 name: The dict's name.
2397 setName: Sets the dict's name, for use by __repr__.
2399 Overrides the following standard methods:
2401 copy: A thin wrapper for copy.deepcopy.
2402 get: Returns self.d.get
2403 items: Returns self.d.items
2404 keys: Returns self.d.keys
2405 update: Updates self.d from either a dict or a TypedDict.
2406 """
2408 def __init__(self, name: str, keyType: Any, valType: Any) -> None:
2409 self.d: Dict[str, Any] = {}
2410 self._name = name # For __repr__ only.
2411 self.keyType = keyType
2412 self.valType = valType
2413 #@+others
2414 #@+node:ekr.20120205022040.17770: *4* td.__repr__ & __str__
2415 def __str__(self) -> str:
2416 """Concise: used by repr."""
2417 return (
2418 f"<TypedDict name:{self._name} "
2419 f"keys:{self.keyType.__name__} "
2420 f"values:{self.valType.__name__} "
2421 f"len(keys): {len(list(self.keys()))}>"
2422 )
2424 def __repr__(self) -> str:
2425 """Suitable for g.printObj"""
2426 return f"{g.dictToString(self.d)}\n{str(self)}\n"
2427 #@+node:ekr.20120205022040.17774: *4* td.__setitem__
2428 def __setitem__(self, key: Any, val: Any) -> None:
2429 """Allow d[key] = val"""
2430 if key is None:
2431 g.trace('TypeDict: None is not a valid key', g.callers())
2432 return
2433 self._checkKeyType(key)
2434 try:
2435 for z in val:
2436 self._checkValType(z)
2437 except TypeError:
2438 self._checkValType(val) # val is not iterable.
2439 self.d[key] = val
2440 #@+node:ekr.20190904052828.1: *4* td.add_to_list
2441 def add_to_list(self, key: Any, val: Any) -> None:
2442 """Update the *list*, self.d [key]"""
2443 if key is None:
2444 g.trace('TypeDict: None is not a valid key', g.callers())
2445 return
2446 self._checkKeyType(key)
2447 self._checkValType(val)
2448 aList = self.d.get(key, [])
2449 if val not in aList:
2450 aList.append(val)
2451 self.d[key] = aList
2452 #@+node:ekr.20120206134955.10150: *4* td.checking
2453 def _checkKeyType(self, key: str) -> None:
2454 if key and key.__class__ != self.keyType:
2455 self._reportTypeError(key, self.keyType)
2457 def _checkValType(self, val: Any) -> None:
2458 if val.__class__ != self.valType:
2459 self._reportTypeError(val, self.valType)
2461 def _reportTypeError(self, obj: Any, objType: Any) -> str:
2462 return (
2463 f"{self._name}\n"
2464 f"expected: {obj.__class__.__name__}\n"
2465 f" got: {objType.__name__}")
2466 #@+node:ekr.20120223062418.10422: *4* td.copy
2467 def copy(self, name: str=None) -> Any:
2468 """Return a new dict with the same contents."""
2469 import copy
2470 return copy.deepcopy(self)
2471 #@+node:ekr.20120205022040.17771: *4* td.get & keys & values
2472 def get(self, key: Any, default: Any=None) -> Any:
2473 return self.d.get(key, default)
2475 def items(self) -> Any:
2476 return self.d.items()
2478 def keys(self) -> Any:
2479 return self.d.keys()
2481 def values(self) -> Any:
2482 return self.d.values()
2483 #@+node:ekr.20190903181030.1: *4* td.get_setting & get_string_setting
2484 def get_setting(self, key: str) -> Any:
2485 key = key.replace('-', '').replace('_', '')
2486 gs = self.get(key)
2487 val = gs and gs.val
2488 return val
2490 def get_string_setting(self, key: str) -> Optional[str]:
2491 val = self.get_setting(key)
2492 return val if val and isinstance(val, str) else None
2493 #@+node:ekr.20190904103552.1: *4* td.name & setName
2494 def name(self) -> str:
2495 return self._name
2497 def setName(self, name: str) -> None:
2498 self._name = name
2499 #@+node:ekr.20120205022040.17807: *4* td.update
2500 def update(self, d: Dict[Any, Any]) -> None:
2501 """Update self.d from a the appropriate dict."""
2502 if isinstance(d, TypedDict):
2503 self.d.update(d.d)
2504 else:
2505 self.d.update(d)
2506 #@-others
2507#@+node:ville.20090827174345.9963: *3* class g.UiTypeException & g.assertui
2508class UiTypeException(Exception):
2509 pass
2511def assertUi(uitype: Any) -> None:
2512 if not g.app.gui.guiName() == uitype:
2513 raise UiTypeException
2514#@+node:ekr.20200219071828.1: *3* class TestLeoGlobals (leoGlobals.py)
2515class TestLeoGlobals(unittest.TestCase):
2516 """Tests for leoGlobals.py."""
2517 #@+others
2518 #@+node:ekr.20200219071958.1: *4* test_comment_delims_from_extension
2519 def test_comment_delims_from_extension(self) -> None:
2521 # pylint: disable=import-self
2522 from leo.core import leoGlobals as leo_g
2523 from leo.core import leoApp
2524 leo_g.app = leoApp.LeoApp()
2525 assert leo_g.comment_delims_from_extension(".py") == ('#', '', '')
2526 assert leo_g.comment_delims_from_extension(".c") == ('//', '/*', '*/')
2527 assert leo_g.comment_delims_from_extension(".html") == ('', '<!--', '-->')
2528 #@+node:ekr.20200219072957.1: *4* test_is_sentinel
2529 def test_is_sentinel(self) -> None:
2531 # pylint: disable=import-self
2532 from leo.core import leoGlobals as leo_g
2533 # Python.
2534 py_delims = leo_g.comment_delims_from_extension('.py')
2535 assert leo_g.is_sentinel("#@+node", py_delims)
2536 assert not leo_g.is_sentinel("#comment", py_delims)
2537 # C.
2538 c_delims = leo_g.comment_delims_from_extension('.c')
2539 assert leo_g.is_sentinel("//@+node", c_delims)
2540 assert not g.is_sentinel("//comment", c_delims)
2541 # Html.
2542 html_delims = leo_g.comment_delims_from_extension('.html')
2543 assert leo_g.is_sentinel("<!--@+node-->", html_delims)
2544 assert not leo_g.is_sentinel("<!--comment-->", html_delims)
2545 #@-others
2546#@+node:ekr.20140904112935.18526: *3* g.isTextWrapper & isTextWidget
2547def isTextWidget(w: Any) -> bool:
2548 return g.app.gui.isTextWidget(w)
2550def isTextWrapper(w: Any) -> bool:
2551 return g.app.gui.isTextWrapper(w)
2552#@+node:ekr.20160518074224.1: *3* class g.LinterTable
2553class LinterTable():
2554 """A class to encapsulate lists of leo modules under test."""
2556 def __init__(self) -> None:
2557 """Ctor for LinterTable class."""
2558 # Define self. relative to leo.core.leoGlobals
2559 self.loadDir = g.os_path_finalize_join(g.__file__, '..', '..')
2560 #@+others
2561 #@+node:ekr.20160518074545.2: *4* commands
2562 def commands(self) -> List:
2563 """Return list of all command modules in leo/commands."""
2564 pattern = g.os_path_finalize_join(self.loadDir, 'commands', '*.py')
2565 return self.get_files(pattern)
2566 #@+node:ekr.20160518074545.3: *4* core
2567 def core(self) -> List:
2568 """Return list of all of Leo's core files."""
2569 pattern = g.os_path_finalize_join(self.loadDir, 'core', 'leo*.py')
2570 aList = self.get_files(pattern)
2571 for fn in ['runLeo.py',]:
2572 aList.append(g.os_path_finalize_join(self.loadDir, 'core', fn))
2573 return sorted(aList)
2574 #@+node:ekr.20160518074545.4: *4* external
2575 def external(self) -> List:
2576 """Return list of files in leo/external"""
2577 pattern = g.os_path_finalize_join(self.loadDir, 'external', 'leo*.py')
2578 aList = self.get_files(pattern)
2579 remove = [
2580 'leoSAGlobals.py',
2581 'leoftsindex.py',
2582 ]
2583 remove = [g.os_path_finalize_join(self.loadDir, 'external', fn) for fn in remove]
2584 return sorted([z for z in aList if z not in remove])
2585 #@+node:ekr.20160520093506.1: *4* get_files (LinterTable)
2586 def get_files(self, pattern: str) -> List:
2587 """Return the list of absolute file names matching the pattern."""
2588 aList = sorted([
2589 fn for fn in g.glob_glob(pattern)
2590 if g.os_path_isfile(fn) and g.shortFileName(fn) != '__init__.py'])
2591 return aList
2592 #@+node:ekr.20160518074545.9: *4* get_files_for_scope
2593 def get_files_for_scope(self, scope: str, fn: str) -> List:
2594 """Return a list of absolute filenames for external linters."""
2595 d = {
2596 'all': [self.core, self.commands, self.external, self.plugins],
2597 'commands': [self.commands],
2598 'core': [self.core, self.commands, self.external, self.gui_plugins],
2599 'external': [self.external],
2600 'file': [fn],
2601 'gui': [self.gui_plugins],
2602 'modes': [self.modes],
2603 'plugins': [self.plugins],
2604 'tests': [self.tests],
2605 }
2606 suppress_list = ['freewin.py',]
2607 functions = d.get(scope)
2608 paths = []
2609 if functions:
2610 for func in functions:
2611 files = [func] if isinstance(func, str) else func()
2612 # Bug fix: 2016/10/15
2613 for fn in files:
2614 fn = g.os_path_abspath(fn)
2615 if g.shortFileName(fn) in suppress_list:
2616 print(f"\npylint-leo: skip {fn}")
2617 continue
2618 if g.os_path_exists(fn):
2619 if g.os_path_isfile(fn):
2620 paths.append(fn)
2621 else:
2622 print(f"does not exist: {fn}")
2623 paths = sorted(set(paths))
2624 return paths
2625 print('LinterTable.get_table: bad scope', scope)
2626 return []
2627 #@+node:ekr.20160518074545.5: *4* gui_plugins
2628 def gui_plugins(self) -> List:
2629 """Return list of all of Leo's gui-related files."""
2630 pattern = g.os_path_finalize_join(self.loadDir, 'plugins', 'qt_*.py')
2631 aList = self.get_files(pattern)
2632 # These are not included, because they don't start with 'qt_':
2633 add = ['free_layout.py', 'nested_splitter.py',]
2634 remove = [
2635 'qt_main.py', # auto-generated file.
2636 ]
2637 for fn in add:
2638 aList.append(g.os_path_finalize_join(self.loadDir, 'plugins', fn))
2639 remove = [g.os_path_finalize_join(self.loadDir, 'plugins', fn) for fn in remove]
2640 return sorted(set([z for z in aList if z not in remove]))
2641 #@+node:ekr.20160518074545.6: *4* modes
2642 def modes(self) -> List:
2643 """Return list of all files in leo/modes"""
2644 pattern = g.os_path_finalize_join(self.loadDir, 'modes', '*.py')
2645 return self.get_files(pattern)
2646 #@+node:ekr.20160518074545.8: *4* plugins (LinterTable)
2647 def plugins(self) -> List:
2648 """Return a list of all important plugins."""
2649 aList = []
2650 for theDir in ('', 'importers', 'writers'):
2651 pattern = g.os_path_finalize_join(self.loadDir, 'plugins', theDir, '*.py')
2652 aList.extend(self.get_files(pattern))
2653 # Don't use get_files here.
2654 # for fn in g.glob_glob(pattern):
2655 # sfn = g.shortFileName(fn)
2656 # if sfn != '__init__.py':
2657 # sfn = os.sep.join([theDir, sfn]) if theDir else sfn
2658 # aList.append(sfn)
2659 remove = [
2660 # 2016/05/20: *do* include gui-related plugins.
2661 # This allows the -a option not to doubly-include gui-related plugins.
2662 # 'free_layout.py', # Gui-related.
2663 # 'nested_splitter.py', # Gui-related.
2664 'gtkDialogs.py', # Many errors, not important.
2665 'leofts.py', # Not (yet) in leoPlugins.leo.
2666 'qtGui.py', # Dummy file
2667 'qt_main.py', # Created automatically.
2668 'viewrendered2.py', # To be removed.
2669 'rst3.py', # Obsolete
2670 ]
2671 remove = [g.os_path_finalize_join(self.loadDir, 'plugins', fn) for fn in remove]
2672 aList = sorted([z for z in aList if z not in remove])
2673 return sorted(set(aList))
2674 #@+node:ekr.20211115103929.1: *4* tests (LinterTable)
2675 def tests(self) -> List:
2676 """Return list of files in leo/unittests"""
2677 aList = []
2678 for theDir in ('', 'commands', 'core', 'plugins'):
2679 pattern = g.os_path_finalize_join(self.loadDir, 'unittests', theDir, '*.py')
2680 aList.extend(self.get_files(pattern))
2681 remove = [
2682 'py3_test_grammar.py',
2683 ]
2684 remove = [g.os_path_finalize_join(self.loadDir, 'unittests', fn) for fn in remove]
2685 return sorted([z for z in aList if z not in remove])
2686 #@-others
2687#@+node:ekr.20140711071454.17649: ** g.Debugging, GC, Stats & Timing
2688#@+node:ekr.20031218072017.3104: *3* g.Debugging
2689#@+node:ekr.20180415144534.1: *4* g.assert_is
2690def assert_is(obj: Any, list_or_class: Any, warn: bool=True) -> bool:
2692 if warn:
2693 ok = isinstance(obj, list_or_class)
2694 if not ok:
2695 g.es_print(
2696 f"can not happen. {obj !r}: "
2697 f"expected {list_or_class}, "
2698 f"got: {obj.__class__.__name__}")
2699 g.es_print(g.callers())
2700 return ok
2701 ok = isinstance(obj, list_or_class)
2702 assert ok, (obj, obj.__class__.__name__, g.callers())
2703 return ok
2704#@+node:ekr.20180420081530.1: *4* g._assert
2705def _assert(condition: Any, show_callers: bool=True) -> bool:
2706 """A safer alternative to a bare assert."""
2707 if g.unitTesting:
2708 assert condition
2709 return True
2710 ok = bool(condition)
2711 if ok:
2712 return True
2713 g.es_print('\n===== g._assert failed =====\n')
2714 if show_callers:
2715 g.es_print(g.callers())
2716 return False
2717#@+node:ekr.20051023083258: *4* g.callers & g.caller & _callerName
2718def callers(n: int=4, count: int=0, excludeCaller: bool=True, verbose: bool=False) -> str:
2719 """
2720 Return a string containing a comma-separated list of the callers
2721 of the function that called g.callerList.
2723 excludeCaller: True (the default), g.callers itself is not on the list.
2725 If the `verbose` keyword is True, return a list separated by newlines.
2726 """
2727 # Be careful to call g._callerName with smaller values of i first:
2728 # sys._getframe throws ValueError if there are less than i entries.
2729 result = []
2730 i = 3 if excludeCaller else 2
2731 while 1:
2732 s = _callerName(n=i, verbose=verbose)
2733 if s:
2734 result.append(s)
2735 if not s or len(result) >= n:
2736 break
2737 i += 1
2738 result.reverse()
2739 if count > 0:
2740 result = result[:count]
2741 if verbose:
2742 return ''.join([f"\n {z}" for z in result])
2743 return ','.join(result)
2744#@+node:ekr.20031218072017.3107: *5* g._callerName
2745def _callerName(n: int, verbose: bool=False) -> str:
2746 try:
2747 # get the function name from the call stack.
2748 f1 = sys._getframe(n) # The stack frame, n levels up.
2749 code1 = f1.f_code # The code object
2750 sfn = shortFilename(code1.co_filename) # The file name.
2751 locals_ = f1.f_locals # The local namespace.
2752 name = code1.co_name
2753 line = code1.co_firstlineno
2754 if verbose:
2755 obj = locals_.get('self')
2756 full_name = f"{obj.__class__.__name__}.{name}" if obj else name
2757 return f"line {line:4} {sfn:>30} {full_name}"
2758 return name
2759 except ValueError:
2760 return ''
2761 # The stack is not deep enough OR
2762 # sys._getframe does not exist on this platform.
2763 except Exception:
2764 es_exception()
2765 return '' # "<no caller name>"
2766#@+node:ekr.20180328170441.1: *5* g.caller
2767def caller(i: int=1) -> str:
2768 """Return the caller name i levels up the stack."""
2769 return g.callers(i + 1).split(',')[0]
2770#@+node:ekr.20031218072017.3109: *4* g.dump
2771def dump(s: str) -> str:
2772 out = ""
2773 for i in s:
2774 out += str(ord(i)) + ","
2775 return out
2777def oldDump(s: str) -> str:
2778 out = ""
2779 for i in s:
2780 if i == '\n':
2781 out += "["
2782 out += "n"
2783 out += "]"
2784 if i == '\t':
2785 out += "["
2786 out += "t"
2787 out += "]"
2788 elif i == ' ':
2789 out += "["
2790 out += " "
2791 out += "]"
2792 else:
2793 out += i
2794 return out
2795#@+node:ekr.20210904114446.1: *4* g.dump_tree & g.tree_to_string
2796def dump_tree(c: Cmdr, dump_body: bool=False, msg: str=None) -> None:
2797 if msg:
2798 print(msg.rstrip())
2799 else:
2800 print('')
2801 for p in c.all_positions():
2802 print(f"clone? {int(p.isCloned())} {' '*p.level()} {p.h}")
2803 if dump_body:
2804 for z in g.splitLines(p.b):
2805 print(z.rstrip())
2807def tree_to_string(c: Cmdr, dump_body: bool=False, msg: str=None) -> str:
2808 result = ['\n']
2809 if msg:
2810 result.append(msg)
2811 for p in c.all_positions():
2812 result.append(f"clone? {int(p.isCloned())} {' '*p.level()} {p.h}")
2813 if dump_body:
2814 for z in g.splitLines(p.b):
2815 result.append(z.rstrip())
2816 return '\n'.join(result)
2817#@+node:ekr.20150227102835.8: *4* g.dump_encoded_string
2818def dump_encoded_string(encoding: str, s: str) -> None:
2819 """Dump s, assumed to be an encoded string."""
2820 # Can't use g.trace here: it calls this function!
2821 print(f"dump_encoded_string: {g.callers()}")
2822 print(f"dump_encoded_string: encoding {encoding}\n")
2823 print(s)
2824 in_comment = False
2825 for ch in s:
2826 if ch == '#':
2827 in_comment = True
2828 elif not in_comment:
2829 print(f"{ord(ch):02x} {repr(ch)}")
2830 elif ch == '\n':
2831 in_comment = False
2832#@+node:ekr.20031218072017.1317: *4* g.file/module/plugin_date
2833def module_date(mod: Any, format: str=None) -> str:
2834 theFile = g.os_path_join(app.loadDir, mod.__file__)
2835 root, ext = g.os_path_splitext(theFile)
2836 return g.file_date(root + ".py", format=format)
2838def plugin_date(plugin_mod: Any, format: str=None) -> str:
2839 theFile = g.os_path_join(app.loadDir, "..", "plugins", plugin_mod.__file__)
2840 root, ext = g.os_path_splitext(theFile)
2841 return g.file_date(root + ".py", format=str)
2843def file_date(theFile: Any, format: str=None) -> str:
2844 if theFile and g.os_path_exists(theFile):
2845 try:
2846 n = g.os_path_getmtime(theFile)
2847 if format is None:
2848 format = "%m/%d/%y %H:%M:%S"
2849 return time.strftime(format, time.gmtime(n))
2850 except(ImportError, NameError):
2851 pass # Time module is platform dependent.
2852 return ""
2853#@+node:ekr.20031218072017.3127: *4* g.get_line & get_line__after
2854# Very useful for tracing.
2856def get_line(s: str, i: int) -> str:
2857 nl = ""
2858 if g.is_nl(s, i):
2859 i = g.skip_nl(s, i)
2860 nl = "[nl]"
2861 j = g.find_line_start(s, i)
2862 k = g.skip_to_end_of_line(s, i)
2863 return nl + s[j:k]
2865# Important: getLine is a completely different function.
2866# getLine = get_line
2868def get_line_after(s: str, i: int) -> str:
2869 nl = ""
2870 if g.is_nl(s, i):
2871 i = g.skip_nl(s, i)
2872 nl = "[nl]"
2873 k = g.skip_to_end_of_line(s, i)
2874 return nl + s[i:k]
2876getLineAfter = get_line_after
2877#@+node:ekr.20080729142651.1: *4* g.getIvarsDict and checkUnchangedIvars
2878def getIvarsDict(obj: Any) -> Dict[str, Any]:
2879 """Return a dictionary of ivars:values for non-methods of obj."""
2880 d: Dict[str, Any] = dict(
2881 [[key, getattr(obj, key)] for key in dir(obj) # type:ignore
2882 if not isinstance(getattr(obj, key), types.MethodType)])
2883 return d
2885def checkUnchangedIvars(
2886 obj: Any,
2887 d: Dict[str, Any],
2888 exceptions: Sequence[str]=None,
2889) -> bool:
2890 if not exceptions:
2891 exceptions = []
2892 ok = True
2893 for key in d:
2894 if key not in exceptions:
2895 if getattr(obj, key) != d.get(key):
2896 g.trace(
2897 f"changed ivar: {key} "
2898 f"old: {repr(d.get(key))} "
2899 f"new: {repr(getattr(obj, key))}")
2900 ok = False
2901 return ok
2902#@+node:ekr.20031218072017.3128: *4* g.pause
2903def pause(s: str) -> None:
2904 g.pr(s)
2905 i = 0
2906 while i < 1000 * 1000:
2907 i += 1
2908#@+node:ekr.20041105091148: *4* g.pdb
2909def pdb(message: str='') -> None:
2910 """Fall into pdb."""
2911 import pdb # Required: we have just defined pdb as a function!
2912 if app and not app.useIpython:
2913 try:
2914 from leo.core.leoQt import QtCore
2915 QtCore.pyqtRemoveInputHook()
2916 except Exception:
2917 pass
2918 if message:
2919 print(message)
2920 # pylint: disable=forgotten-debug-statement
2921 pdb.set_trace()
2922#@+node:ekr.20041224080039: *4* g.dictToString
2923def dictToString(d: Dict[str, str], indent: str='', tag: str=None) -> str:
2924 """Pretty print a Python dict to a string."""
2925 # pylint: disable=unnecessary-lambda
2926 if not d:
2927 return '{}'
2928 result = ['{\n']
2929 indent2 = indent + ' ' * 4
2930 n = 2 + len(indent) + max([len(repr(z)) for z in d.keys()])
2931 for i, key in enumerate(sorted(d, key=lambda z: repr(z))):
2932 pad = ' ' * max(0, (n - len(repr(key))))
2933 result.append(f"{pad}{key}:")
2934 result.append(objToString(d.get(key), indent=indent2))
2935 if i + 1 < len(d.keys()):
2936 result.append(',')
2937 result.append('\n')
2938 result.append(indent + '}')
2939 s = ''.join(result)
2940 return f"{tag}...\n{s}\n" if tag else s
2941#@+node:ekr.20041126060136: *4* g.listToString
2942def listToString(obj: Any, indent: str='', tag: str=None) -> str:
2943 """Pretty print a Python list to a string."""
2944 if not obj:
2945 return '[]'
2946 result = ['[']
2947 indent2 = indent + ' ' * 4
2948 # I prefer not to compress lists.
2949 for i, obj2 in enumerate(obj):
2950 result.append('\n' + indent2)
2951 result.append(objToString(obj2, indent=indent2))
2952 if i + 1 < len(obj) > 1:
2953 result.append(',')
2954 else:
2955 result.append('\n' + indent)
2956 result.append(']')
2957 s = ''.join(result)
2958 return f"{tag}...\n{s}\n" if tag else s
2959#@+node:ekr.20050819064157: *4* g.objToSTring & g.toString
2960def objToString(obj: Any, indent: str='', printCaller: bool=False, tag: str=None) -> str:
2961 """Pretty print any Python object to a string."""
2962 # pylint: disable=undefined-loop-variable
2963 # Looks like a a pylint bug.
2964 #
2965 # Compute s.
2966 if isinstance(obj, dict):
2967 s = dictToString(obj, indent=indent)
2968 elif isinstance(obj, list):
2969 s = listToString(obj, indent=indent)
2970 elif isinstance(obj, tuple):
2971 s = tupleToString(obj, indent=indent)
2972 elif isinstance(obj, str):
2973 # Print multi-line strings as lists.
2974 s = obj
2975 lines = g.splitLines(s)
2976 if len(lines) > 1:
2977 s = listToString(lines, indent=indent)
2978 else:
2979 s = repr(s)
2980 else:
2981 s = repr(obj)
2982 #
2983 # Compute the return value.
2984 if printCaller and tag:
2985 prefix = f"{g.caller()}: {tag}"
2986 elif printCaller or tag:
2987 prefix = g.caller() if printCaller else tag
2988 else:
2989 prefix = ''
2990 if prefix:
2991 sep = '\n' if '\n' in s else ' '
2992 return f"{prefix}:{sep}{s}"
2993 return s
2995toString = objToString
2996#@+node:ekr.20140401054342.16844: *4* g.run_pylint
2997def run_pylint(
2998 fn: str, # Path to file under test.
2999 rc: str, # Path to settings file.
3000 dots: bool=True, # Show level dots in Sherlock traces.
3001 patterns: List[str]=None, # List of Sherlock trace patterns.
3002 sherlock: bool=False, # Enable Sherlock tracing.
3003 show_return: bool=True, # Show returns in Sherlock traces.
3004 stats_patterns: bool=None, # Patterns for Sherlock statistics.
3005 verbose: bool=True, # Show filenames in Sherlock traces.
3006) -> None:
3007 """
3008 Run pylint with the given args, with Sherlock tracing if requested.
3010 **Do not assume g.app exists.**
3012 run() in pylint-leo.py and PylintCommand.run_pylint *optionally* call this function.
3013 """
3014 try:
3015 from pylint import lint #type:ignore
3016 except ImportError:
3017 g.trace('can not import pylint')
3018 return
3019 if not g.os_path_exists(fn):
3020 g.trace('does not exist:', fn)
3021 return
3022 if not g.os_path_exists(rc):
3023 g.trace('does not exist', rc)
3024 return
3025 args = [f"--rcfile={rc}"]
3026 # Prints error number.
3027 # args.append('--msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg}')
3028 args.append(fn)
3029 if sherlock:
3030 sherlock = g.SherlockTracer(
3031 dots=dots,
3032 show_return=show_return,
3033 verbose=True, # verbose: show filenames.
3034 patterns=patterns or [],
3035 )
3036 try:
3037 sherlock.run()
3038 lint.Run(args)
3039 finally:
3040 sherlock.stop()
3041 sherlock.print_stats(patterns=stats_patterns or [])
3042 else:
3043 # print('run_pylint: %s' % g.shortFileName(fn))
3044 try:
3045 lint.Run(args) # does sys.exit
3046 finally:
3047 # Printing does not work well here.
3048 # When not waiting, printing from severl process can be interspersed.
3049 pass
3050#@+node:ekr.20120912153732.10597: *4* g.wait
3051def sleep(n: float) -> None:
3052 """Wait about n milliseconds."""
3053 from time import sleep # type:ignore
3054 sleep(n) # type:ignore
3055#@+node:ekr.20171023140544.1: *4* g.printObj & aliases
3056def printObj(obj: Any, indent: str='', printCaller: bool=False, tag: str=None) -> None:
3057 """Pretty print any Python object using g.pr."""
3058 g.pr(objToString(obj, indent=indent, printCaller=printCaller, tag=tag))
3060printDict = printObj
3061printList = printObj
3062printTuple = printObj
3063#@+node:ekr.20171023110057.1: *4* g.tupleToString
3064def tupleToString(obj: Any, indent: str='', tag: str=None) -> str:
3065 """Pretty print a Python tuple to a string."""
3066 if not obj:
3067 return '(),'
3068 result = ['(']
3069 indent2 = indent + ' ' * 4
3070 for i, obj2 in enumerate(obj):
3071 if len(obj) > 1:
3072 result.append('\n' + indent2)
3073 result.append(objToString(obj2, indent=indent2))
3074 if len(obj) == 1 or i + 1 < len(obj):
3075 result.append(',')
3076 elif len(obj) > 1:
3077 result.append('\n' + indent)
3078 result.append(')')
3079 s = ''.join(result)
3080 return f"{tag}...\n{s}\n" if tag else s
3081#@+node:ekr.20031218072017.1588: *3* g.Garbage Collection
3082#@+node:ekr.20031218072017.1589: *4* g.clearAllIvars
3083def clearAllIvars(o: Any) -> None:
3084 """Clear all ivars of o, a member of some class."""
3085 if o:
3086 o.__dict__.clear()
3087#@+node:ekr.20060127162818: *4* g.enable_gc_debug
3088def enable_gc_debug() -> None:
3090 gc.set_debug(
3091 gc.DEBUG_STATS | # prints statistics.
3092 gc.DEBUG_LEAK | # Same as all below.
3093 gc.DEBUG_COLLECTABLE |
3094 gc.DEBUG_UNCOLLECTABLE |
3095 # gc.DEBUG_INSTANCES |
3096 # gc.DEBUG_OBJECTS |
3097 gc.DEBUG_SAVEALL)
3098#@+node:ekr.20031218072017.1592: *4* g.printGc
3099# Formerly called from unit tests.
3101def printGc() -> None:
3102 """Called from trace_gc_plugin."""
3103 g.printGcSummary()
3104 g.printGcObjects()
3105 g.printGcRefs()
3106#@+node:ekr.20060127164729.1: *4* g.printGcObjects
3107lastObjectCount = 0
3109def printGcObjects() -> int:
3110 """Print a summary of GC statistics."""
3111 global lastObjectCount
3112 n = len(gc.garbage)
3113 n2 = len(gc.get_objects())
3114 delta = n2 - lastObjectCount
3115 print('-' * 30)
3116 print(f"garbage: {n}")
3117 print(f"{delta:6d} = {n2:7d} totals")
3118 # print number of each type of object.
3119 d: Dict[str, int] = {}
3120 count = 0
3121 for obj in gc.get_objects():
3122 key = str(type(obj))
3123 n = d.get(key, 0)
3124 d[key] = n + 1
3125 count += 1
3126 print(f"{count:7} objects...")
3127 # Invert the dict.
3128 d2: Dict[int, str] = {v: k for k, v in d.items()}
3129 for key in reversed(sorted(d2.keys())): # type:ignore
3130 val = d2.get(key) # type:ignore
3131 print(f"{key:7} {val}")
3132 lastObjectCount = count
3133 return delta
3134#@+node:ekr.20031218072017.1593: *4* g.printGcRefs
3135def printGcRefs() -> None:
3137 refs = gc.get_referrers(app.windowList[0])
3138 print(f"{len(refs):d} referers")
3139#@+node:ekr.20060205043324.1: *4* g.printGcSummary
3140def printGcSummary() -> None:
3142 g.enable_gc_debug()
3143 try:
3144 n = len(gc.garbage)
3145 n2 = len(gc.get_objects())
3146 s = f"printGCSummary: garbage: {n}, objects: {n2}"
3147 print(s)
3148 except Exception:
3149 traceback.print_exc()
3150#@+node:ekr.20180528151850.1: *3* g.printTimes
3151def printTimes(times: List) -> None:
3152 """
3153 Print the differences in the times array.
3155 times: an array of times (calls to time.process_time()).
3156 """
3157 for n, junk in enumerate(times[:-1]):
3158 t = times[n + 1] - times[n]
3159 if t > 0.1:
3160 g.trace(f"*** {n} {t:5.4f} sec.")
3161#@+node:ekr.20031218072017.3133: *3* g.Statistics
3162#@+node:ekr.20031218072017.3134: *4* g.clearStats
3163def clearStats() -> None:
3165 g.app.statsDict = {}
3166#@+node:ekr.20031218072017.3135: *4* g.printStats
3167@command('show-stats')
3168def printStats(event: Any=None, name: str=None) -> None:
3169 """
3170 Print all gathered statistics.
3172 Here is the recommended code to gather stats for one method/function:
3174 if not g.app.statsLockout:
3175 g.app.statsLockout = True
3176 try:
3177 d = g.app.statsDict
3178 key = 'g.isUnicode:' + g.callers()
3179 d [key] = d.get(key, 0) + 1
3180 finally:
3181 g.app.statsLockout = False
3182 """
3183 if name:
3184 if not isinstance(name, str):
3185 name = repr(name)
3186 else:
3187 # Get caller name 2 levels back.
3188 name = g._callerName(n=2)
3189 # Print the stats, organized by number of calls.
3190 d = g.app.statsDict
3191 print('g.app.statsDict...')
3192 for key in reversed(sorted(d)):
3193 print(f"{key:7} {d.get(key)}")
3194#@+node:ekr.20031218072017.3136: *4* g.stat
3195def stat(name: str=None) -> None:
3196 """Increments the statistic for name in g.app.statsDict
3197 The caller's name is used by default.
3198 """
3199 d = g.app.statsDict
3200 if name:
3201 if not isinstance(name, str):
3202 name = repr(name)
3203 else:
3204 name = g._callerName(n=2) # Get caller name 2 levels back.
3205 d[name] = 1 + d.get(name, 0)
3206#@+node:ekr.20031218072017.3137: *3* g.Timing
3207def getTime() -> float:
3208 return time.time()
3210def esDiffTime(message: str, start: float) -> float:
3211 delta = time.time() - start
3212 g.es('', f"{message} {delta:5.2f} sec.")
3213 return time.time()
3215def printDiffTime(message: str, start: float) -> float:
3216 delta = time.time() - start
3217 g.pr(f"{message} {delta:5.2f} sec.")
3218 return time.time()
3220def timeSince(start: float) -> str:
3221 return f"{time.time()-start:5.2f} sec."
3222#@+node:ekr.20031218072017.1380: ** g.Directives
3223# Weird pylint bug, activated by TestLeoGlobals class.
3224# Disabling this will be safe, because pyflakes will still warn about true redefinitions
3225# pylint: disable=function-redefined
3226#@+node:EKR.20040504150046.4: *3* g.comment_delims_from_extension
3227def comment_delims_from_extension(filename: str) -> Tuple[str, str, str]:
3228 """
3229 Return the comment delims corresponding to the filename's extension.
3230 """
3231 if filename.startswith('.'):
3232 root, ext = None, filename
3233 else:
3234 root, ext = os.path.splitext(filename)
3235 if ext == '.tmp':
3236 root, ext = os.path.splitext(root)
3237 language = g.app.extension_dict.get(ext[1:])
3238 if ext:
3239 return g.set_delims_from_language(language)
3240 g.trace(
3241 f"unknown extension: {ext!r}, "
3242 f"filename: {filename!r}, "
3243 f"root: {root!r}")
3244 return '', '', ''
3245#@+node:ekr.20170201150505.1: *3* g.findAllValidLanguageDirectives
3246def findAllValidLanguageDirectives(s: str) -> List:
3247 """Return list of all valid @language directives in p.b"""
3248 if not s.strip():
3249 return []
3250 languages = set()
3251 for m in g.g_language_pat.finditer(s):
3252 language = m.group(1)
3253 if g.isValidLanguage(language):
3254 languages.add(language)
3255 return list(sorted(languages))
3256#@+node:ekr.20090214075058.8: *3* g.findAtTabWidthDirectives (must be fast)
3257def findTabWidthDirectives(c: Cmdr, p: Pos) -> Optional[str]:
3258 """Return the language in effect at position p."""
3259 if c is None:
3260 return None # c may be None for testing.
3261 w = None
3262 # 2009/10/02: no need for copy arg to iter
3263 for p in p.self_and_parents(copy=False):
3264 if w:
3265 break
3266 for s in p.h, p.b:
3267 if w:
3268 break
3269 anIter = g_tabwidth_pat.finditer(s)
3270 for m in anIter:
3271 word = m.group(0)
3272 i = m.start(0)
3273 j = g.skip_ws(s, i + len(word))
3274 junk, w = g.skip_long(s, j)
3275 if w == 0:
3276 w = None
3277 return w
3278#@+node:ekr.20170127142001.5: *3* g.findFirstAtLanguageDirective
3279def findFirstValidAtLanguageDirective(s: str) -> Optional[str]:
3280 """Return the first *valid* @language directive ins."""
3281 if not s.strip():
3282 return None
3283 for m in g.g_language_pat.finditer(s):
3284 language = m.group(1)
3285 if g.isValidLanguage(language):
3286 return language
3287 return None
3288#@+node:ekr.20090214075058.6: *3* g.findLanguageDirectives (must be fast)
3289def findLanguageDirectives(c: Cmdr, p: Pos) -> Optional[str]:
3290 """Return the language in effect at position p."""
3291 if c is None or p is None:
3292 return None # c may be None for testing.
3294 v0 = p.v
3296 def find_language(p_or_v: Any) -> Optional[str]:
3297 for s in p_or_v.h, p_or_v.b:
3298 for m in g_language_pat.finditer(s):
3299 language = m.group(1)
3300 if g.isValidLanguage(language):
3301 return language
3302 return None
3304 # First, search up the tree.
3305 for p in p.self_and_parents(copy=False):
3306 language = find_language(p)
3307 if language:
3308 return language
3309 # #1625: Second, expand the search for cloned nodes.
3310 seen = [] # vnodes that have already been searched.
3311 parents = v0.parents[:] # vnodes whose ancestors are to be searched.
3312 while parents:
3313 parent_v = parents.pop()
3314 if parent_v in seen:
3315 continue
3316 seen.append(parent_v)
3317 language = find_language(parent_v)
3318 if language:
3319 return language
3320 for grand_parent_v in parent_v.parents:
3321 if grand_parent_v not in seen:
3322 parents.append(grand_parent_v)
3323 # Finally, fall back to the defaults.
3324 return c.target_language.lower() if c.target_language else 'python'
3325#@+node:ekr.20031218072017.1385: *3* g.findReference
3326# Called from the syntax coloring method that colorizes section references.
3327# Also called from write at.putRefAt.
3329def findReference(name: str, root: Pos) -> Optional[Pos]:
3330 """Return the position containing the section definition for name."""
3331 for p in root.subtree(copy=False):
3332 assert p != root
3333 if p.matchHeadline(name) and not p.isAtIgnoreNode():
3334 return p.copy()
3335 return None
3336#@+node:ekr.20090214075058.9: *3* g.get_directives_dict (must be fast)
3337# The caller passes [root_node] or None as the second arg.
3338# This allows us to distinguish between None and [None].
3340def get_directives_dict(p: Pos, root: Any=None) -> Dict[str, str]:
3341 """
3342 Scan p for Leo directives found in globalDirectiveList.
3344 Returns a dict containing the stripped remainder of the line
3345 following the first occurrence of each recognized directive
3346 """
3347 if root:
3348 root_node = root[0]
3349 d = {}
3350 #
3351 # #1688: legacy: Always compute the pattern.
3352 # g.directives_pat is updated whenever loading a plugin.
3353 #
3354 # The headline has higher precedence because it is more visible.
3355 for kind, s in (('head', p.h), ('body', p.b)):
3356 anIter = g.directives_pat.finditer(s)
3357 for m in anIter:
3358 word = m.group(1).strip()
3359 i = m.start(1)
3360 if word in d:
3361 continue
3362 j = i + len(word)
3363 if j < len(s) and s[j] not in ' \t\n':
3364 continue
3365 # Not a valid directive: just ignore it.
3366 # A unit test tests that @path:any is invalid.
3367 k = g.skip_line(s, j)
3368 val = s[j:k].strip()
3369 d[word] = val
3370 if root:
3371 anIter = g_noweb_root.finditer(p.b)
3372 for m in anIter:
3373 if root_node:
3374 d["root"] = 0 # value not immportant
3375 else:
3376 g.es(f'{g.angleBrackets("*")} may only occur in a topmost node (i.e., without a parent)')
3377 break
3378 return d
3379#@+node:ekr.20080827175609.1: *3* g.get_directives_dict_list (must be fast)
3380def get_directives_dict_list(p: Pos) -> List[Dict]:
3381 """Scans p and all its ancestors for directives.
3383 Returns a list of dicts containing pointers to
3384 the start of each directive"""
3385 result = []
3386 p1 = p.copy()
3387 for p in p1.self_and_parents(copy=False):
3388 root = None if p.hasParent() else [p]
3389 # No copy necessary: g.get_directives_dict does not change p.
3390 result.append(g.get_directives_dict(p, root=root))
3391 return result
3392#@+node:ekr.20111010082822.15545: *3* g.getLanguageFromAncestorAtFileNode
3393def getLanguageFromAncestorAtFileNode(p: Pos) -> Optional[str]:
3394 """
3395 Return the language in effect at node p.
3397 1. Use an unambiguous @language directive in p itself.
3398 2. Search p's "extended parents" for an @<file> node.
3399 3. Search p's "extended parents" for an unambiguous @language directive.
3400 """
3401 v0 = p.v
3402 seen: Set[VNode]
3404 # The same generator as in v.setAllAncestorAtFileNodesDirty.
3405 # Original idea by Виталије Милошевић (Vitalije Milosevic).
3406 # Modified by EKR.
3408 def v_and_parents(v: "VNode") -> Generator:
3409 if v in seen:
3410 return
3411 seen.add(v)
3412 yield v
3413 for parent_v in v.parents:
3414 if parent_v not in seen:
3415 yield from v_and_parents(parent_v)
3417 def find_language(v: "VNode", phase: int) -> Optional[str]:
3418 """
3419 A helper for all searches.
3420 Phase one searches only @<file> nodes.
3421 """
3422 if phase == 1 and not v.isAnyAtFileNode():
3423 return None
3424 # #1693: Scan v.b for an *unambiguous* @language directive.
3425 languages = g.findAllValidLanguageDirectives(v.b)
3426 if len(languages) == 1: # An unambiguous language
3427 return languages[0]
3428 if v.isAnyAtFileNode():
3429 # Use the file's extension.
3430 name = v.anyAtFileNodeName()
3431 junk, ext = g.os_path_splitext(name)
3432 ext = ext[1:] # strip the leading period.
3433 language = g.app.extension_dict.get(ext)
3434 if g.isValidLanguage(language):
3435 return language
3436 return None
3438 # First, see if p contains any @language directive.
3439 language = g.findFirstValidAtLanguageDirective(p.b)
3440 if language:
3441 return language
3442 #
3443 # Phase 1: search only @<file> nodes: #2308.
3444 # Phase 2: search all nodes.
3445 for phase in (1, 2):
3446 # Search direct parents.
3447 for p2 in p.self_and_parents(copy=False):
3448 language = find_language(p2.v, phase)
3449 if language:
3450 return language
3451 # Search all extended parents.
3452 seen = set([v0.context.hiddenRootNode])
3453 for v in v_and_parents(v0):
3454 language = find_language(v, phase)
3455 if language:
3456 return language
3457 return None
3458#@+node:ekr.20150325075144.1: *3* g.getLanguageFromPosition
3459def getLanguageAtPosition(c: Cmdr, p: Pos) -> str:
3460 """
3461 Return the language in effect at position p.
3462 This is always a lowercase language name, never None.
3463 """
3464 aList = g.get_directives_dict_list(p)
3465 d = g.scanAtCommentAndAtLanguageDirectives(aList)
3466 language = (
3467 d and d.get('language') or
3468 g.getLanguageFromAncestorAtFileNode(p) or
3469 c.config.getString('target-language') or
3470 'python'
3471 )
3472 return language.lower()
3473#@+node:ekr.20031218072017.1386: *3* g.getOutputNewline
3474def getOutputNewline(c: Cmdr=None, name: str=None) -> str:
3475 """Convert the name of a line ending to the line ending itself.
3477 Priority:
3478 - Use name if name given
3479 - Use c.config.output_newline if c given,
3480 - Otherwise use g.app.config.output_newline.
3481 """
3482 if name:
3483 s = name
3484 elif c:
3485 s = c.config.output_newline
3486 else:
3487 s = app.config.output_newline
3488 if not s:
3489 s = ''
3490 s = s.lower()
3491 if s in ("nl", "lf"):
3492 s = '\n'
3493 elif s == "cr":
3494 s = '\r'
3495 elif s == "platform":
3496 s = os.linesep # 12/2/03: emakital
3497 elif s == "crlf":
3498 s = "\r\n"
3499 else:
3500 s = '\n' # Default for erroneous values.
3501 assert isinstance(s, str), repr(s)
3502 return s
3503#@+node:ekr.20200521075143.1: *3* g.inAtNosearch
3504def inAtNosearch(p: Pos) -> bool:
3505 """Return True if p or p's ancestors contain an @nosearch directive."""
3506 if not p:
3507 return False # #2288.
3508 for p in p.self_and_parents():
3509 if p.is_at_ignore() or re.search(r'(^@|\n@)nosearch\b', p.b):
3510 return True
3511 return False
3512#@+node:ekr.20131230090121.16528: *3* g.isDirective
3513def isDirective(s: str) -> bool:
3514 """Return True if s starts with a directive."""
3515 m = g_is_directive_pattern.match(s)
3516 if m:
3517 s2 = s[m.end(1) :]
3518 if s2 and s2[0] in ".(":
3519 return False
3520 return bool(m.group(1) in g.globalDirectiveList)
3521 return False
3522#@+node:ekr.20200810074755.1: *3* g.isValidLanguage
3523def isValidLanguage(language: str) -> bool:
3524 """True if language exists in leo/modes."""
3525 # 2020/08/12: A hack for c++
3526 if language in ('c++', 'cpp'):
3527 language = 'cplusplus'
3528 fn = g.os_path_join(g.app.loadDir, '..', 'modes', f"{language}.py")
3529 return g.os_path_exists(fn)
3530#@+node:ekr.20080827175609.52: *3* g.scanAtCommentAndLanguageDirectives
3531def scanAtCommentAndAtLanguageDirectives(aList: List) -> Optional[Dict[str, str]]:
3532 """
3533 Scan aList for @comment and @language directives.
3535 @comment should follow @language if both appear in the same node.
3536 """
3537 lang = None
3538 for d in aList:
3539 comment = d.get('comment')
3540 language = d.get('language')
3541 # Important: assume @comment follows @language.
3542 if language:
3543 lang, delim1, delim2, delim3 = g.set_language(language, 0)
3544 if comment:
3545 delim1, delim2, delim3 = g.set_delims_from_string(comment)
3546 if comment or language:
3547 delims = delim1, delim2, delim3
3548 d = {'language': lang, 'comment': comment, 'delims': delims}
3549 return d
3550 return None
3551#@+node:ekr.20080827175609.32: *3* g.scanAtEncodingDirectives
3552def scanAtEncodingDirectives(aList: List) -> Optional[str]:
3553 """Scan aList for @encoding directives."""
3554 for d in aList:
3555 encoding = d.get('encoding')
3556 if encoding and g.isValidEncoding(encoding):
3557 return encoding
3558 if encoding and not g.unitTesting:
3559 g.error("invalid @encoding:", encoding)
3560 return None
3561#@+node:ekr.20080827175609.53: *3* g.scanAtHeaderDirectives
3562def scanAtHeaderDirectives(aList: List) -> None:
3563 """scan aList for @header and @noheader directives."""
3564 for d in aList:
3565 if d.get('header') and d.get('noheader'):
3566 g.error("conflicting @header and @noheader directives")
3567#@+node:ekr.20080827175609.33: *3* g.scanAtLineendingDirectives
3568def scanAtLineendingDirectives(aList: List) -> Optional[str]:
3569 """Scan aList for @lineending directives."""
3570 for d in aList:
3571 e = d.get('lineending')
3572 if e in ("cr", "crlf", "lf", "nl", "platform"):
3573 lineending = g.getOutputNewline(name=e)
3574 return lineending
3575 # else:
3576 # g.error("invalid @lineending directive:",e)
3577 return None
3578#@+node:ekr.20080827175609.34: *3* g.scanAtPagewidthDirectives
3579def scanAtPagewidthDirectives(aList: List, issue_error_flag: bool=False) -> Optional[str]:
3580 """Scan aList for @pagewidth directives."""
3581 for d in aList:
3582 s = d.get('pagewidth')
3583 if s is not None:
3584 i, val = g.skip_long(s, 0)
3585 if val is not None and val > 0:
3586 return val
3587 if issue_error_flag and not g.unitTesting:
3588 g.error("ignoring @pagewidth", s)
3589 return None
3590#@+node:ekr.20101022172109.6108: *3* g.scanAtPathDirectives
3591def scanAtPathDirectives(c: Cmdr, aList: List) -> str:
3592 path = c.scanAtPathDirectives(aList)
3593 return path
3595def scanAllAtPathDirectives(c: Cmdr, p: Pos) -> str:
3596 aList = g.get_directives_dict_list(p)
3597 path = c.scanAtPathDirectives(aList)
3598 return path
3599#@+node:ekr.20080827175609.37: *3* g.scanAtTabwidthDirectives
3600def scanAtTabwidthDirectives(aList: List, issue_error_flag: bool=False) -> Optional[int]:
3601 """Scan aList for @tabwidth directives."""
3602 for d in aList:
3603 s = d.get('tabwidth')
3604 if s is not None:
3605 junk, val = g.skip_long(s, 0)
3606 if val not in (None, 0):
3607 return val
3608 if issue_error_flag and not g.unitTesting:
3609 g.error("ignoring @tabwidth", s)
3610 return None
3612def scanAllAtTabWidthDirectives(c: Cmdr, p: Pos) -> Optional[int]:
3613 """Scan p and all ancestors looking for @tabwidth directives."""
3614 if c and p:
3615 aList = g.get_directives_dict_list(p)
3616 val = g.scanAtTabwidthDirectives(aList)
3617 ret = c.tab_width if val is None else val
3618 else:
3619 ret = None
3620 return ret
3621#@+node:ekr.20080831084419.4: *3* g.scanAtWrapDirectives
3622def scanAtWrapDirectives(aList: List, issue_error_flag: bool=False) -> Optional[bool]:
3623 """Scan aList for @wrap and @nowrap directives."""
3624 for d in aList:
3625 if d.get('wrap') is not None:
3626 return True
3627 if d.get('nowrap') is not None:
3628 return False
3629 return None
3631def scanAllAtWrapDirectives(c: Cmdr, p: Pos) -> Optional[bool]:
3632 """Scan p and all ancestors looking for @wrap/@nowrap directives."""
3633 if c and p:
3634 default = bool(c and c.config.getBool("body-pane-wraps"))
3635 aList = g.get_directives_dict_list(p)
3636 val = g.scanAtWrapDirectives(aList)
3637 ret = default if val is None else val
3638 else:
3639 ret = None
3640 return ret
3641#@+node:ekr.20040715155607: *3* g.scanForAtIgnore
3642def scanForAtIgnore(c: Cmdr, p: Pos) -> bool:
3643 """Scan position p and its ancestors looking for @ignore directives."""
3644 if g.unitTesting:
3645 return False # For unit tests.
3646 for p in p.self_and_parents(copy=False):
3647 d = g.get_directives_dict(p)
3648 if 'ignore' in d:
3649 return True
3650 return False
3651#@+node:ekr.20040712084911.1: *3* g.scanForAtLanguage
3652def scanForAtLanguage(c: Cmdr, p: Pos) -> str:
3653 """Scan position p and p's ancestors looking only for @language and @ignore directives.
3655 Returns the language found, or c.target_language."""
3656 # Unlike the code in x.scanAllDirectives, this code ignores @comment directives.
3657 if c and p:
3658 for p in p.self_and_parents(copy=False):
3659 d = g.get_directives_dict(p)
3660 if 'language' in d:
3661 z = d["language"]
3662 language, delim1, delim2, delim3 = g.set_language(z, 0)
3663 return language
3664 return c.target_language
3665#@+node:ekr.20041123094807: *3* g.scanForAtSettings
3666def scanForAtSettings(p: Pos) -> bool:
3667 """Scan position p and its ancestors looking for @settings nodes."""
3668 for p in p.self_and_parents(copy=False):
3669 h = p.h
3670 h = g.app.config.canonicalizeSettingName(h)
3671 if h.startswith("@settings"):
3672 return True
3673 return False
3674#@+node:ekr.20031218072017.1382: *3* g.set_delims_from_language
3675def set_delims_from_language(language: str) -> Tuple[str, str, str]:
3676 """Return a tuple (single,start,end) of comment delims."""
3677 val = g.app.language_delims_dict.get(language)
3678 if val:
3679 delim1, delim2, delim3 = g.set_delims_from_string(val)
3680 if delim2 and not delim3:
3681 return '', delim1, delim2
3682 # 0,1 or 3 params.
3683 return delim1, delim2, delim3
3684 return '', '', ''
3685 # Indicate that no change should be made
3686#@+node:ekr.20031218072017.1383: *3* g.set_delims_from_string
3687def set_delims_from_string(s: str) -> Tuple[str, str, str]:
3688 """
3689 Return (delim1, delim2, delim2), the delims following the @comment
3690 directive.
3692 This code can be called from @language logic, in which case s can
3693 point at @comment
3694 """
3695 # Skip an optional @comment
3696 tag = "@comment"
3697 i = 0
3698 if g.match_word(s, i, tag):
3699 i += len(tag)
3700 count = 0
3701 delims = ['', '', '']
3702 while count < 3 and i < len(s):
3703 i = j = g.skip_ws(s, i)
3704 while i < len(s) and not g.is_ws(s[i]) and not g.is_nl(s, i):
3705 i += 1
3706 if j == i:
3707 break
3708 delims[count] = s[j:i] or ''
3709 count += 1
3710 # 'rr 09/25/02
3711 if count == 2: # delims[0] is always the single-line delim.
3712 delims[2] = delims[1]
3713 delims[1] = delims[0]
3714 delims[0] = ''
3715 for i in range(0, 3):
3716 if delims[i]:
3717 if delims[i].startswith("@0x"):
3718 # Allow delimiter definition as @0x + hexadecimal encoded delimiter
3719 # to avoid problems with duplicate delimiters on the @comment line.
3720 # If used, whole delimiter must be encoded.
3721 if len(delims[i]) == 3:
3722 g.warning(f"'{delims[i]}' delimiter is invalid")
3723 return None, None, None
3724 try:
3725 delims[i] = binascii.unhexlify(delims[i][3:]) # type:ignore
3726 delims[i] = g.toUnicode(delims[i])
3727 except Exception as e:
3728 g.warning(f"'{delims[i]}' delimiter is invalid: {e}")
3729 return None, None, None
3730 else:
3731 # 7/8/02: The "REM hack": replace underscores by blanks.
3732 # 9/25/02: The "perlpod hack": replace double underscores by newlines.
3733 delims[i] = delims[i].replace("__", '\n').replace('_', ' ')
3734 return delims[0], delims[1], delims[2]
3735#@+node:ekr.20031218072017.1384: *3* g.set_language
3736def set_language(s: str, i: int, issue_errors_flag: bool=False) -> Tuple:
3737 """Scan the @language directive that appears at s[i:].
3739 The @language may have been stripped away.
3741 Returns (language, delim1, delim2, delim3)
3742 """
3743 tag = "@language"
3744 assert i is not None
3745 if g.match_word(s, i, tag):
3746 i += len(tag)
3747 # Get the argument.
3748 i = g.skip_ws(s, i)
3749 j = i
3750 i = g.skip_c_id(s, i)
3751 # Allow tcl/tk.
3752 arg = s[j:i].lower()
3753 if app.language_delims_dict.get(arg):
3754 language = arg
3755 delim1, delim2, delim3 = g.set_delims_from_language(language)
3756 return language, delim1, delim2, delim3
3757 if issue_errors_flag:
3758 g.es("ignoring:", g.get_line(s, i))
3759 return None, None, None, None
3760#@+node:ekr.20071109165315: *3* g.stripPathCruft
3761def stripPathCruft(path: str) -> str:
3762 """Strip cruft from a path name."""
3763 if not path:
3764 return path # Retain empty paths for warnings.
3765 if len(path) > 2 and (
3766 (path[0] == '<' and path[-1] == '>') or
3767 (path[0] == '"' and path[-1] == '"') or
3768 (path[0] == "'" and path[-1] == "'")
3769 ):
3770 path = path[1:-1].strip()
3771 # We want a *relative* path, not an absolute path.
3772 return path
3773#@+node:ekr.20090214075058.10: *3* g.update_directives_pat
3774def update_directives_pat() -> None:
3775 """Init/update g.directives_pat"""
3776 global globalDirectiveList, directives_pat
3777 # Use a pattern that guarantees word matches.
3778 aList = [
3779 fr"\b{z}\b" for z in globalDirectiveList if z != 'others'
3780 ]
3781 pat = "^@(%s)" % "|".join(aList)
3782 directives_pat = re.compile(pat, re.MULTILINE)
3784# #1688: Initialize g.directives_pat
3785update_directives_pat()
3786#@+node:ekr.20031218072017.3116: ** g.Files & Directories
3787#@+node:ekr.20080606074139.2: *3* g.chdir
3788def chdir(path: str) -> None:
3789 if not g.os_path_isdir(path):
3790 path = g.os_path_dirname(path)
3791 if g.os_path_isdir(path) and g.os_path_exists(path):
3792 os.chdir(path)
3793#@+node:ekr.20120222084734.10287: *3* g.compute...Dir
3794# For compatibility with old code.
3796def computeGlobalConfigDir() -> str:
3797 return g.app.loadManager.computeGlobalConfigDir()
3799def computeHomeDir() -> str:
3800 return g.app.loadManager.computeHomeDir()
3802def computeLeoDir() -> str:
3803 return g.app.loadManager.computeLeoDir()
3805def computeLoadDir() -> str:
3806 return g.app.loadManager.computeLoadDir()
3808def computeMachineName() -> str:
3809 return g.app.loadManager.computeMachineName()
3811def computeStandardDirectories() -> str:
3812 return g.app.loadManager.computeStandardDirectories()
3813#@+node:ekr.20031218072017.3103: *3* g.computeWindowTitle
3814def computeWindowTitle(fileName: str) -> str:
3816 branch, commit = g.gitInfoForFile(fileName) # #1616
3817 if not fileName:
3818 return branch + ": untitled" if branch else 'untitled'
3819 path, fn = g.os_path_split(fileName)
3820 if path:
3821 title = fn + " in " + path
3822 else:
3823 title = fn
3824 # Yet another fix for bug 1194209: regularize slashes.
3825 if os.sep in '/\\':
3826 title = title.replace('/', os.sep).replace('\\', os.sep)
3827 if branch:
3828 title = branch + ": " + title
3829 return title
3830#@+node:ekr.20031218072017.3117: *3* g.create_temp_file
3831def create_temp_file(textMode: bool=False) -> Tuple[Any, str]:
3832 """
3833 Return a tuple (theFile,theFileName)
3835 theFile: a file object open for writing.
3836 theFileName: the name of the temporary file.
3837 """
3838 try:
3839 # fd is an handle to an open file as would be returned by os.open()
3840 fd, theFileName = tempfile.mkstemp(text=textMode)
3841 mode = 'w' if textMode else 'wb'
3842 theFile = os.fdopen(fd, mode)
3843 except Exception:
3844 g.error('unexpected exception in g.create_temp_file')
3845 g.es_exception()
3846 theFile, theFileName = None, ''
3847 return theFile, theFileName
3848#@+node:ekr.20210307060731.1: *3* g.createHiddenCommander
3849def createHiddenCommander(fn: str) -> Optional[Cmdr]:
3850 """Read the file into a hidden commander (Similar to g.openWithFileName)."""
3851 from leo.core.leoCommands import Commands
3852 c = Commands(fn, gui=g.app.nullGui)
3853 theFile = g.app.loadManager.openAnyLeoFile(fn)
3854 if theFile:
3855 c.fileCommands.openLeoFile( # type:ignore
3856 theFile, fn, readAtFileNodesFlag=True, silent=True)
3857 return c
3858 return None
3859#@+node:vitalije.20170714085545.1: *3* g.defaultLeoFileExtension
3860def defaultLeoFileExtension(c: Cmdr=None) -> str:
3861 conf = c.config if c else g.app.config
3862 return conf.getString('default-leo-extension') or '.leo'
3863#@+node:ekr.20031218072017.3118: *3* g.ensure_extension
3864def ensure_extension(name: str, ext: str) -> str:
3866 theFile, old_ext = g.os_path_splitext(name)
3867 if not name:
3868 return name # don't add to an empty name.
3869 if old_ext in ('.db', '.leo'):
3870 return name
3871 if old_ext and old_ext == ext:
3872 return name
3873 return name + ext
3874#@+node:ekr.20150403150655.1: *3* g.fullPath
3875def fullPath(c: Cmdr, p: Pos, simulate: bool=False) -> str:
3876 """
3877 Return the full path (including fileName) in effect at p. Neither the
3878 path nor the fileName will be created if it does not exist.
3879 """
3880 # Search p and p's parents.
3881 for p in p.self_and_parents(copy=False):
3882 aList = g.get_directives_dict_list(p)
3883 path = c.scanAtPathDirectives(aList)
3884 fn = p.h if simulate else p.anyAtFileNodeName()
3885 # Use p.h for unit tests.
3886 if fn:
3887 # Fix #102: expand path expressions.
3888 fn = c.expand_path_expression(fn) # #1341.
3889 fn = os.path.expanduser(fn) # 1900.
3890 return g.os_path_finalize_join(path, fn) # #1341.
3891 return ''
3892#@+node:ekr.20190327192721.1: *3* g.get_files_in_directory
3893def get_files_in_directory(directory: str, kinds: List=None, recursive: bool=True) -> List[str]:
3894 """
3895 Return a list of all files of the given file extensions in the directory.
3896 Default kinds: ['*.py'].
3897 """
3898 files: List[str] = []
3899 sep = os.path.sep
3900 if not g.os.path.exists(directory):
3901 g.es_print('does not exist', directory)
3902 return files
3903 try:
3904 if kinds:
3905 kinds = [z if z.startswith('*') else '*' + z for z in kinds]
3906 else:
3907 kinds = ['*.py']
3908 if recursive:
3909 # Works for all versions of Python.
3910 for root, dirnames, filenames in os.walk(directory):
3911 for kind in kinds:
3912 for filename in fnmatch.filter(filenames, kind):
3913 files.append(os.path.join(root, filename))
3914 else:
3915 for kind in kinds:
3916 files.extend(glob.glob(directory + sep + kind))
3917 return list(set(sorted(files)))
3918 except Exception:
3919 g.es_exception()
3920 return []
3921#@+node:ekr.20031218072017.1264: *3* g.getBaseDirectory
3922# Handles the conventions applying to the "relative_path_base_directory" configuration option.
3924def getBaseDirectory(c: Cmdr) -> str:
3925 """Convert '!' or '.' to proper directory references."""
3926 base = app.config.relative_path_base_directory
3927 if base and base == "!":
3928 base = app.loadDir
3929 elif base and base == ".":
3930 base = c.openDirectory
3931 if base and g.os_path_isabs(base):
3932 # Set c.chdir_to_relative_path as needed.
3933 if not hasattr(c, 'chdir_to_relative_path'):
3934 c.chdir_to_relative_path = c.config.getBool('chdir-to-relative-path')
3935 # Call os.chdir if requested.
3936 if c.chdir_to_relative_path:
3937 os.chdir(base)
3938 return base # base need not exist yet.
3939 return "" # No relative base given.
3940#@+node:ekr.20170223093758.1: *3* g.getEncodingAt
3941def getEncodingAt(p: Pos, s: str=None) -> str:
3942 """
3943 Return the encoding in effect at p and/or for string s.
3945 Read logic: s is not None.
3946 Write logic: s is None.
3947 """
3948 # A BOM overrides everything.
3949 if s:
3950 e, junk_s = g.stripBOM(s)
3951 if e:
3952 return e
3953 aList = g.get_directives_dict_list(p)
3954 e = g.scanAtEncodingDirectives(aList)
3955 if s and s.strip() and not e:
3956 e = 'utf-8'
3957 return e
3958#@+node:ville.20090701144325.14942: *3* g.guessExternalEditor
3959def guessExternalEditor(c: Cmdr=None) -> Optional[str]:
3960 """ Return a 'sensible' external editor """
3961 editor = (
3962 os.environ.get("LEO_EDITOR") or
3963 os.environ.get("EDITOR") or
3964 g.app.db and g.app.db.get("LEO_EDITOR") or
3965 c and c.config.getString('external-editor'))
3966 if editor:
3967 return editor
3968 # fallbacks
3969 platform = sys.platform.lower()
3970 if platform.startswith('win'):
3971 return "notepad"
3972 if platform.startswith('linux'):
3973 return 'gedit'
3974 g.es(
3975 '''No editor set.
3976Please set LEO_EDITOR or EDITOR environment variable,
3977or do g.app.db['LEO_EDITOR'] = "gvim"''',
3978 )
3979 return None
3980#@+node:ekr.20160330204014.1: *3* g.init_dialog_folder
3981def init_dialog_folder(c: Cmdr, p: Pos, use_at_path: bool=True) -> str:
3982 """Return the most convenient folder to open or save a file."""
3983 if c and p and use_at_path:
3984 path = g.fullPath(c, p)
3985 if path:
3986 dir_ = g.os_path_dirname(path)
3987 if dir_ and g.os_path_exists(dir_):
3988 return dir_
3989 table = (
3990 ('c.last_dir', c and c.last_dir),
3991 ('os.curdir', g.os_path_abspath(os.curdir)),
3992 )
3993 for kind, dir_ in table:
3994 if dir_ and g.os_path_exists(dir_):
3995 return dir_
3996 return ''
3997#@+node:ekr.20100329071036.5744: *3* g.is_binary_file/external_file/string
3998def is_binary_file(f: Any) -> bool:
3999 return f and isinstance(f, io.BufferedIOBase)
4001def is_binary_external_file(fileName: str) -> bool:
4002 try:
4003 with open(fileName, 'rb') as f:
4004 s = f.read(1024) # bytes, in Python 3.
4005 return g.is_binary_string(s)
4006 except IOError:
4007 return False
4008 except Exception:
4009 g.es_exception()
4010 return False
4012def is_binary_string(s: str) -> bool:
4013 # http://stackoverflow.com/questions/898669
4014 # aList is a list of all non-binary characters.
4015 aList = [7, 8, 9, 10, 12, 13, 27] + list(range(0x20, 0x100))
4016 return bool(s.translate(None, bytes(aList))) # type:ignore
4017#@+node:EKR.20040504154039: *3* g.is_sentinel
4018def is_sentinel(line: str, delims: Sequence) -> bool:
4019 """Return True if line starts with a sentinel comment."""
4020 delim1, delim2, delim3 = delims
4021 line = line.lstrip()
4022 if delim1:
4023 return line.startswith(delim1 + '@')
4024 if delim2 and delim3:
4025 i = line.find(delim2 + '@')
4026 j = line.find(delim3)
4027 return 0 == i < j
4028 g.error(f"is_sentinel: can not happen. delims: {repr(delims)}")
4029 return False
4030#@+node:ekr.20031218072017.3119: *3* g.makeAllNonExistentDirectories
4031def makeAllNonExistentDirectories(theDir: str) -> Optional[str]:
4032 """
4033 A wrapper from os.makedirs.
4034 Attempt to make all non-existent directories.
4036 Return True if the directory exists or was created successfully.
4037 """
4038 # Return True if the directory already exists.
4039 theDir = g.os_path_normpath(theDir)
4040 ok = g.os_path_isdir(theDir) and g.os_path_exists(theDir)
4041 if ok:
4042 return theDir
4043 # #1450: Create the directory with os.makedirs.
4044 try:
4045 os.makedirs(theDir, mode=0o777, exist_ok=False)
4046 return theDir
4047 except Exception:
4048 return None
4049#@+node:ekr.20071114113736: *3* g.makePathRelativeTo
4050def makePathRelativeTo(fullPath: str, basePath: str) -> str:
4051 if fullPath.startswith(basePath):
4052 s = fullPath[len(basePath) :]
4053 if s.startswith(os.path.sep):
4054 s = s[len(os.path.sep) :]
4055 return s
4056 return fullPath
4057#@+node:ekr.20090520055433.5945: *3* g.openWithFileName
4058def openWithFileName(fileName: str, old_c: Cmdr=None, gui: str=None) -> Cmdr:
4059 """
4060 Create a Leo Frame for the indicated fileName if the file exists.
4062 Return the commander of the newly-opened outline.
4063 """
4064 return g.app.loadManager.loadLocalFile(fileName, gui, old_c)
4065#@+node:ekr.20150306035851.7: *3* g.readFileIntoEncodedString
4066def readFileIntoEncodedString(fn: str, silent: bool=False) -> Optional[bytes]:
4067 """Return the raw contents of the file whose full path is fn."""
4068 try:
4069 with open(fn, 'rb') as f:
4070 return f.read()
4071 except IOError:
4072 if not silent:
4073 g.error('can not open', fn)
4074 except Exception:
4075 if not silent:
4076 g.error(f"readFileIntoEncodedString: exception reading {fn}")
4077 g.es_exception()
4078 return None
4079#@+node:ekr.20100125073206.8710: *3* g.readFileIntoString
4080def readFileIntoString(
4081 fileName: str,
4082 encoding: str='utf-8', # BOM may override this.
4083 kind: str=None, # @file, @edit, ...
4084 verbose: bool=True,
4085) -> Tuple[Any, Any]:
4086 """
4087 Return the contents of the file whose full path is fileName.
4089 Return (s,e)
4090 s is the string, converted to unicode, or None if there was an error.
4091 e is the encoding of s, computed in the following order:
4092 - The BOM encoding if the file starts with a BOM mark.
4093 - The encoding given in the # -*- coding: utf-8 -*- line for python files.
4094 - The encoding given by the 'encoding' keyword arg.
4095 - None, which typically means 'utf-8'.
4096 """
4097 if not fileName:
4098 if verbose:
4099 g.trace('no fileName arg given')
4100 return None, None
4101 if g.os_path_isdir(fileName):
4102 if verbose:
4103 g.trace('not a file:', fileName)
4104 return None, None
4105 if not g.os_path_exists(fileName):
4106 if verbose:
4107 g.error('file not found:', fileName)
4108 return None, None
4109 try:
4110 e = None
4111 with open(fileName, 'rb') as f:
4112 s = f.read()
4113 # Fix #391.
4114 if not s:
4115 return '', None
4116 # New in Leo 4.11: check for unicode BOM first.
4117 e, s = g.stripBOM(s)
4118 if not e:
4119 # Python's encoding comments override everything else.
4120 junk, ext = g.os_path_splitext(fileName)
4121 if ext == '.py':
4122 e = g.getPythonEncodingFromString(s)
4123 s = g.toUnicode(s, encoding=e or encoding)
4124 return s, e
4125 except IOError:
4126 # Translate 'can not open' and kind, but not fileName.
4127 if verbose:
4128 g.error('can not open', '', (kind or ''), fileName)
4129 except Exception:
4130 g.error(f"readFileIntoString: unexpected exception reading {fileName}")
4131 g.es_exception()
4132 return None, None
4133#@+node:ekr.20160504062833.1: *3* g.readFileToUnicodeString
4134def readFileIntoUnicodeString(fn: str, encoding: Optional[str]=None, silent: bool=False) -> Optional[str]:
4135 """Return the raw contents of the file whose full path is fn."""
4136 try:
4137 with open(fn, 'rb') as f:
4138 s = f.read()
4139 return g.toUnicode(s, encoding=encoding)
4140 except IOError:
4141 if not silent:
4142 g.error('can not open', fn)
4143 except Exception:
4144 g.error(f"readFileIntoUnicodeString: unexpected exception reading {fn}")
4145 g.es_exception()
4146 return None
4147#@+node:ekr.20031218072017.3120: *3* g.readlineForceUnixNewline
4148#@+at Stephen P. Schaefer 9/7/2002
4149#
4150# The Unix readline() routine delivers "\r\n" line end strings verbatim,
4151# while the windows versions force the string to use the Unix convention
4152# of using only "\n". This routine causes the Unix readline to do the
4153# same.
4154#@@c
4156def readlineForceUnixNewline(f: Any, fileName: Optional[str]=None) -> str:
4157 try:
4158 s = f.readline()
4159 except UnicodeDecodeError:
4160 g.trace(f"UnicodeDecodeError: {fileName}", f, g.callers())
4161 s = ''
4162 if len(s) >= 2 and s[-2] == "\r" and s[-1] == "\n":
4163 s = s[0:-2] + "\n"
4164 return s
4165#@+node:ekr.20031218072017.3124: *3* g.sanitize_filename
4166def sanitize_filename(s: str) -> str:
4167 """
4168 Prepares string s to be a valid file name:
4170 - substitute '_' for whitespace and special path characters.
4171 - eliminate all other non-alphabetic characters.
4172 - convert double quotes to single quotes.
4173 - strip leading and trailing whitespace.
4174 - return at most 128 characters.
4175 """
4176 result = []
4177 for ch in s:
4178 if ch in string.ascii_letters:
4179 result.append(ch)
4180 elif ch == '\t':
4181 result.append(' ')
4182 elif ch == '"':
4183 result.append("'")
4184 elif ch in '\\/:|<>*:._':
4185 result.append('_')
4186 s = ''.join(result).strip()
4187 while len(s) > 1:
4188 n = len(s)
4189 s = s.replace('__', '_')
4190 if len(s) == n:
4191 break
4192 return s[:128]
4193#@+node:ekr.20060328150113: *3* g.setGlobalOpenDir
4194def setGlobalOpenDir(fileName: str) -> None:
4195 if fileName:
4196 g.app.globalOpenDir = g.os_path_dirname(fileName)
4197 # g.es('current directory:',g.app.globalOpenDir)
4198#@+node:ekr.20031218072017.3125: *3* g.shortFileName & shortFilename
4199def shortFileName(fileName: str, n: int=None) -> str:
4200 """Return the base name of a path."""
4201 if n is not None:
4202 g.trace('"n" keyword argument is no longer used')
4203 return g.os_path_basename(fileName) if fileName else ''
4205shortFilename = shortFileName
4206#@+node:ekr.20150610125813.1: *3* g.splitLongFileName
4207def splitLongFileName(fn: str, limit: int=40) -> str:
4208 """Return fn, split into lines at slash characters."""
4209 aList = fn.replace('\\', '/').split('/')
4210 n, result = 0, []
4211 for i, s in enumerate(aList):
4212 n += len(s)
4213 result.append(s)
4214 if i + 1 < len(aList):
4215 result.append('/')
4216 n += 1
4217 if n > limit:
4218 result.append('\n')
4219 n = 0
4220 return ''.join(result)
4221#@+node:ekr.20190114061452.26: *3* g.writeFile
4222def writeFile(contents: Union[bytes, str], encoding: str, fileName: str) -> bool:
4223 """Create a file with the given contents."""
4224 try:
4225 if isinstance(contents, str):
4226 contents = g.toEncodedString(contents, encoding=encoding)
4227 # 'wb' preserves line endings.
4228 with open(fileName, 'wb') as f:
4229 f.write(contents) # type:ignore
4230 return True
4231 except Exception as e:
4232 print(f"exception writing: {fileName}:\n{e}")
4233 # g.trace(g.callers())
4234 # g.es_exception()
4235 return False
4236#@+node:ekr.20031218072017.3151: ** g.Finding & Scanning
4237#@+node:ekr.20140602083643.17659: *3* g.find_word
4238def find_word(s: str, word: str, i: int=0) -> int:
4239 """
4240 Return the index of the first occurance of word in s, or -1 if not found.
4242 g.find_word is *not* the same as s.find(i,word);
4243 g.find_word ensures that only word-matches are reported.
4244 """
4245 while i < len(s):
4246 progress = i
4247 i = s.find(word, i)
4248 if i == -1:
4249 return -1
4250 # Make sure we are at the start of a word.
4251 if i > 0:
4252 ch = s[i - 1]
4253 if ch == '_' or ch.isalnum():
4254 i += len(word)
4255 continue
4256 if g.match_word(s, i, word):
4257 return i
4258 i += len(word)
4259 assert progress < i
4260 return -1
4261#@+node:ekr.20211029090118.1: *3* g.findAncestorVnodeByPredicate
4262def findAncestorVnodeByPredicate(p: Pos, v_predicate: Any) -> Optional["VNode"]:
4263 """
4264 Return first ancestor vnode matching the predicate.
4266 The predicate must must be a function of a single vnode argument.
4267 """
4268 if not p:
4269 return None
4270 # First, look up the tree.
4271 for p2 in p.self_and_parents():
4272 if v_predicate(p2.v):
4273 return p2.v
4274 # Look at parents of all cloned nodes.
4275 if not p.isCloned():
4276 return None
4277 seen = [] # vnodes that have already been searched.
4278 parents = p.v.parents[:] # vnodes to be searched.
4279 while parents:
4280 parent_v = parents.pop()
4281 if parent_v in seen:
4282 continue
4283 seen.append(parent_v)
4284 if v_predicate(parent_v):
4285 return parent_v
4286 for grand_parent_v in parent_v.parents:
4287 if grand_parent_v not in seen:
4288 parents.append(grand_parent_v)
4289 return None
4290#@+node:ekr.20170220103251.1: *3* g.findRootsWithPredicate
4291def findRootsWithPredicate(c: Cmdr, root: Pos, predicate: Any=None) -> List[Pos]:
4292 """
4293 Commands often want to find one or more **roots**, given a position p.
4294 A root is the position of any node matching a predicate.
4296 This function formalizes the search order used by the black,
4297 pylint, pyflakes and the rst3 commands, returning a list of zero
4298 or more found roots.
4299 """
4300 seen = []
4301 roots = []
4302 if predicate is None:
4304 # A useful default predicate for python.
4305 # pylint: disable=function-redefined
4307 def predicate(p: Pos) -> bool:
4308 return p.isAnyAtFileNode() and p.h.strip().endswith('.py')
4310 # 1. Search p's tree.
4311 for p in root.self_and_subtree(copy=False):
4312 if predicate(p) and p.v not in seen:
4313 seen.append(p.v)
4314 roots.append(p.copy())
4315 if roots:
4316 return roots
4317 # 2. Look up the tree.
4318 for p in root.parents():
4319 if predicate(p):
4320 return [p.copy()]
4321 # 3. Expand the search if root is a clone.
4322 clones = []
4323 for p in root.self_and_parents(copy=False):
4324 if p.isCloned():
4325 clones.append(p.v)
4326 if clones:
4327 for p in c.all_positions(copy=False):
4328 if predicate(p):
4329 # Match if any node in p's tree matches any clone.
4330 for p2 in p.self_and_subtree():
4331 if p2.v in clones:
4332 return [p.copy()]
4333 return []
4334#@+node:ekr.20031218072017.3156: *3* g.scanError
4335# It is dubious to bump the Tangle error count here, but it really doesn't hurt.
4337def scanError(s: str) -> None:
4338 """Bump the error count in the tangle command."""
4339 # New in Leo 4.4b1: just set this global.
4340 g.app.scanErrors += 1
4341 g.es('', s)
4342#@+node:ekr.20031218072017.3157: *3* g.scanf
4343# A quick and dirty sscanf. Understands only %s and %d.
4345def scanf(s: str, pat: str) -> List[str]:
4346 count = pat.count("%s") + pat.count("%d")
4347 pat = pat.replace("%s", r"(\S+)")
4348 pat = pat.replace("%d", r"(\d+)")
4349 parts = re.split(pat, s)
4350 result: List[str] = []
4351 for part in parts:
4352 if part and len(result) < count:
4353 result.append(part)
4354 return result
4355#@+node:ekr.20031218072017.3158: *3* g.Scanners: calling scanError
4356#@+at These scanners all call g.scanError() directly or indirectly, so they
4357# will call g.es if they find an error. g.scanError() also bumps
4358# c.tangleCommands.errors, which is harmless if we aren't tangling, and
4359# useful if we are.
4360#
4361# These routines are called by the Import routines and the Tangle routines.
4362#@+node:ekr.20031218072017.3159: *4* g.skip_block_comment
4363# Scans past a block comment (an old_style C comment).
4365def skip_block_comment(s: str, i: int) -> int:
4366 assert g.match(s, i, "/*")
4367 j = i
4368 i += 2
4369 n = len(s)
4370 k = s.find("*/", i)
4371 if k == -1:
4372 g.scanError("Run on block comment: " + s[j:i])
4373 return n
4374 return k + 2
4375#@+node:ekr.20031218072017.3160: *4* g.skip_braces
4376#@+at This code is called only from the import logic, so we are allowed to
4377# try some tricks. In particular, we assume all braces are matched in
4378# if blocks.
4379#@@c
4381def skip_braces(s: str, i: int) -> int:
4382 """
4383 Skips from the opening to the matching brace.
4385 If no matching is found i is set to len(s)
4386 """
4387 # start = g.get_line(s,i)
4388 assert g.match(s, i, '{')
4389 level = 0
4390 n = len(s)
4391 while i < n:
4392 c = s[i]
4393 if c == '{':
4394 level += 1
4395 i += 1
4396 elif c == '}':
4397 level -= 1
4398 if level <= 0:
4399 return i
4400 i += 1
4401 elif c == '\'' or c == '"':
4402 i = g.skip_string(s, i)
4403 elif g.match(s, i, '//'):
4404 i = g.skip_to_end_of_line(s, i)
4405 elif g.match(s, i, '/*'):
4406 i = g.skip_block_comment(s, i)
4407 # 7/29/02: be more careful handling conditional code.
4408 elif (
4409 g.match_word(s, i, "#if") or
4410 g.match_word(s, i, "#ifdef") or
4411 g.match_word(s, i, "#ifndef")
4412 ):
4413 i, delta = g.skip_pp_if(s, i)
4414 level += delta
4415 else: i += 1
4416 return i
4417#@+node:ekr.20031218072017.3162: *4* g.skip_parens
4418def skip_parens(s: str, i: int) -> int:
4419 """
4420 Skips from the opening ( to the matching ).
4422 If no matching is found i is set to len(s).
4423 """
4424 level = 0
4425 n = len(s)
4426 assert g.match(s, i, '('), repr(s[i])
4427 while i < n:
4428 c = s[i]
4429 if c == '(':
4430 level += 1
4431 i += 1
4432 elif c == ')':
4433 level -= 1
4434 if level <= 0:
4435 return i
4436 i += 1
4437 elif c == '\'' or c == '"':
4438 i = g.skip_string(s, i)
4439 elif g.match(s, i, "//"):
4440 i = g.skip_to_end_of_line(s, i)
4441 elif g.match(s, i, "/*"):
4442 i = g.skip_block_comment(s, i)
4443 else:
4444 i += 1
4445 return i
4446#@+node:ekr.20031218072017.3163: *4* g.skip_pascal_begin_end
4447def skip_pascal_begin_end(s: str, i: int) -> int:
4448 """
4449 Skips from begin to matching end.
4450 If found, i points to the end. Otherwise, i >= len(s)
4451 The end keyword matches begin, case, class, record, and try.
4452 """
4453 assert g.match_c_word(s, i, "begin")
4454 level = 1
4455 i = g.skip_c_id(s, i) # Skip the opening begin.
4456 while i < len(s):
4457 ch = s[i]
4458 if ch == '{':
4459 i = g.skip_pascal_braces(s, i)
4460 elif ch == '"' or ch == '\'':
4461 i = g.skip_pascal_string(s, i)
4462 elif g.match(s, i, "//"):
4463 i = g.skip_line(s, i)
4464 elif g.match(s, i, "(*"):
4465 i = g.skip_pascal_block_comment(s, i)
4466 elif g.match_c_word(s, i, "end"):
4467 level -= 1
4468 if level == 0:
4469 return i
4470 i = g.skip_c_id(s, i)
4471 elif g.is_c_id(ch):
4472 j = i
4473 i = g.skip_c_id(s, i)
4474 name = s[j:i]
4475 if name in ["begin", "case", "class", "record", "try"]:
4476 level += 1
4477 else:
4478 i += 1
4479 return i
4480#@+node:ekr.20031218072017.3164: *4* g.skip_pascal_block_comment
4481def skip_pascal_block_comment(s: str, i: int) -> int:
4482 """Scan past a pascal comment delimited by (* and *)."""
4483 j = i
4484 assert g.match(s, i, "(*")
4485 i = s.find("*)", i)
4486 if i > -1:
4487 return i + 2
4488 g.scanError("Run on comment" + s[j:i])
4489 return len(s)
4490#@+node:ekr.20031218072017.3165: *4* g.skip_pascal_string
4491def skip_pascal_string(s: str, i: int) -> int:
4492 j = i
4493 delim = s[i]
4494 i += 1
4495 assert delim == '"' or delim == '\''
4496 while i < len(s):
4497 if s[i] == delim:
4498 return i + 1
4499 i += 1
4500 g.scanError("Run on string: " + s[j:i])
4501 return i
4502#@+node:ekr.20031218072017.3166: *4* g.skip_heredoc_string
4503def skip_heredoc_string(s: str, i: int) -> int:
4504 """
4505 08-SEP-2002 DTHEIN.
4506 A heredoc string in PHP looks like:
4508 <<<EOS
4509 This is my string.
4510 It is mine. I own it.
4511 No one else has it.
4512 EOS
4514 It begins with <<< plus a token (naming same as PHP variable names).
4515 It ends with the token on a line by itself (must start in first position.
4516 """
4517 j = i
4518 assert g.match(s, i, "<<<")
4519 m = re.match(r"\<\<\<([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)", s[i:])
4520 if m is None:
4521 i += 3
4522 return i
4523 # 14-SEP-2002 DTHEIN: needed to add \n to find word, not just string
4524 delim = m.group(1) + '\n'
4525 i = g.skip_line(s, i) # 14-SEP-2002 DTHEIN: look after \n, not before
4526 n = len(s)
4527 while i < n and not g.match(s, i, delim):
4528 i = g.skip_line(s, i) # 14-SEP-2002 DTHEIN: move past \n
4529 if i >= n:
4530 g.scanError("Run on string: " + s[j:i])
4531 elif g.match(s, i, delim):
4532 i += len(delim)
4533 return i
4534#@+node:ekr.20031218072017.3167: *4* g.skip_pp_directive
4535def skip_pp_directive(s: str, i: int) -> int:
4536 """Now handles continuation lines and block comments."""
4537 while i < len(s):
4538 if g.is_nl(s, i):
4539 if g.escaped(s, i):
4540 i = g.skip_nl(s, i)
4541 else:
4542 break
4543 elif g.match(s, i, "//"):
4544 i = g.skip_to_end_of_line(s, i)
4545 elif g.match(s, i, "/*"):
4546 i = g.skip_block_comment(s, i)
4547 else:
4548 i += 1
4549 return i
4550#@+node:ekr.20031218072017.3168: *4* g.skip_pp_if
4551# Skips an entire if or if def statement, including any nested statements.
4553def skip_pp_if(s: str, i: int) -> Tuple[int, int]:
4554 start_line = g.get_line(s, i) # used for error messages.
4555 assert(
4556 g.match_word(s, i, "#if") or
4557 g.match_word(s, i, "#ifdef") or
4558 g.match_word(s, i, "#ifndef"))
4559 i = g.skip_line(s, i)
4560 i, delta1 = g.skip_pp_part(s, i)
4561 i = g.skip_ws(s, i)
4562 if g.match_word(s, i, "#else"):
4563 i = g.skip_line(s, i)
4564 i = g.skip_ws(s, i)
4565 i, delta2 = g.skip_pp_part(s, i)
4566 if delta1 != delta2:
4567 g.es("#if and #else parts have different braces:", start_line)
4568 i = g.skip_ws(s, i)
4569 if g.match_word(s, i, "#endif"):
4570 i = g.skip_line(s, i)
4571 else:
4572 g.es("no matching #endif:", start_line)
4573 return i, delta1
4574#@+node:ekr.20031218072017.3169: *4* g.skip_pp_part
4575# Skip to an #else or #endif. The caller has eaten the #if, #ifdef, #ifndef or #else
4577def skip_pp_part(s: str, i: int) -> Tuple[int, int]:
4579 delta = 0
4580 while i < len(s):
4581 c = s[i]
4582 if (
4583 g.match_word(s, i, "#if") or
4584 g.match_word(s, i, "#ifdef") or
4585 g.match_word(s, i, "#ifndef")
4586 ):
4587 i, delta1 = g.skip_pp_if(s, i)
4588 delta += delta1
4589 elif g.match_word(s, i, "#else") or g.match_word(s, i, "#endif"):
4590 return i, delta
4591 elif c == '\'' or c == '"':
4592 i = g.skip_string(s, i)
4593 elif c == '{':
4594 delta += 1
4595 i += 1
4596 elif c == '}':
4597 delta -= 1
4598 i += 1
4599 elif g.match(s, i, "//"):
4600 i = g.skip_line(s, i)
4601 elif g.match(s, i, "/*"):
4602 i = g.skip_block_comment(s, i)
4603 else:
4604 i += 1
4605 return i, delta
4606#@+node:ekr.20031218072017.3171: *4* g.skip_to_semicolon
4607# Skips to the next semicolon that is not in a comment or a string.
4609def skip_to_semicolon(s: str, i: int) -> int:
4610 n = len(s)
4611 while i < n:
4612 c = s[i]
4613 if c == ';':
4614 return i
4615 if c == '\'' or c == '"':
4616 i = g.skip_string(s, i)
4617 elif g.match(s, i, "//"):
4618 i = g.skip_to_end_of_line(s, i)
4619 elif g.match(s, i, "/*"):
4620 i = g.skip_block_comment(s, i)
4621 else:
4622 i += 1
4623 return i
4624#@+node:ekr.20031218072017.3172: *4* g.skip_typedef
4625def skip_typedef(s: str, i: int) -> int:
4626 n = len(s)
4627 while i < n and g.is_c_id(s[i]):
4628 i = g.skip_c_id(s, i)
4629 i = g.skip_ws_and_nl(s, i)
4630 if g.match(s, i, '{'):
4631 i = g.skip_braces(s, i)
4632 i = g.skip_to_semicolon(s, i)
4633 return i
4634#@+node:ekr.20201127143342.1: *3* g.see_more_lines
4635def see_more_lines(s: str, ins: int, n: int=4) -> int:
4636 """
4637 Extend index i within string s to include n more lines.
4638 """
4639 # Show more lines, if they exist.
4640 if n > 0:
4641 for z in range(n):
4642 if ins >= len(s):
4643 break
4644 i, j = g.getLine(s, ins)
4645 ins = j
4646 return max(0, min(ins, len(s)))
4647#@+node:ekr.20031218072017.3195: *3* g.splitLines
4648def splitLines(s: str) -> List[str]:
4649 """
4650 Split s into lines, preserving the number of lines and
4651 the endings of all lines, including the last line.
4652 """
4653 return s.splitlines(True) if s else [] # This is a Python string function!
4655splitlines = splitLines
4656#@+node:ekr.20031218072017.3173: *3* Scanners: no error messages
4657#@+node:ekr.20031218072017.3174: *4* g.escaped
4658# Returns True if s[i] is preceded by an odd number of backslashes.
4660def escaped(s: str, i: int) -> bool:
4661 count = 0
4662 while i - 1 >= 0 and s[i - 1] == '\\':
4663 count += 1
4664 i -= 1
4665 return (count % 2) == 1
4666#@+node:ekr.20031218072017.3175: *4* g.find_line_start
4667def find_line_start(s: str, i: int) -> int:
4668 """Return the index in s of the start of the line containing s[i]."""
4669 if i < 0:
4670 return 0 # New in Leo 4.4.5: add this defensive code.
4671 # bug fix: 11/2/02: change i to i+1 in rfind
4672 i = s.rfind('\n', 0, i + 1) # Finds the highest index in the range.
4673 return 0 if i == -1 else i + 1
4674#@+node:ekr.20031218072017.3176: *4* g.find_on_line
4675def find_on_line(s: str, i: int, pattern: str) -> int:
4676 j = s.find('\n', i)
4677 if j == -1:
4678 j = len(s)
4679 k = s.find(pattern, i, j)
4680 return k
4681#@+node:ekr.20031218072017.3179: *4* g.g.is_special
4682def is_special(s: str, directive: str) -> Tuple[bool, int]:
4683 """Return True if the body text contains the @ directive."""
4684 assert(directive and directive[0] == '@')
4685 lws = directive in ("@others", "@all")
4686 # Most directives must start the line.
4687 pattern_s = r'^\s*(%s\b)' if lws else r'^(%s\b)'
4688 pattern = re.compile(pattern_s % directive, re.MULTILINE)
4689 m = re.search(pattern, s)
4690 if m:
4691 return True, m.start(1)
4692 return False, -1
4693#@+node:ekr.20031218072017.3177: *4* g.is_c_id
4694def is_c_id(ch: str) -> bool:
4695 return g.isWordChar(ch)
4696#@+node:ekr.20031218072017.3178: *4* g.is_nl
4697def is_nl(s: str, i: int) -> bool:
4698 return i < len(s) and (s[i] == '\n' or s[i] == '\r')
4699#@+node:ekr.20031218072017.3180: *4* g.is_ws & is_ws_or_nl
4700def is_ws(ch: str) -> bool:
4701 return ch == '\t' or ch == ' '
4703def is_ws_or_nl(s: str, i: int) -> bool:
4704 return g.is_nl(s, i) or (i < len(s) and g.is_ws(s[i]))
4705#@+node:ekr.20031218072017.3181: *4* g.match
4706# Warning: this code makes no assumptions about what follows pattern.
4708def match(s: str, i: int, pattern: str) -> bool:
4709 return bool(s and pattern and s.find(pattern, i, i + len(pattern)) == i)
4710#@+node:ekr.20031218072017.3182: *4* g.match_c_word
4711def match_c_word(s: str, i: int, name: str) -> bool:
4712 n = len(name)
4713 return bool(
4714 name and
4715 name == s[i : i + n] and
4716 (i + n == len(s) or not g.is_c_id(s[i + n]))
4717 )
4718#@+node:ekr.20031218072017.3183: *4* g.match_ignoring_case
4719def match_ignoring_case(s1: str, s2: str) -> bool:
4720 return bool(s1 and s2 and s1.lower() == s2.lower())
4721#@+node:ekr.20031218072017.3184: *4* g.match_word & g.match_words
4722def match_word(s: str, i: int, pattern: str) -> bool:
4724 # Using a regex is surprisingly tricky.
4725 if pattern is None:
4726 return False
4727 if i > 0 and g.isWordChar(s[i - 1]): # Bug fix: 2017/06/01.
4728 return False
4729 j = len(pattern)
4730 if j == 0:
4731 return False
4732 if s.find(pattern, i, i + j) != i:
4733 return False
4734 if i + j >= len(s):
4735 return True
4736 ch = s[i + j]
4737 return not g.isWordChar(ch)
4739def match_words(s: str, i: int, patterns: Sequence[str]) -> bool:
4740 return any(g.match_word(s, i, pattern) for pattern in patterns)
4741#@+node:ekr.20031218072017.3185: *4* g.skip_blank_lines
4742# This routine differs from skip_ws_and_nl in that
4743# it does not advance over whitespace at the start
4744# of a non-empty or non-nl terminated line
4746def skip_blank_lines(s: str, i: int) -> int:
4747 while i < len(s):
4748 if g.is_nl(s, i):
4749 i = g.skip_nl(s, i)
4750 elif g.is_ws(s[i]):
4751 j = g.skip_ws(s, i)
4752 if g.is_nl(s, j):
4753 i = j
4754 else: break
4755 else: break
4756 return i
4757#@+node:ekr.20031218072017.3186: *4* g.skip_c_id
4758def skip_c_id(s: str, i: int) -> int:
4759 n = len(s)
4760 while i < n and g.isWordChar(s[i]):
4761 i += 1
4762 return i
4763#@+node:ekr.20040705195048: *4* g.skip_id
4764def skip_id(s: str, i: int, chars: str=None) -> int:
4765 chars = g.toUnicode(chars) if chars else ''
4766 n = len(s)
4767 while i < n and (g.isWordChar(s[i]) or s[i] in chars):
4768 i += 1
4769 return i
4770#@+node:ekr.20031218072017.3187: *4* g.skip_line, skip_to_start/end_of_line
4771#@+at These methods skip to the next newline, regardless of whether the
4772# newline may be preceeded by a backslash. Consequently, they should be
4773# used only when we know that we are not in a preprocessor directive or
4774# string.
4775#@@c
4777def skip_line(s: str, i: int) -> int:
4778 if i >= len(s):
4779 return len(s)
4780 if i < 0:
4781 i = 0
4782 i = s.find('\n', i)
4783 if i == -1:
4784 return len(s)
4785 return i + 1
4787def skip_to_end_of_line(s: str, i: int) -> int:
4788 if i >= len(s):
4789 return len(s)
4790 if i < 0:
4791 i = 0
4792 i = s.find('\n', i)
4793 if i == -1:
4794 return len(s)
4795 return i
4797def skip_to_start_of_line(s: str, i: int) -> int:
4798 if i >= len(s):
4799 return len(s)
4800 if i <= 0:
4801 return 0
4802 # Don't find s[i], so it doesn't matter if s[i] is a newline.
4803 i = s.rfind('\n', 0, i)
4804 if i == -1:
4805 return 0
4806 return i + 1
4807#@+node:ekr.20031218072017.3188: *4* g.skip_long
4808def skip_long(s: str, i: int) -> Tuple[int, Optional[int]]:
4809 """
4810 Scan s[i:] for a valid int.
4811 Return (i, val) or (i, None) if s[i] does not point at a number.
4812 """
4813 val = 0
4814 i = g.skip_ws(s, i)
4815 n = len(s)
4816 if i >= n or (not s[i].isdigit() and s[i] not in '+-'):
4817 return i, None
4818 j = i
4819 if s[i] in '+-': # Allow sign before the first digit
4820 i += 1
4821 while i < n and s[i].isdigit():
4822 i += 1
4823 try: # There may be no digits.
4824 val = int(s[j:i])
4825 return i, val
4826 except Exception:
4827 return i, None
4828#@+node:ekr.20031218072017.3190: *4* g.skip_nl
4829# We need this function because different systems have different end-of-line conventions.
4831def skip_nl(s: str, i: int) -> int:
4832 """Skips a single "logical" end-of-line character."""
4833 if g.match(s, i, "\r\n"):
4834 return i + 2
4835 if g.match(s, i, '\n') or g.match(s, i, '\r'):
4836 return i + 1
4837 return i
4838#@+node:ekr.20031218072017.3191: *4* g.skip_non_ws
4839def skip_non_ws(s: str, i: int) -> int:
4840 n = len(s)
4841 while i < n and not g.is_ws(s[i]):
4842 i += 1
4843 return i
4844#@+node:ekr.20031218072017.3192: *4* g.skip_pascal_braces
4845# Skips from the opening { to the matching }.
4847def skip_pascal_braces(s: str, i: int) -> int:
4848 # No constructs are recognized inside Pascal block comments!
4849 if i == -1:
4850 return len(s)
4851 return s.find('}', i)
4852#@+node:ekr.20031218072017.3170: *4* g.skip_python_string
4853def skip_python_string(s: str, i: int) -> int:
4854 if g.match(s, i, "'''") or g.match(s, i, '"""'):
4855 delim = s[i] * 3
4856 i += 3
4857 k = s.find(delim, i)
4858 if k > -1:
4859 return k + 3
4860 return len(s)
4861 return g.skip_string(s, i)
4862#@+node:ekr.20031218072017.2369: *4* g.skip_string
4863def skip_string(s: str, i: int) -> int:
4864 """Scan forward to the end of a string."""
4865 delim = s[i]
4866 i += 1
4867 assert delim in '\'"', (repr(delim), repr(s))
4868 n = len(s)
4869 while i < n and s[i] != delim:
4870 if s[i] == '\\':
4871 i += 2
4872 else:
4873 i += 1
4874 if i >= n:
4875 pass
4876 elif s[i] == delim:
4877 i += 1
4878 return i
4879#@+node:ekr.20031218072017.3193: *4* g.skip_to_char
4880def skip_to_char(s: str, i: int, ch: str) -> Tuple[int, str]:
4881 j = s.find(ch, i)
4882 if j == -1:
4883 return len(s), s[i:]
4884 return j, s[i:j]
4885#@+node:ekr.20031218072017.3194: *4* g.skip_ws, skip_ws_and_nl
4886def skip_ws(s: str, i: int) -> int:
4887 n = len(s)
4888 while i < n and g.is_ws(s[i]):
4889 i += 1
4890 return i
4892def skip_ws_and_nl(s: str, i: int) -> int:
4893 n = len(s)
4894 while i < n and (g.is_ws(s[i]) or g.is_nl(s, i)):
4895 i += 1
4896 return i
4897#@+node:ekr.20170414034616.1: ** g.Git
4898#@+node:ekr.20180325025502.1: *3* g.backupGitIssues
4899def backupGitIssues(c: Cmdr, base_url: str=None) -> None:
4900 """Get a list of issues from Leo's GitHub site."""
4901 if base_url is None:
4902 base_url = 'https://api.github.com/repos/leo-editor/leo-editor/issues'
4904 root = c.lastTopLevel().insertAfter()
4905 root.h = f'Backup of issues: {time.strftime("%Y/%m/%d")}'
4906 label_list: List[str] = []
4907 GitIssueController().backup_issues(base_url, c, label_list, root)
4908 root.expand()
4909 c.selectPosition(root)
4910 c.redraw()
4911 g.trace('done')
4912#@+node:ekr.20170616102324.1: *3* g.execGitCommand
4913def execGitCommand(command: str, directory: str) -> List[str]:
4914 """Execute the given git command in the given directory."""
4915 git_dir = g.os_path_finalize_join(directory, '.git')
4916 if not g.os_path_exists(git_dir):
4917 g.trace('not found:', git_dir, g.callers())
4918 return []
4919 if '\n' in command:
4920 g.trace('removing newline from', command)
4921 command = command.replace('\n', '')
4922 # #1777: Save/restore os.curdir
4923 old_dir = os.getcwd()
4924 if directory:
4925 os.chdir(directory)
4926 try:
4927 p = subprocess.Popen(
4928 shlex.split(command),
4929 stdout=subprocess.PIPE,
4930 stderr=None, # Shows error traces.
4931 shell=False,
4932 )
4933 out, err = p.communicate()
4934 lines = [g.toUnicode(z) for z in g.splitLines(out or [])]
4935 finally:
4936 os.chdir(old_dir)
4937 return lines
4938#@+node:ekr.20180126043905.1: *3* g.getGitIssues
4939def getGitIssues(c: Cmdr,
4940 base_url: str=None,
4941 label_list: List=None,
4942 milestone: str=None,
4943 state: Optional[str]=None, # in (None, 'closed', 'open')
4944) -> None:
4945 """Get a list of issues from Leo's GitHub site."""
4946 if base_url is None:
4947 base_url = 'https://api.github.com/repos/leo-editor/leo-editor/issues'
4948 if isinstance(label_list, (list, tuple)):
4949 root = c.lastTopLevel().insertAfter()
4950 root.h = 'Issues for ' + milestone if milestone else 'Backup'
4951 GitIssueController().backup_issues(base_url, c, label_list, root)
4952 root.expand()
4953 c.selectPosition(root)
4954 c.redraw()
4955 g.trace('done')
4956 else:
4957 g.trace('label_list must be a list or tuple', repr(label_list))
4958#@+node:ekr.20180126044602.1: *4* class GitIssueController
4959class GitIssueController:
4960 """
4961 A class encapsulating the retrieval of GitHub issues.
4963 The GitHub api: https://developer.github.com/v3/issues/
4964 """
4965 #@+others
4966 #@+node:ekr.20180325023336.1: *5* git.backup_issues
4967 def backup_issues(self, base_url: str, c: Cmdr, label_list: List, root: Pos, state: Any=None) -> None:
4969 self.base_url = base_url
4970 self.root = root
4971 self.milestone = None
4972 if label_list:
4973 for state in ('closed', 'open'):
4974 for label in label_list:
4975 self.get_one_issue(label, state)
4976 elif state is None:
4977 for state in ('closed', 'open'):
4978 organizer = root.insertAsLastChild()
4979 organizer.h = f"{state} issues..."
4980 self.get_all_issues(label_list, organizer, state)
4981 elif state in ('closed', 'open'):
4982 self.get_all_issues(label_list, root, state)
4983 else:
4984 g.es_print('state must be in (None, "open", "closed")')
4985 #@+node:ekr.20180325024334.1: *5* git.get_all_issues
4986 def get_all_issues(self, label_list: List, root: Pos, state: Any, limit: int=100) -> None:
4987 """Get all issues for the base url."""
4988 try:
4989 import requests
4990 except Exception:
4991 g.trace('requests not found: `pip install requests`')
4992 return
4993 label = None
4994 assert state in ('open', 'closed')
4995 page_url = self.base_url + '?&state=%s&page=%s'
4996 page, total = 1, 0
4997 while True:
4998 url = page_url % (state, page)
4999 r = requests.get(url)
5000 try:
5001 done, n = self.get_one_page(label, page, r, root)
5002 # Do not remove this trace. It's reassuring.
5003 g.trace(f"done: {done:5} page: {page:3} found: {n} label: {label}")
5004 except AttributeError:
5005 g.trace('Possible rate limit')
5006 self.print_header(r)
5007 g.es_exception()
5008 break
5009 total += n
5010 if done:
5011 break
5012 page += 1
5013 if page > limit:
5014 g.trace('too many pages')
5015 break
5016 #@+node:ekr.20180126044850.1: *5* git.get_issues
5017 def get_issues(self, base_url: str, label_list: List, milestone: Any, root: Pos, state: Any) -> None:
5018 """Create a list of issues for each label in label_list."""
5019 self.base_url = base_url
5020 self.milestone = milestone
5021 self.root = root
5022 for label in label_list:
5023 self.get_one_issue(label, state)
5024 #@+node:ekr.20180126043719.3: *5* git.get_one_issue
5025 def get_one_issue(self, label: str, state: Any, limit: int=20) -> None:
5026 """Create a list of issues with the given label."""
5027 try:
5028 import requests
5029 except Exception:
5030 g.trace('requests not found: `pip install requests`')
5031 return
5032 root = self.root.insertAsLastChild()
5033 page, total = 1, 0
5034 page_url = self.base_url + '?labels=%s&state=%s&page=%s'
5035 while True:
5036 url = page_url % (label, state, page)
5037 r = requests.get(url)
5038 try:
5039 done, n = self.get_one_page(label, page, r, root)
5040 # Do not remove this trace. It's reassuring.
5041 g.trace(f"done: {done:5} page: {page:3} found: {n:3} label: {label}")
5042 except AttributeError:
5043 g.trace('Possible rate limit')
5044 self.print_header(r)
5045 g.es_exception()
5046 break
5047 total += n
5048 if done:
5049 break
5050 page += 1
5051 if page > limit:
5052 g.trace('too many pages')
5053 break
5054 state = state.capitalize()
5055 if self.milestone:
5056 root.h = f"{total} {state} {label} issues for milestone {self.milestone}"
5057 else:
5058 root.h = f"{total} {state} {label} issues"
5059 #@+node:ekr.20180126043719.4: *5* git.get_one_page
5060 def get_one_page(self, label: str, page: int, r: Any, root: Pos) -> Tuple[bool, int]:
5062 if self.milestone:
5063 aList = [
5064 z for z in r.json()
5065 if z.get('milestone') is not None and
5066 self.milestone == z.get('milestone').get('title')
5067 ]
5068 else:
5069 aList = [z for z in r.json()]
5070 for d in aList:
5071 n, title = d.get('number'), d.get('title')
5072 html_url = d.get('html_url') or self.base_url
5073 p = root.insertAsNthChild(0)
5074 p.h = f"#{n}: {title}"
5075 p.b = f"{html_url}\n\n"
5076 p.b += d.get('body').strip()
5077 link = r.headers.get('Link')
5078 done = not link or link.find('rel="next"') == -1
5079 return done, len(aList)
5080 #@+node:ekr.20180127092201.1: *5* git.print_header
5081 def print_header(self, r: Any) -> None:
5083 # r.headers is a CaseInsensitiveDict
5084 # so g.printObj(r.headers) is just repr(r.headers)
5085 if 0:
5086 print('Link', r.headers.get('Link'))
5087 else:
5088 for key in r.headers:
5089 print(f"{key:35}: {r.headers.get(key)}")
5090 #@-others
5091#@+node:ekr.20190428173354.1: *3* g.getGitVersion
5092def getGitVersion(directory: str=None) -> Tuple[str, str, str]:
5093 """Return a tuple (author, build, date) from the git log, or None."""
5094 #
5095 # -n: Get only the last log.
5096 trace = 'git' in g.app.debug
5097 try:
5098 s = subprocess.check_output(
5099 'git log -n 1 --date=iso',
5100 cwd=directory or g.app.loadDir,
5101 stderr=subprocess.DEVNULL,
5102 shell=True,
5103 )
5104 # #1209.
5105 except subprocess.CalledProcessError as e:
5106 s = e.output
5107 if trace:
5108 g.trace('return code', e.returncode)
5109 g.trace('value', repr(s))
5110 g.es_print('Exception in g.getGitVersion')
5111 g.es_exception()
5112 s = g.toUnicode(s)
5113 if not isinstance(s, str):
5114 return '', '', ''
5115 except Exception:
5116 if trace:
5117 g.es_print('Exception in g.getGitVersion')
5118 g.es_exception()
5119 return '', '', ''
5121 info = [g.toUnicode(z) for z in s.splitlines()]
5123 def find(kind: str) -> str:
5124 """Return the given type of log line."""
5125 for z in info:
5126 if z.startswith(kind):
5127 return z.lstrip(kind).lstrip(':').strip()
5128 return ''
5130 return find('Author'), find('commit')[:10], find('Date')
5131#@+node:ekr.20170414034616.2: *3* g.gitBranchName
5132def gitBranchName(path: str=None) -> str:
5133 """
5134 Return the git branch name associated with path/.git, or the empty
5135 string if path/.git does not exist. If path is None, use the leo-editor
5136 directory.
5137 """
5138 branch, commit = g.gitInfo(path)
5139 return branch
5140#@+node:ekr.20170414034616.4: *3* g.gitCommitNumber
5141def gitCommitNumber(path: str=None) -> str:
5142 """
5143 Return the git commit number associated with path/.git, or the empty
5144 string if path/.git does not exist. If path is None, use the leo-editor
5145 directory.
5146 """
5147 branch, commit = g.gitInfo(path)
5148 return commit
5149#@+node:ekr.20200724132432.1: *3* g.gitInfoForFile
5150def gitInfoForFile(filename: str) -> Tuple[str, str]:
5151 """
5152 Return the git (branch, commit) info associated for the given file.
5153 """
5154 # g.gitInfo and g.gitHeadPath now do all the work.
5155 return g.gitInfo(filename)
5156#@+node:ekr.20200724133754.1: *3* g.gitInfoForOutline
5157def gitInfoForOutline(c: Cmdr) -> Tuple[str, str]:
5158 """
5159 Return the git (branch, commit) info associated for commander c.
5160 """
5161 return g.gitInfoForFile(c.fileName())
5162#@+node:maphew.20171112205129.1: *3* g.gitDescribe
5163def gitDescribe(path: str=None) -> Tuple[str, str, str]:
5164 """
5165 Return the Git tag, distance-from-tag, and commit hash for the
5166 associated path. If path is None, use the leo-editor directory.
5168 Given `git describe` cmd line output: `x-leo-v5.6-55-ge1129da\n`
5169 This function returns ('x-leo-v5.6', '55', 'e1129da')
5170 """
5171 describe = g.execGitCommand('git describe --tags --long', path)
5172 tag, distance, commit = describe[0].rsplit('-', 2)
5173 # rsplit not split, as '-' might be in tag name
5174 if 'g' in commit[0:]:
5175 # leading 'g' isn't part of the commit hash
5176 commit = commit[1:]
5177 commit = commit.rstrip()
5178 return tag, distance, commit
5179#@+node:ekr.20170414034616.6: *3* g.gitHeadPath
5180def gitHeadPath(path_s: str) -> Optional[str]:
5181 """
5182 Compute the path to .git/HEAD given the path.
5183 """
5184 path = Path(path_s)
5185 # #1780: Look up the directory tree, looking the .git directory.
5186 while os.path.exists(path):
5187 head = os.path.join(path, '.git', 'HEAD')
5188 if os.path.exists(head):
5189 return head
5190 if path == path.parent:
5191 break
5192 path = path.parent
5193 return None
5194#@+node:ekr.20170414034616.3: *3* g.gitInfo
5195def gitInfo(path: str=None) -> Tuple[str, str]:
5196 """
5197 Path may be a directory or file.
5199 Return the branch and commit number or ('', '').
5200 """
5201 branch, commit = '', '' # Set defaults.
5202 if path is None:
5203 # Default to leo/core.
5204 path = os.path.dirname(__file__)
5205 if not os.path.isdir(path):
5206 path = os.path.dirname(path)
5207 # Does path/../ref exist?
5208 path = g.gitHeadPath(path)
5209 if not path:
5210 return branch, commit
5211 try:
5212 with open(path) as f:
5213 s = f.read()
5214 if not s.startswith('ref'):
5215 branch = 'None'
5216 commit = s[:7]
5217 return branch, commit
5218 # On a proper branch
5219 pointer = s.split()[1]
5220 dirs = pointer.split('/')
5221 branch = dirs[-1]
5222 except IOError:
5223 g.trace('can not open:', path)
5224 return branch, commit
5225 # Try to get a better commit number.
5226 git_dir = g.os_path_finalize_join(path, '..')
5227 try:
5228 path = g.os_path_finalize_join(git_dir, pointer)
5229 with open(path) as f: # type:ignore
5230 s = f.read()
5231 commit = s.strip()[0:12]
5232 # shorten the hash to a unique shortname
5233 except IOError:
5234 try:
5235 path = g.os_path_finalize_join(git_dir, 'packed-refs')
5236 with open(path) as f: # type:ignore
5237 for line in f:
5238 if line.strip().endswith(' ' + pointer):
5239 commit = line.split()[0][0:12]
5240 break
5241 except IOError:
5242 pass
5243 return branch, commit
5244#@+node:ekr.20031218072017.3139: ** g.Hooks & Plugins
5245#@+node:ekr.20101028131948.5860: *3* g.act_on_node
5246def dummy_act_on_node(c: Cmdr, p: Pos, event: Any) -> None:
5247 pass
5249# This dummy definition keeps pylint happy.
5250# Plugins can change this.
5252act_on_node = dummy_act_on_node
5253#@+node:ville.20120502221057.7500: *3* g.childrenModifiedSet, g.contentModifiedSet
5254childrenModifiedSet: Set["VNode"] = set()
5255contentModifiedSet: Set["VNode"] = set()
5256#@+node:ekr.20031218072017.1596: *3* g.doHook
5257def doHook(tag: str, *args: Any, **keywords: Any) -> Any:
5258 """
5259 This global function calls a hook routine. Hooks are identified by the
5260 tag param.
5262 Returns the value returned by the hook routine, or None if the there is
5263 an exception.
5265 We look for a hook routine in three places:
5266 1. c.hookFunction
5267 2. app.hookFunction
5268 3. leoPlugins.doPlugins()
5270 Set app.hookError on all exceptions.
5271 Scripts may reset app.hookError to try again.
5272 """
5273 if g.app.killed or g.app.hookError:
5274 return None
5275 if args:
5276 # A minor error in Leo's core.
5277 g.pr(f"***ignoring args param. tag = {tag}")
5278 if not g.app.config.use_plugins:
5279 if tag in ('open0', 'start1'):
5280 g.warning("Plugins disabled: use_plugins is 0 in a leoSettings.leo file.")
5281 return None
5282 # Get the hook handler function. Usually this is doPlugins.
5283 c = keywords.get("c")
5284 # pylint: disable=consider-using-ternary
5285 f = (c and c.hookFunction) or g.app.hookFunction
5286 if not f:
5287 g.app.hookFunction = f = g.app.pluginsController.doPlugins
5288 try:
5289 # Pass the hook to the hook handler.
5290 # g.pr('doHook',f.__name__,keywords.get('c'))
5291 return f(tag, keywords)
5292 except Exception:
5293 g.es_exception()
5294 g.app.hookError = True # Supress this function.
5295 g.app.idle_time_hooks_enabled = False
5296 return None
5297#@+node:ekr.20100910075900.5950: *3* g.Wrappers for g.app.pluginController methods
5298# Important: we can not define g.pc here!
5299#@+node:ekr.20100910075900.5951: *4* g.Loading & registration
5300def loadOnePlugin(pluginName: str, verbose: bool=False) -> Any:
5301 pc = g.app.pluginsController
5302 return pc.loadOnePlugin(pluginName, verbose=verbose)
5304def registerExclusiveHandler(tags: List[str], fn: str) -> Any:
5305 pc = g.app.pluginsController
5306 return pc.registerExclusiveHandler(tags, fn)
5308def registerHandler(tags: Any, fn: Any) -> Any:
5309 pc = g.app.pluginsController
5310 return pc.registerHandler(tags, fn)
5312def plugin_signon(module_name: str, verbose: bool=False) -> Any:
5313 pc = g.app.pluginsController
5314 return pc.plugin_signon(module_name, verbose)
5316def unloadOnePlugin(moduleOrFileName: str, verbose: bool=False) -> Any:
5317 pc = g.app.pluginsController
5318 return pc.unloadOnePlugin(moduleOrFileName, verbose)
5320def unregisterHandler(tags: Any, fn: Any) -> Any:
5321 pc = g.app.pluginsController
5322 return pc.unregisterHandler(tags, fn)
5323#@+node:ekr.20100910075900.5952: *4* g.Information
5324def getHandlersForTag(tags: List[str]) -> List:
5325 pc = g.app.pluginsController
5326 return pc.getHandlersForTag(tags)
5328def getLoadedPlugins() -> List:
5329 pc = g.app.pluginsController
5330 return pc.getLoadedPlugins()
5332def getPluginModule(moduleName: str) -> Any:
5333 pc = g.app.pluginsController
5334 return pc.getPluginModule(moduleName)
5336def pluginIsLoaded(fn: str) -> bool:
5337 pc = g.app.pluginsController
5338 return pc.isLoaded(fn)
5339#@+node:ekr.20031218072017.1315: ** g.Idle time functions
5340#@+node:EKR.20040602125018.1: *3* g.disableIdleTimeHook
5341def disableIdleTimeHook() -> None:
5342 """Disable the global idle-time hook."""
5343 g.app.idle_time_hooks_enabled = False
5344#@+node:EKR.20040602125018: *3* g.enableIdleTimeHook
5345def enableIdleTimeHook(*args: Any, **keys: Any) -> None:
5346 """Enable idle-time processing."""
5347 g.app.idle_time_hooks_enabled = True
5348#@+node:ekr.20140825042850.18410: *3* g.IdleTime
5349def IdleTime(handler: Any, delay: int=500, tag: str=None) -> Any:
5350 """
5351 A thin wrapper for the LeoQtGui.IdleTime class.
5353 The IdleTime class executes a handler with a given delay at idle time.
5354 The handler takes a single argument, the IdleTime instance::
5356 def handler(timer):
5357 '''IdleTime handler. timer is an IdleTime instance.'''
5358 delta_t = timer.time-timer.starting_time
5359 g.trace(timer.count, '%2.4f' % (delta_t))
5360 if timer.count >= 5:
5361 g.trace('done')
5362 timer.stop()
5364 # Execute handler every 500 msec. at idle time.
5365 timer = g.IdleTime(handler,delay=500)
5366 if timer: timer.start()
5368 Timer instances are completely independent::
5370 def handler1(timer):
5371 delta_t = timer.time-timer.starting_time
5372 g.trace('%2s %2.4f' % (timer.count,delta_t))
5373 if timer.count >= 5:
5374 g.trace('done')
5375 timer.stop()
5377 def handler2(timer):
5378 delta_t = timer.time-timer.starting_time
5379 g.trace('%2s %2.4f' % (timer.count,delta_t))
5380 if timer.count >= 10:
5381 g.trace('done')
5382 timer.stop()
5384 timer1 = g.IdleTime(handler1, delay=500)
5385 timer2 = g.IdleTime(handler2, delay=1000)
5386 if timer1 and timer2:
5387 timer1.start()
5388 timer2.start()
5389 """
5390 try:
5391 return g.app.gui.idleTimeClass(handler, delay, tag)
5392 except Exception:
5393 return None
5394#@+node:ekr.20161027205025.1: *3* g.idleTimeHookHandler (stub)
5395def idleTimeHookHandler(timer: Any) -> None:
5396 """This function exists for compatibility."""
5397 g.es_print('Replaced by IdleTimeManager.on_idle')
5398 g.trace(g.callers())
5399#@+node:ekr.20041219095213: ** g.Importing
5400#@+node:ekr.20040917061619: *3* g.cantImport
5401def cantImport(moduleName: str, pluginName: str=None, verbose: bool=True) -> None:
5402 """Print a "Can't Import" message and return None."""
5403 s = f"Can not import {moduleName}"
5404 if pluginName:
5405 s = s + f" from {pluginName}"
5406 if not g.app or not g.app.gui:
5407 print(s)
5408 elif g.unitTesting:
5409 return
5410 else:
5411 g.warning('', s)
5412#@+node:ekr.20191220044128.1: *3* g.import_module
5413def import_module(name: str, package: str=None) -> Any:
5414 """
5415 A thin wrapper over importlib.import_module.
5416 """
5417 trace = 'plugins' in g.app.debug and not g.unitTesting
5418 exceptions = []
5419 try:
5420 m = importlib.import_module(name, package=package)
5421 except Exception as e:
5422 m = None
5423 if trace:
5424 t, v, tb = sys.exc_info()
5425 del tb # don't need the traceback
5426 v = v or str(t) # type:ignore
5427 # # in case v is empty, we'll at least have the execption type
5428 if v not in exceptions:
5429 exceptions.append(v)
5430 g.trace(f"Can not import {name}: {e}")
5431 return m
5432#@+node:ekr.20140711071454.17650: ** g.Indices, Strings, Unicode & Whitespace
5433#@+node:ekr.20140711071454.17647: *3* g.Indices
5434#@+node:ekr.20050314140957: *4* g.convertPythonIndexToRowCol
5435def convertPythonIndexToRowCol(s: str, i: int) -> Tuple[int, int]:
5436 """Convert index i into string s into zero-based row/col indices."""
5437 if not s or i <= 0:
5438 return 0, 0
5439 i = min(i, len(s))
5440 # works regardless of what s[i] is
5441 row = s.count('\n', 0, i) # Don't include i
5442 if row == 0:
5443 return row, i
5444 prevNL = s.rfind('\n', 0, i) # Don't include i
5445 return row, i - prevNL - 1
5446#@+node:ekr.20050315071727: *4* g.convertRowColToPythonIndex
5447def convertRowColToPythonIndex(s: str, row: int, col: int, lines: List[str]=None) -> int:
5448 """Convert zero-based row/col indices into a python index into string s."""
5449 if row < 0:
5450 return 0
5451 if lines is None:
5452 lines = g.splitLines(s)
5453 if row >= len(lines):
5454 return len(s)
5455 col = min(col, len(lines[row]))
5456 # A big bottleneck
5457 prev = 0
5458 for line in lines[:row]:
5459 prev += len(line)
5460 return prev + col
5461#@+node:ekr.20061031102333.2: *4* g.getWord & getLine
5462def getWord(s: str, i: int) -> Tuple[int, int]:
5463 """Return i,j such that s[i:j] is the word surrounding s[i]."""
5464 if i >= len(s):
5465 i = len(s) - 1
5466 if i < 0:
5467 i = 0
5468 # Scan backwards.
5469 while 0 <= i < len(s) and g.isWordChar(s[i]):
5470 i -= 1
5471 i += 1
5472 # Scan forwards.
5473 j = i
5474 while 0 <= j < len(s) and g.isWordChar(s[j]):
5475 j += 1
5476 return i, j
5478def getLine(s: str, i: int) -> Tuple[int, int]:
5479 """
5480 Return i,j such that s[i:j] is the line surrounding s[i].
5481 s[i] is a newline only if the line is empty.
5482 s[j] is a newline unless there is no trailing newline.
5483 """
5484 if i > len(s):
5485 i = len(s) - 1
5486 if i < 0:
5487 i = 0
5488 # A newline *ends* the line, so look to the left of a newline.
5489 j = s.rfind('\n', 0, i)
5490 if j == -1:
5491 j = 0
5492 else:
5493 j += 1
5494 k = s.find('\n', i)
5495 if k == -1:
5496 k = len(s)
5497 else:
5498 k = k + 1
5499 return j, k
5500#@+node:ekr.20111114151846.9847: *4* g.toPythonIndex
5501def toPythonIndex(s: str, index: int) -> int:
5502 """
5503 Convert index to a Python int.
5505 index may be a Tk index (x.y) or 'end'.
5506 """
5507 if index is None:
5508 return 0
5509 if isinstance(index, int):
5510 return index
5511 if index == '1.0':
5512 return 0
5513 if index == 'end':
5514 return len(s)
5515 data = index.split('.')
5516 if len(data) == 2:
5517 row, col = data
5518 row, col = int(row), int(col)
5519 i = g.convertRowColToPythonIndex(s, row - 1, col)
5520 return i
5521 g.trace(f"bad string index: {index}")
5522 return 0
5523#@+node:ekr.20140526144610.17601: *3* g.Strings
5524#@+node:ekr.20190503145501.1: *4* g.isascii
5525def isascii(s: str) -> bool:
5526 # s.isascii() is defined in Python 3.7.
5527 return all(ord(ch) < 128 for ch in s)
5528#@+node:ekr.20031218072017.3106: *4* g.angleBrackets & virtual_event_name
5529def angleBrackets(s: str) -> str:
5530 """Returns < < s > >"""
5531 lt = "<<"
5532 rt = ">>"
5533 return lt + s + rt
5535virtual_event_name = angleBrackets
5536#@+node:ekr.20090516135452.5777: *4* g.ensureLeading/TrailingNewlines
5537def ensureLeadingNewlines(s: str, n: int) -> str:
5538 s = g.removeLeading(s, '\t\n\r ')
5539 return ('\n' * n) + s
5541def ensureTrailingNewlines(s: str, n: int) -> str:
5542 s = g.removeTrailing(s, '\t\n\r ')
5543 return s + '\n' * n
5544#@+node:ekr.20050920084036.4: *4* g.longestCommonPrefix & g.itemsMatchingPrefixInList
5545def longestCommonPrefix(s1: str, s2: str) -> str:
5546 """Find the longest prefix common to strings s1 and s2."""
5547 prefix = ''
5548 for ch in s1:
5549 if s2.startswith(prefix + ch):
5550 prefix = prefix + ch
5551 else:
5552 return prefix
5553 return prefix
5555def itemsMatchingPrefixInList(s: str, aList: List[str], matchEmptyPrefix: bool=False) -> Tuple[List, str]:
5556 """This method returns a sorted list items of aList whose prefix is s.
5558 It also returns the longest common prefix of all the matches.
5559 """
5560 if s:
5561 pmatches = [a for a in aList if a.startswith(s)]
5562 elif matchEmptyPrefix:
5563 pmatches = aList[:]
5564 else: pmatches = []
5565 if pmatches:
5566 pmatches.sort()
5567 common_prefix = reduce(g.longestCommonPrefix, pmatches)
5568 else:
5569 common_prefix = ''
5570 return pmatches, common_prefix
5571#@+node:ekr.20090516135452.5776: *4* g.removeLeading/Trailing
5572# Warning: g.removeTrailingWs already exists.
5573# Do not change it!
5575def removeLeading(s: str, chars: str) -> str:
5576 """Remove all characters in chars from the front of s."""
5577 i = 0
5578 while i < len(s) and s[i] in chars:
5579 i += 1
5580 return s[i:]
5582def removeTrailing(s: str, chars: str) -> str:
5583 """Remove all characters in chars from the end of s."""
5584 i = len(s) - 1
5585 while i >= 0 and s[i] in chars:
5586 i -= 1
5587 i += 1
5588 return s[:i]
5589#@+node:ekr.20060410112600: *4* g.stripBrackets
5590def stripBrackets(s: str) -> str:
5591 """Strip leading and trailing angle brackets."""
5592 if s.startswith('<'):
5593 s = s[1:]
5594 if s.endswith('>'):
5595 s = s[:-1]
5596 return s
5597#@+node:ekr.20170317101100.1: *4* g.unCamel
5598def unCamel(s: str) -> List[str]:
5599 """Return a list of sub-words in camelCased string s."""
5600 result: List[str] = []
5601 word: List[str] = []
5602 for ch in s:
5603 if ch.isalpha() and ch.isupper():
5604 if word:
5605 result.append(''.join(word))
5606 word = [ch]
5607 elif ch.isalpha():
5608 word.append(ch)
5609 elif word:
5610 result.append(''.join(word))
5611 word = []
5612 if word:
5613 result.append(''.join(word))
5614 return result
5615#@+node:ekr.20031218072017.1498: *3* g.Unicode
5616#@+node:ekr.20190505052756.1: *4* g.checkUnicode
5617checkUnicode_dict: Dict[str, bool] = {}
5619def checkUnicode(s: str, encoding: str=None) -> str:
5620 """
5621 Warn when converting bytes. Report *all* errors.
5623 This method is meant to document defensive programming. We don't expect
5624 these errors, but they might arise as the result of problems in
5625 user-defined plugins or scripts.
5626 """
5627 tag = 'g.checkUnicode'
5628 if s is None and g.unitTesting:
5629 return ''
5630 if isinstance(s, str):
5631 return s
5632 if not isinstance(s, bytes):
5633 g.error(f"{tag}: unexpected argument: {s!r}")
5634 return ''
5635 #
5636 # Report the unexpected conversion.
5637 callers = g.callers(1)
5638 if callers not in checkUnicode_dict:
5639 g.trace(g.callers())
5640 g.error(f"\n{tag}: expected unicode. got: {s!r}\n")
5641 checkUnicode_dict[callers] = True
5642 #
5643 # Convert to unicode, reporting all errors.
5644 if not encoding:
5645 encoding = 'utf-8'
5646 try:
5647 s = s.decode(encoding, 'strict')
5648 except(UnicodeDecodeError, UnicodeError):
5649 # https://wiki.python.org/moin/UnicodeDecodeError
5650 s = s.decode(encoding, 'replace')
5651 g.trace(g.callers())
5652 g.error(f"{tag}: unicode error. encoding: {encoding!r}, s:\n{s!r}")
5653 except Exception:
5654 g.trace(g.callers())
5655 g.es_excption()
5656 g.error(f"{tag}: unexpected error! encoding: {encoding!r}, s:\n{s!r}")
5657 return s
5658#@+node:ekr.20100125073206.8709: *4* g.getPythonEncodingFromString
5659def getPythonEncodingFromString(s: str) -> str:
5660 """Return the encoding given by Python's encoding line.
5661 s is the entire file.
5662 """
5663 encoding = None
5664 tag, tag2 = '# -*- coding:', '-*-'
5665 n1, n2 = len(tag), len(tag2)
5666 if s:
5667 # For Python 3.x we must convert to unicode before calling startswith.
5668 # The encoding doesn't matter: we only look at the first line, and if
5669 # the first line is an encoding line, it will contain only ascii characters.
5670 s = g.toUnicode(s, encoding='ascii', reportErrors=False)
5671 lines = g.splitLines(s)
5672 line1 = lines[0].strip()
5673 if line1.startswith(tag) and line1.endswith(tag2):
5674 e = line1[n1 : -n2].strip()
5675 if e and g.isValidEncoding(e):
5676 encoding = e
5677 elif g.match_word(line1, 0, '@first'): # 2011/10/21.
5678 line1 = line1[len('@first') :].strip()
5679 if line1.startswith(tag) and line1.endswith(tag2):
5680 e = line1[n1 : -n2].strip()
5681 if e and g.isValidEncoding(e):
5682 encoding = e
5683 return encoding
5684#@+node:ekr.20031218072017.1500: *4* g.isValidEncoding
5685def isValidEncoding(encoding: str) -> bool:
5686 """Return True if the encooding is valid."""
5687 if not encoding:
5688 return False
5689 if sys.platform == 'cli':
5690 return True
5691 try:
5692 codecs.lookup(encoding)
5693 return True
5694 except LookupError: # Windows
5695 return False
5696 except AttributeError: # Linux
5697 return False
5698 except Exception:
5699 # UnicodeEncodeError
5700 g.es_print('Please report the following error')
5701 g.es_exception()
5702 return False
5703#@+node:ekr.20061006152327: *4* g.isWordChar & g.isWordChar1
5704def isWordChar(ch: str) -> bool:
5705 """Return True if ch should be considered a letter."""
5706 return bool(ch and (ch.isalnum() or ch == '_'))
5708def isWordChar1(ch: str) -> bool:
5709 return bool(ch and (ch.isalpha() or ch == '_'))
5710#@+node:ekr.20130910044521.11304: *4* g.stripBOM
5711def stripBOM(s: str) -> Tuple[Optional[str], str]:
5712 """
5713 If there is a BOM, return (e,s2) where e is the encoding
5714 implied by the BOM and s2 is the s stripped of the BOM.
5716 If there is no BOM, return (None,s)
5718 s must be the contents of a file (a string) read in binary mode.
5719 """
5720 table = (
5721 # Important: test longer bom's first.
5722 (4, 'utf-32', codecs.BOM_UTF32_BE),
5723 (4, 'utf-32', codecs.BOM_UTF32_LE),
5724 (3, 'utf-8', codecs.BOM_UTF8),
5725 (2, 'utf-16', codecs.BOM_UTF16_BE),
5726 (2, 'utf-16', codecs.BOM_UTF16_LE),
5727 )
5728 if s:
5729 for n, e, bom in table:
5730 assert len(bom) == n
5731 if bom == s[: len(bom)]:
5732 return e, s[len(bom) :]
5733 return None, s
5734#@+node:ekr.20050208093800: *4* g.toEncodedString
5735def toEncodedString(s: str, encoding: str='utf-8', reportErrors: bool=False) -> bytes:
5736 """Convert unicode string to an encoded string."""
5737 if not isinstance(s, str):
5738 return s
5739 if not encoding:
5740 encoding = 'utf-8'
5741 # These are the only significant calls to s.encode in Leo.
5742 try:
5743 s = s.encode(encoding, "strict") # type:ignore
5744 except UnicodeError:
5745 s = s.encode(encoding, "replace") # type:ignore
5746 if reportErrors:
5747 g.error(f"Error converting {s} from unicode to {encoding} encoding")
5748 # Tracing these calls directly yields thousands of calls.
5749 return s # type:ignore
5750#@+node:ekr.20050208093800.1: *4* g.toUnicode
5751unicode_warnings: Dict[str, bool] = {} # Keys are g.callers.
5753def toUnicode(s: Any, encoding: str=None, reportErrors: bool=False) -> str:
5754 """Convert bytes to unicode if necessary."""
5755 if isinstance(s, str):
5756 return s
5757 tag = 'g.toUnicode'
5758 if not isinstance(s, bytes):
5759 if not isinstance(s, (NullObject, TracingNullObject)):
5760 callers = g.callers()
5761 if callers not in unicode_warnings:
5762 unicode_warnings[callers] = True
5763 g.error(f"{tag}: unexpected argument of type {s.__class__.__name__}")
5764 g.trace(callers)
5765 return ''
5766 if not encoding:
5767 encoding = 'utf-8'
5768 try:
5769 s = s.decode(encoding, 'strict')
5770 except(UnicodeDecodeError, UnicodeError):
5771 # https://wiki.python.org/moin/UnicodeDecodeError
5772 s = s.decode(encoding, 'replace')
5773 if reportErrors:
5774 g.error(f"{tag}: unicode error. encoding: {encoding!r}, s:\n{s!r}")
5775 g.trace(g.callers())
5776 except Exception:
5777 g.es_exception()
5778 g.error(f"{tag}: unexpected error! encoding: {encoding!r}, s:\n{s!r}")
5779 g.trace(g.callers())
5780 return s
5781#@+node:ekr.20031218072017.3197: *3* g.Whitespace
5782#@+node:ekr.20031218072017.3198: *4* g.computeLeadingWhitespace
5783# Returns optimized whitespace corresponding to width with the indicated tab_width.
5785def computeLeadingWhitespace(width: int, tab_width: int) -> str:
5786 if width <= 0:
5787 return ""
5788 if tab_width > 1:
5789 tabs = int(width / tab_width)
5790 blanks = int(width % tab_width)
5791 return ('\t' * tabs) + (' ' * blanks)
5792 # Negative tab width always gets converted to blanks.
5793 return ' ' * width
5794#@+node:ekr.20120605172139.10263: *4* g.computeLeadingWhitespaceWidth
5795# Returns optimized whitespace corresponding to width with the indicated tab_width.
5797def computeLeadingWhitespaceWidth(s: str, tab_width: int) -> int:
5798 w = 0
5799 for ch in s:
5800 if ch == ' ':
5801 w += 1
5802 elif ch == '\t':
5803 w += (abs(tab_width) - (w % abs(tab_width)))
5804 else:
5805 break
5806 return w
5807#@+node:ekr.20031218072017.3199: *4* g.computeWidth
5808# Returns the width of s, assuming s starts a line, with indicated tab_width.
5810def computeWidth(s: str, tab_width: int) -> int:
5811 w = 0
5812 for ch in s:
5813 if ch == '\t':
5814 w += (abs(tab_width) - (w % abs(tab_width)))
5815 elif ch == '\n': # Bug fix: 2012/06/05.
5816 break
5817 else:
5818 w += 1
5819 return w
5820#@+node:ekr.20110727091744.15083: *4* g.wrap_lines (newer)
5821#@@language rest
5822#@+at
5823# Important note: this routine need not deal with leading whitespace.
5824#
5825# Instead, the caller should simply reduce pageWidth by the width of
5826# leading whitespace wanted, then add that whitespace to the lines
5827# returned here.
5828#
5829# The key to this code is the invarient that line never ends in whitespace.
5830#@@c
5831#@@language python
5833def wrap_lines(lines: List[str], pageWidth: int, firstLineWidth: int=None) -> List[str]:
5834 """Returns a list of lines, consisting of the input lines wrapped to the given pageWidth."""
5835 if pageWidth < 10:
5836 pageWidth = 10
5837 # First line is special
5838 if not firstLineWidth:
5839 firstLineWidth = pageWidth
5840 if firstLineWidth < 10:
5841 firstLineWidth = 10
5842 outputLineWidth = firstLineWidth
5843 # Sentence spacing
5844 # This should be determined by some setting, and can only be either 1 or 2
5845 sentenceSpacingWidth = 1
5846 assert 0 < sentenceSpacingWidth < 3
5847 result = [] # The lines of the result.
5848 line = "" # The line being formed. It never ends in whitespace.
5849 for s in lines:
5850 i = 0
5851 while i < len(s):
5852 assert len(line) <= outputLineWidth # DTHEIN 18-JAN-2004
5853 j = g.skip_ws(s, i)
5854 k = g.skip_non_ws(s, j)
5855 word = s[j:k]
5856 assert k > i
5857 i = k
5858 # DTHEIN 18-JAN-2004: wrap at exactly the text width,
5859 # not one character less
5860 #
5861 wordLen = len(word)
5862 if line.endswith('.') or line.endswith('?') or line.endswith('!'):
5863 space = ' ' * sentenceSpacingWidth
5864 else:
5865 space = ' '
5866 if line and wordLen > 0:
5867 wordLen += len(space)
5868 if wordLen + len(line) <= outputLineWidth:
5869 if wordLen > 0:
5870 #@+<< place blank and word on the present line >>
5871 #@+node:ekr.20110727091744.15084: *5* << place blank and word on the present line >>
5872 if line:
5873 # Add the word, preceeded by a blank.
5874 line = space.join((line, word))
5875 else:
5876 # Just add the word to the start of the line.
5877 line = word
5878 #@-<< place blank and word on the present line >>
5879 else: pass # discard the trailing whitespace.
5880 else:
5881 #@+<< place word on a new line >>
5882 #@+node:ekr.20110727091744.15085: *5* << place word on a new line >>
5883 # End the previous line.
5884 if line:
5885 result.append(line)
5886 outputLineWidth = pageWidth # DTHEIN 3-NOV-2002: width for remaining lines
5887 # Discard the whitespace and put the word on a new line.
5888 line = word
5889 # Careful: the word may be longer than pageWidth.
5890 if len(line) > pageWidth: # DTHEIN 18-JAN-2004: line can equal pagewidth
5891 result.append(line)
5892 outputLineWidth = pageWidth # DTHEIN 3-NOV-2002: width for remaining lines
5893 line = ""
5894 #@-<< place word on a new line >>
5895 if line:
5896 result.append(line)
5897 return result
5898#@+node:ekr.20031218072017.3200: *4* g.get_leading_ws
5899def get_leading_ws(s: str) -> str:
5900 """Returns the leading whitespace of 's'."""
5901 i = 0
5902 n = len(s)
5903 while i < n and s[i] in (' ', '\t'):
5904 i += 1
5905 return s[0:i]
5906#@+node:ekr.20031218072017.3201: *4* g.optimizeLeadingWhitespace
5907# Optimize leading whitespace in s with the given tab_width.
5909def optimizeLeadingWhitespace(line: str, tab_width: int) -> str:
5910 i, width = g.skip_leading_ws_with_indent(line, 0, tab_width)
5911 s = g.computeLeadingWhitespace(width, tab_width) + line[i:]
5912 return s
5913#@+node:ekr.20040723093558: *4* g.regularizeTrailingNewlines
5914#@+at The caller should call g.stripBlankLines before calling this routine
5915# if desired.
5916#
5917# This routine does _not_ simply call rstrip(): that would delete all
5918# trailing whitespace-only lines, and in some cases that would change
5919# the meaning of program or data.
5920#@@c
5922def regularizeTrailingNewlines(s: str, kind: str) -> None:
5923 """Kind is 'asis', 'zero' or 'one'."""
5924 pass
5925#@+node:ekr.20091229090857.11698: *4* g.removeBlankLines
5926def removeBlankLines(s: str) -> str:
5927 lines = g.splitLines(s)
5928 lines = [z for z in lines if z.strip()]
5929 return ''.join(lines)
5930#@+node:ekr.20091229075924.6235: *4* g.removeLeadingBlankLines
5931def removeLeadingBlankLines(s: str) -> str:
5932 lines = g.splitLines(s)
5933 result = []
5934 remove = True
5935 for line in lines:
5936 if remove and not line.strip():
5937 pass
5938 else:
5939 remove = False
5940 result.append(line)
5941 return ''.join(result)
5942#@+node:ekr.20031218072017.3202: *4* g.removeLeadingWhitespace
5943# Remove whitespace up to first_ws wide in s, given tab_width, the width of a tab.
5945def removeLeadingWhitespace(s: str, first_ws: int, tab_width: int) -> str:
5946 j = 0
5947 ws = 0
5948 first_ws = abs(first_ws)
5949 for ch in s:
5950 if ws >= first_ws:
5951 break
5952 elif ch == ' ':
5953 j += 1
5954 ws += 1
5955 elif ch == '\t':
5956 j += 1
5957 ws += (abs(tab_width) - (ws % abs(tab_width)))
5958 else:
5959 break
5960 if j > 0:
5961 s = s[j:]
5962 return s
5963#@+node:ekr.20031218072017.3203: *4* g.removeTrailingWs
5964# Warning: string.rstrip also removes newlines!
5966def removeTrailingWs(s: str) -> str:
5967 j = len(s) - 1
5968 while j >= 0 and (s[j] == ' ' or s[j] == '\t'):
5969 j -= 1
5970 return s[: j + 1]
5971#@+node:ekr.20031218072017.3204: *4* g.skip_leading_ws
5972# Skips leading up to width leading whitespace.
5974def skip_leading_ws(s: str, i: int, ws: int, tab_width: int) -> int:
5975 count = 0
5976 while count < ws and i < len(s):
5977 ch = s[i]
5978 if ch == ' ':
5979 count += 1
5980 i += 1
5981 elif ch == '\t':
5982 count += (abs(tab_width) - (count % abs(tab_width)))
5983 i += 1
5984 else: break
5985 return i
5986#@+node:ekr.20031218072017.3205: *4* g.skip_leading_ws_with_indent
5987def skip_leading_ws_with_indent(s: str, i: int, tab_width: int) -> Tuple[int, int]:
5988 """Skips leading whitespace and returns (i, indent),
5990 - i points after the whitespace
5991 - indent is the width of the whitespace, assuming tab_width wide tabs."""
5992 count = 0
5993 n = len(s)
5994 while i < n:
5995 ch = s[i]
5996 if ch == ' ':
5997 count += 1
5998 i += 1
5999 elif ch == '\t':
6000 count += (abs(tab_width) - (count % abs(tab_width)))
6001 i += 1
6002 else: break
6003 return i, count
6004#@+node:ekr.20040723093558.1: *4* g.stripBlankLines
6005def stripBlankLines(s: str) -> str:
6006 lines = g.splitLines(s)
6007 for i, line in enumerate(lines):
6008 j = g.skip_ws(line, 0)
6009 if j >= len(line):
6010 lines[i] = ''
6011 elif line[j] == '\n':
6012 lines[i] = '\n'
6013 return ''.join(lines)
6014#@+node:ekr.20031218072017.3108: ** g.Logging & Printing
6015# g.es and related print to the Log window.
6016# g.pr prints to the console.
6017# g.es_print and related print to both the Log window and the console.
6018#@+node:ekr.20080821073134.2: *3* g.doKeywordArgs
6019def doKeywordArgs(keys: Dict, d: Dict=None) -> Dict:
6020 """
6021 Return a result dict that is a copy of the keys dict
6022 with missing items replaced by defaults in d dict.
6023 """
6024 if d is None:
6025 d = {}
6026 result = {}
6027 for key, default_val in d.items():
6028 isBool = default_val in (True, False)
6029 val = keys.get(key)
6030 if isBool and val in (True, 'True', 'true'):
6031 result[key] = True
6032 elif isBool and val in (False, 'False', 'false'):
6033 result[key] = False
6034 elif val is None:
6035 result[key] = default_val
6036 else:
6037 result[key] = val
6038 return result
6039#@+node:ekr.20031218072017.1474: *3* g.enl, ecnl & ecnls
6040def ecnl(tabName: str='Log') -> None:
6041 g.ecnls(1, tabName)
6043def ecnls(n: int, tabName: str='Log') -> None:
6044 log = app.log
6045 if log and not log.isNull:
6046 while log.newlines < n:
6047 g.enl(tabName)
6049def enl(tabName: str='Log') -> None:
6050 log = app.log
6051 if log and not log.isNull:
6052 log.newlines += 1
6053 log.putnl(tabName)
6054#@+node:ekr.20100914094836.5892: *3* g.error, g.note, g.warning, g.red, g.blue
6055def blue(*args: Any, **keys: Any) -> None:
6056 g.es_print(color='blue', *args, **keys)
6058def error(*args: Any, **keys: Any) -> None:
6059 g.es_print(color='error', *args, **keys)
6061def note(*args: Any, **keys: Any) -> None:
6062 g.es_print(color='note', *args, **keys)
6064def red(*args: Any, **keys: Any) -> None:
6065 g.es_print(color='red', *args, **keys)
6067def warning(*args: Any, **keys: Any) -> None:
6068 g.es_print(color='warning', *args, **keys)
6069#@+node:ekr.20070626132332: *3* g.es
6070def es(*args: Any, **keys: Any) -> None:
6071 """Put all non-keyword args to the log pane.
6072 The first, third, fifth, etc. arg translated by g.translateString.
6073 Supports color, comma, newline, spaces and tabName keyword arguments.
6074 """
6075 if not app or app.killed:
6076 return
6077 if app.gui and app.gui.consoleOnly:
6078 return
6079 log = app.log
6080 # Compute the effective args.
6081 d = {
6082 'color': None,
6083 'commas': False,
6084 'newline': True,
6085 'spaces': True,
6086 'tabName': 'Log',
6087 'nodeLink': None,
6088 }
6089 d = g.doKeywordArgs(keys, d)
6090 color = d.get('color')
6091 if color == 'suppress':
6092 return # New in 4.3.
6093 color = g.actualColor(color)
6094 tabName = d.get('tabName') or 'Log'
6095 newline = d.get('newline')
6096 s = g.translateArgs(args, d)
6097 # Do not call g.es, g.es_print, g.pr or g.trace here!
6098 # sys.__stdout__.write('\n===== g.es: %r\n' % s)
6099 if app.batchMode:
6100 if app.log:
6101 app.log.put(s)
6102 elif g.unitTesting:
6103 if log and not log.isNull:
6104 # This makes the output of unit tests match the output of scripts.
6105 g.pr(s, newline=newline)
6106 elif log and app.logInited:
6107 if newline:
6108 s += '\n'
6109 log.put(s, color=color, tabName=tabName, nodeLink=d['nodeLink'])
6110 # Count the number of *trailing* newlines.
6111 for ch in s:
6112 if ch == '\n':
6113 log.newlines += 1
6114 else:
6115 log.newlines = 0
6116 else:
6117 app.logWaiting.append((s, color, newline, d),)
6119log = es
6120#@+node:ekr.20060917120951: *3* g.es_dump
6121def es_dump(s: str, n: int=30, title: str=None) -> None:
6122 if title:
6123 g.es_print('', title)
6124 i = 0
6125 while i < len(s):
6126 aList = ''.join([f"{ord(ch):2x} " for ch in s[i : i + n]])
6127 g.es_print('', aList)
6128 i += n
6129#@+node:ekr.20031218072017.3110: *3* g.es_error & es_print_error
6130def es_error(*args: Any, **keys: Any) -> None:
6131 color = keys.get('color')
6132 if color is None and g.app.config:
6133 keys['color'] = g.app.config.getColor("log-error-color") or 'red'
6134 g.es(*args, **keys)
6136def es_print_error(*args: Any, **keys: Any) -> None:
6137 color = keys.get('color')
6138 if color is None and g.app.config:
6139 keys['color'] = g.app.config.getColor("log-error-color") or 'red'
6140 g.es_print(*args, **keys)
6141#@+node:ekr.20031218072017.3111: *3* g.es_event_exception
6142def es_event_exception(eventName: str, full: bool=False) -> None:
6143 g.es("exception handling ", eventName, "event")
6144 typ, val, tb = sys.exc_info()
6145 if full:
6146 errList = traceback.format_exception(typ, val, tb)
6147 else:
6148 errList = traceback.format_exception_only(typ, val)
6149 for i in errList:
6150 g.es('', i)
6151 if not g.stdErrIsRedirected(): # 2/16/04
6152 traceback.print_exc()
6153#@+node:ekr.20031218072017.3112: *3* g.es_exception
6154def es_exception(full: bool=True, c: Cmdr=None, color: str="red") -> Tuple[str, int]:
6155 typ, val, tb = sys.exc_info()
6156 # val is the second argument to the raise statement.
6157 if full:
6158 lines = traceback.format_exception(typ, val, tb)
6159 else:
6160 lines = traceback.format_exception_only(typ, val)
6161 for line in lines:
6162 g.es_print_error(line, color=color)
6163 fileName, n = g.getLastTracebackFileAndLineNumber()
6164 return fileName, n
6165#@+node:ekr.20061015090538: *3* g.es_exception_type
6166def es_exception_type(c: Cmdr=None, color: str="red") -> None:
6167 # exctype is a Exception class object; value is the error message.
6168 exctype, value = sys.exc_info()[:2]
6169 g.es_print('', f"{exctype.__name__}, {value}", color=color) # type:ignore
6170#@+node:ekr.20050707064040: *3* g.es_print
6171# see: http://www.diveintopython.org/xml_processing/unicode.html
6173def es_print(*args: Any, **keys: Any) -> None:
6174 """
6175 Print all non-keyword args, and put them to the log pane.
6177 The first, third, fifth, etc. arg translated by g.translateString.
6178 Supports color, comma, newline, spaces and tabName keyword arguments.
6179 """
6180 g.pr(*args, **keys)
6181 if g.app and not g.unitTesting:
6182 g.es(*args, **keys)
6183#@+node:ekr.20111107181638.9741: *3* g.print_exception
6184def print_exception(full: bool=True, c: Cmdr=None, flush: bool=False, color: str="red") -> Tuple[str, int]:
6185 """Print exception info about the last exception."""
6186 typ, val, tb = sys.exc_info()
6187 # val is the second argument to the raise statement.
6188 if full:
6189 lines = traceback.format_exception(typ, val, tb)
6190 else:
6191 lines = traceback.format_exception_only(typ, val)
6192 print(''.join(lines), flush=flush)
6193 try:
6194 fileName, n = g.getLastTracebackFileAndLineNumber()
6195 return fileName, n
6196 except Exception:
6197 return "<no file>", 0
6198#@+node:ekr.20050707065530: *3* g.es_trace
6199def es_trace(*args: Any, **keys: Any) -> None:
6200 if args:
6201 try:
6202 s = args[0]
6203 g.trace(g.toEncodedString(s, 'ascii'))
6204 except Exception:
6205 pass
6206 g.es(*args, **keys)
6207#@+node:ekr.20040731204831: *3* g.getLastTracebackFileAndLineNumber
6208def getLastTracebackFileAndLineNumber() -> Tuple[str, int]:
6209 typ, val, tb = sys.exc_info()
6210 if typ == SyntaxError:
6211 # IndentationError is a subclass of SyntaxError.
6212 return val.filename, val.lineno
6213 #
6214 # Data is a list of tuples, one per stack entry.
6215 # Tupls have the form (filename,lineNumber,functionName,text).
6216 data = traceback.extract_tb(tb)
6217 if data:
6218 item = data[-1] # Get the item at the top of the stack.
6219 filename, n, functionName, text = item
6220 return filename, n
6221 # Should never happen.
6222 return '<string>', 0
6223#@+node:ekr.20150621095017.1: *3* g.goto_last_exception
6224def goto_last_exception(c: Cmdr) -> None:
6225 """Go to the line given by sys.last_traceback."""
6226 typ, val, tb = sys.exc_info()
6227 if tb:
6228 file_name, line_number = g.getLastTracebackFileAndLineNumber()
6229 line_number = max(0, line_number - 1)
6230 # Convert to zero-based.
6231 if file_name.endswith('scriptFile.py'):
6232 # A script.
6233 c.goToScriptLineNumber(line_number, c.p)
6234 else:
6235 for p in c.all_nodes():
6236 if p.isAnyAtFileNode() and p.h.endswith(file_name):
6237 c.goToLineNumber(line_number) # 2021/07/28: fixed by mypy.
6238 return
6239 else:
6240 g.trace('No previous exception')
6241#@+node:ekr.20100126062623.6240: *3* g.internalError
6242def internalError(*args: Any) -> None:
6243 """Report a serious interal error in Leo."""
6244 callers = g.callers(20).split(',')
6245 caller = callers[-1]
6246 g.error('\nInternal Leo error in', caller)
6247 g.es_print(*args)
6248 g.es_print('Called from', ', '.join(callers[:-1]))
6249 g.es_print('Please report this error to Leo\'s developers', color='red')
6250#@+node:ekr.20150127060254.5: *3* g.log_to_file
6251def log_to_file(s: str, fn: str=None) -> None:
6252 """Write a message to ~/test/leo_log.txt."""
6253 if fn is None:
6254 fn = g.os_path_expanduser('~/test/leo_log.txt')
6255 if not s.endswith('\n'):
6256 s = s + '\n'
6257 try:
6258 with open(fn, 'a') as f:
6259 f.write(s)
6260 except Exception:
6261 g.es_exception()
6262#@+node:ekr.20080710101653.1: *3* g.pr
6263# see: http://www.diveintopython.org/xml_processing/unicode.html
6265def pr(*args: Any, **keys: Any) -> None:
6266 """
6267 Print all non-keyword args. This is a wrapper for the print statement.
6269 The first, third, fifth, etc. arg translated by g.translateString.
6270 Supports color, comma, newline, spaces and tabName keyword arguments.
6271 """
6272 # Compute the effective args.
6273 d = {'commas': False, 'newline': True, 'spaces': True}
6274 d = doKeywordArgs(keys, d)
6275 newline = d.get('newline')
6276 stdout = sys.stdout if sys.stdout and g.unitTesting else sys.__stdout__
6277 # Unit tests require sys.stdout.
6278 if not stdout:
6279 # #541.
6280 return
6281 if sys.platform.lower().startswith('win'):
6282 encoding = 'ascii' # 2011/11/9.
6283 elif getattr(stdout, 'encoding', None):
6284 # sys.stdout is a TextIOWrapper with a particular encoding.
6285 encoding = stdout.encoding
6286 else:
6287 encoding = 'utf-8'
6288 s = translateArgs(args, d)
6289 # Translates everything to unicode.
6290 s = g.toUnicode(s, encoding=encoding, reportErrors=False)
6291 if newline:
6292 s += '\n'
6293 #
6294 # Python's print statement *can* handle unicode, but
6295 # sitecustomize.py must have sys.setdefaultencoding('utf-8')
6296 try:
6297 # #783: print-* commands fail under pythonw.
6298 stdout.write(s)
6299 except Exception:
6300 pass
6301#@+node:ekr.20060221083356: *3* g.prettyPrintType
6302def prettyPrintType(obj: Any) -> str:
6303 if isinstance(obj, str): # type:ignore
6304 return 'string'
6305 t: Any = type(obj)
6306 if t in (types.BuiltinFunctionType, types.FunctionType):
6307 return 'function'
6308 if t == types.ModuleType:
6309 return 'module'
6310 if t in [types.MethodType, types.BuiltinMethodType]:
6311 return 'method'
6312 # Fall back to a hack.
6313 t = str(type(obj)) # type:ignore
6314 if t.startswith("<type '"):
6315 t = t[7:]
6316 if t.endswith("'>"):
6317 t = t[:-2]
6318 return t
6319#@+node:ekr.20031218072017.3113: *3* g.printBindings
6320def print_bindings(name: str, window: Any) -> None:
6321 bindings = window.bind()
6322 g.pr("\nBindings for", name)
6323 for b in bindings:
6324 g.pr(b)
6325#@+node:ekr.20070510074941: *3* g.printEntireTree
6326def printEntireTree(c: Cmdr, tag: str='') -> None:
6327 g.pr('printEntireTree', '=' * 50)
6328 g.pr('printEntireTree', tag, 'root', c.rootPosition())
6329 for p in c.all_positions():
6330 g.pr('..' * p.level(), p.v)
6331#@+node:ekr.20031218072017.3114: *3* g.printGlobals
6332def printGlobals(message: str=None) -> None:
6333 # Get the list of globals.
6334 globs = list(globals())
6335 globs.sort()
6336 # Print the list.
6337 if message:
6338 leader = "-" * 10
6339 g.pr(leader, ' ', message, ' ', leader)
6340 for name in globs:
6341 g.pr(name)
6342#@+node:ekr.20031218072017.3115: *3* g.printLeoModules
6343def printLeoModules(message: str=None) -> None:
6344 # Create the list.
6345 mods = []
6346 for name in sys.modules:
6347 if name and name[0:3] == "leo":
6348 mods.append(name)
6349 # Print the list.
6350 if message:
6351 leader = "-" * 10
6352 g.pr(leader, ' ', message, ' ', leader)
6353 mods.sort()
6354 for m in mods:
6355 g.pr(m, newline=False)
6356 g.pr('')
6357#@+node:ekr.20041122153823: *3* g.printStack
6358def printStack() -> None:
6359 traceback.print_stack()
6360#@+node:ekr.20031218072017.2317: *3* g.trace
6361def trace(*args: Any, **keys: Any) -> None:
6362 """Print a tracing message."""
6363 # Don't use g here: in standalone mode g is a NullObject!
6364 # Compute the effective args.
6365 d: Dict[str, Any] = {'align': 0, 'before': '', 'newline': True, 'caller_level': 1, 'noname': False}
6366 d = doKeywordArgs(keys, d)
6367 newline = d.get('newline')
6368 align = d.get('align', 0)
6369 caller_level = d.get('caller_level', 1)
6370 noname = d.get('noname')
6371 # Compute the caller name.
6372 if noname:
6373 name = ''
6374 else:
6375 try: # get the function name from the call stack.
6376 f1 = sys._getframe(caller_level) # The stack frame, one level up.
6377 code1 = f1.f_code # The code object
6378 name = code1.co_name # The code name
6379 except Exception:
6380 name = g.shortFileName(__file__)
6381 if name == '<module>':
6382 name = g.shortFileName(__file__)
6383 if name.endswith('.pyc'):
6384 name = name[:-1]
6385 # Pad the caller name.
6386 if align != 0 and len(name) < abs(align):
6387 pad = ' ' * (abs(align) - len(name))
6388 if align > 0:
6389 name = name + pad
6390 else:
6391 name = pad + name
6392 # Munge *args into s.
6393 result = [name] if name else []
6394 #
6395 # Put leading newlines into the prefix.
6396 if isinstance(args, tuple):
6397 args = list(args) # type:ignore
6398 if args and isinstance(args[0], str):
6399 prefix = ''
6400 while args[0].startswith('\n'):
6401 prefix += '\n'
6402 args[0] = args[0][1:] # type:ignore
6403 else:
6404 prefix = ''
6405 for arg in args:
6406 if isinstance(arg, str):
6407 pass
6408 elif isinstance(arg, bytes):
6409 arg = toUnicode(arg)
6410 else:
6411 arg = repr(arg)
6412 if result:
6413 result.append(" " + arg)
6414 else:
6415 result.append(arg)
6416 s = d.get('before') + ''.join(result)
6417 if prefix:
6418 prefix = prefix[1:] # One less newline.
6419 pr(prefix)
6420 pr(s, newline=newline)
6421#@+node:ekr.20080220111323: *3* g.translateArgs
6422console_encoding = None
6424def translateArgs(args: Iterable[Any], d: Dict[str, Any]) -> str:
6425 """
6426 Return the concatenation of s and all args, with odd args translated.
6427 """
6428 global console_encoding
6429 if not console_encoding:
6430 e = sys.getdefaultencoding()
6431 console_encoding = e if isValidEncoding(e) else 'utf-8'
6432 # print 'translateArgs',console_encoding
6433 result: List[str] = []
6434 n, spaces = 0, d.get('spaces')
6435 for arg in args:
6436 n += 1
6437 # First, convert to unicode.
6438 if isinstance(arg, str):
6439 arg = toUnicode(arg, console_encoding)
6440 # Now translate.
6441 if not isinstance(arg, str):
6442 arg = repr(arg)
6443 elif (n % 2) == 1:
6444 arg = translateString(arg)
6445 else:
6446 pass # The arg is an untranslated string.
6447 if arg:
6448 if result and spaces:
6449 result.append(' ')
6450 result.append(arg)
6451 return ''.join(result)
6452#@+node:ekr.20060810095921: *3* g.translateString & tr
6453def translateString(s: str) -> str:
6454 """Return the translated text of s."""
6455 # pylint: disable=undefined-loop-variable
6456 # looks like a pylint bug
6457 upper = app and getattr(app, 'translateToUpperCase', None)
6458 if not isinstance(s, str):
6459 s = str(s, 'utf-8')
6460 if upper:
6461 s = s.upper()
6462 else:
6463 s = gettext.gettext(s)
6464 return s
6466tr = translateString
6467#@+node:EKR.20040612114220: ** g.Miscellaneous
6468#@+node:ekr.20120928142052.10116: *3* g.actualColor
6469def actualColor(color: str) -> str:
6470 """Return the actual color corresponding to the requested color."""
6471 c = g.app.log and g.app.log.c
6472 # Careful: c.config may not yet exist.
6473 if not c or not c.config:
6474 return color
6475 # Don't change absolute colors.
6476 if color and color.startswith('#'):
6477 return color
6478 # #788: Translate colors to theme-defined colors.
6479 if color is None:
6480 # Prefer text_foreground_color'
6481 color2 = c.config.getColor('log-text-foreground-color')
6482 if color2:
6483 return color2
6484 # Fall back to log_black_color.
6485 color2 = c.config.getColor('log-black-color')
6486 return color2 or 'black'
6487 if color == 'black':
6488 # Prefer log_black_color.
6489 color2 = c.config.getColor('log-black-color')
6490 if color2:
6491 return color2
6492 # Fall back to log_text_foreground_color.
6493 color2 = c.config.getColor('log-text-foreground-color')
6494 return color2 or 'black'
6495 color2 = c.config.getColor(f"log_{color}_color")
6496 return color2 or color
6497#@+node:ekr.20060921100435: *3* g.CheckVersion & helpers
6498# Simplified version by EKR: stringCompare not used.
6500def CheckVersion(
6501 s1: str,
6502 s2: str,
6503 condition: str=">=",
6504 stringCompare: bool=None,
6505 delimiter: str='.',
6506 trace: bool=False,
6507) -> bool:
6508 # CheckVersion is called early in the startup process.
6509 vals1 = [g.CheckVersionToInt(s) for s in s1.split(delimiter)]
6510 n1 = len(vals1)
6511 vals2 = [g.CheckVersionToInt(s) for s in s2.split(delimiter)]
6512 n2 = len(vals2)
6513 n = max(n1, n2)
6514 if n1 < n:
6515 vals1.extend([0 for i in range(n - n1)])
6516 if n2 < n:
6517 vals2.extend([0 for i in range(n - n2)])
6518 for cond, val in (
6519 ('==', vals1 == vals2), ('!=', vals1 != vals2),
6520 ('<', vals1 < vals2), ('<=', vals1 <= vals2),
6521 ('>', vals1 > vals2), ('>=', vals1 >= vals2),
6522 ):
6523 if condition == cond:
6524 result = val
6525 break
6526 else:
6527 raise EnvironmentError(
6528 "condition must be one of '>=', '>', '==', '!=', '<', or '<='.")
6529 return result
6530#@+node:ekr.20070120123930: *4* g.CheckVersionToInt
6531def CheckVersionToInt(s: str) -> int:
6532 try:
6533 return int(s)
6534 except ValueError:
6535 aList = []
6536 for ch in s:
6537 if ch.isdigit():
6538 aList.append(ch)
6539 else:
6540 break
6541 if aList:
6542 s = ''.join(aList)
6543 return int(s)
6544 return 0
6545#@+node:ekr.20111103205308.9657: *3* g.cls
6546@command('cls')
6547def cls(event: Any=None) -> None:
6548 """Clear the screen."""
6549 if sys.platform.lower().startswith('win'):
6550 os.system('cls')
6551#@+node:ekr.20131114124839.16665: *3* g.createScratchCommander
6552def createScratchCommander(fileName: str=None) -> None:
6553 c = g.app.newCommander(fileName)
6554 frame = c.frame
6555 frame.createFirstTreeNode()
6556 assert c.rootPosition()
6557 frame.setInitialWindowGeometry()
6558 frame.resizePanesToRatio(frame.ratio, frame.secondary_ratio)
6559#@+node:ekr.20031218072017.3126: *3* g.funcToMethod (Python Cookbook)
6560def funcToMethod(f: Any, theClass: Any, name: str=None) -> None:
6561 """
6562 From the Python Cookbook...
6564 The following method allows you to add a function as a method of
6565 any class. That is, it converts the function to a method of the
6566 class. The method just added is available instantly to all
6567 existing instances of the class, and to all instances created in
6568 the future.
6570 The function's first argument should be self.
6572 The newly created method has the same name as the function unless
6573 the optional name argument is supplied, in which case that name is
6574 used as the method name.
6575 """
6576 setattr(theClass, name or f.__name__, f)
6577#@+node:ekr.20060913090832.1: *3* g.init_zodb
6578init_zodb_import_failed = False
6579init_zodb_failed: Dict[str, bool] = {} # Keys are paths, values are True.
6580init_zodb_db: Dict[str, Any] = {} # Keys are paths, values are ZODB.DB instances.
6582def init_zodb(pathToZodbStorage: str, verbose: bool=True) -> Any:
6583 """
6584 Return an ZODB.DB instance from the given path.
6585 return None on any error.
6586 """
6587 global init_zodb_db, init_zodb_failed, init_zodb_import_failed
6588 db = init_zodb_db.get(pathToZodbStorage)
6589 if db:
6590 return db
6591 if init_zodb_import_failed:
6592 return None
6593 failed = init_zodb_failed.get(pathToZodbStorage)
6594 if failed:
6595 return None
6596 try:
6597 import ZODB # type:ignore
6598 except ImportError:
6599 if verbose:
6600 g.es('g.init_zodb: can not import ZODB')
6601 g.es_exception()
6602 init_zodb_import_failed = True
6603 return None
6604 try:
6605 storage = ZODB.FileStorage.FileStorage(pathToZodbStorage)
6606 init_zodb_db[pathToZodbStorage] = db = ZODB.DB(storage)
6607 return db
6608 except Exception:
6609 if verbose:
6610 g.es('g.init_zodb: exception creating ZODB.DB instance')
6611 g.es_exception()
6612 init_zodb_failed[pathToZodbStorage] = True
6613 return None
6614#@+node:ekr.20170206080908.1: *3* g.input_
6615def input_(message: str='', c: Cmdr=None) -> str:
6616 """
6617 Safely execute python's input statement.
6619 c.executeScriptHelper binds 'input' to be a wrapper that calls g.input_
6620 with c and handler bound properly.
6621 """
6622 if app.gui.isNullGui:
6623 return ''
6624 # Prompt for input from the console, assuming there is one.
6625 # pylint: disable=no-member
6626 from leo.core.leoQt import QtCore
6627 QtCore.pyqtRemoveInputHook()
6628 return input(message)
6629#@+node:ekr.20110609125359.16493: *3* g.isMacOS
6630def isMacOS() -> bool:
6631 return sys.platform == 'darwin'
6632#@+node:ekr.20181027133311.1: *3* g.issueSecurityWarning
6633def issueSecurityWarning(setting: str) -> None:
6634 g.es('Security warning! Ignoring...', color='red')
6635 g.es(setting, color='red')
6636 g.es('This setting can be set only in')
6637 g.es('leoSettings.leo or myLeoSettings.leo')
6638#@+node:ekr.20031218072017.3144: *3* g.makeDict (Python Cookbook)
6639# From the Python cookbook.
6641def makeDict(**keys: Any) -> Dict:
6642 """Returns a Python dictionary from using the optional keyword arguments."""
6643 return keys
6644#@+node:ekr.20140528065727.17963: *3* g.pep8_class_name
6645def pep8_class_name(s: str) -> str:
6646 """Return the proper class name for s."""
6647 # Warning: s.capitalize() does not work.
6648 # It lower cases all but the first letter!
6649 return ''.join([z[0].upper() + z[1:] for z in s.split('_') if z])
6651if 0: # Testing:
6652 cls()
6653 aList = (
6654 '_',
6655 '__',
6656 '_abc',
6657 'abc_',
6658 'abc',
6659 'abc_xyz',
6660 'AbcPdQ',
6661 )
6662 for s in aList:
6663 print(pep8_class_name(s))
6664#@+node:ekr.20160417174224.1: *3* g.plural
6665def plural(obj: Any) -> str:
6666 """Return "s" or "" depending on n."""
6667 if isinstance(obj, (list, tuple, str)):
6668 n = len(obj)
6669 else:
6670 n = obj
6671 return '' if n == 1 else 's'
6672#@+node:ekr.20160331194701.1: *3* g.truncate
6673def truncate(s: str, n: int) -> str:
6674 """Return s truncated to n characters."""
6675 if len(s) <= n:
6676 return s
6677 # Fail: weird ws.
6678 s2 = s[: n - 3] + f"...({len(s)})"
6679 if s.endswith('\n'):
6680 return s2 + '\n'
6681 return s2
6682#@+node:ekr.20031218072017.3150: *3* g.windows
6683def windows() -> Optional[List]:
6684 return app and app.windowList
6685#@+node:ekr.20031218072017.2145: ** g.os_path_ Wrappers
6686#@+at Note: all these methods return Unicode strings. It is up to the user to
6687# convert to an encoded string as needed, say when opening a file.
6688#@+node:ekr.20180314120442.1: *3* g.glob_glob
6689def glob_glob(pattern: str) -> List:
6690 """Return the regularized glob.glob(pattern)"""
6691 aList = glob.glob(pattern)
6692 # os.path.normpath does the *reverse* of what we want.
6693 if g.isWindows:
6694 aList = [z.replace('\\', '/') for z in aList]
6695 return aList
6696#@+node:ekr.20031218072017.2146: *3* g.os_path_abspath
6697def os_path_abspath(path: str) -> str:
6698 """Convert a path to an absolute path."""
6699 if not path:
6700 return ''
6701 if '\x00' in path:
6702 g.trace('NULL in', repr(path), g.callers())
6703 path = path.replace('\x00', '') # Fix Python 3 bug on Windows 10.
6704 path = os.path.abspath(path)
6705 # os.path.normpath does the *reverse* of what we want.
6706 if g.isWindows:
6707 path = path.replace('\\', '/')
6708 return path
6709#@+node:ekr.20031218072017.2147: *3* g.os_path_basename
6710def os_path_basename(path: str) -> str:
6711 """Return the second half of the pair returned by split(path)."""
6712 if not path:
6713 return ''
6714 path = os.path.basename(path)
6715 # os.path.normpath does the *reverse* of what we want.
6716 if g.isWindows:
6717 path = path.replace('\\', '/')
6718 return path
6719#@+node:ekr.20031218072017.2148: *3* g.os_path_dirname
6720def os_path_dirname(path: str) -> str:
6721 """Return the first half of the pair returned by split(path)."""
6722 if not path:
6723 return ''
6724 path = os.path.dirname(path)
6725 # os.path.normpath does the *reverse* of what we want.
6726 if g.isWindows:
6727 path = path.replace('\\', '/')
6728 return path
6729#@+node:ekr.20031218072017.2149: *3* g.os_path_exists
6730def os_path_exists(path: str) -> bool:
6731 """Return True if path exists."""
6732 if not path:
6733 return False
6734 if '\x00' in path:
6735 g.trace('NULL in', repr(path), g.callers())
6736 path = path.replace('\x00', '') # Fix Python 3 bug on Windows 10.
6737 return os.path.exists(path)
6738#@+node:ekr.20080921060401.13: *3* g.os_path_expanduser
6739def os_path_expanduser(path: str) -> str:
6740 """wrap os.path.expanduser"""
6741 if not path:
6742 return ''
6743 result = os.path.normpath(os.path.expanduser(path))
6744 # os.path.normpath does the *reverse* of what we want.
6745 if g.isWindows:
6746 path = path.replace('\\', '/')
6747 return result
6748#@+node:ekr.20080921060401.14: *3* g.os_path_finalize
6749def os_path_finalize(path: str) -> str:
6750 """
6751 Expand '~', then return os.path.normpath, os.path.abspath of the path.
6752 There is no corresponding os.path method
6753 """
6754 if '\x00' in path:
6755 g.trace('NULL in', repr(path), g.callers())
6756 path = path.replace('\x00', '') # Fix Python 3 bug on Windows 10.
6757 path = os.path.expanduser(path) # #1383.
6758 path = os.path.abspath(path)
6759 path = os.path.normpath(path)
6760 # os.path.normpath does the *reverse* of what we want.
6761 if g.isWindows:
6762 path = path.replace('\\', '/')
6763 # calling os.path.realpath here would cause problems in some situations.
6764 return path
6765#@+node:ekr.20140917154740.19483: *3* g.os_path_finalize_join
6766def os_path_finalize_join(*args: Any, **keys: Any) -> str:
6767 """
6768 Join and finalize.
6770 **keys may contain a 'c' kwarg, used by g.os_path_join.
6771 """
6772 path = g.os_path_join(*args, **keys)
6773 path = g.os_path_finalize(path)
6774 return path
6775#@+node:ekr.20031218072017.2150: *3* g.os_path_getmtime
6776def os_path_getmtime(path: str) -> float:
6777 """Return the modification time of path."""
6778 if not path:
6779 return 0
6780 try:
6781 return os.path.getmtime(path)
6782 except Exception:
6783 return 0
6784#@+node:ekr.20080729142651.2: *3* g.os_path_getsize
6785def os_path_getsize(path: str) -> int:
6786 """Return the size of path."""
6787 return os.path.getsize(path) if path else 0
6788#@+node:ekr.20031218072017.2151: *3* g.os_path_isabs
6789def os_path_isabs(path: str) -> bool:
6790 """Return True if path is an absolute path."""
6791 return os.path.isabs(path) if path else False
6792#@+node:ekr.20031218072017.2152: *3* g.os_path_isdir
6793def os_path_isdir(path: str) -> bool:
6794 """Return True if the path is a directory."""
6795 return os.path.isdir(path) if path else False
6796#@+node:ekr.20031218072017.2153: *3* g.os_path_isfile
6797def os_path_isfile(path: str) -> bool:
6798 """Return True if path is a file."""
6799 return os.path.isfile(path) if path else False
6800#@+node:ekr.20031218072017.2154: *3* g.os_path_join
6801def os_path_join(*args: Any, **keys: Any) -> str:
6802 """
6803 Join paths, like os.path.join, with enhancements:
6805 A '!!' arg prepends g.app.loadDir to the list of paths.
6806 A '.' arg prepends c.openDirectory to the list of paths,
6807 provided there is a 'c' kwarg.
6808 """
6809 c = keys.get('c')
6810 uargs = [z for z in args if z]
6811 if not uargs:
6812 return ''
6813 # Note: This is exactly the same convention as used by getBaseDirectory.
6814 if uargs[0] == '!!':
6815 uargs[0] = g.app.loadDir
6816 elif uargs[0] == '.':
6817 c = keys.get('c')
6818 if c and c.openDirectory:
6819 uargs[0] = c.openDirectory
6820 try:
6821 path = os.path.join(*uargs)
6822 except TypeError:
6823 g.trace(uargs, args, keys, g.callers())
6824 raise
6825 # May not be needed on some Pythons.
6826 if '\x00' in path:
6827 g.trace('NULL in', repr(path), g.callers())
6828 path = path.replace('\x00', '') # Fix Python 3 bug on Windows 10.
6829 # os.path.normpath does the *reverse* of what we want.
6830 if g.isWindows:
6831 path = path.replace('\\', '/')
6832 return path
6833#@+node:ekr.20031218072017.2156: *3* g.os_path_normcase
6834def os_path_normcase(path: str) -> str:
6835 """Normalize the path's case."""
6836 if not path:
6837 return ''
6838 path = os.path.normcase(path)
6839 if g.isWindows:
6840 path = path.replace('\\', '/')
6841 return path
6842#@+node:ekr.20031218072017.2157: *3* g.os_path_normpath
6843def os_path_normpath(path: str) -> str:
6844 """Normalize the path."""
6845 if not path:
6846 return ''
6847 path = os.path.normpath(path)
6848 # os.path.normpath does the *reverse* of what we want.
6849 if g.isWindows:
6850 path = path.replace('\\', '/').lower() # #2049: ignore case!
6851 return path
6852#@+node:ekr.20180314081254.1: *3* g.os_path_normslashes
6853def os_path_normslashes(path: str) -> str:
6855 # os.path.normpath does the *reverse* of what we want.
6856 if g.isWindows and path:
6857 path = path.replace('\\', '/')
6858 return path
6859#@+node:ekr.20080605064555.2: *3* g.os_path_realpath
6860def os_path_realpath(path: str) -> str:
6861 """Return the canonical path of the specified filename, eliminating any
6862 symbolic links encountered in the path (if they are supported by the
6863 operating system).
6864 """
6865 if not path:
6866 return ''
6867 path = os.path.realpath(path)
6868 # os.path.normpath does the *reverse* of what we want.
6869 if g.isWindows:
6870 path = path.replace('\\', '/')
6871 return path
6872#@+node:ekr.20031218072017.2158: *3* g.os_path_split
6873def os_path_split(path: str) -> Tuple[str, str]:
6874 if not path:
6875 return '', ''
6876 head, tail = os.path.split(path)
6877 return head, tail
6878#@+node:ekr.20031218072017.2159: *3* g.os_path_splitext
6879def os_path_splitext(path: str) -> Tuple[str, str]:
6881 if not path:
6882 return '', ''
6883 head, tail = os.path.splitext(path)
6884 return head, tail
6885#@+node:ekr.20090829140232.6036: *3* g.os_startfile
6886def os_startfile(fname: str) -> None:
6887 #@+others
6888 #@+node:bob.20170516112250.1: *4* stderr2log()
6889 def stderr2log(g: Any, ree: Any, fname: str) -> None:
6890 """ Display stderr output in the Leo-Editor log pane
6892 Arguments:
6893 g: Leo-Editor globals
6894 ree: Read file descriptor for stderr
6895 fname: file pathname
6897 Returns:
6898 None
6899 """
6901 while True:
6902 emsg = ree.read().decode('utf-8')
6903 if emsg:
6904 g.es_print_error(f"xdg-open {fname} caused output to stderr:\n{emsg}")
6905 else:
6906 break
6907 #@+node:bob.20170516112304.1: *4* itPoll()
6908 def itPoll(fname: str, ree: Any, subPopen: Any, g: Any, ito: Any) -> None:
6909 """ Poll for subprocess done
6911 Arguments:
6912 fname: File name
6913 ree: stderr read file descriptor
6914 subPopen: URL open subprocess object
6915 g: Leo-Editor globals
6916 ito: Idle time object for itPoll()
6918 Returns:
6919 None
6920 """
6922 stderr2log(g, ree, fname)
6923 rc = subPopen.poll()
6924 if not rc is None:
6925 ito.stop()
6926 ito.destroy_self()
6927 if rc != 0:
6928 g.es_print(f"xdg-open {fname} failed with exit code {rc}")
6929 stderr2log(g, ree, fname)
6930 ree.close()
6931 #@-others
6932 if fname.find('"') > -1:
6933 quoted_fname = f"'{fname}'"
6934 else:
6935 quoted_fname = f'"{fname}"'
6936 if sys.platform.startswith('win'):
6937 # pylint: disable=no-member
6938 os.startfile(quoted_fname)
6939 # Exists only on Windows.
6940 elif sys.platform == 'darwin':
6941 # From Marc-Antoine Parent.
6942 try:
6943 # Fix bug 1226358: File URL's are broken on MacOS:
6944 # use fname, not quoted_fname, as the argument to subprocess.call.
6945 subprocess.call(['open', fname])
6946 except OSError:
6947 pass # There may be a spurious "Interrupted system call"
6948 except ImportError:
6949 os.system(f"open {quoted_fname}")
6950 else:
6951 try:
6952 ree = None
6953 wre = tempfile.NamedTemporaryFile()
6954 ree = io.open(wre.name, 'rb', buffering=0)
6955 except IOError:
6956 g.trace(f"error opening temp file for {fname!r}")
6957 if ree:
6958 ree.close()
6959 return
6960 try:
6961 subPopen = subprocess.Popen(['xdg-open', fname], stderr=wre, shell=False)
6962 except Exception:
6963 g.es_print(f"error opening {fname!r}")
6964 g.es_exception()
6965 try:
6966 itoPoll = g.IdleTime(
6967 (lambda ito: itPoll(fname, ree, subPopen, g, ito)),
6968 delay=1000,
6969 )
6970 itoPoll.start()
6971 # Let the Leo-Editor process run
6972 # so that Leo-Editor is usable while the file is open.
6973 except Exception:
6974 g.es_exception(f"exception executing g.startfile for {fname!r}")
6975#@+node:ekr.20111115155710.9859: ** g.Parsing & Tokenizing
6976#@+node:ekr.20031218072017.822: *3* g.createTopologyList
6977def createTopologyList(c: Cmdr, root: Pos=None, useHeadlines: bool=False) -> List:
6978 """Creates a list describing a node and all its descendents"""
6979 if not root:
6980 root = c.rootPosition()
6981 v = root
6982 if useHeadlines:
6983 aList = [(v.numberOfChildren(), v.headString()),] # type: ignore
6984 else:
6985 aList = [v.numberOfChildren()] # type: ignore
6986 child = v.firstChild()
6987 while child:
6988 aList.append(g.createTopologyList(c, child, useHeadlines)) # type: ignore
6989 child = child.next()
6990 return aList
6991#@+node:ekr.20111017204736.15898: *3* g.getDocString
6992def getDocString(s: str) -> str:
6993 """Return the text of the first docstring found in s."""
6994 tags = ('"""', "'''")
6995 tag1, tag2 = tags
6996 i1, i2 = s.find(tag1), s.find(tag2)
6997 if i1 == -1 and i2 == -1:
6998 return ''
6999 if i1 > -1 and i2 > -1:
7000 i = min(i1, i2)
7001 else:
7002 i = max(i1, i2)
7003 tag = s[i : i + 3]
7004 assert tag in tags
7005 j = s.find(tag, i + 3)
7006 if j > -1:
7007 return s[i + 3 : j]
7008 return ''
7009#@+node:ekr.20111017211256.15905: *3* g.getDocStringForFunction
7010def getDocStringForFunction(func: Any) -> str:
7011 """Return the docstring for a function that creates a Leo command."""
7013 def name(func: Any) -> str:
7014 return func.__name__ if hasattr(func, '__name__') else '<no __name__>'
7016 def get_defaults(func: str, i: int) -> Any:
7017 defaults = inspect.getfullargspec(func)[3]
7018 return defaults[i]
7020 # Fix bug 1251252: https://bugs.launchpad.net/leo-editor/+bug/1251252
7021 # Minibuffer commands created by mod_scripting.py have no docstrings.
7022 # Do special cases first.
7024 s = ''
7025 if name(func) == 'minibufferCallback':
7026 func = get_defaults(func, 0)
7027 if hasattr(func, 'func.__doc__') and func.__doc__.strip():
7028 s = func.__doc__
7029 if not s and name(func) == 'commonCommandCallback':
7030 script = get_defaults(func, 1)
7031 s = g.getDocString(script)
7032 # Do a text scan for the function.
7033 # Now the general cases. Prefer __doc__ to docstring()
7034 if not s and hasattr(func, '__doc__'):
7035 s = func.__doc__
7036 if not s and hasattr(func, 'docstring'):
7037 s = func.docstring
7038 return s
7039#@+node:ekr.20111115155710.9814: *3* g.python_tokenize (not used)
7040def python_tokenize(s: str) -> List:
7041 """
7042 Tokenize string s and return a list of tokens (kind, value, line_number)
7044 where kind is in ('comment,'id','nl','other','string','ws').
7045 """
7046 result: List[Tuple[str, str, int]] = []
7047 i, line_number = 0, 0
7048 while i < len(s):
7049 progress = j = i
7050 ch = s[i]
7051 if ch == '\n':
7052 kind, i = 'nl', i + 1
7053 elif ch in ' \t':
7054 kind = 'ws'
7055 while i < len(s) and s[i] in ' \t':
7056 i += 1
7057 elif ch == '#':
7058 kind, i = 'comment', g.skip_to_end_of_line(s, i)
7059 elif ch in '"\'':
7060 kind, i = 'string', g.skip_python_string(s, i)
7061 elif ch == '_' or ch.isalpha():
7062 kind, i = 'id', g.skip_id(s, i)
7063 else:
7064 kind, i = 'other', i + 1
7065 assert progress < i and j == progress
7066 val = s[j:i]
7067 assert val
7068 line_number += val.count('\n') # A comment.
7069 result.append((kind, val, line_number),)
7070 return result
7071#@+node:ekr.20040327103735.2: ** g.Scripting
7072#@+node:ekr.20161223090721.1: *3* g.exec_file
7073def exec_file(path: str, d: Dict[str, str], script: str=None) -> None:
7074 """Simulate python's execfile statement for python 3."""
7075 if script is None:
7076 with open(path) as f:
7077 script = f.read()
7078 exec(compile(script, path, 'exec'), d)
7079#@+node:ekr.20131016032805.16721: *3* g.execute_shell_commands
7080def execute_shell_commands(commands: Any, trace: bool=False) -> None:
7081 """
7082 Execute each shell command in a separate process.
7083 Wait for each command to complete, except those starting with '&'
7084 """
7085 if isinstance(commands, str):
7086 commands = [commands]
7087 for command in commands:
7088 wait = not command.startswith('&')
7089 if trace:
7090 g.trace(command)
7091 if command.startswith('&'):
7092 command = command[1:].strip()
7093 proc = subprocess.Popen(command, shell=True)
7094 if wait:
7095 proc.communicate()
7096 else:
7097 if trace:
7098 print('Start:', proc)
7099 # #1489: call proc.poll at idle time.
7101 def proc_poller(timer: Any, proc: Any=proc) -> None:
7102 val = proc.poll()
7103 if val is not None:
7104 # This trace can be disruptive.
7105 if trace:
7106 print(' End:', proc, val)
7107 timer.stop()
7109 g.IdleTime(proc_poller, delay=0).start()
7110#@+node:ekr.20180217113719.1: *3* g.execute_shell_commands_with_options & helpers
7111def execute_shell_commands_with_options(
7112 base_dir: str=None,
7113 c: Cmdr=None,
7114 command_setting: str=None,
7115 commands: List=None,
7116 path_setting: str=None,
7117 trace: bool=False,
7118 warning: str=None,
7119) -> None:
7120 """
7121 A helper for prototype commands or any other code that
7122 runs programs in a separate process.
7124 base_dir: Base directory to use if no config path given.
7125 commands: A list of commands, for g.execute_shell_commands.
7126 commands_setting: Name of @data setting for commands.
7127 path_setting: Name of @string setting for the base directory.
7128 warning: A warning to be printed before executing the commands.
7129 """
7130 base_dir = g.computeBaseDir(c, base_dir, path_setting, trace)
7131 if not base_dir:
7132 return
7133 commands = g.computeCommands(c, commands, command_setting, trace)
7134 if not commands:
7135 return
7136 if warning:
7137 g.es_print(warning)
7138 os.chdir(base_dir) # Can't do this in the commands list.
7139 g.execute_shell_commands(commands)
7140#@+node:ekr.20180217152624.1: *4* g.computeBaseDir
7141def computeBaseDir(c: Cmdr, base_dir: str, path_setting: str, trace: bool=False) -> Optional[str]:
7142 """
7143 Compute a base_directory.
7144 If given, @string path_setting takes precedence.
7145 """
7146 # Prefer the path setting to the base_dir argument.
7147 if path_setting:
7148 if not c:
7149 g.es_print('@string path_setting requires valid c arg')
7150 return None
7151 # It's not an error for the setting to be empty.
7152 base_dir2 = c.config.getString(path_setting)
7153 if base_dir2:
7154 base_dir2 = base_dir2.replace('\\', '/')
7155 if g.os_path_exists(base_dir2):
7156 return base_dir2
7157 g.es_print(f"@string {path_setting} not found: {base_dir2!r}")
7158 return None
7159 # Fall back to given base_dir.
7160 if base_dir:
7161 base_dir = base_dir.replace('\\', '/')
7162 if g.os_path_exists(base_dir):
7163 return base_dir
7164 g.es_print(f"base_dir not found: {base_dir!r}")
7165 return None
7166 g.es_print(f"Please use @string {path_setting}")
7167 return None
7168#@+node:ekr.20180217153459.1: *4* g.computeCommands
7169def computeCommands(c: Cmdr, commands: List[str], command_setting: str, trace: bool=False) -> List[str]:
7170 """
7171 Get the list of commands.
7172 If given, @data command_setting takes precedence.
7173 """
7174 if not commands and not command_setting:
7175 g.es_print('Please use commands, command_setting or both')
7176 return []
7177 # Prefer the setting to the static commands.
7178 if command_setting:
7179 if c:
7180 aList = c.config.getData(command_setting)
7181 # It's not an error for the setting to be empty.
7182 # Fall back to the commands.
7183 return aList or commands
7184 g.es_print('@data command_setting requires valid c arg')
7185 return []
7186 return commands
7187#@+node:ekr.20050503112513.7: *3* g.executeFile
7188def executeFile(filename: str, options: str='') -> None:
7189 if not os.access(filename, os.R_OK):
7190 return
7191 fdir, fname = g.os_path_split(filename)
7192 # New in Leo 4.10: alway use subprocess.
7194 def subprocess_wrapper(cmdlst: str) -> Tuple:
7196 p = subprocess.Popen(cmdlst, cwd=fdir,
7197 universal_newlines=True,
7198 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
7199 stdo, stde = p.communicate()
7200 return p.wait(), stdo, stde
7202 rc, so, se = subprocess_wrapper(f"{sys.executable} {fname} {options}")
7203 if rc:
7204 g.pr('return code', rc)
7205 g.pr(so, se)
7206#@+node:ekr.20040321065415: *3* g.find*Node*
7207#@+others
7208#@+node:ekr.20210303123423.3: *4* findNodeAnywhere
7209def findNodeAnywhere(c: Cmdr, headline: str, exact: bool=True) -> Optional[Pos]:
7210 h = headline.strip()
7211 for p in c.all_unique_positions(copy=False):
7212 if p.h.strip() == h:
7213 return p.copy()
7214 if not exact:
7215 for p in c.all_unique_positions(copy=False):
7216 if p.h.strip().startswith(h):
7217 return p.copy()
7218 return None
7219#@+node:ekr.20210303123525.1: *4* findNodeByPath
7220def findNodeByPath(c: Cmdr, path: str) -> Optional[Pos]:
7221 """Return the first @<file> node in Cmdr c whose path is given."""
7222 if not os.path.isabs(path): # #2049. Only absolute paths could possibly work.
7223 g.trace(f"path not absolute: {path}")
7224 return None
7225 path = g.os_path_normpath(path) # #2049. Do *not* use os.path.normpath.
7226 for p in c.all_positions():
7227 if p.isAnyAtFileNode():
7228 if path == g.os_path_normpath(g.fullPath(c, p)): # #2049. Do *not* use os.path.normpath.
7229 return p
7230 return None
7231#@+node:ekr.20210303123423.1: *4* findNodeInChildren
7232def findNodeInChildren(c: Cmdr, p: Pos, headline: str, exact: bool=True) -> Optional[Pos]:
7233 """Search for a node in v's tree matching the given headline."""
7234 p1 = p.copy()
7235 h = headline.strip()
7236 for p in p1.children():
7237 if p.h.strip() == h:
7238 return p.copy()
7239 if not exact:
7240 for p in p1.children():
7241 if p.h.strip().startswith(h):
7242 return p.copy()
7243 return None
7244#@+node:ekr.20210303123423.2: *4* findNodeInTree
7245def findNodeInTree(c: Cmdr, p: Pos, headline: str, exact: bool=True) -> Optional[Pos]:
7246 """Search for a node in v's tree matching the given headline."""
7247 h = headline.strip()
7248 p1 = p.copy()
7249 for p in p1.subtree():
7250 if p.h.strip() == h:
7251 return p.copy()
7252 if not exact:
7253 for p in p1.subtree():
7254 if p.h.strip().startswith(h):
7255 return p.copy()
7256 return None
7257#@+node:ekr.20210303123423.4: *4* findTopLevelNode
7258def findTopLevelNode(c: Cmdr, headline: str, exact: bool=True) -> Optional[Pos]:
7259 h = headline.strip()
7260 for p in c.rootPosition().self_and_siblings(copy=False):
7261 if p.h.strip() == h:
7262 return p.copy()
7263 if not exact:
7264 for p in c.rootPosition().self_and_siblings(copy=False):
7265 if p.h.strip().startswith(h):
7266 return p.copy()
7267 return None
7268#@-others
7269#@+node:EKR.20040614071102.1: *3* g.getScript & helpers
7270def getScript(
7271 c: Cmdr,
7272 p: Pos,
7273 useSelectedText: bool=True,
7274 forcePythonSentinels: bool=True,
7275 useSentinels: bool=True,
7276) -> str:
7277 """
7278 Return the expansion of the selected text of node p.
7279 Return the expansion of all of node p's body text if
7280 p is not the current node or if there is no text selection.
7281 """
7282 w = c.frame.body.wrapper
7283 if not p:
7284 p = c.p
7285 try:
7286 if g.app.inBridge:
7287 s = p.b
7288 elif w and p == c.p and useSelectedText and w.hasSelection():
7289 s = w.getSelectedText()
7290 else:
7291 s = p.b
7292 # Remove extra leading whitespace so the user may execute indented code.
7293 s = textwrap.dedent(s)
7294 s = g.extractExecutableString(c, p, s)
7295 script = g.composeScript(c, p, s,
7296 forcePythonSentinels=forcePythonSentinels,
7297 useSentinels=useSentinels)
7298 except Exception:
7299 g.es_print("unexpected exception in g.getScript")
7300 g.es_exception()
7301 script = ''
7302 return script
7303#@+node:ekr.20170228082641.1: *4* g.composeScript
7304def composeScript(
7305 c: Cmdr,
7306 p: Pos,
7307 s: str,
7308 forcePythonSentinels: bool=True,
7309 useSentinels: bool=True,
7310) -> str:
7311 """Compose a script from p.b."""
7312 # This causes too many special cases.
7313 # if not g.unitTesting and forceEncoding:
7314 # aList = g.get_directives_dict_list(p)
7315 # encoding = scanAtEncodingDirectives(aList) or 'utf-8'
7316 # s = g.insertCodingLine(encoding,s)
7317 if not s.strip():
7318 return ''
7319 at = c.atFileCommands # type:ignore
7320 old_in_script = g.app.inScript
7321 try:
7322 # #1297: set inScript flags.
7323 g.app.inScript = g.inScript = True
7324 g.app.scriptDict["script1"] = s
7325 # Important: converts unicode to utf-8 encoded strings.
7326 script = at.stringToString(p.copy(), s,
7327 forcePythonSentinels=forcePythonSentinels,
7328 sentinels=useSentinels)
7329 script = script.replace("\r\n", "\n") # Use brute force.
7330 # Important, the script is an **encoded string**, not a unicode string.
7331 g.app.scriptDict["script2"] = script
7332 finally:
7333 g.app.inScript = g.inScript = old_in_script
7334 return script
7335#@+node:ekr.20170123074946.1: *4* g.extractExecutableString
7336def extractExecutableString(c: Cmdr, p: Pos, s: str) -> str:
7337 """
7338 Return all lines for the given @language directive.
7340 Ignore all lines under control of any other @language directive.
7341 """
7342 #
7343 # Rewritten to fix #1071.
7344 if g.unitTesting:
7345 return s # Regretable, but necessary.
7346 #
7347 # Return s if no @language in effect. Should never happen.
7348 language = g.scanForAtLanguage(c, p)
7349 if not language:
7350 return s
7351 #
7352 # Return s if @language is unambiguous.
7353 pattern = r'^@language\s+(\w+)'
7354 matches = list(re.finditer(pattern, s, re.MULTILINE))
7355 if len(matches) < 2:
7356 return s
7357 #
7358 # Scan the lines, extracting only the valid lines.
7359 extracting, result = False, []
7360 for i, line in enumerate(g.splitLines(s)):
7361 m = re.match(pattern, line)
7362 if m:
7363 # g.trace(language, m.group(1))
7364 extracting = m.group(1) == language
7365 elif extracting:
7366 result.append(line)
7367 return ''.join(result)
7368#@+node:ekr.20060624085200: *3* g.handleScriptException
7369def handleScriptException(c: Cmdr, p: Pos, script: str, script1: str) -> None:
7370 g.warning("exception executing script")
7371 full = c.config.getBool('show-full-tracebacks-in-scripts')
7372 fileName, n = g.es_exception(full=full)
7373 # Careful: this test is no longer guaranteed.
7374 if p.v.context == c:
7375 try:
7376 c.goToScriptLineNumber(n, p)
7377 #@+<< dump the lines near the error >>
7378 #@+node:EKR.20040612215018: *4* << dump the lines near the error >>
7379 if g.os_path_exists(fileName):
7380 with open(fileName) as f:
7381 lines = f.readlines()
7382 else:
7383 lines = g.splitLines(script)
7384 s = '-' * 20
7385 g.es_print('', s)
7386 # Print surrounding lines.
7387 i = max(0, n - 2)
7388 j = min(n + 2, len(lines))
7389 while i < j:
7390 ch = '*' if i == n - 1 else ' '
7391 s = f"{ch} line {i+1:d}: {lines[i]}"
7392 g.es('', s, newline=False)
7393 i += 1
7394 #@-<< dump the lines near the error >>
7395 except Exception:
7396 g.es_print('Unexpected exception in g.handleScriptException')
7397 g.es_exception()
7398#@+node:ekr.20140209065845.16767: *3* g.insertCodingLine
7399def insertCodingLine(encoding: str, script: str) -> str:
7400 """
7401 Insert a coding line at the start of script s if no such line exists.
7402 The coding line must start with @first because it will be passed to
7403 at.stringToString.
7404 """
7405 if script:
7406 tag = '@first # -*- coding:'
7407 lines = g.splitLines(script)
7408 for s in lines:
7409 if s.startswith(tag):
7410 break
7411 else:
7412 lines.insert(0, f"{tag} {encoding} -*-\n")
7413 script = ''.join(lines)
7414 return script
7415#@+node:ekr.20070524083513: ** g.Unit Tests
7416#@+node:ekr.20210901071523.1: *3* g.run_coverage_tests
7417def run_coverage_tests(module: str='', filename: str='') -> None:
7418 """
7419 Run the coverage tests given by the module and filename strings.
7420 """
7421 leo_editor_dir = os.path.join(g.app.loadDir, '..', '..')
7422 os.chdir(leo_editor_dir)
7423 prefix = r"python -m pytest --cov-report html --cov-report term-missing --cov "
7424 command = f"{prefix} {module} {filename}"
7425 g.execute_shell_commands(command, trace=False)
7426#@+node:ekr.20200221050038.1: *3* g.run_unit_test_in_separate_process
7427def run_unit_test_in_separate_process(command: str) -> None:
7428 """
7429 A script to be run from unitTest.leo.
7431 Run the unit testing command (say `python -m leo.core.leoAst`) in a separate process.
7432 """
7433 leo_editor_dir = os.path.join(g.app.loadDir, '..', '..')
7434 os.chdir(leo_editor_dir)
7435 p = subprocess.Popen(
7436 shlex.split(command),
7437 stdout=subprocess.PIPE,
7438 stderr=subprocess.PIPE,
7439 shell=sys.platform.startswith('win'),
7440 )
7441 out, err = p.communicate()
7442 err = g.toUnicode(err)
7443 out = g.toUnicode(out)
7444 print('')
7445 print(command)
7446 if out.strip():
7447 # print('traces...')
7448 print(out.rstrip())
7449 print(err.rstrip())
7450 # There may be skipped tests...
7451 err_lines = g.splitLines(err.rstrip())
7452 if not err_lines[-1].startswith('OK'):
7453 g.trace('Test failed')
7454 g.printObj(err_lines, tag='err_lines')
7455 assert False
7456#@+node:ekr.20210901065224.1: *3* g.run_unit_tests
7457def run_unit_tests(tests: str=None, verbose: bool=False) -> None:
7458 """
7459 Run the unit tests given by the "tests" string.
7461 Run *all* unit tests if "tests" is not given.
7462 """
7463 leo_editor_dir = g.os_path_finalize_join(g.app.loadDir, '..', '..')
7464 os.chdir(leo_editor_dir)
7465 verbosity = '-v' if verbose else ''
7466 command = f"python -m unittest {verbosity} {tests or ''} "
7467 # pytest reports too many errors.
7468 # command = f"python -m pytest --pdb {tests or ''}"
7469 g.execute_shell_commands(command, trace=False)
7470#@+node:ekr.20120311151914.9916: ** g.Urls & UNLs
7471unl_regex = re.compile(r'\bunl:.*$')
7473kinds = '(file|ftp|gopher|http|https|mailto|news|nntp|prospero|telnet|wais)'
7474url_regex = re.compile(fr"""{kinds}://[^\s'"]+[\w=/]""")
7475#@+node:ekr.20120320053907.9776: *3* g.computeFileUrl
7476def computeFileUrl(fn: str, c: Cmdr=None, p: Pos=None) -> str:
7477 """
7478 Compute finalized url for filename fn.
7479 """
7480 # First, replace special characters (especially %20, by their equivalent).
7481 url = urllib.parse.unquote(fn)
7482 # Finalize the path *before* parsing the url.
7483 i = url.find('~')
7484 if i > -1:
7485 # Expand '~'.
7486 path = url[i:]
7487 path = g.os_path_expanduser(path)
7488 # #1338: This is way too dangerous, and a serious security violation.
7489 # path = c.os_path_expandExpression(path)
7490 path = g.os_path_finalize(path)
7491 url = url[:i] + path
7492 else:
7493 tag = 'file://'
7494 tag2 = 'file:///'
7495 if sys.platform.startswith('win') and url.startswith(tag2):
7496 path = url[len(tag2) :].lstrip()
7497 elif url.startswith(tag):
7498 path = url[len(tag) :].lstrip()
7499 else:
7500 path = url
7501 # #1338: This is way too dangerous, and a serious security violation.
7502 # path = c.os_path_expandExpression(path)
7503 # Handle ancestor @path directives.
7504 if c and c.openDirectory:
7505 base = c.getNodePath(p)
7506 path = g.os_path_finalize_join(c.openDirectory, base, path)
7507 else:
7508 path = g.os_path_finalize(path)
7509 url = f"{tag}{path}"
7510 return url
7511#@+node:ekr.20190608090856.1: *3* g.es_clickable_link
7512def es_clickable_link(c: Cmdr, p: Pos, line_number: int, message: str) -> None:
7513 """
7514 Write a clickable message to the given line number of p.b.
7516 Negative line numbers indicate global lines.
7518 """
7519 unl = p.get_UNL()
7520 c.frame.log.put(message.strip() + '\n', nodeLink=f"{unl}::{line_number}")
7521#@+node:tbrown.20140311095634.15188: *3* g.findUNL & helpers
7522def findUNL(unlList1: List[str], c: Cmdr) -> Optional[Pos]:
7523 """
7524 Find and move to the unl given by the unlList in the commander c.
7525 Return the found position, or None.
7526 """
7527 # Define the unl patterns.
7528 old_pat = re.compile(r'^(.*):(\d+),?(\d+)?,?([-\d]+)?,?(\d+)?$') # ':' is the separator.
7529 new_pat = re.compile(r'^(.*?)(::)([-\d]+)?$') # '::' is the separator.
7531 #@+others # Define helper functions
7532 #@+node:ekr.20220213142925.1: *4* function: convert_unl_list
7533 def convert_unl_list(aList: List[str]) -> List[str]:
7534 """
7535 Convert old-style UNLs to new UNLs, retaining line numbers if possible.
7536 """
7537 result = []
7538 for s in aList:
7539 # Try to get the line number.
7540 for m, line_group in (
7541 (old_pat.match(s), 4),
7542 (new_pat.match(s), 3),
7543 ):
7544 if m:
7545 try:
7546 n = int(m.group(line_group))
7547 result.append(f"{m.group(1)}::{n}")
7548 continue
7549 except Exception:
7550 pass
7551 # Finally, just add the whole UNL.
7552 result.append(s)
7553 return result
7554 #@+node:ekr.20220213142735.1: *4* function: full_match
7555 def full_match(p: Pos) -> bool:
7556 """Return True if the headlines of p and all p's parents match unlList."""
7557 # Careful: make copies.
7558 aList, p1 = unlList[:], p.copy()
7559 while aList and p1:
7560 m = new_pat.match(aList[-1])
7561 if m and m.group(1) != p1.h:
7562 return False
7563 if not m and aList[-1] != p1.h:
7564 return False
7565 aList.pop()
7566 p1.moveToParent()
7567 return not aList
7568 #@-others
7570 unlList = convert_unl_list(unlList1)
7571 if not unlList:
7572 return None
7573 # Find all target headlines.
7574 targets = []
7575 m = new_pat.match(unlList[-1])
7576 target = m and m.group(1) or unlList[-1]
7577 targets.append(target)
7578 targets.extend(unlList[:-1])
7579 # Find all target positions. Prefer later positions.
7580 positions = list(reversed(list(z for z in c.all_positions() if z.h in targets)))
7581 while unlList:
7582 for p in positions:
7583 p1 = p.copy()
7584 if full_match(p):
7585 assert p == p1, (p, p1)
7586 n = 0 # The default line number.
7587 # Parse the last target.
7588 m = new_pat.match(unlList[-1])
7589 if m:
7590 line = m.group(3)
7591 try:
7592 n = int(line)
7593 except(TypeError, ValueError):
7594 g.trace('bad line number', line)
7595 if n == 0:
7596 c.redraw(p)
7597 elif n < 0:
7598 p, offset, ok = c.gotoCommands.find_file_line(-n, p) # Calls c.redraw().
7599 return p if ok else None
7600 elif n > 0:
7601 insert_point = sum(len(i) + 1 for i in p.b.split('\n')[: n - 1])
7602 c.redraw(p)
7603 c.frame.body.wrapper.setInsertPoint(insert_point)
7604 c.frame.bringToFront()
7605 c.bodyWantsFocusNow()
7606 return p
7607 # Not found. Pop the first parent from unlList.
7608 unlList.pop(0)
7609 return None
7610#@+node:ekr.20120311151914.9917: *3* g.getUrlFromNode
7611def getUrlFromNode(p: Pos) -> Optional[str]:
7612 """
7613 Get an url from node p:
7614 1. Use the headline if it contains a valid url.
7615 2. Otherwise, look *only* at the first line of the body.
7616 """
7617 if not p:
7618 return None
7619 c = p.v.context
7620 assert c
7621 table = [p.h, g.splitLines(p.b)[0] if p.b else '']
7622 table = [s[4:] if g.match_word(s, 0, '@url') else s for s in table]
7623 table = [s.strip() for s in table if s.strip()]
7624 # First, check for url's with an explicit scheme.
7625 for s in table:
7626 if g.isValidUrl(s):
7627 return s
7628 # Next check for existing file and add a file:// scheme.
7629 for s in table:
7630 tag = 'file://'
7631 url = computeFileUrl(s, c=c, p=p)
7632 if url.startswith(tag):
7633 fn = url[len(tag) :].lstrip()
7634 fn = fn.split('#', 1)[0]
7635 if g.os_path_isfile(fn):
7636 # Return the *original* url, with a file:// scheme.
7637 # g.handleUrl will call computeFileUrl again.
7638 return 'file://' + s
7639 # Finally, check for local url's.
7640 for s in table:
7641 if s.startswith("#"):
7642 return s
7643 return None
7644#@+node:ekr.20170221063527.1: *3* g.handleUnl
7645def handleUnl(unl: str, c: Cmdr) -> Any:
7646 """
7647 Handle a Leo UNL. This must *never* open a browser.
7649 Return the commander for the found UNL, or None.
7651 Redraw the commander if the UNL is found.
7652 """
7653 if not unl:
7654 return None
7655 unll = unl.lower()
7656 if unll.startswith('unl://'):
7657 unl = unl[6:]
7658 elif unll.startswith('file://'):
7659 unl = unl[7:]
7660 unl = unl.strip()
7661 if not unl:
7662 return None
7663 unl = g.unquoteUrl(unl)
7664 # Compute path and unl.
7665 if '#' not in unl and '-->' not in unl:
7666 # The path is the entire unl.
7667 path, unl = unl, None
7668 elif '#' not in unl:
7669 # The path is empty.
7670 # Move to the unl in *this* commander.
7671 p = g.findUNL(unl.split("-->"), c)
7672 if p:
7673 c.redraw(p)
7674 return c
7675 else:
7676 path, unl = unl.split('#', 1)
7677 if unl and not path: # #2407
7678 # Move to the unl in *this* commander.
7679 p = g.findUNL(unl.split("-->"), c)
7680 if p:
7681 c.redraw(p)
7682 return c
7683 if c:
7684 base = g.os_path_dirname(c.fileName())
7685 c_path = g.os_path_finalize_join(base, path)
7686 else:
7687 c_path = None
7688 # Look for the file in various places.
7689 table = (
7690 c_path,
7691 g.os_path_finalize_join(g.app.loadDir, '..', path),
7692 g.os_path_finalize_join(g.app.loadDir, '..', '..', path),
7693 g.os_path_finalize_join(g.app.loadDir, '..', 'core', path),
7694 g.os_path_finalize_join(g.app.loadDir, '..', 'config', path),
7695 g.os_path_finalize_join(g.app.loadDir, '..', 'dist', path),
7696 g.os_path_finalize_join(g.app.loadDir, '..', 'doc', path),
7697 g.os_path_finalize_join(g.app.loadDir, '..', 'test', path),
7698 g.app.loadDir,
7699 g.app.homeDir,
7700 )
7701 for path2 in table:
7702 if path2 and path2.lower().endswith('.leo') and os.path.exists(path2):
7703 path = path2
7704 break
7705 else:
7706 g.es_print('path not found', repr(path))
7707 return None
7708 # End editing in *this* outline, so typing in the new outline works.
7709 c.endEditing()
7710 c.redraw()
7711 # Open the path.
7712 c2 = g.openWithFileName(path, old_c=c)
7713 if not c2:
7714 return None
7715 # Find the UNL, select the node, and redraw.
7716 p = g.findUNL(unl.split("-->"), c2)
7717 if not p:
7718 return None
7719 c2.redraw(p)
7720 c2.bringToFront()
7721 c2.bodyWantsFocusNow()
7722 return c2
7723#@+node:tbrown.20090219095555.63: *3* g.handleUrl & helpers
7724def handleUrl(url: str, c: Cmdr=None, p: Pos=None) -> Any:
7725 """Open a url or a unl."""
7726 if c and not p:
7727 p = c.p
7728 urll = url.lower()
7729 if urll.startswith('@url'):
7730 url = url[4:].lstrip()
7731 if (
7732 urll.startswith('unl://') or
7733 urll.startswith('file://') and url.find('-->') > -1 or
7734 urll.startswith('#')
7735 ):
7736 return g.handleUnl(url, c)
7737 try:
7738 return g.handleUrlHelper(url, c, p)
7739 except Exception:
7740 g.es_print("g.handleUrl: exception opening", repr(url))
7741 g.es_exception()
7742 return None
7743#@+node:ekr.20170226054459.1: *4* g.handleUrlHelper
7744def handleUrlHelper(url: str, c: Cmdr, p: Pos) -> None:
7745 """Open a url. Most browsers should handle:
7746 ftp://ftp.uu.net/public/whatever
7747 http://localhost/MySiteUnderDevelopment/index.html
7748 file:///home/me/todolist.html
7749 """
7750 tag = 'file://'
7751 original_url = url
7752 if url.startswith(tag) and not url.startswith(tag + '#'):
7753 # Finalize the path *before* parsing the url.
7754 url = g.computeFileUrl(url, c=c, p=p)
7755 parsed = urlparse.urlparse(url)
7756 if parsed.netloc:
7757 leo_path = os.path.join(parsed.netloc, parsed.path)
7758 # "readme.txt" gets parsed into .netloc...
7759 else:
7760 leo_path = parsed.path
7761 if leo_path.endswith('\\'):
7762 leo_path = leo_path[:-1]
7763 if leo_path.endswith('/'):
7764 leo_path = leo_path[:-1]
7765 if parsed.scheme == 'file' and leo_path.endswith('.leo'):
7766 g.handleUnl(original_url, c)
7767 elif parsed.scheme in ('', 'file'):
7768 unquote_path = g.unquoteUrl(leo_path)
7769 if g.unitTesting:
7770 pass
7771 elif g.os_path_exists(leo_path):
7772 g.os_startfile(unquote_path)
7773 else:
7774 g.es(f"File '{leo_path}' does not exist")
7775 else:
7776 if g.unitTesting:
7777 pass
7778 else:
7779 # Mozilla throws a weird exception, then opens the file!
7780 try:
7781 webbrowser.open(url)
7782 except Exception:
7783 pass
7784#@+node:ekr.20170226060816.1: *4* g.traceUrl
7785def traceUrl(c: Cmdr, path: str, parsed: Any, url: str) -> None:
7787 print()
7788 g.trace('url ', url)
7789 g.trace('c.frame.title', c.frame.title)
7790 g.trace('path ', path)
7791 g.trace('parsed.fragment', parsed.fragment)
7792 g.trace('parsed.netloc', parsed.netloc)
7793 g.trace('parsed.path ', parsed.path)
7794 g.trace('parsed.scheme', repr(parsed.scheme))
7795#@+node:ekr.20120311151914.9918: *3* g.isValidUrl
7796def isValidUrl(url: str) -> bool:
7797 """Return true if url *looks* like a valid url."""
7798 table = (
7799 'file', 'ftp', 'gopher', 'hdl', 'http', 'https', 'imap',
7800 'mailto', 'mms', 'news', 'nntp', 'prospero', 'rsync', 'rtsp', 'rtspu',
7801 'sftp', 'shttp', 'sip', 'sips', 'snews', 'svn', 'svn+ssh', 'telnet', 'wais',
7802 )
7803 if url.lower().startswith('unl://') or url.startswith('#'):
7804 # All Leo UNL's.
7805 return True
7806 if url.startswith('@'):
7807 return False
7808 parsed = urlparse.urlparse(url)
7809 scheme = parsed.scheme
7810 for s in table:
7811 if scheme.startswith(s):
7812 return True
7813 return False
7814#@+node:ekr.20120315062642.9744: *3* g.openUrl
7815def openUrl(p: Pos) -> None:
7816 """
7817 Open the url of node p.
7818 Use the headline if it contains a valid url.
7819 Otherwise, look *only* at the first line of the body.
7820 """
7821 if p:
7822 url = g.getUrlFromNode(p)
7823 if url:
7824 c = p.v.context
7825 if not g.doHook("@url1", c=c, p=p, url=url):
7826 g.handleUrl(url, c=c, p=p)
7827 g.doHook("@url2", c=c, p=p, url=url)
7828#@+node:ekr.20110605121601.18135: *3* g.openUrlOnClick (open-url-under-cursor)
7829def openUrlOnClick(event: Any, url: str=None) -> Optional[str]:
7830 """Open the URL under the cursor. Return it for unit testing."""
7831 # This can be called outside Leo's command logic, so catch all exceptions.
7832 try:
7833 return openUrlHelper(event, url)
7834 except Exception:
7835 g.es_exception()
7836 return None
7837#@+node:ekr.20170216091704.1: *4* g.openUrlHelper
7838def openUrlHelper(event: Any, url: str=None) -> Optional[str]:
7839 """Open the UNL or URL under the cursor. Return it for unit testing."""
7840 c = getattr(event, 'c', None)
7841 if not c:
7842 return None
7843 w = getattr(event, 'w', c.frame.body.wrapper)
7844 if not g.app.gui.isTextWrapper(w):
7845 g.internalError('must be a text wrapper', w)
7846 return None
7847 setattr(event, 'widget', w)
7848 # Part 1: get the url.
7849 if url is None:
7850 s = w.getAllText()
7851 ins = w.getInsertPoint()
7852 i, j = w.getSelectionRange()
7853 if i != j:
7854 return None # So find doesn't open the url.
7855 row, col = g.convertPythonIndexToRowCol(s, ins)
7856 i, j = g.getLine(s, ins)
7857 line = s[i:j]
7858 # Find the url on the line.
7859 for match in g.url_regex.finditer(line):
7860 # Don't open if we click after the url.
7861 if match.start() <= col < match.end():
7862 url = match.group()
7863 if g.isValidUrl(url):
7864 break
7865 else:
7866 # Look for the unl:
7867 for match in g.unl_regex.finditer(line):
7868 # Don't open if we click after the unl.
7869 if match.start() <= col < match.end():
7870 unl = match.group()
7871 g.handleUnl(unl, c)
7872 return None
7873 elif not isinstance(url, str):
7874 url = url.toString()
7875 url = g.toUnicode(url)
7876 # Fix #571
7877 if url and g.isValidUrl(url):
7878 # Part 2: handle the url
7879 p = c.p
7880 if not g.doHook("@url1", c=c, p=p, url=url):
7881 g.handleUrl(url, c=c, p=p)
7882 g.doHook("@url2", c=c, p=p)
7883 return url
7884 # Part 3: call find-def.
7885 if not w.hasSelection():
7886 c.editCommands.extendToWord(event, select=True)
7887 word = w.getSelectedText().strip()
7888 if word:
7889 c.findCommands.find_def_strict(event)
7890 return None
7891#@+node:ekr.20170226093349.1: *3* g.unquoteUrl
7892def unquoteUrl(url: str) -> str:
7893 """Replace special characters (especially %20, by their equivalent)."""
7894 return urllib.parse.unquote(url)
7895#@-others
7896# set g when the import is about to complete.
7897g: Any = sys.modules.get('leo.core.leoGlobals')
7898assert g, sorted(sys.modules.keys())
7899if __name__ == '__main__':
7900 unittest.main()
7902#@@language python
7903#@@tabwidth -4
7904#@@pagewidth 70
7905#@-leo