fast-dev-cli 0.14.1__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.1 → 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.1 → fast_dev_cli-0.15.0}/fast_dev_cli/cli.py +142 -62
- {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/pyproject.toml +2 -1
- {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/test_bump.py +79 -36
- {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/test_functions.py +16 -8
- {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/test_lint.py +64 -6
- {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/test_upgrade.py +36 -0
- {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/utils.py +22 -15
- fast_dev_cli-0.14.1/fast_dev_cli/__init__.py +0 -1
- {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/LICENSE +0 -0
- {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/README.md +0 -0
- {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/fast_dev_cli/__main__.py +0 -0
- {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/fast_dev_cli/py.typed +0 -0
- {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/pdm_build.py +0 -0
- {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/scripts/check.py +0 -0
- {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/scripts/format.py +0 -0
- {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/scripts/test.py +0 -0
- {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/__init__.py +0 -0
- {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/conftest.py +0 -0
- {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/test_fast_test.py +0 -0
- {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/test_poetry_version_plugin.py +0 -0
- {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/test_runserver.py +0 -0
- {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/test_sync.py +0 -0
- {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/test_tag.py +0 -0
- {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/test_upload.py +0 -0
- {fast_dev_cli-0.14.1 → 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
|
|
@@ -38,8 +45,12 @@ else: # pragma: no cover
|
|
|
38
45
|
|
|
39
46
|
|
|
40
47
|
cli = typer.Typer()
|
|
48
|
+
DryOption = Option(False, "--dry", help="Only print, not really run shell command")
|
|
41
49
|
TOML_FILE = "pyproject.toml"
|
|
42
50
|
ToolName = Literal["poetry", "pdm", "uv"]
|
|
51
|
+
ToolOption = Option(
|
|
52
|
+
"auto", "--tool", help="Explicit declare manage tool (default to auto detect)"
|
|
53
|
+
)
|
|
43
54
|
|
|
44
55
|
|
|
45
56
|
class ShellCommandError(Exception): ...
|
|
@@ -52,7 +63,7 @@ def poetry_module_name(name: str) -> str:
|
|
|
52
63
|
return canonicalize_name(name).replace("-", "_").replace(" ", "_")
|
|
53
64
|
|
|
54
65
|
|
|
55
|
-
def load_bool(name: str, default=False) -> bool:
|
|
66
|
+
def load_bool(name: str, default: bool = False) -> bool:
|
|
56
67
|
if not (v := os.getenv(name)):
|
|
57
68
|
return default
|
|
58
69
|
if (lower := v.lower()) in ("0", "false", "f", "off", "no", "n"):
|
|
@@ -70,13 +81,15 @@ def is_venv() -> bool:
|
|
|
70
81
|
)
|
|
71
82
|
|
|
72
83
|
|
|
73
|
-
def _run_shell(cmd: list[str] | str, **kw) -> subprocess.CompletedProcess:
|
|
84
|
+
def _run_shell(cmd: list[str] | str, **kw: Any) -> subprocess.CompletedProcess[str]:
|
|
74
85
|
if isinstance(cmd, str):
|
|
75
86
|
kw.setdefault("shell", True)
|
|
76
87
|
return subprocess.run(cmd, **kw) # nosec:B603
|
|
77
88
|
|
|
78
89
|
|
|
79
|
-
def run_and_echo(
|
|
90
|
+
def run_and_echo(
|
|
91
|
+
cmd: str, *, dry: bool = False, verbose: bool = True, **kw: Any
|
|
92
|
+
) -> int:
|
|
80
93
|
"""Run shell command with subprocess and print it"""
|
|
81
94
|
if verbose:
|
|
82
95
|
echo(f"--> {cmd}")
|
|
@@ -90,7 +103,9 @@ def check_call(cmd: str) -> bool:
|
|
|
90
103
|
return r.returncode == 0
|
|
91
104
|
|
|
92
105
|
|
|
93
|
-
def capture_cmd_output(
|
|
106
|
+
def capture_cmd_output(
|
|
107
|
+
command: list[str] | str, *, raises: bool = False, **kw: Any
|
|
108
|
+
) -> str:
|
|
94
109
|
if isinstance(command, str) and not kw.get("shell"):
|
|
95
110
|
command = shlex.split(command)
|
|
96
111
|
r = _run_shell(command, capture_output=True, encoding="utf-8", **kw)
|
|
@@ -99,12 +114,12 @@ def capture_cmd_output(command: list[str] | str, *, raises=False, **kw) -> str:
|
|
|
99
114
|
return r.stdout.strip() or r.stderr
|
|
100
115
|
|
|
101
116
|
|
|
102
|
-
def _parse_version(line: str, pattern: re.Pattern) -> str:
|
|
117
|
+
def _parse_version(line: str, pattern: re.Pattern[str]) -> str:
|
|
103
118
|
return pattern.sub("", line).split("#")[0].strip(" '\"")
|
|
104
119
|
|
|
105
120
|
|
|
106
121
|
def read_version_from_file(
|
|
107
|
-
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
|
|
108
123
|
) -> str:
|
|
109
124
|
if toml_text is None:
|
|
110
125
|
toml_text = Project.load_toml_text()
|
|
@@ -135,7 +150,9 @@ def read_version_from_file(
|
|
|
135
150
|
|
|
136
151
|
|
|
137
152
|
def get_current_version(
|
|
138
|
-
verbose
|
|
153
|
+
verbose: bool = False,
|
|
154
|
+
is_poetry: bool | None = None,
|
|
155
|
+
package_name: str | None = None,
|
|
139
156
|
) -> str:
|
|
140
157
|
if is_poetry is None:
|
|
141
158
|
is_poetry = Project.manage_by_poetry()
|
|
@@ -163,9 +180,19 @@ def _ensure_bool(value: bool | OptionInfo) -> bool:
|
|
|
163
180
|
return value
|
|
164
181
|
|
|
165
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
|
+
|
|
166
189
|
def exit_if_run_failed(
|
|
167
|
-
cmd: str,
|
|
168
|
-
|
|
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]:
|
|
169
196
|
run_and_echo(cmd, dry=True)
|
|
170
197
|
if _ensure_bool(dry):
|
|
171
198
|
return subprocess.CompletedProcess("", 0)
|
|
@@ -180,7 +207,7 @@ def exit_if_run_failed(
|
|
|
180
207
|
|
|
181
208
|
|
|
182
209
|
class DryRun:
|
|
183
|
-
def __init__(self: Self, _exit=False, dry=False) -> None:
|
|
210
|
+
def __init__(self: Self, _exit: bool = False, dry: bool = False) -> None:
|
|
184
211
|
self.dry = dry
|
|
185
212
|
self._exit = _exit
|
|
186
213
|
|
|
@@ -198,7 +225,11 @@ class BumpUp(DryRun):
|
|
|
198
225
|
major = "major"
|
|
199
226
|
|
|
200
227
|
def __init__(
|
|
201
|
-
self: Self,
|
|
228
|
+
self: Self,
|
|
229
|
+
commit: bool,
|
|
230
|
+
part: str,
|
|
231
|
+
filename: str | None = None,
|
|
232
|
+
dry: bool = False,
|
|
202
233
|
) -> None:
|
|
203
234
|
self.commit = commit
|
|
204
235
|
self.part = part
|
|
@@ -208,9 +239,9 @@ class BumpUp(DryRun):
|
|
|
208
239
|
super().__init__(dry=dry)
|
|
209
240
|
|
|
210
241
|
@staticmethod
|
|
211
|
-
def get_last_commit_message() -> str:
|
|
242
|
+
def get_last_commit_message(raises: bool = False) -> str:
|
|
212
243
|
cmd = 'git show --pretty=format:"%s" -s HEAD'
|
|
213
|
-
return capture_cmd_output(cmd)
|
|
244
|
+
return capture_cmd_output(cmd, raises=raises)
|
|
214
245
|
|
|
215
246
|
@classmethod
|
|
216
247
|
def should_add_emoji(cls) -> bool:
|
|
@@ -218,9 +249,12 @@ class BumpUp(DryRun):
|
|
|
218
249
|
If last commit message is startswith emoji,
|
|
219
250
|
add a ⬆️ flag at the prefix of bump up commit message.
|
|
220
251
|
"""
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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)
|
|
224
258
|
|
|
225
259
|
@staticmethod
|
|
226
260
|
def parse_filename() -> str:
|
|
@@ -348,7 +382,7 @@ def bump_version(
|
|
|
348
382
|
commit: bool = Option(
|
|
349
383
|
False, "--commit", "-c", help="Whether run `git commit` after version changed"
|
|
350
384
|
),
|
|
351
|
-
dry: bool =
|
|
385
|
+
dry: bool = DryOption,
|
|
352
386
|
) -> None:
|
|
353
387
|
"""Bump up version string in pyproject.toml"""
|
|
354
388
|
return BumpUp(_ensure_bool(commit), getattr(part, "value", part), dry=dry).run()
|
|
@@ -378,7 +412,9 @@ class Project:
|
|
|
378
412
|
return 'build-backend = "poetry' in text
|
|
379
413
|
|
|
380
414
|
@staticmethod
|
|
381
|
-
def work_dir(
|
|
415
|
+
def work_dir(
|
|
416
|
+
name: str, parent: Path, depth: int, be_file: bool = False
|
|
417
|
+
) -> Path | None:
|
|
382
418
|
for _ in range(depth):
|
|
383
419
|
if (f := parent.joinpath(name)).exists():
|
|
384
420
|
if be_file:
|
|
@@ -390,10 +426,10 @@ class Project:
|
|
|
390
426
|
@classmethod
|
|
391
427
|
def get_work_dir(
|
|
392
428
|
cls: type[Self],
|
|
393
|
-
name=TOML_FILE,
|
|
429
|
+
name: str = TOML_FILE,
|
|
394
430
|
cwd: Path | None = None,
|
|
395
|
-
allow_cwd=False,
|
|
396
|
-
be_file=False,
|
|
431
|
+
allow_cwd: bool = False,
|
|
432
|
+
be_file: bool = False,
|
|
397
433
|
) -> Path:
|
|
398
434
|
cwd = cwd or Path.cwd()
|
|
399
435
|
if d := cls.work_dir(name, cwd, cls.path_depth, be_file):
|
|
@@ -403,7 +439,7 @@ class Project:
|
|
|
403
439
|
raise EnvError(f"{name} not found! Make sure this is a poetry project.")
|
|
404
440
|
|
|
405
441
|
@classmethod
|
|
406
|
-
def load_toml_text(cls: type[Self], name=TOML_FILE) -> str:
|
|
442
|
+
def load_toml_text(cls: type[Self], name: str = TOML_FILE) -> str:
|
|
407
443
|
toml_file = cls.get_work_dir(name, be_file=True)
|
|
408
444
|
return toml_file.read_text("utf8")
|
|
409
445
|
|
|
@@ -420,7 +456,7 @@ class Project:
|
|
|
420
456
|
else:
|
|
421
457
|
for name in get_args(ToolName):
|
|
422
458
|
if f"[tool.{name}]" in text:
|
|
423
|
-
return name
|
|
459
|
+
return cast(ToolName, name)
|
|
424
460
|
# Poetry 2.0 default to not include the '[tool.poetry]' section
|
|
425
461
|
if cls.is_poetry_v2(text):
|
|
426
462
|
return "poetry"
|
|
@@ -446,6 +482,12 @@ class ParseError(Exception):
|
|
|
446
482
|
|
|
447
483
|
|
|
448
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
|
+
|
|
449
491
|
class DevFlag(StrEnum):
|
|
450
492
|
new = "[tool.poetry.group.dev.dependencies]"
|
|
451
493
|
old = "[tool.poetry.dev-dependencies]"
|
|
@@ -537,7 +579,7 @@ class UpgradeDependencies(Project, DryRun):
|
|
|
537
579
|
return cls.DevFlag.new in text or cls.DevFlag.old in text
|
|
538
580
|
|
|
539
581
|
@staticmethod
|
|
540
|
-
def parse_item(toml_str) -> list[str]:
|
|
582
|
+
def parse_item(toml_str: str) -> list[str]:
|
|
541
583
|
lines: list[str] = []
|
|
542
584
|
for line in toml_str.splitlines():
|
|
543
585
|
if (line := line.strip()).startswith("["):
|
|
@@ -607,20 +649,26 @@ class UpgradeDependencies(Project, DryRun):
|
|
|
607
649
|
return _upgrade
|
|
608
650
|
|
|
609
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"
|
|
610
656
|
return self.gen_cmd() + " && poetry lock && poetry update"
|
|
611
657
|
|
|
612
658
|
|
|
613
659
|
@cli.command()
|
|
614
660
|
def upgrade(
|
|
615
|
-
|
|
661
|
+
tool: str = ToolOption,
|
|
662
|
+
dry: bool = DryOption,
|
|
616
663
|
) -> None:
|
|
617
664
|
"""Upgrade dependencies in pyproject.toml to latest versions"""
|
|
618
|
-
if (tool :=
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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()
|
|
622
669
|
else:
|
|
623
|
-
|
|
670
|
+
secho(f"Unknown tool {tool!r}", fg=typer.colors.YELLOW)
|
|
671
|
+
raise typer.Exit(1)
|
|
624
672
|
|
|
625
673
|
|
|
626
674
|
class GitTag(DryRun):
|
|
@@ -666,7 +714,7 @@ class GitTag(DryRun):
|
|
|
666
714
|
@cli.command()
|
|
667
715
|
def tag(
|
|
668
716
|
message: str = Option("", "-m", "--message"),
|
|
669
|
-
dry: bool =
|
|
717
|
+
dry: bool = DryOption,
|
|
670
718
|
) -> None:
|
|
671
719
|
"""Run shell command: git tag -a <current-version-in-pyproject.toml> -m {message}"""
|
|
672
720
|
GitTag(message, dry=dry).run()
|
|
@@ -675,19 +723,21 @@ def tag(
|
|
|
675
723
|
class LintCode(DryRun):
|
|
676
724
|
def __init__(
|
|
677
725
|
self: Self,
|
|
678
|
-
args,
|
|
679
|
-
check_only=False,
|
|
680
|
-
_exit=False,
|
|
681
|
-
dry=False,
|
|
682
|
-
bandit=False,
|
|
683
|
-
skip_mypy=False,
|
|
684
|
-
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,
|
|
685
734
|
) -> None:
|
|
686
735
|
self.args = args
|
|
687
736
|
self.check_only = check_only
|
|
688
737
|
self._bandit = bandit
|
|
689
738
|
self._skip_mypy = skip_mypy
|
|
690
739
|
self._use_dmypy = dmypy
|
|
740
|
+
self._tool = tool
|
|
691
741
|
super().__init__(_exit, dry)
|
|
692
742
|
|
|
693
743
|
@staticmethod
|
|
@@ -695,7 +745,7 @@ class LintCode(DryRun):
|
|
|
695
745
|
return check_call("ruff --version")
|
|
696
746
|
|
|
697
747
|
@staticmethod
|
|
698
|
-
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:
|
|
699
749
|
return (
|
|
700
750
|
paths == "."
|
|
701
751
|
and any(t.startswith("mypy") for t in tools)
|
|
@@ -705,7 +755,8 @@ class LintCode(DryRun):
|
|
|
705
755
|
@staticmethod
|
|
706
756
|
def get_package_name() -> str:
|
|
707
757
|
root = Project.get_work_dir(allow_cwd=True)
|
|
708
|
-
|
|
758
|
+
module_name = root.name.replace("-", "_").replace(" ", "_")
|
|
759
|
+
package_maybe = (module_name, "src")
|
|
709
760
|
for name in package_maybe:
|
|
710
761
|
if root.joinpath(name).is_dir():
|
|
711
762
|
return name
|
|
@@ -719,7 +770,10 @@ class LintCode(DryRun):
|
|
|
719
770
|
bandit: bool = False,
|
|
720
771
|
skip_mypy: bool = False,
|
|
721
772
|
use_dmypy: bool = False,
|
|
773
|
+
tool: str = ToolOption.default,
|
|
722
774
|
) -> str:
|
|
775
|
+
if paths != "." and all(i.endswith(".html") for i in paths.split()):
|
|
776
|
+
return f"prettier -w {paths}"
|
|
723
777
|
cmd = ""
|
|
724
778
|
tools = ["ruff format", "ruff check --extend-select=I,B,SIM --fix", "mypy"]
|
|
725
779
|
if check_only:
|
|
@@ -745,8 +799,11 @@ class LintCode(DryRun):
|
|
|
745
799
|
secho(f"{tip}\n\n {command}\n", fg="yellow")
|
|
746
800
|
else:
|
|
747
801
|
should_run_by_tool = True
|
|
748
|
-
if should_run_by_tool and
|
|
749
|
-
|
|
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 "
|
|
750
807
|
if cls.prefer_dmypy(paths, tools, use_dmypy=use_dmypy):
|
|
751
808
|
tools[-1] = "dmypy run"
|
|
752
809
|
cmd += lint_them.format(prefix, paths, *tools)
|
|
@@ -763,7 +820,9 @@ class LintCode(DryRun):
|
|
|
763
820
|
return cmd
|
|
764
821
|
|
|
765
822
|
def gen(self: Self) -> str:
|
|
766
|
-
|
|
823
|
+
if isinstance(args := self.args, str):
|
|
824
|
+
args = args.split()
|
|
825
|
+
paths = " ".join(map(str, args)) if args else "."
|
|
767
826
|
return self.to_cmd(
|
|
768
827
|
paths, self.check_only, self._bandit, self._skip_mypy, self._use_dmypy
|
|
769
828
|
)
|
|
@@ -773,15 +832,31 @@ def parse_files(args: list[str] | tuple[str, ...]) -> list[str]:
|
|
|
773
832
|
return [i for i in args if not i.startswith("-")]
|
|
774
833
|
|
|
775
834
|
|
|
776
|
-
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:
|
|
777
843
|
if files is None:
|
|
778
844
|
files = parse_files(sys.argv[1:])
|
|
779
845
|
if files and files[0] == "lint":
|
|
780
846
|
files = files[1:]
|
|
781
|
-
LintCode(
|
|
847
|
+
LintCode(
|
|
848
|
+
files, dry=dry, skip_mypy=skip_mypy, bandit=bandit, dmypy=dmypy, tool=tool
|
|
849
|
+
).run()
|
|
782
850
|
|
|
783
851
|
|
|
784
|
-
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:
|
|
785
860
|
LintCode(
|
|
786
861
|
files,
|
|
787
862
|
check_only=True,
|
|
@@ -790,46 +865,51 @@ def check(files=None, dry=False, bandit=False, skip_mypy=False, dmypy=False) ->
|
|
|
790
865
|
bandit=bandit,
|
|
791
866
|
skip_mypy=skip_mypy,
|
|
792
867
|
dmypy=dmypy,
|
|
868
|
+
tool=tool,
|
|
793
869
|
).run()
|
|
794
870
|
|
|
795
871
|
|
|
796
872
|
@cli.command(name="lint")
|
|
797
873
|
def make_style(
|
|
798
|
-
files: Optional[list[
|
|
874
|
+
files: Optional[list[str]] = typer.Argument(default=None), # noqa:B008
|
|
799
875
|
check_only: bool = Option(False, "--check-only", "-c"),
|
|
800
876
|
bandit: bool = Option(False, "--bandit", help="Run `bandit -r <package_dir>`"),
|
|
801
877
|
skip_mypy: bool = Option(False, "--skip-mypy"),
|
|
802
878
|
use_dmypy: bool = Option(
|
|
803
879
|
False, "--dmypy", help="Use `dmypy run` instead of `mypy`"
|
|
804
880
|
),
|
|
805
|
-
|
|
881
|
+
tool: str = ToolOption,
|
|
882
|
+
dry: bool = DryOption,
|
|
806
883
|
) -> None:
|
|
807
884
|
"""Run: ruff check/format to reformat code and then mypy to check"""
|
|
808
885
|
if getattr(files, "default", files) is None:
|
|
809
|
-
files = [
|
|
886
|
+
files = ["."]
|
|
810
887
|
elif isinstance(files, str):
|
|
811
888
|
files = [files]
|
|
812
889
|
skip = _ensure_bool(skip_mypy)
|
|
813
890
|
dmypy = _ensure_bool(use_dmypy)
|
|
814
891
|
bandit = _ensure_bool(bandit)
|
|
892
|
+
tool = _ensure_str(tool)
|
|
815
893
|
if _ensure_bool(check_only):
|
|
816
|
-
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)
|
|
817
895
|
else:
|
|
818
|
-
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)
|
|
819
897
|
|
|
820
898
|
|
|
821
899
|
@cli.command(name="check")
|
|
822
900
|
def only_check(
|
|
823
901
|
bandit: bool = Option(False, "--bandit", help="Run `bandit -r <package_dir>`"),
|
|
824
902
|
skip_mypy: bool = Option(False, "--skip-mypy"),
|
|
825
|
-
dry: bool =
|
|
903
|
+
dry: bool = DryOption,
|
|
826
904
|
) -> None:
|
|
827
905
|
"""Check code style without reformat"""
|
|
828
906
|
check(dry=dry, bandit=bandit, skip_mypy=_ensure_bool(skip_mypy))
|
|
829
907
|
|
|
830
908
|
|
|
831
909
|
class Sync(DryRun):
|
|
832
|
-
def __init__(
|
|
910
|
+
def __init__(
|
|
911
|
+
self: Self, filename: str, extras: str, save: bool, dry: bool = False
|
|
912
|
+
) -> None:
|
|
833
913
|
self.filename = filename
|
|
834
914
|
self.extras = extras
|
|
835
915
|
self._save = save
|
|
@@ -868,12 +948,12 @@ class Sync(DryRun):
|
|
|
868
948
|
|
|
869
949
|
@cli.command()
|
|
870
950
|
def sync(
|
|
871
|
-
filename="dev_requirements.txt",
|
|
951
|
+
filename: str = "dev_requirements.txt",
|
|
872
952
|
extras: str = Option("", "--extras", "-E"),
|
|
873
953
|
save: bool = Option(
|
|
874
954
|
False, "--save", "-s", help="Whether save the requirement file"
|
|
875
955
|
),
|
|
876
|
-
dry: bool =
|
|
956
|
+
dry: bool = DryOption,
|
|
877
957
|
) -> None:
|
|
878
958
|
"""Export dependencies by poetry to a txt file then install by pip."""
|
|
879
959
|
Sync(filename, extras, save, dry=dry).run()
|
|
@@ -886,7 +966,7 @@ def _should_run_test_script(path: Path = Path("scripts")) -> Path | None:
|
|
|
886
966
|
return None
|
|
887
967
|
|
|
888
968
|
|
|
889
|
-
def test(dry: bool, ignore_script=False) -> None:
|
|
969
|
+
def test(dry: bool, ignore_script: bool = False) -> None:
|
|
890
970
|
cwd = Path.cwd()
|
|
891
971
|
root = Project.get_work_dir(cwd=cwd, allow_cwd=True)
|
|
892
972
|
script_dir = root / "scripts"
|
|
@@ -907,7 +987,7 @@ def test(dry: bool, ignore_script=False) -> None:
|
|
|
907
987
|
|
|
908
988
|
@cli.command(name="test")
|
|
909
989
|
def coverage_test(
|
|
910
|
-
dry: bool =
|
|
990
|
+
dry: bool = DryOption,
|
|
911
991
|
ignore_script: bool = Option(False, "--ignore-script", "-i"),
|
|
912
992
|
) -> None:
|
|
913
993
|
"""Run unittest by pytest and report coverage"""
|
|
@@ -930,7 +1010,7 @@ class Publish:
|
|
|
930
1010
|
|
|
931
1011
|
@cli.command()
|
|
932
1012
|
def upload(
|
|
933
|
-
dry: bool =
|
|
1013
|
+
dry: bool = DryOption,
|
|
934
1014
|
) -> None:
|
|
935
1015
|
"""Shortcut for package publish"""
|
|
936
1016
|
cmd = Publish.gen()
|
|
@@ -941,13 +1021,13 @@ def dev(
|
|
|
941
1021
|
port: int | None | OptionInfo,
|
|
942
1022
|
host: str | None | OptionInfo,
|
|
943
1023
|
file: str | None | ArgumentInfo = None,
|
|
944
|
-
dry=False,
|
|
1024
|
+
dry: bool = False,
|
|
945
1025
|
) -> None:
|
|
946
1026
|
cmd = "fastapi dev"
|
|
947
1027
|
no_port_yet = True
|
|
948
1028
|
if file is not None:
|
|
949
1029
|
try:
|
|
950
|
-
port = int(str(file))
|
|
1030
|
+
port = int(str(file))
|
|
951
1031
|
except ValueError:
|
|
952
1032
|
cmd += f" {file}"
|
|
953
1033
|
else:
|
|
@@ -969,7 +1049,7 @@ def runserver(
|
|
|
969
1049
|
file_or_port: Optional[str] = typer.Argument(default=None),
|
|
970
1050
|
port: Optional[int] = Option(None, "-p", "--port"),
|
|
971
1051
|
host: Optional[str] = Option(None, "-h", "--host"),
|
|
972
|
-
dry: bool =
|
|
1052
|
+
dry: bool = DryOption,
|
|
973
1053
|
) -> None:
|
|
974
1054
|
"""Start a fastapi server(only for fastapi>=0.111.0)"""
|
|
975
1055
|
if getattr(file_or_port, "default", file_or_port):
|
|
@@ -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
|
|
@@ -17,10 +18,11 @@ from fast_dev_cli.cli import (
|
|
|
17
18
|
StrEnum,
|
|
18
19
|
bump,
|
|
19
20
|
bump_version,
|
|
21
|
+
capture_cmd_output,
|
|
20
22
|
get_current_version,
|
|
21
23
|
)
|
|
22
24
|
|
|
23
|
-
from .utils import chdir, mock_sys_argv
|
|
25
|
+
from .utils import chdir, mock_sys_argv, prepare_poetry_project
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
def test_enum():
|
|
@@ -53,12 +55,14 @@ def test_bump_dry(mocker):
|
|
|
53
55
|
mocker.patch("fast_dev_cli.cli.Project.manage_by_poetry", return_value=True)
|
|
54
56
|
with pytest.raises(ShellCommandError):
|
|
55
57
|
get_current_version()
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
mocker.patch("fast_dev_cli.cli.Project.manage_by_poetry", return_value=False)
|
|
59
|
+
version = get_current_version()
|
|
60
|
+
patch_without_commit, patch_with_commit, minor_with_commit = _bump_commands(
|
|
61
|
+
version, filename="fast_dev_cli/__init__.py"
|
|
62
|
+
)
|
|
63
|
+
assert BumpUp(part="patch", commit=False, dry=True).gen() == patch_without_commit
|
|
64
|
+
assert BumpUp(part="patch", commit=True, dry=True).gen() == patch_with_commit
|
|
65
|
+
assert BumpUp(part="minor", commit=True, dry=True).gen() == minor_with_commit
|
|
62
66
|
|
|
63
67
|
|
|
64
68
|
def test_bump(
|
|
@@ -145,36 +149,46 @@ def test_bump_with_emoji(mocker, tmp_path, monkeypatch):
|
|
|
145
149
|
mocker.patch("fast_dev_cli.cli.Project.manage_by_poetry", return_value=True)
|
|
146
150
|
with pytest.raises(ShellCommandError):
|
|
147
151
|
get_current_version()
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
152
|
+
mocker.patch("fast_dev_cli.cli.Project.manage_by_poetry", return_value=False)
|
|
153
|
+
version = get_current_version()
|
|
154
|
+
patch_without_commit, patch_with_commit, minor_with_commit = _bump_commands(
|
|
155
|
+
version, filename="fast_dev_cli/__init__.py", emoji=True
|
|
156
|
+
)
|
|
157
|
+
last_commit = "📝 Update release notes"
|
|
158
|
+
mocker.patch(
|
|
159
|
+
"fast_dev_cli.cli.BumpUp.get_last_commit_message",
|
|
160
|
+
return_value=last_commit,
|
|
161
|
+
)
|
|
162
|
+
assert BumpUp(part="patch", commit=False, dry=True).gen() == patch_without_commit
|
|
163
|
+
assert BumpUp(part="patch", commit=True, dry=True).gen() == patch_with_commit
|
|
164
|
+
assert BumpUp(part="minor", commit=True, dry=True).gen() == minor_with_commit
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_bump_with_emoji_in_poetry_project(mocker, tmp_path, monkeypatch):
|
|
161
168
|
# real bump
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
169
|
+
last_commit = "📝 Update release notes"
|
|
170
|
+
_, patch_with_commit, __ = _bump_commands("0.1.0", emoji=True)
|
|
171
|
+
with prepare_poetry_project(tmp_path):
|
|
172
|
+
subprocess.run(["git", "init"])
|
|
173
|
+
subprocess.run(["git", "add", "."])
|
|
174
|
+
subprocess.run(["git", "config", "user.name", "sb"])
|
|
175
|
+
subprocess.run(["git", "config", "user.email", "sb@foo.com"])
|
|
176
|
+
assert BumpUp.should_add_emoji() is False
|
|
177
|
+
subprocess.run(["git", "commit", "-m", last_commit])
|
|
178
|
+
monkeypatch.setenv("DONT_GIT_PUSH", "1")
|
|
179
|
+
command = BumpUp(part="patch", commit=True).gen()
|
|
180
|
+
expected = patch_with_commit.split("&&")[0].strip().replace('""', '"0.1.0"')
|
|
181
|
+
assert expected == command
|
|
182
|
+
subprocess.run(["poetry", "run", "pip", "install", "bumpversion2"])
|
|
183
|
+
subprocess.run(["fast", "bump", "patch", "--commit"])
|
|
184
|
+
out = capture_cmd_output(["git", "log"])
|
|
185
|
+
assert BumpUp.should_add_emoji()
|
|
186
|
+
Path("a.txt").touch()
|
|
187
|
+
subprocess.run(["git", "add", "."])
|
|
188
|
+
subprocess.run(["git", "commit", "-m", "no emoji"])
|
|
189
|
+
assert BumpUp.should_add_emoji() is False
|
|
190
|
+
new_commit = "⬆️ Bump version: 0.1.0 → 0.1.1"
|
|
191
|
+
assert new_commit in out
|
|
178
192
|
|
|
179
193
|
|
|
180
194
|
def test_bump_with_uv(tmp_path):
|
|
@@ -187,3 +201,32 @@ def test_bump_with_uv(tmp_path):
|
|
|
187
201
|
Path(TOML_FILE).write_text("[project]" + os.linesep + 'version = "0.1.0"')
|
|
188
202
|
command = BumpUp(part="patch", commit=True).gen()
|
|
189
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"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from contextlib import redirect_stdout
|
|
3
3
|
from io import StringIO
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
import pytest
|
|
6
7
|
import typer
|
|
@@ -16,6 +17,8 @@ from fast_dev_cli.cli import (
|
|
|
16
17
|
run_and_echo,
|
|
17
18
|
)
|
|
18
19
|
|
|
20
|
+
from .utils import prepare_poetry_project
|
|
21
|
+
|
|
19
22
|
|
|
20
23
|
def test_utils(capsys):
|
|
21
24
|
# parse files
|
|
@@ -64,19 +67,15 @@ def test_run_shell():
|
|
|
64
67
|
# current version
|
|
65
68
|
with pytest.raises(ShellCommandError):
|
|
66
69
|
get_current_version(True, is_poetry=True)
|
|
67
|
-
# TODO: add [tool.poetry] to pyproject.toml
|
|
68
|
-
# stream = StringIO()
|
|
69
|
-
# write_to_stream = redirect_stdout(stream)
|
|
70
|
-
# with write_to_stream:
|
|
71
|
-
# get_current_version(True, is_poetry=True)
|
|
72
|
-
# assert "poetry version -s" in stream.getvalue()
|
|
73
70
|
|
|
74
71
|
name = "TEST_EXIT_IF_RUN_FAILED"
|
|
75
72
|
value = "foo"
|
|
76
73
|
cmd = 'python -c "import os;print(list(os.environ))"'
|
|
77
74
|
with redirect_stdout(StringIO()):
|
|
78
|
-
r = exit_if_run_failed(
|
|
79
|
-
|
|
75
|
+
r = exit_if_run_failed(
|
|
76
|
+
cmd, env={name: value}, capture_output=True, encoding="utf-8"
|
|
77
|
+
)
|
|
78
|
+
assert name in r.stdout
|
|
80
79
|
|
|
81
80
|
assert run_and_echo("echo foo", capture_output=True) == 0
|
|
82
81
|
|
|
@@ -92,6 +91,15 @@ def test_run_shell():
|
|
|
92
91
|
A().run()
|
|
93
92
|
|
|
94
93
|
|
|
94
|
+
def test_get_version_in_poetry_project(tmp_path: Path):
|
|
95
|
+
with prepare_poetry_project(tmp_path):
|
|
96
|
+
stream = StringIO()
|
|
97
|
+
write_to_stream = redirect_stdout(stream)
|
|
98
|
+
with write_to_stream:
|
|
99
|
+
get_current_version(True, is_poetry=True)
|
|
100
|
+
assert "poetry version -s" in stream.getvalue()
|
|
101
|
+
|
|
102
|
+
|
|
95
103
|
def test_ensure_bool():
|
|
96
104
|
assert _ensure_bool(True) is True
|
|
97
105
|
assert _ensure_bool(False) is False
|
|
@@ -65,7 +65,7 @@ def test_check(mock_no_dmypy, monkeypatch, mocker):
|
|
|
65
65
|
for cmd in CHECK_CMD.split(SEP):
|
|
66
66
|
assert cmd in command
|
|
67
67
|
command2 = capture_cmd_output("fast check --bandit --dry")
|
|
68
|
-
assert command2 == command + " && bandit -r
|
|
68
|
+
assert command2 == command + " && bandit -c pyproject.toml -r ."
|
|
69
69
|
monkeypatch.setenv("FASTDEVCLI_BANDIT", "1")
|
|
70
70
|
command3 = capture_cmd_output("fast check --dry")
|
|
71
71
|
assert command3 == command2
|
|
@@ -82,10 +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):
|
|
94
|
+
assert LintCode.get_package_name() == "."
|
|
87
95
|
command = capture_cmd_output("fast check --bandit --dry")
|
|
88
|
-
|
|
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
|
|
89
100
|
|
|
90
101
|
|
|
91
102
|
def test_check_skip_mypy(mock_skip_mypy_0, mocker, capsys):
|
|
@@ -132,6 +143,23 @@ def test_lint_cmd(mock_no_dmypy):
|
|
|
132
143
|
)
|
|
133
144
|
|
|
134
145
|
|
|
146
|
+
def test_lint_html():
|
|
147
|
+
run = "pdm run "
|
|
148
|
+
lint_cmd = f"{run}python fast_dev_cli/cli.py lint"
|
|
149
|
+
command = capture_cmd_output(f"{lint_cmd} index.html --dry")
|
|
150
|
+
assert "prettier -w index.html" in command
|
|
151
|
+
command = capture_cmd_output(f"{lint_cmd} index.html flv.html --dry")
|
|
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"
|
|
161
|
+
|
|
162
|
+
|
|
135
163
|
def test_lint_by_global_fast():
|
|
136
164
|
run = "pdm run "
|
|
137
165
|
fast = Path.home() / ".local" / "bin" / "fast"
|
|
@@ -141,20 +169,29 @@ def test_lint_by_global_fast():
|
|
|
141
169
|
|
|
142
170
|
|
|
143
171
|
def test_with_dmypy():
|
|
144
|
-
|
|
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")
|
|
145
176
|
assert "dmypy run ." in command
|
|
177
|
+
command = LintCode.to_cmd(use_dmypy=False, tool="pdm")
|
|
178
|
+
assert "dmypy run ." not in command
|
|
146
179
|
|
|
147
180
|
|
|
148
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
|
|
149
184
|
monkeypatch.setenv("FASTDEVCLI_DMYPY", "1")
|
|
150
185
|
command = capture_cmd_output("python -m fast_dev_cli lint --dry .")
|
|
151
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
|
|
152
189
|
|
|
153
190
|
|
|
154
191
|
def test_lint_with_prefix(mocker):
|
|
155
192
|
mocker.patch("fast_dev_cli.cli.is_venv", return_value=False)
|
|
156
193
|
with capture_stdout() as stream:
|
|
157
|
-
make_style([
|
|
194
|
+
make_style(["."], check_only=False, dry=True)
|
|
158
195
|
assert "pdm run" in stream.getvalue()
|
|
159
196
|
|
|
160
197
|
|
|
@@ -164,13 +201,13 @@ def test_make_style(mock_skip_mypy_0, mocker, mock_no_dmypy):
|
|
|
164
201
|
make_style(check_only=False, dry=True)
|
|
165
202
|
assert LINT_CMD in stream.getvalue()
|
|
166
203
|
with capture_stdout() as stream:
|
|
167
|
-
make_style([
|
|
204
|
+
make_style(["."], check_only=False, dry=True)
|
|
168
205
|
assert LINT_CMD in stream.getvalue()
|
|
169
206
|
with capture_stdout() as stream:
|
|
170
207
|
make_style(".", check_only=False, dry=True) # type:ignore[arg-type]
|
|
171
208
|
assert LINT_CMD in stream.getvalue()
|
|
172
209
|
with capture_stdout() as stream:
|
|
173
|
-
make_style([
|
|
210
|
+
make_style(["."], check_only=True, dry=True)
|
|
174
211
|
assert CHECK_CMD in stream.getvalue()
|
|
175
212
|
with capture_stdout() as stream:
|
|
176
213
|
only_check(dry=True)
|
|
@@ -287,3 +324,24 @@ def test_get_manage_tool(tmp_path):
|
|
|
287
324
|
assert Project.get_manage_tool() == "pdm"
|
|
288
325
|
Path(TOML_FILE).write_text("[tool.uv]")
|
|
289
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,27 +1,25 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import subprocess
|
|
2
3
|
import sys
|
|
3
|
-
from contextlib import contextmanager, redirect_stdout
|
|
4
|
+
from contextlib import AbstractContextManager, contextmanager, redirect_stdout
|
|
4
5
|
from io import StringIO
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
7
|
-
if sys.version_info >= (3, 11):
|
|
8
|
-
from contextlib import chdir # type:ignore[attr-defined]
|
|
9
|
-
else:
|
|
10
|
-
from contextlib import AbstractContextManager
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
|
|
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."""
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
def __init__(self, path):
|
|
14
|
+
self.path = path
|
|
15
|
+
self._old_cwd = []
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
def __enter__(self):
|
|
18
|
+
self._old_cwd.append(os.getcwd())
|
|
19
|
+
os.chdir(self.path)
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
def __exit__(self, *excinfo):
|
|
22
|
+
os.chdir(self._old_cwd.pop())
|
|
25
23
|
|
|
26
24
|
|
|
27
25
|
__all__ = (
|
|
@@ -66,3 +64,12 @@ def temp_file(name: str, text=""):
|
|
|
66
64
|
yield
|
|
67
65
|
if path.exists():
|
|
68
66
|
path.unlink()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@contextmanager
|
|
70
|
+
def prepare_poetry_project(tmp_path: Path):
|
|
71
|
+
with chdir(tmp_path):
|
|
72
|
+
project = "foo"
|
|
73
|
+
subprocess.run(["poetry", "new", project])
|
|
74
|
+
with chdir(tmp_path / project):
|
|
75
|
+
yield
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.14.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
|