Skip to content

Commit 78c536f

Browse files
committed
Add json output to list and list-depends
1 parent ef51949 commit 78c536f

File tree

10 files changed

+157
-18
lines changed

10 files changed

+157
-18
lines changed

docs/cli.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ $ manifestoo list [OPTIONS]
9090

9191
**Options**:
9292

93-
* `--separator TEXT`: Separator charater to use (by default, print one item per line).
93+
* `--format [names|json]`: Output format [default: names]
94+
* `--separator TEXT`: Separator character to use for the 'names' format (by
95+
default, print one item per line).
9496
* `--help`: Show this message and exit.
9597

9698
## `manifestoo list-depends`
@@ -105,7 +107,9 @@ $ manifestoo list-depends [OPTIONS]
105107

106108
**Options**:
107109

108-
* `--separator TEXT`: Separator charater to use (by default, print one item per line).
110+
* `--format [names|json]`: Output format [default: names]
111+
* `--separator TEXT`: Separator character to use for the 'names' format (by
112+
default, print one item per line).
109113
* `--transitive`: Print all transitive dependencies.
110114
* `--include-selected`: Print the selected addons along with their dependencies.
111115
* `--ignore-missing`: Do not fail if dependencies are not found in addons path. This only applies to top level (selected) addons and transitive dependencies.

news/11.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add rich `json` output format to `list` and `list-depends` commands.

src/manifestoo/addon.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
1+
import sys
12
from pathlib import Path
23

3-
from .manifest import InvalidManifest, Manifest
4+
from .manifest import InvalidManifest, Manifest, ManifestDict
5+
6+
if sys.version_info >= (3, 8):
7+
from typing import TypedDict
8+
9+
class AddonDict(TypedDict):
10+
manifest: ManifestDict
11+
manifest_path: str
12+
path: str
13+
14+
15+
else:
16+
from typing import Any, Dict
17+
18+
AddonDict = Dict[str, Any]
419

520

621
class AddonNotFound(Exception):
@@ -52,3 +67,11 @@ def from_addon_dir(
5267
except InvalidManifest as e:
5368
raise AddonNotFoundInvalidManifest(str(e)) from e
5469
return cls(manifest)
70+
71+
def as_dict(self) -> AddonDict:
72+
"""Convert to a dictionary suitable for json output."""
73+
return dict(
74+
manifest=self.manifest.manifest_dict,
75+
manifest_path=str(self.manifest_path),
76+
path=str(self.path),
77+
)

src/manifestoo/commands/tree.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Dict, List, Optional, Set
1+
from typing import Dict, List, Optional, Set, cast
22

33
import typer
44

@@ -57,7 +57,8 @@ def _print(indent: List[str], node: Node) -> None:
5757

5858
def sversion(self, odoo_series: OdooSeries) -> Optional[str]:
5959
if not self.addon:
60-
return typer.style("✘ not installed", fg=typer.colors.RED)
60+
# typer.style (from click, actually) miss a type annotation
61+
return cast(str, typer.style("✘ not installed", fg=typer.colors.RED))
6162
elif is_core_ce_addon(self.addon_name, odoo_series):
6263
return f"{odoo_series}+{OdooEdition.CE}"
6364
elif is_core_ee_addon(self.addon_name, odoo_series):

src/manifestoo/main.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from enum import Enum
12
from pathlib import Path
23
from typing import List, Optional
34

@@ -13,7 +14,7 @@
1314
from .core_addons import get_core_addons
1415
from .odoo_series import OdooSeries, detect_from_addons_set
1516
from .options import MainOptions
16-
from .utils import ensure_odoo_series, not_implemented, print_list
17+
from .utils import ensure_odoo_series, not_implemented, print_addons_as_json, print_list
1718
from .version import __version__
1819

1920
app = typer.Typer()
@@ -185,26 +186,48 @@ def callback(
185186
ctx.obj = main_options
186187

187188

189+
class Format(str, Enum):
190+
names = "names"
191+
json = "json"
192+
193+
188194
@app.command()
189195
def list(
190196
ctx: typer.Context,
197+
format: Format = typer.Option(
198+
Format.names,
199+
help="Output format",
200+
),
191201
separator: Optional[str] = typer.Option(
192202
None,
193-
help="Separator charater to use (by default, print one item per line).",
203+
help=(
204+
"Separator character to use for the 'names' format "
205+
"(by default, print one item per line)."
206+
),
194207
),
195208
) -> None:
196209
"""Print the selected addons."""
197210
main_options: MainOptions = ctx.obj
198-
result = list_command(main_options.addons_selection)
199-
print_list(result, separator or main_options.separator or "\n")
211+
names = list_command(main_options.addons_selection)
212+
if format == Format.names:
213+
print_list(names, separator or main_options.separator or "\n")
214+
else:
215+
print_addons_as_json(names, main_options.addons_set)
200216

201217

202218
@app.command()
203219
def list_depends(
204220
ctx: typer.Context,
221+
format: Format = typer.Option(
222+
Format.names,
223+
help="Output format",
224+
),
205225
separator: Optional[str] = typer.Option(
206226
None,
207-
help="Separator charater to use (by default, print one item per line).",
227+
help=(
228+
"Separator character to use for the 'names' format "
229+
"(by default, print one item per line)."
230+
),
208231
),
209232
transitive: bool = typer.Option(
210233
False,
@@ -238,7 +261,7 @@ def list_depends(
238261
main_options: MainOptions = ctx.obj
239262
if as_pip_requirements:
240263
not_implemented("--as-pip-requirement")
241-
result, missing = list_depends_command(
264+
names, missing = list_depends_command(
242265
main_options.addons_selection,
243266
main_options.addons_set,
244267
transitive,
@@ -247,10 +270,13 @@ def list_depends(
247270
if missing and not ignore_missing:
248271
echo.error("not found in addons path: " + ",".join(sorted(missing)))
249272
raise typer.Abort()
250-
print_list(
251-
result,
252-
separator or main_options.separator or "\n",
253-
)
273+
if format == Format.names:
274+
print_list(
275+
names,
276+
separator or main_options.separator or "\n",
277+
)
278+
else:
279+
print_addons_as_json(names, main_options.addons_set)
254280

255281

256282
@app.command()

src/manifestoo/manifest.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,11 @@ class InvalidManifest(Exception):
5656
pass
5757

5858

59+
ManifestDict = Dict[Any, Any]
60+
61+
5962
class Manifest:
60-
def __init__(self, manifest_path: Path, manifest_dict: Dict[Any, Any]) -> None:
63+
def __init__(self, manifest_path: Path, manifest_dict: ManifestDict) -> None:
6164
self.manifest_path = manifest_path
6265
self.manifest_dict = manifest_dict
6366

src/manifestoo/utils.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import json
12
import sys
2-
from typing import Iterable, List, Optional
3+
from typing import Any, Dict, Iterable, List, Optional
34

45
import typer
56

67
from . import echo
8+
from .addon import AddonDict
9+
from .addons_set import AddonsSet
710
from .odoo_series import OdooSeries
811

912

@@ -29,6 +32,21 @@ def print_list(lst: Iterable[str], separator: str) -> None:
2932
sys.stdout.write("\n")
3033

3134

35+
def print_json(obj: Any) -> None:
36+
json.dump(obj, sys.stdout)
37+
sys.stdout.write("\n")
38+
39+
40+
def print_addons_as_json(names: Iterable[str], addons_set: AddonsSet) -> None:
41+
d: Dict[str, Optional[AddonDict]] = {}
42+
for name in names:
43+
if name in addons_set:
44+
d[name] = addons_set[name].as_dict()
45+
else:
46+
d[name] = None
47+
print_json(d)
48+
49+
3250
def notice_or_abort(msg: str, abort: bool) -> None:
3351
if abort:
3452
echo.error(msg)

tests/test_cmd_list.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import json
2+
13
from typer.testing import CliRunner
24

35
from manifestoo.commands.list import list_command
@@ -26,3 +28,26 @@ def test_integration(tmp_path):
2628
assert not result.exception
2729
assert result.exit_code == 0, result.stderr
2830
assert result.stdout == "a\nb\n"
31+
32+
33+
def test_integration_json(tmp_path):
34+
addons = {
35+
"a": {"name": "A"},
36+
"b": {},
37+
}
38+
populate_addons_dir(tmp_path, addons)
39+
runner = CliRunner(mix_stderr=False)
40+
result = runner.invoke(
41+
app,
42+
[f"--select-addons-dir={tmp_path}", "list", "--format=json"],
43+
catch_exceptions=False,
44+
)
45+
assert not result.exception
46+
assert result.exit_code == 0, result.stderr
47+
json_output = json.loads(result.stdout)
48+
assert "a" in json_output
49+
assert "b" in json_output
50+
assert "manifest" in json_output["a"]
51+
assert "manifest_path" in json_output["a"]
52+
assert "path" in json_output["a"]
53+
assert json_output["a"]["manifest"] == addons["a"]

tests/test_cmd_list_depends.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import json
2+
13
from typer.testing import CliRunner
24

35
from manifestoo.commands.list_depends import list_depends_command
@@ -159,3 +161,28 @@ def test_integration(tmp_path):
159161
assert not result.exception
160162
assert result.exit_code == 0, result.stderr
161163
assert result.stdout == "b\n"
164+
165+
166+
def test_integration_json(tmp_path):
167+
addons = {
168+
"a": {"depends": ["b"]},
169+
"b": {"name": "B"},
170+
}
171+
populate_addons_dir(tmp_path, addons)
172+
runner = CliRunner(mix_stderr=False)
173+
result = runner.invoke(
174+
app,
175+
[
176+
f"--addons-path={tmp_path}",
177+
"--select-include",
178+
"a",
179+
"list-depends",
180+
"--format=json",
181+
],
182+
catch_exceptions=False,
183+
)
184+
assert not result.exception
185+
assert result.exit_code == 0, result.stderr
186+
json_output = json.loads(result.stdout)
187+
assert len(json_output) == 1
188+
assert json_output["b"]["manifest"] == addons["b"]

tests/test_utils.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import pytest
22
import typer
33

4-
from manifestoo.utils import comma_split, not_implemented, notice_or_abort, print_list
4+
from manifestoo.utils import (
5+
comma_split,
6+
not_implemented,
7+
notice_or_abort,
8+
print_json,
9+
print_list,
10+
)
511

612

713
@pytest.mark.parametrize(
@@ -24,6 +30,11 @@ def test_print_list(capsys):
2430
assert capsys.readouterr().out == "b,a\n"
2531

2632

33+
def test_print_json(capsys):
34+
print_json(["b", "a"])
35+
assert capsys.readouterr().out == '["b", "a"]\n'
36+
37+
2738
def test_print_empty_list(capsys):
2839
print_list([], ",")
2940
assert capsys.readouterr().out == ""

0 commit comments

Comments
 (0)