jenkins-bot has submitted this change. ( 
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/857001 )

Change subject: [IMPR] Improvements for editor
......................................................................

[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(-)

Approvals:
  Xqt: Looks good to me, approved
  jenkins-bot: Verified




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 https://gerrit.wikimedia.org/r/c/pywikibot/core/+/857001
To unsubscribe, or for help writing mail filters, visit 
https://gerrit.wikimedia.org/r/settings

Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: I84b097c09ee2a4a582582878e08afb3cbb086264
Gerrit-Change-Number: 857001
Gerrit-PatchSet: 8
Gerrit-Owner: Xqt <[email protected]>
Gerrit-Reviewer: Xqt <[email protected]>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged
_______________________________________________
Pywikibot-commits mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to