Skip to content

Commit

Permalink
add option to set application in settings
Browse files Browse the repository at this point in the history
  • Loading branch information
livioribeiro committed Dec 16, 2023
1 parent e6198ea commit fa4f1b9
Show file tree
Hide file tree
Showing 14 changed files with 110 additions and 83 deletions.
10 changes: 10 additions & 0 deletions src/selva/_util/dotenv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import os

from dotenv import load_dotenv

__all__ = ("init",)


def init():
dotenv_path = os.getenv("SELVA_DOTENV", os.path.join(os.getcwd(), ".env"))
load_dotenv(dotenv_path)
4 changes: 2 additions & 2 deletions src/selva/_util/pydantic/dotted_path.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import typing
from typing import Any, Generic, Type, TypeVar
from typing import Any, Generic, TypeVar

from pydantic import GetCoreSchemaHandler, ValidatorFunctionWrapHandler
from pydantic_core import PydanticCustomError, core_schema
Expand Down Expand Up @@ -34,7 +34,7 @@ def validate_from_str(
"invalid_dotted_path",
"unable to import '{path}': {error}",
{"path": value, "error": e.msg},
)
) from e

return function_handler(item)

Expand Down
3 changes: 2 additions & 1 deletion src/selva/configuration/defaults.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
default_settings = {
"components": [],
"application": "application",
"modules": [],
"middleware": [],
"logging": {
"setup": "selva.logging.setup.setup_logger",
Expand Down
10 changes: 6 additions & 4 deletions src/selva/configuration/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,11 @@ def replace_variables_recursive(
for key, value in data.items():
data[key] = replace_variables_recursive(value, environ)
return data
elif isinstance(data, list):

if isinstance(data, list):
return [replace_variables_recursive(value, environ) for value in data]
elif isinstance(data, str):

if isinstance(data, str):
return replace_variables_with_env(data, environ)
else:
return data

return data
9 changes: 2 additions & 7 deletions src/selva/run.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import os

from dotenv import load_dotenv

from selva._util import dotenv
from selva.configuration.settings import get_settings
from selva.web.application import Selva

dotenv_path = os.getenv("SELVA_DOTENV", os.path.join(os.getcwd(), ".env"))
load_dotenv(dotenv_path)

dotenv.init()
settings = get_settings()
app = Selva(settings)
76 changes: 22 additions & 54 deletions src/selva/web/application.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import functools
import importlib
import inspect
import typing
from http import HTTPStatus
from types import FunctionType, ModuleType
from importlib.util import find_spec
from typing import Any
from uuid import uuid4

Expand All @@ -16,7 +15,7 @@
from selva._util.base_types import get_base_types
from selva._util.import_item import import_item
from selva._util.maybe_async import maybe_async
from selva.configuration.settings import Settings, _get_settings_nocache
from selva.configuration.settings import Settings
from selva.di.container import Container
from selva.di.decorator import DI_SERVICE_ATTRIBUTE
from selva.web.converter import (
Expand Down Expand Up @@ -68,24 +67,7 @@ def __init__(self, settings: Settings):

self.di.define(Router, self.router)

self.di.scan(
from_request_impl,
param_extractor_impl,
param_converter_impl,
)

try:
import jinja2

from selva.web import templates

self.di.scan(templates)
except ImportError:
pass

components = self.settings.components
self.di.scan(components)
self._register_components(components)
self._register_modules()

setup_logger = import_item(self.settings.logging.setup)
setup_logger(self.settings)
Expand All @@ -100,33 +82,23 @@ async def __call__(self, scope, receive, send):
case _:
raise RuntimeError(f"unknown scope '{scope['type']}'")

def _register_components(
self, components: list[str | ModuleType | type | FunctionType]
):
def _register_modules(self):
try:
app = importlib.import_module("application")

if app not in components or app.__name__ not in components:
components.append(app)
except ImportError as err:
if err.name != "application":
self.di.scan(self.settings.application)
except ModuleNotFoundError:
if not self.settings.modules:
raise

services = []
packages = []

for component in components:
if _is_service(component):
services.append(component)
elif _is_module(component):
packages.append(component)
else:
raise TypeError(f"Invalid component: {component}")
self.di.scan(
from_request_impl,
param_extractor_impl,
param_converter_impl,
)

self.di.scan(*packages)
self.di.scan(*self.settings.modules)

for service in services:
self.di.service(service)
if find_spec("jinja2") is not None:
self.di.scan("selva.web.templates")

for _iface, impl, _name in self.di.iter_all_services():
if _is_controller(impl):
Expand All @@ -139,26 +111,22 @@ async def _initialize_middleware(self):

middleware = [import_item(name) for name in middleware]

if middleware_errors := [
m for m in middleware if not issubclass(m, Middleware)
]:
mid_classes = [
f"{m.__module__}.{m.__qualname__}" for m in middleware_errors
]
if errors := [m for m in middleware if not issubclass(m, Middleware)]:
mid_classes = [f"{m.__module__}.{m.__qualname__}" for m in errors]
mid_class_name = f"{Middleware.__module__}.{Middleware.__qualname__}"
raise TypeError(
f"Middleware classes must inherit from '{mid_class_name}': {mid_classes}"
f"Middleware classes must be of type '{mid_class_name}': {mid_classes}"
)

for cls in reversed(middleware):
mid = await self.di.create(cls)
chain = functools.partial(mid, self.handler)
self.handler = chain

async def _initialize(self):
async def _lifespan_startup(self):
await self._initialize_middleware()

async def _finalize(self):
async def _lifespan_shutdown(self):
await self.di.run_finalizers()

async def _handle_lifespan(self, _scope, receive, send):
Expand All @@ -167,7 +135,7 @@ async def _handle_lifespan(self, _scope, receive, send):
if message["type"] == "lifespan.startup":
logger.trace("Handling lifespan startup")
try:
await self._initialize()
await self._lifespan_startup()
logger.trace("Lifespan startup complete")
await send({"type": "lifespan.startup.complete"})
except Exception as err:
Expand All @@ -176,7 +144,7 @@ async def _handle_lifespan(self, _scope, receive, send):
elif message["type"] == "lifespan.shutdown":
logger.trace("Handling lifespan shutdown")
try:
await self._finalize()
await self._lifespan_shutdown()
logger.trace("Lifespan shutdown complete")
await send({"type": "lifespan.shutdown.complete"})
except Exception as err:
Expand Down
6 changes: 2 additions & 4 deletions src/selva/web/converter/from_request_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
from asgikit.requests import Request, read_form, read_json
from pydantic import BaseModel as PydanticModel

from selva.di.decorator import service
from selva.web.converter.decorator import register_from_request
from selva.web.converter.from_request import FromRequest
from selva.web.exception import HTTPBadRequestException, HTTPException


Expand All @@ -22,7 +20,7 @@ async def from_request(
) -> PydanticModel:
if request.method not in (HTTPMethod.POST, HTTPMethod.PUT, HTTPMethod.PATCH):
# TODO: improve error
raise Exception(
raise TypeError(
"Pydantic model parameter on method that does not accept body"
)

Expand Down Expand Up @@ -51,7 +49,7 @@ async def from_request(
) -> list[PydanticModel]:
if request.method not in (HTTPMethod.POST, HTTPMethod.PUT, HTTPMethod.PATCH):
# TODO: improve error
raise Exception("Pydantic parameter on method that does not accept body")
raise TypeError("Pydantic parameter on method that does not accept body")

if "application/json" in request.content_type:
data = await read_json(request)
Expand Down
3 changes: 2 additions & 1 deletion src/selva/web/routing/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ def inner(arg: Callable):
req_param = params[1].annotation
if req_param is not inspect.Signature.empty and req_param is not Request:
raise TypeError(
f"Handler request parameter must be of type '{Request.__module__}.{Request.__name__}'"
f"Handler request parameter must be of type "
f"'{Request.__module__}.{Request.__name__}'"
)

if any(p.annotation is inspect.Signature.empty for p in params[2:]):
Expand Down
2 changes: 1 addition & 1 deletion src/selva/web/templates/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class Template(ABC):
async def respond(
self,
response: Response,
name: str,
template_name: str,
context: dict,
status: HTTPStatus = HTTPStatus.OK,
):
Expand Down
6 changes: 4 additions & 2 deletions tests/configuration/test_dotenv.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import os
from pathlib import Path

from selva._util import dotenv


def test_dotenv(monkeypatch):
monkeypatch.chdir(Path(__file__).parent / "dotenv")
from selva import run
dotenv.init()

assert "TEST_VARIABLE" in os.environ
assert os.environ["TEST_VARIABLE"] == "value"


def test_dotenv_custom_location(monkeypatch):
monkeypatch.setenv("SELVA_DOTENV", str(Path(__file__).parent / "dotenv" / ".env"))
from selva import run
dotenv.init()

assert "TEST_VARIABLE" in os.environ
assert os.environ["TEST_VARIABLE"] == "value"
1 change: 1 addition & 0 deletions tests/configuration/test_settings.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import os
from pathlib import Path

import pytest
Expand Down
3 changes: 2 additions & 1 deletion tests/web/application/application.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from asgikit.responses import respond_text

from selva.web import controller, get


@controller
class Controller:
@get
async def index(self, request):
await respond_text(request.response, "Selva")
await respond_text(request.response, "Ok")
22 changes: 16 additions & 6 deletions tests/web/application/test_application.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import sys
from pathlib import Path
from http import HTTPStatus

from httpx import AsyncClient

Expand All @@ -9,11 +8,22 @@


async def test_application():
settings = Settings(default_settings | {
"components": ["tests.web.application.application"]
})
settings = Settings(
default_settings | {"application": "tests.web.application.application"}
)
app = Selva(settings)

client = AsyncClient(app=app)
response = await client.get("http://localhost:8000/")
assert response.text == "Selva"
assert response.text == "Ok"


async def test_not_found():
settings = Settings(
default_settings | {"application": "tests.web.application.application"}
)
app = Selva(settings)

client = AsyncClient(app=app)
response = await client.get("http://localhost:8000/not-found")
assert response.status_code == HTTPStatus.NOT_FOUND
38 changes: 38 additions & 0 deletions tests/web/application/test_middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from asgikit.requests import Request
from httpx import AsyncClient

from selva.configuration import Settings
from selva.configuration.defaults import default_settings
from selva.di import service
from selva.web.application import Selva
from selva.web.middleware import Middleware


@service
class TestMiddleware(Middleware):
async def __call__(self, call, request):
send = request.asgi.send

async def new_send(event: dict):
if event["type"] == "http.response.body":
event["body"] = b"Middleware Ok"
await send(event)

new_request = Request(request.asgi.scope, request.asgi.receive, new_send)
await call(new_request)


async def test_middleware():
settings = Settings(
default_settings
| {
"application": "tests.web.application.application",
"middleware": ["tests.web.application.test_middleware.TestMiddleware"],
}
)
app = Selva(settings)
await app._lifespan_startup()

client = AsyncClient(app=app)
response = await client.get("http://localhost:8000/")
assert response.text == "Middleware Ok"

0 comments on commit fa4f1b9

Please sign in to comment.