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