fast-dev-cli 0.14.2__tar.gz → 0.15.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/PKG-INFO +1 -1
  2. fast_dev_cli-0.15.0/fast_dev_cli/__init__.py +1 -0
  3. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/fast_dev_cli/cli.py +130 -53
  4. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/pyproject.toml +2 -1
  5. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/test_bump.py +30 -0
  6. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/test_functions.py +4 -2
  7. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/test_lint.py +53 -5
  8. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/test_upgrade.py +36 -0
  9. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/utils.py +12 -15
  10. fast_dev_cli-0.14.2/fast_dev_cli/__init__.py +0 -1
  11. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/LICENSE +0 -0
  12. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/README.md +0 -0
  13. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/fast_dev_cli/__main__.py +0 -0
  14. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/fast_dev_cli/py.typed +0 -0
  15. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/pdm_build.py +0 -0
  16. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/scripts/check.py +0 -0
  17. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/scripts/format.py +0 -0
  18. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/scripts/test.py +0 -0
  19. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/__init__.py +0 -0
  20. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/conftest.py +0 -0
  21. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/test_fast_test.py +0 -0
  22. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/test_poetry_version_plugin.py +0 -0
  23. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/test_runserver.py +0 -0
  24. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/test_sync.py +0 -0
  25. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/test_tag.py +0 -0
  26. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/test_upload.py +0 -0
  27. {fast_dev_cli-0.14.2 → fast_dev_cli-0.15.0}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fast-dev-cli
3
- Version: 0.14.2
3
+ Version: 0.15.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.15.0"
@@ -8,7 +8,14 @@ import subprocess # nosec:B404
8
8
  import sys
9
9
  from functools import cached_property
10
10
  from pathlib import Path
11
- from typing import Literal, Optional, get_args # Optional is required by typers
11
+ from typing import (
12
+ Any,
13
+ Literal,
14
+ # Optional is required by typers
15
+ Optional,
16
+ cast,
17
+ get_args,
18
+ )
12
19
 
13
20
  import emoji
14
21
  import typer
@@ -41,6 +48,9 @@ cli = typer.Typer()
41
48
  DryOption = Option(False, "--dry", help="Only print, not really run shell command")
42
49
  TOML_FILE = "pyproject.toml"
43
50
  ToolName = Literal["poetry", "pdm", "uv"]
51
+ ToolOption = Option(
52
+ "auto", "--tool", help="Explicit declare manage tool (default to auto detect)"
53
+ )
44
54
 
45
55
 
46
56
  class ShellCommandError(Exception): ...
@@ -53,7 +63,7 @@ def poetry_module_name(name: str) -> str:
53
63
  return canonicalize_name(name).replace("-", "_").replace(" ", "_")
54
64
 
55
65
 
56
- def load_bool(name: str, default=False) -> bool:
66
+ def load_bool(name: str, default: bool = False) -> bool:
57
67
  if not (v := os.getenv(name)):
58
68
  return default
59
69
  if (lower := v.lower()) in ("0", "false", "f", "off", "no", "n"):
@@ -71,13 +81,15 @@ def is_venv() -> bool:
71
81
  )
72
82
 
73
83
 
74
- def _run_shell(cmd: list[str] | str, **kw) -> subprocess.CompletedProcess:
84
+ def _run_shell(cmd: list[str] | str, **kw: Any) -> subprocess.CompletedProcess[str]:
75
85
  if isinstance(cmd, str):
76
86
  kw.setdefault("shell", True)
77
87
  return subprocess.run(cmd, **kw) # nosec:B603
78
88
 
79
89
 
80
- def run_and_echo(cmd: str, *, dry=False, verbose=True, **kw) -> int:
90
+ def run_and_echo(
91
+ cmd: str, *, dry: bool = False, verbose: bool = True, **kw: Any
92
+ ) -> int:
81
93
  """Run shell command with subprocess and print it"""
82
94
  if verbose:
83
95
  echo(f"--> {cmd}")
@@ -91,7 +103,9 @@ def check_call(cmd: str) -> bool:
91
103
  return r.returncode == 0
92
104
 
93
105
 
94
- def capture_cmd_output(command: list[str] | str, *, raises=False, **kw) -> str:
106
+ def capture_cmd_output(
107
+ command: list[str] | str, *, raises: bool = False, **kw: Any
108
+ ) -> str:
95
109
  if isinstance(command, str) and not kw.get("shell"):
96
110
  command = shlex.split(command)
97
111
  r = _run_shell(command, capture_output=True, encoding="utf-8", **kw)
@@ -100,12 +114,12 @@ def capture_cmd_output(command: list[str] | str, *, raises=False, **kw) -> str:
100
114
  return r.stdout.strip() or r.stderr
101
115
 
102
116
 
103
- def _parse_version(line: str, pattern: re.Pattern) -> str:
117
+ def _parse_version(line: str, pattern: re.Pattern[str]) -> str:
104
118
  return pattern.sub("", line).split("#")[0].strip(" '\"")
105
119
 
106
120
 
107
121
  def read_version_from_file(
108
- package_name: str, work_dir=None, toml_text: str | None = None
122
+ package_name: str, work_dir: Path | None = None, toml_text: str | None = None
109
123
  ) -> str:
110
124
  if toml_text is None:
111
125
  toml_text = Project.load_toml_text()
@@ -136,7 +150,9 @@ def read_version_from_file(
136
150
 
137
151
 
138
152
  def get_current_version(
139
- verbose=False, is_poetry: bool | None = None, package_name: str | None = None
153
+ verbose: bool = False,
154
+ is_poetry: bool | None = None,
155
+ package_name: str | None = None,
140
156
  ) -> str:
141
157
  if is_poetry is None:
142
158
  is_poetry = Project.manage_by_poetry()
@@ -164,9 +180,19 @@ def _ensure_bool(value: bool | OptionInfo) -> bool:
164
180
  return value
165
181
 
166
182
 
183
+ def _ensure_str(value: str | OptionInfo) -> str:
184
+ if not isinstance(value, str):
185
+ value = getattr(value, "default", "")
186
+ return value
187
+
188
+
167
189
  def exit_if_run_failed(
168
- cmd: str, env=None, _exit=False, dry=False, **kw
169
- ) -> subprocess.CompletedProcess:
190
+ cmd: str,
191
+ env: dict[str, str] | None = None,
192
+ _exit: bool = False,
193
+ dry: bool = False,
194
+ **kw: Any,
195
+ ) -> subprocess.CompletedProcess[str]:
170
196
  run_and_echo(cmd, dry=True)
171
197
  if _ensure_bool(dry):
172
198
  return subprocess.CompletedProcess("", 0)
@@ -181,7 +207,7 @@ def exit_if_run_failed(
181
207
 
182
208
 
183
209
  class DryRun:
184
- def __init__(self: Self, _exit=False, dry=False) -> None:
210
+ def __init__(self: Self, _exit: bool = False, dry: bool = False) -> None:
185
211
  self.dry = dry
186
212
  self._exit = _exit
187
213
 
@@ -199,7 +225,11 @@ class BumpUp(DryRun):
199
225
  major = "major"
200
226
 
201
227
  def __init__(
202
- self: Self, commit: bool, part: str, filename: str | None = None, dry=False
228
+ self: Self,
229
+ commit: bool,
230
+ part: str,
231
+ filename: str | None = None,
232
+ dry: bool = False,
203
233
  ) -> None:
204
234
  self.commit = commit
205
235
  self.part = part
@@ -209,9 +239,9 @@ class BumpUp(DryRun):
209
239
  super().__init__(dry=dry)
210
240
 
211
241
  @staticmethod
212
- def get_last_commit_message() -> str:
242
+ def get_last_commit_message(raises: bool = False) -> str:
213
243
  cmd = 'git show --pretty=format:"%s" -s HEAD'
214
- return capture_cmd_output(cmd)
244
+ return capture_cmd_output(cmd, raises=raises)
215
245
 
216
246
  @classmethod
217
247
  def should_add_emoji(cls) -> bool:
@@ -219,9 +249,12 @@ class BumpUp(DryRun):
219
249
  If last commit message is startswith emoji,
220
250
  add a ⬆️ flag at the prefix of bump up commit message.
221
251
  """
222
- if out := cls.get_last_commit_message():
223
- return emoji.is_emoji(out[0])
224
- return False
252
+ try:
253
+ first_char = cls.get_last_commit_message(raises=True)[0]
254
+ except (IndexError, ShellCommandError):
255
+ return False
256
+ else:
257
+ return emoji.is_emoji(first_char)
225
258
 
226
259
  @staticmethod
227
260
  def parse_filename() -> str:
@@ -379,7 +412,9 @@ class Project:
379
412
  return 'build-backend = "poetry' in text
380
413
 
381
414
  @staticmethod
382
- def work_dir(name: str, parent: Path, depth: int, be_file=False) -> Path | None:
415
+ def work_dir(
416
+ name: str, parent: Path, depth: int, be_file: bool = False
417
+ ) -> Path | None:
383
418
  for _ in range(depth):
384
419
  if (f := parent.joinpath(name)).exists():
385
420
  if be_file:
@@ -391,10 +426,10 @@ class Project:
391
426
  @classmethod
392
427
  def get_work_dir(
393
428
  cls: type[Self],
394
- name=TOML_FILE,
429
+ name: str = TOML_FILE,
395
430
  cwd: Path | None = None,
396
- allow_cwd=False,
397
- be_file=False,
431
+ allow_cwd: bool = False,
432
+ be_file: bool = False,
398
433
  ) -> Path:
399
434
  cwd = cwd or Path.cwd()
400
435
  if d := cls.work_dir(name, cwd, cls.path_depth, be_file):
@@ -404,7 +439,7 @@ class Project:
404
439
  raise EnvError(f"{name} not found! Make sure this is a poetry project.")
405
440
 
406
441
  @classmethod
407
- def load_toml_text(cls: type[Self], name=TOML_FILE) -> str:
442
+ def load_toml_text(cls: type[Self], name: str = TOML_FILE) -> str:
408
443
  toml_file = cls.get_work_dir(name, be_file=True)
409
444
  return toml_file.read_text("utf8")
410
445
 
@@ -421,7 +456,7 @@ class Project:
421
456
  else:
422
457
  for name in get_args(ToolName):
423
458
  if f"[tool.{name}]" in text:
424
- return name
459
+ return cast(ToolName, name)
425
460
  # Poetry 2.0 default to not include the '[tool.poetry]' section
426
461
  if cls.is_poetry_v2(text):
427
462
  return "poetry"
@@ -447,6 +482,12 @@ class ParseError(Exception):
447
482
 
448
483
 
449
484
  class UpgradeDependencies(Project, DryRun):
485
+ def __init__(
486
+ self: Self, _exit: bool = False, dry: bool = False, tool: ToolName = "poetry"
487
+ ) -> None:
488
+ super().__init__(_exit, dry)
489
+ self._tool = tool
490
+
450
491
  class DevFlag(StrEnum):
451
492
  new = "[tool.poetry.group.dev.dependencies]"
452
493
  old = "[tool.poetry.dev-dependencies]"
@@ -538,7 +579,7 @@ class UpgradeDependencies(Project, DryRun):
538
579
  return cls.DevFlag.new in text or cls.DevFlag.old in text
539
580
 
540
581
  @staticmethod
541
- def parse_item(toml_str) -> list[str]:
582
+ def parse_item(toml_str: str) -> list[str]:
542
583
  lines: list[str] = []
543
584
  for line in toml_str.splitlines():
544
585
  if (line := line.strip()).startswith("["):
@@ -608,20 +649,26 @@ class UpgradeDependencies(Project, DryRun):
608
649
  return _upgrade
609
650
 
610
651
  def gen(self: Self) -> str:
652
+ if self._tool == "uv":
653
+ return "uv lock --upgrade --verbose && uv sync --frozen"
654
+ elif self._tool == "pdm":
655
+ return "pdm update --verbose && pdm install"
611
656
  return self.gen_cmd() + " && poetry lock && poetry update"
612
657
 
613
658
 
614
659
  @cli.command()
615
660
  def upgrade(
661
+ tool: str = ToolOption,
616
662
  dry: bool = DryOption,
617
663
  ) -> None:
618
664
  """Upgrade dependencies in pyproject.toml to latest versions"""
619
- if (tool := Project.get_manage_tool()) == "uv":
620
- exit_if_run_failed("uv lock --upgrade && uv sync", dry=dry)
621
- elif tool == "pdm":
622
- exit_if_run_failed("pdm update && pdm install", dry=dry)
665
+ if not (tool := _ensure_str(tool)) or tool == ToolOption.default:
666
+ tool = Project.get_manage_tool() or "uv"
667
+ if tool in get_args(ToolName):
668
+ UpgradeDependencies(dry=dry, tool=cast(ToolName, tool)).run()
623
669
  else:
624
- UpgradeDependencies(dry=dry).run()
670
+ secho(f"Unknown tool {tool!r}", fg=typer.colors.YELLOW)
671
+ raise typer.Exit(1)
625
672
 
626
673
 
627
674
  class GitTag(DryRun):
@@ -676,19 +723,21 @@ def tag(
676
723
  class LintCode(DryRun):
677
724
  def __init__(
678
725
  self: Self,
679
- args,
680
- check_only=False,
681
- _exit=False,
682
- dry=False,
683
- bandit=False,
684
- skip_mypy=False,
685
- dmypy=False,
726
+ args: list[str] | str | None,
727
+ check_only: bool = False,
728
+ _exit: bool = False,
729
+ dry: bool = False,
730
+ bandit: bool = False,
731
+ skip_mypy: bool = False,
732
+ dmypy: bool = False,
733
+ tool: str = ToolOption.default,
686
734
  ) -> None:
687
735
  self.args = args
688
736
  self.check_only = check_only
689
737
  self._bandit = bandit
690
738
  self._skip_mypy = skip_mypy
691
739
  self._use_dmypy = dmypy
740
+ self._tool = tool
692
741
  super().__init__(_exit, dry)
693
742
 
694
743
  @staticmethod
@@ -696,7 +745,7 @@ class LintCode(DryRun):
696
745
  return check_call("ruff --version")
697
746
 
698
747
  @staticmethod
699
- def prefer_dmypy(paths: str, tools: list[str], use_dmypy=False) -> bool:
748
+ def prefer_dmypy(paths: str, tools: list[str], use_dmypy: bool = False) -> bool:
700
749
  return (
701
750
  paths == "."
702
751
  and any(t.startswith("mypy") for t in tools)
@@ -706,7 +755,8 @@ class LintCode(DryRun):
706
755
  @staticmethod
707
756
  def get_package_name() -> str:
708
757
  root = Project.get_work_dir(allow_cwd=True)
709
- package_maybe = (root.name.replace("-", "_"), "src")
758
+ module_name = root.name.replace("-", "_").replace(" ", "_")
759
+ package_maybe = (module_name, "src")
710
760
  for name in package_maybe:
711
761
  if root.joinpath(name).is_dir():
712
762
  return name
@@ -720,6 +770,7 @@ class LintCode(DryRun):
720
770
  bandit: bool = False,
721
771
  skip_mypy: bool = False,
722
772
  use_dmypy: bool = False,
773
+ tool: str = ToolOption.default,
723
774
  ) -> str:
724
775
  if paths != "." and all(i.endswith(".html") for i in paths.split()):
725
776
  return f"prettier -w {paths}"
@@ -748,8 +799,11 @@ class LintCode(DryRun):
748
799
  secho(f"{tip}\n\n {command}\n", fg="yellow")
749
800
  else:
750
801
  should_run_by_tool = True
751
- if should_run_by_tool and (manage_tool := Project.get_manage_tool()):
752
- prefix = manage_tool + " run "
802
+ if should_run_by_tool and tool:
803
+ if tool == ToolOption.default:
804
+ tool = Project.get_manage_tool() or ""
805
+ if tool:
806
+ prefix = tool + " run "
753
807
  if cls.prefer_dmypy(paths, tools, use_dmypy=use_dmypy):
754
808
  tools[-1] = "dmypy run"
755
809
  cmd += lint_them.format(prefix, paths, *tools)
@@ -766,7 +820,9 @@ class LintCode(DryRun):
766
820
  return cmd
767
821
 
768
822
  def gen(self: Self) -> str:
769
- paths = " ".join(map(str, self.args)) if self.args else "."
823
+ if isinstance(args := self.args, str):
824
+ args = args.split()
825
+ paths = " ".join(map(str, args)) if args else "."
770
826
  return self.to_cmd(
771
827
  paths, self.check_only, self._bandit, self._skip_mypy, self._use_dmypy
772
828
  )
@@ -776,15 +832,31 @@ def parse_files(args: list[str] | tuple[str, ...]) -> list[str]:
776
832
  return [i for i in args if not i.startswith("-")]
777
833
 
778
834
 
779
- def lint(files=None, dry=False, bandit=False, skip_mypy=False, dmypy=False) -> None:
835
+ def lint(
836
+ files: list[str] | str | None = None,
837
+ dry: bool = False,
838
+ bandit: bool = False,
839
+ skip_mypy: bool = False,
840
+ dmypy: bool = False,
841
+ tool: str = ToolOption.default,
842
+ ) -> None:
780
843
  if files is None:
781
844
  files = parse_files(sys.argv[1:])
782
845
  if files and files[0] == "lint":
783
846
  files = files[1:]
784
- LintCode(files, dry=dry, skip_mypy=skip_mypy, bandit=bandit, dmypy=dmypy).run()
847
+ LintCode(
848
+ files, dry=dry, skip_mypy=skip_mypy, bandit=bandit, dmypy=dmypy, tool=tool
849
+ ).run()
785
850
 
786
851
 
787
- def check(files=None, dry=False, bandit=False, skip_mypy=False, dmypy=False) -> None:
852
+ def check(
853
+ files: list[str] | str | None = None,
854
+ dry: bool = False,
855
+ bandit: bool = False,
856
+ skip_mypy: bool = False,
857
+ dmypy: bool = False,
858
+ tool: str = ToolOption.default,
859
+ ) -> None:
788
860
  LintCode(
789
861
  files,
790
862
  check_only=True,
@@ -793,32 +865,35 @@ def check(files=None, dry=False, bandit=False, skip_mypy=False, dmypy=False) ->
793
865
  bandit=bandit,
794
866
  skip_mypy=skip_mypy,
795
867
  dmypy=dmypy,
868
+ tool=tool,
796
869
  ).run()
797
870
 
798
871
 
799
872
  @cli.command(name="lint")
800
873
  def make_style(
801
- files: Optional[list[Path]] = typer.Argument(default=None), # noqa:B008
874
+ files: Optional[list[str]] = typer.Argument(default=None), # noqa:B008
802
875
  check_only: bool = Option(False, "--check-only", "-c"),
803
876
  bandit: bool = Option(False, "--bandit", help="Run `bandit -r <package_dir>`"),
804
877
  skip_mypy: bool = Option(False, "--skip-mypy"),
805
878
  use_dmypy: bool = Option(
806
879
  False, "--dmypy", help="Use `dmypy run` instead of `mypy`"
807
880
  ),
881
+ tool: str = ToolOption,
808
882
  dry: bool = DryOption,
809
883
  ) -> None:
810
884
  """Run: ruff check/format to reformat code and then mypy to check"""
811
885
  if getattr(files, "default", files) is None:
812
- files = [Path(".")]
886
+ files = ["."]
813
887
  elif isinstance(files, str):
814
888
  files = [files]
815
889
  skip = _ensure_bool(skip_mypy)
816
890
  dmypy = _ensure_bool(use_dmypy)
817
891
  bandit = _ensure_bool(bandit)
892
+ tool = _ensure_str(tool)
818
893
  if _ensure_bool(check_only):
819
- check(files, dry=dry, skip_mypy=skip, dmypy=dmypy, bandit=bandit)
894
+ check(files, dry=dry, skip_mypy=skip, dmypy=dmypy, bandit=bandit, tool=tool)
820
895
  else:
821
- lint(files, dry=dry, skip_mypy=skip, dmypy=dmypy, bandit=bandit)
896
+ lint(files, dry=dry, skip_mypy=skip, dmypy=dmypy, bandit=bandit, tool=tool)
822
897
 
823
898
 
824
899
  @cli.command(name="check")
@@ -832,7 +907,9 @@ def only_check(
832
907
 
833
908
 
834
909
  class Sync(DryRun):
835
- def __init__(self: Self, filename: str, extras: str, save: bool, dry=False) -> None:
910
+ def __init__(
911
+ self: Self, filename: str, extras: str, save: bool, dry: bool = False
912
+ ) -> None:
836
913
  self.filename = filename
837
914
  self.extras = extras
838
915
  self._save = save
@@ -871,7 +948,7 @@ class Sync(DryRun):
871
948
 
872
949
  @cli.command()
873
950
  def sync(
874
- filename="dev_requirements.txt",
951
+ filename: str = "dev_requirements.txt",
875
952
  extras: str = Option("", "--extras", "-E"),
876
953
  save: bool = Option(
877
954
  False, "--save", "-s", help="Whether save the requirement file"
@@ -889,7 +966,7 @@ def _should_run_test_script(path: Path = Path("scripts")) -> Path | None:
889
966
  return None
890
967
 
891
968
 
892
- def test(dry: bool, ignore_script=False) -> None:
969
+ def test(dry: bool, ignore_script: bool = False) -> None:
893
970
  cwd = Path.cwd()
894
971
  root = Project.get_work_dir(cwd=cwd, allow_cwd=True)
895
972
  script_dir = root / "scripts"
@@ -944,13 +1021,13 @@ def dev(
944
1021
  port: int | None | OptionInfo,
945
1022
  host: str | None | OptionInfo,
946
1023
  file: str | None | ArgumentInfo = None,
947
- dry=False,
1024
+ dry: bool = False,
948
1025
  ) -> None:
949
1026
  cmd = "fastapi dev"
950
1027
  no_port_yet = True
951
1028
  if file is not None:
952
1029
  try:
953
- port = int(str(file)) # type:ignore[arg-type]
1030
+ port = int(str(file))
954
1031
  except ValueError:
955
1032
  cmd += f" {file}"
956
1033
  else:
@@ -41,7 +41,7 @@ dependencies = [
41
41
  "pytest >=8.2.0,<9",
42
42
  "packaging>=20.5",
43
43
  ]
44
- version = "0.14.2"
44
+ version = "0.15.0"
45
45
 
46
46
  [project.urls]
47
47
  Homepage = "https://github.com/waketzheng/fast-dev-cli"
@@ -124,6 +124,7 @@ pretty = true
124
124
  python_version = "3.9"
125
125
  ignore_missing_imports = true
126
126
  check_untyped_defs = true
127
+ warn_unused_ignores = true
127
128
  exclude = [
128
129
  "^fabfile\\.py$",
129
130
  "two\\.pyi$",
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import shutil
2
3
  import subprocess
3
4
  from contextlib import redirect_stdout
4
5
  from io import StringIO
@@ -200,3 +201,32 @@ def test_bump_with_uv(tmp_path):
200
201
  Path(TOML_FILE).write_text("[project]" + os.linesep + 'version = "0.1.0"')
201
202
  command = BumpUp(part="patch", commit=True).gen()
202
203
  assert TOML_FILE in command
204
+
205
+
206
+ def test_parse_filename(tmp_path):
207
+ pyproject = """
208
+ [tool.poetry]
209
+ version = "0"
210
+ """
211
+ project_dir = tmp_path / "helloworld"
212
+ project_dir.mkdir()
213
+ with chdir(project_dir):
214
+ toml_file = project_dir.joinpath(TOML_FILE)
215
+ toml_file.write_text(pyproject)
216
+ src_dir = project_dir / project_dir.name
217
+ src_dir.mkdir()
218
+ init_file = src_dir / "__init__.py"
219
+ init_file.write_text('__version__ = "0.1.0"')
220
+ another_dir = project_dir / "hello"
221
+ another_dir.mkdir()
222
+ shutil.copy(init_file, another_dir / init_file.name)
223
+ assert BumpUp.parse_filename() == "helloworld/__init__.py"
224
+ toml_file.write_text(pyproject.strip() + '\npackages=[{include="hello"}]')
225
+ assert BumpUp.parse_filename() == "hello/__init__.py"
226
+ toml_file.write_text(
227
+ pyproject.strip() + '\npackages=[{include="hello",from="py"}]'
228
+ )
229
+ from_dir = project_dir / "py"
230
+ from_dir.mkdir()
231
+ shutil.move(another_dir, from_dir)
232
+ assert BumpUp.parse_filename() == "py/hello/__init__.py"
@@ -72,8 +72,10 @@ def test_run_shell():
72
72
  value = "foo"
73
73
  cmd = 'python -c "import os;print(list(os.environ))"'
74
74
  with redirect_stdout(StringIO()):
75
- r = exit_if_run_failed(cmd, env={name: value}, capture_output=True)
76
- assert name in r.stdout.decode()
75
+ r = exit_if_run_failed(
76
+ cmd, env={name: value}, capture_output=True, encoding="utf-8"
77
+ )
78
+ assert name in r.stdout
77
79
 
78
80
  assert run_and_echo("echo foo", capture_output=True) == 0
79
81
 
@@ -82,11 +82,21 @@ def test_check_bandit(tmp_path):
82
82
  src_dir = package_path / "src"
83
83
  if not src_dir.exists(): # For poetry<2.1
84
84
  src_dir = src_dir.parent / package_path.name
85
+ with chdir(package_path):
86
+ package_name = src_dir.name
87
+ assert f"bandit -r {package_name}" in LintCode.to_cmd(bandit=True)
88
+ toml_file = Path(TOML_FILE)
89
+ content = toml_file.read_text()
90
+ toml_file.write_text(content + '\n[tool.bandit]\nexclude_dirs = ["tests"]')
91
+ assert f"bandit -c {TOML_FILE} -r ." in LintCode.to_cmd(bandit=True)
85
92
  shutil.rmtree(src_dir)
86
93
  with chdir(package_path):
87
94
  assert LintCode.get_package_name() == "."
88
95
  command = capture_cmd_output("fast check --bandit --dry")
89
- assert "bandit -r ." in command
96
+ assert f"bandit -c {TOML_FILE} -r ." in command
97
+ toml_file.write_text(content)
98
+ command = capture_cmd_output("fast check --bandit --dry")
99
+ assert "bandit -r ." in command
90
100
 
91
101
 
92
102
  def test_check_skip_mypy(mock_skip_mypy_0, mocker, capsys):
@@ -140,6 +150,14 @@ def test_lint_html():
140
150
  assert "prettier -w index.html" in command
141
151
  command = capture_cmd_output(f"{lint_cmd} index.html flv.html --dry")
142
152
  assert "prettier -w index.html flv.html" in command
153
+ cmd = "fast lint index.html --dry"
154
+ assert "prettier -w index.html" in capture_cmd_output(cmd)
155
+ assert "prettier -w index.html" in capture_cmd_output("pdm run " + cmd)
156
+ cmd = "fast lint index.html flv.html --dry"
157
+ assert "prettier -w index.html flv.html" in capture_cmd_output(cmd)
158
+ assert "prettier -w index.html flv.html" in capture_cmd_output("pdm run " + cmd)
159
+ assert LintCode.to_cmd("index.html") == "prettier -w index.html"
160
+ assert LintCode.to_cmd("index.html flv.html") == "prettier -w index.html flv.html"
143
161
 
144
162
 
145
163
  def test_lint_by_global_fast():
@@ -151,20 +169,29 @@ def test_lint_by_global_fast():
151
169
 
152
170
 
153
171
  def test_with_dmypy():
154
- command = capture_cmd_output("fast lint --dmypy --dry .")
172
+ cmd = "fast lint --dmypy --dry ."
173
+ assert "dmypy run ." in capture_cmd_output(cmd)
174
+ assert "dmypy run ." in capture_cmd_output("pdm run " + cmd)
175
+ command = LintCode.to_cmd(use_dmypy=True, tool="pdm")
155
176
  assert "dmypy run ." in command
177
+ command = LintCode.to_cmd(use_dmypy=False, tool="pdm")
178
+ assert "dmypy run ." not in command
156
179
 
157
180
 
158
181
  def test_dmypy_run(monkeypatch):
182
+ command = capture_cmd_output("python -m fast_dev_cli lint --dry .")
183
+ assert "dmypy run ." not in command
159
184
  monkeypatch.setenv("FASTDEVCLI_DMYPY", "1")
160
185
  command = capture_cmd_output("python -m fast_dev_cli lint --dry .")
161
186
  assert "dmypy run ." in command
187
+ command = capture_cmd_output("python -m fast_dev_cli lint --skip-mypy --dry .")
188
+ assert "dmypy run ." not in command
162
189
 
163
190
 
164
191
  def test_lint_with_prefix(mocker):
165
192
  mocker.patch("fast_dev_cli.cli.is_venv", return_value=False)
166
193
  with capture_stdout() as stream:
167
- make_style([Path(".")], check_only=False, dry=True)
194
+ make_style(["."], check_only=False, dry=True)
168
195
  assert "pdm run" in stream.getvalue()
169
196
 
170
197
 
@@ -174,13 +201,13 @@ def test_make_style(mock_skip_mypy_0, mocker, mock_no_dmypy):
174
201
  make_style(check_only=False, dry=True)
175
202
  assert LINT_CMD in stream.getvalue()
176
203
  with capture_stdout() as stream:
177
- make_style([Path(".")], check_only=False, dry=True)
204
+ make_style(["."], check_only=False, dry=True)
178
205
  assert LINT_CMD in stream.getvalue()
179
206
  with capture_stdout() as stream:
180
207
  make_style(".", check_only=False, dry=True) # type:ignore[arg-type]
181
208
  assert LINT_CMD in stream.getvalue()
182
209
  with capture_stdout() as stream:
183
- make_style([Path(".")], check_only=True, dry=True)
210
+ make_style(["."], check_only=True, dry=True)
184
211
  assert CHECK_CMD in stream.getvalue()
185
212
  with capture_stdout() as stream:
186
213
  only_check(dry=True)
@@ -297,3 +324,24 @@ def test_get_manage_tool(tmp_path):
297
324
  assert Project.get_manage_tool() == "pdm"
298
325
  Path(TOML_FILE).write_text("[tool.uv]")
299
326
  assert Project.get_manage_tool() == "uv"
327
+
328
+
329
+ class TestGetPackageName:
330
+ project = "hello-world"
331
+
332
+ def test_get_package_name(self, tmp_path):
333
+ project_dir = tmp_path / self.project
334
+ project_dir.mkdir()
335
+ module_name = project_dir.name.replace("-", "_").replace(" ", "_")
336
+ with chdir(project_dir):
337
+ Path(TOML_FILE).touch()
338
+ Path(module_name).mkdir()
339
+ assert LintCode.get_package_name() == module_name
340
+ Path("src").mkdir()
341
+ assert LintCode.get_package_name() == module_name
342
+ shutil.rmtree(module_name)
343
+ assert LintCode.get_package_name() == "src"
344
+
345
+
346
+ class TestGetPackageNameWithSpace(TestGetPackageName):
347
+ project = "hello world"
@@ -6,9 +6,14 @@ from contextlib import redirect_stdout
6
6
  from io import StringIO
7
7
  from pathlib import Path
8
8
 
9
+ import pytest
10
+ import typer
11
+
12
+ import fast_dev_cli
9
13
  from fast_dev_cli.cli import (
10
14
  TOML_FILE,
11
15
  UpgradeDependencies,
16
+ capture_cmd_output,
12
17
  run_and_echo,
13
18
  upgrade,
14
19
  )
@@ -283,3 +288,34 @@ fastapi = "^0.112.2"
283
288
  [],
284
289
  "--dev",
285
290
  )
291
+
292
+
293
+ def test_upgrade_uv_project():
294
+ cmd = "fast upgrade --tool=uv --dry"
295
+ expected = "uv lock --upgrade --verbose && uv sync --frozen"
296
+ assert expected in capture_cmd_output(cmd)
297
+ assert expected in capture_cmd_output("pdm run " + cmd)
298
+ assert UpgradeDependencies(tool="uv").gen() == expected
299
+
300
+
301
+ def test_upgrade_pdm_project():
302
+ cmd = "fast upgrade --tool=pdm --dry"
303
+ expected = "pdm update --verbose && pdm install"
304
+ assert expected in capture_cmd_output(cmd)
305
+ assert expected in capture_cmd_output("pdm run " + cmd)
306
+ assert UpgradeDependencies(tool="pdm").gen() == expected
307
+
308
+
309
+ def test_upgrade_unknown_tool(mocker):
310
+ cmd = "fast upgrade --tool=hatch --dry"
311
+ expected = "Unknown tool 'hatch'"
312
+ assert expected in capture_cmd_output(cmd)
313
+ assert expected in capture_cmd_output("pdm run " + cmd)
314
+ assert run_and_echo(cmd, verbose=False) == 1
315
+ assert run_and_echo("pdm run " + cmd, verbose=False) == 1
316
+ mocker.patch("fast_dev_cli.cli.secho")
317
+ with pytest.raises(typer.Exit):
318
+ upgrade(tool="pipenv")
319
+ fast_dev_cli.cli.secho.assert_called_once_with( # type:ignore
320
+ "Unknown tool 'pipenv'", fg=typer.colors.YELLOW
321
+ )
@@ -1,28 +1,25 @@
1
1
  import os
2
2
  import subprocess
3
3
  import sys
4
- from contextlib import contextmanager, redirect_stdout
4
+ from contextlib import AbstractContextManager, contextmanager, redirect_stdout
5
5
  from io import StringIO
6
6
  from pathlib import Path
7
7
 
8
- if sys.version_info >= (3, 11):
9
- from contextlib import chdir # type:ignore[attr-defined]
10
- else:
11
- from contextlib import AbstractContextManager
12
8
 
13
- class chdir(AbstractContextManager): # type:ignore[no-redef]
14
- """Non thread-safe context manager to change the current working directory."""
9
+ # TODO: use `from contextlib import chdir` instead when drop support for Python3.10
10
+ class chdir(AbstractContextManager): # Copied from source code of Python3.13
11
+ """Non thread-safe context manager to change the current working directory."""
15
12
 
16
- def __init__(self, path):
17
- self.path = path
18
- self._old_cwd = []
13
+ def __init__(self, path):
14
+ self.path = path
15
+ self._old_cwd = []
19
16
 
20
- def __enter__(self):
21
- self._old_cwd.append(os.getcwd())
22
- os.chdir(self.path)
17
+ def __enter__(self):
18
+ self._old_cwd.append(os.getcwd())
19
+ os.chdir(self.path)
23
20
 
24
- def __exit__(self, *excinfo):
25
- os.chdir(self._old_cwd.pop())
21
+ def __exit__(self, *excinfo):
22
+ os.chdir(self._old_cwd.pop())
26
23
 
27
24
 
28
25
  __all__ = (
@@ -1 +0,0 @@
1
- __version__ = "0.14.2"
File without changes
File without changes