Skip to content

Commit

Permalink
[IMP] list_depends: allow to sort topologically
Browse files Browse the repository at this point in the history
  • Loading branch information
ThomasBinsfeld committed Oct 31, 2023
1 parent c3ab1f8 commit 5a13e9a
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 11 deletions.
5 changes: 4 additions & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ $ manifestoo [OPTIONS] COMMAND [ARGS]...
* `--addons-path-from-import-odoo / --no-addons-path-from-import-odoo`: Expand addons path by trying to `import odoo` and looking at `odoo.addons.__path__`. This option is useful when addons have been installed with pip. [default: addons-path-from-import-odoo]
* `--addons-path-python PYTHON`: The python executable to use when importing `odoo.addons.__path__`. Defaults to the `python` executable found in PATH.
* `--addons-path-from-odoo-cfg FILE`: Expand addons path by looking into the provided Odoo configuration file. [env var: ODOO_RC]
* `--odoo-series [8.0|9.0|10.0|11.0|12.0|13.0|14.0|15.0|16.0]`: Odoo series to use, in case it is not autodetected from addons version. [env var: ODOO_VERSION, ODOO_SERIES]
* `--odoo-series [8.0|9.0|10.0|11.0|12.0|13.0|14.0|15.0|16.0|17.0]`: Odoo series to use, in case it is not autodetected from addons version. [env var: ODOO_VERSION, ODOO_SERIES]
* `-v, --verbose`
* `-q, --quiet`
* `--version`
Expand Down Expand Up @@ -95,6 +95,7 @@ $ manifestoo list [OPTIONS]
**Options**:

* `--separator TEXT`: Separator character to use (by default, print one item per line).
* `--sorting, --sort TEXT`: Choice between 'alphabetical' and 'topological'.Topological sorting is useful when seeking a migration order. [default: alphabetical]
* `--help`: Show this message and exit.

## `manifestoo list-codepends`
Expand All @@ -115,6 +116,7 @@ $ manifestoo list-codepends [OPTIONS]
* `--separator TEXT`: Separator character to use (by default, print one item per line).
* `--transitive / --no-transitive`: Print all transitive co-dependencies. [default: transitive]
* `--include-selected / --no-include-selected`: Print the selected addons along with their co-dependencies. [default: include-selected]
* `--sorting, --sort TEXT`: Choice between 'alphabetical' and 'topological'.Topological sorting is useful when seeking a migration order. [default: alphabetical]
* `--help`: Show this message and exit.

## `manifestoo list-depends`
Expand All @@ -134,6 +136,7 @@ $ manifestoo list-depends [OPTIONS]
* `--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.
* `--as-pip-requirements`
* `--sorting, --sort TEXT`: Choice between 'alphabetical' and 'topological'.Topological sorting is useful when seeking a migration order. [default: alphabetical]
* `--help`: Show this message and exit.

## `manifestoo list-external-dependencies`
Expand Down
1 change: 1 addition & 0 deletions news/62.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add ``--sorting`` option on list, list-depends and list-codepends.
67 changes: 67 additions & 0 deletions src/manifestoo/addon_sorter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import sys
from typing import Dict, Iterable, Set

import typer

from manifestoo_core.addons_set import AddonsSet

from . import echo
from .exceptions import CycleErrorExit


class AddonSorter:
@staticmethod
def from_name(name: str) -> "AddonSorter":
if name == "alphabetical":
return AddonSorterAlphabetical()
elif name == "topological":
if sys.version_info < (3, 9):
echo.error(
"The 'topological' sorter requires Python 3.9 or later",
err=False,
)
raise typer.Exit(1)
return AddonSorterTopological()
else:
echo.error(f"Unknown sorter {name}", err=False)
raise typer.Exit(1)

def sort(
self, addons_selection: Iterable[str], addon_set: AddonsSet
) -> Iterable[str]:
raise NotImplementedError()


class AddonSorterAlphabetical(AddonSorter):
def sort(
self, addons_selection: Iterable[str], addon_set: AddonsSet
) -> Iterable[str]:
return sorted(addons_selection)


class AddonSorterTopological(AddonSorter):
def sort(
self, addons_selection: Iterable[str], addon_set: AddonsSet
) -> Iterable[str]:
result_dict: Dict[str, Set[str]] = {}
for addon_name in addons_selection:
try:
addon = addon_set[addon_name]
result_dict[addon_name] = set(
[
depend
for depend in addon.manifest.depends
if depend in addons_selection
]
)
except KeyError:
echo.debug(f"Addon {addon_name} not found in addon set")
from graphlib import CycleError, TopologicalSorter

topological_sorted_res = TopologicalSorter(result_dict)
try:
res = list(topological_sorted_res.static_order())
except CycleError as e:
echo.error("Cycle detected in dependencies", err=False)
raise CycleErrorExit(1) from e
return res
15 changes: 12 additions & 3 deletions src/manifestoo/commands/list.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
from typing import Iterable
from typing import Iterable, Optional

from manifestoo_core.addons_set import AddonsSet

from ..addon_sorter import AddonSorter, AddonSorterAlphabetical
from ..addons_selection import AddonsSelection


def list_command(addons_selection: AddonsSelection) -> Iterable[str]:
return sorted(addons_selection)
def list_command(
addons_selection: AddonsSelection,
addons_set: AddonsSet,
addon_sorter: Optional[AddonSorter] = None,
) -> Iterable[str]:
if not addon_sorter:
addon_sorter = AddonSorterAlphabetical()
return addon_sorter.sort(addons_selection, addons_set)
9 changes: 7 additions & 2 deletions src/manifestoo/commands/list_codepends.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import Iterable, Set
from typing import Iterable, Optional, Set

from manifestoo_core.addons_set import AddonsSet

from ..addon_sorter import AddonSorter, AddonSorterAlphabetical
from ..addons_selection import AddonsSelection


Expand All @@ -10,14 +11,18 @@ def list_codepends_command(
addons_set: AddonsSet,
transitive: bool = True,
include_selected: bool = True,
addon_sorter: Optional[AddonSorter] = None,
) -> Iterable[str]:
if not addon_sorter:
addon_sorter = AddonSorterAlphabetical()
result: Set[str] = set(addons_selection) if include_selected else set()
codeps = direct_codependencies(addons_selection, addons_set, result)
result |= codeps
while transitive and codeps:
codeps = direct_codependencies(codeps, addons_set, result)
result |= codeps
return result if include_selected else result - addons_selection
res = result if include_selected else result - set(addons_selection)
return set(addon_sorter.sort(res, addons_set))


def direct_codependencies(
Expand Down
8 changes: 6 additions & 2 deletions src/manifestoo/commands/list_depends.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import Iterable, Set, Tuple
from typing import Iterable, Optional, Set, Tuple

from manifestoo_core.addons_set import AddonsSet

from ..addon_sorter import AddonSorter, AddonSorterAlphabetical
from ..addons_selection import AddonsSelection
from ..dependency_iterator import dependency_iterator

Expand All @@ -11,7 +12,10 @@ def list_depends_command(
addons_set: AddonsSet,
transitive: bool = False,
include_selected: bool = False,
addon_sorter: Optional[AddonSorter] = None,
) -> Tuple[Iterable[str], Iterable[str]]:
if not addon_sorter:
addon_sorter = AddonSorterAlphabetical()
result: Set[str] = set()
missing: Set[str] = set()
for addon_name, addon in dependency_iterator(
Expand All @@ -23,4 +27,4 @@ def list_depends_command(
missing.add(addon_name)
else:
result.update(set(addon.manifest.depends) - set(addons_selection))
return sorted(result), missing
return addon_sorter.sort(result, addons_set), missing
5 changes: 5 additions & 0 deletions src/manifestoo/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from typer import Exit


class CycleErrorExit(Exit):
pass
40 changes: 39 additions & 1 deletion src/manifestoo/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from manifestoo_core.odoo_series import OdooSeries, detect_from_addons_set

from . import echo
from .addon_sorter import AddonSorter
from .commands.check_dev_status import check_dev_status_command
from .commands.check_licenses import check_licenses_command
from .commands.interactive_tree import interactive_tree_command
Expand Down Expand Up @@ -225,10 +226,23 @@ def list(
None,
help="Separator character to use (by default, print one item per line).",
),
sorting: str = typer.Option(
"alphabetical",
"--sorting",
"--sort",
help=(
"Choice between 'alphabetical' and 'topological'."
"Topological sorting is useful when seeking a migration order."
),
show_default=True,
),
) -> None:
"""Print the selected addons."""
main_options: MainOptions = ctx.obj
result = list_command(main_options.addons_selection)
addon_sorter = AddonSorter.from_name(sorting)
result = list_command(
main_options.addons_selection, main_options.addons_set, addon_sorter
)
print_list(result, separator or main_options.separator or "\n")


Expand Down Expand Up @@ -266,16 +280,28 @@ def list_depends(
"--as-pip-requirements",
show_default=False,
),
sorting: str = typer.Option(
"alphabetical",
"--sorting",
"--sort",
help=(
"Choice between 'alphabetical' and 'topological'."
"Topological sorting is useful when seeking a migration order."
),
show_default=True,
),
) -> None:
"""Print the dependencies of selected addons."""
main_options: MainOptions = ctx.obj
if as_pip_requirements:
not_implemented("--as-pip-requirement")
addon_sorter = AddonSorter.from_name(sorting)
result, missing = list_depends_command(
main_options.addons_selection,
main_options.addons_set,
transitive,
include_selected,
addon_sorter,
)
if missing and not ignore_missing:
echo.error("not found in addons path: " + ",".join(sorted(missing)))
Expand All @@ -301,18 +327,30 @@ def list_codepends(
True,
help="Print the selected addons along with their co-dependencies.",
),
sorting: str = typer.Option(
"alphabetical",
"--sorting",
"--sort",
help=(
"Choice between 'alphabetical' and 'topological'."
"Topological sorting is useful when seeking a migration order."
),
show_default=True,
),
) -> None:
"""Print the co-dependencies of selected addons.
Co-dependencies is the set of addons that depend on the selected
addons.
"""
main_options: MainOptions = ctx.obj
addon_sorter = AddonSorter.from_name(sorting)
result = list_codepends_command(
main_options.addons_selection,
main_options.addons_set,
transitive,
include_selected,
addon_sorter,
)
print_list(result, separator or main_options.separator or "\n")

Expand Down
10 changes: 8 additions & 2 deletions tests/test_cmd_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@
from manifestoo.commands.list import list_command
from manifestoo.main import app

from .common import mock_addons_selection, populate_addons_dir
from .common import mock_addons_selection, mock_addons_set, populate_addons_dir


def test_basic():
addons_selection = mock_addons_selection("b,a")
assert list_command(addons_selection) == ["a", "b"]
addons_set = mock_addons_set(
{
"a": {},
"b": {},
}
)
assert list_command(addons_selection, addons_set) == ["a", "b"]


def test_integration(tmp_path):
Expand Down
53 changes: 53 additions & 0 deletions tests/test_cmd_list_codepends.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import sys

import pytest
from typer.testing import CliRunner

from manifestoo.addon_sorter import AddonSorterTopological
from manifestoo.commands.list_codepends import list_codepends_command
from manifestoo.main import app

Expand Down Expand Up @@ -33,6 +37,32 @@ def test_transitive():
) == {"b"}
assert list_codepends_command(
addons_selection, addons_set, transitive=True, include_selected=False
) == {"a", "b"}


@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires python3.9 or higher")
def test_transitive_topological():
addons_set = mock_addons_set(
{
"a": {"depends": ["b"]},
"b": {"depends": ["c"]},
"c": {},
}
)
addons_selection = mock_addons_selection("c")
assert list_codepends_command(
addons_selection,
addons_set,
transitive=False,
include_selected=False,
addon_sorter=AddonSorterTopological(),
) == {"b"}
assert list_codepends_command(
addons_selection,
addons_set,
transitive=True,
include_selected=False,
addon_sorter=AddonSorterTopological(),
) == {"b", "a"}


Expand Down Expand Up @@ -87,6 +117,29 @@ def test_include_selected():
) == {"a", "b"}


@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires python3.9 or higher")
def test_include_selected_topological():
addons_set = mock_addons_set(
{
"a": {"depends": ["b"]},
"b": {},
}
)
addons_selection = mock_addons_selection("b")
assert list_codepends_command(
addons_selection,
addons_set,
include_selected=False,
addon_sorter=AddonSorterTopological(),
) == {"a"}
assert list_codepends_command(
addons_selection,
addons_set,
include_selected=True,
addon_sorter=AddonSorterTopological(),
) == {"b", "a"}


def test_integration(tmp_path):
addons = {
"a": {"depends": ["b"]},
Expand Down
Loading

0 comments on commit 5a13e9a

Please sign in to comment.