pymenu-cli 1.0.6__tar.gz → 1.0.7__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.
- {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/PKG-INFO +84 -2
- {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/README.md +9 -1
- pymenu-cli-1.0.7/pymenu_cli/__init__.py +1 -0
- pymenu-cli-1.0.7/pymenu_cli/models/menu.py +158 -0
- {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/pymenu_cli/models/menu_item.py +30 -4
- {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/pymenu_cli/pymenu.py +49 -19
- {pymenu_cli-1.0.6/pymenu_cli/UI → pymenu-cli-1.0.7/pymenu_cli/ui}/styles.py +20 -2
- {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/pymenu_cli.egg-info/PKG-INFO +84 -2
- {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/pymenu_cli.egg-info/SOURCES.txt +7 -3
- {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/pymenu_cli.egg-info/top_level.txt +1 -0
- {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/setup.py +11 -4
- pymenu-cli-1.0.7/tests/test_menu.py +106 -0
- pymenu-cli-1.0.7/tests/test_menu_item.py +75 -0
- pymenu-cli-1.0.7/tests/test_pymenu.py +153 -0
- pymenu_cli-1.0.6/pymenu_cli/models/menu.py +0 -108
- {pymenu_cli-1.0.6/pymenu_cli/UI → pymenu-cli-1.0.7/pymenu_cli/models}/__init__.py +0 -0
- {pymenu_cli-1.0.6/pymenu_cli → pymenu-cli-1.0.7/pymenu_cli/ui}/__init__.py +0 -0
- {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/pymenu_cli.egg-info/dependency_links.txt +0 -0
- {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/pymenu_cli.egg-info/entry_points.txt +0 -0
- {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/pymenu_cli.egg-info/requires.txt +0 -0
- {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/setup.cfg +0 -0
- {pymenu_cli-1.0.6/pymenu_cli/models → pymenu-cli-1.0.7/tests}/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pymenu-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.7
|
|
4
4
|
Summary: A Python library for creating interactive CLI menus
|
|
5
5
|
Home-page: https://github.com/moraneus/pymenu-cli
|
|
6
6
|
Author: Moraneus
|
|
@@ -18,6 +18,13 @@ Requires-Dist: art
|
|
|
18
18
|
|
|
19
19
|
pymenu-cli is a Python library that simplifies the creation of interactive command-line interface (CLI) menus. It provides a convenient way to define hierarchical menu structures and associate actions with menu items.
|
|
20
20
|
|
|
21
|
+
<video width="320" height="240" autoplay loop muted>
|
|
22
|
+
<source src="docs/example.mp4" type="video/mp4">
|
|
23
|
+
Your browser does not support the video tag.
|
|
24
|
+
</video>
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
21
28
|
## Features
|
|
22
29
|
|
|
23
30
|
- Define menus and submenus using a simple JSON file format
|
|
@@ -42,8 +49,9 @@ pip install pymenu-cli
|
|
|
42
49
|
2. Implement the corresponding action functions in a separate Python file (`actions.py`)
|
|
43
50
|
|
|
44
51
|
### Using the Python API
|
|
52
|
+
|
|
45
53
|
```python
|
|
46
|
-
from pymenu_cli.
|
|
54
|
+
from pymenu_cli.menu import load_menu
|
|
47
55
|
|
|
48
56
|
# Define the 'menu' and the 'action' files
|
|
49
57
|
menu_file_path = 'menu.json'
|
|
@@ -275,3 +283,77 @@ python3 menu_example.py
|
|
|
275
283
|
|
|
276
284
|
### License
|
|
277
285
|
This project is licensed under the MIT License.
|
|
286
|
+
|
|
287
|
+
## Contributors
|
|
288
|
+
# Contributing to pymenu-cli
|
|
289
|
+
|
|
290
|
+
Thank you for considering contributing to pymenu-cli! We welcome all contributions, whether they are bug reports, feature requests, or code improvements. Please take a moment to review this document before submitting your contributions.
|
|
291
|
+
|
|
292
|
+
## How to Contribute
|
|
293
|
+
|
|
294
|
+
### Reporting Bugs
|
|
295
|
+
|
|
296
|
+
If you find a bug, please report it by [opening an issue](https://github.com/moraneus/pymenu-cli/issues). Include as much detail as possible to help us reproduce and fix the issue quickly. Make sure to include:
|
|
297
|
+
|
|
298
|
+
- A clear and descriptive title.
|
|
299
|
+
- A detailed description of the problem.
|
|
300
|
+
- Steps to reproduce the issue.
|
|
301
|
+
- Any relevant logs or screenshots.
|
|
302
|
+
|
|
303
|
+
### Suggesting Enhancements
|
|
304
|
+
|
|
305
|
+
We welcome suggestions for new features and enhancements. To suggest an enhancement, please [open an issue](https://github.com/moraneus/pymenu-cli/issues) and provide:
|
|
306
|
+
|
|
307
|
+
- A clear and descriptive title.
|
|
308
|
+
- A detailed description of the proposed enhancement.
|
|
309
|
+
- Any relevant examples or mockups.
|
|
310
|
+
|
|
311
|
+
### Submitting Pull Requests
|
|
312
|
+
|
|
313
|
+
To submit a pull request (PR), follow these steps:
|
|
314
|
+
|
|
315
|
+
1. **Fork the repository**: Click the "Fork" button at the top of this page to create a copy of the repository on your GitHub account.
|
|
316
|
+
|
|
317
|
+
2. **Clone your fork**: Clone the forked repository to your local machine using the following command:
|
|
318
|
+
```bash
|
|
319
|
+
git clone https://github.com/moraneus/pymenu-cli.git
|
|
320
|
+
cd pymenu-cli
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
3. **Create a new branch**: Create a new branch for your work. Use a descriptive name for the branch:
|
|
324
|
+
```bash
|
|
325
|
+
git checkout -b feature/my-new-feature
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
4. **Make your changes**: Make your changes in the new branch.
|
|
329
|
+
|
|
330
|
+
5. **Commit your changes**: Commit your changes with a clear and concise commit message:
|
|
331
|
+
```bash
|
|
332
|
+
git add .
|
|
333
|
+
git commit -m "Add feature: my new feature"
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
6. **Push to your fork**: Push your changes to your forked repository:
|
|
337
|
+
```bash
|
|
338
|
+
git push origin feature/my-new-feature
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
7. **Open a pull request**: Go to the original repository and open a pull request from your forked repository. Provide a clear and descriptive title and description for your PR.
|
|
342
|
+
|
|
343
|
+
### Code Style and Guidelines
|
|
344
|
+
|
|
345
|
+
- Follow the existing code style and conventions.
|
|
346
|
+
- Write clear and concise commit messages.
|
|
347
|
+
- Write tests for new features and bug fixes.
|
|
348
|
+
- Ensure your code passes all existing tests.
|
|
349
|
+
|
|
350
|
+
### Running Tests
|
|
351
|
+
|
|
352
|
+
Before submitting your PR, make sure all tests pass. You can run the tests using the following commands:
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
# Install dependencies
|
|
356
|
+
pip install -r requirements.txt
|
|
357
|
+
|
|
358
|
+
# Run tests
|
|
359
|
+
pytest
|
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
pymenu-cli is a Python library that simplifies the creation of interactive command-line interface (CLI) menus. It provides a convenient way to define hierarchical menu structures and associate actions with menu items.
|
|
8
8
|
|
|
9
|
+
<video width="320" height="240" autoplay loop muted>
|
|
10
|
+
<source src="docs/example.mp4" type="video/mp4">
|
|
11
|
+
Your browser does not support the video tag.
|
|
12
|
+
</video>
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
9
16
|
## Features
|
|
10
17
|
|
|
11
18
|
- Define menus and submenus using a simple JSON file format
|
|
@@ -30,8 +37,9 @@ pip install pymenu-cli
|
|
|
30
37
|
2. Implement the corresponding action functions in a separate Python file (`actions.py`)
|
|
31
38
|
|
|
32
39
|
### Using the Python API
|
|
40
|
+
|
|
33
41
|
```python
|
|
34
|
-
from pymenu_cli.
|
|
42
|
+
from pymenu_cli.menu import load_menu
|
|
35
43
|
|
|
36
44
|
# Define the 'menu' and the 'action' files
|
|
37
45
|
menu_file_path = 'menu.json'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .pymenu import main
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines the Menu class, which represents a text-based menu system.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Optional, List, Dict
|
|
8
|
+
|
|
9
|
+
import art
|
|
10
|
+
|
|
11
|
+
from pymenu_cli.ui.styles import Styles, TextColors, BackgroundColors
|
|
12
|
+
from pymenu_cli.models.menu_item import MenuItem
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Menu:
|
|
16
|
+
"""Represents a menu.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
__m_title (str): The title of the menu.
|
|
20
|
+
__m_items (List[MenuItem]): A list of items in the menu.
|
|
21
|
+
__m_actions (Optional[object]): An object containing callable actions.
|
|
22
|
+
__m_color (Optional[Dict]): The color settings for the menu title.
|
|
23
|
+
__m_banner (Optional[Dict]): The banner for the menu.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, i_title: str, i_config: Optional[Dict] = None):
|
|
27
|
+
"""
|
|
28
|
+
Initialize the Menu instance.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
i_title (str): The title of the menu.
|
|
32
|
+
i_config (Optional[Dict]): A dictionary containing optional settings for items,
|
|
33
|
+
actions, color, and banner.
|
|
34
|
+
"""
|
|
35
|
+
i_config = i_config or {}
|
|
36
|
+
self.__m_title = i_title
|
|
37
|
+
self.__m_items = i_config.get('items', [])
|
|
38
|
+
self.__m_actions = i_config.get('actions')
|
|
39
|
+
self.__m_color = i_config.get('color')
|
|
40
|
+
self.__m_banner = i_config.get('banner')
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def title(self) -> str:
|
|
44
|
+
"""
|
|
45
|
+
Gets the title of the menu item.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
str: The title of the menu item.
|
|
49
|
+
"""
|
|
50
|
+
return self.__m_title
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def color(self) -> Optional[Dict]:
|
|
54
|
+
"""
|
|
55
|
+
Gets the color settings of the menu item.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Optional[Dict]: The color settings of the menu item.
|
|
59
|
+
"""
|
|
60
|
+
return self.__m_color
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def items(self) -> List[MenuItem]:
|
|
64
|
+
"""
|
|
65
|
+
Gets the items in the menu.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
List[MenuItem]: A list of items in the menu.
|
|
69
|
+
"""
|
|
70
|
+
return self.__m_items
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def actions(self) -> Optional[object]:
|
|
74
|
+
"""
|
|
75
|
+
Gets the actions associated with the menu.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Optional[object]: An object containing callable actions.
|
|
79
|
+
"""
|
|
80
|
+
return self.__m_actions
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def banner(self) -> Optional[Dict]:
|
|
84
|
+
"""
|
|
85
|
+
Gets the banner for the menu.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Optional[Dict]: The banner for the menu.
|
|
89
|
+
"""
|
|
90
|
+
return self.__m_banner
|
|
91
|
+
|
|
92
|
+
def add_item(self, item: MenuItem) -> None:
|
|
93
|
+
"""Adds an item to the menu.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
item (MenuItem): The item to add.
|
|
97
|
+
"""
|
|
98
|
+
self.__m_items.append(item)
|
|
99
|
+
|
|
100
|
+
def display(self) -> None:
|
|
101
|
+
"""Displays the menu and handles user input."""
|
|
102
|
+
while True:
|
|
103
|
+
os.system('cls' if os.name == 'nt' else 'clear')
|
|
104
|
+
|
|
105
|
+
if self.__m_banner:
|
|
106
|
+
self.print_banner()
|
|
107
|
+
|
|
108
|
+
title_color = Menu.get_color_string(self.__m_color)
|
|
109
|
+
print(f"\n{title_color}{self.__m_title}{Styles.RESET_ALL}\n")
|
|
110
|
+
|
|
111
|
+
for i, item in enumerate(self.__m_items, start=1):
|
|
112
|
+
item_color = self.get_color_string(item.color)
|
|
113
|
+
print(f"{i}. {item_color}{item.title}{Styles.RESET_ALL}")
|
|
114
|
+
|
|
115
|
+
print("\nB. Back")
|
|
116
|
+
print("X. Exit")
|
|
117
|
+
|
|
118
|
+
choice = input("\nEnter your choice: ").upper()
|
|
119
|
+
|
|
120
|
+
if choice == 'B':
|
|
121
|
+
return
|
|
122
|
+
if choice == 'X':
|
|
123
|
+
sys.exit()
|
|
124
|
+
try:
|
|
125
|
+
index = int(choice) - 1
|
|
126
|
+
if 0 <= index < len(self.__m_items):
|
|
127
|
+
selected_item = self.__m_items[index]
|
|
128
|
+
if selected_item.submenu:
|
|
129
|
+
selected_item.submenu.display()
|
|
130
|
+
elif selected_item.action:
|
|
131
|
+
getattr(self.__m_actions, selected_item.action)()
|
|
132
|
+
else:
|
|
133
|
+
raise ValueError
|
|
134
|
+
except (ValueError, IndexError):
|
|
135
|
+
input("\nInvalid choice. Press Enter to try again.")
|
|
136
|
+
|
|
137
|
+
def print_banner(self) -> None:
|
|
138
|
+
"""Prints the banner using the art library."""
|
|
139
|
+
banner_text = self.__m_banner.get('title', '')
|
|
140
|
+
banner_font = self.__m_banner.get('font', 'standard')
|
|
141
|
+
banner = art.text2art(banner_text, font=banner_font, chr_ignore=True)
|
|
142
|
+
print(banner)
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def get_color_string(color: Optional[Dict]) -> str:
|
|
146
|
+
"""Gets the color string based on the provided color settings.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
color (Optional[Dict]): The color settings.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
str: The color string.
|
|
153
|
+
"""
|
|
154
|
+
if color:
|
|
155
|
+
text_color = getattr(TextColors, color.get('text', 'WHITE').upper())
|
|
156
|
+
background_color = getattr(BackgroundColors, color.get('background', 'BLACK').upper())
|
|
157
|
+
return f"{text_color}{background_color}"
|
|
158
|
+
return ""
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
""" This module defines the MenuItem class, which represents an item in a menu. """
|
|
2
|
+
|
|
1
3
|
from typing import Optional
|
|
2
4
|
|
|
3
5
|
|
|
4
6
|
class MenuItem:
|
|
5
|
-
"""
|
|
7
|
+
"""
|
|
8
|
+
Represents an item in a menu.
|
|
6
9
|
|
|
7
10
|
Attributes:
|
|
8
11
|
__m_title (str): The title of the menu item.
|
|
@@ -16,7 +19,8 @@ class MenuItem:
|
|
|
16
19
|
i_title: str,
|
|
17
20
|
i_action: Optional[str] = None,
|
|
18
21
|
i_submenu: Optional['Menu'] = None,
|
|
19
|
-
i_color: Optional[dict] = None
|
|
22
|
+
i_color: Optional[dict] = None
|
|
23
|
+
):
|
|
20
24
|
"""
|
|
21
25
|
Args:
|
|
22
26
|
i_title (str): The title of the menu item.
|
|
@@ -31,18 +35,40 @@ class MenuItem:
|
|
|
31
35
|
|
|
32
36
|
@property
|
|
33
37
|
def title(self) -> str:
|
|
38
|
+
"""
|
|
39
|
+
Gets the title of the menu item.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
str: The title of the menu item.
|
|
43
|
+
"""
|
|
34
44
|
return self.__m_title
|
|
35
45
|
|
|
36
46
|
@property
|
|
37
47
|
def color(self) -> Optional[dict]:
|
|
48
|
+
"""
|
|
49
|
+
Gets the color settings of the menu item.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Optional[dict]: The color settings of the menu item.
|
|
53
|
+
"""
|
|
38
54
|
return self.__m_color
|
|
39
55
|
|
|
40
56
|
@property
|
|
41
57
|
def action(self) -> Optional[str]:
|
|
58
|
+
"""
|
|
59
|
+
Gets the action associated with the menu item.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Optional[str]: The action associated with the menu item.
|
|
63
|
+
"""
|
|
42
64
|
return self.__m_action
|
|
43
65
|
|
|
44
66
|
@property
|
|
45
67
|
def submenu(self) -> Optional['Menu']:
|
|
46
|
-
|
|
47
|
-
|
|
68
|
+
"""
|
|
69
|
+
Gets the submenu associated with the menu item.
|
|
48
70
|
|
|
71
|
+
Returns:
|
|
72
|
+
Optional['Menu']: The submenu associated with the menu item.
|
|
73
|
+
"""
|
|
74
|
+
return self.__m_submenu
|
|
@@ -1,12 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides functionality to load and display menus from JSON files,
|
|
3
|
+
and to associate actions defined in a Python module with the menu items.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
|
|
1
9
|
import argparse
|
|
2
10
|
import importlib.util
|
|
3
11
|
import json
|
|
12
|
+
from typing import Dict
|
|
13
|
+
|
|
14
|
+
# Add the project root directory to the PYTHONPATH
|
|
15
|
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
16
|
+
|
|
4
17
|
from pymenu_cli.models.menu import Menu
|
|
5
18
|
from pymenu_cli.models.menu_item import MenuItem
|
|
6
19
|
|
|
7
20
|
|
|
8
21
|
def load_menu(file_path: str, actions_path: str) -> Menu:
|
|
9
|
-
"""
|
|
22
|
+
"""
|
|
23
|
+
Loads a menu from a JSON file.
|
|
10
24
|
|
|
11
25
|
Args:
|
|
12
26
|
file_path (str): The path to the JSON file.
|
|
@@ -21,23 +35,24 @@ def load_menu(file_path: str, actions_path: str) -> Menu:
|
|
|
21
35
|
FileNotFoundError: If the actions Python file is not found.
|
|
22
36
|
"""
|
|
23
37
|
try:
|
|
24
|
-
with open(file_path, 'r') as file:
|
|
38
|
+
with open(file_path, 'r', encoding='utf-8') as file:
|
|
25
39
|
menu_data = json.load(file)
|
|
26
|
-
except FileNotFoundError:
|
|
27
|
-
raise FileNotFoundError(f"Menu JSON file not found: {file_path}")
|
|
28
|
-
except json.JSONDecodeError as
|
|
29
|
-
raise json.JSONDecodeError(f"Invalid JSON format in menu file: {
|
|
40
|
+
except FileNotFoundError as exc:
|
|
41
|
+
raise FileNotFoundError(f"Menu JSON file not found: {file_path}") from exc
|
|
42
|
+
except json.JSONDecodeError as exc:
|
|
43
|
+
raise json.JSONDecodeError(f"Invalid JSON format in menu file: {exc.msg}", exc.doc, exc.pos)
|
|
30
44
|
|
|
31
45
|
try:
|
|
32
46
|
actions = load_actions_module(actions_path)
|
|
33
|
-
except FileNotFoundError:
|
|
34
|
-
raise FileNotFoundError(f"Actions Python file not found: {actions_path}")
|
|
47
|
+
except FileNotFoundError as exc:
|
|
48
|
+
raise FileNotFoundError(f"Actions Python file not found: {actions_path}") from exc
|
|
35
49
|
|
|
36
50
|
return create_menu_from_data(menu_data, actions)
|
|
37
51
|
|
|
38
52
|
|
|
39
|
-
def create_menu_from_data(menu_data:
|
|
40
|
-
"""
|
|
53
|
+
def create_menu_from_data(menu_data: Dict, actions: object) -> Menu:
|
|
54
|
+
"""
|
|
55
|
+
Creates a menu from dictionary data.
|
|
41
56
|
|
|
42
57
|
Args:
|
|
43
58
|
menu_data (dict): The menu data.
|
|
@@ -46,24 +61,40 @@ def create_menu_from_data(menu_data: dict, actions: object) -> Menu:
|
|
|
46
61
|
Returns:
|
|
47
62
|
Menu: The created menu.
|
|
48
63
|
"""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
64
|
+
config = {
|
|
65
|
+
'items': [],
|
|
66
|
+
'actions': actions,
|
|
67
|
+
'color': menu_data.get('color'),
|
|
68
|
+
'banner': menu_data.get('banner')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
menu = Menu(i_title=menu_data['title'], i_config=config)
|
|
72
|
+
|
|
54
73
|
for item_data in menu_data['items']:
|
|
55
74
|
if 'submenu' in item_data:
|
|
56
75
|
submenu = create_menu_from_data(item_data['submenu'], actions)
|
|
57
76
|
menu.add_item(
|
|
58
|
-
MenuItem(
|
|
77
|
+
MenuItem(
|
|
78
|
+
item_data['title'],
|
|
79
|
+
i_submenu=submenu,
|
|
80
|
+
i_color=item_data.get('color')
|
|
81
|
+
)
|
|
82
|
+
)
|
|
59
83
|
else:
|
|
60
84
|
menu.add_item(
|
|
61
|
-
MenuItem(
|
|
85
|
+
MenuItem(
|
|
86
|
+
item_data['title'],
|
|
87
|
+
i_action=item_data.get('action'),
|
|
88
|
+
i_color=item_data.get('color')
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
62
92
|
return menu
|
|
63
93
|
|
|
64
94
|
|
|
65
95
|
def load_actions_module(actions_path: str) -> object:
|
|
66
|
-
"""
|
|
96
|
+
"""
|
|
97
|
+
Loads an actions module from a file.
|
|
67
98
|
|
|
68
99
|
Args:
|
|
69
100
|
actions_path (str): The path to the actions Python file.
|
|
@@ -82,7 +113,6 @@ def main() -> None:
|
|
|
82
113
|
parser = argparse.ArgumentParser(description='pymenu-cli - Create interactive CLI menus')
|
|
83
114
|
parser.add_argument('-m', '--menu', type=str, help='Path to the menu JSON file')
|
|
84
115
|
parser.add_argument('-a', '--actions', type=str, help='Path to the actions Python file')
|
|
85
|
-
|
|
86
116
|
args = parser.parse_args()
|
|
87
117
|
|
|
88
118
|
if args.menu and args.actions:
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for defining text and background colors as well as text styles
|
|
3
|
+
using the colorama library.
|
|
4
|
+
"""
|
|
5
|
+
|
|
1
6
|
from enum import Enum
|
|
2
7
|
from colorama import Fore, Style, Back
|
|
3
8
|
|
|
4
9
|
|
|
5
|
-
# Used for returning the value from an enum as a string
|
|
6
10
|
class StrEnum(Enum):
|
|
11
|
+
"""
|
|
12
|
+
Enum class that returns the value as a string.
|
|
13
|
+
"""
|
|
14
|
+
|
|
7
15
|
def __str__(self):
|
|
8
16
|
return self.value
|
|
9
17
|
|
|
@@ -12,6 +20,9 @@ class StrEnum(Enum):
|
|
|
12
20
|
|
|
13
21
|
|
|
14
22
|
class TextColors(StrEnum):
|
|
23
|
+
"""
|
|
24
|
+
Enum for defining text colors using colorama's Fore class.
|
|
25
|
+
"""
|
|
15
26
|
RED = Fore.RED
|
|
16
27
|
LIGHT_RED = Fore.LIGHTRED_EX
|
|
17
28
|
BLUE = Fore.BLUE
|
|
@@ -31,6 +42,9 @@ class TextColors(StrEnum):
|
|
|
31
42
|
|
|
32
43
|
|
|
33
44
|
class BackgroundColors(StrEnum):
|
|
45
|
+
"""
|
|
46
|
+
Enum for defining background colors using colorama's Back class.
|
|
47
|
+
"""
|
|
34
48
|
RED = Back.RED
|
|
35
49
|
LIGHT_RED = Back.LIGHTRED_EX
|
|
36
50
|
BLUE = Back.BLUE
|
|
@@ -50,9 +64,13 @@ class BackgroundColors(StrEnum):
|
|
|
50
64
|
|
|
51
65
|
|
|
52
66
|
class Styles(StrEnum):
|
|
67
|
+
"""
|
|
68
|
+
Enum for defining text styles using colorama's Style class.
|
|
69
|
+
"""
|
|
53
70
|
BRIGHT = Style.BRIGHT
|
|
54
71
|
NORMAL = Style.NORMAL
|
|
55
72
|
DIM = Style.DIM
|
|
56
73
|
RESET_ALL = Style.RESET_ALL
|
|
57
74
|
|
|
58
|
-
#
|
|
75
|
+
# Example usage:
|
|
76
|
+
# print(f"{BackgroundColors.WHITE}{TextColors.BLACK}test_text{Styles.RESET_ALL}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pymenu-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.7
|
|
4
4
|
Summary: A Python library for creating interactive CLI menus
|
|
5
5
|
Home-page: https://github.com/moraneus/pymenu-cli
|
|
6
6
|
Author: Moraneus
|
|
@@ -18,6 +18,13 @@ Requires-Dist: art
|
|
|
18
18
|
|
|
19
19
|
pymenu-cli is a Python library that simplifies the creation of interactive command-line interface (CLI) menus. It provides a convenient way to define hierarchical menu structures and associate actions with menu items.
|
|
20
20
|
|
|
21
|
+
<video width="320" height="240" autoplay loop muted>
|
|
22
|
+
<source src="docs/example.mp4" type="video/mp4">
|
|
23
|
+
Your browser does not support the video tag.
|
|
24
|
+
</video>
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
21
28
|
## Features
|
|
22
29
|
|
|
23
30
|
- Define menus and submenus using a simple JSON file format
|
|
@@ -42,8 +49,9 @@ pip install pymenu-cli
|
|
|
42
49
|
2. Implement the corresponding action functions in a separate Python file (`actions.py`)
|
|
43
50
|
|
|
44
51
|
### Using the Python API
|
|
52
|
+
|
|
45
53
|
```python
|
|
46
|
-
from pymenu_cli.
|
|
54
|
+
from pymenu_cli.menu import load_menu
|
|
47
55
|
|
|
48
56
|
# Define the 'menu' and the 'action' files
|
|
49
57
|
menu_file_path = 'menu.json'
|
|
@@ -275,3 +283,77 @@ python3 menu_example.py
|
|
|
275
283
|
|
|
276
284
|
### License
|
|
277
285
|
This project is licensed under the MIT License.
|
|
286
|
+
|
|
287
|
+
## Contributors
|
|
288
|
+
# Contributing to pymenu-cli
|
|
289
|
+
|
|
290
|
+
Thank you for considering contributing to pymenu-cli! We welcome all contributions, whether they are bug reports, feature requests, or code improvements. Please take a moment to review this document before submitting your contributions.
|
|
291
|
+
|
|
292
|
+
## How to Contribute
|
|
293
|
+
|
|
294
|
+
### Reporting Bugs
|
|
295
|
+
|
|
296
|
+
If you find a bug, please report it by [opening an issue](https://github.com/moraneus/pymenu-cli/issues). Include as much detail as possible to help us reproduce and fix the issue quickly. Make sure to include:
|
|
297
|
+
|
|
298
|
+
- A clear and descriptive title.
|
|
299
|
+
- A detailed description of the problem.
|
|
300
|
+
- Steps to reproduce the issue.
|
|
301
|
+
- Any relevant logs or screenshots.
|
|
302
|
+
|
|
303
|
+
### Suggesting Enhancements
|
|
304
|
+
|
|
305
|
+
We welcome suggestions for new features and enhancements. To suggest an enhancement, please [open an issue](https://github.com/moraneus/pymenu-cli/issues) and provide:
|
|
306
|
+
|
|
307
|
+
- A clear and descriptive title.
|
|
308
|
+
- A detailed description of the proposed enhancement.
|
|
309
|
+
- Any relevant examples or mockups.
|
|
310
|
+
|
|
311
|
+
### Submitting Pull Requests
|
|
312
|
+
|
|
313
|
+
To submit a pull request (PR), follow these steps:
|
|
314
|
+
|
|
315
|
+
1. **Fork the repository**: Click the "Fork" button at the top of this page to create a copy of the repository on your GitHub account.
|
|
316
|
+
|
|
317
|
+
2. **Clone your fork**: Clone the forked repository to your local machine using the following command:
|
|
318
|
+
```bash
|
|
319
|
+
git clone https://github.com/moraneus/pymenu-cli.git
|
|
320
|
+
cd pymenu-cli
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
3. **Create a new branch**: Create a new branch for your work. Use a descriptive name for the branch:
|
|
324
|
+
```bash
|
|
325
|
+
git checkout -b feature/my-new-feature
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
4. **Make your changes**: Make your changes in the new branch.
|
|
329
|
+
|
|
330
|
+
5. **Commit your changes**: Commit your changes with a clear and concise commit message:
|
|
331
|
+
```bash
|
|
332
|
+
git add .
|
|
333
|
+
git commit -m "Add feature: my new feature"
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
6. **Push to your fork**: Push your changes to your forked repository:
|
|
337
|
+
```bash
|
|
338
|
+
git push origin feature/my-new-feature
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
7. **Open a pull request**: Go to the original repository and open a pull request from your forked repository. Provide a clear and descriptive title and description for your PR.
|
|
342
|
+
|
|
343
|
+
### Code Style and Guidelines
|
|
344
|
+
|
|
345
|
+
- Follow the existing code style and conventions.
|
|
346
|
+
- Write clear and concise commit messages.
|
|
347
|
+
- Write tests for new features and bug fixes.
|
|
348
|
+
- Ensure your code passes all existing tests.
|
|
349
|
+
|
|
350
|
+
### Running Tests
|
|
351
|
+
|
|
352
|
+
Before submitting your PR, make sure all tests pass. You can run the tests using the following commands:
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
# Install dependencies
|
|
356
|
+
pip install -r requirements.txt
|
|
357
|
+
|
|
358
|
+
# Run tests
|
|
359
|
+
pytest
|
|
@@ -8,8 +8,12 @@ pymenu_cli.egg-info/dependency_links.txt
|
|
|
8
8
|
pymenu_cli.egg-info/entry_points.txt
|
|
9
9
|
pymenu_cli.egg-info/requires.txt
|
|
10
10
|
pymenu_cli.egg-info/top_level.txt
|
|
11
|
-
pymenu_cli/UI/__init__.py
|
|
12
|
-
pymenu_cli/UI/styles.py
|
|
13
11
|
pymenu_cli/models/__init__.py
|
|
14
12
|
pymenu_cli/models/menu.py
|
|
15
|
-
pymenu_cli/models/menu_item.py
|
|
13
|
+
pymenu_cli/models/menu_item.py
|
|
14
|
+
pymenu_cli/ui/__init__.py
|
|
15
|
+
pymenu_cli/ui/styles.py
|
|
16
|
+
tests/__init__.py
|
|
17
|
+
tests/test_menu.py
|
|
18
|
+
tests/test_menu_item.py
|
|
19
|
+
tests/test_pymenu.py
|
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
"""Module for setting up the pymenu-cli package."""
|
|
2
|
+
|
|
2
3
|
from setuptools import setup, find_packages
|
|
3
4
|
|
|
5
|
+
# Read the contents of the README and CONTRIBUTORS files
|
|
4
6
|
with open("README.md", "r", encoding="utf-8") as fh:
|
|
5
|
-
|
|
7
|
+
readme = fh.read()
|
|
8
|
+
|
|
9
|
+
with open("CONTRIBUTING.md", "r", encoding="utf-8") as fh:
|
|
10
|
+
contributing = fh.read()
|
|
11
|
+
|
|
12
|
+
long_description = f"{readme}\n\n## Contributors\n{contributing}"
|
|
6
13
|
|
|
7
14
|
setup(
|
|
8
15
|
name='pymenu-cli',
|
|
9
|
-
version='1.0.
|
|
16
|
+
version='1.0.7',
|
|
10
17
|
description='A Python library for creating interactive CLI menus',
|
|
11
18
|
long_description=long_description,
|
|
12
19
|
long_description_content_type="text/markdown",
|
|
@@ -24,4 +31,4 @@ setup(
|
|
|
24
31
|
]
|
|
25
32
|
},
|
|
26
33
|
license='MIT',
|
|
27
|
-
)
|
|
34
|
+
)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import patch, Mock
|
|
3
|
+
|
|
4
|
+
from pymenu_cli.models.menu import Menu
|
|
5
|
+
from pymenu_cli.models.menu_item import MenuItem
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Tests for the Menu class
|
|
9
|
+
def test_menu_init():
|
|
10
|
+
"""
|
|
11
|
+
Test that the Menu instance is correctly initialized with the
|
|
12
|
+
provided title and configuration.
|
|
13
|
+
"""
|
|
14
|
+
title = "Main Menu"
|
|
15
|
+
config = {
|
|
16
|
+
"items": [],
|
|
17
|
+
"actions": Mock(),
|
|
18
|
+
"color": {"text": "red", "background": "blue"},
|
|
19
|
+
"banner": {"title": "Banner", "font": "standard"},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
menu = Menu(title, i_config=config)
|
|
23
|
+
|
|
24
|
+
assert menu.title == title
|
|
25
|
+
assert menu.items == config["items"]
|
|
26
|
+
assert menu.actions == config["actions"]
|
|
27
|
+
assert menu.color == config["color"]
|
|
28
|
+
assert menu.banner == config["banner"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_menu_add_item():
|
|
32
|
+
"""
|
|
33
|
+
Test that the add_item method of the Menu class correctly adds
|
|
34
|
+
a new MenuItem to the menu.
|
|
35
|
+
"""
|
|
36
|
+
menu = Menu("Test Menu")
|
|
37
|
+
item = MenuItem("Item 1")
|
|
38
|
+
|
|
39
|
+
menu.add_item(item)
|
|
40
|
+
|
|
41
|
+
assert len(menu.items) == 1
|
|
42
|
+
assert menu.items[0] == item
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_menu_display(capsys, monkeypatch):
|
|
46
|
+
"""
|
|
47
|
+
Test that the display method of the Menu class correctly displays
|
|
48
|
+
the menu and handles user input.
|
|
49
|
+
"""
|
|
50
|
+
actions = Mock() # Create a Mock object for actions
|
|
51
|
+
|
|
52
|
+
menu = Menu("Test Menu", i_config={"actions": actions})
|
|
53
|
+
item1 = MenuItem("Item 1", i_action="action1")
|
|
54
|
+
item2 = MenuItem("Item 2", i_submenu=Menu("Submenu"))
|
|
55
|
+
menu.add_item(item1)
|
|
56
|
+
menu.add_item(item2)
|
|
57
|
+
|
|
58
|
+
# Mock the user input
|
|
59
|
+
user_inputs = iter(["1", "2", "B"])
|
|
60
|
+
monkeypatch.setattr("builtins.input", lambda _: next(user_inputs))
|
|
61
|
+
|
|
62
|
+
# Mock the actions and submenu.display methods
|
|
63
|
+
mock_action1 = Mock()
|
|
64
|
+
mock_submenu_display = Mock()
|
|
65
|
+
actions.action1 = mock_action1 # Assign the Mock to actions.action1
|
|
66
|
+
menu.items[1].submenu.display = mock_submenu_display
|
|
67
|
+
|
|
68
|
+
# Call the display method
|
|
69
|
+
menu.display()
|
|
70
|
+
|
|
71
|
+
# Assert that the actions and submenu.display methods were called
|
|
72
|
+
mock_action1.assert_called_once()
|
|
73
|
+
mock_submenu_display.assert_called_once()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_menu_get_color_string():
|
|
77
|
+
"""
|
|
78
|
+
Test that the get_color_string method of the Menu class
|
|
79
|
+
correctly returns the color string based on the provided color settings.
|
|
80
|
+
"""
|
|
81
|
+
# Test with valid color settings
|
|
82
|
+
color = {"text": "red", "background": "blue"}
|
|
83
|
+
expected_color_string = "\x1b[31m\x1b[44m"
|
|
84
|
+
assert Menu.get_color_string(color) == expected_color_string
|
|
85
|
+
|
|
86
|
+
# Test with invalid color settings
|
|
87
|
+
color = {"text": "invalid", "background": "invalid"}
|
|
88
|
+
with pytest.raises(AttributeError):
|
|
89
|
+
Menu.get_color_string(color)
|
|
90
|
+
|
|
91
|
+
# Test with no color settings
|
|
92
|
+
assert Menu.get_color_string(None) == ""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_menu_print_banner(capsys):
|
|
96
|
+
"""
|
|
97
|
+
Test that the print_banner method of the Menu class
|
|
98
|
+
correctly prints the banner using the art library.
|
|
99
|
+
"""
|
|
100
|
+
menu = Menu("Test Menu", i_config={"banner": {"title": "Banner Text", "font": "standard"}})
|
|
101
|
+
|
|
102
|
+
# Mock the art.text2art function
|
|
103
|
+
with patch("art.text2art", return_value="ASCII_ART"):
|
|
104
|
+
menu.print_banner()
|
|
105
|
+
captured = capsys.readouterr()
|
|
106
|
+
assert captured.out == "ASCII_ART\n"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from pymenu_cli.models.menu_item import MenuItem
|
|
2
|
+
from pymenu_cli.models.menu import Menu
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# Tests for the MenuItem class
|
|
6
|
+
def test_menu_item_init():
|
|
7
|
+
"""
|
|
8
|
+
Test that the MenuItem instance is correctly initialized with the
|
|
9
|
+
provided title, action, submenu, and color settings.
|
|
10
|
+
"""
|
|
11
|
+
title = "Menu Item"
|
|
12
|
+
action = "action_name"
|
|
13
|
+
submenu = Menu("Submenu")
|
|
14
|
+
color = {"text": "green", "background": "black"}
|
|
15
|
+
|
|
16
|
+
item = MenuItem(title, action, submenu, color)
|
|
17
|
+
|
|
18
|
+
assert item.title == title
|
|
19
|
+
assert item.action == action
|
|
20
|
+
assert item.submenu == submenu
|
|
21
|
+
assert item.color == color
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_menu_item_properties():
|
|
25
|
+
"""
|
|
26
|
+
Test that the properties of the MenuItem class
|
|
27
|
+
return the correct values.
|
|
28
|
+
"""
|
|
29
|
+
title = "Menu Item"
|
|
30
|
+
action = "action_name"
|
|
31
|
+
submenu = Menu("Submenu")
|
|
32
|
+
color = {"text": "green", "background": "black"}
|
|
33
|
+
|
|
34
|
+
item = MenuItem(title, action, submenu, color)
|
|
35
|
+
|
|
36
|
+
assert item.title == title
|
|
37
|
+
assert item.action == action
|
|
38
|
+
assert item.submenu == submenu
|
|
39
|
+
assert item.color == color
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_menu_item_with_optional_args():
|
|
43
|
+
"""
|
|
44
|
+
Test that the MenuItem instance is correctly initialized
|
|
45
|
+
when some arguments are not provided.
|
|
46
|
+
"""
|
|
47
|
+
title = "Menu Item"
|
|
48
|
+
|
|
49
|
+
# No action, submenu, or color
|
|
50
|
+
item = MenuItem(title)
|
|
51
|
+
assert item.title == title
|
|
52
|
+
assert item.action is None
|
|
53
|
+
assert item.submenu is None
|
|
54
|
+
assert item.color is None
|
|
55
|
+
|
|
56
|
+
# Only action
|
|
57
|
+
item = MenuItem(title, i_action="action_name")
|
|
58
|
+
assert item.title == title
|
|
59
|
+
assert item.action == "action_name"
|
|
60
|
+
assert item.submenu is None
|
|
61
|
+
assert item.color is None
|
|
62
|
+
|
|
63
|
+
# Only submenu
|
|
64
|
+
item = MenuItem(title, i_submenu=Menu("Submenu"))
|
|
65
|
+
assert item.title == title
|
|
66
|
+
assert item.action is None
|
|
67
|
+
assert isinstance(item.submenu, Menu)
|
|
68
|
+
assert item.color is None
|
|
69
|
+
|
|
70
|
+
# Only color
|
|
71
|
+
item = MenuItem(title, i_color={"text": "red"})
|
|
72
|
+
assert item.title == title
|
|
73
|
+
assert item.action is None
|
|
74
|
+
assert item.submenu is None
|
|
75
|
+
assert item.color == {"text": "red"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import pytest
|
|
3
|
+
from unittest.mock import Mock, patch
|
|
4
|
+
|
|
5
|
+
from pymenu_cli.pymenu import load_menu, create_menu_from_data, load_actions_module, main
|
|
6
|
+
from pymenu_cli.models.menu import Menu
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Tests for load_menu function
|
|
10
|
+
def test_load_menu_with_valid_paths(tmp_path):
|
|
11
|
+
"""
|
|
12
|
+
Test that the load_menu function correctly loads a menu from a JSON file
|
|
13
|
+
and associates actions from a Python module when provided with valid file paths.
|
|
14
|
+
"""
|
|
15
|
+
# Create temporary files
|
|
16
|
+
menu_file = tmp_path / "menu.json"
|
|
17
|
+
actions_file = tmp_path / "actions.py"
|
|
18
|
+
|
|
19
|
+
# Write sample data to the temporary files
|
|
20
|
+
menu_data = {"title": "Main Menu", "items": [{"title": "Item 1"}, {"title": "Item 2"}]}
|
|
21
|
+
with open(menu_file, "w", encoding="utf-8") as f:
|
|
22
|
+
json.dump(menu_data, f)
|
|
23
|
+
|
|
24
|
+
with open(actions_file, "w", encoding="utf-8") as f:
|
|
25
|
+
f.write("def action1(): pass\ndef action2(): pass")
|
|
26
|
+
|
|
27
|
+
# Call the load_menu function
|
|
28
|
+
menu = load_menu(str(menu_file), str(actions_file))
|
|
29
|
+
|
|
30
|
+
# Assert that the menu and its items were loaded correctly
|
|
31
|
+
assert isinstance(menu, Menu)
|
|
32
|
+
assert menu.title == "Main Menu"
|
|
33
|
+
assert len(menu.items) == 2
|
|
34
|
+
assert menu.items[0].title == "Item 1"
|
|
35
|
+
assert menu.items[1].title == "Item 2"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_load_menu_with_missing_files():
|
|
39
|
+
"""
|
|
40
|
+
Test that the load_menu function raises a FileNotFoundError when the
|
|
41
|
+
provided file paths are invalid or the files are missing.
|
|
42
|
+
"""
|
|
43
|
+
with pytest.raises(FileNotFoundError):
|
|
44
|
+
load_menu("invalid_path.json", "actions.py")
|
|
45
|
+
|
|
46
|
+
with pytest.raises(FileNotFoundError):
|
|
47
|
+
load_menu("menu.json", "invalid_path.py")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Tests for create_menu_from_data function
|
|
51
|
+
def test_create_menu_from_data():
|
|
52
|
+
"""
|
|
53
|
+
Test that the create_menu_from_data function correctly creates a Menu object
|
|
54
|
+
and its associated MenuItem objects from a dictionary of menu data.
|
|
55
|
+
"""
|
|
56
|
+
menu_data = {
|
|
57
|
+
"title": "Main Menu",
|
|
58
|
+
"items": [
|
|
59
|
+
{"title": "Item 1", "action": "action1"},
|
|
60
|
+
{"title": "Item 2", "submenu": {"title": "Submenu", "items": [{"title": "Subitem 1"}]}},
|
|
61
|
+
],
|
|
62
|
+
}
|
|
63
|
+
actions = Mock()
|
|
64
|
+
actions.action1 = Mock()
|
|
65
|
+
|
|
66
|
+
menu = create_menu_from_data(menu_data, actions)
|
|
67
|
+
|
|
68
|
+
assert isinstance(menu, Menu)
|
|
69
|
+
assert menu.title == "Main Menu"
|
|
70
|
+
assert len(menu.items) == 2
|
|
71
|
+
assert menu.items[0].title == "Item 1"
|
|
72
|
+
assert menu.items[0].action == "action1"
|
|
73
|
+
assert menu.items[1].title == "Item 2"
|
|
74
|
+
assert isinstance(menu.items[1].submenu, Menu)
|
|
75
|
+
assert menu.items[1].submenu.title == "Submenu"
|
|
76
|
+
assert len(menu.items[1].submenu.items) == 1
|
|
77
|
+
assert menu.items[1].submenu.items[0].title == "Subitem 1"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Tests for load_actions_module function
|
|
81
|
+
def test_load_actions_module(tmp_path):
|
|
82
|
+
"""
|
|
83
|
+
Test that the load_actions_module function correctly loads a Python module
|
|
84
|
+
from a file and returns an object containing the module's callable functions.
|
|
85
|
+
"""
|
|
86
|
+
actions_file = tmp_path / "actions.py"
|
|
87
|
+
|
|
88
|
+
with open(actions_file, "w", encoding="utf-8") as f:
|
|
89
|
+
f.write("def action1(): pass\ndef action2(): pass")
|
|
90
|
+
|
|
91
|
+
actions = load_actions_module(str(actions_file))
|
|
92
|
+
|
|
93
|
+
assert hasattr(actions, "action1")
|
|
94
|
+
assert hasattr(actions, "action2")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_load_actions_module_with_missing_file():
|
|
98
|
+
"""
|
|
99
|
+
Test that the load_actions_module function raises a FileNotFoundError
|
|
100
|
+
when the provided file path is invalid or the file is missing.
|
|
101
|
+
"""
|
|
102
|
+
with pytest.raises(FileNotFoundError):
|
|
103
|
+
load_actions_module("invalid_path.py")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# Tests for the main function
|
|
107
|
+
def test_main_with_valid_args(tmp_path, monkeypatch, capsys):
|
|
108
|
+
"""
|
|
109
|
+
Test that the main function correctly loads and displays the menu
|
|
110
|
+
when provided with valid command-line arguments.
|
|
111
|
+
"""
|
|
112
|
+
# Create temporary files
|
|
113
|
+
menu_file = tmp_path / "menu.json"
|
|
114
|
+
actions_file = tmp_path / "actions.py"
|
|
115
|
+
|
|
116
|
+
# Write sample data to the temporary files
|
|
117
|
+
menu_data = {"title": "Main Menu", "items": [{"title": "Item 1"}, {"title": "Item 2"}]}
|
|
118
|
+
with open(menu_file, "w", encoding="utf-8") as f:
|
|
119
|
+
json.dump(menu_data, f)
|
|
120
|
+
|
|
121
|
+
with open(actions_file, "w", encoding="utf-8") as f:
|
|
122
|
+
f.write("def action1(): pass\ndef action2(): pass")
|
|
123
|
+
|
|
124
|
+
# Mock the display method of the Menu class
|
|
125
|
+
with patch.object(Menu, "display") as mock_display:
|
|
126
|
+
# Mock the argparse.ArgumentParser.parse_args method
|
|
127
|
+
monkeypatch.setattr("argparse.ArgumentParser.parse_args",
|
|
128
|
+
lambda self: Mock(menu=str(menu_file), actions=str(actions_file)))
|
|
129
|
+
|
|
130
|
+
# Call the main function
|
|
131
|
+
main()
|
|
132
|
+
|
|
133
|
+
# Assert that the display method was called
|
|
134
|
+
mock_display.assert_called_once()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_main_with_missing_args(capsys):
|
|
138
|
+
"""
|
|
139
|
+
Test that the main function prints the help message
|
|
140
|
+
when command-line arguments are missing.
|
|
141
|
+
"""
|
|
142
|
+
# Mock the argparse.ArgumentParser.parse_args method
|
|
143
|
+
with patch("argparse.ArgumentParser.parse_args", return_value=Mock(menu=None, actions=None)):
|
|
144
|
+
with patch("argparse.ArgumentParser.print_help") as mock_print_help:
|
|
145
|
+
# Call the main function
|
|
146
|
+
main()
|
|
147
|
+
|
|
148
|
+
# Assert that the print_help method was called
|
|
149
|
+
mock_print_help.assert_called_once()
|
|
150
|
+
|
|
151
|
+
# Capture the printed output
|
|
152
|
+
captured = capsys.readouterr()
|
|
153
|
+
assert captured.out == "" # No output should be printed
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from typing import Optional, List
|
|
3
|
-
|
|
4
|
-
import art
|
|
5
|
-
|
|
6
|
-
from pymenu_cli.UI.styles import Styles, TextColors, BackgroundColors
|
|
7
|
-
from pymenu_cli.models.menu_item import MenuItem
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class Menu:
|
|
11
|
-
"""Represents a menu.
|
|
12
|
-
|
|
13
|
-
Attributes:
|
|
14
|
-
__m_title (str): The title of the menu.
|
|
15
|
-
__m_items (List[MenuItem]): A list of items in the menu.
|
|
16
|
-
__m_actions (Optional[object]): An object containing callable actions.
|
|
17
|
-
__m_color (Optional[dict]): The color settings for the menu title.
|
|
18
|
-
__m_banner (Optional[dict]): The banner for the menu.
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
def __init__(
|
|
22
|
-
self,
|
|
23
|
-
i_title: str,
|
|
24
|
-
i_items: Optional[List[MenuItem]] = None,
|
|
25
|
-
i_actions: Optional[object] = None,
|
|
26
|
-
i_color: Optional[dict] = None,
|
|
27
|
-
i_banner: Optional[dict] = None):
|
|
28
|
-
"""
|
|
29
|
-
Args:
|
|
30
|
-
i_title (str): The title of the menu.
|
|
31
|
-
i_items (Optional[List[MenuItem]]): A list of items in the menu. Defaults to None.
|
|
32
|
-
i_actions (Optional[object]): An object containing callable actions. Defaults to None.
|
|
33
|
-
i_color (Optional[dict]): The color settings for the menu title. Defaults to None.
|
|
34
|
-
i_banner (Optional[dict]): The banner for the menu. Defaults to None.
|
|
35
|
-
"""
|
|
36
|
-
self.__m_title = i_title
|
|
37
|
-
self.__m_items = i_items or []
|
|
38
|
-
self.__m_actions = i_actions
|
|
39
|
-
self.__m_color = i_color
|
|
40
|
-
self.__m_banner = i_banner
|
|
41
|
-
|
|
42
|
-
def add_item(self, i_item: MenuItem) -> None:
|
|
43
|
-
"""Adds an item to the menu.
|
|
44
|
-
|
|
45
|
-
Args:
|
|
46
|
-
i_item (MenuItem): The item to add.
|
|
47
|
-
"""
|
|
48
|
-
self.__m_items.append(i_item)
|
|
49
|
-
|
|
50
|
-
def display(self) -> None:
|
|
51
|
-
"""Displays the menu and handles user input."""
|
|
52
|
-
while True:
|
|
53
|
-
os.system('cls' if os.name == 'nt' else 'clear')
|
|
54
|
-
|
|
55
|
-
if self.__m_banner:
|
|
56
|
-
self.print_banner()
|
|
57
|
-
|
|
58
|
-
title_color = Menu.get_color_string(self.__m_color)
|
|
59
|
-
print(f"\n{title_color}{self.__m_title}{Styles.RESET_ALL}\n")
|
|
60
|
-
|
|
61
|
-
for i, item in enumerate(self.__m_items, start=1):
|
|
62
|
-
item_color = self.get_color_string(item.color)
|
|
63
|
-
print(f"{i}. {item_color}{item.title}{Styles.RESET_ALL}")
|
|
64
|
-
|
|
65
|
-
print("\nB. Back")
|
|
66
|
-
print("X. Exit")
|
|
67
|
-
|
|
68
|
-
choice = input("\nEnter your choice: ").upper()
|
|
69
|
-
|
|
70
|
-
if choice == 'B':
|
|
71
|
-
return
|
|
72
|
-
elif choice == 'X':
|
|
73
|
-
exit()
|
|
74
|
-
else:
|
|
75
|
-
try:
|
|
76
|
-
index = int(choice) - 1
|
|
77
|
-
if 0 <= index < len(self.__m_items):
|
|
78
|
-
selected_item = self.__m_items[index]
|
|
79
|
-
if selected_item.submenu:
|
|
80
|
-
selected_item.submenu.display()
|
|
81
|
-
elif selected_item.action:
|
|
82
|
-
getattr(self.__m_actions, selected_item.action)()
|
|
83
|
-
else:
|
|
84
|
-
raise ValueError
|
|
85
|
-
except (ValueError, IndexError):
|
|
86
|
-
input("\nInvalid choice. Press Enter to try again.")
|
|
87
|
-
|
|
88
|
-
def print_banner(self) -> None:
|
|
89
|
-
banner_text = self.__m_banner.get('title', '')
|
|
90
|
-
banner_font = self.__m_banner.get('font', 'standard')
|
|
91
|
-
banner = art.text2art(banner_text, font=banner_font, chr_ignore=True)
|
|
92
|
-
print(banner)
|
|
93
|
-
|
|
94
|
-
@staticmethod
|
|
95
|
-
def get_color_string(i_color: Optional[dict]) -> str:
|
|
96
|
-
"""Gets the color string based on the provided color settings.
|
|
97
|
-
|
|
98
|
-
Args:
|
|
99
|
-
i_color (Optional[dict]): The color settings.
|
|
100
|
-
|
|
101
|
-
Returns:
|
|
102
|
-
str: The color string.
|
|
103
|
-
"""
|
|
104
|
-
if i_color:
|
|
105
|
-
text_color = getattr(TextColors, i_color.get('text', 'WHITE').upper())
|
|
106
|
-
background_color = getattr(BackgroundColors, i_color.get('background', 'BLACK').upper())
|
|
107
|
-
return f"{text_color}{background_color}"
|
|
108
|
-
return ""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|