Repository: zalando/patroni

Issue:
patronictl edit-config fails under Windows
The python library `cdiff` (or as it's now known, `ydiff`) is hardwired to use `less` in version <1.2.0
Regular Windows installs only have `more` and that is not enough.

When running `edit-config` in quiet mode, no diffs are generated, thus this option is currently the only one that works on Windows.

When running in regular mode `FileNotFoundError` raised:
```
...
  File "C:\Program Files\Python38\lib\subprocess.py", line 854, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "C:\Program Files\Python38\lib\subprocess.py", line 1307, in _execute_child
    hp, ht, pid, tid = _winapi.CreateProcess(executable, args,
  File "c:\Users\pasha\.vscode\extensions\ms-python.python-2020.8.108011\pythonFiles\lib\python\debugpy\_vendored\pydevd\_pydev_bundle\pydev_monkey.py", line 632, in new_CreateProcess
    return getattr(_subprocess, original_name)(app_name, cmd_line, *args)
FileNotFoundError: [WinError 2] The system cannot find the file specified
```


Discussion:

Resolving patch:
diff --git a/patroni/ctl.py b/patroni/ctl.py
--- a/patroni/ctl.py
+++ b/patroni/ctl.py
@@ -7,7 +7,6 @@
 import datetime
 import dateutil.parser
 import dateutil.tz
-import cdiff
 import copy
 import difflib
 import io
@@ -34,6 +33,10 @@
 from patroni.version import __version__
 from prettytable import ALL, FRAME, PrettyTable
 from six.moves.urllib_parse import urlparse
+try:
+    from ydiff import markup_to_pager, PatchStream
+except ImportError:  # pragma: no cover
+    from cdiff import markup_to_pager, PatchStream
 
 CONFIG_DIR_PATH = click.get_app_dir('patroni')
 CONFIG_FILE_PATH = os.path.join(CONFIG_DIR_PATH, 'patronictl.yaml')
@@ -1086,7 +1089,14 @@ class opts:
             side_by_side = False
             width = 80
             tab_width = 8
-        cdiff.markup_to_pager(cdiff.PatchStream(buf), opts)
+            wrap = True
+            if find_executable('less'):
+                pager = None
+            else:
+                pager = 'more.com' if sys.platform == 'win32' else 'more'
+            pager_options = None
+
+        markup_to_pager(PatchStream(buf), opts)
     else:
         for line in unified_diff:
             click.echo(line.rstrip('\n'))


Test patch:
diff --git a/tests/test_ctl.py b/tests/test_ctl.py
--- a/tests/test_ctl.py
+++ b/tests/test_ctl.py
@@ -561,7 +561,7 @@ def test_apply_config_changes(self):
         self.assertRaises(PatroniCtlException, apply_config_changes, before_editing, config, ['a'])
 
     @patch('sys.stdout.isatty', return_value=False)
-    @patch('cdiff.markup_to_pager')
+    @patch('patroni.ctl.markup_to_pager')
     def test_show_diff(self, mock_markup_to_pager, mock_isatty):
         show_diff("foo:\n  bar: 1\n", "foo:\n  bar: 2\n")
         mock_markup_to_pager.assert_not_called()
@@ -570,6 +570,9 @@ def test_show_diff(self, mock_markup_to_pager, mock_isatty):
         show_diff("foo:\n  bar: 1\n", "foo:\n  bar: 2\n")
         mock_markup_to_pager.assert_called_once()
 
+        with patch('patroni.ctl.find_executable', Mock(return_value=None)):
+            show_diff("foo:\n  bar: 1\n", "foo:\n  bar: 2\n")
+
         # Test that unicode handling doesn't fail with an exception
         show_diff(b"foo:\n  bar: \xc3\xb6\xc3\xb6\n".decode('utf-8'),
                   b"foo:\n  bar: \xc3\xbc\xc3\xbc\n".decode('utf-8'))