Skip to content

Commit 2d39824

Browse files
Fixed info plugin's exclusion logic (#6874)
Added more information to platform file Added processed directory indicator Changed slash removal in resolved patterns Changed regex match function to search to support subprojects Constrained dynamic patterns to the root Reverted build assets gathering dc808ca Reverted info.gitignore file 79129d5
1 parent f325238 commit 2d39824

File tree

7 files changed

+169
-157
lines changed

7 files changed

+169
-157
lines changed

Diff for: material/plugins/info/info.gitignore

-37
This file was deleted.

Diff for: material/plugins/info/patterns.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
def get_exclusion_patterns():
2+
"""
3+
Regex patterns, which will be compared against directory and file names
4+
case-sensitively. https://docs.python.org/3/library/re.html#re.search is the
5+
matching function and scans the whole string to find any pattern match. Used
6+
with the https://pypi.org/project/regex/ module.
7+
8+
Additional remarks for pattern creation:
9+
- The compared paths will be always in POSIX format.
10+
- Each directory path will have a / at the end to allow to distinguish them
11+
from files.
12+
- Patterns for dynamic or custom paths like Virtual Environments (venv) or
13+
build site directories are created during plugin runtime.
14+
"""
15+
return [
16+
r"/__pycache__/", # Python cache directory
17+
18+
r"/\.DS_Store$", # macOS
19+
20+
r"/[^/]+\.zip$", # Generated files and folders
21+
22+
r"/[^/]*\.cache($|/)", # .cache files and folders
23+
24+
r"/\.vscode/", # Common autogenerated IDE directories
25+
r"/\.vs/",
26+
r"/\.idea/",
27+
]

Diff for: material/plugins/info/plugin.py

+57-41
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from zipfile import ZipFile, ZIP_DEFLATED
4040

4141
from .config import InfoConfig
42+
from .patterns import get_exclusion_patterns
4243

4344
# -----------------------------------------------------------------------------
4445
# Classes
@@ -56,6 +57,7 @@ def __init__(self, *args, **kwargs):
5657

5758
# Initialize empty members
5859
self.exclusion_patterns = []
60+
self.excluded_entries = []
5961

6062
# Determine whether we're serving the site
6163
def on_startup(self, *, command, dirty):
@@ -183,15 +185,18 @@ def on_config(self, config):
183185
example, _ = os.path.splitext(example)
184186
example = "-".join([present, slugify(example, "-")])
185187

186-
# Load exclusion patterns
187-
self.exclusion_patterns = _load_exclusion_patterns()
188+
# Get local copy of the exclusion patterns
189+
self.exclusion_patterns = get_exclusion_patterns()
190+
self.excluded_entries = []
188191

189192
# Exclude the site_dir at project root
190193
if config.site_dir.startswith(os.getcwd()):
191194
self.exclusion_patterns.append(_resolve_pattern(config.site_dir))
192195

193-
# Exclude the site-packages directory
194-
for path in site.getsitepackages():
196+
# Exclude the Virtual Environment directory. site.getsitepackages() has
197+
# inconsistent results across operating systems, and relies on the
198+
# PREFIXES that will contain the absolute path to the activated venv.
199+
for path in site.PREFIXES:
195200
if path.startswith(os.getcwd()):
196201
self.exclusion_patterns.append(_resolve_pattern(path))
197202

@@ -211,24 +216,17 @@ def on_config(self, config):
211216
files: list[str] = []
212217
with ZipFile(archive, "a", ZIP_DEFLATED, False) as f:
213218
for abs_root, dirnames, filenames in os.walk(os.getcwd()):
219+
# Set and print progress indicator
220+
indicator = f"Processing: {abs_root}"
221+
print(indicator, end="\r", flush=True)
222+
214223
# Prune the folders in-place to prevent their processing
215224
for name in list(dirnames):
216225
# Resolve the absolute directory path
217226
path = os.path.join(abs_root, name)
218227

219228
# Exclude the directory and all subdirectories
220-
if self._is_excluded(_resolve_pattern(path)):
221-
dirnames.remove(name)
222-
continue
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
229-
sitemap_gz = os.path.join(path, "sitemap.xml.gz")
230-
if os.path.exists(sitemap_gz):
231-
log.debug(f"Excluded site_dir: {path}")
229+
if self._is_excluded(path):
232230
dirnames.remove(name)
233231

234232
# Write files to the in-memory archive
@@ -237,13 +235,16 @@ def on_config(self, config):
237235
path = os.path.join(abs_root, name)
238236

239237
# Exclude the file
240-
if self._is_excluded(_resolve_pattern(path)):
238+
if self._is_excluded(path):
241239
continue
242240

243241
# Resolve the relative path to create a matching structure
244242
path = os.path.relpath(path, os.path.curdir)
245243
f.write(path, os.path.join(example, path))
246244

245+
# Clear the line for the next indicator
246+
print(" " * len(indicator), end="\r", flush=True)
247+
247248
# Add information on installed packages
248249
f.writestr(
249250
os.path.join(example, "requirements.lock.txt"),
@@ -261,11 +262,14 @@ def on_config(self, config):
261262
"system": platform.platform(),
262263
"architecture": platform.architecture(),
263264
"python": platform.python_version(),
265+
"cwd": os.getcwd(),
264266
"command": " ".join([
265267
sys.argv[0].rsplit(os.sep, 1)[-1],
266268
*sys.argv[1:]
267269
]),
268-
"sys.path": sys.path
270+
"env:$PYTHONPATH": os.getenv("PYTHONPATH", ""),
271+
"sys.path": sys.path,
272+
"excluded_entries": self.excluded_entries
269273
},
270274
default = str,
271275
indent = 2
@@ -363,24 +367,45 @@ def _help_on_not_in_cwd(self, outside_root):
363367
print(Style.NORMAL)
364368
for path in outside_root:
365369
print(f" {path}")
366-
print(" \nTo assure that all project files are found please adjust")
370+
print("\n To assure that all project files are found please adjust")
367371
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")
372+
print(" root directory of the project.")
373+
print("\n Please also make sure `mkdocs build` is run in the actual")
370374
print(" root directory of the project.")
371375
print(Style.RESET_ALL)
372376

373377
# Exit, unless explicitly told not to
374378
if self.config.archive_stop_on_violation:
375379
sys.exit(1)
376380

377-
# Exclude files which we don't want in our zip file
378-
def _is_excluded(self, posix_path: str) -> bool:
381+
# Check if path is excluded and should be omitted from the zip. Use pattern
382+
# matching for files and folders, and lookahead specific files in folders to
383+
# skip them. Side effect: Save excluded paths to save them in the zip file.
384+
def _is_excluded(self, abspath: str) -> bool:
385+
386+
# Resolve the path into POSIX format to match the patterns
387+
pattern_path = _resolve_pattern(abspath, return_path = True)
388+
379389
for pattern in self.exclusion_patterns:
380-
if regex.match(pattern, posix_path):
381-
log.debug(f"Excluded pattern '{pattern}': {posix_path}")
390+
if regex.search(pattern, pattern_path):
391+
log.debug(f"Excluded pattern '{pattern}': {abspath}")
392+
self.excluded_entries.append(f"{pattern} - {pattern_path}")
382393
return True
383394

395+
# File exclusion should be limited to pattern matching
396+
if os.path.isfile(abspath):
397+
return False
398+
399+
# Projects, which don't use the projects plugin for multi-language
400+
# support could have separate build folders for each config file or
401+
# language. Therefore, we exclude them with the assumption a site_dir
402+
# contains the sitemap file. Example of such a setup: https://t.ly/DLQcy
403+
sitemap_gz = os.path.join(abspath, "sitemap.xml.gz")
404+
if os.path.exists(sitemap_gz):
405+
log.debug(f"Excluded site_dir: {abspath}")
406+
self.excluded_entries.append(f"sitemap.xml.gz - {pattern_path}")
407+
return True
408+
384409
return False
385410

386411
# -----------------------------------------------------------------------------
@@ -435,31 +460,22 @@ def _load_yaml(abs_src_path: str):
435460

436461
return result
437462

438-
# Load info.gitignore, ignore any empty lines or # comments
439-
def _load_exclusion_patterns(path: str = None):
440-
if path is None:
441-
path = os.path.dirname(os.path.abspath(__file__))
442-
path = os.path.join(path, "info.gitignore")
443-
444-
with open(path, encoding = "utf-8") as file:
445-
lines = map(str.strip, file.readlines())
446-
447-
return [line for line in lines if line and not line.startswith("#")]
448-
449463
# Get a normalized POSIX path for the pattern matching with removed current
450464
# working directory prefix. Directory paths end with a '/' to allow more control
451-
# in the pattern creation for files and directories.
452-
def _resolve_pattern(abspath: str):
453-
path = abspath.replace(os.getcwd(), "", 1).replace(os.sep, "/")
465+
# in the pattern creation for files and directories. The patterns are matched
466+
# using the search function, so they are prefixed with ^ for specificity.
467+
def _resolve_pattern(abspath: str, return_path: bool = False):
468+
path = abspath.replace(os.getcwd(), "", 1)
469+
path = path.replace(os.sep, "/").rstrip("/")
454470

455471
if not path:
456472
return "/"
457473

458474
# Check abspath, as the file needs to exist
459475
if not os.path.isfile(abspath):
460-
return path.rstrip("/") + "/"
476+
path = path + "/"
461477

462-
return path
478+
return path if return_path else f"^{path}"
463479

464480
# Get project configuration with resolved absolute paths for validation
465481
def _get_project_config(project_config_file: str):

Diff for: src/plugins/info/info.gitignore

-37
This file was deleted.

Diff for: src/plugins/info/patterns.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
def get_exclusion_patterns():
2+
"""
3+
Regex patterns, which will be compared against directory and file names
4+
case-sensitively. https://docs.python.org/3/library/re.html#re.search is the
5+
matching function and scans the whole string to find any pattern match. Used
6+
with the https://pypi.org/project/regex/ module.
7+
8+
Additional remarks for pattern creation:
9+
- The compared paths will be always in POSIX format.
10+
- Each directory path will have a / at the end to allow to distinguish them
11+
from files.
12+
- Patterns for dynamic or custom paths like Virtual Environments (venv) or
13+
build site directories are created during plugin runtime.
14+
"""
15+
return [
16+
r"/__pycache__/", # Python cache directory
17+
18+
r"/\.DS_Store$", # macOS
19+
20+
r"/[^/]+\.zip$", # Generated files and folders
21+
22+
r"/[^/]*\.cache($|/)", # .cache files and folders
23+
24+
r"/\.vscode/", # Common autogenerated IDE directories
25+
r"/\.vs/",
26+
r"/\.idea/",
27+
]

0 commit comments

Comments
 (0)