fast-dev-cli 0.17.0__tar.gz → 0.17.2__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 (30) hide show
  1. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/PKG-INFO +1 -1
  2. fast_dev_cli-0.17.2/fast_dev_cli/__init__.py +1 -0
  3. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/fast_dev_cli/cli.py +199 -67
  4. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/pyproject.toml +5 -2
  5. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/tests/conftest.py +5 -3
  6. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/tests/test_bump.py +21 -8
  7. fast_dev_cli-0.17.2/tests/test_help.py +7 -0
  8. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/tests/test_lint.py +9 -9
  9. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/tests/test_poetry_version_plugin.py +21 -36
  10. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/tests/test_tag.py +14 -6
  11. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/tests/test_upgrade.py +2 -2
  12. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/tests/test_version.py +20 -2
  13. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/tests/utils.py +13 -21
  14. fast_dev_cli-0.17.0/fast_dev_cli/__init__.py +0 -1
  15. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/LICENSE +0 -0
  16. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/README.md +0 -0
  17. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/fast_dev_cli/__main__.py +0 -0
  18. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/fast_dev_cli/py.typed +0 -0
  19. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/pdm_build.py +0 -0
  20. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/scripts/check.py +0 -0
  21. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/scripts/deps.py +0 -0
  22. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/scripts/format.py +0 -0
  23. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/scripts/test.py +0 -0
  24. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/tests/__init__.py +0 -0
  25. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/tests/test_exec.py +0 -0
  26. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/tests/test_fast_test.py +0 -0
  27. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/tests/test_functions.py +0 -0
  28. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/tests/test_runserver.py +0 -0
  29. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/tests/test_sync.py +0 -0
  30. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.2}/tests/test_upload.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fast-dev-cli
3
- Version: 0.17.0
3
+ Version: 0.17.2
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.2"
@@ -10,6 +10,7 @@ import sys
10
10
  from functools import cached_property
11
11
  from pathlib import Path
12
12
  from typing import (
13
+ TYPE_CHECKING,
13
14
  Any,
14
15
  Literal,
15
16
  # Optional is required by Option generated by typer
@@ -23,17 +24,6 @@ import typer
23
24
  from typer import Exit, Option, echo, secho
24
25
  from typer.models import ArgumentInfo, OptionInfo
25
26
 
26
- try:
27
- from emoji import is_emoji
28
- except ImportError:
29
-
30
- def is_emoji(char: str) -> bool: # type:ignore[misc]
31
- return (
32
- not re.match(r"[\w\d\s]", char)
33
- and not "\u4e00" <= char <= "\u9fff" # Chinese
34
- )
35
-
36
-
37
27
  try:
38
28
  from . import __version__
39
29
  except ImportError: # pragma: no cover
@@ -43,20 +33,24 @@ except ImportError: # pragma: no cover
43
33
 
44
34
  if sys.version_info >= (3, 11): # pragma: no cover
45
35
  from enum import StrEnum
46
- from typing import Self
47
36
 
48
37
  import tomllib
49
38
  else: # pragma: no cover
50
39
  from enum import Enum
51
40
 
52
41
  import tomli as tomllib
53
- from typing_extensions import Self
54
42
 
55
43
  class StrEnum(str, Enum):
56
44
  __str__ = str.__str__
57
45
 
58
46
 
59
- cli = typer.Typer()
47
+ if TYPE_CHECKING:
48
+ if sys.version_info >= (3, 11):
49
+ from typing import Self
50
+ else:
51
+ from typing_extensions import Self
52
+
53
+ cli = typer.Typer(no_args_is_help=True)
60
54
  DryOption = Option(False, "--dry", help="Only print, not really run shell command")
61
55
  TOML_FILE = "pyproject.toml"
62
56
  ToolName = Literal["poetry", "pdm", "uv"]
@@ -83,6 +77,22 @@ def poetry_module_name(name: str) -> str:
83
77
  return canonicalize_name(name).replace("-", "_").replace(" ", "_")
84
78
 
85
79
 
80
+ def is_emoji(char: str) -> bool:
81
+ try:
82
+ import emoji
83
+ except ImportError:
84
+ pass
85
+ else:
86
+ return emoji.is_emoji(char)
87
+ if re.match(r"[\w\d\s]", char):
88
+ return False
89
+ return not "\u4e00" <= char <= "\u9fff" # Chinese character
90
+
91
+
92
+ def yellow_warn(msg: str) -> None:
93
+ secho(msg, fg="yellow")
94
+
95
+
86
96
  def load_bool(name: str, default: bool = False) -> bool:
87
97
  if not (v := os.getenv(name)):
88
98
  return default
@@ -116,7 +126,7 @@ def run_and_echo(
116
126
  if dry:
117
127
  return 0
118
128
  command: list[str] | str = cmd
119
- if "shell" not in kw and not (set(cmd) & {"|", ">"}):
129
+ if "shell" not in kw and not (set(cmd) & {"|", ">", "&"}):
120
130
  command = shlex.split(cmd)
121
131
  return _run_shell(command, **kw).returncode
122
132
 
@@ -155,9 +165,9 @@ def read_version_from_file(
155
165
  toml_text = Project.load_toml_text()
156
166
  context = tomllib.loads(toml_text)
157
167
  with contextlib.suppress(KeyError):
158
- return context["project"]["version"]
168
+ return cast(str, context["project"]["version"])
159
169
  with contextlib.suppress(KeyError): # Poetry V1
160
- return context["tool"]["poetry"]["version"]
170
+ return cast(str, context["tool"]["poetry"]["version"])
161
171
  secho(f"WARNING: can not find 'version' item in {version_file}!")
162
172
  return "0.0.0"
163
173
  pattern = re.compile(r"__version__\s*=")
@@ -274,14 +284,14 @@ def exit_if_run_failed(
274
284
 
275
285
 
276
286
  class DryRun:
277
- def __init__(self: Self, _exit: bool = False, dry: bool = False) -> None:
287
+ def __init__(self, _exit: bool = False, dry: bool = False) -> None:
278
288
  self.dry = dry
279
289
  self._exit = _exit
280
290
 
281
- def gen(self: Self) -> str:
291
+ def gen(self) -> str:
282
292
  raise NotImplementedError
283
293
 
284
- def run(self: Self) -> None:
294
+ def run(self) -> None:
285
295
  exit_if_run_failed(self.gen(), _exit=self._exit, dry=self.dry)
286
296
 
287
297
 
@@ -292,17 +302,21 @@ class BumpUp(DryRun):
292
302
  major = "major"
293
303
 
294
304
  def __init__(
295
- self: Self,
305
+ self,
296
306
  commit: bool,
297
307
  part: str,
298
308
  filename: str | None = None,
299
309
  dry: bool = False,
310
+ no_sync: bool = False,
311
+ emoji: bool | None = None,
300
312
  ) -> None:
301
313
  self.commit = commit
302
314
  self.part = part
303
315
  if filename is None:
304
316
  filename = self.parse_filename()
305
317
  self.filename = filename
318
+ self._no_sync = no_sync
319
+ self._emoji = emoji
306
320
  super().__init__(dry=dry)
307
321
 
308
322
  @staticmethod
@@ -352,7 +366,9 @@ class BumpUp(DryRun):
352
366
  work_dir = Project.get_work_dir()
353
367
  for tool in ("pdm", "hatch"):
354
368
  with contextlib.suppress(KeyError):
355
- version_path = context["tool"][tool]["version"]["path"]
369
+ version_path = cast(
370
+ str, context["tool"][tool]["version"]["path"]
371
+ )
356
372
  if (
357
373
  Path(version_path).exists()
358
374
  or work_dir.joinpath(version_path).exists()
@@ -425,10 +441,8 @@ class BumpUp(DryRun):
425
441
  echo(f"Invalid part: {s!r}")
426
442
  raise Exit(1) from e
427
443
 
428
- def gen(self: Self) -> str:
444
+ def gen(self) -> str:
429
445
  should_sync, _version = get_current_version(check_version=True)
430
- if should_sync:
431
- Project.sync_dependencies()
432
446
  filename = self.filename
433
447
  echo(f"Current version(@{filename}): {_version}")
434
448
  if self.part:
@@ -444,15 +458,17 @@ class BumpUp(DryRun):
444
458
  if part != "patch":
445
459
  cmd += " --tag"
446
460
  cmd += " --commit"
447
- if self.should_add_emoji():
461
+ if self._emoji or (self._emoji is None and self.should_add_emoji()):
448
462
  cmd += " --message-emoji=1"
449
463
  if not load_bool("DONT_GIT_PUSH"):
450
464
  cmd += " && git push && git push --tags && git log -1"
451
465
  else:
452
466
  cmd += " --allow-dirty"
467
+ if should_sync and not self._no_sync and (sync := Project.get_sync_command()):
468
+ cmd = f"{sync} && " + cmd
453
469
  return cmd
454
470
 
455
- def run(self: Self) -> None:
471
+ def run(self) -> None:
456
472
  super().run()
457
473
  if not self.commit and not self.dry:
458
474
  new_version = get_current_version(True)
@@ -464,7 +480,19 @@ class BumpUp(DryRun):
464
480
  @cli.command()
465
481
  def version() -> None:
466
482
  """Show the version of this tool"""
467
- echo(f"Fast Dev Cli version: {__version__}")
483
+ echo("Fast Dev Cli Version: " + typer.style(__version__, fg=typer.colors.BLUE))
484
+ with contextlib.suppress(FileNotFoundError, KeyError):
485
+ toml_text = Project.load_toml_text()
486
+ doc = tomllib.loads(toml_text)
487
+ version_file = doc["tool"]["pdm"]["version"]["path"]
488
+ text = Project.get_work_dir().joinpath(version_file).read_text()
489
+ varname = "__version__"
490
+ for line in text.splitlines():
491
+ if line.strip().startswith(varname):
492
+ value = line.split("=", 1)[-1].strip().strip('"').strip("'")
493
+ styled = typer.style(value, bold=True)
494
+ echo(f"Version value in {version_file}: " + styled)
495
+ break
468
496
 
469
497
 
470
498
  @cli.command(name="bump")
@@ -473,10 +501,24 @@ def bump_version(
473
501
  commit: bool = Option(
474
502
  False, "--commit", "-c", help="Whether run `git commit` after version changed"
475
503
  ),
504
+ emoji: Optional[bool] = Option(
505
+ None, "--emoji", help="Whether add emoji prefix to commit message"
506
+ ),
507
+ no_sync: bool = Option(
508
+ False, "--no-sync", help="Do not run sync command to update version"
509
+ ),
476
510
  dry: bool = DryOption,
477
511
  ) -> None:
478
512
  """Bump up version string in pyproject.toml"""
479
- return BumpUp(_ensure_bool(commit), getattr(part, "value", part), dry=dry).run()
513
+ if emoji is not None:
514
+ emoji = _ensure_bool(emoji)
515
+ return BumpUp(
516
+ _ensure_bool(commit),
517
+ getattr(part, "value", part),
518
+ no_sync=_ensure_bool(no_sync),
519
+ emoji=emoji,
520
+ dry=dry,
521
+ ).run()
480
522
 
481
523
 
482
524
  def bump() -> None:
@@ -488,7 +530,7 @@ def bump() -> None:
488
530
  if not a.startswith("-"):
489
531
  part = a
490
532
  break
491
- return BumpUp(commit, part, dry="--dry" in args).run()
533
+ return BumpUp(commit, part, no_sync="--no-sync" in args, dry="--dry" in args).run()
492
534
 
493
535
 
494
536
  class EnvError(Exception):
@@ -594,14 +636,30 @@ class Project:
594
636
  return True
595
637
 
596
638
  @classmethod
597
- def sync_dependencies(cls) -> None:
639
+ def get_sync_command(cls, prod: bool = True, doc: dict | None = None) -> str:
598
640
  if cls.is_pdm_project():
599
- cmd = "pdm sync --prod"
600
- run_and_echo(cmd)
641
+ return "pdm sync" + " --prod" * prod
601
642
  elif cls.manage_by_poetry(cache=True):
602
- run_and_echo("poetry install --only=main")
643
+ cmd = "poetry install"
644
+ if prod:
645
+ if doc is None:
646
+ doc = tomllib.loads(cls.load_toml_text())
647
+ if doc.get("project", {}).get("dependencies") or any(
648
+ i != "python"
649
+ for i in doc.get("tool", {})
650
+ .get("poetry", {})
651
+ .get("dependencies", [])
652
+ ):
653
+ cmd += " --only=main"
654
+ return cmd
603
655
  elif cls.get_manage_tool(cache=True) == "uv":
604
- run_and_echo("uv sync")
656
+ return "uv sync --inexact" + " --no-dev" * prod
657
+ return ""
658
+
659
+ @classmethod
660
+ def sync_dependencies(cls, prod: bool = True) -> None:
661
+ if cmd := cls.get_sync_command():
662
+ run_and_echo(cmd)
605
663
 
606
664
 
607
665
  class ParseError(Exception):
@@ -612,7 +670,7 @@ class ParseError(Exception):
612
670
 
613
671
  class UpgradeDependencies(Project, DryRun):
614
672
  def __init__(
615
- self: Self, _exit: bool = False, dry: bool = False, tool: ToolName = "poetry"
673
+ self, _exit: bool = False, dry: bool = False, tool: ToolName = "poetry"
616
674
  ) -> None:
617
675
  super().__init__(_exit, dry)
618
676
  self._tool = tool
@@ -777,11 +835,13 @@ class UpgradeDependencies(Project, DryRun):
777
835
  _upgrade += f" && poetry add {' '.join(single)}"
778
836
  return _upgrade
779
837
 
780
- def gen(self: Self) -> str:
838
+ def gen(self) -> str:
781
839
  if self._tool == "uv":
782
- return "uv lock --upgrade --verbose && uv sync --frozen --all-groups"
840
+ up = "uv lock --upgrade --verbose"
841
+ deps = "uv sync --inexact --frozen --all-groups --all-extras"
842
+ return f"{up} && {deps}"
783
843
  elif self._tool == "pdm":
784
- return "pdm update --verbose && pdm sync -G :all"
844
+ return "pdm update --verbose && pdm sync -G :all --frozen"
785
845
  return self.gen_cmd() + " && poetry lock && poetry update"
786
846
 
787
847
 
@@ -801,36 +861,35 @@ def upgrade(
801
861
 
802
862
 
803
863
  class GitTag(DryRun):
804
- def __init__(self: Self, message: str, dry: bool) -> None:
864
+ def __init__(self, message: str, dry: bool, no_sync: bool = False) -> None:
805
865
  self.message = message
866
+ self._no_sync = no_sync
806
867
  super().__init__(dry=dry)
807
868
 
808
869
  @staticmethod
809
870
  def has_v_prefix() -> bool:
810
871
  return "v" in capture_cmd_output("git tag")
811
872
 
812
- def should_push(self: Self) -> bool:
873
+ def should_push(self) -> bool:
813
874
  return "git push" in self.git_status
814
875
 
815
- def gen(self: Self) -> str:
876
+ def gen(self) -> str:
816
877
  should_sync, _version = get_current_version(verbose=False, check_version=True)
817
- if should_sync:
818
- Project.sync_dependencies()
819
878
  if self.has_v_prefix():
820
879
  # Add `v` at prefix to compare with bumpversion tool
821
880
  _version = "v" + _version
822
881
  cmd = f"git tag -a {_version} -m {self.message!r} && git push --tags"
823
882
  if self.should_push():
824
883
  cmd += " && git push"
825
- if Project.is_pdm_project():
826
- cmd = "pdm sync --prod && " + cmd
884
+ if should_sync and not self._no_sync and (sync := Project.get_sync_command()):
885
+ cmd = f"{sync} && " + cmd
827
886
  return cmd
828
887
 
829
888
  @cached_property
830
- def git_status(self: Self) -> str:
889
+ def git_status(self) -> str:
831
890
  return capture_cmd_output("git status")
832
891
 
833
- def mark_tag(self: Self) -> bool:
892
+ def mark_tag(self) -> bool:
834
893
  if not re.search(r"working (tree|directory) clean", self.git_status) and (
835
894
  "无文件要提交,干净的工作区" not in self.git_status
836
895
  ):
@@ -839,7 +898,7 @@ class GitTag(DryRun):
839
898
  return False
840
899
  return bool(super().run())
841
900
 
842
- def run(self: Self) -> None:
901
+ def run(self) -> None:
843
902
  if self.mark_tag() and not self.dry:
844
903
  echo("You may want to publish package:\n poetry publish --build")
845
904
 
@@ -847,15 +906,18 @@ class GitTag(DryRun):
847
906
  @cli.command()
848
907
  def tag(
849
908
  message: str = Option("", "-m", "--message"),
909
+ no_sync: bool = Option(
910
+ False, "--no-sync", help="Do not run sync command to update version"
911
+ ),
850
912
  dry: bool = DryOption,
851
913
  ) -> None:
852
914
  """Run shell command: git tag -a <current-version-in-pyproject.toml> -m {message}"""
853
- GitTag(message, dry=dry).run()
915
+ GitTag(message, dry=dry, no_sync=_ensure_bool(no_sync)).run()
854
916
 
855
917
 
856
918
  class LintCode(DryRun):
857
919
  def __init__(
858
- self: Self,
920
+ self,
859
921
  args: list[str] | str | None,
860
922
  check_only: bool = False,
861
923
  _exit: bool = False,
@@ -864,6 +926,7 @@ class LintCode(DryRun):
864
926
  skip_mypy: bool = False,
865
927
  dmypy: bool = False,
866
928
  tool: str = ToolOption.default,
929
+ prefix: bool = False,
867
930
  ) -> None:
868
931
  self.args = args
869
932
  self.check_only = check_only
@@ -871,6 +934,7 @@ class LintCode(DryRun):
871
934
  self._skip_mypy = skip_mypy
872
935
  self._use_dmypy = dmypy
873
936
  self._tool = tool
937
+ self._prefix = prefix
874
938
  super().__init__(_exit, dry)
875
939
 
876
940
  @staticmethod
@@ -904,6 +968,7 @@ class LintCode(DryRun):
904
968
  skip_mypy: bool = False,
905
969
  use_dmypy: bool = False,
906
970
  tool: str = ToolOption.default,
971
+ with_prefix: bool = False,
907
972
  ) -> str:
908
973
  if paths != "." and all(i.endswith(".html") for i in paths.split()):
909
974
  return f"prettier -w {paths}"
@@ -921,22 +986,42 @@ class LintCode(DryRun):
921
986
  lint_them = " && ".join(
922
987
  "{0}{" + str(i) + "} {1}" for i in range(2, len(tools) + 2)
923
988
  )
989
+ if ruff_exists := cls.check_lint_tool_installed():
990
+ # `ruff <command>` get the same result with `pdm run ruff <command>`
991
+ # While `mypy .`(installed global and env not activated),
992
+ # does not the same as `pdm run mypy .`
993
+ lint_them = " && ".join(
994
+ ("" if tool.startswith("ruff") else "{0}")
995
+ + (
996
+ "{%d} {1}" % i # noqa: UP031
997
+ )
998
+ for i, tool in enumerate(tools, 2)
999
+ )
924
1000
  prefix = ""
925
- should_run_by_tool = False
926
- if is_venv() and Path(sys.argv[0]).parent != Path.home().joinpath(".local/bin"):
927
- if not cls.check_lint_tool_installed():
1001
+ should_run_by_tool = with_prefix
1002
+ if not should_run_by_tool:
1003
+ if is_venv() and Path(sys.argv[0]).parent != Path.home().joinpath(
1004
+ ".local/bin"
1005
+ ):
1006
+ if not ruff_exists:
1007
+ should_run_by_tool = True
1008
+ if check_call('python -c "import fast_dev_cli"'):
1009
+ command = 'python -m pip install -U "fast-dev-cli"'
1010
+ yellow_warn(
1011
+ "You may need to run the following command"
1012
+ f" to install lint tools:\n\n {command}\n"
1013
+ )
1014
+ else:
928
1015
  should_run_by_tool = True
929
- if check_call('python -c "import fast_dev_cli"'):
930
- command = 'python -m pip install -U "fast-dev-cli"'
931
- tip = "You may need to run following command to install lint tools:"
932
- secho(f"{tip}\n\n {command}\n", fg="yellow")
933
- else:
934
- should_run_by_tool = True
935
1016
  if should_run_by_tool and tool:
936
1017
  if tool == ToolOption.default:
937
1018
  tool = Project.get_manage_tool() or ""
938
1019
  if tool:
939
- prefix = tool + " run "
1020
+ prefix = (
1021
+ bin_dir
1022
+ if tool == "uv" and Path(bin_dir := ".venv/bin/").exists()
1023
+ else (tool + " run ")
1024
+ )
940
1025
  if cls.prefer_dmypy(paths, tools, use_dmypy=use_dmypy):
941
1026
  tools[-1] = "dmypy run"
942
1027
  cmd += lint_them.format(prefix, paths, *tools)
@@ -952,12 +1037,18 @@ class LintCode(DryRun):
952
1037
  cmd += " && " + command
953
1038
  return cmd
954
1039
 
955
- def gen(self: Self) -> str:
1040
+ def gen(self) -> str:
956
1041
  if isinstance(args := self.args, str):
957
1042
  args = args.split()
958
1043
  paths = " ".join(map(str, args)) if args else "."
959
1044
  return self.to_cmd(
960
- paths, self.check_only, self._bandit, self._skip_mypy, self._use_dmypy
1045
+ paths,
1046
+ self.check_only,
1047
+ self._bandit,
1048
+ self._skip_mypy,
1049
+ self._use_dmypy,
1050
+ tool=self._tool,
1051
+ with_prefix=self._prefix,
961
1052
  )
962
1053
 
963
1054
 
@@ -972,13 +1063,20 @@ def lint(
972
1063
  skip_mypy: bool = False,
973
1064
  dmypy: bool = False,
974
1065
  tool: str = ToolOption.default,
1066
+ prefix: bool = False,
975
1067
  ) -> None:
976
1068
  if files is None:
977
1069
  files = parse_files(sys.argv[1:])
978
1070
  if files and files[0] == "lint":
979
1071
  files = files[1:]
980
1072
  LintCode(
981
- files, dry=dry, skip_mypy=skip_mypy, bandit=bandit, dmypy=dmypy, tool=tool
1073
+ files,
1074
+ dry=dry,
1075
+ skip_mypy=skip_mypy,
1076
+ bandit=bandit,
1077
+ dmypy=dmypy,
1078
+ tool=tool,
1079
+ prefix=prefix,
982
1080
  ).run()
983
1081
 
984
1082
 
@@ -1007,6 +1105,11 @@ def make_style(
1007
1105
  files: Optional[list[str]] = typer.Argument(default=None), # noqa:B008
1008
1106
  check_only: bool = Option(False, "--check-only", "-c"),
1009
1107
  bandit: bool = Option(False, "--bandit", help="Run `bandit -r <package_dir>`"),
1108
+ prefix: bool = Option(
1109
+ False,
1110
+ "--prefix",
1111
+ help="Run lint command with tool prefix, e.g.: pdm run ruff ...",
1112
+ ),
1010
1113
  skip_mypy: bool = Option(False, "--skip-mypy"),
1011
1114
  use_dmypy: bool = Option(
1012
1115
  False, "--dmypy", help="Use `dmypy run` instead of `mypy`"
@@ -1022,11 +1125,20 @@ def make_style(
1022
1125
  skip = _ensure_bool(skip_mypy)
1023
1126
  dmypy = _ensure_bool(use_dmypy)
1024
1127
  bandit = _ensure_bool(bandit)
1128
+ prefix = _ensure_bool(prefix)
1025
1129
  tool = _ensure_str(tool)
1026
1130
  if _ensure_bool(check_only):
1027
1131
  check(files, dry=dry, skip_mypy=skip, dmypy=dmypy, bandit=bandit, tool=tool)
1028
1132
  else:
1029
- lint(files, dry=dry, skip_mypy=skip, dmypy=dmypy, bandit=bandit, tool=tool)
1133
+ lint(
1134
+ files,
1135
+ dry=dry,
1136
+ skip_mypy=skip,
1137
+ dmypy=dmypy,
1138
+ bandit=bandit,
1139
+ tool=tool,
1140
+ prefix=prefix,
1141
+ )
1030
1142
 
1031
1143
 
1032
1144
  @cli.command(name="check")
@@ -1041,7 +1153,7 @@ def only_check(
1041
1153
 
1042
1154
  class Sync(DryRun):
1043
1155
  def __init__(
1044
- self: Self, filename: str, extras: str, save: bool, dry: bool = False
1156
+ self, filename: str, extras: str, save: bool, dry: bool = False
1045
1157
  ) -> None:
1046
1158
  self.filename = filename
1047
1159
  self.extras = extras
@@ -1192,7 +1304,7 @@ def runserver(
1192
1304
 
1193
1305
  @cli.command(name="exec")
1194
1306
  def run_by_subprocess(cmd: str, dry: bool = DryOption) -> None:
1195
- """Run cmd by subprocess, auto set shell=True when cmd contains '|'"""
1307
+ """Run cmd by subprocess, auto set shell=True when cmd contains '|>'"""
1196
1308
  try:
1197
1309
  rc = run_and_echo(cmd, verbose=True, dry=_ensure_bool(dry))
1198
1310
  except FileNotFoundError as e:
@@ -1205,6 +1317,26 @@ def run_by_subprocess(cmd: str, dry: bool = DryOption) -> None:
1205
1317
  raise Exit(rc)
1206
1318
 
1207
1319
 
1320
+ def version_callback(value: bool) -> None:
1321
+ if value:
1322
+ echo("Fast Dev Cli Version: " + typer.style(__version__, bold=True))
1323
+ raise Exit()
1324
+
1325
+
1326
+ @cli.callback()
1327
+ def common(
1328
+ version: bool = Option(
1329
+ None,
1330
+ "--version",
1331
+ "-V",
1332
+ callback=version_callback,
1333
+ is_eager=True,
1334
+ help="Show the version of this tool",
1335
+ ),
1336
+ ) -> None:
1337
+ pass
1338
+
1339
+
1208
1340
  def main() -> None:
1209
1341
  cli()
1210
1342
 
@@ -42,7 +42,7 @@ dependencies = [
42
42
  "bumpversion2 >=1.4.3,<2",
43
43
  "pytest >=8.2.0,<9",
44
44
  ]
45
- version = "0.17.0"
45
+ version = "0.17.2"
46
46
 
47
47
  [project.urls]
48
48
  Homepage = "https://github.com/waketzheng/fast-dev-cli"
@@ -59,6 +59,9 @@ fast = "fast_dev_cli:cli.main"
59
59
  dev = [
60
60
  "twine>=6.1.0",
61
61
  ]
62
+ test = [
63
+ "asynctor>=0.8.6",
64
+ ]
62
65
 
63
66
  [tool.pdm]
64
67
  distribution = true
@@ -150,7 +153,7 @@ omit = [
150
153
  context = "${CONTEXT}"
151
154
 
152
155
  [tool.coverage.report]
153
- exclude_lines = [
156
+ exclude_also = [
154
157
  "pragma: no cover",
155
158
  "@overload",
156
159
  "if __name__ == \"__main__\":",
@@ -1,10 +1,10 @@
1
+ import sys
1
2
  from pathlib import Path
2
3
 
3
4
  import pytest
5
+ from asynctor.compat import chdir
4
6
 
5
- from fast_dev_cli.cli import TOML_FILE
6
-
7
- from .utils import chdir
7
+ from fast_dev_cli.cli import TOML_FILE, run_and_echo
8
8
 
9
9
  # toml file content before migrate to pdm
10
10
  TOML_CONTENT = """
@@ -59,4 +59,6 @@ def tmp_work_dir(tmp_path):
59
59
  @pytest.fixture
60
60
  def tmp_poetry_project(tmp_work_dir: Path):
61
61
  Path(TOML_FILE).write_text(TOML_CONTENT)
62
+ if sys.version_info >= (3, 10):
63
+ run_and_echo("poetry config --local virtualenvs.in-project true")
62
64
  yield tmp_work_dir
@@ -22,6 +22,7 @@ from fast_dev_cli.cli import (
22
22
  bump_version,
23
23
  capture_cmd_output,
24
24
  get_current_version,
25
+ run_and_echo,
25
26
  )
26
27
 
27
28
  from .utils import chdir, mock_sys_argv, prepare_poetry_project
@@ -102,35 +103,38 @@ def test_bump(
102
103
 
103
104
  def test_bump_with_poetry(mocker, tmp_poetry_project, tmp_path):
104
105
  mocker.patch("builtins.input", return_value=" ")
105
- version = get_current_version()
106
+ version = get_current_version(check_version=False)
106
107
  patch_without_commit, patch_with_commit, minor_with_commit = _bump_commands(
107
108
  version, add_sync=False
108
109
  )
109
110
  stream = StringIO()
110
111
  with redirect_stdout(stream):
111
- BumpUp(part="patch", commit=False).run()
112
+ BumpUp(part="patch", commit=False, no_sync=True).run()
112
113
  assert f"Current version(@{TOML_FILE}):" in stream.getvalue()
113
114
  stream = StringIO()
114
115
  with redirect_stdout(stream):
115
- BumpUp(part="minor", commit=False).run()
116
+ BumpUp(part="minor", commit=False, no_sync=True).run()
116
117
  assert "You may want to pin tag by `fast tag`" in stream.getvalue()
117
118
  stream = StringIO()
118
119
  new_version = get_current_version()
119
120
  with redirect_stdout(stream):
120
- bump_version(BumpUp.PartChoices.patch, commit=False, dry=True)
121
+ bump_version(BumpUp.PartChoices.patch, commit=False, dry=True, no_sync=True)
121
122
  assert patch_without_commit.replace(version, new_version) in stream.getvalue()
122
123
  stream = StringIO()
123
- with redirect_stdout(stream), mock_sys_argv(["patch", "--dry"]):
124
+ with redirect_stdout(stream), mock_sys_argv(["patch", "--dry", "--no-sync"]):
124
125
  bump()
125
126
  assert patch_without_commit.replace(version, new_version) in stream.getvalue()
126
127
  stream = StringIO()
127
- with redirect_stdout(stream), mock_sys_argv(["patch", "--commit", "--dry"]):
128
+ with (
129
+ redirect_stdout(stream),
130
+ mock_sys_argv(["patch", "--commit", "--dry", "--no-sync"]),
131
+ ):
128
132
  bump()
129
133
  assert patch_with_commit.replace(version, new_version) in stream.getvalue()
130
134
  stream = StringIO()
131
135
  with (
132
136
  redirect_stdout(stream),
133
- mock_sys_argv(["-c", "minor", "--commit", "--dry"]),
137
+ mock_sys_argv(["-c", "minor", "--commit", "--dry", "--no-sync"]),
134
138
  ):
135
139
  bump()
136
140
  assert minor_with_commit.replace(version, new_version) in stream.getvalue()
@@ -182,7 +186,7 @@ def test_bump_with_emoji_in_poetry_project(mocker, tmp_path, monkeypatch):
182
186
  assert BumpUp.should_add_emoji() is False
183
187
  subprocess.run(["git", "commit", "-m", last_commit])
184
188
  monkeypatch.setenv("DONT_GIT_PUSH", "1")
185
- command = BumpUp(part="patch", commit=True).gen()
189
+ command = BumpUp(part="patch", commit=True, no_sync=True).gen()
186
190
  expected = patch_with_commit.split("&&")[0].strip().replace('""', '"0.1.0"')
187
191
  assert expected == command
188
192
  subprocess.run(["poetry", "run", "pip", "install", "bumpversion2"])
@@ -267,6 +271,15 @@ def test_pdm_project(dynamic_pdm_project):
267
271
  out = capture_cmd_output("fast bump patch")
268
272
  assert str(init_file) in out
269
273
  assert "0.2.1" in init_file.read_text()
274
+ run_and_echo("git init")
275
+ shutil.copy(Path(__file__).parent.parent / ".gitignore", ".")
276
+ run_and_echo("git add .")
277
+ run_and_echo("git config user.name xxx")
278
+ run_and_echo("git config user.email xxx@a.com")
279
+ run_and_echo('git commit -m "xxx"')
280
+ out = capture_cmd_output("fast bump patch --emoji --commit")
281
+ assert "--message-emoji=1" in out
282
+ assert "⬆️ Bump version: 0.2.1 → 0.2.2" in capture_cmd_output("git log")
270
283
 
271
284
 
272
285
  def test_installed_version_is_0_0_0(dynamic_pdm_project):
@@ -0,0 +1,7 @@
1
+ from asynctor import Shell
2
+
3
+
4
+ def test_help():
5
+ out = Shell("fast").capture_output()
6
+ out_help = Shell("fast --help").capture_output()
7
+ assert out.strip() == out_help.strip()
@@ -160,14 +160,6 @@ def test_lint_html():
160
160
  assert LintCode.to_cmd("index.html flv.html") == "prettier -w index.html flv.html"
161
161
 
162
162
 
163
- def test_lint_by_global_fast():
164
- run = "pdm run "
165
- fast = Path.home() / ".local" / "bin" / "fast"
166
- command = capture_cmd_output(f"{fast} lint --dry")
167
- for cmd in command.split(SEP):
168
- assert run in cmd
169
-
170
-
171
163
  def test_with_dmypy():
172
164
  cmd = "fast lint --dmypy --dry ."
173
165
  assert "dmypy run ." in capture_cmd_output(cmd)
@@ -195,6 +187,14 @@ def test_lint_with_prefix(mocker):
195
187
  assert "pdm run" in stream.getvalue()
196
188
 
197
189
 
190
+ def test_fast_lint_with_uv():
191
+ command = capture_cmd_output("fast lint --tool=uv --prefix --dry")
192
+ assert (
193
+ command
194
+ == "--> ruff format . && ruff check --extend-select=I,B,SIM --fix . && .venv/bin/mypy ."
195
+ )
196
+
197
+
198
198
  def test_make_style(mock_skip_mypy_0, mocker, mock_no_dmypy):
199
199
  mocker.patch("fast_dev_cli.cli.is_venv", return_value=True)
200
200
  with capture_stdout() as stream:
@@ -247,7 +247,7 @@ def test_lint_without_ruff_installed(mocker, mock_no_dmypy):
247
247
  output = stream.getvalue()
248
248
  cmd = 'python -m pip install -U "fast-dev-cli"'
249
249
  assert cmd in output
250
- tip = "You may need to run following command to install lint tools"
250
+ tip = "You may need to run the following command to install lint tools"
251
251
  assert tip in output
252
252
  assert f"{tip}:\n\n {cmd}" in output
253
253
 
@@ -38,21 +38,20 @@ def _prepare_package(
38
38
  a, b = 'version = "0.1.0"', f'version = "{mark}"'
39
39
  if define_include:
40
40
  b += f'\npackages = [{{include = "{package_name}"}}]'
41
+ py_version = "{}.{}".format(*sys.version_info)
41
42
  with chdir(package_path.parent):
42
- run_and_echo(f'poetry new "{package_path.name}"')
43
+ run_and_echo(
44
+ f'poetry new --python="^{py_version}" --no-interaction "{package_path.name}"'
45
+ )
43
46
  src_dir = package_path / "src"
44
47
  if is_src_layout := src_dir.exists():
45
48
  # poetry v2 default to use src/<package_name> layout
46
49
  init_file = src_dir / package_name / init_file.name
47
- toml_file.unlink()
48
- py_version = "{}.{}".format(*sys.version_info)
49
50
  with chdir(package_path):
50
- run_and_echo(f'poetry init --python="^{py_version}" --no-interaction')
51
+ run_and_echo("poetry config --local virtualenvs.in-project true")
51
52
  text = toml_file.read_text().replace(a, b)
52
53
  if " " in package_path.name:
53
- text = text.replace(
54
- f'name = "{package_path.name}"', f'name = "{package_name}"'
55
- )
54
+ text = text.replace(f'"{package_path.name}"', f'"{package_name}"')
56
55
  toml_file.write_text(text + CONF)
57
56
  if package_path.name != package_name:
58
57
  if is_src_layout:
@@ -63,15 +62,21 @@ def _prepare_package(
63
62
  yield init_file
64
63
 
65
64
 
66
- def _build_bump_cmd(init_file: Path, project_path: Path) -> str:
65
+ def _build_bump_cmd(
66
+ init_file: Path, project_path: Path, should_sync: bool = False
67
+ ) -> str:
67
68
  relative_path = init_file.relative_to(project_path).as_posix()
68
- return rf'bumpversion --parse "(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)" --current-version="0.0.1" patch {relative_path} --allow-dirty'
69
+ cmd = rf'bumpversion --parse "(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)" --current-version="0.0.1" patch {relative_path} --allow-dirty'
70
+ if should_sync:
71
+ cmd = "poetry install && " + cmd
72
+ return cmd
69
73
 
70
74
 
71
- def test_version_plugin(tmp_path: Path) -> None:
75
+ @pytest.mark.parametrize("mark", ["0", "0.0.0"])
76
+ def test_version_plugin(mark, tmp_path: Path) -> None:
72
77
  project_path = tmp_path / "helloworld"
73
- with _prepare_package(project_path) as init_file:
74
- command = _build_bump_cmd(init_file, project_path)
78
+ with _prepare_package(project_path, mark=mark) as init_file:
79
+ command = _build_bump_cmd(init_file, project_path, should_sync=True)
75
80
  assert BumpUp(part="patch", commit=False, dry=True).gen() == command
76
81
  run_and_echo("poetry run fast bump patch")
77
82
  assert init_file.read_text() == '__version__ = "0.0.2"\n'
@@ -80,31 +85,11 @@ def test_version_plugin(tmp_path: Path) -> None:
80
85
  BumpUp(part="patch", commit=False, dry=True).gen()
81
86
 
82
87
 
83
- def test_version_plugin_2(tmp_path: Path) -> None:
84
- project_path = tmp_path / "helloworld"
85
- with _prepare_package(project_path, mark="0.0.0") as init_file:
86
- command = _build_bump_cmd(init_file, project_path)
87
- assert BumpUp(part="patch", commit=False, dry=True).gen() == command
88
- run_and_echo("poetry run fast bump patch")
89
- assert init_file.read_text() == '__version__ = "0.0.2"\n'
90
- init_file.unlink()
91
- with pytest.raises(ParseError, match=r"Version file not found!.*"):
92
- BumpUp(part="patch", commit=False, dry=True).gen()
93
-
94
-
95
- def test_version_plugin_include_defined(tmp_path: Path) -> None:
96
- project_path = tmp_path / "hello world"
97
- with _prepare_package(project_path, True) as init_file:
98
- command = _build_bump_cmd(init_file, project_path)
99
- assert BumpUp(part="patch", commit=False, dry=True).gen() == command
100
- run_and_echo("poetry run fast bump patch")
101
- assert init_file.read_text() == '__version__ = "0.0.2"\n'
102
-
103
-
104
- def test_version_plugin_include_defined_2(tmp_path: Path) -> None:
88
+ @pytest.mark.parametrize("mark", ["0", "0.0.0"])
89
+ def test_version_plugin_include_defined(mark, tmp_path: Path) -> None:
105
90
  project_path = tmp_path / "hello world"
106
- with _prepare_package(project_path, True, mark="0.0.0") as init_file:
107
- command = _build_bump_cmd(init_file, project_path)
91
+ with _prepare_package(project_path, True, mark=mark) as init_file:
92
+ command = _build_bump_cmd(init_file, project_path, should_sync=True)
108
93
  assert BumpUp(part="patch", commit=False, dry=True).gen() == command
109
94
  run_and_echo("poetry run fast bump patch")
110
95
  assert init_file.read_text() == '__version__ = "0.0.2"\n'
@@ -50,16 +50,24 @@ def _clear_tags():
50
50
  def test_with_push(mocker):
51
51
  git_tag = GitTag("", dry=True)
52
52
  mocker.patch.object(git_tag, "git_status", return_value="git push")
53
- version = get_current_version()
53
+ should_sync, version = get_current_version(check_version=True)
54
54
  prefix = "v" if "v" in capture_cmd_output(["git", "tag"]) else ""
55
55
  sync = "pdm sync --prod"
56
56
  push = "git push --tags"
57
- assert git_tag.gen() == f"{sync} && git tag -a {prefix}{version} -m '' && {push}"
57
+ expected = f"git tag -a {prefix}{version} -m '' && {push}"
58
+ if should_sync:
59
+ expected = f"{sync} && " + expected
60
+ assert git_tag.gen() == expected
58
61
  with _clear_tags():
59
62
  git_tag_cmd = git_tag.gen()
60
- assert git_tag_cmd == f"{sync} && git tag -a {version} -m '' && {push}"
63
+ expected = f"git tag -a {version} -m '' && {push}"
64
+ if should_sync:
65
+ expected = f"{sync} && " + expected
66
+ assert git_tag_cmd == expected
61
67
  mocker.patch.object(git_tag, "has_v_prefix", return_value=True)
62
- tag_cmd = f"{sync} && git tag -a v{version} -m '' && {push}"
63
- assert git_tag.gen() == tag_cmd
68
+ expected = f"git tag -a v{version} -m '' && {push}"
69
+ if should_sync:
70
+ expected = f"{sync} && " + expected
71
+ assert git_tag.gen() == expected
64
72
  mocker.patch.object(git_tag, "should_push", return_value=True)
65
- assert git_tag.gen() == tag_cmd + " && git push"
73
+ assert git_tag.gen() == expected + " && 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 --all-groups"
295
+ expected = "uv lock --upgrade --verbose && uv sync --inexact --frozen --all-groups --all-extras"
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 sync -G :all"
303
+ expected = "pdm update --verbose && pdm sync -G :all --frozen"
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
@@ -2,19 +2,21 @@ import re
2
2
  from pathlib import Path
3
3
 
4
4
  import pytest
5
+ from asynctor import Shell
6
+ from asynctor.compat import chdir
5
7
 
6
8
  from fast_dev_cli import __version__
7
9
  from fast_dev_cli.cli import (
8
10
  TOML_FILE,
11
+ Exit,
9
12
  ShellCommandError,
10
13
  _parse_version,
11
14
  get_current_version,
12
15
  read_version_from_file,
13
16
  version,
17
+ version_callback,
14
18
  )
15
19
 
16
- from .utils import chdir
17
-
18
20
 
19
21
  def test_version(capsys):
20
22
  version()
@@ -58,3 +60,19 @@ def test_parse_version():
58
60
  assert _parse_version(line, pattern) == "0.0.1"
59
61
  line = '__version__ = "0.0.1" # 0.0.2'
60
62
  assert _parse_version(line, pattern) == "0.0.1"
63
+
64
+
65
+ def test_display_self_version(capsys):
66
+ with pytest.raises(Exit):
67
+ version_callback(True)
68
+ assert __version__ in capsys.readouterr().out
69
+ out = Shell("fast --version").capture_output().strip()
70
+ assert out == f"Fast Dev Cli Version: {__version__}"
71
+ out_v = Shell("fast -V").capture_output().strip()
72
+ assert out_v == out
73
+
74
+
75
+ def test_fast_version(capsys):
76
+ out = Shell("fast version").capture_output().strip()
77
+ assert f"Fast Dev Cli Version: {__version__}" in out
78
+ assert f"fast_dev_cli/__init__.py: {__version__}" in out
@@ -1,29 +1,13 @@
1
- import os
2
- import subprocess
1
+ import shutil
3
2
  import sys
4
- from contextlib import AbstractContextManager, contextmanager, redirect_stdout
3
+ from contextlib import contextmanager, redirect_stdout
5
4
  from io import StringIO
6
5
  from pathlib import Path
7
6
 
8
-
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."""
12
-
13
- def __init__(self, path):
14
- self.path = path
15
- self._old_cwd = []
16
-
17
- def __enter__(self):
18
- self._old_cwd.append(os.getcwd())
19
- os.chdir(self.path)
20
-
21
- def __exit__(self, *excinfo):
22
- os.chdir(self._old_cwd.pop())
23
-
7
+ from asynctor import Shell
8
+ from asynctor.compat import chdir
24
9
 
25
10
  __all__ = (
26
- "chdir",
27
11
  "mock_sys_argv",
28
12
  "capture_stdout",
29
13
  "temp_file",
@@ -68,8 +52,16 @@ def temp_file(name: str, text=""):
68
52
 
69
53
  @contextmanager
70
54
  def prepare_poetry_project(tmp_path: Path):
55
+ py = "{}.{}".format(*sys.version_info)
56
+ poetry = "poetry"
57
+ if shutil.which(poetry) is None:
58
+ poetry = "uvx " + poetry
71
59
  with chdir(tmp_path):
72
60
  project = "foo"
73
- subprocess.run(["poetry", "new", project])
61
+ Shell.run_by_subprocess(f"{poetry} new {project} --python=^{py}")
74
62
  with chdir(tmp_path / project):
63
+ Shell.run_by_subprocess(
64
+ f"{poetry} config --local virtualenvs.in-project true"
65
+ )
66
+ Shell.run_by_subprocess(f"{poetry} env use {py}")
75
67
  yield
@@ -1 +0,0 @@
1
- __version__ = "0.17.0"
File without changes
File without changes