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
21 changes: 21 additions & 0 deletions Doc/library/argparse.rst
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,27 @@ are set.

.. versionadded:: 3.14

To highlight inline code in your description or epilog text, you can use
backticks::

>>> parser = argparse.ArgumentParser(
... formatter_class=argparse.RawDescriptionHelpFormatter,
... epilog='''Examples:
... `python -m myapp --verbose`
... `python -m myapp --config settings.json`
... ''')

When colors are enabled, the text inside backticks will be displayed in a
distinct color to help examples stand out. When colors are disabled, backticks
are preserved as-is, which is readable in plain text.

.. note::

Backtick markup only applies to description and epilog text. It does not
apply to individual argument ``help`` strings.

.. versionadded:: 3.15


The add_argument() method
-------------------------
Expand Down
6 changes: 6 additions & 0 deletions Doc/library/importlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ Functions
:exc:`ModuleNotFoundError` is raised when the module being reloaded lacks
a :class:`~importlib.machinery.ModuleSpec`.

.. versionchanged:: next
If *module* is a lazy module that has not yet been materialized (i.e.,
loaded via :class:`importlib.util.LazyLoader` and not yet accessed),
calling :func:`reload` is a no-op and returns the module unchanged.
This prevents the reload from unintentionally triggering the lazy load.

.. warning::
This function is not thread-safe. Calling it from multiple threads can result
in unexpected behavior. It's recommended to use the :class:`threading.Lock`
Expand Down
4 changes: 2 additions & 2 deletions Doc/library/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2869,8 +2869,8 @@ ABCs and Protocols for working with I/O
---------------------------------------

.. class:: IO[AnyStr]
TextIO[AnyStr]
BinaryIO[AnyStr]
TextIO
BinaryIO

Generic class ``IO[AnyStr]`` and its subclasses ``TextIO(IO[str])``
and ``BinaryIO(IO[bytes])``
Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,10 @@ argparse
default to ``True``. This enables suggestions for mistyped arguments by default.
(Contributed by Jakob Schluse in :gh:`140450`.)

* Added backtick markup support in description and epilog text to highlight
inline code when color output is enabled.
(Contributed by Savannah Ostrowski in :gh:`142390`.)

calendar
--------

Expand Down
22 changes: 21 additions & 1 deletion Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,27 @@ def _format_text(self, text):
text = text % dict(prog=self._prog)
text_width = max(self._width - self._current_indent, 11)
indent = ' ' * self._current_indent
return self._fill_text(text, text_width, indent) + '\n\n'
text = self._fill_text(text, text_width, indent)
text = self._apply_text_markup(text)
return text + '\n\n'

def _apply_text_markup(self, text):
"""Apply color markup to text.

Supported markup:
`...` - inline code (rendered with prog_extra color)

When colors are disabled, backticks are preserved as-is.
"""
t = self._theme
if not t.reset:
return text
text = _re.sub(
r'`([^`]+)`',
rf'{t.prog_extra}\1{t.reset}',
text,
)
return text

def _format_action(self, action):
# determine the required width and the entry label
Expand Down
5 changes: 5 additions & 0 deletions Lib/importlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ def reload(module):
The module must have been successfully imported before.

"""
# If a LazyModule has not yet been materialized, reload is a no-op.
if importlib_util := sys.modules.get('importlib.util'):
if lazy_module_type := getattr(importlib_util, '_LazyModule', None):
if isinstance(module, lazy_module_type):
return module
try:
name = module.__spec__.name
except AttributeError:
Expand Down
26 changes: 10 additions & 16 deletions Lib/profiling/sampling/_flamegraph_assets/flamegraph.css
Original file line number Diff line number Diff line change
Expand Up @@ -275,16 +275,8 @@ body.resizing-sidebar {
}

/* View Mode Section */
.view-mode-section {
padding-bottom: 20px;
border-bottom: 1px solid var(--border);
}

.view-mode-section .section-title {
margin-bottom: 12px;
}

.view-mode-section .toggle-switch {
.view-mode-section .section-content {
display: flex;
justify-content: center;
}

Expand Down Expand Up @@ -316,15 +308,17 @@ body.resizing-sidebar {
}

.section-content {
transition: max-height var(--transition-normal), opacity var(--transition-normal);
transition: max-height var(--transition-slow) ease-out, opacity var(--transition-normal) ease-out, padding var(--transition-normal) ease-out;
max-height: 1000px;
opacity: 1;
}

.collapsible.collapsed .section-content {
max-height: 0;
opacity: 0;
margin-bottom: -10px;
padding-top: 0;
pointer-events: none;
transition: max-height var(--transition-slow) ease-in, opacity var(--transition-normal) ease-in, padding var(--transition-normal) ease-in;
}

/* --------------------------------------------------------------------------
Expand Down Expand Up @@ -634,10 +628,6 @@ body.resizing-sidebar {
Legend
-------------------------------------------------------------------------- */

.legend-section {
margin-top: auto;
padding-top: 12px;
}

.legend {
display: flex;
Expand Down Expand Up @@ -1023,3 +1013,7 @@ body.resizing-sidebar {
border-color: #8e44ad;
box-shadow: 0 0 8px rgba(142, 68, 173, 0.3);
}

.toggle-switch:focus-visible {
border-radius: 4px;
}
11 changes: 11 additions & 0 deletions Lib/profiling/sampling/_flamegraph_assets/flamegraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -1302,6 +1302,17 @@ function initFlamegraph() {
}
}

// Keyboard shortcut: Enter/Space activates toggle switches
document.addEventListener('keydown', function(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
return;
}
if ((e.key === 'Enter' || e.key === ' ') && e.target.classList.contains('toggle-switch')) {
e.preventDefault();
e.target.click();
}
});

if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initFlamegraph);
} else {
Expand Down
19 changes: 13 additions & 6 deletions Lib/profiling/sampling/_flamegraph_assets/flamegraph_template.html
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,19 @@
</div>

<!-- View Mode Section -->
<section class="sidebar-section view-mode-section">
<h3 class="section-title">View Mode</h3>
<div class="toggle-switch" id="toggle-invert">
<span class="toggle-label active">Flamegraph</span>
<div class="toggle-track"></div>
<span class="toggle-label">Inverted Flamegraph</span>
<section class="sidebar-section view-mode-section collapsible" id="view-mode-section">
<button class="section-header" onclick="toggleSection('view-mode-section')">
<h3 class="section-title">View Mode</h3>
<svg class="section-chevron" width="12" height="12" viewBox="0 0 12 12" fill="none">
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div class="section-content">
<div class="toggle-switch" id="toggle-invert" title="Toggle between standard and inverted flamegraph view" tabindex="0">
<span class="toggle-label active" data-text="Flamegraph">Flamegraph</span>
<div class="toggle-track"></div>
<span class="toggle-label" data-text="Inverted Flamegraph">Inverted Flamegraph</span>
</div>
</div>
</section>

Expand Down
1 change: 1 addition & 0 deletions Lib/profiling/sampling/_shared_assets/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
/* Transitions */
--transition-fast: 0.15s ease;
--transition-normal: 0.25s ease;
--transition-slow: 0.3s ease;
}

/* Light theme (default) */
Expand Down
95 changes: 95 additions & 0 deletions Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -7568,6 +7568,101 @@ def test_error_and_warning_not_colorized_when_disabled(self):
self.assertNotIn('\x1b[', warn)
self.assertIn('warning:', warn)

def test_backtick_markup_in_epilog(self):
parser = argparse.ArgumentParser(
prog='PROG',
color=True,
epilog='Example: `python -m myapp --verbose`',
)

prog_extra = self.theme.prog_extra
reset = self.theme.reset

help_text = parser.format_help()
self.assertIn(f'Example: {prog_extra}python -m myapp --verbose{reset}',
help_text)
self.assertNotIn('`', help_text)

def test_backtick_markup_in_description(self):
parser = argparse.ArgumentParser(
prog='PROG',
color=True,
description='Run `python -m myapp` to start.',
)

prog_extra = self.theme.prog_extra
reset = self.theme.reset

help_text = parser.format_help()
self.assertIn(f'Run {prog_extra}python -m myapp{reset} to start.',
help_text)

def test_backtick_markup_multiple(self):
parser = argparse.ArgumentParser(
prog='PROG',
color=True,
epilog='Try `app run` or `app test`.',
)

prog_extra = self.theme.prog_extra
reset = self.theme.reset

help_text = parser.format_help()
self.assertIn(f'{prog_extra}app run{reset}', help_text)
self.assertIn(f'{prog_extra}app test{reset}', help_text)

def test_backtick_markup_not_applied_when_color_disabled(self):
# When color is disabled, backticks are preserved as-is
parser = argparse.ArgumentParser(
prog='PROG',
color=False,
epilog='Example: `python -m myapp`',
)

help_text = parser.format_help()
self.assertIn('`python -m myapp`', help_text)
self.assertNotIn('\x1b[', help_text)

def test_backtick_markup_with_format_string(self):
parser = argparse.ArgumentParser(
prog='myapp',
color=True,
epilog='Run `%(prog)s --help` for more info.',
)

prog_extra = self.theme.prog_extra
reset = self.theme.reset

help_text = parser.format_help()
self.assertIn(f'{prog_extra}myapp --help{reset}', help_text)

def test_backtick_markup_in_subparser(self):
parser = argparse.ArgumentParser(prog='PROG', color=True)
subparsers = parser.add_subparsers()
sub = subparsers.add_parser(
'sub',
description='Run `PROG sub --foo` to start.',
)

prog_extra = self.theme.prog_extra
reset = self.theme.reset

help_text = sub.format_help()
self.assertIn(f'{prog_extra}PROG sub --foo{reset}', help_text)

def test_backtick_markup_special_regex_chars(self):
parser = argparse.ArgumentParser(
prog='PROG',
color=True,
epilog='`grep "foo.*bar" | sort`',
)

prog_extra = self.theme.prog_extra
reset = self.theme.reset

help_text = parser.format_help()
self.assertIn(f'{prog_extra}grep "foo.*bar" | sort{reset}', help_text)

def test_print_help_uses_target_file_for_color_decision(self):
parser = argparse.ArgumentParser(prog='PROG', color=True)
parser.add_argument('--opt')
Expand Down
80 changes: 80 additions & 0 deletions Lib/test/test_free_threading/test_zlib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import itertools
import unittest

from test.support import import_helper, threading_helper
from test.support.threading_helper import run_concurrently

zlib = import_helper.import_module("zlib")

from test.test_zlib import HAMLET_SCENE


NTHREADS = 10


@threading_helper.requires_working_threading()
class TestZlib(unittest.TestCase):
def test_compressor(self):
comp = zlib.compressobj()

# First compress() outputs zlib header
header = comp.compress(HAMLET_SCENE)
self.assertGreater(len(header), 0)

def worker():
# it should return empty bytes as it buffers data internally
data = comp.compress(HAMLET_SCENE)
self.assertEqual(data, b"")

run_concurrently(worker_func=worker, nthreads=NTHREADS - 1)
full_compressed = header + comp.flush()
decompressed = zlib.decompress(full_compressed)
# The decompressed data should be HAMLET_SCENE repeated NTHREADS times
self.assertEqual(decompressed, HAMLET_SCENE * NTHREADS)

def test_decompressor_concurrent_attribute_reads(self):
input_data = HAMLET_SCENE * NTHREADS
compressed = zlib.compress(input_data)

decomp = zlib.decompressobj()
decomp_size_per_loop = len(input_data) // 1000
decompressed_parts = []

def decomp_worker():
# Decompress in chunks, which updates eof, unused_data, unconsumed_tail
decompressed_parts.append(
decomp.decompress(compressed, decomp_size_per_loop)
)
while decomp.unconsumed_tail:
decompressed_parts.append(
decomp.decompress(
decomp.unconsumed_tail, decomp_size_per_loop
)
)

def decomp_attr_reader():
# Read attributes concurrently while another thread decompresses
for _ in range(1000):
_ = decomp.unused_data
_ = decomp.unconsumed_tail
_ = decomp.eof

counter = itertools.count()

def worker():
# First thread decompresses, others read attributes
if next(counter) == 0:
decomp_worker()
else:
decomp_attr_reader()

run_concurrently(worker_func=worker, nthreads=NTHREADS)

self.assertTrue(decomp.eof)
self.assertEqual(decomp.unused_data, b"")
decompressed = b"".join(decompressed_parts)
self.assertEqual(decompressed, HAMLET_SCENE * NTHREADS)


if __name__ == "__main__":
unittest.main()
Loading
Loading