Skip to content

Commit 602b6a4

Browse files
committed
Merge in the main branch
2 parents 1e90d89 + 234c12c commit 602b6a4

30 files changed

Lines changed: 673 additions & 213 deletions

Doc/library/pickletools.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ Command-line options
7979

8080
A pickle file to read, or ``-`` to indicate reading from standard input.
8181

82+
.. versionadded:: next
83+
Output is in color by default and can be
84+
:ref:`controlled using environment variables <using-on-controlling-color>`.
8285

8386

8487
Programmatic interface

Doc/whatsnew/3.15.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,13 +1046,29 @@ os.path
10461046
(Contributed by Petr Viktorin for :cve:`2025-4517`.)
10471047

10481048

1049+
pdb
1050+
---
1051+
1052+
* Use the new interactive shell as the default input shell for :mod:`pdb`.
1053+
(Contributed by Tian Gao in :gh:`145379`.)
1054+
1055+
10491056
pickle
10501057
------
10511058

10521059
* Add support for pickling private methods and nested classes.
10531060
(Contributed by Zackery Spytz and Serhiy Storchaka in :gh:`77188`.)
10541061

10551062

1063+
pickletools
1064+
-----------
1065+
1066+
* The output of the :mod:`pickletools` command-line interface is colored by
1067+
default. This can be controlled with
1068+
:ref:`environment variables <using-on-controlling-color>`.
1069+
(Contributed by Hugo van Kemenade in :gh:`149026`.)
1070+
1071+
10561072
pprint
10571073
------
10581074

Lib/_colorize.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,23 @@ class LiveProfiler(ThemeSection):
359359
)
360360

361361

362+
@dataclass(frozen=True, kw_only=True)
363+
class Pickletools(ThemeSection):
364+
annotation: str = ANSIColors.GREY
365+
arg_number: str = ANSIColors.YELLOW
366+
arg_string: str = ANSIColors.GREEN
367+
mark: str = ANSIColors.GREY
368+
op_call: str = ANSIColors.GREEN
369+
op_container: str = ANSIColors.INTENSE_BLUE
370+
op_memo: str = ANSIColors.MAGENTA
371+
op_meta: str = ANSIColors.GREY
372+
op_stack: str = ANSIColors.BOLD_RED
373+
opcode_code: str = ANSIColors.CYAN
374+
position: str = ANSIColors.GREY
375+
proto: str = ANSIColors.YELLOW
376+
reset: str = ANSIColors.RESET
377+
378+
362379
@dataclass(frozen=True, kw_only=True)
363380
class Syntax(ThemeSection):
364381
prompt: str = ANSIColors.BOLD_MAGENTA
@@ -429,6 +446,7 @@ class Theme:
429446
fancycompleter: FancyCompleter = field(default_factory=FancyCompleter)
430447
http_server: HttpServer = field(default_factory=HttpServer)
431448
live_profiler: LiveProfiler = field(default_factory=LiveProfiler)
449+
pickletools: Pickletools = field(default_factory=Pickletools)
432450
syntax: Syntax = field(default_factory=Syntax)
433451
timeit: Timeit = field(default_factory=Timeit)
434452
tokenize: Tokenize = field(default_factory=Tokenize)
@@ -444,6 +462,7 @@ def copy_with(
444462
fancycompleter: FancyCompleter | None = None,
445463
http_server: HttpServer | None = None,
446464
live_profiler: LiveProfiler | None = None,
465+
pickletools: Pickletools | None = None,
447466
syntax: Syntax | None = None,
448467
timeit: Timeit | None = None,
449468
tokenize: Tokenize | None = None,
@@ -462,6 +481,7 @@ def copy_with(
462481
fancycompleter=fancycompleter or self.fancycompleter,
463482
http_server=http_server or self.http_server,
464483
live_profiler=live_profiler or self.live_profiler,
484+
pickletools=pickletools or self.pickletools,
465485
syntax=syntax or self.syntax,
466486
timeit=timeit or self.timeit,
467487
tokenize=tokenize or self.tokenize,
@@ -484,6 +504,7 @@ def no_colors(cls) -> Self:
484504
fancycompleter=FancyCompleter.no_colors(),
485505
http_server=HttpServer.no_colors(),
486506
live_profiler=LiveProfiler.no_colors(),
507+
pickletools=Pickletools.no_colors(),
487508
syntax=Syntax.no_colors(),
488509
timeit=Timeit.no_colors(),
489510
tokenize=Tokenize.no_colors(),

Lib/dataclasses.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -178,17 +178,12 @@ def __repr__(self):
178178
return '<factory>'
179179
_HAS_DEFAULT_FACTORY = _HAS_DEFAULT_FACTORY_CLASS()
180180

181-
# A sentinel object to detect if a parameter is supplied or not. Use
182-
# a class to give it a better repr.
183-
class _MISSING_TYPE:
184-
pass
185-
MISSING = _MISSING_TYPE()
181+
# A sentinel object to detect if a parameter is supplied or not.
182+
MISSING = sentinel("MISSING")
186183

187184
# A sentinel object to indicate that following fields are keyword-only by
188-
# default. Use a class to give it a better repr.
189-
class _KW_ONLY_TYPE:
190-
pass
191-
KW_ONLY = _KW_ONLY_TYPE()
185+
# default.
186+
KW_ONLY = sentinel("KW_ONLY")
192187

193188
# Since most per-field metadata will be unused, create an empty
194189
# read-only dictionary that can be shared among all fields.

Lib/ensurepip/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111

1212
__all__ = ["version", "bootstrap"]
13-
_PIP_VERSION = "26.0.1"
13+
_PIP_VERSION = "26.1"
1414

1515
# Directory of system wheel packages. Some Linux distribution packaging
1616
# policies recommend against bundling dependencies. For example, Fedora
1.7 MB
Binary file not shown.

Lib/http/cookiejar.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,10 +1032,13 @@ def set_ok_domain(self, cookie, request):
10321032
if j == 0: # domain like .foo.bar
10331033
tld = domain[i+1:]
10341034
sld = domain[j+1:i]
1035-
if sld.lower() in ("co", "ac", "com", "edu", "org", "net",
1036-
"gov", "mil", "int", "aero", "biz", "cat", "coop",
1037-
"info", "jobs", "mobi", "museum", "name", "pro",
1038-
"travel", "eu") and len(tld) == 2:
1035+
known_slds = (
1036+
"co", "ac", "com", "edu", "org", "net",
1037+
"gov", "mil", "int", "aero", "biz", "cat", "coop",
1038+
"info", "jobs", "mobi", "museum", "name", "pro",
1039+
"travel", "eu", "tv", "or", "nom", "sch", "web",
1040+
)
1041+
if sld.lower() in known_slds and len(tld) == 2:
10391042
# domain like .co.uk
10401043
_debug(" country-code second level domain %s", domain)
10411044
return False

Lib/pdb.py

Lines changed: 162 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -318,12 +318,34 @@ def namespace(self):
318318

319319

320320
class _PdbInteractiveConsole(code.InteractiveConsole):
321-
def __init__(self, ns, message):
321+
def __init__(self, ns=None, message=None):
322322
self._message = message
323323
super().__init__(locals=ns, local_exit=True)
324324

325325
def write(self, data):
326-
self._message(data, end='')
326+
if self._message is not None:
327+
self._message(data, end='')
328+
else:
329+
super().write(data)
330+
331+
def more_lines(self, text):
332+
# Generic Python multi-line completeness heuristic.
333+
# Strips pyrepl's trailing auto-indent before compiling.
334+
# This should be functionally identical to simple_interact._more_lines
335+
src = text.rstrip(" \t")
336+
n = len(src)
337+
if n > 0 and text[n-1] == '\n':
338+
text = src
339+
try:
340+
code_obj = self.compile(text, "<stdin>", "single")
341+
except (OverflowError, SyntaxError, ValueError):
342+
lines = text.splitlines(keepends=True)
343+
if len(lines) == 1:
344+
return False
345+
last = lines[-1]
346+
return ((last.startswith((" ", "\t")) or last.strip() != "")
347+
and not last.endswith("\n"))
348+
return code_obj is None
327349

328350

329351
# Interaction prompt line will separate file and call info from code
@@ -352,6 +374,96 @@ def get_default_backend():
352374
return _default_backend
353375

354376

377+
def _pyrepl_available():
378+
"""return whether pdb should use _pyrepl for input"""
379+
if not os.getenv("PYTHON_BASIC_REPL"):
380+
CAN_USE_PYREPL = False
381+
else:
382+
try:
383+
from _pyrepl.main import CAN_USE_PYREPL
384+
except ModuleNotFoundError:
385+
CAN_USE_PYREPL = False
386+
return CAN_USE_PYREPL
387+
388+
389+
class PdbPyReplInput:
390+
def __init__(self, pdb_instance, stdin, stdout, prompt):
391+
import _pyrepl.readline
392+
393+
self.pdb_instance = pdb_instance
394+
self.prompt = prompt
395+
self.console = _PdbInteractiveConsole()
396+
if not (os.isatty(stdin.fileno())):
397+
raise ValueError("stdin is not a TTY")
398+
self.readline_wrapper = _pyrepl.readline._ReadlineWrapper(
399+
f_in=stdin.fileno(),
400+
f_out=stdout.fileno(),
401+
config=_pyrepl.readline.ReadlineConfig(
402+
completer_delims=frozenset(' \t\n`@#%^&*()=+[{]}\\|;:\'",<>?')
403+
)
404+
)
405+
406+
def readline(self):
407+
408+
def more_lines(text):
409+
if text.strip() == "\x1a":
410+
# Ctrl + Z raises EOFError to quit pdb
411+
# This is similarly handled in simple_interact.py
412+
raise EOFError
413+
cmd, _, line = self.pdb_instance.parseline(text)
414+
if not line or not cmd:
415+
return False
416+
func = getattr(self.pdb_instance, 'do_' + cmd, None)
417+
if func is not None:
418+
return False
419+
return self.console.more_lines(text)
420+
421+
try:
422+
pyrepl_completer = self.readline_wrapper.get_completer()
423+
self.readline_wrapper.set_completer(self.complete)
424+
multiline = (
425+
self.readline_wrapper.multiline_input(
426+
more_lines,
427+
self.prompt,
428+
'... ' + ' ' * (len(self.prompt) - 4)
429+
) + '\n'
430+
)
431+
return multiline
432+
except EOFError:
433+
return 'EOF'
434+
finally:
435+
self.readline_wrapper.set_completer(pyrepl_completer)
436+
437+
def complete(self, text, state):
438+
"""
439+
This function is very similar to cmd.Cmd.complete.
440+
However, cmd.Cmd.complete assumes that we use readline module, but
441+
pyrepl does not use it.
442+
"""
443+
if state == 0:
444+
origline = self.readline_wrapper.get_line_buffer()
445+
line = origline.lstrip()
446+
stripped = len(origline) - len(line)
447+
begidx = self.readline_wrapper.get_begidx() - stripped
448+
endidx = self.readline_wrapper.get_endidx() - stripped
449+
if begidx > 0:
450+
cmd, args, foo = self.pdb_instance.parseline(line)
451+
if not cmd:
452+
compfunc = self.pdb_instance.completedefault
453+
else:
454+
try:
455+
compfunc = getattr(self.pdb_instance, 'complete_' + cmd)
456+
except AttributeError:
457+
compfunc = self.pdb_instance.completedefault
458+
else:
459+
compfunc = self.pdb_instance.completenames
460+
self.completion_matches = compfunc(text, line, begidx, endidx)
461+
try:
462+
return self.completion_matches[state]
463+
except IndexError:
464+
return None
465+
466+
355467
class Pdb(bdb.Bdb, cmd.Cmd):
356468
_previous_sigint_handler = None
357469

@@ -386,6 +498,12 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None,
386498
except ImportError:
387499
pass
388500

501+
self.pyrepl_input = None
502+
if _pyrepl_available():
503+
try:
504+
self.pyrepl_input = PdbPyReplInput(self, self.stdin, self.stdout, self.prompt)
505+
except Exception:
506+
pass
389507
self.allow_kbdint = False
390508
self.nosigint = nosigint
391509
# Consider these characters as part of the command so when the users type
@@ -624,14 +742,40 @@ def user_exception(self, frame, exc_info):
624742
self.message('%s%s' % (prefix, self._format_exc(exc_value)))
625743
self.interaction(frame, exc_traceback)
626744

745+
@contextmanager
746+
def _replace_attribute(self, attrs):
747+
original_attrs = {}
748+
for attr, value in attrs.items():
749+
original_attrs[attr] = getattr(self, attr)
750+
setattr(self, attr, value)
751+
try:
752+
yield
753+
finally:
754+
for attr, value in original_attrs.items():
755+
setattr(self, attr, value)
756+
757+
@contextmanager
758+
def _maybe_use_pyrepl_as_stdin(self):
759+
if self.pyrepl_input is None:
760+
yield
761+
return
762+
763+
with self._replace_attribute({
764+
'stdin': self.pyrepl_input,
765+
'use_rawinput': False,
766+
'prompt': '',
767+
}):
768+
yield
769+
627770
# General interaction function
628771
def _cmdloop(self):
629772
while True:
630773
try:
631774
# keyboard interrupts allow for an easy way to cancel
632775
# the current command, so allow them during interactive input
633776
self.allow_kbdint = True
634-
self.cmdloop()
777+
with self._maybe_use_pyrepl_as_stdin():
778+
self.cmdloop()
635779
self.allow_kbdint = False
636780
break
637781
except KeyboardInterrupt:
@@ -2364,10 +2508,21 @@ def do_interact(self, arg):
23642508
contains all the (global and local) names found in the current scope.
23652509
"""
23662510
ns = {**self.curframe.f_globals, **self.curframe.f_locals}
2367-
with self._enable_rlcompleter(ns):
2368-
console = _PdbInteractiveConsole(ns, message=self.message)
2369-
console.interact(banner="*pdb interact start*",
2370-
exitmsg="*exit from pdb interact command*")
2511+
console = _PdbInteractiveConsole(ns, message=self.message)
2512+
banner = "*pdb interact start*"
2513+
exitmsg = "*exit from pdb interact command*"
2514+
if self.pyrepl_input is not None:
2515+
from _pyrepl.simple_interact import run_multiline_interactive_console
2516+
self.message(banner)
2517+
try:
2518+
run_multiline_interactive_console(console)
2519+
except SystemExit:
2520+
pass
2521+
self.message(exitmsg)
2522+
else:
2523+
with self._enable_rlcompleter(ns):
2524+
console.interact(banner=banner,
2525+
exitmsg=exitmsg)
23712526

23722527
def do_alias(self, arg):
23732528
"""alias [name [command]]

0 commit comments

Comments
 (0)