ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

repo 报错 No module named formatter 修复指南

repo 报错 No module named formatter 修复指南 前阵子在群里看到一张报错截图命令行里只甩出一行ModuleNotFoundError: No module named formatter上面还带着 repo 的字样发图的人一句repo 是不是坏了底下一群人开始猜有人让他把.repo删了重拉有人让他git reset --hard还有人建议直接重装系统。我看了一眼就知道大部分人方向找错了——这行报错的锅真的不在 repo 本身而在于跑 repo 的那个 Python 解释器。ModuleNotFoundError、No module named、formatter这三个关键词凑在一起本质上是老工具 新解释器的经典组合拳。repo 本身只是被顺带牵连的那个倒霉蛋。这篇东西我打算把这类报错从根上拆一遍为什么formatter会突然找不到、怎么用五条命令锁定真凶、四种修法各自适合什么场景、踩坑点在哪。写这个的出发点很简单——这类报错在网上被讨论得极碎搜出来的答案东一句西一句真到自己机器上还是不知道怎么动手。技术上有点底子的可以直接跳到第 3 节看定位命令刚接手一台新机器、连 repo 和 yum repo 都还没分清的朋友建议从头看第 1 节会先把repo这个词掰开这步不做后面全是白忙。另外我把 CentOS 上那套cannot find a valid baseurl for repo也一并放进来聊了因为这两个报错经常在同一个下午一起冒出来很多人会误以为是同一个问题。1. 先搞清楚这条报错里到底有几个reporepo这个词在工程圈是被严重滥用的。它至少在四个完全不同的语境里出现而且这四个语境各有各的报错风格、各有各的修法。看到repo就开修等于医生看到疼就开止痛药。1.1 三个 repo三套完全不同的故障模型第一种是repo 命令也就是大家最常说的那个。它本质上是一个用 Python 写的多仓库管理工具配合repo init/repo sync/repo forall这套子命令使用管的是几十上百个 git 仓库的批量同步。有意思的是它自己的安装方式也挺特别你在/usr/local/bin或者~/bin里放一个叫repo的启动器launcher启动器第一次运行时会把自己下到~/.repo/repo/目录之后真正的逻辑都跑在那里面。所以repo 坏了和repo 管的那堆仓库坏了是两件完全不同的事。第二种是git repo也就是单个版本库。这个的报错通常是fatal: not a git repository、fatal: refusing to merge unrelated histories这种跟 Python 一点关系都没有。第三种是yum/dnf repo软件源仓库配置文件躺在/etc/yum.repos.d/下面一水儿的.repo后缀。它报错的样子是cannot find a valid baseurl for repo: base/7/x86_64或者cannot find a valid baseurl for repo: centos-sclo-rh/x86_64。注意这个报错里也有repo这个词但它是源的名字不是工具。第四种是各种包管理器自造的 repo 概念apt 的 sources.list、npm 的 registry 配置、pip 的 index-url都可以被口语化地叫成源或repo。这些基本不会出现在今天的报错里先放一边。1.2 从报错文本的前缀直接判断归属判断归属其实有个非常省事的办法看报错里除了 repo 之外还出现了什么词。我把常见组合整理成下面这张表遇到报错先查表能省十分钟以上。报错里的伴生关键词归属排查入口ModuleNotFoundError/ImportError/TracebackPython 解释器问题repo 只是被牵连python3 -V、完整 tracebackbaseurl/mirrorlist/yum/dnf软件源配置问题/etc/yum.repos.d/*.repofatal:/HEAD/branch/mergegit 仓库状态问题git status、repo statusPermission denied/EACCES文件权限或运行用户问题看是不是用了sudo这个表里最容易被忽略的是第一行和第二行的区别。它们的共同点是都带repo区别在于Python 那行报错里的repo通常出现在命令行本身比如你敲的是repo sync而 yum 那行报错里的repo出现在冒号后面是源的名字。这个细节特别小但能帮你瞬间排除掉一半的错误方向。还有个小技巧把报错整行复制到搜索框之前先手动删掉路径里的用户名和主机名。同一台机器上的路径里往往带着工号、项目代号这些信息泄漏出去完全没必要而且会让搜索结果变差——搜索引擎会被这些噪音带偏。2. formatter 模块的死亡时间线与三类 ModuleNotFoundError 的分野要理解为什么formatter会找不到得先知道它是什么、什么时候没的。这一步搞清楚了后面所有修法都是顺理成章的。2.1 formatter 是什么它是什么时候被拿掉的formatter是 Python 标准库里的一个老模块干的事情是通用文本排版——把文本流按指定宽度做换行、缩进、段落处理输出到文件或终端。它内部有一整套抽象AbstractFormatter负责管格式AbstractWriter负责管输出DumbWriter是给纯文本终端用的简易输出器NullWriter就是个黑洞什么都不输出专门用来测量排版结果。在 Python 2 时代不少命令行工具的帮助信息美化、表格输出都靠它。这个模块的命运很清晰从 Python 3.4 开始被标记为废弃deprecated文档里明明白白写了不建议再使用推荐用textwrap代替。然后到了Python 3.10它被正式从标准库里移除了。同一批被移走的还有parser和symbol这两个解析器相关的模块。所以你机器上的 Python 版本一旦到了 3.10 及以上3.11、3.12、3.13 都算只要代码里有import formatter百分百报ModuleNotFoundError: No module named formatter。而 3.9 及以下跑同一份代码顶多打一个DeprecationWarning功能照常。这就是为什么很多老项目在旧机器上岁月静好一换新机器立刻炸锅。2.2 三类 ModuleNotFoundError三种完全不同的修法这里是我觉得最值得单独讲一段的东西。ModuleNotFoundError: No module named xxx这个报错看起来长得一模一样但它背后的成因至少分三类修法完全不同。很多人一看没这个模块就去pip install结果装完照旧白忙一场。类型典型模块名本质原因正确的修法已被移除的标准库formatter、parser、imp、distutils解释器版本升上去了模块没了降版本 / 打兼容垫片 / 改代码第三方包没装yaml、numpy、opencv、cdsapi环境里确实没有这个包pip install注意装对解释器项目内的私有模块utils.features、utils.features这类带点的运行目录不对或PYTHONPATH没设从项目根目录启动或设PYTHONPATH打包/环境错乱pkg_resources、_cffi_backend依赖装了一半、编译失败、解释器错配重装对应包看编译日志看到formatter落在第一类了吧这一类的标准答案是 pip install是错的——PyPI 上确实有个叫formatter的包但那是个跟标准库毫无关系的第三方格式化工具装上去只会让你更迷惑。我见过不止一个人在这上面绕了半个多小时。顺带说一句pkg_resources。它属于setuptools这个包正常环境下应该跟着 setuptools 一起装好。如果它报找不到说明你环境里的 setuptools 要么被卸了要么被某个虚拟环境隔离了要么是 pip 和 python3 指向了不同的解释器。这个报错的排查思路和formatter完全不是一回事。2.3 为什么偏偏在 repo 场景下集中爆发把 2.1 和 2.2 串起来看就明白了。你手上那个repo启动器很可能是个挺老的版本有些公司内网镜像里躺的还是几年前抓下来的它内部依赖的一些文本输出逻辑用了formatter而你新装的机器或者新拉的容器镜像系统自带的python3已经是 3.11 或者 3.12 了。老代码碰上新解释器报错就这么来的。另外还有一层repo启动器的 shebang 一般写的是#!/usr/bin/env python3这个写法的意思是用 PATH 里第一个 python3。如果你机器上装了多个 Python系统自带的 pyenv 的 conda 的PATH 顺序稍微一变repo 跑起来的解释器就换了个人。同一份代码今天能跑明天不能跑八成是这里。3. 定位实操五条命令锁定真凶到这一步该动手了。下面这五步我按执行顺序排好了绝大部分情况下走完前三步就定位到根上了。3.1 第一步确认解释器和版本which -a python3 python3 -V python3 -c import sys; print(sys.executable)which -a会把 PATH 里所有同名的 python3 都列出来这个信息很关键——如果你看到三个路径那到底用的哪个就是必须确认的事。第二条打印版本号3.10 及以上就要开始往formatter这条线上想了。第三条打印解释器的绝对路径它会告诉你真正在跑的是哪个二进制。如果确认是 repo 命令报的错再加一条head -n 5 $(which repo) ls -l ~/.repo/repo/第一眼看启动器用的什么解释器第二眼看本地缓存的那份 repo 代码是什么时候拉下来的。3.2 第二步拿到完整 traceback这一步我要重点强调因为它是所有人最容易偷懒的地方。很多人截图只截最后一行而ModuleNotFoundError的价值恰恰在上面那几行。python3 ./your_script.py 21 | tail -n 40或者直接跑 repo 内部的主脚本让它把栈打全python3 ~/.repo/repo/main.py sync 21 | tail -n 40完整的 traceback 长这样Traceback (most recent call last): File /home/me/.repo/repo/main.py, line 42, in module from subcmds import all_commands File /home/me/.repo/repo/subcmds/xxx.py, line 18, in module import formatter ModuleNotFoundError: No module named formatter最底下那行是结果中间的File ..., line N才是凶手地址。有了这个路径和行号你甚至不需要猜了。3.3 第三步grep 出谁在 import formatter拿着上一步的路径去搜或者直接全目录扫一遍grep -rn --include*.py -E ^[[:space:]]*(import|from)[[:space:]]formatter ~/.repo/repo/ grep -rn formatter ~/.repo/repo/ | head -n 20如果不在.repo里就把范围扩大到你的项目目录、构建脚本目录、CI 脚本目录。有些团队会在仓库里放自定义的hooks/或者tools/脚本那些脚本才是真正 import 的人repo 只是恰好在调用链上游。grep的时候注意要同时搜import formatter和from formatter import xxx两种写法前者漏了后者很常见。3.4 第四步排掉文件名遮蔽这个干扰项Python 的模块查找有个特性当前目录和PYTHONPATH里的路径优先级高于标准库。如果你项目根目录下恰好有个formatter.py或者有个叫formatter的目录Python 3.3 之后没有__init__.py的目录也能被当成命名空间包那import formatter命中的就是它而不是你以为的那个。python3 -c import formatter; print(formatter.__file__ if hasattr(formatter, __file__) else namespace)这里要说清楚一点避免误导文件名遮蔽本身不会导致ModuleNotFoundError——它只会让 import 成功但行为诡异比如找不到AbstractFormatter属性或者拿到一个空的命名空间包。所以在纯报找不到模块的场景下第四步的作用是为后面的垫片方案排雷你得确认自己放的那个兼容文件真的被加载了而不是被别的东西抢先命中。这一点在踩坑记录里我会再展开。3.5 第五步检查环境串台环境串台是这类问题的隐形杀手尤其在你用了sudo之后。pip -V echo $PYTHONPATH env | grep -i pythonpip -V会打印 pip 绑定的解释器路径。如果它和你which python3出来的路径对不上那你一直装的包根本没装到跑代码的那个环境里。PYTHONPATH里如果有历史遗留的脏路径也可能把正常的模块查找顺序搞乱比如指向了一个早已删除的目录。还有一个高频场景你的虚拟环境没激活。source venv/bin/activate之前和之后python3指向的完全是两个东西。用sudo跑命令时更明显sudo默认会重置环境变量虚拟环境直接失效跑起来的是系统 Python。4. 四种修复方案按代价从低到高排定位完了就该修。我按改动范围从小到大的顺序排了四种方案优先选改动小的能不动代码就不动代码。4.1 方案一升级工具链首选成本最低如果 importformatter的是某个开源工具本身那最干净的做法是把它升级到新版本让维护者去处理这个兼容性问题。对 repo 来说重新获取一份最新的启动器覆盖掉旧的就行。方法是把~/bin/repo或者/usr/local/bin/repo替换成最新的那个文件然后删掉本地缓存让它重新下载rm -rf ~/.repo/repo repo --version第一次运行会自动重新拉取。想先确认当前版本有多老的话cd ~/.repo/repo git log -1 --dateshort --format%h %ad %s打印出来的日期如果是两年前的基本可以确定要升级。我处理过的一个案例里本地缓存的那份 repo 代码是三年半前的快照升级之后formatter的报错直接消失了。如果是公司内部封装的工具先看看有没有新版本发布如果没有就把问题报给维护者同时在本地临时用方案二顶着。4.2 方案二打兼容垫片把老模块请回来升级路走不通的时候垫片是最实用的选择。思路很直接Python 3.9 里formatter.py还在把那个文件拿出来放到能被 import 到的地方就行了。这个文件依赖的都是sys、re这类一直没被动过的老牌标准库模块单独拿出来可以正常使用。具体步骤# 1. 建一个专门的垫片目录别直接往 site-packages 里塞 SHIM_DIR$HOME/.pyshim mkdir -p $SHIM_DIR # 2. 找到 formatter.py # 路线 A本机如果还残留 3.9直接找 find / -name formatter.py -path *python3.9* 2/dev/null # 路线 B从 CPython 3.9 的源码包里的 Lib/formatter.py 取 # 源码包解压后直接复制那个文件即可 # 3. 复制到垫片目录 cp /path/to/formatter.py $SHIM_DIR/ # 4. 临时验证 PYTHONPATH$SHIM_DIR python3 -c import formatter; print(formatter.__file__)第 4 步能打印出$SHIM_DIR/formatter.py这个路径就说明垫片生效了。想让它长期生效把环境变量写进 shell 配置echo export PYTHONPATH$HOME/.pyshim:$PYTHONPATH ~/.bashrc source ~/.bashrc注意PYTHONPATH是全局生效的加之前先ls -la $SHIM_DIR确认目录里只有你放的那一个文件。如果里面混了别的东西可能会意外覆盖掉标准库里的同名模块那就不是修 bug 而是造 bug 了。注意别pip install formatter。PyPI 上那个同名包是个独立的代码格式化工具跟标准库的formatter没有任何关系装了也解决不了问题。这个坑我见人踩过。用垫片有个明显的代价它是在还技术债不是消灭技术债。将来 Python 再升一级或者换个更严格的运行环境垫片可能又不好使了。所以它适合当过渡手段别当长期方案。4.3 方案三换解释器版本如果这个工具链短期内没法升级代码也不方便动那把运行环境降回 Python 3.9 是最省事的做法。用 pyenv 或者 conda 建一个 3.9 的环境在里面跑就完事了。# 以 pyenv 为例 pyenv install 3.9.18 pyenv local 3.9.18 python3 -V注意两个坑。第一repo这类工具脚本可能写死了 shebang环境切换之后which python3变了但 shebang 指向的还是老路径。这时候需要确认启动器里到底写的什么。第二3.9 本身也在逐渐退出维护周期这属于把问题往后推不是解决。容器场景下就是换 base image 的 tag把python:3.11-slim换成python:3.9-slim。这里要额外留意base image 换了之后之前pip install的那批依赖可能全要重装因为 wheel 包的 ABI 标签对不上。4.4 方案四改代码把 formatter 的用法替掉这是最彻底的方案前提是你能改源码。formatter干的活其实textwrap全能干而且后者是现在的标准做法。常见用法的对应关系老写法formatter现代替代textwrapformatter.DumbWriterAbstractFormattertextwrap.TextWrapperformatter.format()单次排版textwrap.fill(text, width)formatter.NullWriter丢弃输出直接不输出或用io.StringIO()AbstractWriter自定义输出继承textwrap.TextWrapper重写write回调举个实际替换的例子。老代码可能是这样import formatter import sys writer formatter.DumbWriter(sys.stdout, maxcol72) fmt formatter.AbstractFormatter(writer) fmt.add_flowing_data(一段很长的文本……) fmt.flush()换成textwrap之后import textwrap text 一段很长的文本…… print(textwrap.fill(text, width72))如果原文是多段结构中间有空行textwrap.fill会把它当成一整段处理空行会被吃掉。这时候按空行切开逐段处理import textwrap def wrap_paragraphs(text, width72): paras text.split(\n\n) return \n\n.join(textwrap.fill(p, widthwidth) for p in paras if p.strip()) print(wrap_paragraphs(第一段……\n\n第二段……, width72))改动量不大但要注意逐段处理时缩进会丢如果原文依赖缩进表达层级得自己在 fill 之前把每行的前导空格提取出来处理完再补回去。这个细节不注意的话输出看着就是对齐全乱了。4.5 四种方案怎么选方案适用场景改动成本遗留风险升级工具链开源工具且新版已修复低几乎无打兼容垫片工具无法升级临时顶用低技术债需后续清理换 Python 版本全链路依赖老版本短期无法升级中版本本身也会 EOL改代码源码可控有长期维护打算中高需回归测试覆盖我的建议顺序是能升级就升级升不了打垫片垫片长期挂着就安排改代码。换 Python 版本这一招放在最后因为它会把问题从一个工具扩散到整个环境。5. 顺带一手CentOS 上 repo / baseurl 那点事前面反复提到 yum 那个 repo 报错这里单独拿出来说清楚。它和 Python 的formatter报错经常在同一台新机器上前后脚出现很容易被当成同一个问题。5.1 cannot find a valid baseurl 到底在说什么cannot find a valid baseurl for repo: base/7/x86_64这句话的意思是yum 想从名字叫base的源对应 7 版本的 x86_64 架构去下载东西但翻遍了配置文件里的baseurl和mirrorlist没找到一个能用得上的地址。常见成因有这么几个一是系统的.repo配置文件里用的还是官方的mirrorlist地址而这个地址在特定版本进入生命周期末期之后不再对外提供正常响应。你在内网机器上执行curl -I试一下那个地址大概率是连不上的。二是baseurl那一行被注释掉了或者写错了。yum 的配置文件里baseurl和mirrorlist是二选一的关系两个都注释掉就等于没有源。三是机器本身出不了外网。这种情况换什么源都没用得先解决网络出口的问题或者搭一个内网的镜像站。cannot find a valid baseurl for repo: centos-sclo-rh/x86_64属于同一类问题只是那个源名字不一样。SCLo 是软件集合相关的源很多老机器上都有它比基础源更早停止对外服务。5.2 换源的实操与验证思路是把官方源配置文件备份走换成能访问的镜像源。下面这套流程我执行过很多次比较稳。# 1. 先备份别直接覆盖 mkdir -p /etc/yum.repos.d/backup mv /etc/yum.repos.d/CentOS-*.repo /etc/yum.repos.d/backup/ # 2. 拉一个可用的源配置以镜像站为例 curl -o /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo # 3. 清缓存、重建缓存 yum clean all yum makecache # 4. 验证 yum repolist第 4 步的yum repolist是必须做的它会列出当前所有启用中的源和各自的包数量。能看到条目才算真的成了如果输出的表是空的或者某个源后面标着status有问题那就还得回头看第 2 步拉下来的文件内容。动第 2 步之前建议先确认机器能访问镜像站curl -I https://mirrors.aliyun.com/repo/Centos-7.repo看到 HTTP 状态码再往下走比拉完一堆配置再排查要省事得多。如果你只想手工写一个最小可用的源配置大概是这个结构[base] nameCentOS-$releasever - Base baseurlhttps://mirrors.aliyun.com/centos/$releasever/os/$basearch/ gpgcheck1 gpgkeyfile:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 enabled1注意$releasever和$basearch是 yum 内置变量会分别替换成版本号和架构名。别手动把它们替换成固定值除非你确定这台机器一辈子不升级。写成固定值之后换机器迁移会出问题。注意gpgcheck1是更稳妥的做法配合gpgkey指到本机的公钥文件。有些内网环境为了省事会设成gpgcheck0短期图方便可以理解但要知道这样做等于放弃了包完整性校验。5.3 别把两类报错混在一起修这两个报错容易混是因为它们都带repo这个词而且经常是同一台新机器上的连招你刚装完系统先配源配完源装 Python装完 Python 拉代码然后撞上formatter。但它们真的一点因果关系都没有。判断方法很简单看报错里有没有这几个词baseurl、mirrorlist、yum、dnf。有就是源的问题去/etc/yum.repos.d/找没有而是带Traceback、ModuleNotFoundError、File ...就是 Python 解释器的问题去查版本和依赖。我见过有人在配源阶段折腾两小时最后发现真正卡住的是 Python 版本也见过反过来改了半天sys.path结果问题是源不可用导致依赖根本没装上。先分类再动手。6. 常见问题速查表与踩坑记录最后这部分是我自己踩过和见过的坑整理成速查表遇到问题先扫一眼。6.1 ModuleNotFoundError 家族速查表现象大概率原因一句话处理No module named formatterPython ≥3.10 跑了老代码升级工具或放兼容垫片No module named yaml没装 PyYAMLpip install pyyaml注意解释器No module named numpy/opencv依赖缺失或装到了别的环境对齐解释器后重装No module named pkg_resourcessetuptools 缺失或被隔离pip install -U setuptoolsNo module named utils.features运行目录不对私有包没进路径cd 到项目根目录再跑No module named _cffi_backendcffi 编译失败或装了一半重装 cffi看编译日志No module named cdsapi第三方包没装pip install cdsapiNo module named yaml但明明装过装到了另一个 Pythonpip -V对比which python3cannot find a valid baseurl for repo源的 URL 不可达换源 yum clean allImportError: cannot import name xxx from yyy包版本不对不是没装对比包版本和方法名注意最后一行和前面几行的区别。ImportError: cannot import name和ModuleNotFoundError: No module named是两回事前者是包装上了但版本不对后者是根本没找着。修法完全不同看清楚再动手。6.2 我踩过的八个坑第一个坑是只看最后一行报错。这几乎是所有人的默认习惯但ModuleNotFoundError的全部价值都在上面那几行File ...。看不到调用链等于闭着眼睛修车。第二个坑是**pip install formatter**。前面说过PyPI 上的同名包是另一回事。任何标准库模块找不到的情况都要先在脑子里过一遍这玩意儿是不是被 Python 删了而不是反射性地去 pip。第三个坑是用sudo跑命令。sudo默认会重置环境变量你的虚拟环境瞬间失效跑的是系统解释器。表现出来就是我明明装了包怎么还说找不到。要么不用sudo要么用sudo -E保留环境前提是路径权限对。第四个坑是**python和python3不是一回事**。有些机器上python还是 Python 2有些机器上python根本不存在。脚本里写python xxx.py和写python3 xxx.py跑起来可能是两个世界。第五个坑是**__pycache__里的旧字节码**。放完垫片之后如果还是报错试试把相关目录的__pycache__清掉find ~/.repo/repo -name __pycache__ -type d -exec rm -rf {} 2/dev/null这种情况不常见但一旦遇上会非常迷惑因为文件明明在那里。第六个坑是垫片放对了目录但脚本从别的目录启动。Python 的sys.path[0]是启动脚本所在的目录不是你的当前工作目录。你把formatter.py放在项目根目录但从tools/下面启动脚本那它可能就找不到——除非项目根目录正好在PYTHONPATH里或者你的启动方式把它加进去了。第七个坑是把垫片塞进全局 site-packages。短期能用但 Python 小版本一升site-packages 的路径就变了比如从python3.11变成python3.12垫片又失效了而且失效得毫无征兆。用一个独立的垫片目录配合PYTHONPATH更可控。第八个坑是多个 Python 共存时的 pip 错配。pip install成功python3 -c import xxx失败。解决办法就是用python3 -m pip install xxx让 pip 永远跟着你指定的那个解释器走。这条规则值得刻在脑子里能省掉大量无谓的排查。6.3 别急着 reset报错和仓库内容无关最后一个踩坑点比较特殊但我觉得值得单独说。看到 repo 报错很多人的第一反应是仓库状态坏了然后顺手就是一套repo forall -c git reset --hard repo forall -c git clean -df这是最亏的操作。本次讨论的formatter报错属于工具层问题你的仓库内容根本没被动过——工具连跑都没跑起来怎么可能改到你的代码这一套下去本地正在做的改动全没了而且不可恢复。正确的顺序是repo status repo diff先看清楚现在仓库里有什么改动再决定要不要动。真的需要临时保存改动的话git stash比reset --hard温和得多repo forall -c git stash push -m before-fix这样至少在修完问题之后还能git stash pop找回来。reset --hard是没有后悔药的。顺便说如果哪天你确实需要把某个仓库恢复成刚拉下来的样子比较稳妥的做法也是先 stash 或者先建分支repo forall -c git stash push -u -m backup-$(date %F) repo sync -drepo sync -d会把工作区切回清单里记录的那个版本比手工 reset 可控一些。但前提还是那句先确认你要丢的东西真的不要了。我个人在实际操作中的体会是ModuleNotFoundError这类报错里九成的排查时间花在搞清到底是哪一层出问题上剩下那一成才是动手改。把版本、路径、调用链这三件事查清楚剩下的基本都是体力活。
返回列表