From bd0f9dac89b908f9ef62d460c8d86575313f19fa Mon Sep 17 00:00:00 2001 From: ByteOtter Date: Fri, 2 Jun 2023 09:06:13 +0200 Subject: [PATCH 1/9] CI: Add testing workflow --- .github/workflows/lint.yml | 3 ++- .github/workflows/testing.yml | 38 +++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/testing.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e986c0c..8af65df 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -47,7 +47,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pyright + pip install importlib-resources + pip install .[lint_requires,tests_require] - name: Analysing the code with pyright run: | pyright $(git ls-files '*.py') diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 0000000..88b4804 --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,38 @@ +name: Execute tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.x + + - name: Install dependencies + run: pip install .[tests_require] + + - name: Run tests + run: pytest -v --cov=src/ tests/ + - name: Generate coverage report + run: coverage xml + + # https://github.com/codacy/codacy-coverage-reporter-action + - name: Run codacy-coverage-reporter + uses: codacy/codacy-coverage-reporter-action@v1 + continue-on-error: true + with: + project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} + coverage-reports: coverage.xml From 2c00bbf785e9ca5faccdc1a3dca476f441c5ed0a Mon Sep 17 00:00:00 2001 From: Enno Gotthold Date: Wed, 14 Jun 2023 08:52:32 +0200 Subject: [PATCH 2/9] CI: Make prospector black compatible --- .prospector.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.prospector.yml b/.prospector.yml index 5049fed..7c5246e 100644 --- a/.prospector.yml +++ b/.prospector.yml @@ -11,8 +11,22 @@ dodgy: mccabe: run: true +pycodestyle: + run: true + disable: + # https://github.com/psf/black/issues/354#issuecomment-397685631 + - E203 + - W503 + pydocstyle: run: true + disable: + # https://github.com/PyCQA/pydocstyle/issues/627 + - D202 + # https://github.com/PyCQA/pydocstyle/issues/141 + - D203 + # https://github.com/PyCQA/pydocstyle/issues/475 + - D212 pyflakes: run: true From 8e3adc504313cd59092f88a1bc93cffba6d328b7 Mon Sep 17 00:00:00 2001 From: Enno Gotthold Date: Mon, 12 Jun 2023 16:26:18 +0200 Subject: [PATCH 3/9] ignore: Add IDE directories --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index b6e4761..9c3c4c3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,10 @@ __pycache__/ # C extensions *.so +# IDEs +.idea/ +.vscode/ + # Distribution / packaging .Python build/ From 01ea66a7720a1b3c57a994bffb180d4aff6546b8 Mon Sep 17 00:00:00 2001 From: ByteOtter Date: Tue, 11 Apr 2023 17:09:01 +0200 Subject: [PATCH 4/9] Add changelog --- .gitignore | 4 ++++ changelog.d/7.added | 1 + 2 files changed, 5 insertions(+) create mode 100644 changelog.d/7.added diff --git a/.gitignore b/.gitignore index 9c3c4c3..6f35c34 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ __pycache__/ .idea/ .vscode/ +# Documentation +changelog/* +!changelog/.gitkeep + # Distribution / packaging .Python build/ diff --git a/changelog.d/7.added b/changelog.d/7.added new file mode 100644 index 0000000..78917fb --- /dev/null +++ b/changelog.d/7.added @@ -0,0 +1 @@ +Add parsing of application settings \ No newline at end of file From cd90b5fe43192e597b2cd16a83b0ce7c80663922 Mon Sep 17 00:00:00 2001 From: Enno Gotthold Date: Mon, 12 Jun 2023 16:26:40 +0200 Subject: [PATCH 5/9] setup.cfg: Added new dependencies for Settings feature --- setup.cfg | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index 4ac5ae1..cc24940 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,18 +20,26 @@ package_dir= packages = find: include_package_data = True setup_requires = - setuptools>=44.11.0 - setuptools-scm~=7.1.0 - wheel>=0.37.1 + setuptools + setuptools-scm + wheel install_requires = fbtftp>=0.5 - python-daemon>=3.0.1 + python-daemon>=3.0.1; python_version>"3.6" + python-daemon==2.3.2; python_version=="3.6" pyyaml>=6.0 + click>=8.0.4; python_version>"3.6" + click==8.0.4; python_version=="3.6" + importlib-resources==5.4.0; python_version=="3.6" + schema>=0.6.7; python_version>"3.6" + schema==0.6.7; python_version=="3.6" [options.extras_require] tests_require = pytest>=7.0.1 pytest-mock>=3.6.1 + pytest-cov>=4.1.0; python_version>"3.6" + pytest-cov==3.0.0; python_version=="3.6" lint_requires = pre-commit>=2.0.1 black>=22.1.0 From 73fa5ed8d58d2a41ca399e82e803a06376724eb8 Mon Sep 17 00:00:00 2001 From: Enno Gotthold Date: Mon, 12 Jun 2023 16:29:38 +0200 Subject: [PATCH 6/9] Add custom exceptions for CobblerTftp --- src/cobbler_tftp/exceptions/__init__.py | 9 +++++++ .../exceptions/settings_exceptions.py | 25 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 src/cobbler_tftp/exceptions/__init__.py create mode 100644 src/cobbler_tftp/exceptions/settings_exceptions.py diff --git a/src/cobbler_tftp/exceptions/__init__.py b/src/cobbler_tftp/exceptions/__init__.py new file mode 100644 index 0000000..42e9956 --- /dev/null +++ b/src/cobbler_tftp/exceptions/__init__.py @@ -0,0 +1,9 @@ +"""Custom exceptions for cobbler-tftp.""" + + +class CobblerTftpException(Exception): + """Generic cobbler-tftp exception.""" + + def __init__(self, message: str = "CobblerTFTPException"): + """Create custom generic CobblerTFTPException.""" + super().__init__(message) diff --git a/src/cobbler_tftp/exceptions/settings_exceptions.py b/src/cobbler_tftp/exceptions/settings_exceptions.py new file mode 100644 index 0000000..a8652a6 --- /dev/null +++ b/src/cobbler_tftp/exceptions/settings_exceptions.py @@ -0,0 +1,25 @@ +"""Custom exceptions for cobbler-tftp's settings module.""" + + +class CobblerTftpSettingsException(Exception): + """Generic cobbler-tftp exception.""" + + def __init__(self, message: str = "An Error occured!"): + """Create custom generic settings exception.""" + super().__init__(message) + + +class CobblerTftpMissingConfigParameterException(KeyError): + """Exception to handle a missing but required config parameter.""" + + def __init__( + self, + message="MissingConfigParameterException: Application settings missing required parameter!", + parameter: str = "NONE", + ): + """Create custom exception to raise when a specific config parameter is missing for the application settings.""" + if parameter is None or parameter == "NONE": + raise ValueError("Parameter cannot be 'NONE'") + self.parameter = parameter + self.message = str.join(message, parameter) + super().__init__(message, parameter) From ad65a8b2c4f6e4ad81665d3677758b65e3d1afbf Mon Sep 17 00:00:00 2001 From: ByteOtter Date: Tue, 4 Apr 2023 14:36:21 +0200 Subject: [PATCH 7/9] Add application settings This commit includes logic for settings input validation and migration between future settings versions. The tests for this feature were also added in this commit. --- src/cobbler_tftp/__init__.py | 5 +- src/cobbler_tftp/settings/__init__.py | 259 +++++++++++++ src/cobbler_tftp/settings/data/__init__.py | 1 + src/cobbler_tftp/settings/data/settings.yml | 11 + .../settings/migrations/__init__.py | 351 ++++++++++++++++++ src/cobbler_tftp/settings/migrations/v1_0.py | 61 +++ .../settings/migrations/versioning.cfg | 2 + .../settings/migrations/whitelist__init__.py | 6 + .../settings/migrations/whitelist_v1_0.py | 7 + src/cobbler_tftp/types/__init__.py | 8 + tests/.gitkeep | 0 tests/__init__.py | 3 + tests/conftest.py | 13 + tests/test_data/invalid_config.yml | 2 + tests/test_data/password_file | 1 + tests/test_data/valid_config.yml | 7 + tests/unittests/__init__.py | 3 + .../application_settings/__init__.py | 3 + .../application_settings/conftest.py | 35 ++ .../application_settings/test_migrations.py | 254 +++++++++++++ .../test_schema_version.py | 49 +++ .../application_settings/test_settings.py | 96 +++++ 22 files changed, 1173 insertions(+), 4 deletions(-) create mode 100644 src/cobbler_tftp/settings/__init__.py create mode 100644 src/cobbler_tftp/settings/data/__init__.py create mode 100644 src/cobbler_tftp/settings/data/settings.yml create mode 100644 src/cobbler_tftp/settings/migrations/__init__.py create mode 100644 src/cobbler_tftp/settings/migrations/v1_0.py create mode 100644 src/cobbler_tftp/settings/migrations/versioning.cfg create mode 100644 src/cobbler_tftp/settings/migrations/whitelist__init__.py create mode 100644 src/cobbler_tftp/settings/migrations/whitelist_v1_0.py create mode 100644 src/cobbler_tftp/types/__init__.py delete mode 100644 tests/.gitkeep create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_data/invalid_config.yml create mode 100644 tests/test_data/password_file create mode 100644 tests/test_data/valid_config.yml create mode 100644 tests/unittests/__init__.py create mode 100644 tests/unittests/application_settings/__init__.py create mode 100644 tests/unittests/application_settings/conftest.py create mode 100644 tests/unittests/application_settings/test_migrations.py create mode 100644 tests/unittests/application_settings/test_schema_version.py create mode 100644 tests/unittests/application_settings/test_settings.py diff --git a/src/cobbler_tftp/__init__.py b/src/cobbler_tftp/__init__.py index 3bf0357..536fe00 100644 --- a/src/cobbler_tftp/__init__.py +++ b/src/cobbler_tftp/__init__.py @@ -1,4 +1 @@ -""" -Cobbler-TFTP provides a stateless TFTP-Server to provide an alternative method of operations -for cobbler-sync. -""" +"""Cobbler-TFTP provides a stateless TFTP-Server as an alternative method of operations for ``cobbler sync``.""" diff --git a/src/cobbler_tftp/settings/__init__.py b/src/cobbler_tftp/settings/__init__.py new file mode 100644 index 0000000..4680399 --- /dev/null +++ b/src/cobbler_tftp/settings/__init__.py @@ -0,0 +1,259 @@ +"""Build an object containing all cobbler-tftp configuration parameters.""" + +import os +from pathlib import Path +from typing import Dict, Optional, Union + +import yaml + +from cobbler_tftp.settings import migrations +from cobbler_tftp.types import SettingsDict + +try: + from importlib.resources import files +except ImportError: + from importlib_resources import files # type: ignore + + +class Settings: + """ + Represents the application settings. + + By default, these are read from the default ``settings.yml`` file that is embedded into the application. + """ + + def __init__( + self, + auto_migrate_settings: bool, + is_daemon: bool, + uri: str, + username: str, + password: Union[str, None], + password_file: Union[Path, None], + ) -> None: + """ + Initialize a new instance of the Settings. + + :param auto_migrate_settings: Enable/Disable automatic migration of application settings. + :param is_daemon: Enable/Disable running cobbler-tftp as daemon. + :param uri: URI of the cobbler server. + :param username: Username to authenticate at Cobbler's API. + :param password: Password for authentication with Cobbler. + :param password_file: Path to the file containing the password. + """ + # pylint: disable=R0913 + + self.auto_migrate_settings: bool = auto_migrate_settings + self.is_daemon: bool = is_daemon + self.uri: str = uri + self.user: str = username + self.__password: Union[str, None] = password + self.__password_file: Union[Path, None] = password_file + + def __repr__(self): + """ + Print current cobbler-tftp settings to the terminal. + + :return: A string representation of the Settings object. + :rtype: str + """ + + return f""" + Cobbler-tftp Application Settings: + ----------------------------------\n + Settings auto migration: {self.auto_migrate_settings}\n + Runs as daemon: {self.is_daemon} + + Connection Settings: + --------------------\n + URI: {self.uri}\n + Username: {self.user}\n + """ + + @property + def password(self) -> str: + """ + Get the password from the password file. + + :return: Password string + """ + + if self.__password_file is not None: + return self.__password_file.read_text() + if self.__password is not None: + return self.__password + return "" + + +class SettingsFactory: + """Factory to make it easy building a settings object.""" + + def __init__(self) -> None: + """Initialize a new Settings dicitionary.""" + self._settings_dict: SettingsDict = {} + + def build_settings(self, config_path: Optional[Path], cli_flags) -> Settings: + """ + Build new Settings object using parameters from all sources. + + :return: Settings object + """ + + # Load config file + self.load_config_file(config_path) + + # Load environment variables + self.load_env_variables() + + # Load CLI options + self.load_cli_options(cli_flags) + + if not migrations.validate(self._settings_dict): + raise ValueError( + """Validation Error: Configuration Parameters could not be validated!\n + This may be due to an invalid configuration file or path.""" + ) + + # Extract parameters from _settings_dict and pass them to the Settings object. + # Type ignores are necessary as at this point it is not known what value comes from that key. + auto_migrate_settings: bool = self._settings_dict.get("auto_migrate_settings", False) # type: ignore + is_daemon: bool = self._settings_dict.get("is_daemon", False) # type: ignore + cobbler_settings = self._settings_dict.get("cobbler", None) + uri: str = cobbler_settings.get("uri", "") # type: ignore + username: str = cobbler_settings.get("username", "") # type: ignore + password: str = cobbler_settings.get("password", "") # type: ignore + if cobbler_settings.get("password_file", None) is not None: # type: ignore + password_file: Optional[Path] = Path(cobbler_settings.get("password_file", None)) # type: ignore + else: + password_file = None + + # Create and return a new Settings object + settings = Settings( + auto_migrate_settings, + is_daemon, + uri, + username, + password, + password_file, + ) + + return settings + + def load_config_file(self, config_path: Union[Path, None]) -> SettingsDict: + """ + Get config file at given path. Load contents and put into settings dict. + + :param config_path: Path to configuration file. Can be either customized via CLI or default if none + :return _settings_dict: Dictionary containing all settings from the settings.yml file + """ + + config_file = str(config_path).rsplit("/", maxsplit=1)[-1] + config_pure_path = Path(str(config_path).replace(config_file, "")) + config_import_path = str(config_pure_path).replace("/", ".", -1) + + if not config_path or config_path == "" or not Path.exists(config_path): + if config_path and not Path.exists(config_path): # type: ignore + # Prompt the user that no configuration file could be found and the default will be used + print( + f"Warning: No configuration file found at {config_path}! Using default configuration file..." + ) + try: + config_file_content = ( + files("cobbler_tftp.settings.data") + .joinpath("settings.yml") + .read_text(encoding="UTF-8") + ) + self._settings_dict = yaml.safe_load(config_file_content) + except yaml.YAMLError: + print(f"Error: No valid configuration file found at {config_path}!") + elif config_path and Path.exists(config_path): + try: + config_file_content = ( + files(config_import_path).joinpath(config_file).read_text("utf-8") + ) + self._settings_dict = yaml.safe_load(config_file_content) + except yaml.YAMLError: + print(f"Error: No valid configuration file found at {config_path}!") + return self._settings_dict + + def load_env_variables(self) -> SettingsDict: + """ + Get environment variables containing relevant settings. + + These will override keys taken from the ``settings.yml`` file if applicable. + """ + + cobbler_keys = [x for x in os.environ if x.startswith("COBBLER_TFTP__")] + + # return the settings dictionary if no environment variables exist + if len(cobbler_keys) == 0: + return self._settings_dict + + for variable in cobbler_keys: + key_path = variable.split("__") + key_to_update = key_path[-1] + + if len(key_path) == 2: + try: + self._settings_dict.update( + {key_to_update.lower(): str(os.environ[variable])} + ) + except KeyError as exc: + print(exc) + else: + setting_to_update = {key_to_update.lower(): str(os.environ[variable])} + + for pos in range(len(key_path) - 2, 1, -1): + setting_to_update = {key_path[pos]: setting_to_update} + + self._settings_dict.update(setting_to_update) # type: ignore + + return self._settings_dict + return self._settings_dict + + def load_cli_options( + self, + daemon: Optional[bool] = None, + enable_automigration: Optional[bool] = None, + settings: Optional[Dict[str, Union[str, Path]]] = None, + ) -> SettingsDict: + """ + Get parameters and flags from CLI. + + These will override the ones taken from the settings file or environment variables and are meant for + controlling parameters of the application temporarily. + + :param daemon: If the application should be run in the background as a daemon or not. + :param enable_automigration: Whether to enable the automigration or not. + :param settings: List of custom settings which can be entered manually. + Each entry has the format: ``..<...>.=`` + :return _settings_dict: Settings dictionary. + """ + + if not daemon and not enable_automigration and not settings: + return self._settings_dict + + if daemon: + self._settings_dict["is_daemon"] = daemon + if enable_automigration: + self._settings_dict["auto_migrate_settings"] = enable_automigration + + if settings is None: + raise ValueError + for setting in settings: + option_list = setting.split("=", 1) + + if "." not in option_list[0]: + self._settings_dict.update({option_list[0]: option_list[1]}) + else: + parent = option_list[0].split(".") + + setting_to_update = {parent[-1]: option_list[1]} + + for key in range(len(parent), 0, -1): + setting_to_update = {parent[key]: setting_to_update} + + self._settings_dict.update(setting_to_update) # type: ignore + + return self._settings_dict + return self._settings_dict diff --git a/src/cobbler_tftp/settings/data/__init__.py b/src/cobbler_tftp/settings/data/__init__.py new file mode 100644 index 0000000..1ced827 --- /dev/null +++ b/src/cobbler_tftp/settings/data/__init__.py @@ -0,0 +1 @@ +"""Data package that is used by :mod:`importlib.resources` during runtime.""" diff --git a/src/cobbler_tftp/settings/data/settings.yml b/src/cobbler_tftp/settings/data/settings.yml new file mode 100644 index 0000000..cbc7d7d --- /dev/null +++ b/src/cobbler_tftp/settings/data/settings.yml @@ -0,0 +1,11 @@ +# This file is a reference for cobbler-tftp to validate all given configuration parameters against +schema: 1.0 +auto_migrate_settings: false +# Run cobbler-tftp as a daemon in the background +is_daemon: true +# Specifications of the cobbler-server +cobbler: + uri: "http://localhost/cobbler_api" + username: "cobbler" + password: "cobbler" + # password_file: "/etc/cobbler-tftp/cobbler_password" \ No newline at end of file diff --git a/src/cobbler_tftp/settings/migrations/__init__.py b/src/cobbler_tftp/settings/migrations/__init__.py new file mode 100644 index 0000000..89bb2a5 --- /dev/null +++ b/src/cobbler_tftp/settings/migrations/__init__.py @@ -0,0 +1,351 @@ +""" +Schemas for cobbler-tftp settings for validating the parsed settings. + +The name of the migration contains the target version. +One migration should be able to update from version x to version x+1, where x is any cobbler-tftp version. +The validation logic of the current version is located in the version's migration file. +""" + +import re +from importlib import import_module +from inspect import signature +from pathlib import Path +from types import ModuleType +from typing import Dict, List + +from schema import Schema + +from cobbler_tftp.exceptions.settings_exceptions import ( + CobblerTftpMissingConfigParameterException, +) +from cobbler_tftp.types import SettingsDict + +try: + import importlib.resources as importlib_resources +except ImportError: + import importlib_resources + + +class CobblerTftpSchemaVersion: + """Specifies the version of cobbler-tftp.""" + + def __init__(self, major: int = 0, minor: int = 0) -> None: + """Construct a CobblerTFTPVersion object.""" + self.major = int(major) + self.minor = int(minor) + + def __eq__(self, other: object) -> bool: + """Compare two cobbler-tftp version objects.""" + if not isinstance(other, CobblerTftpSchemaVersion): + return False + return self.major == other.major and self.minor == other.minor + + def __ne__(self, other: object) -> bool: + """Compare if two cobbler-tftp version objects are not equal.""" + return not self.__eq__(other) + + def __lt__(self, other: object) -> bool: + """Compare if the current cobbler-tftp version object is less that another.""" + if not isinstance(other, CobblerTftpSchemaVersion): + raise TypeError + if self.major < other.major: + return True + if self.major.__eq__(other.major): + if self.minor < other.minor: + return True + if self.minor.__eq__(other.minor): + return True + return False + + def __le__(self, other: object) -> bool: + """Compare if current CobblerTFTPVersion object is less or equal another.""" + if self.__lt__(other) or self.__eq__(other): + return True + return False + + def __gt__(self, other: object) -> bool: + """Compare if current CobblerTFTPVersion object is greater than another.""" + if not isinstance(other, CobblerTftpSchemaVersion): + raise TypeError + if self.major > other.major: + return True + if self.major.__eq__(other.major): + if self.minor > other.minor: + return True + if self.minor.__eq__(other.minor): + return False + return False + + def __ge__(self, other: object) -> bool: + """Compare if current CobblerTFTPVersion object is greater or equal to another.""" + if self.__gt__(other) or self.__eq__(other): + return True + return False + + def __hash__(self) -> int: + """ + Create hash from settings version. + + :return: Hash of settings major and minor version. + :rtype: hash + """ + return hash((self.major, self.minor)) + + def __str__(self) -> str: + """ + Return a string representation of the Cobbler-TFTP-Settings object. + + :return: A string containing the version number of the Cobbler-TFTP-Settings object. + :rtype: str + """ + return f"Cobbler-TFTP-Settings version: {self.major}.{self.minor}" + + def __repr__(self) -> str: + """ + Represent the version of the cobbler-tftp settings on the terminal. + + :return: cobbler-tftp settings version as string + :rtype: str + """ + return f"CobblerTFTPVersion(major={self.major}, minor={self.minor})" + + +EMPTY_VERSION: CobblerTftpSchemaVersion = CobblerTftpSchemaVersion() +VERSION_LIST: Dict[CobblerTftpSchemaVersion, ModuleType] = {} +_CONFIG_FILE_PATH: Path = Path() + +with importlib_resources.path(__package__, "versioning.cfg") as config_path: + _CONFIG_FILE_PATH = config_path + + +def __validate_module(name: ModuleType) -> bool: + """ + Validate if the module is valid and return a boolean result. + + Validate a given module according to two criteria: + * module must have certain methods implemented + * methods mut have a certain signature + + :param name: the name of the module to validate + :return: True if every criteria is met otherwise False + """ + # noqa for these lines because we can't use the custom types to check this. + module_methods = { + "validate": "(settings_dict:Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[str,pathlib.Path]]]])->bool", # noqa + "normalize": "(settings_dict:Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[str,pathlib.Path]]]])->Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[str,pathlib.Path]]]]", # noqa + "migrate": "(settings_dict:Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[str,pathlib.Path]]]])->Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[str,pathlib.Path]]]]", # noqa + } + + for key, value in module_methods.items(): + if not hasattr(name, key): + return False + sig = str(signature(getattr(name, key))).replace(" ", "") + if value != sig: + return False + return True + + +def __load_migration_modules(name: str, version: List[str]) -> None: + """ + Load migration specific modules and if valid adds it to ``VERSION_LIST``. + + :param name: The name of the module to load + :param version: The migration version as list + """ + module = import_module(f"cobbler_tftp.settings.migrations.{name}") + if __validate_module(module): + version_list_int = [int(i) for i in version] + VERSION_LIST[CobblerTftpSchemaVersion(*version_list_int)] = module + else: + raise RuntimeError( + f"An Error occured when loading migrations module '{name}' - Module could not be validated." + ) + + +def get_schema_version(settings_dict: SettingsDict) -> CobblerTftpSchemaVersion: + """ + Retrieve current cobbler-tftp settings schema version from the settings dict. + + :param settings_dict: The dictionary of settings parameters + """ + schema_version: list[str] = [] + try: + schema_version = str(settings_dict.get("schema")).split(".") + except KeyError as key_error: + raise CobblerTftpMissingConfigParameterException( + parameter="schema" + ) from key_error + return CobblerTftpSchemaVersion(int(schema_version[0]), int(schema_version[1])) + + +def discover_migrations() -> None: + """ + Discovers all available migrations and loads valid ones. + + Discovers the migration module for each cobbler-tftp version and loads it if it is valid according to certain + conditions: + * the module must contain the following methods: ``validate()``, ``normalize()``, ``migrate()`` + * those version must have a certain signature + """ + # importlib.resources.contents is deprecated with 3.11 but files().iterdir() is not yet available in 3.7 + folder_iterator = importlib_resources.contents("cobbler_tftp.settings.migrations") + filename_regex = r"v[0-9]*_[0-9]*.py" + for files in folder_iterator: + if not re.match(filename_regex, files): + continue + files = Path(files) + if files.is_symlink(): + continue + migration_name = "" + if files.suffix == ".py": + migration_name = files.name[:-3] + # migration_name should now be something like v3_0 + # Remove leading V. Necessary to save values into CobblerVersion object + version = migration_name[1:].split("_") + __load_migration_modules(migration_name, version) + + +def get_schema(version: CobblerTftpSchemaVersion) -> Schema: + """ + Return a schema for a given cobbler-tftp version. + + :param version: The cobbler-tftp version object + :return: The schema of the cobbler-tftp version + """ + # Unable to use custom protocol from 3.8+ instead of ModuleType + return VERSION_LIST[version].settings_schema # type: ignore + + +def get_current_schema_version() -> CobblerTftpSchemaVersion: + """ + Get the highest available schema version. + + :return: The highest :class:`CobblerTftpSchemaVersion`. + """ + highest_version = EMPTY_VERSION + for version in VERSION_LIST: + if version > highest_version: + highest_version = version + return highest_version + + +def migrate( + settings_dict: SettingsDict, + settings_path: Path, + old: CobblerTftpSchemaVersion = EMPTY_VERSION, + new: CobblerTftpSchemaVersion = EMPTY_VERSION, +) -> SettingsDict: + """ + Migrate to a specific version. If no old and new version is supplied it will call :func:`.auto_migrate`. + + :param settings_dict: The dict of settings parameters to migrate + :param settings_path: The path of the settings dict + :param old: The version to migrate from, defaults to :const:`EMPTY_VERSION` + :param new: The version to migrate to, defaults to :const:`EMPTY_VERSION` + :raises ValueError: Raised if attempting to downgrade + :return: The migrated dict + """ + data: SettingsDict = settings_dict + + if old == EMPTY_VERSION and new == EMPTY_VERSION: + return auto_migrate(settings_dict, settings_path) + + if EMPTY_VERSION in (old, new): + raise ValueError("Either both or no versions must be specified to migrate!") + + if old > new: + raise ValueError("Downgrades are not supported!") + + if old == new: + return settings_dict + + sorted_version_list = sorted(list(VERSION_LIST.keys())) + migration_list = sorted_version_list[ + sorted_version_list.index(old) + 1 : sorted_version_list.index(new) + 1 + ] + + for key in migration_list: + data = VERSION_LIST[key].migrate(settings_dict) + + return data + + +def auto_migrate( + settings_dict: SettingsDict, + settings_path: Path, +) -> SettingsDict: + """ + Auto migration to the most recent version. + + :param settings_dict: The dictionary of configuration parameters + :param settings_path: the path of the migration + :return: The migrated settings as dict + """ + + if not settings_dict.get("auto_migrate_settings", True): + raise RuntimeError( + "Automatic migration of settings disabled, but required to run daemon!" + ) + settings_schema = settings_dict.get("schema", None) + if settings_schema is None: + settings_version = get_current_schema_version() + else: + if isinstance(settings_schema, float): + settings_schema_tuple = ( + int(str(settings_schema).split(".", 1)[0]), + int(str(settings_schema).split(".", 1)[1]), + ) + settings_version = CobblerTftpSchemaVersion( + settings_schema_tuple[0], settings_schema_tuple[1] + ) + else: + raise ValueError("Invalid Schema version number!") + if settings_version == EMPTY_VERSION: + raise RuntimeError( + "Automigration of settings failed! Settings schema undiscoverable!" + ) + + sorted_version_list = sorted(list(VERSION_LIST.keys())) # type: ignore + migrations = sorted_version_list[sorted_version_list.index(settings_version) :] + + for index in range(0, len(migrations) - 1): + if index == len(migrations) - 1: + break + settings_dict = migrate(settings_dict, settings_path) + + return settings_dict + + +def validate( + settings_dict: SettingsDict, +) -> bool: + """ + Tail-call for the methods of the individual migration modules. + + :param settings_dict: The settings dict to validate. + :return: True if settings are valid, otherwise False. + """ + version = get_current_schema_version() + + # Extra keys are excluded from validation + result: bool = VERSION_LIST[version].validate(settings_dict) + return result + + +def normalize( + settings_dict: SettingsDict, +) -> SettingsDict: + """ + If data in ``settings_dict`` is valid the normalized data is returned. + + :param settings_dict: The settings dict to validate + :return: The validated dict + """ + version = get_schema_version(settings_dict) + + result: SettingsDict = VERSION_LIST[version].normalize(settings_dict) + + return result + + +discover_migrations() diff --git a/src/cobbler_tftp/settings/migrations/v1_0.py b/src/cobbler_tftp/settings/migrations/v1_0.py new file mode 100644 index 0000000..9961948 --- /dev/null +++ b/src/cobbler_tftp/settings/migrations/v1_0.py @@ -0,0 +1,61 @@ +"""Module for the validation, normalization and migration of the schema version "1.0".""" + +from pathlib import Path + +from schema import Optional, Or, Schema, SchemaError, SchemaWrongKeyError + +from cobbler_tftp.types import SettingsDict + +settings_schema: Schema = Schema( + { + Optional("schema"): float, + Optional("auto_migrate_settings"): bool, + Optional("is_daemon"): bool, + Optional("cobbler"): { + Optional("uri"): str, + Optional("username"): str, + Optional(Or("password", "password_file", only_one=True)): Or(str, Path), + }, + } +) + + +def validate(settings_dict: SettingsDict) -> bool: + """ + Validate the given dictionary of configuration parameters to the reference ``schema``. + + :param settings_dict: The dictionary of configuration parameters to validate + :return bool: True/False depending on whether the dicts match or not + """ + if settings_dict == {} or settings_dict is None: + return False + + try: + settings_schema.validate(settings_dict) + except (SchemaError, SchemaWrongKeyError) as exc: + print(exc) + return False + return True + + +def normalize(settings_dict: SettingsDict) -> SettingsDict: + """ + If data in ``settings_dict`` is valid, the validated data is returned. + + :param settings_dict: The dictionary of configuration parameters to validate + :return: the validated dict + :rtype dict: + """ + return settings_schema.validate(settings_dict) + + +def migrate(settings_dict: SettingsDict) -> SettingsDict: + """ + Migrate settings dict from previous to current version. + + :param settings_dict: The dictionary to migrate + :return: The settings dict + """ + if not validate(settings_dict): + raise SchemaError("v1.0.0: Schema error while validating") + return normalize(settings_dict) diff --git a/src/cobbler_tftp/settings/migrations/versioning.cfg b/src/cobbler_tftp/settings/migrations/versioning.cfg new file mode 100644 index 0000000..70685d5 --- /dev/null +++ b/src/cobbler_tftp/settings/migrations/versioning.cfg @@ -0,0 +1,2 @@ +[cobbler-tftp] +version = 0.1.0 diff --git a/src/cobbler_tftp/settings/migrations/whitelist__init__.py b/src/cobbler_tftp/settings/migrations/whitelist__init__.py new file mode 100644 index 0000000..fd11b6d --- /dev/null +++ b/src/cobbler_tftp/settings/migrations/whitelist__init__.py @@ -0,0 +1,6 @@ +"""Whitelist for vulture to ignore certain methods as unused.""" + +from cobbler_tftp.settings.migrations import EMPTY_VERSION, get_schema + +# Ignore because we potentially require this later when implementing other features. +get_schema(EMPTY_VERSION) diff --git a/src/cobbler_tftp/settings/migrations/whitelist_v1_0.py b/src/cobbler_tftp/settings/migrations/whitelist_v1_0.py new file mode 100644 index 0000000..7a4ab2f --- /dev/null +++ b/src/cobbler_tftp/settings/migrations/whitelist_v1_0.py @@ -0,0 +1,7 @@ +"""Whitelist for vulture to ignore certain methods as unused.""" + +import cobbler_tftp.settings.migrations.v1_0 as migration + +migration.migrate({}) +migration.normalize({}) +migration.validate({}) diff --git a/src/cobbler_tftp/types/__init__.py b/src/cobbler_tftp/types/__init__.py new file mode 100644 index 0000000..fe7f153 --- /dev/null +++ b/src/cobbler_tftp/types/__init__.py @@ -0,0 +1,8 @@ +"""Custom types and aliases required to make typing in cobbler-tftp easier.""" + +from pathlib import Path +from typing import Dict, Union + +# Dictionary type for configuration parameters +# if this type changes: changes __valdiate_module function in migrations/__init__.py +SettingsDict = Dict[str, Union[float, bool, str, Path, Dict[str, Union[str, Path]]]] diff --git a/tests/.gitkeep b/tests/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..02d2f90 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +""" +Cobbler-tftp test module +""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..05e4857 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,13 @@ +""" +Fixtures for all tests +""" + +from contextlib import contextmanager + + +@contextmanager +def does_not_raise(): + """ + Fixture that represents a context manager that will expect that no raise occurs. + """ + yield diff --git a/tests/test_data/invalid_config.yml b/tests/test_data/invalid_config.yml new file mode 100644 index 0000000..f1ca4ad --- /dev/null +++ b/tests/test_data/invalid_config.yml @@ -0,0 +1,2 @@ +This is an invalid yaml file. : dsafkölgdsf +bye \ No newline at end of file diff --git a/tests/test_data/password_file b/tests/test_data/password_file new file mode 100644 index 0000000..7aa311a --- /dev/null +++ b/tests/test_data/password_file @@ -0,0 +1 @@ +password \ No newline at end of file diff --git a/tests/test_data/valid_config.yml b/tests/test_data/valid_config.yml new file mode 100644 index 0000000..75afccf --- /dev/null +++ b/tests/test_data/valid_config.yml @@ -0,0 +1,7 @@ +schema: 1.0 +auto_migrate_settings : true +is_daemon: false +cobbler: + uri: 'http://testmachine.testnetwork.com/api' + username: 'cobbler' + password_file: 'tests/test_data/password_file' \ No newline at end of file diff --git a/tests/unittests/__init__.py b/tests/unittests/__init__.py new file mode 100644 index 0000000..6f5a985 --- /dev/null +++ b/tests/unittests/__init__.py @@ -0,0 +1,3 @@ +""" +Cobblert-tftp unittests module. +""" diff --git a/tests/unittests/application_settings/__init__.py b/tests/unittests/application_settings/__init__.py new file mode 100644 index 0000000..48d27ef --- /dev/null +++ b/tests/unittests/application_settings/__init__.py @@ -0,0 +1,3 @@ +""" +Cobbler-tftp unittest module for application settings component. +""" diff --git a/tests/unittests/application_settings/conftest.py b/tests/unittests/application_settings/conftest.py new file mode 100644 index 0000000..e7617d1 --- /dev/null +++ b/tests/unittests/application_settings/conftest.py @@ -0,0 +1,35 @@ +""" +This module implements all necessary fixtures for running the unittests using pytests. They are automaticall discovered. +""" +import pytest + +from cobbler_tftp.types import SettingsDict + +try: + import importlib.resources as importlib_resources +except ImportError: + import importlib_resources as importlib_resources # type: ignore + + +@pytest.fixture +def fake_settings_dict() -> SettingsDict: + # Test data + fake_settings_dict: SettingsDict = { + "schema": 1.0, + "auto_migrate_settings": True, + "is_daemon": True, + "cobbler": { + "uri": "http://localhost/cobbler_api", + "username": "cobbler", + "password": "cobbler", + }, + } + return fake_settings_dict + + +@pytest.fixture +def settings_path(): + with importlib_resources.path( + "src.cobbler_tftp.settings.data", "settings.yml" + ) as settings_path: + return settings_path diff --git a/tests/unittests/application_settings/test_migrations.py b/tests/unittests/application_settings/test_migrations.py new file mode 100644 index 0000000..15a1f6f --- /dev/null +++ b/tests/unittests/application_settings/test_migrations.py @@ -0,0 +1,254 @@ +""" +Tests for the settings schemas in the migrations module. +""" + +from pathlib import Path +from typing import List + +import pytest +import pytest_mock +from schema import Schema + +import cobbler_tftp.settings.migrations as migrations +from cobbler_tftp.types import SettingsDict +from tests.conftest import does_not_raise +from tests.unittests.application_settings.conftest import fake_settings_dict + +fake_new_settings_dict: SettingsDict = { + "schema": 2.0, + "auto_migrate_settings": True, + "is_daemon": True, + "cobbler": { + "uri": "http://localhost/cobbler_api", + "username": "cobbler", + "password": "cobbler", + }, +} + + +@pytest.fixture(autouse=True) +def reset_version_list(): + # Arrange + migrations.VERSION_LIST = {} + # Act + yield + # Cleanup + migrations.discover_migrations() + + +def test_get_schema_version(fake_settings_dict): + # Arrange & Act + schema_version = migrations.get_schema_version(fake_settings_dict) + + # Assert + assert schema_version.major == 1 + assert schema_version.minor == 0 + + +def test_discover_migrations(mocker: "pytest_mock.MockerFixture"): + # Arrange + # Define a list of mock migration module names + mock_migrations: List[str] = [ + "v1_0.py", + "v2_0.py", + "v3_0.py", + "not_a_migration.py", + "v4_0.py", + ] + # Replace importlib_resources.contents with a mock object that returns the mock contents + mock_importlib = mocker.patch( + "cobbler_tftp.settings.migrations.importlib_resources.contents", + return_value=mock_migrations, + ) + # Replace __load_migration_modules with a mock object that does nothing + mock_load_migration = mocker.patch( + "cobbler_tftp.settings.migrations.__load_migration_modules", + return_value=None, + ) + + # Act + migrations.discover_migrations() + + # Assert + # types will be ignored as assert_has_calls expects unittest.mock instead of mocker calls + mock_load_migration.assert_has_calls( + [ + mocker.call("v1_0", ["1", "0"]), # type: ignore + mocker.call("v2_0", ["2", "0"]), # type: ignore + mocker.call("v3_0", ["3", "0"]), # type: ignore + mocker.call("v4_0", ["4", "0"]), # type: ignore + ] + ) + + mock_importlib.assert_called_once_with("cobbler_tftp.settings.migrations") + + +def test_get_schema(mocker: "pytest_mock.MockerFixture"): + # Arrange + # Fake schema and version + version = migrations.CobblerTftpSchemaVersion(1, 0) + schema = Schema({}) + module_mock = mocker.MagicMock() + module_mock.settings_schema = schema + migrations.VERSION_LIST[version] = module_mock + + # Act + result = migrations.get_schema(version) + + # Assert + assert result == schema + + +def test_get_current_schema_version(mocker: "pytest_mock.MockerFixture"): + # Arrange + version = migrations.CobblerTftpSchemaVersion(99, 0) + schema = Schema({}) + module_mock = mocker.MagicMock() + module_mock.settings_schema = schema + migrations.VERSION_LIST[version] = module_mock + migrations.discover_migrations() + + # Act + result = migrations.get_current_schema_version() + + # Assert + assert result == version + + +def test_migrate_without_parameters(mocker): + # Arrange + mock_auto_migrate = mocker.patch( + "cobbler_tftp.settings.migrations.migrate", return_value=SettingsDict + ) + + # Act + migrations.migrate({}, Path()) + + # Assert + mock_auto_migrate.assert_called_once() + + +@pytest.mark.parametrize( + "settings_dict, settings_path, old, new, expected, expected_exception", + [ + ( + fake_settings_dict, + Path(), + migrations.CobblerTftpSchemaVersion(1, 0), + None, + None, + pytest.raises(TypeError), + ), + ( + fake_settings_dict, + Path(), + migrations.CobblerTftpSchemaVersion(2, 0), + migrations.CobblerTftpSchemaVersion(1, 0), + None, + pytest.raises(ValueError), + ), + ( + fake_settings_dict, + Path(), + migrations.CobblerTftpSchemaVersion(1, 0), + migrations.CobblerTftpSchemaVersion(1, 0), + fake_settings_dict, + does_not_raise(), + ), + ], +) +def test_migrate(settings_dict, settings_path, old, new, expected, expected_exception): + # Arrange + if expected is not None and old and new is None: + expected = expected() + # Act + with expected_exception: + result = migrations.migrate(settings_dict, settings_path, old, new) + # Assert + assert result == expected + + +def test_auto_migrate_calls_migrate( + mocker: "pytest_mock.MockerFixture", fake_settings_dict, settings_path +): + # Arrange + migrations.VERSION_LIST[ + migrations.CobblerTftpSchemaVersion(1, 0) + ] = mocker.MagicMock() + migrations.VERSION_LIST[ + migrations.CobblerTftpSchemaVersion(2, 0) + ] = mocker.MagicMock() + mock_migrate = mocker.patch("cobbler_tftp.settings.migrations.migrate") + + # Act + migrations.auto_migrate(fake_settings_dict, settings_path) + + # Assert + mock_migrate.assert_called_once_with(fake_settings_dict, settings_path) + + +def test_auto_migrate_raises_runtime_error(fake_settings_dict, settings_path): + # Arrange + fake_settings_dict["auto_migrate_settings"] = False + + # Act and Assert + with pytest.raises(RuntimeError): + migrations.auto_migrate(fake_settings_dict, settings_path) + + +def test_auto_migrate_raises_value_error(fake_settings_dict, settings_path): + # Arrange + fake_settings_dict["schema"] = "not float" + + # Act and Assert + with pytest.raises(ValueError): + migrations.auto_migrate(fake_settings_dict, settings_path) + + +def test_auto_migrate_raises_value_error_if_verions_empty( + fake_settings_dict, settings_path +): + # Arrange + fake_settings_dict["schema"] = migrations.EMPTY_VERSION + + # Act and Assert + with pytest.raises(ValueError): + migrations.auto_migrate(fake_settings_dict, settings_path) + + +def test_validate(mocker, fake_settings_dict): + # Arrange + mock_validation_module = mocker.MagicMock() + mock_validation_module.validate = mocker.MagicMock(return_value=True) + version = migrations.CobblerTftpSchemaVersion(1, 0) + mocker.patch( + "cobbler_tftp.settings.migrations.get_current_schema_version", + return_value=version, + ) + migrations.VERSION_LIST[version] = mock_validation_module + + # Act + result = migrations.validate(fake_settings_dict) + + # Assert + mock_validation_module.validate.assert_called_once_with(fake_settings_dict) + assert result is True + + +def test_normalize(mocker, fake_settings_dict): + # Arrange + mock_validation_module = mocker.MagicMock() + mock_validation_module.normalize = mocker.MagicMock(return_value=True) + version = migrations.CobblerTftpSchemaVersion(1, 0) + mocker.patch( + "cobbler_tftp.settings.migrations.get_schema_version", + return_value=version, + ) + migrations.VERSION_LIST[version] = mock_validation_module + + # Act + result = migrations.normalize(fake_settings_dict) + + # Assert + mock_validation_module.normalize.assert_called_once_with(fake_settings_dict) + assert result is True diff --git a/tests/unittests/application_settings/test_schema_version.py b/tests/unittests/application_settings/test_schema_version.py new file mode 100644 index 0000000..ace2607 --- /dev/null +++ b/tests/unittests/application_settings/test_schema_version.py @@ -0,0 +1,49 @@ +""" +Tests for the CobblerTftpSchemaVersion class +""" + +from cobbler_tftp.settings.migrations import CobblerTftpSchemaVersion + + +class TestCobblertTftpSchemaVersion: + def setup_method(self): + self.v1_0 = CobblerTftpSchemaVersion(1, 0) + self.v2_0 = CobblerTftpSchemaVersion(2, 0) + self.v2_1 = CobblerTftpSchemaVersion(2, 1) + + def test_init(self): + assert isinstance(self.v1_0, CobblerTftpSchemaVersion) + assert isinstance(self.v2_0, CobblerTftpSchemaVersion) + assert isinstance(self.v2_1, CobblerTftpSchemaVersion) + assert self.v1_0.major == 1 + assert self.v1_0.minor == 0 + + def test_eq(self): + assert self.v1_0 == self.v1_0 + assert not self.v1_0 == self.v2_0 + + def test_ne(self): + assert self.v1_0 != self.v2_0 + assert not self.v1_0 != self.v1_0 + + def test_lt(self): + assert self.v1_0 < self.v2_0 + assert self.v2_0 < self.v2_1 + assert not self.v2_1 < self.v1_0 + + def test_le(self): + assert self.v1_0 <= self.v1_0 + assert self.v1_0 <= self.v2_0 + assert self.v2_0 <= self.v2_1 + assert not self.v2_0 <= self.v1_0 + + def test_gt(self): + assert self.v2_0 > self.v1_0 + assert self.v2_1 > self.v2_0 + assert not self.v1_0 > self.v1_0 + assert not self.v1_0 > self.v2_1 + + def test_ge(self): + assert self.v2_0 >= self.v1_0 + assert self.v2_1 >= self.v2_0 + assert self.v2_1 >= self.v2_1 diff --git a/tests/unittests/application_settings/test_settings.py b/tests/unittests/application_settings/test_settings.py new file mode 100644 index 0000000..e6d4ecd --- /dev/null +++ b/tests/unittests/application_settings/test_settings.py @@ -0,0 +1,96 @@ +""" +Tests for the application settings module. +""" + +from pathlib import Path + +import pytest + +from cobbler_tftp.settings import Settings, SettingsFactory + + +@pytest.fixture +def settings_factory(): + """ + Fixture that represends the SettingsFactory class + """ + return SettingsFactory() + + +def test_build_settings_with_default_config_file( + settings_factory: SettingsFactory, mocker +): + """ + Test the ``build_settings`` function without passing a config file path or additional arguments. + + :assert: True if build successfull and the values of the Settings object correspond to the values inside the default + config file + """ + # Call the build_settings method with None as the config_path argument + settings = settings_factory.build_settings(None, None) + + # Assert that the expected values are set in the Settings object + assert isinstance(settings, Settings) + assert settings.auto_migrate_settings is False + assert settings.is_daemon is True + assert settings.uri == "http://localhost/cobbler_api" + assert settings.user == "cobbler" + assert settings.password == "cobbler" + + +def test_build_settings_with_valid_config_file( + settings_factory: SettingsFactory, mocker +): + valid_file_path = Path("tests/test_data/valid_config.yml") + settings = settings_factory.build_settings(valid_file_path, None) + + assert isinstance(settings, Settings) + assert settings.auto_migrate_settings is True + assert settings.is_daemon is False + assert settings.uri == "http://testmachine.testnetwork.com/api" + assert settings.user == "cobbler" + assert settings.password == "password" + + +def test_build_settings_with_invalid_config_file( + settings_factory: SettingsFactory, mocker +): + # Pass path to invalid yaml file in /test_data/invalid_config.yml + # Assert that YAMLError gets raised + path = Path("tests/test_data/invalid_config.yml") + + with pytest.raises(ValueError) as exc: + settings_factory.build_settings(path, None) + + assert """Validation Error: Configuration Parameters could not be validated!\n + This may be due to an invalid configuration file or path.""" in str( + exc.value + ) + + +def test_build_settings_with_missing_config_file( + settings_factory: SettingsFactory, capsys +): + """ + Test the ``build_settings`` function while passing it a path without a config file present. + Should return a ``Settings`` object with default parameters. + + :assert: Whether a Settings object is built, it contains all default parameters and a stdout with a warning + is printed + """ + # Arrange + path = Path("tests/test_data/missing_file.yml") + expected_message = f"Warning: No configuration file found at {path}! Using default configuration file...\n" + + # Act + settings = settings_factory.build_settings(path, None) + + # Assert + captured_message = capsys.readouterr() + assert captured_message.out == expected_message + assert isinstance(settings, Settings) + assert settings.auto_migrate_settings is False + assert settings.is_daemon is True + assert settings.uri == "http://localhost/cobbler_api" + assert settings.user == "cobbler" + assert settings.password == "cobbler" From cbcd51ec62e3b9b43f18112e91081fc157e254e3 Mon Sep 17 00:00:00 2001 From: Enno Gotthold Date: Wed, 14 Jun 2023 09:22:50 +0200 Subject: [PATCH 8/9] Docs: Regenerated docs according to new feature --- docs/_static/extend_width.css | 3 +++ docs/code-autodoc/cobbler_tftp.exceptions.rst | 21 +++++++++++++++++++ docs/code-autodoc/cobbler_tftp.rst | 15 +++++++------ .../cobbler_tftp.settings.data.rst | 10 +++++++++ .../cobbler_tftp.settings.migrations.rst | 21 +++++++++++++++++++ docs/code-autodoc/cobbler_tftp.settings.rst | 19 +++++++++++++++++ docs/code-autodoc/cobbler_tftp.types.rst | 10 +++++++++ docs/code-autodoc/main.rst | 7 ------- docs/code-autodoc/modules.rst | 6 ++---- docs/code-autodoc/service.rst | 7 ------- docs/conf.py | 1 + docs/index.rst | 2 ++ 12 files changed, 96 insertions(+), 26 deletions(-) create mode 100644 docs/_static/extend_width.css create mode 100644 docs/code-autodoc/cobbler_tftp.exceptions.rst create mode 100644 docs/code-autodoc/cobbler_tftp.settings.data.rst create mode 100644 docs/code-autodoc/cobbler_tftp.settings.migrations.rst create mode 100644 docs/code-autodoc/cobbler_tftp.settings.rst create mode 100644 docs/code-autodoc/cobbler_tftp.types.rst delete mode 100644 docs/code-autodoc/main.rst delete mode 100644 docs/code-autodoc/service.rst diff --git a/docs/_static/extend_width.css b/docs/_static/extend_width.css new file mode 100644 index 0000000..51c5a49 --- /dev/null +++ b/docs/_static/extend_width.css @@ -0,0 +1,3 @@ +.wy-nav-content { + max-width: 90% !important; +} \ No newline at end of file diff --git a/docs/code-autodoc/cobbler_tftp.exceptions.rst b/docs/code-autodoc/cobbler_tftp.exceptions.rst new file mode 100644 index 0000000..f69deee --- /dev/null +++ b/docs/code-autodoc/cobbler_tftp.exceptions.rst @@ -0,0 +1,21 @@ +cobbler\_tftp.exceptions package +================================ + +Submodules +---------- + +cobbler\_tftp.exceptions.settings\_exceptions module +---------------------------------------------------- + +.. automodule:: cobbler_tftp.exceptions.settings_exceptions + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: cobbler_tftp.exceptions + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/code-autodoc/cobbler_tftp.rst b/docs/code-autodoc/cobbler_tftp.rst index bea404d..ccee212 100644 --- a/docs/code-autodoc/cobbler_tftp.rst +++ b/docs/code-autodoc/cobbler_tftp.rst @@ -1,16 +1,15 @@ cobbler\_tftp package ===================== -Submodules ----------- +Subpackages +----------- -cobbler\_tftp.authentication module ------------------------------------ +.. toctree:: + :maxdepth: 4 -.. automodule:: cobbler_tftp.authentication - :members: - :undoc-members: - :show-inheritance: + cobbler_tftp.exceptions + cobbler_tftp.settings + cobbler_tftp.types Module contents --------------- diff --git a/docs/code-autodoc/cobbler_tftp.settings.data.rst b/docs/code-autodoc/cobbler_tftp.settings.data.rst new file mode 100644 index 0000000..6569389 --- /dev/null +++ b/docs/code-autodoc/cobbler_tftp.settings.data.rst @@ -0,0 +1,10 @@ +cobbler\_tftp.settings.data package +=================================== + +Module contents +--------------- + +.. automodule:: cobbler_tftp.settings.data + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/code-autodoc/cobbler_tftp.settings.migrations.rst b/docs/code-autodoc/cobbler_tftp.settings.migrations.rst new file mode 100644 index 0000000..6a97620 --- /dev/null +++ b/docs/code-autodoc/cobbler_tftp.settings.migrations.rst @@ -0,0 +1,21 @@ +cobbler\_tftp.settings.migrations package +========================================= + +Submodules +---------- + +cobbler\_tftp.settings.migrations.v1\_0 module +---------------------------------------------- + +.. automodule:: cobbler_tftp.settings.migrations.v1_0 + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: cobbler_tftp.settings.migrations + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/code-autodoc/cobbler_tftp.settings.rst b/docs/code-autodoc/cobbler_tftp.settings.rst new file mode 100644 index 0000000..91fef1c --- /dev/null +++ b/docs/code-autodoc/cobbler_tftp.settings.rst @@ -0,0 +1,19 @@ +cobbler\_tftp.settings package +============================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + cobbler_tftp.settings.data + cobbler_tftp.settings.migrations + +Module contents +--------------- + +.. automodule:: cobbler_tftp.settings + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/code-autodoc/cobbler_tftp.types.rst b/docs/code-autodoc/cobbler_tftp.types.rst new file mode 100644 index 0000000..a77eb41 --- /dev/null +++ b/docs/code-autodoc/cobbler_tftp.types.rst @@ -0,0 +1,10 @@ +cobbler\_tftp.types package +=========================== + +Module contents +--------------- + +.. automodule:: cobbler_tftp.types + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/code-autodoc/main.rst b/docs/code-autodoc/main.rst deleted file mode 100644 index eace87b..0000000 --- a/docs/code-autodoc/main.rst +++ /dev/null @@ -1,7 +0,0 @@ -main module -=========== - -.. automodule:: main - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/code-autodoc/modules.rst b/docs/code-autodoc/modules.rst index 903a4ce..3b6bb47 100644 --- a/docs/code-autodoc/modules.rst +++ b/docs/code-autodoc/modules.rst @@ -1,9 +1,7 @@ -src -=== +cobbler_tftp +============ .. toctree:: :maxdepth: 4 cobbler_tftp - main - service diff --git a/docs/code-autodoc/service.rst b/docs/code-autodoc/service.rst deleted file mode 100644 index 5a0b941..0000000 --- a/docs/code-autodoc/service.rst +++ /dev/null @@ -1,7 +0,0 @@ -service module -============== - -.. automodule:: service - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/conf.py b/docs/conf.py index b3d2218..53d6f7b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,3 +24,4 @@ html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] +html_css_files = ["extend_width.css"] diff --git a/docs/index.rst b/docs/index.rst index 987b8fc..188ca1d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,6 +10,8 @@ Welcome to Cobbler-TFTP's documentation! :maxdepth: 2 :caption: Contents: + Code Documentation + Indices and tables From b7c20eb29eb62c546c6003e4fcc88adcdc3bcf26 Mon Sep 17 00:00:00 2001 From: Enno Gotthold Date: Wed, 14 Jun 2023 10:29:24 +0200 Subject: [PATCH 9/9] setup.py: Make pydoc happy with docstring --- setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 17659aa..b4bc6b4 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,4 @@ -""" -This file only serves to allow compatibility with legacy build tools -""" +"""File only serves to allow compatibility with legacy build tools.""" from setuptools import setup