Skip to content

Commit fa1c01f

Browse files
committed
Factor out logging into separate mixin class
No more `self.page.document.baker.debug()` However, configuration instances log through their "parent", not directly (so PDFBakerPage uses `self.page.debug()`)
1 parent eba6a3b commit fa1c01f

5 files changed

Lines changed: 133 additions & 63 deletions

File tree

src/pdfbaker/__main__.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,23 @@ def cli() -> None:
2626
)
2727
@click.option("-q", "--quiet", is_flag=True, help="Show errors only")
2828
@click.option("-v", "--verbose", is_flag=True, help="Show debug information")
29+
@click.option(
30+
"-t",
31+
"--trace",
32+
is_flag=True,
33+
help="Show trace information (even more detailed than debug)",
34+
)
2935
@click.option("--keep-build", is_flag=True, help="Keep build artifacts")
3036
@click.option(
3137
"--debug", is_flag=True, help="Debug mode (implies --verbose and --keep-build)"
3238
)
3339
def bake(
34-
config_file: Path, quiet: bool, verbose: bool, keep_build: bool, debug: bool
40+
config_file: Path,
41+
quiet: bool,
42+
verbose: bool,
43+
trace: bool,
44+
keep_build: bool,
45+
debug: bool,
3546
) -> int:
3647
"""Parse config file and bake PDFs."""
3748
if debug:
@@ -40,7 +51,11 @@ def bake(
4051

4152
try:
4253
baker = PDFBaker(
43-
config_file, quiet=quiet, verbose=verbose, keep_build=keep_build
54+
config_file,
55+
quiet=quiet,
56+
verbose=verbose,
57+
trace=trace,
58+
keep_build=keep_build,
4459
)
4560
baker.bake()
4661
return 0

src/pdfbaker/baker.py

Lines changed: 26 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .config import PDFBakerConfiguration
1414
from .document import PDFBakerDocument
1515
from .errors import ConfigurationError
16+
from .logging import TRACE, LoggingMixin
1617

1718
__all__ = ["PDFBaker"]
1819

@@ -27,15 +28,20 @@
2728
}
2829

2930

30-
class PDFBaker:
31+
class PDFBaker(LoggingMixin):
3132
"""Main class for PDF document generation."""
3233

3334
class Configuration(PDFBakerConfiguration):
3435
"""PDFBaker configuration."""
3536

36-
def __init__(self, base_config: dict[str, Any], config_file: Path) -> None:
37+
def __init__(
38+
self, baker: "PDFBaker", base_config: dict[str, Any], config_file: Path
39+
) -> None:
3740
"""Initialize baker configuration (needs documents)."""
41+
self.baker = baker
42+
self.baker.log_debug_section("Loading main configuration: %s", config_file)
3843
super().__init__(base_config, config_file)
44+
self.baker.log_trace(self.pprint())
3945
if "documents" not in self:
4046
raise ConfigurationError(
4147
'Key "documents" missing - is this the main configuration file?'
@@ -49,64 +55,42 @@ def __init__(
4955
config_file: Path,
5056
quiet: bool = False,
5157
verbose: bool = False,
58+
trace: bool = False,
5259
keep_build: bool = False,
5360
) -> None:
54-
"""Initialize PDFBaker with config file path.
61+
"""Initialize PDFBaker with config file path. Set logging level.
5562
5663
Args:
5764
config_file: Path to config file, document directory is its parent
65+
quiet: Show errors only
66+
verbose: Show debug information
67+
trace: Show trace information (even more detailed than debug)
68+
keep_build: Keep build artifacts
5869
"""
70+
super().__init__()
5971
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
60-
self.logger = logging.getLogger(__name__)
6172
if quiet:
6273
logging.getLogger().setLevel(logging.ERROR)
74+
elif trace:
75+
logging.getLogger().setLevel(TRACE)
6376
elif verbose:
6477
logging.getLogger().setLevel(logging.DEBUG)
6578
else:
6679
logging.getLogger().setLevel(logging.INFO)
6780
self.keep_build = keep_build
6881
self.config = self.Configuration(
82+
baker=self,
6983
base_config=DEFAULT_CONFIG,
7084
config_file=config_file,
7185
)
7286

73-
def debug(self, msg: str, *args: Any, **kwargs: Any) -> None:
74-
"""Log a debug message."""
75-
self.logger.debug(msg, *args, **kwargs)
76-
77-
def info(self, msg: str, *args: Any, **kwargs: Any) -> None:
78-
"""Log an info message."""
79-
self.logger.info(msg, *args, **kwargs)
80-
81-
def info_section(self, msg: str, *args: Any, **kwargs: Any) -> None:
82-
"""Log an info message as a main section header."""
83-
self.logger.info(f"──── {msg} ────", *args, **kwargs)
84-
85-
def info_subsection(self, msg: str, *args: Any, **kwargs: Any) -> None:
86-
"""Log an info message as a subsection header."""
87-
self.logger.info(f" ── {msg} ──", *args, **kwargs)
88-
89-
def warning(self, msg: str, *args: Any, **kwargs: Any) -> None:
90-
"""Log a warning message."""
91-
self.logger.warning(msg, *args, **kwargs)
92-
93-
def error(self, msg: str, *args: Any, **kwargs: Any) -> None:
94-
"""Log an error message."""
95-
self.logger.error(f"**** {msg} ****", *args, **kwargs)
96-
97-
def critical(self, msg: str, *args: Any, **kwargs: Any) -> None:
98-
"""Log a critical message."""
99-
self.logger.critical(msg, *args, **kwargs)
100-
10187
def bake(self) -> None:
10288
"""Generate PDFs from documents."""
10389
pdfs_created: list[Path] = []
10490
failed_docs: list[tuple[str, str]] = []
10591

106-
self.debug("Main configuration:")
107-
self.debug(self.config.pprint())
108-
self.debug("Documents to process:")
109-
self.debug(self.config.documents)
92+
self.log_debug_subsection("Documents to process:")
93+
self.log_debug(self.config.documents)
11094
for doc_config in self.config.documents:
11195
doc = PDFBakerDocument(
11296
baker=self,
@@ -115,7 +99,7 @@ def bake(self) -> None:
11599
)
116100
pdf_files, error_message = doc.process_document()
117101
if pdf_files is None:
118-
self.error(
102+
self.log_error(
119103
"Failed to process document '%s': %s",
120104
doc.config.name,
121105
error_message,
@@ -129,17 +113,17 @@ def bake(self) -> None:
129113
doc.teardown()
130114

131115
if pdfs_created:
132-
self.info("Created PDFs:")
116+
self.log_info("Created PDFs:")
133117
for pdf in pdfs_created:
134-
self.info(" %s", pdf)
118+
self.log_info(" %s", pdf)
135119
else:
136-
self.warning("No PDFs were created.")
120+
self.log_warning("No PDFs were created.")
137121

138122
if failed_docs:
139-
self.warning(
123+
self.log_warning(
140124
"Failed to process %d document%s:",
141125
len(failed_docs),
142126
"" if len(failed_docs) == 1 else "s",
143127
)
144128
for doc_name, error in failed_docs:
145-
self.error(" %s: %s", doc_name, error)
129+
self.log_error(" %s: %s", doc_name, error)

src/pdfbaker/document.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
PDFCombineError,
2424
PDFCompressionError,
2525
)
26+
from .logging import LoggingMixin
2627
from .page import PDFBakerPage
2728
from .pdf import (
2829
combine_pdfs,
@@ -34,7 +35,7 @@
3435
__all__ = ["PDFBakerDocument"]
3536

3637

37-
class PDFBakerDocument:
38+
class PDFBakerDocument(LoggingMixin):
3839
"""A document being processed."""
3940

4041
class Configuration(PDFBakerConfiguration):
@@ -52,18 +53,25 @@ def __init__(
5253
base_config: The PDFBaker configuration to merge with
5354
config_file: The document configuration (YAML file)
5455
"""
56+
self.document = document
57+
self.document.log_debug_subsection("Parsing document config: %s", config)
5558
if config.is_dir():
5659
self.name = config.name
5760
config = config / DEFAULT_DOCUMENT_CONFIG_FILE
5861
else:
5962
self.name = config.stem
63+
self.directory = config.parent
64+
self.document.log_trace(self.pprint())
65+
self.document.log_debug_section(
66+
'Merging document config for "%s"...', self.name
67+
)
6068
super().__init__(base_config, config)
61-
self.document = document
69+
self.document.log_trace(self.pprint())
70+
self.document.log_debug_subsection("Document config for %s:", self.name)
6271
if "pages" not in self:
6372
raise ConfigurationError(
6473
'Document "{document.name}" is missing key "pages"'
6574
)
66-
self.directory = config.parent
6775
self.pages_dir = self.resolve_path(self["pages_dir"])
6876
self.pages = []
6977
for page_spec in self["pages"]:
@@ -73,8 +81,7 @@ def __init__(
7381
self.pages.append(page)
7482
self.build_dir = self.resolve_path(self["build_dir"])
7583
self.dist_dir = self.resolve_path(self["dist_dir"])
76-
self.document.baker.debug("Document config for %s:", self.name)
77-
self.document.baker.debug(self.pprint())
84+
self.document.log_trace(self.pprint())
7885

7986
def __init__(
8087
self,
@@ -83,6 +90,7 @@ def __init__(
8390
config: Path,
8491
):
8592
"""Initialize a document."""
93+
super().__init__()
8694
self.baker = baker
8795
self.config = self.Configuration(base_config, config, document=self)
8896

@@ -96,9 +104,7 @@ def process_document(self) -> tuple[Path | list[Path] | None, str | None]:
96104
FIXME: could have created SOME PDF files
97105
- error_message is a string describing the error, or None if successful
98106
"""
99-
self.baker.info_section(
100-
'Processing document "%s"...', self.config.directory.name
101-
)
107+
self.log_info_section('Processing document "%s"...', self.config.directory.name)
102108

103109
self.config.build_dir.mkdir(parents=True, exist_ok=True)
104110
self.config.dist_dir.mkdir(parents=True, exist_ok=True)
@@ -150,9 +156,7 @@ def process(self) -> Path | list[Path]:
150156
# Multiple PDF documents
151157
pdf_files = []
152158
for variant in self.config["variants"]:
153-
self.baker.info_subsection(
154-
'Processing variant "%s"...', variant["name"]
155-
)
159+
self.log_info_subsection('Processing variant "%s"...', variant["name"])
156160
variant_config = deep_merge(doc_config, variant)
157161
variant_config["variant"] = variant
158162
# variant_config = deep_merge(variant_config, self.config)
@@ -197,17 +201,17 @@ def _combine_and_compress(
197201
if doc_config.get("compress_pdf", False):
198202
try:
199203
compress_pdf(combined_pdf, output_path)
200-
self.baker.info("PDF compressed successfully")
204+
self.log_info("PDF compressed successfully")
201205
except PDFCompressionError as exc:
202-
self.baker.warning(
206+
self.log_warning(
203207
"Compression failed, using uncompressed version: %s",
204208
exc,
205209
)
206210
os.rename(combined_pdf, output_path)
207211
else:
208212
os.rename(combined_pdf, output_path)
209213

210-
self.baker.info("Created PDF: %s", output_path)
214+
self.log_info("Created PDF: %s", output_path)
211215
return output_path
212216

213217
def teardown(self) -> None:

src/pdfbaker/logging.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Logging mixin for pdfbaker classes."""
2+
3+
import logging
4+
from typing import Any
5+
6+
TRACE = 5
7+
logging.addLevelName(TRACE, 'TRACE')
8+
9+
class LoggingMixin:
10+
"""Mixin providing consistent logging functionality across pdfbaker classes."""
11+
12+
def __init__(self) -> None:
13+
"""Initialize logger for the class."""
14+
self.logger = logging.getLogger(self.__class__.__module__)
15+
16+
def log_trace(self, msg: str, *args: Any, **kwargs: Any) -> None:
17+
"""Log a trace message (more detailed than debug)."""
18+
self.logger.log(TRACE, msg, *args, **kwargs)
19+
20+
def log_trace_section(self, msg: str, *args: Any, **kwargs: Any) -> None:
21+
"""Log a trace message as a main section header."""
22+
self.logger.log(TRACE, f"──── {msg} ────", *args, **kwargs)
23+
24+
def log_trace_subsection(self, msg: str, *args: Any, **kwargs: Any) -> None:
25+
"""Log a trace message as a subsection header."""
26+
self.logger.log(TRACE, f" ── {msg} ──", *args, **kwargs)
27+
28+
def log_debug(self, msg: str, *args: Any, **kwargs: Any) -> None:
29+
"""Log a debug message."""
30+
self.logger.debug(msg, *args, **kwargs)
31+
32+
def log_debug_section(self, msg: str, *args: Any, **kwargs: Any) -> None:
33+
"""Log a debug message as a main section header."""
34+
self.logger.debug(f"──── {msg} ────", *args, **kwargs)
35+
36+
def log_debug_subsection(self, msg: str, *args: Any, **kwargs: Any) -> None:
37+
"""Log a debug message as a subsection header."""
38+
self.logger.debug(f" ── {msg} ──", *args, **kwargs)
39+
40+
def log_info(self, msg: str, *args: Any, **kwargs: Any) -> None:
41+
"""Log an info message."""
42+
self.logger.info(msg, *args, **kwargs)
43+
44+
def log_info_section(self, msg: str, *args: Any, **kwargs: Any) -> None:
45+
"""Log an info message as a main section header."""
46+
self.logger.info(f"──── {msg} ────", *args, **kwargs)
47+
48+
def log_info_subsection(self, msg: str, *args: Any, **kwargs: Any) -> None:
49+
"""Log an info message as a subsection header."""
50+
self.logger.info(f" ── {msg} ──", *args, **kwargs)
51+
52+
def log_warning(self, msg: str, *args: Any, **kwargs: Any) -> None:
53+
"""Log a warning message."""
54+
self.logger.warning(msg, *args, **kwargs)
55+
56+
def log_error(self, msg: str, *args: Any, **kwargs: Any) -> None:
57+
"""Log an error message."""
58+
self.logger.error(f"**** {msg} ****", *args, **kwargs)
59+
60+
def log_critical(self, msg: str, *args: Any, **kwargs: Any) -> None:
61+
"""Log a critical message."""
62+
self.logger.critical(msg, *args, **kwargs)

0 commit comments

Comments
 (0)