Skip to content

Commit 31f8664

Browse files
committed
🔨 refactor(cli)!: replace typer with click for cli commands and tests
1 parent 86bd5e7 commit 31f8664

16 files changed

+246
-204
lines changed

pyproject.toml

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ dependencies = [
1111
"jinja2>=3.1.4",
1212
"questionary>=2.0.1",
1313
"beautifulsoup4>=4.13.3",
14-
"typer>=0.15.1",
14+
"click>=8.0.0",
1515
]
1616
readme = { content-type = "text/markdown", file = "README.md" }
1717
requires-python = ">= 3.11"
@@ -80,19 +80,15 @@ ignore = [
8080
"ARG002",
8181
"PLR0913",
8282
"PT011",
83-
"B017"
83+
"B017",
8484
]
8585

8686
[tool.pytest.ini_options]
8787
testpaths = ["tests"]
8888
python_files = ["test_*.py", "*_test.py"]
8989
python_classes = ["Test*"]
9090
python_functions = ["test_*"]
91-
addopts = [
92-
"--strict-markers",
93-
"--disable-warnings",
94-
"-v",
95-
]
91+
addopts = ["--strict-markers", "--disable-warnings", "-v"]
9692
markers = [
9793
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
9894
"integration: marks tests as integration tests",

tests/unit/test_add.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pytest
22
from unittest.mock import patch, MagicMock
3+
from click.testing import CliRunner
34

45
from tgit.add import add
56

@@ -10,47 +11,56 @@ class TestAdd:
1011
@patch("tgit.add.simple_run_command")
1112
def test_add_single_file(self, mock_simple_run_command):
1213
"""Test adding a single file"""
13-
files = ["test.txt"]
14-
add(files)
14+
runner = CliRunner()
15+
result = runner.invoke(add, ["test.txt"])
1516

17+
assert result.exit_code == 0
1618
mock_simple_run_command.assert_called_once_with("git add test.txt")
1719

1820
@patch("tgit.add.simple_run_command")
1921
def test_add_multiple_files(self, mock_simple_run_command):
2022
"""Test adding multiple files"""
21-
files = ["file1.txt", "file2.py", "file3.md"]
22-
add(files)
23+
runner = CliRunner()
24+
result = runner.invoke(add, ["file1.txt", "file2.py", "file3.md"])
2325

26+
assert result.exit_code == 0
2427
mock_simple_run_command.assert_called_once_with("git add file1.txt file2.py file3.md")
2528

2629
@patch("tgit.add.simple_run_command")
2730
def test_add_files_with_spaces(self, mock_simple_run_command):
2831
"""Test adding files with spaces in names"""
29-
files = ["file with spaces.txt", "another file.py"]
30-
add(files)
32+
runner = CliRunner()
33+
result = runner.invoke(add, ["file with spaces.txt", "another file.py"])
3134

35+
assert result.exit_code == 0
3236
mock_simple_run_command.assert_called_once_with("git add file with spaces.txt another file.py")
3337

3438
@patch("tgit.add.simple_run_command")
3539
def test_add_empty_list(self, mock_simple_run_command):
3640
"""Test adding empty file list"""
37-
files = []
38-
add(files)
41+
runner = CliRunner()
42+
result = runner.invoke(add, [])
3943

40-
mock_simple_run_command.assert_called_once_with("git add ")
44+
# Since files argument is required=True, this should fail
45+
assert result.exit_code == 2
46+
mock_simple_run_command.assert_not_called()
4147

4248
@patch("tgit.add.simple_run_command")
4349
def test_add_files_with_special_characters(self, mock_simple_run_command):
4450
"""Test adding files with special characters"""
45-
files = ["file-with-dashes.txt", "file_with_underscores.py", "file.with.dots.md"]
46-
add(files)
51+
runner = CliRunner()
52+
result = runner.invoke(add, ["file-with-dashes.txt", "file_with_underscores.py", "file.with.dots.md"])
4753

54+
assert result.exit_code == 0
4855
mock_simple_run_command.assert_called_once_with("git add file-with-dashes.txt file_with_underscores.py file.with.dots.md")
4956

5057
@patch("tgit.add.simple_run_command")
5158
def test_add_propagates_exception(self, mock_simple_run_command):
5259
"""Test that exceptions from simple_run_command are propagated"""
5360
mock_simple_run_command.side_effect = Exception("Git command failed")
61+
runner = CliRunner()
5462

55-
with pytest.raises(Exception, match="Git command failed"):
56-
add(["test.txt"])
63+
result = runner.invoke(add, ["test.txt"])
64+
65+
# Exception should cause non-zero exit code
66+
assert result.exit_code != 0

tests/unit/test_changelog.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from datetime import datetime, UTC
77
import git
88
import re
9+
from click.testing import CliRunner
910

1011

1112
from tgit.changelog import (
@@ -765,8 +766,10 @@ class TestChangelogFunction:
765766
@patch("tgit.changelog.handle_changelog")
766767
def test_changelog_function_defaults(self, mock_handle):
767768
"""Test changelog function with default arguments."""
768-
changelog()
769-
769+
runner = CliRunner()
770+
result = runner.invoke(changelog, ["."])
771+
772+
assert result.exit_code == 0
770773
mock_handle.assert_called_once()
771774
# Get the actual args passed to handle_changelog - it should be a ChangelogArgs object
772775
called_args = mock_handle.call_args[0][0]
@@ -779,17 +782,21 @@ def test_changelog_function_defaults(self, mock_handle):
779782
@patch("tgit.changelog.handle_changelog")
780783
def test_changelog_function_with_output_flag(self, mock_handle):
781784
"""Test changelog function with output flag."""
782-
changelog(output="")
783-
785+
runner = CliRunner()
786+
result = runner.invoke(changelog, [".", "--output", ""])
787+
788+
assert result.exit_code == 0
784789
mock_handle.assert_called_once()
785790
args = mock_handle.call_args[0][0]
786791
assert args.output == "CHANGELOG.md"
787792

788793
@patch("tgit.changelog.handle_changelog")
789794
def test_changelog_function_with_custom_args(self, mock_handle):
790795
"""Test changelog function with custom arguments."""
791-
changelog(path="/tmp", from_raw="v1.0.0", to_raw="v1.1.0", verbose=2, output="custom.md") # noqa: S108
792-
796+
runner = CliRunner()
797+
result = runner.invoke(changelog, ["/tmp", "--from", "v1.0.0", "--to", "v1.1.0", "-vv", "--output", "custom.md"]) # noqa: S108
798+
799+
assert result.exit_code == 0
793800
mock_handle.assert_called_once()
794801
args = mock_handle.call_args[0][0]
795802
assert args.path == "/tmp" # noqa: S108

tests/unit/test_cli.py

Lines changed: 30 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,68 @@
11
import pytest
22
from unittest.mock import patch, MagicMock
3-
import typer
3+
import click
44
import threading
55
import time
6+
from click.testing import CliRunner
67

7-
from tgit.cli import app, version_callback, main
8+
from tgit.cli import app, version_callback
89

910

1011
class TestCLI:
1112
"""Test cases for the CLI module"""
1213

1314
def test_app_instance(self):
14-
"""Test that app is a Typer instance with correct configuration"""
15-
assert isinstance(app, typer.Typer)
16-
assert app.info.name == "tgit"
17-
assert app.info.help == "TGIT cli"
18-
assert app.info.no_args_is_help is True
15+
"""Test that app is a Click group with correct configuration"""
16+
assert isinstance(app, click.Group)
17+
assert app.name == "tgit"
18+
assert app.help == "TGIT cli"
19+
assert app.no_args_is_help is True
1920

2021
def test_commands_registered(self):
2122
"""Test that all expected commands are registered"""
2223
# Just verify the app object exists and has the right type
23-
# since the command registration details may vary by Typer version
24-
assert isinstance(app, typer.Typer)
24+
# since the command registration details may vary by Click version
25+
assert isinstance(app, click.Group)
2526

2627
@patch("tgit.cli.importlib.metadata.version")
2728
@patch("tgit.cli.console.print")
2829
def test_version_callback_true(self, mock_print, mock_version):
2930
"""Test version callback when value is True"""
3031
mock_version.return_value = "1.0.0"
32+
mock_ctx = MagicMock()
33+
mock_ctx.resilient_parsing = False
34+
mock_param = MagicMock()
3135

32-
with pytest.raises(typer.Exit):
33-
version_callback(value=True)
36+
version_callback(ctx=mock_ctx, _param=mock_param, value=True)
3437

3538
mock_version.assert_called_once_with("tgit")
3639
mock_print.assert_called_once_with("TGIT - ver.1.0.0", highlight=False)
40+
mock_ctx.exit.assert_called_once()
3741

3842
@patch("tgit.cli.importlib.metadata.version")
3943
@patch("tgit.cli.console.print")
4044
def test_version_callback_false(self, mock_print, mock_version):
4145
"""Test version callback when value is False"""
42-
version_callback(value=False)
46+
mock_ctx = MagicMock()
47+
mock_ctx.resilient_parsing = False
48+
mock_param = MagicMock()
49+
50+
version_callback(ctx=mock_ctx, _param=mock_param, value=False)
4351

4452
mock_version.assert_not_called()
4553
mock_print.assert_not_called()
54+
mock_ctx.exit.assert_not_called()
4655

4756
@patch("tgit.cli.threading.Thread")
48-
def test_main_starts_openai_import_thread(self, mock_thread):
49-
"""Test that main starts a thread for OpenAI import"""
50-
mock_thread_instance = MagicMock()
51-
mock_thread.return_value = mock_thread_instance
52-
53-
main(_version=False)
54-
55-
mock_thread.assert_called_once()
56-
mock_thread_instance.start.assert_called_once()
57-
58-
@patch("tgit.cli.threading.Thread")
59-
def test_main_with_version_false(self, mock_thread):
60-
"""Test main function with version=False"""
57+
def test_app_starts_openai_import_thread(self, mock_thread):
58+
"""Test that app starts a thread for OpenAI import"""
6159
mock_thread_instance = MagicMock()
6260
mock_thread.return_value = mock_thread_instance
6361

64-
main(_version=False)
62+
# Directly call the app function to test the callback
63+
app.callback()
6564

66-
# Should still start the import thread
65+
# The app should run the callback and start the thread
6766
mock_thread.assert_called_once()
6867
mock_thread_instance.start.assert_called_once()
6968

@@ -82,23 +81,8 @@ def mock_import():
8281

8382
assert import_called.is_set()
8483

85-
@patch("tgit.cli.threading.Thread")
86-
def test_openai_import_with_exception_suppression(self, mock_thread):
87-
"""Test that OpenAI import exceptions are suppressed"""
88-
# Create a mock thread that we can control
89-
mock_thread_instance = MagicMock()
90-
mock_thread.return_value = mock_thread_instance
91-
92-
main(_version=False)
93-
94-
# Just verify that the thread was created and started
95-
mock_thread.assert_called_once()
96-
mock_thread_instance.start.assert_called_once()
97-
9884
def test_app_callback_registration(self):
99-
"""Test that main is registered as app callback"""
100-
# The callback should be registered - check if app has the callback mechanism
101-
# This may vary by Typer version, so we'll check if the app object is properly configured
102-
assert isinstance(app, typer.Typer)
103-
# In newer Typer versions, the callback structure might be different
104-
# We'll just verify the app exists and is configured correctly
85+
"""Test that app is properly configured"""
86+
# Check if app has the callback mechanism
87+
assert isinstance(app, click.Group)
88+
# Verify the app exists and is configured correctly

tests/unit/test_commit.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import git
66
from pathlib import Path
77
import tempfile
8+
from click.testing import CliRunner
89

910
from tgit.commit import (
1011
CommitArgs,
@@ -543,8 +544,10 @@ class TestCommitFunction:
543544
@patch("tgit.commit.handle_commit")
544545
def test_commit_function_default_args(self, mock_handle_commit):
545546
"""Test commit function with default arguments."""
546-
commit()
547+
runner = CliRunner()
548+
result = runner.invoke(commit, [])
547549

550+
assert result.exit_code == 0
548551
# Check that handle_commit was called once
549552
mock_handle_commit.assert_called_once()
550553

@@ -558,8 +561,10 @@ def test_commit_function_default_args(self, mock_handle_commit):
558561
@patch("tgit.commit.handle_commit")
559562
def test_commit_function_with_args(self, mock_handle_commit):
560563
"""Test commit function with custom arguments."""
561-
commit(message=["feat", "add feature"], emoji=True, breaking=True, ai=True)
564+
runner = CliRunner()
565+
result = runner.invoke(commit, ["feat", "add feature", "--emoji", "--breaking", "--ai"])
562566

567+
assert result.exit_code == 0
563568
expected_args = CommitArgs(message=["feat", "add feature"], emoji=True, breaking=True, ai=True)
564569
mock_handle_commit.assert_called_once()
565570

0 commit comments

Comments
 (0)