fast-dev-cli 0.14.2__tar.gz → 0.15.1__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.1}/PKG-INFO +2 -2
- fast_dev_cli-0.15.1/fast_dev_cli/__init__.py +1 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.1}/fast_dev_cli/cli.py +152 -65
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.1}/pyproject.toml +3 -2
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.1}/tests/conftest.py +8 -3
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.1}/tests/test_bump.py +93 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.1}/tests/test_functions.py +4 -2
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.1}/tests/test_lint.py +53 -5
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.1}/tests/test_upgrade.py +36 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.1}/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.1}/LICENSE +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.1}/README.md +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.1}/fast_dev_cli/__main__.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.1}/fast_dev_cli/py.typed +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.1}/pdm_build.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.1}/scripts/check.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.1}/scripts/format.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.1}/scripts/test.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.1}/tests/__init__.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.1}/tests/test_fast_test.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.1}/tests/test_poetry_version_plugin.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.1}/tests/test_runserver.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.1}/tests/test_sync.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.1}/tests/test_tag.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.1}/tests/test_upload.py +0 -0
- {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.1}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fast-dev-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.1
|
|
4
4
|
Summary: Python project development tool.
|
|
5
5
|
Author-Email: Waket Zheng <waketzheng@gmail.com>>
|
|
6
6
|
Classifier: Development Status :: 4 - Beta
|
|
@@ -23,12 +23,12 @@ Project-URL: Homepage, https://github.com/waketzheng/fast-dev-cli
|
|
|
23
23
|
Requires-Python: >=3.9
|
|
24
24
|
Requires-Dist: typer<1,>=0.12.3
|
|
25
25
|
Requires-Dist: emoji<3,>=2.12.1
|
|
26
|
+
Requires-Dist: packaging>=20.5
|
|
26
27
|
Requires-Dist: tomli<3,>=2.0.1; python_version < "3.11"
|
|
27
28
|
Requires-Dist: coverage<8,>=7.5.1
|
|
28
29
|
Requires-Dist: mypy<2,>=1.15.0
|
|
29
30
|
Requires-Dist: bumpversion2<2,>=1.4.3
|
|
30
31
|
Requires-Dist: pytest<9,>=8.2.0
|
|
31
|
-
Requires-Dist: packaging>=20.5
|
|
32
32
|
Provides-Extra: include-optional-dependencies
|
|
33
33
|
Requires-Dist: all; extra == "include-optional-dependencies"
|
|
34
34
|
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.15.1"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
import importlib.metadata as importlib_metadata
|
|
4
5
|
import os
|
|
5
6
|
import re
|
|
@@ -8,7 +9,14 @@ import subprocess # nosec:B404
|
|
|
8
9
|
import sys
|
|
9
10
|
from functools import cached_property
|
|
10
11
|
from pathlib import Path
|
|
11
|
-
from typing import
|
|
12
|
+
from typing import (
|
|
13
|
+
Any,
|
|
14
|
+
Literal,
|
|
15
|
+
# Optional is required by Option generated by typer
|
|
16
|
+
Optional,
|
|
17
|
+
cast,
|
|
18
|
+
get_args,
|
|
19
|
+
)
|
|
12
20
|
|
|
13
21
|
import emoji
|
|
14
22
|
import typer
|
|
@@ -41,6 +49,9 @@ cli = typer.Typer()
|
|
|
41
49
|
DryOption = Option(False, "--dry", help="Only print, not really run shell command")
|
|
42
50
|
TOML_FILE = "pyproject.toml"
|
|
43
51
|
ToolName = Literal["poetry", "pdm", "uv"]
|
|
52
|
+
ToolOption = Option(
|
|
53
|
+
"auto", "--tool", help="Explicit declare manage tool (default to auto detect)"
|
|
54
|
+
)
|
|
44
55
|
|
|
45
56
|
|
|
46
57
|
class ShellCommandError(Exception): ...
|
|
@@ -53,7 +64,7 @@ def poetry_module_name(name: str) -> str:
|
|
|
53
64
|
return canonicalize_name(name).replace("-", "_").replace(" ", "_")
|
|
54
65
|
|
|
55
66
|
|
|
56
|
-
def load_bool(name: str, default=False) -> bool:
|
|
67
|
+
def load_bool(name: str, default: bool = False) -> bool:
|
|
57
68
|
if not (v := os.getenv(name)):
|
|
58
69
|
return default
|
|
59
70
|
if (lower := v.lower()) in ("0", "false", "f", "off", "no", "n"):
|
|
@@ -71,13 +82,15 @@ def is_venv() -> bool:
|
|
|
71
82
|
)
|
|
72
83
|
|
|
73
84
|
|
|
74
|
-
def _run_shell(cmd: list[str] | str, **kw) -> subprocess.CompletedProcess:
|
|
85
|
+
def _run_shell(cmd: list[str] | str, **kw: Any) -> subprocess.CompletedProcess[str]:
|
|
75
86
|
if isinstance(cmd, str):
|
|
76
87
|
kw.setdefault("shell", True)
|
|
77
88
|
return subprocess.run(cmd, **kw) # nosec:B603
|
|
78
89
|
|
|
79
90
|
|
|
80
|
-
def run_and_echo(
|
|
91
|
+
def run_and_echo(
|
|
92
|
+
cmd: str, *, dry: bool = False, verbose: bool = True, **kw: Any
|
|
93
|
+
) -> int:
|
|
81
94
|
"""Run shell command with subprocess and print it"""
|
|
82
95
|
if verbose:
|
|
83
96
|
echo(f"--> {cmd}")
|
|
@@ -91,7 +104,9 @@ def check_call(cmd: str) -> bool:
|
|
|
91
104
|
return r.returncode == 0
|
|
92
105
|
|
|
93
106
|
|
|
94
|
-
def capture_cmd_output(
|
|
107
|
+
def capture_cmd_output(
|
|
108
|
+
command: list[str] | str, *, raises: bool = False, **kw: Any
|
|
109
|
+
) -> str:
|
|
95
110
|
if isinstance(command, str) and not kw.get("shell"):
|
|
96
111
|
command = shlex.split(command)
|
|
97
112
|
r = _run_shell(command, capture_output=True, encoding="utf-8", **kw)
|
|
@@ -100,12 +115,12 @@ def capture_cmd_output(command: list[str] | str, *, raises=False, **kw) -> str:
|
|
|
100
115
|
return r.stdout.strip() or r.stderr
|
|
101
116
|
|
|
102
117
|
|
|
103
|
-
def _parse_version(line: str, pattern: re.Pattern) -> str:
|
|
118
|
+
def _parse_version(line: str, pattern: re.Pattern[str]) -> str:
|
|
104
119
|
return pattern.sub("", line).split("#")[0].strip(" '\"")
|
|
105
120
|
|
|
106
121
|
|
|
107
122
|
def read_version_from_file(
|
|
108
|
-
package_name: str, work_dir=None, toml_text: str | None = None
|
|
123
|
+
package_name: str, work_dir: Path | None = None, toml_text: str | None = None
|
|
109
124
|
) -> str:
|
|
110
125
|
if toml_text is None:
|
|
111
126
|
toml_text = Project.load_toml_text()
|
|
@@ -113,10 +128,10 @@ def read_version_from_file(
|
|
|
113
128
|
invalid = ("0", "0.0.0")
|
|
114
129
|
for line in toml_text.splitlines():
|
|
115
130
|
if pattern.match(line):
|
|
116
|
-
|
|
117
|
-
if
|
|
131
|
+
lib_version = _parse_version(line, pattern)
|
|
132
|
+
if lib_version.startswith("{") or lib_version in invalid:
|
|
118
133
|
break
|
|
119
|
-
return
|
|
134
|
+
return lib_version
|
|
120
135
|
if work_dir is None:
|
|
121
136
|
work_dir = Project.get_work_dir()
|
|
122
137
|
package_dir = work_dir / package_name
|
|
@@ -136,7 +151,9 @@ def read_version_from_file(
|
|
|
136
151
|
|
|
137
152
|
|
|
138
153
|
def get_current_version(
|
|
139
|
-
verbose
|
|
154
|
+
verbose: bool = False,
|
|
155
|
+
is_poetry: bool | None = None,
|
|
156
|
+
package_name: str | None = None,
|
|
140
157
|
) -> str:
|
|
141
158
|
if is_poetry is None:
|
|
142
159
|
is_poetry = Project.manage_by_poetry()
|
|
@@ -164,9 +181,19 @@ def _ensure_bool(value: bool | OptionInfo) -> bool:
|
|
|
164
181
|
return value
|
|
165
182
|
|
|
166
183
|
|
|
184
|
+
def _ensure_str(value: str | OptionInfo) -> str:
|
|
185
|
+
if not isinstance(value, str):
|
|
186
|
+
value = getattr(value, "default", "")
|
|
187
|
+
return value
|
|
188
|
+
|
|
189
|
+
|
|
167
190
|
def exit_if_run_failed(
|
|
168
|
-
cmd: str,
|
|
169
|
-
|
|
191
|
+
cmd: str,
|
|
192
|
+
env: dict[str, str] | None = None,
|
|
193
|
+
_exit: bool = False,
|
|
194
|
+
dry: bool = False,
|
|
195
|
+
**kw: Any,
|
|
196
|
+
) -> subprocess.CompletedProcess[str]:
|
|
170
197
|
run_and_echo(cmd, dry=True)
|
|
171
198
|
if _ensure_bool(dry):
|
|
172
199
|
return subprocess.CompletedProcess("", 0)
|
|
@@ -181,7 +208,7 @@ def exit_if_run_failed(
|
|
|
181
208
|
|
|
182
209
|
|
|
183
210
|
class DryRun:
|
|
184
|
-
def __init__(self: Self, _exit=False, dry=False) -> None:
|
|
211
|
+
def __init__(self: Self, _exit: bool = False, dry: bool = False) -> None:
|
|
185
212
|
self.dry = dry
|
|
186
213
|
self._exit = _exit
|
|
187
214
|
|
|
@@ -199,7 +226,11 @@ class BumpUp(DryRun):
|
|
|
199
226
|
major = "major"
|
|
200
227
|
|
|
201
228
|
def __init__(
|
|
202
|
-
self: Self,
|
|
229
|
+
self: Self,
|
|
230
|
+
commit: bool,
|
|
231
|
+
part: str,
|
|
232
|
+
filename: str | None = None,
|
|
233
|
+
dry: bool = False,
|
|
203
234
|
) -> None:
|
|
204
235
|
self.commit = commit
|
|
205
236
|
self.part = part
|
|
@@ -209,9 +240,9 @@ class BumpUp(DryRun):
|
|
|
209
240
|
super().__init__(dry=dry)
|
|
210
241
|
|
|
211
242
|
@staticmethod
|
|
212
|
-
def get_last_commit_message() -> str:
|
|
243
|
+
def get_last_commit_message(raises: bool = False) -> str:
|
|
213
244
|
cmd = 'git show --pretty=format:"%s" -s HEAD'
|
|
214
|
-
return capture_cmd_output(cmd)
|
|
245
|
+
return capture_cmd_output(cmd, raises=raises)
|
|
215
246
|
|
|
216
247
|
@classmethod
|
|
217
248
|
def should_add_emoji(cls) -> bool:
|
|
@@ -219,9 +250,12 @@ class BumpUp(DryRun):
|
|
|
219
250
|
If last commit message is startswith emoji,
|
|
220
251
|
add a ⬆️ flag at the prefix of bump up commit message.
|
|
221
252
|
"""
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
253
|
+
try:
|
|
254
|
+
first_char = cls.get_last_commit_message(raises=True)[0]
|
|
255
|
+
except (IndexError, ShellCommandError):
|
|
256
|
+
return False
|
|
257
|
+
else:
|
|
258
|
+
return emoji.is_emoji(first_char)
|
|
225
259
|
|
|
226
260
|
@staticmethod
|
|
227
261
|
def parse_filename() -> str:
|
|
@@ -243,6 +277,16 @@ class BumpUp(DryRun):
|
|
|
243
277
|
version_value = context["tool"]["poetry"]["version"]
|
|
244
278
|
except KeyError:
|
|
245
279
|
if not Project.manage_by_poetry():
|
|
280
|
+
for tool in ("pdm", "hatch"):
|
|
281
|
+
with contextlib.suppress(KeyError):
|
|
282
|
+
version_path = context["tool"][tool]["version"]["path"]
|
|
283
|
+
if (
|
|
284
|
+
Path(version_path).exists()
|
|
285
|
+
or Project.get_work_dir()
|
|
286
|
+
.joinpath(version_path)
|
|
287
|
+
.exists()
|
|
288
|
+
):
|
|
289
|
+
return version_path
|
|
246
290
|
# version = { source = "file", path = "fast_dev_cli/__init__.py" }
|
|
247
291
|
v_key = "version = "
|
|
248
292
|
p_key = 'path = "'
|
|
@@ -379,7 +423,9 @@ class Project:
|
|
|
379
423
|
return 'build-backend = "poetry' in text
|
|
380
424
|
|
|
381
425
|
@staticmethod
|
|
382
|
-
def work_dir(
|
|
426
|
+
def work_dir(
|
|
427
|
+
name: str, parent: Path, depth: int, be_file: bool = False
|
|
428
|
+
) -> Path | None:
|
|
383
429
|
for _ in range(depth):
|
|
384
430
|
if (f := parent.joinpath(name)).exists():
|
|
385
431
|
if be_file:
|
|
@@ -391,10 +437,10 @@ class Project:
|
|
|
391
437
|
@classmethod
|
|
392
438
|
def get_work_dir(
|
|
393
439
|
cls: type[Self],
|
|
394
|
-
name=TOML_FILE,
|
|
440
|
+
name: str = TOML_FILE,
|
|
395
441
|
cwd: Path | None = None,
|
|
396
|
-
allow_cwd=False,
|
|
397
|
-
be_file=False,
|
|
442
|
+
allow_cwd: bool = False,
|
|
443
|
+
be_file: bool = False,
|
|
398
444
|
) -> Path:
|
|
399
445
|
cwd = cwd or Path.cwd()
|
|
400
446
|
if d := cls.work_dir(name, cwd, cls.path_depth, be_file):
|
|
@@ -404,7 +450,7 @@ class Project:
|
|
|
404
450
|
raise EnvError(f"{name} not found! Make sure this is a poetry project.")
|
|
405
451
|
|
|
406
452
|
@classmethod
|
|
407
|
-
def load_toml_text(cls: type[Self], name=TOML_FILE) -> str:
|
|
453
|
+
def load_toml_text(cls: type[Self], name: str = TOML_FILE) -> str:
|
|
408
454
|
toml_file = cls.get_work_dir(name, be_file=True)
|
|
409
455
|
return toml_file.read_text("utf8")
|
|
410
456
|
|
|
@@ -421,7 +467,7 @@ class Project:
|
|
|
421
467
|
else:
|
|
422
468
|
for name in get_args(ToolName):
|
|
423
469
|
if f"[tool.{name}]" in text:
|
|
424
|
-
return name
|
|
470
|
+
return cast(ToolName, name)
|
|
425
471
|
# Poetry 2.0 default to not include the '[tool.poetry]' section
|
|
426
472
|
if cls.is_poetry_v2(text):
|
|
427
473
|
return "poetry"
|
|
@@ -447,6 +493,12 @@ class ParseError(Exception):
|
|
|
447
493
|
|
|
448
494
|
|
|
449
495
|
class UpgradeDependencies(Project, DryRun):
|
|
496
|
+
def __init__(
|
|
497
|
+
self: Self, _exit: bool = False, dry: bool = False, tool: ToolName = "poetry"
|
|
498
|
+
) -> None:
|
|
499
|
+
super().__init__(_exit, dry)
|
|
500
|
+
self._tool = tool
|
|
501
|
+
|
|
450
502
|
class DevFlag(StrEnum):
|
|
451
503
|
new = "[tool.poetry.group.dev.dependencies]"
|
|
452
504
|
old = "[tool.poetry.dev-dependencies]"
|
|
@@ -538,7 +590,7 @@ class UpgradeDependencies(Project, DryRun):
|
|
|
538
590
|
return cls.DevFlag.new in text or cls.DevFlag.old in text
|
|
539
591
|
|
|
540
592
|
@staticmethod
|
|
541
|
-
def parse_item(toml_str) -> list[str]:
|
|
593
|
+
def parse_item(toml_str: str) -> list[str]:
|
|
542
594
|
lines: list[str] = []
|
|
543
595
|
for line in toml_str.splitlines():
|
|
544
596
|
if (line := line.strip()).startswith("["):
|
|
@@ -608,20 +660,26 @@ class UpgradeDependencies(Project, DryRun):
|
|
|
608
660
|
return _upgrade
|
|
609
661
|
|
|
610
662
|
def gen(self: Self) -> str:
|
|
663
|
+
if self._tool == "uv":
|
|
664
|
+
return "uv lock --upgrade --verbose && uv sync --frozen"
|
|
665
|
+
elif self._tool == "pdm":
|
|
666
|
+
return "pdm update --verbose && pdm install"
|
|
611
667
|
return self.gen_cmd() + " && poetry lock && poetry update"
|
|
612
668
|
|
|
613
669
|
|
|
614
670
|
@cli.command()
|
|
615
671
|
def upgrade(
|
|
672
|
+
tool: str = ToolOption,
|
|
616
673
|
dry: bool = DryOption,
|
|
617
674
|
) -> None:
|
|
618
675
|
"""Upgrade dependencies in pyproject.toml to latest versions"""
|
|
619
|
-
if (tool :=
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
676
|
+
if not (tool := _ensure_str(tool)) or tool == ToolOption.default:
|
|
677
|
+
tool = Project.get_manage_tool() or "uv"
|
|
678
|
+
if tool in get_args(ToolName):
|
|
679
|
+
UpgradeDependencies(dry=dry, tool=cast(ToolName, tool)).run()
|
|
623
680
|
else:
|
|
624
|
-
|
|
681
|
+
secho(f"Unknown tool {tool!r}", fg=typer.colors.YELLOW)
|
|
682
|
+
raise typer.Exit(1)
|
|
625
683
|
|
|
626
684
|
|
|
627
685
|
class GitTag(DryRun):
|
|
@@ -676,19 +734,21 @@ def tag(
|
|
|
676
734
|
class LintCode(DryRun):
|
|
677
735
|
def __init__(
|
|
678
736
|
self: Self,
|
|
679
|
-
args,
|
|
680
|
-
check_only=False,
|
|
681
|
-
_exit=False,
|
|
682
|
-
dry=False,
|
|
683
|
-
bandit=False,
|
|
684
|
-
skip_mypy=False,
|
|
685
|
-
dmypy=False,
|
|
737
|
+
args: list[str] | str | None,
|
|
738
|
+
check_only: bool = False,
|
|
739
|
+
_exit: bool = False,
|
|
740
|
+
dry: bool = False,
|
|
741
|
+
bandit: bool = False,
|
|
742
|
+
skip_mypy: bool = False,
|
|
743
|
+
dmypy: bool = False,
|
|
744
|
+
tool: str = ToolOption.default,
|
|
686
745
|
) -> None:
|
|
687
746
|
self.args = args
|
|
688
747
|
self.check_only = check_only
|
|
689
748
|
self._bandit = bandit
|
|
690
749
|
self._skip_mypy = skip_mypy
|
|
691
750
|
self._use_dmypy = dmypy
|
|
751
|
+
self._tool = tool
|
|
692
752
|
super().__init__(_exit, dry)
|
|
693
753
|
|
|
694
754
|
@staticmethod
|
|
@@ -696,7 +756,7 @@ class LintCode(DryRun):
|
|
|
696
756
|
return check_call("ruff --version")
|
|
697
757
|
|
|
698
758
|
@staticmethod
|
|
699
|
-
def prefer_dmypy(paths: str, tools: list[str], use_dmypy=False) -> bool:
|
|
759
|
+
def prefer_dmypy(paths: str, tools: list[str], use_dmypy: bool = False) -> bool:
|
|
700
760
|
return (
|
|
701
761
|
paths == "."
|
|
702
762
|
and any(t.startswith("mypy") for t in tools)
|
|
@@ -706,7 +766,8 @@ class LintCode(DryRun):
|
|
|
706
766
|
@staticmethod
|
|
707
767
|
def get_package_name() -> str:
|
|
708
768
|
root = Project.get_work_dir(allow_cwd=True)
|
|
709
|
-
|
|
769
|
+
module_name = root.name.replace("-", "_").replace(" ", "_")
|
|
770
|
+
package_maybe = (module_name, "src")
|
|
710
771
|
for name in package_maybe:
|
|
711
772
|
if root.joinpath(name).is_dir():
|
|
712
773
|
return name
|
|
@@ -720,6 +781,7 @@ class LintCode(DryRun):
|
|
|
720
781
|
bandit: bool = False,
|
|
721
782
|
skip_mypy: bool = False,
|
|
722
783
|
use_dmypy: bool = False,
|
|
784
|
+
tool: str = ToolOption.default,
|
|
723
785
|
) -> str:
|
|
724
786
|
if paths != "." and all(i.endswith(".html") for i in paths.split()):
|
|
725
787
|
return f"prettier -w {paths}"
|
|
@@ -748,8 +810,11 @@ class LintCode(DryRun):
|
|
|
748
810
|
secho(f"{tip}\n\n {command}\n", fg="yellow")
|
|
749
811
|
else:
|
|
750
812
|
should_run_by_tool = True
|
|
751
|
-
if should_run_by_tool and
|
|
752
|
-
|
|
813
|
+
if should_run_by_tool and tool:
|
|
814
|
+
if tool == ToolOption.default:
|
|
815
|
+
tool = Project.get_manage_tool() or ""
|
|
816
|
+
if tool:
|
|
817
|
+
prefix = tool + " run "
|
|
753
818
|
if cls.prefer_dmypy(paths, tools, use_dmypy=use_dmypy):
|
|
754
819
|
tools[-1] = "dmypy run"
|
|
755
820
|
cmd += lint_them.format(prefix, paths, *tools)
|
|
@@ -766,7 +831,9 @@ class LintCode(DryRun):
|
|
|
766
831
|
return cmd
|
|
767
832
|
|
|
768
833
|
def gen(self: Self) -> str:
|
|
769
|
-
|
|
834
|
+
if isinstance(args := self.args, str):
|
|
835
|
+
args = args.split()
|
|
836
|
+
paths = " ".join(map(str, args)) if args else "."
|
|
770
837
|
return self.to_cmd(
|
|
771
838
|
paths, self.check_only, self._bandit, self._skip_mypy, self._use_dmypy
|
|
772
839
|
)
|
|
@@ -776,15 +843,31 @@ def parse_files(args: list[str] | tuple[str, ...]) -> list[str]:
|
|
|
776
843
|
return [i for i in args if not i.startswith("-")]
|
|
777
844
|
|
|
778
845
|
|
|
779
|
-
def lint(
|
|
846
|
+
def lint(
|
|
847
|
+
files: list[str] | str | None = None,
|
|
848
|
+
dry: bool = False,
|
|
849
|
+
bandit: bool = False,
|
|
850
|
+
skip_mypy: bool = False,
|
|
851
|
+
dmypy: bool = False,
|
|
852
|
+
tool: str = ToolOption.default,
|
|
853
|
+
) -> None:
|
|
780
854
|
if files is None:
|
|
781
855
|
files = parse_files(sys.argv[1:])
|
|
782
856
|
if files and files[0] == "lint":
|
|
783
857
|
files = files[1:]
|
|
784
|
-
LintCode(
|
|
858
|
+
LintCode(
|
|
859
|
+
files, dry=dry, skip_mypy=skip_mypy, bandit=bandit, dmypy=dmypy, tool=tool
|
|
860
|
+
).run()
|
|
785
861
|
|
|
786
862
|
|
|
787
|
-
def check(
|
|
863
|
+
def check(
|
|
864
|
+
files: list[str] | str | None = None,
|
|
865
|
+
dry: bool = False,
|
|
866
|
+
bandit: bool = False,
|
|
867
|
+
skip_mypy: bool = False,
|
|
868
|
+
dmypy: bool = False,
|
|
869
|
+
tool: str = ToolOption.default,
|
|
870
|
+
) -> None:
|
|
788
871
|
LintCode(
|
|
789
872
|
files,
|
|
790
873
|
check_only=True,
|
|
@@ -793,32 +876,35 @@ def check(files=None, dry=False, bandit=False, skip_mypy=False, dmypy=False) ->
|
|
|
793
876
|
bandit=bandit,
|
|
794
877
|
skip_mypy=skip_mypy,
|
|
795
878
|
dmypy=dmypy,
|
|
879
|
+
tool=tool,
|
|
796
880
|
).run()
|
|
797
881
|
|
|
798
882
|
|
|
799
883
|
@cli.command(name="lint")
|
|
800
884
|
def make_style(
|
|
801
|
-
files: Optional[list[
|
|
885
|
+
files: Optional[list[str]] = typer.Argument(default=None), # noqa:B008
|
|
802
886
|
check_only: bool = Option(False, "--check-only", "-c"),
|
|
803
887
|
bandit: bool = Option(False, "--bandit", help="Run `bandit -r <package_dir>`"),
|
|
804
888
|
skip_mypy: bool = Option(False, "--skip-mypy"),
|
|
805
889
|
use_dmypy: bool = Option(
|
|
806
890
|
False, "--dmypy", help="Use `dmypy run` instead of `mypy`"
|
|
807
891
|
),
|
|
892
|
+
tool: str = ToolOption,
|
|
808
893
|
dry: bool = DryOption,
|
|
809
894
|
) -> None:
|
|
810
895
|
"""Run: ruff check/format to reformat code and then mypy to check"""
|
|
811
896
|
if getattr(files, "default", files) is None:
|
|
812
|
-
files = [
|
|
897
|
+
files = ["."]
|
|
813
898
|
elif isinstance(files, str):
|
|
814
899
|
files = [files]
|
|
815
900
|
skip = _ensure_bool(skip_mypy)
|
|
816
901
|
dmypy = _ensure_bool(use_dmypy)
|
|
817
902
|
bandit = _ensure_bool(bandit)
|
|
903
|
+
tool = _ensure_str(tool)
|
|
818
904
|
if _ensure_bool(check_only):
|
|
819
|
-
check(files, dry=dry, skip_mypy=skip, dmypy=dmypy, bandit=bandit)
|
|
905
|
+
check(files, dry=dry, skip_mypy=skip, dmypy=dmypy, bandit=bandit, tool=tool)
|
|
820
906
|
else:
|
|
821
|
-
lint(files, dry=dry, skip_mypy=skip, dmypy=dmypy, bandit=bandit)
|
|
907
|
+
lint(files, dry=dry, skip_mypy=skip, dmypy=dmypy, bandit=bandit, tool=tool)
|
|
822
908
|
|
|
823
909
|
|
|
824
910
|
@cli.command(name="check")
|
|
@@ -832,7 +918,9 @@ def only_check(
|
|
|
832
918
|
|
|
833
919
|
|
|
834
920
|
class Sync(DryRun):
|
|
835
|
-
def __init__(
|
|
921
|
+
def __init__(
|
|
922
|
+
self: Self, filename: str, extras: str, save: bool, dry: bool = False
|
|
923
|
+
) -> None:
|
|
836
924
|
self.filename = filename
|
|
837
925
|
self.extras = extras
|
|
838
926
|
self._save = save
|
|
@@ -846,23 +934,22 @@ class Sync(DryRun):
|
|
|
846
934
|
raise EnvError("There project is not managed by uv/pdm/poetry!")
|
|
847
935
|
return f"python -m pip install -r {self.filename}"
|
|
848
936
|
prefix = "" if is_venv() else f"{tool} run "
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
if check_call(prefix + "python -m pip --version"):
|
|
853
|
-
ensurepip = ""
|
|
854
|
-
elif tool in ("poetry", "pdm"):
|
|
937
|
+
ensure_pip = " {1}python -m ensurepip && {1}python -m pip install -U pip &&"
|
|
938
|
+
export_cmd = "uv export --no-hashes --all-extras --frozen"
|
|
939
|
+
if tool in ("poetry", "pdm"):
|
|
855
940
|
export_cmd = f"{tool} export --without-hashes --with=dev"
|
|
856
941
|
if tool == "poetry":
|
|
857
|
-
|
|
942
|
+
ensure_pip = ""
|
|
858
943
|
if not UpgradeDependencies.should_with_dev():
|
|
859
944
|
export_cmd = export_cmd.replace(" --with=dev", "")
|
|
860
945
|
if extras and isinstance(extras, (str, list)):
|
|
861
946
|
export_cmd += f" --{extras=}".replace("'", '"')
|
|
862
947
|
elif check_call(prefix + "python -m pip --version"):
|
|
863
|
-
|
|
948
|
+
ensure_pip = ""
|
|
949
|
+
elif check_call(prefix + "python -m pip --version"):
|
|
950
|
+
ensure_pip = ""
|
|
864
951
|
install_cmd = (
|
|
865
|
-
f"{{2}} -o {{0}} &&{
|
|
952
|
+
f"{{2}} -o {{0}} &&{ensure_pip} {{1}}python -m pip install -r {{0}}"
|
|
866
953
|
)
|
|
867
954
|
if should_remove and not save:
|
|
868
955
|
install_cmd += " && rm -f {0}"
|
|
@@ -871,7 +958,7 @@ class Sync(DryRun):
|
|
|
871
958
|
|
|
872
959
|
@cli.command()
|
|
873
960
|
def sync(
|
|
874
|
-
filename="dev_requirements.txt",
|
|
961
|
+
filename: str = "dev_requirements.txt",
|
|
875
962
|
extras: str = Option("", "--extras", "-E"),
|
|
876
963
|
save: bool = Option(
|
|
877
964
|
False, "--save", "-s", help="Whether save the requirement file"
|
|
@@ -889,7 +976,7 @@ def _should_run_test_script(path: Path = Path("scripts")) -> Path | None:
|
|
|
889
976
|
return None
|
|
890
977
|
|
|
891
978
|
|
|
892
|
-
def test(dry: bool, ignore_script=False) -> None:
|
|
979
|
+
def test(dry: bool, ignore_script: bool = False) -> None:
|
|
893
980
|
cwd = Path.cwd()
|
|
894
981
|
root = Project.get_work_dir(cwd=cwd, allow_cwd=True)
|
|
895
982
|
script_dir = root / "scripts"
|
|
@@ -944,13 +1031,13 @@ def dev(
|
|
|
944
1031
|
port: int | None | OptionInfo,
|
|
945
1032
|
host: str | None | OptionInfo,
|
|
946
1033
|
file: str | None | ArgumentInfo = None,
|
|
947
|
-
dry=False,
|
|
1034
|
+
dry: bool = False,
|
|
948
1035
|
) -> None:
|
|
949
1036
|
cmd = "fastapi dev"
|
|
950
1037
|
no_port_yet = True
|
|
951
1038
|
if file is not None:
|
|
952
1039
|
try:
|
|
953
|
-
port = int(str(file))
|
|
1040
|
+
port = int(str(file))
|
|
954
1041
|
except ValueError:
|
|
955
1042
|
cmd += f" {file}"
|
|
956
1043
|
else:
|
|
@@ -34,14 +34,14 @@ classifiers = [
|
|
|
34
34
|
dependencies = [
|
|
35
35
|
"typer>=0.12.3,<1",
|
|
36
36
|
"emoji >=2.12.1,<3",
|
|
37
|
+
"packaging>=20.5",
|
|
37
38
|
"tomli>=2.0.1,<3; python_version < '3.11'",
|
|
38
39
|
"coverage >=7.5.1,<8",
|
|
39
40
|
"mypy >=1.15.0,<2",
|
|
40
41
|
"bumpversion2 >=1.4.3,<2",
|
|
41
42
|
"pytest >=8.2.0,<9",
|
|
42
|
-
"packaging>=20.5",
|
|
43
43
|
]
|
|
44
|
-
version = "0.
|
|
44
|
+
version = "0.15.1"
|
|
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$",
|
|
@@ -51,7 +51,12 @@ build-backend = "poetry.core.masonry.api"
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
@pytest.fixture
|
|
54
|
-
def
|
|
54
|
+
def tmp_work_dir(tmp_path):
|
|
55
55
|
with chdir(tmp_path):
|
|
56
|
-
tmp_path
|
|
57
|
-
|
|
56
|
+
yield tmp_path
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.fixture
|
|
60
|
+
def tmp_poetry_project(tmp_work_dir: Path):
|
|
61
|
+
Path(TOML_FILE).write_text(TOML_CONTENT)
|
|
62
|
+
yield tmp_work_dir
|
|
@@ -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,95 @@ 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"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
PDM_DYNAMIC_VERSION = """
|
|
236
|
+
[tool.pdm.version]
|
|
237
|
+
source = "file"
|
|
238
|
+
path = "app/__init__.py"
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_pdm_project(tmp_work_dir):
|
|
243
|
+
capture_cmd_output("pdm new my-project --non-interactive")
|
|
244
|
+
with chdir("my-project"):
|
|
245
|
+
toml_file = Path("pyproject.toml")
|
|
246
|
+
content = toml_file.read_text().replace(
|
|
247
|
+
'version = "0.1.0"', 'dynamic = ["version"]'
|
|
248
|
+
)
|
|
249
|
+
toml_file.write_text(content)
|
|
250
|
+
with toml_file.open("a+") as f:
|
|
251
|
+
f.write(PDM_DYNAMIC_VERSION)
|
|
252
|
+
app = Path("app")
|
|
253
|
+
app.mkdir()
|
|
254
|
+
init_file = app.joinpath("__init__.py")
|
|
255
|
+
init_file.write_text('__version__ = "0.2.0"')
|
|
256
|
+
out = capture_cmd_output("fast bump patch")
|
|
257
|
+
assert str(init_file) in out
|
|
258
|
+
assert "0.2.1" in init_file.read_text()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def test_hatch_project(tmp_work_dir):
|
|
262
|
+
project_name = "httpx"
|
|
263
|
+
Path(project_name).mkdir()
|
|
264
|
+
with chdir(project_name):
|
|
265
|
+
toml_file = Path("pyproject.toml")
|
|
266
|
+
toml_file.write_text("""
|
|
267
|
+
[build-system]
|
|
268
|
+
requires = ["hatchling", "hatch-fancy-pypi-readme"]
|
|
269
|
+
build-backend = "hatchling.build"
|
|
270
|
+
|
|
271
|
+
[project]
|
|
272
|
+
name = "httpx"
|
|
273
|
+
description = "The next generation HTTP client."
|
|
274
|
+
license = "BSD-3-Clause"
|
|
275
|
+
requires-python = ">=3.8"
|
|
276
|
+
authors = [
|
|
277
|
+
{ name = "Tom Christie", email = "tom@tomchristie.com" },
|
|
278
|
+
]
|
|
279
|
+
dependencies = [
|
|
280
|
+
"certifi",
|
|
281
|
+
"httpcore==1.*",
|
|
282
|
+
"anyio",
|
|
283
|
+
"idna",
|
|
284
|
+
]
|
|
285
|
+
dynamic = ["readme", "version"]
|
|
286
|
+
|
|
287
|
+
[tool.hatch.version]
|
|
288
|
+
path = "httpx/__version__.py"
|
|
289
|
+
""")
|
|
290
|
+
Path(project_name).mkdir()
|
|
291
|
+
Path("httpx/__version__.py").write_text("""
|
|
292
|
+
__title__ = "httpx"
|
|
293
|
+
__description__ = "A next generation HTTP client, for Python 3."
|
|
294
|
+
__version__ = "0.28.1"
|
|
295
|
+
""")
|
|
@@ -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
|