mkdocs 源码剖析 ¶
约 1636 个字 135 行代码 预计阅读时间 7 分钟
包配置、入口点 ¶
包配置 ¶
一些基础配置不必说,主要是 entry_points 配置:
setup(
...
entry_points={
'console_scripts': [
'mkdocs = mkdocs.__main__:cli',
],
'mkdocs.themes': [
'mkdocs = mkdocs.themes.mkdocs',
'readthedocs = mkdocs.themes.readthedocs',
],
'mkdocs.plugins': [
'search = mkdocs.contrib.search:SearchPlugin',
],
},
...
)
- CLI 命令 mkdocs,入口点 mkdocs.__main__:cli
- mkdocs.themes,用于接入外部包定义的主题
- mkdocs.plugins,用于接入外部包定义的插件
入口点 ¶
命令行的入口点为 mkdocs.__main__:cli。下分析 __main__.py:
- log 相关:
- 一个自定义的 ColorFormatter(
我不是很喜欢这个样式,可以改掉) - 一个 State 用于维护不同命令的 log level(后有
click.make_pass_decorator(State, ensure=True)
)
- 一个自定义的 ColorFormatter(
- cli 相关:
- 利用 click 库
@click.group(context_settings={'help_option_names': ['-h', '--help']}) # 设置 help 命令 @click.version_option( # 设置 version 命令和显示信息 __version__, '-V', '--version', message=f'%(prog)s, version %(version)s from { PKG_DIR } (Python { PYTHON_VERSION })', ) @common_options # 通用设置 def cli(): # 命令行主命令 mkdocs """ MkDocs - Project documentation with Markdown. """
- 一些子命令,选项添加与上面类似,不赘述
- serve 子命令:调用 mkdocs.commands.serve.serve 函数
- build 子命令:处理 config(在 mkdocs.config 模块中详细定义
) 、启动插件、调用 mkdocs.commands.build.build 函数,未失败则运行后关闭插件 - gh-deploy 子命令:在 build 后调用 mkdocs.commands.gh_deploy.gh_deploy
- new 子命令:调用 mkdocs.commands.new.new 函数
- 利用 click 库
包结构分析 ¶
除去 tests 和其它冗余代码后,大致分析的整个包结构:
mkdocs
├── __init__.py # 定义了版本号
├── __main__.py # CLI 入口点
├── commands # CLI 定义
│ ├── __init__.py
│ ├── babel.py # 处理语言文件(不是 CLI 命令)
│ ├── build.py # 构建文档(mkdocs build)
│ ├── gh_deploy.py # 部署到 Pages(mkdocs gh-deploy)
│ ├── new.py # 创建新项目(mkdocs new)
│ ├── serve.py # 开启本地预览服务(mkdocs serve)
│ └── setup.py # 目测没用?
├── config
│ ├── __init__.py # 只导出 base 和 config_options
│ ├── base.py # 基础配置
│ ├── config_options.py # 各种参数以及验证方式
│ └── defaults.py # 默认配置
├── contrib
│ ├── __init__.py
│ └── search
│ ├── __init__.py
│ ├── lunr-language
│ │ └── ...
│ ├── prebuild-index.js
│ ├── search_index.py
│ └── templates
│ └── search
│ ├── lunr.js
│ ├── main.js
│ └── worker.js
├── exceptions.py # 一些定义的异常
├── livereload
│ └── __init__.py # 本地预览自动刷新服务
├── localization.py # 本地化相关代码
├── plugins.py # 插件管理
├── structure # 页面结构
│ ├── __init__.py
│ ├── files.py
│ ├── nav.py
│ ├── pages.py
│ └── toc.py
├── templates
│ └── sitemap.xml
├── theme.py # 主题类
├── themes
│ └── ...
└── utils
├── __init__.py
├── babel_stub.py
├── filters.py
└── meta.py
一些工具性代码 ¶
和运行主逻辑无大关系的一些代码:
exceptions.py¶
定义了五个异常类:
- 基类 MkDocsException,不会直接使用。继承自 ClickException,使 click 能够处理并显示
- Abort,终止执行,可以带有信息
- ConfigurationError,由于配置文件原因导致的错误
- BuildError,构建过程中出现的错误,mkdocs 源码中并未直接抛出此类错误,但有子类 PluginError,应该是给第三方插件使用的
- PluginError,在插件中可以抛出的异常,继承自 BuildError
utils¶
build 流程 ¶
从 mkdocs build 这一命令的执行流程来逐步自顶向下分析
首先从 __main__.py 进入,调用到 build_command 函数:
_enable_warnings() 可以不用管。之后的流程就是:
- 调用 config.load_config 加载配置
- 从配置中找到插件,并触发其 startup 事件
- 尝试调用 build.build 函数构建文档
- 若不成功则直接挂掉程序
- 若成功则触发插件的 shutdown 事件,运行结束
加载配置 ¶
config/base.py 中 261 行开始的函数 load_config(删除了注释和一些空行
def load_config(config_file: Optional[Union[str, IO]] = None, **kwargs) -> Config:
options = kwargs.copy()
for key, value in options.copy().items():
if value is None:
options.pop(key)
with _open_config_file(config_file) as fd:
options['config_file_path'] = getattr(fd, 'name', '')
from mkdocs.config import defaults
cfg = Config(schema=defaults.get_schema(), config_file_path=options['config_file_path'])
cfg.load_file(fd)
cfg.load_dict(options)
errors, warnings = cfg.validate()
for config_name, warning in warnings:
log.warning(f"Config value: '{config_name}'. Warning: {warning}")
for config_name, error in errors:
log.error(f"Config value: '{config_name}'. Error: {error}")
for key, value in cfg.items():
log.debug(f"Config value: '{key}' = {value!r}")
if len(errors) > 0:
raise exceptions.Abort(f"Aborted with {len(errors)} Configuration Errors!")
elif cfg['strict'] and len(warnings) > 0:
raise exceptions.Abort(
f"Aborted with {len(warnings)} Configuration Warnings in 'strict' mode!"
)
return cfg
这个函数中的流程:
- 将 kwargs 拷贝到 options 中(也就是通过命令行传入的参数)并删掉值为 None 的配置
- 通过 _open_config_file 这个上下文管理器来打开配置文件,然后:
- 获取实际使用的配置文件路径名,并存到 options 中
- 根据 defaults 创建一个 Config 名叫 cfg,其配置文件路径为上面获得到的文件名
- 将配置文件中的配置载入 cfg
- 将命令行参数 options 载入 cfg(此时有覆盖,即命令行配置优先级高于配置文件)
- 检查 cfg 配置中是否有非法信息
- 以 debug level 输出所有配置项
- 如果有非法配置的话输出全部 warnings 和 errors
- 如果有 error,则直接 abort
- 如果有 warning,且开启了 strict 模式,也直接 abort
- 返回得到的 cfg 配置实例
其中的一些细节:
_open_config_file¶
是用 contextmanager 装饰器包装得到的上下文管理器,其接收一个参数 config_file,是从命令行 --config-file 参数获得的配置文件路径,如果运行时没有这一参数则 config_file 为 None。其内部流程:
- 如果 config_file 为 None(即未指定
) ,则默认尝试读取当前文件夹下的配置文件 mkdocs.yml 或 .yaml - 如果 config_file 为字符串,则尝试读取该字符串指定的文件
- 如果 config_file 是文件对象
- 如果文件是开启的,则直接将文件指针移到开头
- 如果文件时关闭的,则获取名称,打开文件
- 读取完成后,关掉文件
创建 Config、载入配置 ¶
Config 是一个基于 UserDict 的类,即可以直接通过 [] 来读取其中 .data 属性(字典)中的值,其在初始化时发生了下面几件事:
- 从参数设置 _schema,即从 defaults 中读取到的默认参数部分。以及 _schema_keys
- 设置 config_file_path(会自动解码 bytes 类型)
- 创建 .data 字典属性,以及一个空的 user_configs 列表,里面存放载入的字典原数据
- 调用 set_defaults 方法设置默认值
- 即从 schema 中读取设置
在调用 load_file 方法时实际上会读取 yaml 文件内容,解析成字典然后调用 load_dict 方法。调用 load_dict 方法时先将传入的内容直接添加到 user_configs 列表中,然后用其 update data 属性
验证 Config ¶
调用 Config.validate 验证有以下几步:
- 调用 _pre_validate() 方法
- 其会遍历 _schema 每一个键值对,调用 config_option 上的 pre_validation 方法,具体由实际子类进行实现
- 调用 _validate() 方法
- 也会遍历 _schema 每一个键值对,但此时会将键在 config 中对应的值传入 config_option 的 validate 方法(由子类的 run_validation 方法实现,返回一个修正后的值,重新赋值回来
- 除此之外还会检查不在 _schema_keys 中的键,并抛出警告
- 如果前面都没有出现 error,则调用 _post_validate 方法
- 同 pre,只不过对 config_option 调用的方法从 pre 换成了 post
具体的各个参数的定义和验证方法都在 config/config_options.py 中,很详细易懂
触发 Plugin 事件 ¶
在前面 ConfigOptions 中有一个 Plugins 子类,来存放插件相关对象,其初始化时会调用 plugins.get_plugins() 函数来全局检查安装的包的 entry_points 是否含有 mkdocs.plugins,来得到一个 Dict[str, EntryPoint] 类型的 installed_plugins 属性(即通过包名映射到插件入口点,即插件类)
这个 Plugins 类在上面所说的 run_validation 过程中会将插件名传入 load_plugin 方法(里面会检查是否安装等一系列问题
在为 PluginCollection setitem 的时候会将插件的所有 on_ 开头的方法都注册为 event(如 on_startup 方法会注册一个名为 startup 的 event
调用 build 函数构建文档 ¶
创建日期: 2022年1月6日 00:26:43