fast-dev-cli 0.14.2__tar.gz → 0.15.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.14.2 → fast_dev_cli-0.15.0}/PKG-INFO +1 -1
- fast_dev_cli-0.15.0/fast_dev_cli/__init__.py +1 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/fast_dev_cli/cli.py +130 -53
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/pyproject.toml +2 -1
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/test_bump.py +30 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/test_functions.py +4 -2
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/test_lint.py +53 -5
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/test_upgrade.py +36 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/utils.py +12 -15
- fast_dev_cli-0.14.2/fast_dev_cli/__init__.py +0 -1
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/LICENSE +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/README.md +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/fast_dev_cli/__main__.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/fast_dev_cli/py.typed +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/pdm_build.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/scripts/check.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/scripts/format.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/scripts/test.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/__init__.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/conftest.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/test_fast_test.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/test_poetry_version_plugin.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/test_runserver.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/test_sync.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/test_tag.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/test_upload.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/test_version.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.15.0"
|
|
@@ -8,7 +8,14 @@ import subprocess # nosec:B404
|
|
|
8
8
|
import sys
|
|
9
9
|
from functools import cached_property
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import
|
|
11
|
+
from typing import (
|
|
12
|
+
Any,
|
|
13
|
+
Literal,
|
|
14
|
+
# Optional is required by typers
|
|
15
|
+
Optional,
|
|
16
|
+
cast,
|
|
17
|
+
get_args,
|
|
18
|
+
)
|
|
12
19
|
|
|
13
20
|
import emoji
|
|
14
21
|
import typer
|
|
@@ -41,6 +48,9 @@ cli = typer.Typer()
|
|
|
41
48
|
DryOption = Option(False, "--dry", help="Only print, not really run shell command")
|
|
42
49
|
TOML_FILE = "pyproject.toml"
|
|
43
50
|
ToolName = Literal["poetry", "pdm", "uv"]
|
|
51
|
+
ToolOption = Option(
|
|
52
|
+
"auto", "--tool", help="Explicit declare manage tool (default to auto detect)"
|
|
53
|
+
)
|
|
44
54
|
|
|
45
55
|
|
|
46
56
|
class ShellCommandError(Exception): ...
|
|
@@ -53,7 +63,7 @@ def poetry_module_name(name: str) -> str:
|
|
|
53
63
|
return canonicalize_name(name).replace("-", "_").replace(" ", "_")
|
|
54
64
|
|
|
55
65
|
|
|
56
|
-
def load_bool(name: str, default=False) -> bool:
|
|
66
|
+
def load_bool(name: str, default: bool = False) -> bool:
|
|
57
67
|
if not (v := os.getenv(name)):
|
|
58
68
|
return default
|
|
59
69
|
if (lower := v.lower()) in ("0", "false", "f", "off", "no", "n"):
|
|
@@ -71,13 +81,15 @@ def is_venv() -> bool:
|
|
|
71
81
|
)
|
|
72
82
|
|
|
73
83
|
|
|
74
|
-
def _run_shell(cmd: list[str] | str, **kw) -> subprocess.CompletedProcess:
|
|
84
|
+
def _run_shell(cmd: list[str] | str, **kw: Any) -> subprocess.CompletedProcess[str]:
|
|
75
85
|
if isinstance(cmd, str):
|
|
76
86
|
kw.setdefault("shell", True)
|
|
77
87
|
return subprocess.run(cmd, **kw) # nosec:B603
|
|
78
88
|
|
|
79
89
|
|
|
80
|
-
def run_and_echo(
|
|
90
|
+
def run_and_echo(
|
|
91
|
+
cmd: str, *, dry: bool = False, verbose: bool = True, **kw: Any
|
|
92
|
+
) -> int:
|
|
81
93
|
"""Run shell command with subprocess and print it"""
|
|
82
94
|
if verbose:
|
|
83
95
|
echo(f"--> {cmd}")
|
|
@@ -91,7 +103,9 @@ def check_call(cmd: str) -> bool:
|
|
|
91
103
|
return r.returncode == 0
|
|
92
104
|
|
|
93
105
|
|
|
94
|
-
def capture_cmd_output(
|
|
106
|
+
def capture_cmd_output(
|
|
107
|
+
command: list[str] | str, *, raises: bool = False, **kw: Any
|
|
108
|
+
) -> str:
|
|
95
109
|
if isinstance(command, str) and not kw.get("shell"):
|
|
96
110
|
command = shlex.split(command)
|
|
97
111
|
r = _run_shell(command, capture_output=True, encoding="utf-8", **kw)
|
|
@@ -100,12 +114,12 @@ def capture_cmd_output(command: list[str] | str, *, raises=False, **kw) -> str:
|
|
|
100
114
|
return r.stdout.strip() or r.stderr
|
|
101
115
|
|
|
102
116
|
|
|
103
|
-
def _parse_version(line: str, pattern: re.Pattern) -> str:
|
|
117
|
+
def _parse_version(line: str, pattern: re.Pattern[str]) -> str:
|
|
104
118
|
return pattern.sub("", line).split("#")[0].strip(" '\"")
|
|
105
119
|
|
|
106
120
|
|
|
107
121
|
def read_version_from_file(
|
|
108
|
-
package_name: str, work_dir=None, toml_text: str | None = None
|
|
122
|
+
package_name: str, work_dir: Path | None = None, toml_text: str | None = None
|
|
109
123
|
) -> str:
|
|
110
124
|
if toml_text is None:
|
|
111
125
|
toml_text = Project.load_toml_text()
|
|
@@ -136,7 +150,9 @@ def read_version_from_file(
|
|
|
136
150
|
|
|
137
151
|
|
|
138
152
|
def get_current_version(
|
|
139
|
-
verbose
|
|
153
|
+
verbose: bool = False,
|
|
154
|
+
is_poetry: bool | None = None,
|
|
155
|
+
package_name: str | None = None,
|
|
140
156
|
) -> str:
|
|
141
157
|
if is_poetry is None:
|
|
142
158
|
is_poetry = Project.manage_by_poetry()
|
|
@@ -164,9 +180,19 @@ def _ensure_bool(value: bool | OptionInfo) -> bool:
|
|
|
164
180
|
return value
|
|
165
181
|
|
|
166
182
|
|
|
183
|
+
def _ensure_str(value: str | OptionInfo) -> str:
|
|
184
|
+
if not isinstance(value, str):
|
|
185
|
+
value = getattr(value, "default", "")
|
|
186
|
+
return value
|
|
187
|
+
|
|
188
|
+
|
|
167
189
|
def exit_if_run_failed(
|
|
168
|
-
cmd: str,
|
|
169
|
-
|
|
190
|
+
cmd: str,
|
|
191
|
+
env: dict[str, str] | None = None,
|
|
192
|
+
_exit: bool = False,
|
|
193
|
+
dry: bool = False,
|
|
194
|
+
**kw: Any,
|
|
195
|
+
) -> subprocess.CompletedProcess[str]:
|
|
170
196
|
run_and_echo(cmd, dry=True)
|
|
171
197
|
if _ensure_bool(dry):
|
|
172
198
|
return subprocess.CompletedProcess("", 0)
|
|
@@ -181,7 +207,7 @@ def exit_if_run_failed(
|
|
|
181
207
|
|
|
182
208
|
|
|
183
209
|
class DryRun:
|
|
184
|
-
def __init__(self: Self, _exit=False, dry=False) -> None:
|
|
210
|
+
def __init__(self: Self, _exit: bool = False, dry: bool = False) -> None:
|
|
185
211
|
self.dry = dry
|
|
186
212
|
self._exit = _exit
|
|
187
213
|
|
|
@@ -199,7 +225,11 @@ class BumpUp(DryRun):
|
|
|
199
225
|
major = "major"
|
|
200
226
|
|
|
201
227
|
def __init__(
|
|
202
|
-
self: Self,
|
|
228
|
+
self: Self,
|
|
229
|
+
commit: bool,
|
|
230
|
+
part: str,
|
|
231
|
+
filename: str | None = None,
|
|
232
|
+
dry: bool = False,
|
|
203
233
|
) -> None:
|
|
204
234
|
self.commit = commit
|
|
205
235
|
self.part = part
|
|
@@ -209,9 +239,9 @@ class BumpUp(DryRun):
|
|
|
209
239
|
super().__init__(dry=dry)
|
|
210
240
|
|
|
211
241
|
@staticmethod
|
|
212
|
-
def get_last_commit_message() -> str:
|
|
242
|
+
def get_last_commit_message(raises: bool = False) -> str:
|
|
213
243
|
cmd = 'git show --pretty=format:"%s" -s HEAD'
|
|
214
|
-
return capture_cmd_output(cmd)
|
|
244
|
+
return capture_cmd_output(cmd, raises=raises)
|
|
215
245
|
|
|
216
246
|
@classmethod
|
|
217
247
|
def should_add_emoji(cls) -> bool:
|
|
@@ -219,9 +249,12 @@ class BumpUp(DryRun):
|
|
|
219
249
|
If last commit message is startswith emoji,
|
|
220
250
|
add a ⬆️ flag at the prefix of bump up commit message.
|
|
221
251
|
"""
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
252
|
+
try:
|
|
253
|
+
first_char = cls.get_last_commit_message(raises=True)[0]
|
|
254
|
+
except (IndexError, ShellCommandError):
|
|
255
|
+
return False
|
|
256
|
+
else:
|
|
257
|
+
return emoji.is_emoji(first_char)
|
|
225
258
|
|
|
226
259
|
@staticmethod
|
|
227
260
|
def parse_filename() -> str:
|
|
@@ -379,7 +412,9 @@ class Project:
|
|
|
379
412
|
return 'build-backend = "poetry' in text
|
|
380
413
|
|
|
381
414
|
@staticmethod
|
|
382
|
-
def work_dir(
|
|
415
|
+
def work_dir(
|
|
416
|
+
name: str, parent: Path, depth: int, be_file: bool = False
|
|
417
|
+
) -> Path | None:
|
|
383
418
|
for _ in range(depth):
|
|
384
419
|
if (f := parent.joinpath(name)).exists():
|
|
385
420
|
if be_file:
|
|
@@ -391,10 +426,10 @@ class Project:
|
|
|
391
426
|
@classmethod
|
|
392
427
|
def get_work_dir(
|
|
393
428
|
cls: type[Self],
|
|
394
|
-
name=TOML_FILE,
|
|
429
|
+
name: str = TOML_FILE,
|
|
395
430
|
cwd: Path | None = None,
|
|
396
|
-
allow_cwd=False,
|
|
397
|
-
be_file=False,
|
|
431
|
+
allow_cwd: bool = False,
|
|
432
|
+
be_file: bool = False,
|
|
398
433
|
) -> Path:
|
|
399
434
|
cwd = cwd or Path.cwd()
|
|
400
435
|
if d := cls.work_dir(name, cwd, cls.path_depth, be_file):
|
|
@@ -404,7 +439,7 @@ class Project:
|
|
|
404
439
|
raise EnvError(f"{name} not found! Make sure this is a poetry project.")
|
|
405
440
|
|
|
406
441
|
@classmethod
|
|
407
|
-
def load_toml_text(cls: type[Self], name=TOML_FILE) -> str:
|
|
442
|
+
def load_toml_text(cls: type[Self], name: str = TOML_FILE) -> str:
|
|
408
443
|
toml_file = cls.get_work_dir(name, be_file=True)
|
|
409
444
|
return toml_file.read_text("utf8")
|
|
410
445
|
|
|
@@ -421,7 +456,7 @@ class Project:
|
|
|
421
456
|
else:
|
|
422
457
|
for name in get_args(ToolName):
|
|
423
458
|
if f"[tool.{name}]" in text:
|
|
424
|
-
return name
|
|
459
|
+
return cast(ToolName, name)
|
|
425
460
|
# Poetry 2.0 default to not include the '[tool.poetry]' section
|
|
426
461
|
if cls.is_poetry_v2(text):
|
|
427
462
|
return "poetry"
|
|
@@ -447,6 +482,12 @@ class ParseError(Exception):
|
|
|
447
482
|
|
|
448
483
|
|
|
449
484
|
class UpgradeDependencies(Project, DryRun):
|
|
485
|
+
def __init__(
|
|
486
|
+
self: Self, _exit: bool = False, dry: bool = False, tool: ToolName = "poetry"
|
|
487
|
+
) -> None:
|
|
488
|
+
super().__init__(_exit, dry)
|
|
489
|
+
self._tool = tool
|
|
490
|
+
|
|
450
491
|
class DevFlag(StrEnum):
|
|
451
492
|
new = "[tool.poetry.group.dev.dependencies]"
|
|
452
493
|
old = "[tool.poetry.dev-dependencies]"
|
|
@@ -538,7 +579,7 @@ class UpgradeDependencies(Project, DryRun):
|
|
|
538
579
|
return cls.DevFlag.new in text or cls.DevFlag.old in text
|
|
539
580
|
|
|
540
581
|
@staticmethod
|
|
541
|
-
def parse_item(toml_str) -> list[str]:
|
|
582
|
+
def parse_item(toml_str: str) -> list[str]:
|
|
542
583
|
lines: list[str] = []
|
|
543
584
|
for line in toml_str.splitlines():
|
|
544
585
|
if (line := line.strip()).startswith("["):
|
|
@@ -608,20 +649,26 @@ class UpgradeDependencies(Project, DryRun):
|
|
|
608
649
|
return _upgrade
|
|
609
650
|
|
|
610
651
|
def gen(self: Self) -> str:
|
|
652
|
+
if self._tool == "uv":
|
|
653
|
+
return "uv lock --upgrade --verbose && uv sync --frozen"
|
|
654
|
+
elif self._tool == "pdm":
|
|
655
|
+
return "pdm update --verbose && pdm install"
|
|
611
656
|
return self.gen_cmd() + " && poetry lock && poetry update"
|
|
612
657
|
|
|
613
658
|
|
|
614
659
|
@cli.command()
|
|
615
660
|
def upgrade(
|
|
661
|
+
tool: str = ToolOption,
|
|
616
662
|
dry: bool = DryOption,
|
|
617
663
|
) -> None:
|
|
618
664
|
"""Upgrade dependencies in pyproject.toml to latest versions"""
|
|
619
|
-
if (tool :=
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
665
|
+
if not (tool := _ensure_str(tool)) or tool == ToolOption.default:
|
|
666
|
+
tool = Project.get_manage_tool() or "uv"
|
|
667
|
+
if tool in get_args(ToolName):
|
|
668
|
+
UpgradeDependencies(dry=dry, tool=cast(ToolName, tool)).run()
|
|
623
669
|
else:
|
|
624
|
-
|
|
670
|
+
secho(f"Unknown tool {tool!r}", fg=typer.colors.YELLOW)
|
|
671
|
+
raise typer.Exit(1)
|
|
625
672
|
|
|
626
673
|
|
|
627
674
|
class GitTag(DryRun):
|
|
@@ -676,19 +723,21 @@ def tag(
|
|
|
676
723
|
class LintCode(DryRun):
|
|
677
724
|
def __init__(
|
|
678
725
|
self: Self,
|
|
679
|
-
args,
|
|
680
|
-
check_only=False,
|
|
681
|
-
_exit=False,
|
|
682
|
-
dry=False,
|
|
683
|
-
bandit=False,
|
|
684
|
-
skip_mypy=False,
|
|
685
|
-
dmypy=False,
|
|
726
|
+
args: list[str] | str | None,
|
|
727
|
+
check_only: bool = False,
|
|
728
|
+
_exit: bool = False,
|
|
729
|
+
dry: bool = False,
|
|
730
|
+
bandit: bool = False,
|
|
731
|
+
skip_mypy: bool = False,
|
|
732
|
+
dmypy: bool = False,
|
|
733
|
+
tool: str = ToolOption.default,
|
|
686
734
|
) -> None:
|
|
687
735
|
self.args = args
|
|
688
736
|
self.check_only = check_only
|
|
689
737
|
self._bandit = bandit
|
|
690
738
|
self._skip_mypy = skip_mypy
|
|
691
739
|
self._use_dmypy = dmypy
|
|
740
|
+
self._tool = tool
|
|
692
741
|
super().__init__(_exit, dry)
|
|
693
742
|
|
|
694
743
|
@staticmethod
|
|
@@ -696,7 +745,7 @@ class LintCode(DryRun):
|
|
|
696
745
|
return check_call("ruff --version")
|
|
697
746
|
|
|
698
747
|
@staticmethod
|
|
699
|
-
def prefer_dmypy(paths: str, tools: list[str], use_dmypy=False) -> bool:
|
|
748
|
+
def prefer_dmypy(paths: str, tools: list[str], use_dmypy: bool = False) -> bool:
|
|
700
749
|
return (
|
|
701
750
|
paths == "."
|
|
702
751
|
and any(t.startswith("mypy") for t in tools)
|
|
@@ -706,7 +755,8 @@ class LintCode(DryRun):
|
|
|
706
755
|
@staticmethod
|
|
707
756
|
def get_package_name() -> str:
|
|
708
757
|
root = Project.get_work_dir(allow_cwd=True)
|
|
709
|
-
|
|
758
|
+
module_name = root.name.replace("-", "_").replace(" ", "_")
|
|
759
|
+
package_maybe = (module_name, "src")
|
|
710
760
|
for name in package_maybe:
|
|
711
761
|
if root.joinpath(name).is_dir():
|
|
712
762
|
return name
|
|
@@ -720,6 +770,7 @@ class LintCode(DryRun):
|
|
|
720
770
|
bandit: bool = False,
|
|
721
771
|
skip_mypy: bool = False,
|
|
722
772
|
use_dmypy: bool = False,
|
|
773
|
+
tool: str = ToolOption.default,
|
|
723
774
|
) -> str:
|
|
724
775
|
if paths != "." and all(i.endswith(".html") for i in paths.split()):
|
|
725
776
|
return f"prettier -w {paths}"
|
|
@@ -748,8 +799,11 @@ class LintCode(DryRun):
|
|
|
748
799
|
secho(f"{tip}\n\n {command}\n", fg="yellow")
|
|
749
800
|
else:
|
|
750
801
|
should_run_by_tool = True
|
|
751
|
-
if should_run_by_tool and
|
|
752
|
-
|
|
802
|
+
if should_run_by_tool and tool:
|
|
803
|
+
if tool == ToolOption.default:
|
|
804
|
+
tool = Project.get_manage_tool() or ""
|
|
805
|
+
if tool:
|
|
806
|
+
prefix = tool + " run "
|
|
753
807
|
if cls.prefer_dmypy(paths, tools, use_dmypy=use_dmypy):
|
|
754
808
|
tools[-1] = "dmypy run"
|
|
755
809
|
cmd += lint_them.format(prefix, paths, *tools)
|
|
@@ -766,7 +820,9 @@ class LintCode(DryRun):
|
|
|
766
820
|
return cmd
|
|
767
821
|
|
|
768
822
|
def gen(self: Self) -> str:
|
|
769
|
-
|
|
823
|
+
if isinstance(args := self.args, str):
|
|
824
|
+
args = args.split()
|
|
825
|
+
paths = " ".join(map(str, args)) if args else "."
|
|
770
826
|
return self.to_cmd(
|
|
771
827
|
paths, self.check_only, self._bandit, self._skip_mypy, self._use_dmypy
|
|
772
828
|
)
|
|
@@ -776,15 +832,31 @@ def parse_files(args: list[str] | tuple[str, ...]) -> list[str]:
|
|
|
776
832
|
return [i for i in args if not i.startswith("-")]
|
|
777
833
|
|
|
778
834
|
|
|
779
|
-
def lint(
|
|
835
|
+
def lint(
|
|
836
|
+
files: list[str] | str | None = None,
|
|
837
|
+
dry: bool = False,
|
|
838
|
+
bandit: bool = False,
|
|
839
|
+
skip_mypy: bool = False,
|
|
840
|
+
dmypy: bool = False,
|
|
841
|
+
tool: str = ToolOption.default,
|
|
842
|
+
) -> None:
|
|
780
843
|
if files is None:
|
|
781
844
|
files = parse_files(sys.argv[1:])
|
|
782
845
|
if files and files[0] == "lint":
|
|
783
846
|
files = files[1:]
|
|
784
|
-
LintCode(
|
|
847
|
+
LintCode(
|
|
848
|
+
files, dry=dry, skip_mypy=skip_mypy, bandit=bandit, dmypy=dmypy, tool=tool
|
|
849
|
+
).run()
|
|
785
850
|
|
|
786
851
|
|
|
787
|
-
def check(
|
|
852
|
+
def check(
|
|
853
|
+
files: list[str] | str | None = None,
|
|
854
|
+
dry: bool = False,
|
|
855
|
+
bandit: bool = False,
|
|
856
|
+
skip_mypy: bool = False,
|
|
857
|
+
dmypy: bool = False,
|
|
858
|
+
tool: str = ToolOption.default,
|
|
859
|
+
) -> None:
|
|
788
860
|
LintCode(
|
|
789
861
|
files,
|
|
790
862
|
check_only=True,
|
|
@@ -793,32 +865,35 @@ def check(files=None, dry=False, bandit=False, skip_mypy=False, dmypy=False) ->
|
|
|
793
865
|
bandit=bandit,
|
|
794
866
|
skip_mypy=skip_mypy,
|
|
795
867
|
dmypy=dmypy,
|
|
868
|
+
tool=tool,
|
|
796
869
|
).run()
|
|
797
870
|
|
|
798
871
|
|
|
799
872
|
@cli.command(name="lint")
|
|
800
873
|
def make_style(
|
|
801
|
-
files: Optional[list[
|
|
874
|
+
files: Optional[list[str]] = typer.Argument(default=None), # noqa:B008
|
|
802
875
|
check_only: bool = Option(False, "--check-only", "-c"),
|
|
803
876
|
bandit: bool = Option(False, "--bandit", help="Run `bandit -r <package_dir>`"),
|
|
804
877
|
skip_mypy: bool = Option(False, "--skip-mypy"),
|
|
805
878
|
use_dmypy: bool = Option(
|
|
806
879
|
False, "--dmypy", help="Use `dmypy run` instead of `mypy`"
|
|
807
880
|
),
|
|
881
|
+
tool: str = ToolOption,
|
|
808
882
|
dry: bool = DryOption,
|
|
809
883
|
) -> None:
|
|
810
884
|
"""Run: ruff check/format to reformat code and then mypy to check"""
|
|
811
885
|
if getattr(files, "default", files) is None:
|
|
812
|
-
files = [
|
|
886
|
+
files = ["."]
|
|
813
887
|
elif isinstance(files, str):
|
|
814
888
|
files = [files]
|
|
815
889
|
skip = _ensure_bool(skip_mypy)
|
|
816
890
|
dmypy = _ensure_bool(use_dmypy)
|
|
817
891
|
bandit = _ensure_bool(bandit)
|
|
892
|
+
tool = _ensure_str(tool)
|
|
818
893
|
if _ensure_bool(check_only):
|
|
819
|
-
check(files, dry=dry, skip_mypy=skip, dmypy=dmypy, bandit=bandit)
|
|
894
|
+
check(files, dry=dry, skip_mypy=skip, dmypy=dmypy, bandit=bandit, tool=tool)
|
|
820
895
|
else:
|
|
821
|
-
lint(files, dry=dry, skip_mypy=skip, dmypy=dmypy, bandit=bandit)
|
|
896
|
+
lint(files, dry=dry, skip_mypy=skip, dmypy=dmypy, bandit=bandit, tool=tool)
|
|
822
897
|
|
|
823
898
|
|
|
824
899
|
@cli.command(name="check")
|
|
@@ -832,7 +907,9 @@ def only_check(
|
|
|
832
907
|
|
|
833
908
|
|
|
834
909
|
class Sync(DryRun):
|
|
835
|
-
def __init__(
|
|
910
|
+
def __init__(
|
|
911
|
+
self: Self, filename: str, extras: str, save: bool, dry: bool = False
|
|
912
|
+
) -> None:
|
|
836
913
|
self.filename = filename
|
|
837
914
|
self.extras = extras
|
|
838
915
|
self._save = save
|
|
@@ -871,7 +948,7 @@ class Sync(DryRun):
|
|
|
871
948
|
|
|
872
949
|
@cli.command()
|
|
873
950
|
def sync(
|
|
874
|
-
filename="dev_requirements.txt",
|
|
951
|
+
filename: str = "dev_requirements.txt",
|
|
875
952
|
extras: str = Option("", "--extras", "-E"),
|
|
876
953
|
save: bool = Option(
|
|
877
954
|
False, "--save", "-s", help="Whether save the requirement file"
|
|
@@ -889,7 +966,7 @@ def _should_run_test_script(path: Path = Path("scripts")) -> Path | None:
|
|
|
889
966
|
return None
|
|
890
967
|
|
|
891
968
|
|
|
892
|
-
def test(dry: bool, ignore_script=False) -> None:
|
|
969
|
+
def test(dry: bool, ignore_script: bool = False) -> None:
|
|
893
970
|
cwd = Path.cwd()
|
|
894
971
|
root = Project.get_work_dir(cwd=cwd, allow_cwd=True)
|
|
895
972
|
script_dir = root / "scripts"
|
|
@@ -944,13 +1021,13 @@ def dev(
|
|
|
944
1021
|
port: int | None | OptionInfo,
|
|
945
1022
|
host: str | None | OptionInfo,
|
|
946
1023
|
file: str | None | ArgumentInfo = None,
|
|
947
|
-
dry=False,
|
|
1024
|
+
dry: bool = False,
|
|
948
1025
|
) -> None:
|
|
949
1026
|
cmd = "fastapi dev"
|
|
950
1027
|
no_port_yet = True
|
|
951
1028
|
if file is not None:
|
|
952
1029
|
try:
|
|
953
|
-
port = int(str(file))
|
|
1030
|
+
port = int(str(file))
|
|
954
1031
|
except ValueError:
|
|
955
1032
|
cmd += f" {file}"
|
|
956
1033
|
else:
|
|
@@ -41,7 +41,7 @@ dependencies = [
|
|
|
41
41
|
"pytest >=8.2.0,<9",
|
|
42
42
|
"packaging>=20.5",
|
|
43
43
|
]
|
|
44
|
-
version = "0.
|
|
44
|
+
version = "0.15.0"
|
|
45
45
|
|
|
46
46
|
[project.urls]
|
|
47
47
|
Homepage = "https://github.com/waketzheng/fast-dev-cli"
|
|
@@ -124,6 +124,7 @@ pretty = true
|
|
|
124
124
|
python_version = "3.9"
|
|
125
125
|
ignore_missing_imports = true
|
|
126
126
|
check_untyped_defs = true
|
|
127
|
+
warn_unused_ignores = true
|
|
127
128
|
exclude = [
|
|
128
129
|
"^fabfile\\.py$",
|
|
129
130
|
"two\\.pyi$",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import shutil
|
|
2
3
|
import subprocess
|
|
3
4
|
from contextlib import redirect_stdout
|
|
4
5
|
from io import StringIO
|
|
@@ -200,3 +201,32 @@ def test_bump_with_uv(tmp_path):
|
|
|
200
201
|
Path(TOML_FILE).write_text("[project]" + os.linesep + 'version = "0.1.0"')
|
|
201
202
|
command = BumpUp(part="patch", commit=True).gen()
|
|
202
203
|
assert TOML_FILE in command
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_parse_filename(tmp_path):
|
|
207
|
+
pyproject = """
|
|
208
|
+
[tool.poetry]
|
|
209
|
+
version = "0"
|
|
210
|
+
"""
|
|
211
|
+
project_dir = tmp_path / "helloworld"
|
|
212
|
+
project_dir.mkdir()
|
|
213
|
+
with chdir(project_dir):
|
|
214
|
+
toml_file = project_dir.joinpath(TOML_FILE)
|
|
215
|
+
toml_file.write_text(pyproject)
|
|
216
|
+
src_dir = project_dir / project_dir.name
|
|
217
|
+
src_dir.mkdir()
|
|
218
|
+
init_file = src_dir / "__init__.py"
|
|
219
|
+
init_file.write_text('__version__ = "0.1.0"')
|
|
220
|
+
another_dir = project_dir / "hello"
|
|
221
|
+
another_dir.mkdir()
|
|
222
|
+
shutil.copy(init_file, another_dir / init_file.name)
|
|
223
|
+
assert BumpUp.parse_filename() == "helloworld/__init__.py"
|
|
224
|
+
toml_file.write_text(pyproject.strip() + '\npackages=[{include="hello"}]')
|
|
225
|
+
assert BumpUp.parse_filename() == "hello/__init__.py"
|
|
226
|
+
toml_file.write_text(
|
|
227
|
+
pyproject.strip() + '\npackages=[{include="hello",from="py"}]'
|
|
228
|
+
)
|
|
229
|
+
from_dir = project_dir / "py"
|
|
230
|
+
from_dir.mkdir()
|
|
231
|
+
shutil.move(another_dir, from_dir)
|
|
232
|
+
assert BumpUp.parse_filename() == "py/hello/__init__.py"
|
|
@@ -72,8 +72,10 @@ def test_run_shell():
|
|
|
72
72
|
value = "foo"
|
|
73
73
|
cmd = 'python -c "import os;print(list(os.environ))"'
|
|
74
74
|
with redirect_stdout(StringIO()):
|
|
75
|
-
r = exit_if_run_failed(
|
|
76
|
-
|
|
75
|
+
r = exit_if_run_failed(
|
|
76
|
+
cmd, env={name: value}, capture_output=True, encoding="utf-8"
|
|
77
|
+
)
|
|
78
|
+
assert name in r.stdout
|
|
77
79
|
|
|
78
80
|
assert run_and_echo("echo foo", capture_output=True) == 0
|
|
79
81
|
|
|
@@ -82,11 +82,21 @@ def test_check_bandit(tmp_path):
|
|
|
82
82
|
src_dir = package_path / "src"
|
|
83
83
|
if not src_dir.exists(): # For poetry<2.1
|
|
84
84
|
src_dir = src_dir.parent / package_path.name
|
|
85
|
+
with chdir(package_path):
|
|
86
|
+
package_name = src_dir.name
|
|
87
|
+
assert f"bandit -r {package_name}" in LintCode.to_cmd(bandit=True)
|
|
88
|
+
toml_file = Path(TOML_FILE)
|
|
89
|
+
content = toml_file.read_text()
|
|
90
|
+
toml_file.write_text(content + '\n[tool.bandit]\nexclude_dirs = ["tests"]')
|
|
91
|
+
assert f"bandit -c {TOML_FILE} -r ." in LintCode.to_cmd(bandit=True)
|
|
85
92
|
shutil.rmtree(src_dir)
|
|
86
93
|
with chdir(package_path):
|
|
87
94
|
assert LintCode.get_package_name() == "."
|
|
88
95
|
command = capture_cmd_output("fast check --bandit --dry")
|
|
89
|
-
|
|
96
|
+
assert f"bandit -c {TOML_FILE} -r ." in command
|
|
97
|
+
toml_file.write_text(content)
|
|
98
|
+
command = capture_cmd_output("fast check --bandit --dry")
|
|
99
|
+
assert "bandit -r ." in command
|
|
90
100
|
|
|
91
101
|
|
|
92
102
|
def test_check_skip_mypy(mock_skip_mypy_0, mocker, capsys):
|
|
@@ -140,6 +150,14 @@ def test_lint_html():
|
|
|
140
150
|
assert "prettier -w index.html" in command
|
|
141
151
|
command = capture_cmd_output(f"{lint_cmd} index.html flv.html --dry")
|
|
142
152
|
assert "prettier -w index.html flv.html" in command
|
|
153
|
+
cmd = "fast lint index.html --dry"
|
|
154
|
+
assert "prettier -w index.html" in capture_cmd_output(cmd)
|
|
155
|
+
assert "prettier -w index.html" in capture_cmd_output("pdm run " + cmd)
|
|
156
|
+
cmd = "fast lint index.html flv.html --dry"
|
|
157
|
+
assert "prettier -w index.html flv.html" in capture_cmd_output(cmd)
|
|
158
|
+
assert "prettier -w index.html flv.html" in capture_cmd_output("pdm run " + cmd)
|
|
159
|
+
assert LintCode.to_cmd("index.html") == "prettier -w index.html"
|
|
160
|
+
assert LintCode.to_cmd("index.html flv.html") == "prettier -w index.html flv.html"
|
|
143
161
|
|
|
144
162
|
|
|
145
163
|
def test_lint_by_global_fast():
|
|
@@ -151,20 +169,29 @@ def test_lint_by_global_fast():
|
|
|
151
169
|
|
|
152
170
|
|
|
153
171
|
def test_with_dmypy():
|
|
154
|
-
|
|
172
|
+
cmd = "fast lint --dmypy --dry ."
|
|
173
|
+
assert "dmypy run ." in capture_cmd_output(cmd)
|
|
174
|
+
assert "dmypy run ." in capture_cmd_output("pdm run " + cmd)
|
|
175
|
+
command = LintCode.to_cmd(use_dmypy=True, tool="pdm")
|
|
155
176
|
assert "dmypy run ." in command
|
|
177
|
+
command = LintCode.to_cmd(use_dmypy=False, tool="pdm")
|
|
178
|
+
assert "dmypy run ." not in command
|
|
156
179
|
|
|
157
180
|
|
|
158
181
|
def test_dmypy_run(monkeypatch):
|
|
182
|
+
command = capture_cmd_output("python -m fast_dev_cli lint --dry .")
|
|
183
|
+
assert "dmypy run ." not in command
|
|
159
184
|
monkeypatch.setenv("FASTDEVCLI_DMYPY", "1")
|
|
160
185
|
command = capture_cmd_output("python -m fast_dev_cli lint --dry .")
|
|
161
186
|
assert "dmypy run ." in command
|
|
187
|
+
command = capture_cmd_output("python -m fast_dev_cli lint --skip-mypy --dry .")
|
|
188
|
+
assert "dmypy run ." not in command
|
|
162
189
|
|
|
163
190
|
|
|
164
191
|
def test_lint_with_prefix(mocker):
|
|
165
192
|
mocker.patch("fast_dev_cli.cli.is_venv", return_value=False)
|
|
166
193
|
with capture_stdout() as stream:
|
|
167
|
-
make_style([
|
|
194
|
+
make_style(["."], check_only=False, dry=True)
|
|
168
195
|
assert "pdm run" in stream.getvalue()
|
|
169
196
|
|
|
170
197
|
|
|
@@ -174,13 +201,13 @@ def test_make_style(mock_skip_mypy_0, mocker, mock_no_dmypy):
|
|
|
174
201
|
make_style(check_only=False, dry=True)
|
|
175
202
|
assert LINT_CMD in stream.getvalue()
|
|
176
203
|
with capture_stdout() as stream:
|
|
177
|
-
make_style([
|
|
204
|
+
make_style(["."], check_only=False, dry=True)
|
|
178
205
|
assert LINT_CMD in stream.getvalue()
|
|
179
206
|
with capture_stdout() as stream:
|
|
180
207
|
make_style(".", check_only=False, dry=True) # type:ignore[arg-type]
|
|
181
208
|
assert LINT_CMD in stream.getvalue()
|
|
182
209
|
with capture_stdout() as stream:
|
|
183
|
-
make_style([
|
|
210
|
+
make_style(["."], check_only=True, dry=True)
|
|
184
211
|
assert CHECK_CMD in stream.getvalue()
|
|
185
212
|
with capture_stdout() as stream:
|
|
186
213
|
only_check(dry=True)
|
|
@@ -297,3 +324,24 @@ def test_get_manage_tool(tmp_path):
|
|
|
297
324
|
assert Project.get_manage_tool() == "pdm"
|
|
298
325
|
Path(TOML_FILE).write_text("[tool.uv]")
|
|
299
326
|
assert Project.get_manage_tool() == "uv"
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class TestGetPackageName:
|
|
330
|
+
project = "hello-world"
|
|
331
|
+
|
|
332
|
+
def test_get_package_name(self, tmp_path):
|
|
333
|
+
project_dir = tmp_path / self.project
|
|
334
|
+
project_dir.mkdir()
|
|
335
|
+
module_name = project_dir.name.replace("-", "_").replace(" ", "_")
|
|
336
|
+
with chdir(project_dir):
|
|
337
|
+
Path(TOML_FILE).touch()
|
|
338
|
+
Path(module_name).mkdir()
|
|
339
|
+
assert LintCode.get_package_name() == module_name
|
|
340
|
+
Path("src").mkdir()
|
|
341
|
+
assert LintCode.get_package_name() == module_name
|
|
342
|
+
shutil.rmtree(module_name)
|
|
343
|
+
assert LintCode.get_package_name() == "src"
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class TestGetPackageNameWithSpace(TestGetPackageName):
|
|
347
|
+
project = "hello world"
|
|
@@ -6,9 +6,14 @@ from contextlib import redirect_stdout
|
|
|
6
6
|
from io import StringIO
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
|
+
import pytest
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
import fast_dev_cli
|
|
9
13
|
from fast_dev_cli.cli import (
|
|
10
14
|
TOML_FILE,
|
|
11
15
|
UpgradeDependencies,
|
|
16
|
+
capture_cmd_output,
|
|
12
17
|
run_and_echo,
|
|
13
18
|
upgrade,
|
|
14
19
|
)
|
|
@@ -283,3 +288,34 @@ fastapi = "^0.112.2"
|
|
|
283
288
|
[],
|
|
284
289
|
"--dev",
|
|
285
290
|
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def test_upgrade_uv_project():
|
|
294
|
+
cmd = "fast upgrade --tool=uv --dry"
|
|
295
|
+
expected = "uv lock --upgrade --verbose && uv sync --frozen"
|
|
296
|
+
assert expected in capture_cmd_output(cmd)
|
|
297
|
+
assert expected in capture_cmd_output("pdm run " + cmd)
|
|
298
|
+
assert UpgradeDependencies(tool="uv").gen() == expected
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def test_upgrade_pdm_project():
|
|
302
|
+
cmd = "fast upgrade --tool=pdm --dry"
|
|
303
|
+
expected = "pdm update --verbose && pdm install"
|
|
304
|
+
assert expected in capture_cmd_output(cmd)
|
|
305
|
+
assert expected in capture_cmd_output("pdm run " + cmd)
|
|
306
|
+
assert UpgradeDependencies(tool="pdm").gen() == expected
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def test_upgrade_unknown_tool(mocker):
|
|
310
|
+
cmd = "fast upgrade --tool=hatch --dry"
|
|
311
|
+
expected = "Unknown tool 'hatch'"
|
|
312
|
+
assert expected in capture_cmd_output(cmd)
|
|
313
|
+
assert expected in capture_cmd_output("pdm run " + cmd)
|
|
314
|
+
assert run_and_echo(cmd, verbose=False) == 1
|
|
315
|
+
assert run_and_echo("pdm run " + cmd, verbose=False) == 1
|
|
316
|
+
mocker.patch("fast_dev_cli.cli.secho")
|
|
317
|
+
with pytest.raises(typer.Exit):
|
|
318
|
+
upgrade(tool="pipenv")
|
|
319
|
+
fast_dev_cli.cli.secho.assert_called_once_with( # type:ignore
|
|
320
|
+
"Unknown tool 'pipenv'", fg=typer.colors.YELLOW
|
|
321
|
+
)
|
|
@@ -1,28 +1,25 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import subprocess
|
|
3
3
|
import sys
|
|
4
|
-
from contextlib import contextmanager, redirect_stdout
|
|
4
|
+
from contextlib import AbstractContextManager, contextmanager, redirect_stdout
|
|
5
5
|
from io import StringIO
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
-
if sys.version_info >= (3, 11):
|
|
9
|
-
from contextlib import chdir # type:ignore[attr-defined]
|
|
10
|
-
else:
|
|
11
|
-
from contextlib import AbstractContextManager
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
# TODO: use `from contextlib import chdir` instead when drop support for Python3.10
|
|
10
|
+
class chdir(AbstractContextManager): # Copied from source code of Python3.13
|
|
11
|
+
"""Non thread-safe context manager to change the current working directory."""
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
def __init__(self, path):
|
|
14
|
+
self.path = path
|
|
15
|
+
self._old_cwd = []
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
def __enter__(self):
|
|
18
|
+
self._old_cwd.append(os.getcwd())
|
|
19
|
+
os.chdir(self.path)
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
def __exit__(self, *excinfo):
|
|
22
|
+
os.chdir(self._old_cwd.pop())
|
|
26
23
|
|
|
27
24
|
|
|
28
25
|
__all__ = (
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.14.2"
|
|
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
|