diff --git a/pyproject.toml b/pyproject.toml index 9618c75..86b7563 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dynamic = ["version"] [project.optional-dependencies] jinja = ["Jinja2>=3.1.2"] -markdown = ["anchovy[jinja]", "commonmark>=0.9.1"] +markdown = ["anchovy[jinja]", "markdown_it_py>=3.0.0"] css = ["tinycss2>=1.1.1"] pretty = ["rich>=12.5.1"] pillow = ["Pillow>=9.2.0"] diff --git a/src/anchovy/__init__.py b/src/anchovy/__init__.py index dc48e05..2b717ba 100644 --- a/src/anchovy/__init__.py +++ b/src/anchovy/__init__.py @@ -1,6 +1,12 @@ from .core import Context, InputBuildSettings, Matcher, PathCalc, Rule, Step from .css import AnchovyCSSStep -from .dependencies import Dependency, import_install_check, which_install_check +from .dependencies import ( + Dependency, + import_install_check, + pip_dependency, + web_exec_dependency, + which_install_check, +) from .images import CWebPStep, ImageMagickStep, IMThumbnailStep, PillowStep, OptipngStep from .jinja import JinjaMarkdownStep, JinjaRenderStep from .paths import DirPathCalc, OutputDirPathCalc, REMatcher, WorkingDirPathCalc diff --git a/src/anchovy/cli.py b/src/anchovy/cli.py index f811a2c..da4ce69 100644 --- a/src/anchovy/cli.py +++ b/src/anchovy/cli.py @@ -104,7 +104,7 @@ def pprint_step(step: t.Type[Step]): Prettily display dependency information for the given Step class. """ missing = [ - d.name for d in step.get_dependencies() + str(d) for d in step.get_dependencies() if d.needed and not d.satisfied ] diff --git a/src/anchovy/dependencies.py b/src/anchovy/dependencies.py index 429f84d..9eec22c 100644 --- a/src/anchovy/dependencies.py +++ b/src/anchovy/dependencies.py @@ -28,7 +28,7 @@ def import_install_check(dependency: Dependency): An install checker which tries to import a Python module. """ try: - importlib.import_module(dependency.name) + importlib.import_module(dependency.check_name) except ImportError: return False return True @@ -38,7 +38,22 @@ def which_install_check(dependency: Dependency): """ An install checker using `shutil.which()` to look for an executable. """ - return bool(shutil.which(dependency.name)) + return bool(shutil.which(dependency.check_name)) + + +def pip_dependency(name: str, source: str | None = None, check_name: str | None = None): + """ + A shortcut function for creating typical pip-based Dependencys. + """ + return Dependency(name, 'pip', import_install_check, source, check_name) + + +def web_exec_dependency(name: str, source: str | None = None, check_name: str | None = None): + """ + A shortcut function for creating typical Dependencys for general + internet-sourced executables. + """ + return Dependency(name, 'web', which_install_check, source, check_name) class Dependency: @@ -49,11 +64,13 @@ def __init__(self, name: str, type: str, install_check: t.Callable[[Dependency], bool], - source: str | None = None): + source: str | None = None, + check_name: str | None = None): self.name = name self.type = type self.install_check = install_check self.source = source or name + self.check_name = check_name or name @property def satisfied(self): @@ -79,6 +96,9 @@ def install_hint(self): def __repr__(self): return f'Dependency(name={self.name}, needed={self.needed}, satisfied={self.satisfied})' + def __str__(self): + return self.name + def __or__(self, other: Dependency): return _OrDependency(self, other) @@ -94,6 +114,9 @@ def __init__(self, left: Dependency, right: Dependency): def __repr__(self): return f'{self.left} | {self.right}' + def __str__(self): + return repr(self) + @property def satisfied(self): """ @@ -129,6 +152,9 @@ def __init__(self, left: Dependency, right: Dependency): def __repr__(self): return f'{self.left} & {self.right}' + def __str__(self): + return repr(self) + @property def satisfied(self): """ diff --git a/src/anchovy/images.py b/src/anchovy/images.py index a029667..3521c57 100644 --- a/src/anchovy/images.py +++ b/src/anchovy/images.py @@ -5,7 +5,7 @@ from pathlib import Path from .core import Step -from .dependencies import Dependency, import_install_check, which_install_check +from .dependencies import pip_dependency, web_exec_dependency from .simple import BaseCommandStep if t.TYPE_CHECKING: @@ -33,14 +33,9 @@ def __init__(self, self.options.extend(options) @classmethod - def get_dependencies(cls) -> set[Dependency]: + def get_dependencies(cls): return super().get_dependencies() | { - Dependency( - 'cwebp', - 'web', - which_install_check, - 'https://developers.google.com/speed/webp/download' - ), + web_exec_dependency('cwebp', 'https://developers.google.com/speed/webp/download'), } def get_command(self, input_path: Path, output_path: Path) -> list[StrOrBytesPath]: @@ -60,13 +55,12 @@ def __init__(self, options: t.Iterable[str] = ()): self.options = options @classmethod - def get_dependencies(cls) -> set[Dependency]: + def get_dependencies(cls): return super().get_dependencies() | { - Dependency( - 'magick', - 'web', - which_install_check, - 'https://imagemagick.org/script/download.php' + web_exec_dependency( + 'imagemagick', + 'https://imagemagick.org/script/download.php', + 'magick' ), } @@ -109,13 +103,11 @@ def __init__(self, thumbnail: tuple[int, int] | None = None): self.thumbnail = thumbnail @classmethod - def get_dependencies(cls) -> set[Dependency]: + def get_dependencies(cls): return super().get_dependencies() | { - Dependency( - 'PIL', - 'pip', - import_install_check, - 'Pillow' + pip_dependency( + 'Pillow', + check_name='PIL' ), } @@ -148,9 +140,9 @@ def __init__(self, self.options.extend(extra_options) @classmethod - def get_dependencies(cls) -> set[Dependency]: + def get_dependencies(cls): return super().get_dependencies() | { - Dependency('optipng', 'web', which_install_check, 'http://optipng.sourceforge.net'), + web_exec_dependency('optipng', 'http://optipng.sourceforge.net'), } def get_command(self, input_path: Path, output_path: Path) -> list[StrOrBytesPath]: diff --git a/src/anchovy/jinja.py b/src/anchovy/jinja.py index 4a4e1df..4f33136 100644 --- a/src/anchovy/jinja.py +++ b/src/anchovy/jinja.py @@ -2,10 +2,11 @@ import shutil import typing as t +from functools import reduce from pathlib import Path from .core import Context, Step -from .dependencies import Dependency, import_install_check +from .dependencies import pip_dependency, Dependency if t.TYPE_CHECKING: import commonmark @@ -13,6 +14,9 @@ from jinja2 import Environment +MDProcessor = t.Callable[[str], str] + + class JinjaRenderStep(Step): """ Abstract base class for Steps using Jinja rendering. @@ -20,9 +24,9 @@ class JinjaRenderStep(Step): env: Environment @classmethod - def get_dependencies(cls) -> set[Dependency]: + def get_dependencies(cls): return super().get_dependencies() | { - Dependency('jinja2', 'pip', import_install_check), + pip_dependency('jinja2'), } def __init__(self, @@ -73,35 +77,64 @@ class JinjaMarkdownStep(JinjaRenderStep): encoding = 'utf-8' @classmethod - def get_dependencies(cls) -> set[Dependency]: - return super().get_dependencies() | { - Dependency('commonmark', 'pip', import_install_check), - } + def _build_markdownit(cls): + import markdown_it + processor = markdown_it.MarkdownIt() + + def convert(s: str) -> str: + return processor.render(s) + + return convert + + @classmethod + def _build_commonmark(cls): + import commonmark + parser = commonmark.Parser() + renderer = commonmark.HtmlRenderer() + + def convert(s: str) -> str: + return renderer.render(parser.parse(s)) + + return convert + + @classmethod + def get_options(cls): + return [ + (pip_dependency('markdown-it-py', None, 'markdown_it'), cls._build_markdownit), + (pip_dependency('commonmark'), cls._build_commonmark), + ] + + @classmethod + def get_dependencies(cls): + deps = [option[0] for option in cls.get_options()] + dep_set = {reduce(lambda x, y: x | y, deps)} if deps else set[Dependency]() + + return super().get_dependencies() | dep_set def __init__(self, default_template: str | None = None, - md_parser: commonmark.Parser | None = None, - md_renderer: commonmark.render.renderer.Renderer | None = None, + md_processor: MDProcessor | None = None, jinja_env: Environment | None = None, jinja_globals: dict[str, t.Any] | None = None): super().__init__(jinja_env, jinja_globals) self.default_template = default_template + self._md_processor = md_processor + + @property + def md_processor(self): + if not self._md_processor: + for dep, factory in self.get_options(): + if dep.satisfied: + self._md_processor = factory() + break + else: + raise RuntimeError('Markdown processor could not be initialized!') + return self._md_processor - if md_parser: - self.md_parser = md_parser - else: - import commonmark - self.md_parser = commonmark.Parser() - if md_renderer: - self.md_renderer = md_renderer - else: - import commonmark - self.md_renderer = commonmark.HtmlRenderer() def __call__(self, path: Path, output_paths: list[Path]): meta, content = self.extract_metadata(path.read_text(self.encoding)) - ast = self.md_parser.parse(content.strip()) - meta |= {'rendered_markdown': self.md_renderer.render(ast).strip()} + meta |= {'rendered_markdown': self.md_processor(content.strip()).strip()} self.render_template( meta.get('template', self.default_template),