fast-dev-cli 0.24.3__tar.gz → 0.25.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fast-dev-cli
3
- Version: 0.24.3
3
+ Version: 0.25.1
4
4
  Summary: Python project development tool.
5
5
  Author-Email: Waket Zheng <waketzheng@gmail.com>>
6
6
  Classifier: Development Status :: 4 - Beta
@@ -0,0 +1 @@
1
+ __version__ = "0.25.1"
@@ -114,13 +114,19 @@ def yellow_warn(msg: str) -> None:
114
114
  def load_bool(name: str, default: bool = False, *, verbose: bool = True) -> bool:
115
115
  if not (v := os.getenv(name)):
116
116
  return default
117
+ if (b := _convert_bool(v)) is not None:
118
+ return b
119
+ if verbose:
120
+ secho(f"WARNING: can not convert value({v!r}) of {name} to bool!")
121
+ return default
122
+
123
+
124
+ def _convert_bool(v: str) -> bool | None:
117
125
  if (lower := v.lower()) in ("0", "false", "f", "off", "no", "n"):
118
126
  return False
119
127
  elif lower in ("1", "true", "t", "on", "yes", "y"):
120
128
  return True
121
- if verbose:
122
- secho(f"WARNING: can not convert value({v!r}) of {name} to bool!")
123
- return default
129
+ return None
124
130
 
125
131
 
126
132
  def is_venv() -> bool:
@@ -141,7 +147,8 @@ class Shell:
141
147
  ) -> subprocess.CompletedProcess[str]:
142
148
  if isinstance(cmd, str):
143
149
  kw.setdefault("shell", True)
144
- return subprocess.run(cmd, **kw) # nosec:B603
150
+ check = kw.pop("check", False)
151
+ return subprocess.run(cmd, check=check, **kw) # nosec:B603
145
152
 
146
153
  @property
147
154
  def command(self) -> list[str] | str:
@@ -260,13 +267,12 @@ def read_version_from_file(
260
267
  for line in all_lines:
261
268
  if pattern.match(line):
262
269
  return _parse_version(line, pattern)
263
- else:
264
- pattern = re.compile(r"VERSION\s*=")
265
- for line in all_lines:
266
- if pattern.match(line):
267
- return _parse_version(line, pattern)
268
- secho(f"WARNING: can not find version pattern in {version_file}!")
269
- return "0.0.0"
270
+ pattern = re.compile(r"VERSION\s*=")
271
+ for line in all_lines:
272
+ if pattern.match(line):
273
+ return _parse_version(line, pattern)
274
+ secho(f"WARNING: can not find version pattern in {version_file}!")
275
+ return "0.0.0"
270
276
 
271
277
 
272
278
  def _get_frontend_version() -> tuple[Path, str] | None:
@@ -325,9 +331,9 @@ def get_current_version(
325
331
  if package_name is None:
326
332
  try:
327
333
  work_dir = Project.get_work_dir()
328
- except EnvError as e:
334
+ except EnvError:
329
335
  if (res := _get_frontend_version()) is None:
330
- raise e
336
+ raise
331
337
  current_version = res[1]
332
338
  if check_version:
333
339
  return False, current_version
@@ -468,7 +474,7 @@ class BumpUp(DryRun):
468
474
  toml_text: str | None = None,
469
475
  work_dir: Path | None = None,
470
476
  package_name: str | None = None,
471
- context: dict | None = None,
477
+ context: dict[str, Any] | None = None,
472
478
  ) -> str:
473
479
  if context is None:
474
480
  if toml_text is None:
@@ -528,11 +534,11 @@ class BumpUp(DryRun):
528
534
  ds: list[Path] = []
529
535
  if package_name is not None:
530
536
  packages.insert(0, (package_name, ""))
531
- for package_name, source_dir in packages:
532
- ds.append(cwd / package_name)
533
- ds.append(cwd / "src" / package_name)
537
+ for pack_name, source_dir in packages:
538
+ ds.append(cwd / pack_name)
539
+ ds.append(cwd / "src" / pack_name)
534
540
  if source_dir and source_dir != "src":
535
- ds.append(cwd / source_dir / package_name)
541
+ ds.append(cwd / source_dir / pack_name)
536
542
  module_name = poetry_module_name(cwd.name)
537
543
  ds.extend([cwd / module_name, cwd / "src" / module_name, cwd])
538
544
  for d in ds:
@@ -756,7 +762,7 @@ class Project:
756
762
  if skip_uv:
757
763
  for name in ("pdm", "poetry"):
758
764
  if f"[tool.{name}]" in text:
759
- cls._tool = cast(ToolName, name)
765
+ cls._tool = name
760
766
  return cls._tool
761
767
  work_dir = cls.get_work_dir(allow_cwd=True)
762
768
  for name in ("pdm", "poetry"):
@@ -791,11 +797,11 @@ class Project:
791
797
  if backend:
792
798
  for t in ("pdm", "poetry"):
793
799
  if t in backend:
794
- cls._tool = cast(ToolName, t)
800
+ cls._tool = t
795
801
  return cls._tool
796
802
  for name in ("pdm", "poetry"):
797
803
  if f"[tool.{name}]" in text:
798
- cls._tool = cast(ToolName, name)
804
+ cls._tool = name
799
805
  return cls._tool
800
806
  if x == 2:
801
807
  cls._tool = (
@@ -919,7 +925,7 @@ class UpgradeDependencies(Project, DryRun):
919
925
  elif v == "[":
920
926
  echo(f"Skip complex dependence: {line}")
921
927
  return True
922
- elif v.startswith(">") or v.startswith("<") or v[0].isdigit():
928
+ elif v.startswith((">", "<")) or v[0].isdigit():
923
929
  echo(f"Ignore bigger/smaller/equal: {line}")
924
930
  return True
925
931
  return False
@@ -1230,50 +1236,54 @@ class LintCode(DryRun):
1230
1236
  should_run_by_tool = with_prefix
1231
1237
  requires_mypy = any(tool.startswith("mypy") for tool in tools)
1232
1238
  global_mypy = False
1233
- if requires_mypy and Path.home().joinpath(".local/bin/mypy").exists():
1234
- global_mypy = True
1235
- mypy_opt = "--python-executable=.venv/bin/python"
1236
- for i, t in enumerate(tools):
1237
- if t.startswith("mypy"):
1238
- if mypy_opt not in t:
1239
- tools[i] = t + " " + mypy_opt
1240
- break
1241
- if requires_mypy and not should_run_by_tool:
1242
- if is_venv() and Path(sys.argv[0]).parent != Path.home().joinpath(
1243
- ".local/bin"
1244
- ): # Virtual environment activated and fast-dev-cli is installed in it
1245
- if not ruff_exists:
1246
- should_run_by_tool = True
1247
- command = "pipx install ruff"
1248
- if shutil.which("pipx") is None:
1249
- ensure_pipx = "pip install --user pipx\n pipx ensurepath\n "
1250
- command = ensure_pipx + command
1251
- elif prefer_uv_tool():
1252
- command = "uv tool install ruff"
1253
- yellow_warn(
1254
- "You may need to run the following command"
1255
- f" to install ruff:\n\n {command}\n"
1256
- )
1257
- elif global_mypy:
1258
- should_run_by_tool = True
1259
- elif cls.missing_mypy_exec():
1260
- should_run_by_tool = True
1261
- if check_call('python -c "import fast_dev_cli"'):
1262
- command = "python -m pip install -U mypy"
1239
+ if requires_mypy:
1240
+ # TODO: move this long logic to a single function
1241
+ local_bin = Path.home().joinpath(".local/bin")
1242
+ if local_bin.joinpath("mypy").exists():
1243
+ global_mypy = True
1244
+ mypy_opt = "--python-executable=.venv/bin/python"
1245
+ for i, t in enumerate(tools):
1246
+ if t.startswith("mypy"):
1247
+ if mypy_opt not in t:
1248
+ tools[i] = t + " " + mypy_opt
1249
+ break
1250
+ if not should_run_by_tool:
1251
+ if is_venv() and Path(sys.argv[0]).parent != local_bin:
1252
+ # Virtual environment activated and fast-dev-cli is installed in it
1253
+ if not ruff_exists:
1254
+ should_run_by_tool = True
1255
+ command = "pipx install ruff"
1256
+ if shutil.which("pipx") is None:
1257
+ ensure_pipx = (
1258
+ "pip install --user pipx\n pipx ensurepath\n "
1259
+ )
1260
+ command = ensure_pipx + command
1261
+ elif prefer_uv_tool():
1262
+ command = "uv tool install ruff"
1263
1263
  yellow_warn(
1264
1264
  "You may need to run the following command"
1265
- f" to install lint tools:\n\n {command}\n"
1265
+ f" to install ruff:\n\n {command}\n"
1266
1266
  )
1267
- elif tool == ToolOption.default:
1268
- root = Project.get_work_dir(allow_cwd=True)
1269
- if py := shutil.which("python"):
1270
- try:
1271
- Path(py).relative_to(root)
1272
- except ValueError:
1273
- # Virtual environment not activated
1267
+ elif global_mypy:
1274
1268
  should_run_by_tool = True
1275
- else:
1276
- should_run_by_tool = True
1269
+ elif cls.missing_mypy_exec():
1270
+ should_run_by_tool = True
1271
+ if check_call('python -c "import fast_dev_cli"'):
1272
+ command = "python -m pip install -U mypy"
1273
+ yellow_warn(
1274
+ "You may need to run the following command"
1275
+ f" to install lint tools:\n\n {command}\n"
1276
+ )
1277
+ elif tool == ToolOption.default:
1278
+ root = Project.get_work_dir(allow_cwd=True)
1279
+ if py := shutil.which("python"):
1280
+ try:
1281
+ Path(py).relative_to(root)
1282
+ except ValueError:
1283
+ # Virtual environment not activated
1284
+ should_run_by_tool = True
1285
+ else:
1286
+ should_run_by_tool = True
1277
1287
  if should_run_by_tool and tool:
1278
1288
  if tool == ToolOption.default:
1279
1289
  tool = Project.get_manage_tool() or ""
@@ -1419,6 +1429,14 @@ def check(
1419
1429
  ).run()
1420
1430
 
1421
1431
 
1432
+ def _should_bandit() -> bool:
1433
+ if v := os.getenv("FASTDEVCLI_BANDIT"):
1434
+ return _convert_bool(v) or True
1435
+ toml_text = Project.load_toml_text()
1436
+ lines = toml_text.splitlines()
1437
+ return any(i.startswith("[tool.bandit") for i in lines)
1438
+
1439
+
1422
1440
  @cli.command(name="lint")
1423
1441
  def make_style(
1424
1442
  files: list[str] | None = typer.Argument(default=None), # noqa:B008
@@ -1440,6 +1458,9 @@ def make_style(
1440
1458
  strict: bool = Option(False, help="Whether run mypy with --strict"),
1441
1459
  ty: bool = Option(False, help="Whether use ty instead of mypy"),
1442
1460
  fix: bool | None = Option(None, help="Whether ruff check with --fix"),
1461
+ auto_bandit: bool | None = Option(
1462
+ None, help="Whether to run bandit if `[tool.bandit]` in pyproject.toml"
1463
+ ),
1443
1464
  ) -> None:
1444
1465
  """Run: ruff check/format to reformat code and then mypy to check"""
1445
1466
  if getattr(files, "default", files) is None:
@@ -1448,7 +1469,9 @@ def make_style(
1448
1469
  files = [files]
1449
1470
  skip = _ensure_bool(skip_mypy)
1450
1471
  dmypy = _ensure_bool(use_dmypy)
1451
- bandit = _ensure_bool(bandit)
1472
+ bandit = _ensure_bool(bandit) or (
1473
+ auto_bandit is not None and _ensure_bool(auto_bandit) and _should_bandit()
1474
+ )
1452
1475
  tool = _ensure_str(tool) or ""
1453
1476
  up = _ensure_bool(up)
1454
1477
  sim = _ensure_bool(sim)
@@ -1581,18 +1604,34 @@ class Publish:
1581
1604
  twine = "python -m build && twine upload"
1582
1605
 
1583
1606
  @classmethod
1584
- def gen(cls) -> str:
1585
- if tool := Project.get_manage_tool():
1607
+ def gen(cls, tool: str | None, verbose: bool) -> str:
1608
+ if tool == "auto":
1609
+ tool = Project.get_manage_tool() or ""
1610
+ if tool:
1611
+ if tool == "uv":
1612
+ envs = ["UV_PUBLISH_INDEX", "UV_PUBLISH_URL", "UV_PUBLISH_TOKEN"]
1613
+ if not any(os.getenv(i) for i in envs):
1614
+ if verbose:
1615
+ yellow_warn(f"Skip uv publish as envs ({envs}) not set")
1616
+ return "uv build"
1617
+ elif tool == "pdm":
1618
+ env = "PDM_PUBLISH_REPO"
1619
+ if not os.getenv(env):
1620
+ if verbose:
1621
+ yellow_warn(f"Skip pdm publish as env ({env}) not set")
1622
+ return "pdm build"
1586
1623
  return cls.CommandEnum[tool]
1587
1624
  return cls.CommandEnum.twine
1588
1625
 
1589
1626
 
1590
1627
  @cli.command()
1591
1628
  def upload(
1629
+ verbose: bool = False,
1630
+ tool: str = ToolOption,
1592
1631
  dry: bool = DryOption,
1593
1632
  ) -> None:
1594
1633
  """Shortcut for package publish"""
1595
- cmd = Publish.gen()
1634
+ cmd = Publish.gen(_ensure_str(tool), verbose)
1596
1635
  exit_if_run_failed(cmd, dry=dry)
1597
1636
 
1598
1637
 
@@ -1630,7 +1669,9 @@ def _prefer_just_dev(f: Path) -> bool:
1630
1669
  return False
1631
1670
 
1632
1671
 
1633
- def _parse_serve_file(uvicorn, filename: str, cmd: str, args: list) -> str:
1672
+ def _parse_serve_file(
1673
+ uvicorn: bool | None, filename: str, cmd: str, args: list[str]
1674
+ ) -> str:
1634
1675
  if m := re.search(r"(.*):(\d+)$", filename):
1635
1676
  h, p = m.group(1), m.group(2)
1636
1677
  if h and "--host" not in str(args):
@@ -1657,7 +1698,12 @@ def _parse_serve_file(uvicorn, filename: str, cmd: str, args: list) -> str:
1657
1698
  return cmd
1658
1699
 
1659
1700
 
1660
- def _runserver(uvicorn, host, port, file) -> tuple[str, list[str]]:
1701
+ def _runserver(
1702
+ uvicorn: bool | None,
1703
+ host: OptionInfo | str | None,
1704
+ port: OptionInfo | int | None,
1705
+ file: ArgumentInfo | str | None,
1706
+ ) -> tuple[str, list[str]]:
1661
1707
  cmd = "uvicorn" if uvicorn else "fastapi dev"
1662
1708
  args = []
1663
1709
  if (host := getattr(host, "default", host)) and host not in (
@@ -1690,10 +1736,11 @@ def dev(
1690
1736
  uvicorn: bool | None = None,
1691
1737
  prod: bool | None = None,
1692
1738
  reload: bool | None = None,
1739
+ just: bool | None = None,
1693
1740
  file: str | None | ArgumentInfo = None,
1694
1741
  dry: bool = False,
1695
1742
  ) -> None:
1696
- if should_use_just():
1743
+ if just is not False and should_use_just():
1697
1744
  args = [i for i in sys.argv[2:] if i != "--dry"]
1698
1745
  cmd = "just dev"
1699
1746
  else:
@@ -1712,10 +1759,13 @@ def runserver(
1712
1759
  uvicorn: bool | None = None,
1713
1760
  prod: bool | None = None,
1714
1761
  reload: bool | None = None,
1762
+ just: bool | None = None,
1715
1763
  dry: bool = DryOption,
1716
1764
  ) -> None:
1717
1765
  """Start a fastapi server(only for fastapi>=0.111.0)"""
1718
- f = functools.partial(dev, port, host, fastapi, uvicorn, prod, reload, dry=dry)
1766
+ f = functools.partial(
1767
+ dev, port, host, fastapi, uvicorn, prod, reload, just, dry=dry
1768
+ )
1719
1769
  if getattr(file_or_port, "default", file_or_port):
1720
1770
  f(file=file_or_port)
1721
1771
  else:
@@ -1734,7 +1784,7 @@ def run_by_subprocess(cmd: str, dry: bool = DryOption) -> None:
1734
1784
  ):
1735
1785
  echo(f"Command not found: {command}")
1736
1786
  raise Exit(1) from None
1737
- raise e
1787
+ raise
1738
1788
  else:
1739
1789
  if rc:
1740
1790
  raise Exit(rc)
@@ -1804,15 +1854,17 @@ class MakeDeps(DryRun):
1804
1854
  tool_section = doc["tool"]
1805
1855
  uv_package = tool_section.get("uv", {}).get("package")
1806
1856
  if uv_package is not None:
1807
- return doc["project"]["name"] if uv_package else ""
1808
- match doc["build-system"]["build-backend"]:
1809
- case "pdm.backend":
1810
- if not tool_section.get("pdm", {}).get("distribution", True):
1811
- return ""
1812
- case x if x.startswith("poetry"):
1813
- if not tool_section.get("poetry", {}).get("package-mode", True):
1814
- return ""
1815
- return doc["project"]["name"]
1857
+ if not uv_package:
1858
+ return ""
1859
+ else:
1860
+ match doc["build-system"]["build-backend"]:
1861
+ case "pdm.backend":
1862
+ if not tool_section.get("pdm", {}).get("distribution", True):
1863
+ return ""
1864
+ case x if x.startswith("poetry"):
1865
+ if not tool_section.get("poetry", {}).get("package-mode", True):
1866
+ return ""
1867
+ return cast(str, doc["project"]["name"])
1816
1868
  return ""
1817
1869
 
1818
1870
  def _gen(self) -> str:
@@ -1928,7 +1980,7 @@ class UvPypi(DryRun):
1928
1980
 
1929
1981
  @staticmethod
1930
1982
  def get_target_content(
1931
- text: str, verbose: bool, target_registry, target_host: str
1983
+ text: str, verbose: bool, target_registry: str, target_host: str
1932
1984
  ) -> str | None:
1933
1985
  registry_pattern = r'(registry = ")(.*?)"'
1934
1986
  registry_urls = {i[1] for i in re.findall(registry_pattern, text)}
@@ -29,7 +29,7 @@ dependencies = [
29
29
  "typer>=0.24.0,<1",
30
30
  "tomli >=2.0.1,<3; python_version < '3.11'",
31
31
  ]
32
- version = "0.24.3"
32
+ version = "0.25.1"
33
33
 
34
34
  [project.urls]
35
35
  Homepage = "https://github.com/waketzheng/fast-dev-cli"
@@ -184,10 +184,13 @@ extend-select = [
184
184
  [tool.ruff.lint.per-file-ignores]
185
185
  "test_*.py" = [
186
186
  "E501",
187
+ "BLE001",
188
+ "PLW1510",
187
189
  ]
188
190
  "scripts/*.py" = [
189
191
  "UP009",
190
192
  "UP032",
193
+ "PLW1510",
191
194
  ]
192
195
  "fast_dev_cli/cli.py" = [
193
196
  "UP007",
@@ -1 +0,0 @@
1
- __version__ = "0.24.3"
File without changes
File without changes