Skip to content

Commit 9e7f3ab

Browse files
committed
Restructuring, linting
1 parent f04005c commit 9e7f3ab

File tree

4 files changed

+148
-106
lines changed

4 files changed

+148
-106
lines changed

src/pdfbaker/__init__.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
11
"""Core functionality for document generation."""
22

33
from .common import (
4-
load_pages,
5-
compress_pdf,
64
combine_pdfs,
5+
compress_pdf,
76
convert_svg_to_pdf,
7+
load_pages,
88
)
9-
109
from .render import (
11-
process_template_data,
1210
create_env,
11+
encode_image,
12+
encode_images,
1313
highlight,
14-
process_style,
15-
process_text_with_jinja,
1614
process_list_item_texts,
1715
process_list_items,
16+
process_style,
17+
process_template_data,
18+
process_text_with_jinja,
1819
space_bullets,
19-
encode_image,
20-
encode_images,
2120
)
2221

2322
__all__ = [

src/pdfbaker/__main__.py

Lines changed: 122 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -27,109 +27,153 @@ def _deep_merge(base, update):
2727
return merged
2828

2929

30-
def main(config_path=None):
31-
"""Main function for the document generator."""
32-
# Get config path from arguments if not provided
30+
def _get_config_path(config_path=None):
31+
"""Get and validate the configuration file path."""
3332
if config_path is None:
3433
if len(sys.argv) < 2:
3534
print("Error: Config file path is required")
3635
print("Usage: python -m pdfbaker <config_file_path>")
37-
return 1
36+
return None
3837
config_path = sys.argv[1]
39-
40-
# Load main configuration
41-
config_path = Path(config_path).resolve() # Get absolute path
38+
39+
config_path = Path(config_path).resolve()
4240
if not config_path.exists():
4341
print(f"Error: Configuration file not found: {config_path}")
44-
return 1
45-
42+
return None
43+
44+
return config_path
45+
46+
47+
def _load_config(config_path):
48+
"""Load configuration from a YAML file."""
4649
with open(config_path, encoding="utf-8") as f:
47-
config = yaml.safe_load(f)
48-
49-
# Create root output directories relative to config file
50-
base_dir = config_path.parent
50+
return yaml.safe_load(f)
51+
52+
53+
def _setup_output_directories(base_dir):
54+
"""Create and return build and dist directories."""
5155
build_dir = base_dir / "build"
5256
dist_dir = base_dir / "dist"
5357
os.makedirs(build_dir, exist_ok=True)
5458
os.makedirs(dist_dir, exist_ok=True)
59+
return build_dir, dist_dir
5560

56-
# Allow document_paths to be absolute or relative to config file
57-
documents = config.get("documents", [])
61+
62+
def _resolve_document_paths(base_dir, documents):
63+
"""Resolve document paths to absolute paths."""
5864
document_paths = {}
59-
65+
6066
for doc_name in documents:
61-
# Check if document path is specified
6267
if isinstance(doc_name, dict):
6368
# Format: {"name": "doc_name", "path": "/absolute/path/to/doc"}
6469
doc_path = Path(doc_name["path"])
6570
doc_name = doc_name["name"]
6671
else:
6772
# Default: document in subdirectory with same name as doc_name
6873
doc_path = base_dir / doc_name
69-
70-
# Store the absolute path
74+
7175
document_paths[doc_name] = doc_path.resolve()
7276

73-
# Process each document
74-
for doc_name, doc_path in document_paths.items():
75-
if not doc_path.is_dir():
76-
print(f'Warning: Directory missing for document "{doc_name}" at {doc_path} - skipping')
77-
continue
78-
79-
bake_path = doc_path / "bake.py"
80-
if not bake_path.exists():
81-
print(f'Warning: bake.py missing for document "{doc_name}" - skipping')
82-
continue
83-
84-
config_yml_path = doc_path / "config.yml"
85-
if not config_yml_path.exists():
86-
print(f'Warning: config.yml missing for document "{doc_name}" - skipping')
87-
continue
88-
89-
print(f'Processing document "{doc_name}" from {doc_path}...')
90-
91-
# Load document-specific bake
92-
doc_bake = importlib.util.spec_from_file_location(
93-
f"documents.{doc_name}.bake",
94-
bake_path
77+
return document_paths
78+
79+
80+
def _validate_document(doc_name, doc_path):
81+
"""Validate that a document has all required files."""
82+
if not doc_path.is_dir():
83+
print(
84+
f'Warning: Directory missing for document "{doc_name}" '
85+
f"at {doc_path} - skipping"
9586
)
96-
module = importlib.util.module_from_spec(doc_bake)
97-
doc_bake.loader.exec_module(module)
98-
99-
# Merge document configuration
100-
with open(config_yml_path, encoding="utf-8") as f:
101-
doc_config = yaml.safe_load(f)
102-
merged_config = _deep_merge(config, doc_config)
103-
104-
# Create document-specific output directories
105-
doc_build_dir = build_dir / doc_name
106-
doc_dist_dir = dist_dir / doc_name
107-
os.makedirs(doc_build_dir, exist_ok=True)
108-
os.makedirs(doc_dist_dir, exist_ok=True)
109-
110-
# Clean document-specific output directories
111-
for dir_path in [doc_build_dir, doc_dist_dir]:
112-
for file in os.listdir(dir_path):
113-
file_path = dir_path / file
114-
if os.path.isfile(file_path):
115-
os.remove(file_path)
116-
117-
# Prepare paths for the document processor
118-
paths = {
119-
"doc_dir": doc_path,
120-
"templates_dir": doc_path / "templates",
121-
"pages_dir": doc_path / "pages",
122-
"images_dir": doc_path / "images",
123-
"build_dir": doc_build_dir,
124-
"dist_dir": doc_dist_dir,
125-
}
126-
127-
# Prepare Jinja environment
128-
jinja_env = create_env(paths["templates_dir"])
129-
130-
# Process the document
131-
module.process_document(paths, merged_config, jinja_env)
132-
87+
return False
88+
89+
bake_path = doc_path / "bake.py"
90+
if not bake_path.exists():
91+
print(f'Warning: bake.py missing for document "{doc_name}" - skipping')
92+
return False
93+
94+
config_yml_path = doc_path / "config.yml"
95+
if not config_yml_path.exists():
96+
print(f'Warning: config.yml missing for document "{doc_name}" - skipping')
97+
return False
98+
99+
return bake_path, config_yml_path
100+
101+
102+
def _load_document_bake_module(doc_name, bake_path):
103+
"""Load the document's bake.py module."""
104+
doc_bake = importlib.util.spec_from_file_location(
105+
f"documents.{doc_name}.bake", bake_path
106+
)
107+
module = importlib.util.module_from_spec(doc_bake)
108+
doc_bake.loader.exec_module(module)
109+
return module
110+
111+
112+
def _setup_document_directories(build_dir, dist_dir, doc_name):
113+
"""Set up and clean document-specific build and dist directories."""
114+
doc_build_dir = build_dir / doc_name
115+
doc_dist_dir = dist_dir / doc_name
116+
os.makedirs(doc_build_dir, exist_ok=True)
117+
os.makedirs(doc_dist_dir, exist_ok=True)
118+
119+
# Clean document-specific output directories
120+
for dir_path in [doc_build_dir, doc_dist_dir]:
121+
for file in os.listdir(dir_path):
122+
file_path = dir_path / file
123+
if os.path.isfile(file_path):
124+
os.remove(file_path)
125+
126+
return doc_build_dir, doc_dist_dir
127+
128+
129+
def _process_document(doc_name, doc_path, config, build_dir, dist_dir):
130+
"""Process an individual document."""
131+
validation_result = _validate_document(doc_name, doc_path)
132+
if not validation_result:
133+
return
134+
135+
print(f'Processing document "{doc_name}" from {doc_path}...')
136+
bake_path, config_yml_path = validation_result
137+
bake_module = _load_document_bake_module(doc_name, bake_path)
138+
with open(config_yml_path, encoding="utf-8") as f:
139+
doc_config = yaml.safe_load(f)
140+
141+
doc_build_dir, doc_dist_dir = _setup_document_directories(
142+
build_dir, dist_dir, doc_name
143+
)
144+
paths = {
145+
"doc_dir": doc_path,
146+
"templates_dir": doc_path / "templates",
147+
"pages_dir": doc_path / "pages",
148+
"images_dir": doc_path / "images",
149+
"build_dir": doc_build_dir,
150+
"dist_dir": doc_dist_dir,
151+
}
152+
153+
bake_module.process_document(
154+
paths=paths,
155+
config=_deep_merge(config, doc_config),
156+
jinja_env=create_env(paths["templates_dir"]),
157+
)
158+
159+
160+
def main(config_path=None):
161+
"""Main function for the document generator."""
162+
config_path = _get_config_path(config_path)
163+
if config_path is None:
164+
return 1
165+
166+
config = _load_config(config_path)
167+
168+
base_dir = config_path.parent
169+
build_dir, dist_dir = _setup_output_directories(base_dir)
170+
171+
documents = config.get("documents", [])
172+
document_paths = _resolve_document_paths(base_dir, documents)
173+
174+
for doc_name, doc_path in document_paths.items():
175+
_process_document(doc_name, doc_path, config, build_dir, dist_dir)
176+
133177
return 0
134178

135179

src/pdfbaker/common.py

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
"""Common functionality for document generation."""
22

33
import subprocess
4-
from pathlib import Path
54

6-
# TODO: Switch to CairoSVG (Ubuntu installs 2.7.1)
7-
# as soon as the SVG templates are tidied up (update README)
8-
# from cairosvg import svg2pdf
95
import pypdf
106
import yaml
117

8+
try:
9+
from cairosvg import svg2pdf
10+
11+
CAIROSVG_AVAILABLE = True
12+
except ImportError:
13+
CAIROSVG_AVAILABLE = False
14+
1215

1316
def load_pages(pages_dir):
1417
"""Load page configurations from a specific subdirectory."""
@@ -70,26 +73,22 @@ def convert_svg_to_pdf(svg_path, pdf_path, backend="cairosvg"):
7073
"""
7174
if backend == "inkscape":
7275
return _convert_with_inkscape(svg_path, pdf_path)
73-
else: # Default to cairosvg
74-
return _convert_with_cairosvg(svg_path, pdf_path)
76+
return _convert_with_cairosvg(svg_path, pdf_path)
7577

7678

7779
def _convert_with_cairosvg(svg_path, pdf_path):
7880
"""Convert SVG to PDF using CairoSVG."""
79-
try:
80-
# Only import cairosvg when needed
81-
from cairosvg import svg2pdf
82-
83-
with open(svg_path, 'rb') as svg_file:
84-
svg2pdf(file_obj=svg_file, write_to=pdf_path)
85-
86-
return pdf_path
87-
except ImportError:
81+
if not CAIROSVG_AVAILABLE:
8882
raise ImportError(
8983
"CairoSVG is not installed. Please install it with 'pip install cairosvg' "
9084
"or set svg2pdf_backend to 'inkscape' in your config."
9185
)
9286

87+
with open(svg_path, "rb") as svg_file:
88+
svg2pdf(file_obj=svg_file, write_to=pdf_path)
89+
90+
return pdf_path
91+
9392

9493
def _convert_with_inkscape(svg_path, pdf_path):
9594
"""Convert SVG to PDF using Inkscape."""
@@ -103,8 +102,8 @@ def _convert_with_inkscape(svg_path, pdf_path):
103102
check=True,
104103
)
105104
return pdf_path
106-
except (subprocess.SubprocessError, FileNotFoundError):
105+
except (subprocess.SubprocessError, FileNotFoundError) as exc:
107106
raise RuntimeError(
108-
"Inkscape command failed. Please ensure Inkscape is installed and in your PATH "
109-
"or set svg2pdf_backend to 'cairosvg' in your config."
110-
)
107+
"Inkscape command failed. Please ensure Inkscape is installed "
108+
"and in your PATH or set svg2pdf_backend to 'cairosvg' in your config."
109+
) from exc

src/pdfbaker/render.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def encode_image(filename, images_dir):
152152
image_path = os.path.join(images_dir, filename)
153153
if not os.path.exists(image_path):
154154
raise FileNotFoundError(f"Image not found: {image_path}")
155-
155+
156156
with open(image_path, "rb") as f:
157157
binary_fc = f.read()
158158
base64_utf8_str = base64.b64encode(binary_fc).decode("utf-8")

0 commit comments

Comments
 (0)