from nbdev.process import extract_directives
from nbdev.processors import Processor
from fastcore.basics import listify
from string import Template
编写 nbdev 插件
本文将介绍什么?
借助 nbdev
,可以通过一个深思熟虑且可扩展的框架进一步定制和扩展其标准功能。您的特定库或需求是否要求您在某些单元格中注入自定义的 Quarto 添加项?或者如果您想做一些更简单的事情,比如寻找快捷方式更容易地替换复杂的 Quarto 指令(例如将 ::: {.column-margin}
替换为 #| margin
)呢?
使用 nbdev
编写自定义插件是实现此目标的最简单方法,通过本教程,我们将帮助您快速掌握如何利用它创建自己的插件,以扩展和简化您使用 nbdev
和 Quarto 进行文学编程的体验。
具体来说,我们将构建一个 processor(处理 notebook 单元格的东西),它将使我们能够快速写出任何 Quarto 特定的头部(如 ::: {.some_annotation}
),并将其替换为一个 div
快捷方式。这当然是一个非常特定于 Quarto 的示例,发生在构建文档时,但此技术也可以用于在库导出期间实现自定义行为。
注意:这里我们使用
div
是因为它更像相关的 Quarto 指令的行为,它们在 HTML 代码中类似于<div>
标签
入门:nbdev
如何使其变得容易?
首先,让我们可视化一下我们要实现的目标。
与其执行以下代码(它将把 "Some text"
添加到侧边栏,如当前侧边所示)
一些文本
::: {.column-margin}
Some text
:::
我们将创建一个更短的方式来写出它,利用 nbdev 和 Quarto 编写其指令的方式
在本教程结束时,我们将创建一个看起来像下面的东西
#| div column-margin
Some text
这还将包括 div
应该跨越多个单元格的情况,通过指定 start
和 end
。
注意:请查阅 文章布局 Quarto 文档,以找到此自定义指令的最佳用例示例,包括刚刚展示的
column-margin
这可以在不到 50 行代码内实现!
nbdev
让我们创建所谓的 processors(例如,#| export
就是通过这种方式将代码放入模块的)。这些处理器作用于 notebook 的每个单元格,并可以修改其内容。然后可以将它们封装到一个模块中,就像 nbdev 处理 nbdev_export
或 nbdev_docs
一样。由于编写自定义 nbdev
扩展的强大功能,无需深入了解框架的内部工作原理!
引入所需的组件
我们实际需要从 nbdev
导入的并不多!我们只需要两个:- extract_directives
,用于读取编写的 #|
列表 - Processor
类,它将实际对 notebook 单元格执行我们想要的操作。
其余的导入是为了让我们的工作更轻松,稍后会解释
最后,出于测试目的,我们将利用 nbdev
的 mk_cell
函数和 NBProcessor
类,这将使我们能够在“真实”的 notebook 上模拟运行我们的处理器!
from nbdev.processors import mk_cell, NBProcessor
编写转换器
第一步是创建一种快速简便的方法,将我们想要使用的 nbdev
指令(例如 #| div column-margin
)快速转换为 Quarto 可以读取的内容(例如 ::: {.column-margin}
)。
我们可以创建一个字符串 Template
来为我们执行此操作
= Template("::: {.$layout}\n${content}\n") _LAYOUT_STR
这不必是字符串模板,我只是发现它最容易使用!
_LAYOUT_STR.substitute(="column-margin",
layout="Some text to go on the sidebar"
content )
'::: {.column-margin}\nSome text to go on the sidebar\n'
接下来,我们需要编写一个在单元格级别操作的简单转换器
def convert_layout(
dict, # A single cell from a Jupyter Notebook
cell:=False # Whether the div should be wrapped around multiple cells
is_multicell
):"Takes a code cell that contains `div` in the directives and modifies the contents to the proper Quarto format"
= cell.source
content = cell.source.splitlines(True)
code = cell.directives_["div"]
div_ # We check if end is in the first line of the cell source
if "end" in div_:
# If it is, just fill the text with `:::` if no code exists there
= ":::" if len(code) == 1 else f'{code.source}:::'
cell.source else:
# Actually modify the code
= _LAYOUT_STR.substitute(layout=" ".join(div_), content=content)
cell.source if not is_multicell: cell.source += ":::"
让我们详细了解这里发生了什么。
= cell.source content
notebook 单元格中存在的任何内容的源文本都将位于 .source
中。
= cell.source.splitlines(True) code
然后我想提取单元格的内容并将其分成多行,用换行符分隔。这使我们可以检查单元格是否仅包含 #| div end
,这意味着之前开始的 div 应该停止。
= cell.directives_["div"] div_
任何指令(任何用 #|
标记的单元格中的注释)将作为字典存在于 directives_
属性中。对于我们的特定处理器,我们只关心 div
指令
if "end" in div_:
# If it is, just fill the text with `:::` if no code exists there
= ":::" if len(code) == 1 else f'{code.source}:::'
cell.source else:
# Actually modify the code
= _LAYOUT_STR.substitute(layout=" ".join(div_), content=content)
cell.source if not is_multicell: cell.source += ":::"
从那里,这最后一部分检查是向单元格添加结束的 :::
块,还是使用 _LAYOUT_STR
并为 Quarto 注入样板 div CSS 代码。
让我们看看它的实际效果
= mk_cell(
cell """#| div margin-column
Here is something for the sidebar!""",
="markdown"
cell_type )
nbdev
将提取这些指令并使用 extract_directives
函数将它们存储在单元格的 directives_
属性中
= extract_directives(cell, "#")
cell.directives_ cell.directives_
{'div': ['margin-column']}
现在我们可以测试一下我们的 convert_layout
函数是否有效!
convert_layout(cell)print(cell.source)
::: {.margin-column}
Here is something for the sidebar!
:::
注意:我在这里打印
cell.source
是为了让其文本看起来更清晰,就像我们在Markdown
单元格中看到的那样
看起来完全符合我们之前的要求!太棒了!
我们如何告诉 nbdev 使用它并创建前面提到的 Processor
类?
编写 Processor
这里的倒数第二步是创建自定义的 Processor
,nbdev 利用它来应用 procs(修改单元格内容的东西)。对它们的简单理解是:您应该创建一个类,让它继承 Processor
,并且所有应该进行的修改都必须在一个 cell
函数中定义,该函数接受一个 cell
并就地修改它。
class LayoutProc(Processor):
"A processor that will turn `div` based tags into proper quarto ones"
= False
has_multiple_cells def cell(self, cell):
if cell.cell_type == "markdown" and "div" in cell.directives_:
= cell.directives_["div"]
div_ if self.has_multiple_cells and "end" in div_:
convert_layout(cell)else:
= div_[-1] == "start"
is_start if is_start:
self.has_multiple_cells = True
"start")
div_.remove( convert_layout(cell, is_start)
我们如何测试它是否会工作?
一个最小的 Jupyter Notebook 只是一个字典,其中的单元格位于 cells
键中,而单元格本身是一个遵循特殊格式的 notebook 单元格列表。我们在上面已经创建了一个这样的示例。nbdev
有一个 dict2nb
函数,可以让我们快速将这个最小的 Jupyter Notebook 概念转换为真实的对象。
之后,我们可以通过 NBProcessor
类(nbdev
用于应用这些处理器)将处理器应用于这些单元格
from nbdev.process import NBProcessor, dict2nb
= {
nb "cells":[
"""#| div column-margin
mk_cell(A test""", "markdown"),
"""#| div column-margin start
mk_cell(A test""", "markdown"),
"""#| div end""", "markdown"),
mk_cell( ]}
mk_cell
函数将根据一些 content
和一个 cell type
创建一个单元格。我们构建的特定扩展适用于 Markdown
单元格,所以我们将类型设置为 markdown
。
NBProcessor
接受一个应该应用的 procs(处理器)列表和一个打开的 Jupyter Notebook
= NBProcessor(procs=LayoutProc, nb=dict2nb(nb)) processor
应用这些处理器的操作是通过调用 .process():
函数完成的
processor.process()
现在我们可以看到那些代码单元格已被更改
for i in range(3):
print(f"Before:\n{nb['cells'][i].source}\n")
print(f"After:\n{processor.nb.cells[i].source}\n")
Before:
#| div column-margin
A test
After:
::: {.column-margin}
A test
:::
Before:
#| div column-margin start
A test
After:
::: {.column-margin}
A test
Before:
#| div end
After:
:::
太棒了!我们成功创建了一个 nbdev 插件,可以让我们轻松地偷懒编写 Markdown Quarto 指令。我们如何在项目中实际使用它呢?
如何在您的项目中启用插件
这需要在您的 settings.ini
中进行两处更改。
首先,如果这段代码存在于 nbdev
中,我们可以添加一个特殊的 procs
键并指定处理器来自哪里
procs =
nbdev.extensions:LayoutProc
它遵循 library.module:processor_name
的格式
如果这是从外部库使用(例如此处理器基于 nbdev-extensions 中的处理器),您应该将其添加到您的项目需求中
requirements = nbdev-extensions
这样就完成了!现在当调用 nbdev_docs
或 nbdev_preview
时,我们刚刚创建的处理器将自动应用于您的 notebooks 并执行此转换!
总结、nbdev-extensions 以及关于我的一些介绍!
基本上,如果单元格的任何部分以及它在导出模块、构建文档或创建自己的特殊命令执行后处理时的外观,都可以利用 nbdev 提供的这个 Processor
类快速高效地完成!
(Zachary Mueller 撰写)如果您有兴趣查看更多 nbdev-extensions 的示例以及它能做什么,我编写了一个专门的库,名为 nbdev-extensions,任何可能对我处理 nbdev 有益的想法,我都会将其转化为一个扩展供大家使用。
感谢阅读!