Skip to content

Commit e64a8dc

Browse files
committed
feat: resolve linting, formatting, and testing issues
1 parent 00bd742 commit e64a8dc

File tree

12 files changed

+195
-164
lines changed

12 files changed

+195
-164
lines changed

craft_cli/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
# names included here only to be exposed as external API; the particular order of imports
3131
# is to break cyclic dependencies
32-
from .messages import EmitterMode, emit,Emitter # isort:skip
32+
from .messages import EmitterMode, emit, Emitter # isort:skip
3333
from .dispatcher import BaseCommand, CommandGroup, Dispatcher, GlobalArgument
3434
from .completion import complete
3535
from .errors import (
@@ -53,5 +53,5 @@
5353
"ProvideHelpException",
5454
"emit",
5555
"complete",
56-
"Emitter"
56+
"Emitter",
5757
]

craft_cli/completion/completion.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from pathlib import Path
2727
from typing import Any, cast
2828

29-
import jinja2
29+
import jinja2 # type: ignore[import-not-found]
3030
from typing_extensions import Self, override
3131

3232
import craft_cli
@@ -247,11 +247,12 @@ def complete(shell_cmd: str, get_app_info: Callable[[], DispatcherAndConfig]) ->
247247
if arg.type == "option"
248248
]
249249

250-
return template.render(
250+
result: str = template.render(
251251
shell_cmd=shell_cmd,
252252
commands=command_map,
253253
global_opts=global_opts,
254254
)
255+
return result
255256

256257

257258
def _validate_app_info(raw_ref: str) -> Callable[[], DispatcherAndConfig]:

craft_cli/dispatcher.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ def fill_parser(self, parser: _CustomArgumentParser) -> None:
181181
"""
182182
parser.add_argument(
183183
"--format",
184-
choices=["json","table"],
184+
choices=["json", "table"],
185185
default="table",
186186
help="Format for structured output",
187187
)
@@ -198,15 +198,14 @@ def run(self, parsed_args: argparse.Namespace) -> int | None:
198198
:param parsed_args: The parsed arguments that were defined in :meth:`fill_parser`.
199199
:return: This method should return ``None`` or the desired process' return code.
200200
"""
201-
emit=Emitter()
202-
sample_data=[{"name":"App A","version":"1.0.1"},{"name":"App B","version":"2.3.0"}]
203-
emit.data(sample_data,format=parsed_args.format)
204-
return 0
201+
raise NotImplementedError
205202

206203

207204
class _CustomArgumentParser(argparse.ArgumentParser):
208205
"""ArgumentParser with custom error manager."""
209206

207+
_help_builder: HelpBuilder
208+
210209
def __init__(
211210
self,
212211
help_builder: HelpBuilder,
@@ -395,11 +394,13 @@ def _get_requested_help( # noqa: PLR0912 (too many branches)
395394
command.fill_parser(parser)
396395

397396
# produce the complete help message for the command
398-
command_options = self._get_global_options()
397+
command_options: list[tuple[str, str]] = []
399398
for action in parser._actions: # noqa: SLF001
400399
# store the different options if present, otherwise it's just the dest
401400
help_text = "" if action.help is None else action.help
402401
if action.option_strings:
402+
if "--format" in action.option_strings:
403+
continue
403404
command_options.append((", ".join(action.option_strings), help_text))
404405
else:
405406
if action.metavar is None:

craft_cli/helptexts.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ def _build_plain_command_help(
328328
textblocks.append(f"Summary:{overview}")
329329

330330
# column alignment is dictated by longest options title
331-
max_title_len = max(len(title) for title, _ in options)
331+
max_title_len = max((len(title) for title, _ in options), default=0)
332332

333333
if parameters:
334334
# command positional arguments
@@ -344,10 +344,11 @@ def _build_plain_command_help(
344344
)
345345

346346
# command options
347-
option_lines = ["Options:"]
348-
for title, text in options:
349-
option_lines.extend(_build_item_plain(title, text, max_title_len))
350-
textblocks.append("\n".join(option_lines))
347+
if options:
348+
option_lines = ["Options:"]
349+
for title, text in options:
350+
option_lines.extend(_build_item_plain(title, text, max_title_len))
351+
textblocks.append("\n".join(option_lines))
351352

352353
if other_command_names:
353354
see_also_block = ["See also:"]

craft_cli/messages.py

Lines changed: 96 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -26,77 +26,93 @@
2626
import enum
2727
import functools
2828
import getpass
29+
import json
2930
import logging
3031
import os
3132
import pathlib
3233
import select
3334
import sys
3435
import threading
3536
import traceback
37+
from abc import ABC, abstractmethod
3638
from collections.abc import Callable, Generator
3739
from contextlib import contextmanager
3840
from datetime import datetime
39-
from typing import TYPE_CHECKING, Any, Literal, TextIO, TypeVar, cast, Union, List, Dict
40-
import json
41-
import platformdirs
41+
from typing import TYPE_CHECKING, Any, Literal, TextIO, TypeVar, cast
42+
43+
import platformdirs # type: ignore [import-not-found]
4244

4345
from craft_cli import errors
4446
from craft_cli.printer import Printer
45-
from abc import ABC, abstractmethod
46-
#from .formatters import BaseFormatter
4747

48-
TabularData=Union[List[Dict[str,Any]],Dict[str,Any]]
48+
TabularData = list[dict[str, Any]] | dict[str, Any]
49+
4950

5051
class BaseFormatter(ABC):
5152
@abstractmethod
52-
def format(self,data:TabularData,headers:dict[str,str] | None=None)->str:
53+
def format(self, data: TabularData, headers: dict[str, str] | None = None) -> str:
5354
pass
55+
56+
5457
class JSONFormatter(BaseFormatter):
55-
"""
56-
Format data into JSON.
58+
"""Format data into JSON.
5759
58-
Example
60+
Example:
5961
-------
60-
>>> formatter=JsonFormatter()
62+
>>> formatter = JsonFormatter()
6163
>>> formatter.format([{"name":"Alice","age":30})
6264
'{"name":"Alice","age":30}'
65+
6366
"""
64-
def format(self,data: TabularData,headers: dict[str,str] | None=None)->str:
65-
return json.dumps(data,indent=2,default=str)
67+
68+
def format(self, data: TabularData, headers: dict[str, str] | None = None) -> str:
69+
_ = headers
70+
return json.dumps(data, indent=2, default=str)
71+
72+
6673
class TableFormatter(BaseFormatter):
67-
"""
68-
Format data into a pretty table.
74+
r"""Format data into a pretty table.
6975
70-
Example
76+
Example:
7177
-------
72-
>>> formatter=TableFormatter()
73-
>>> formatter.format([["Name","Age"],["Alice",30]])
78+
>>> formatter = TableFormatter()
79+
>>> formatter.format([["Name", "Age"], ["Alice", 30]])
7480
'Name Age\nAlice 30'
81+
7582
"""
76-
def format(self,data:TabularData,headers:dict[str,str] | None=None)->str:
83+
84+
def format(self, data: TabularData, headers: dict[str, str] | None = None) -> str:
85+
_ = headers
7786
"""Format a list of rows into a table string."""
7887
if not data:
7988
return "[no data]"
80-
table_headers:list[str]
81-
if isinstance(data,list):
82-
#list_data=cast(List[Dict[str,Any]],data)
83-
all_keys: set[str]=set().union(*(row.keys() for row in data))
84-
table_headers:list[str]=sorted(list(all_keys))
85-
rows=[[str(row.get(h,"")) for h in table_headers] for row in data]
89+
if isinstance(data, list):
90+
all_keys: set[str] = set()
91+
for row in data:
92+
all_keys.update(str(k) for k in row)
93+
table_headers = sorted(all_keys)
94+
rows = [[str(row.get(h, "")) for h in table_headers] for row in data]
8695
else:
87-
table_headers=["Key", "Value"]
88-
#data_dict=cast(Dict[str,Any],data)
89-
rows=[[str(k),str(v)] for k,v in data.items()]
90-
cols_widths=[max(len(str(item)) for item in col) for col in zip(*([table_headers]+rows))]
91-
92-
def format_row(row: list[str])->str:
93-
return " | ".join(str(cell).ljust(width) for cell, width in zip(row,cols_widths))
94-
table=[format_row(table_headers)]
95-
table.append("-" * sum(cols_widths)+"---"*(len(table_headers)-1))
96-
table.extend(format_row(row) for row in rows)
96+
table_headers = ["Key", "Value"]
97+
rows = [[str(k), str(v)] for k, v in data.items()]
98+
cols_widths = [
99+
max(len(str(item)) for item in col) for col in zip(table_headers, *rows)
100+
]
101+
102+
def format_row(row: list[str]) -> str:
103+
return " | ".join(
104+
cell.ljust(width) for cell, width in zip(row, cols_widths)
105+
)
106+
107+
separator = "-+-".join("-" * width for width in cols_widths)
108+
table = [
109+
format_row(table_headers),
110+
separator,
111+
*(format_row(row) for row in rows),
112+
]
97113
return "\n".join(table)
98-
99-
114+
115+
100116
if TYPE_CHECKING:
101117
from types import TracebackType
102118

@@ -516,10 +532,10 @@ def __init__(self) -> None:
516532
self._log_handler: _Handler = None # type: ignore[assignment]
517533
self._streaming_brief = False
518534
self._docs_base_url: str | None = None
519-
self._formatters={
520-
"json":JSONFormatter(),
521-
"table":TableFormatter(),
522-
}
535+
self._formatters = {
536+
"json": JSONFormatter(),
537+
"table": TableFormatter(),
538+
}
523539

524540
def init(
525541
self,
@@ -785,6 +801,45 @@ def open_stream(self, text: str | None = None) -> _StreamContextManager:
785801
ephemeral_mode=ephemeral,
786802
)
787803

804+
@_active_guard()
805+
def data(
806+
self,
807+
data: TabularData,
808+
output_format: Literal["json", "table"] = "table",
809+
headers: dict[str, str] | None = None,
810+
) -> None:
811+
"""Output structured data to the terminal in a specific format.
812+
813+
:param data: The structured data to output( list of dicts or a single dict).
814+
:param format: The format( defaults to 'table')
815+
:param headers: Optional dictionary to map internal data keys to displayed header.
816+
"""
817+
formatter = self._formatters.get(output_format)
818+
if not formatter:
819+
raise ValueError(f"Unsupported format: {format}")
820+
821+
formatted_data = formatter.format(data, headers)
822+
stream = None if self._mode == EmitterMode.QUIET else sys.stdout
823+
self._printer.show(stream, formatted_data, raw_output=True)
824+
825+
@_active_guard()
826+
def table(
827+
self,
828+
data: TabularData,
829+
headers: dict[str, str] | None = None,
830+
) -> None:
831+
"""Output data as table."""
832+
self.data(data, output_format="table", headers=headers)
833+
834+
@_active_guard()
835+
def json(
836+
self,
837+
data: TabularData,
838+
headers: dict[str, str] | None = None,
839+
) -> None:
840+
"""Output data as JSON."""
841+
self.data(data, output_format="json", headers=headers)
842+
788843
@_active_guard()
789844
@contextmanager
790845
def pause(self) -> Generator[None, None, None]:
@@ -946,14 +1001,6 @@ def prompt(self, prompt_text: str, *, hide: bool = False) -> str:
9461001
if not val:
9471002
raise errors.CraftError("input cannot be empty")
9481003
return val
949-
950-
@_active_guard()
951-
def data(self,records: list[dict], format:str="table")->None:
952-
if format not in self._formatters:
953-
raise ValueError(f"Unsupported format: {format}")
954-
formatter=self._formatters[format]
955-
formatted_output=formatter.format(records)
956-
self.message(formatted_output)
9571004

9581005
@property
9591006
def log_filepath(self) -> pathlib.Path:
@@ -971,4 +1018,3 @@ def _format_details(details: str) -> str:
9711018
if "\n" in details:
9721019
return details if details.startswith("\n") else f"\n{details}"
9731020
return details
974-

craft_cli/printer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,13 +444,14 @@ def show(
444444
use_timestamp: bool = False,
445445
end_line: bool = False,
446446
avoid_logging: bool = False,
447+
raw_output: bool = False,
447448
) -> None:
448449
"""Show a text to the given stream if not stopped."""
449450
if self.stopped:
450451
return
451452

452453
text = self._apply_secrets(text)
453-
454+
_ = raw_output
454455
msg = _MessageInfo(
455456
stream=stream,
456457
text=text.rstrip(),

craft_cli/pytest_plugin.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@
2525
from typing import TYPE_CHECKING, Any, Literal
2626
from unittest.mock import call
2727

28-
29-
import pytest
28+
import pytest # type: ignore[import-not-found]
3029
from typing_extensions import Self
3130

3231
from craft_cli import messages, printer
@@ -35,17 +34,15 @@
3534
from unittest.mock import _Call # type: ignore[reportPrivateUsage]
3635

3736

38-
@pytest.fixture(autouse=True)
39-
def init_emitter(monkeypatch: pytest.MonkeyPatch) -> Generator[None]:
37+
@pytest.fixture(autouse=True) # type: ignore[misc]
38+
def init_emitter(monkeypatch: pytest.MonkeyPatch) -> Generator[None, None, None]:
4039
"""Ensure ``emit`` is always clean, and initiated (in test mode).
4140
4241
Note that the ``init`` is done in the current instance that all modules already
4342
acquired.
4443
4544
This is an "autouse" fixture, so it just works, no need to declare it in your tests.
4645
"""
47-
# messages.emit._initiated = False
48-
# messages.emit._stopped = False
4946
# initiate with a custom log filepath so user directories are not involved here; note that
5047
# we're not using pytest's standard tmp_path as Emitter would write logs there, and in
5148
# effect we would be polluting that temporary directory (potentially messing with
@@ -62,8 +59,7 @@ def init_emitter(monkeypatch: pytest.MonkeyPatch) -> Generator[None]:
6259
yield
6360
# end machinery (just in case it was not ended before; note it's ok to "double end")
6461
messages.emit.ended_ok()
65-
# messages.emit._initiated = False
66-
# messages.emit._stopped = True
62+
6763

6864
class _RegexComparingText(str):
6965
"""A string that compares for equality using regex.match."""
@@ -245,7 +241,7 @@ def advance(self, *a: Any, **k: Any) -> None:
245241
self.recording_emitter.record("advance", a, k)
246242

247243

248-
@pytest.fixture
244+
@pytest.fixture # type: ignore[misc]
249245
def emitter(monkeypatch: pytest.MonkeyPatch) -> RecordingEmitter:
250246
"""Provide a helper to test everything that was shown using the Emitter."""
251247
recording_emitter = RecordingEmitter()

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def run_apidoc(_):
4646
import os
4747
import sys
4848

49-
from sphinx.ext.apidoc import main
49+
from sphinx.ext.apidoc import main # type: ignore[reportMissingImports]
5050

5151
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
5252
cur_dir = os.path.abspath(os.path.dirname(__file__))

0 commit comments

Comments
 (0)