started using pyright and added type annotations to most of the code

(still some pyright errors)
This commit is contained in:
Amy G. Bowersox 2024-07-27 00:52:18 -06:00
parent ab2b775e3a
commit ba28ea0abf
5 changed files with 104 additions and 69 deletions

View File

@ -23,10 +23,33 @@ build-backend = "hatchling.build"
[tool.rye] [tool.rye]
managed = true managed = true
dev-dependencies = [] dev-dependencies = [
"pyright~=1.1.373",
]
[tool.hatch.metadata] [tool.hatch.metadata]
allow-direct-references = true allow-direct-references = true
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["src/dragonglass"] packages = ["src/dragonglass"]
[tool.ruff]
line-length = 120
[tool.ruff.lint]
select = [
"A", # shadowing built-ins
"E", # style and whitespace
"F", # important pyflakes lints
"I", # import sorting
"N", # naming
"T100" # breakpoints
]
[tool.ruff.lint.isort]
known-first-party = ["dragonglass"]
[tool.pyright]
venvPath = "."
venv = ".venv"
strict = ["**/*.py"]

View File

@ -17,5 +17,8 @@ markdown==3.6
markupsafe==2.1.5 markupsafe==2.1.5
# via dragonglass # via dragonglass
# via jinja2 # via jinja2
nodeenv==1.9.1
# via pyright
pyright==1.1.373
pyyaml==6.0.1 pyyaml==6.0.1
# via dragonglass # via dragonglass

View File

@ -3,8 +3,9 @@
import argparse import argparse
import yaml import yaml
from pathlib import Path from pathlib import Path
from tree import SourceNode, SourceIndex from typing import Any
from mparse import create_markdown_parser from .tree import generate_list, SourceNode, SourceIndex
from .mparse import create_markdown_parser
# The command line parser # The command line parser
parser = argparse.ArgumentParser(prog='dragonglass') parser = argparse.ArgumentParser(prog='dragonglass')
@ -14,26 +15,27 @@ parser.add_argument('-C', '--config', help='Specifies an alternate name for the
class Context: class Context:
def __init__(self): def __init__(self) -> None:
self.source_dir = None self.source_dir: Path | None = None
self.config = {} self.config: dict[str, Any] = {}
self.src_index = None self.src_index: SourceIndex | None = None
self.current_node = None self.current_node: SourceNode | None = None
def load_config(self, args): def load_config(self, args: argparse.Namespace) -> None:
config_filename = args.config if args.config else ".dragonglass" config_filename: str = str(args.config) if args.config else ".dragonglass"
assert self.source_dir is not None
config_path = self.source_dir / config_filename config_path = self.source_dir / config_filename
if config_path.exists() and config_path.is_file(): if config_path.exists() and config_path.is_file():
with open(config_path, "r") as f: with open(config_path, "r") as f:
self.config = yaml.full_load(f) self.config = yaml.full_load(f)
@property @property
def url_prefix(self): def url_prefix(self) -> str:
rc = self.config.get("url_prefix", "/") rc = self.config.get("url_prefix", "/")
return rc if rc.endswith("/") else rc + '/' return rc if rc.endswith("/") else rc + '/'
def main(): def main() -> int:
args = parser.parse_args() args = parser.parse_args()
context = Context() context = Context()
@ -49,7 +51,7 @@ def main():
context.load_config(args) context.load_config(args)
nodes = SourceNode.generate_list(context.source_dir) nodes = generate_list(context.source_dir)
for node in nodes: for node in nodes:
context.current_node = node context.current_node = node
node.load_metadata(context.source_dir) node.load_metadata(context.source_dir)

View File

@ -3,16 +3,18 @@
import re import re
import markdown import markdown
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
from typing import Any
from urllib.parse import urlparse from urllib.parse import urlparse
from urllib.parse import unquote as urlunquote from urllib.parse import unquote as urlunquote
from markdown.extensions import Extension from markdown.extensions import Extension
from markdown.inlinepatterns import InlineProcessor, SimpleTagInlineProcessor from markdown.inlinepatterns import InlineProcessor, SimpleTagInlineProcessor
from markdown.preprocessors import Preprocessor from markdown.preprocessors import Preprocessor
from .dragonglass import Context
PRIO_BASE = 10000 # priority base for our extensions PRIO_BASE = 10000 # priority base for our extensions
def is_proper_url(s): def is_proper_url(s: str) -> bool:
""" """
Checks to see if a string is a "proper" URL. Checks to see if a string is a "proper" URL.
@ -26,7 +28,7 @@ def is_proper_url(s):
return True if parseout.scheme else False return True if parseout.scheme else False
def sanitize_reference(s): def sanitize_reference(s: str) -> str:
""" """
Sanitizes an internal reference to a file by removing URL-quoted characters and any Markdown suffix. Sanitizes an internal reference to a file by removing URL-quoted characters and any Markdown suffix.
@ -49,7 +51,7 @@ class MetaStripper(Extension):
""" """
class MetaStripperProc(Preprocessor): class MetaStripperProc(Preprocessor):
def run(self, lines): def run(self, lines: list[str]) -> list[str]:
if lines[0] == '---': if lines[0] == '---':
lines.pop(0) lines.pop(0)
while lines[0] != '---': while lines[0] != '---':
@ -57,7 +59,7 @@ class MetaStripper(Extension):
lines.pop(0) lines.pop(0)
return lines return lines
def extendMarkdown(self, md): def extendMarkdown(self, md: markdown.Markdown) -> None:
md.preprocessors.register(MetaStripper.MetaStripperProc(md), 'metastripper', PRIO_BASE) md.preprocessors.register(MetaStripper.MetaStripperProc(md), 'metastripper', PRIO_BASE)
@ -65,15 +67,15 @@ class ObsidianImages(Extension):
"""An extension that supports image tags the way Obsidian handles them.""" """An extension that supports image tags the way Obsidian handles them."""
DIMS = re.compile(r'(.*)\|(\d+)(?:x(\d+))?') DIMS = re.compile(r'(.*)\|(\d+)(?:x(\d+))?')
def __init__(self, context, **kwargs): def __init__(self, context: Context, **kwargs: dict[str, Any]) -> None:
super(ObsidianImages, self).__init__(**kwargs) super(ObsidianImages, self).__init__(**kwargs)
self._context = context self._context = context
@property @property
def invalid_reference_classname(self): def invalid_reference_classname(self) -> str:
return 'invalid-reference' return 'invalid-reference'
def _parse_dimensions(self, s): def _parse_dimensions(self, s: str) -> tuple[str, int, int]:
m = self.DIMS.match(s) m = self.DIMS.match(s)
if m: if m:
width = int(m.group(2)) width = int(m.group(2))
@ -82,18 +84,19 @@ class ObsidianImages(Extension):
else: else:
return s, -1, -1 return s, -1, -1
def _lookup_image_reference(self, name): def _lookup_image_reference(self, name: str) -> str | None:
assert self._context.src_index is not None
node, _ = self._context.src_index.lookup(name) node, _ = self._context.src_index.lookup(name)
if node: if node:
return node.link_target(self._context.url_prefix) return node.link_target(self._context.url_prefix)
return None return None
class ObsidianImageProc(InlineProcessor): class ObsidianImageProc(InlineProcessor):
def __init__(self, pattern, md, extref): def __init__(self, pattern: str, md: markdown.Markdown, extref: Any) -> None:
super(ObsidianImages.ObsidianImageProc, self).__init__(pattern, md) super(ObsidianImages.ObsidianImageProc, self).__init__(pattern, md)
self._extref = extref self._extref = extref
def handleMatch(self, m, data): def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element, int, int]:
name, width, height = self._extref._parse_dimensions(m.group(1)) name, width, height = self._extref._parse_dimensions(m.group(1))
link = self._extref._lookup_image_reference(name) link = self._extref._lookup_image_reference(name)
if link is None: if link is None:
@ -111,11 +114,11 @@ class ObsidianImages(Extension):
return el, m.start(0), m.end(0) return el, m.start(0), m.end(0)
class GenericImageProc(InlineProcessor): class GenericImageProc(InlineProcessor):
def __init__(self, pattern, md, extref): def __init__(self, pattern: str, md: markdown.Markdown, extref: Any) -> None:
super(ObsidianImages.GenericImageProc, self).__init__(pattern, md) super(ObsidianImages.GenericImageProc, self).__init__(pattern, md)
self._extref = extref self._extref = extref
def handleMatch(self, m, data): def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element, int, int]:
name, width, height = self._extref._parse_dimensions(m.group(1)) name, width, height = self._extref._parse_dimensions(m.group(1))
link = m.group(2) link = m.group(2)
if is_proper_url(link): if is_proper_url(link):
@ -144,7 +147,7 @@ class ObsidianImages(Extension):
el.text = link el.text = link
return el, m.start(0), m.end(0) return el, m.start(0), m.end(0)
def extendMarkdown(self, md): def extendMarkdown(self, md: markdown.Markdown) -> None:
OBSIMAGE_PATTERN = r'!\[\[(.*?)\]\]' OBSIMAGE_PATTERN = r'!\[\[(.*?)\]\]'
GENERICIMAGE_PATTERN = r'!\[(.*?)\]\((.*?)\)' GENERICIMAGE_PATTERN = r'!\[(.*?)\]\((.*?)\)'
md.inlinePatterns.register(ObsidianImages.ObsidianImageProc(OBSIMAGE_PATTERN, md, self), md.inlinePatterns.register(ObsidianImages.ObsidianImageProc(OBSIMAGE_PATTERN, md, self),
@ -158,26 +161,27 @@ class ObsidianLinks(Extension):
An extension that processes Obsidian internal links in the [[page name]] format, as well as overrides the standard An extension that processes Obsidian internal links in the [[page name]] format, as well as overrides the standard
Markdown link processing to handle Obsidian internal links as well as external links. Markdown link processing to handle Obsidian internal links as well as external links.
""" """
def __init__(self, context, **kwargs): def __init__(self, context: Context, **kwargs: dict[str, Any]) -> None:
super(ObsidianLinks, self).__init__(**kwargs) super(ObsidianLinks, self).__init__(**kwargs)
self._context = context self._context = context
@property @property
def obsidian_link_classname(self): def obsidian_link_classname(self) -> str:
return 'obsidian-link' return 'obsidian-link'
@property @property
def invalid_reference_classname(self): def invalid_reference_classname(self) -> str:
return 'invalid-reference' return 'invalid-reference'
def _parse_reference(self, contents): def _parse_reference(self, contents: str) -> tuple[str | None, str]:
text = None text = None
t = contents.split('|') t = contents.split('|')
if len(t) > 1: if len(t) > 1:
text = t[1] text = t[1]
contents = t[0] contents = t[0]
node, linktype = self._context.src_index.lookup(contents) assert self._context.src_index is not None
node, _ = self._context.src_index.lookup(contents)
if not text: if not text:
text = contents text = contents
if node: if node:
@ -185,11 +189,11 @@ class ObsidianLinks(Extension):
return None, text return None, text
class ObsidianLinksProc(InlineProcessor): class ObsidianLinksProc(InlineProcessor):
def __init__(self, pattern, md, extref): def __init__(self, pattern: str, md: markdown.Markdown, extref: Any) -> None:
super(ObsidianLinks.ObsidianLinksProc, self).__init__(pattern, md) super(ObsidianLinks.ObsidianLinksProc, self).__init__(pattern, md)
self._extref = extref self._extref = extref
def handleMatch(self, m, data): def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element, int, int]:
link, text = self._extref._parse_reference(m.group(1)) link, text = self._extref._parse_reference(m.group(1))
if link is None: if link is None:
el = etree.Element('span') el = etree.Element('span')
@ -203,11 +207,11 @@ class ObsidianLinks(Extension):
return el, m.start(0), m.end(0) return el, m.start(0), m.end(0)
class GenericLinksProc(InlineProcessor): class GenericLinksProc(InlineProcessor):
def __init__(self, pattern, md, extref): def __init__(self, pattern: str, md: markdown.Markdown, extref: Any) -> None:
super(ObsidianLinks.GenericLinksProc, self).__init__(pattern, md) super(ObsidianLinks.GenericLinksProc, self).__init__(pattern, md)
self._extref = extref self._extref = extref
def handleMatch(self, m, data): def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element, int, int]:
text = m.group(1) text = m.group(1)
link = m.group(2) link = m.group(2)
if is_proper_url(link): if is_proper_url(link):
@ -227,7 +231,7 @@ class ObsidianLinks(Extension):
el.text = text el.text = text
return el, m.start(0), m.end(0) return el, m.start(0), m.end(0)
def extendMarkdown(self, md): def extendMarkdown(self, md: markdown.Markdown) -> None:
OBSLINK_PATTERN = r'\[\[(.*?)\]\]' OBSLINK_PATTERN = r'\[\[(.*?)\]\]'
GENERICLINK_PATTERN = r'\[(.*?)\]\((.*?)\)' GENERICLINK_PATTERN = r'\[(.*?)\]\((.*?)\)'
md.inlinePatterns.register(ObsidianLinks.ObsidianLinksProc(OBSLINK_PATTERN, md, self), md.inlinePatterns.register(ObsidianLinks.ObsidianLinksProc(OBSLINK_PATTERN, md, self),
@ -238,10 +242,10 @@ class ObsidianLinks(Extension):
class ObsidianInlines(Extension): class ObsidianInlines(Extension):
"""An extension that handles the special Obsidian markdown format sequences.""" """An extension that handles the special Obsidian markdown format sequences."""
def extendMarkdown(self, md): def extendMarkdown(self, md: markdown.Markdown) -> None:
md.inlinePatterns.register(SimpleTagInlineProcessor(r'()~~(.*?)~~', 'del'), 'strikeout', PRIO_BASE) md.inlinePatterns.register(SimpleTagInlineProcessor(r'()~~(.*?)~~', 'del'), 'strikeout', PRIO_BASE)
md.inlinePatterns.register(SimpleTagInlineProcessor(r'()\=\=(.*?)\=\=', 'ins'), 'highlight', PRIO_BASE + 1) md.inlinePatterns.register(SimpleTagInlineProcessor(r'()\=\=(.*?)\=\=', 'ins'), 'highlight', PRIO_BASE + 1)
def create_markdown_parser(context): def create_markdown_parser(context: Context) -> markdown.Markdown:
return markdown.Markdown(extensions=[MetaStripper(), ObsidianImages(context), ObsidianLinks(context), ObsidianInlines()]) return markdown.Markdown(extensions=[MetaStripper(), ObsidianImages(context), ObsidianLinks(context), ObsidianInlines()])

View File

@ -1,6 +1,9 @@
#!/usr/bin/env python #!/usr/bin/env python
from pathlib import Path
from typing import Any
from urllib.parse import quote as urlquote from urllib.parse import quote as urlquote
import markdown
import yaml import yaml
# The paths that are always to be ignored. # The paths that are always to be ignored.
@ -13,63 +16,63 @@ MARKDOWN_PAT = '*.md'
class SourceNode: class SourceNode:
def __init__(self, root, path, is_dir): def __init__(self, root: Path, path: Path, is_dir: bool) -> None:
self._root = root self._root = root
self._path = path self._path = path
self._is_dir = is_dir self._is_dir = is_dir
self._is_md = path.match(MARKDOWN_PAT) self._is_md = path.match(MARKDOWN_PAT)
self.metadata = None self.metadata: dict[str, Any] | None = None
self.text = None self.text: str | None = None
def __str__(self): def __str__(self) -> str:
return f"SourceNode({self._path}, {self._is_dir}) [is_md={self._is_md}]" return f"SourceNode({self._path}, {self._is_dir}) [is_md={self._is_md}]"
def link_target(self, prefix="/"): def link_target(self, prefix: str = "/") -> str:
xpath = self._path.with_suffix('.html') if self._is_md else self._path xpath = self._path.with_suffix('.html') if self._is_md else self._path
return urlquote(prefix + xpath.as_posix()) return urlquote(prefix + xpath.as_posix())
@classmethod def load_metadata(self, source_dir: Path) -> None:
def generate_list(cls, source_root):
nodes = []
dirs = [source_root]
while len(dirs) > 0:
current_dir = dirs.pop(0)
for child in current_dir.iterdir():
rchild = child.relative_to(source_root)
add_me = True
for pat in STATIC_IGNORE:
if rchild.match(pat):
add_me = False
break
if add_me:
nodes.append(SourceNode(source_root, rchild, child.is_dir()))
if child.is_dir():
dirs.append(child)
return nodes
def load_metadata(self, source_dir):
if self._is_md and not self._is_dir: if self._is_md and not self._is_dir:
with open(source_dir / self._path, "r", encoding="utf-8") as f: with open(source_dir / self._path, "r", encoding="utf-8") as f:
cur_line = f.readline() cur_line = f.readline()
if cur_line == '---\n': if cur_line == '---\n':
metalines = [] metalines: list[str] = []
cur_line = f.readline() cur_line = f.readline()
while cur_line != '---\n': while cur_line != '---\n':
metalines.append(cur_line) metalines.append(cur_line)
cur_line = f.readline() cur_line = f.readline()
self.metadata = yaml.full_load(''.join(metalines)) self.metadata = yaml.full_load(''.join(metalines))
def parse_markdown(self, source_dir, markdown_parser): def parse_markdown(self, source_dir: Path, markdown_parser: markdown.Markdown) -> None:
if self._is_md and not self._is_dir: if self._is_md and not self._is_dir:
markdown_parser.reset() markdown_parser.reset()
with open(source_dir / self._path, "r", encoding="utf-8") as f: with open(source_dir / self._path, "r", encoding="utf-8") as f:
self.text = markdown_parser.convert(f.read()) self.text = markdown_parser.convert(f.read())
def generate_list(source_root: Path) -> list[SourceNode]:
nodes: list[SourceNode] = []
dirs = [source_root]
while len(dirs) > 0:
current_dir = dirs.pop(0)
for child in current_dir.iterdir():
rchild = child.relative_to(source_root)
add_me = True
for pat in STATIC_IGNORE:
if rchild.match(pat):
add_me = False
break
if add_me:
nodes.append(SourceNode(source_root, rchild, child.is_dir()))
if child.is_dir():
dirs.append(child)
return nodes
class SourceIndex: class SourceIndex:
def __init__(self, nodelist): def __init__(self, nodelist: list[SourceNode]) -> None:
self._byname = {} self._byname: dict[str, SourceNode] = {}
self._byalias = {} self._byalias: dict[str, SourceNode] = {}
for node in nodelist: for node in nodelist:
if node._is_dir: if node._is_dir:
continue continue
@ -91,7 +94,7 @@ class SourceIndex:
self._byname[key] = node self._byname[key] = node
self._byname[node._path.as_posix()] = node self._byname[node._path.as_posix()] = node
def lookup(self, reference): def lookup(self, reference: str) -> tuple[SourceNode | None, str | None]:
if reference in self._byname: if reference in self._byname:
return self._byname[reference], 'NAME' return self._byname[reference], 'NAME'
elif reference in self._byalias: elif reference in self._byalias: