Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add json output to list and list-depends #12

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ $ manifestoo list [OPTIONS]

**Options**:

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

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

**Options**:

* `--separator TEXT`: Separator charater to use (by default, print one item per line).
* `--format [names|json]`: Output format [default: names]
* `--separator TEXT`: Separator character to use for the 'names' format (by
default, print one item per line).
* `--transitive`: Print all transitive dependencies.
* `--include-selected`: Print the selected addons along with their dependencies.
* `--ignore-missing`: Do not fail if dependencies are not found in addons path. This only applies to top level (selected) addons and transitive dependencies.
Expand Down
1 change: 1 addition & 0 deletions news/11.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add rich `json` output format to `list` and `list-depends` commands.
25 changes: 24 additions & 1 deletion src/manifestoo/addon.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import sys
from pathlib import Path

from .manifest import InvalidManifest, Manifest
from .manifest import InvalidManifest, Manifest, ManifestDict

if sys.version_info >= (3, 8):
from typing import TypedDict

class AddonDict(TypedDict):
manifest: ManifestDict
manifest_path: str
path: str


else:
from typing import Any, Dict

AddonDict = Dict[str, Any]


class AddonNotFound(Exception):
Expand Down Expand Up @@ -52,3 +67,11 @@ def from_addon_dir(
except InvalidManifest as e:
raise AddonNotFoundInvalidManifest(str(e)) from e
return cls(manifest)

def as_dict(self) -> AddonDict:
"""Convert to a dictionary suitable for json output."""
return dict(
manifest=self.manifest.manifest_dict,
manifest_path=str(self.manifest_path),
path=str(self.path),
)
5 changes: 3 additions & 2 deletions src/manifestoo/commands/tree.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, List, Optional, Set
from typing import Dict, List, Optional, Set, cast

import typer

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

def sversion(self, odoo_series: OdooSeries) -> Optional[str]:
if not self.addon:
return typer.style("✘ not installed", fg=typer.colors.RED)
# typer.style (from click, actually) miss a type annotation
return cast(str, typer.style("✘ not installed", fg=typer.colors.RED))
elif is_core_ce_addon(self.addon_name, odoo_series):
return f"{odoo_series}+{OdooEdition.CE}"
elif is_core_ee_addon(self.addon_name, odoo_series):
Expand Down
46 changes: 36 additions & 10 deletions src/manifestoo/main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from enum import Enum
from pathlib import Path
from typing import List, Optional

Expand All @@ -13,7 +14,7 @@
from .core_addons import get_core_addons
from .odoo_series import OdooSeries, detect_from_addons_set
from .options import MainOptions
from .utils import ensure_odoo_series, not_implemented, print_list
from .utils import ensure_odoo_series, not_implemented, print_addons_as_json, print_list
from .version import __version__

app = typer.Typer()
Expand Down Expand Up @@ -185,26 +186,48 @@ def callback(
ctx.obj = main_options


class Format(str, Enum):
names = "names"
json = "json"


@app.command()
def list(
ctx: typer.Context,
format: Format = typer.Option(
Format.names,
help="Output format",
),
separator: Optional[str] = typer.Option(
None,
help="Separator charater to use (by default, print one item per line).",
help=(
"Separator character to use for the 'names' format "
"(by default, print one item per line)."
),
),
) -> None:
"""Print the selected addons."""
main_options: MainOptions = ctx.obj
result = list_command(main_options.addons_selection)
print_list(result, separator or main_options.separator or "\n")
names = list_command(main_options.addons_selection)
if format == Format.names:
print_list(names, separator or main_options.separator or "\n")
else:
print_addons_as_json(names, main_options.addons_set)


@app.command()
def list_depends(
ctx: typer.Context,
format: Format = typer.Option(
Format.names,
help="Output format",
),
separator: Optional[str] = typer.Option(
None,
help="Separator charater to use (by default, print one item per line).",
help=(
"Separator character to use for the 'names' format "
"(by default, print one item per line)."
),
),
transitive: bool = typer.Option(
False,
Expand Down Expand Up @@ -238,7 +261,7 @@ def list_depends(
main_options: MainOptions = ctx.obj
if as_pip_requirements:
not_implemented("--as-pip-requirement")
result, missing = list_depends_command(
names, missing = list_depends_command(
main_options.addons_selection,
main_options.addons_set,
transitive,
Expand All @@ -247,10 +270,13 @@ def list_depends(
if missing and not ignore_missing:
echo.error("not found in addons path: " + ",".join(sorted(missing)))
raise typer.Abort()
print_list(
result,
separator or main_options.separator or "\n",
)
if format == Format.names:
print_list(
names,
separator or main_options.separator or "\n",
)
else:
print_addons_as_json(names, main_options.addons_set)


@app.command()
Expand Down
5 changes: 4 additions & 1 deletion src/manifestoo/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,11 @@ class InvalidManifest(Exception):
pass


ManifestDict = Dict[Any, Any]


class Manifest:
def __init__(self, manifest_path: Path, manifest_dict: Dict[Any, Any]) -> None:
def __init__(self, manifest_path: Path, manifest_dict: ManifestDict) -> None:
self.manifest_path = manifest_path
self.manifest_dict = manifest_dict

Expand Down
20 changes: 19 additions & 1 deletion src/manifestoo/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import json
import sys
from typing import Iterable, List, Optional
from typing import Any, Dict, Iterable, List, Optional

import typer

from . import echo
from .addon import AddonDict
from .addons_set import AddonsSet
from .odoo_series import OdooSeries


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


def print_json(obj: Any) -> None:
json.dump(obj, sys.stdout)
sys.stdout.write("\n")


def print_addons_as_json(names: Iterable[str], addons_set: AddonsSet) -> None:
d: Dict[str, Optional[AddonDict]] = {}
for name in names:
if name in addons_set:
d[name] = addons_set[name].as_dict()
else:
d[name] = None
print_json(d)


def notice_or_abort(msg: str, abort: bool) -> None:
if abort:
echo.error(msg)
Expand Down
25 changes: 25 additions & 0 deletions tests/test_cmd_list.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import json

from typer.testing import CliRunner

from manifestoo.commands.list import list_command
Expand Down Expand Up @@ -26,3 +28,26 @@ def test_integration(tmp_path):
assert not result.exception
assert result.exit_code == 0, result.stderr
assert result.stdout == "a\nb\n"


def test_integration_json(tmp_path):
addons = {
"a": {"name": "A"},
"b": {},
}
populate_addons_dir(tmp_path, addons)
runner = CliRunner(mix_stderr=False)
result = runner.invoke(
app,
[f"--select-addons-dir={tmp_path}", "list", "--format=json"],
catch_exceptions=False,
)
assert not result.exception
assert result.exit_code == 0, result.stderr
json_output = json.loads(result.stdout)
assert "a" in json_output
assert "b" in json_output
assert "manifest" in json_output["a"]
assert "manifest_path" in json_output["a"]
assert "path" in json_output["a"]
assert json_output["a"]["manifest"] == addons["a"]
27 changes: 27 additions & 0 deletions tests/test_cmd_list_depends.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import json

from typer.testing import CliRunner

from manifestoo.commands.list_depends import list_depends_command
Expand Down Expand Up @@ -159,3 +161,28 @@ def test_integration(tmp_path):
assert not result.exception
assert result.exit_code == 0, result.stderr
assert result.stdout == "b\n"


def test_integration_json(tmp_path):
addons = {
"a": {"depends": ["b"]},
"b": {"name": "B"},
}
populate_addons_dir(tmp_path, addons)
runner = CliRunner(mix_stderr=False)
result = runner.invoke(
app,
[
f"--addons-path={tmp_path}",
"--select-include",
"a",
"list-depends",
"--format=json",
],
catch_exceptions=False,
)
assert not result.exception
assert result.exit_code == 0, result.stderr
json_output = json.loads(result.stdout)
assert len(json_output) == 1
assert json_output["b"]["manifest"] == addons["b"]
13 changes: 12 additions & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import pytest
import typer

from manifestoo.utils import comma_split, not_implemented, notice_or_abort, print_list
from manifestoo.utils import (
comma_split,
not_implemented,
notice_or_abort,
print_json,
print_list,
)


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


def test_print_json(capsys):
print_json(["b", "a"])
assert capsys.readouterr().out == '["b", "a"]\n'


def test_print_empty_list(capsys):
print_list([], ",")
assert capsys.readouterr().out == ""
Expand Down