From 3c0691df1b7ff1fd04e22ab6055e84cc2137504e Mon Sep 17 00:00:00 2001 From: David Luevano Alvarado Date: Sun, 17 Apr 2022 02:13:36 -0600 Subject: refactor config handler and more Refactored the configuration handling to use configparser; also the argument parser; generally added more 'logging'; updated template resources; and more minor things --- src/pyssg/arg_parser.py | 50 ++++++++------- src/pyssg/builder.py | 32 +++++----- src/pyssg/configuration.py | 151 +++++++++------------------------------------ src/pyssg/page.py | 34 +++++----- src/pyssg/parser.py | 18 +++--- src/pyssg/plt/default.ini | 22 ++++--- src/pyssg/plt/index.html | 6 +- src/pyssg/plt/page.html | 4 +- src/pyssg/plt/rss.xml | 18 +++--- src/pyssg/plt/sitemap.xml | 2 +- src/pyssg/plt/tag.html | 4 +- src/pyssg/pyssg.py | 85 +++++++++++++------------ src/pyssg/utils.py | 28 +++++++++ 13 files changed, 204 insertions(+), 250 deletions(-) create mode 100644 src/pyssg/utils.py (limited to 'src') diff --git a/src/pyssg/arg_parser.py b/src/pyssg/arg_parser.py index 4ee7d57..90fb8c1 100644 --- a/src/pyssg/arg_parser.py +++ b/src/pyssg/arg_parser.py @@ -3,21 +3,40 @@ from argparse import ArgumentParser, Namespace def get_parsed_arguments() -> Namespace: parser = ArgumentParser(prog='pyssg', - description='''Static Site Generator that reads - Markdown files and creates HTML files.\nIf - [-c]onfig file is provided (or exists in default - location) all other options are ignored.\nFor - datetime formats see: + description='''Static Site Generator that parses + Markdown files into HTML files. For datetime + formats see: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes''') parser.add_argument('-v', '--version', action='store_true', help='''print program version''') parser.add_argument('-c', '--config', - default='$XDG_CONFIG_HOME/pyssg/pyssgrc', + # don't give a default here, as it would seem like + # --config was passed + # default='$XDG_CONFIG_HOME/pyssg/config.ini', type=str, - help='''config file (path) to read from; defaults to - 'pyssgrc' first, then - '$XDG_CONFIG_HOME/pyssg/pyssgrc' ''') + help='''config file (path) to read from; if not passed, + '$XDG_CONFIG_HOME/pyssg/config.ini' is used''') + parser.add_argument('--copy-default-config', + action='store_true', + help='''copies the default config to path specified in + --config flag''') + parser.add_argument('-i', '--init', + action='store_true', + help='''initializes the directory structures and copies + over default templates''') + parser.add_argument('-b', '--build', + action='store_true', + help='''generates all HTML files by parsing MD files + present in source directory and copies over manually + written HTML files''') + parser.add_argument('-f', '--force', + action='store_true', + help='''force building all pages and not only the + updated ones''') + # really not needed, too much bloat and case scenarios to check for, + # instead, just read from config file or default config file + """ parser.add_argument('-s', '--src', default='src', type=str, @@ -68,17 +87,6 @@ def get_parsed_arguments() -> Namespace: help='''date format used for the separator between page entries in a list; defaults to '%%B %%Y' ('March 2021', for example)''') - parser.add_argument('-i', '--init', - action='store_true', - help='''initializes the dir structure, templates, - as well as the 'src' and 'dst' directories''') - parser.add_argument('-b', '--build', - action='store_true', - help='''generates all html files and passes over - existing (handmade) ones''') - parser.add_argument('-f', '--force', - action='store_true', - help='''force building all pages and not only the - updated ones''') + """ return parser.parse_args() diff --git a/src/pyssg/builder.py b/src/pyssg/builder.py index 84494da..130062e 100644 --- a/src/pyssg/builder.py +++ b/src/pyssg/builder.py @@ -4,8 +4,8 @@ from copy import deepcopy from operator import itemgetter from jinja2 import Environment, Template from markdown import Markdown +from configparser import ConfigParser -from .configuration import Configuration from .database import Database from .parser import MDParser from .page import Page @@ -13,11 +13,11 @@ from .discovery import get_file_list, get_dir_structure class Builder: - def __init__(self, config: Configuration, + def __init__(self, config: ConfigParser, env: Environment, db: Database, md: Markdown): - self.config: Configuration = config + self.config: ConfigParser = config self.env: Environment = env self.db: Database = db self.md: Markdown = md @@ -33,15 +33,19 @@ class Builder: def build(self) -> None: - self.dirs = get_dir_structure(self.config.src, ['templates']) - self.md_files = get_file_list(self.config.src, ['.md'], ['templates']) - self.html_files = get_file_list(self.config.src, ['.html'], ['templates']) + self.dirs = get_dir_structure(self.config.get('path', 'src'), + ['templates']) + self.md_files = get_file_list(self.config.get('path', 'src'), + ['.md'], + ['templates']) + self.html_files = get_file_list(self.config.get('path', 'src'), + ['.html'], + ['templates']) self.__create_dir_structure() self.__copy_html_files() - parser: MDParser = MDParser(self.config.src, - self.md_files, + parser: MDParser = MDParser(self.md_files, self.config, self.db, self.md) @@ -69,7 +73,7 @@ class Builder: # for the dir structure, # doesn't matter if the dir already exists try: - os.makedirs(os.path.join(self.config.dst, d)) + os.makedirs(os.path.join(self.config.get('path', 'dst'), d)) except FileExistsError: pass @@ -79,18 +83,18 @@ class Builder: dst_file: str = None for f in self.html_files: - src_file = os.path.join(self.config.src, f) - dst_file = os.path.join(self.config.dst, f) + src_file = os.path.join(self.config.get('path', 'src'), f) + dst_file = os.path.join(self.config.get('path', 'dst'), f) # only copy files if they have been modified (or are new) - if self.db.update(src_file, remove=f'{self.config.src}/'): + if self.db.update(src_file, remove=f'{self.config.get("path", "src")}/'): shutil.copy2(src_file, dst_file) def __render_articles(self) -> None: article_vars: dict = deepcopy(self.common_vars) # check if only updated should be created - if self.config.force: + if self.config.getboolean('other', 'force'): for p in self.all_pages: article_vars['page'] = p self.__render_template("page.html", @@ -132,5 +136,5 @@ class Builder: template: Template = self.env.get_template(template_name) content: str = template.render(**template_vars) - with open(os.path.join(self.config.dst, file_name), 'w') as f: + with open(os.path.join(self.config.get('path', 'dst'), file_name), 'w') as f: f.write(content) diff --git a/src/pyssg/configuration.py b/src/pyssg/configuration.py index 5a09a7a..dd6bfaa 100644 --- a/src/pyssg/configuration.py +++ b/src/pyssg/configuration.py @@ -1,133 +1,42 @@ -from typing import Union +import sys from importlib.metadata import version +from importlib.resources import path as rpath from datetime import datetime, timezone +from configparser import ConfigParser -class Configuration: - def __init__(self, path: str): - self.path: str = path - # config file specific - self.src: str = None - self.dst: str = None - self.plt: str = None - self.url: str = None - self.static_url: str = None - self.default_image_url: str = None - self.title: str = None - self.dformat: str = None - self.l_dformat: str = None - self.lsep_dformat: str = None - self.force: bool = None +DEFAULT_CONFIG_PATH = '$XDG_CONFIG_HOME/pyssg/config.ini' +VERSION = version('pyssg') - # other - self.version: str = version('pyssg') - self.dformat_rss: str = '%a, %d %b %Y %H:%M:%S GMT' - self.dformat_sitemap: str = '%Y-%m-%d' - self.run_date_rss = datetime.now(tz=timezone.utc).strftime(self.dformat_rss) - self.run_date_sitemap = \ - datetime.now(tz=timezone.utc).strftime(self.dformat_sitemap) +def __check_well_formed_config(config: ConfigParser) -> None: + default_config: ConfigParser = ConfigParser() + with rpath('pyssg.plt', 'default.ini') as p: + default_config.read(p) - def read(self): - try: - lines: list[str] = None - with open(self.path, 'r') as f: - lines = f.readlines() + for section in default_config.sections(): + if not config.has_section(section): + print(f'config does not have section "{section}"') + sys.exit(1) + for option in default_config.options(section): + if not config.has_option(section, option): + print(f'config does not have option "{option}" in section "{section}"') + sys.exit(1) - opts: dict[str, Union[str, bool]] = dict() - for l in lines: - kv: list[str] = l.split('=', 1) - if len(kv) != 2: - raise Exception('wrong config syntax') - k: str = kv[0].strip().lower() - v_temp: str = kv[1].strip() - # check if value should be a boolean true - v: Union[str, bool] = v_temp\ - if v_temp.lower() not in ['true', '1', 'yes']\ - else True +def get_parsed_config(path: str) -> ConfigParser: + config: ConfigParser = ConfigParser() + config.read(path) - opts[k] = v + __check_well_formed_config(config) - try: - self.src = opts['src'] - except KeyError: pass + # set other required options + config.set('fmt', 'rss_date', '%%a, %%d %%b %%Y %%H:%%M:%%S GMT') + config.set('fmt', 'sitemap_date', '%%Y-%%m-%%d') + config.set('info', 'version', VERSION) + config.set('info', 'rss_run_date', datetime.now( + tz=timezone.utc).strftime(config.get('fmt', 'rss_date'))) + config.set('info', 'sitemap_run_date', datetime.now( + tz=timezone.utc).strftime(config.get('fmt', 'sitemap_date'))) - try: - self.dst = opts['dst'] - except KeyError: pass - - try: - self.plt = opts['plt'] - except KeyError: pass - - try: - self.url = opts['url'] - except KeyError: pass - - try: - self.static_url = opts['static_url'] - except KeyError: pass - - try: - self.default_image_url = opts['default_image_url'] - except KeyError: pass - - try: - self.title = opts['title'] - except KeyError: pass - - try: - self.dformat = opts['date_format'] - except KeyError: pass - - try: - self.l_dformat = opts['list_date_format'] - except KeyError: pass - - try: - self.lsep_dformat = opts['list_sep_date_format'] - except KeyError: pass - - try: - # if the parser above didn't read a boolean true, then take it - # as a false anyways - self.force = opts['force'] if opts['force'] is True else False - except KeyError: pass - - except OSError: pass - - - def fill_missing(self, opts: dict[str, Union[str, bool]]) -> None: - if self.src is None: - self.src = opts['src'] - - if self.dst is None: - self.dst = opts['dst'] - - if self.plt is None: - self.plt = opts['plt'] - - if self.url is None: - self.url = opts['url'] - - if self.static_url is None: - self.static_url = opts['static_url'] - - if self.default_image_url is None: - self.default_image_url = opts['default_image_url'] - - if self.title is None: - self.title = opts['title'] - - if self.dformat is None: - self.dformat = opts['date_format'] - - if self.l_dformat is None: - self.l_dformat = opts['list_date_format'] - - if self.lsep_dformat is None: - self.lsep_dformat = opts['list_sep_date_format'] - - if self.force is None: - self.force = opts['force'] + return config diff --git a/src/pyssg/page.py b/src/pyssg/page.py index 6b83d39..784749c 100644 --- a/src/pyssg/page.py +++ b/src/pyssg/page.py @@ -1,6 +1,6 @@ from datetime import datetime, timezone -from .configuration import Configuration +from configparser import ConfigParser class Page: @@ -10,14 +10,14 @@ class Page: mtime: float, html: str, meta: dict, - config: Configuration): + config: ConfigParser): # initial data self.name: str = name self.ctimestamp: float = ctime self.mtimestamp: float = mtime self.content: str = html self.meta: dict = meta - self.config: Configuration = config + self.config: ConfigParser = config # data from self.meta self.title: str = '' @@ -66,23 +66,23 @@ class Page: # dates self.cdatetime = datetime.fromtimestamp(self.ctimestamp, tz=timezone.utc) - self.cdate = self.cdatetime.strftime(self.config.dformat) - self.cdate_list = self.cdatetime.strftime(self.config.l_dformat) - self.cdate_list_sep = self.cdatetime.strftime(self.config.lsep_dformat) - self.cdate_rss = self.cdatetime.strftime(self.config.dformat_rss) + self.cdate = self.cdatetime.strftime(self.config.get('fmt', 'date')) + self.cdate_list = self.cdatetime.strftime(self.config.get('fmt', 'list_date')) + self.cdate_list_sep = self.cdatetime.strftime(self.config.get('fmt', 'list_sep_date')) + self.cdate_rss = self.cdatetime.strftime(self.config.get('fmt', 'rss_date')) self.cdate_sitemap = \ - self.cdatetime.strftime(self.config.dformat_sitemap) + self.cdatetime.strftime(self.config.get('fmt', 'sitemap_date')) # only if file/page has been modified if self.mtimestamp != 0.0: self.mdatetime = datetime.fromtimestamp(self.mtimestamp, tz=timezone.utc) - self.mdate = self.mdatetime.strftime(self.config.dformat) - self.mdate_list = self.mdatetime.strftime(self.config.l_dformat) - self.mdate_list_sep = self.mdatetime.strftime(self.config.lsep_dformat) - self.mdate_rss = self.mdatetime.strftime(self.config.dformat_rss) + self.mdate = self.mdatetime.strftime(self.config.get('fmt', 'date')) + self.mdate_list = self.mdatetime.strftime(self.config.get('fmt', 'list_date')) + self.mdate_list_sep = self.mdatetime.strftime(self.config.get('fmt', 'list_sep_date')) + self.mdate_rss = self.mdatetime.strftime(self.config.get('fmt', 'rss_date')) self.mdate_sitemap = \ - self.mdatetime.strftime(self.config.dformat_sitemap) + self.mdatetime.strftime(self.config.get('fmt', 'sitemap_date')) # not always contains tags try: @@ -91,17 +91,17 @@ class Page: for t in tags_only: self.tags.append((t, - f'{self.config.url}/tag/@{t}.html')) + f'{self.config.get("url", "main")}/tag/@{t}.html')) except KeyError: pass - self.url = f'{self.config.url}/{self.name.replace(".md", ".html")}' + self.url = f'{self.config.get("url", "main")}/{self.name.replace(".md", ".html")}' try: self.image_url = \ - f'{self.config.static_url}/{self.meta["image_url"][0]}' + f'{self.config.get("url", "static")}/{self.meta["image_url"][0]}' except KeyError: self.image_url = \ - f'{self.config.static_url}/{self.config.default_image_url}' + f'{self.config.get("url", "static")}/{self.config.get("url", "default_image")}' # if contains open graph elements try: diff --git a/src/pyssg/parser.py b/src/pyssg/parser.py index f2d23eb..2888fcb 100644 --- a/src/pyssg/parser.py +++ b/src/pyssg/parser.py @@ -1,24 +1,21 @@ import os from operator import itemgetter -from datetime import datetime from markdown import Markdown +from configparser import ConfigParser from .database import Database -from .configuration import Configuration from .page import Page # parser of md files, stores list of pages and tags class MDParser: - def __init__(self, src: str, - files: list[str], - config: Configuration, + def __init__(self, files: list[str], + config: ConfigParser, db: Database, md: Markdown): - self.src: str = src self.files: list[str] = files - self.config: Configuration = config + self.config: ConfigParser = config self.db: Database = db self.md: Markdown = md @@ -32,12 +29,13 @@ class MDParser: self.all_pages = [] self.updated_pages = [] self.all_tags = [] - all_tag_names: list[str] = [] + # not used, not sure why i had this + # all_tag_names: list[str] = [] for f in self.files: - src_file: str = os.path.join(self.src, f) + src_file: str = os.path.join(self.config.get('path', 'src'), f) # get flag if update is successful - updated: bool = self.db.update(src_file, remove=f'{self.src}/') + updated: bool = self.db.update(src_file, remove=f'{self.config.get("path", "src")}/') content: str = self.md.reset().convert(open(src_file).read()) page: Page = Page(f, diff --git a/src/pyssg/plt/default.ini b/src/pyssg/plt/default.ini index 2700d28..ab4eac1 100644 --- a/src/pyssg/plt/default.ini +++ b/src/pyssg/plt/default.ini @@ -1,14 +1,16 @@ -[dir_paths] +[path] src=src dst=dst plt=plt -[urls] -url=https://example.com -static_url=https://static.example.com -default_image_url=/images/default.png -[formats] -date_format=%%a, %%b %%d, %%Y @ %%H:%%M %%Z -list_date_format=%%b %%d -list_sep_date_format=%%B %%Y +[url] +main=https://example.com +static=https://static.example.com +default_image=/images/default.png +[fmt] +date=%%a, %%b %%d, %%Y @ %%H:%%M %%Z +list_date=%%b %%d +list_sep_date=%%B %%Y [info] -title=Example site \ No newline at end of file +title=Example site +[other] +force=False \ No newline at end of file diff --git a/src/pyssg/plt/index.html b/src/pyssg/plt/index.html index e06efdb..09ca786 100644 --- a/src/pyssg/plt/index.html +++ b/src/pyssg/plt/index.html @@ -2,11 +2,11 @@ - - Index -- {{config.title}} + + Index -- {{config.get('info', 'title')}} -

Index -- {{config.title}}

+

Index -- {{config.get('info', 'title')}}

Some text here.

Tags: diff --git a/src/pyssg/plt/page.html b/src/pyssg/plt/page.html index 2fc3943..15663fa 100644 --- a/src/pyssg/plt/page.html +++ b/src/pyssg/plt/page.html @@ -2,8 +2,8 @@ - - {{page.title}} -- {{config.title}} + + {{page.title}} -- {{config.get('info', 'title')}}

{{page.title}}

diff --git a/src/pyssg/plt/rss.xml b/src/pyssg/plt/rss.xml index 42020d7..be6ddf0 100644 --- a/src/pyssg/plt/rss.xml +++ b/src/pyssg/plt/rss.xml @@ -3,24 +3,24 @@ xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"> - {{config.title}} - {{config.url}} - + {{config.get('info', 'title')}} + {{config.get('url', 'main')}} + Short site description. en-us Blog Copyright 2021 Somebody some@one.com (Sombody) some@one.com (Sombody) - {{config.run_date_rss}} - {{run_date_rss}} - pyssg v{{config.version}} + {{config.get('info', 'rss_run_date')}} + {{config.get('info', 'rss_run_date')}} + pyssg v{{config.get('info', 'version')}} https://validator.w3.org/feed/docs/rss2.html 30 - {{config.static_url}}/images/blog.png - {{config.title}} - {{config.url}} + {{config.get('url', 'static')}}/images/blog.png + {{config.get('info', 'title')}} + {{config.get('url', 'main')}} {%for p in all_pages%} diff --git a/src/pyssg/plt/sitemap.xml b/src/pyssg/plt/sitemap.xml index 26ee5c1..af1212a 100644 --- a/src/pyssg/plt/sitemap.xml +++ b/src/pyssg/plt/sitemap.xml @@ -14,7 +14,7 @@ {%for t in all_tags%} {{t[1]}} - {{config.run_date_sitemap}} + {{config.get('info', 'sitemap_run_date')}} daily 0.5 diff --git a/src/pyssg/plt/tag.html b/src/pyssg/plt/tag.html index d856ce4..ffd1956 100644 --- a/src/pyssg/plt/tag.html +++ b/src/pyssg/plt/tag.html @@ -2,8 +2,8 @@ - - Posts filtered by {{tag[0]}} -- {{config.title}} + + Posts filtered by {{tag[0]}} -- {{config.get('info', 'title')}}

Posts filtered by {{tag[0]}}

diff --git a/src/pyssg/pyssg.py b/src/pyssg/pyssg.py index e694565..9b82231 100644 --- a/src/pyssg/pyssg.py +++ b/src/pyssg/pyssg.py @@ -1,7 +1,8 @@ import os -import shutil -from importlib.resources import path +import sys +from importlib.resources import path as rpath from typing import Union +from configparser import ConfigParser from jinja2 import Environment, FileSystemLoader from markdown import Markdown @@ -9,61 +10,65 @@ from yafg import YafgExtension from MarkdownHighlight.highlight import HighlightExtension from markdown_checklist.extension import ChecklistExtension +from .utils import create_dir, copy_file, sanity_check_path from .arg_parser import get_parsed_arguments -from .configuration import Configuration +from .configuration import get_parsed_config, DEFAULT_CONFIG_PATH, VERSION from .database import Database from .builder import Builder def main() -> None: - opts: dict[str, Union[str, bool]] = vars(get_parsed_arguments()) - conf_path: str = opts['config'] - conf_path = os.path.expandvars(conf_path) - - - config: Configuration = None - if os.path.exists('pyssgrc'): - config = Configuration('pyssgrc') - else: - config = Configuration(conf_path) - - config.read() - config.fill_missing(opts) - - if opts['version']: - print(f'pyssg v{config.version}') - return - - if opts['init']: - try: - os.mkdir(config.src) - os.makedirs(os.path.join(config.dst, 'tag')) - os.mkdir(config.plt) - except FileExistsError: - pass - - # copy basic template files + args: dict[str, Union[str, bool]] = vars(get_parsed_arguments()) + if not len(sys.argv) > 1: + print(f'pyssg v{VERSION} - no arguments passed, --help for more') + sys.exit(0) + + if args['version']: + print(f'pyssg v{VERSION}') + sys.exit(0) + + config_path: str = args['config'] if args['config'] else DEFAULT_CONFIG_PATH + config_path = os.path.normpath(os.path.expandvars(config_path)) + sanity_check_path(config_path) + config_dir, _ = os.path.split(config_path) + + if args['copy_default_config']: + create_dir(config_dir) + with rpath('pyssg.plt', 'default.ini') as p: + copy_file(p, config_path) + sys.exit(0) + + if not os.path.exists(config_path): + print(f'''config file does't exist in path "{config_path}"; make sure + the path is correct; use --copy-default-config to if you + haven't already''') + sys.exit(1) + + config: ConfigParser = get_parsed_config(config_path) + + if args['init']: + create_dir(config.get('path', 'src')) + create_dir(os.path.join(config.get('path', 'dst'), 'tag'), True) + create_dir(config.get('path', 'plt')) files: list[str] = ('index.html', 'page.html', 'tag.html', 'rss.xml', 'sitemap.xml') for f in files: - plt_file: str = os.path.join(config.plt, f) - with path('pyssg.plt', f) as p: - if not os.path.exists(plt_file): - shutil.copy(p, plt_file) - - return + plt_file: str = os.path.join(config.get('path', 'plt'), f) + with rpath('pyssg.plt', f) as p: + copy_file(p, plt_file) + sys.exit(0) - if opts['build']: + if args['build']: # start the db - db: Database = Database(os.path.join(config.src, '.files')) + db: Database = Database(os.path.join(config.get('path', 'src'), '.files')) db.read() # the autoescape option could be a security risk if used in a dynamic # website, as far as i can tell - env: Environment = Environment(loader=FileSystemLoader(config.plt), + env: Environment = Environment(loader=FileSystemLoader(config.get('path', 'plt')), autoescape=False, trim_blocks=True, lstrip_blocks=True) @@ -92,4 +97,4 @@ def main() -> None: builder.build() db.write() - return + sys.exit(0) diff --git a/src/pyssg/utils.py b/src/pyssg/utils.py new file mode 100644 index 0000000..8e5d90e --- /dev/null +++ b/src/pyssg/utils.py @@ -0,0 +1,28 @@ +import os +import sys +import shutil + + +def create_dir(path: str, p: bool=False) -> None: + try: + if p: + os.makedirs(path) + else: + os.mkdir(path) + print(f'created directory "{path}"') + except FileExistsError: + print(f'directory "{path}" already exists') + + +def copy_file(src: str, dst: str) -> None: + if not os.path.exists(dst): + shutil.copy(src, dst) + print(f'copied file "{src}" to "{dst}"') + else: + print(f'"{dst}" already exists') + + +def sanity_check_path(path: str) -> None: + if '$' in path: + print(f'"$" character found in path: "{path}"; could be due to non-existant env var.') + sys.exit(1) -- cgit v1.2.3-70-g09d2