rhythmos-cli 26.8.2__tar.gz → 26.10.4__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,8 +1,10 @@
1
1
  /.venv
2
2
  /.ipynb_checkpoints
3
3
 
4
- /.pytest_cache
5
- /.ruff_cache
4
+ # Generated by hatch-vcs on build / editable install
5
+ /src/rhythmos/_version.py
6
+
7
+ /.pytest_cache/.ruff_cache
6
8
  /.mypy_cache
7
9
  /.tox
8
10
  /.direnv
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: rhythmos-cli
3
- Version: 26.8.2
3
+ Version: 26.10.4
4
4
  Summary: A lightweight CLI framework with project-local auto-discovered commands
5
5
  Project-URL: Homepage, https://gitlab.com/JoanMbela/rhythmos-cli
6
6
  Project-URL: Repository, https://gitlab.com/JoanMbela/rhythmos-cli
@@ -20,6 +20,8 @@ Classifier: Programming Language :: Python :: 3.12
20
20
  Classifier: Programming Language :: Python :: 3.13
21
21
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
22
22
  Requires-Python: >=3.9
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8.0; extra == 'dev'
23
25
  Description-Content-Type: text/markdown
24
26
 
25
27
  # Rhythmos CLI
@@ -74,15 +76,20 @@ See `examples/commands/hello.py` for a ready example.
74
76
  ```bash
75
77
  python -m venv .venv
76
78
  source .venv/bin/activate
77
- pip install -e .
79
+ pip install -e ".[dev]"
78
80
  RHYTHMOS_COMMANDS=examples/commands rhythmos-cli hello World
81
+ pytest
79
82
  ```
80
83
 
81
84
  ## Release (GitLab CI)
82
85
 
83
- Publishes to PyPI when you push a version tag matching the version in `pyproject.toml`.
86
+ Version comes from **git tags** (no manual edits in `pyproject.toml`). Pushing a tag builds and publishes that version:
84
87
 
85
88
  ```bash
86
- git tag v0.1.1
87
- git push origin v0.1.1
89
+ git add -A && git commit -m "..."
90
+ git tag v26.10.2
91
+ git push origin v26.10.2
88
92
  ```
93
+
94
+ Requires CI variable `PYPI_TOKEN` (masked + protected) and protected tags `v*`.
95
+ If a tag pipeline fails, push a **new** tag — protected tags usually cannot be deleted via `git`.
@@ -50,15 +50,20 @@ See `examples/commands/hello.py` for a ready example.
50
50
  ```bash
51
51
  python -m venv .venv
52
52
  source .venv/bin/activate
53
- pip install -e .
53
+ pip install -e ".[dev]"
54
54
  RHYTHMOS_COMMANDS=examples/commands rhythmos-cli hello World
55
+ pytest
55
56
  ```
56
57
 
57
58
  ## Release (GitLab CI)
58
59
 
59
- Publishes to PyPI when you push a version tag matching the version in `pyproject.toml`.
60
+ Version comes from **git tags** (no manual edits in `pyproject.toml`). Pushing a tag builds and publishes that version:
60
61
 
61
62
  ```bash
62
- git tag v0.1.1
63
- git push origin v0.1.1
63
+ git add -A && git commit -m "..."
64
+ git tag v26.10.2
65
+ git push origin v26.10.2
64
66
  ```
67
+
68
+ Requires CI variable `PYPI_TOKEN` (masked + protected) and protected tags `v*`.
69
+ If a tag pipeline fails, push a **new** tag — protected tags usually cannot be deleted via `git`.
@@ -1,10 +1,10 @@
1
1
  [build-system]
2
- requires = ["hatchling"]
2
+ requires = ["hatchling", "hatch-vcs"]
3
3
  build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "rhythmos-cli"
7
- version = "26.8.2"
7
+ dynamic = ["version"]
8
8
  description = "A lightweight CLI framework with project-local auto-discovered commands"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -31,10 +31,22 @@ classifiers = [
31
31
  Homepage = "https://gitlab.com/JoanMbela/rhythmos-cli"
32
32
  Repository = "https://gitlab.com/JoanMbela/rhythmos-cli"
33
33
 
34
+ [project.optional-dependencies]
35
+ dev = ["pytest>=8.0"]
36
+
34
37
  [project.scripts]
35
38
  rhythmos-cli = "rhythmos.cli:main"
36
39
  rhythmos = "rhythmos.cli:main"
37
40
 
41
+ [tool.hatch.version]
42
+ source = "vcs"
43
+
44
+ [tool.hatch.version.raw-options]
45
+ local_scheme = "no-local-version"
46
+
47
+ [tool.hatch.build.hooks.vcs]
48
+ version-file = "src/rhythmos/_version.py"
49
+
38
50
  [tool.hatch.build.targets.wheel]
39
51
  packages = ["src/rhythmos"]
40
52
 
@@ -42,6 +54,11 @@ packages = ["src/rhythmos"]
42
54
  include = [
43
55
  "/src",
44
56
  "/examples",
57
+ "/tests",
45
58
  "/README.md",
46
59
  "/LICENCE",
47
60
  ]
61
+
62
+ [tool.pytest.ini_options]
63
+ testpaths = ["tests"]
64
+ pythonpath = ["src"]
@@ -0,0 +1,15 @@
1
+ """Rhythmos — a lightweight project CLI framework."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ from rhythmos.base_command import BaseCommand
6
+
7
+ try:
8
+ __version__ = version("rhythmos-cli")
9
+ except PackageNotFoundError: # pragma: no cover - editable / source tree
10
+ try:
11
+ from rhythmos._version import __version__
12
+ except ImportError:
13
+ __version__ = "0.0.0"
14
+
15
+ __all__ = ["BaseCommand", "__version__"]
@@ -1,4 +1,4 @@
1
1
  from rhythmos.cli import main
2
2
 
3
3
  if __name__ == "__main__":
4
- main()
4
+ main(include_system_commands=True)
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '26.10.4'
22
+ __version_tuple__ = version_tuple = (26, 10, 4)
23
+
24
+ __commit_id__ = commit_id = None
@@ -5,10 +5,18 @@ from rhythmos.base_command import BaseCommand
5
5
  from rhythmos.discovery import commands_dir, discover_commands, invoked_as
6
6
  from rhythmos.system_commands import SYSTEM_COMMANDS
7
7
 
8
+ OFFICIAL_CLIS = frozenset({"rhythmos", "rhythmos-cli"})
8
9
 
9
- def resolve_commands() -> dict[str, type[BaseCommand]]:
10
+
11
+ def resolve_commands(
12
+ include_system_commands: bool = True,
13
+ ) -> dict[str, type[BaseCommand]]:
10
14
  """Merge system + user commands. User commands win on name clashes."""
11
15
  user = discover_commands()
16
+
17
+ if not include_system_commands:
18
+ return {**user}
19
+
12
20
  for name in user:
13
21
  if name in SYSTEM_COMMANDS:
14
22
  print(
@@ -18,16 +26,23 @@ def resolve_commands() -> dict[str, type[BaseCommand]]:
18
26
  return {**SYSTEM_COMMANDS, **user}
19
27
 
20
28
 
21
- def main(argv: list[str] | None = None) -> None:
29
+ def main(
30
+ argv: list[str] | None = None,
31
+ *,
32
+ include_system_commands: bool | None = None,
33
+ ) -> None:
22
34
  argv = list(sys.argv[1:] if argv is None else argv)
23
35
  cli = invoked_as()
36
+ if include_system_commands is None:
37
+ # Official entry points expose make/list; project wrappers do not by default.
38
+ include_system_commands = cli in OFFICIAL_CLIS
24
39
 
25
40
  parser = argparse.ArgumentParser(prog=cli, description="Rhythmos CLI")
26
41
  subparsers = parser.add_subparsers(
27
42
  dest="command", help="Command to execute", required=True
28
43
  )
29
44
 
30
- commands = resolve_commands()
45
+ commands = resolve_commands(include_system_commands)
31
46
  command_instances = {}
32
47
 
33
48
  for command_name, command_class in commands.items():
@@ -0,0 +1,118 @@
1
+ import pytest
2
+
3
+ from rhythmos.base_command import BaseCommand
4
+ from rhythmos.cli import main, resolve_commands
5
+ from rhythmos.discovery import (
6
+ COMMANDS_DIR_ENV,
7
+ discover_commands,
8
+ scaffold_command,
9
+ to_class_name,
10
+ to_snake,
11
+ )
12
+ from rhythmos.system_commands import SYSTEM_COMMANDS
13
+ from rhythmos.system_commands.list import ListCommand
14
+ from rhythmos.system_commands.make import MakeCommand
15
+
16
+
17
+ HELLO_CMD = """\
18
+ from rhythmos import BaseCommand
19
+
20
+
21
+ class HelloCommand(BaseCommand):
22
+ def get_description(self):
23
+ return "Say hello"
24
+
25
+ def add_arguments(self):
26
+ self.parser.add_argument("name")
27
+
28
+ def handle(self, name):
29
+ print(f"Hello, {name}!")
30
+ """
31
+
32
+
33
+ @pytest.fixture
34
+ def commands_home(tmp_path, monkeypatch):
35
+ directory = tmp_path / "commands"
36
+ directory.mkdir()
37
+ monkeypatch.setenv(COMMANDS_DIR_ENV, str(directory))
38
+ return directory
39
+
40
+
41
+ def test_to_snake_and_class_name():
42
+ assert to_snake("HelloWorld") == "hello_world"
43
+ assert to_snake("foo-bar") == "foo_bar"
44
+ assert to_class_name("foo_bar") == "FooBar"
45
+
46
+
47
+ def test_discover_commands_loads_user_modules(commands_home):
48
+ (commands_home / "hello.py").write_text(HELLO_CMD, encoding="utf-8")
49
+ found = discover_commands()
50
+ assert set(found) == {"hello"}
51
+ assert issubclass(found["hello"], BaseCommand)
52
+
53
+
54
+ def test_resolve_commands_excludes_system_when_disabled(commands_home):
55
+ (commands_home / "hello.py").write_text(HELLO_CMD, encoding="utf-8")
56
+ resolved = resolve_commands(include_system_commands=False)
57
+ assert set(resolved) == {"hello"}
58
+ assert "list" not in resolved
59
+ assert "make" not in resolved
60
+
61
+
62
+ def test_resolve_commands_includes_system_when_enabled(commands_home):
63
+ (commands_home / "hello.py").write_text(HELLO_CMD, encoding="utf-8")
64
+ resolved = resolve_commands(include_system_commands=True)
65
+ assert set(resolved) == {"hello", "list", "make"}
66
+ assert resolved["list"] is ListCommand
67
+ assert resolved["make"] is MakeCommand
68
+
69
+
70
+ def test_resolve_commands_user_overrides_system(commands_home, capsys):
71
+ (commands_home / "list.py").write_text(
72
+ """\
73
+ from rhythmos import BaseCommand
74
+
75
+ class ListCommand(BaseCommand):
76
+ def get_description(self):
77
+ return "user list"
78
+
79
+ def handle(self):
80
+ print("user")
81
+ """,
82
+ encoding="utf-8",
83
+ )
84
+ resolved = resolve_commands(include_system_commands=True)
85
+ assert resolved["list"] is not ListCommand
86
+ assert resolved["list"] is not SYSTEM_COMMANDS["list"]
87
+ err = capsys.readouterr().err
88
+ assert "overrides system command 'list'" in err
89
+
90
+
91
+ def test_scaffold_command_writes_stub(commands_home):
92
+ path = scaffold_command("greet", "Greet someone")
93
+ assert path == commands_home / "greet.py"
94
+ text = path.read_text(encoding="utf-8")
95
+ assert "class GreetCommand" in text
96
+ assert "Greet someone" in text
97
+ assert "from rhythmos import BaseCommand" in text
98
+
99
+
100
+ def test_scaffold_command_rejects_duplicates(commands_home):
101
+ scaffold_command("greet")
102
+ with pytest.raises(FileExistsError):
103
+ scaffold_command("greet")
104
+
105
+
106
+ def test_main_with_system_commands_runs_list(commands_home, capsys):
107
+ (commands_home / "hello.py").write_text(HELLO_CMD, encoding="utf-8")
108
+ main(["list"], include_system_commands=True)
109
+ out = capsys.readouterr().out
110
+ assert "hello" in out
111
+ assert "[user" in out
112
+ assert "[system]" in out
113
+
114
+
115
+ def test_main_without_system_commands_rejects_list(commands_home):
116
+ (commands_home / "hello.py").write_text(HELLO_CMD, encoding="utf-8")
117
+ with pytest.raises(SystemExit):
118
+ main(["list"], include_system_commands=False)
@@ -1,6 +0,0 @@
1
- """Rhythmos — a lightweight project CLI framework."""
2
-
3
- from rhythmos.base_command import BaseCommand
4
-
5
- __version__ = "0.1.0"
6
- __all__ = ["BaseCommand", "__version__"]
File without changes