diff --git a/TODO.todo b/TODO.todo index e6f1e79..2ad76f5 100644 --- a/TODO.todo +++ b/TODO.todo @@ -42,10 +42,23 @@ Documentation: Overwrite `__init__`, access them in pytest with `.value.$args` Access them in a try,except with `raise $excpetion as $name; $name.$arg` + ☐ Document capturing stdout + Use `capsys` + `assert capsys.readouterr().out` + A new line may be inserted if using `click.echo()` + + ☐ Document using datadir with a module rather than a shared one. Link to tembo as an example. ☐ Can prospector ignore tests dir? document this in the gist if so ☐ Redo the documentation on a CLI, reorganise and inocropoate all the new tembo layouts + Testing: + ☐ Document importing in inidivudal tests using `importlib.reload` + Globally import the module + Use `importlib.reload(module)` in each test instead of explicitly importing the module. + This is because the import is cached. + + Functionality: ☐ Replace loggers with `click.echo` for command outputs. Keep logging messages for actual logging messages? Define a format: [TEMBO:$datetime] $message 🐘 - document this in general python for CLI @@ -61,6 +74,8 @@ Functionality: ☐ Update poetry ☐ Build docs ☐ Document using Duty + ☐ Duty for auto insert version from `poetry version`. + Need to decide what file to place `__version__` in. Logging: ☐ Make all internal tembo logs be debug diff --git a/pyproject.toml b/pyproject.toml index c3e2c86..c2626b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,4 +24,4 @@ requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] -"tembo" = "tembo.cli:run" +"tembo" = "tembo.cli.cli:run" diff --git a/tembo/__init__.py b/tembo/__init__.py index c688a8d..d15ffc9 100644 --- a/tembo/__init__.py +++ b/tembo/__init__.py @@ -1,30 +1,4 @@ -import os - -import panaetius -from panaetius.exceptions import LoggingDirectoryDoesNotExistException +from .journal.pages import ScopedPageCreator, PageCreatorOptions +from . import exceptions __version__ = "0.1.0" - -if (config_path := os.environ.get("TEMBO_CONFIG")) is not None: - CONFIG = panaetius.Config("tembo", config_path) -else: - CONFIG = panaetius.Config("tembo", "~/tembo/.config", skip_header_init=True) - - -panaetius.set_config(CONFIG, "base_path", "~/tembo") -panaetius.set_config(CONFIG, "template_path", "~/tembo/.templates") -panaetius.set_config(CONFIG, "scopes", {}) -panaetius.set_config(CONFIG, "logging.level", "DEBUG") -panaetius.set_config(CONFIG, "logging.path") - -try: - logger = panaetius.set_logger( - CONFIG, panaetius.SimpleLogger(logging_level=CONFIG.logging_level) - ) -except LoggingDirectoryDoesNotExistException: - _LOGGING_PATH = CONFIG.logging_path - CONFIG.logging_path = "" - logger = panaetius.set_logger( - CONFIG, panaetius.SimpleLogger(logging_level=CONFIG.logging_level) - ) - logger.warning("Logging directory %s does not exist", _LOGGING_PATH) diff --git a/tembo/cli.py b/tembo/cli.py deleted file mode 100644 index ac42cb7..0000000 --- a/tembo/cli.py +++ /dev/null @@ -1,192 +0,0 @@ -import pathlib - -import click - -import tembo -from tembo.journal import pages -from tembo import exceptions - - -CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) - - -@click.group(context_settings=CONTEXT_SETTINGS, options_metavar="") -@click.version_option( - tembo.__version__, - "-v", - "--version", - prog_name="Tembo", - message=f"Tembo v{tembo.__version__} 🐘", -) -def run(): - """ - Tembo - an organiser for work notes. - """ - - -@click.command(options_metavar="", name="list") -def list_all(): - """List all scopes defined in the config.yml""" - _all_scopes = [user_scope["name"] for user_scope in tembo.CONFIG.scopes] - tembo.logger.info( - "%s names found in config.yml: '%s'", len(_all_scopes), "', '".join(_all_scopes) - ) - raise SystemExit(0) - - -@click.command(options_metavar="") -@click.argument("scope", metavar="") -@click.argument( - "inputs", - nargs=-1, - metavar="", -) -@click.option("--dry-run", is_flag=True, default=False) -@click.option("--example", is_flag=True, default=False) -def new(scope, inputs, dry_run, example): - r""" - Create a new page. - - \n - The name of the scope in the config.yml. - - \n - Any input token values that are defined in the config.yml for this scope. - Accepts multiple inputs separated by a space. - - Example: tembo new meeting my_presentation - """ - - # get the name from the tembo config.yml - _cli_verify_name_exists(scope) - - # get the scope information from the tembo config.yml - config_scope = _cli_get_config_scope(scope) - - # print the example to the user - if example: - tembo.logger.info( - "Example for 'tembo new %s': %s", - config_scope["name"], - config_scope["example"] - if isinstance(config_scope["example"], str) - else "No example in config.yml", - ) - raise SystemExit(0) - - # if the name is in the config.yml, create the scoped page - page_creator_options = pages.PageCreatorOptions( - base_path=tembo.CONFIG.base_path, - page_path=config_scope["path"], - filename=config_scope["filename"], - extension=config_scope["extension"], - name=config_scope["name"], - example=config_scope["example"], - user_input=inputs, - template_filename=config_scope["template_filename"], - template_path=tembo.CONFIG.template_path, - ) - try: - scoped_page = pages.ScopedPageCreator(page_creator_options).create_page() - except exceptions.BasePathDoesNotExistError as base_path_does_not_exist_error: - tembo.logger.critical(base_path_does_not_exist_error) - raise SystemExit(1) from base_path_does_not_exist_error - except exceptions.TemplateFileNotFoundError as template_file_not_found_error: - tembo.logger.critical(template_file_not_found_error.args[0]) - raise SystemExit(1) from template_file_not_found_error - except exceptions.MismatchedTokenError as mismatched_token_error: - if config_scope["example"] is not None: - tembo.logger.critical( - "Your tembo config.yml/template specifies %s input tokens, you gave %s. Example: %s", - mismatched_token_error.expected, - mismatched_token_error.given, - config_scope["example"], - ) - raise SystemExit(1) from mismatched_token_error - tembo.logger.critical( - "Your tembo config.yml/template specifies %s input tokens, you gave %s", - mismatched_token_error.expected, - mismatched_token_error.given, - ) - raise SystemExit(1) from mismatched_token_error - - if dry_run: - click.echo(cli_message(f"{scoped_page.path} will be created")) - raise SystemExit(0) - - try: - scoped_page.save_to_disk() - raise SystemExit(0) - except exceptions.ScopedPageAlreadyExists as scoped_page_already_exists: - cli_message(f"File {scoped_page_already_exists}") - raise SystemExit(0) from scoped_page_already_exists - - -def _cli_verify_name_exists(scope: str) -> None: - _name_found = scope in [ - user_scope["name"] for user_scope in tembo.CONFIG.scopes - ] - if _name_found: - return - if len(tembo.CONFIG.scopes) > 0: - # if the name is missing in the config.yml, raise error - cli_message(f"Command {scope} not found in config.yml.") - raise SystemExit(0) - # raise error if no config.yml found - if pathlib.Path(tembo.CONFIG.config_path).exists(): - tembo.logger.critical( - "Config.yml found in %s is empty - exiting", tembo.CONFIG.config_path - ) - else: - tembo.logger.critical( - "No config.yml found in %s - exiting", tembo.CONFIG.config_path - ) - raise SystemExit(1) - - -def _cli_get_config_scope(scope: str) -> dict: - config_scope = {} - for option in [ - "name", - "example", - "path", - "filename", - "extension", - "template_filename", - ]: - try: - config_scope.update( - { - option: str(user_scope[option]) - for user_scope in tembo.CONFIG.scopes - if user_scope["name"] == scope - } - ) - except KeyError as key_error: - if key_error.args[0] in ["example", "template_filename"]: - config_scope.update({key_error.args[0]: None}) - continue - tembo.logger.critical( - "Key %s not found in config. yml - exiting", key_error - ) - raise SystemExit(1) from key_error - return config_scope - - -def cli_message(message: str) -> None: - click.echo(f"[TEMBO] {message} 🐘") - - -run.add_command(new) -run.add_command(list_all) - - -if __name__ == "__main__": - # new(["meeting", "robs presentation", "meeting on gcp"]) - new(["meeting", "a", "b", "c", "d"]) - # new(["meeting", "robs presentation"]) - - # pyinstaller - # if getattr(sys, "frozen", False): - # run(sys.argv[1:]) - # run(sys.argv[1:]) diff --git a/tembo/cli/__init__.py b/tembo/cli/__init__.py new file mode 100644 index 0000000..f4076cb --- /dev/null +++ b/tembo/cli/__init__.py @@ -0,0 +1,30 @@ +import os + +import panaetius +from panaetius.exceptions import LoggingDirectoryDoesNotExistException + +__version__ = "0.1.0" + +if (config_path := os.environ.get("TEMBO_CONFIG")) is not None: + CONFIG = panaetius.Config("tembo", config_path, skip_header_init=True) +else: + CONFIG = panaetius.Config("tembo", "~/tembo/.config", skip_header_init=True) + + +panaetius.set_config(CONFIG, "base_path", "~/tembo") +panaetius.set_config(CONFIG, "template_path", "~/tembo/.templates") +panaetius.set_config(CONFIG, "scopes", {}) +panaetius.set_config(CONFIG, "logging.level", "DEBUG") +panaetius.set_config(CONFIG, "logging.path") + +try: + logger = panaetius.set_logger( + CONFIG, panaetius.SimpleLogger(logging_level=CONFIG.logging_level) + ) +except LoggingDirectoryDoesNotExistException: + _LOGGING_PATH = CONFIG.logging_path + CONFIG.logging_path = "" + logger = panaetius.set_logger( + CONFIG, panaetius.SimpleLogger(logging_level=CONFIG.logging_level) + ) + logger.warning("Logging directory %s does not exist", _LOGGING_PATH) diff --git a/tembo/cli/cli.py b/tembo/cli/cli.py new file mode 100644 index 0000000..e0df85b --- /dev/null +++ b/tembo/cli/cli.py @@ -0,0 +1,198 @@ +from __future__ import annotations + +import pathlib +from typing import Collection + +import click + +import tembo.cli +from tembo.journal import pages +from tembo.utils import Success +from tembo import exceptions + + +CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) + + +@click.group(context_settings=CONTEXT_SETTINGS, options_metavar="") +@click.version_option( + tembo.__version__, + "-v", + "--version", + prog_name="Tembo", + message=f"Tembo v{tembo.__version__} 🐘", +) +def run(): + """ + Tembo - an organiser for work notes. + """ + + +@click.command(options_metavar="", name="list") +def list_all(): + """List all scopes defined in the config.yml.""" + _all_scopes = [user_scope["name"] for user_scope in tembo.cli.CONFIG.scopes] + _all_scopes_joined = "', '".join(_all_scopes) + cli_message(f"{len(_all_scopes)} names found in config.yml: '{_all_scopes_joined}'") + raise SystemExit(0) + + +@click.command(options_metavar="") +@click.argument("scope", metavar="") +@click.argument( + "inputs", + nargs=-1, + metavar="", +) +@click.option("--dry-run", is_flag=True, default=False) +@click.option("--example", is_flag=True, default=False) +def new(scope: str, inputs: Collection[str], dry_run: bool, example: bool): + r""" + Create a new page. + + \n + The name of the scope in the config.yml. + + \n + Any input token values that are defined in the config.yml for this scope. + Accepts multiple inputs separated by a space. + + Example: tembo new meeting my_presentation + """ + + # check that the name exists in the config.yml + try: + _new_verify_name_exists(scope) + except ( + exceptions.ScopeNotFound, + exceptions.EmptyConfigYML, + exceptions.MissingConfigYML, + ) as tembo_exception: + cli_message(tembo_exception.args[0]) + raise SystemExit(1) from tembo_exception + + # get the scope configuration from the config.yml + try: + config_scope = _new_get_config_scope(scope) + except exceptions.MandatoryKeyNotFound as mandatory_key_not_found: + cli_message(mandatory_key_not_found.args[0]) + raise SystemExit(1) from mandatory_key_not_found + + # if --example flag, return the example to the user + _new_show_example(example, config_scope) + + # if the name is in the config.yml, create the scoped page + scoped_page = new_create_scoped_page(config_scope, inputs) + + if dry_run: + cli_message(f"{scoped_page.path} will be created") + raise SystemExit(0) + + try: + result = scoped_page.save_to_disk() + if isinstance(result, Success): + cli_message(f"Saved {result.message} to disk") + raise SystemExit(0) + except exceptions.ScopedPageAlreadyExists as scoped_page_already_exists: + cli_message(f"File {scoped_page_already_exists}") + raise SystemExit(0) from scoped_page_already_exists + + +def new_create_scoped_page(config_scope: dict, inputs: Collection[str]) -> pages.Page: + page_creator_options = pages.PageCreatorOptions( + base_path=tembo.cli.CONFIG.base_path, + template_path=tembo.cli.CONFIG.template_path, + page_path=config_scope["path"], + filename=config_scope["filename"], + extension=config_scope["extension"], + name=config_scope["name"], + example=config_scope["example"], + user_input=inputs, + template_filename=config_scope["template_filename"], + ) + try: + return pages.ScopedPageCreator(page_creator_options).create_page() + except exceptions.BasePathDoesNotExistError as base_path_does_not_exist_error: + cli_message(base_path_does_not_exist_error.args[0]) + raise SystemExit(1) from base_path_does_not_exist_error + except exceptions.TemplateFileNotFoundError as template_file_not_found_error: + cli_message(template_file_not_found_error.args[0]) + raise SystemExit(1) from template_file_not_found_error + except exceptions.MismatchedTokenError as mismatched_token_error: + if config_scope["example"] is not None: + cli_message( + f'Your tembo config.yml/template specifies {mismatched_token_error.expected} input tokens, you gave {mismatched_token_error.given}. Example: {config_scope["example"]}' + ) + raise SystemExit(1) from mismatched_token_error + cli_message( + f"Your tembo config.yml/template specifies {mismatched_token_error.expected} input tokens, you gave {mismatched_token_error.given}" + ) + + raise SystemExit(1) from mismatched_token_error + + +def _new_verify_name_exists(scope: str) -> None: + _name_found = scope in [ + user_scope["name"] for user_scope in tembo.cli.CONFIG.scopes + ] + if _name_found: + return + if len(tembo.cli.CONFIG.scopes) > 0: + # if the name is missing in the config.yml, raise error + raise exceptions.ScopeNotFound(f"Scope {scope} not found in config.yml") + # raise error if no config.yml found + if pathlib.Path(tembo.cli.CONFIG.config_path).exists(): + raise exceptions.EmptyConfigYML( + f"Config.yml found in {tembo.cli.CONFIG.config_path} is empty" + ) + raise exceptions.MissingConfigYML( + f"No config.yml found in {tembo.cli.CONFIG.config_path}" + ) + + +def _new_get_config_scope(scope: str) -> dict: + config_scope = {} + optional_keys = ["example", "template_filename"] + for option in [ + "name", + "path", + "filename", + "extension", + "example", + "template_filename", + ]: + try: + config_scope.update( + { + option: str(user_scope[option]) + for user_scope in tembo.cli.CONFIG.scopes + if user_scope["name"] == scope + } + ) + except KeyError as key_error: + if key_error.args[0] in optional_keys: + config_scope.update({key_error.args[0]: None}) + continue + raise exceptions.MandatoryKeyNotFound( + f"Key {key_error} not found in config.yml" + ) + return config_scope + + +def _new_show_example(example: bool, config_scope: dict) -> None: + if example: + if isinstance(config_scope["example"], str): + cli_message( + f'Example for {config_scope["name"]}: {config_scope["example"]}' + ) + else: + cli_message("No example in config.yml") + raise SystemExit(0) + + +def cli_message(message: str) -> None: + click.echo(f"[TEMBO] {message} 🐘") + + +run.add_command(new) +run.add_command(list_all) diff --git a/tembo/exceptions.py b/tembo/exceptions.py index 581fcb0..df6a885 100644 --- a/tembo/exceptions.py +++ b/tembo/exceptions.py @@ -18,3 +18,19 @@ class TemplateFileNotFoundError(Exception): class ScopedPageAlreadyExists(Exception): """Raised if the scoped page file already exists.""" + + +class MissingConfigYML(Exception): + """Raised if the config.yml file is missing.""" + + +class EmptyConfigYML(Exception): + """Raised if the config.yml file is empty.""" + + +class ScopeNotFound(Exception): + """Raised if the scope does not exist in the config.yml.""" + + +class MandatoryKeyNotFound(Exception): + """Raised if a mandatory key is not found in the config.yml.""" diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index c7783f9..4810cdf 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -12,6 +12,7 @@ from jinja2.exceptions import TemplateNotFound import tembo from tembo import exceptions +import tembo.utils # TODO: flesh this out with details for the optional args @@ -216,8 +217,13 @@ class Page(metaclass=ABCMeta): def __init__(self, path: pathlib.Path, page_content: str) -> None: raise NotImplementedError + @property @abstractmethod - def save_to_disk(self) -> None: + def path(self) -> pathlib.Path: + raise NotImplementedError + + @abstractmethod + def save_to_disk(self) -> tembo.utils.Success: raise NotImplementedError @@ -236,13 +242,17 @@ class ScopedPage(Page): path (pathlib.Path): a `pathlib.Path` object of the page's filepath. page_content (str): the content of the page from the template. """ - self.path = path + self._path = path self.page_content = page_content def __str__(self) -> str: - return f"ScopedPage({self.path})" + return f"ScopedPage(\"{self.path}\")" - def save_to_disk(self) -> None: + @property + def path(self) -> pathlib.Path: + return self._path + + def save_to_disk(self) -> tembo.utils.Success: """Save the scoped page to disk and write the `page_content`. If the page already exists a message will be logged to stdout and no file @@ -265,8 +275,6 @@ class ScopedPage(Page): scoped_page_file.parents[0].mkdir(parents=True, exist_ok=True) if scoped_page_file.exists(): raise exceptions.ScopedPageAlreadyExists(f"{self.path} already exists") - with scoped_page_file.open("w", encoding="utf-8") as scoped_page: scoped_page.write(self.page_content) - # TODO: pass this back somehow - tembo.logger.info("Saved %s to disk", self.path) + return tembo.utils.Success(str(self.path)) diff --git a/tembo/utils/__init__.py b/tembo/utils/__init__.py new file mode 100644 index 0000000..2ab70fe --- /dev/null +++ b/tembo/utils/__init__.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass + + +@dataclass +class Success: + """Success message. + + Attributes: + message (str): A success message. + """ + + message: str diff --git a/tests/test_cli/__init__.py b/tests/test_cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_cli/data/config/empty/config.yml b/tests/test_cli/data/config/empty/config.yml new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/tests/test_cli/data/config/empty/config.yml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/test_cli/data/config/missing_keys/config.yml b/tests/test_cli/data/config/missing_keys/config.yml new file mode 100644 index 0000000..6f6a2f8 --- /dev/null +++ b/tests/test_cli/data/config/missing_keys/config.yml @@ -0,0 +1,5 @@ +tembo: + scopes: + - name: some_scope + path: "some_scope" + extension: md diff --git a/tests/test_cli/data/config/optional_keys/config.yml b/tests/test_cli/data/config/optional_keys/config.yml new file mode 100644 index 0000000..2af3099 --- /dev/null +++ b/tests/test_cli/data/config/optional_keys/config.yml @@ -0,0 +1,6 @@ +tembo: + scopes: + - name: some_scope + path: "some_scope" + filename: "{name}" + extension: md diff --git a/tests/test_cli/data/config/success/config.yml b/tests/test_cli/data/config/success/config.yml new file mode 100644 index 0000000..0550bbe --- /dev/null +++ b/tests/test_cli/data/config/success/config.yml @@ -0,0 +1,12 @@ +tembo: + scopes: + - name: some_scope + example: tembo new some_scope + path: "some_scope" + filename: "{name}" + extension: md + - name: another_some_scope + example: tembo new another_some_scope + path: "another_some_scope" + filename: "{name}" + extension: md diff --git a/tests/test_cli/data/some_scope/some_scope.md b/tests/test_cli/data/some_scope/some_scope.md new file mode 100644 index 0000000..ce7e948 --- /dev/null +++ b/tests/test_cli/data/some_scope/some_scope.md @@ -0,0 +1 @@ +already exists diff --git a/tests/test_cli/test_cli.py b/tests/test_cli/test_cli.py new file mode 100644 index 0000000..a89346a --- /dev/null +++ b/tests/test_cli/test_cli.py @@ -0,0 +1,461 @@ +import importlib +import os +import pathlib + +import pytest + +import tembo.exceptions +import tembo.cli +from tembo.cli.cli import ( + _new_verify_name_exists, + _new_get_config_scope, + _new_show_example, + new_create_scoped_page, + new, + list_all, +) + + +def test_new_verify_name_exists_success(shared_datadir): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + importlib.reload(tembo.cli) + + # act + verified_name = _new_verify_name_exists("some_scope") + + # assert + assert verified_name is None + + # cleanup + del os.environ["TEMBO_CONFIG"] + + +def test_new_verify_name_exists_scope_not_found(shared_datadir): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + importlib.reload(tembo.cli) + from tembo.cli.cli import _new_verify_name_exists + + # act + with pytest.raises(tembo.exceptions.ScopeNotFound) as scope_not_found: + _new_verify_name_exists("some_missing_scope") + + # assert + assert ( + str(scope_not_found.value) == "Scope some_missing_scope not found in config.yml" + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + + +def test_new_verify_name_exists_empty_config(shared_datadir): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "empty") + importlib.reload(tembo.cli) + from tembo.cli.cli import _new_verify_name_exists + + # act + with pytest.raises(tembo.exceptions.EmptyConfigYML) as empty_config_yml: + _new_verify_name_exists("some_missing_scope") + + # assert + assert ( + str(empty_config_yml.value) + == f'Config.yml found in {os.environ["TEMBO_CONFIG"]} is empty' + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + + +def test_new_verify_name_exists_missing_config(shared_datadir): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "missing") + importlib.reload(tembo.cli) + from tembo.cli.cli import _new_verify_name_exists + + # act + with pytest.raises(tembo.exceptions.MissingConfigYML) as missing_config_yml: + _new_verify_name_exists("some_missing_scope") + + # assert + assert ( + str(missing_config_yml.value) + == f'No config.yml found in {os.environ["TEMBO_CONFIG"]}' + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + + +def test_new_get_config_scope_success(shared_datadir): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "optional_keys") + importlib.reload(tembo.cli) + + # act + config_scope = _new_get_config_scope("some_scope") + + # assert + assert config_scope == { + "name": "some_scope", + "path": "some_scope", + "filename": "{name}", + "extension": "md", + "example": None, + "template_filename": None, + } + + # cleanup + del os.environ["TEMBO_CONFIG"] + + +def test_new_get_config_scope_key_not_found(shared_datadir): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "missing_keys") + importlib.reload(tembo.cli) + + # act + with pytest.raises( + tembo.exceptions.MandatoryKeyNotFound + ) as mandatory_key_not_found: + config_scope = _new_get_config_scope("some_scope") + + # assert + assert ( + str(mandatory_key_not_found.value) == "Key 'filename' not found in config.yml" + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + + +@pytest.mark.parametrize( + "path,message", + [ + ("success", "[TEMBO] Example for some_scope: tembo new some_scope 🐘\n"), + ("optional_keys", "[TEMBO] No example in config.yml 🐘\n"), + ], +) +def test_new_show_example(path, message, shared_datadir, capsys): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / path) + importlib.reload(tembo.cli) + config_scope = _new_get_config_scope("some_scope") + + # act + with pytest.raises(SystemExit) as system_exit: + _new_show_example(True, config_scope) + + # assert + assert capsys.readouterr().out == message + assert system_exit.value.code == 0 + + # cleanup + del os.environ["TEMBO_CONFIG"] + + +def test_new_create_scoped_page_success(shared_datadir, tmpdir): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir) + importlib.reload(tembo.cli) + + config_scope = _new_get_config_scope("some_scope") + inputs = () + scoped_page_file = pathlib.Path(tmpdir / "some_scope" / "some_scope").with_suffix( + ".md" + ) + + # act + scoped_page = new_create_scoped_page(config_scope, inputs) + + # assert + assert scoped_page.path == scoped_page_file + assert scoped_page.page_content == "" + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + + +def test_new_create_scoped_page_base_path_does_not_exist( + shared_datadir, tmpdir, capsys +): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir / "nonexistent" / "path") + importlib.reload(tembo.cli) + + config_scope = _new_get_config_scope("some_scope") + inputs = () + + # act + with pytest.raises(SystemExit) as system_exit: + new_create_scoped_page(config_scope, inputs) + + # assert + assert system_exit.value.code == 1 + assert ( + capsys.readouterr().out + == f'[TEMBO] Tembo base path of {os.environ["TEMBO_BASE_PATH"]} does not exist. 🐘\n' + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + + +def test_new_create_scoped_page_template_file_does_not_exist( + shared_datadir, tmpdir, capsys +): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir) + os.environ["TEMBO_TEMPLATE_PATH"] = str(tmpdir) + importlib.reload(tembo.cli) + + config_scope = _new_get_config_scope("some_scope") + config_scope["template_filename"] = "some_nonexistent_template.md.tpl" + inputs = () + + # act + with pytest.raises(SystemExit) as system_exit: + new_create_scoped_page(config_scope, inputs) + + # assert + assert system_exit.value.code == 1 + assert ( + capsys.readouterr().out + == f'[TEMBO] Template file {os.environ["TEMBO_TEMPLATE_PATH"]}/{config_scope["template_filename"]} does not exist. 🐘\n' + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + + +@pytest.mark.parametrize("example", [(True,), (False,)]) +def test_new_create_scoped_page_mismatched_token( + example, shared_datadir, tmpdir, capsys +): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir) + importlib.reload(tembo.cli) + + config_scope = _new_get_config_scope("some_scope") + inputs = ("some_input",) + if not example[0]: + config_scope["example"] = None + + # act + with pytest.raises(SystemExit) as system_exit: + new_create_scoped_page(config_scope, inputs) + + # assert + assert system_exit.value.code == 1 + if not example[0]: + assert ( + capsys.readouterr().out + == f"[TEMBO] Your tembo config.yml/template specifies 0 input tokens, you gave 1 🐘\n" + ) + else: + assert ( + capsys.readouterr().out + == f"[TEMBO] Your tembo config.yml/template specifies 0 input tokens, you gave 1. Example: tembo new some_scope 🐘\n" + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + + +def test_new_dry_run(shared_datadir, tmpdir, capsys): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir) + importlib.reload(tembo.cli) + scope = "some_scope" + dry_run = "--dry-run" + + # act + with pytest.raises(SystemExit) as system_exit: + new([scope, dry_run]) + + # assert + assert system_exit.value.code == 0 + assert ( + capsys.readouterr().out + == f"[TEMBO] {tmpdir}/some_scope/some_scope.md will be created 🐘\n" + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + + +def test_new_success(shared_datadir, tmpdir, capsys): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir) + importlib.reload(tembo.cli) + scoped_page_file = pathlib.Path(tmpdir / "some_scope" / "some_scope").with_suffix( + ".md" + ) + + # act + with pytest.raises(SystemExit) as system_exit: + new(["some_scope"]) + + # assert + assert scoped_page_file.exists() + assert system_exit.value.code == 0 + assert capsys.readouterr().out == f"[TEMBO] Saved {scoped_page_file} to disk 🐘\n" + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + + +def test_new_success_already_exists(shared_datadir, capsys): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + os.environ["TEMBO_BASE_PATH"] = str(shared_datadir) + importlib.reload(tembo.cli) + scoped_page_file = pathlib.Path( + shared_datadir / "some_scope" / "some_scope" + ).with_suffix(".md") + + # act + with pytest.raises(SystemExit) as system_exit: + new(["some_scope"]) + + # assert + assert scoped_page_file.exists() + assert system_exit.value.code == 0 + assert ( + capsys.readouterr().out == f"[TEMBO] File {scoped_page_file} already exists 🐘\n" + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + + +def test_new_scope_not_found(shared_datadir, tmpdir, capsys): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir) + importlib.reload(tembo.cli) + scoped_page_file = pathlib.Path(tmpdir / "some_scope" / "some_scope").with_suffix( + ".md" + ) + + # act + with pytest.raises(SystemExit) as system_exit: + new(["some_nonexistent_scope"]) + + # assert + assert not scoped_page_file.exists() + assert system_exit.value.code == 1 + assert ( + capsys.readouterr().out + == f"[TEMBO] Scope some_nonexistent_scope not found in config.yml 🐘\n" + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + + +def test_new_empty_config(shared_datadir, tmpdir, capsys): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "empty") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir) + importlib.reload(tembo.cli) + + # act + with pytest.raises(SystemExit) as system_exit: + new(["some_nonexistent_scope"]) + + # assert + assert system_exit.value.code == 1 + assert ( + capsys.readouterr().out + == f"[TEMBO] Config.yml found in {shared_datadir}/config/empty is empty 🐘\n" + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + + +def test_new_missing_config(shared_datadir, tmpdir, capsys): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "missing") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir) + importlib.reload(tembo.cli) + + # act + with pytest.raises(SystemExit) as system_exit: + new(["some_nonexistent_scope"]) + + # assert + assert system_exit.value.code == 1 + assert ( + capsys.readouterr().out + == f"[TEMBO] No config.yml found in {shared_datadir}/config/missing 🐘\n" + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + + +def test_new_missing_mandatory_key(shared_datadir, tmpdir, capsys): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "missing_keys") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir) + importlib.reload(tembo.cli) + + # act + with pytest.raises(SystemExit) as system_exit: + new(["some_scope"]) + + # assert + assert system_exit.value.code == 1 + assert ( + capsys.readouterr().out == f"[TEMBO] Key 'filename' not found in config.yml 🐘\n" + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + + +def test_list_all_success(shared_datadir, tmpdir, capsys): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir) + importlib.reload(tembo.cli) + scoped_page_file = pathlib.Path(tmpdir / "some_scope" / "some_scope").with_suffix( + ".md" + ) + + # act + with pytest.raises(SystemExit) as system_exit: + list_all([]) + + # assert + assert system_exit.value.code == 0 + assert ( + capsys.readouterr().out + == f"[TEMBO] 2 names found in config.yml: 'some_scope', 'another_some_scope' 🐘\n" + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] diff --git a/tests/test_journal/test_pages.py b/tests/test_journal/test_pages.py index 7cf101a..399c6c8 100644 --- a/tests/test_journal/test_pages.py +++ b/tests/test_journal/test_pages.py @@ -3,8 +3,9 @@ import pathlib import pytest -from tembo.journal.pages import PageCreatorOptions, ScopedPageCreator +from tembo import PageCreatorOptions, ScopedPageCreator from tembo import exceptions +from tembo.utils import Success DATE_TODAY = date.today().strftime("%d-%m-%Y") @@ -90,7 +91,7 @@ def test_create_page_already_exists(datadir): # act scoped_page = ScopedPageCreator(options).create_page() with pytest.raises(exceptions.ScopedPageAlreadyExists) as page_already_exists: - scoped_page.save_to_disk() + result = scoped_page.save_to_disk() # assert assert scoped_page_file.exists() @@ -99,7 +100,7 @@ def test_create_page_already_exists(datadir): assert scoped_page_contents.readlines() == ["this file already exists\n"] -def test_create_page_without_template(tmpdir, caplog): +def test_create_page_without_template(tmpdir): # arrange options = PageCreatorOptions( base_path=str(tmpdir), @@ -112,18 +113,18 @@ def test_create_page_without_template(tmpdir, caplog): template_filename=None, template_path=None, ) - # TODO: copy this pattern creation into the other tests scoped_page_file = ( pathlib.Path(options.base_path) / options.page_path / options.filename ).with_suffix(f".{options.extension}") # act scoped_page = ScopedPageCreator(options).create_page() - scoped_page.save_to_disk() + result = scoped_page.save_to_disk() # assert assert scoped_page_file.exists() - assert caplog.records[0].message == f"Saved {scoped_page_file} to disk" + assert isinstance(result, Success) + assert result.message == str(scoped_page_file) with scoped_page_file.open("r", encoding="utf-8") as scoped_page_contents: assert scoped_page_contents.readlines() == [] @@ -147,11 +148,12 @@ def test_create_page_with_template(datadir, caplog): # act scoped_page = ScopedPageCreator(options).create_page() - scoped_page.save_to_disk() + result = scoped_page.save_to_disk() # assert assert scoped_page_file.exists() - assert caplog.records[0].message == f"Saved {scoped_page_file} to disk" + assert isinstance(result, Success) + assert result.message == str(scoped_page_file) with scoped_page_file.open("r", encoding="utf-8") as scoped_page_contents: assert scoped_page_contents.readlines() == [ "scoped page file\n", @@ -193,11 +195,12 @@ def test_create_tokened_page_tokens_in_template( # act scoped_page = ScopedPageCreator(options).create_page() - scoped_page.save_to_disk() + result = scoped_page.save_to_disk() # assert assert scoped_page_file.exists() - assert caplog.records[0].message == f"Saved {scoped_page_file} to disk" + assert isinstance(result, Success) + assert result.message == str(scoped_page_file) with scoped_page_file.open("r", encoding="utf-8") as scoped_page_contents: assert scoped_page_contents.readline() == page_contents @@ -236,11 +239,12 @@ def test_create_tokened_page_tokens_in_filename( # act scoped_page = ScopedPageCreator(options).create_page() - scoped_page.save_to_disk() + result = scoped_page.save_to_disk() # assert assert scoped_page_file.exists() - assert caplog.records[0].message == f"Saved {scoped_page_file} to disk" + assert isinstance(result, Success) + assert result.message == str(scoped_page_file) def test_create_tokened_page_input_tokens_preserve_order(datadir, caplog): @@ -263,11 +267,12 @@ def test_create_tokened_page_input_tokens_preserve_order(datadir, caplog): # act scoped_page = ScopedPageCreator(options).create_page() - scoped_page.save_to_disk() + result = scoped_page.save_to_disk() # assert assert scoped_page_file.exists() - assert caplog.records[0].message == f"Saved {scoped_page_file} to disk" + assert isinstance(result, Success) + assert result.message == str(scoped_page_file) with scoped_page_file.open(mode="r", encoding="utf-8") as scoped_page_contents: assert scoped_page_contents.readline() == "third_input second_input" @@ -324,11 +329,12 @@ def test_create_page_spaces_in_path(tmpdir, caplog): # act scoped_page = ScopedPageCreator(options).create_page() - scoped_page.save_to_disk() + result = scoped_page.save_to_disk() # assert assert scoped_page_file.exists() - assert caplog.records[0].message == f"Saved {scoped_page_file} to disk" + assert isinstance(result, Success) + assert result.message == str(scoped_page_file) def test_create_page_dot_in_extension(tmpdir, caplog): @@ -350,11 +356,12 @@ def test_create_page_dot_in_extension(tmpdir, caplog): # act scoped_page = ScopedPageCreator(options).create_page() - scoped_page.save_to_disk() + result = scoped_page.save_to_disk() # assert assert scoped_page_file.exists() - assert caplog.records[0].message == f"Saved {scoped_page_file} to disk" + assert isinstance(result, Success) + assert result.message == str(scoped_page_file) def test_create_page_str_representation(tmpdir): @@ -378,4 +385,4 @@ def test_create_page_str_representation(tmpdir): scoped_page = ScopedPageCreator(options).create_page() # assert - assert str(scoped_page) == f"ScopedPage({scoped_page_file})" + assert str(scoped_page) == f"ScopedPage(\"{scoped_page_file}\")"