uv-globfilteruv 的 PEP 639 受限 Glob 解析与目录遍历预过滤实现【免费下载链接】uvAn extremely fast Python package and project manager, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/uv/uvuv-globfilter是 uv 内部的一个组件 crate目标是实现跨语言、跨操作系统的受限 Glob 语法并在此基础上提供一个目录遍历预过滤器用于在walkdir遍历中尽早跳过内部不可能出现目标文件的目录。读完本篇你将掌握 PEP 639 受限 Glob 的完整语法规则、GlobDirFilter基于 DFA 的目录匹配算法、与WalkDir::filter_entry的集成方式以及它在 uv 构建后端打包license-files、source-include等配置项时的真实用法。一、定位与动机crates/uv-globfilter/README.md 对该 crate 的定义是Portable directory walking with includes and excludes.带 include/exclude 的跨平台目录遍历。其核心动机是Motivating example: You want to allow the user to select paths within a project.即允许用户以 glob 方式在pyproject.toml中声明哪些路径要包含进构建产物、哪些要排除例如include [src, License.txt, resources/icons/*.svg] exclude [target, /dist, .cache, *.tmp]在遍历目录树时可以调用GlobDirFilter::from_globs(...)?.match_directory(relative)并在walkdir的filter_entry回调中用它跳过那些永远不会命中的目录从而避免对大目录树的无谓 I/O。这正是预过滤的含义不是判断当前条目本身是否匹配而是判断当前目录的子树是否还有任何可能匹配 glob 的路径。该 crate 目前仅作为 uv 的内部组件使用crates/uv-globfilter/Cargo.toml 中description This is an internal component crate of uv版本0.0.76其公开 API 只有两个类型外加一个演示用的二进制入口GlobDirFilter—— 目录遍历预过滤器见 crates/uv-globfilter/src/glob_dir_filter.rsPortableGlobParser/PortableGlobError—— PEP 639 受限 glob 的解析器与错误类型见 crates/uv-globfilter/src/portable_glob.rs演示二进制main.rs—— 展示 include/exclude 配合WalkDir的完整用法见 crates/uv-globfilter/src/main.rs。crate 的 lib.rs 模块文档明确了设计目标The goal is globs that are portable between languages and operating systems.也就是说同一个 glob 模式在 Python 工具、Node 工具、Rust 工具中应表现一致且在 Windows 与类 Unix 系统上一致。为此它选择了 PEP 639 定义的那套受限 glob 语法并在GlobBuilder层面强制literal_separator(true)路径分隔符按字面量处理*不跨目录。二、PEP 639 受限 Glob 语法README 核心规则完整继承README 明确声明支持的是PEP 639 的跨语言受限 glob 语法规则如下与 portable_glob.rs 中PortableGlobParser::parse的文档注释一一对应规则说明逐字匹配字符字母数字、下划线_、连字符-、点.按字面量匹配*匹配任意数量的字符但不跨路径分隔符?匹配单个字符不跨路径分隔符**匹配任意数量的字符包含路径分隔符可跨目录[...]字符集仅包含逐字匹配字符内部连字符-表示与 locale 无关的范围按 Unicode 码点排序如a-z位于开头或结尾的连字符按字面量匹配路径分隔符固定为斜杠/模式相对于给定目录不支持以/开头的绝对路径..父目录指示符不允许出现README 最后还点出了这条规则链的推论These rules mean that matching the backslash (\) is forbidden, which avoids collisions with the windows path separator.由于/是唯一合法分隔符、反斜杠禁止匹配Windows 下的反斜杠分隔符问题被从语法层面彻底消除。uv 扩展变体PortableGlobParser::Uv源码中PortableGlobParser是一个两变体的枚举portable_glob.rs#L64-L73pub enum PortableGlobParser { /// Follow the PEP 639 rules strictly. Pep639, /// In addition to the PEP 639 syntax, allow escaping characters with backslashes. /// /// For cross-platform compatibility, escaping path separators is not allowed, i.e., forward /// slashes and backslashes cant be escaped. Uv, }Pep639严格遵循 PEP 639反斜杠是非法字符Uv额外允许用反斜杠转义字符以便匹配、空格等 PEP 639 不逐字匹配的字符但出于跨平台兼容不允许转义/和\本身。测试用例 portable_glob.rs#L303-L332 给出了两套合法模式样例可作为语法参考// Pep639 与 Uv 均合法 rlicenses/*.txt rlicenses/**/*.txt rLICEN[CS]E.txt rLICEN?E.txt r[a-z].txt r[a-z._-].txt r*/** rLICENSE..txt rLICENSE_file-1.txt rlicenses/라이센스*.txt // 韩文 rlicenses/ライセンス*.txt // 日文 rlicenses/执照*.txt // 中文 rsrc/** // 仅 Uv 变体合法反斜杠转义 rpublic-domain/Gulliver\’s\ Travels.txt r**/\test三、解析实现check()前置校验与GlobBuilder构建PortableGlobParser::parseportable_glob.rs#L99-L106的实现分两步pub fn parse(self, glob: str) - ResultGlob, PortableGlobError { self.check(glob)?; Ok(GlobBuilder::new(glob) .literal_separator(true) // No need to support Windows-style paths, so the backslash can be used a escape. .backslash_escape(self.backslash_escape()) .build()?) }literal_separator(true)保证了*/?不会跨越/**的跨目录语义由globset内部处理。check()portable_glob.rs#L109-L219是一台逐字符的状态机在交给globset之前先做 PEP 639 合规性预校验覆盖以下细节星号数量限制***以及**后紧跟非/如**license都会报TooManyStars。源码注释说明原因这些形式可以用更少的星号等价表示globcrate 禁止、globset允许、而 PEP 639 文本本身存在歧义所以这里主动过滤。父目录检测..出现在字符串开头或/之后才判定为ParentDirectory如licenses/..位置 9 报错而LICENSE..txt这类文件名中的连续点是合法的。字符集校验[...]内部只允许字母数字、_、-、.出现其他字符如!、?报InvalidCharacterRange。反斜杠Pep639模式下任何\都是InvalidBackslashUv模式下\后必须是可转义字符转义/或\报InvalidEscapee结尾悬空报TrailingEscape。其他字符报InvalidCharacterPep639或InvalidCharacterUvUv多带一条 hint。错误类型与快照测试PortableGlobError共 8 个变体portable_glob.rs#L7-L46每种都有带错误位置 原始 glob的格式化消息。test_error测试portable_glob.rs#L228-L301用 insta 快照锁定了代表性错误输出例如The parent directory operator (..) at position 0 is not allowed in glob: .. The parent directory operator (..) at position 9 is not allowed in glob: licenses/.. Invalid character ! at position 14 in glob: licenses/LICEN!E.txt Invalid character ! in range at position 15 in glob: licenses/LICEN[!C]E.txt Too many at stars at position 9 in glob: licenses/**license Only forward slashes are allowed as path separator, invalid character at position 8 in glob: licenses\eula.txtUv 变体还演示了 hint 的呈现**/test报错后会附带hint: Characters can be escaped with a backslash见下一节。四、GlobDirFilter从 Glob 到 DFA 的目录预过滤构建from_globsGlobDirFilter持有两个字段glob_dir_filter.rs#L15-L18pub struct GlobDirFilter { glob_set: GlobSet, dfa: Optiondfa::dense::DFAVecu32, }from_globsglob_dir_filter.rs#L24-L73做了三件事正则转换把每个 glob 的regex()输出去掉(?-u)前缀glob 本身是逐字节匹配的非 unicode 正则并把模式中的/替换为平台的MAIN_SEPARATOR使在 Windows 上运行时能匹配反斜杠路径构建GlobSet供match_path做精确的该路径是否匹配判断构建 dense DFA用regex_automata的dfa::dense::Builder以Anchored起始方式编译所有 glob 正则并设置dfa_size_limit与determinize_size_limit两者共用常量DFA_SIZE_LIMIT 1_000_000字节源码注释直言Chosen at a whim。若 DFA 构建失败通常是组合爆炸超出限制则记录warn!并置dfa None退化为完整目录遍历即match_directory恒返回true保证功能正确性优先于剪枝效率。匹配算法match_directory与match_pathmatch_pathglob_dir_filter.rs#L78-L80是文件或目录是否匹配任一 glob的最终判定pub fn match_path(self, path: Path) - bool { self.match_directory(path) || self.glob_set.is_match(path) }match_directoryglob_dir_filter.rs#L86-L120才是预过滤的核心。它的语义是该目录或其任意子孙可能命中——永不漏判不会因返回 false 而丢掉实际匹配的子孙但允许误报返回 true 但最终没有子孙命中。算法要点根路径空Path直接放行没有 DFA 时恒返回true即退化路径逐字节把路径喂给 DFAanchored 起始状态得到读到路径末尾的状态state计算两个后继状态eoi_state next_eoi_state(state)目录自身是否完整匹配某个 glob例如 glob 是foo/*时目录foo/bar本身可命中slash_state next_state(state, MAIN_SEPARATOR)目录之后还能不能再接路径分量例如 glob 是foo/bar/*时目录foo/bar需要继续下钻。注意源码特意不对slash_state调next_eoi_state因为要检查的是还能不能再加字符而不是此处是否到达$锚点返回is_match_state(eoi_state) || !is_dead_state(slash_state)。这一自身匹配或可继续下钻的双条件恰好与filter_entry的需求吻合filter_entry只需要知道该子树值不值得进入。测试证据预过滤确实剪掉了分支prefilter测试glob_dir_filter.rs#L164-L218在临时目录里构造了 5 条path*/dir*/subdir/a.txt文件链配合 5 个代表性模式const PATTERNS: [str; 5] [ path1/*, // 只需下钻一级 path2/dir2, // 只需下钻一级 path3/dir3/subdir/a.txt, // 精确到文件 path4/**/*, // 需要完整下钻 path5, // 只匹配目录本身无需下钻 ];断言结果显示WalkDir实际访问的条目为、path1、path1/dir1、 path2、path2/dir2、 path3、path3/dir3、path3/dir3/subdir、path3/dir3/subdir/a.txt、 path4、path4/dir4、path4/dir4/subdir、path4/dir4/subdir/a.txt、 path5 ← 注意path5/dir5 及更深层完全未被访问path5只匹配目录自身match_directory(path5/dir5)返回false于是filter_entry剪掉了整个子树——这正是目录永远不会再匹配的跳过语义。同文件的walk_dir测试glob_dir_filter.rs#L221-L282进一步验证在filter_entry用match_directory剪枝、对留下的条目用match_path做最终筛选后得到的文件集合与预期完全一致即预过滤不会造成漏选。五、与WalkDir的集成范式main.rs演示crates/uv-globfilter/src/main.rs 是一个可运行的参考实现展示了 README 中 motivating example 的完整落地方式let includes [src/**, pyproject.toml]; let excludes [__pycache__, *.pyc, *.pyo]; // include用 PortableGlobParser::Pep639 解析后交给 GlobDirFilter let include_matcher GlobDirFilter::from_globs(include_globs).unwrap(); // exclude构造 unanchored GlobSet let mut exclude_builder GlobSetBuilder::new(); for exclude in excludes { // Excludes are unanchored let exclude if let Some(exclude) exclude.strip_prefix(/) { exclude.to_string() } else { format!(**/{exclude}).to_string() }; let glob PortableGlobParser::Pep639.parse(exclude).unwrap(); exclude_builder.add(glob); } let exclude_matcher exclude_builder.build().unwrap();两个值得注意的设计exclude 是非锚定的不带前导/的排除模式会被自动包上**/前缀因此exclude [__pycache__]会排除任意深度的__pycache__目录而/dist这类带前导/的模式去掉/后只匹配相对根目录的顶层路径。这与 README 示例中exclude [target, /dist, .cache, *.tmp]的写法完全对应。两级过滤在WalkDir上先以include_matcher.match_directory(relative) !exclude_matcher.is_match(relative)做filter_entry剪枝再对幸存条目做if !include_matcher.match_path(relative) || exclude_matcher.is_match(relative) { continue; }做最终包含/排除判定。剪枝阶段宁松勿漏最终阶段才精确裁决。注意演示二进制的includes/excludes是硬编码的当前仓库中如此它的作用是在开发期验证过滤行为生产代码如 uv-build-backend则从pyproject.toml读取同构的配置。六、在 uv 构建后端中的真实应用uv-globfilter目前唯一的下游使用方是 crates/uv-build-backenduv 的 PEP 517 构建后端实现有三条调用链分别对应不同的配置项与 parser 变体。6.1project.license-filesPEP 639严格Pep639变体PEP 639 定义了license-files字段用于声明许可证文件 glob。uv 在构建 source dist 与 wheel 时都消费它源分发包source_dist.rs#L138-L147 中pyproject_toml.license_files_source_dist()返回的每个 glob 都用PortableGlobParser::Pep639.parse(...)解析错误上下文标记为project.license-fileswheelwheel.rs#L208-L225 中wheel 构建把命中的许可证文件写入{dist-info-name}/{version}.dist-info/licenses/目录通过wheel_subdir_from_globs内部同样使用Pep639变体与GlobDirFilter::from_globs元数据metadata.rs#L712-L729 中wheel 的METADATA也按 PEP 639 变体解析license_files且 metadata.rs#L589-L599 显示一旦检测到project.license-files或license { text ... }SPDX 表达式METADATA 版本即提升到 2.4。因为这是标准字段必须严格遵循 PEP 639故使用Pep639变体而非Uv变体。6.2tool.uv.build-backend.source-include宽松Uv变体自定义源包含是 uv 的扩展配置允许反斜杠转义以便匹配带特殊字符的文件名因此使用Uv变体。source_dist.rs#L114-L122for include in includes { let glob PortableGlobParser::Uv .parse(include) .map_err(|err| Error::PortableGlob { field: tool.uv.build-backend.source-include.to_string(), source: err, })?; include_globs.push(glob); }配置项文档settings.rs#L51-L60给出的官方示例即[tool.uv.build-backend] source-include [tests/**]同一段构建流程中数据目录tool.uv.build-backend.data.name也以{dir}/**形式经Uv变体解析后加入 include globssource_dist.rs#L149-L171而模块目录与pyproject.toml、readme 始终被强制包含。最终所有 include globs 经GlobDirFilter::from_globs合并为唯一include_matchersource_dist.rs#L180-L184与本文第五节的main.rs范式一致。6.3source-exclude与默认排除排除侧的逻辑在 source_dist.rs#L186-L200default-excludes默认true内置默认排除为__pycache__、*.pyc、*.pyosettings.rs#L62-L70用户source-exclude与默认值合并去重后构造exclude_matcher有一个硬性校验若排除规则命中pyproject.toml直接报错Error::PyprojectTomlExcluded错误文案见 lib.rs#L59pyproject.tomlmust not be excluded from source distribution build因为源分发包没有pyproject.toml就无法再被构建。排除模式在build_exclude_matcher中同样遵循main.rs展示的非锚定约定自动补**/前缀且这些排除对 source dist 与 wheel 生效保证从源树直接构 wheel与先构 sdist 再构 wheel产物一致settings.rs#L72-L79 的注释明确了这一点。七、错误提示设计Hint与构建后端的呈现PortableGlobError实现了uv_errors::Hinttraitportable_glob.rs#L48-L57只有InvalidCharacterUv变体会附带 hint——Characters can be escaped with a backslash。这是刻意针对Uv变体的当用户写了 PEP 639 不支持逐字匹配的字符如提示应引导其使用反斜杠转义而不是直接失败。构建后端如何呈现这条 hint可在 lib.rs#L513-L528 的快照测试format_err_renders_portable_glob_hints中验证Unsupported glob expression in: tool.uv.build-backend.source-include Caused by: Invalid character at position 3 in glob: **/test hint: Characters can be escaped with a backslash外层Error::PortableGlob携带出错配置项的字段名内层PortableGlobError携带位置与原始 glob配合 hint 形成字段 原因 修复建议三层信息。八、小结与边界说明语法边界uv-globfilter实现的是 PEP 639 的受限glob 子集——不支持绝对路径无前导/、不支持..、不支持反斜杠路径分隔符Uv变体是唯一合法的反斜杠用途转义非分隔符字符。预过滤语义match_directory是可能包含判断存在误报、不存在漏报真正的包含判定必须由match_path完成。两者配合WalkDir::filter_entry与最终filter_map使用glob_dir_filter.rs的测试保证了该组合下文件集合的正确性。性能边界DFA 大小上限为 1,000,000 字节超限后退化为完整遍历正确性不变仅失去剪枝收益globset层面的GlobSet构建失败则由调用方如source-include场景包装为GlobSetTooLarge错误向用户报错。当前适用范围截至当前仓库版本uv-globfilter 0.0.76该 crate 仅被uv-build-backend使用服务于 sdist/wheel 构建中的文件选择仓库中未见其他 crate 依赖它除自身main.rs演示外。如需深入建议按以下顺序阅读源码crates/uv-globfilter/src/portable_glob.rs语法与错误、crates/uv-globfilter/src/glob_dir_filter.rsDFA 预过滤、crates/uv-globfilter/src/main.rs集成范式再到 crates/uv-build-backend/src/source_dist.rs 与 crates/uv-build-backend/src/wheel.rs生产调用链。【免费下载链接】uvAn extremely fast Python package and project manager, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/uv/uv创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考