diff --git a/mkdocs/config/base.py b/mkdocs/config/base.py index bd875191..f4e8279e 100644 --- a/mkdocs/config/base.py +++ b/mkdocs/config/base.py @@ -4,13 +4,27 @@ import functools import logging import os import sys +import warnings from collections import UserDict from contextlib import contextmanager -from typing import IO, TYPE_CHECKING, Generic, Iterator, List, Sequence, Tuple, TypeVar, overload +from typing import ( + IO, + TYPE_CHECKING, + Any, + Generic, + Iterator, + List, + Mapping, + Sequence, + Tuple, + TypeVar, + overload, +) from yaml import YAMLError from mkdocs import exceptions, utils +from mkdocs.utils import weak_property if TYPE_CHECKING: from mkdocs.config.defaults import MkDocsConfig @@ -143,7 +157,7 @@ class Config(UserDict): def __init__(self, config_file_path: str | bytes | None = None): super().__init__() - self.user_configs: list[dict] = [] + self.__user_configs: list[dict] = [] self.set_defaults() self._schema_keys = {k for k, v in self._schema} @@ -237,7 +251,7 @@ class Config(UserDict): f"{type(patch)}" ) - self.user_configs.append(patch) + self.__user_configs.append(patch) self.update(patch) def load_file(self, config_file: IO) -> None: @@ -250,6 +264,13 @@ class Config(UserDict): f"MkDocs encountered an error parsing the configuration file: {e}" ) + @weak_property + def user_configs(self) -> Sequence[Mapping[str, Any]]: + warnings.warn( + "user_configs is never used in MkDocs and will be removed soon.", DeprecationWarning + ) + return self.__user_configs + @functools.lru_cache(maxsize=None) def get_schema(cls: type) -> PlainConfigSchema: diff --git a/mkdocs/localization.py b/mkdocs/localization.py index 73391489..59bb6ed3 100644 --- a/mkdocs/localization.py +++ b/mkdocs/localization.py @@ -33,7 +33,7 @@ class NoBabelExtension(InternationalizationExtension): # pragma: no cover ) -def parse_locale(locale) -> Locale: +def parse_locale(locale: str) -> Locale: try: return Locale.parse(locale, sep='_') except (ValueError, UnknownLocaleError, TypeError) as e: diff --git a/mkdocs/tests/__init__.py b/mkdocs/tests/__init__.py index 53d6d1a4..e0248786 100644 --- a/mkdocs/tests/__init__.py +++ b/mkdocs/tests/__init__.py @@ -1,4 +1,7 @@ import logging +import unittest.util + +unittest.util._MAX_LENGTH = 100000 class DisallowLogsHandler(logging.Handler): diff --git a/mkdocs/tests/config/config_options_tests.py b/mkdocs/tests/config/config_options_tests.py index 80588c7d..c8dbff0b 100644 --- a/mkdocs/tests/config/config_options_tests.py +++ b/mkdocs/tests/config/config_options_tests.py @@ -1104,6 +1104,7 @@ class ThemeTest(TestCase): conf = self.get_config(Schema, {'option': config}) self.assertEqual(conf.option.name, 'mkdocs') + self.assertEqual(conf.option.custom_dir, custom_dir) self.assertIn(custom_dir, conf.option.dirs) self.assertEqual( conf.option.static_templates, @@ -1228,7 +1229,7 @@ class ThemeTest(TestCase): theme = c.Theme() conf = self.get_config(Schema, config) - self.assertEqual(conf.theme['locale'].language, 'fr') + self.assertEqual(conf.theme.locale.language, 'fr') class NavTest(TestCase): diff --git a/mkdocs/theme.py b/mkdocs/theme.py index a8e8ed13..c58a2f7b 100644 --- a/mkdocs/theme.py +++ b/mkdocs/theme.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging import os -from typing import Any +from typing import Any, Collection, MutableMapping import jinja2 @@ -13,25 +13,32 @@ from mkdocs.utils import filters log = logging.getLogger(__name__) -class Theme: +class Theme(MutableMapping[str, Any]): """ A Theme object. - Keywords: - + Parameters: name: The name of the theme as defined by its entrypoint. - custom_dir: User defined directory for custom templates. - static_templates: A list of templates to render as static pages. All other keywords are passed as-is and made available as a key/value mapping. - """ - def __init__(self, name: str | None = None, **user_config) -> None: + def __init__( + self, + name: str | None = None, + *, + custom_dir: str | None = None, + static_templates: Collection[str] = (), + locale: str | None = None, + **user_config, + ) -> None: self.name = name - self._vars = {'name': name, 'locale': 'en'} + self._custom_dir = custom_dir + _vars: dict[str, Any] = {'name': name, 'locale': 'en'} + # _vars is soft-deprecated, intentionally hide it from mypy. + setattr(self, '_vars', _vars) # MkDocs provided static templates are always included package_dir = os.path.abspath(os.path.dirname(__file__)) @@ -41,8 +48,8 @@ class Theme: # Build self.dirs from various sources in order of precedence self.dirs = [] - if 'custom_dir' in user_config: - self.dirs.append(user_config.pop('custom_dir')) + if custom_dir is not None: + self.dirs.append(custom_dir) if name: self._load_theme_config(name) @@ -51,32 +58,54 @@ class Theme: self.dirs.append(mkdocs_templates) # Handle remaining user configs. Override theme configs (if set) - self.static_templates.update(user_config.pop('static_templates', [])) - self._vars.update(user_config) + self.static_templates.update(static_templates) + _vars.update(user_config) # Validate locale and convert to Locale object - self._vars['locale'] = localization.parse_locale(self._vars['locale']) + _vars['locale'] = localization.parse_locale( + locale if locale is not None else _vars['locale'] + ) + + name: str | None + + @property + def locale(self) -> localization.Locale: + return self['locale'] + + @property + def custom_dir(self) -> str | None: + return self._custom_dir + + dirs: list[str] + + static_templates: set[str] def __repr__(self) -> str: - return "{}(name='{}', dirs={}, static_templates={}, {})".format( + return "{}(name={!r}, dirs={!r}, static_templates={!r}, {})".format( self.__class__.__name__, self.name, self.dirs, - list(self.static_templates), - ', '.join(f'{k}={v!r}' for k, v in self._vars.items()), + self.static_templates, + ', '.join(f'{k}={v!r}' for k, v in self.items()), ) def __getitem__(self, key: str) -> Any: - return self._vars[key] + return self._vars[key] # type: ignore[attr-defined] - def __setitem__(self, key, value): - self._vars[key] = value + def __setitem__(self, key: str, value): + self._vars[key] = value # type: ignore[attr-defined] - def __contains__(self, item: str) -> bool: - return item in self._vars + def __delitem__(self, key: str): + del self._vars[key] # type: ignore[attr-defined] + + def __contains__(self, item: object) -> bool: + return item in self._vars # type: ignore[attr-defined] + + def __len__(self): + return len(self._vars) # type: ignore[attr-defined] def __iter__(self): - return iter(self._vars) + return iter(self._vars) # type: ignore[attr-defined] def _load_theme_config(self, name: str) -> None: """Recursively load theme and any parent themes.""" @@ -108,7 +137,7 @@ class Theme: self._load_theme_config(parent_theme) self.static_templates.update(theme_config.pop('static_templates', [])) - self._vars.update(theme_config) + self._vars.update(theme_config) # type: ignore[attr-defined] def get_env(self) -> jinja2.Environment: """Return a Jinja environment for the theme.""" @@ -117,5 +146,5 @@ class Theme: # No autoreload because editing a template in the middle of a build is not useful. env = jinja2.Environment(loader=loader, auto_reload=False) env.filters['url'] = filters.url_filter - localization.install_translations(env, self._vars['locale'], self.dirs) + localization.install_translations(env, self.locale, self.dirs) return env