23
23
import logging
24
24
import os
25
25
import platform
26
+ import regex
26
27
import requests
27
28
import site
28
29
import sys
29
-
30
30
import yaml
31
+
31
32
from colorama import Fore , Style
32
33
from importlib .metadata import distributions , version
33
34
from io import BytesIO
34
35
from markdown .extensions .toc import slugify
35
36
from mkdocs .config .defaults import MkDocsConfig
36
37
from mkdocs .plugins import BasePlugin , event_priority
37
38
from mkdocs .utils import get_yaml_loader
38
- import regex
39
39
from zipfile import ZipFile , ZIP_DEFLATED
40
40
41
41
from .config import InfoConfig
@@ -110,21 +110,24 @@ def on_config(self, config):
110
110
log .error ("Please remove 'hooks' setting." )
111
111
self ._help_on_customizations_and_exit ()
112
112
113
- # Assure that possible relative paths, which will be validated
114
- # or used to generate other paths are absolute.
113
+ # Assure all paths that will be validated are absolute. Convert possible
114
+ # relative config_file_path to absolute. Its absolute directory path is
115
+ # being later used to resolve other paths.
115
116
config .config_file_path = _convert_to_abs (config .config_file_path )
116
117
config_file_parent = os .path .dirname (config .config_file_path )
117
118
118
- # The theme.custom_dir property cannot be set, therefore a helper
119
- # variable is used.
120
- custom_dir = config .theme .custom_dir
121
- if custom_dir :
122
- custom_dir = _convert_to_abs (
123
- custom_dir ,
119
+ # Convert relative custom_dir path to absolute. The Theme.custom_dir
120
+ # property cannot be set, therefore a helper variable is used.
121
+ if config .theme .custom_dir :
122
+ abs_custom_dir = _convert_to_abs (
123
+ config .theme .custom_dir ,
124
124
abs_prefix = config_file_parent
125
125
)
126
+ else :
127
+ abs_custom_dir = ""
126
128
127
- # Support projects plugin
129
+ # Extract the absolute path to projects plugin's directory to explicitly
130
+ # support path validation and dynamic exclusion for the plugin
128
131
projects_plugin = config .plugins .get ("material/projects" )
129
132
if projects_plugin :
130
133
abs_projects_dir = _convert_to_abs (
@@ -134,29 +137,39 @@ def on_config(self, config):
134
137
else :
135
138
abs_projects_dir = ""
136
139
137
- # Load the current MkDocs config(s) to get access to INHERIT
140
+ # MkDocs removes the INHERIT configuration key during load, and doesn't
141
+ # expose the information in any way, as the parent configuration is
142
+ # merged into one. To validate that the INHERIT config file will be
143
+ # included in the ZIP file the current config file must be loaded again
144
+ # without parsing. Each file can have their own INHERIT key, so a list
145
+ # of configurations is supported. The INHERIT path is converted during
146
+ # load to absolute.
138
147
loaded_configs = _load_yaml (config .config_file_path )
139
148
if not isinstance (loaded_configs , list ):
140
149
loaded_configs = [loaded_configs ]
141
150
142
- # Validate different MkDocs paths to assure that
143
- # they're children of the current working directory.
151
+ # We need to make sure the user put every file in the current working
152
+ # directory. To assure the reproduction inside the ZIP file can be run,
153
+ # validate that the MkDocs paths are children of the current root.
144
154
paths_to_validate = [
145
155
config .config_file_path ,
146
156
config .docs_dir ,
147
- custom_dir or "" ,
157
+ abs_custom_dir ,
148
158
abs_projects_dir ,
149
159
* [cfg .get ("INHERIT" , "" ) for cfg in loaded_configs ]
150
160
]
151
161
162
+ # Convert relative hook paths to absolute path
152
163
for hook in config .hooks :
153
164
path = _convert_to_abs (hook , abs_prefix = config_file_parent )
154
165
paths_to_validate .append (path )
155
166
167
+ # Remove valid paths from the list
156
168
for path in list (paths_to_validate ):
157
169
if not path or path .startswith (os .getcwd ()):
158
170
paths_to_validate .remove (path )
159
171
172
+ # Report the invalid paths to the user
160
173
if paths_to_validate :
161
174
log .error (f"One or more paths aren't children of root" )
162
175
self ._help_on_not_in_cwd (paths_to_validate )
@@ -198,26 +211,36 @@ def on_config(self, config):
198
211
files : list [str ] = []
199
212
with ZipFile (archive , "a" , ZIP_DEFLATED , False ) as f :
200
213
for abs_root , dirnames , filenames in os .walk (os .getcwd ()):
201
- # Prune the folders in-place to prevent
202
- # scanning excluded folders
214
+ # Prune the folders in-place to prevent their processing
203
215
for name in list (dirnames ):
216
+ # Resolve the absolute directory path
204
217
path = os .path .join (abs_root , name )
218
+
219
+ # Exclude the directory and all subdirectories
205
220
if self ._is_excluded (_resolve_pattern (path )):
206
221
dirnames .remove (name )
207
222
continue
208
- # Multi-language setup from #2346 separates the
209
- # language config, so each mkdocs.yml file is
210
- # unaware of other site_dir directories. Therefore,
211
- # we add this with the assumption a site_dir contains
212
- # the sitemap file.
223
+
224
+ # Projects, which don't use the projects plugin for
225
+ # multi-language support could have separate build folders
226
+ # for each config file or language. Therefore, we exclude
227
+ # them with the assumption a site_dir contains the sitemap
228
+ # file. Example of such a setup: https://t.ly/DLQcy
213
229
sitemap_gz = os .path .join (path , "sitemap.xml.gz" )
214
230
if os .path .exists (sitemap_gz ):
215
231
log .debug (f"Excluded site_dir: { path } " )
216
232
dirnames .remove (name )
233
+
234
+ # Write files to the in-memory archive
217
235
for name in filenames :
236
+ # Resolve the absolute file path
218
237
path = os .path .join (abs_root , name )
238
+
239
+ # Exclude the file
219
240
if self ._is_excluded (_resolve_pattern (path )):
220
241
continue
242
+
243
+ # Resolve the relative path to create a matching structure
221
244
path = os .path .relpath (path , os .path .curdir )
222
245
f .write (path , os .path .join (example , path ))
223
246
@@ -320,27 +343,31 @@ def _help_on_customizations_and_exit(self):
320
343
print (Style .NORMAL )
321
344
print (" - extra_css" )
322
345
print (" - extra_javascript" )
346
+ print (Fore .YELLOW )
347
+ print (" If you're using customizations from the theme's documentation" )
348
+ print (" and you want to report a bug specific to those customizations" )
349
+ print (" then set the 'archive_stop_on_violation: false' option in the" )
350
+ print (" info plugin config." )
323
351
print (Style .RESET_ALL )
324
352
325
353
# Exit, unless explicitly told not to
326
354
if self .config .archive_stop_on_violation :
327
355
sys .exit (1 )
328
356
329
357
# Print help on not in current working directory and exit
330
- def _help_on_not_in_cwd (self , bad_paths ):
358
+ def _help_on_not_in_cwd (self , outside_root ):
331
359
print (Fore .RED )
332
360
print (" The current working (root) directory:\n " )
333
361
print (f" { os .getcwd ()} \n " )
334
362
print (" is not a parent of the following paths:" )
335
363
print (Style .NORMAL )
336
- for path in bad_paths :
364
+ for path in outside_root :
337
365
print (f" { path } " )
338
- print ()
339
- print (" To assure that all project files are found" )
340
- print (" please adjust your config or file structure and" )
341
- print (" put everything within the root directory of the project.\n " )
342
- print (" Please also make sure `mkdocs build` is run in" )
343
- print (" the actual root directory of the project." )
366
+ print (" \n To assure that all project files are found please adjust" )
367
+ print (" your config or file structure and put everything within the" )
368
+ print (" root directory of the project.\n " )
369
+ print (" Please also make sure `mkdocs build` is run in the actual" )
370
+ print (" root directory of the project." )
344
371
print (Style .RESET_ALL )
345
372
346
373
# Exit, unless explicitly told not to
@@ -370,17 +397,19 @@ def _size(value, factor = 1):
370
397
return f"{ color } { value :3.1f} { unit } "
371
398
value /= 1000.0
372
399
373
- # To validate if a file is within the file tree,
374
- # it needs to be absolute, so that it is possible to
400
+ # Get the absolute path with set prefix. To validate if a file is inside the
401
+ # current working directory it needs to be absolute, so that it is possible to
375
402
# check the prefix.
376
403
def _convert_to_abs (path : str , abs_prefix : str = None ) -> str :
377
404
if os .path .isabs (path ): return path
378
405
if abs_prefix is None : abs_prefix = os .getcwd ()
379
406
return os .path .normpath (os .path .join (abs_prefix , path ))
380
407
381
- # Custom YAML loader - required to handle the parent INHERIT config.
382
- # It converts the INHERIT path to absolute as a side effect.
383
- # Returns the loaded config, or a list of all loaded configs.
408
+ # Get the loaded config, or a list with all loaded configs. MkDocs removes the
409
+ # INHERIT configuration key during load, and doesn't expose the information in
410
+ # any way, as the parent configuration is merged into one. The INHERIT path is
411
+ # needed for validation. This custom YAML loader replicates MkDocs' loading
412
+ # logic. Side effect: It converts the INHERIT path to absolute.
384
413
def _load_yaml (abs_src_path : str ):
385
414
386
415
with open (abs_src_path , "r" , encoding = "utf-8-sig" ) as file :
@@ -417,11 +446,9 @@ def _load_exclusion_patterns(path: str = None):
417
446
418
447
return [line for line in lines if line and not line .startswith ("#" )]
419
448
420
- # For the pattern matching it is best to remove the CWD
421
- # prefix and keep only the relative root of the reproduction.
422
- # Additionally, as the patterns are in POSIX format,
423
- # assure that the path is also in POSIX format.
424
- # Side-effect: It appends "/" for directory patterns.
449
+ # Get a normalized POSIX path for the pattern matching with removed current
450
+ # working directory prefix. Directory paths end with a '/' to allow more control
451
+ # in the pattern creation for files and directories.
425
452
def _resolve_pattern (abspath : str ):
426
453
path = abspath .replace (os .getcwd (), "" , 1 ).replace (os .sep , "/" )
427
454
@@ -434,7 +461,7 @@ def _resolve_pattern(abspath: str):
434
461
435
462
return path
436
463
437
- # Get project configuration
464
+ # Get project configuration with resolved absolute paths for validation
438
465
def _get_project_config (project_config_file : str ):
439
466
with open (project_config_file , encoding = "utf-8" ) as file :
440
467
config = MkDocsConfig (config_file_path = project_config_file )
0 commit comments