Skip to content

Commit

Permalink
Add the 'sort' command for auto-arranging plugins
Browse files Browse the repository at this point in the history
Add new 'sort' command that arranges plugin order according to order of
enabled mods that provide those plugins. This is done in reverse to
ensure only conflict winners are considered for ordering.

Add several tests to test_pending_changes to ensure commands either
create a pending change or require an absence of pending changes as
expected.
  • Loading branch information
cyberrumor committed Feb 3, 2024
1 parent d6bf032 commit faa7488
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 22 deletions.
14 changes: 1 addition & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,23 +82,14 @@ pip3 install --user --force-reinstall . || pip3 install --user --break-system-pa
| move | (mod\|plugin) \<from_index> \<to_index> | Larger numbers win file conflicts |
| refresh | | Abandon pending changes |
| rename | (mod\|download) \<index> \<name> | Names may contain alphanumerics and underscores |
| sort | | Arrange plugins by mod order |


### Usage Tips and Tricks

- Note that the `{de}activate (mod|plugin)` command supports `all` in place of `<index>`.
This will activate or deactivate all mods or plugins that are visible. Combine this
with the `find` command to quickly organize groups of components with related names.
You can leverage this to automatically sort your plugins to the same order as your
mod list:

```sh
deactivate mod all
# sort your mods with the move command
activate mod all
activate plugin all
commit
```

- The `find` command accepts a special `fomods` argument that will filter by fomods.

Expand All @@ -107,7 +98,6 @@ pip3 install --user --force-reinstall . || pip3 install --user --break-system-pa
keyword. This is an additive filter, so more words equals more matches.

- You can easily return to vanilla like this:

```sh
deactivate mod all
commit
Expand All @@ -121,8 +111,6 @@ pip3 install --user --force-reinstall . || pip3 install --user --break-system-pa
- If you have several downloads and you want to install all of them at once, simply
`install all`.

- Combining `find` filters with `all` is a great way to quickly manage groups of
related components, as the `all` keyword only operates on visible components.

## Contributing

Expand Down
21 changes: 19 additions & 2 deletions ammo/mod_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,6 @@ def _autocomplete(self, text: str, state: int) -> Union[str, None]:
name, *args = buf.split()
completions = []

# print(f"{name=}")
# print(f"{args=}")
assert name in dir(self)

# Identify the method we're calling.
Expand Down Expand Up @@ -538,6 +536,25 @@ def deactivate(self, component: ComponentEnum, index: Union[int, str]) -> None:
# Demote IndexErrors
raise Warning(e)

def sort(self) -> None:
"""
Arrange plugins by mod order
"""
plugins = []
for mod in self.mods[::-1]:
if not mod.enabled:
continue
for plugin_name in mod.plugins:
for plugin in self.plugins:
if plugin.name == plugin_name and plugin.name not in (
i.name for i in plugins
):
plugins.insert(0, plugin)
break

self.plugins = plugins
self.changes = True

def rename(self, component: RenameEnum, index: int, name: str) -> None:
"""
Names may contain alphanumerics and underscores
Expand Down
201 changes: 194 additions & 7 deletions test/test_pending_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from ammo.component import (
ComponentEnum,
DeleteEnum,
RenameEnum,
)
from common import (
AmmoController,
Expand All @@ -13,12 +14,12 @@
)


def test_pending_change_restrictions():
def test_pending_change_restrictions_delete_mod():
"""
Actions that require the persistent state and in-memory state to be the
same should not be possible to perform when there are pending changes.
Test that the commands configure, delete, install and rename are blocked.
Test delete mod.
"""
with AmmoController() as controller:
install_mod(controller, "normal_mod")
Expand All @@ -27,11 +28,110 @@ def test_pending_change_restrictions():
with pytest.raises(Warning):
controller.delete(DeleteEnum.MOD, 0)


def test_pending_change_restrictions_delete_download():
"""
Actions that require the persistent state and in-memory state to be the
same should not be possible to perform when there are pending changes.
Test delete download.
"""
with AmmoController() as controller:
# Create a temp download that we can manipulate,
# just in case we're not restricted from deleting it.
# Don't want to mess up the expected files in test/Downloads.
temp_download = controller.downloads_dir / "temp_download.7z"
with open(temp_download, "w") as f:
f.write("")

controller.refresh()
controller.changes = True

download_index = [i.name for i in controller.downloads].index(
"temp_download.7z"
)

try:
with pytest.raises(Warning):
controller.delete(DeleteEnum.DOWNLOAD, download_index)
finally:
temp_download.unlink(missing_ok=True)


def test_pending_change_restrictions_delete_plugin():
"""
Actions that require the persistent state and in-memory state to be the
same should not be possible to perform when there are pending changes.
Test delete plugin.
"""
with AmmoController() as controller:
install_mod(controller, "normal_mod")
controller.changes = True

with pytest.raises(Warning):
controller.delete(DeleteEnum.PLUGIN, 0)


def test_pending_change_restrictions_install():
"""
Actions that require the persistent state and in-memory state to be the
same should not be possible to perform when there are pending changes.
Test install.
"""
with AmmoController() as controller:
install_mod(controller, "normal_mod")
controller.changes = True
with pytest.raises(Warning):
controller.install(1)


def test_pending_change_restrictions_rename_mod():
"""
Actions that require the persistent state and in-memory state to be the
same should not be possible to perform when there are pending changes.
Test rename mod.
"""
with AmmoController() as controller:
install_mod(controller, "normal_mod")
controller.changes = True

with pytest.raises(Warning):
controller.rename(ComponentEnum.MOD, 0, "new name")
controller.rename(RenameEnum.MOD, 0, "new_name")


def test_pending_change_restrictions_rename_download():
"""
Actions that require the persistent state and in-memory state to be the
same should not be possible to perform when there are pending changes.
Test rename download.
"""
with AmmoController() as controller:
# Create a temp download that we can manipulate,
# just in case we're not restricted from renaming it.
# Don't want to mess up the expected files in test/Downloads.
temp_download = controller.downloads_dir / "temp_download.7z"
with open(temp_download, "w") as f:
f.write("")

controller.refresh()
controller.changes = True

download_index = [i.name for i in controller.downloads].index(
"temp_download.7z"
)

try:
with pytest.raises(Warning):
controller.rename(
RenameEnum.DOWNLOAD, download_index, "new_download_name"
)
finally:
temp_download.unlink(missing_ok=True)
(controller.downloads_dir / "new_download_name.7z").unlink(missing_ok=True)


def test_pending_change_move():
Expand Down Expand Up @@ -132,12 +232,99 @@ def test_pending_change_install():
assert controller.changes is False, "Install command created a pending change"


def test_pending_change_delete():
def test_pending_change_delete_mod():
"""
Tests that delete does not create a pending change.
"""
with AmmoController() as controller:
install_mod(controller, "normal_mod")
with pytest.raises(TypeError):
controller.delete(ComponentEnum.MOD, 0)
assert controller.changes is False, "Delete command created a pending change."
controller.delete(DeleteEnum.MOD, 0)
assert (
controller.changes is False
), "Delete mod command created a pending change."


def test_pending_change_delete_download():
"""
Tests that delete download does not create a pending change.
"""
with AmmoController() as controller:
temp_download = controller.downloads_dir / "temp_download.7z"
with open(temp_download, "w") as f:
f.write("")
controller.refresh()

download_index = [i.name for i in controller.downloads].index(
"temp_download.7z"
)

try:
controller.delete(DeleteEnum.DOWNLOAD, download_index)
assert (
controller.changes is False
), "Delete download command created a pending change."
finally:
temp_download.unlink(missing_ok=True)
(controller.downloads_dir / "new_download_name.7z").unlink(missing_ok=True)


def test_pending_change_delete_plugin():
"""
Tests that delete plugin does not create a pending change.
"""
with AmmoController() as controller:
install_mod(controller, "normal_mod")
controller.delete(DeleteEnum.PLUGIN, 0)
assert (
controller.changes is False
), "Delete plugin command created a pending change."


def test_pending_change_rename_mod():
"""
Tests that renaming a mod does not create a pending change.
"""
with AmmoController() as controller:
install_mod(controller, "normal_mod")
controller.rename(RenameEnum.MOD, 0, "new_mod_name")
assert (
controller.changes is False
), "Rename mod command created a pending change."


def test_pending_change_rename_download():
"""
Tests that renaming a download does not create a pending change.
"""
# generate an expendable download file, delete
# download in range.
with AmmoController() as controller:
temp_download = controller.downloads_dir / "temp_download.7z"
with open(temp_download, "w") as f:
f.write("")

controller.refresh()

download_index = [i.name for i in controller.downloads].index(
"temp_download.7z"
)

try:
controller.rename(RenameEnum.DOWNLOAD, download_index, "new_download_name")
assert (
controller.changes is False
), "Rename download command created a pending change."
finally:
temp_download.unlink(missing_ok=True)
(controller.downloads_dir / "new_download_name.7z").unlink(missing_ok=True)


def test_pending_change_sort():
"""
Tests that the 'sort' command creates a pending change.
"""
with AmmoController() as controller:
controller.sort()
assert (
controller.changes is True
), "Sort command didn't create a pending change."

0 comments on commit faa7488

Please sign in to comment.