Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Use argparse instead sys.argv[..] (Rework of #19) #21

Merged
merged 1 commit into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 34 additions & 24 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,31 @@
import time
import os

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src')))
sys.path.insert(
0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src"))
)


class TestTkreloadApp(unittest.TestCase):

@patch('tkreload.main.subprocess.Popen')
@patch('tkreload.main.show_progress')
@patch("tkreload.main.subprocess.Popen")
@patch("tkreload.main.show_progress")
def test_run_tkinter_app(self, mock_show_progress, mock_popen):
app = TkreloadApp('example/sample_app.py')
app = TkreloadApp("example/sample_app.py")
process = Mock()
mock_popen.return_value = process

result = app.run_tkinter_app()
mock_show_progress.assert_called_once()
mock_popen.assert_called_once_with([sys.executable, 'example/sample_app.py'])
mock_popen.assert_called_once_with([sys.executable, "example/sample_app.py"])
self.assertEqual(result, process)
@patch('tkreload.main.Observer')
@patch('tkreload.main.AppFileEventHandler')

@patch("tkreload.main.Observer")
@patch("tkreload.main.AppFileEventHandler")
def test_monitor_file_changes(self, mock_event_handler, mock_observer):
app = TkreloadApp('example/sample_app.py')
app = TkreloadApp("example/sample_app.py")
mock_callback = Mock()

observer = app.monitor_file_changes(mock_callback)
mock_event_handler.assert_called_once()
mock_observer().schedule.assert_called_once()
Expand All @@ -39,27 +42,34 @@ def test_monitor_file_changes(self, mock_event_handler, mock_observer):
# app = TkreloadApp('example/sample_app.py')
# mock_process = Mock()
# mock_popen.return_value = mock_process

# with self.assertRaises(SystemExit):
# app.start()

# mock_process.terminate.assert_called_once()

@patch('tkreload.main.sys.argv', ['tkreload', 'example/sample_app.py'])
@patch('tkreload.main.file_exists', return_value=True)
@patch('tkreload.main.TkreloadApp')
@patch("tkreload.main.sys.argv", ["tkreload", "example/sample_app.py"])
@patch("tkreload.main.file_exists", return_value=True)
@patch("tkreload.main.TkreloadApp")
def test_main_function(self, mock_tkreload_app, mock_file_exists):
main()
mock_file_exists.assert_called_once_with('example/sample_app.py')
mock_tkreload_app.assert_called_once_with('example/sample_app.py')
mock_file_exists.assert_called_once_with("example/sample_app.py")
mock_tkreload_app.assert_called_once_with("example/sample_app.py")
mock_tkreload_app().start.assert_called_once()

@patch('tkreload.main.sys.argv', ['tkreload'])
@patch('tkreload.main.Console')
def test_main_function_no_file_provided(self, mock_console):
with self.assertRaises(SystemExit):
@patch("tkreload.main.sys.argv", ["tkreload"])
@patch("tkreload.main.Console")
@patch("tkreload.main.argparse.ArgumentParser")
def test_main_function_no_file_provided(self, mock_parser, mock_console):
mock_parser_instance = Mock()
mock_parser.return_value = mock_parser_instance
mock_parser_instance.parse_args.side_effect = SystemExit(2)

with self.assertRaises(SystemExit) as cm:
main()
mock_console().print.assert_called_once_with("[bold red]Error: No Tkinter app file provided![/bold red]")

if __name__ == '__main__':
self.assertEqual(cm.exception.code, 2)


if __name__ == "__main__":
unittest.main()
54 changes: 40 additions & 14 deletions tkreload/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os
import select
import platform
import argparse
from rich.console import Console
from watchdog.observers import Observer
from .app_event_handler import AppFileEventHandler
Expand All @@ -16,6 +17,7 @@
if platform.system() == "Windows":
import msvcrt


class TkreloadApp:
"""Main application class for managing the Tkinter app."""

Expand All @@ -39,17 +41,23 @@ def monitor_file_changes(self, on_reload):
self.observer.stop()
self.observer.join()

event_handler = AppFileEventHandler(on_reload, self.app_file, self.auto_reload_manager)
event_handler = AppFileEventHandler(
on_reload, self.app_file, self.auto_reload_manager
)
self.observer = Observer()
self.observer.schedule(event_handler, path=os.path.dirname(self.app_file) or '.', recursive=False)
self.observer.schedule(
event_handler, path=os.path.dirname(self.app_file) or ".", recursive=False
)
self.observer.start()
return self.observer

def restart_app(self):
"""Restarts the Tkinter app."""
if self.process:
self.reload_count += 1
self.console.log(f"[bold yellow]Restarting the Tkinter app... (x{self.reload_count})[/bold yellow]")
self.console.log(
f"[bold yellow]Restarting the Tkinter app... (x{self.reload_count})[/bold yellow]"
)
self.process.terminate()
self.process.wait()
time.sleep(1)
Expand All @@ -61,17 +69,27 @@ def start(self):
self.monitor_file_changes(self.restart_app)

try:
self.console.print("\n\n\t[bold cyan]Tkreload[/bold cyan] [bold blue]is running ✅\n\t[/bold blue]- Press [bold cyan]H[/bold cyan] for help,\n\t[bold cyan]- R[/bold cyan] to restart,\n\t[bold cyan]- A[/bold cyan] to toggle auto-reload (currently [bold magenta]{}[/bold magenta]),\n\t[bold red]- Ctrl + C[/bold red] to exit.".format("Disabled" if not self.auto_reload_manager.get_status() else "Enabled"))
self.console.print(
"\n\n\t[bold cyan]Tkreload[/bold cyan] [bold blue]is running ✅\n\t[/bold blue]- Press [bold cyan]H[/bold cyan] for help,\n\t[bold cyan]- R[/bold cyan] to restart,\n\t[bold cyan]- A[/bold cyan] to toggle auto-reload (currently [bold magenta]{}[/bold magenta]),\n\t[bold red]- Ctrl + C[/bold red] to exit.".format(
"Disabled"
if not self.auto_reload_manager.get_status()
else "Enabled"
)
)

while True:
if platform.system() == "Windows":
if msvcrt.kbhit(): # Check for keyboard input (Windows only)
user_input = msvcrt.getch().decode('utf-8').lower() # Read single character input
user_input = (
msvcrt.getch().decode("utf-8").lower()
) # Read single character input
self.handle_input(user_input)
else:
# Use select for Unix-like systems
if sys.stdin in select.select([sys.stdin], [], [], 0)[0]:
user_input = sys.stdin.read(1).lower() # Capture a single character input
user_input = sys.stdin.read(
1
).lower() # Capture a single character input
self.handle_input(user_input)

time.sleep(0.1)
Expand All @@ -85,11 +103,13 @@ def start(self):

def handle_input(self, user_input):
"""Handles the user input commands."""
if user_input == 'h':
show_help("Enabled" if self.auto_reload_manager.get_status() else "Disabled")
elif user_input == 'r':
if user_input == "h":
show_help(
"Enabled" if self.auto_reload_manager.get_status() else "Disabled"
)
elif user_input == "r":
self.restart_app()
elif user_input == 'a':
elif user_input == "a":
self.toggle_auto_reload()

def toggle_auto_reload(self):
Expand All @@ -99,12 +119,17 @@ def toggle_auto_reload(self):
self.reload_count = 0
status = "Enabled" if self.auto_reload_manager.get_status() else "Disabled"


def main():
if len(sys.argv) < 2:
Console().print("[bold red]Error: No Tkinter app file provided![/bold red]")
sys.exit(1)
parser = argparse.ArgumentParser(
description="Real-time reload Tkinter app",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("app_file", help="Tkinter app file path")

app_file = sys.argv[1]
args = parser.parse_args()

app_file = args.app_file

if not file_exists(app_file):
Console().print(f"[bold red]Error: File '{app_file}' not found![/bold red]")
Expand All @@ -113,6 +138,7 @@ def main():
tkreload_app = TkreloadApp(app_file)
tkreload_app.start()


if __name__ == "__main__":
clear_terminal()
main()