Refactor main app

This commit is contained in:
Kivi Kaitaniemi
2025-12-13 17:35:21 +02:00
parent 364bf3d7b9
commit 9471bbf6ac
5 changed files with 215 additions and 153 deletions
+187 -132
View File
@@ -28,8 +28,8 @@ def main():
parser.add_argument("--source", action="store", help="python file containing configuration") parser.add_argument("--source", action="store", help="python file containing configuration")
parser.add_argument( parser.add_argument(
"--print",
"--dry-run", "--dry-run",
"--print",
action="store_true", action="store_true",
default=False, default=False,
help="print what would happen as a result of running decman", help="print what would happen as a result of running decman",
@@ -62,28 +62,27 @@ def main():
with _store.Store(_STORE_FILE, args.dry_run) as store: with _store.Store(_STORE_FILE, args.dry_run) as store:
try: try:
_execute_source(store, args) _execute_source(store, args)
failed = run_decman(store, args) failed = not run_decman(store, args)
except OSError as error:
output.print_error(
f"Unexpected OSError while running decman: {error.strerror or str(error)}"
)
output.print_traceback()
except errors.SourceError as error: except errors.SourceError as error:
output.print_error(f"Error raised manually in the source: {error}") output.print_error(f"Error raised manually in the source: {error}")
output.print_traceback() output.print_traceback()
failed = True
except errors.CommandFailedError as error: except errors.CommandFailedError as error:
output.print_error(f"{error}") output.print_error(f"{error}")
output.print_traceback() output.print_traceback()
failed = True
except errors.InvalidOnDisableError as error: except errors.InvalidOnDisableError as error:
output.print_error(f"Invalid source. {error}") output.print_error(f"Invalid source. {error}")
output.print_traceback() output.print_traceback()
failed = True
except Exception as error:
output.print_error(f"Unexpected error while running decman: {error}")
output.print_traceback()
failed = True
except OSError as error: except OSError as error:
output.print_error(f"Failed to access decman store file '{_STORE_FILE}': {error.strerror}.") output.print_error(f"Failed to access decman store file '{_STORE_FILE}': {error.strerror}.")
output.print_error("This may cause already completed operations to run again.") output.print_error("This may cause already completed operations to run again.")
output.print_traceback() output.print_traceback()
except Exception as error:
output.print_error(f"Unexpected error while running decman: {error}")
output.print_traceback()
finally: finally:
os.chdir(original_wd) os.chdir(original_wd)
@@ -91,129 +90,18 @@ def main():
sys.exit(1) sys.exit(1)
def run_decman(store: _store.Store, args: argparse.Namespace) -> bool:
# Ensure execution order is correct. Remove steps not executed now.
execution_order = []
if args.only:
output.print_debug("Argument '--only' is set. Pruning execution steps.")
for step in decman.execution_order:
if step in args.only:
output.print_debug(f"Adding {step} to execution order.")
execution_order.append(step)
else:
execution_order = decman.execution_order
for skip in args.skip:
output.print_debug(f"Skipping step {skip}.")
execution_order.remove(skip)
output.print_debug(f"Execution order is: {', '.join(execution_order)}.")
# Find newly enabled and disabled modules.
store.ensure("enabled_modules", [])
store.ensure("module_on_disable_scripts", {})
new_modules = []
disabled_modules = []
for module in decman.modules:
if module.name not in store["enabled_modules"]:
new_modules.append(module.name)
for module_name in store["enabled_modules"]:
if module_name not in decman.modules:
disabled_modules.append(module_name)
output.print_debug(f"New modules are: {', '.join(new_modules)}.")
output.print_debug(f"Disabled modules are: {', '.join(disabled_modules)}.")
# Disable hooks should be run before anything else because they might depend on packages that
# are going to get removed.
if not args.no_hooks:
output.print_summary("Running 'before update' -hooks.")
for module in decman.modules:
output.print_info(f"Running 'before update' for {module.name}.")
if not args.dry_run:
module.before_update()
if disabled_modules:
output.print_summary("Running 'on disable' -scripts.")
for disabled_module in disabled_modules:
on_disable_script = store["module_on_disable_scripts"].get(disabled_module, None)
if on_disable_script:
output.print_info(f"Running 'on disable' for {disabled_module}.")
if not args.dry_run:
decman.prg([on_disable_script])
store["enabled_modules"].remove(disabled_module)
store["module_on_disable_scripts"].pop(disabled_module)
# Run main execution order
for step in execution_order:
output.print_debug(f"Running step '{step}'.")
match step:
case "files":
if not file_manager.update_files(
store, decman.modules, decman.files, decman.directories, dry_run=args.dry_run
):
return False
case plugin_name:
plugin = decman.plugins.get(plugin_name, None)
if plugin:
plugin.process_modules(store, decman.modules)
if not plugin.apply(store, dry_run=args.dry_run, params=args.params):
return False
else:
output.print_warning(
f"Plugin '{plugin_name}' configured in execution_order\
but not found in available plugins."
)
# On enable and on change should be ran last since they might depend on effects caused by
# execution steps.
if not args.no_hooks:
output.print_summary("Running 'on enable' -hooks.")
for module in decman.modules:
if module.name in new_modules:
output.print_info(f"Running 'on enable' for {module.name}.")
if not args.dry_run:
module.on_enable()
store["enabled_modules"].append(module.name)
try:
script = _module.write_on_disable_script(
module, conf.module_on_disable_scripts_dir
)
if script:
store["module_on_disable_scripts"][module.name] = script
except OSError as error:
output.print_error(
f"Failed to create 'on disable' script for module {module.name}:\
{error.strerror or str(error)}."
)
output.print_warning(
"This script will NOT be created when decman runs the next time."
)
output.print_summary("Running 'on change' -hooks.")
for module in decman.modules:
if module._changed:
output.print_info(f"Running 'on change' for {module.name}.")
if not args.dry_run:
module.on_change()
output.print_summary("Running 'after update' -hooks.")
for module in decman.modules:
output.print_info(f"Running 'after update' for {module.name}.")
if not args.dry_run:
module.after_update()
return True
def _execute_source(store: _store.Store, args: argparse.Namespace): def _execute_source(store: _store.Store, args: argparse.Namespace):
"""
Runs decman source. May call ``sys.exit(1)`` if user aborts running the source or reading the
source fails.
Raises:
``SourceError``
If code in the source raises this error manually.
``InvalidOnDisableError``
If modules in the source have invalid on_disable functions.
"""
source = store.get("source_file", None) source = store.get("source_file", None)
source_changed = False source_changed = False
@@ -254,3 +142,170 @@ def _execute_source(store: _store.Store, args: argparse.Namespace):
os.chdir(source_dir) os.chdir(source_dir)
sys.path.append(".") sys.path.append(".")
exec(content) exec(content)
def run_decman(store: _store.Store, args: argparse.Namespace) -> bool:
"""
Runs decman with the given arguments and a store.
Returns ``True`` if executed succesfully. Otherwise ``False``.
Raises:
``SourceError``
If code in the source raises this error manually.
``CommandFailedError``
If running any command fails.
"""
store.ensure("enabled_modules", [])
store.ensure("module_on_disable_scripts", {})
execution_order = _determine_execution_order(args)
new_modules = _find_new_modules(store)
disabled_modules = _find_disabled_modules(store)
# Disable hooks should be run before anything else because they might depend on packages that
# are going to get removed.
if not args.no_hooks:
_run_before_update(store, args)
_run_on_disable(store, args, disabled_modules)
# Run main execution order
for step in execution_order:
output.print_debug(f"Running step '{step}'.")
match step:
case "files":
if not file_manager.update_files(
store, decman.modules, decman.files, decman.directories, dry_run=args.dry_run
):
return False
case plugin_name:
plugin = decman.plugins.get(plugin_name, None)
if plugin:
plugin.process_modules(store, decman.modules)
if not plugin.apply(store, dry_run=args.dry_run, params=args.params):
return False
else:
output.print_warning(
f"Plugin '{plugin_name}' configured in execution_order\
but not found in available plugins."
)
# On enable and on change should be ran last since they might depend on effects caused by
# execution steps.
if not args.no_hooks:
_run_on_enable(store, args, new_modules)
_run_on_change(store, args)
_run_after_update(store, args)
return True
def _determine_execution_order(args: argparse.Namespace) -> list[str]:
execution_order = []
if args.only:
output.print_debug("Argument '--only' is set. Pruning execution steps.")
for step in decman.execution_order:
if step in args.only:
output.print_debug(f"Adding {step} to execution order.")
execution_order.append(step)
else:
execution_order = decman.execution_order
for skip in args.skip:
output.print_debug(f"Skipping step {skip}.")
execution_order.remove(skip)
output.print_debug(f"Execution order is: {', '.join(execution_order)}.")
return execution_order
def _find_new_modules(store: _store.Store):
new_modules = []
for module in decman.modules:
if module.name not in store["enabled_modules"]:
new_modules.append(module.name)
output.print_debug(f"New modules are: {', '.join(new_modules)}.")
return new_modules
def _find_disabled_modules(store: _store.Store):
disabled_modules = []
for module_name in store["enabled_modules"]:
if module_name not in decman.modules:
disabled_modules.append(module_name)
output.print_debug(f"Disabled modules are: {', '.join(disabled_modules)}.")
return disabled_modules
def _run_before_update(store: _store.Store, args: argparse.Namespace):
output.print_summary("Running 'before update' -hooks.")
for module in decman.modules:
output.print_info(f"Running 'before update' for {module.name}.")
if not args.dry_run:
module.before_update(store)
def _run_on_disable(store: _store.Store, args: argparse.Namespace, disabled_modules: list[str]):
if not disabled_modules:
return
output.print_summary("Running 'on disable' -scripts.")
for disabled_module in disabled_modules:
on_disable_script = store["module_on_disable_scripts"].get(disabled_module, None)
if on_disable_script:
output.print_info(f"Running 'on disable' for {disabled_module}.")
if not args.dry_run:
decman.prg([on_disable_script])
store["enabled_modules"].remove(disabled_module)
store["module_on_disable_scripts"].pop(disabled_module)
def _run_on_enable(store: _store.Store, args: argparse.Namespace, new_modules: list[str]):
output.print_summary("Running 'on enable' -hooks.")
for module in decman.modules:
if module.name in new_modules:
output.print_info(f"Running 'on enable' for {module.name}.")
if not args.dry_run:
module.on_enable(store)
store["enabled_modules"].append(module.name)
try:
script = _module.write_on_disable_script(
module, conf.module_on_disable_scripts_dir
)
if script:
store["module_on_disable_scripts"][module.name] = script
except OSError as error:
output.print_error(
f"Failed to create 'on disable' script for module {module.name}:\
{error.strerror or str(error)}."
)
output.print_warning(
"This script will NOT be created when decman runs the next time."
)
output.print_warning(
"You should probably investigate the reason for the error. \
Try to fix it, and re-enable this module."
)
def _run_on_change(store: _store.Store, args: argparse.Namespace):
output.print_summary("Running 'on change' -hooks.")
for module in decman.modules:
if module._changed:
output.print_info(f"Running 'on change' for {module.name}.")
if not args.dry_run:
module.on_change(store)
def _run_after_update(store: _store.Store, args: argparse.Namespace):
output.print_summary("Running 'after update' -hooks.")
for module in decman.modules:
output.print_info(f"Running 'after update' for {module.name}.")
if not args.dry_run:
module.after_update(store)
-2
View File
@@ -42,8 +42,6 @@ def update_files(
Returns: Returns:
True if all operations completed successfully, False if installation failed. True if all operations completed successfully, False if installation failed.
""" """
output.print_summary("Installing files.")
all_checked_files = [] all_checked_files = []
all_changed_files = [] all_changed_files = []
store.ensure("all_files", []) store.ensure("all_files", [])
+13 -4
View File
@@ -8,6 +8,7 @@ import typing
import decman.core.error as errors import decman.core.error as errors
import decman.core.fs as fs import decman.core.fs as fs
import decman.core.store as _store
class Module: class Module:
@@ -41,35 +42,43 @@ class Module:
_validate_on_disable(f"{cls.__module__}.{cls.__name__}", func) _validate_on_disable(f"{cls.__module__}.{cls.__name__}", func)
def before_update(self): def before_update(self, store: _store.Store):
""" """
Override this method to run python code before updating the system. Override this method to run python code before updating the system.
``store`` can be used to save persistent data between decman runs.
Handle errors within this function. If an error should abort running decman, Handle errors within this function. If an error should abort running decman,
raise SourceError or CommandFailedError. raise SourceError or CommandFailedError.
""" """
def after_update(self): def after_update(self, store: _store.Store):
""" """
Override this method to run python code after updating the system. Override this method to run python code after updating the system.
``store`` can be used to save persistent data between decman runs.
Handle errors within this function. If an error should abort running decman, Handle errors within this function. If an error should abort running decman,
raise SourceError or CommandFailedError. raise SourceError or CommandFailedError.
""" """
def on_enable(self): def on_enable(self, store: _store.Store):
""" """
Override this method to run python code when this module gets enabled. Override this method to run python code when this module gets enabled.
``store`` can be used to save persistent data between decman runs.
Handle errors within this function. If an error should abort running decman, Handle errors within this function. If an error should abort running decman,
raise SourceError or CommandFailedError. raise SourceError or CommandFailedError.
""" """
def on_change(self): def on_change(self, store: _store.Store):
""" """
Override this method to run python code after the contents of this module have been Override this method to run python code after the contents of this module have been
changed in the source. changed in the source.
``store`` can be used to save persistent data between decman runs.
Handle errors within this function. If an error should abort running decman, Handle errors within this function. If an error should abort running decman,
raise SourceError or CommandFailedError. raise SourceError or CommandFailedError.
""" """
+3 -3
View File
@@ -1,7 +1,7 @@
import importlib.metadata as metadata import importlib.metadata as metadata
import decman.core.module as module import decman.core.module as module
import decman.core.store as cstore import decman.core.store as _store
class Plugin: class Plugin:
@@ -25,7 +25,7 @@ class Plugin:
return True return True
def apply( def apply(
self, store: cstore.Store, dry_run: bool = False, params: list[str] | None = None self, store: _store.Store, dry_run: bool = False, params: list[str] | None = None
) -> bool: ) -> bool:
""" """
Ensures that the state managed by this plugin is present. Ensures that the state managed by this plugin is present.
@@ -39,7 +39,7 @@ class Plugin:
""" """
return True return True
def process_modules(self, store: cstore.Store, modules: set[module.Module]): def process_modules(self, store: _store.Store, modules: set[module.Module]):
""" """
Processes a module. Processes a module.
""" """
+4 -4
View File
@@ -33,16 +33,16 @@ class DummyModule:
self.on_change_called = False self.on_change_called = False
self.after_update_called = False self.after_update_called = False
def before_update(self): def before_update(self, store):
self.before_update_called = True self.before_update_called = True
def on_enable(self): def on_enable(self, store):
self.on_enable_called = True self.on_enable_called = True
def on_change(self): def on_change(self, store):
self.on_change_called = True self.on_change_called = True
def after_update(self): def after_update(self, store):
self.after_update_called = True self.after_update_called = True
@staticmethod @staticmethod