plain.dev 0.39.0__tar.gz → 0.40.0__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.
Files changed (34) hide show
  1. {plain_dev-0.39.0 → plain_dev-0.40.0}/PKG-INFO +2 -2
  2. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/CHANGELOG.md +20 -0
  3. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/README.md +1 -1
  4. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/alias.py +6 -6
  5. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/cli.py +21 -9
  6. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/contribute/cli.py +13 -13
  7. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/core.py +16 -11
  8. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/debug.py +3 -3
  9. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/entrypoints.py +1 -1
  10. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/mkcert.py +5 -4
  11. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/pdb.py +24 -13
  12. plain_dev-0.40.0/plain/dev/poncho/__init__.py +0 -0
  13. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/poncho/color.py +3 -1
  14. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/poncho/compat.py +7 -7
  15. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/poncho/manager.py +22 -14
  16. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/poncho/printer.py +12 -11
  17. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/poncho/process.py +16 -4
  18. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/precommit/cli.py +4 -4
  19. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/process.py +2 -1
  20. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/services.py +3 -2
  21. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/utils.py +1 -1
  22. {plain_dev-0.39.0 → plain_dev-0.40.0}/pyproject.toml +1 -1
  23. plain_dev-0.39.0/plain/dev/poncho/__init__.py +0 -4
  24. {plain_dev-0.39.0 → plain_dev-0.40.0}/.gitignore +0 -0
  25. {plain_dev-0.39.0 → plain_dev-0.40.0}/LICENSE +0 -0
  26. {plain_dev-0.39.0 → plain_dev-0.40.0}/README.md +0 -0
  27. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/AGENTS.md +0 -0
  28. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/__init__.py +0 -0
  29. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/contribute/README.md +0 -0
  30. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/contribute/__init__.py +0 -0
  31. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/default_settings.py +0 -0
  32. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/gunicorn_logging.json +0 -0
  33. {plain_dev-0.39.0 → plain_dev-0.40.0}/plain/dev/precommit/__init__.py +0 -0
  34. {plain_dev-0.39.0 → plain_dev-0.40.0}/tests/settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.dev
3
- Version: 0.39.0
3
+ Version: 0.40.0
4
4
  Summary: A single command that runs everything you need for local development.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-Expression: BSD-3-Clause
@@ -78,7 +78,7 @@ postgres = {cmd = "docker run --name app-postgres --rm -p 54321:5432 -v $(pwd)/.
78
78
 
79
79
  #### Custom processes
80
80
 
81
- Unlike [services](#services), custom processes are _only_ run during `plain dev`. This is a good place to run something like [ngrok](https://ngrok.com/) or a [Plain worker](../../../plain-worker), which you might need to use your local site, but don't need running for executing tests, for example.
81
+ Unlike [services](#services), custom processes are _only_ run during `plain dev`. This is a good place to run something like [ngrok](https://ngrok.com/) or a [Plain job worker](../../../plain-jobs), which you might need to use your local site, but don't need running for executing tests, for example.
82
82
 
83
83
  ```toml
84
84
  # pyproject.toml
@@ -1,5 +1,25 @@
1
1
  # plain-dev changelog
2
2
 
3
+ ## [0.40.0](https://github.com/dropseed/plain/releases/plain-dev@0.40.0) (2025-10-10)
4
+
5
+ ### What's changed
6
+
7
+ - Updated documentation to reference `plain-jobs` instead of the deprecated `plain-worker` package name ([24219856e0](https://github.com/dropseed/plain/commit/24219856e0))
8
+
9
+ ### Upgrade instructions
10
+
11
+ - No changes required
12
+
13
+ ## [0.39.1](https://github.com/dropseed/plain/releases/plain-dev@0.39.1) (2025-10-06)
14
+
15
+ ### What's changed
16
+
17
+ - Added comprehensive type annotations across the entire package to improve IDE support and type checking ([1d00e9f](https://github.com/dropseed/plain/commit/1d00e9f6f))
18
+
19
+ ### Upgrade instructions
20
+
21
+ - No changes required
22
+
3
23
  ## [0.39.0](https://github.com/dropseed/plain/releases/plain-dev@0.39.0) (2025-09-30)
4
24
 
5
25
  ### What's changed
@@ -61,7 +61,7 @@ postgres = {cmd = "docker run --name app-postgres --rm -p 54321:5432 -v $(pwd)/.
61
61
 
62
62
  #### Custom processes
63
63
 
64
- Unlike [services](#services), custom processes are _only_ run during `plain dev`. This is a good place to run something like [ngrok](https://ngrok.com/) or a [Plain worker](../../../plain-worker), which you might need to use your local site, but don't need running for executing tests, for example.
64
+ Unlike [services](#services), custom processes are _only_ run during `plain dev`. This is a good place to run something like [ngrok](https://ngrok.com/) or a [Plain job worker](../../../plain-jobs), which you might need to use your local site, but don't need running for executing tests, for example.
65
65
 
66
66
  ```toml
67
67
  # pyproject.toml
@@ -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(ctx, port, hostname, log_level, start, stop):
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(port=port, hostname=hostname, log_level=log_level)
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
- packages = list(packages)
38
+ packages_list = list(packages)
39
39
 
40
- repo = Path(repo)
41
- if not repo.exists():
42
- click.secho(f"Repo not found at {repo}", fg="red")
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=repo,
53
+ cwd=repo_path,
54
54
  )
55
55
  .decode()
56
56
  .strip()
57
57
  )
58
- click.secho(f"Using repo at {repo} ({repo_branch} branch)", bold=True)
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
- packages = []
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
- packages.append(package)
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 packages:
91
+ for package in packages_list:
92
92
  package = package.replace(".", "-")
93
- click.secho(f"Linking {package} to {repo}", bold=True)
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(repo / package))
95
+ plain_packages.append(str(repo_path / package))
96
96
  elif package.startswith("plainx-"):
97
- plainx_packages.append(str(repo))
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(self, *, port, hostname, log_level):
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()
@@ -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
- plain_path = Path(find_spec("plain.runtime").origin).parent.parent
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]
@@ -5,7 +5,7 @@ from dotenv import load_dotenv
5
5
  from .debug import set_breakpoint_hook
6
6
 
7
7
 
8
- def setup():
8
+ def setup() -> None:
9
9
  # Make sure our clis are registered
10
10
  # since this isn't an installed app
11
11
  from .cli import cli # noqa
@@ -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(self, lines, nl_rex=re.compile("\r?\n")):
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__(self, host, port, patch_stdstreams=False, quiet=False):
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=None, host="127.0.0.1", port=4444, patch_stdstreams=False, quiet=False
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(self, name, cmd, quiet=False, env=None, cwd=None):
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__(self, cmd, name=None, color=None, quiet=False, env=None, cwd=None):
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(self, events=None, ignore_signals=False):
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)
@@ -1,5 +1,5 @@
1
1
  from pathlib import Path
2
2
 
3
3
 
4
- def has_pyproject_toml(target_path):
4
+ def has_pyproject_toml(target_path: str | Path) -> bool:
5
5
  return (Path(target_path) / "pyproject.toml").exists()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "plain.dev"
3
- version = "0.39.0"
3
+ version = "0.40.0"
4
4
  description = "A single command that runs everything you need for local development."
5
5
  authors = [{name = "Dave Gaeddert", email = "dave.gaeddert@dropseed.dev"}]
6
6
  license = "BSD-3-Clause"
@@ -1,4 +0,0 @@
1
- try:
2
- from ._version import __version__
3
- except ImportError:
4
- __version__ = "0.0.0+unknown"
File without changes
File without changes
File without changes
File without changes