diff --git a/python/zensical/__init__.py b/python/zensical/__init__.py index 8c78752..59bbcf7 100644 --- a/python/zensical/__init__.py +++ b/python/zensical/__init__.py @@ -21,8 +21,8 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. -from .zensical import * +from zensical.zensical import * # noqa: F403 -__doc__ = zensical.__doc__ -if hasattr(zensical, "__all__"): - __all__ = zensical.__all__ +__doc__ = zensical.__doc__ # noqa: F405 +if hasattr(zensical, "__all__"): # noqa: F405 + __all__ = zensical.__all__ # noqa: F405 diff --git a/python/zensical/config.py b/python/zensical/config.py index 884a729..8d5bf38 100644 --- a/python/zensical/config.py +++ b/python/zensical/config.py @@ -27,21 +27,22 @@ import hashlib import importlib import os import pickle +from typing import IO, Any +from urllib.parse import urlparse + import yaml +from click import ClickException +from deepmerge import always_merger +from yaml import BaseLoader, Loader, YAMLError +from yaml.constructor import ConstructorError + +from zensical.extensions.emoji import to_svg, twemoji try: import tomllib except ModuleNotFoundError: - import tomli as tomllib # type: ignore + import tomli as tomllib -from click import ClickException -from deepmerge import always_merger -from typing import Any, IO -from yaml import BaseLoader, Loader, YAMLError -from yaml.constructor import ConstructorError -from urllib.parse import urlparse - -from .extensions.emoji import to_svg, twemoji # ---------------------------------------------------------------------------- # Globals @@ -64,9 +65,7 @@ side, and use it directly when needed. It's a hack but will do for now. class ConfigurationError(ClickException): - """ - Configuration resolution or validation failed. - """ + """Configuration resolution or validation failed.""" # ---------------------------------------------------------------------------- @@ -75,22 +74,17 @@ class ConfigurationError(ClickException): def parse_config(path: str) -> dict: - """ - Parse configuration file. - """ + """Parse configuration file.""" # Decide by extension; no need to convert to Path _, ext = os.path.splitext(path) if ext.lower() == ".toml": return parse_zensical_config(path) - else: - return parse_mkdocs_config(path) + return parse_mkdocs_config(path) def parse_zensical_config(path: str) -> dict: - """ - Parse zensical.toml configuration file. - """ - global _CONFIG + """Parse zensical.toml configuration file.""" + global _CONFIG # noqa: PLW0603 with open(path, "rb") as f: config = tomllib.load(f) if "project" in config: @@ -102,11 +96,9 @@ def parse_zensical_config(path: str) -> dict: def parse_mkdocs_config(path: str) -> dict: - """ - Parse mkdocs.yml configuration file. - """ - global _CONFIG - with open(path, "r") as f: + """Parse mkdocs.yml configuration file.""" + global _CONFIG # noqa: PLW0603 + with open(path) as f: config = _yaml_load(f) # Apply defaults and return parsed configuration @@ -114,24 +106,19 @@ def parse_mkdocs_config(path: str) -> dict: return _CONFIG -def get_config(): - """ - Return configuration. - """ +def get_config() -> dict | None: + """Return configuration.""" return _CONFIG def get_theme_dir() -> str: - """ - Return the theme directory. - """ + """Return the theme directory.""" path = os.path.dirname(os.path.abspath(__file__)) return os.path.join(path, "templates") def _apply_defaults(config: dict, path: str) -> dict: - """ - Apply default settings in configuration. + """Apply default settings in configuration. Note that this is loosely based on the defaults that MkDocs sets in its own configuration system, which we won't port for compatibility right now, as @@ -473,9 +460,7 @@ def _apply_defaults(config: dict, path: str) -> dict: def set_default( entry: dict, key: str, default: Any, data_type: type | None = None ) -> Any: - """ - Set a key to a default value if it isn't set, and optionally cast it to the specified data type. - """ + """Set a key to a default value if it isn't set, and optionally cast it to the specified data type.""" if key in entry and entry[key] is None: del entry[key] @@ -487,24 +472,22 @@ def set_default( try: entry[key] = data_type(entry[key]) except (ValueError, TypeError) as e: - raise ValueError(f"Failed to cast key '{key}' to {data_type}: {e}") + raise ValueError( + f"Failed to cast key '{key}' to {data_type}: {e}" + ) from e # Return the resulting value return entry[key] def _hash(data: Any) -> int: - """ - Compute a hash for the given data. - """ - hash = hashlib.sha1(pickle.dumps(data)) + """Compute a hash for the given data.""" + hash = hashlib.sha1(pickle.dumps(data)) # noqa: S324 return int(hash.hexdigest(), 16) % (2**64) def _convert_extra(data: dict | list) -> dict | list: - """ - Recursively convert all None values in a dictionary or list to empty strings. - """ + """Recursively convert all None values in a dictionary or list to empty strings.""" if isinstance(data, dict): # Process each key-value pair in the dictionary return { @@ -513,7 +496,7 @@ def _convert_extra(data: dict | list) -> dict | list: else ("" if value is None else value) for key, value in data.items() } - elif isinstance(data, list): + if isinstance(data, list): # Process each item in the list return [ _convert_extra(item) @@ -521,14 +504,11 @@ def _convert_extra(data: dict | list) -> dict | list: else ("" if item is None else item) for item in data ] - else: - return data + return data -def _resolve(symbol: str): - """ - Resolve a symbol to its corresponding Python object. - """ +def _resolve(symbol: str) -> Any: + """Resolve a symbol to its corresponding Python object.""" module_path, func_name = symbol.rsplit(".", 1) module = importlib.import_module(module_path) return getattr(module, func_name) @@ -538,16 +518,14 @@ def _resolve(symbol: str): def _convert_nav(nav: list) -> list: - """ - Convert MkDocs navigation - """ + """Convert MkDocs navigation.""" return [_convert_nav_item(entry) for entry in nav] def _convert_nav_item(item: str | dict | list) -> dict | list: - """ - Convert MkDocs shorthand navigation structure into something more manageable - as we need to annotate each item with a title, URL, icon, and children. + """Convert MkDocs shorthand navigation structure into something more manageable. + + We need to annotate each item with a title, URL, icon, and children. """ if isinstance(item, str): return { @@ -561,7 +539,7 @@ def _convert_nav_item(item: str | dict | list) -> dict | list: } # Handle Title: URL - elif isinstance(item, dict): + if isinstance(item, dict): for title, value in item.items(): if isinstance(value, str): return { @@ -573,7 +551,7 @@ def _convert_nav_item(item: str | dict | list) -> dict | list: "is_index": _is_index(value.strip()), "active": False, } - elif isinstance(value, list): + if isinstance(value, list): return { "title": str(title), "url": None, @@ -583,28 +561,25 @@ def _convert_nav_item(item: str | dict | list) -> dict | list: "is_index": False, "active": False, } + raise TypeError(f"Unknown nav item value type: {type(value)}") # Handle a list of items elif isinstance(item, list): return [_convert_nav_item(child) for child in item] - else: - raise ValueError(f"Unknown nav item type: {type(item)}") + + raise TypeError(f"Unknown nav item type: {type(item)}") def _is_index(path: str) -> bool: - """ - Returns, whether the given path points to a section index. - """ + """Returns, whether the given path points to a section index.""" return os.path.basename(path) in ("index.md", "README.md") # ----------------------------------------------------------------------------- -def _convert_extra_javascript(value: list[Any]) -> list: - """ - Ensure extra_javascript uses a structured format. - """ +def _convert_extra_javascript(value: list) -> list: + """Ensure extra_javascript uses a structured format.""" for i, item in enumerate(value): if isinstance(item, str): value[i] = { @@ -619,9 +594,7 @@ def _convert_extra_javascript(value: list[Any]) -> list: item.setdefault("async", False) item.setdefault("defer", False) else: - raise ValueError( - f"Unknown extra_javascript item type: {type(item)}" - ) + raise TypeError(f"Unknown extra_javascript item type: {type(item)}") # Return resulting value return value @@ -630,10 +603,8 @@ def _convert_extra_javascript(value: list[Any]) -> list: # ----------------------------------------------------------------------------- -def _convert_markdown_extensions(value: Any): - """ - Convert Markdown extensions configuration to what Python Markdown expects. - """ +def _convert_markdown_extensions(value: Any) -> tuple[list[str], dict]: + """Convert Markdown extensions configuration to what Python Markdown expects.""" markdown_extensions = ["toc", "tables"] mdx_configs = {"toc": {}, "tables": {}} @@ -643,24 +614,24 @@ def _convert_markdown_extensions(value: Any): # actually parse the configuration. if "pymdownx" in value: pymdownx = value.pop("pymdownx") - for ext, config in pymdownx.items(): + for ext, conf in pymdownx.items(): # Special case for blocks extension, which has another level of # nesting. This is the only extension that requires this. if ext == "blocks": - for block, config in config.items(): + for block, config in conf.items(): value[f"pymdownx.{ext}.{block}"] = config else: - value[f"pymdownx.{ext}"] = config + value[f"pymdownx.{ext}"] = conf # Same as for Python Markdown extensions, see above if "zensical" in value: zensical = value.pop("zensical") - for ext, config in zensical.items(): + for ext, conf in zensical.items(): if ext == "extensions": - for key, config in config.items(): + for key, config in conf.items(): value[f"zensical.{ext}.{key}"] = config else: - value[f"zensical.{ext}"] = config + value[f"zensical.{ext}"] = conf # Extensions can be defined as a dict if isinstance(value, dict): @@ -686,15 +657,12 @@ def _convert_markdown_extensions(value: Any): def _convert_plugins(value: Any, config: dict) -> dict: - """ - Convert plugins configuration to something we can work with. - """ + """Convert plugins configuration to something we can work with.""" plugins = {} # Plugins can be defined as a dict if isinstance(value, dict): - for name, data in value.items(): - plugins[name] = data + plugins.update(value) # Plugins can also be defined as a list else: @@ -751,8 +719,7 @@ def _convert_plugins(value: Any, config: dict) -> dict: def _yaml_load( source: IO, loader: type[BaseLoader] | None = None ) -> dict[str, Any]: - """ - Load configuration file and resolve environment variables and parent files. + """Load configuration file and resolve environment variables and parent files. Note that INHERIT is only a bandaid that was introduced to allow for some degree of modularity, but with serious shortcomings. Zensical will use a @@ -767,12 +734,12 @@ def _yaml_load( source.read() .replace("material.extensions", "zensical.extensions") .replace("materialx", "zensical.extensions"), - Loader=Loader, + Loader=Loader, # noqa: S506 ) except YAMLError as e: raise ConfigurationError( f"Encountered an error parsing the configuration file: {e}" - ) + ) from e if config is None: return {} @@ -786,7 +753,7 @@ def _yaml_load( raise ConfigurationError( f"Inherited config file '{relpath}' doesn't exist at '{abspath}'." ) - with open(abspath, "r") as fd: + with open(abspath) as fd: parent = _yaml_load(fd, loader) config = always_merger.merge(parent, config) @@ -794,9 +761,8 @@ def _yaml_load( return config -def _construct_env_tag(loader: yaml.Loader, node: yaml.Node): - """ - Assign value of ENV variable referenced at node. +def _construct_env_tag(loader: yaml.Loader, node: yaml.Node) -> Any: + """Assign value of ENV variable referenced at node. MkDocs supports the use of !ENV to reference environment variables in YAML configuration files. We won't likely support this in Zensical, but for now diff --git a/python/zensical/extensions/emoji.py b/python/zensical/extensions/emoji.py index 31cd9fd..a496937 100644 --- a/python/zensical/extensions/emoji.py +++ b/python/zensical/extensions/emoji.py @@ -26,21 +26,22 @@ from __future__ import annotations import codecs import functools import os - from glob import iglob -from markdown import Markdown -from pymdownx import emoji, twemoji_db +from typing import TYPE_CHECKING from xml.etree.ElementTree import Element +from pymdownx import emoji, twemoji_db + +if TYPE_CHECKING: + from markdown import Markdown + # ----------------------------------------------------------------------------- # Functions # ----------------------------------------------------------------------------- -def twemoji(options: object, md: Markdown): - """ - Create twemoji index. - """ +def twemoji(options: dict, md: Markdown) -> dict: # noqa: ARG001 + """Create twemoji index.""" paths = options.get("custom_icons", [])[:] return _load_twemoji_index(tuple(paths)) @@ -53,12 +54,10 @@ def to_svg( alt: str, title: str, category: str, - options: object, + options: dict, md: Markdown, -): - """ - Load icon. - """ +) -> Element[str]: + """Load icon.""" if not uc: icons = md.inlinePatterns["emoji"].emoji_index["emoji"] @@ -78,20 +77,16 @@ def to_svg( # ----------------------------------------------------------------------------- -@functools.lru_cache(maxsize=None) -def _load(file: str): - """ - Load icon from file. - """ +@functools.cache +def _load(file: str) -> str: + """Load icon from file.""" with codecs.open(file, encoding="utf-8") as f: return f.read() -@functools.lru_cache(maxsize=None) -def _load_twemoji_index(paths): - """ - Load twemoji index and add icons. - """ +@functools.cache +def _load_twemoji_index(paths: tuple[str, ...]) -> dict: + """Load twemoji index and add icons.""" index = { "name": "twemoji", "emoji": twemoji_db.emoji, diff --git a/python/zensical/extensions/links.py b/python/zensical/extensions/links.py index 9abc32c..543c349 100644 --- a/python/zensical/extensions/links.py +++ b/python/zensical/extensions/links.py @@ -23,12 +23,17 @@ from __future__ import annotations +from pathlib import PurePosixPath +from typing import TYPE_CHECKING +from urllib.parse import urlparse + from markdown import Extension, Markdown from markdown.treeprocessors import Treeprocessor from markdown.util import AMP_SUBSTITUTE -from pathlib import PurePosixPath -from xml.etree.ElementTree import Element -from urllib.parse import urlparse + +if TYPE_CHECKING: + from xml.etree.ElementTree import Element + # ----------------------------------------------------------------------------- # Classes @@ -36,8 +41,7 @@ from urllib.parse import urlparse class LinksProcessor(Treeprocessor): - """ - Tree processor to replace links in Markdown with URLs. + """Tree processor to replace links in Markdown with URLs. Note that we view this as a bandaid until we can do processing on proper HTML ASTs in Rust. In the meantime, we just replace them as we find them. @@ -50,7 +54,7 @@ class LinksProcessor(Treeprocessor): self.path = path # Current page self.use_directory_urls = use_directory_urls - def run(self, root: Element): + def run(self, root: Element) -> None: # Now, we determine whether the current page is an index page, as we # must apply slightly different handling in case of directory URLs current_is_index = get_name(self.path) in ("index.md", "README.md") @@ -101,21 +105,15 @@ class LinksProcessor(Treeprocessor): class LinksExtension(Extension): - """ - A Markdown extension to resolve links to other Markdown files. - """ + """A Markdown extension to resolve links to other Markdown files.""" def __init__(self, path: str, use_directory_urls: bool): - """ - Initialize the extension. - """ + """Initialize the extension.""" self.path = path # Current page self.use_directory_urls = use_directory_urls - def extendMarkdown(self, md: Markdown): - """ - Register Markdown extension. - """ + def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 + """Register Markdown extension.""" md.registerExtension(self) # Create and register treeprocessor - we use the same priority as the @@ -132,8 +130,6 @@ class LinksExtension(Extension): def get_name(path: str) -> str: - """ - Get the name of a file from a given path. - """ + """Get the name of a file from a given path.""" path = PurePosixPath(path) return path.name diff --git a/python/zensical/extensions/preview.py b/python/zensical/extensions/preview.py index 7ebeedf..022ddb8 100644 --- a/python/zensical/extensions/preview.py +++ b/python/zensical/extensions/preview.py @@ -24,14 +24,17 @@ from __future__ import annotations import posixpath +from typing import TYPE_CHECKING, Any +from urllib.parse import urlparse from markdown import Extension, Markdown from markdown.treeprocessors import Treeprocessor -from urllib.parse import urlparse -from xml.etree.ElementTree import Element -from .links import LinksProcessor -from .utilities.filter import Filter +from zensical.extensions.links import LinksProcessor +from zensical.extensions.utilities.filter import Filter + +if TYPE_CHECKING: + from xml.etree.ElementTree import Element # ----------------------------------------------------------------------------- # Classes @@ -39,24 +42,19 @@ from .utilities.filter import Filter class PreviewProcessor(Treeprocessor): - """ - A Markdown treeprocessor to enable instant previews on links. + """A Markdown treeprocessor to enable instant previews on links. Note that this treeprocessor is dependent on the `links` treeprocessor registered programmatically before rendering a page. """ def __init__(self, md: Markdown, config: dict): - """ - Initialize the treeprocessor. - """ + """Initialize the treeprocessor.""" super().__init__(md) self.config = config - def run(self, root: Element): - """ - Run the treeprocessor. - """ + def run(self, root: Element) -> None: + """Run the treeprocessor.""" at = self.md.treeprocessors.get_index_for_name("zrelpath") # Hack: Python Markdown has no notion of where it is, i.e., which file @@ -84,9 +82,10 @@ class PreviewProcessor(Treeprocessor): # Walk through all configurations - @todo refactor so that we don't # iterate multiple times over the same elements for configuration in configurations: - if not configuration.get("sources"): - if not configuration.get("targets"): - continue + if not configuration.get("sources") and not configuration.get( + "targets" + ): + continue # Skip if page should not be considered filter = get_filter(configuration, "sources") @@ -123,8 +122,7 @@ class PreviewProcessor(Treeprocessor): class PreviewExtension(Extension): - """ - A Markdown extension to enable instant previews on links. + """A Markdown extension to enable instant previews on links. This extensions allows to automatically add the `data-preview` attribute to internal links matching specific criteria, so Material for MkDocs renders a @@ -132,10 +130,8 @@ class PreviewExtension(Extension): add previews to links in a programmatic way. """ - def __init__(self, *args, **kwargs): - """ - Initialize the extension. - """ + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize the extension.""" self.config = { "configurations": [[], "Filter configurations"], "sources": [{}, "Link sources"], @@ -143,10 +139,8 @@ class PreviewExtension(Extension): } super().__init__(*args, **kwargs) - def extendMarkdown(self, md: Markdown): - """ - Register Markdown extension. - """ + def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 + """Register Markdown extension.""" md.registerExtension(self) # Create and register treeprocessor - we use the same priority as the @@ -162,17 +156,13 @@ class PreviewExtension(Extension): # ----------------------------------------------------------------------------- -def get_filter(settings: dict, key: str): - """ - Get file filter from settings. - """ - return Filter(config=settings.get(key, {})) # type: ignore +def get_filter(settings: dict, key: str) -> Filter: + """Get file filter from settings.""" + return Filter(config=settings.get(key, {})) def resolve(processor_path: str, url_path: str) -> str: - """ - Resolve a relative URL path against the processor path. - """ + """Resolve a relative URL path against the processor path.""" # Remove the file name from the processor path to get the directory base_path = posixpath.dirname(processor_path) @@ -194,8 +184,6 @@ def resolve(processor_path: str, url_path: str) -> str: return posixpath.join(*base_segments) -def makeExtension(**kwargs): - """ - Register Markdown extension. - """ +def makeExtension(**kwargs: Any) -> PreviewExtension: # noqa: N802 + """Register Markdown extension.""" return PreviewExtension(**kwargs) diff --git a/python/zensical/extensions/search.py b/python/zensical/extensions/search.py index 5f0fe6b..b298288 100644 --- a/python/zensical/extensions/search.py +++ b/python/zensical/extensions/search.py @@ -23,8 +23,9 @@ from html import escape from html.parser import HTMLParser +from typing import Any -from markdown import Extension +from markdown import Extension, Markdown from markdown.postprocessors import Postprocessor # ----------------------------------------------------------------------------- @@ -33,17 +34,14 @@ from markdown.postprocessors import Postprocessor class SearchProcessor(Postprocessor): - """ - Post processor that extracts searchable content from the rendered HTML. - """ + """Post processor that extracts searchable content from the rendered HTML.""" - def __init__(self, md): + def __init__(self, md: Markdown) -> None: super().__init__(md) self.data = [] - def run(self, html): + def run(self, html: str) -> str: """Process the rendered HTML and extract text length.""" - # Divide page content into sections parser = Parser() parser.feed(html) @@ -76,17 +74,17 @@ class SearchProcessor(Postprocessor): class SearchExtension(Extension): """Markdown extension for search indexing.""" - def __init__(self, **kwargs): + def __init__(self, **kwargs: Any) -> None: self.config = {"keep": [set(), "Set of HTML tags to keep in output"]} super().__init__(**kwargs) - def extendMarkdown(self, md): + def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 """Register the PostProcessor with Markdown.""" processor = SearchProcessor(md) md.postprocessors.register(processor, "search", 0) -def makeExtension(**kwargs): +def makeExtension(**kwargs: Any) -> SearchExtension: # noqa: N802 """Factory function for creating the extension.""" return SearchExtension(**kwargs) @@ -96,13 +94,16 @@ def makeExtension(**kwargs): # HTML element class Element: - """ + """HTML element. + An element with attributes, essentially a small wrapper object for the parser to access attributes in other callbacks than handle_starttag. """ # Initialize HTML element - def __init__(self, tag, attrs=None): + def __init__( + self, tag: str, attrs: dict[str, str | None] | None = None + ) -> None: self.tag = tag self.attrs = attrs or {} @@ -111,18 +112,17 @@ class Element: return self.tag # Support comparison (compare by tag only) - def __eq__(self, other): - if other is Element: + def __eq__(self, other: object) -> bool: + if isinstance(other, Element): return self.tag == other.tag - else: - return self.tag == other + return self.tag == other # Support set operations def __hash__(self): return hash(self.tag) # Check whether the element should be excluded - def is_excluded(self): + def is_excluded(self) -> bool: return "data-search-exclude" in self.attrs @@ -131,13 +131,14 @@ class Element: # HTML section class Section: - """ + """HTML section. + A block of text with markup, preceded by a title (with markup), i.e., a headline with a certain level (h1-h6). Internally used by the parser. """ # Initialize HTML section - def __init__(self, el, level, depth=0): + def __init__(self, el: Element, level: int, depth: int = 0) -> None: self.el = el self.depth = depth self.level = level @@ -150,12 +151,11 @@ class Section: # String representation def __repr__(self): if self.id: - return "#".join([self.el.tag, self.id]) - else: - return self.el.tag + return f"{self.el.tag}#{self.id}" + return self.el.tag # Check whether the section should be excluded - def is_excluded(self): + def is_excluded(self) -> bool: return self.el.is_excluded() @@ -164,7 +164,8 @@ class Section: # HTML parser class Parser(HTMLParser): - """ + """Section divider. + This parser divides the given string of HTML into a list of sections, each of which are preceded by a h1-h6 level heading. A white- and blacklist of tags dictates which tags should be preserved as part of the index, and @@ -172,17 +173,15 @@ class Parser(HTMLParser): """ # Initialize HTML parser - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) # Tags to skip - self.skip = set( - [ - "object", # Objects - "script", # Scripts - "style", # Styles - ] - ) + self.skip = { + "object", # Objects + "script", # Scripts + "style", # Styles + } # Current context and section self.context = [] @@ -192,11 +191,13 @@ class Parser(HTMLParser): self.data = [] # Called at the start of every HTML tag - def handle_starttag(self, tag, attrs): - attrs = dict(attrs) + def handle_starttag( + self, tag: str, attrs: list[tuple[str, str | None]] + ) -> None: + attrs_dict = dict(attrs) # Ignore self-closing tags - el = Element(tag, attrs) + el = Element(tag, attrs_dict) if tag not in void: self.context.append(el) else: @@ -205,7 +206,7 @@ class Parser(HTMLParser): # Handle heading if tag in ([f"h{x}" for x in range(1, 7)]): depth = len(self.context) - if "id" in attrs: + if "id" in attrs_dict: # Ensure top-level section if tag != "h1" and not self.data: self.section = Section(Element("hx"), 1, depth) @@ -214,7 +215,7 @@ class Parser(HTMLParser): # Set identifier, if not first section self.section = Section(el, int(tag[1:2]), depth) if self.data: - self.section.id = attrs["id"] + self.section.id = attrs_dict["id"] # Append section to list self.data.append(self.section) @@ -225,7 +226,7 @@ class Parser(HTMLParser): self.data.append(self.section) # Handle special cases to skip - for key, value in attrs.items(): + for key, value in attrs_dict.items(): # Skip block if explicitly excluded from search if key == "data-search-exclude": self.skip.add(el) @@ -247,7 +248,7 @@ class Parser(HTMLParser): data.append(f"<{tag}>") # Called at the end of every HTML tag - def handle_endtag(self, tag): + def handle_endtag(self, tag: str) -> None: if not self.context or self.context[-1] != tag: return @@ -295,7 +296,7 @@ class Parser(HTMLParser): data.append(f"") # Called for the text contents of each tag - def handle_data(self, data): + def handle_data(self, data: str) -> None: if self.skip.intersection(self.context): return @@ -324,9 +325,11 @@ class Parser(HTMLParser): # Collapse adjacent whitespace elif data.isspace(): - if not self.section.text or not self.section.text[-1].isspace(): - self.section.text.append(data) - elif "pre" in self.context: + if ( + not self.section.text + or not self.section.text[-1].isspace() + or "pre" in self.context + ): self.section.text.append(data) # Handle everything else @@ -339,35 +342,31 @@ class Parser(HTMLParser): # ----------------------------------------------------------------------------- # Tags to keep -keep = set( - [ - "p", - "code", - "pre", - "li", - "ol", - "ul", - "sub", - "sup", - ] -) +keep = { + "p", + "code", + "pre", + "li", + "ol", + "ul", + "sub", + "sup", +} # Tags that are self-closing -void = set( - [ - "area", - "base", - "br", - "col", - "embed", - "hr", - "img", - "input", - "link", - "meta", - "param", - "source", - "track", - "wbr", - ] -) +void = { + "area", + "base", + "br", + "col", + "embed", + "hr", + "img", + "input", + "link", + "meta", + "param", + "source", + "track", + "wbr", +} diff --git a/python/zensical/extensions/utilities/filter.py b/python/zensical/extensions/utilities/filter.py index 4e3f19b..10a3357 100644 --- a/python/zensical/extensions/utilities/filter.py +++ b/python/zensical/extensions/utilities/filter.py @@ -31,13 +31,10 @@ from fnmatch import fnmatch class Filter: - """ - A filter. - """ + """A filter.""" def __init__(self, config: dict): - """ - Initialize the filter. + """Initialize the filter. Arguments: config: The filter configuration. @@ -45,8 +42,7 @@ class Filter: self.config = config def __call__(self, value: str) -> bool: - """ - Filter a value. + """Filter a value. First, the inclusion patterns are checked. Regardless of whether they are present, the exclusion patterns are checked afterwards. This allows @@ -59,7 +55,6 @@ class Filter: Returns: Whether the value should be included. """ - # Check if value matches one of the inclusion patterns if "include" in self.config: for pattern in self.config["include"]: diff --git a/python/zensical/main.py b/python/zensical/main.py index 8eaf149..02d9174 100644 --- a/python/zensical/main.py +++ b/python/zensical/main.py @@ -23,14 +23,15 @@ from __future__ import annotations -import click import os import shutil from pathlib import Path +from typing import Any +import click from click import ClickException -from zensical import build, serve, version +from zensical import build, serve, version # ---------------------------------------------------------------------------- # Commands @@ -39,8 +40,8 @@ from zensical import build, serve, version @click.version_option(version=version(), message="%(version)s") @click.group() -def cli(): - """Zensical - A modern static site generator""" +def cli() -> None: + """Zensical - A modern static site generator.""" @cli.command(name="build") @@ -65,10 +66,8 @@ def cli(): is_flag=True, help="Strict mode (currently unsupported).", ) -def execute_build(config_file: str | None, **kwargs): - """ - Build a project. - """ +def execute_build(config_file: str | None, **kwargs: Any) -> None: + """Build a project.""" if config_file is None: for file in ["zensical.toml", "mkdocs.yml", "mkdocs.yaml"]: if os.path.exists(file): @@ -112,10 +111,8 @@ def execute_build(config_file: str | None, **kwargs): is_flag=True, help="Strict mode (currently unsupported).", ) -def execute_serve(config_file: str | None, **kwargs): - """ - Build and serve a project. - """ +def execute_serve(config_file: str | None, **kwargs: Any) -> None: + """Build and serve a project.""" if config_file is None: for file in ["zensical.toml", "mkdocs.yml", "mkdocs.yaml"]: if os.path.exists(file): @@ -137,10 +134,8 @@ def execute_serve(config_file: str | None, **kwargs): type=click.Path(file_okay=False, dir_okay=True, writable=True), required=False, ) -def new_project(directory: str | None, **kwargs): - """ - Create a new template project in the current directory or in the given - directory. +def new_project(directory: str | None, **kwargs: Any) -> None: # noqa: ARG001 + """Create a new template project in the current directory or in the given directory. Raises: ClickException: if the directory already contains a zensical.toml or a diff --git a/python/zensical/markdown.py b/python/zensical/markdown.py index c5be44d..4291f70 100644 --- a/python/zensical/markdown.py +++ b/python/zensical/markdown.py @@ -24,15 +24,16 @@ from __future__ import annotations import re -import yaml - from datetime import date, datetime +from typing import Any + +import yaml from markdown import Markdown from yaml import SafeLoader -from .config import get_config -from .extensions.links import LinksExtension -from .extensions.search import SearchExtension +from zensical.config import get_config +from zensical.extensions.links import LinksExtension +from zensical.extensions.search import SearchExtension # ---------------------------------------------------------------------------- # Constants @@ -53,8 +54,7 @@ Regex pattern to extract front matter. def render(content: str, path: str) -> dict: - """ - Render Markdown and return HTML. + """Render Markdown and return HTML. This function returns rendered HTML as well as the table of contents and metadata. Now, this is the part where Zensical needs to call into Python, @@ -91,7 +91,7 @@ def render(content: str, path: str) -> dict: content = content[match.end() :].lstrip("\n") else: meta = {} - except Exception: + except Exception: # noqa: BLE001 pass # Convert Markdown and set nullish metadata to empty string, since we @@ -120,10 +120,8 @@ def render(content: str, path: str) -> dict: } -def _convert_toc(item: any): - """ - Convert a table of contents item to navigation item format. - """ +def _convert_toc(item: Any) -> dict: + """Convert a table of contents item to navigation item format.""" toc_item = { "title": item["data-toc-label"] or item["name"], "id": item["id"], diff --git a/python/zensical/zensical.pyi b/python/zensical/zensical.pyi index 68c43e3..bd67195 100644 --- a/python/zensical/zensical.pyi +++ b/python/zensical/zensical.pyi @@ -25,20 +25,14 @@ # Functions # ---------------------------------------------------------------------------- -def build(config_file: str, clean: bool): - """ - Builds the project. - """ +def build(config_file: str, clean: bool) -> None: + """Builds the project.""" -def serve(config_file: str, dev_addr: str): - """ - Builds and serves the project. - """ +def serve(config_file: str, dev_addr: str) -> None: + """Builds and serves the project.""" def version() -> str: - """ - Returns the current version. - """ + """Returns the current version.""" # ---------------------------------------------------------------------------- diff --git a/scripts/commit.py b/scripts/commit.py index 2f78377..5b64fae 100755 --- a/scripts/commit.py +++ b/scripts/commit.py @@ -25,11 +25,14 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. -import os, re, sys, tomllib # noqa: E401 - +import os +import re +import sys from dataclasses import dataclass from glob import glob +import tomllib + # ---------------------------------------------------------------------------- # Classes # ---------------------------------------------------------------------------- @@ -48,8 +51,7 @@ class TypeError(ValueError): @dataclass class Message: - """ - Commit message. + """Commit message. This class represents a commit message with a scope, type, and description. It provides methods to parse and validate commit messages according to our @@ -59,9 +61,7 @@ class Message: @classmethod def parse(cls, message: str) -> "Message": - """ - Parse a commit message string into an object. - """ + """Parse a commit message string into an object.""" match = re.match(r"^([^:]+):([^\s]+) - (.+)$", message) if not match: raise ValueError("Required format: : - ") @@ -71,9 +71,7 @@ class Message: return cls(scope=scope, type=type, description=description) def validate(self, scopes: dict[str, str]) -> None: - """ - Validate the commit message against the given scopes and types. - """ + """Validate the commit message against the given scopes and types.""" if self.scope not in scopes: raise ScopeError(f"Invalid scope: {self.scope}") @@ -119,8 +117,7 @@ class Message: def resolve(directory: str) -> dict[str, str] | None: - """ - Return commit scopes for a cargo project. + """Return commit scopes for a cargo project. This function checks, if the given directory contains a `Cargo.toml` file, and if so, parses it to extract the workspace members. It then resolves the @@ -129,7 +126,7 @@ def resolve(directory: str) -> dict[str, str] | None: """ path = os.path.join(directory, "Cargo.toml") if not os.path.isfile(path): - return + return None # Open and parse the Cargo.toml file with open(path, "rb") as f: @@ -155,6 +152,8 @@ def resolve(directory: str) -> dict[str, str] | None: if package and "name" in package: return {package["name"]: directory} + return None + # ---------------------------------------------------------------------------- # Constants @@ -199,10 +198,8 @@ ANSI escape code to reset formatting. # ---------------------------------------------------------------------------- -def main(): - """ - Commit message linter. - """ +def main() -> None: + """Commit message linter.""" if len(sys.argv) < 2: print("No commit message provided.") sys.exit(1) @@ -210,7 +207,7 @@ def main(): # Commit message might be passed as string, or in a file commit = sys.argv[1] if os.path.isfile(commit): - with open(sys.argv[1], "r") as f: + with open(sys.argv[1]) as f: message = f.read().strip() else: message = commit.strip() @@ -229,9 +226,9 @@ def main(): # If an error happened, print it except ValueError as e: print(f"{FG_RED}✘{RESET} {BG_RED} Error {RESET} {e}") - print("") + print() print(" Commit rejected.") - print("") + print() # Exit with error return sys.exit(1) diff --git a/scripts/dev.py b/scripts/dev.py index 9151265..3eb2e7f 100755 --- a/scripts/dev.py +++ b/scripts/dev.py @@ -25,16 +25,17 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. -import os, shutil, subprocess # noqa: E401 +import os +import shutil +import subprocess # ---------------------------------------------------------------------------- # Program # ---------------------------------------------------------------------------- -def main(): - """ - Set up development environment. +def main() -> None: + """Set up development environment. This script clones the Zensical UI repository, and symbolically links the build artifacts into the Python package directory for development use. diff --git a/scripts/prepare.py b/scripts/prepare.py index 95ba4a6..ba8471f 100755 --- a/scripts/prepare.py +++ b/scripts/prepare.py @@ -25,17 +25,16 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. -import os, subprocess # noqa: E401 +import os +import subprocess # ---------------------------------------------------------------------------- # Program # ---------------------------------------------------------------------------- -def main(): - """ - Prepare production build. - """ +def main() -> None: + """Prepare production build.""" os.makedirs("tmp", exist_ok=True) # Clone UI repository into tmp directory