plain.dev 0.38.0__tar.gz → 0.39.1__tar.gz
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.
- {plain_dev-0.38.0 → plain_dev-0.39.1}/PKG-INFO +1 -1
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/CHANGELOG.md +20 -0
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/alias.py +6 -6
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/cli.py +21 -9
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/contribute/cli.py +13 -13
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/core.py +17 -12
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/debug.py +3 -3
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/entrypoints.py +1 -1
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/mkcert.py +5 -4
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/pdb.py +24 -13
- plain_dev-0.39.1/plain/dev/poncho/__init__.py +0 -0
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/poncho/color.py +3 -1
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/poncho/compat.py +7 -7
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/poncho/manager.py +22 -14
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/poncho/printer.py +12 -11
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/poncho/process.py +16 -4
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/precommit/cli.py +4 -4
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/process.py +2 -1
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/services.py +3 -2
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/utils.py +1 -1
- {plain_dev-0.38.0 → plain_dev-0.39.1}/pyproject.toml +1 -1
- plain_dev-0.38.0/plain/dev/poncho/__init__.py +0 -4
- {plain_dev-0.38.0 → plain_dev-0.39.1}/.gitignore +0 -0
- {plain_dev-0.38.0 → plain_dev-0.39.1}/LICENSE +0 -0
- {plain_dev-0.38.0 → plain_dev-0.39.1}/README.md +0 -0
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/AGENTS.md +0 -0
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/README.md +0 -0
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/__init__.py +0 -0
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/contribute/README.md +0 -0
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/contribute/__init__.py +0 -0
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/default_settings.py +0 -0
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/gunicorn_logging.json +0 -0
- {plain_dev-0.38.0 → plain_dev-0.39.1}/plain/dev/precommit/__init__.py +0 -0
- {plain_dev-0.38.0 → plain_dev-0.39.1}/tests/settings.py +0 -0
@@ -1,5 +1,25 @@
|
|
1
1
|
# plain-dev changelog
|
2
2
|
|
3
|
+
## [0.39.1](https://github.com/dropseed/plain/releases/plain-dev@0.39.1) (2025-10-06)
|
4
|
+
|
5
|
+
### What's changed
|
6
|
+
|
7
|
+
- Added comprehensive type annotations across the entire package to improve IDE support and type checking ([1d00e9f](https://github.com/dropseed/plain/commit/1d00e9f6f))
|
8
|
+
|
9
|
+
### Upgrade instructions
|
10
|
+
|
11
|
+
- No changes required
|
12
|
+
|
13
|
+
## [0.39.0](https://github.com/dropseed/plain/releases/plain-dev@0.39.0) (2025-09-30)
|
14
|
+
|
15
|
+
### What's changed
|
16
|
+
|
17
|
+
- The `plain dev` command now sets both `PLAIN_LOG_LEVEL` and `PLAIN_FRAMEWORK_LOG_LEVEL` environment variables when a log level is specified, replacing the previous `APP_LOG_LEVEL` setting ([4c5f216](https://github.com/dropseed/plain/commit/4c5f2166c1))
|
18
|
+
|
19
|
+
### Upgrade instructions
|
20
|
+
|
21
|
+
- No changes required
|
22
|
+
|
3
23
|
## [0.38.0](https://github.com/dropseed/plain/releases/plain-dev@0.38.0) (2025-09-30)
|
4
24
|
|
5
25
|
### What's changed
|
@@ -15,7 +15,7 @@ class AliasManager:
|
|
15
15
|
ALIAS_NAME = "p"
|
16
16
|
|
17
17
|
@cached_property
|
18
|
-
def shell(self):
|
18
|
+
def shell(self) -> str | None:
|
19
19
|
"""Detect the current shell."""
|
20
20
|
shell = os.environ.get("SHELL", "")
|
21
21
|
if "zsh" in shell:
|
@@ -27,7 +27,7 @@ class AliasManager:
|
|
27
27
|
return None
|
28
28
|
|
29
29
|
@cached_property
|
30
|
-
def shell_config_file(self):
|
30
|
+
def shell_config_file(self) -> Path | None:
|
31
31
|
"""Get the appropriate shell configuration file."""
|
32
32
|
home = Path.home()
|
33
33
|
|
@@ -43,7 +43,7 @@ class AliasManager:
|
|
43
43
|
|
44
44
|
return None
|
45
45
|
|
46
|
-
def _command_exists(self, command):
|
46
|
+
def _command_exists(self, command: str) -> bool:
|
47
47
|
"""Check if a command exists in the system."""
|
48
48
|
try:
|
49
49
|
result = subprocess.run(
|
@@ -53,7 +53,7 @@ class AliasManager:
|
|
53
53
|
except Exception:
|
54
54
|
return False
|
55
55
|
|
56
|
-
def _alias_exists(self):
|
56
|
+
def _alias_exists(self) -> bool:
|
57
57
|
"""Check if the 'p' alias already exists."""
|
58
58
|
# First check if 'p' is already a command
|
59
59
|
if self._command_exists(self.ALIAS_NAME):
|
@@ -73,7 +73,7 @@ class AliasManager:
|
|
73
73
|
except (subprocess.TimeoutExpired, Exception):
|
74
74
|
return False
|
75
75
|
|
76
|
-
def _add_alias_to_shell(self):
|
76
|
+
def _add_alias_to_shell(self) -> bool:
|
77
77
|
"""Add the alias to the shell configuration file."""
|
78
78
|
if not self.shell_config_file or not self.shell_config_file.exists():
|
79
79
|
return False
|
@@ -106,7 +106,7 @@ class AliasManager:
|
|
106
106
|
)
|
107
107
|
return False
|
108
108
|
|
109
|
-
def check_and_prompt(self):
|
109
|
+
def check_and_prompt(self) -> None:
|
110
110
|
"""Check if alias exists and prompt user to set it up if needed."""
|
111
111
|
# Only suggest if project uses uv (has uv.lock file)
|
112
112
|
if not Path("uv.lock").exists():
|
@@ -3,6 +3,7 @@ import subprocess
|
|
3
3
|
import sys
|
4
4
|
import time
|
5
5
|
from importlib.metadata import entry_points
|
6
|
+
from typing import Any
|
6
7
|
|
7
8
|
import click
|
8
9
|
|
@@ -17,12 +18,12 @@ from .services import ServicesProcess
|
|
17
18
|
class DevGroup(click.Group):
|
18
19
|
"""Custom group that ensures *services* are running on CLI startup."""
|
19
20
|
|
20
|
-
def __init__(self, *args, **kwargs):
|
21
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
21
22
|
super().__init__(*args, **kwargs)
|
22
23
|
self._auto_start_services()
|
23
24
|
|
24
25
|
@staticmethod
|
25
|
-
def _auto_start_services():
|
26
|
+
def _auto_start_services() -> None:
|
26
27
|
"""Start dev *services* in the background if not already running."""
|
27
28
|
|
28
29
|
# Check if we're in CI and auto-start is not explicitly enabled
|
@@ -118,7 +119,14 @@ class DevGroup(click.Group):
|
|
118
119
|
default=False,
|
119
120
|
help="Stop the background process",
|
120
121
|
)
|
121
|
-
def cli(
|
122
|
+
def cli(
|
123
|
+
ctx: click.Context,
|
124
|
+
port: str,
|
125
|
+
hostname: str | None,
|
126
|
+
log_level: str,
|
127
|
+
start: bool,
|
128
|
+
stop: bool,
|
129
|
+
) -> None:
|
122
130
|
"""Start local development"""
|
123
131
|
if ctx.invoked_subcommand:
|
124
132
|
return
|
@@ -173,17 +181,21 @@ def cli(ctx, port, hostname, log_level, start, stop):
|
|
173
181
|
# Check and prompt for alias setup
|
174
182
|
AliasManager().check_and_prompt()
|
175
183
|
|
176
|
-
dev.setup(
|
184
|
+
dev.setup(
|
185
|
+
port=int(port) if port else None,
|
186
|
+
hostname=hostname,
|
187
|
+
log_level=log_level if log_level else None,
|
188
|
+
)
|
177
189
|
returncode = dev.run()
|
178
190
|
if returncode:
|
179
191
|
sys.exit(returncode)
|
180
192
|
|
181
193
|
|
182
194
|
@cli.command()
|
183
|
-
def debug():
|
195
|
+
def debug() -> None:
|
184
196
|
"""Connect to the remote debugger"""
|
185
197
|
|
186
|
-
def _connect():
|
198
|
+
def _connect() -> subprocess.CompletedProcess[bytes]:
|
187
199
|
if subprocess.run(["which", "nc"], capture_output=True).returncode == 0:
|
188
200
|
return subprocess.run(["nc", "-C", "localhost", "4444"])
|
189
201
|
else:
|
@@ -208,7 +220,7 @@ def debug():
|
|
208
220
|
@cli.command()
|
209
221
|
@click.option("--start", is_flag=True, help="Start in the background")
|
210
222
|
@click.option("--stop", is_flag=True, help="Stop the background process")
|
211
|
-
def services(start, stop):
|
223
|
+
def services(start: bool, stop: bool) -> None:
|
212
224
|
"""Start additional services defined in pyproject.toml"""
|
213
225
|
|
214
226
|
if start and stop:
|
@@ -248,7 +260,7 @@ def services(start, stop):
|
|
248
260
|
@click.option("--pid", type=int, help="PID to show logs for")
|
249
261
|
@click.option("--path", is_flag=True, help="Output log file path")
|
250
262
|
@click.option("--services", is_flag=True, help="Show logs for services")
|
251
|
-
def logs(follow, pid, path, services):
|
263
|
+
def logs(follow: bool, pid: int | None, path: bool, services: bool) -> None:
|
252
264
|
"""Show logs from recent plain dev runs."""
|
253
265
|
|
254
266
|
if services:
|
@@ -284,7 +296,7 @@ def logs(follow, pid, path, services):
|
|
284
296
|
"--list", "-l", "show_list", is_flag=True, help="List available entrypoints"
|
285
297
|
)
|
286
298
|
@click.argument("entrypoint", required=False)
|
287
|
-
def entrypoint(show_list, entrypoint):
|
299
|
+
def entrypoint(show_list: bool, entrypoint: str | None) -> None:
|
288
300
|
"""Entrypoints registered under plain.dev"""
|
289
301
|
if not show_list and not entrypoint:
|
290
302
|
raise click.UsageError("Please provide an entrypoint name or use --list")
|
@@ -17,7 +17,7 @@ from plain.cli import register_cli
|
|
17
17
|
"--all", "all_packages", is_flag=True, help="Link all installed plain packages"
|
18
18
|
)
|
19
19
|
@click.argument("packages", nargs=-1)
|
20
|
-
def cli(packages, repo, reset, all_packages):
|
20
|
+
def cli(packages: tuple[str, ...], repo: str, reset: bool, all_packages: bool) -> None:
|
21
21
|
"""Contribute to plain by linking packages locally."""
|
22
22
|
|
23
23
|
if reset:
|
@@ -35,11 +35,11 @@ def cli(packages, repo, reset, all_packages):
|
|
35
35
|
|
36
36
|
return
|
37
37
|
|
38
|
-
|
38
|
+
packages_list = list(packages)
|
39
39
|
|
40
|
-
|
41
|
-
if not
|
42
|
-
click.secho(f"Repo not found at {
|
40
|
+
repo_path = Path(repo)
|
41
|
+
if not repo_path.exists():
|
42
|
+
click.secho(f"Repo not found at {repo_path}", fg="red")
|
43
43
|
return
|
44
44
|
|
45
45
|
repo_branch = (
|
@@ -50,12 +50,12 @@ def cli(packages, repo, reset, all_packages):
|
|
50
50
|
"--abbrev-ref",
|
51
51
|
"HEAD",
|
52
52
|
],
|
53
|
-
cwd=
|
53
|
+
cwd=repo_path,
|
54
54
|
)
|
55
55
|
.decode()
|
56
56
|
.strip()
|
57
57
|
)
|
58
|
-
click.secho(f"Using repo at {
|
58
|
+
click.secho(f"Using repo at {repo_path} ({repo_branch} branch)", bold=True)
|
59
59
|
|
60
60
|
plain_packages = []
|
61
61
|
plainx_packages = []
|
@@ -70,7 +70,7 @@ def cli(packages, repo, reset, all_packages):
|
|
70
70
|
click.secho("No installed packages found", fg="red")
|
71
71
|
sys.exit(1)
|
72
72
|
|
73
|
-
|
73
|
+
packages_list = []
|
74
74
|
for line in installed_packages.splitlines():
|
75
75
|
if not line.startswith("plain"):
|
76
76
|
continue
|
@@ -78,7 +78,7 @@ def cli(packages, repo, reset, all_packages):
|
|
78
78
|
if package.startswith("plainx-"):
|
79
79
|
skipped_plainx_packages.append(package)
|
80
80
|
else:
|
81
|
-
|
81
|
+
packages_list.append(package)
|
82
82
|
|
83
83
|
if skipped_plainx_packages:
|
84
84
|
click.secho(
|
@@ -88,13 +88,13 @@ def cli(packages, repo, reset, all_packages):
|
|
88
88
|
fg="yellow",
|
89
89
|
)
|
90
90
|
|
91
|
-
for package in
|
91
|
+
for package in packages_list:
|
92
92
|
package = package.replace(".", "-")
|
93
|
-
click.secho(f"Linking {package} to {
|
93
|
+
click.secho(f"Linking {package} to {repo_path}", bold=True)
|
94
94
|
if package == "plain" or package.startswith("plain-"):
|
95
|
-
plain_packages.append(str(
|
95
|
+
plain_packages.append(str(repo_path / package))
|
96
96
|
elif package.startswith("plainx-"):
|
97
|
-
plainx_packages.append(str(
|
97
|
+
plainx_packages.append(str(repo_path))
|
98
98
|
else:
|
99
99
|
raise click.UsageError(f"Unknown package {package}")
|
100
100
|
|
@@ -26,7 +26,9 @@ class DevProcess(ProcessManager):
|
|
26
26
|
pidfile = PLAIN_TEMP_PATH / "dev" / "dev.pid"
|
27
27
|
log_dir = PLAIN_TEMP_PATH / "dev" / "logs" / "run"
|
28
28
|
|
29
|
-
def setup(
|
29
|
+
def setup(
|
30
|
+
self, *, port: int | None, hostname: str | None, log_level: str | None
|
31
|
+
) -> None:
|
30
32
|
if not hostname:
|
31
33
|
project_name = os.path.basename(
|
32
34
|
os.getcwd()
|
@@ -70,8 +72,8 @@ class DevProcess(ProcessManager):
|
|
70
72
|
}
|
71
73
|
|
72
74
|
if log_level:
|
75
|
+
self.plain_env["PLAIN_FRAMEWORK_LOG_LEVEL"] = log_level.upper()
|
73
76
|
self.plain_env["PLAIN_LOG_LEVEL"] = log_level.upper()
|
74
|
-
self.plain_env["APP_LOG_LEVEL"] = log_level.upper()
|
75
77
|
|
76
78
|
self.custom_process_env = {
|
77
79
|
**self.plain_env,
|
@@ -112,19 +114,19 @@ class DevProcess(ProcessManager):
|
|
112
114
|
|
113
115
|
self.init_poncho(self.console.out)
|
114
116
|
|
115
|
-
def _find_open_port(self, start_port):
|
117
|
+
def _find_open_port(self, start_port: int) -> int:
|
116
118
|
port = start_port
|
117
119
|
while not self._port_available(port):
|
118
120
|
port += 1
|
119
121
|
return port
|
120
122
|
|
121
|
-
def _port_available(self, port):
|
123
|
+
def _port_available(self, port: int) -> bool:
|
122
124
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
123
125
|
sock.settimeout(0.5)
|
124
126
|
result = sock.connect_ex(("127.0.0.1", port))
|
125
127
|
return result != 0
|
126
128
|
|
127
|
-
def run(self):
|
129
|
+
def run(self) -> int:
|
128
130
|
self.write_pidfile()
|
129
131
|
mkcert_manager = MkcertManager()
|
130
132
|
mkcert_manager.setup_mkcert(install_path=Path.home() / ".plain" / "dev")
|
@@ -181,9 +183,12 @@ class DevProcess(ProcessManager):
|
|
181
183
|
|
182
184
|
return self.poncho.returncode
|
183
185
|
|
184
|
-
def symlink_plain_src(self):
|
186
|
+
def symlink_plain_src(self) -> None:
|
185
187
|
"""Symlink the plain package into .plain so we can look at it easily"""
|
186
|
-
|
188
|
+
spec = find_spec("plain.runtime")
|
189
|
+
if spec is None or spec.origin is None:
|
190
|
+
return None
|
191
|
+
plain_path = Path(spec.origin).parent.parent
|
187
192
|
if not PLAIN_TEMP_PATH.exists():
|
188
193
|
PLAIN_TEMP_PATH.mkdir()
|
189
194
|
|
@@ -204,7 +209,7 @@ class DevProcess(ProcessManager):
|
|
204
209
|
if plain_path.exists() and not symlink_path.exists():
|
205
210
|
symlink_path.symlink_to(plain_path)
|
206
211
|
|
207
|
-
def modify_hosts_file(self):
|
212
|
+
def modify_hosts_file(self) -> None:
|
208
213
|
"""Modify the hosts file to map the custom domain to 127.0.0.1."""
|
209
214
|
entry_identifier = "# Added by plain"
|
210
215
|
hosts_entry = f"127.0.0.1 {self.hostname} {entry_identifier}"
|
@@ -259,14 +264,14 @@ class DevProcess(ProcessManager):
|
|
259
264
|
)
|
260
265
|
sys.exit(1)
|
261
266
|
|
262
|
-
def run_preflight(self):
|
267
|
+
def run_preflight(self) -> None:
|
263
268
|
if subprocess.run(
|
264
269
|
["plain", "preflight", "check", "--quiet"], env=self.plain_env
|
265
270
|
).returncode:
|
266
271
|
click.secho("Preflight check failed!", fg="red")
|
267
272
|
sys.exit(1)
|
268
273
|
|
269
|
-
def add_gunicorn(self):
|
274
|
+
def add_gunicorn(self) -> None:
|
270
275
|
# Watch .env files for reload
|
271
276
|
extra_watch_files = []
|
272
277
|
for f in os.listdir(APP_PATH.parent):
|
@@ -306,7 +311,7 @@ class DevProcess(ProcessManager):
|
|
306
311
|
|
307
312
|
self.poncho.add_process("plain", gunicorn, env=self.plain_env)
|
308
313
|
|
309
|
-
def add_entrypoints(self):
|
314
|
+
def add_entrypoints(self) -> None:
|
310
315
|
for entry_point in entry_points().select(group=ENTRYPOINT_GROUP):
|
311
316
|
self.poncho.add_process(
|
312
317
|
entry_point.name,
|
@@ -314,7 +319,7 @@ class DevProcess(ProcessManager):
|
|
314
319
|
env=self.plain_env,
|
315
320
|
)
|
316
321
|
|
317
|
-
def add_pyproject_run(self):
|
322
|
+
def add_pyproject_run(self) -> None:
|
318
323
|
"""Additional processes that only run during `plain dev`."""
|
319
324
|
if not has_pyproject_toml(APP_PATH.parent):
|
320
325
|
return
|
@@ -7,7 +7,7 @@ from pathlib import Path
|
|
7
7
|
from . import pdb
|
8
8
|
|
9
9
|
|
10
|
-
def set_breakpoint_hook():
|
10
|
+
def set_breakpoint_hook() -> None:
|
11
11
|
"""
|
12
12
|
If a `plain dev` process is running, set a
|
13
13
|
breakpoint hook to trigger a remote debugger.
|
@@ -18,7 +18,7 @@ def set_breakpoint_hook():
|
|
18
18
|
# we're in a process managed by `plain dev`
|
19
19
|
return
|
20
20
|
|
21
|
-
def _breakpoint():
|
21
|
+
def _breakpoint() -> None:
|
22
22
|
system = platform.system()
|
23
23
|
|
24
24
|
if system == "Darwin":
|
@@ -38,4 +38,4 @@ def set_breakpoint_hook():
|
|
38
38
|
frame=sys._getframe().f_back,
|
39
39
|
)
|
40
40
|
|
41
|
-
sys.breakpointhook = _breakpoint
|
41
|
+
sys.breakpointhook = _breakpoint # type: ignore[assignment]
|
@@ -4,15 +4,16 @@ import subprocess
|
|
4
4
|
import sys
|
5
5
|
import time
|
6
6
|
import urllib.request
|
7
|
+
from pathlib import Path
|
7
8
|
|
8
9
|
import click
|
9
10
|
|
10
11
|
|
11
12
|
class MkcertManager:
|
12
|
-
def __init__(self):
|
13
|
+
def __init__(self) -> None:
|
13
14
|
self.mkcert_bin = None
|
14
15
|
|
15
|
-
def setup_mkcert(self, install_path):
|
16
|
+
def setup_mkcert(self, install_path: Path) -> None:
|
16
17
|
"""Set up mkcert by checking if it's installed or downloading the binary and installing the local CA."""
|
17
18
|
if mkcert_path := shutil.which("mkcert"):
|
18
19
|
# mkcert is already installed somewhere
|
@@ -59,7 +60,7 @@ class MkcertManager:
|
|
59
60
|
)
|
60
61
|
subprocess.run([self.mkcert_bin, "-install"], check=True)
|
61
62
|
|
62
|
-
def is_mkcert_ca_installed(self):
|
63
|
+
def is_mkcert_ca_installed(self) -> bool:
|
63
64
|
"""Check if mkcert local CA is already installed using mkcert -check."""
|
64
65
|
try:
|
65
66
|
result = subprocess.run([self.mkcert_bin, "-check"], capture_output=True)
|
@@ -71,7 +72,7 @@ class MkcertManager:
|
|
71
72
|
click.secho(f"Error checking mkcert CA installation: {e}", fg="red")
|
72
73
|
return False
|
73
74
|
|
74
|
-
def generate_certs(self, domain, storage_path):
|
75
|
+
def generate_certs(self, domain: str, storage_path: Path) -> tuple[Path, Path]:
|
75
76
|
cert_path = storage_path / f"{domain}-cert.pem"
|
76
77
|
key_path = storage_path / f"{domain}-key.pem"
|
77
78
|
timestamp_path = storage_path / f"{domain}.timestamp"
|
@@ -3,19 +3,22 @@ import logging
|
|
3
3
|
import re
|
4
4
|
import socket
|
5
5
|
import sys
|
6
|
+
from collections.abc import Iterator
|
6
7
|
from pdb import Pdb
|
8
|
+
from types import FrameType
|
9
|
+
from typing import Any
|
7
10
|
|
8
11
|
log = logging.getLogger(__name__)
|
9
12
|
|
10
13
|
|
11
|
-
def cry(message, stderr=sys.__stderr__):
|
14
|
+
def cry(message: str, stderr: Any = sys.__stderr__) -> None:
|
12
15
|
log.critical(message)
|
13
16
|
print(message, file=stderr)
|
14
|
-
stderr.flush()
|
17
|
+
stderr.flush() # type: ignore[possibly-unbound]
|
15
18
|
|
16
19
|
|
17
20
|
class LF2CRLF_FileWrapper:
|
18
|
-
def __init__(self, connection):
|
21
|
+
def __init__(self, connection: socket.socket) -> None:
|
19
22
|
self.connection = connection
|
20
23
|
self.stream = fh = connection.makefile("rw")
|
21
24
|
self.read = fh.read
|
@@ -30,17 +33,19 @@ class LF2CRLF_FileWrapper:
|
|
30
33
|
self._send = connection.sendall
|
31
34
|
|
32
35
|
@property
|
33
|
-
def encoding(self):
|
36
|
+
def encoding(self) -> str | None:
|
34
37
|
return self.stream.encoding
|
35
38
|
|
36
|
-
def __iter__(self):
|
39
|
+
def __iter__(self) -> Iterator[str]:
|
37
40
|
return self.stream.__iter__()
|
38
41
|
|
39
|
-
def write(self, data, nl_rex=re.compile("\r?\n")):
|
42
|
+
def write(self, data: str, nl_rex: re.Pattern[str] = re.compile("\r?\n")) -> None:
|
40
43
|
data = nl_rex.sub("\r\n", data)
|
41
44
|
self._send(data)
|
42
45
|
|
43
|
-
def writelines(
|
46
|
+
def writelines(
|
47
|
+
self, lines: list[str], nl_rex: re.Pattern[str] = re.compile("\r?\n")
|
48
|
+
) -> None:
|
44
49
|
for line in lines:
|
45
50
|
self.write(line, nl_rex)
|
46
51
|
|
@@ -62,7 +67,9 @@ class DevPdb(Pdb):
|
|
62
67
|
|
63
68
|
active_instance = None
|
64
69
|
|
65
|
-
def __init__(
|
70
|
+
def __init__(
|
71
|
+
self, host: str, port: int, patch_stdstreams: bool = False, quiet: bool = False
|
72
|
+
) -> None:
|
66
73
|
self._quiet = quiet
|
67
74
|
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
68
75
|
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
|
@@ -93,7 +100,7 @@ class DevPdb(Pdb):
|
|
93
100
|
setattr(sys, name, self.handle)
|
94
101
|
DevPdb.active_instance = self
|
95
102
|
|
96
|
-
def __restore(self):
|
103
|
+
def __restore(self) -> None:
|
97
104
|
if self.backup and not self._quiet:
|
98
105
|
cry(f"Restoring streams: {self.backup} ...")
|
99
106
|
for name, fh in self.backup:
|
@@ -101,13 +108,13 @@ class DevPdb(Pdb):
|
|
101
108
|
self.handle.close()
|
102
109
|
DevPdb.active_instance = None
|
103
110
|
|
104
|
-
def do_quit(self, arg):
|
111
|
+
def do_quit(self, arg: str) -> Any:
|
105
112
|
self.__restore()
|
106
113
|
return Pdb.do_quit(self, arg)
|
107
114
|
|
108
115
|
do_q = do_exit = do_quit
|
109
116
|
|
110
|
-
def set_trace(self, frame=None):
|
117
|
+
def set_trace(self, frame: FrameType | None = None) -> None:
|
111
118
|
if frame is None:
|
112
119
|
frame = sys._getframe().f_back
|
113
120
|
try:
|
@@ -118,8 +125,12 @@ class DevPdb(Pdb):
|
|
118
125
|
|
119
126
|
|
120
127
|
def set_trace(
|
121
|
-
frame
|
122
|
-
|
128
|
+
frame: FrameType | None = None,
|
129
|
+
host: str = "127.0.0.1",
|
130
|
+
port: int = 4444,
|
131
|
+
patch_stdstreams: bool = False,
|
132
|
+
quiet: bool = False,
|
133
|
+
) -> None:
|
123
134
|
"""
|
124
135
|
Opens a remote PDB over a host:port.
|
125
136
|
"""
|
File without changes
|
@@ -1,3 +1,5 @@
|
|
1
|
+
from collections.abc import Iterator
|
2
|
+
|
1
3
|
ANSI_COLOURS = ["grey", "red", "green", "yellow", "blue", "magenta", "cyan", "white"]
|
2
4
|
|
3
5
|
for i, name in enumerate(ANSI_COLOURS):
|
@@ -5,7 +7,7 @@ for i, name in enumerate(ANSI_COLOURS):
|
|
5
7
|
globals()["intense_" + name] = str(30 + i) + ";1"
|
6
8
|
|
7
9
|
|
8
|
-
def get_colors():
|
10
|
+
def get_colors() -> Iterator[str]:
|
9
11
|
cs = [
|
10
12
|
"cyan",
|
11
13
|
"yellow",
|
@@ -18,15 +18,15 @@ if ON_WINDOWS:
|
|
18
18
|
class ProcessManager:
|
19
19
|
if ON_WINDOWS:
|
20
20
|
|
21
|
-
def terminate(self, pid):
|
21
|
+
def terminate(self, pid: int) -> None:
|
22
22
|
# The first argument to OpenProcess represents the desired access
|
23
23
|
# to the process. 1 represents the PROCESS_TERMINATE access right.
|
24
|
-
handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
|
25
|
-
ctypes.windll.kernel32.TerminateProcess(handle, -1)
|
26
|
-
ctypes.windll.kernel32.CloseHandle(handle)
|
24
|
+
handle = ctypes.windll.kernel32.OpenProcess(1, False, pid) # type: ignore[attr-defined]
|
25
|
+
ctypes.windll.kernel32.TerminateProcess(handle, -1) # type: ignore[attr-defined]
|
26
|
+
ctypes.windll.kernel32.CloseHandle(handle) # type: ignore[attr-defined]
|
27
27
|
else:
|
28
28
|
|
29
|
-
def terminate(self, pid):
|
29
|
+
def terminate(self, pid: int) -> None:
|
30
30
|
try:
|
31
31
|
os.killpg(pid, signal.SIGTERM)
|
32
32
|
except OSError as e:
|
@@ -35,12 +35,12 @@ class ProcessManager:
|
|
35
35
|
|
36
36
|
if ON_WINDOWS:
|
37
37
|
|
38
|
-
def kill(self, pid):
|
38
|
+
def kill(self, pid: int) -> None:
|
39
39
|
# There's no SIGKILL on Win32...
|
40
40
|
self.terminate(pid)
|
41
41
|
else:
|
42
42
|
|
43
|
-
def kill(self, pid):
|
43
|
+
def kill(self, pid: int) -> None:
|
44
44
|
try:
|
45
45
|
os.killpg(pid, signal.SIGKILL)
|
46
46
|
except OSError as e:
|
@@ -2,6 +2,7 @@ import datetime
|
|
2
2
|
import multiprocessing
|
3
3
|
import queue
|
4
4
|
import signal
|
5
|
+
from types import FrameType
|
5
6
|
|
6
7
|
from .color import get_colors
|
7
8
|
from .compat import ProcessManager
|
@@ -45,7 +46,7 @@ class Manager:
|
|
45
46
|
#: this will contain a return code that can be used with `sys.exit`.
|
46
47
|
returncode = None
|
47
48
|
|
48
|
-
def __init__(self, printer=None):
|
49
|
+
def __init__(self, printer: Printer | None = None) -> None:
|
49
50
|
self.events = multiprocessing.Queue()
|
50
51
|
self.returncode = None
|
51
52
|
|
@@ -61,7 +62,14 @@ class Manager:
|
|
61
62
|
|
62
63
|
self._terminating = False
|
63
64
|
|
64
|
-
def add_process(
|
65
|
+
def add_process(
|
66
|
+
self,
|
67
|
+
name: str,
|
68
|
+
cmd: str,
|
69
|
+
quiet: bool = False,
|
70
|
+
env: dict[str, str] | None = None,
|
71
|
+
cwd: str | None = None,
|
72
|
+
) -> Process:
|
65
73
|
"""
|
66
74
|
Add a process to this manager instance. The process will not be started
|
67
75
|
until :func:`~poncho.manager.Manager.loop` is called.
|
@@ -82,13 +90,13 @@ class Manager:
|
|
82
90
|
|
83
91
|
return proc
|
84
92
|
|
85
|
-
def num_processes(self):
|
93
|
+
def num_processes(self) -> int:
|
86
94
|
"""
|
87
95
|
Return the number of processes managed by this instance.
|
88
96
|
"""
|
89
97
|
return len(self._processes)
|
90
98
|
|
91
|
-
def loop(self):
|
99
|
+
def loop(self) -> None:
|
92
100
|
"""
|
93
101
|
Start all the added processes and multiplex their output onto the bound
|
94
102
|
printer (which by default will print to STDOUT).
|
@@ -99,7 +107,7 @@ class Manager:
|
|
99
107
|
This method will block until all the processes have terminated.
|
100
108
|
"""
|
101
109
|
|
102
|
-
def _terminate(signum, frame):
|
110
|
+
def _terminate(signum: int, frame: FrameType | None) -> None:
|
103
111
|
self._system_print("{} received\n".format(SIGNALS[signum]["name"]))
|
104
112
|
self.returncode = SIGNALS[signum]["rc"]
|
105
113
|
self.terminate()
|
@@ -149,7 +157,7 @@ class Manager:
|
|
149
157
|
if waiting > datetime.timedelta(seconds=KILL_WAIT):
|
150
158
|
self.kill()
|
151
159
|
|
152
|
-
def terminate(self):
|
160
|
+
def terminate(self) -> None:
|
153
161
|
"""
|
154
162
|
Terminate all processes managed by this ProcessManager.
|
155
163
|
"""
|
@@ -158,13 +166,13 @@ class Manager:
|
|
158
166
|
self._terminating = True
|
159
167
|
self._killall()
|
160
168
|
|
161
|
-
def kill(self):
|
169
|
+
def kill(self) -> None:
|
162
170
|
"""
|
163
171
|
Kill all processes managed by this ProcessManager.
|
164
172
|
"""
|
165
173
|
self._killall(force=True)
|
166
174
|
|
167
|
-
def _killall(self, force=False):
|
175
|
+
def _killall(self, force: bool = False) -> None:
|
168
176
|
"""Kill all remaining processes, forcefully if requested."""
|
169
177
|
for_termination = []
|
170
178
|
|
@@ -183,26 +191,26 @@ class Manager:
|
|
183
191
|
else:
|
184
192
|
self._procmgr.terminate(p["pid"])
|
185
193
|
|
186
|
-
def _start_process(self, name):
|
194
|
+
def _start_process(self, name: str) -> None:
|
187
195
|
p = self._processes[name]
|
188
196
|
p["process"] = multiprocessing.Process(
|
189
197
|
name=name, target=p["obj"].run, args=(self.events, True)
|
190
198
|
)
|
191
199
|
p["process"].start()
|
192
200
|
|
193
|
-
def _is_running(self):
|
201
|
+
def _is_running(self) -> bool:
|
194
202
|
return any(p.get("pid") is not None for _, p in self._processes.items())
|
195
203
|
|
196
|
-
def _all_started(self):
|
204
|
+
def _all_started(self) -> bool:
|
197
205
|
return all(p.get("pid") is not None for _, p in self._processes.items())
|
198
206
|
|
199
|
-
def _all_stopped(self):
|
207
|
+
def _all_stopped(self) -> bool:
|
200
208
|
return all(p.get("returncode") is not None for _, p in self._processes.items())
|
201
209
|
|
202
|
-
def _any_stopped(self):
|
210
|
+
def _any_stopped(self) -> bool:
|
203
211
|
return any(p.get("returncode") is not None for _, p in self._processes.items())
|
204
212
|
|
205
|
-
def _system_print(self, data):
|
213
|
+
def _system_print(self, data: str) -> None:
|
206
214
|
self._printer.write(
|
207
215
|
Message(
|
208
216
|
type="line",
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import re
|
2
2
|
from collections import namedtuple
|
3
3
|
from pathlib import Path
|
4
|
+
from typing import Any
|
4
5
|
|
5
6
|
Message = namedtuple("Message", "type data time name color")
|
6
7
|
|
@@ -14,13 +15,13 @@ class Printer:
|
|
14
15
|
|
15
16
|
def __init__(
|
16
17
|
self,
|
17
|
-
print_func,
|
18
|
-
time_format="%H:%M:%S",
|
19
|
-
width=0,
|
20
|
-
color=True,
|
21
|
-
prefix=True,
|
22
|
-
log_file=None,
|
23
|
-
):
|
18
|
+
print_func: Any,
|
19
|
+
time_format: str = "%H:%M:%S",
|
20
|
+
width: int = 0,
|
21
|
+
color: bool = True,
|
22
|
+
prefix: bool = True,
|
23
|
+
log_file: Path | str | None = None,
|
24
|
+
) -> None:
|
24
25
|
self.print_func = print_func
|
25
26
|
self.time_format = time_format
|
26
27
|
self.width = width
|
@@ -33,7 +34,7 @@ class Printer:
|
|
33
34
|
else:
|
34
35
|
self.log_file = None
|
35
36
|
|
36
|
-
def write(self, message):
|
37
|
+
def write(self, message: Message) -> None:
|
37
38
|
if message.type != "line":
|
38
39
|
raise RuntimeError('Printer can only process messages of type "line"')
|
39
40
|
|
@@ -72,13 +73,13 @@ class Printer:
|
|
72
73
|
self.log_file.write(plain + "\n")
|
73
74
|
self.log_file.flush()
|
74
75
|
|
75
|
-
def close(self):
|
76
|
+
def close(self) -> None:
|
76
77
|
if self.log_file and hasattr(self.log_file, "close"):
|
77
78
|
self.log_file.close()
|
78
79
|
|
79
80
|
|
80
|
-
def _color_string(color, s):
|
81
|
-
def _ansi(code):
|
81
|
+
def _color_string(color: str, s: str) -> str:
|
82
|
+
def _ansi(code: str | int) -> str:
|
82
83
|
return f"\033[{code}m"
|
83
84
|
|
84
85
|
return f"{_ansi(0)}{_ansi(color)}{s}{_ansi(0)}"
|
@@ -2,6 +2,8 @@ import datetime
|
|
2
2
|
import os
|
3
3
|
import signal
|
4
4
|
import subprocess
|
5
|
+
from queue import Queue
|
6
|
+
from typing import Any
|
5
7
|
|
6
8
|
from .compat import ON_WINDOWS
|
7
9
|
from .printer import Message
|
@@ -14,7 +16,15 @@ class Process:
|
|
14
16
|
lifecycle events and output to a queue.
|
15
17
|
"""
|
16
18
|
|
17
|
-
def __init__(
|
19
|
+
def __init__(
|
20
|
+
self,
|
21
|
+
cmd: str,
|
22
|
+
name: str | None = None,
|
23
|
+
color: str | None = None,
|
24
|
+
quiet: bool = False,
|
25
|
+
env: dict[str, str] | None = None,
|
26
|
+
cwd: str | None = None,
|
27
|
+
) -> None:
|
18
28
|
self.cmd = cmd
|
19
29
|
self.color = color
|
20
30
|
self.quiet = quiet
|
@@ -26,7 +36,9 @@ class Process:
|
|
26
36
|
self._child = None
|
27
37
|
self._child_ctor = Popen
|
28
38
|
|
29
|
-
def run(
|
39
|
+
def run(
|
40
|
+
self, events: Queue[Message] | None = None, ignore_signals: bool = False
|
41
|
+
) -> None:
|
30
42
|
self._events = events
|
31
43
|
self._child = self._child_ctor(self.cmd, env=self.env, cwd=self.cwd)
|
32
44
|
self._send_message({"pid": self._child.pid}, type="start")
|
@@ -46,7 +58,7 @@ class Process:
|
|
46
58
|
|
47
59
|
self._send_message({"returncode": self._child.returncode}, type="stop")
|
48
60
|
|
49
|
-
def _send_message(self, data, type="line"):
|
61
|
+
def _send_message(self, data: bytes | dict[str, Any], type: str = "line") -> None:
|
50
62
|
if self._events is not None:
|
51
63
|
self._events.put(
|
52
64
|
Message(
|
@@ -60,7 +72,7 @@ class Process:
|
|
60
72
|
|
61
73
|
|
62
74
|
class Popen(subprocess.Popen):
|
63
|
-
def __init__(self, cmd, **kwargs):
|
75
|
+
def __init__(self, cmd: str, **kwargs: Any) -> None:
|
64
76
|
start_new_session = kwargs.pop("start_new_session", True)
|
65
77
|
options = {
|
66
78
|
"stdout": subprocess.PIPE,
|
@@ -11,7 +11,7 @@ from plain.cli import register_cli
|
|
11
11
|
from plain.cli.print import print_event
|
12
12
|
|
13
13
|
|
14
|
-
def install_git_hook():
|
14
|
+
def install_git_hook() -> None:
|
15
15
|
hook_path = os.path.join(".git", "hooks", "pre-commit")
|
16
16
|
if os.path.exists(hook_path):
|
17
17
|
print("pre-commit hook already exists")
|
@@ -28,7 +28,7 @@ plain pre-commit"""
|
|
28
28
|
@register_cli("pre-commit")
|
29
29
|
@click.command()
|
30
30
|
@click.option("--install", is_flag=True)
|
31
|
-
def cli(install):
|
31
|
+
def cli(install: bool) -> None:
|
32
32
|
"""Git pre-commit checks"""
|
33
33
|
if install:
|
34
34
|
install_git_hook()
|
@@ -96,7 +96,7 @@ def cli(install):
|
|
96
96
|
sys.exit(result.returncode)
|
97
97
|
|
98
98
|
|
99
|
-
def plain_db_connected():
|
99
|
+
def plain_db_connected() -> bool:
|
100
100
|
result = subprocess.run(
|
101
101
|
[
|
102
102
|
"plain",
|
@@ -109,7 +109,7 @@ def plain_db_connected():
|
|
109
109
|
return result.returncode == 0
|
110
110
|
|
111
111
|
|
112
|
-
def check_short(message, *args):
|
112
|
+
def check_short(message: str, *args: str) -> None:
|
113
113
|
print_event(message, newline=False)
|
114
114
|
result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
115
115
|
if result.returncode != 0:
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import os
|
2
2
|
import time
|
3
3
|
from pathlib import Path
|
4
|
+
from typing import Any
|
4
5
|
|
5
6
|
from .poncho.manager import Manager as PonchoManager
|
6
7
|
from .poncho.printer import Printer
|
@@ -110,7 +111,7 @@ class ProcessManager:
|
|
110
111
|
self.log_path = self.log_dir / f"{self.pid}.log"
|
111
112
|
return self.log_path
|
112
113
|
|
113
|
-
def init_poncho(self, print_func) -> PonchoManager: # noqa: D401
|
114
|
+
def init_poncho(self, print_func: Any) -> PonchoManager: # noqa: D401
|
114
115
|
"""Return a :class:`~plain.dev.poncho.manager.Manager` instance."""
|
115
116
|
if self.log_path is None:
|
116
117
|
self.prepare_log()
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import os
|
2
2
|
import tomllib
|
3
3
|
from pathlib import Path
|
4
|
+
from typing import Any
|
4
5
|
|
5
6
|
from plain.runtime import APP_PATH, PLAIN_TEMP_PATH
|
6
7
|
|
@@ -13,7 +14,7 @@ class ServicesProcess(ProcessManager):
|
|
13
14
|
log_dir = PLAIN_TEMP_PATH / "dev" / "logs" / "services"
|
14
15
|
|
15
16
|
@staticmethod
|
16
|
-
def get_services(root):
|
17
|
+
def get_services(root: str | Path) -> dict[str, Any]:
|
17
18
|
if not has_pyproject_toml(root):
|
18
19
|
return {}
|
19
20
|
|
@@ -27,7 +28,7 @@ class ServicesProcess(ProcessManager):
|
|
27
28
|
.get("services", {})
|
28
29
|
)
|
29
30
|
|
30
|
-
def run(self):
|
31
|
+
def run(self) -> None:
|
31
32
|
self.write_pidfile()
|
32
33
|
self.prepare_log()
|
33
34
|
self.init_poncho(print)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|