plain 0.76.0__py3-none-any.whl → 0.77.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of plain might be problematic. Click here for more details.

plain/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # plain changelog
2
2
 
3
+ ## [0.77.0](https://github.com/dropseed/plain/releases/plain@0.77.0) (2025-10-13)
4
+
5
+ ### What's changed
6
+
7
+ - The `plain server --reload` now uses `watchfiles` for improved cross-platform file watching ([92e95c5032](https://github.com/dropseed/plain/commit/92e95c5032))
8
+ - Server reloader now watches `.env*` files for changes and triggers automatic reload ([92e95c5032](https://github.com/dropseed/plain/commit/92e95c5032))
9
+ - HTML template additions and deletions now trigger automatic server reload when using `--reload` ([f2f31c288b](https://github.com/dropseed/plain/commit/f2f31c288b))
10
+ - Internal server worker type renamed from "gthread" to "thread" for clarity ([6470748e91](https://github.com/dropseed/plain/commit/6470748e91))
11
+
12
+ ### Upgrade instructions
13
+
14
+ - No changes required
15
+
3
16
  ## [0.76.0](https://github.com/dropseed/plain/releases/plain@0.76.0) (2025-10-12)
4
17
 
5
18
  ### What's changed
plain/cli/server.py CHANGED
@@ -58,12 +58,6 @@ from plain.cli.runtime import without_runtime_setup
58
58
  is_flag=True,
59
59
  help="Restart workers when code changes (dev only)",
60
60
  )
61
- @click.option(
62
- "--reload-extra-file",
63
- multiple=True,
64
- type=click.Path(exists=True),
65
- help="Additional files to watch for reload (can be used multiple times)",
66
- )
67
61
  @click.option(
68
62
  "--access-log",
69
63
  default="-",
@@ -109,7 +103,6 @@ def server(
109
103
  keyfile: str | None,
110
104
  log_level: str,
111
105
  reload: bool,
112
- reload_extra_file: tuple[str, ...],
113
106
  access_log: str,
114
107
  error_log: str,
115
108
  log_format: str,
@@ -130,7 +123,6 @@ def server(
130
123
  timeout=timeout,
131
124
  max_requests=max_requests,
132
125
  reload=reload,
133
- reload_extra_files=list(reload_extra_file) if reload_extra_file else [],
134
126
  pidfile=pidfile,
135
127
  certfile=certfile,
136
128
  keyfile=keyfile,
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import os.path
5
+ import re
6
+ import sys
7
+ import threading
8
+ from collections.abc import Callable
9
+
10
+ import watchfiles
11
+
12
+ COMPILED_EXT_RE = re.compile(r"py[co]$")
13
+
14
+
15
+ class Reloader(threading.Thread):
16
+ """File change reloader using watchfiles for cross-platform native file watching."""
17
+
18
+ def __init__(self, callback: Callable[[str], None], watch_html: bool) -> None:
19
+ super().__init__()
20
+ self.daemon = True
21
+ self._callback = callback
22
+ self._watch_html = watch_html
23
+
24
+ def get_watch_paths(self) -> set[str]:
25
+ """Get all directories to watch for changes."""
26
+ paths = set()
27
+
28
+ # Get directories from loaded Python modules
29
+ for module in tuple(sys.modules.values()):
30
+ if not hasattr(module, "__file__") or not module.__file__:
31
+ continue
32
+ # Convert .pyc/.pyo to .py and get directory
33
+ file_path = COMPILED_EXT_RE.sub("py", module.__file__)
34
+ dir_path = os.path.dirname(os.path.abspath(file_path))
35
+ if os.path.isdir(dir_path):
36
+ paths.add(dir_path)
37
+
38
+ # Add current working directory for .env files
39
+ cwd = os.getcwd()
40
+ if os.path.isdir(cwd):
41
+ paths.add(cwd)
42
+
43
+ return paths
44
+
45
+ def run(self) -> None:
46
+ """Watch for file changes and trigger callback."""
47
+ watch_paths = self.get_watch_paths()
48
+
49
+ for changes in watchfiles.watch(*watch_paths, rust_timeout=1000):
50
+ for change_type, file_path in changes:
51
+ should_reload = False
52
+ filename = os.path.basename(file_path)
53
+
54
+ # Python files: reload on modify/add
55
+ if change_type in (watchfiles.Change.modified, watchfiles.Change.added):
56
+ if file_path.endswith(".py"):
57
+ should_reload = True
58
+
59
+ # .env files: reload on modify/add/delete
60
+ if change_type in (
61
+ watchfiles.Change.modified,
62
+ watchfiles.Change.added,
63
+ watchfiles.Change.deleted,
64
+ ):
65
+ if filename.startswith(".env"):
66
+ should_reload = True
67
+
68
+ # HTML files: only reload on add/delete (Jinja auto-reloads modifications)
69
+ if self._watch_html and change_type in (
70
+ watchfiles.Change.added,
71
+ watchfiles.Change.deleted,
72
+ ):
73
+ if file_path.endswith(".html"):
74
+ should_reload = True
75
+
76
+ if should_reload:
77
+ self._callback(file_path)
plain/server/README.md CHANGED
@@ -39,8 +39,7 @@ Common options:
39
39
  - `--workers` / `-w` - Number of worker processes (default: 1, or `$WEB_CONCURRENCY` env var)
40
40
  - `--threads` - Number of threads per worker (default: 1)
41
41
  - `--timeout` / `-t` - Worker timeout in seconds (default: 30)
42
- - `--reload` - Enable auto-reload on code changes (default: False)
43
- - `--reload-extra-file` - Additional files to watch for reloading (can be used multiple times)
42
+ - `--reload` - Enable auto-reload on code changes, including `.env*` files (default: False)
44
43
  - `--certfile` - Path to SSL certificate file
45
44
  - `--keyfile` - Path to SSL key file
46
45
  - `--log-level` - Logging level: debug, info, warning, error, critical (default: info)
plain/server/arbiter.py CHANGED
@@ -490,7 +490,7 @@ class Arbiter:
490
490
  # Process Child
491
491
  worker.pid = os.getpid()
492
492
  try:
493
- self.log.info("Plain server worker started pid=%s", worker.pid)
493
+ self.log.info("Server worker started pid=%s", worker.pid)
494
494
  worker.init_process()
495
495
  sys.exit(0)
496
496
  except SystemExit:
@@ -506,7 +506,7 @@ class Arbiter:
506
506
  sys.exit(self.WORKER_BOOT_ERROR)
507
507
  sys.exit(-1)
508
508
  finally:
509
- self.log.info("Worker exiting (pid: %s)", worker.pid)
509
+ self.log.info("Server worker exiting (pid: %s)", worker.pid)
510
510
  try:
511
511
  worker.tmp.close()
512
512
  except Exception:
plain/server/config.py CHANGED
@@ -10,6 +10,8 @@ import os
10
10
  from dataclasses import dataclass
11
11
 
12
12
  from . import util
13
+ from .workers.sync import SyncWorker
14
+ from .workers.thread import ThreadWorker
13
15
 
14
16
 
15
17
  @dataclass
@@ -27,7 +29,6 @@ class Config:
27
29
  timeout: int
28
30
  max_requests: int
29
31
  reload: bool
30
- reload_extra_files: list[str]
31
32
  pidfile: str | None
32
33
  certfile: str | None
33
34
  keyfile: str | None
@@ -41,20 +42,19 @@ class Config:
41
42
  def worker_class_str(self) -> str:
42
43
  # Auto-select based on threads
43
44
  if self.threads > 1:
44
- return "gthread"
45
+ return "thread"
45
46
  return "sync"
46
47
 
47
48
  @property
48
49
  def worker_class(self) -> type:
49
50
  # Auto-select based on threads
50
51
  if self.threads > 1:
51
- uri = "plain.server.workers.gthread.ThreadWorker"
52
+ worker_class = ThreadWorker
52
53
  else:
53
- uri = "plain.server.workers.sync.SyncWorker"
54
+ worker_class = SyncWorker
54
55
 
55
- worker_class = util.load_class(uri)
56
56
  if hasattr(worker_class, "setup"):
57
- worker_class.setup() # type: ignore[call-non-callable] # hasattr check doesn't narrow type
57
+ worker_class.setup()
58
58
  return worker_class
59
59
 
60
60
  @property
plain/server/util.py CHANGED
@@ -10,8 +10,6 @@ import email.utils
10
10
  import errno
11
11
  import fcntl
12
12
  import html
13
- import importlib
14
- import inspect
15
13
  import io
16
14
  import os
17
15
  import random
@@ -20,14 +18,11 @@ import socket
20
18
  import sys
21
19
  import textwrap
22
20
  import time
23
- import traceback
24
21
  import urllib.parse
25
22
  import warnings
26
23
  from collections.abc import Callable
27
24
  from typing import Any
28
25
 
29
- from .workers import SUPPORTED_WORKERS
30
-
31
26
  # Server and Date aren't technically hop-by-hop
32
27
  # headers, but they are in the purview of the
33
28
  # origin server which the WSGI spec says we should
@@ -44,55 +39,6 @@ hop_headers = set(
44
39
  """.split()
45
40
  )
46
41
 
47
-
48
- def load_class(
49
- uri: str | type,
50
- default: str = "plain.server.workers.sync.SyncWorker",
51
- section: str = "plain.server.workers",
52
- ) -> type:
53
- if inspect.isclass(uri):
54
- return uri # type: ignore[return-value]
55
-
56
- components = uri.split(".") # type: ignore[union-attr]
57
- if len(components) == 1:
58
- # Handle short names like "sync" or "gthread"
59
- if uri.startswith("#"): # type: ignore[union-attr]
60
- uri = uri[1:] # type: ignore[union-attr]
61
-
62
- if uri in SUPPORTED_WORKERS:
63
- components = SUPPORTED_WORKERS[uri].split(".")
64
- else:
65
- exc_msg = f"Worker type {uri!r} not found in SUPPORTED_WORKERS"
66
- raise RuntimeError(exc_msg)
67
-
68
- klass = components.pop(-1)
69
-
70
- try:
71
- mod = importlib.import_module(".".join(components))
72
- except Exception:
73
- exc = traceback.format_exc()
74
- msg = "class uri %r invalid or not found: \n\n[%s]"
75
- raise RuntimeError(msg % (uri, exc))
76
- return getattr(mod, klass)
77
-
78
-
79
- positionals = (
80
- inspect.Parameter.POSITIONAL_ONLY,
81
- inspect.Parameter.POSITIONAL_OR_KEYWORD,
82
- )
83
-
84
-
85
- def get_arity(f: Callable[..., Any]) -> int:
86
- sig = inspect.signature(f)
87
- arity = 0
88
-
89
- for param in sig.parameters.values():
90
- if param.kind in positionals:
91
- arity += 1
92
-
93
- return arity
94
-
95
-
96
42
  if sys.platform.startswith("win"):
97
43
 
98
44
  def _waitfor(
@@ -317,19 +263,6 @@ def has_fileno(obj: Any) -> bool:
317
263
  return True
318
264
 
319
265
 
320
- def warn(msg: str) -> None:
321
- print("!!!", file=sys.stderr)
322
-
323
- lines = msg.splitlines()
324
- for i, line in enumerate(lines):
325
- if i == 0:
326
- line = f"WARNING: {line}"
327
- print(f"!!! {line}", file=sys.stderr)
328
-
329
- print("!!!\n", file=sys.stderr)
330
- sys.stderr.flush()
331
-
332
-
333
266
  def make_fail_app(msg: str | bytes) -> Callable[..., Any]:
334
267
  msg = to_bytestring(msg)
335
268
 
@@ -4,9 +4,3 @@
4
4
  # See the LICENSE for more information.
5
5
  #
6
6
  # Vendored and modified for Plain.
7
-
8
- # Supported workers
9
- SUPPORTED_WORKERS = {
10
- "sync": "plain.server.workers.sync.SyncWorker",
11
- "gthread": "plain.server.workers.gthread.ThreadWorker",
12
- }
@@ -17,6 +17,8 @@ from random import randint
17
17
  from ssl import SSLError
18
18
  from typing import TYPE_CHECKING, Any
19
19
 
20
+ from plain.internal.reloader import Reloader
21
+
20
22
  from .. import util
21
23
  from ..http.errors import (
22
24
  ConfigurationProblem,
@@ -32,7 +34,6 @@ from ..http.errors import (
32
34
  UnsupportedTransferCoding,
33
35
  )
34
36
  from ..http.wsgi import Response, default_environ
35
- from ..reloader import reloader_engines
36
37
  from .workertmp import WorkerTmp
37
38
 
38
39
  if TYPE_CHECKING:
@@ -143,16 +144,13 @@ class Worker:
143
144
  if self.cfg.reload:
144
145
 
145
146
  def changed(fname: str) -> None:
146
- self.log.info("Worker reloading: %s modified", fname)
147
+ self.log.debug("Server worker reloading: %s modified", fname)
147
148
  self.alive = False
148
149
  os.write(self.PIPE[1], b"1")
149
150
  time.sleep(0.1)
150
151
  sys.exit(0)
151
152
 
152
- reloader_cls = reloader_engines["auto"]
153
- self.reloader = reloader_cls(
154
- extra_files=self.cfg.reload_extra_files, callback=changed
155
- )
153
+ self.reloader = Reloader(callback=changed, watch_html=True)
156
154
 
157
155
  self.load_wsgi()
158
156
  if self.reloader:
@@ -177,7 +175,6 @@ class Worker:
177
175
  # delete the traceback after use.
178
176
  try:
179
177
  _, exc_val, exc_tb = sys.exc_info()
180
- self.reloader.add_extra_file(exc_val.filename)
181
178
 
182
179
  tb_string = io.StringIO()
183
180
  traceback.print_tb(exc_tb, file=tb_string)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.76.0
3
+ Version: 0.77.0
4
4
  Summary: A web framework for building products with Python.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-File: LICENSE
@@ -9,6 +9,7 @@ Requires-Dist: click>=8.0.0
9
9
  Requires-Dist: jinja2>=3.1.2
10
10
  Requires-Dist: opentelemetry-api>=1.34.1
11
11
  Requires-Dist: opentelemetry-semantic-conventions>=0.55b1
12
+ Requires-Dist: watchfiles>=0.18.0
12
13
  Description-Content-Type: text/markdown
13
14
 
14
15
  # Plain
@@ -1,5 +1,5 @@
1
1
  plain/AGENTS.md,sha256=As6EFSWWHJ9lYIxb2LMRqNRteH45SRs7a_VFslzF53M,1046
2
- plain/CHANGELOG.md,sha256=pAk-j9vETAedPDTgTHXkh_v9wt-DCakHmVKdJzmd1zc,25908
2
+ plain/CHANGELOG.md,sha256=A05-ohzcEeeRrrxOXNt1FLH8F88kgS-_IjmwsHR1g9Y,26688
3
3
  plain/README.md,sha256=VvzhXNvf4I6ddmjBP9AExxxFXxs7RwyoxdgFm-W5dsg,4072
4
4
  plain/__main__.py,sha256=GK39854Lc_LO_JP8DzY9Y2MIQ4cQEl7SXFJy244-lC8,110
5
5
  plain/debug.py,sha256=C2OnFHtRGMrpCiHSt-P2r58JypgQZ62qzDBpV4mhtFM,855
@@ -34,7 +34,7 @@ plain/cli/print.py,sha256=7kv9ddXpwOHRSWp6FFLfX4wbmhV7neoOBlE0VcXWccw,238
34
34
  plain/cli/registry.py,sha256=Z52nVE2bC2h_B_SENnXctn3mx3UWB0qYg969DVP7XX8,1106
35
35
  plain/cli/runtime.py,sha256=YbGYfwkH0VxfuIMbOCwM9wSWiQKusPn_gVeGod8OFaE,743
36
36
  plain/cli/scaffold.py,sha256=AMAVnTYySgR5nz4sVp3mn0gEGfTKE1N8ZlrVrg2SrFU,1364
37
- plain/cli/server.py,sha256=qe4N-eCnFVuEP5ogIgzU9leurv8gi1riSplvin9TjAI,3297
37
+ plain/cli/server.py,sha256=ngUMtxB90t5FnG7HQwOIkN-pwY6ltKPZZ4PLSqN2Y3E,3001
38
38
  plain/cli/settings.py,sha256=kafbcPzy8khdtzLRyOHRl215va3E7U_h5USOA39UA3k,2008
39
39
  plain/cli/shell.py,sha256=urTp24D4UsKmYi9nT7OOdlT4WhXjkpFVrGYfNNVsXEE,1980
40
40
  plain/cli/startup.py,sha256=1nxXQucDkBxXriEN4wI2tiwG96PBNFndVrOyfzvJFdI,1061
@@ -63,6 +63,7 @@ plain/http/multipartparser.py,sha256=3W9osVGV9LshNF3aAUCBp7OBYTgD6hN2jS7T15BIKCs
63
63
  plain/http/request.py,sha256=ficL1Lh-71tU1SVFKD4beLEJsPk7eesZG0nPPbACMTk,26462
64
64
  plain/http/response.py,sha256=efAJ2M_uwK8EYMXchOk-b0Jrx3Hukch_rPOW9nG5AV8,24842
65
65
  plain/internal/__init__.py,sha256=n2AgdfNelt_tp8CS9JDzHMy_aiTUMPGZiFFwKmNz2fg,262
66
+ plain/internal/reloader.py,sha256=n7B-F-WeUXp37pAnvzKX9tcEbUxHSlYqa4gItyA_zko,2662
66
67
  plain/internal/files/__init__.py,sha256=VctFgox4Q1AWF3klPaoCC5GIw5KeLafYjY5JmN8mAVw,63
67
68
  plain/internal/files/base.py,sha256=TiUIAqBSQCslgAmf5vjrwjbCe2px5Pt0wWLlGc66jXw,4683
68
69
  plain/internal/files/locks.py,sha256=jvLL9kroOo50kUo8dbuajDiFvgSL5NH6x5hudRPPjiQ,4022
@@ -105,17 +106,16 @@ plain/runtime/global_settings.py,sha256=Q-bQP3tNnnuJZvfevGai639RIF_jhd7Dszt-DzTT
105
106
  plain/runtime/user_settings.py,sha256=Tbd0J6bxp18tKmFsQcdlxeFhUQU68PYtsswzQ2IcfNc,11467
106
107
  plain/runtime/utils.py,sha256=sHOv9SWCalBtg32GtZofimM2XaQtf_jAyyf6RQuOlGc,851
107
108
  plain/server/LICENSE,sha256=Xt_dw4qYwQI9qSi2u8yMZeb4HuMRp5tESRKhtvvJBgA,1707
108
- plain/server/README.md,sha256=2dwgogY6rBDUA4dgDrrm7zIK66M_ZdULKNsyuE8UTRE,2731
109
+ plain/server/README.md,sha256=6jXQeZJVDt6Jn-Ff8Q7cZ1Xsh6fYPFfC-rtQ7zSYzag,2661
109
110
  plain/server/__init__.py,sha256=DtRgEcr4IxF4mrtCHloIprk_Q4k1oju4F2VHoyvu4ow,212
110
111
  plain/server/app.py,sha256=ozaqdb-a_T3ps7T5EJwIPM63F_497J4o7kw9Pbq7Ga0,1229
111
- plain/server/arbiter.py,sha256=qqTNgbrL0UzZ8sNUV0QP4Z_nzE4D4Y2smvHL33O-QcE,17403
112
- plain/server/config.py,sha256=LSBi_E5JNmyzNY_XqAPYQ4-rDRw-NqxrMNyvQpFWbzQ,3335
112
+ plain/server/arbiter.py,sha256=89_4CZn6v0kmfF3-h6y5Ax59Tm9vhYiZw7psuoHMe_A,17404
113
+ plain/server/config.py,sha256=-T1w8dbUwwLd898P1HNufe8Kw8VJDYJaeKY-UuKzvTo,3221
113
114
  plain/server/errors.py,sha256=sKl_OJ5Uw-a_r_dZ2o4I8JaKeTrjvY_LR12F6B_p4-g,956
114
115
  plain/server/glogging.py,sha256=Ab49Btbr9UvGRgBk8KGVrzsJaFKN1uD0ZurmIC0GYFY,9692
115
116
  plain/server/pidfile.py,sha256=8Fcl9u7gvUJjY5z01qGAeRsi13_jAM8CRdeyqL3h2i0,2538
116
- plain/server/reloader.py,sha256=x3Oe1qmlprnkNRa-RQeuANdfmpRQvMSFfdTtBPIA5z0,4517
117
117
  plain/server/sock.py,sha256=NFKtlrMstOT3xU2yKI6BLAzv_WE7VEGt3dHDkFPnPSo,6192
118
- plain/server/util.py,sha256=_CjcbHr3ES9lP84s6ficFpZPxrVjntBYqHX4h5lQbCA,10472
118
+ plain/server/util.py,sha256=vlTzH4jk8s-ZxJt0oAz-LaQZq_8lEk86zMDVClluuwY,8758
119
119
  plain/server/http/__init__.py,sha256=kQwTk1l3hYJwVrzr1p-XNAbWYe0icsD7l0ZyGRXMbOI,300
120
120
  plain/server/http/body.py,sha256=cz18F4C_gm9h5XvsStV0ncanBjZO1-pegbmbmiMpsQA,8352
121
121
  plain/server/http/errors.py,sha256=uqrzOzjjdqXfFim64pc58cxPlbsoxRNiLx0WzIJSzkw,3719
@@ -123,10 +123,10 @@ plain/server/http/message.py,sha256=5UdOA7CMqiXW_xy0c3d_rPHOvj5YQQHhbkx4Hf3Ttbc,
123
123
  plain/server/http/parser.py,sha256=TLcqdRS9_008n8MpgLORmjomyZKLuKNzdKA8Cej8mII,1681
124
124
  plain/server/http/unreader.py,sha256=jD2PGZ574FGmQOZlqWfs1VWzp-ttfIso_GzYaId6KYQ,2238
125
125
  plain/server/http/wsgi.py,sha256=D-wVKdgKppDR_ZQd834yXtju60t1Jg5bFc5QEr7Iz38,13897
126
- plain/server/workers/__init__.py,sha256=jRQzuvFZu3pMJqs4JgURZxhxlqBCNiO3do5uIuP8-JA,302
127
- plain/server/workers/base.py,sha256=O0-s-WS0AjTVD8YHGoIGUZupGfYAi9joUkJpwsvMTY0,9761
128
- plain/server/workers/gthread.py,sha256=6F_YfhrlPV3hmxwI8_-jgGq4pCmkQBKVO15Wrg-iQ7Y,13504
126
+ plain/server/workers/__init__.py,sha256=sLq8nrIIf9Wjw5_qQsh6cnHnY7eIqCpzSedKrNmmn7s,145
127
+ plain/server/workers/base.py,sha256=B0aniofq4lT5gYa82W34VTLvEQInOe43QvuG9X14vdE,9602
129
128
  plain/server/workers/sync.py,sha256=I28Icl1aKNIOlVaM76UOqQvjtSetkG-tdc5eiAvAmYA,7272
129
+ plain/server/workers/thread.py,sha256=6F_YfhrlPV3hmxwI8_-jgGq4pCmkQBKVO15Wrg-iQ7Y,13504
130
130
  plain/server/workers/workertmp.py,sha256=egGReVvldlOBQfQGcpLpjt0zvPwR4C_N-UJKG-U_6w4,1299
131
131
  plain/signals/README.md,sha256=XefXqROlDhzw7Z5l_nx6Mhq6n9jjQ-ECGbH0vvhKWYg,272
132
132
  plain/signals/__init__.py,sha256=eAs0kLqptuP6I31dWXeAqRNji3svplpAV4Ez6ktjwXM,131
@@ -188,8 +188,8 @@ plain/views/forms.py,sha256=ESZOXuo6IeYixp1RZvPb94KplkowRiwO2eGJCM6zJI0,2400
188
188
  plain/views/objects.py,sha256=5y0PoPPo07dQTTcJ_9kZcx0iI1O7regsooAIK4VqXQ0,5579
189
189
  plain/views/redirect.py,sha256=mIpSAFcaEyeLDyv4Fr6g_ektduG4Wfa6s6L-rkdazmM,2154
190
190
  plain/views/templates.py,sha256=9LgDMCv4C7JzLiyw8jHF-i4350ukwgixC_9y4faEGu0,1885
191
- plain-0.76.0.dist-info/METADATA,sha256=ypCMyEzKG6CwHD_QhK2NAYdWYeTNY_-9ZRaWe4RwB3o,4482
192
- plain-0.76.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
193
- plain-0.76.0.dist-info/entry_points.txt,sha256=wvMzY-iREvfqRgyLm77htPp4j_8CQslLoZA15_AnNo8,171
194
- plain-0.76.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
195
- plain-0.76.0.dist-info/RECORD,,
191
+ plain-0.77.0.dist-info/METADATA,sha256=EXhUsvn36qZfmmMwZ42LlEhEeKcH0k5YXo2WmoRCXss,4516
192
+ plain-0.77.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
193
+ plain-0.77.0.dist-info/entry_points.txt,sha256=wvMzY-iREvfqRgyLm77htPp4j_8CQslLoZA15_AnNo8,171
194
+ plain-0.77.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
195
+ plain-0.77.0.dist-info/RECORD,,
plain/server/reloader.py DELETED
@@ -1,158 +0,0 @@
1
- from __future__ import annotations
2
-
3
- #
4
- #
5
- # This file is part of gunicorn released under the MIT license.
6
- # See the LICENSE for more information.
7
- #
8
- # Vendored and modified for Plain.
9
- import os
10
- import os.path
11
- import re
12
- import sys
13
- import threading
14
- import time
15
- from collections.abc import Callable, Iterable
16
-
17
- COMPILED_EXT_RE = re.compile(r"py[co]$")
18
-
19
-
20
- class Reloader(threading.Thread):
21
- def __init__(
22
- self,
23
- extra_files: Iterable[str] | None = None,
24
- interval: int = 1,
25
- callback: Callable[[str], None] | None = None,
26
- ) -> None:
27
- super().__init__()
28
- self.daemon = True
29
- self._extra_files: set[str] = set(extra_files or ())
30
- self._interval = interval
31
- self._callback = callback
32
-
33
- def add_extra_file(self, filename: str) -> None:
34
- self._extra_files.add(filename)
35
-
36
- def get_files(self) -> list[str]:
37
- fnames = [
38
- COMPILED_EXT_RE.sub("py", module.__file__) # type: ignore[arg-type]
39
- for module in tuple(sys.modules.values())
40
- if getattr(module, "__file__", None)
41
- ]
42
-
43
- fnames.extend(self._extra_files)
44
-
45
- return fnames
46
-
47
- def run(self) -> None:
48
- mtimes: dict[str, float] = {}
49
- while True:
50
- for filename in self.get_files():
51
- try:
52
- mtime = os.stat(filename).st_mtime
53
- except OSError:
54
- continue
55
- old_time = mtimes.get(filename)
56
- if old_time is None:
57
- mtimes[filename] = mtime
58
- continue
59
- elif mtime > old_time:
60
- if self._callback:
61
- self._callback(filename)
62
- time.sleep(self._interval)
63
-
64
-
65
- has_inotify = False
66
- if sys.platform.startswith("linux"):
67
- try:
68
- import inotify.constants
69
- from inotify.adapters import Inotify
70
-
71
- has_inotify = True
72
- except ImportError:
73
- pass
74
-
75
-
76
- if has_inotify:
77
-
78
- class InotifyReloader(threading.Thread):
79
- event_mask = (
80
- inotify.constants.IN_CREATE
81
- | inotify.constants.IN_DELETE
82
- | inotify.constants.IN_DELETE_SELF
83
- | inotify.constants.IN_MODIFY
84
- | inotify.constants.IN_MOVE_SELF
85
- | inotify.constants.IN_MOVED_FROM
86
- | inotify.constants.IN_MOVED_TO
87
- )
88
-
89
- def __init__(
90
- self,
91
- extra_files: Iterable[str] | None = None,
92
- callback: Callable[[str], None] | None = None,
93
- ) -> None:
94
- super().__init__()
95
- self.daemon = True
96
- self._callback = callback
97
- self._dirs: set[str] = set()
98
- self._watcher = Inotify()
99
-
100
- if extra_files:
101
- for extra_file in extra_files:
102
- self.add_extra_file(extra_file)
103
-
104
- def add_extra_file(self, filename: str) -> None:
105
- dirname = os.path.dirname(filename)
106
-
107
- if dirname in self._dirs:
108
- return None
109
-
110
- self._watcher.add_watch(dirname, mask=self.event_mask)
111
- self._dirs.add(dirname)
112
-
113
- def get_dirs(self) -> set[str]:
114
- fnames = [
115
- os.path.dirname(
116
- os.path.abspath(COMPILED_EXT_RE.sub("py", module.__file__)) # type: ignore[arg-type]
117
- )
118
- for module in tuple(sys.modules.values())
119
- if getattr(module, "__file__", None)
120
- ]
121
-
122
- return set(fnames)
123
-
124
- def run(self) -> None:
125
- self._dirs = self.get_dirs()
126
-
127
- for dirname in self._dirs:
128
- if os.path.isdir(dirname):
129
- self._watcher.add_watch(dirname, mask=self.event_mask)
130
-
131
- for event in self._watcher.event_gen(): # type: ignore[attr-defined]
132
- if event is None:
133
- continue
134
-
135
- filename = event[3] # type: ignore[index]
136
-
137
- self._callback(filename) # type: ignore[misc]
138
-
139
- else:
140
-
141
- class InotifyReloader:
142
- def __init__(
143
- self,
144
- extra_files: Iterable[str] | None = None,
145
- callback: Callable[[str], None] | None = None,
146
- ) -> None:
147
- raise ImportError(
148
- "You must have the inotify module installed to use the inotify reloader"
149
- )
150
-
151
-
152
- preferred_reloader = InotifyReloader if has_inotify else Reloader
153
-
154
- reloader_engines = {
155
- "auto": preferred_reloader,
156
- "poll": Reloader,
157
- "inotify": InotifyReloader,
158
- }
File without changes
File without changes