mirror of
https://github.com/python/cpython.git
synced 2026-04-20 02:40:59 +00:00
gh-146292: Add colour to http.server logs (GH-146293)
Co-authored-by: Brian Schubert <brianm.schubert@gmail.com>
This commit is contained in:
parent
756358524e
commit
2c8f26cf5c
8 changed files with 123 additions and 11 deletions
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
-------
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
@ -378,6 +394,7 @@ 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)
|
||||
traceback: Traceback = field(default_factory=Traceback)
|
||||
|
|
@ -389,6 +406,7 @@ 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,
|
||||
traceback: Traceback | None = None,
|
||||
|
|
@ -403,6 +421,7 @@ 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,
|
||||
traceback=traceback or self.traceback,
|
||||
|
|
@ -421,6 +440,7 @@ 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(),
|
||||
traceback=Traceback.no_colors(),
|
||||
|
|
|
|||
|
|
@ -85,6 +85,8 @@
|
|||
|
||||
from http import HTTPStatus
|
||||
|
||||
lazy import _colorize
|
||||
|
||||
|
||||
# Default error message template
|
||||
DEFAULT_ERROR_MESSAGE = """\
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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))
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -623,12 +651,22 @@ def log_message(self, format, *args):
|
|||
before writing the output to stderr.
|
||||
|
||||
"""
|
||||
message = (format % args).translate(self._control_char_table)
|
||||
t = _colorize.get_theme(tty_file=sys.stderr).http_server
|
||||
|
||||
message = format % args
|
||||
sys.stderr.write("%s - - [%s] %s\n" %
|
||||
(self.address_string(),
|
||||
self.log_date_time_string(),
|
||||
message.translate(self._control_char_table)))
|
||||
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."""
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -28,10 +28,12 @@
|
|||
import threading
|
||||
from unittest import mock
|
||||
from io import BytesIO, StringIO
|
||||
from _colorize import get_theme
|
||||
|
||||
import unittest
|
||||
from test import support
|
||||
from test.support import (
|
||||
force_not_colorized,
|
||||
is_apple, import_helper, os_helper, threading_helper
|
||||
)
|
||||
from test.support.script_helper import kill_python, spawn_python
|
||||
|
|
@ -480,6 +482,7 @@ def do_GET(self):
|
|||
def do_ERROR(self):
|
||||
self.send_error(HTTPStatus.NOT_FOUND, 'File not found')
|
||||
|
||||
@force_not_colorized
|
||||
def test_get(self):
|
||||
self.con = http.client.HTTPConnection(self.HOST, self.PORT)
|
||||
self.con.connect()
|
||||
|
|
@ -490,6 +493,7 @@ def test_get(self):
|
|||
|
||||
self.assertEndsWith(err.getvalue(), '"GET / HTTP/1.1" 200 -\n')
|
||||
|
||||
@force_not_colorized
|
||||
def test_err(self):
|
||||
self.con = http.client.HTTPConnection(self.HOST, self.PORT)
|
||||
self.con.connect()
|
||||
|
|
@ -503,6 +507,39 @@ def test_err(self):
|
|||
self.assertEndsWith(lines[1], '"ERROR / HTTP/1.1" 404 -')
|
||||
|
||||
|
||||
@support.force_colorized_test_class
|
||||
class RequestHandlerColorizedLoggingTestCase(RequestHandlerLoggingTestCase):
|
||||
|
||||
def test_get(self):
|
||||
t = get_theme(force_color=True).http_server
|
||||
self.con = http.client.HTTPConnection(self.HOST, self.PORT)
|
||||
self.con.connect()
|
||||
|
||||
with support.captured_stderr() as err:
|
||||
self.con.request("GET", "/")
|
||||
self.con.getresponse()
|
||||
|
||||
output = err.getvalue()
|
||||
self.assertIn(f"{t.path}/{t.reset}", output)
|
||||
self.assertIn(f"{t.status_ok}200", output)
|
||||
self.assertIn(t.reset, output)
|
||||
|
||||
def test_err(self):
|
||||
t = get_theme(force_color=True).http_server
|
||||
self.con = http.client.HTTPConnection(self.HOST, self.PORT)
|
||||
self.con.connect()
|
||||
|
||||
with support.captured_stderr() as err:
|
||||
self.con.request("ERROR", "/")
|
||||
self.con.getresponse()
|
||||
|
||||
lines = err.getvalue().split("\n")
|
||||
self.assertIn(
|
||||
f"{t.error}code 404, message File not found{t.reset}", lines[0]
|
||||
)
|
||||
self.assertIn(f"{t.status_client_error}404", lines[1])
|
||||
|
||||
|
||||
class SimpleHTTPServerTestCase(BaseTestCase):
|
||||
class request_handler(NoLogRequestHandler, SimpleHTTPRequestHandler):
|
||||
pass
|
||||
|
|
@ -935,6 +972,7 @@ def verify_http_server_response(self, response):
|
|||
match = self.HTTPResponseMatch.search(response)
|
||||
self.assertIsNotNone(match)
|
||||
|
||||
@force_not_colorized
|
||||
def test_unprintable_not_logged(self):
|
||||
# We call the method from the class directly as our Socketless
|
||||
# Handler subclass overrode it... nice for everything BUT this test.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from unittest import mock
|
||||
from test import support
|
||||
from test.support import socket_helper, control_characters_c0
|
||||
from test.support import force_not_colorized, socket_helper, control_characters_c0
|
||||
from test.test_httpservers import NoLogRequestHandler
|
||||
from unittest import TestCase
|
||||
from wsgiref.util import setup_testing_defaults
|
||||
|
|
@ -192,6 +192,7 @@ def bad_app(e,s):
|
|||
err.splitlines()[-2], "AssertionError"
|
||||
)
|
||||
|
||||
@force_not_colorized
|
||||
def test_bytes_validation(self):
|
||||
def app(e, s):
|
||||
s("200 OK", [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
Add colour to :mod:`~http.server.BaseHTTPRequestHandler` logs, as used by
|
||||
the :mod:`http.server` CLI. Patch by Hugo van Kemenade.
|
||||
Loading…
Add table
Add a link
Reference in a new issue