mirror of
https://github.com/dtomlinson91/tembo.git
synced 2025-12-22 04:15:44 +00:00
Merge branch 'refactor/optional_user_inputs' into develop
This commit is contained in:
27
TODO.todo
27
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: <https://stackoverflow.com/questions/53125305/testing-logging-output-with-pytest>
|
||||
document this
|
||||
@@ -25,17 +22,14 @@ Documentation:
|
||||
<https://stackoverflow.com/questions/53125305/testing-logging-output-with-pytest>
|
||||
|
||||
☐ 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
|
||||
<https://github.com/pawamoy/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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,3 +3,7 @@
|
||||
|
||||
class MismatchedTokenError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BasePathDoesNotExistError(Exception):
|
||||
pass
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user