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.
Files changed (22) hide show
  1. {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/PKG-INFO +84 -2
  2. {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/README.md +9 -1
  3. pymenu-cli-1.0.7/pymenu_cli/__init__.py +1 -0
  4. pymenu-cli-1.0.7/pymenu_cli/models/menu.py +158 -0
  5. {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/pymenu_cli/models/menu_item.py +30 -4
  6. {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/pymenu_cli/pymenu.py +49 -19
  7. {pymenu_cli-1.0.6/pymenu_cli/UI → pymenu-cli-1.0.7/pymenu_cli/ui}/styles.py +20 -2
  8. {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/pymenu_cli.egg-info/PKG-INFO +84 -2
  9. {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/pymenu_cli.egg-info/SOURCES.txt +7 -3
  10. {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/pymenu_cli.egg-info/top_level.txt +1 -0
  11. {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/setup.py +11 -4
  12. pymenu-cli-1.0.7/tests/test_menu.py +106 -0
  13. pymenu-cli-1.0.7/tests/test_menu_item.py +75 -0
  14. pymenu-cli-1.0.7/tests/test_pymenu.py +153 -0
  15. pymenu_cli-1.0.6/pymenu_cli/models/menu.py +0 -108
  16. {pymenu_cli-1.0.6/pymenu_cli/UI → pymenu-cli-1.0.7/pymenu_cli/models}/__init__.py +0 -0
  17. {pymenu_cli-1.0.6/pymenu_cli → pymenu-cli-1.0.7/pymenu_cli/ui}/__init__.py +0 -0
  18. {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/pymenu_cli.egg-info/dependency_links.txt +0 -0
  19. {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/pymenu_cli.egg-info/entry_points.txt +0 -0
  20. {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/pymenu_cli.egg-info/requires.txt +0 -0
  21. {pymenu_cli-1.0.6 → pymenu-cli-1.0.7}/setup.cfg +0 -0
  22. {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.6
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.pymenu import load_menu
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.pymenu import load_menu
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
- """Represents an item in a menu.
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
- return self.__m_submenu
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
- """Loads a menu from a JSON file.
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 e:
29
- raise json.JSONDecodeError(f"Invalid JSON format in menu file: {e}", e.doc, e.pos)
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: dict, actions: object) -> Menu:
40
- """Creates a menu from dictionary data.
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
- menu = Menu(
50
- menu_data['title'],
51
- i_actions=actions,
52
- i_color=menu_data.get('color'),
53
- i_banner=menu_data.get('banner'))
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(item_data['title'], i_submenu=submenu, i_color=item_data.get('color')))
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(item_data['title'], i_action=item_data.get('action'), i_color=item_data.get('color')))
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
- """Loads an actions module from a file.
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
- # print(f"{BackgroundColors.WHITE}{TextColors.BLACK}sdfsdfsdfsdf{Styles.RESET_ALL}")
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.6
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.pymenu import load_menu
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
- import colorama
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
- long_description = fh.read()
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.6',
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