diff --git a/Lib/_pyrepl/commands.py b/Lib/_pyrepl/commands.py index 10127e58897a58..e603dd00d8ba49 100644 --- a/Lib/_pyrepl/commands.py +++ b/Lib/_pyrepl/commands.py @@ -169,7 +169,7 @@ class unix_word_rubout(KillCommand): def do(self) -> None: r = self.reader for i in range(r.get_arg()): - self.kill_range(r.bow(), r.pos) + self.kill_range(r.bow_whitespace(), r.pos) class kill_word(KillCommand): diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index 9ab92f64d1ef63..27838834980478 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -417,6 +417,22 @@ def bow(self, p: int | None = None) -> int: p -= 1 return p + 1 + def bow_whitespace(self, p: int | None = None) -> int: + """Return the 0-based index of the whitespace-delimited word break + preceding p most immediately. + + p defaults to self.pos; only whitespace is considered a word + boundary, matching the behavior of unix-word-rubout in bash/readline.""" + if p is None: + p = self.pos + b = self.buffer + p -= 1 + while p >= 0 and b[p] in (" ", "\n"): + p -= 1 + while p >= 0 and b[p] not in (" ", "\n"): + p -= 1 + return p + 1 + def eow(self, p: int | None = None) -> int: """Return the 0-based index of the word break following p most immediately. diff --git a/Lib/test/test_pyrepl/test_reader.py b/Lib/test/test_pyrepl/test_reader.py index b1b6ae16a1e592..c9d6184135378d 100644 --- a/Lib/test/test_pyrepl/test_reader.py +++ b/Lib/test/test_pyrepl/test_reader.py @@ -558,3 +558,47 @@ def test_control_characters(self): reader, _ = handle_all_events(events) self.assert_screen_equal(reader, 'flag = "🏳️\\u200d🌈"', clean=True) self.assert_screen_equal(reader, 'flag {o}={z} {s}"🏳️\\u200d🌈"{z}'.format(**colors)) + + +class TestBowWhitespace(TestCase): + def test_bow_whitespace_stops_at_whitespace(self): + # GH#146044 + # unix-word-rubout (ctrl-w) should use whitespace boundaries, + # not punctuation boundaries like bow() does + reader = prepare_reader(prepare_console()) + reader.buffer = list("foo.bar baz") + reader.pos = len(reader.buffer) # cursor at end + + # bow_whitespace from end should jump to start of "baz" + result = reader.bow_whitespace() + self.assertEqual(result, 8) # index of 'b' in "baz" + + def test_bow_whitespace_includes_punctuation_in_word(self): + # GH#146044 + reader = prepare_reader(prepare_console()) + reader.buffer = list("foo.bar(baz) qux") + reader.pos = 12 # cursor after ")" + + # bow_whitespace should treat "foo.bar(baz)" as one word + result = reader.bow_whitespace() + self.assertEqual(result, 0) + + def test_bow_stops_at_punctuation(self): + # Verify existing bow() still uses syntax_table (punctuation boundary) + reader = prepare_reader(prepare_console()) + reader.buffer = list("foo.bar baz") + reader.pos = len(reader.buffer) + + result = reader.bow() + self.assertEqual(result, 8) # same — "baz" is all word chars + + def test_bow_vs_bow_whitespace_difference(self): + # The key difference: bow() stops at '.', bow_whitespace() does not + reader = prepare_reader(prepare_console()) + reader.buffer = list("foo.bar") + reader.pos = len(reader.buffer) + + # bow() stops at '.' → returns index of 'b' in "bar" + self.assertEqual(reader.bow(), 4) + # bow_whitespace() treats entire "foo.bar" as one word + self.assertEqual(reader.bow_whitespace(), 0) diff --git a/Lib/test/test_trace.py b/Lib/test/test_trace.py index 19eee19bdea6d5..0f882cae7dc6aa 100644 --- a/Lib/test/test_trace.py +++ b/Lib/test/test_trace.py @@ -590,5 +590,33 @@ def test_no_source_file(self): self.assertIn(f"{filename}({firstlineno + 4})", out[4]) +class TestCoverageResultsInit(unittest.TestCase): + def test_counts_dict_is_copied(self): + # gh-145865: CoverageResults.__init__ should copy the counts dict + # to avoid mutating the caller's dict on update() + from trace import CoverageResults + + counts = {} + cr = CoverageResults(counts=counts) + cr.update(CoverageResults(counts={("file.py", 1): 5})) + self.assertEqual(counts, {}) # original must not be mutated + + def test_calledfuncs_dict_is_copied(self): + from trace import CoverageResults + + calledfuncs = {} + cr = CoverageResults(calledfuncs=calledfuncs) + cr.update(CoverageResults(calledfuncs={("file.py", "mod", "func"): 1})) + self.assertEqual(calledfuncs, {}) + + def test_callers_dict_is_copied(self): + from trace import CoverageResults + + callers = {} + cr = CoverageResults(callers=callers) + cr.update(CoverageResults(callers={(("a.py", "m", "f"), ("b.py", "m", "g")): 1})) + self.assertEqual(callers, {}) + + if __name__ == '__main__': unittest.main() diff --git a/Lib/trace.py b/Lib/trace.py index cd3a6d30661da3..15894921d3e434 100644 --- a/Lib/trace.py +++ b/Lib/trace.py @@ -155,7 +155,7 @@ def __init__(self, counts=None, calledfuncs=None, infile=None, self.counts = counts if self.counts is None: self.counts = {} - self.counter = self.counts.copy() # map (filename, lineno) to count + self.counts = self.counts.copy() # map (filename, lineno) to count self.calledfuncs = calledfuncs if self.calledfuncs is None: self.calledfuncs = {}