fast-dev-cli 0.10.1__tar.gz → 0.11.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.10.1 → fast_dev_cli-0.11.1}/PKG-INFO +11 -3
  2. {fast_dev_cli-0.10.1 → fast_dev_cli-0.11.1}/README.md +10 -2
  3. fast_dev_cli-0.11.1/fast_dev_cli/__init__.py +1 -0
  4. {fast_dev_cli-0.10.1 → fast_dev_cli-0.11.1}/fast_dev_cli/cli.py +117 -35
  5. {fast_dev_cli-0.10.1 → fast_dev_cli-0.11.1}/pyproject.toml +1 -1
  6. {fast_dev_cli-0.10.1 → fast_dev_cli-0.11.1}/scripts/check.py +2 -1
  7. {fast_dev_cli-0.10.1 → fast_dev_cli-0.11.1}/scripts/format.py +1 -0
  8. {fast_dev_cli-0.10.1 → fast_dev_cli-0.11.1}/scripts/test.py +1 -0
  9. {fast_dev_cli-0.10.1 → fast_dev_cli-0.11.1}/tests/test_lint.py +33 -1
  10. {fast_dev_cli-0.10.1 → fast_dev_cli-0.11.1}/tests/test_poetry_version_plugin.py +27 -3
  11. fast_dev_cli-0.11.1/tests/test_sync.py +214 -0
  12. fast_dev_cli-0.11.1/tests/test_upload.py +43 -0
  13. fast_dev_cli-0.11.1/tests/test_version.py +56 -0
  14. fast_dev_cli-0.10.1/fast_dev_cli/__init__.py +0 -1
  15. fast_dev_cli-0.10.1/tests/test_sync.py +0 -64
  16. fast_dev_cli-0.10.1/tests/test_upload.py +0 -7
  17. fast_dev_cli-0.10.1/tests/test_version.py +0 -10
  18. {fast_dev_cli-0.10.1 → fast_dev_cli-0.11.1}/LICENSE +0 -0
  19. {fast_dev_cli-0.10.1 → fast_dev_cli-0.11.1}/fast_dev_cli/__main__.py +0 -0
  20. {fast_dev_cli-0.10.1 → fast_dev_cli-0.11.1}/fast_dev_cli/py.typed +0 -0
  21. {fast_dev_cli-0.10.1 → fast_dev_cli-0.11.1}/pdm_build.py +0 -0
  22. {fast_dev_cli-0.10.1 → fast_dev_cli-0.11.1}/tests/__init__.py +0 -0
  23. {fast_dev_cli-0.10.1 → fast_dev_cli-0.11.1}/tests/conftest.py +0 -0
  24. {fast_dev_cli-0.10.1 → fast_dev_cli-0.11.1}/tests/test_bump.py +0 -0
  25. {fast_dev_cli-0.10.1 → fast_dev_cli-0.11.1}/tests/test_fast_test.py +0 -0
  26. {fast_dev_cli-0.10.1 → fast_dev_cli-0.11.1}/tests/test_functions.py +0 -0
  27. {fast_dev_cli-0.10.1 → fast_dev_cli-0.11.1}/tests/test_runserver.py +0 -0
  28. {fast_dev_cli-0.10.1 → fast_dev_cli-0.11.1}/tests/test_tag.py +0 -0
  29. {fast_dev_cli-0.10.1 → fast_dev_cli-0.11.1}/tests/test_upgrade.py +0 -0
  30. {fast_dev_cli-0.10.1 → fast_dev_cli-0.11.1}/tests/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fast-dev-cli
3
- Version: 0.10.1
3
+ Version: 0.11.1
4
4
  Summary: Python project development tool.
5
5
  Author-Email: Waket Zheng <waketzheng@gmail.com>>
6
6
  Classifier: Development Status :: 4 - Beta
@@ -96,7 +96,10 @@ fast check
96
96
  ```
97
97
  - Bump up version in pyproject.toml
98
98
  ```bash
99
- fast bump
99
+ fast bump patch # 0.1.0 -> 0.1.1
100
+ fast bump minor # 0.1.0 -> 0.2.0
101
+ fast bump major # 0.1.0 -> 1.0.0
102
+ fast bump <part> --commit # bump version and run `git commit`
100
103
  ```
101
104
  - Run unittest and report coverage
102
105
  ```bash
@@ -106,7 +109,7 @@ fast test
106
109
  ```bash
107
110
  fast sync
108
111
  ```
109
- - Upgrade main/dev dependencies to latest version
112
+ - Upgrade main/dev dependencies to latest version (only for poetry project)
110
113
  ```bash
111
114
  fast upgrade
112
115
  ```
@@ -114,3 +117,8 @@ fast upgrade
114
117
  ```bash
115
118
  fast dev
116
119
  ```
120
+ - Publish to pypi
121
+ ```bash
122
+ fast upload
123
+ ```
124
+ *Note: all command support the `--dry` option*
@@ -61,7 +61,10 @@ fast check
61
61
  ```
62
62
  - Bump up version in pyproject.toml
63
63
  ```bash
64
- fast bump
64
+ fast bump patch # 0.1.0 -> 0.1.1
65
+ fast bump minor # 0.1.0 -> 0.2.0
66
+ fast bump major # 0.1.0 -> 1.0.0
67
+ fast bump <part> --commit # bump version and run `git commit`
65
68
  ```
66
69
  - Run unittest and report coverage
67
70
  ```bash
@@ -71,7 +74,7 @@ fast test
71
74
  ```bash
72
75
  fast sync
73
76
  ```
74
- - Upgrade main/dev dependencies to latest version
77
+ - Upgrade main/dev dependencies to latest version (only for poetry project)
75
78
  ```bash
76
79
  fast upgrade
77
80
  ```
@@ -79,3 +82,8 @@ fast upgrade
79
82
  ```bash
80
83
  fast dev
81
84
  ```
85
+ - Publish to pypi
86
+ ```bash
87
+ fast upload
88
+ ```
89
+ *Note: all command support the `--dry` option*
@@ -0,0 +1 @@
1
+ __version__ = "0.11.1"
@@ -8,7 +8,7 @@ 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, Type
11
+ from typing import Literal, Optional, Type, TypeAlias, get_args
12
12
 
13
13
  import emoji
14
14
  import typer
@@ -37,8 +37,9 @@ else: # pragma: no cover
37
37
  __str__ = str.__str__
38
38
 
39
39
 
40
- TOML_FILE = "pyproject.toml"
41
40
  cli = typer.Typer()
41
+ TOML_FILE = "pyproject.toml"
42
+ ToolName: TypeAlias = Literal["poetry", "pdm", "uv", ""]
42
43
 
43
44
 
44
45
  def load_bool(name: str, default=False) -> bool:
@@ -86,15 +87,56 @@ def capture_cmd_output(command: list[str] | str, **kw) -> str:
86
87
  return r.stdout.strip().decode()
87
88
 
88
89
 
90
+ def _parse_version(line: str, pattern: re.Pattern) -> str:
91
+ return pattern.sub("", line).split("#")[0].strip(" '\"")
92
+
93
+
94
+ def read_version_from_file(
95
+ package_name: str, work_dir=None, toml_text: str | None = None
96
+ ) -> str:
97
+ if toml_text is None:
98
+ toml_text = Project.load_toml_text()
99
+ pattern = re.compile(r"version\s*=")
100
+ invalid = ("0", "0.0.0")
101
+ for line in toml_text.splitlines():
102
+ if pattern.match(line):
103
+ version = _parse_version(line, pattern)
104
+ if version.startswith("{") or version in invalid:
105
+ break
106
+ return version
107
+ if work_dir is None:
108
+ work_dir = Project.get_work_dir()
109
+ package_dir = work_dir / package_name
110
+ if (
111
+ not (init_file := package_dir / "__init__.py").exists()
112
+ and not (init_file := work_dir / "src" / package_name / init_file.name).exists()
113
+ and not (init_file := work_dir / "app" / init_file.name).exists()
114
+ ):
115
+ secho("WARNING: __init__.py file does not exist!")
116
+ return "0.0.0"
117
+ pattern = re.compile(r"__version__\s*=")
118
+ for line in init_file.read_text().splitlines():
119
+ if pattern.match(line):
120
+ return _parse_version(line, pattern)
121
+ secho(f"WARNING: can not find '__version__' var in {init_file}!")
122
+ return "0.0.0"
123
+
124
+
89
125
  def get_current_version(
90
- verbose=False,
91
- is_poetry: bool | None = None,
92
- package_name=Path(__file__).parent.name,
126
+ verbose=False, is_poetry: bool | None = None, package_name: str | None = None
93
127
  ) -> str:
94
128
  if is_poetry is None:
95
129
  is_poetry = Project.manage_by_poetry()
96
130
  if not is_poetry:
97
- return importlib_metadata.version(package_name)
131
+ work_dir = None
132
+ if package_name is None:
133
+ work_dir = Project.get_work_dir()
134
+ package_name = re.sub(r"[- ]", "_", work_dir.name)
135
+ try:
136
+ return importlib_metadata.version(package_name)
137
+ except importlib_metadata.PackageNotFoundError:
138
+ return read_version_from_file(package_name, work_dir)
139
+
98
140
  cmd = ["poetry", "version", "-s"]
99
141
  if verbose:
100
142
  echo(f"--> {' '.join(cmd)}")
@@ -182,14 +224,14 @@ class BumpUp(DryRun):
182
224
  version_value = context["tool"]["poetry"]["version"]
183
225
  except KeyError:
184
226
  return TOML_FILE
185
- if version_value == "0":
227
+ if version_value in ("0", "0.0.0"):
186
228
  try:
187
229
  package_item = context["tool"]["poetry"]["packages"]
188
230
  except KeyError:
189
231
  packages = []
190
232
  else:
191
233
  packages = [j for i in package_item if (j := i.get("include"))]
192
- # In case of managed by `poetry-version-plugin`
234
+ # In case of managed by `poetry-plugin-version`
193
235
  cwd = Path.cwd()
194
236
  pattern = re.compile(r"__version__\s*=\s*['\"]")
195
237
  ds = [cwd / i for i in packages] + [cwd / cwd.name.replace("-", "_"), cwd]
@@ -321,16 +363,16 @@ class Project:
321
363
  return "[tool.poetry]" in cls.load_toml_text()
322
364
 
323
365
  @classmethod
324
- def get_manage_tool(cls: Type[Self]) -> Literal["poetry", "pdm", ""]:
366
+ def get_manage_tool(cls: Type[Self]) -> ToolName:
325
367
  try:
326
368
  text = cls.load_toml_text()
327
369
  except EnvError:
328
370
  pass
329
371
  else:
330
- if "[tool.poetry]" in text:
331
- return "poetry"
332
- elif "[tool.pdm]" in text:
333
- return "pdm"
372
+ name: ToolName
373
+ for name in get_args(ToolName):
374
+ if f"[tool.{name}]" in text:
375
+ return name
334
376
  return ""
335
377
 
336
378
  @staticmethod
@@ -573,11 +615,18 @@ def tag(
573
615
 
574
616
  class LintCode(DryRun):
575
617
  def __init__(
576
- self: Self, args, check_only=False, _exit=False, dry=False, bandit=False
618
+ self: Self,
619
+ args,
620
+ check_only=False,
621
+ _exit=False,
622
+ dry=False,
623
+ bandit=False,
624
+ skip_mypy=False,
577
625
  ) -> None:
578
626
  self.args = args
579
627
  self.check_only = check_only
580
628
  self._bandit = bandit
629
+ self._skip_mypy = skip_mypy
581
630
  super().__init__(_exit, dry)
582
631
 
583
632
  @staticmethod
@@ -602,14 +651,16 @@ class LintCode(DryRun):
602
651
  return "."
603
652
 
604
653
  @classmethod
605
- def to_cmd(cls: Type[Self], paths=".", check_only=False, bandit=False) -> str:
654
+ def to_cmd(
655
+ cls: Type[Self], paths=".", check_only=False, bandit=False, skip_mypy=False
656
+ ) -> str:
606
657
  cmd = ""
607
658
  tools = ["ruff format", "ruff check --extend-select=I,B,SIM --fix", "mypy"]
608
659
  if check_only:
609
660
  tools[0] += " --check"
610
661
  if check_only or load_bool("NO_FIX"):
611
662
  tools[1] = tools[1].replace(" --fix", "")
612
- if load_bool("SKIP_MYPY"):
663
+ if skip_mypy or load_bool("SKIP_MYPY"):
613
664
  # Sometimes mypy is too slow
614
665
  tools = tools[:-1]
615
666
  elif load_bool("IGNORE_MISSING_IMPORTS"):
@@ -640,29 +691,32 @@ class LintCode(DryRun):
640
691
 
641
692
  def gen(self: Self) -> str:
642
693
  paths = " ".join(map(str, self.args)) if self.args else "."
643
- return self.to_cmd(paths, self.check_only, self._bandit)
694
+ return self.to_cmd(paths, self.check_only, self._bandit, self._skip_mypy)
644
695
 
645
696
 
646
697
  def parse_files(args: list[str] | tuple[str, ...]) -> list[str]:
647
698
  return [i for i in args if not i.startswith("-")]
648
699
 
649
700
 
650
- def lint(files=None, dry=False) -> None:
701
+ def lint(files=None, dry=False, skip_mypy=False) -> None:
651
702
  if files is None:
652
703
  files = parse_files(sys.argv[1:])
653
704
  if files and files[0] == "lint":
654
705
  files = files[1:]
655
- LintCode(files, dry=dry).run()
706
+ LintCode(files, dry=dry, skip_mypy=skip_mypy).run()
656
707
 
657
708
 
658
- def check(files=None, dry=False, bandit=False) -> None:
659
- LintCode(files, check_only=True, _exit=True, dry=dry, bandit=bandit).run()
709
+ def check(files=None, dry=False, bandit=False, skip_mypy=False) -> None:
710
+ LintCode(
711
+ files, check_only=True, _exit=True, dry=dry, bandit=bandit, skip_mypy=skip_mypy
712
+ ).run()
660
713
 
661
714
 
662
715
  @cli.command(name="lint")
663
716
  def make_style(
664
717
  files: Annotated[Optional[list[Path]], typer.Argument()] = None,
665
718
  check_only: bool = Option(False, "--check-only", "-c"),
719
+ skip_mypy: bool = Option(False, "--skip-mypy"),
666
720
  dry: bool = Option(False, "--dry", help="Only print, not really run shell command"),
667
721
  ) -> None:
668
722
  """Run: ruff check/format to reformat code and then mypy to check"""
@@ -670,19 +724,21 @@ def make_style(
670
724
  files = [Path(".")]
671
725
  elif isinstance(files, str):
672
726
  files = [files]
727
+ skip = _ensure_bool(skip_mypy)
673
728
  if _ensure_bool(check_only):
674
- check(files, dry=dry)
729
+ check(files, dry=dry, skip_mypy=skip)
675
730
  else:
676
- lint(files, dry=dry)
731
+ lint(files, dry=dry, skip_mypy=skip)
677
732
 
678
733
 
679
734
  @cli.command(name="check")
680
735
  def only_check(
681
736
  bandit: bool = Option(False, "--bandit", help="Run `bandit -r <package_dir>`"),
737
+ skip_mypy: bool = Option(False, "--skip-mypy"),
682
738
  dry: bool = Option(False, "--dry", help="Only print, not really run shell command"),
683
739
  ) -> None:
684
740
  """Check code style without reformat"""
685
- check(dry=dry, bandit=bandit)
741
+ check(dry=dry, bandit=bandit, skip_mypy=_ensure_bool(skip_mypy))
686
742
 
687
743
 
688
744
  class Sync(DryRun):
@@ -695,18 +751,30 @@ class Sync(DryRun):
695
751
  def gen(self) -> str:
696
752
  extras, save = self.extras, self._save
697
753
  should_remove = not Path.cwd().joinpath(self.filename).exists()
698
- prefix = "" if is_venv() else "poetry run "
699
- install_cmd = (
700
- "poetry export --with=dev --without-hashes -o {0}"
701
- " && {1}pip install -r {0}"
702
- )
703
- if not UpgradeDependencies.should_with_dev():
704
- install_cmd = install_cmd.replace(" --with=dev", "")
705
- if extras and isinstance(extras, str | list):
706
- install_cmd = install_cmd.replace("export", f"export --{extras=}")
754
+ if not (tool := Project.get_manage_tool()):
755
+ if should_remove or not is_venv():
756
+ raise EnvError("There project is not managed by uv/pdm/poetry!")
757
+ return f"python -m pip install -r {self.filename}"
758
+ prefix = "" if is_venv() else f"{tool} run "
759
+ ensurepip = " {1}python -m ensurepip && {1}python -m pip install -U pip &&"
760
+ if tool == "uv":
761
+ export_cmd = "uv export --no-hashes --all-extras --frozen"
762
+ if check_call(prefix + "python -m pip --version"):
763
+ ensurepip = ""
764
+ elif tool in ("poetry", "pdm"):
765
+ export_cmd = f"{tool} export --without-hashes --with=dev"
766
+ if tool == "poetry":
767
+ ensurepip = ""
768
+ if not UpgradeDependencies.should_with_dev():
769
+ export_cmd = export_cmd.replace(" --with=dev", "")
770
+ if extras and isinstance(extras, str | list):
771
+ export_cmd += f" --{extras=}".replace("'", '"')
772
+ elif check_call(prefix + "python -m pip --version"):
773
+ ensurepip = ""
774
+ install_cmd = "{2} -o {0} &&%s {1}python -m pip install -r {0}" % ensurepip
707
775
  if should_remove and not save:
708
776
  install_cmd += " && rm -f {0}"
709
- return install_cmd.format(self.filename, prefix)
777
+ return install_cmd.format(self.filename, prefix, export_cmd)
710
778
 
711
779
 
712
780
  @cli.command()
@@ -757,12 +825,26 @@ def coverage_test(
757
825
  return test(dry, ignore_script)
758
826
 
759
827
 
828
+ class Publish:
829
+ class CommandEnum(StrEnum):
830
+ poetry = "poetry publish --build"
831
+ pdm = "pdm publish"
832
+ uv = "uv build && uv publish"
833
+ twine = "python -m build && twine upload"
834
+
835
+ @classmethod
836
+ def gen(cls) -> str:
837
+ if (tool := Project.get_manage_tool()) and tool in get_args(ToolName):
838
+ return cls.CommandEnum[tool]
839
+ return cls.CommandEnum.twine
840
+
841
+
760
842
  @cli.command()
761
843
  def upload(
762
844
  dry: bool = Option(False, "--dry", help="Only print, not really run shell command"),
763
845
  ) -> None:
764
846
  """Shortcut for package publish"""
765
- cmd = "poetry publish --build"
847
+ cmd = Publish.gen()
766
848
  exit_if_run_failed(cmd, dry=dry)
767
849
 
768
850
 
@@ -40,7 +40,7 @@ dependencies = [
40
40
  "bumpversion2 >=1.4.2,<2",
41
41
  "pytest >=8.2.0,<9",
42
42
  ]
43
- version = "0.10.1"
43
+ version = "0.11.1"
44
44
 
45
45
  [project.urls]
46
46
  Homepage = "https://github.com/waketzheng/fast-dev-cli"
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env python
2
+ # -*- coding:utf-8 -*-
2
3
  import os
3
4
  import sys
4
5
 
@@ -16,4 +17,4 @@ cmd = "pdm run bandit -r {}".format(package_name)
16
17
  print("-->", cmd)
17
18
  if os.system(cmd) != 0:
18
19
  sys.exit(1)
19
- print("Done.")
20
+ print("Done. ✨ 🍰 ✨")
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env python
2
+ # -*- coding:utf-8 -*-
2
3
  import os
3
4
  import shlex
4
5
  import subprocess
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env python
2
+ # -*- coding:utf-8 -*-
2
3
  import os
3
4
  import shlex
4
5
  import subprocess
@@ -75,6 +75,7 @@ def test_check(mock_no_dmypy, monkeypatch, mocker):
75
75
  def test_check_bandit(tmp_path):
76
76
  package_path = tmp_path / "foo"
77
77
  with chdir(tmp_path):
78
+ assert LintCode.get_package_name() == "."
78
79
  run_and_echo(f"poetry new {package_path.name}")
79
80
  shutil.rmtree(package_path / package_path.name)
80
81
  with chdir(package_path):
@@ -82,6 +83,18 @@ def test_check_bandit(tmp_path):
82
83
  assert "bandit -r ." in command
83
84
 
84
85
 
86
+ def test_check_skip_mypy(mock_skip_mypy_0, mocker, capsys):
87
+ cmd = "fast check --skip-mypy --dry"
88
+ cmd2 = "fast lint --check-only --skip-mypy --dry"
89
+ mocker.patch("fast_dev_cli.cli.is_venv", return_value=True)
90
+ command = capture_cmd_output(cmd)
91
+ command2 = capture_cmd_output(cmd2)
92
+ expected = "--> " + SEP.join(
93
+ filter(lambda i: not i.startswith("mypy"), CHECK_CMD.split(SEP))
94
+ )
95
+ assert command == command2 == expected
96
+
97
+
85
98
  def test_fast_check():
86
99
  _fast_check()
87
100
 
@@ -128,7 +141,7 @@ def test_lint_with_prefix(mocker):
128
141
  assert "pdm run" in stream.getvalue()
129
142
 
130
143
 
131
- def test_make_style(mocker, mock_no_dmypy):
144
+ def test_make_style(mock_skip_mypy_0, mocker, mock_no_dmypy):
132
145
  mocker.patch("fast_dev_cli.cli.is_venv", return_value=True)
133
146
  with capture_stdout() as stream:
134
147
  make_style(check_only=False, dry=True)
@@ -196,6 +209,23 @@ def test_skip_mypy(mock_skip_mypy, mocker):
196
209
  assert LintCode(".").gen() == SEP.join(i for i in cmds if not i.startswith("mypy"))
197
210
 
198
211
 
212
+ def test_skip_mypy_option(mock_skip_mypy_0, mocker):
213
+ mocker.patch("fast_dev_cli.cli.is_venv", return_value=True)
214
+ cmds = LINT_CMD.split(SEP)
215
+ assert LintCode(".", skip_mypy=True).gen() == SEP.join(
216
+ i for i in cmds if not i.startswith("mypy")
217
+ )
218
+
219
+
220
+ def test_skip_mypy_fast_lint(mock_skip_mypy_0, mocker, capsys):
221
+ mocker.patch("fast_dev_cli.cli.is_venv", return_value=True)
222
+ command = capture_cmd_output("fast lint --skip-mypy --dry")
223
+ cmds = LINT_CMD.split(SEP)
224
+ assert command.replace("--> ", "") == SEP.join(
225
+ i for i in cmds if not i.startswith("mypy")
226
+ )
227
+
228
+
199
229
  def test_skip_mypy_0(mock_skip_mypy_0, mocker, mock_no_dmypy):
200
230
  mocker.patch("fast_dev_cli.cli.is_venv", return_value=True)
201
231
  assert LintCode(".").gen() == LINT_CMD
@@ -238,3 +268,5 @@ def test_get_manage_tool(tmp_path):
238
268
  assert Project.get_manage_tool() == "poetry"
239
269
  Path(TOML_FILE).write_text("[tool.pdm]")
240
270
  assert Project.get_manage_tool() == "pdm"
271
+ Path(TOML_FILE).write_text("[tool.uv]")
272
+ assert Project.get_manage_tool() == "uv"
@@ -18,19 +18,24 @@ from .utils import chdir
18
18
 
19
19
  CONF = """
20
20
 
21
- [tool.poetry-version-plugin]
21
+ [tool.poetry-plugin-version]
22
22
  source = "init"
23
23
  """
24
+ CONF_2 = """
25
+
26
+ [tool.poetry-dynamic-versioning]
27
+ enable = true
28
+ """
24
29
 
25
30
 
26
31
  @contextmanager
27
32
  def _prepare_package(
28
- package_path: Path, define_include=False
33
+ package_path: Path, define_include=False, mark="0"
29
34
  ) -> Generator[Path, None, None]:
30
35
  toml_file = package_path / TOML_FILE
31
36
  package_name = package_path.name.replace(" ", "_")
32
37
  init_file = package_path / package_name / "__init__.py"
33
- a, b = 'version = "0.1.0"', 'version = "0"'
38
+ a, b = 'version = "0.1.0"', f'version = "{mark}"'
34
39
  if define_include:
35
40
  b += '\npackages = [{include = "%s"}]' % package_name
36
41
  with chdir(package_path.parent):
@@ -62,9 +67,28 @@ def test_version_plugin(tmp_path: Path) -> None:
62
67
  BumpUp(part="patch", commit=False, dry=True).gen()
63
68
 
64
69
 
70
+ def test_version_plugin_2(tmp_path: Path) -> None:
71
+ with _prepare_package(tmp_path / "helloworld", mark="0.0.0") as init_file:
72
+ command = _build_bump_cmd(init_file)
73
+ assert BumpUp(part="patch", commit=False, dry=True).gen() == command
74
+ run_and_echo("poetry run fast bump patch")
75
+ assert init_file.read_text() == '__version__ = "0.0.2"\n'
76
+ init_file.unlink()
77
+ with pytest.raises(ParseError, match=r"Version file not found!.*"):
78
+ BumpUp(part="patch", commit=False, dry=True).gen()
79
+
80
+
65
81
  def test_version_plugin_include_defined(tmp_path: Path) -> None:
66
82
  with _prepare_package(tmp_path / "hello world", True) as init_file:
67
83
  command = _build_bump_cmd(init_file)
68
84
  assert BumpUp(part="patch", commit=False, dry=True).gen() == command
69
85
  run_and_echo("poetry run fast bump patch")
70
86
  assert init_file.read_text() == '__version__ = "0.0.2"\n'
87
+
88
+
89
+ def test_version_plugin_include_defined_2(tmp_path: Path) -> None:
90
+ with _prepare_package(tmp_path / "hello world", True, mark="0.0.0") as init_file:
91
+ command = _build_bump_cmd(init_file)
92
+ assert BumpUp(part="patch", commit=False, dry=True).gen() == command
93
+ run_and_echo("poetry run fast bump patch")
94
+ assert init_file.read_text() == '__version__ = "0.0.2"\n'
@@ -0,0 +1,214 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+
5
+ from fast_dev_cli.cli import TOML_FILE, EnvError, Sync, run_and_echo, sync
6
+
7
+ from .utils import chdir, temp_file
8
+
9
+ TOML_TEXT = """
10
+ [tool.poetry]
11
+ name = "foo"
12
+ version = "0.1.0"
13
+ description = ""
14
+ authors = []
15
+ readme = ""
16
+
17
+ [tool.poetry.dependencies]
18
+ python = "^3.11"
19
+ click = ">=7.1.1"
20
+ anyio = {version = "^4.0", optional = true}
21
+
22
+ [tool.poetry.extras]
23
+ all = ["anyio"]
24
+
25
+ [build-system]
26
+ requires = ["poetry-core"]
27
+ build-backend = "poetry.core.masonry.api"
28
+ """
29
+
30
+
31
+ def test_sync_not_in_venv(mocker, capsys):
32
+ mocker.patch("fast_dev_cli.cli.is_venv", return_value=False)
33
+ test_dir = Path(__file__).parent
34
+ with temp_file(TOML_FILE, TOML_TEXT), chdir(test_dir):
35
+ cmd = Sync("req.txt", "all", save=False, dry=True).gen()
36
+ assert (
37
+ cmd
38
+ == 'poetry export --without-hashes --extras="all" -o req.txt && poetry run python -m pip install -r req.txt && rm -f req.txt'
39
+ )
40
+ sync(extras="all", save=False, dry=True)
41
+ assert "pip install -r" in capsys.readouterr().out
42
+ mocker.patch(
43
+ "fast_dev_cli.cli.UpgradeDependencies.should_with_dev", return_value=True
44
+ )
45
+ assert (
46
+ Sync("req.txt", "", True, dry=True).gen()
47
+ == "pdm export --without-hashes --with=dev -o req.txt && pdm run python -m pip install -r req.txt"
48
+ )
49
+
50
+
51
+ def test_sync(mocker):
52
+ mocker.patch("fast_dev_cli.cli.is_venv", return_value=True)
53
+ test_dir = Path(__file__).parent
54
+ with temp_file(TOML_FILE, TOML_TEXT), chdir(test_dir):
55
+ cmd = Sync("req.txt", "all", save=False, dry=True).gen()
56
+ assert (
57
+ cmd
58
+ == 'poetry export --without-hashes --extras="all" -o req.txt && python -m pip install -r req.txt && rm -f req.txt'
59
+ )
60
+ mocker.patch(
61
+ "fast_dev_cli.cli.UpgradeDependencies.should_with_dev", return_value=True
62
+ )
63
+ assert (
64
+ Sync("req.txt", "", True, dry=True).gen()
65
+ == "pdm export --without-hashes --with=dev -o req.txt && python -m pip install -r req.txt"
66
+ )
67
+
68
+
69
+ UV_TOML_EXAMPLE = """
70
+ [project]
71
+ name = "examples"
72
+ version = "0.1.0"
73
+ description = "Add your description here"
74
+ readme = "README.md"
75
+ requires-python = ">=3.10"
76
+ dependencies = [
77
+ "asynctor>=0.6.6",
78
+ ]
79
+
80
+ [tool.uv]
81
+ index-url = "https://mirrors.cloud.tencent.com/pypi/simple/"
82
+ """
83
+ UV_LOCK_EXAMPLE = """
84
+ version = 1
85
+ requires-python = ">=3.10"
86
+
87
+ [[package]]
88
+ name = "anyio"
89
+ version = "4.6.2.post1"
90
+ source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple/" }
91
+ dependencies = [
92
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
93
+ { name = "idna" },
94
+ { name = "sniffio" },
95
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
96
+ ]
97
+ sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c" }
98
+ wheels = [
99
+ { url = "https://mirrors.cloud.tencent.com/pypi/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d" },
100
+ ]
101
+
102
+ [[package]]
103
+ name = "async-timeout"
104
+ version = "4.0.3"
105
+ source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple/" }
106
+ sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/87/d6/21b30a550dafea84b1b8eee21b5e23fa16d010ae006011221f33dcd8d7f8/async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f" }
107
+ wheels = [
108
+ { url = "https://mirrors.cloud.tencent.com/pypi/packages/a7/fa/e01228c2938de91d47b307831c62ab9e4001e747789d0b05baf779a6488c/async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" },
109
+ ]
110
+
111
+ [[package]]
112
+ name = "asynctor"
113
+ version = "0.6.6"
114
+ source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple/" }
115
+ dependencies = [
116
+ { name = "anyio" },
117
+ { name = "redis" },
118
+ ]
119
+ sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/38/d0/452d78491eda3b70bd7b8d401093e5e77999f0232a836b4465e0395223c5/asynctor-0.6.6.tar.gz", hash = "sha256:594c60a38483e399d5a6be513256657989fa2bbf069572074c6b529fa72219ef" }
120
+ wheels = [
121
+ { url = "https://mirrors.cloud.tencent.com/pypi/packages/77/01/31143ebf73a22b5e11a1c6dd3f3af53f1d045d9ab0f13a0159b32df71440/asynctor-0.6.6-py3-none-any.whl", hash = "sha256:54785f1cfd32bd9f9bcffe9445ffd02421aab9c3501c134dbd955901606cf1b4" },
122
+ ]
123
+
124
+ [[package]]
125
+ name = "examples"
126
+ version = "0.1.0"
127
+ source = { virtual = "." }
128
+ dependencies = [
129
+ { name = "asynctor" },
130
+ ]
131
+
132
+ [package.metadata]
133
+ requires-dist = [{ name = "asynctor", specifier = ">=0.6.6" }]
134
+
135
+ [[package]]
136
+ name = "exceptiongroup"
137
+ version = "1.2.2"
138
+ source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple/" }
139
+ sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" }
140
+ wheels = [
141
+ { url = "https://mirrors.cloud.tencent.com/pypi/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b" },
142
+ ]
143
+
144
+ [[package]]
145
+ name = "idna"
146
+ version = "3.10"
147
+ source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple/" }
148
+ sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9" }
149
+ wheels = [
150
+ { url = "https://mirrors.cloud.tencent.com/pypi/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" },
151
+ ]
152
+
153
+ [[package]]
154
+ name = "redis"
155
+ version = "5.1.1"
156
+ source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple/" }
157
+ dependencies = [
158
+ { name = "async-timeout", marker = "python_full_version < '3.11.3'" },
159
+ ]
160
+ sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/e0/58/dcf97c3c09d429c3bb830d6075322256da3dba42df25359bd1c82b442d20/redis-5.1.1.tar.gz", hash = "sha256:f6c997521fedbae53387307c5d0bf784d9acc28d9f1d058abeac566ec4dbed72" }
161
+ wheels = [
162
+ { url = "https://mirrors.cloud.tencent.com/pypi/packages/15/f1/feeeaaaac0f589bcbc12c02da357cf635ee383c9128b77230a1e99118885/redis-5.1.1-py3-none-any.whl", hash = "sha256:f8ea06b7482a668c6475ae202ed8d9bcaa409f6e87fb77ed1043d912afd62e24" },
163
+ ]
164
+
165
+ [[package]]
166
+ name = "sniffio"
167
+ version = "1.3.1"
168
+ source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple/" }
169
+ sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" }
170
+ wheels = [
171
+ { url = "https://mirrors.cloud.tencent.com/pypi/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2" },
172
+ ]
173
+
174
+ [[package]]
175
+ name = "typing-extensions"
176
+ version = "4.12.2"
177
+ source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple/" }
178
+ sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" }
179
+ wheels = [
180
+ { url = "https://mirrors.cloud.tencent.com/pypi/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d" },
181
+ ]
182
+ """
183
+
184
+
185
+ def test_sync_uv(mocker, tmp_path):
186
+ mocker.patch("fast_dev_cli.cli.is_venv", return_value=False)
187
+ with chdir(tmp_path):
188
+ toml = tmp_path / TOML_FILE
189
+ toml.write_text(UV_TOML_EXAMPLE)
190
+ toml.with_name("uv.lock").write_text(UV_LOCK_EXAMPLE)
191
+ assert (
192
+ Sync("req.txt", "", True, dry=True).gen()
193
+ == "uv export --no-hashes --all-extras --frozen -o req.txt && uv run python -m ensurepip && uv run python -m pip install -U pip && uv run python -m pip install -r req.txt"
194
+ )
195
+ run_and_echo("uv run python -m ensurepip")
196
+ assert (
197
+ Sync("req.txt", "", True, dry=True).gen()
198
+ == "uv export --no-hashes --all-extras --frozen -o req.txt && uv run python -m pip install -r req.txt"
199
+ )
200
+
201
+
202
+ def test_sync_no_tool(mocker, tmp_path):
203
+ mocker.patch("fast_dev_cli.cli.is_venv", return_value=False)
204
+ with chdir(tmp_path):
205
+ with pytest.raises(EnvError):
206
+ Sync("req.txt", "", True, dry=True).gen()
207
+ Path("req.txt").write_text("six")
208
+ with pytest.raises(EnvError):
209
+ Sync("req.txt", "", True, dry=True).gen()
210
+ mocker.patch("fast_dev_cli.cli.is_venv", return_value=True)
211
+ assert (
212
+ Sync("req.txt", "", True, dry=True).gen()
213
+ == "python -m pip install -r req.txt"
214
+ )
@@ -0,0 +1,43 @@
1
+ import os
2
+
3
+ from fast_dev_cli.cli import TOML_FILE, run_and_echo, upload
4
+
5
+ from .utils import chdir
6
+
7
+
8
+ def test_upload(capsys):
9
+ upload(dry=True)
10
+ out = capsys.readouterr().out.strip()
11
+ assert out.replace("--> ", "") == "pdm publish"
12
+
13
+
14
+ def test_upload_poetry(tmp_path, capsys):
15
+ with chdir(tmp_path):
16
+ run_and_echo("poetry new foo", verbose=False)
17
+ with chdir(tmp_path / "foo"):
18
+ upload(dry=True)
19
+ out = capsys.readouterr().out.strip()
20
+ assert out.replace("--> ", "") == "poetry publish --build"
21
+
22
+
23
+ def test_upload_uv(tmp_path, capsys):
24
+ project_dir = tmp_path / "uv_proj"
25
+ project_dir.mkdir()
26
+ toml_file = project_dir / TOML_FILE
27
+ with chdir(project_dir):
28
+ run_and_echo("uv init", verbose=False)
29
+ if (s := "[tool.uv]") not in (text := toml_file.read_text()):
30
+ toml_file.write_text(text + os.linesep + s)
31
+ upload(dry=True)
32
+ out = capsys.readouterr().out.strip()
33
+ assert out.replace("--> ", "") == "uv build && uv publish"
34
+
35
+
36
+ def test_upload_other(tmp_path, capsys):
37
+ project_dir = tmp_path / "proj"
38
+ project_dir.mkdir()
39
+ with chdir(project_dir):
40
+ run_and_echo("uv init", verbose=False)
41
+ upload(dry=True)
42
+ out = capsys.readouterr().out.strip()
43
+ assert out.replace("--> ", "") == "python -m build && twine upload"
@@ -0,0 +1,56 @@
1
+ import re
2
+ from pathlib import Path
3
+
4
+ from fast_dev_cli import __version__
5
+ from fast_dev_cli.cli import (
6
+ TOML_FILE,
7
+ _parse_version,
8
+ get_current_version,
9
+ read_version_from_file,
10
+ version,
11
+ )
12
+
13
+ from .utils import chdir
14
+
15
+
16
+ def test_version(capsys):
17
+ version()
18
+ assert get_current_version(is_poetry=False) in capsys.readouterr().out
19
+ assert get_current_version(is_poetry=False) == __version__
20
+ assert get_current_version(is_poetry=True) == ""
21
+ assert get_current_version() == __version__
22
+
23
+
24
+ def test_read_version(tmp_path: Path, capsys):
25
+ assert read_version_from_file("fast_dev_cli") == __version__
26
+ toml_text = 'version = "0.1.0"'
27
+ assert read_version_from_file("", toml_text=toml_text) == "0.1.0"
28
+ with chdir(tmp_path):
29
+ tmp_path.joinpath(TOML_FILE).write_text("")
30
+ assert read_version_from_file("") == "0.0.0"
31
+ assert "WARNING" in capsys.readouterr().out
32
+ init_file = tmp_path / "app" / "__init__.py"
33
+ init_file.parent.mkdir()
34
+ init_file.write_text("")
35
+ assert read_version_from_file("") == "0.0.0"
36
+ assert "WARNING" in capsys.readouterr().out
37
+ assert get_current_version() == "0.0.0"
38
+
39
+
40
+ def test_parse_version():
41
+ pattern = re.compile(r"version\s*=")
42
+ line = 'version="0.0.1" # adsfasf version="0.0.2"'
43
+ assert _parse_version(line, pattern) == "0.0.1"
44
+ line = 'version = "0.0.1" # adsfasf version="0.0.2"'
45
+ assert _parse_version(line, pattern) == "0.0.1"
46
+ line = "version = '0.0.1' # adsfasf version=\"0.0.2\""
47
+ assert _parse_version(line, pattern) == "0.0.1"
48
+ line = "version='0.0.1' # adsfasf version=\"0.0.2\""
49
+ assert _parse_version(line, pattern) == "0.0.1"
50
+ line = 'version="0.0.1"'
51
+ assert _parse_version(line, pattern) == "0.0.1"
52
+ pattern = re.compile(r"__version__\s*=")
53
+ line = '__version__ = "0.0.1"'
54
+ assert _parse_version(line, pattern) == "0.0.1"
55
+ line = '__version__ = "0.0.1" # 0.0.2'
56
+ assert _parse_version(line, pattern) == "0.0.1"
@@ -1 +0,0 @@
1
- __version__ = "0.10.1"
@@ -1,64 +0,0 @@
1
- from pathlib import Path
2
-
3
- from fast_dev_cli.cli import TOML_FILE, Sync, sync
4
-
5
- from .utils import chdir, temp_file
6
-
7
- TOML_TEXT = """
8
- [tool.poetry]
9
- name = "foo"
10
- version = "0.1.0"
11
- description = ""
12
- authors = []
13
- readme = ""
14
-
15
- [tool.poetry.dependencies]
16
- python = "^3.11"
17
- click = ">=7.1.1"
18
- anyio = {version = "^4.0", optional = true}
19
-
20
- [tool.poetry.extras]
21
- all = ["anyio"]
22
-
23
- [build-system]
24
- requires = ["poetry-core"]
25
- build-backend = "poetry.core.masonry.api"
26
- """
27
-
28
-
29
- def test_sync_not_in_venv(mocker, capsys):
30
- mocker.patch("fast_dev_cli.cli.is_venv", return_value=False)
31
- test_dir = Path(__file__).parent
32
- with temp_file(TOML_FILE, TOML_TEXT), chdir(test_dir):
33
- cmd = Sync("req.txt", "all", save=False, dry=True).gen()
34
- assert (
35
- cmd
36
- == "poetry export --extras='all' --without-hashes -o req.txt && poetry run pip install -r req.txt && rm -f req.txt"
37
- )
38
- sync(extras="all", save=False, dry=True)
39
- assert "pip install -r" in capsys.readouterr().out
40
- mocker.patch(
41
- "fast_dev_cli.cli.UpgradeDependencies.should_with_dev", return_value=True
42
- )
43
- assert (
44
- Sync("req.txt", "", True, dry=True).gen()
45
- == "poetry export --with=dev --without-hashes -o req.txt && poetry run pip install -r req.txt"
46
- )
47
-
48
-
49
- def test_sync(mocker):
50
- mocker.patch("fast_dev_cli.cli.is_venv", return_value=True)
51
- test_dir = Path(__file__).parent
52
- with temp_file(TOML_FILE, TOML_TEXT), chdir(test_dir):
53
- cmd = Sync("req.txt", "all", save=False, dry=True).gen()
54
- assert (
55
- cmd
56
- == "poetry export --extras='all' --without-hashes -o req.txt && pip install -r req.txt && rm -f req.txt"
57
- )
58
- mocker.patch(
59
- "fast_dev_cli.cli.UpgradeDependencies.should_with_dev", return_value=True
60
- )
61
- assert (
62
- Sync("req.txt", "", True, dry=True).gen()
63
- == "poetry export --with=dev --without-hashes -o req.txt && pip install -r req.txt"
64
- )
@@ -1,7 +0,0 @@
1
- from fast_dev_cli.cli import upload
2
-
3
-
4
- def test_upload(capsys):
5
- upload(dry=True)
6
- out = capsys.readouterr().out.strip()
7
- assert out.replace("--> ", "") == "poetry publish --build"
@@ -1,10 +0,0 @@
1
- from fast_dev_cli import __version__
2
- from fast_dev_cli.cli import get_current_version, version
3
-
4
-
5
- def test_version(capsys):
6
- version()
7
- assert get_current_version(is_poetry=False) in capsys.readouterr().out
8
- assert get_current_version(is_poetry=False) == __version__
9
- assert get_current_version(is_poetry=True) == ""
10
- assert get_current_version() == __version__
File without changes