wynd 0.1.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.
- wynd/__init__.py +0 -0
- wynd/cli.py +112 -0
- wynd/host.py +28 -0
- wynd/project.py +141 -0
- wynd/py.typed +0 -0
- wynd/shake.py +438 -0
- wynd/stage.py +147 -0
- wynd/sync.py +177 -0
- wynd/targets.py +67 -0
- wynd/typegen.py +232 -0
- wynd/wrangler.py +33 -0
- wynd-0.1.0.dist-info/METADATA +10 -0
- wynd-0.1.0.dist-info/RECORD +15 -0
- wynd-0.1.0.dist-info/WHEEL +4 -0
- wynd-0.1.0.dist-info/entry_points.txt +3 -0
wynd/__init__.py
ADDED
|
File without changes
|
wynd/cli.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from . import wrangler
|
|
7
|
+
from .host import warn_on_venv_mismatch
|
|
8
|
+
from .project import WyndError, load_project
|
|
9
|
+
from .shake import ShakePlan
|
|
10
|
+
from .stage import stage
|
|
11
|
+
from .sync import select_sdk, sync
|
|
12
|
+
from .targets import select_target
|
|
13
|
+
from .typegen import write_env
|
|
14
|
+
|
|
15
|
+
__all__ = ["main"]
|
|
16
|
+
|
|
17
|
+
# Reads the flags that choose the target and leaves everything else to Wrangler.
|
|
18
|
+
# --env-file and --profile are Wrangler's other global value flags, read only so
|
|
19
|
+
# their values aren't taken for the command.
|
|
20
|
+
_parser = argparse.ArgumentParser(add_help=False, allow_abbrev=False)
|
|
21
|
+
_parser.add_argument("-c", "--config")
|
|
22
|
+
_parser.add_argument("-e", "--env")
|
|
23
|
+
_parser.add_argument("--cwd")
|
|
24
|
+
_parser.add_argument("--env-file", action="append")
|
|
25
|
+
_parser.add_argument("--profile")
|
|
26
|
+
# Wrangler flags that change the target after Wynd has installed for the
|
|
27
|
+
# config's. Wynd refuses them.
|
|
28
|
+
_parser.add_argument("--compatibility-date")
|
|
29
|
+
_parser.add_argument("--compatibility-flags", "--compatibility-flag", action="append")
|
|
30
|
+
_parser.add_argument("--latest", action="store_true")
|
|
31
|
+
|
|
32
|
+
# Wrangler 4.129.0's subcommands. Of these, only `versions upload` uploads code,
|
|
33
|
+
# and `preview` uploads code when it has none of them.
|
|
34
|
+
SUBCOMMANDS = {
|
|
35
|
+
"versions": {"view", "list", "upload", "deploy", "secret"},
|
|
36
|
+
"preview": {"delete", "settings", "secret", "base-config"},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Takes only the config flag, so the remaining arguments keep their order.
|
|
40
|
+
_config_parser = argparse.ArgumentParser(add_help=False, allow_abbrev=False)
|
|
41
|
+
_config_parser.add_argument("-c", "--config")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main(argv: list[str] | None = None) -> int:
|
|
45
|
+
"""Run a `wynd` command line. Commands Wynd doesn't own go to Wrangler."""
|
|
46
|
+
args = sys.argv[1:] if argv is None else argv
|
|
47
|
+
options, rest = _parser.parse_known_args(args)
|
|
48
|
+
flow = _flow(rest)
|
|
49
|
+
if flow == "passthrough":
|
|
50
|
+
return wrangler.run(args)
|
|
51
|
+
|
|
52
|
+
if flow in ("dev", "upload") and (
|
|
53
|
+
options.compatibility_date or options.compatibility_flags or options.latest
|
|
54
|
+
):
|
|
55
|
+
print(
|
|
56
|
+
"wynd: error: --compatibility-date, --compatibility-flags and --latest "
|
|
57
|
+
"change the Python version after Wynd installs packages. Set "
|
|
58
|
+
"compatibility_date and compatibility_flags in the Wrangler config.",
|
|
59
|
+
file=sys.stderr,
|
|
60
|
+
)
|
|
61
|
+
return 1
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
cwd = Path.cwd() / options.cwd if options.cwd else Path.cwd()
|
|
65
|
+
env = options.env or os.environ.get("CLOUDFLARE_ENV")
|
|
66
|
+
project = load_project(cwd, options.config, env)
|
|
67
|
+
if flow == "types":
|
|
68
|
+
write_env(project, check="--check" in rest)
|
|
69
|
+
return 0
|
|
70
|
+
target = select_target(project)
|
|
71
|
+
sync(project, target)
|
|
72
|
+
if flow != "sync" and select_sdk(project.dependencies) == "crosswind-workers":
|
|
73
|
+
write_env(project)
|
|
74
|
+
staged = stage(project) if flow == "upload" else None
|
|
75
|
+
except WyndError as error:
|
|
76
|
+
print(f"wynd: error: {error}", file=sys.stderr)
|
|
77
|
+
return 1
|
|
78
|
+
|
|
79
|
+
if flow == "sync":
|
|
80
|
+
return 0
|
|
81
|
+
if staged is not None:
|
|
82
|
+
return _upload(args, rest, *staged)
|
|
83
|
+
|
|
84
|
+
warn_on_venv_mismatch(project, target)
|
|
85
|
+
return wrangler.run(args)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _flow(rest: list[str]) -> str:
|
|
89
|
+
"""Sort a command line into the upload, dev, sync, types or passthrough flow."""
|
|
90
|
+
words = [arg for arg in rest if not arg.startswith("-")]
|
|
91
|
+
command = words[0] if words else ""
|
|
92
|
+
# A known name, not the second word, because a flag's value can come first.
|
|
93
|
+
known = SUBCOMMANDS.get(command, set())
|
|
94
|
+
subcommand = next((word for word in words[1:] if word in known), "")
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
command == "deploy"
|
|
98
|
+
or (command == "versions" and subcommand == "upload")
|
|
99
|
+
or (command == "preview" and not subcommand)
|
|
100
|
+
):
|
|
101
|
+
return "upload"
|
|
102
|
+
return command if command in ("dev", "sync", "types") else "passthrough"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _upload(args: list[str], rest: list[str], path: Path, plan: ShakePlan) -> int:
|
|
106
|
+
"""Run Wrangler on the staged config in place of the project's."""
|
|
107
|
+
if "--dry-run" in rest:
|
|
108
|
+
# Flushed, so it comes before Wrangler's output in a pipe.
|
|
109
|
+
print(f"wynd: {plan.size_report()}", flush=True)
|
|
110
|
+
|
|
111
|
+
_, rest = _config_parser.parse_known_args(args)
|
|
112
|
+
return wrangler.run([*rest, "--config", str(path)])
|
wynd/host.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from .project import Project
|
|
5
|
+
from .targets import Target
|
|
6
|
+
|
|
7
|
+
__all__ = ["warn_on_venv_mismatch"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def warn_on_venv_mismatch(project: Project, target: Target) -> None:
|
|
11
|
+
"""Warn when the editor's .venv runs another Python minor than the target."""
|
|
12
|
+
try:
|
|
13
|
+
pyvenv = (project.root / ".venv" / "pyvenv.cfg").read_text()
|
|
14
|
+
except FileNotFoundError:
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
match = re.search(r"^version_info\s*=\s*(\d+\.\d+)", pyvenv, re.MULTILINE)
|
|
18
|
+
if match is None or match.group(1) == target.version:
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
flags = ", ".join(f for f in project.compatibility_flags if "python_workers" in f)
|
|
22
|
+
print(
|
|
23
|
+
f"warning: .venv is Python {match.group(1)} but {project.config_path.name}"
|
|
24
|
+
f" targets {target.version} ({flags}, {project.compatibility_date}).\n"
|
|
25
|
+
"Editor types may differ from production."
|
|
26
|
+
f" To match: uv venv --python {target.version} && uv sync",
|
|
27
|
+
file=sys.stderr,
|
|
28
|
+
)
|
wynd/project.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import tomllib
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import jsonc
|
|
7
|
+
|
|
8
|
+
__all__ = ["Project", "WyndError", "load_project"]
|
|
9
|
+
|
|
10
|
+
# Wrangler walks every parent for one name before it tries the next.
|
|
11
|
+
CONFIG_NAMES = ("wrangler.json", "wrangler.jsonc", "wrangler.toml")
|
|
12
|
+
|
|
13
|
+
# Top-level keys Wynd reads that an environment inherits when it leaves them out.
|
|
14
|
+
# Bindings, vars and the rest stay with the table that sets them.
|
|
15
|
+
INHERITED = (
|
|
16
|
+
"main",
|
|
17
|
+
"base_dir",
|
|
18
|
+
"compatibility_date",
|
|
19
|
+
"compatibility_flags",
|
|
20
|
+
"rules",
|
|
21
|
+
"assets",
|
|
22
|
+
"build",
|
|
23
|
+
"python_modules",
|
|
24
|
+
# Wrangler 4.129 inherits logfwdr, although it holds bindings.
|
|
25
|
+
"logfwdr",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class WyndError(Exception):
|
|
30
|
+
"""An error Wynd reports to the user as one message."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class Project:
|
|
35
|
+
root: Path
|
|
36
|
+
config_path: Path
|
|
37
|
+
# The config file as parsed, every environment included.
|
|
38
|
+
config: dict[str, Any]
|
|
39
|
+
# The chosen environment's table over the top-level keys it inherits, or
|
|
40
|
+
# the top level without an environment.
|
|
41
|
+
settings: dict[str, Any]
|
|
42
|
+
main: str | None
|
|
43
|
+
base_dir: str | None
|
|
44
|
+
compatibility_date: str | None
|
|
45
|
+
compatibility_flags: tuple[str, ...]
|
|
46
|
+
dependencies: tuple[str, ...]
|
|
47
|
+
allow_build: bool
|
|
48
|
+
# Modules the shaker keeps although nothing imports them by a literal name.
|
|
49
|
+
keep: tuple[str, ...]
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def python_modules(self) -> Path:
|
|
53
|
+
# Wrangler collects python_modules/ beside the config file.
|
|
54
|
+
return self.config_path.parent / "python_modules"
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def source_dir(self) -> Path:
|
|
58
|
+
"""The module root: base_dir, or the entrypoint's directory."""
|
|
59
|
+
if self.main is None:
|
|
60
|
+
raise WyndError(f"{self.config_path.name} sets no main")
|
|
61
|
+
config_dir = self.config_path.parent.resolve()
|
|
62
|
+
return (config_dir / (self.base_dir or Path(self.main).parent)).resolve()
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def wynd_dir(self) -> Path:
|
|
66
|
+
# Wynd's builds and state.
|
|
67
|
+
return self.root / ".wynd"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def load_project(cwd: Path, config: str | None, env: str | None) -> Project:
|
|
71
|
+
"""Read the project the way Wrangler would for `-c config` and `-e env`."""
|
|
72
|
+
root = _find_up(cwd, "pyproject.toml")
|
|
73
|
+
if root is None:
|
|
74
|
+
raise WyndError(f"no pyproject.toml in {cwd} or any parent directory")
|
|
75
|
+
|
|
76
|
+
config_path = cwd / config if config else _find_config(cwd)
|
|
77
|
+
raw = _read(config_path)
|
|
78
|
+
settings = _environment(raw, env, config_path.name)
|
|
79
|
+
|
|
80
|
+
pyproject = _read(root / "pyproject.toml")
|
|
81
|
+
wynd = pyproject.get("tool", {}).get("wynd", {})
|
|
82
|
+
keep = wynd.get("keep", [])
|
|
83
|
+
if not isinstance(keep, list) or not all(isinstance(k, str) for k in keep):
|
|
84
|
+
raise WyndError("[tool.wynd] keep must be a list of module names")
|
|
85
|
+
|
|
86
|
+
date = settings.get("compatibility_date")
|
|
87
|
+
return Project(
|
|
88
|
+
root=root,
|
|
89
|
+
config_path=config_path,
|
|
90
|
+
config=raw,
|
|
91
|
+
settings=settings,
|
|
92
|
+
main=settings.get("main"),
|
|
93
|
+
base_dir=settings.get("base_dir"),
|
|
94
|
+
# TOML parses a bare date as datetime.date. Wrangler reads it as text.
|
|
95
|
+
compatibility_date=None if date is None else str(date),
|
|
96
|
+
compatibility_flags=tuple(settings.get("compatibility_flags", ())),
|
|
97
|
+
dependencies=tuple(pyproject.get("project", {}).get("dependencies", ())),
|
|
98
|
+
allow_build=bool(wynd.get("allow-build")),
|
|
99
|
+
keep=tuple(keep),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _environment(config: dict[str, Any], env: str | None, name: str) -> dict[str, Any]:
|
|
104
|
+
"""The chosen environment's table over the top-level keys it inherits."""
|
|
105
|
+
envs = config.get("env") or {}
|
|
106
|
+
# Without any env table, Wrangler warns and uses the top level, as Wynd does.
|
|
107
|
+
if env is None or not envs:
|
|
108
|
+
return config
|
|
109
|
+
if env not in envs:
|
|
110
|
+
raise WyndError(f"{name} has no environment named {env!r}")
|
|
111
|
+
|
|
112
|
+
# An environment's value replaces the top-level one; nothing merges.
|
|
113
|
+
inherited = {key: config[key] for key in INHERITED if key in config}
|
|
114
|
+
return {**inherited, **envs[env]}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _find_up(start: Path, name: str) -> Path | None:
|
|
118
|
+
for directory in (start, *start.parents):
|
|
119
|
+
if (directory / name).is_file():
|
|
120
|
+
return directory
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _find_config(cwd: Path) -> Path:
|
|
125
|
+
for name in CONFIG_NAMES:
|
|
126
|
+
directory = _find_up(cwd, name)
|
|
127
|
+
if directory is not None:
|
|
128
|
+
return directory / name
|
|
129
|
+
raise WyndError(f"no Wrangler config ({', '.join(CONFIG_NAMES)}) found from {cwd}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _read(path: Path) -> dict[str, Any]:
|
|
133
|
+
try:
|
|
134
|
+
text = path.read_text()
|
|
135
|
+
if path.suffix == ".toml":
|
|
136
|
+
return tomllib.loads(text)
|
|
137
|
+
return jsonc.loads(text)
|
|
138
|
+
except OSError as error:
|
|
139
|
+
raise WyndError(f"cannot read {path}: {error.strerror}") from None
|
|
140
|
+
except (tomllib.TOMLDecodeError, ValueError) as error:
|
|
141
|
+
raise WyndError(f"cannot parse {path}: {error}") from None
|
wynd/py.typed
ADDED
|
File without changes
|