Skip to content

Commit f6f5f99

Browse files
committed
Refactor server app and config
1 parent eaa1141 commit f6f5f99

File tree

7 files changed

+51
-190
lines changed

7 files changed

+51
-190
lines changed

plain/plain/server/__init__.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,6 @@
44
#
55
# Vendored and modified for Plain.
66

7-
import plain.runtime
7+
from .app import ServerApplication
88

9-
from .core import PlainServerApp, run_server
10-
11-
SERVER = "plain"
12-
SERVER_SOFTWARE = f"{SERVER}/{plain.runtime.__version__}"
13-
14-
__all__ = ["run_server", "PlainServerApp", "SERVER_SOFTWARE"]
9+
__all__ = ["ServerApplication"]

plain/plain/server/app.py

Lines changed: 18 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,64 +7,43 @@
77

88
from __future__ import annotations
99

10-
import argparse
1110
import sys
12-
from typing import Any
11+
from typing import TYPE_CHECKING, Any
1312

1413
from .arbiter import Arbiter
15-
from .config import Config
1614

15+
if TYPE_CHECKING:
16+
from .config import Config
1717

18-
class BaseApplication:
18+
19+
class ServerApplication:
1920
"""
20-
An application interface for configuring and loading
21-
the various necessities for any given web framework.
21+
Plain's server application.
22+
23+
This class provides the interface for running the WSGI server.
2224
"""
2325

24-
def __init__(self, usage: str | None = None, prog: str | None = None) -> None:
25-
self.usage: str | None = usage
26-
self.cfg: Config | None = None
26+
def __init__(self, cfg: Config) -> None:
27+
self.cfg: Config = cfg
2728
self.callable: Any = None
28-
self.prog: str | None = prog
29-
self.logger: Any = None
30-
self.do_load_config()
31-
32-
def do_load_config(self) -> None:
33-
"""
34-
Loads the configuration
35-
"""
36-
try:
37-
self.load_config()
38-
except Exception as e:
39-
print(f"\nError: {str(e)}", file=sys.stderr)
40-
sys.stderr.flush()
41-
sys.exit(1)
42-
43-
def init(
44-
self, parser: argparse.ArgumentParser, opts: argparse.Namespace, args: list[str]
45-
) -> None:
46-
raise NotImplementedError
4729

4830
def load(self) -> Any:
49-
raise NotImplementedError
31+
"""Load the WSGI application."""
32+
# Import locally to avoid circular dependencies and allow
33+
# the WSGI module to handle Plain runtime setup
34+
from plain.wsgi import app
5035

51-
def load_config(self) -> None:
52-
"""
53-
This method is used to load the configuration from one or several input(s).
54-
Custom Command line, configuration file.
55-
You have to override this method in your class.
56-
"""
57-
raise NotImplementedError
58-
59-
def reload(self) -> None:
60-
self.do_load_config()
36+
return app
6137

6238
def wsgi(self) -> Any:
39+
"""Get the WSGI application."""
6340
if self.callable is None:
6441
self.callable = self.load()
6542
return self.callable
6643

6744
def run(self) -> None:
45+
"""Run the server."""
46+
6847
try:
6948
Arbiter(self).run()
7049
except RuntimeError as e:

plain/plain/server/arbiter.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from .pidfile import Pidfile
2626

2727
if TYPE_CHECKING:
28-
from .app import BaseApplication
28+
from .app import ServerApplication
2929
from .config import Config
3030
from .glogging import Logger
3131
from .workers.base import Worker
@@ -64,7 +64,7 @@ class Arbiter:
6464
if name[:3] == "SIG" and name[3] != "_"
6565
}
6666

67-
def __init__(self, app: BaseApplication):
67+
def __init__(self, app: ServerApplication):
6868
os.environ["SERVER_SOFTWARE"] = f"plain/{plain.runtime.__version__}"
6969

7070
self._num_workers: int | None = None
@@ -92,8 +92,8 @@ def _set_num_workers(self, value: int) -> None:
9292

9393
num_workers = property(_get_num_workers, _set_num_workers)
9494

95-
def setup(self, app: BaseApplication) -> None:
96-
self.app: BaseApplication = app
95+
def setup(self, app: ServerApplication) -> None:
96+
self.app: ServerApplication = app
9797
assert app.cfg is not None, "Application config must be initialized"
9898
self.cfg: Config = app.cfg
9999

@@ -332,8 +332,6 @@ def stop(self, graceful: bool = True) -> None:
332332
def reload(self) -> None:
333333
old_address = self.cfg.address
334334

335-
# reload conf
336-
self.app.reload()
337335
self.setup(self.app)
338336

339337
# reopen log files

plain/plain/server/config.py

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,35 @@
77
#
88
# Vendored and modified for Plain.
99
import os
10-
from dataclasses import dataclass, field
10+
from dataclasses import dataclass
1111

1212
from . import util
1313

1414

1515
@dataclass
1616
class Config:
17-
"""Plain server configuration."""
17+
"""Plain server configuration.
18+
19+
All configuration values are required and provided by the CLI.
20+
Defaults are defined in the CLI layer, not here.
21+
"""
1822

1923
# Core settings (from CLI)
20-
bind: list[str] = field(default_factory=lambda: ["127.0.0.1:8000"])
21-
workers: int = 1
22-
threads: int = 1
23-
timeout: int = 30
24-
max_requests: int = 0
25-
reload: bool = False
26-
reload_extra_files: list[str] = field(default_factory=list)
27-
pidfile: str | None = None
28-
certfile: str | None = None
29-
keyfile: str | None = None
30-
loglevel: str = "info"
31-
accesslog: str | None = None
32-
errorlog: str = "-"
33-
access_log_format: str = (
34-
'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
35-
)
36-
logconfig_json: str | None = None
37-
38-
# Internal
39-
env_orig: dict[str, str] = field(default_factory=lambda: os.environ.copy())
24+
bind: list[str]
25+
workers: int
26+
threads: int
27+
timeout: int
28+
max_requests: int
29+
reload: bool
30+
reload_extra_files: list[str]
31+
pidfile: str | None
32+
certfile: str | None
33+
keyfile: str | None
34+
loglevel: str
35+
accesslog: str
36+
errorlog: str
37+
access_log_format: str
38+
logconfig_json: str | None
4039

4140
@property
4241
def worker_class_str(self) -> str:

plain/plain/server/http/wsgi.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
from collections.abc import Callable, Iterator
1616
from typing import TYPE_CHECKING, Any, cast
1717

18-
from .. import SERVER_SOFTWARE, util
18+
import plain.runtime
19+
20+
from .. import util
1921
from .errors import ConfigurationProblem, InvalidHeader, InvalidHeaderName
2022
from .message import TOKEN_RE
2123

@@ -86,7 +88,7 @@ def base_environ(cfg: Config) -> dict[str, Any]:
8688
"wsgi.run_once": False,
8789
"wsgi.file_wrapper": FileWrapper,
8890
"wsgi.input_terminated": True,
89-
"SERVER_SOFTWARE": SERVER_SOFTWARE,
91+
"SERVER_SOFTWARE": f"plain/{plain.runtime.__version__}",
9092
}
9193

9294

plain/plain/server/util.py

Lines changed: 0 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,13 @@
66
# See the LICENSE for more information.
77
#
88
# Vendored and modified for Plain.
9-
import ast
109
import email.utils
1110
import errno
1211
import fcntl
1312
import html
1413
import importlib
1514
import inspect
1615
import io
17-
import logging
1816
import os
1917
import random
2018
import re
@@ -28,7 +26,6 @@
2826
from collections.abc import Callable
2927
from typing import Any
3028

31-
from .errors import AppImportError
3229
from .workers import SUPPORTED_WORKERS
3330

3431
# Server and Date aren't technically hop-by-hop
@@ -256,115 +253,6 @@ def write_error(sock: socket.socket, status_int: int, reason: str, mesg: str) ->
256253
write_nonblock(sock, http.encode("latin1"))
257254

258255

259-
def _called_with_wrong_args(f: Any) -> bool:
260-
"""Check whether calling a function raised a ``TypeError`` because
261-
the call failed or because something in the function raised the
262-
error.
263-
264-
:param f: The function that was called.
265-
:return: ``True`` if the call failed.
266-
"""
267-
tb = sys.exc_info()[2]
268-
269-
try:
270-
while tb is not None:
271-
if tb.tb_frame.f_code is f.__code__:
272-
# In the function, it was called successfully.
273-
return False
274-
275-
tb = tb.tb_next
276-
277-
# Didn't reach the function.
278-
return True
279-
finally:
280-
# Delete tb to break a circular reference in Python 2.
281-
# https://docs.python.org/2/library/sys.html#sys.exc_info
282-
del tb
283-
284-
285-
def import_app(module: str) -> Callable[..., Any]:
286-
parts = module.split(":", 1)
287-
if len(parts) == 1:
288-
obj = "application"
289-
else:
290-
module, obj = parts[0], parts[1]
291-
292-
try:
293-
mod = importlib.import_module(module)
294-
except ImportError:
295-
if module.endswith(".py") and os.path.exists(module):
296-
msg = "Failed to find application, did you mean '%s:%s'?"
297-
raise ImportError(msg % (module.rsplit(".", 1)[0], obj))
298-
raise
299-
300-
# Parse obj as a single expression to determine if it's a valid
301-
# attribute name or function call.
302-
try:
303-
expression = ast.parse(obj, mode="eval").body
304-
except SyntaxError:
305-
raise AppImportError(
306-
f"Failed to parse {obj!r} as an attribute name or function call."
307-
)
308-
309-
if isinstance(expression, ast.Name):
310-
name = expression.id
311-
args = kwargs = None
312-
elif isinstance(expression, ast.Call):
313-
# Ensure the function name is an attribute name only.
314-
if not isinstance(expression.func, ast.Name):
315-
raise AppImportError(f"Function reference must be a simple name: {obj!r}")
316-
317-
name = expression.func.id
318-
319-
# Parse the positional and keyword arguments as literals.
320-
try:
321-
args = [ast.literal_eval(arg) for arg in expression.args]
322-
kwargs = {kw.arg: ast.literal_eval(kw.value) for kw in expression.keywords}
323-
except ValueError:
324-
# literal_eval gives cryptic error messages, show a generic
325-
# message with the full expression instead.
326-
raise AppImportError(
327-
f"Failed to parse arguments as literal values: {obj!r}"
328-
)
329-
else:
330-
raise AppImportError(
331-
f"Failed to parse {obj!r} as an attribute name or function call."
332-
)
333-
334-
is_debug = logging.root.level == logging.DEBUG
335-
try:
336-
app = getattr(mod, name)
337-
except AttributeError:
338-
if is_debug:
339-
traceback.print_exception(*sys.exc_info())
340-
raise AppImportError(f"Failed to find attribute {name!r} in {module!r}.")
341-
342-
# If the expression was a function call, call the retrieved object
343-
# to get the real application.
344-
if args is not None:
345-
try:
346-
app = app(*args, **kwargs)
347-
except TypeError as e:
348-
# If the TypeError was due to bad arguments to the factory
349-
# function, show Python's nice error message without a
350-
# traceback.
351-
if _called_with_wrong_args(app):
352-
raise AppImportError(
353-
"".join(traceback.format_exception_only(TypeError, e)).strip()
354-
)
355-
356-
# Otherwise it was raised from within the function, show the
357-
# full traceback.
358-
raise
359-
360-
if app is None:
361-
raise AppImportError(f"Failed to find application object: {obj!r}")
362-
363-
if not callable(app):
364-
raise AppImportError("Application object must be callable.")
365-
return app
366-
367-
368256
def getcwd() -> str:
369257
# get current path, try to use PWD env first
370258
try:

plain/plain/server/workers/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
if TYPE_CHECKING:
3939
import socket
4040

41-
from ..app import BaseApplication
41+
from ..app import ServerApplication
4242
from ..config import Config
4343
from ..glogging import Logger
4444
from ..http.message import Request
@@ -60,7 +60,7 @@ def __init__(
6060
age: int,
6161
ppid: int,
6262
sockets: list[socket.socket],
63-
app: BaseApplication,
63+
app: ServerApplication,
6464
timeout: int,
6565
cfg: Config,
6666
log: Logger,

0 commit comments

Comments
 (0)