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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: a27a2e47c7751b639d2b5badf0ef6ff11fee893f # frozen: v0.15.4
rev: e05c5c0818279e5ac248ac9e954431ba58865e61 # frozen: v0.15.7
hooks:
- id: ruff-check
name: Run Ruff (lint) on Platforms/Apple/
Expand Down
15 changes: 15 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,17 @@ http.cookies
(Contributed by Nick Burns and Senthil Kumaran in :gh:`92936`.)


http.server
-----------

* The logging of :mod:`~http.server.BaseHTTPRequestHandler`,
as used by the :ref:`command-line interface <http-server-cli>`,
is colored by default.
This can be controlled with :ref:`environment variables
<using-on-controlling-color>`.
(Contributed by Hugo van Kemenade in :gh:`146292`.)


inspect
-------

Expand Down Expand Up @@ -1097,6 +1108,10 @@ tarfile
timeit
------

* The output of the :mod:`timeit` command-line interface is colored by default.
This can be controlled with
:ref:`environment variables <using-on-controlling-color>`.
(Contributed by Hugo van Kemenade in :gh:`146609`.)
* The command-line interface now colorizes error tracebacks
by default. This can be controlled with
:ref:`environment variables <using-on-controlling-color>`.
Expand Down
4 changes: 2 additions & 2 deletions Include/internal/pycore_opcode_metadata.h

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

4 changes: 2 additions & 2 deletions Include/internal/pycore_uop_ids.h

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

16 changes: 8 additions & 8 deletions Include/internal/pycore_uop_metadata.h

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

36 changes: 36 additions & 0 deletions Lib/_colorize.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,22 @@ class FancyCompleter(ThemeSection):
str: str = ANSIColors.BOLD_GREEN


@dataclass(frozen=True, kw_only=True)
class HttpServer(ThemeSection):
error: str = ANSIColors.YELLOW
path: str = ANSIColors.CYAN
serving: str = ANSIColors.GREEN
size: str = ANSIColors.GREY
status_informational: str = ANSIColors.RESET
status_ok: str = ANSIColors.GREEN
status_redirect: str = ANSIColors.INTENSE_CYAN
status_client_error: str = ANSIColors.YELLOW
status_server_error: str = ANSIColors.RED
timestamp: str = ANSIColors.GREY
url: str = ANSIColors.CYAN
reset: str = ANSIColors.RESET


@dataclass(frozen=True, kw_only=True)
class LiveProfiler(ThemeSection):
"""Theme section for the live profiling TUI (Tachyon profiler).
Expand Down Expand Up @@ -346,6 +362,18 @@ class Syntax(ThemeSection):
reset: str = ANSIColors.RESET


@dataclass(frozen=True, kw_only=True)
class Timeit(ThemeSection):
timing: str = ANSIColors.CYAN
best: str = ANSIColors.BOLD_GREEN
per_loop: str = ANSIColors.GREEN
punctuation: str = ANSIColors.GREY
warning: str = ANSIColors.YELLOW
warning_worst: str = ANSIColors.MAGENTA
warning_best: str = ANSIColors.GREEN
reset: str = ANSIColors.RESET


@dataclass(frozen=True, kw_only=True)
class Traceback(ThemeSection):
type: str = ANSIColors.BOLD_MAGENTA
Expand Down Expand Up @@ -378,8 +406,10 @@ class Theme:
argparse: Argparse = field(default_factory=Argparse)
difflib: Difflib = field(default_factory=Difflib)
fancycompleter: FancyCompleter = field(default_factory=FancyCompleter)
http_server: HttpServer = field(default_factory=HttpServer)
live_profiler: LiveProfiler = field(default_factory=LiveProfiler)
syntax: Syntax = field(default_factory=Syntax)
timeit: Timeit = field(default_factory=Timeit)
traceback: Traceback = field(default_factory=Traceback)
unittest: Unittest = field(default_factory=Unittest)

Expand All @@ -389,8 +419,10 @@ def copy_with(
argparse: Argparse | None = None,
difflib: Difflib | None = None,
fancycompleter: FancyCompleter | None = None,
http_server: HttpServer | None = None,
live_profiler: LiveProfiler | None = None,
syntax: Syntax | None = None,
timeit: Timeit | None = None,
traceback: Traceback | None = None,
unittest: Unittest | None = None,
) -> Self:
Expand All @@ -403,8 +435,10 @@ def copy_with(
argparse=argparse or self.argparse,
difflib=difflib or self.difflib,
fancycompleter=fancycompleter or self.fancycompleter,
http_server=http_server or self.http_server,
live_profiler=live_profiler or self.live_profiler,
syntax=syntax or self.syntax,
timeit=timeit or self.timeit,
traceback=traceback or self.traceback,
unittest=unittest or self.unittest,
)
Expand All @@ -421,8 +455,10 @@ def no_colors(cls) -> Self:
argparse=Argparse.no_colors(),
difflib=Difflib.no_colors(),
fancycompleter=FancyCompleter.no_colors(),
http_server=HttpServer.no_colors(),
live_profiler=LiveProfiler.no_colors(),
syntax=Syntax.no_colors(),
timeit=Timeit.no_colors(),
traceback=Traceback.no_colors(),
unittest=Unittest.no_colors(),
)
Expand Down
58 changes: 49 additions & 9 deletions Lib/http/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@

from http import HTTPStatus

lazy import _colorize


# Default error message template
DEFAULT_ERROR_MESSAGE = """\
Expand Down Expand Up @@ -574,6 +576,31 @@ def flush_headers(self):
self.wfile.write(b"".join(self._headers_buffer))
self._headers_buffer = []

def _colorize_request(self, code, size, t):
try:
code_int = int(code)
except (TypeError, ValueError):
code_color = ""
else:
if code_int >= 500:
code_color = t.status_server_error
elif code_int >= 400:
code_color = t.status_client_error
elif code_int >= 300:
code_color = t.status_redirect
elif code_int >= 200:
code_color = t.status_ok
else:
code_color = t.status_informational

request_line = self.requestline.translate(self._control_char_table)
parts = request_line.split(None, 2)
if len(parts) == 3:
method, path, version = parts
request_line = f"{method} {t.path}{path}{t.reset} {version}"

return f'"{request_line}" {code_color}{code} {t.size}{size}{t.reset}'

def log_request(self, code='-', size='-'):
"""Log an accepted request.

Expand All @@ -582,6 +609,7 @@ def log_request(self, code='-', size='-'):
"""
if isinstance(code, HTTPStatus):
code = code.value
self._log_request_info = (code, size)
self.log_message('"%s" %s %s',
self.requestline, str(code), str(size))

Expand All @@ -596,7 +624,7 @@ def log_error(self, format, *args):
XXX This should go to the separate error log.

"""

self._log_is_error = True
self.log_message(format, *args)

# https://en.wikipedia.org/wiki/List_of_Unicode_characters#Control_codes
Expand All @@ -623,12 +651,22 @@ def log_message(self, format, *args):
before writing the output to stderr.

"""

message = format % args
sys.stderr.write("%s - - [%s] %s\n" %
(self.address_string(),
self.log_date_time_string(),
message.translate(self._control_char_table)))
message = (format % args).translate(self._control_char_table)
t = _colorize.get_theme(tty_file=sys.stderr).http_server

info = getattr(self, "_log_request_info", None)
if info is not None:
self._log_request_info = None
message = self._colorize_request(*info, t)
elif getattr(self, "_log_is_error", False):
self._log_is_error = False
message = f"{t.error}{message}{t.reset}"

sys.stderr.write(
f"{t.timestamp}{self.address_string()} - - "
f"[{self.log_date_time_string()}]{t.reset} "
f"{message}\n"
)

def version_string(self):
"""Return the server software version string."""
Expand Down Expand Up @@ -994,9 +1032,11 @@ def test(HandlerClass=BaseHTTPRequestHandler,
host, port = httpd.socket.getsockname()[:2]
url_host = f'[{host}]' if ':' in host else host
protocol = 'HTTPS' if tls_cert else 'HTTP'
t = _colorize.get_theme().http_server
url = f"{protocol.lower()}://{url_host}:{port}/"
print(
f"Serving {protocol} on {host} port {port} "
f"({protocol.lower()}://{url_host}:{port}/) ..."
f"{t.serving}Serving {protocol} on {host} port {port}{t.reset} "
f"({t.url}{url}{t.reset}) ..."
)
try:
httpd.serve_forever()
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/.ruff.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
extend = "../../.ruff.toml" # Inherit the project-wide settings

# Unlike Tools/, tests can use newer syntax than PYTHON_FOR_REGEN
target-version = "py314"
target-version = "py315"

extend-exclude = [
# Excluded (run with the other AC files in its own separate ruff job in pre-commit)
Expand Down
26 changes: 22 additions & 4 deletions Lib/test/test_capi/test_opt.py
Original file line number Diff line number Diff line change
Expand Up @@ -1470,8 +1470,8 @@ class Bar:
opnames = list(iter_opnames(ex))
self.assertEqual(res, TIER2_THRESHOLD * 2 + 2)
call = opnames.index("_CALL_BUILTIN_FAST")
load_attr_top = opnames.index("_POP_TOP_LOAD_CONST_INLINE_BORROW", 0, call)
load_attr_bottom = opnames.index("_POP_TOP_LOAD_CONST_INLINE_BORROW", call)
load_attr_top = opnames.index("_LOAD_CONST_INLINE_BORROW", 0, call)
load_attr_bottom = opnames.index("_LOAD_CONST_INLINE_BORROW", call)
self.assertEqual(opnames[:load_attr_top].count("_GUARD_TYPE_VERSION"), 1)
self.assertEqual(opnames[call:load_attr_bottom].count("_CHECK_VALIDITY"), 2)

Expand All @@ -1493,8 +1493,8 @@ class Foo:
self.assertIsNotNone(ex)
self.assertEqual(res, TIER2_THRESHOLD * 2)
call = opnames.index("_CALL_BUILTIN_FAST_WITH_KEYWORDS")
load_attr_top = opnames.index("_POP_TOP_LOAD_CONST_INLINE_BORROW", 0, call)
load_attr_bottom = opnames.index("_POP_TOP_LOAD_CONST_INLINE_BORROW", call)
load_attr_top = opnames.index("_LOAD_CONST_INLINE_BORROW", 0, call)
load_attr_bottom = opnames.index("_LOAD_CONST_INLINE_BORROW", call)
self.assertEqual(opnames[:load_attr_top].count("_GUARD_TYPE_VERSION"), 1)
self.assertEqual(opnames[call:load_attr_bottom].count("_CHECK_VALIDITY"), 2)

Expand Down Expand Up @@ -2340,6 +2340,21 @@ def testfunc(n):
self.assertIn("_BINARY_OP_SUBSCR_DICT_KNOWN_HASH", uops)
self.assertNotIn("_BINARY_OP_SUBSCR_DICT", uops)


def test_binary_op_subscr_constant_frozendict_known_hash(self):
def testfunc(n):
x = 0
for _ in range(n):
x += FROZEN_DICT_CONST['x']
return x

res, ex = self._run_with_optimizer(testfunc, 2 * TIER2_THRESHOLD)
self.assertEqual(res, 2 * TIER2_THRESHOLD)
self.assertIsNotNone(ex)
uops = get_opnames(ex)
self.assertNotIn("_BINARY_OP_SUBSCR_DICT_KNOWN_HASH", uops)
self.assertNotIn("_BINARY_OP_SUBSCR_DICT", uops)

def test_store_subscr_dict_known_hash(self):
# str, int, bytes, float, complex, tuple and any python object which has generic hash
def testfunc(n):
Expand Down Expand Up @@ -2780,9 +2795,12 @@ def testfunc(n):
self.assertEqual(res, TIER2_THRESHOLD * 5)
self.assertIsNotNone(ex)
uops = get_opnames(ex)

self.assertIn("_CALL_METHOD_DESCRIPTOR_NOARGS_INLINE", uops)
self.assertNotIn("_CALL_METHOD_DESCRIPTOR_NOARGS", uops)
self.assertNotIn("_GUARD_CALLABLE_METHOD_DESCRIPTOR_NOARGS", uops)
self.assertGreaterEqual(count_ops(ex, "_POP_TOP"), 5)
self.assertGreaterEqual(count_ops(ex, "_POP_TOP"), 3)

def test_call_method_descriptor_fast(self):
def testfunc(n):
Expand Down
Loading
Loading