fast-dev-cli 0.16.1__tar.gz → 0.17.0__tar.gz

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