fast-dev-cli 0.17.0__tar.gz → 0.17.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.
Files changed (30) hide show
  1. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/PKG-INFO +1 -1
  2. fast_dev_cli-0.17.1/fast_dev_cli/__init__.py +1 -0
  3. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/fast_dev_cli/cli.py +149 -56
  4. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/pyproject.toml +5 -2
  5. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/tests/conftest.py +5 -3
  6. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/tests/test_bump.py +21 -8
  7. fast_dev_cli-0.17.1/tests/test_help.py +7 -0
  8. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/tests/test_lint.py +0 -8
  9. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/tests/test_poetry_version_plugin.py +21 -36
  10. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/tests/test_tag.py +14 -6
  11. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/tests/test_upgrade.py +2 -2
  12. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/tests/test_version.py +20 -2
  13. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/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.1}/LICENSE +0 -0
  16. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/README.md +0 -0
  17. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/fast_dev_cli/__main__.py +0 -0
  18. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/fast_dev_cli/py.typed +0 -0
  19. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/pdm_build.py +0 -0
  20. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/scripts/check.py +0 -0
  21. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/scripts/deps.py +0 -0
  22. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/scripts/format.py +0 -0
  23. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/scripts/test.py +0 -0
  24. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/tests/__init__.py +0 -0
  25. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/tests/test_exec.py +0 -0
  26. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/tests/test_fast_test.py +0 -0
  27. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/tests/test_functions.py +0 -0
  28. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/tests/test_runserver.py +0 -0
  29. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/tests/test_sync.py +0 -0
  30. {fast_dev_cli-0.17.0 → fast_dev_cli-0.17.1}/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.1
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.1"
@@ -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,18 @@ 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
+
86
92
  def load_bool(name: str, default: bool = False) -> bool:
87
93
  if not (v := os.getenv(name)):
88
94
  return default
@@ -116,7 +122,7 @@ def run_and_echo(
116
122
  if dry:
117
123
  return 0
118
124
  command: list[str] | str = cmd
119
- if "shell" not in kw and not (set(cmd) & {"|", ">"}):
125
+ if "shell" not in kw and not (set(cmd) & {"|", ">", "&"}):
120
126
  command = shlex.split(cmd)
121
127
  return _run_shell(command, **kw).returncode
122
128
 
@@ -155,9 +161,9 @@ def read_version_from_file(
155
161
  toml_text = Project.load_toml_text()
156
162
  context = tomllib.loads(toml_text)
157
163
  with contextlib.suppress(KeyError):
158
- return context["project"]["version"]
164
+ return cast(str, context["project"]["version"])
159
165
  with contextlib.suppress(KeyError): # Poetry V1
160
- return context["tool"]["poetry"]["version"]
166
+ return cast(str, context["tool"]["poetry"]["version"])
161
167
  secho(f"WARNING: can not find 'version' item in {version_file}!")
162
168
  return "0.0.0"
163
169
  pattern = re.compile(r"__version__\s*=")
@@ -274,14 +280,14 @@ def exit_if_run_failed(
274
280
 
275
281
 
276
282
  class DryRun:
277
- def __init__(self: Self, _exit: bool = False, dry: bool = False) -> None:
283
+ def __init__(self, _exit: bool = False, dry: bool = False) -> None:
278
284
  self.dry = dry
279
285
  self._exit = _exit
280
286
 
281
- def gen(self: Self) -> str:
287
+ def gen(self) -> str:
282
288
  raise NotImplementedError
283
289
 
284
- def run(self: Self) -> None:
290
+ def run(self) -> None:
285
291
  exit_if_run_failed(self.gen(), _exit=self._exit, dry=self.dry)
286
292
 
287
293
 
@@ -292,17 +298,21 @@ class BumpUp(DryRun):
292
298
  major = "major"
293
299
 
294
300
  def __init__(
295
- self: Self,
301
+ self,
296
302
  commit: bool,
297
303
  part: str,
298
304
  filename: str | None = None,
299
305
  dry: bool = False,
306
+ no_sync: bool = False,
307
+ emoji: bool | None = None,
300
308
  ) -> None:
301
309
  self.commit = commit
302
310
  self.part = part
303
311
  if filename is None:
304
312
  filename = self.parse_filename()
305
313
  self.filename = filename
314
+ self._no_sync = no_sync
315
+ self._emoji = emoji
306
316
  super().__init__(dry=dry)
307
317
 
308
318
  @staticmethod
@@ -352,7 +362,9 @@ class BumpUp(DryRun):
352
362
  work_dir = Project.get_work_dir()
353
363
  for tool in ("pdm", "hatch"):
354
364
  with contextlib.suppress(KeyError):
355
- version_path = context["tool"][tool]["version"]["path"]
365
+ version_path = cast(
366
+ str, context["tool"][tool]["version"]["path"]
367
+ )
356
368
  if (
357
369
  Path(version_path).exists()
358
370
  or work_dir.joinpath(version_path).exists()
@@ -425,10 +437,8 @@ class BumpUp(DryRun):
425
437
  echo(f"Invalid part: {s!r}")
426
438
  raise Exit(1) from e
427
439
 
428
- def gen(self: Self) -> str:
440
+ def gen(self) -> str:
429
441
  should_sync, _version = get_current_version(check_version=True)
430
- if should_sync:
431
- Project.sync_dependencies()
432
442
  filename = self.filename
433
443
  echo(f"Current version(@{filename}): {_version}")
434
444
  if self.part:
@@ -444,15 +454,17 @@ class BumpUp(DryRun):
444
454
  if part != "patch":
445
455
  cmd += " --tag"
446
456
  cmd += " --commit"
447
- if self.should_add_emoji():
457
+ if self._emoji or (self._emoji is None and self.should_add_emoji()):
448
458
  cmd += " --message-emoji=1"
449
459
  if not load_bool("DONT_GIT_PUSH"):
450
460
  cmd += " && git push && git push --tags && git log -1"
451
461
  else:
452
462
  cmd += " --allow-dirty"
463
+ if should_sync and not self._no_sync and (sync := Project.get_sync_command()):
464
+ cmd = f"{sync} && " + cmd
453
465
  return cmd
454
466
 
455
- def run(self: Self) -> None:
467
+ def run(self) -> None:
456
468
  super().run()
457
469
  if not self.commit and not self.dry:
458
470
  new_version = get_current_version(True)
@@ -464,7 +476,19 @@ class BumpUp(DryRun):
464
476
  @cli.command()
465
477
  def version() -> None:
466
478
  """Show the version of this tool"""
467
- echo(f"Fast Dev Cli version: {__version__}")
479
+ echo("Fast Dev Cli Version: " + typer.style(__version__, fg=typer.colors.BLUE))
480
+ with contextlib.suppress(FileNotFoundError, KeyError):
481
+ toml_text = Project.load_toml_text()
482
+ doc = tomllib.loads(toml_text)
483
+ version_file = doc["tool"]["pdm"]["version"]["path"]
484
+ text = Project.get_work_dir().joinpath(version_file).read_text()
485
+ varname = "__version__"
486
+ for line in text.splitlines():
487
+ if line.strip().startswith(varname):
488
+ value = line.split("=", 1)[-1].strip().strip('"').strip("'")
489
+ styled = typer.style(value, bold=True)
490
+ echo(f"Version value in {version_file}: " + styled)
491
+ break
468
492
 
469
493
 
470
494
  @cli.command(name="bump")
@@ -473,10 +497,24 @@ def bump_version(
473
497
  commit: bool = Option(
474
498
  False, "--commit", "-c", help="Whether run `git commit` after version changed"
475
499
  ),
500
+ emoji: Optional[bool] = Option(
501
+ None, "--emoji", help="Whether add emoji prefix to commit message"
502
+ ),
503
+ no_sync: bool = Option(
504
+ False, "--no-sync", help="Do not run sync command to update version"
505
+ ),
476
506
  dry: bool = DryOption,
477
507
  ) -> None:
478
508
  """Bump up version string in pyproject.toml"""
479
- return BumpUp(_ensure_bool(commit), getattr(part, "value", part), dry=dry).run()
509
+ if emoji is not None:
510
+ emoji = _ensure_bool(emoji)
511
+ return BumpUp(
512
+ _ensure_bool(commit),
513
+ getattr(part, "value", part),
514
+ no_sync=_ensure_bool(no_sync),
515
+ emoji=emoji,
516
+ dry=dry,
517
+ ).run()
480
518
 
481
519
 
482
520
  def bump() -> None:
@@ -488,7 +526,7 @@ def bump() -> None:
488
526
  if not a.startswith("-"):
489
527
  part = a
490
528
  break
491
- return BumpUp(commit, part, dry="--dry" in args).run()
529
+ return BumpUp(commit, part, no_sync="--no-sync" in args, dry="--dry" in args).run()
492
530
 
493
531
 
494
532
  class EnvError(Exception):
@@ -594,14 +632,30 @@ class Project:
594
632
  return True
595
633
 
596
634
  @classmethod
597
- def sync_dependencies(cls) -> None:
635
+ def get_sync_command(cls, prod: bool = True, doc: dict | None = None) -> str:
598
636
  if cls.is_pdm_project():
599
- cmd = "pdm sync --prod"
600
- run_and_echo(cmd)
637
+ return "pdm sync" + " --prod" * prod
601
638
  elif cls.manage_by_poetry(cache=True):
602
- run_and_echo("poetry install --only=main")
639
+ cmd = "poetry install"
640
+ if prod:
641
+ if doc is None:
642
+ doc = tomllib.loads(cls.load_toml_text())
643
+ if doc.get("project", {}).get("dependencies") or any(
644
+ i != "python"
645
+ for i in doc.get("tool", {})
646
+ .get("poetry", {})
647
+ .get("dependencies", [])
648
+ ):
649
+ cmd += " --only=main"
650
+ return cmd
603
651
  elif cls.get_manage_tool(cache=True) == "uv":
604
- run_and_echo("uv sync")
652
+ return "uv sync --inexact" + " --no-dev" * prod
653
+ return ""
654
+
655
+ @classmethod
656
+ def sync_dependencies(cls, prod: bool = True) -> None:
657
+ if cmd := cls.get_sync_command():
658
+ run_and_echo(cmd)
605
659
 
606
660
 
607
661
  class ParseError(Exception):
@@ -612,7 +666,7 @@ class ParseError(Exception):
612
666
 
613
667
  class UpgradeDependencies(Project, DryRun):
614
668
  def __init__(
615
- self: Self, _exit: bool = False, dry: bool = False, tool: ToolName = "poetry"
669
+ self, _exit: bool = False, dry: bool = False, tool: ToolName = "poetry"
616
670
  ) -> None:
617
671
  super().__init__(_exit, dry)
618
672
  self._tool = tool
@@ -777,11 +831,13 @@ class UpgradeDependencies(Project, DryRun):
777
831
  _upgrade += f" && poetry add {' '.join(single)}"
778
832
  return _upgrade
779
833
 
780
- def gen(self: Self) -> str:
834
+ def gen(self) -> str:
781
835
  if self._tool == "uv":
782
- return "uv lock --upgrade --verbose && uv sync --frozen --all-groups"
836
+ up = "uv lock --upgrade --verbose"
837
+ deps = "uv sync --inexact --frozen --all-groups --all-extras"
838
+ return f"{up} && {deps}"
783
839
  elif self._tool == "pdm":
784
- return "pdm update --verbose && pdm sync -G :all"
840
+ return "pdm update --verbose && pdm sync -G :all --frozen"
785
841
  return self.gen_cmd() + " && poetry lock && poetry update"
786
842
 
787
843
 
@@ -801,36 +857,35 @@ def upgrade(
801
857
 
802
858
 
803
859
  class GitTag(DryRun):
804
- def __init__(self: Self, message: str, dry: bool) -> None:
860
+ def __init__(self, message: str, dry: bool, no_sync: bool = False) -> None:
805
861
  self.message = message
862
+ self._no_sync = no_sync
806
863
  super().__init__(dry=dry)
807
864
 
808
865
  @staticmethod
809
866
  def has_v_prefix() -> bool:
810
867
  return "v" in capture_cmd_output("git tag")
811
868
 
812
- def should_push(self: Self) -> bool:
869
+ def should_push(self) -> bool:
813
870
  return "git push" in self.git_status
814
871
 
815
- def gen(self: Self) -> str:
872
+ def gen(self) -> str:
816
873
  should_sync, _version = get_current_version(verbose=False, check_version=True)
817
- if should_sync:
818
- Project.sync_dependencies()
819
874
  if self.has_v_prefix():
820
875
  # Add `v` at prefix to compare with bumpversion tool
821
876
  _version = "v" + _version
822
877
  cmd = f"git tag -a {_version} -m {self.message!r} && git push --tags"
823
878
  if self.should_push():
824
879
  cmd += " && git push"
825
- if Project.is_pdm_project():
826
- cmd = "pdm sync --prod && " + cmd
880
+ if should_sync and not self._no_sync and (sync := Project.get_sync_command()):
881
+ cmd = f"{sync} && " + cmd
827
882
  return cmd
828
883
 
829
884
  @cached_property
830
- def git_status(self: Self) -> str:
885
+ def git_status(self) -> str:
831
886
  return capture_cmd_output("git status")
832
887
 
833
- def mark_tag(self: Self) -> bool:
888
+ def mark_tag(self) -> bool:
834
889
  if not re.search(r"working (tree|directory) clean", self.git_status) and (
835
890
  "无文件要提交,干净的工作区" not in self.git_status
836
891
  ):
@@ -839,7 +894,7 @@ class GitTag(DryRun):
839
894
  return False
840
895
  return bool(super().run())
841
896
 
842
- def run(self: Self) -> None:
897
+ def run(self) -> None:
843
898
  if self.mark_tag() and not self.dry:
844
899
  echo("You may want to publish package:\n poetry publish --build")
845
900
 
@@ -847,15 +902,18 @@ class GitTag(DryRun):
847
902
  @cli.command()
848
903
  def tag(
849
904
  message: str = Option("", "-m", "--message"),
905
+ no_sync: bool = Option(
906
+ False, "--no-sync", help="Do not run sync command to update version"
907
+ ),
850
908
  dry: bool = DryOption,
851
909
  ) -> None:
852
910
  """Run shell command: git tag -a <current-version-in-pyproject.toml> -m {message}"""
853
- GitTag(message, dry=dry).run()
911
+ GitTag(message, dry=dry, no_sync=_ensure_bool(no_sync)).run()
854
912
 
855
913
 
856
914
  class LintCode(DryRun):
857
915
  def __init__(
858
- self: Self,
916
+ self,
859
917
  args: list[str] | str | None,
860
918
  check_only: bool = False,
861
919
  _exit: bool = False,
@@ -921,10 +979,21 @@ class LintCode(DryRun):
921
979
  lint_them = " && ".join(
922
980
  "{0}{" + str(i) + "} {1}" for i in range(2, len(tools) + 2)
923
981
  )
982
+ if ruff_exists := cls.check_lint_tool_installed():
983
+ # `ruff <command>` get the same result with `pdm run ruff <command>`
984
+ # While `mypy .`(installed global and env not activated),
985
+ # does not the same as `pdm run mypy .`
986
+ lint_them = " && ".join(
987
+ ("" if tool.startswith("ruff") else "{0}")
988
+ + (
989
+ "{%d} {1}" % i # noqa: UP031
990
+ )
991
+ for i, tool in enumerate(tools, 2)
992
+ )
924
993
  prefix = ""
925
994
  should_run_by_tool = False
926
995
  if is_venv() and Path(sys.argv[0]).parent != Path.home().joinpath(".local/bin"):
927
- if not cls.check_lint_tool_installed():
996
+ if not ruff_exists:
928
997
  should_run_by_tool = True
929
998
  if check_call('python -c "import fast_dev_cli"'):
930
999
  command = 'python -m pip install -U "fast-dev-cli"'
@@ -936,7 +1005,11 @@ class LintCode(DryRun):
936
1005
  if tool == ToolOption.default:
937
1006
  tool = Project.get_manage_tool() or ""
938
1007
  if tool:
939
- prefix = tool + " run "
1008
+ prefix = (
1009
+ str(bin_dir)
1010
+ if tool == "uv" and (bin_dir := Path(".venv/bin/")).exists()
1011
+ else (tool + " run ")
1012
+ )
940
1013
  if cls.prefer_dmypy(paths, tools, use_dmypy=use_dmypy):
941
1014
  tools[-1] = "dmypy run"
942
1015
  cmd += lint_them.format(prefix, paths, *tools)
@@ -952,7 +1025,7 @@ class LintCode(DryRun):
952
1025
  cmd += " && " + command
953
1026
  return cmd
954
1027
 
955
- def gen(self: Self) -> str:
1028
+ def gen(self) -> str:
956
1029
  if isinstance(args := self.args, str):
957
1030
  args = args.split()
958
1031
  paths = " ".join(map(str, args)) if args else "."
@@ -1041,7 +1114,7 @@ def only_check(
1041
1114
 
1042
1115
  class Sync(DryRun):
1043
1116
  def __init__(
1044
- self: Self, filename: str, extras: str, save: bool, dry: bool = False
1117
+ self, filename: str, extras: str, save: bool, dry: bool = False
1045
1118
  ) -> None:
1046
1119
  self.filename = filename
1047
1120
  self.extras = extras
@@ -1192,7 +1265,7 @@ def runserver(
1192
1265
 
1193
1266
  @cli.command(name="exec")
1194
1267
  def run_by_subprocess(cmd: str, dry: bool = DryOption) -> None:
1195
- """Run cmd by subprocess, auto set shell=True when cmd contains '|'"""
1268
+ """Run cmd by subprocess, auto set shell=True when cmd contains '|>'"""
1196
1269
  try:
1197
1270
  rc = run_and_echo(cmd, verbose=True, dry=_ensure_bool(dry))
1198
1271
  except FileNotFoundError as e:
@@ -1205,6 +1278,26 @@ def run_by_subprocess(cmd: str, dry: bool = DryOption) -> None:
1205
1278
  raise Exit(rc)
1206
1279
 
1207
1280
 
1281
+ def version_callback(value: bool) -> None:
1282
+ if value:
1283
+ echo("Fast Dev Cli Version: " + typer.style(__version__, bold=True))
1284
+ raise Exit()
1285
+
1286
+
1287
+ @cli.callback()
1288
+ def common(
1289
+ version: bool = Option(
1290
+ None,
1291
+ "--version",
1292
+ "-V",
1293
+ callback=version_callback,
1294
+ is_eager=True,
1295
+ help="Show the version of this tool",
1296
+ ),
1297
+ ) -> None:
1298
+ pass
1299
+
1300
+
1208
1301
  def main() -> None:
1209
1302
  cli()
1210
1303
 
@@ -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.1"
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 == out_help
@@ -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)
@@ -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 == "Fast Dev Cli Version: 0.17.0"
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 "Fast Dev Cli Version: 0.17.0" in out
78
+ assert "fast_dev_cli/__init__.py: 0.17.0" 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