diff --git a/hatch.toml b/hatch.toml index 75577ae..a353dbb 100644 --- a/hatch.toml +++ b/hatch.toml @@ -17,12 +17,10 @@ enable-by-default = true [build.targets.wheel.hooks.scikit-build] experimental = true -[build.targets.wheel.hooks.scikit-build.cmake] -verbose = true -build-type = "RelWithDebInfo" -source-dir = "." +[envs.foo.scripts] +x = "python -c 'import view'" -[envs.test] +[envs.hatch-test] features = ["full"] dev-mode = false dependencies = [ @@ -30,18 +28,12 @@ dependencies = [ "pytest", "pytest-memray", "pytest-asyncio", - "pytest-rich", ] platforms = ["linux", "macos"] -[envs.test.overrides] +[envs.hatch-test.overrides] platform.windows.dependencies = [ "coverage", "pytest", "pytest-asyncio", ] - -[envs.test.scripts] -no-cov = "pytest" -memray = "pytest --enable-leak-tracking" -cov = "coverage run -m pytest" diff --git a/pyproject.toml b/pyproject.toml index b48c2fd..a8f7af7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["scikit-build-core>=0.9.0", "hatchling", "pyawaitable"] +requires = ["scikit-build-core>=0.9.0", "hatchling>=1", "pyawaitable==1.2.0"] build-backend = "hatchling.build" [project] @@ -21,14 +21,15 @@ classifiers = [ "Programming Language :: Python :: Implementation :: CPython", ] dependencies = [ - "rich", - "click", + "rich>=13", + "click>=8", "typing_extensions", - "ujson", - "configzen", - "aiofiles", - "prompts.py", - "pyawaitable" + "ujson>=5", + "pydantic_settings>=2", + "toml~=0.10", + "aiofiles>=24", + "prompts.py~=0.1", + "pyawaitable==1.2.0" ] dynamic = ["version", "license"] @@ -72,3 +73,8 @@ Funding = "https://github.com/sponsors/ZeroIntensity" [project.scripts] view = "view.__main__:main" view-py = "view.__main__:main" + +[tool.scikit-build] +cmake.verbose = true +cmake.build-type = "RelWithDebInfo" +cmake.source-dir = "." diff --git a/src/_view/errors.c b/src/_view/errors.c index c696373..d21bf7a 100644 --- a/src/_view/errors.c +++ b/src/_view/errors.c @@ -38,6 +38,8 @@ show_error(bool dev) Py_INCREF(err); // Save a reference to it PyErr_SetRaisedException(err); PyErr_Print(); + // PyErr_Print() clears the error indicator, so + // we need to reset it. PyErr_SetRaisedException(err); } else PyErr_Clear(); } @@ -699,6 +701,7 @@ route_error( PyObject *err ) { + puts("1"); if (((PyObject *) Py_TYPE(err)) == ws_disconnect_err) { // the socket prematurely disconnected, let's complain about it @@ -740,6 +743,7 @@ route_error( PyObject *send; bool handler_was_called; + puts("2"); if ( PyAwaitable_UnpackValues( awaitable, @@ -770,8 +774,10 @@ route_error( if (PyAwaitable_UnpackIntValues(awaitable, &is_http) < 0) return -1; + puts("3"); if (((PyObject *) Py_TYPE(err)) == self->error_type) { + puts("4"); PyObject *status_obj = PyObject_GetAttrString( err, "status" @@ -798,6 +804,7 @@ route_error( return -2; } + puts("5"); const char *message = NULL; if (msg_obj != Py_None) @@ -831,6 +838,7 @@ route_error( Py_DECREF(status_obj); Py_DECREF(msg_obj); + puts("6"); return 0; } @@ -894,6 +902,7 @@ route_error( return 0; } + puts("7"); if ( server_err_exc( self, @@ -909,12 +918,14 @@ route_error( return -1; } + puts("8"); if (!handler_was_called) { PyErr_SetRaisedException(err); PyErr_Print(); } + puts("9"); return 0; } diff --git a/src/_view/handling.c b/src/_view/handling.c index 8e0d46e..faf7461 100644 --- a/src/_view/handling.c +++ b/src/_view/handling.c @@ -30,7 +30,9 @@ #include -#define INITIAL_BUF_SIZE 1024 +// NOTE: This should be below 512 for PyMalloc to be effective +// on the first call. +#define INITIAL_BUF_SIZE 256 /* * Call a route object with both query and body parameters. @@ -274,7 +276,7 @@ handle_route(PyObject *awaitable, char *query) PyAwaitable_SaveArbValues( aw, 4, - &buf, + buf, size, used, query diff --git a/src/_view/inputs.c b/src/_view/inputs.c index 8f143cb..3ae6407 100644 --- a/src/_view/inputs.c +++ b/src/_view/inputs.c @@ -1,7 +1,7 @@ /* * view.py route inputs implementation * - * This file is responsible for parsing route inputs through query + // * This file is responsible for parsing route inputs through query * strings and body parameters. * * If a route has no inputs, then the parsing @@ -70,7 +70,6 @@ body_inc_buf(PyObject *awaitable, PyObject *result) ); if (!more_body) { - Py_DECREF(body); return PyErr_BadASGI(); } @@ -85,12 +84,10 @@ body_inc_buf(PyObject *awaitable, PyObject *result) ) < 0 ) { - Py_DECREF(body); - Py_DECREF(more_body); return -1; } - char **buf; + char *buf; Py_ssize_t *size; Py_ssize_t *used; char *query; @@ -105,17 +102,21 @@ body_inc_buf(PyObject *awaitable, PyObject *result) ) < 0 ) { - Py_DECREF(body); - Py_DECREF(more_body); return -1; } - char *nbuf = *buf; + char *nbuf = buf; + bool needs_realloc = false; while (((*used) + buf_inc_size) > (*size)) { // The buffer would overflow, we need to reallocate it *size *= 2; + needs_realloc = true; + } + + if (needs_realloc) + { nbuf = PyMem_Realloc( buf, (*size) @@ -123,8 +124,6 @@ body_inc_buf(PyObject *awaitable, PyObject *result) if (!nbuf) { - Py_DECREF(body); - Py_DECREF(more_body); PyErr_NoMemory(); return -1; } @@ -136,8 +135,11 @@ body_inc_buf(PyObject *awaitable, PyObject *result) buf_inc_size ); *used += buf_inc_size; - // TODO: Add SetArbValue here - *buf = nbuf; + PyAwaitable_SetArbValue( + awaitable, + 0, + nbuf + ); PyObject *aw; PyObject *receive; @@ -150,8 +152,6 @@ body_inc_buf(PyObject *awaitable, PyObject *result) ) < 0 ) { - Py_DECREF(more_body); - Py_DECREF(body); return -1; } @@ -168,8 +168,6 @@ body_inc_buf(PyObject *awaitable, PyObject *result) ) < 0 ) { - Py_DECREF(more_body); - Py_DECREF(body); Py_DECREF(receive_coro); PyMem_Free(query); PyMem_Free(nbuf); @@ -187,15 +185,12 @@ body_inc_buf(PyObject *awaitable, PyObject *result) ) < 0 ) { - Py_DECREF(more_body); - Py_DECREF(body); PyMem_Free(nbuf); return -1; } - } - Py_DECREF(more_body); - Py_DECREF(body); + PyMem_Free(nbuf); + } return 0; } diff --git a/src/view/__init__.py b/src/view/__init__.py index 3373164..1f23f85 100644 --- a/src/view/__init__.py +++ b/src/view/__init__.py @@ -36,7 +36,6 @@ def index(): from .default_page import * from .exceptions import * from .integrations import * -from .logging import * from .patterns import * from .response import * from .routing import * diff --git a/src/view/app.py b/src/view/app.py index 9705400..e6e8b4f 100644 --- a/src/view/app.py +++ b/src/view/app.py @@ -44,7 +44,7 @@ import ujson from rich import print from rich.traceback import install -from typing_extensions import ParamSpec, TypeAlias, Unpack +from typing_extensions import ParamSpec, TypeAlias from _view import InvalidStatusError, ViewApp, register_ws_cls @@ -70,7 +70,6 @@ ViewInternalError, WebSocketDisconnectError, ) -from .logging import _LogArgs, log from .response import HTML from .routing import Path as _RouteDeco from .routing import ( @@ -918,43 +917,6 @@ async def index(): path, doc, cache_rate, options, steps, parallel_build ) - def _set_log_arg(self, kwargs: _LogArgs, key: str) -> None: - if key not in kwargs: - kwargs[key] = getattr(self.config.log.user, key) # type: ignore - - def _splat_log_args(self, kwargs: _LogArgs) -> _LogArgs: - self._set_log_arg(kwargs, "log_file") - self._set_log_arg(kwargs, "show_time") - self._set_log_arg(kwargs, "show_caller") - self._set_log_arg(kwargs, "show_color") - self._set_log_arg(kwargs, "show_urgency") - self._set_log_arg(kwargs, "file_write") - self._set_log_arg(kwargs, "strftime") - - if "caller_frame" not in kwargs: - frame = inspect.currentframe() - assert frame, "failed to get frame" - back = frame.f_back - assert back, "frame has no f_back" - kwargs["caller_frame"] = back - - return kwargs - - def debug(self, *messages: object, **kwargs: Unpack[_LogArgs]) -> None: - log(*messages, urgency="debug", **self._splat_log_args(kwargs)) - - def info(self, *messages: object, **kwargs: Unpack[_LogArgs]) -> None: - log(*messages, urgency="info", **self._splat_log_args(kwargs)) - - def warning(self, *messages: object, **kwargs: Unpack[_LogArgs]) -> None: - log(*messages, urgency="warning", **self._splat_log_args(kwargs)) - - def error(self, *messages: object, **kwargs: Unpack[_LogArgs]) -> None: - log(*messages, urgency="error", **self._splat_log_args(kwargs)) - - def critical(self, *messages: object, **kwargs: Unpack[_LogArgs]) -> None: - log(*messages, urgency="critical", **self._splat_log_args(kwargs)) - def query( self, name: str, diff --git a/src/view/config.py b/src/view/config.py index 201ef52..5fb87b0 100644 --- a/src/view/config.py +++ b/src/view/config.py @@ -5,24 +5,20 @@ """ from __future__ import annotations - -import importlib.util -import sys +import runpy from ipaddress import IPv4Address from pathlib import Path from typing import Any, Dict, List, Literal, Union - -from configzen import ConfigField, ConfigModel, field_validator +from pydantic import Field +from pydantic_settings import BaseSettings from typing_extensions import TypeAlias - +import toml from .exceptions import ViewInternalError -from .logging import FileWriteMethod, Urgency from .typing import TemplateEngine __all__ = ( "AppConfig", "ServerConfig", - "UserLogConfig", "LogConfig", "MongoConfig", "PostgresConfig", @@ -37,59 +33,28 @@ ) -# https://github.com/python/mypy/issues/11036 -class AppConfig(ConfigModel, env_prefix="view_app_"): # type: ignore +class AppConfig(BaseSettings): loader: Literal["manual", "simple", "filesystem", "patterns", "custom"] = "manual" - app_path: str = ConfigField("app.py:app") + app_path: str = "app.py:app" uvloop: Union[Literal["decide"], bool] = "decide" loader_path: Path = Path("./routes") - @field_validator("loader") - @classmethod - def validate_loader(cls, loader: str): - return loader - - @field_validator("loader_path") - @classmethod - def validate_loader_path(cls, loader_path: Path, values: dict): - loader = values["loader"] - if loader == "manual": - return loader_path - - if (loader == "patterns") and (loader_path == Path("./routes")): - return Path("./urls.py").resolve() - - return loader_path.resolve() - - -class ServerConfig(ConfigModel, env_prefix="view_server_"): # type: ignore +class ServerConfig(BaseSettings): host: IPv4Address = IPv4Address("0.0.0.0") port: int = 5000 backend: Literal["uvicorn", "hypercorn", "daphne"] = "uvicorn" - extra_args: Dict[str, Any] = ConfigField(default_factory=dict) - + extra_args: Dict[str, Any] = Field(default_factory=dict) -class UserLogConfig(ConfigModel, env_prefix="view_user_log_"): # type: ignore - urgency: Urgency = "info" - log_file: Union[Path, str, None] = None - show_time: bool = True - show_caller: bool = True - show_color: bool = True - show_urgency: bool = True - file_write: FileWriteMethod = "both" - strftime: str = "%H:%M:%S" - -class LogConfig(ConfigModel, env_prefix="view_log_"): # type: ignore +class LogConfig(BaseSettings): level: Union[Literal["debug", "info", "warning", "error", "critical"], int] = "info" fancy: bool = True server_logger: bool = False pretty_tracebacks: bool = True - user: UserLogConfig = ConfigField(default_factory=UserLogConfig) startup_message: bool = True -class MongoConfig(ConfigModel, env_prefix="view_mongo_"): # type: ignore +class MongoConfig(BaseSettings): host: IPv4Address port: int username: str @@ -97,7 +62,7 @@ class MongoConfig(ConfigModel, env_prefix="view_mongo_"): # type: ignore database: str -class PostgresConfig(ConfigModel, env_prefix="view_postgres_"): # type: ignore +class PostgresConfig(BaseSettings): database: str user: str password: str @@ -105,26 +70,26 @@ class PostgresConfig(ConfigModel, env_prefix="view_postgres_"): # type: ignore port: int -class SQLiteConfig(ConfigModel, env_prefix="view_sqlite_"): # type: ignore +class SQLiteConfig(BaseSettings): file: Path -class MySQLConfig(ConfigModel, env_prefix="view_mysql_"): # type: ignore +class MySQLConfig(BaseSettings): host: IPv4Address user: str password: str database: str -class DatabaseConfig(ConfigModel, env_prefix="view_database_"): # type: ignore +class DatabaseConfig(BaseSettings): type: Literal["sqlite", "mysql", "postgres", "mongo"] = "sqlite" mongo: Union[MongoConfig, None] = None postgres: Union[PostgresConfig, None] = None - sqlite: Union[SQLiteConfig, None] = SQLiteConfig(file="view.db") + sqlite: Union[SQLiteConfig, None] = SQLiteConfig(file=Path("view.db")) mysql: Union[MySQLConfig, None] = None -class TemplatesConfig(ConfigModel, env_prefix="view_templates_"): # type: ignore +class TemplatesConfig(BaseSettings): directory: Path = Path("./templates") locals: bool = True globals: bool = True @@ -136,30 +101,30 @@ class TemplatesConfig(ConfigModel, env_prefix="view_templates_"): # type: ignor ] -class BuildStep(ConfigModel): # type: ignore +class BuildStep(BaseSettings): platform: Union[List[Platform], Platform, None] = None - requires: List[str] = ConfigField(default_factory=list) + requires: List[str] = Field(default_factory=list) command: Union[str, None, List[str]] = None script: Union[Path, None, List[Path]] = None -class BuildConfig(ConfigModel, env_prefix="view_build_"): # type: ignore +class BuildConfig(BaseSettings): path: Path = Path("./build") default_steps: Union[List[str], None] = None - steps: Dict[str, Union[BuildStep, List[BuildStep]]] = ConfigField( + steps: Dict[str, Union[BuildStep, List[BuildStep]]] = Field( default_factory=dict ) parallel: bool = False -class Config(ConfigModel): # type: ignore +class Config(BaseSettings): dev: bool = True - env: Dict[str, Any] = ConfigField(default_factory=dict) - app: AppConfig = ConfigField(default_factory=AppConfig) - server: ServerConfig = ConfigField(default_factory=ServerConfig) - log: LogConfig = ConfigField(default_factory=LogConfig) - templates: TemplatesConfig = ConfigField(default_factory=TemplatesConfig) - build: BuildConfig = ConfigField(default_factory=BuildConfig) + env: Dict[str, Any] = Field(default_factory=dict) + app: AppConfig = Field(default_factory=AppConfig) + server: ServerConfig = Field(default_factory=ServerConfig) + log: LogConfig = Field(default_factory=LogConfig) + templates: TemplatesConfig = Field(default_factory=TemplatesConfig) + build: BuildConfig = Field(default_factory=BuildConfig) B_OPEN = "{" @@ -210,32 +175,10 @@ def make_preset(tp: str, loader: str) -> str: "log": {B_OC} {B_CLOSE}""" - if tp == "ini": - return f"""dev = 'true' - -[app] -loader = {loader} - -[server] - -[log] -""" - - if tp in {"yml", "yaml"}: - return f""" -app: - loader: "{loader}" -""" - if tp == "py": - return f"""dev = True + return """from view import Config -app = {B_OPEN} - "loader": "{loader}" -{B_CLOSE} - -server = {B_OC} -log = {B_OC}""" +CONFIG = Config()""" raise ViewInternalError("bad file type") @@ -255,18 +198,15 @@ def load_config( paths = ( "view.toml", "view.json", - "view.ini", - "view.yaml", - "view.yml", "view_config.py", "config.py", ) if path: if directory: - return Config.load(directory / path) - # idk why someone would do this, but i guess its good to support it - return Config.load(path) + return Config.model_validate(toml.load(directory / path)) + # Not sure why someone would do this, but it's good to support it + return Config.model_validate(toml.load(path)) for i in paths: p = Path(i) if not directory else directory / i @@ -275,15 +215,12 @@ def load_config( continue if p.suffix == ".py": - spec = importlib.util.spec_from_file_location(str(p)) - assert spec, "spec is none" - mod = importlib.util.module_from_spec(spec) - assert mod, "mod is none" - sys.modules[p.stem] = mod - assert spec.loader, "spec.loader is none" - spec.loader.exec_module(mod) - return Config.wrap_module(mod) - - return Config.load(p) + glbls = runpy.run_path(str(p)) + config = glbls.get("CONFIG") + if not isinstance(config, Config): + raise TypeError(f"{config!r} is not an instance of Config") + return config + + return Config.model_validate(toml.load(p)) return Config() diff --git a/src/view/logging.py b/src/view/logging.py deleted file mode 100644 index 0e9d9da..0000000 --- a/src/view/logging.py +++ /dev/null @@ -1,143 +0,0 @@ -from __future__ import annotations - -import inspect -import sys -from datetime import datetime as DateTime -from pathlib import Path -from types import FrameType as Frame -from typing import IO, TypedDict - -from rich.console import Console -from typing_extensions import NotRequired, Unpack - -from ._logging import _QUEUE, QueueItem -from .typing import FileWriteMethod -from .typing import LogLevel as Urgency - -__all__ = ( - "log", - "Urgency", - "log", - "debug", - "info", - "warning", - "error", - "critical", -) - -_CurrentFrame = None -_Now = None -_StandardOut = None -_NoFile = None - -_URGENCY_COLORS: dict[str, str] = { - "debug": "blue", - "info": "green", - "warning": "dim yellow", - "error": "red", - "critical": "dim red", -} - - -def log( - *messages: object, - urgency: Urgency = "info", - file_out: IO[str] | None = _StandardOut, - log_file: Path | str | IO[str] | None = _NoFile, - caller_frame: Frame | None = _CurrentFrame, - time: DateTime | None = _Now, - show_time: bool = True, - show_caller: bool = True, - show_color: bool = True, - show_urgency: bool = True, - file_write: FileWriteMethod = "both", - strftime: str = "%H:%M:%S", -) -> None: - f = caller_frame or inspect.currentframe() - assert f is not None - assert f.f_back is not None - time = time or DateTime.now() - time_msg = f"[bold dim blue]{time.strftime(strftime)}[/] " if show_time else "" - caller_msg = ( - f"[bold magenta]{f.f_back.f_code.co_filename}:{f.f_back.f_lineno}[/] " - if show_caller - else "" - ) - urgency_msg = ( - f"[bold {_URGENCY_COLORS[urgency]}]{urgency}[/]: " if show_urgency else "" - ) - msg = time_msg + caller_msg + urgency_msg + " ".join([str(i) for i in messages]) - - if file_write != "only": - Console(file=file_out or sys.stdout).print( - msg, - markup=show_color, - highlight=show_color, - ) - _QUEUE.put_nowait(QueueItem(True, False, urgency, msg + "\n", is_stdout=True)) - - if (file_write != "never") and log_file: - log_f: IO[str] - if isinstance(log_file, (str, Path)): - log_path = Path(log_file) - - if log_path.exists(): - log_f = open(log_path, "a") - else: - log_f = open(log_path, "w") - else: - log_f = log_file - - Console(file=log_f).print( - msg, - markup=show_color, - highlight=show_color, - ) - - -class _LogArgs(TypedDict): - file_out: NotRequired[IO[str]] - log_file: NotRequired[Path | str | IO[str]] - caller_frame: NotRequired[Frame] - time: NotRequired[DateTime] - show_time: NotRequired[bool] - show_caller: NotRequired[bool] - show_color: NotRequired[bool] - show_urgency: NotRequired[bool] - file_write: NotRequired[FileWriteMethod] - strftime: NotRequired[str] - - -def _get_last_frame() -> Frame: - f = inspect.currentframe() - assert f, "failed to get frame" - assert f.f_back, "failed to get f_back" - assert f.f_back.f_back, "f_back has no previous frame" - return f.f_back.f_back - - -def _splat_args(args: _LogArgs) -> _LogArgs: - if "caller_frame" not in args: - args["caller_frame"] = _get_last_frame() - - return args - - -def debug(*messages: object, **kwargs: Unpack[_LogArgs]) -> None: - log(*messages, urgency="debug", **_splat_args(kwargs)) - - -def info(*messages: object, **kwargs: Unpack[_LogArgs]) -> None: - log(*messages, urgency="info", **_splat_args(kwargs)) - - -def warning(*messages: object, **kwargs: Unpack[_LogArgs]) -> None: - log(*messages, urgency="warning", **_splat_args(kwargs)) - - -def error(*messages: object, **kwargs: Unpack[_LogArgs]) -> None: - log(*messages, urgency="error", **_splat_args(kwargs)) - - -def critical(*messages: object, **kwargs: Unpack[_LogArgs]) -> None: - log(*messages, urgency="critical", **_splat_args(kwargs)) diff --git a/src/view/routing.py b/src/view/routing.py index 9fe9316..08852bd 100644 --- a/src/view/routing.py +++ b/src/view/routing.py @@ -13,33 +13,16 @@ from contextlib import suppress from dataclasses import dataclass, field from enum import Enum -from typing import ( - Any, - Callable, - Generic, - Iterable, - Literal, - Type, - TypeVar, - Union, - overload, -) +from typing import (Any, Callable, Generic, Iterable, Literal, Type, TypeVar, + Union, overload) from typing_extensions import ParamSpec, TypeAlias from ._logging import Service from ._util import LoadChecker, make_hint from .exceptions import InvalidRouteError, MissingAppError, MistakeError -from .typing import ( - TYPE_CHECKING, - Middleware, - StrMethod, - Validator, - ValueType, - ViewResult, - ViewRoute, - WebSocketRoute, -) +from .typing import (TYPE_CHECKING, Middleware, StrMethod, Validator, + ValueType, ViewResult, ViewRoute, WebSocketRoute) if TYPE_CHECKING: from .app import App @@ -205,7 +188,7 @@ async def call_next() -> ViewResult: if isinstance(result, Awaitable): return await result - # type checker still thinks its async for some reason + # The type checker still thinks it's asynchronous, for some reason return result # type: ignore @@ -266,7 +249,7 @@ def _method( if not util_path.startswith("/"): raise MistakeError( "paths must started with a slash", - hint=make_hint(f'This should be "/{util_path}" instead', back_lines=2), + hint=make_hint(f'This should be "/{util_path}" instead', back_lines=2,), ) if util_path.endswith("/") and (len(util_path) != 1): diff --git a/src/view/typing.py b/src/view/typing.py index 68aa359..8237b94 100644 --- a/src/view/typing.py +++ b/src/view/typing.py @@ -140,7 +140,6 @@ class Part(Protocol[V]): DocsType = Dict[Tuple[Union[str, Tuple[str, ...]], str], "RouteDoc"] LogLevel = Literal["debug", "info", "warning", "error", "critical"] -FileWriteMethod = Literal["only", "never", "both"] StrMethod = Literal[ "get", "post",