diff --git a/TODO.todo b/TODO.todo index da7030b..f7c5ce0 100644 --- a/TODO.todo +++ b/TODO.todo @@ -1,7 +1,4 @@ Priority: - ✔ Document the python/logging/typing in Trilium @done(21-10-25 14:33) - ✔ Update typing annotations to include generics instead @done(21-10-25 22:38) - https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes ☐ Write the tests ☐ test logs: document this @@ -25,17 +22,14 @@ Documentation: ☐ 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 Functionality: - ✔ Move any `tembo.CONFIG` calls out of `pages.py` and ensure these are passed in from the cli. @done(21-10-28 19:44) - ✔ Make `config scope` a dict in `cli.py`. @done(21-10-28 19:44) - ✔ Make example optional @done(21-10-29 00:15) - ✔ Add the `--example` output to the miscounted token message so the user knows the correct command to use. @done(21-10-29 00:15) - ✔ Page options dataclass @done(21-10-28 20:09) ☐ Replace loggers with `click.echo` for command outputs. Keep logging messages for actual logging messages? - ☐ Look at `_convert_to_path()` and see if it can be rewritten to make it clearer when there isn't a base path. - Currently checks to see if base_path is not None but this is never the case as a string must be passed in and if there isn't a base_path we pass in an empty string. - ☐ Replace scoped page creator inputs so that the whole class uses the options dict rather than the variables passed around. + ☐ Refactor the tembo new command so the cli is split out into manageable methods + ☐ Use the complicated CLI example so the tembo new has its own module to define functions in + ☐ Replace all logger errors with exceptions, move logger messages to the cli. + ✔ Make options a property on the class, add to abstract @done(21-10-30 19:31) ☐ Use the python runner Duty ☐ Run tests @@ -74,6 +68,17 @@ Logging: clone the repo, delete .git, git init, configure and add git origin Archive: + ✔ Document the python/logging/typing in Trilium @done(21-10-25 14:33) @project(Priority) + ✔ Update typing annotations to include generics instead @done(21-10-25 22:38) @project(Priority) + https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes + ✔ Move any `tembo.CONFIG` calls out of `pages.py` and ensure these are passed in from the cli. @done(21-10-28 19:44) @project(Functionality) + ✔ Make `config scope` a dict in `cli.py`. @done(21-10-28 19:44) @project(Functionality) + ✔ Make example optional @done(21-10-29 00:15) @project(Functionality) + ✔ Add the `--example` output to the miscounted token message so the user knows the correct command to use. @done(21-10-29 00:15) @project(Functionality) + ✔ Page options dataclass @done(21-10-28 20:09) @project(Functionality) + ✔ Make user_input optional @important @done(21-10-30 03:20) @project(Functionality) + ✔ Look at `_convert_to_path()` and see if it can be rewritten to make it clearer when there isn't a base path. @done(21-10-30 02:14) @project(Functionality) + ✔ Replace scoped page creator inputs so that the whole class uses the options dict rather than the variables passed around. @done(21-10-30 03:20) @project(Functionality) ✔ Go through code TODOs @done(21-10-25 05:52) @project(Priority) ✔ Check code order and make sure things are where they should be @done(21-10-25 13:31) @project(Priority) ✔ Add version option @done(21-10-25 13:40) @project(Functionality) diff --git a/dev/notes/test.md b/dev/notes/test.md index 1c065d0..4a1a293 100644 --- a/dev/notes/test.md +++ b/dev/notes/test.md @@ -1,9 +1,10 @@ # testing notes optional: -- template_path +- user_input - example - template_filename +- template_path required: - base_path @@ -11,20 +12,46 @@ required: - filename - extension - name -- user_input - should be optional - - - -- page with a template -- page without a template -- page using date tokens -- page using input tokens -- page using name tokens +- page with/without a template +- user input is None +- the given base path does not exist +- page using/not using input tokens - user input does not match number of input tokens + - no user input + - mismatched user input + - with/without example +- page using/not using date tokens +- page using/not using name tokens + + +- path/page filenames can contain spaces and they are converted +@dataclass +class PageCreatorOptions: + """Options dataclass to create a Page. -- user input does match number of input tokensE + Attributes: + base_path (str): + page_path (str): + filename (str): + extension (str): + name (str): + user_input (Collection[str] | None, optional): + example (str | None, optional): + template_filename (str | None, optional): + template_path (str | None, optional): + """ + + base_path: str + page_path: str + filename: str + extension: str + name: str + user_input: Collection[str] | None = None + example: str | None = None + template_filename: str | None = None + template_path: str | None = None diff --git a/tembo/exceptions.py b/tembo/exceptions.py index a71ea99..9746128 100644 --- a/tembo/exceptions.py +++ b/tembo/exceptions.py @@ -3,3 +3,7 @@ class MismatchedTokenError(Exception): pass + + +class BasePathDoesNotExistError(Exception): + pass diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index d9f3ea6..4870b47 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -13,69 +13,90 @@ from jinja2.exceptions import TemplateNotFound import tembo +# TODO: flesh this out with details for the optional args @dataclass class PageCreatorOptions: + """Options dataclass to create a Page. + + Attributes: + base_path (str): + page_path (str): + filename (str): + extension (str): + name (str): + user_input (Collection[str] | None, optional): + example (str | None, optional): + template_filename (str | None, optional): + template_path (str | None, optional): + """ + base_path: str page_path: str filename: str extension: str name: str - user_input: Collection[str] + user_input: Collection[str] | None = None example: str | None = None template_filename: str | None = None template_path: str | None = None class PageCreator: + def __init__(self, options: PageCreatorOptions) -> None: + raise NotImplementedError + + @property + @abstractmethod + def options(self) -> PageCreatorOptions: + raise NotImplementedError + @abstractmethod def create_page(self, options: PageCreatorOptions) -> Page: raise NotImplementedError - @staticmethod - def _convert_to_path( - base_path: str, page_path: str, filename: str, extension: str - ) -> pathlib.Path: + def _convert_base_path_to_path(self) -> pathlib.Path: # check if Tembo base path exists - if not pathlib.Path(base_path).expanduser().exists(): + if not pathlib.Path(self.options.base_path).expanduser().exists(): tembo.logger.critical( - "Tembo base path of %s does not exist - exiting", base_path + "Tembo base path of %s does not exist - exiting", self.options.base_path ) raise SystemExit(1) path_to_file = ( - pathlib.Path(base_path).expanduser() - / pathlib.Path(page_path.replace(" ", "_")).expanduser() - / filename.replace(" ", "_") + pathlib.Path(self.options.base_path).expanduser() + / pathlib.Path(self.options.page_path.replace(" ", "_")).expanduser() + / self.options.filename.replace(" ", "_") ) try: # check for existing `.` in the extension - extension = extension[1:] if extension[0] == "." else extension + extension = ( + self.options.extension[1:] + if self.options.extension[0] == "." + else self.options.extension + ) except IndexError: # IndexError means the path is not a file, just a path return path_to_file # return path with a file return path_to_file.with_suffix(f".{extension}") - def _load_template( - self, base_path: str, template_filename: str, template_path: str | None - ) -> str: - # check for overriden template_path - if template_path is not None: - converted_template_path = pathlib.Path(template_path).expanduser() + def _load_template(self) -> str: + if self.options.template_filename is None: + return "" + if self.options.template_path is not None: + converted_template_path = pathlib.Path( + self.options.template_path + ).expanduser() else: - # default template_path is base_path / .templates - converted_template_path = self._convert_to_path( - base_path, ".templates", "", "" - ) - # load the template folder + converted_template_path = pathlib.Path() + file_loader = jinja2.FileSystemLoader(converted_template_path) env = jinja2.Environment(loader=file_loader, autoescape=True) - # load the template contents try: - loaded_template = env.get_template(template_filename) + loaded_template = env.get_template(self.options.template_filename) except TemplateNotFound as template_not_found: tembo.logger.critical( "Template file %s not found - exiting", - str(template_path) + "/" + str(template_not_found.message), + str(self.options.template_path) + "/" + str(template_not_found.message), ) raise SystemExit(1) from template_not_found return loaded_template.render() @@ -92,124 +113,102 @@ class ScopedPageCreator(PageCreator): """ def __init__(self) -> None: - self.base_path = "" - self.page_path = "" - self.filename = "" - self.extension = "" self._all_input_tokens: list[str] = [] + self._options: PageCreatorOptions + + @property + def options(self) -> PageCreatorOptions: + return self._options def create_page(self, options: PageCreatorOptions) -> Page: - self.base_path = options.base_path - self.page_path = options.page_path - self.filename = options.filename - self.extension = options.extension + self._options = options - # verify the user input length matches the number of input tokens in the - # tembo config/templates - self._all_input_tokens = self._get_input_tokens( - options.template_filename, options.template_path - ) - self._verify_input_tokens(options.user_input, options.example) + self._all_input_tokens = self._get_input_tokens() + self._verify_input_tokens() - # get the path of the scoped page - path = self._convert_to_path( - self.base_path, self.page_path, self.filename, self.extension - ) + path = self._convert_base_path_to_path() + path = pathlib.Path(self._substitute_tokens(str(path))) - # substitute tokens in the filepath - path = pathlib.Path( - self._substitute_tokens(str(path), options.user_input, options.name) - ) - - # get the template file - template_contents = self._get_template_contents( - options.template_filename, options.template_path - ) - # substitute tokens in template_contents + template_contents = self._load_template() if options.template_filename is not None: - template_contents = self._substitute_tokens( - template_contents, options.user_input, options.name - ) + template_contents = self._substitute_tokens(template_contents) + return ScopedPage(path, template_contents) - def _get_template_contents( - self, template_filename: str | None, template_path: str | None - ) -> str: - return ( - self._load_template(self.base_path, template_filename, template_path) - if template_filename is not None - else "" - ) - - def _get_input_tokens( - self, template_filename: str | None, template_path: str | None - ) -> list[str]: + def _get_input_tokens(self) -> list[str]: path = str( pathlib.Path( - self.base_path, self.page_path, self.filename, self.extension + self.options.base_path, + self.options.page_path, + self.options.filename, + self.options.extension, ).expanduser() ) - template_contents = self._get_template_contents( - template_filename, template_path - ) + template_contents = self._load_template() # get the input tokens from both the path and the template all_input_tokens = [] for tokenified_string in (path, template_contents): all_input_tokens.extend(re.findall(r"(\{input\d*\})", tokenified_string)) return sorted(all_input_tokens) - def _verify_input_tokens( - self, user_input: Collection[str], example: str | None - ) -> None: - if len(self._all_input_tokens) != len(user_input): - if example is not None: + def _verify_input_tokens(self) -> None: + if len(self._all_input_tokens) > 0 and self.options.user_input is None: + if self.options.example is not None: + tembo.logger.critical( + "Your tembo.config/template specifies %s input tokens, you gave 0. Example command: %s", + len(self._all_input_tokens), + self.options.example, + ) + else: + tembo.logger.critical( + "Your tembo.config/template specifies %s input tokens, you gave 0.", + len(self._all_input_tokens), + ) + raise SystemExit(1) + if self.options.user_input is None: + return + if len(self._all_input_tokens) != len(self.options.user_input): + if self.options.example is not None: tembo.logger.critical( "Your tembo.config/template specifies %s input tokens, you gave %s. Example command: %s", len(self._all_input_tokens), - len(user_input), - example, + len(self.options.user_input), + self.options.example, ) else: tembo.logger.critical( "Your tembo.config/template specifies %s input tokens, you gave %s.", len(self._all_input_tokens), - len(user_input), + len(self.options.user_input), ) raise SystemExit(1) + return - def _substitute_tokens( - self, - tokenified_string: str, - user_input: Collection[str], - name: str, - ) -> str: + def _substitute_tokens(self, tokenified_string: str) -> str: """For a tokened string, substitute input, name and date tokens.""" - tokenified_string = self.__substitute_input_tokens( - tokenified_string, user_input - ) - tokenified_string = self.__substitute_name_tokens(tokenified_string, name) + tokenified_string = self.__substitute_input_tokens(tokenified_string) + tokenified_string = self.__substitute_name_tokens(tokenified_string) tokenified_string = self.__substitute_date_tokens(tokenified_string) return tokenified_string - def __substitute_input_tokens( - self, - tokenified_string: str, - user_input: Collection[str], - ) -> str: - for input_value, extracted_token in zip(user_input, self._all_input_tokens): - # REVIEW: test this for spaces in the filename/input token - tokenified_string = tokenified_string.replace( - extracted_token, input_value.replace(" ", "_") - ) + def __substitute_input_tokens(self, tokenified_string: str) -> str: + if self.options.user_input is not None: + for input_value, extracted_token in zip( + self.options.user_input, self._all_input_tokens + ): + tokenified_string = tokenified_string.replace( + extracted_token, input_value.replace(" ", "_") + ) return tokenified_string - @staticmethod - def __substitute_name_tokens(tokenified_string: str, name: str) -> str: + def __substitute_name_tokens(self, tokenified_string: str) -> str: """Find any `{name}` tokens and substitute for the name value.""" name_extraction = re.findall(r"(\{name\})", tokenified_string) for extracted_input in name_extraction: - tokenified_string = tokenified_string.replace(extracted_input, name) + tokenified_string = tokenified_string.replace( + extracted_input, self.options.name + ) return tokenified_string @staticmethod diff --git a/tests/test_journal/old_test_pages.py b/tests/test_journal/old_test_pages.py index 03c62d6..7805193 100644 --- a/tests/test_journal/old_test_pages.py +++ b/tests/test_journal/old_test_pages.py @@ -15,7 +15,7 @@ def test_page_creator_convert_to_path_missing_base_path(caplog): # act with pytest.raises(SystemExit) as system_exit: - PageCreator._convert_to_path( + PageCreator._convert_base_path_to_path( base_path=base_path, page_path=page_path, filename=filename, @@ -50,7 +50,7 @@ def test_page_creator_convert_to_path_full_path_to_file( base_path = tmpdir # act - converted_path = PageCreator._convert_to_path( + converted_path = PageCreator._convert_base_path_to_path( base_path, page_path, filename, extension ) @@ -67,7 +67,7 @@ def test_page_creator_convert_to_path_full_path_no_file(tmpdir): extension = "" # act - converted_path = PageCreator._convert_to_path( + converted_path = PageCreator._convert_base_path_to_path( base_path, page_path, filename, extension ) diff --git a/tests/test_journal/test_pages.py b/tests/test_journal/test_pages.py index abc14c5..d751287 100644 --- a/tests/test_journal/test_pages.py +++ b/tests/test_journal/test_pages.py @@ -1,13 +1,31 @@ import pytest -from tembo.journal.pages import PageCreatorOptions +from tembo.journal.pages import PageCreatorOptions, ScopedPageCreator -def test_scoped_page_creator_create_page_missing_base_path(): +def test_create_page_base_path_does_not_exist(tmpdir, caplog): # arrange - options = PageCreatorOptions() + base_path = str(tmpdir / "nonexistent" / "path") + options = PageCreatorOptions( + base_path=base_path, + page_path="", + filename="", + extension="", + name="", + user_input=None, + example=None, + template_filename=None, + template_path=None, + ) # act + with pytest.raises(SystemExit) as system_exit: + scoped_page_creator = ScopedPageCreator().create_page(options) # assert - pass + assert system_exit.value.code == 1 + assert ( + caplog.records[0].message + == f"Tembo base path of {base_path} does not exist - exiting" + ) + assert caplog.records[0].levelname == "CRITICAL"