Compare commits

...
3 Commits
3 changed files with 39 additions and 7 deletions
+28 -2
View File
@@ -10,6 +10,7 @@ from __future__ import annotations
import io import io
import logging import logging
import os
import re import re
import sys import sys
from contextlib import suppress from contextlib import suppress
@@ -43,6 +44,21 @@ def strip_ansi(text: str) -> str:
return ANSI_ESCAPE_RE.sub("", text) return ANSI_ESCAPE_RE.sub("", text)
def use_color(stream: io.TextIOBase = sys.stderr) -> bool:
"""Test if the stream supports color codes."""
if os.environ.get("NO_COLOR"): # Non empty means no (no-color.org)
return False
if os.environ.get("FORCE_COLOR", "") not in {"", "0"}: # force-color.org, node
return True
if hasattr(stream, "isatty") and stream.isatty():
return True
with suppress(KeyError, ValueError, OSError): # Journald does color (-ocat)
dev, ino = map(int, os.environ["JOURNAL_STREAM"].split(":", 1))
st = os.fstat(stream.fileno())
return st.st_dev == dev and st.st_ino == ino
return False
_LEVEL_EMOJI = { _LEVEL_EMOJI = {
logging.DEBUG: "🐛", logging.DEBUG: "🐛",
logging.INFO: "🔷", logging.INFO: "🔷",
@@ -94,7 +110,7 @@ class Formatter(logging.Formatter):
if use_colors in (True, False): if use_colors in (True, False):
self.use_colors = use_colors self.use_colors = use_colors
else: else:
self.use_colors = sys.stdout.isatty() self.use_colors = use_color(sys.stdout)
super().__init__(fmt=fmt, datefmt=datefmt, style=style) super().__init__(fmt=fmt, datefmt=datefmt, style=style)
def formatMessage(self, record: logging.LogRecord) -> str: # noqa: N802 def formatMessage(self, record: logging.LogRecord) -> str: # noqa: N802
@@ -296,7 +312,10 @@ def patch_log_config(log_config, *, access_log: bool = True): # noqa: ANN001, A
routine INFO lines, an emoji-level-prefix Formatter in place of routine INFO lines, an emoji-level-prefix Formatter in place of
uvicorn's stock ``default`` formatter (a user-supplied one wins), a root uvicorn's stock ``default`` formatter (a user-supplied one wins), a root
logger entry so ``logging.info()`` et al. print through the default logger entry so ``logging.info()`` et al. print through the default
handler, and a no-prefix ``kanta`` logger entry (likewise). handler, and a no-prefix ``kanta`` logger entry (likewise). The
``watchfiles.main`` logger is lifted to WARNING so its INFO "N changes
detected" line is dropped while the WARNING "Reloading..." line (logged
to ``uvicorn.error``) still shows; a user-supplied level wins.
With ``access_log``, additionally rewires the ``access`` formatter to With ``access_log``, additionally rewires the ``access`` formatter to
our Formatter and attaches its handler to our ``fastapi_vue.access`` our Formatter and attaches its handler to our ``fastapi_vue.access``
logger. We must not logger. We must not
@@ -343,6 +362,13 @@ def patch_log_config(log_config, *, access_log: bool = True): # noqa: ANN001, A
if "default" not in root_handlers: if "default" not in root_handlers:
root_handlers.append("default") root_handlers.append("default")
# watchfiles logs "N changes detected" to its own logger at INFO; only the
# WARNING "Reloading..." line (uvicorn.error) should show.
with suppress(Exception):
config.setdefault("loggers", {}).setdefault("watchfiles.main", {}).setdefault(
"level", "WARNING"
)
# kanta-style output (diffs, colored headers) prints without prefixes, # kanta-style output (diffs, colored headers) prints without prefixes,
# like our access log. A user-supplied "kanta" logger entry wins. # like our access log. A user-supplied "kanta" logger entry wins.
with suppress(Exception): with suppress(Exception):
+6
View File
@@ -18,11 +18,17 @@ from .logging import (
patch_lifespan_logging, patch_lifespan_logging,
patch_log_config, patch_log_config,
patch_server_error_middleware, patch_server_error_middleware,
use_color,
) )
from .startupbox import print_box from .startupbox import print_box
tracerite.load() # Early load on CLI load (import server); uvicorn workers reload via log config tracerite.load() # Early load on CLI load (import server); uvicorn workers reload via log config
# Install force color to aid tracerite and any external software to use full color when available
# Define NO_COLOR or FORCE_COLOR beforehand to avoid this
if "FORCE_COLOR" not in os.environ and use_color():
os.environ["FORCE_COLOR"] = "3"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_WILDCARD_HOSTS = frozenset({"0.0.0.0", "::"}) # noqa: S104 _WILDCARD_HOSTS = frozenset({"0.0.0.0", "::"}) # noqa: S104
+5 -5
View File
@@ -1596,11 +1596,11 @@ def cmd_setup(args: argparse.Namespace) -> int:
print("✅ Created .gitignore") print("✅ Created .gitignore")
# === Add dependencies using uv === # === Add dependencies using uv ===
# Pin fastapi-vue to the same major.minor as this setup tool (both are released # Pin fastapi-vue to the same major.minor.patch as this setup tool (both are
# from the same tags). Patch/dev releases may deviate, which also keeps this # released from the same tags). This makes freshly set up projects request the
# resolvable when running a development version of fastapi-vue-setup. # matching patch release directly, while `~=` still allows compatible updates.
mm = re.match(r"(\d+)\.(\d+)", version) mmp = re.match(r"(\d+)\.(\d+)\.(\d+)", version)
fastapi_vue_req = f"fastapi-vue~={mm[1]}.{mm[2]}.0" if mm else "fastapi-vue" fastapi_vue_req = f"fastapi-vue~={mmp[1]}.{mmp[2]}.{mmp[3]}" if mmp else "fastapi-vue"
if dry: if dry:
print(f"📦 Would add: fastapi[standard], {fastapi_vue_req}") print(f"📦 Would add: fastapi[standard], {fastapi_vue_req}")
else: else: