Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Doc/library/readline.rst
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,9 @@ with a custom completer, a different set of word delimiters should be set.
Get the beginning or ending index of the completion scope.
These indexes are the *start* and *end* arguments passed to the
:c:data:`rl_attempted_completion_function` callback of the
underlying library.
underlying library. The values may be different in the same
input editing scenario based on the underlying C readline implemtation.
Ex: libedit is known to behave differently than libreadline.


.. function:: set_completer_delims(string)
Expand Down
21 changes: 19 additions & 2 deletions Lib/test/test_readline.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,15 @@ def test_write_read_append(self):

# test 'no such file' behaviour
os.unlink(hfilename)
with self.assertRaises(FileNotFoundError):
try:
readline.append_history_file(1, hfilename)
except FileNotFoundError:
pass # Some implementations return this error (libreadline).
else:
os.unlink(hfilename) # Some create it anyways (libedit).
# If the file wasn't created, unlink will fail.
# We're just testing that one of the two expected behaviors happens
# instead of an incorrect error.

# write_history_file can create the target
readline.write_history_file(hfilename)
Expand Down Expand Up @@ -228,7 +235,17 @@ def display(substitution, matches, longest_match_length):
output = run_pty(script, input)
self.assertIn(b"text 't\\xeb'\r\n", output)
self.assertIn(b"line '[\\xefnserted]|t\\xeb[after]'\r\n", output)
self.assertIn(b"indexes 11 13\r\n", output)
if sys.platform == "darwin" or not is_editline:
self.assertIn(b"indexes 11 13\r\n", output)
# Non-macOS libedit does not handle non-ASCII bytes
# the same way and generates character indices
# rather than byte indices via get_begidx() and
# get_endidx(). Ex: libedit2 3.1-20191231-2 on Debian
# winds up with "indexes 10 12". Stemming from the
# start and end values calls back into readline.c's
# rl_attempted_completion_function = flex_complete with:
# (11, 13) instead of libreadline's (12, 15).

if not is_editline and hasattr(readline, "set_pre_input_hook"):
self.assertIn(b"substitution 't\\xeb'\r\n", output)
self.assertIn(b"matches ['t\\xebnt', 't\\xebxt']\r\n", output)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
The readline module now passes its tests when built directly against
libedit. Existing irreconcilable API differences remain in
:func:`readline.get_begidx` and :func:`readline.get_endidx` behavior based
on libreadline vs libedit use.
10 changes: 6 additions & 4 deletions Modules/clinic/readline.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 30 additions & 8 deletions Modules/readline.c
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ extern char **completion_matches(char *, CPFunction *);
static int using_libedit_emulation = 0;
static const char libedit_version_tag[] = "EditLine wrapper";

static int libedit_history_start = 0;
static int8_t libedit_history_start = 0;
static int8_t libedit_append_replace_history_offset = 0;

#ifdef HAVE_RL_COMPLETION_DISPLAY_MATCHES_HOOK
static void
Expand Down Expand Up @@ -320,7 +321,8 @@ readline_append_history_file_impl(PyObject *module, int nelements,
filename_bytes = NULL;
filename = NULL;
}
errno = err = append_history(nelements, filename);
errno = err = append_history(
nelements - libedit_append_replace_history_offset, filename);
if (!err && _history_length >= 0)
history_truncate_file(filename, _history_length);
Py_XDECREF(filename_bytes);
Expand Down Expand Up @@ -592,12 +594,12 @@ readline.remove_history_item
pos as entry_number: int
/

Remove history item given by its position.
Remove history item given by its zero-based position.
[clinic start generated code]*/

static PyObject *
readline_remove_history_item_impl(PyObject *module, int entry_number)
/*[clinic end generated code: output=ab114f029208c7e8 input=c8520ac3da50224e]*/
/*[clinic end generated code: output=ab114f029208c7e8 input=f248beb720ff1838]*/
{
HIST_ENTRY *entry;

Expand Down Expand Up @@ -626,12 +628,14 @@ readline.replace_history_item
/

Replaces history item given by its position with contents of line.

pos is zero-based.
[clinic start generated code]*/

static PyObject *
readline_replace_history_item_impl(PyObject *module, int entry_number,
PyObject *line)
/*[clinic end generated code: output=f8cec2770ca125eb input=b7ccef0780ae041b]*/
/*[clinic end generated code: output=f8cec2770ca125eb input=368bb66fe5ee5222]*/
{
PyObject *encoded;
HIST_ENTRY *old_entry;
Expand All @@ -645,7 +649,9 @@ readline_replace_history_item_impl(PyObject *module, int entry_number,
if (encoded == NULL) {
return NULL;
}
old_entry = replace_history_entry(entry_number, PyBytes_AS_STRING(encoded), (void *)NULL);
old_entry = replace_history_entry(
entry_number + libedit_append_replace_history_offset,
PyBytes_AS_STRING(encoded), (void *)NULL);
Py_DECREF(encoded);
if (!old_entry) {
PyErr_Format(PyExc_ValueError,
Expand Down Expand Up @@ -786,12 +792,12 @@ readline.get_history_item
index as idx: int
/

Return the current contents of history item at index.
Return the current contents of history item at one-based index.
[clinic start generated code]*/

static PyObject *
readline_get_history_item_impl(PyObject *module, int idx)
/*[clinic end generated code: output=83d3e53ea5f34b3d input=63fff0c3c4323269]*/
/*[clinic end generated code: output=83d3e53ea5f34b3d input=8adf5c80e6c7ff2b]*/
{
HIST_ENTRY *hist_ent;

Expand Down Expand Up @@ -1191,6 +1197,22 @@ setup_readline(readlinestate *mod_state)
} else {
libedit_history_start = 1;
}
/* Some libedit implementations use 1 based indexing on
* replace_history_entry where libreadline uses 0 based.
* The API our module presents is supposed to be 0 based.
* It's a mad mad mad mad world.
*/
{
add_history("2");
HIST_ENTRY *old_entry = replace_history_entry(1, "X", NULL);
_py_free_history_entry(old_entry);
HIST_ENTRY *item = history_get(libedit_history_start);
if (item && item->line && strcmp(item->line, "X")) {
libedit_append_replace_history_offset = 0;
} else {
libedit_append_replace_history_offset = 1;
}
}
clear_history();

using_history();
Expand Down