From 494caaa7cef75df0a6a4c321937f5bcffe8ae239 Mon Sep 17 00:00:00 2001 From: Cody Scott Date: Sun, 4 Feb 2024 21:38:01 -0500 Subject: [PATCH] Add templates folder to config and improve error handling --- htmd/cli.py | 27 +++++++++-------- htmd/example_site/config.toml | 7 +++-- htmd/site.py | 44 ++++++++++++++++----------- tests/test_build.py | 56 +++++++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 32 deletions(-) diff --git a/htmd/cli.py b/htmd/cli.py index 23bd906..1ef711b 100644 --- a/htmd/cli.py +++ b/htmd/cli.py @@ -149,18 +149,21 @@ def build( from . import site app = site.app - if css_minify: - assert app.static_folder is not None - combine_and_minify_css(Path(app.static_folder)) - - if js_minify: - assert app.static_folder is not None - combine_and_minify_js(Path(app.static_folder)) - - if css_minify or js_minify: - # reload to set app.config['INCLUDE_CSS'] and app.config['INCLUDE_JS'] - # setting them here doesn't work - importlib.reload(site) + assert app.static_folder is not None + static_path = Path(app.static_folder) + if static_path.is_dir(): + if css_minify: + assert app.static_folder is not None + combine_and_minify_css(static_path) + + if js_minify: + assert app.static_folder is not None + combine_and_minify_js(static_path) + + if css_minify or js_minify: + # reload to set app.config['INCLUDE_CSS'] and app.config['INCLUDE_JS'] + # setting them here doesn't work + importlib.reload(site) set_posts_datetime(site.app, site.posts) diff --git a/htmd/example_site/config.toml b/htmd/example_site/config.toml index b27f28a..cbd35a4 100644 --- a/htmd/example_site/config.toml +++ b/htmd/example_site/config.toml @@ -13,10 +13,11 @@ url = "" # Where to look for files [folders] -static = "static" -posts = "posts" -pages = "pages" build = "build" +pages = "pages" +posts = "posts" +static = "static" +templates = "templates" [posts] extension = ".md" diff --git a/htmd/site.py b/htmd/site.py index 41e99f5..37b9fb5 100644 --- a/htmd/site.py +++ b/htmd/site.py @@ -38,13 +38,6 @@ def get_project_dir() -> Path: project_dir = get_project_dir() -app = Flask( - __name__, - static_folder=project_dir / 'static', - template_folder=this_dir / 'example_site' / 'templates', -) - - try: with (project_dir / 'config.toml').open('rb') as config_file: htmd_config = tomllib.load(config_file) @@ -64,24 +57,34 @@ def get_project_dir() -> Path: 'SITE_TWITTER': ('site', 'twitter', ''), 'SITE_FACEBOOK': ('site', 'facebook', ''), 'FACEBOOK_APP_ID': ('site', 'facebook_app_id', ''), - 'STATIC_FOLDER': ('folders', 'static', 'static'), - 'POSTS_FOLDER': ('folders', 'posts', 'posts'), - 'PAGES_FOLDER': ('folders', 'pages', 'pages'), + 'BUILD_FOLDER': ('folders', 'build', 'build'), + 'PAGES_FOLDER': ('folders', 'pages', 'pages'), + 'POSTS_FOLDER': ('folders', 'posts', 'posts'), + 'STATIC_FOLDER': ('folders', 'static', 'static'), + 'TEMPLATE_FOLDER': ('folders', 'templates', 'templates'), + 'POSTS_EXTENSION': ('posts', 'extension', '.md'), + 'PRETTY_HTML': ('html', 'pretty', False), 'MINIFY_HTML': ('html', 'minify', False), + 'SHOW_AUTHOR': ('author', 'show', True), 'DEFAULT_AUTHOR': ('author', 'default_name', ''), 'DEFAULT_AUTHOR_TWITTER': ('author', 'default_twitter', ''), 'DEFAULT_AUTHOR_FACEBOOK': ('author', 'default_facebook', ''), } +app = Flask( + __name__, + # default templates + template_folder=this_dir / 'example_site' / 'templates', +) # Update app.config using the configuration keys for flask_key, (table, key, default) in config_keys.items(): app.config[flask_key] = htmd_config.get(table, {}).get(key, default) +app.static_folder = project_dir / app.config['STATIC_FOLDER'] assert app.static_folder is not None - # To avoid full paths in config.toml app.config['FLATPAGES_ROOT'] = ( project_dir / app.config['POSTS_FOLDER'] @@ -92,8 +95,9 @@ def get_project_dir() -> Path: app.config['FREEZER_REMOVE_EXTRA_FILES'] = False app.config['FLATPAGES_EXTENSION'] = app.config['POSTS_EXTENSION'] -app.config['INCLUDE_CSS'] = 'combined.min.css' in os.listdir(app.static_folder) -app.config['INCLUDE_JS'] = 'combined.min.js' in os.listdir(app.static_folder) +if Path(app.static_folder).is_dir(): + app.config['INCLUDE_CSS'] = 'combined.min.css' in os.listdir(app.static_folder) + app.config['INCLUDE_JS'] = 'combined.min.js' in os.listdir(app.static_folder) posts = FlatPages(app) @@ -122,7 +126,7 @@ def truncate_post_html(post_html: str) -> str: # Include current htmd site templates app.jinja_loader = ChoiceLoader([ # type: ignore[assignment] - FileSystemLoader(project_dir / 'templates/'), + FileSystemLoader(project_dir / app.config['TEMPLATE_FOLDER']), app.jinja_loader, # type: ignore[list-item] ]) @@ -167,7 +171,10 @@ def format_html(response: Response) -> ResponseReturnValue: def page(path: str) -> ResponseReturnValue: # ensure page is from pages directory # otherwise this will load any templates in the template folder - for page_path in (project_dir / 'pages').iterdir(): + pages_folder = project_dir / app.config['PAGES_FOLDER'] + if not pages_folder.is_dir(): + abort(404) + for page_path in pages_folder.iterdir(): if path == page_path.stem: break else: @@ -436,8 +443,11 @@ def draft() -> Iterator[dict]: # noqa: F811 @freezer.register_generator # type: ignore[no-redef] def page() -> Iterator[str]: # noqa: F811 - for page in (project_dir / 'pages').iterdir(): + pages_folder = project_dir / app.config['PAGES_FOLDER'] + if not pages_folder.is_dir(): + return + for page in pages_folder.iterdir(): # Need to create for pages.page # Since this route is in a different Blueprint - # URL works + # Using the URL works yield f'/{page.stem}/' diff --git a/tests/test_build.py b/tests/test_build.py index b83fc24..cf66fc9 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -178,3 +178,59 @@ def test_build_page_without_link(run_start: CliRunner) -> None: assert result.exit_code == 0 with (Path('build') / 'new' / 'index.html').open('r') as page_file: assert 'Totally new' in page_file.read() + + +def test_build_empty_directory() -> None: + expected = 'Can not find config.toml\n' + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(build) + assert result.exit_code == 1 + assert result.output == expected + + +def test_build_without_static(run_start: CliRunner) -> None: + path = Path('static') + shutil.rmtree(path) + result = run_start.invoke(build) + assert result.exit_code == 0 + assert re.search(SUCCESS_REGEX, result.output) + + +def test_build_without_posts(run_start: CliRunner) -> None: + path = Path('posts') + shutil.rmtree(path) + result = run_start.invoke(build) + assert result.exit_code == 0 + assert re.search(SUCCESS_REGEX, result.output) + + +def test_build_without_pages(run_start: CliRunner) -> None: + path = Path('pages') + shutil.rmtree(path) + + result = run_start.invoke(build) + assert result.exit_code == 1 + assert "Unexpected status '404 NOT FOUND' on URL /about/" in result.output + + # Remove link from _layout.html + layout_path = Path('templates') / '_layout.html' + with layout_path.open('r') as layout_file: + lines = layout_file.readlines() + with layout_path.open('w') as layout_file: + for line in lines: + if 'about' in line: + continue + layout_file.write(line) + + result = run_start.invoke(build) + assert result.exit_code == 0 + assert re.search(SUCCESS_REGEX, result.output) + + +def test_build_without_templates(run_start: CliRunner) -> None: + path = Path('templates') + shutil.rmtree(path) + result = run_start.invoke(build) + assert result.exit_code == 0 + assert re.search(SUCCESS_REGEX, result.output)