jenkins-bot submitted this change.

View Change


Approvals: Xqt: Looks good to me, approved jenkins-bot: Verified
[IMPR] Improvements for editor

- Redefine config.editor setting:
No longer call os.environment here.
config.editor may be str, bool or None.
str: use this as editor command
True: use Tkinter
False: use 'break' or 'true' as command which does nothing
(mostly used for tests purpose)
None: Use EDITOR environment value if present or on Windows detect
editor from registry or fially ue Tkinter
- Move editor detection functions from config.py to editor.py
- copy EXTERNAL EDITOR SETTINGS section in generate_user_files.py
- use pathlib.Path in TextEditor.edit()

Change-Id: I84b097c09ee2a4a582582878e08afb3cbb086264
---
M pywikibot/editor.py
M pywikibot/scripts/generate_user_files.py
M pywikibot/config.py
3 files changed, 151 insertions(+), 104 deletions(-)

diff --git a/pywikibot/config.py b/pywikibot/config.py
index 64cc696..1746a02 100644
--- a/pywikibot/config.py
+++ b/pywikibot/config.py
@@ -29,6 +29,9 @@

.. versionchanged:: 6.2
config2 was renamed to config
+.. versionchanged:: 8.0
+ Editor settings has been revised. *editor* variable is None by
+ default. Editor detection functions were moved to :mod:`editor`.
"""
#
# (C) Pywikibot team, 2003-2022
@@ -71,8 +74,6 @@

OSWIN32 = (sys.platform == 'win32')

-if OSWIN32:
- import winreg

# This frozen set should contain all imported modules/variables, so it must
# occur directly after the imports. At that point globals() only contains the
@@ -568,9 +569,14 @@
tkvertsize = 800

# ############# EXTERNAL EDITOR SETTINGS ##############
-# The command for the editor you want to use. If set to None, a simple Tkinter
-# editor will be used.
-editor = os.environ.get('EDITOR')
+# The command for the editor you want to use. If set to True, Tkinter
+# editor will be used. If set to False, no editor will be used. In
+# script tests to be a noop (like /bin/true) so the script continues.
+# If set to None, the EDITOR environment variable will be used as
+# command. If EDITOR is not set, on windows plattforms it tries to
+# determine the default text editor from registry. Finally, Tkinter is
+# used as fallback.
+editor: Union[bool, str, None] = None

# Warning: DO NOT use an editor which doesn't support Unicode to edit pages!
# You will BREAK non-ASCII symbols!
@@ -925,51 +931,6 @@
return path


-def _win32_extension_command(extension: str) -> Optional[str]:
- """Get the command from the Win32 registry for an extension."""
- fileexts_key = \
- r'Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts'
- key_name = fileexts_key + r'\.' + extension + r'\OpenWithProgids'
- try:
- key1 = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_name)
- _prog_id = winreg.EnumValue(key1, 0)[0]
- _key2 = winreg.OpenKey(winreg.HKEY_CLASSES_ROOT,
- fr'{_prog_id}\shell\open\command')
- _cmd = winreg.QueryValueEx(_key2, '')[0]
- # See T102465 for issues relating to using this value.
- cmd = _cmd
- if cmd.find('%1'):
- cmd = cmd[:cmd.find('%1')]
- # Remove any trailing character, which should be a quote or space
- # and then remove all whitespace.
- return cmd[:-1].strip()
- except OSError as e:
- # Catch any key lookup errors
- output('Unable to detect program for file extension "{}": {!r}'
- .format(extension, e))
- return None
-
-
-def _detect_win32_editor() -> Optional[str]:
- """Detect the best Win32 editor."""
- # Notepad is even worse than our Tkinter editor.
- unusable_exes = ['notepad.exe',
- 'py.exe',
- 'pyw.exe',
- 'python.exe',
- 'pythonw.exe']
-
- for ext in ['py', 'txt']:
- editor = _win32_extension_command(ext)
- if editor:
- for unusable in unusable_exes:
- if unusable in editor.lower():
- break
- else:
- return editor
- return None
-
-
# System-level and User-level changes.
# Store current variables and their types.
_public_globals = {
@@ -1107,18 +1068,6 @@
if console_encoding is None:
console_encoding = 'utf-8'

-if OSWIN32 and editor is None:
- editor = _detect_win32_editor()
-
-# single character string literals from
-# https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals
-# encode('unicode-escape') also changes Unicode characters
-if OSWIN32 and editor and set(editor) & set('\a\b\f\n\r\t\v'):
- warning(
- 'The editor path contains probably invalid escaped characters. Make '
- 'sure to use a raw-string (r"..." or r\'...\'), forward slashes as a '
- 'path delimiter or to escape the normal path delimiter.')
-
if userinterface_lang is None:
userinterface_lang = os.getenv('PYWIKIBOT_USERINTERFACE_LANG') \
or getlocale()[0]
diff --git a/pywikibot/editor.py b/pywikibot/editor.py
index e29b2de..29e4829 100644
--- a/pywikibot/editor.py
+++ b/pywikibot/editor.py
@@ -4,10 +4,10 @@
#
# Distributed under the terms of the MIT license.
#
-import codecs
import os
import subprocess
import tempfile
+from pathlib import Path
from sys import platform
from textwrap import fill
from typing import Optional
@@ -24,14 +24,36 @@
GUI_ERROR = e


+OSWIN32 = platform == 'win32'
+if OSWIN32:
+ import winreg
+
+
class TextEditor:

- """Text editor."""
+ """Text editor.

- @staticmethod
- def _command(file_name: str, text: str,
+ .. versionchanged:: 8.0
+ Editor detection functions were moved from :mod:`config`.
+ """
+
+ def __init__(self):
+ """Setup external Editor."""
+ self.editor: str
+ if config.editor is True:
+ self.editor = ''
+ elif config.editor is False:
+ self.editor = 'break' if OSWIN32 else 'true'
+ elif config.editor is None:
+ self.editor = os.environ.get('EDITOR', '')
+ if OSWIN32 and not self.editor:
+ self.editor = self._detect_win32_editor()
+ else:
+ self.editor = config.editor
+
+ def _command(self, file_name: str, text: str,
jump_index: Optional[int] = None) -> List[str]:
- """Return editor selected in user config file (user-config.py)."""
+ """Return command of editor selected in user config file."""
if jump_index:
# Some editors make it possible to mark occurrences of substrings,
# or to jump to the line of the first occurrence.
@@ -41,28 +63,28 @@
column = jump_index - (text[:jump_index].rfind('\n') + 1)
else:
line = column = 0
+
# Linux editors. We use startswith() because some users might use
# parameters.
- assert config.editor is not None
- if config.editor.startswith('kate'):
+ if self.editor.startswith('kate'):
command = ['-l', str(line + 1), '-c', str(column + 1)]
- elif config.editor.startswith(('gedit', 'emacs')):
+ elif self.editor.startswith(('gedit', 'emacs')):
command = [f'+{line + 1}'] # columns seem unsupported
- elif config.editor.startswith('jedit'):
+ elif self.editor.startswith('jedit'):
command = [f'+line:{line + 1}'] # columns seem unsupported
- elif config.editor.startswith('vim'):
+ elif self.editor.startswith('vim'):
command = [f'+{line + 1}'] # columns seem unsupported
- elif config.editor.startswith('nano'):
+ elif self.editor.startswith('nano'):
command = [f'+{line + 1},{column + 1}']
# Windows editors
- elif config.editor.lower().endswith('notepad++.exe'):
+ elif self.editor.lower().endswith('notepad++.exe'):
command = [f'-n{line + 1}'] # seems not to support columns
else:
command = []

- # See T102465 for problems relating to using config.editor unparsed.
- command = [config.editor] + command + [file_name]
- pywikibot.log(f'Running editor: {TextEditor._concat(command)}')
+ # See T102465 for problems relating to using self.editor unparsed.
+ command = [self.editor] + command + [file_name]
+ pywikibot.log(f'Running editor: {self._concat(command)}')
return command

@staticmethod
@@ -83,39 +105,93 @@
:return: the modified text, or None if the user didn't save the text
file in his text editor
"""
- if config.editor:
- handle, tempFilename = tempfile.mkstemp()
- tempFilename = '{}.{}'.format(tempFilename,
- config.editor_filename_extension)
- try:
- with codecs.open(tempFilename, 'w',
- encoding=config.editor_encoding) as tempFile:
- tempFile.write(text)
- creationDate = os.stat(tempFilename).st_mtime
- cmd = self._command(tempFilename, text, jumpIndex)
- subprocess.run(cmd, shell=platform == 'win32', check=True)
- lastChangeDate = os.stat(tempFilename).st_mtime
- if lastChangeDate == creationDate:
- # Nothing changed
- return None
+ if self.editor:
+ handle, filename = tempfile.mkstemp(
+ suffix=f'.{config.editor_filename_extension}', text=True)
+ path = Path(filename)

- with codecs.open(tempFilename, 'r',
- encoding=config.editor_encoding) as temp_file:
- newcontent = temp_file.read()
- return newcontent
+ try:
+ encoding = config.editor_encoding
+ path.write_text(text, encoding=encoding)
+
+ creation_date = path.stat().st_mtime
+ cmd = self._command(filename, text, jumpIndex)
+ subprocess.run(cmd, shell=platform == 'win32', check=True)
+ last_change_date = path.stat().st_mtime
+
+ if last_change_date == creation_date:
+ return None # Nothing changed
+
+ return path.read_text(encoding=encoding)
+
finally:
os.close(handle)
- os.unlink(tempFilename)
+ os.unlink(path)

if GUI_ERROR:
raise ImportError(fill(
- 'Could not load GUI modules: {}. No editor available. '
- 'Set your favourite editor in user-config.py "editor", '
- 'or install python packages tkinter and idlelib, which '
- 'are typically part of Python but may be packaged separately '
- 'on your platform.'.format(GUI_ERROR)) + '\n')
+ f'Could not load GUI modules: {GUI_ERROR}. No editor'
+ ' available. Set your favourite editor in user-config.py'
+ ' "editor", or install python packages tkinter and idlelib,'
+ ' which are typically part of Python but may be packaged'
+ ' separately on your platform.') + '\n')

assert pywikibot.ui is not None
-
return pywikibot.ui.editText(text, jumpIndex=jumpIndex,
highlight=highlight)
+
+ @staticmethod
+ def _win32_extension_command(extension: str) -> Optional[str]:
+ """Get the command from the Win32 registry for an extension."""
+ fileexts_key = \
+ r'Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts'
+ key_name = fr'{fileexts_key}\.{extension}\OpenWithProgids'
+ try:
+ key1 = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_name)
+ _prog_id = winreg.EnumValue(key1, 0)[0]
+ _key2 = winreg.OpenKey(winreg.HKEY_CLASSES_ROOT,
+ fr'{_prog_id}\shell\open\command')
+ _cmd = winreg.QueryValueEx(_key2, '')[0]
+ # See T102465 for issues relating to using this value.
+ cmd = _cmd
+ if cmd.find('%1'):
+ cmd = cmd[:cmd.find('%1')]
+ # Remove any trailing character, which should be a quote or
+ # space and then remove all whitespace.
+ return cmd[:-1].strip()
+ except OSError as e:
+ # Catch any key lookup errors
+ pywikibot.info(f'Unable to detect program for file extension '
+ f'{extension!r}: {e!r}')
+ return None
+
+ @staticmethod
+ def _detect_win32_editor() -> str:
+ """Detect the best Win32 editor."""
+ # Notepad is even worse than our Tkinter editor.
+ unusable_exes = ['notepad.exe',
+ 'py.exe',
+ 'pyw.exe',
+ 'python.exe',
+ 'pythonw.exe']
+
+ for ext in ['py', 'txt']:
+ editor = TextEditor._win32_extension_command(ext)
+ if editor:
+ for unusable in unusable_exes:
+ if unusable in editor.lower():
+ break
+ else:
+ if set(editor) & set('\a\b\f\n\r\t\v'):
+ # single character string literals from
+ # https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals
+ # encode('unicode-escape') also changes Unicode
+ # characters
+ pywikibot.warning(fill(
+ 'The editor path contains probably invalid '
+ 'escaped characters. Make sure to use a '
+ 'raw-string (r"..." or r\'...\'), forward slashes '
+ 'as a path delimiter or to escape the normal path '
+ 'delimiter.'))
+ return editor
+ return ''
diff --git a/pywikibot/scripts/generate_user_files.py b/pywikibot/scripts/generate_user_files.py
index 101a066..ab2005c 100755
--- a/pywikibot/scripts/generate_user_files.py
+++ b/pywikibot/scripts/generate_user_files.py
@@ -5,6 +5,7 @@
moved to pywikibot.scripts folder.
.. versionchanged:: 8.0
let user the choice which section to be copied.
+...Also EXTERNAL EDITOR SETTINGS section can be copied.
"""
#
# (C) Pywikibot team, 2010-2022
@@ -30,7 +31,6 @@
# DISABLED_SECTIONS cannot be copied; variables must be set manually
DISABLED_SECTIONS = {
'USER INTERFACE SETTINGS', # uses sys
- 'EXTERNAL EDITOR SETTINGS', # uses os
}
OBSOLETE_SECTIONS = {
'ACCOUNT SETTINGS', # already set

To view, visit change 857001. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: I84b097c09ee2a4a582582878e08afb3cbb086264
Gerrit-Change-Number: 857001
Gerrit-PatchSet: 8
Gerrit-Owner: Xqt <info@gno.de>
Gerrit-Reviewer: Xqt <info@gno.de>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged