Skip to content

Commit a10e05e

Browse files
committed
Refactor to handle custom locations properly
1 parent d5863f2 commit a10e05e

7 files changed

Lines changed: 210 additions & 142 deletions

File tree

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
title: "Custom Location Example"
22
description: "This page uses custom directory structure"
3-
template: "../other_templates/custom_page.svg.j2"
3+
template:
4+
# If you just wrote this directly it would be relative to the templates directory
5+
# We want it to be relative to the config file:
6+
path: "../other_templates/custom_page.svg.j2"
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
title: "Custom Locations Example"
2-
filename: "custom_locations_custom"
1+
title: Custom Locations Example
2+
filename: custom_locations_custom
33
pages:
44
# Simple notation - uses conventional location (pages/standard_page.yaml)
5-
- "standard_page"
5+
- standard_page
66
# Path notation - uses custom location
7-
- "../other_pages/custom_page.yaml"
7+
- path: ../other_pages/custom_page.yaml
88
# Custom images directory name and location
9-
images_dir: "../your_images"
9+
images_dir: ../your_images

src/pdfbaker/baker.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,13 @@
1919
__all__ = ["PDFBaker", "PDFBakerOptions"]
2020

2121

22-
DEFAULT_CONFIG = {
22+
DEFAULT_BAKER_CONFIG = {
2323
# Default to directories relative to the config file
24-
"documents_dir": ".",
25-
"pages_dir": "pages",
26-
"templates_dir": "templates",
27-
"images_dir": "images",
28-
"build_dir": "build",
29-
"dist_dir": "dist",
24+
"directories": {
25+
"documents": ".",
26+
"build": "build",
27+
"dist": "dist",
28+
},
3029
}
3130

3231

@@ -58,15 +57,16 @@ def __init__(
5857
) -> None:
5958
"""Initialize baker configuration (needs documents)."""
6059
self.baker = baker
61-
self.baker.log_debug_section("Loading main configuration: %s", config_file)
6260
super().__init__(base_config, config_file)
61+
self.baker.log_trace_section("Main configuration: %s", config_file)
6362
self.baker.log_trace(self.pretty())
6463
if "documents" not in self:
6564
raise ConfigurationError(
6665
'Key "documents" missing - is this the main configuration file?'
6766
)
6867
self.documents = [
69-
self.resolve_path(doc_spec) for doc_spec in self["documents"]
68+
self.resolve_path(doc_spec, directory=self["directories"]["documents"])
69+
for doc_spec in self["documents"]
7070
]
7171

7272
def __init__(
@@ -93,9 +93,11 @@ def __init__(
9393
else:
9494
logging.getLogger().setLevel(logging.INFO)
9595
self.keep_build = options.keep_build
96+
base_config = DEFAULT_BAKER_CONFIG.copy()
97+
base_config["directories"]["config"] = config_file.parent.resolve()
9698
self.config = self.Configuration(
9799
baker=self,
98-
base_config=DEFAULT_CONFIG,
100+
base_config=base_config,
99101
config_file=config_file,
100102
)
101103

@@ -110,7 +112,7 @@ def bake(self) -> None:
110112
doc = PDFBakerDocument(
111113
baker=self,
112114
base_config=self.config,
113-
config=doc_config,
115+
config_path=doc_config,
114116
)
115117
pdf_files, error_message = doc.process_document()
116118
if pdf_files is None:

src/pdfbaker/config.py

Lines changed: 84 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from .errors import ConfigurationError
1212
from .types import PathSpec
1313

14-
__all__ = ["PDFBakerConfiguration"]
14+
__all__ = ["PDFBakerConfiguration", "deep_merge", "render_config"]
1515

1616
logger = logging.getLogger(__name__)
1717

@@ -51,25 +51,44 @@ class PDFBakerConfiguration(dict):
5151
def __init__(
5252
self,
5353
base_config: dict[str, Any],
54-
config: Path,
54+
config_file: Path,
5555
) -> None:
5656
"""Initialize configuration from a file.
5757
5858
Args:
5959
base_config: Existing base configuration
6060
config: Path to YAML file to merge with base_config
6161
"""
62-
self.directory = config.parent
63-
super().__init__(deep_merge(base_config, self._load_config(config)))
64-
65-
def _load_config(self, config_file: Path) -> dict[str, Any]:
66-
"""Load configuration from a file."""
6762
try:
6863
with open(config_file, encoding="utf-8") as f:
69-
return yaml.safe_load(f)
64+
config = yaml.safe_load(f)
7065
except Exception as exc:
7166
raise ConfigurationError(f"Failed to load config file: {exc}") from exc
7267

68+
# Determine all relevant directories
69+
directories = {"config": config_file.parent.resolve()}
70+
for directory in (
71+
"documents",
72+
"pages",
73+
"templates",
74+
"images",
75+
"build",
76+
"dist",
77+
):
78+
if directory in config.get("directories", {}):
79+
# Set in this config file
80+
directories[directory] = self.resolve_path(
81+
config["directories"][directory]
82+
)
83+
elif directory in base_config.get("directories", {}):
84+
# Inherited or not yet relevant/mentioned
85+
directories[directory] = self.resolve_path(
86+
str(base_config["directories"][directory]),
87+
directory=base_config["directories"]["config"],
88+
)
89+
super().__init__(deep_merge(base_config, config))
90+
self["directories"] = directories
91+
7392
def resolve_path(self, spec: PathSpec, directory: Path | None = None) -> Path:
7493
"""Resolve a possibly relative path specification.
7594
@@ -79,7 +98,7 @@ def resolve_path(self, spec: PathSpec, directory: Path | None = None) -> Path:
7998
Returns:
8099
Resolved Path object
81100
"""
82-
directory = directory or self.directory
101+
directory = directory or self["directories"]["config"]
83102
if isinstance(spec, str):
84103
return directory / spec
85104

@@ -91,35 +110,63 @@ def resolve_path(self, spec: PathSpec, directory: Path | None = None) -> Path:
91110

92111
return directory / spec["name"]
93112

94-
def render(self) -> dict[str, Any]:
95-
"""Resolve all template strings in config using its own values.
96-
97-
This allows the use of "{{ variant }}" in the "filename" etc.
98-
99-
Returns:
100-
Resolved configuration dictionary
101-
102-
Raises:
103-
ConfigurationError: If maximum number of iterations is reached
104-
(circular references)
105-
"""
106-
max_iterations = 10
107-
config = self
108-
for _ in range(max_iterations):
109-
config_yaml = Template(yaml.dump(config))
110-
resolved_yaml = config_yaml.render(**config)
111-
new_config = yaml.safe_load(resolved_yaml)
112-
113-
if new_config == config: # No more changes
114-
return new_config
115-
config = new_config
116-
117-
raise ConfigurationError(
118-
"Maximum number of iterations reached. "
119-
"Check for circular references in your configuration."
120-
)
121-
122113
def pretty(self, max_string=60) -> str:
123114
"""Return readable presentation (for debugging)."""
124115
truncated = _truncate_strings(self, max_string)
125116
return pprint.pformat(truncated, indent=2)
117+
118+
119+
def _convert_paths_to_strings(config: dict[str, Any]) -> dict[str, Any]:
120+
"""Convert all Path objects in config to strings."""
121+
result = {}
122+
for key, value in config.items():
123+
if isinstance(value, Path):
124+
result[key] = str(value)
125+
elif isinstance(value, dict):
126+
result[key] = _convert_paths_to_strings(value)
127+
elif isinstance(value, list):
128+
result[key] = [
129+
_convert_paths_to_strings(item)
130+
if isinstance(item, dict)
131+
else str(item)
132+
if isinstance(item, Path)
133+
else item
134+
for item in value
135+
]
136+
else:
137+
result[key] = value
138+
return result
139+
140+
141+
def render_config(config: dict[str, Any]) -> dict[str, Any]:
142+
"""Resolve all template strings in config using its own values.
143+
144+
This allows the use of "{{ variant }}" in the "filename" etc.
145+
146+
Args:
147+
config: Configuration dictionary to render
148+
149+
Returns:
150+
Resolved configuration dictionary
151+
152+
Raises:
153+
ConfigurationError: If maximum number of iterations is reached
154+
(circular references)
155+
"""
156+
max_iterations = 10
157+
current_config = dict(config)
158+
current_config = _convert_paths_to_strings(current_config)
159+
160+
for _ in range(max_iterations):
161+
config_yaml = Template(yaml.dump(current_config))
162+
resolved_yaml = config_yaml.render(**current_config)
163+
new_config = yaml.safe_load(resolved_yaml)
164+
165+
if new_config == current_config: # No more changes
166+
return new_config
167+
current_config = new_config
168+
169+
raise ConfigurationError(
170+
"Maximum number of iterations reached. "
171+
"Check for circular references in your configuration."
172+
)

0 commit comments

Comments
 (0)