Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 29 additions & 23 deletions src/vanillian/log/canonicalizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,59 +9,48 @@
logger = logging.getLogger(__name__)


class _LogSection(StrEnum):
DOC = "Doc"
PAPER = "Paper"
WIKI = "Wiki"
Other = "Other"


def _match_section(
line: str, section_patterns: dict[_LogSection, re.Pattern]
) -> _LogSection | None:
"""Return the log section type if the line matches any section pattern, else None."""
for section_type, pattern in section_patterns.items():
if pattern.fullmatch(line):
return section_type
return None


class LogCanonicalizer:
def __init__(self) -> None:
self.section_patterns = {
_LogSection.DOC: re.compile(r"\\\[Doc\\\]\s"),
_LogSection.PAPER: re.compile(r"\\\[Paper\\\]\s"),
_LogSection.WIKI: re.compile(r"\\\[Wiki\\\]\s"),
_LogSection.Other: re.compile(r"\\\[.+\\\]\s"),
_LogSection.OTHER: re.compile(r"\\\[.+\\\]\s"),
Comment on lines 12 to +18
}
self.log_item_pattern = re.compile(r"^- \[\[.+\]\]:?")
self.paper_file_name_pattern = re.compile(
r"^(?P<title>.+)-(?P<year>\d{4})(-(?P<publisher>.+))?$"
)

def canonicalize(self, lines: list[str], *, force: bool = False) -> None:
def canonicalize(self, lines: list[str], *, force: bool = False) -> list[str]:
"""Canonicalize log items in the provided lines.

Modifies the input lines in place.
Modify the input lines in place.

Args:
lines (list[str]): The lines of the log file.
force (bool, optional): If True, canonicalize without asking for confirmation. Defaults to False.

Returns:
list[str]: The canonicalized log lines.
Comment on lines +28 to +35
"""
logger.info("Starting log canonicalization.")
current_section = _LogSection.Other

current_section = _LogSection.OTHER
for idx, line in enumerate(lines):
if (match := _match_section(line, self.section_patterns)) is not None:
level = logger.info if current_section != _LogSection.Other else logger.debug
level = logger.info if current_section != _LogSection.OTHER else logger.debug
level("Line: %d: Entering section: '%s'", idx, match.value)
current_section = match
elif self.log_item_pattern.match(line) and current_section != _LogSection.Other:
elif self.log_item_pattern.match(line) and current_section != _LogSection.OTHER:
logger.info("Line: %d: Processing...", idx)
lines[idx] = self._canonicalize_line(current_section, line, force=force)
logger.info("Line: %d: Finished processing.", idx)
Comment on lines +25 to 48

logger.info("Log canonicalization completed.")

return lines

def _canonicalize_line(
self,
log_section: _LogSection,
Expand Down Expand Up @@ -152,3 +141,20 @@ def _canonicalize_item(self, log_section: _LogSection, log_item: str, sep: str =
raise ValueError(f"Unhandled log section: {log_section}")

return f"{path}{sep}{new_title}"


class _LogSection(StrEnum):
DOC = "Doc"
PAPER = "Paper"
WIKI = "Wiki"
OTHER = "Other"


def _match_section(
line: str, section_patterns: dict[_LogSection, re.Pattern]
) -> _LogSection | None:
Comment on lines +146 to +155
"""Return the log section type if the line matches any section pattern, else None."""
for section_type, pattern in section_patterns.items():
if pattern.fullmatch(line):
return section_type
return None
Comment on lines +146 to +160

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Moving _LogSection and _match_section to the bottom of the file causes a NameError at runtime when importing this module.\n\nIn Python, type annotations in function/method signatures (such as log_section: _LogSection in _canonicalize_line and _canonicalize_item) are evaluated when the class/function is defined. Since _LogSection is now defined at the bottom of the file (after LogCanonicalizer), importing canonicalizer.py will fail with:\nNameError: name '_LogSection' is not defined\n\nTo resolve this, please move _LogSection and _match_section back to the top of the file (before LogCanonicalizer), or import from __future__ import annotations at the very top of the file.

4 changes: 2 additions & 2 deletions src/vanillian/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pathlib import Path
from typing import TYPE_CHECKING

from .parse import parse_args
from .parser import parse_args

if TYPE_CHECKING:
from collections.abc import Callable
Expand Down Expand Up @@ -47,7 +47,7 @@ def main() -> int:
lines = read_lines(log_path)

canonicalizer = LogCanonicalizer()
canonicalizer.canonicalize(lines, force=args.force)
lines = canonicalizer.canonicalize(lines, force=args.force)

dest_path = log_path if args.overwrite else log_path.with_suffix(".canonicalized.md")
write_lines_atomic(dest_path, lines)
Expand Down
2 changes: 1 addition & 1 deletion src/vanillian/parse.py → src/vanillian/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def parse_args() -> argparse.Namespace:
prog = Path(sys.argv[0]).name
parser = argparse.ArgumentParser(
prog=prog,
description="The dedicated steward for vault-vanilla.",
description="The toolbox for vault-vanilla.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
subparsers = parser.add_subparsers(dest="command", required=True)
Expand Down
Loading