fast-dev-cli 0.16.1__tar.gz → 0.17.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.
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/PKG-INFO +1 -1
- fast_dev_cli-0.17.0/fast_dev_cli/__init__.py +1 -0
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/fast_dev_cli/cli.py +128 -34
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/pyproject.toml +43 -9
- fast_dev_cli-0.17.0/scripts/deps.py +156 -0
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/tests/test_bump.py +38 -50
- fast_dev_cli-0.17.0/tests/test_exec.py +41 -0
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/tests/test_tag.py +5 -3
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/tests/test_upgrade.py +2 -2
- fast_dev_cli-0.16.1/fast_dev_cli/__init__.py +0 -1
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/LICENSE +0 -0
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/README.md +0 -0
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/fast_dev_cli/__main__.py +0 -0
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/fast_dev_cli/py.typed +0 -0
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/pdm_build.py +0 -0
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/scripts/check.py +0 -0
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/scripts/format.py +0 -0
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/scripts/test.py +0 -0
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/tests/__init__.py +0 -0
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/tests/conftest.py +0 -0
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/tests/test_fast_test.py +0 -0
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/tests/test_functions.py +0 -0
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/tests/test_lint.py +0 -0
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/tests/test_poetry_version_plugin.py +0 -0
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/tests/test_runserver.py +0 -0
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/tests/test_sync.py +0 -0
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/tests/test_upload.py +0 -0
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/tests/test_version.py +0 -0
- {fast_dev_cli-0.16.1 → fast_dev_cli-0.17.0}/tests/utils.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.17.0"
|
|
@@ -16,6 +16,7 @@ from typing import (
|
|
|
16
16
|
Optional,
|
|
17
17
|
cast,
|
|
18
18
|
get_args,
|
|
19
|
+
overload,
|
|
19
20
|
)
|
|
20
21
|
|
|
21
22
|
import typer
|
|
@@ -64,7 +65,10 @@ ToolOption = Option(
|
|
|
64
65
|
)
|
|
65
66
|
|
|
66
67
|
|
|
67
|
-
class
|
|
68
|
+
class FastDevCliError(Exception): ...
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ShellCommandError(FastDevCliError): ...
|
|
68
72
|
|
|
69
73
|
|
|
70
74
|
def poetry_module_name(name: str) -> str:
|
|
@@ -111,7 +115,10 @@ def run_and_echo(
|
|
|
111
115
|
echo(f"--> {cmd}")
|
|
112
116
|
if dry:
|
|
113
117
|
return 0
|
|
114
|
-
|
|
118
|
+
command: list[str] | str = cmd
|
|
119
|
+
if "shell" not in kw and not (set(cmd) & {"|", ">"}):
|
|
120
|
+
command = shlex.split(cmd)
|
|
121
|
+
return _run_shell(command, **kw).returncode
|
|
115
122
|
|
|
116
123
|
|
|
117
124
|
def check_call(cmd: str) -> bool:
|
|
@@ -177,33 +184,61 @@ def read_version_from_file(
|
|
|
177
184
|
return "0.0.0"
|
|
178
185
|
|
|
179
186
|
|
|
187
|
+
@overload
|
|
180
188
|
def get_current_version(
|
|
181
189
|
verbose: bool = False,
|
|
182
190
|
is_poetry: bool | None = None,
|
|
183
191
|
package_name: str | None = None,
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if not is_poetry:
|
|
188
|
-
work_dir = None
|
|
189
|
-
if package_name is None:
|
|
190
|
-
work_dir = Project.get_work_dir()
|
|
191
|
-
package_name = re.sub(r"[- ]", "_", work_dir.name)
|
|
192
|
-
try:
|
|
193
|
-
installed_version = importlib_metadata.version(package_name)
|
|
194
|
-
except importlib_metadata.PackageNotFoundError:
|
|
195
|
-
...
|
|
196
|
-
else:
|
|
197
|
-
if installed_version != "0.0.0":
|
|
198
|
-
return installed_version
|
|
199
|
-
return read_version_from_file(package_name, work_dir)
|
|
192
|
+
*,
|
|
193
|
+
check_version: Literal[False] = False,
|
|
194
|
+
) -> str: ...
|
|
200
195
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
196
|
+
|
|
197
|
+
@overload
|
|
198
|
+
def get_current_version(
|
|
199
|
+
verbose: bool = False,
|
|
200
|
+
is_poetry: bool | None = None,
|
|
201
|
+
package_name: str | None = None,
|
|
202
|
+
*,
|
|
203
|
+
check_version: Literal[True] = True,
|
|
204
|
+
) -> tuple[bool, str]: ...
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def get_current_version(
|
|
208
|
+
verbose: bool = False,
|
|
209
|
+
is_poetry: bool | None = None,
|
|
210
|
+
package_name: str | None = None,
|
|
211
|
+
*,
|
|
212
|
+
check_version: bool = False,
|
|
213
|
+
) -> str | tuple[bool, str]:
|
|
214
|
+
if is_poetry is True or Project.manage_by_poetry():
|
|
215
|
+
cmd = ["poetry", "version", "-s"]
|
|
216
|
+
if verbose:
|
|
217
|
+
echo(f"--> {' '.join(cmd)}")
|
|
218
|
+
if out := capture_cmd_output(cmd, raises=True):
|
|
219
|
+
out = out.splitlines()[-1].strip().split()[-1]
|
|
220
|
+
if check_version:
|
|
221
|
+
return True, out
|
|
222
|
+
return out
|
|
223
|
+
toml_text = work_dir = None
|
|
224
|
+
if package_name is None:
|
|
225
|
+
work_dir = Project.get_work_dir()
|
|
226
|
+
toml_text = Project.load_toml_text()
|
|
227
|
+
doc = tomllib.loads(toml_text)
|
|
228
|
+
project_name = doc.get("project", {}).get("name", work_dir.name)
|
|
229
|
+
package_name = re.sub(r"[- ]", "_", project_name)
|
|
230
|
+
local_version = read_version_from_file(package_name, work_dir, toml_text)
|
|
231
|
+
try:
|
|
232
|
+
installed_version = importlib_metadata.version(package_name)
|
|
233
|
+
except importlib_metadata.PackageNotFoundError:
|
|
234
|
+
installed_version = ""
|
|
235
|
+
current_version = local_version or installed_version
|
|
236
|
+
if not current_version:
|
|
237
|
+
raise FastDevCliError(f"Failed to get current version of {package_name!r}")
|
|
238
|
+
if check_version:
|
|
239
|
+
is_conflict = bool(local_version) and local_version != installed_version
|
|
240
|
+
return is_conflict, current_version
|
|
241
|
+
return current_version
|
|
207
242
|
|
|
208
243
|
|
|
209
244
|
def _ensure_bool(value: bool | OptionInfo) -> bool:
|
|
@@ -391,7 +426,9 @@ class BumpUp(DryRun):
|
|
|
391
426
|
raise Exit(1) from e
|
|
392
427
|
|
|
393
428
|
def gen(self: Self) -> str:
|
|
394
|
-
_version = get_current_version()
|
|
429
|
+
should_sync, _version = get_current_version(check_version=True)
|
|
430
|
+
if should_sync:
|
|
431
|
+
Project.sync_dependencies()
|
|
395
432
|
filename = self.filename
|
|
396
433
|
echo(f"Current version(@{filename}): {_version}")
|
|
397
434
|
if self.part:
|
|
@@ -460,6 +497,7 @@ class EnvError(Exception):
|
|
|
460
497
|
|
|
461
498
|
class Project:
|
|
462
499
|
path_depth = 5
|
|
500
|
+
_tool: ToolName | None = None
|
|
463
501
|
|
|
464
502
|
@staticmethod
|
|
465
503
|
def is_poetry_v2(text: str) -> bool:
|
|
@@ -490,7 +528,7 @@ class Project:
|
|
|
490
528
|
return d
|
|
491
529
|
if allow_cwd:
|
|
492
530
|
return cls.get_root_dir(cwd)
|
|
493
|
-
raise EnvError(f"{name} not found! Make sure this is a
|
|
531
|
+
raise EnvError(f"{name} not found! Make sure this is a python project.")
|
|
494
532
|
|
|
495
533
|
@classmethod
|
|
496
534
|
def load_toml_text(cls: type[Self], name: str = TOML_FILE) -> str:
|
|
@@ -498,22 +536,40 @@ class Project:
|
|
|
498
536
|
return toml_file.read_text("utf8")
|
|
499
537
|
|
|
500
538
|
@classmethod
|
|
501
|
-
def manage_by_poetry(cls: type[Self]) -> bool:
|
|
502
|
-
return cls.get_manage_tool() == "poetry"
|
|
539
|
+
def manage_by_poetry(cls: type[Self], cache: bool = False) -> bool:
|
|
540
|
+
return cls.get_manage_tool(cache=cache) == "poetry"
|
|
503
541
|
|
|
504
542
|
@classmethod
|
|
505
|
-
def get_manage_tool(cls: type[Self]) -> ToolName | None:
|
|
543
|
+
def get_manage_tool(cls: type[Self], cache: bool = False) -> ToolName | None:
|
|
544
|
+
if cache and cls._tool:
|
|
545
|
+
return cls._tool
|
|
506
546
|
try:
|
|
507
547
|
text = cls.load_toml_text()
|
|
508
548
|
except EnvError:
|
|
509
549
|
pass
|
|
510
550
|
else:
|
|
551
|
+
with contextlib.suppress(KeyError, tomllib.TOMLDecodeError):
|
|
552
|
+
doc = tomllib.loads(text)
|
|
553
|
+
backend = doc["build-system"]["build-backend"]
|
|
554
|
+
if "poetry" in backend:
|
|
555
|
+
cls._tool = "poetry"
|
|
556
|
+
return cls._tool
|
|
557
|
+
elif "pdm" in backend:
|
|
558
|
+
cls._tool = "pdm"
|
|
559
|
+
work_dir = cls.get_work_dir(allow_cwd=True)
|
|
560
|
+
if not Path(work_dir, "pdm.lock").exists() and (
|
|
561
|
+
"[tool.uv]" in text or Path(work_dir, "uv.lock").exists()
|
|
562
|
+
):
|
|
563
|
+
cls._tool = "uv"
|
|
564
|
+
return cls._tool
|
|
511
565
|
for name in get_args(ToolName):
|
|
512
566
|
if f"[tool.{name}]" in text:
|
|
513
|
-
|
|
567
|
+
cls._tool = cast(ToolName, name)
|
|
568
|
+
return cls._tool
|
|
514
569
|
# Poetry 2.0 default to not include the '[tool.poetry]' section
|
|
515
570
|
if cls.is_poetry_v2(text):
|
|
516
|
-
|
|
571
|
+
cls._tool = "poetry"
|
|
572
|
+
return cls._tool
|
|
517
573
|
return None
|
|
518
574
|
|
|
519
575
|
@staticmethod
|
|
@@ -528,6 +584,25 @@ class Project:
|
|
|
528
584
|
root = venv_parent
|
|
529
585
|
return root
|
|
530
586
|
|
|
587
|
+
@classmethod
|
|
588
|
+
def is_pdm_project(cls, strict: bool = True, cache: bool = False) -> bool:
|
|
589
|
+
if cls.get_manage_tool(cache=cache) != "pdm":
|
|
590
|
+
return False
|
|
591
|
+
if strict:
|
|
592
|
+
lock_file = cls.get_work_dir() / "pdm.lock"
|
|
593
|
+
return lock_file.exists()
|
|
594
|
+
return True
|
|
595
|
+
|
|
596
|
+
@classmethod
|
|
597
|
+
def sync_dependencies(cls) -> None:
|
|
598
|
+
if cls.is_pdm_project():
|
|
599
|
+
cmd = "pdm sync --prod"
|
|
600
|
+
run_and_echo(cmd)
|
|
601
|
+
elif cls.manage_by_poetry(cache=True):
|
|
602
|
+
run_and_echo("poetry install --only=main")
|
|
603
|
+
elif cls.get_manage_tool(cache=True) == "uv":
|
|
604
|
+
run_and_echo("uv sync")
|
|
605
|
+
|
|
531
606
|
|
|
532
607
|
class ParseError(Exception):
|
|
533
608
|
"""Raise this if parse dependence line error"""
|
|
@@ -704,9 +779,9 @@ class UpgradeDependencies(Project, DryRun):
|
|
|
704
779
|
|
|
705
780
|
def gen(self: Self) -> str:
|
|
706
781
|
if self._tool == "uv":
|
|
707
|
-
return "uv lock --upgrade --verbose && uv sync --frozen"
|
|
782
|
+
return "uv lock --upgrade --verbose && uv sync --frozen --all-groups"
|
|
708
783
|
elif self._tool == "pdm":
|
|
709
|
-
return "pdm update --verbose && pdm
|
|
784
|
+
return "pdm update --verbose && pdm sync -G :all"
|
|
710
785
|
return self.gen_cmd() + " && poetry lock && poetry update"
|
|
711
786
|
|
|
712
787
|
|
|
@@ -738,13 +813,17 @@ class GitTag(DryRun):
|
|
|
738
813
|
return "git push" in self.git_status
|
|
739
814
|
|
|
740
815
|
def gen(self: Self) -> str:
|
|
741
|
-
_version = get_current_version(verbose=False)
|
|
816
|
+
should_sync, _version = get_current_version(verbose=False, check_version=True)
|
|
817
|
+
if should_sync:
|
|
818
|
+
Project.sync_dependencies()
|
|
742
819
|
if self.has_v_prefix():
|
|
743
820
|
# Add `v` at prefix to compare with bumpversion tool
|
|
744
821
|
_version = "v" + _version
|
|
745
822
|
cmd = f"git tag -a {_version} -m {self.message!r} && git push --tags"
|
|
746
823
|
if self.should_push():
|
|
747
824
|
cmd += " && git push"
|
|
825
|
+
if Project.is_pdm_project():
|
|
826
|
+
cmd = "pdm sync --prod && " + cmd
|
|
748
827
|
return cmd
|
|
749
828
|
|
|
750
829
|
@cached_property
|
|
@@ -1111,6 +1190,21 @@ def runserver(
|
|
|
1111
1190
|
dev(port, host, dry=dry)
|
|
1112
1191
|
|
|
1113
1192
|
|
|
1193
|
+
@cli.command(name="exec")
|
|
1194
|
+
def run_by_subprocess(cmd: str, dry: bool = DryOption) -> None:
|
|
1195
|
+
"""Run cmd by subprocess, auto set shell=True when cmd contains '|'"""
|
|
1196
|
+
try:
|
|
1197
|
+
rc = run_and_echo(cmd, verbose=True, dry=_ensure_bool(dry))
|
|
1198
|
+
except FileNotFoundError as e:
|
|
1199
|
+
if e.filename == cmd.split()[0]:
|
|
1200
|
+
echo(f"Command not found: {e.filename}")
|
|
1201
|
+
raise Exit(1) from None
|
|
1202
|
+
raise e
|
|
1203
|
+
else:
|
|
1204
|
+
if rc:
|
|
1205
|
+
raise Exit(rc)
|
|
1206
|
+
|
|
1207
|
+
|
|
1114
1208
|
def main() -> None:
|
|
1115
1209
|
cli()
|
|
1116
1210
|
|
|
@@ -42,7 +42,7 @@ dependencies = [
|
|
|
42
42
|
"bumpversion2 >=1.4.3,<2",
|
|
43
43
|
"pytest >=8.2.0,<9",
|
|
44
44
|
]
|
|
45
|
-
version = "0.
|
|
45
|
+
version = "0.17.0"
|
|
46
46
|
|
|
47
47
|
[project.urls]
|
|
48
48
|
Homepage = "https://github.com/waketzheng/fast-dev-cli"
|
|
@@ -55,6 +55,11 @@ include-optional-dependencies = [
|
|
|
55
55
|
[project.scripts]
|
|
56
56
|
fast = "fast_dev_cli:cli.main"
|
|
57
57
|
|
|
58
|
+
[dependency-groups]
|
|
59
|
+
dev = [
|
|
60
|
+
"twine>=6.1.0",
|
|
61
|
+
]
|
|
62
|
+
|
|
58
63
|
[tool.pdm]
|
|
59
64
|
distribution = true
|
|
60
65
|
|
|
@@ -72,6 +77,42 @@ dev = [
|
|
|
72
77
|
"-e file:///${PROJECT_ROOT}/#egg=fast-dev-cli[all]",
|
|
73
78
|
]
|
|
74
79
|
|
|
80
|
+
[tool.pdm.scripts]
|
|
81
|
+
up = "pdm update -G :all"
|
|
82
|
+
tree = "pdm list --tree {args}"
|
|
83
|
+
deps = "pdm run python scripts/deps.py {args}"
|
|
84
|
+
prod = "pdm install --prod --frozen {args}"
|
|
85
|
+
test = "pdm run fast test {args}"
|
|
86
|
+
lint = "pdm run fast lint {args}"
|
|
87
|
+
check = "pdm run fast check {args}"
|
|
88
|
+
tag = "pdm run fast tag {args}"
|
|
89
|
+
bump = "pdm run fast bump patch --commit {args}"
|
|
90
|
+
|
|
91
|
+
[tool.pdm.scripts.fresh]
|
|
92
|
+
composite = [
|
|
93
|
+
"up",
|
|
94
|
+
"deps",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
[tool.pdm.scripts.ci]
|
|
98
|
+
composite = [
|
|
99
|
+
"deps",
|
|
100
|
+
"check",
|
|
101
|
+
"test",
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
[tool.pdm.scripts.start]
|
|
105
|
+
composite = [
|
|
106
|
+
"pre-commit install",
|
|
107
|
+
"deps",
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
[tool.pdm.scripts.style]
|
|
111
|
+
composite = [
|
|
112
|
+
"ruff format",
|
|
113
|
+
"ruff check --fix --unsafe-fixes {args}",
|
|
114
|
+
]
|
|
115
|
+
|
|
75
116
|
[tool.waketzheng._internal-slim-build.packages.fastdevcli-slim.project]
|
|
76
117
|
name = "fastdevcli-slim"
|
|
77
118
|
|
|
@@ -155,6 +196,7 @@ extend-select = [
|
|
|
155
196
|
]
|
|
156
197
|
"fast_dev_cli/cli.py" = [
|
|
157
198
|
"UP007",
|
|
199
|
+
"UP045",
|
|
158
200
|
]
|
|
159
201
|
|
|
160
202
|
[tool.bandit]
|
|
@@ -164,11 +206,3 @@ exclude_dirs = [
|
|
|
164
206
|
"examples",
|
|
165
207
|
".venv",
|
|
166
208
|
]
|
|
167
|
-
|
|
168
|
-
[dependency-groups]
|
|
169
|
-
ci = [
|
|
170
|
-
"coveralls @ git+https://github.com/waketzheng/coveralls-python@4.1.1",
|
|
171
|
-
]
|
|
172
|
-
dev = [
|
|
173
|
-
"twine>=6.1.0",
|
|
174
|
-
]
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Install deps by `pdm install -G :all --frozen`
|
|
4
|
+
if value of `pdm config use_uv` is True,
|
|
5
|
+
otherwise by the following shells:
|
|
6
|
+
|
|
7
|
+
pdm run python -m ensurepip
|
|
8
|
+
pdm run python -m pip install --upgrade pip
|
|
9
|
+
pdm run python -m pip install --group dev -e .
|
|
10
|
+
|
|
11
|
+
Usage::
|
|
12
|
+
pdm run python scripts/deps.py
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import platform
|
|
19
|
+
import shlex
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.3"
|
|
25
|
+
__updated_at__ = "2025.07.21"
|
|
26
|
+
SHELL = """
|
|
27
|
+
pdm run python -m ensurepip
|
|
28
|
+
pdm run python -m pip install --upgrade pip
|
|
29
|
+
pdm run python -m pip install --group dev -e .
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run_and_echo(
|
|
34
|
+
cmd: str,
|
|
35
|
+
env: dict[str, str] | None = None,
|
|
36
|
+
dry: bool = False,
|
|
37
|
+
verbose: bool = True,
|
|
38
|
+
**kw,
|
|
39
|
+
) -> int:
|
|
40
|
+
if verbose:
|
|
41
|
+
print("-->", cmd)
|
|
42
|
+
if dry:
|
|
43
|
+
return 0
|
|
44
|
+
if env is not None:
|
|
45
|
+
env = dict(os.environ, **env)
|
|
46
|
+
return subprocess.run(shlex.split(cmd), env=env, **kw).returncode
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def capture_output(cmd: str) -> str:
|
|
50
|
+
r = subprocess.run(
|
|
51
|
+
shlex.split(cmd), capture_output=True, text=True, encoding="utf-8"
|
|
52
|
+
)
|
|
53
|
+
return r.stdout.strip()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def is_using_uv() -> bool:
|
|
57
|
+
return capture_output("pdm config use_uv").lower() == "true"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def pop_if_contains(args: list[str], flag: str) -> bool:
|
|
61
|
+
try:
|
|
62
|
+
index = args.index(flag)
|
|
63
|
+
except ValueError:
|
|
64
|
+
return False
|
|
65
|
+
else:
|
|
66
|
+
args.pop(index)
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def prefer_pdm(args: list[str]) -> bool:
|
|
71
|
+
if pop_if_contains(args, "--pip"):
|
|
72
|
+
return False
|
|
73
|
+
return platform.system() != "Windows" or is_using_uv()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def not_distribution() -> bool:
|
|
77
|
+
toml = Path(__file__).parent / "pyproject.toml"
|
|
78
|
+
if not toml.exists():
|
|
79
|
+
toml = toml.parent.parent / toml.name
|
|
80
|
+
if not toml.exists():
|
|
81
|
+
return False
|
|
82
|
+
if sys.version_info >= (3, 11):
|
|
83
|
+
import tomllib
|
|
84
|
+
else:
|
|
85
|
+
try:
|
|
86
|
+
import tomli as tomllib
|
|
87
|
+
except ImportError:
|
|
88
|
+
return False
|
|
89
|
+
doc = tomllib.loads(toml.read_text("utf8"))
|
|
90
|
+
try:
|
|
91
|
+
return not doc["tool"]["pdm"]["distribution"]
|
|
92
|
+
except KeyError:
|
|
93
|
+
...
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def run_shell(command: str, args: list[str], dry: bool) -> int | None:
|
|
98
|
+
if args:
|
|
99
|
+
command += " " + " ".join(i if i.startswith("-") else repr(i) for i in args)
|
|
100
|
+
if run_and_echo(command, dry=dry) == 0:
|
|
101
|
+
return None
|
|
102
|
+
return 1
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def pip_available() -> bool:
|
|
106
|
+
rc = run_and_echo(
|
|
107
|
+
"pdm run python -m pip --version", capture_output=True, verbose=False
|
|
108
|
+
)
|
|
109
|
+
return rc == 0
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main() -> int | None:
|
|
113
|
+
args = sys.argv[1:]
|
|
114
|
+
if len(args) == 1:
|
|
115
|
+
a1 = args[0]
|
|
116
|
+
if a1 == "-h" or "--help" in args:
|
|
117
|
+
print(__doc__)
|
|
118
|
+
return None
|
|
119
|
+
elif a1 in ("-v", "--version"):
|
|
120
|
+
print(__version__)
|
|
121
|
+
return None
|
|
122
|
+
dry = pop_if_contains(args, "--dry") or pop_if_contains(args, "--dry-run")
|
|
123
|
+
if pop_if_contains(args, "--uv"):
|
|
124
|
+
if os.path.exists("uv.lock"):
|
|
125
|
+
return run_shell("uv sync --all-extras --all-groups", args, dry)
|
|
126
|
+
command = (
|
|
127
|
+
"uv install --all-extras -r pyproject.toml --group dev --no-verify-hashes"
|
|
128
|
+
)
|
|
129
|
+
return run_shell(command, args, dry)
|
|
130
|
+
if prefer_pdm(args):
|
|
131
|
+
command = "pdm install --frozen -G :all"
|
|
132
|
+
return run_shell(command, args, dry)
|
|
133
|
+
shell = SHELL.strip()
|
|
134
|
+
if pop_if_contains(args, "--no-dev"):
|
|
135
|
+
shell = shell.replace(" --group dev", "")
|
|
136
|
+
if args:
|
|
137
|
+
extras = " ".join(i if i.startswith("-") else repr(i) for i in args)
|
|
138
|
+
if extras.startswith("-"):
|
|
139
|
+
shell += " " + extras
|
|
140
|
+
else:
|
|
141
|
+
shell += extras # e.g.: '[xls,fastapi]'
|
|
142
|
+
elif not_distribution():
|
|
143
|
+
shell = shell.replace(" -e .", "")
|
|
144
|
+
cmds = shell.splitlines()
|
|
145
|
+
if pip_available(): # If pip exists, skip installing it by ensurepip
|
|
146
|
+
cmds = cmds[1:]
|
|
147
|
+
for cmd in cmds:
|
|
148
|
+
if run_and_echo(cmd, dry=dry) != 0:
|
|
149
|
+
return 1
|
|
150
|
+
if not dry:
|
|
151
|
+
print("Done.")
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
if __name__ == "__main__":
|
|
156
|
+
sys.exit(main())
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import re
|
|
2
3
|
import shutil
|
|
3
4
|
import subprocess
|
|
5
|
+
import sys
|
|
4
6
|
from contextlib import redirect_stdout
|
|
5
7
|
from io import StringIO
|
|
6
8
|
from pathlib import Path
|
|
@@ -39,12 +41,14 @@ def test_enum():
|
|
|
39
41
|
|
|
40
42
|
|
|
41
43
|
def _bump_commands(
|
|
42
|
-
version: str, filename=TOML_FILE, emoji=False
|
|
44
|
+
version: str, filename=TOML_FILE, emoji=False, add_sync=False
|
|
43
45
|
) -> tuple[str, str, str]:
|
|
44
46
|
cmd = rf'bumpversion --parse "(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)" --current-version="{version}"'
|
|
45
47
|
suffix = " --commit && git push && git push --tags && git log -1"
|
|
46
48
|
if emoji:
|
|
47
49
|
suffix = suffix.replace("--commit", "--commit --message-emoji=1")
|
|
50
|
+
if add_sync:
|
|
51
|
+
cmd = "pdm sync --prod && " + cmd
|
|
48
52
|
patch_without_commit = cmd + f" patch {filename} --allow-dirty"
|
|
49
53
|
patch_with_commit = cmd + f" patch {filename}" + suffix
|
|
50
54
|
minor_with_commit = cmd + f" minor {filename} --tag" + suffix
|
|
@@ -99,7 +103,9 @@ def test_bump(
|
|
|
99
103
|
def test_bump_with_poetry(mocker, tmp_poetry_project, tmp_path):
|
|
100
104
|
mocker.patch("builtins.input", return_value=" ")
|
|
101
105
|
version = get_current_version()
|
|
102
|
-
patch_without_commit, patch_with_commit, minor_with_commit = _bump_commands(
|
|
106
|
+
patch_without_commit, patch_with_commit, minor_with_commit = _bump_commands(
|
|
107
|
+
version, add_sync=False
|
|
108
|
+
)
|
|
103
109
|
stream = StringIO()
|
|
104
110
|
with redirect_stdout(stream):
|
|
105
111
|
BumpUp(part="patch", commit=False).run()
|
|
@@ -167,7 +173,7 @@ def test_bump_with_emoji(mocker, tmp_path, monkeypatch):
|
|
|
167
173
|
def test_bump_with_emoji_in_poetry_project(mocker, tmp_path, monkeypatch):
|
|
168
174
|
# real bump
|
|
169
175
|
last_commit = "📝 Update release notes"
|
|
170
|
-
_, patch_with_commit, __ = _bump_commands("0.1.0", emoji=True)
|
|
176
|
+
_, patch_with_commit, __ = _bump_commands("0.1.0", emoji=True, add_sync=False)
|
|
171
177
|
with prepare_poetry_project(tmp_path):
|
|
172
178
|
subprocess.run(["git", "init"])
|
|
173
179
|
subprocess.run(["git", "add", "."])
|
|
@@ -239,23 +245,41 @@ path = "app/__init__.py"
|
|
|
239
245
|
"""
|
|
240
246
|
|
|
241
247
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
248
|
+
@pytest.fixture
|
|
249
|
+
def dynamic_pdm_project(tmp_work_dir):
|
|
250
|
+
py = "{}.{}".format(*sys.version_info)
|
|
251
|
+
project = "my-project"
|
|
252
|
+
capture_cmd_output(f"pdm new {project} --non-interactive --python={py} --no-git")
|
|
253
|
+
with chdir(project):
|
|
245
254
|
toml_file = Path("pyproject.toml")
|
|
246
255
|
content = toml_file.read_text().replace(
|
|
247
256
|
'version = "0.1.0"', 'dynamic = ["version"]'
|
|
248
257
|
)
|
|
249
|
-
toml_file.write_text(content)
|
|
250
|
-
with toml_file.open("a+") as f:
|
|
251
|
-
f.write(PDM_DYNAMIC_VERSION)
|
|
258
|
+
toml_file.write_text(content + PDM_DYNAMIC_VERSION)
|
|
252
259
|
app = Path("app")
|
|
253
260
|
app.mkdir()
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
261
|
+
yield app
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def test_pdm_project(dynamic_pdm_project):
|
|
265
|
+
init_file = dynamic_pdm_project / "__init__.py"
|
|
266
|
+
init_file.write_text('__version__ = "0.2.0"')
|
|
267
|
+
out = capture_cmd_output("fast bump patch")
|
|
268
|
+
assert str(init_file) in out
|
|
269
|
+
assert "0.2.1" in init_file.read_text()
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_installed_version_is_0_0_0(dynamic_pdm_project):
|
|
273
|
+
version_file = dynamic_pdm_project / "__init__.py"
|
|
274
|
+
version_file.write_text('__version__ = "0.0.0"')
|
|
275
|
+
toml_file = Path("pyproject.toml")
|
|
276
|
+
text = toml_file.read_text()
|
|
277
|
+
toml_file.write_text(re.sub(r"(distribution = )false", r"\1true", text))
|
|
278
|
+
capture_cmd_output("pdm install")
|
|
279
|
+
version_file.write_text('__version__ = "0.3.0"')
|
|
280
|
+
out = capture_cmd_output("fast bump patch")
|
|
281
|
+
assert str(version_file) in out
|
|
282
|
+
assert "0.3.1" in version_file.read_text()
|
|
259
283
|
|
|
260
284
|
|
|
261
285
|
def test_hatch_project(tmp_work_dir):
|
|
@@ -365,39 +389,3 @@ build-backend = "pdm.backend"
|
|
|
365
389
|
out = capture_cmd_output("fast bump patch")
|
|
366
390
|
assert str(version_file) in out
|
|
367
391
|
assert "0.3.1" in version_file.read_text()
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
def test_installed_version_is_0_0_0(tmp_work_dir):
|
|
371
|
-
project_name = "my-project"
|
|
372
|
-
package_name = "app"
|
|
373
|
-
Path(project_name).mkdir()
|
|
374
|
-
with chdir(project_name):
|
|
375
|
-
Path(package_name).mkdir()
|
|
376
|
-
version_file = Path(package_name, "__init__.py")
|
|
377
|
-
version_file.write_text('__version__ = "0.3.0"')
|
|
378
|
-
toml_file = Path("pyproject.toml")
|
|
379
|
-
toml_file.write_text("""
|
|
380
|
-
[project]
|
|
381
|
-
name = "my-project"
|
|
382
|
-
description = ""
|
|
383
|
-
authors = [{name="Waket Zheng", email="waketzheng@gmail.com"}]
|
|
384
|
-
readme = "README.md"
|
|
385
|
-
dynamic = ["version"]
|
|
386
|
-
keywords = []
|
|
387
|
-
requires-python = ">=3.9"
|
|
388
|
-
dependencies = []
|
|
389
|
-
|
|
390
|
-
[tool.pdm]
|
|
391
|
-
distribution = false
|
|
392
|
-
|
|
393
|
-
[tool.pdm.version]
|
|
394
|
-
source = "file"
|
|
395
|
-
path = "app/__init__.py"
|
|
396
|
-
|
|
397
|
-
[build-system]
|
|
398
|
-
requires = ["pdm-backend"]
|
|
399
|
-
build-backend = "pdm.backend"
|
|
400
|
-
""")
|
|
401
|
-
out = capture_cmd_output("fast bump patch")
|
|
402
|
-
assert str(version_file) in out
|
|
403
|
-
assert "0.3.1" in version_file.read_text()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from fast_dev_cli.cli import Exit, capture_cmd_output, run_by_subprocess
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_exec_dry():
|
|
7
|
+
out = capture_cmd_output('fast exec "echo hello" --dry')
|
|
8
|
+
assert "--> echo hello" in out
|
|
9
|
+
assert out.count("hello") == 1
|
|
10
|
+
out = capture_cmd_output('fast exec "echo hello|grep h" --dry')
|
|
11
|
+
assert "--> echo hello|grep h" in out
|
|
12
|
+
assert out.count("hello") == 1
|
|
13
|
+
out = capture_cmd_output(
|
|
14
|
+
'fast exec "invalid command" --dry && echo success || echo failed', shell=True
|
|
15
|
+
)
|
|
16
|
+
assert "success" in out
|
|
17
|
+
assert "failed" not in out
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_exec():
|
|
21
|
+
out = capture_cmd_output('fast exec "echo hello"')
|
|
22
|
+
assert "--> echo hello" in out
|
|
23
|
+
assert out.count("hello") == 2
|
|
24
|
+
out = capture_cmd_output('fast exec "echo hello|grep h"')
|
|
25
|
+
assert "--> echo hello|grep h" in out
|
|
26
|
+
assert out.count("hello") == 2
|
|
27
|
+
out = capture_cmd_output(
|
|
28
|
+
'fast exec "invalid command" && echo success || echo failed', shell=True
|
|
29
|
+
)
|
|
30
|
+
assert "failed" in out
|
|
31
|
+
assert "success" not in out
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_run_by_subprocess(capsys):
|
|
35
|
+
with pytest.raises(Exit):
|
|
36
|
+
run_by_subprocess("python -c 'import sys;sys.exit(1)'")
|
|
37
|
+
with pytest.raises(Exit, match="1"):
|
|
38
|
+
run_by_subprocess("not-exit-command")
|
|
39
|
+
out = capsys.readouterr().out
|
|
40
|
+
assert "Command not found: not-exit-command" in out
|
|
41
|
+
assert not run_by_subprocess(f"cat {__file__}|grep xxx")
|
|
@@ -52,12 +52,14 @@ def test_with_push(mocker):
|
|
|
52
52
|
mocker.patch.object(git_tag, "git_status", return_value="git push")
|
|
53
53
|
version = get_current_version()
|
|
54
54
|
prefix = "v" if "v" in capture_cmd_output(["git", "tag"]) else ""
|
|
55
|
-
|
|
55
|
+
sync = "pdm sync --prod"
|
|
56
|
+
push = "git push --tags"
|
|
57
|
+
assert git_tag.gen() == f"{sync} && git tag -a {prefix}{version} -m '' && {push}"
|
|
56
58
|
with _clear_tags():
|
|
57
59
|
git_tag_cmd = git_tag.gen()
|
|
58
|
-
assert git_tag_cmd == f"git tag -a {version} -m '' &&
|
|
60
|
+
assert git_tag_cmd == f"{sync} && git tag -a {version} -m '' && {push}"
|
|
59
61
|
mocker.patch.object(git_tag, "has_v_prefix", return_value=True)
|
|
60
|
-
tag_cmd = f"git tag -a v{version} -m '' &&
|
|
62
|
+
tag_cmd = f"{sync} && git tag -a v{version} -m '' && {push}"
|
|
61
63
|
assert git_tag.gen() == tag_cmd
|
|
62
64
|
mocker.patch.object(git_tag, "should_push", return_value=True)
|
|
63
65
|
assert git_tag.gen() == tag_cmd + " && git push"
|
|
@@ -292,7 +292,7 @@ fastapi = "^0.112.2"
|
|
|
292
292
|
|
|
293
293
|
def test_upgrade_uv_project():
|
|
294
294
|
cmd = "fast upgrade --tool=uv --dry"
|
|
295
|
-
expected = "uv lock --upgrade --verbose && uv sync --frozen"
|
|
295
|
+
expected = "uv lock --upgrade --verbose && uv sync --frozen --all-groups"
|
|
296
296
|
assert expected in capture_cmd_output(cmd)
|
|
297
297
|
assert expected in capture_cmd_output("pdm run " + cmd)
|
|
298
298
|
assert UpgradeDependencies(tool="uv").gen() == expected
|
|
@@ -300,7 +300,7 @@ def test_upgrade_uv_project():
|
|
|
300
300
|
|
|
301
301
|
def test_upgrade_pdm_project():
|
|
302
302
|
cmd = "fast upgrade --tool=pdm --dry"
|
|
303
|
-
expected = "pdm update --verbose && pdm
|
|
303
|
+
expected = "pdm update --verbose && pdm sync -G :all"
|
|
304
304
|
assert expected in capture_cmd_output(cmd)
|
|
305
305
|
assert expected in capture_cmd_output("pdm run " + cmd)
|
|
306
306
|
assert UpgradeDependencies(tool="pdm").gen() == expected
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.16.1"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|