fast-dev-cli 0.14.1__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.1 → 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.1 → fast_dev_cli-0.15.0}/fast_dev_cli/cli.py +142 -62
  4. {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/pyproject.toml +2 -1
  5. {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/test_bump.py +79 -36
  6. {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/test_functions.py +16 -8
  7. {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/test_lint.py +64 -6
  8. {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/test_upgrade.py +36 -0
  9. {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/utils.py +22 -15
  10. fast_dev_cli-0.14.1/fast_dev_cli/__init__.py +0 -1
  11. {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/LICENSE +0 -0
  12. {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/README.md +0 -0
  13. {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/fast_dev_cli/__main__.py +0 -0
  14. {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/fast_dev_cli/py.typed +0 -0
  15. {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/pdm_build.py +0 -0
  16. {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/scripts/check.py +0 -0
  17. {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/scripts/format.py +0 -0
  18. {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/scripts/test.py +0 -0
  19. {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/__init__.py +0 -0
  20. {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/conftest.py +0 -0
  21. {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/test_fast_test.py +0 -0
  22. {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/test_poetry_version_plugin.py +0 -0
  23. {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/test_runserver.py +0 -0
  24. {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/test_sync.py +0 -0
  25. {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/test_tag.py +0 -0
  26. {fast_dev_cli-0.14.1 → fast_dev_cli-0.15.0}/tests/test_upload.py +0 -0
  27. {fast_dev_cli-0.14.1 → 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.1
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
@@ -38,8 +45,12 @@ else: # pragma: no cover
38
45
 
39
46
 
40
47
  cli = typer.Typer()
48
+ DryOption = Option(False, "--dry", help="Only print, not really run shell command")
41
49
  TOML_FILE = "pyproject.toml"
42
50
  ToolName = Literal["poetry", "pdm", "uv"]
51
+ ToolOption = Option(
52
+ "auto", "--tool", help="Explicit declare manage tool (default to auto detect)"
53
+ )
43
54
 
44
55
 
45
56
  class ShellCommandError(Exception): ...
@@ -52,7 +63,7 @@ def poetry_module_name(name: str) -> str:
52
63
  return canonicalize_name(name).replace("-", "_").replace(" ", "_")
53
64
 
54
65
 
55
- def load_bool(name: str, default=False) -> bool:
66
+ def load_bool(name: str, default: bool = False) -> bool:
56
67
  if not (v := os.getenv(name)):
57
68
  return default
58
69
  if (lower := v.lower()) in ("0", "false", "f", "off", "no", "n"):
@@ -70,13 +81,15 @@ def is_venv() -> bool:
70
81
  )
71
82
 
72
83
 
73
- def _run_shell(cmd: list[str] | str, **kw) -> subprocess.CompletedProcess:
84
+ def _run_shell(cmd: list[str] | str, **kw: Any) -> subprocess.CompletedProcess[str]:
74
85
  if isinstance(cmd, str):
75
86
  kw.setdefault("shell", True)
76
87
  return subprocess.run(cmd, **kw) # nosec:B603
77
88
 
78
89
 
79
- 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:
80
93
  """Run shell command with subprocess and print it"""
81
94
  if verbose:
82
95
  echo(f"--> {cmd}")
@@ -90,7 +103,9 @@ def check_call(cmd: str) -> bool:
90
103
  return r.returncode == 0
91
104
 
92
105
 
93
- 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:
94
109
  if isinstance(command, str) and not kw.get("shell"):
95
110
  command = shlex.split(command)
96
111
  r = _run_shell(command, capture_output=True, encoding="utf-8", **kw)
@@ -99,12 +114,12 @@ def capture_cmd_output(command: list[str] | str, *, raises=False, **kw) -> str:
99
114
  return r.stdout.strip() or r.stderr
100
115
 
101
116
 
102
- def _parse_version(line: str, pattern: re.Pattern) -> str:
117
+ def _parse_version(line: str, pattern: re.Pattern[str]) -> str:
103
118
  return pattern.sub("", line).split("#")[0].strip(" '\"")
104
119
 
105
120
 
106
121
  def read_version_from_file(
107
- 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
108
123
  ) -> str:
109
124
  if toml_text is None:
110
125
  toml_text = Project.load_toml_text()
@@ -135,7 +150,9 @@ def read_version_from_file(
135
150
 
136
151
 
137
152
  def get_current_version(
138
- 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,
139
156
  ) -> str:
140
157
  if is_poetry is None:
141
158
  is_poetry = Project.manage_by_poetry()
@@ -163,9 +180,19 @@ def _ensure_bool(value: bool | OptionInfo) -> bool:
163
180
  return value
164
181
 
165
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
+
166
189
  def exit_if_run_failed(
167
- cmd: str, env=None, _exit=False, dry=False, **kw
168
- ) -> 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]:
169
196
  run_and_echo(cmd, dry=True)
170
197
  if _ensure_bool(dry):
171
198
  return subprocess.CompletedProcess("", 0)
@@ -180,7 +207,7 @@ def exit_if_run_failed(
180
207
 
181
208
 
182
209
  class DryRun:
183
- def __init__(self: Self, _exit=False, dry=False) -> None:
210
+ def __init__(self: Self, _exit: bool = False, dry: bool = False) -> None:
184
211
  self.dry = dry
185
212
  self._exit = _exit
186
213
 
@@ -198,7 +225,11 @@ class BumpUp(DryRun):
198
225
  major = "major"
199
226
 
200
227
  def __init__(
201
- 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,
202
233
  ) -> None:
203
234
  self.commit = commit
204
235
  self.part = part
@@ -208,9 +239,9 @@ class BumpUp(DryRun):
208
239
  super().__init__(dry=dry)
209
240
 
210
241
  @staticmethod
211
- def get_last_commit_message() -> str:
242
+ def get_last_commit_message(raises: bool = False) -> str:
212
243
  cmd = 'git show --pretty=format:"%s" -s HEAD'
213
- return capture_cmd_output(cmd)
244
+ return capture_cmd_output(cmd, raises=raises)
214
245
 
215
246
  @classmethod
216
247
  def should_add_emoji(cls) -> bool:
@@ -218,9 +249,12 @@ class BumpUp(DryRun):
218
249
  If last commit message is startswith emoji,
219
250
  add a ⬆️ flag at the prefix of bump up commit message.
220
251
  """
221
- if out := cls.get_last_commit_message():
222
- return emoji.is_emoji(out[0])
223
- 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)
224
258
 
225
259
  @staticmethod
226
260
  def parse_filename() -> str:
@@ -348,7 +382,7 @@ def bump_version(
348
382
  commit: bool = Option(
349
383
  False, "--commit", "-c", help="Whether run `git commit` after version changed"
350
384
  ),
351
- dry: bool = Option(False, "--dry", help="Only print, not really run shell command"),
385
+ dry: bool = DryOption,
352
386
  ) -> None:
353
387
  """Bump up version string in pyproject.toml"""
354
388
  return BumpUp(_ensure_bool(commit), getattr(part, "value", part), dry=dry).run()
@@ -378,7 +412,9 @@ class Project:
378
412
  return 'build-backend = "poetry' in text
379
413
 
380
414
  @staticmethod
381
- 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:
382
418
  for _ in range(depth):
383
419
  if (f := parent.joinpath(name)).exists():
384
420
  if be_file:
@@ -390,10 +426,10 @@ class Project:
390
426
  @classmethod
391
427
  def get_work_dir(
392
428
  cls: type[Self],
393
- name=TOML_FILE,
429
+ name: str = TOML_FILE,
394
430
  cwd: Path | None = None,
395
- allow_cwd=False,
396
- be_file=False,
431
+ allow_cwd: bool = False,
432
+ be_file: bool = False,
397
433
  ) -> Path:
398
434
  cwd = cwd or Path.cwd()
399
435
  if d := cls.work_dir(name, cwd, cls.path_depth, be_file):
@@ -403,7 +439,7 @@ class Project:
403
439
  raise EnvError(f"{name} not found! Make sure this is a poetry project.")
404
440
 
405
441
  @classmethod
406
- def load_toml_text(cls: type[Self], name=TOML_FILE) -> str:
442
+ def load_toml_text(cls: type[Self], name: str = TOML_FILE) -> str:
407
443
  toml_file = cls.get_work_dir(name, be_file=True)
408
444
  return toml_file.read_text("utf8")
409
445
 
@@ -420,7 +456,7 @@ class Project:
420
456
  else:
421
457
  for name in get_args(ToolName):
422
458
  if f"[tool.{name}]" in text:
423
- return name
459
+ return cast(ToolName, name)
424
460
  # Poetry 2.0 default to not include the '[tool.poetry]' section
425
461
  if cls.is_poetry_v2(text):
426
462
  return "poetry"
@@ -446,6 +482,12 @@ class ParseError(Exception):
446
482
 
447
483
 
448
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
+
449
491
  class DevFlag(StrEnum):
450
492
  new = "[tool.poetry.group.dev.dependencies]"
451
493
  old = "[tool.poetry.dev-dependencies]"
@@ -537,7 +579,7 @@ class UpgradeDependencies(Project, DryRun):
537
579
  return cls.DevFlag.new in text or cls.DevFlag.old in text
538
580
 
539
581
  @staticmethod
540
- def parse_item(toml_str) -> list[str]:
582
+ def parse_item(toml_str: str) -> list[str]:
541
583
  lines: list[str] = []
542
584
  for line in toml_str.splitlines():
543
585
  if (line := line.strip()).startswith("["):
@@ -607,20 +649,26 @@ class UpgradeDependencies(Project, DryRun):
607
649
  return _upgrade
608
650
 
609
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"
610
656
  return self.gen_cmd() + " && poetry lock && poetry update"
611
657
 
612
658
 
613
659
  @cli.command()
614
660
  def upgrade(
615
- dry: bool = Option(False, "--dry", help="Only print, not really run shell command"),
661
+ tool: str = ToolOption,
662
+ dry: bool = DryOption,
616
663
  ) -> None:
617
664
  """Upgrade dependencies in pyproject.toml to latest versions"""
618
- if (tool := Project.get_manage_tool()) == "uv":
619
- exit_if_run_failed("uv lock --upgrade && uv sync", dry=dry)
620
- elif tool == "pdm":
621
- 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()
622
669
  else:
623
- UpgradeDependencies(dry=dry).run()
670
+ secho(f"Unknown tool {tool!r}", fg=typer.colors.YELLOW)
671
+ raise typer.Exit(1)
624
672
 
625
673
 
626
674
  class GitTag(DryRun):
@@ -666,7 +714,7 @@ class GitTag(DryRun):
666
714
  @cli.command()
667
715
  def tag(
668
716
  message: str = Option("", "-m", "--message"),
669
- dry: bool = Option(False, "--dry", help="Only print, not really run shell command"),
717
+ dry: bool = DryOption,
670
718
  ) -> None:
671
719
  """Run shell command: git tag -a <current-version-in-pyproject.toml> -m {message}"""
672
720
  GitTag(message, dry=dry).run()
@@ -675,19 +723,21 @@ def tag(
675
723
  class LintCode(DryRun):
676
724
  def __init__(
677
725
  self: Self,
678
- args,
679
- check_only=False,
680
- _exit=False,
681
- dry=False,
682
- bandit=False,
683
- skip_mypy=False,
684
- 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,
685
734
  ) -> None:
686
735
  self.args = args
687
736
  self.check_only = check_only
688
737
  self._bandit = bandit
689
738
  self._skip_mypy = skip_mypy
690
739
  self._use_dmypy = dmypy
740
+ self._tool = tool
691
741
  super().__init__(_exit, dry)
692
742
 
693
743
  @staticmethod
@@ -695,7 +745,7 @@ class LintCode(DryRun):
695
745
  return check_call("ruff --version")
696
746
 
697
747
  @staticmethod
698
- 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:
699
749
  return (
700
750
  paths == "."
701
751
  and any(t.startswith("mypy") for t in tools)
@@ -705,7 +755,8 @@ class LintCode(DryRun):
705
755
  @staticmethod
706
756
  def get_package_name() -> str:
707
757
  root = Project.get_work_dir(allow_cwd=True)
708
- package_maybe = (root.name.replace("-", "_"), "src")
758
+ module_name = root.name.replace("-", "_").replace(" ", "_")
759
+ package_maybe = (module_name, "src")
709
760
  for name in package_maybe:
710
761
  if root.joinpath(name).is_dir():
711
762
  return name
@@ -719,7 +770,10 @@ class LintCode(DryRun):
719
770
  bandit: bool = False,
720
771
  skip_mypy: bool = False,
721
772
  use_dmypy: bool = False,
773
+ tool: str = ToolOption.default,
722
774
  ) -> str:
775
+ if paths != "." and all(i.endswith(".html") for i in paths.split()):
776
+ return f"prettier -w {paths}"
723
777
  cmd = ""
724
778
  tools = ["ruff format", "ruff check --extend-select=I,B,SIM --fix", "mypy"]
725
779
  if check_only:
@@ -745,8 +799,11 @@ class LintCode(DryRun):
745
799
  secho(f"{tip}\n\n {command}\n", fg="yellow")
746
800
  else:
747
801
  should_run_by_tool = True
748
- if should_run_by_tool and (manage_tool := Project.get_manage_tool()):
749
- 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 "
750
807
  if cls.prefer_dmypy(paths, tools, use_dmypy=use_dmypy):
751
808
  tools[-1] = "dmypy run"
752
809
  cmd += lint_them.format(prefix, paths, *tools)
@@ -763,7 +820,9 @@ class LintCode(DryRun):
763
820
  return cmd
764
821
 
765
822
  def gen(self: Self) -> str:
766
- 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 "."
767
826
  return self.to_cmd(
768
827
  paths, self.check_only, self._bandit, self._skip_mypy, self._use_dmypy
769
828
  )
@@ -773,15 +832,31 @@ def parse_files(args: list[str] | tuple[str, ...]) -> list[str]:
773
832
  return [i for i in args if not i.startswith("-")]
774
833
 
775
834
 
776
- 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:
777
843
  if files is None:
778
844
  files = parse_files(sys.argv[1:])
779
845
  if files and files[0] == "lint":
780
846
  files = files[1:]
781
- 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()
782
850
 
783
851
 
784
- 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:
785
860
  LintCode(
786
861
  files,
787
862
  check_only=True,
@@ -790,46 +865,51 @@ def check(files=None, dry=False, bandit=False, skip_mypy=False, dmypy=False) ->
790
865
  bandit=bandit,
791
866
  skip_mypy=skip_mypy,
792
867
  dmypy=dmypy,
868
+ tool=tool,
793
869
  ).run()
794
870
 
795
871
 
796
872
  @cli.command(name="lint")
797
873
  def make_style(
798
- files: Optional[list[Path]] = typer.Argument(default=None), # noqa:B008
874
+ files: Optional[list[str]] = typer.Argument(default=None), # noqa:B008
799
875
  check_only: bool = Option(False, "--check-only", "-c"),
800
876
  bandit: bool = Option(False, "--bandit", help="Run `bandit -r <package_dir>`"),
801
877
  skip_mypy: bool = Option(False, "--skip-mypy"),
802
878
  use_dmypy: bool = Option(
803
879
  False, "--dmypy", help="Use `dmypy run` instead of `mypy`"
804
880
  ),
805
- dry: bool = Option(False, "--dry", help="Only print, not really run shell command"),
881
+ tool: str = ToolOption,
882
+ dry: bool = DryOption,
806
883
  ) -> None:
807
884
  """Run: ruff check/format to reformat code and then mypy to check"""
808
885
  if getattr(files, "default", files) is None:
809
- files = [Path(".")]
886
+ files = ["."]
810
887
  elif isinstance(files, str):
811
888
  files = [files]
812
889
  skip = _ensure_bool(skip_mypy)
813
890
  dmypy = _ensure_bool(use_dmypy)
814
891
  bandit = _ensure_bool(bandit)
892
+ tool = _ensure_str(tool)
815
893
  if _ensure_bool(check_only):
816
- 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)
817
895
  else:
818
- 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)
819
897
 
820
898
 
821
899
  @cli.command(name="check")
822
900
  def only_check(
823
901
  bandit: bool = Option(False, "--bandit", help="Run `bandit -r <package_dir>`"),
824
902
  skip_mypy: bool = Option(False, "--skip-mypy"),
825
- dry: bool = Option(False, "--dry", help="Only print, not really run shell command"),
903
+ dry: bool = DryOption,
826
904
  ) -> None:
827
905
  """Check code style without reformat"""
828
906
  check(dry=dry, bandit=bandit, skip_mypy=_ensure_bool(skip_mypy))
829
907
 
830
908
 
831
909
  class Sync(DryRun):
832
- 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:
833
913
  self.filename = filename
834
914
  self.extras = extras
835
915
  self._save = save
@@ -868,12 +948,12 @@ class Sync(DryRun):
868
948
 
869
949
  @cli.command()
870
950
  def sync(
871
- filename="dev_requirements.txt",
951
+ filename: str = "dev_requirements.txt",
872
952
  extras: str = Option("", "--extras", "-E"),
873
953
  save: bool = Option(
874
954
  False, "--save", "-s", help="Whether save the requirement file"
875
955
  ),
876
- dry: bool = Option(False, "--dry", help="Only print, not really run shell command"),
956
+ dry: bool = DryOption,
877
957
  ) -> None:
878
958
  """Export dependencies by poetry to a txt file then install by pip."""
879
959
  Sync(filename, extras, save, dry=dry).run()
@@ -886,7 +966,7 @@ def _should_run_test_script(path: Path = Path("scripts")) -> Path | None:
886
966
  return None
887
967
 
888
968
 
889
- def test(dry: bool, ignore_script=False) -> None:
969
+ def test(dry: bool, ignore_script: bool = False) -> None:
890
970
  cwd = Path.cwd()
891
971
  root = Project.get_work_dir(cwd=cwd, allow_cwd=True)
892
972
  script_dir = root / "scripts"
@@ -907,7 +987,7 @@ def test(dry: bool, ignore_script=False) -> None:
907
987
 
908
988
  @cli.command(name="test")
909
989
  def coverage_test(
910
- dry: bool = Option(False, "--dry", help="Only print, not really run shell command"),
990
+ dry: bool = DryOption,
911
991
  ignore_script: bool = Option(False, "--ignore-script", "-i"),
912
992
  ) -> None:
913
993
  """Run unittest by pytest and report coverage"""
@@ -930,7 +1010,7 @@ class Publish:
930
1010
 
931
1011
  @cli.command()
932
1012
  def upload(
933
- dry: bool = Option(False, "--dry", help="Only print, not really run shell command"),
1013
+ dry: bool = DryOption,
934
1014
  ) -> None:
935
1015
  """Shortcut for package publish"""
936
1016
  cmd = Publish.gen()
@@ -941,13 +1021,13 @@ def dev(
941
1021
  port: int | None | OptionInfo,
942
1022
  host: str | None | OptionInfo,
943
1023
  file: str | None | ArgumentInfo = None,
944
- dry=False,
1024
+ dry: bool = False,
945
1025
  ) -> None:
946
1026
  cmd = "fastapi dev"
947
1027
  no_port_yet = True
948
1028
  if file is not None:
949
1029
  try:
950
- port = int(str(file)) # type:ignore[arg-type]
1030
+ port = int(str(file))
951
1031
  except ValueError:
952
1032
  cmd += f" {file}"
953
1033
  else:
@@ -969,7 +1049,7 @@ def runserver(
969
1049
  file_or_port: Optional[str] = typer.Argument(default=None),
970
1050
  port: Optional[int] = Option(None, "-p", "--port"),
971
1051
  host: Optional[str] = Option(None, "-h", "--host"),
972
- dry: bool = Option(False, "--dry", help="Only print, not really run shell command"),
1052
+ dry: bool = DryOption,
973
1053
  ) -> None:
974
1054
  """Start a fastapi server(only for fastapi>=0.111.0)"""
975
1055
  if getattr(file_or_port, "default", file_or_port):
@@ -41,7 +41,7 @@ dependencies = [
41
41
  "pytest >=8.2.0,<9",
42
42
  "packaging>=20.5",
43
43
  ]
44
- version = "0.14.1"
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
@@ -17,10 +18,11 @@ from fast_dev_cli.cli import (
17
18
  StrEnum,
18
19
  bump,
19
20
  bump_version,
21
+ capture_cmd_output,
20
22
  get_current_version,
21
23
  )
22
24
 
23
- from .utils import chdir, mock_sys_argv
25
+ from .utils import chdir, mock_sys_argv, prepare_poetry_project
24
26
 
25
27
 
26
28
  def test_enum():
@@ -53,12 +55,14 @@ def test_bump_dry(mocker):
53
55
  mocker.patch("fast_dev_cli.cli.Project.manage_by_poetry", return_value=True)
54
56
  with pytest.raises(ShellCommandError):
55
57
  get_current_version()
56
- # TODO:
57
- # version = get_current_version()
58
- # patch_without_commit, patch_with_commit, minor_with_commit = _bump_commands(version)
59
- # assert BumpUp(part="patch", commit=False, dry=True).gen() == patch_without_commit
60
- # assert BumpUp(part="patch", commit=True, dry=True).gen() == patch_with_commit
61
- # assert BumpUp(part="minor", commit=True, dry=True).gen() == minor_with_commit
58
+ mocker.patch("fast_dev_cli.cli.Project.manage_by_poetry", return_value=False)
59
+ version = get_current_version()
60
+ patch_without_commit, patch_with_commit, minor_with_commit = _bump_commands(
61
+ version, filename="fast_dev_cli/__init__.py"
62
+ )
63
+ assert BumpUp(part="patch", commit=False, dry=True).gen() == patch_without_commit
64
+ assert BumpUp(part="patch", commit=True, dry=True).gen() == patch_with_commit
65
+ assert BumpUp(part="minor", commit=True, dry=True).gen() == minor_with_commit
62
66
 
63
67
 
64
68
  def test_bump(
@@ -145,36 +149,46 @@ def test_bump_with_emoji(mocker, tmp_path, monkeypatch):
145
149
  mocker.patch("fast_dev_cli.cli.Project.manage_by_poetry", return_value=True)
146
150
  with pytest.raises(ShellCommandError):
147
151
  get_current_version()
148
- # TODO:
149
- # version = get_current_version()
150
- # patch_without_commit, patch_with_commit, minor_with_commit = _bump_commands(
151
- # version, emoji=True
152
- # )
153
- # last_commit = "📝 Update release notes"
154
- # mocker.patch(
155
- # "fast_dev_cli.cli.BumpUp.get_last_commit_message",
156
- # return_value=last_commit,
157
- # )
158
- # assert BumpUp(part="patch", commit=False, dry=True).gen() == patch_without_commit
159
- # assert BumpUp(part="patch", commit=True, dry=True).gen() == patch_with_commit
160
- # assert BumpUp(part="minor", commit=True, dry=True).gen() == minor_with_commit
152
+ mocker.patch("fast_dev_cli.cli.Project.manage_by_poetry", return_value=False)
153
+ version = get_current_version()
154
+ patch_without_commit, patch_with_commit, minor_with_commit = _bump_commands(
155
+ version, filename="fast_dev_cli/__init__.py", emoji=True
156
+ )
157
+ last_commit = "📝 Update release notes"
158
+ mocker.patch(
159
+ "fast_dev_cli.cli.BumpUp.get_last_commit_message",
160
+ return_value=last_commit,
161
+ )
162
+ assert BumpUp(part="patch", commit=False, dry=True).gen() == patch_without_commit
163
+ assert BumpUp(part="patch", commit=True, dry=True).gen() == patch_with_commit
164
+ assert BumpUp(part="minor", commit=True, dry=True).gen() == minor_with_commit
165
+
166
+
167
+ def test_bump_with_emoji_in_poetry_project(mocker, tmp_path, monkeypatch):
161
168
  # real bump
162
- # with chdir(tmp_path):
163
- # project = "foo"
164
- # subprocess.run(["poetry", "new", project])
165
- # with chdir(tmp_path / project):
166
- # subprocess.run(["git", "init"])
167
- # subprocess.run(["git", "add", "."])
168
- # subprocess.run(["git", "commit", "-m", last_commit])
169
- # monkeypatch.setenv("DONT_GIT_PUSH", "1")
170
- # command = BumpUp(part="patch", commit=True).gen()
171
- # expected = patch_with_commit.split("&&")[0].strip().replace('""', '"0.1.0"')
172
- # assert expected == command
173
- # subprocess.run(["poetry", "run", "pip", "install", "bumpversion2"])
174
- # subprocess.run(["fast", "bump", "patch", "--commit"])
175
- # out = capture_cmd_output(["git", "log"])
176
- # new_commit = "⬆️ Bump version: 0.1.0 → 0.1.1"
177
- # assert new_commit in out
169
+ last_commit = "📝 Update release notes"
170
+ _, patch_with_commit, __ = _bump_commands("0.1.0", emoji=True)
171
+ with prepare_poetry_project(tmp_path):
172
+ subprocess.run(["git", "init"])
173
+ subprocess.run(["git", "add", "."])
174
+ subprocess.run(["git", "config", "user.name", "sb"])
175
+ subprocess.run(["git", "config", "user.email", "sb@foo.com"])
176
+ assert BumpUp.should_add_emoji() is False
177
+ subprocess.run(["git", "commit", "-m", last_commit])
178
+ monkeypatch.setenv("DONT_GIT_PUSH", "1")
179
+ command = BumpUp(part="patch", commit=True).gen()
180
+ expected = patch_with_commit.split("&&")[0].strip().replace('""', '"0.1.0"')
181
+ assert expected == command
182
+ subprocess.run(["poetry", "run", "pip", "install", "bumpversion2"])
183
+ subprocess.run(["fast", "bump", "patch", "--commit"])
184
+ out = capture_cmd_output(["git", "log"])
185
+ assert BumpUp.should_add_emoji()
186
+ Path("a.txt").touch()
187
+ subprocess.run(["git", "add", "."])
188
+ subprocess.run(["git", "commit", "-m", "no emoji"])
189
+ assert BumpUp.should_add_emoji() is False
190
+ new_commit = "⬆️ Bump version: 0.1.0 → 0.1.1"
191
+ assert new_commit in out
178
192
 
179
193
 
180
194
  def test_bump_with_uv(tmp_path):
@@ -187,3 +201,32 @@ def test_bump_with_uv(tmp_path):
187
201
  Path(TOML_FILE).write_text("[project]" + os.linesep + 'version = "0.1.0"')
188
202
  command = BumpUp(part="patch", commit=True).gen()
189
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"
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  from contextlib import redirect_stdout
3
3
  from io import StringIO
4
+ from pathlib import Path
4
5
 
5
6
  import pytest
6
7
  import typer
@@ -16,6 +17,8 @@ from fast_dev_cli.cli import (
16
17
  run_and_echo,
17
18
  )
18
19
 
20
+ from .utils import prepare_poetry_project
21
+
19
22
 
20
23
  def test_utils(capsys):
21
24
  # parse files
@@ -64,19 +67,15 @@ def test_run_shell():
64
67
  # current version
65
68
  with pytest.raises(ShellCommandError):
66
69
  get_current_version(True, is_poetry=True)
67
- # TODO: add [tool.poetry] to pyproject.toml
68
- # stream = StringIO()
69
- # write_to_stream = redirect_stdout(stream)
70
- # with write_to_stream:
71
- # get_current_version(True, is_poetry=True)
72
- # assert "poetry version -s" in stream.getvalue()
73
70
 
74
71
  name = "TEST_EXIT_IF_RUN_FAILED"
75
72
  value = "foo"
76
73
  cmd = 'python -c "import os;print(list(os.environ))"'
77
74
  with redirect_stdout(StringIO()):
78
- r = exit_if_run_failed(cmd, env={name: value}, capture_output=True)
79
- 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
80
79
 
81
80
  assert run_and_echo("echo foo", capture_output=True) == 0
82
81
 
@@ -92,6 +91,15 @@ def test_run_shell():
92
91
  A().run()
93
92
 
94
93
 
94
+ def test_get_version_in_poetry_project(tmp_path: Path):
95
+ with prepare_poetry_project(tmp_path):
96
+ stream = StringIO()
97
+ write_to_stream = redirect_stdout(stream)
98
+ with write_to_stream:
99
+ get_current_version(True, is_poetry=True)
100
+ assert "poetry version -s" in stream.getvalue()
101
+
102
+
95
103
  def test_ensure_bool():
96
104
  assert _ensure_bool(True) is True
97
105
  assert _ensure_bool(False) is False
@@ -65,7 +65,7 @@ def test_check(mock_no_dmypy, monkeypatch, mocker):
65
65
  for cmd in CHECK_CMD.split(SEP):
66
66
  assert cmd in command
67
67
  command2 = capture_cmd_output("fast check --bandit --dry")
68
- assert command2 == command + " && bandit -r fast_dev_cli"
68
+ assert command2 == command + " && bandit -c pyproject.toml -r ."
69
69
  monkeypatch.setenv("FASTDEVCLI_BANDIT", "1")
70
70
  command3 = capture_cmd_output("fast check --dry")
71
71
  assert command3 == command2
@@ -82,10 +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):
94
+ assert LintCode.get_package_name() == "."
87
95
  command = capture_cmd_output("fast check --bandit --dry")
88
- 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
89
100
 
90
101
 
91
102
  def test_check_skip_mypy(mock_skip_mypy_0, mocker, capsys):
@@ -132,6 +143,23 @@ def test_lint_cmd(mock_no_dmypy):
132
143
  )
133
144
 
134
145
 
146
+ def test_lint_html():
147
+ run = "pdm run "
148
+ lint_cmd = f"{run}python fast_dev_cli/cli.py lint"
149
+ command = capture_cmd_output(f"{lint_cmd} index.html --dry")
150
+ assert "prettier -w index.html" in command
151
+ command = capture_cmd_output(f"{lint_cmd} index.html flv.html --dry")
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"
161
+
162
+
135
163
  def test_lint_by_global_fast():
136
164
  run = "pdm run "
137
165
  fast = Path.home() / ".local" / "bin" / "fast"
@@ -141,20 +169,29 @@ def test_lint_by_global_fast():
141
169
 
142
170
 
143
171
  def test_with_dmypy():
144
- 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")
145
176
  assert "dmypy run ." in command
177
+ command = LintCode.to_cmd(use_dmypy=False, tool="pdm")
178
+ assert "dmypy run ." not in command
146
179
 
147
180
 
148
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
149
184
  monkeypatch.setenv("FASTDEVCLI_DMYPY", "1")
150
185
  command = capture_cmd_output("python -m fast_dev_cli lint --dry .")
151
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
152
189
 
153
190
 
154
191
  def test_lint_with_prefix(mocker):
155
192
  mocker.patch("fast_dev_cli.cli.is_venv", return_value=False)
156
193
  with capture_stdout() as stream:
157
- make_style([Path(".")], check_only=False, dry=True)
194
+ make_style(["."], check_only=False, dry=True)
158
195
  assert "pdm run" in stream.getvalue()
159
196
 
160
197
 
@@ -164,13 +201,13 @@ def test_make_style(mock_skip_mypy_0, mocker, mock_no_dmypy):
164
201
  make_style(check_only=False, dry=True)
165
202
  assert LINT_CMD in stream.getvalue()
166
203
  with capture_stdout() as stream:
167
- make_style([Path(".")], check_only=False, dry=True)
204
+ make_style(["."], check_only=False, dry=True)
168
205
  assert LINT_CMD in stream.getvalue()
169
206
  with capture_stdout() as stream:
170
207
  make_style(".", check_only=False, dry=True) # type:ignore[arg-type]
171
208
  assert LINT_CMD in stream.getvalue()
172
209
  with capture_stdout() as stream:
173
- make_style([Path(".")], check_only=True, dry=True)
210
+ make_style(["."], check_only=True, dry=True)
174
211
  assert CHECK_CMD in stream.getvalue()
175
212
  with capture_stdout() as stream:
176
213
  only_check(dry=True)
@@ -287,3 +324,24 @@ def test_get_manage_tool(tmp_path):
287
324
  assert Project.get_manage_tool() == "pdm"
288
325
  Path(TOML_FILE).write_text("[tool.uv]")
289
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,27 +1,25 @@
1
1
  import os
2
+ import subprocess
2
3
  import sys
3
- from contextlib import contextmanager, redirect_stdout
4
+ from contextlib import AbstractContextManager, contextmanager, redirect_stdout
4
5
  from io import StringIO
5
6
  from pathlib import Path
6
7
 
7
- if sys.version_info >= (3, 11):
8
- from contextlib import chdir # type:ignore[attr-defined]
9
- else:
10
- from contextlib import AbstractContextManager
11
8
 
12
- class chdir(AbstractContextManager): # type:ignore[no-redef]
13
- """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."""
14
12
 
15
- def __init__(self, path):
16
- self.path = path
17
- self._old_cwd = []
13
+ def __init__(self, path):
14
+ self.path = path
15
+ self._old_cwd = []
18
16
 
19
- def __enter__(self):
20
- self._old_cwd.append(os.getcwd())
21
- os.chdir(self.path)
17
+ def __enter__(self):
18
+ self._old_cwd.append(os.getcwd())
19
+ os.chdir(self.path)
22
20
 
23
- def __exit__(self, *excinfo):
24
- os.chdir(self._old_cwd.pop())
21
+ def __exit__(self, *excinfo):
22
+ os.chdir(self._old_cwd.pop())
25
23
 
26
24
 
27
25
  __all__ = (
@@ -66,3 +64,12 @@ def temp_file(name: str, text=""):
66
64
  yield
67
65
  if path.exists():
68
66
  path.unlink()
67
+
68
+
69
+ @contextmanager
70
+ def prepare_poetry_project(tmp_path: Path):
71
+ with chdir(tmp_path):
72
+ project = "foo"
73
+ subprocess.run(["poetry", "new", project])
74
+ with chdir(tmp_path / project):
75
+ yield
@@ -1 +0,0 @@
1
- __version__ = "0.14.1"
File without changes
File without changes