zensical 0.0.9__cp310-abi3-musllinux_1_2_i686.whl → 0.0.12__cp310-abi3-musllinux_1_2_i686.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of zensical might be problematic. Click here for more details.

zensical/__init__.py CHANGED
@@ -21,8 +21,8 @@
21
21
  # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
22
22
  # IN THE SOFTWARE.
23
23
 
24
- from .zensical import *
24
+ from zensical.zensical import * # noqa: F403
25
25
 
26
- __doc__ = zensical.__doc__
27
- if hasattr(zensical, "__all__"):
28
- __all__ = zensical.__all__
26
+ __doc__ = zensical.__doc__ # type: ignore[name-defined] # noqa: F405
27
+ if hasattr(zensical, "__all__"): # type: ignore[name-defined] # noqa: F405
28
+ __all__ = zensical.__all__ # type: ignore[name-defined] # noqa: F405
@@ -26,3 +26,4 @@ jobs:
26
26
  with:
27
27
  path: site
28
28
  - uses: actions/deploy-pages@v4
29
+ id: deployment
@@ -57,7 +57,7 @@ Copyright © 2025 The authors
57
57
  #
58
58
  # Read more: https://zensical.org/docs/customization/#additional-css
59
59
  #
60
- #extra_css = ["assets/stylesheets/extra.css"]
60
+ #extra_css = ["stylesheets/extra.css"]
61
61
 
62
62
  # With the `extra_javascript` option you can add your own JavaScript to your
63
63
  # project to customize the behavior according to your needs.
@@ -65,7 +65,7 @@ Copyright © 2025 The authors
65
65
  # The path provided should be relative to the "docs_dir".
66
66
  #
67
67
  # Read more: https://zensical.org/docs/customization/#additional-javascript
68
- #extra_javascript = ["assets/javascript/extra.js"]
68
+ #extra_javascript = ["javascripts/extra.js"]
69
69
 
70
70
  # ----------------------------------------------------------------------------
71
71
  # Section for configuring theme options
@@ -93,7 +93,7 @@ Copyright © 2025 The authors
93
93
  # - https://zensical.org/docs/setup/logo-and-icons/#favicon
94
94
  # - https://developer.mozilla.org/en-US/docs/Glossary/Favicon
95
95
  #
96
- #favicon = "assets/images/favicon.png"
96
+ #favicon = "images/favicon.png"
97
97
 
98
98
  # Zensical supports more than 60 different languages. This means that the
99
99
  # labels and tooltips that Zensical's templates produce are translated.
zensical/config.py CHANGED
@@ -27,21 +27,22 @@ import hashlib
27
27
  import importlib
28
28
  import os
29
29
  import pickle
30
- import yaml
31
-
32
- try:
33
- import tomllib
34
- except ModuleNotFoundError:
35
- import tomli as tomllib # type: ignore
30
+ from typing import IO, Any
31
+ from urllib.parse import urlparse
36
32
 
33
+ import yaml
37
34
  from click import ClickException
38
35
  from deepmerge import always_merger
39
- from typing import Any, IO
40
36
  from yaml import BaseLoader, Loader, YAMLError
41
37
  from yaml.constructor import ConstructorError
42
- from urllib.parse import urlparse
43
38
 
44
- from .extensions.emoji import to_svg, twemoji
39
+ from zensical.extensions.emoji import to_svg, twemoji
40
+
41
+ try:
42
+ import tomllib
43
+ except ModuleNotFoundError:
44
+ import tomli as tomllib # type: ignore[no-redef]
45
+
45
46
 
46
47
  # ----------------------------------------------------------------------------
47
48
  # Globals
@@ -64,9 +65,7 @@ side, and use it directly when needed. It's a hack but will do for now.
64
65
 
65
66
 
66
67
  class ConfigurationError(ClickException):
67
- """
68
- Configuration resolution or validation failed.
69
- """
68
+ """Configuration resolution or validation failed."""
70
69
 
71
70
 
72
71
  # ----------------------------------------------------------------------------
@@ -75,22 +74,17 @@ class ConfigurationError(ClickException):
75
74
 
76
75
 
77
76
  def parse_config(path: str) -> dict:
78
- """
79
- Parse configuration file.
80
- """
77
+ """Parse configuration file."""
81
78
  # Decide by extension; no need to convert to Path
82
79
  _, ext = os.path.splitext(path)
83
80
  if ext.lower() == ".toml":
84
81
  return parse_zensical_config(path)
85
- else:
86
- return parse_mkdocs_config(path)
82
+ return parse_mkdocs_config(path)
87
83
 
88
84
 
89
85
  def parse_zensical_config(path: str) -> dict:
90
- """
91
- Parse zensical.toml configuration file.
92
- """
93
- global _CONFIG
86
+ """Parse zensical.toml configuration file."""
87
+ global _CONFIG # noqa: PLW0603
94
88
  with open(path, "rb") as f:
95
89
  config = tomllib.load(f)
96
90
  if "project" in config:
@@ -102,11 +96,9 @@ def parse_zensical_config(path: str) -> dict:
102
96
 
103
97
 
104
98
  def parse_mkdocs_config(path: str) -> dict:
105
- """
106
- Parse mkdocs.yml configuration file.
107
- """
108
- global _CONFIG
109
- with open(path, "r") as f:
99
+ """Parse mkdocs.yml configuration file."""
100
+ global _CONFIG # noqa: PLW0603
101
+ with open(path, encoding="utf-8") as f:
110
102
  config = _yaml_load(f)
111
103
 
112
104
  # Apply defaults and return parsed configuration
@@ -114,24 +106,20 @@ def parse_mkdocs_config(path: str) -> dict:
114
106
  return _CONFIG
115
107
 
116
108
 
117
- def get_config():
118
- """
119
- Return configuration.
120
- """
121
- return _CONFIG
109
+ def get_config() -> dict:
110
+ """Return configuration."""
111
+ # We assume this function is only called after populating `_CONFIG`.
112
+ return _CONFIG # type: ignore[return-value]
122
113
 
123
114
 
124
115
  def get_theme_dir() -> str:
125
- """
126
- Return the theme directory.
127
- """
116
+ """Return the theme directory."""
128
117
  path = os.path.dirname(os.path.abspath(__file__))
129
118
  return os.path.join(path, "templates")
130
119
 
131
120
 
132
121
  def _apply_defaults(config: dict, path: str) -> dict:
133
- """
134
- Apply default settings in configuration.
122
+ """Apply default settings in configuration.
135
123
 
136
124
  Note that this is loosely based on the defaults that MkDocs sets in its own
137
125
  configuration system, which we won't port for compatibility right now, as
@@ -146,12 +134,12 @@ def _apply_defaults(config: dict, path: str) -> dict:
146
134
 
147
135
  # Set site directory
148
136
  set_default(config, "site_dir", "site", str)
149
- if ".." in config.get("site_dir"):
137
+ if ".." in config.get("site_dir", ""):
150
138
  raise ConfigurationError("site_dir must not contain '..'")
151
139
 
152
140
  # Set docs directory
153
141
  set_default(config, "docs_dir", "docs", str)
154
- if ".." in config.get("docs_dir"):
142
+ if ".." in config.get("docs_dir", ""):
155
143
  raise ConfigurationError("docs_dir must not contain '..'")
156
144
 
157
145
  # Set defaults for core settings
@@ -169,21 +157,26 @@ def _apply_defaults(config: dict, path: str) -> dict:
169
157
  set_default(config, "edit_uri", None, str)
170
158
 
171
159
  # Set defaults for repository name settings
160
+ docs_dir = config.get("docs_dir")
161
+ repo_names = {
162
+ "github.com": "GitHub",
163
+ "gitlab.com": "Gitlab",
164
+ "bitbucket.org": "Bitbucket"
165
+ }
166
+ edit_uris = {
167
+ "github.com": f"edit/master/{docs_dir}",
168
+ "gitlab.com": f"edit/master/{docs_dir}",
169
+ "bitbucket.org": f"src/default/{docs_dir}"
170
+ }
172
171
  repo_url = config.get("repo_url")
173
- if repo_url and not config.get("repo_name"):
174
- docs_dir = config.get("docs_dir")
172
+ if repo_url:
175
173
  host = urlparse(repo_url).hostname or ""
176
- if host == "github.com":
177
- set_default(config, "repo_name", "GitHub", str)
178
- set_default(config, "edit_uri", f"edit/master/{docs_dir}", str)
179
- elif host == "gitlab.com":
180
- set_default(config, "repo_name", "GitLab", str)
181
- set_default(config, "edit_uri", f"edit/master/{docs_dir}", str)
182
- elif host == "bitbucket.org":
183
- set_default(config, "repo_name", "Bitbucket", str)
184
- set_default(config, "edit_uri", f"src/default/{docs_dir}", str)
174
+ if not config.get("repo_name") and host in repo_names:
175
+ set_default(config, "repo_name", repo_names[host], str)
185
176
  elif host:
186
177
  config["repo_name"] = host.split(".")[0].title()
178
+ if host in edit_uris:
179
+ set_default(config, "edit_uri", edit_uris[host], str)
187
180
 
188
181
  # Remove trailing slash from edit_uri if present
189
182
  edit_uri = config.get("edit_uri")
@@ -292,73 +285,13 @@ def _apply_defaults(config: dict, path: str) -> dict:
292
285
  set_default(toggle, "name", None, str)
293
286
 
294
287
  # Set defaults for extra settings
288
+ if "extra" in config and not isinstance(config["extra"], dict):
289
+ raise ConfigurationError("The 'extra' setting must be a mapping/dictionary.")
295
290
  extra = set_default(config, "extra", {}, dict)
296
- set_default(extra, "homepage", None, str)
297
- set_default(extra, "scope", None, str)
298
- set_default(extra, "annotate", {}, dict)
299
- set_default(extra, "tags", {}, dict)
300
- set_default(extra, "generator", True, bool)
291
+
292
+ if "polyfills" in extra and not isinstance(extra["polyfills"], list):
293
+ raise ConfigurationError("The 'extra.polyfills' setting must be a list.")
301
294
  set_default(extra, "polyfills", [], list)
302
- set_default(extra, "analytics", None, dict)
303
-
304
- # Set defaults for extra analytics settings
305
- analytics = extra.get("analytics")
306
- if analytics:
307
- set_default(analytics, "provider", None, str)
308
- set_default(analytics, "property", None, str)
309
- set_default(analytics, "feedback", None, dict)
310
-
311
- # Set defaults for extra analytics feedback settings
312
- feedback = analytics.get("feedback")
313
- if feedback:
314
- set_default(feedback, "title", None, str)
315
- set_default(feedback, "ratings", [], list)
316
-
317
- # Set defaults for each rating entry
318
- ratings = feedback.setdefault("ratings", [])
319
- for entry in ratings:
320
- set_default(entry, "icon", None, str)
321
- set_default(entry, "name", None, str)
322
- set_default(entry, "data", None, str)
323
- set_default(entry, "note", None, str)
324
-
325
- # Set defaults for extra consent settings
326
- consent = extra.setdefault("consent", None)
327
- if consent:
328
- set_default(consent, "title", None, str)
329
- set_default(consent, "description", None, str)
330
- set_default(consent, "actions", [], list)
331
-
332
- # Set defaults for extra consent cookie settings
333
- cookies = consent.setdefault("cookies", {})
334
- for key, value in cookies.items():
335
- if isinstance(value, str):
336
- cookies[key] = {"name": value, "checked": False}
337
-
338
- # Set defaults for each cookie entry
339
- set_default(cookies[key], "name", None, str)
340
- set_default(cookies[key], "checked", False, bool)
341
-
342
- # Set defaults for extra social settings
343
- social = extra.setdefault("social", [])
344
- for entry in social:
345
- set_default(entry, "icon", None, str)
346
- set_default(entry, "name", None, str)
347
- set_default(entry, "link", None, str)
348
-
349
- # Set defaults for extra alternate settings
350
- alternate = extra.setdefault("alternate", [])
351
- for entry in alternate:
352
- set_default(entry, "name", None, str)
353
- set_default(entry, "link", None, str)
354
- set_default(entry, "lang", None, str)
355
-
356
- # Set defaults for extra version settings
357
- version = extra.setdefault("version", None)
358
- if version:
359
- set_default(version, "provider", None, str)
360
- set_default(version, "default", None, str)
361
- set_default(version, "alias", False, bool)
362
295
 
363
296
  # Ensure all non-existent values are all empty strings (for now)
364
297
  config["extra"] = _convert_extra(extra)
@@ -435,19 +368,28 @@ def _apply_defaults(config: dict, path: str) -> dict:
435
368
  tabbed = config["mdx_configs"].get("pymdownx.tabbed", {})
436
369
  if isinstance(tabbed.get("slugify"), dict):
437
370
  object = tabbed["slugify"].get("object", "pymdownx.slugs.slugify")
438
- tabbed["slugify"] = _resolve(object)(**tabbed["slugify"].get("kwds"))
371
+ tabbed["slugify"] = _resolve(object)(**tabbed["slugify"].get("kwds", {}))
439
372
 
440
373
  # Table of contents extension configuration - resolve slugification function
441
374
  toc = config["mdx_configs"]["toc"]
442
375
  if isinstance(toc.get("slugify"), dict):
443
376
  object = toc["slugify"].get("object", "pymdownx.slugs.slugify")
444
- toc["slugify"] = _resolve(object)(**toc["slugify"].get("kwds"))
377
+ toc["slugify"] = _resolve(object)(**toc["slugify"].get("kwds", {}))
445
378
 
446
379
  # Superfences extension configuration - resolve format function
447
380
  superfences = config["mdx_configs"].get("pymdownx.superfences", {})
448
381
  for fence in superfences.get("custom_fences", []):
449
382
  if isinstance(fence.get("format"), str):
450
383
  fence["format"] = _resolve(fence.get("format"))
384
+ elif isinstance(fence.get("format"), dict):
385
+ object = fence["format"].get("object", "pymdownx.superfences.fence_code_format")
386
+ fence["format"] = _resolve(object)(**fence["format"].get("kwds", {}))
387
+ if isinstance(fence.get("validator"), str):
388
+ fence["validator"] = _resolve(fence.get("validator"))
389
+ elif isinstance(fence.get("validator"), dict):
390
+ object = fence["validator"].get("object")
391
+ callable_object = _resolve(object) if object else lambda *args, **kwargs: True
392
+ fence["validator"] = callable_object(**fence["validator"].get("kwds", {}))
451
393
 
452
394
  # Ensure the table of contents title is initialized, as it's used inside
453
395
  # the template, and the table of contents extension is always defined
@@ -456,15 +398,24 @@ def _apply_defaults(config: dict, path: str) -> dict:
456
398
 
457
399
  # Convert plugins configuration
458
400
  config["plugins"] = _convert_plugins(config.get("plugins", []), config)
401
+
402
+ # mkdocstrings configuration
403
+ if "mkdocstrings" in config["plugins"]:
404
+ mkdocstrings_config = config["plugins"]["mkdocstrings"]["config"]
405
+ if mkdocstrings_config.pop("enabled", True):
406
+ mkdocstrings_config["markdown_extensions"] = [
407
+ {ext: mdx_configs.get(ext, {})} for ext in markdown_extensions
408
+ ]
409
+ config["markdown_extensions"].append("mkdocstrings")
410
+ config["mdx_configs"]["mkdocstrings"] = mkdocstrings_config
411
+
459
412
  return config
460
413
 
461
414
 
462
415
  def set_default(
463
- entry: dict, key: str, default: Any, data_type: type = None
464
- ) -> any:
465
- """
466
- Set a key to a default value if it isn't set, and optionally cast it to the specified data type.
467
- """
416
+ entry: dict, key: str, default: Any, data_type: type | None = None
417
+ ) -> Any:
418
+ """Set a key to a default value if it isn't set, and optionally cast it to the specified data type."""
468
419
  if key in entry and entry[key] is None:
469
420
  del entry[key]
470
421
 
@@ -476,24 +427,22 @@ def set_default(
476
427
  try:
477
428
  entry[key] = data_type(entry[key])
478
429
  except (ValueError, TypeError) as e:
479
- raise ValueError(f"Failed to cast key '{key}' to {data_type}: {e}")
430
+ raise ValueError(
431
+ f"Failed to cast key '{key}' to {data_type}: {e}"
432
+ ) from e
480
433
 
481
434
  # Return the resulting value
482
435
  return entry[key]
483
436
 
484
437
 
485
- def _hash(data: any) -> int:
486
- """
487
- Compute a hash for the given data.
488
- """
489
- hash = hashlib.sha1(pickle.dumps(data))
438
+ def _hash(data: Any) -> int:
439
+ """Compute a hash for the given data."""
440
+ hash = hashlib.sha1(pickle.dumps(data)) # noqa: S324
490
441
  return int(hash.hexdigest(), 16) % (2**64)
491
442
 
492
443
 
493
444
  def _convert_extra(data: dict | list) -> dict | list:
494
- """
495
- Recursively convert all None values in a dictionary or list to empty strings.
496
- """
445
+ """Recursively convert all None values in a dictionary or list to empty strings."""
497
446
  if isinstance(data, dict):
498
447
  # Process each key-value pair in the dictionary
499
448
  return {
@@ -502,7 +451,7 @@ def _convert_extra(data: dict | list) -> dict | list:
502
451
  else ("" if value is None else value)
503
452
  for key, value in data.items()
504
453
  }
505
- elif isinstance(data, list):
454
+ if isinstance(data, list):
506
455
  # Process each item in the list
507
456
  return [
508
457
  _convert_extra(item)
@@ -510,14 +459,11 @@ def _convert_extra(data: dict | list) -> dict | list:
510
459
  else ("" if item is None else item)
511
460
  for item in data
512
461
  ]
513
- else:
514
- return data
462
+ return data
515
463
 
516
464
 
517
- def _resolve(symbol: str):
518
- """
519
- Resolve a symbol to its corresponding Python object.
520
- """
465
+ def _resolve(symbol: str) -> Any:
466
+ """Resolve a symbol to its corresponding Python object."""
521
467
  module_path, func_name = symbol.rsplit(".", 1)
522
468
  module = importlib.import_module(module_path)
523
469
  return getattr(module, func_name)
@@ -526,17 +472,15 @@ def _resolve(symbol: str):
526
472
  # -----------------------------------------------------------------------------
527
473
 
528
474
 
529
- def _convert_nav(nav: dict) -> dict:
530
- """
531
- Convert MkDocs navigation
532
- """
475
+ def _convert_nav(nav: list) -> list:
476
+ """Convert MkDocs navigation."""
533
477
  return [_convert_nav_item(entry) for entry in nav]
534
478
 
535
479
 
536
- def _convert_nav_item(item: str | dict | list) -> dict:
537
- """
538
- Convert MkDocs shorthand navigation structure into something more manageable
539
- as we need to annotate each item with a title, URL, icon, and children.
480
+ def _convert_nav_item(item: str | dict | list) -> dict | list:
481
+ """Convert MkDocs shorthand navigation structure into something more manageable.
482
+
483
+ We need to annotate each item with a title, URL, icon, and children.
540
484
  """
541
485
  if isinstance(item, str):
542
486
  return {
@@ -550,19 +494,19 @@ def _convert_nav_item(item: str | dict | list) -> dict:
550
494
  }
551
495
 
552
496
  # Handle Title: URL
553
- elif isinstance(item, dict):
497
+ if isinstance(item, dict):
554
498
  for title, value in item.items():
555
499
  if isinstance(value, str):
556
500
  return {
557
501
  "title": str(title),
558
- "url": value,
502
+ "url": value.strip(),
559
503
  "canonical_url": None,
560
504
  "meta": None,
561
505
  "children": [],
562
- "is_index": _is_index(value),
506
+ "is_index": _is_index(value.strip()),
563
507
  "active": False,
564
508
  }
565
- elif isinstance(value, list):
509
+ if isinstance(value, list):
566
510
  return {
567
511
  "title": str(title),
568
512
  "url": None,
@@ -572,28 +516,25 @@ def _convert_nav_item(item: str | dict | list) -> dict:
572
516
  "is_index": False,
573
517
  "active": False,
574
518
  }
519
+ raise TypeError(f"Unknown nav item value type: {type(value)}")
575
520
 
576
521
  # Handle a list of items
577
522
  elif isinstance(item, list):
578
523
  return [_convert_nav_item(child) for child in item]
579
- else:
580
- raise ValueError(f"Unknown nav item type: {type(item)}")
524
+
525
+ raise TypeError(f"Unknown nav item type: {type(item)}")
581
526
 
582
527
 
583
528
  def _is_index(path: str) -> bool:
584
- """
585
- Returns, whether the given path points to a section index.
586
- """
587
- return path.endswith(("index.md", "README.md"))
529
+ """Returns, whether the given path points to a section index."""
530
+ return os.path.basename(path) in ("index.md", "README.md")
588
531
 
589
532
 
590
533
  # -----------------------------------------------------------------------------
591
534
 
592
535
 
593
- def _convert_extra_javascript(value: list[any]) -> list:
594
- """
595
- Ensure extra_javascript uses a structured format.
596
- """
536
+ def _convert_extra_javascript(value: list) -> list:
537
+ """Ensure extra_javascript uses a structured format."""
597
538
  for i, item in enumerate(value):
598
539
  if isinstance(item, str):
599
540
  value[i] = {
@@ -608,9 +549,7 @@ def _convert_extra_javascript(value: list[any]) -> list:
608
549
  item.setdefault("async", False)
609
550
  item.setdefault("defer", False)
610
551
  else:
611
- raise ValueError(
612
- f"Unknown extra_javascript item type: {type(item)}"
613
- )
552
+ raise TypeError(f"Unknown extra_javascript item type: {type(item)}")
614
553
 
615
554
  # Return resulting value
616
555
  return value
@@ -619,12 +558,10 @@ def _convert_extra_javascript(value: list[any]) -> list:
619
558
  # -----------------------------------------------------------------------------
620
559
 
621
560
 
622
- def _convert_markdown_extensions(value: any):
623
- """
624
- Convert Markdown extensions configuration to what Python Markdown expects.
625
- """
561
+ def _convert_markdown_extensions(value: Any) -> tuple[list[str], dict]:
562
+ """Convert Markdown extensions configuration to what Python Markdown expects."""
626
563
  markdown_extensions = ["toc", "tables"]
627
- mdx_configs = {"toc": {}, "tables": {}}
564
+ mdx_configs: dict[str, dict[str, Any]] = {"toc": {}, "tables": {}}
628
565
 
629
566
  # In case of Python Markdown Extensions, we allow to omit the necessary
630
567
  # quotes around the extension names, so we need to hoist the extensions
@@ -632,14 +569,24 @@ def _convert_markdown_extensions(value: any):
632
569
  # actually parse the configuration.
633
570
  if "pymdownx" in value:
634
571
  pymdownx = value.pop("pymdownx")
635
- for ext, config in pymdownx.items():
572
+ for ext, conf in pymdownx.items():
636
573
  # Special case for blocks extension, which has another level of
637
574
  # nesting. This is the only extension that requires this.
638
575
  if ext == "blocks":
639
- for block, config in config.items():
576
+ for block, config in conf.items():
640
577
  value[f"pymdownx.{ext}.{block}"] = config
641
578
  else:
642
- value[f"pymdownx.{ext}"] = config
579
+ value[f"pymdownx.{ext}"] = conf
580
+
581
+ # Same as for Python Markdown extensions, see above
582
+ if "zensical" in value:
583
+ zensical = value.pop("zensical")
584
+ for ext, conf in zensical.items():
585
+ if ext == "extensions":
586
+ for key, config in conf.items():
587
+ value[f"zensical.{ext}.{key}"] = config
588
+ else:
589
+ value[f"zensical.{ext}"] = conf
643
590
 
644
591
  # Extensions can be defined as a dict
645
592
  if isinstance(value, dict):
@@ -664,16 +611,13 @@ def _convert_markdown_extensions(value: any):
664
611
  # ----------------------------------------------------------------------------
665
612
 
666
613
 
667
- def _convert_plugins(value: any, config: dict) -> list:
668
- """
669
- Convert plugins configuration to something we can work with.
670
- """
614
+ def _convert_plugins(value: Any, config: dict) -> dict:
615
+ """Convert plugins configuration to something we can work with."""
671
616
  plugins = {}
672
617
 
673
618
  # Plugins can be defined as a dict
674
619
  if isinstance(value, dict):
675
- for name, data in value.items():
676
- plugins[name] = data
620
+ plugins.update(value)
677
621
 
678
622
  # Plugins can also be defined as a list
679
623
  else:
@@ -730,8 +674,7 @@ def _convert_plugins(value: any, config: dict) -> list:
730
674
  def _yaml_load(
731
675
  source: IO, loader: type[BaseLoader] | None = None
732
676
  ) -> dict[str, Any]:
733
- """
734
- Load configuration file and resolve environment variables and parent files.
677
+ """Load configuration file and resolve environment variables and parent files.
735
678
 
736
679
  Note that INHERIT is only a bandaid that was introduced to allow for some
737
680
  degree of modularity, but with serious shortcomings. Zensical will use a
@@ -746,12 +689,12 @@ def _yaml_load(
746
689
  source.read()
747
690
  .replace("material.extensions", "zensical.extensions")
748
691
  .replace("materialx", "zensical.extensions"),
749
- Loader=Loader,
692
+ Loader=Loader, # noqa: S506
750
693
  )
751
694
  except YAMLError as e:
752
695
  raise ConfigurationError(
753
696
  f"Encountered an error parsing the configuration file: {e}"
754
- )
697
+ ) from e
755
698
  if config is None:
756
699
  return {}
757
700
 
@@ -765,7 +708,7 @@ def _yaml_load(
765
708
  raise ConfigurationError(
766
709
  f"Inherited config file '{relpath}' doesn't exist at '{abspath}'."
767
710
  )
768
- with open(abspath, "r") as fd:
711
+ with open(abspath, encoding="utf-8") as fd:
769
712
  parent = _yaml_load(fd, loader)
770
713
  config = always_merger.merge(parent, config)
771
714
 
@@ -773,9 +716,11 @@ def _yaml_load(
773
716
  return config
774
717
 
775
718
 
776
- def _construct_env_tag(loader: yaml.Loader, node: yaml.Node):
777
- """
778
- Assign value of ENV variable referenced at node.
719
+ def _construct_env_tag(
720
+ loader: yaml.Loader,
721
+ node: yaml.ScalarNode | yaml.SequenceNode | yaml.MappingNode,
722
+ ) -> Any:
723
+ """Assign value of ENV variable referenced at node.
779
724
 
780
725
  MkDocs supports the use of !ENV to reference environment variables in YAML
781
726
  configuration files. We won't likely support this in Zensical, but for now
@@ -804,7 +749,7 @@ def _construct_env_tag(loader: yaml.Loader, node: yaml.Node):
804
749
  else:
805
750
  raise ConstructorError(
806
751
  context=f"expected a scalar or sequence node, but found {node.id}",
807
- start_mark=node.start_mark,
752
+ context_mark=node.start_mark,
808
753
  )
809
754
 
810
755
  # Resolve environment variable