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/
(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`.)\r
+\r
+\r
inspect
-------
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).
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)
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,
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,
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(),
from http import HTTPStatus
+lazy import _colorize
+
# Default error message template
DEFAULT_ERROR_MESSAGE = """\
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.
"""
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))
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
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."""
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()
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)
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
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()
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()
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
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.
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
err.splitlines()[-2], "AssertionError"
)
+ @force_not_colorized
def test_bytes_validation(self):
def app(e, s):
s("200 OK", [
--- /dev/null
+Add colour to :mod:`~http.server.BaseHTTPRequestHandler` logs, as used by
+the :mod:`http.server` CLI. Patch by Hugo van Kemenade.