From e8ac4e0e1251bac2c186a13382e8ca7f237a6518 Mon Sep 17 00:00:00 2001 From: rjdbcm Date: Sat, 22 Jun 2024 01:10:48 -0500 Subject: [PATCH] :children_crossing: enhanced prompt with menu Signed-off-by: rjdbcm --- ozi/new/__main__.py | 8 +- ozi/new/interactive.py | 517 ++++++++++++++++++++++++++++++----------- 2 files changed, 384 insertions(+), 141 deletions(-) diff --git a/ozi/new/__main__.py b/ozi/new/__main__.py index 27907be1..ddad8dac 100644 --- a/ozi/new/__main__.py +++ b/ozi/new/__main__.py @@ -134,13 +134,13 @@ def wrap(project: Namespace) -> None: # pragma: no cover f.write(template.render()) -def main() -> None: # pragma: no cover +def main(args: list[str] | None = None) -> None: # pragma: no cover """Main ozi.new entrypoint.""" pipe = sys.stdin if not sys.stdin.isatty() else None args = ( list(chain.from_iterable([shlex.split(line.strip()) for line in pipe])) if pipe - else None + else args ) ozi_new = parser.parse_args(args=args) ozi_new.argv = args if args else shlex.join(sys.argv[1:]) @@ -148,9 +148,7 @@ def main() -> None: # pragma: no cover case ozi_new if ozi_new.new in ['i', 'interactive']: args = interactive_prompt() ozi_new = parser.parse_args(args=args) - ozi_new.argv = args - project(ozi_new) - TAP.end() + main(args) case ozi_new if ozi_new.new in ['p', 'project']: project(ozi_new) TAP.end() diff --git a/ozi/new/interactive.py b/ozi/new/interactive.py index 7ad087da..5780730a 100644 --- a/ozi/new/interactive.py +++ b/ozi/new/interactive.py @@ -5,13 +5,14 @@ from __future__ import annotations import re -from typing import TYPE_CHECKING from typing import Sequence import requests +from prompt_toolkit.document import Document from prompt_toolkit.shortcuts import button_dialog from prompt_toolkit.shortcuts import checkboxlist_dialog from prompt_toolkit.shortcuts import input_dialog +from prompt_toolkit.shortcuts import message_dialog from prompt_toolkit.shortcuts import radiolist_dialog from prompt_toolkit.shortcuts import yes_no_dialog from prompt_toolkit.styles import Style @@ -22,26 +23,25 @@ from ozi.trove import Prefix from ozi.trove import from_prefix -if TYPE_CHECKING: - from prompt_toolkit.document import Document - class ProjectNameValidator(Validator): def validate(self, document: Document) -> None: # pragma: no cover # noqa: ANN101 + if len(document.text) == 0: + raise ValidationError(0, 'cannot be empty') if not re.match( - r'\A([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])\Z', + '^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$', document.text, flags=re.IGNORECASE, ): raise ValidationError(0, 'invalid project name') -class SummaryValidator(Validator): +class LengthValidator(Validator): def validate(self, document: Document) -> None: # pragma: no cover # noqa: ANN101 - if len(document.text) < 1: - raise ValidationError(0, 'summary must not be empty') + if len(document.text) == 0: + raise ValidationError(0, 'must not be empty') if len(document.text) > 512: - raise ValidationError(512, 'summary is too long') + raise ValidationError(512, 'input is too long') class PackageValidator(Validator): @@ -74,89 +74,388 @@ def validate(self, document: Document) -> None: # pragma: no cover # noqa: ANN ) +def validate_message(text: str, validator: Validator) -> bool: # pragma: no cover + """Validate a string. + + :param text: string to validate + :type text: str + :param validator: validator instance + :type validator: Validator + :return: False if invalid + :rtype: bool + """ + try: + validator.validate(Document(text)) + except ValidationError: + return False + return True + + def interactive_prompt() -> list[str]: # noqa: C901 # pragma: no cover - output = ['project'] - project_name = input_dialog( + menu = button_dialog( title='ozi-new interactive prompt', - text='What is the name of the project?', - validator=ProjectNameValidator(), + text='Select an option:', + buttons=[ + ('Back', True), + ('Restart', False), + ('Exit', None), + ], style=style, - cancel_text='Skip', - ).run() + ) + + output = ['project'] + while True: + project_name = input_dialog( + title='ozi-new interactive prompt', + text='What is the name of the project?', + validator=ProjectNameValidator(), + style=style, + cancel_text='Menu', + ).run() + if project_name is None: + while True: + match menu.run(): + case True: + break + case False: + return ['interactive', '.'] + case None: + return [] + else: + if validate_message(project_name, ProjectNameValidator()): + break + message_dialog( + title='ozi-new interactive prompt', + text=f'Invalid input "{project_name}"\nPress ENTER to continue.', + ).run() prefix = f'Name: {project_name if project_name else ""}\n' output += [f'--name="{project_name}"'] if project_name else [] - summary = input_dialog( - title='ozi-new interactive prompt', - text=prefix + 'What does the project do?\n(a short summary 1-2 sentences)', - validator=SummaryValidator(), - style=style, - cancel_text='Skip', - ).run() + while True: + summary = input_dialog( + title='ozi-new interactive prompt', + text='\n'.join( + (prefix, 'What does the project do?\n(a short summary 1-2 sentences)'), + ), + validator=LengthValidator(), + style=style, + cancel_text='Menu', + ).run() + if summary is None: + while True: + match menu.run(): + case True: + break + case False: + return ['interactive'] + case None: + return [] + else: + if validate_message(summary, LengthValidator()): + break + message_dialog( + title='ozi-new interactive prompt', + text=f'Invalid input "{summary}"\nPress ENTER to continue.', + ).run() prefix += f'Summary: {summary if summary else ""}\n' output += [f'--summary="{summary if summary else ""}"'] - home_page = input_dialog( - title='ozi-new interactive prompt', - text=prefix + "What is the project's home-page URL?", - style=style, - cancel_text='Skip', - ).run() + while True: + keywords = input_dialog( + title='ozi-new interactive prompt', + text='\n'.join( + (prefix, 'What are some project keywords?\n(comma-separated list)'), + ), + style=style, + cancel_text='Menu', + ).run() + if keywords is None: + while True: + match menu.run(): + case True: + break + case False: + return ['interactive'] + case None: + return [] + else: + keywords = keywords.rstrip(',').split(',') if keywords else None # type: ignore + if validate_message(','.join(keywords) if keywords else '', LengthValidator()): + break + message_dialog( + title='ozi-new interactive prompt', + text=f'Invalid input "{keywords}"\nPress ENTER to continue.', + ).run() + prefix += f'Keywords: {",".join(keywords if keywords else "")}\n' + output += [f'--keywords={",".join(keywords if keywords else [])}'] + + while True: + home_page = input_dialog( + title='ozi-new interactive prompt', + text='\n'.join((prefix, "What is the project's home-page URL?")), + style=style, + cancel_text='Menu', + ).run() + if home_page is None: + while True: + match menu.run(): + case True: + break + case False: + return ['interactive'] + case None: + return [] + else: + if validate_message(home_page, LengthValidator()): + break + message_dialog( + title='ozi-new interactive prompt', + text=f'Invalid input "{home_page}"\nPress ENTER to continue.', + ).run() prefix += f'Home-page: {home_page if home_page else ""}\n' output += [f'--home-page="{home_page}"'] if home_page else [] - author_names = input_dialog( - title='ozi-new interactive prompt', - text=prefix + 'What is the author or authors name?\n(comma-separated list)', - style=style, - cancel_text='Skip', - ).run() - author_names = author_names.rstrip(',').split(',') if author_names else None # type: ignore + while True: + author_names = input_dialog( + title='ozi-new interactive prompt', + text='\n'.join( + (prefix, 'What is the author or authors name?\n(comma-separated list)'), + ), + style=style, + cancel_text='Menu', + ).run() + if author_names is None: + while True: + match menu.run(): + case True: + break + case False: + return ['interactive'] + case None: + return [] + else: + author_names = author_names.rstrip(',').split(',') if author_names else None # type: ignore + if validate_message( + (','.join(author_names) if author_names else ''), + LengthValidator(), + ): + break + message_dialog( + title='ozi-new interactive prompt', + text=f'Invalid input "{author_names}"\nPress ENTER to continue.', + ).run() prefix += f'Author: {",".join(author_names if author_names else "")}\n' output += [f'--author="{a}"' for a in author_names] if author_names else [] - author_emails = input_dialog( - title='ozi-new interactive prompt', - text=prefix - + 'What are the email addresses of the author or authors?\n(comma-separated list)', - style=style, - cancel_text='Skip', - ).run() - author_emails = author_emails.rstrip(',').split(',') if author_emails else None # type: ignore + while True: + author_emails = input_dialog( + title='ozi-new interactive prompt', + text='\n'.join( + ( + prefix, + 'What are the email addresses of the author or authors?\n(comma-separated list)', + ), + ), + style=style, + cancel_text='Menu', + ).run() + if author_emails is None: + while True: + match menu.run(): + case True: + break + case False: + return ['interactive'] + case None: + return [] + else: + author_emails = author_emails.rstrip(',').split(',') if author_emails else None # type: ignore + if validate_message( + ','.join(author_emails) if author_emails else '', + LengthValidator(), + ): + break + message_dialog( + title='ozi-new interactive prompt', + text=f'Invalid input "{author_emails}"\nPress ENTER to continue.', + ).run() prefix += f'Author-email: {",".join(author_emails if author_emails else "")}\n' output += [f'--author-email="{e}"' for e in author_emails] if author_emails else [] - if yes_no_dialog( - title='ozi-new interactive prompt', - text=prefix - + 'Are there any maintainers of this project?\n(other than the author or authors)', - style=style, - ).run(): - maintainer_names = input_dialog( + while True: + license_ = radiolist_dialog( + values=sorted( + (zip(from_prefix(Prefix().license), from_prefix(Prefix().license))), + ), title='ozi-new interactive prompt', - text=prefix - + 'What is the maintainer or maintainers name?\n(comma-separated list)', + text='\n'.join((prefix, 'Please select a license classifier:')), style=style, - cancel_text='Skip', + cancel_text='Menu', ).run() - maintainer_names = ( - maintainer_names.rstrip(',').split(',') if maintainer_names else None # type: ignore + if license_ is None: + while True: + match menu.run(): + case True: + break + case False: + return ['interactive'] + case None: + return [] + else: + if validate_message(license_ if license_ else '', LengthValidator()): + break + message_dialog( + title='ozi-new interactive prompt', + text=f'Invalid input "{license_}"\nPress ENTER to continue.', + ).run() + prefix += f'{Prefix().license}{license_ if license_ else ""}\n' + output += [f'--license="{license_}"'] if license_ else [] + + while True: + possible_spdx: Sequence[str] = METADATA.spec.python.pkg.license.ambiguous.get( + license_, + (), ) + if len(possible_spdx) < 1: + license_expression = input_dialog( + title='ozi-new interactive prompt', + text='\n'.join((prefix, 'Edit SPDX license expression:')), + default='', + style=style, + cancel_text='Skip', + ).run() + elif len(possible_spdx) == 1: + license_expression = input_dialog( + title='ozi-new interactive prompt', + text='\n'.join((prefix, 'Edit SPDX license expression:')), + default=possible_spdx[0], + style=style, + cancel_text='Skip', + ).run() + else: + license_id = radiolist_dialog( + values=sorted(zip(possible_spdx, possible_spdx)), + title='ozi-new interactive prompt', + text='\n'.join((prefix, 'Please select a SPDX license-id:')), + style=style, + cancel_text='Menu', + ).run() + if license_id is None: + while True: + match menu.run(): + case True: + break + case False: + return ['interactive'] + case None: + return [] + else: + license_expression = input_dialog( + title='ozi-new interactive prompt', + text='\n'.join((prefix, 'Edit SPDX license expression:')), + default=license_id if license_id is not None else '', + style=style, + cancel_text='Skip', + ).run() + if validate_message(license_id if license_id else '', LengthValidator()): + break + else: + message_dialog( + title='ozi-new interactive prompt', + text=f'Invalid input "{license_id}"\nPress ENTER to continue.', + ).run() + break + + output += ( + [f'--license-expression="{license_expression}"'] + if license_expression # pyright: ignore + else [] + ) + prefix += f'Extra: License-Expression :: {license_expression if license_expression else ""}\n' # pyright: ignore # noqa: B950, RUF100 + + if yes_no_dialog( + title='ozi-new interactive prompt', + text='\n'.join( + ( + prefix, + 'Are there any maintainers of this project?\n(other than the author or authors)', + ), + ), + style=style, + ).run(): + while True: + maintainer_names = input_dialog( + title='ozi-new interactive prompt', + text='\n'.join( + ( + prefix, + 'What is the maintainer or maintainers name?\n(comma-separated list)', + ), + ), + style=style, + cancel_text='Menu', + ).run() + if maintainer_names is None: + while True: + match menu.run(): + case True: + break + case False: + return ['interactive'] + case None: + return [] + else: + maintainer_names = ( + maintainer_names.rstrip(',').split(',') if maintainer_names else None # type: ignore + ) + if validate_message( + ','.join(maintainer_names) if maintainer_names else '', + LengthValidator(), + ): + break + message_dialog( + title='ozi-new interactive prompt', + text=f'Invalid input "{maintainer_names}"\nPress ENTER to continue.', + ).run() prefix += f'Maintainer: {",".join(maintainer_names if maintainer_names else [])}\n' output += ( [f'--maintainer="{a}"' for a in maintainer_names] if maintainer_names else [] ) - maintainer_emails = input_dialog( - title='ozi-new interactive prompt', - text=prefix - + 'What are the email addresses of the maintainer or maintainers?\n(comma-separated list)', # noqa: B950 - style=style, - cancel_text='Skip', - ).run() - maintainer_emails = ( - maintainer_emails.rstrip(',').split(',') if maintainer_emails else None # type: ignore - ) + while True: + maintainer_emails = input_dialog( + title='ozi-new interactive prompt', + text='\n'.join( + ( + prefix, + 'What are the email addresses of the maintainer or maintainers?\n(comma-separated list)', # noqa: B950, RUF100, E501 + ), + ), + style=style, + cancel_text='Menu', + ).run() + if maintainer_emails is None: + while True: + match menu.run(): + case True: + break + case False: + return ['interactive'] + case None: + return [] + else: + maintainer_emails = ( + maintainer_emails.rstrip(',').split(',') if maintainer_emails else None # type: ignore + ) + if validate_message( + ','.join(maintainer_emails) if maintainer_emails else '', + LengthValidator(), + ): + break + message_dialog( + title='ozi-new interactive prompt', + text=f'Invalid input "{maintainer_emails}"\nPress ENTER to continue.', + ).run() prefix += ( f'Maintainer-email: {",".join(maintainer_emails if maintainer_emails else [])}\n' ) @@ -166,20 +465,10 @@ def interactive_prompt() -> list[str]: # noqa: C901 # pragma: no cover else [] ) - keywords = input_dialog( - title='ozi-new interactive prompt', - text=prefix + 'What are some project keywords?\n(comma-separated list)', - style=style, - cancel_text='Skip', - ).run() - keywords = keywords.rstrip(',').split(',') if keywords else None # type: ignore - prefix += f'Keywords: {",".join(keywords if keywords else "")}\n' - output += [f'--keywords={",".join(keywords if keywords else [])}'] - requires_dist = [] while button_dialog( title='ozi-new interactive prompt', - text=prefix + 'Do you want to add a dependency requirement?', + text='\n'.join((prefix, 'Do you want to add a dependency requirement?')), buttons=[ ('Yes', True), ('No', False), @@ -188,69 +477,23 @@ def interactive_prompt() -> list[str]: # noqa: C901 # pragma: no cover ).run(): requirement = input_dialog( title='ozi-new interactive prompt', - text=prefix + 'Search PyPI packages:', + text='\n'.join((prefix, 'Search PyPI packages:')), validator=PackageValidator(), style=style, - cancel_text='Skip', + cancel_text='Back', ).run() requires_dist += [requirement] if requirement else [] prefix += f'Requires-Dist: {requirement}\n' if requirement else '' output += [f'--requires-dist="{requirement}"'] if requirement else [] - license_ = radiolist_dialog( - values=sorted((zip(from_prefix(Prefix().license), from_prefix(Prefix().license)))), - title='ozi-new interactive prompt', - text=prefix + 'Please select a license classifier:', - style=style, - cancel_text='Skip', - ).run() - prefix += f'{Prefix().license}{license_ if license_ else ""}\n' - output += [f'--license="{license_}"'] if license_ else [] - - possible_spdx: Sequence[str] = METADATA.spec.python.pkg.license.ambiguous.get( - license_, - (), - ) - if len(possible_spdx) < 1: - license_expression = input_dialog( - title='ozi-new interactive prompt', - text=prefix + 'Edit SPDX license expression:', - default='', - style=style, - cancel_text='Skip', - ).run() - elif len(possible_spdx) == 1: - license_expression = input_dialog( - title='ozi-new interactive prompt', - text=prefix + 'Edit SPDX license expression:', - default=possible_spdx[0], - style=style, - cancel_text='Skip', - ).run() - else: - license_id = radiolist_dialog( - values=sorted(zip(possible_spdx, possible_spdx)), - title='ozi-new interactive prompt', - text=prefix + 'Please select a SPDX license-id:', - style=style, - cancel_text='Skip', - ).run() - license_expression = input_dialog( - title='ozi-new interactive prompt', - text=prefix + 'Edit SPDX license expression:', - default=license_id if license_id is not None else '', - style=style, - cancel_text='Skip', - ).run() - output += [f'--license-expression="{license_expression}"'] if license_expression else [] - prefix += ( - f'Extra: License-Expression :: {license_expression if license_expression else ""}\n' - ) - if yes_no_dialog( title='ozi-new interactive prompt', - text=prefix - + 'Do you want to edit default classifiers?\n(audience, language, status, etc.)', + text='\n'.join( + ( + prefix, + 'Do you want to edit default classifiers?\n(audience, language, status, etc.)', + ), + ), style=style, ).run(): for i in ['audience', 'environment', 'framework', 'language']: @@ -264,7 +507,7 @@ def interactive_prompt() -> list[str]: # noqa: C901 # pragma: no cover ), ), title='ozi-new interactive prompt', - text=prefix + f'Please select {i} classifier or classifiers:', + text='\n'.join((prefix, f'Please select {i} classifier or classifiers:')), style=style, cancel_text='Skip', ).run() @@ -281,7 +524,7 @@ def interactive_prompt() -> list[str]: # noqa: C901 # pragma: no cover ), ), title='ozi-new interactive prompt', - text=prefix + f'Please select {i} classifier or classifiers:', + text='\n'.join((prefix, f'Please select {i} classifier or classifiers:')), style=style, cancel_text='Skip', ).run() @@ -290,7 +533,7 @@ def interactive_prompt() -> list[str]: # noqa: C901 # pragma: no cover output += [f'--{i}="{classifier}"'] if classifier else [] if yes_no_dialog( title='ozi-new interactive prompt', - text=prefix + 'Do you want to edit default options?\n(readme-type etc.)', + text='\n'.join((prefix, 'Do you want to edit default options?\n(readme-type etc.)')), style=style, ).run(): readme_type = radiolist_dialog( @@ -300,7 +543,7 @@ def interactive_prompt() -> list[str]: # noqa: C901 # pragma: no cover ('plain text', 'txt'), ), title='ozi-new interactive prompt', - text=prefix + 'Please select README type:', + text='\n'.join((prefix, 'Please select README type:')), style=style, cancel_text='Skip', ).run() @@ -308,9 +551,11 @@ def interactive_prompt() -> list[str]: # noqa: C901 # pragma: no cover output += [f'--readme-type="{readme_type}"'] if readme_type else [] if yes_no_dialog( title='ozi-new interactive prompt', - text=prefix + 'Confirm project creation?\n(answering "No" will exit the prompt)', + text='\n'.join( + (prefix, 'Confirm project creation?\n(answering "No" will exit the prompt)'), + ), style=style, ).run(): return output else: - return ['project'] + return []