1111from .errors import ConfigurationError
1212from .types import PathSpec
1313
14- __all__ = ["PDFBakerConfiguration" ]
14+ __all__ = ["PDFBakerConfiguration" , "deep_merge" , "render_config" ]
1515
1616logger = 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