%PDF- %PDF-
Direktori : /lib/python3/dist-packages/nala/ |
Current File : //lib/python3/dist-packages/nala/options.py |
# __ # ____ _____ | | _____ # / \\__ \ | | \__ \ # | | \/ __ \| |__/ __ \_ # |___| (____ /____(____ / # \/ \/ \/ # # Copyright (C) 2021, 2022 Blake Lee # # This file is part of nala # # nala is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # nala is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with nala. If not, see <https://www.gnu.org/licenses/>. """The options module.""" from __future__ import annotations import sys from pydoc import pager from subprocess import run from typing import NoReturn, Optional, Union, cast import tomli import typer from apt_pkg import config as apt_config from nala import ROOT, _, __version__, color from nala.constants import ERROR_PREFIX, GPL3_LICENSE, NOTICE_PREFIX class Config: """Class for managing configurations.""" def __init__(self) -> None: """Class for managing configurations.""" self.conf = f"{ROOT}/etc/nala/nala.conf" self.data: dict[str, dict[str, str | bool]] = {"Nala": {}} self.apt = apt_config def read_config(self) -> None: """Read the configuration file.""" try: with open(self.conf, "rb") as file: self.data = tomli.load(file) except (tomli.TOMLDecodeError, FileNotFoundError) as error: print(f"{ERROR_PREFIX} {error}") print( _( "{notice} Unable to read config file: {filename}. Using defaults" ).format( notice=NOTICE_PREFIX, filename=color(self.conf, "YELLOW"), file=sys.stderr, ) ) @staticmethod def key_error(key: object, value: object) -> NoReturn: """Exit with key error.""" sys.exit( _("{error} Config key '{key}' should be a bool not {value}").format( error=ERROR_PREFIX, key=key, value=value ) ) def get_bool(self, key: str, default: bool = False) -> bool: """Get Boolean from config.""" value = self.data["Nala"].get(key, default) if isinstance(value, bool): return value self.key_error(key, value) def get_str(self, key: str, default: str = "") -> str: """Get String from config.""" value = self.data["Nala"].get(key, default) if isinstance(value, str): return value self.key_error(key, value) def get_hook(self, key: str) -> dict[str, Union[str, dict[str, str | list[str]]]]: """Get Install Hooks from config.""" return cast( dict[str, Union[str, dict[str, Union[str, list[str]]]]], self.data.get(key, {}), ) def set(self, key: str, value: str | bool) -> None: """Set value in the Nala Config.""" self.data["Nala"][key] = value # # This will likely need a lot of testing to be able to do something like this # # For now it shall remain disabled indefinitely # # # def recurse_config(self, key: str, conf: dict): # """Recurse nala config and pass through apt options. # Example of toml apt option:: # APT.Get.AllowUnauthenticated = true # """ # for new_key, value in conf.items(): # final_key = new_key # if key: # final_key = f"{key}::{new_key}" # if isinstance(value, dict): # self.recurse_config(final_key, value) # continue # if value == False: # value = "false" # elif value == True: # value = "true" # self.apt.set(final_key, value) class Arguments: """Arguments class.""" # pylint: disable=too-many-instance-attributes, too-many-public-methods def __init__(self) -> None: """Arguments class.""" self.command: str = "" self.config = Config() self.config.read_config() # True Global self.verbose: bool self.debug: bool # Semi Global self.download_only: bool self.install_recommends: bool self.install_suggests: bool self.remove_essential: bool self.assume_yes: bool self.assume_no: bool self.update: bool self.raw_dpkg: bool self.purge: bool = False self.fix_broken: bool # Used in Show, List and Search self.all_versions: bool # Used in Search self.all_arches: bool # Search and List Arguments self.names: bool self.upgradable: bool self.installed: bool self.virtual: bool self.full: bool self.history: str | None self.history_id: str self.scroll: bool self.auto_remove: bool self.init_config() def __str__(self) -> str: """Return the state of the object as a string.""" kwarg = "\n ".join( (f"{key} = {value},") for key, value in self.__dict__.items() ) return f"Options = [\n {kwarg}\n]" def set_verbose(self, value: bool) -> None: """Set option.""" if not value: self.verbose = False return self.verbose = True self.scroll = False def set_auto_remove(self, value: bool) -> None: """Set option.""" if value is None: return self.auto_remove = value def set_purge(self, value: bool) -> None: """Set option.""" self.purge = value def set_remove_essential(self, value: bool) -> None: """Set option.""" self.remove_essential = value def set_download_only(self, value: bool) -> None: """Set option.""" self.download_only = value def set_fix_broken(self, value: bool) -> None: """Set option.""" self.fix_broken = value def set_assume_prompt(self, value: Optional[bool]) -> None: """Set option.""" if value is None: self.assume_yes = self.config.get_bool("assume_yes") self.assume_no = False else: self.assume_yes = value self.assume_no = not value # If the configuration is set to true # -y, --assume_yes becomes a toggle to the default behavior if self.config.get_bool("assume_yes"): self.assume_yes = False def set_raw_dpkg(self, value: bool) -> None: """Set option.""" self.raw_dpkg = value def set_all_versions(self, value: bool) -> None: """Set option.""" self.all_versions = value def set_all_arches(self, value: bool) -> None: """Set option.""" self.all_arches = value def set_names(self, value: bool) -> None: """Set option.""" self.names = value def set_installed(self, value: bool) -> None: """Set option.""" self.installed = value def set_upgradable(self, value: bool) -> None: """Set option.""" if not value and hasattr(self, "upgradable"): return self.upgradable = value def set_virtual(self, value: bool) -> None: """Set option.""" self.virtual = value def set_full(self, value: bool) -> None: """Set option.""" self.full = value def set_recommends(self, value: Optional[bool]) -> None: """Set option.""" if value is None: self.install_recommends = self.config.apt.find_b( "APT::Install-Recommends", True ) return self.install_recommends = value if value: self.config.apt.set("APT::Install-Recommends", "1") if not value: self.config.apt.set("APT::Install-Recommends", "0") def set_suggests(self, value: Optional[bool]) -> None: """Set option.""" if value is None: self.install_suggests = self.config.apt.find_b( "APT::Install-Suggests", False ) return self.install_suggests = value if value: self.config.apt.set("APT::Install-Suggests", "1") else: self.config.apt.set("APT::Install-Suggests", "0") def set_update(self, value: Optional[bool]) -> None: """Set option.""" if value is None: return self.update = value def set_debug(self, value: bool) -> None: """Set option.""" self.debug = value def set_nala_option(self, key: str, value: str) -> None: """Set Nala option.""" if value == "false": option: str | bool = False elif value == "true": option = True else: option = value self.config.set(key.split("::", 1)[1], option) def set_dpkg_option(self, value: list[str]) -> list[str]: """Set option.""" if not value: return value try: for opt in value: dpkg, option = opt.split("=", 1) if dpkg.startswith("Nala::"): self.set_nala_option(dpkg, option.strip('"')) continue self.config.apt.set(dpkg, option.strip('"')) # Reinitialize Nala configs in case a Nala option was given self.init_config() except ValueError: sys.exit( _("{error} Option {option}: Configuration item must have a '='").format( error=ERROR_PREFIX, option=opt ) ) return value def init_config(self) -> None: """Initialize Nala Configs.""" self.scroll = self.config.get_bool("scrolling_text", True) self.auto_remove = self.config.get_bool("auto_remove", True) try: self.update = ( self.config.get_bool("auto_update", True) if sys.argv[1] == "upgrade" else False ) except IndexError: self.update = self.config.get_bool("auto_update", True) def state(self) -> str: """Return the state of the object as a string.""" return f"{self}" def is_purge(self) -> bool: """Return if we are to be purging or not.""" return bool(self.purge or "purge" in self.command) arguments = Arguments() nala = typer.Typer(add_completion=True, no_args_is_help=True) history_typer = typer.Typer(name="history") nala.add_typer(history_typer) def print_license(value: bool) -> None: """Print the GPLv3 with `--license`.""" if not value: return if GPL3_LICENSE.exists(): with open(GPL3_LICENSE, encoding="utf-8") as file: pager(file.read()) else: print( _( "It seems the system has no license file\n" "The full GPLv3 can be found at:\n" "https://www.gnu.org/licenses/gpl-3.0.txt" ) ) sys.exit() def version(value: bool) -> None: """Print version.""" if not value: return print(f"nala {__version__}") sys.exit() def help_callback(value: bool) -> None: """Show man page instead of normal help.""" if value: sys.exit( run( # pylint: disable=subprocess-run-check ["man", f"nala-{arguments.command.replace('purge', 'remove')}"] ).returncode ) VERSION = typer.Option( False, "--version", callback=version, is_eager=True, help=_("Show program's version number and exit."), ) LICENSE = typer.Option( False, "--license", callback=print_license, is_eager=True, help=_("Reads the GPLv3 which Nala is licensed under."), ) VERBOSE = typer.Option( False, "-v", "--verbose", callback=arguments.set_verbose, is_eager=True, help=_("Disable scrolling text and print extra information."), ) DEBUG = typer.Option( False, "--debug", callback=arguments.set_debug, is_eager=True, help=_("Logs extra information for debugging."), ) AUTO_REMOVE = typer.Option( None, "--autoremove / --no-autoremove", callback=arguments.set_auto_remove, is_eager=True, help=_("Toggle autoremoving packages."), ) RECOMMENDS = typer.Option( None, callback=arguments.set_recommends, is_eager=True, help=_("Toggle installing recommended packages."), ) SUGGESTS = typer.Option( None, callback=arguments.set_suggests, is_eager=True, help=_("Toggle installing suggested packages."), ) UPDATE = typer.Option( None, callback=arguments.set_update, is_eager=True, help=_("Toggle updating the package list."), ) PURGE = typer.Option( False, "--purge", callback=arguments.set_purge, is_eager=True, help=_("Purge any packages that would be removed."), ) CONFIG = typer.Option( False, "--config", help=_("Purge packages not installed that have config files."), ) REMOVE_ESSENTIAL = typer.Option( False, "--remove-essential", callback=arguments.set_remove_essential, is_eager=True, help=_("Allow the removal of essential packages."), ) DOWNLOAD_ONLY = typer.Option( False, "--download-only", callback=arguments.set_download_only, is_eager=True, help=_("Packages are only retrieved, not unpacked or installed."), ) FIX_BROKEN = typer.Option( True, "-f", "--fix-broken / --no-fix-broken", callback=arguments.set_fix_broken, is_eager=True, help=_("Toggle fix broken packages."), ) ASSUME_YES = typer.Option( None, "-y / -n", "--assume-yes / --assume-no", callback=arguments.set_assume_prompt, is_eager=True, help=_("Assume 'yes' or 'no' to all prompts."), ) OPTION = typer.Option( [], "-o", "--option", callback=arguments.set_dpkg_option, is_eager=True, help=_('Set options like Dpkg::Options::="--force-confnew".'), ) RAW_DPKG = typer.Option( False, "--raw-dpkg", callback=arguments.set_raw_dpkg, is_eager=True, help=_("Skips all formatting and you get raw dpkg output."), ) ALL_VERSIONS = typer.Option( False, "-a", "--all-versions", callback=arguments.set_all_versions, is_eager=True, help=_("Show all versions of a package."), ) ALL_ARCHES = typer.Option( False, "-A", "--all-arches", callback=arguments.set_all_arches, is_eager=True, help=_("Show all architectures of a package."), ) DOWNLOAD_ONLY = typer.Option( False, "--download-only", callback=arguments.set_download_only, is_eager=True, help=_("Packages are only retrieved, not unpacked or installed."), ) NAMES = typer.Option( False, "-n", "--names", callback=arguments.set_names, is_eager=True, help=_("Search only package names."), ) INSTALLED = typer.Option( False, "-i", "--installed", callback=arguments.set_installed, is_eager=True, help=_("Only installed packages."), ) NALA_INSTALLED = typer.Option( False, "-N", "--nala-installed", is_eager=True, help=_("Only packages explicitly installed with Nala."), ) UPGRADABLE = typer.Option( False, "-u", "--upgradable", callback=arguments.set_upgradable, is_eager=True, help=_("Only upgradable packages."), ) UPGRADEABLE = typer.Option( False, "--upgradeable", callback=arguments.set_upgradable, is_eager=True, hidden=True, ) VIRTUAL = typer.Option( False, "-V", "--virtual", callback=arguments.set_virtual, is_eager=True, help=_("Only virtual packages."), ) FULL = typer.Option( False, "--full", callback=arguments.set_full, is_eager=True, help=_("Print the full description of each package."), ) LISTS = typer.Option( False, "--lists", help=_("Remove package lists located in `/var/lib/apt/lists/`."), ) FETCH = typer.Option( False, "--fetch", help=_("Remove `nala-sources.list`."), ) AUTO = typer.Option( False, help=_("Run fetch uninteractively. Will still prompt for overwrite") ) MAN_HELP = typer.Option( False, "-h", "--help", callback=help_callback, is_eager=True, help=_("Show this message and exit."), ) CONTEXT_SETTINGS = { "help_option_names": ["-h", "--help"], } @nala.command("help", hidden=True) @nala.callback( context_settings=CONTEXT_SETTINGS, no_args_is_help=True, invoke_without_command=True ) # pylint: disable=unused-argument def global_options( ctx: typer.Context, _version: bool = VERSION, _license: bool = LICENSE, ) -> None: """Each command has its own help page. For Example: `nala history --help` """ if ctx.invoked_subcommand: arguments.command = ctx.invoked_subcommand if arguments.command == "help": print(ctx.get_help()) sys.exit()