diff --git a/ozi_core/fix/__init__.py b/ozi_core/fix/__init__.py index a72e314..407a3fc 100644 --- a/ozi_core/fix/__init__.py +++ b/ozi_core/fix/__init__.py @@ -5,11 +5,10 @@ """ozi-fix: Project fix script that outputs a meson rewriter JSON array.""" from __future__ import annotations -from contextlib import redirect_stdout, suppress -import io import json import os import sys +from contextlib import suppress from pathlib import Path from typing import TYPE_CHECKING from typing import NoReturn @@ -30,8 +29,8 @@ from ozi_templates.filter import underscorify # pyright: ignore from tap_producer import TAP -from ozi_core.fix.missing import report from ozi_core.fix.interactive import interactive_prompt +from ozi_core.fix.missing import report from ozi_core.fix.parser import parser from ozi_core.fix.rewrite_command import Rewriter from ozi_core.new.validate import valid_copyright_head @@ -75,9 +74,7 @@ def main(args: list[str] | None = None) -> NoReturn: # pragma: no cover args = interactive_prompt(project) termios.tcsetattr(fd, termios.TCSADRAIN, original_attributes) TAP.comment(f'ozi-fix {" ".join(args)}') - json_out = io.StringIO() - with redirect_stdout(json_out): - main(args) + main(args) case [False, True, False]: project, _ = _setup(project) name, *_ = report(project.target) @@ -95,9 +92,7 @@ def main(args: list[str] | None = None) -> NoReturn: # pragma: no cover rewriter -= project.remove TAP.plan() if len(project.add) > 0 or len(project.remove) > 0: - print( - json.dumps(rewriter.commands, indent=4 if project.pretty else None) - ) + print(json.dumps(rewriter.commands, indent=4 if project.pretty else None)) else: parser.print_help() case [False, True, True]: diff --git a/ozi_core/fix/build_definition.py b/ozi_core/fix/build_definition.py index e259bdb..a5d5634 100644 --- a/ozi_core/fix/build_definition.py +++ b/ozi_core/fix/build_definition.py @@ -55,7 +55,7 @@ def process( target: Path, rel_path: Path, found_files: list[str] | None = None, -) -> list[str]: # pragma: no cover +) -> dict[str, list[str]]: # pragma: no cover """Process an OZI project build definition's files.""" try: extra_files = [ @@ -75,7 +75,7 @@ def process( found_files=found_files, extra_files=extra_files, ) - return files['found'] + files['missing'] + return files def validate( @@ -107,9 +107,11 @@ def walk( rel_path: Path, found_files: list[str] | None = None, project_name: str | None = None, -) -> None: # pragma: no cover +) -> Generator[dict[Path, dict[str, list[str]]], None, None]: # pragma: no cover """Walk an OZI standard build definition directory.""" - found_files = process(target, rel_path, found_files) + files = process(target, rel_path, found_files) + yield {rel_path: files} + found_files = files['found'] + files['missing'] children = list( validate( target, diff --git a/ozi_core/fix/build_definition.pyi b/ozi_core/fix/build_definition.pyi index 40be4a6..8a25836 100644 --- a/ozi_core/fix/build_definition.pyi +++ b/ozi_core/fix/build_definition.pyi @@ -7,10 +7,10 @@ from typing import Generator """Build definition check utilities.""" IGNORE_MISSING = ... -def inspect_files(target: Path, rel_path: Path, found_files: list[str], extra_files: list[str]) -> list[str]: +def inspect_files(target: Path, rel_path: Path, found_files: list[str], extra_files: list[str]) -> dict[str, list[str]]: ... -def process(target: Path, rel_path: Path, found_files: list[str] | None = ...) -> list[str]: +def process(target: Path, rel_path: Path, found_files: list[str] | None = ...) -> dict[str, list[str]]: """Process an OZI project build definition's files.""" ... @@ -18,7 +18,11 @@ def validate(target: Path, rel_path: Path, subdirs: list[str], children: set[str """Validate an OZI standard build definition's directories.""" ... -def walk(target: Path, rel_path: Path, found_files: list[str] | None = ..., project_name: str | None = ...) -> None: +def walk( + target: Path, + rel_path: Path, + found_files: list[str] | None = None, + project_name: str | None = None, +) -> Generator[dict[Path, dict[str, list[str]]], None, None]: """Walk an OZI standard build definition directory.""" ... - diff --git a/ozi_core/fix/interactive.py b/ozi_core/fix/interactive.py index ecc59a2..b083f95 100644 --- a/ozi_core/fix/interactive.py +++ b/ozi_core/fix/interactive.py @@ -2,21 +2,22 @@ import os import sys +from io import UnsupportedOperation from typing import TYPE_CHECKING from unittest.mock import Mock -from ozi_core._i18n import TRANSLATION -from ozi_core.fix.build_definition import inspect_files -from ozi_core.fix.missing import get_relpath_expected_files -from ozi_core.ui._style import _style -from ozi_core.ui.dialog import input_dialog -from ozi_core.ui.menu import MenuButton - from prompt_toolkit.shortcuts.dialogs import button_dialog from prompt_toolkit.shortcuts.dialogs import checkboxlist_dialog -from prompt_toolkit.shortcuts.dialogs import radiolist_dialog from prompt_toolkit.shortcuts.dialogs import message_dialog +from prompt_toolkit.shortcuts.dialogs import radiolist_dialog from prompt_toolkit.shortcuts.dialogs import yes_no_dialog +from tap_producer import TAP + +from ozi_core._i18n import TRANSLATION +from ozi_core.fix.build_definition import walk +from ozi_core.fix.missing import get_relpath_expected_files +from ozi_core.ui._style import _style +from ozi_core.ui.menu import MenuButton if sys.platform != 'win32': import curses @@ -27,6 +28,7 @@ if TYPE_CHECKING: from argparse import Namespace + from pathlib import Path def main_menu( # pragma: no cover @@ -68,6 +70,27 @@ def main_menu( # pragma: no cover class Prompt: + def __init__(self: Prompt, target: Path) -> None: + self.target = target + self.fix: str = '' + + def set_fix_mode( + self: Prompt, + project_name: str, + output: dict[str, list[str]], + prefix: dict[str, str], + ) -> tuple[list[str] | str | bool | None, dict[str, list[str]], dict[str, str]]: + self.fix = radiolist_dialog( + title=TRANSLATION('fix-dlg-title'), + text=TRANSLATION('fix-add'), + style=_style, + cancel_text=MenuButton.BACK._str, + values=[('source', 'source'), ('test', 'test'), ('root', 'root')], + ).run() + if self.fix is not None: + output['fix'].append(self.fix) + return None, output, prefix + def add_or_remove( self: Prompt, project_name: str, @@ -96,29 +119,36 @@ def add_or_remove( style=_style, ).run(): case MenuButton.ADD.value: - add_path = radiolist_dialog( - title=TRANSLATION('fix-dlg-title'), - text=TRANSLATION('fix-add'), - style=_style, - cancel_text=MenuButton.BACK._str, - values=[('source', 'source'), ('test', 'test'), ('root', 'root')], - ).run() - if add_path: - rel_path, expected = get_relpath_expected_files(add_path, project_name) - inspect_files(project.target, rel_path, expected) - input_dialog( + rel_path, _ = get_relpath_expected_files(self.fix, project_name) + files = [] + with TAP.suppress(): + for d in walk(self.target, rel_path, []): + for k, v in d.items(): + files += [str(k / i) for i in v['missing']] + if len(files) > 0: + result = checkboxlist_dialog( title=TRANSLATION('fix-dlg-title'), - text='' - ) - add_files += [] - prefix.update( - { - f'Add: {add_path}': ( - f'Add: {add_path}' - ), - }, - ) - output['--add'].append(add_path) + text='', + values=[(i, i) for i in files], + style=_style, + ).run() + if result is not None: + add_files += result + prefix.update( + { + f'Add-{self.fix}: {add_files}': ( + f'Add-{self.fix}: {add_files}' + ), + }, + ) + for f in files: + output['--add'].append(f) + else: + message_dialog( + title=TRANSLATION('fix-dlg-title'), + text=f'no missing {self.fix} files', + style=_style, + ).run() case MenuButton.REMOVE.value: if len(add_files) != 0: del_files = checkboxlist_dialog( @@ -153,18 +183,29 @@ def add_or_remove( return None, output, prefix -def interactive_prompt(project: Namespace) -> list[str]: # pragma: no cover +def interactive_prompt(project: Namespace) -> list[str]: # pragma: no cover # noqa: C901 ret_args = ['source'] - curses.setupterm() - e3 = curses.tigetstr('E3') or b'' - clear_screen_seq = curses.tigetstr('clear') or b'' - os.write(sys.stdout.fileno(), e3 + clear_screen_seq) - p = Prompt() - result, output, prefix = p.add_or_remove(project_name=project.name, output={}, prefix={}) + try: + curses.setupterm() + e3 = curses.tigetstr('E3') or b'' + clear_screen_seq = curses.tigetstr('clear') or b'' + os.write(sys.stdout.fileno(), e3 + clear_screen_seq) + except UnsupportedOperation: + pass + p = Prompt(project.target) + result, output, prefix = p.set_fix_mode( + project_name=project.name, output={'fix': []}, prefix={} + ) + if isinstance(result, list): + return result + result, output, prefix = p.add_or_remove( + project_name=project.name, output=output, prefix=prefix + ) if isinstance(result, list): return result + fix = output.pop('fix') for k, v in output.items(): for i in v: if len(i) > 0: ret_args += [k, i] - return ret_args + ['.'] + return fix + ret_args + ['.'] diff --git a/ozi_core/fix/meson.build b/ozi_core/fix/meson.build index ac3e17e..7141562 100644 --- a/ozi_core/fix/meson.build +++ b/ozi_core/fix/meson.build @@ -7,6 +7,7 @@ config_files = [ '__init__.pyi', 'build_definition.py', 'build_definition.pyi', + 'interactive.py', 'missing.py', 'missing.pyi', 'parser.py', diff --git a/ozi_core/fix/missing.py b/ozi_core/fix/missing.py index 67fee46..ffe0c9e 100644 --- a/ozi_core/fix/missing.py +++ b/ozi_core/fix/missing.py @@ -194,7 +194,14 @@ def required_files( continue # pragma: defer to https://github.com/nedbat/coveragepy/issues/198 TAP.ok(str(f)) found_files.append(file) - walk(target, rel_path, found_files=found_files, project_name=underscorify(name).lower()) + list( + walk( + target, + rel_path, + found_files=found_files, + project_name=underscorify(name).lower(), + ) + ) return found_files diff --git a/ozi_core/fix/missing.pyi b/ozi_core/fix/missing.pyi index c94d2b9..e24d3b0 100644 --- a/ozi_core/fix/missing.pyi +++ b/ozi_core/fix/missing.pyi @@ -16,6 +16,12 @@ if TYPE_CHECKING: ... PKG_INFO = ... readme_ext_to_content_type = ... + +def get_relpath_expected_files( + kind: str, + name: str, +) -> tuple[Path, tuple[str, ...] | tuple[()]]: ... + def render_requirements(target: Path) -> str: """Render requirements.in as it would appear in PKG-INFO""" ... diff --git a/ozi_core/new/interactive/menu.py b/ozi_core/new/interactive/menu.py index f96978e..92e3b1e 100644 --- a/ozi_core/new/interactive/menu.py +++ b/ozi_core/new/interactive/menu.py @@ -9,12 +9,13 @@ from prompt_toolkit.shortcuts import yes_no_dialog # pyright: ignore from ozi_core._i18n import TRANSLATION +from ozi_core.trove import Prefix +from ozi_core.trove import from_prefix from ozi_core.ui._style import _style from ozi_core.ui.dialog import admonition_dialog from ozi_core.ui.dialog import input_dialog -from ozi_core.trove import Prefix -from ozi_core.trove import from_prefix -from ozi_core.ui.menu import MenuButton, checkbox +from ozi_core.ui.menu import MenuButton +from ozi_core.ui.menu import checkbox def main_menu( # pragma: no cover diff --git a/ozi_core/new/interactive/menu.pyi b/ozi_core/new/interactive/menu.pyi index 175893a..9210bc2 100644 --- a/ozi_core/new/interactive/menu.pyi +++ b/ozi_core/new/interactive/menu.pyi @@ -6,7 +6,6 @@ from typing import Any from ozi_spec import METADATA - def main_menu(project: Any, output: dict[str, list[str]], prefix: dict[str, str]) -> tuple[None | list[str] | bool, dict[str, list[str]], dict[str, str]]: ... diff --git a/ozi_core/new/interactive/project.py b/ozi_core/new/interactive/project.py index 0a6857f..bc64982 100644 --- a/ozi_core/new/interactive/project.py +++ b/ozi_core/new/interactive/project.py @@ -12,10 +12,6 @@ from prompt_toolkit.validation import Validator # pyright: ignore from ozi_core._i18n import TRANSLATION -from ozi_core.ui._style import _style -from ozi_core.ui.dialog import admonition_dialog -from ozi_core.ui.dialog import input_dialog -from ozi_core.ui.menu import MenuButton from ozi_core.new.interactive.menu import main_menu from ozi_core.new.interactive.validator import LengthValidator from ozi_core.new.interactive.validator import NotReservedValidator @@ -24,6 +20,10 @@ from ozi_core.new.interactive.validator import validate_message from ozi_core.trove import Prefix from ozi_core.trove import from_prefix +from ozi_core.ui._style import _style +from ozi_core.ui.dialog import admonition_dialog +from ozi_core.ui.dialog import input_dialog +from ozi_core.ui.menu import MenuButton if TYPE_CHECKING: # pragma: no cover from collections.abc import Sequence diff --git a/ozi_core/ui/_style.pyi b/ozi_core/ui/_style.pyi index 4ac8d8f..1286093 100644 --- a/ozi_core/ui/_style.pyi +++ b/ozi_core/ui/_style.pyi @@ -1,6 +1,7 @@ """ This type stub file was generated by pyright. """ +from prompt_toolkit.styles import Style # pyright: ignore -_style_dict = ... -_style = ... +_style_dict: dict[str, str] +_style: Style