恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Python后端工程化与测试03:为什么换个目录就导入失败——用src布局验证安装结果

  • 首页
  • 资讯中心
  • /
  • Python后端工程化与测试03:为什么换个目录就导入失败——用src布局验证安装结果

相关资讯

AAS 代码评审技能深度解析:code-review-and-quality 的五轴评审、严重度分级与结构性修复方法论 2026/10/21 2:22:55
C笔试算法 2026/10/21 2:17:55
Gauge Python运行器安装与配置避坑指南 2026/10/21 2:17:55

最新资讯

C#实现Modbus TCP主从站完整源码解析:无需手写协议栈
机器人地图模块lingbot-map:空间数据处理与区域查询实战
理解物理层:带宽、奈奎斯特与香农如何决定真实网速
课程论文写作难?AI辅助工具如何从选题到降重全流程提效
ESP-Drone 上手指南:用 ESP32 打造一台可飞的四旋翼
从建图到导航:lingbot-map栅格地图的坐标系、分辨率与实战排查

今日推荐

SAP 屏幕上的金额未必存在数据库表里,从 WB2R_SV 的 Net Amount 看懂运行时计算
具身智能中‘指哪打哪’的视觉语言动作联合建模
AI时代人工翻译技术博客的价值与实践指南

本周热门

基于GMM的语音识别系统原理与MATLAB实现详解
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

Python后端工程化与测试03:为什么换个目录就导入失败——用src布局验证安装结果

发布时间:2026/10/21 2:22:55
Python后端工程化与测试03:为什么换个目录就导入失败——用src布局验证安装结果 Python后端工程化与测试03为什么换个目录就导入失败——用src布局验证安装结果“我电脑上能运行”的一个常见前提是终端恰好停在源码目录Python恰好把当前目录加入了导入路径。此时import quoteledger成功只能证明磁盘上有一个同名目录不能证明安装包包含它。本课把项目整理为src布局让“可以安装”成为明确的交付能力。报价规则、状态策略和HTTP合同都不改变。上一课答案先确认隔离仍然成立E02的参考答案使用两个应用实例直接断言A创建的id在B中不存在。数量边界通过pytest.mark.parametrize展开为四个案例True必须保留为布尔值不能写成字符串’true’因为那是另一种输入。运行命令$PY(Resolve-Path.\.venv\Scripts\python.exe).Path $PY-m pytest.\lessons\03\answer_previous.py--rootdir.\lessons\03-q预期为5 passed。这个答案仍指向01阶段说明上一课练习使用的材料没有随着本课安装布局改变而漂移。也可以直接运行该文件它会在独立pytest进程中执行相同测试。lessons/03/answer_previous.pyE02pytest文件也可作为脚本启动独立pytest进程。frompathlibimportPathimportsubprocessimportsysimportpytestfromfastapi.testclientimportTestClient STAGEPath(__file__).resolve().parents[2]/checkpoints/01sys.path.insert(0,str(STAGE))importappasbaseline# noqa: E402defpayload(quantity):return{sku:BOOK-001,supplier:alpha,quantity:quantity,unit_cents:1299,shipping_cents:500,}deftest_apps_do_not_share_quotes():assertPath(baseline.__file__).resolve()(STAGE/app.py).resolve()with(TestClient(baseline.create_app())asfirst,TestClient(baseline.create_app())assecond,):responsefirst.post(/quotes,jsonpayload(1))assertresponse.status_code201quote_idresponse.json()[id]assertfirst.get(f/quotes/{quote_id}).status_code200assertsecond.get(f/quotes/{quote_id}).status_code404pytest.mark.parametrize(quantity, status,[(1,201),(100,201),(0,422),(True,422)])deftest_quantity_boundaries(quantity,status):withTestClient(baseline.create_app())asclient:responseclient.post(/quotes,jsonpayload(quantity))assertresponse.status_codestatus,response.textifstatus201:assertresponse.json()[total_cents]1299*quantity500if__name____main__:# checkTrue让子进程测试失败变成当前脚本的非零退出。herePath(__file__).resolve()subprocess.run([sys.executable,-m,pytest,str(here),--rootdir,str(here.parent),-q,],checkTrue,timeout60,)项目目录与Python包不是同一回事03阶段有一个pyproject.toml和一个src/quoteledger目录。前者描述构建系统、发行名称、版本、依赖及工具配置后者才是Python导入包。发行包名称可包含连字符Python导入名通常使用下划线或普通标识符二者不要求完全相同。本课程直接from quoteledger.api import create_app不会导入名为src的包。src布局把可导入代码放在项目根目录下一层降低“靠当前目录侥幸导入”的可能。测试、文档和临时脚本留在包外也不必为了导入而把整棵项目目录塞进sys.path。__init__.py明确标记包它不应在导入时连接数据库、启动服务器或者读取只有生产机器才有的配置。导入应当轻量这会让测试发现、命令行工具和类型分析更可靠。项目元数据里的requires-python约束解释器版本dependencies是运行这个包所需的依赖。pytest属于开发与验证工具并不是处理一张报价单所需的运行依赖。把两者分开交付时才不会让客户为了启动API也安装所有测试工具。具体安装约束以本课完整pyproject为准需要逐字节可复现的构建时还要进一步冻结传递依赖的解析结果和构建环境。两种安装方式各自验证什么先安装03阶段当前环境不必同时安装多个同名阶段。一个虚拟环境里同一发行包只能有一个当前有效的安装结果顺序安装后面的阶段会替换前面的入口。因此本课程练习答案会显式选择对应快照最终交付验收再使用独立环境。$PY-m pip install-e.\checkpoints\03 $PY-cimport quoteledger; print(quoteledger.__file__)$PY-m uvicorn quoteledger.api:create_app--factory--host 127.0.0.1--port 9200-e表示editable可编辑安装环境保存与源码的关联修改Python源文件通常立刻影响下一次导入。它适合日常开发却容易掩盖构建遗漏。例如源码存在但没有被构建进wheeleditable可能仍然能读到客户安装wheel却失败。--factory告诉Uvicorn调用create_app创建应用不能把函数本身当作ASGI应用直接处理请求。普通安装会构建并安装发行物中的文件wheel是这种预构建分发格式扩展名.whl本质是带元数据的归档。第15课会从干净虚拟环境安装wheel。本课先建立基本意识验证导入时要同时检查“从哪里导入”不能只看“有没有报错”。$outsideJoin-Path([IO.Path]::GetTempPath())quoteledger-outsideNew-Item-ItemType Directory-Path$outside-Force|Out-NullPush-Location$outside$PY-cimport quoteledger; print(quoteledger.__file__)Pop-Location$PY前面被解析成绝对路径所以切换目录后仍能找到解释器。Push-Location保存原目录Pop-Location返回。如果看到路径仍在checkpoints/03/src/quoteledger在editable安装下这是预期如果我们声称验证的是普通安装这个路径就不合格。Python究竟先找到了谁sys.path是导入搜索目录列表Python按顺序查找。某个目录下有一个名为quoteledger.py的临时文件可能遮住真正的quoteledger包把测试文件命名为sqlite3.py、typing.py也会遮住标准库。错误经常表现为“部分初始化模块”“不是一个package”根源却是命名冲突。PYTHONPATH可以临时加目录但它不是打包方案。若开发机长期设置了指向最终project的PYTHONPATH运行03阶段测试时可能偷偷用到未来功能。参考答案为此在新子进程中验证安装目标并检查模块实际路径。子进程让sys.modules重新开始避免同一Python进程先导入08、再把路径换回03却仍复用旧模块对象。命令python -I启用隔离模式会忽略PYTHONPATH等环境影响并减少当前目录带来的导入污染。它不代表新建了一个全新的虚拟环境解释器已安装的依赖依然存在。下一课答案使用临时--target安装目录和隔离子进程验证本课自己的安装结果第15课再加“干净环境”这一更强约束。这两种证据回答的问题不同文章会明确写清。移动文件为什么也需要行为测试把01单文件移到包里时很容易顺手整理变量名、修改异常返回、增加version字段。这样测试失败后就无法区分是导入路径出错还是业务真的改变。03阶段只改变组织与安装方式仍然没有请求版本号confirm和cancel没有body。先保持行为再做结构提取可以让每次失败都更容易解释。如果测试在源码根目录通过在临时目录失败先看安装是否成功以及构建配置是否找到src里的包。不要立即在测试里堆叠sys.path.append直到变绿这会让测试绕过你真正想交付的安装行为。路径修补适合教学快照的显式选取不应伪装成发行物验证。另一个常见误区是直接运行python src/quoteledger/api.py。包内相对导入依赖包上下文直接执行文件时上下文可能丢失。用python -m 模块名或者工具提供的工厂入口让解释器按包语义加载。入口设计清晰后IDE、命令行和CI才能运行同一条路径。读懂pyproject里几个容易混淆的位置[build-system]声明“用什么工具制造发行物”并不声明运行服务需要哪些库。这里使用setuptools构建后端构建前端pip或build根据这份配置调用它。[project]才描述发行物本身可选依赖dev属于开发安装组pip install -e 路径[dev]会把这一组也安装进环境。运行依赖和开发依赖都要有可审查的版本策略。[tool.setuptools.packages.find]里的where指向src表示从那里寻找包。若误写成tests构建过程可能仍然生成一个wheel文件但里面没有应用代码。于是“文件成功生成”只是第一层证据导入与行为验证是下一层。类似地wheel里的版本元数据来自project.version而不是文件夹名03文件夹只是我们保存教学阶段的编号。03阶段使用确定的运行依赖版本便于复现本次课程验收setuptools的构建约束仍是范围因此环境并非完整锁定。需要审计所有依赖变化时应同时保存解析后的依赖清单并检验供应来源。不要把一个写了等号的requirements文件自动等同于已经解决所有跨平台和构建差异。清理虚拟环境时也要尊重文件边界。可重新创建自己的.venv但不要删除全局site-packages更不要为了让某个import消失而手工改共享Python安装。使用 $PY -m pip show quoteledger可以观察当前环境的安装元数据用quoteledger.__file__观察实际加载文件。两项结果不一致时往往存在editable关联、同名模块或者额外路径影响。课程保留多个快照是为了让每课有稳定材料它们不是要同时作为七个不同服务安装在一个环境里。同一Python进程也不适合先后加载多个名为quoteledger的阶段。即使你删除sys.path里的旧目录已导入的模块仍保存在sys.modules中。独立子进程是最容易解释的隔离方式答案因此避免在一个长脚本里循环切换包版本。最后路径问题要能在任何目录复现。脚本通过自身的__file__定位材料与用户当前终端位置无关命令通过绝对解释器路径定位环境。把这两种路径分开处理你就不会因为换一个文件夹既丢失Python入口又意外导入另一份业务代码。安装验证还应检查行为是否属于当前阶段。答案特意断言响应中没有version避免一个已经装好的后续版本恰好通过金额测试却让阶段验收失去意义。E03在源码目录外证明安装成功将checkpoints/03复制到临时构建目录使用当前解释器的pip把它普通安装到一个临时目标目录或者安装到独立虚拟环境。再从与源码无关的工作目录启动一个新的Python进程打印quoteledger.__file__并断言来源属于安装目录。通过TestClient创建quantity2、unit_cents1299、shipping_cents500的报价断言201和3098确认后再次确认应409读取缺失id应404。测试进程不能把checkpoint/src或最终project/src加到sys.path。下一课提供包含临时目录清理的完整答案。本课把“能导入”变成了“知道导入来源且安装结果具备预期行为”。接下来我们才开始拆业务规则目录整齐并不自动意味着责任清晰领域提取要解决的是另一个问题。对当前快照运行回归测试完成上面的本阶段editable安装后在课程根目录执行$PY-m pytest.\checkpoints\03\tests--rootdir.\checkpoints\03-q本次独立进程验收结果为9 passed若依赖库发出弃用警告应区分警告与测试失败。这里的测试只导入03阶段不能用最终project的安装结果替代。若此前设置过PYTHONPATH先恢复它再确认quoteledger.__file__指向本阶段的src目录。本课完整阶段源码与打包配置checkpoints/03/pyproject.toml[build-system] requires [setuptools75] build-backend setuptools.build_meta [project] name quoteledger version 0.3.0 description 逐步演进的报价单服务 requires-python 3.11 dependencies [fastapi0.115.12, pydantic2.13.5, uvicorn0.34.2] [project.optional-dependencies] dev [pytest8.3.5, httpx0.28.1] [tool.setuptools.packages.find] where [src] [tool.pytest.ini_options] testpaths [tests]checkpoints/03/src/quoteledger/__init__.pyQuoteLedger 教学项目。checkpoints/03/src/quoteledger/api.py单文件基线先让合同可测试存储与业务还耦合在路由闭包中。fromdatetimeimportUTC,datetimefromthreadingimportLockfromuuidimportuuid4fromfastapiimportFastAPI,HTTPException,ResponsefrompydanticimportBaseModel,ConfigDict,FieldclassCreateQuoteBody(BaseModel):model_configConfigDict(extraforbid,str_strip_whitespaceTrue)sku:strField(min_length1,max_length64)supplier:strField(min_length1,max_length32)quantity:intField(strictTrue,ge1,le100)unit_cents:intField(strictTrue,ge0,le100_000_000)shipping_cents:intField(default0,strictTrue,ge0,le100_000_000)defcreate_app()-FastAPI:每个工厂拥有独立字典锁保护同进程读改写不能提供跨进程持久化。appFastAPI(titleQuoteLedger baseline)quotes:dict[str,dict[str,object]]{}lockLock()app.get(/health)defhealth()-dict[str,str]:return{status:ok}app.post(/quotes,status_code201)defcreate(body:CreateQuoteBody,response:Response)-dict[str,object]:quote_iduuid4().hexquote{id:quote_id,**body.model_dump(),total_cents:body.unit_cents*body.quantitybody.shipping_cents,status:draft,created_at:datetime.now(UTC).isoformat(),}withlock:quotes[quote_id]quote response.headers[Location]f/quotes/{quote_id}returndict(quote)app.get(/quotes/{quote_id})defget(quote_id:str)-dict[str,object]:withlock:ifquote_idnotinquotes:raiseHTTPException(404,quote not found)returndict(quotes[quote_id])defchange(quote_id:str,target:str)-dict[str,object]:withlock:ifquote_idnotinquotes:raiseHTTPException(404,quote not found)currentquotes[quote_id]ifcurrent[status]!draft:raiseHTTPException(409,terminal quote cannot change)# 替换对象而非原地改字典避免已返回引用被后续操作悄悄改变。changed{**current,status:target}quotes[quote_id]changedreturndict(changed)app.post(/quotes/{quote_id}/confirm)defconfirm(quote_id:str)-dict[str,object]:returnchange(quote_id,confirmed)app.post(/quotes/{quote_id}/cancel)defcancel(quote_id:str)-dict[str,object]:returnchange(quote_id,cancelled)returnappcheckpoints/03/tests/test_behavior.pyimportpytestfromfastapi.testclientimportTestClientfromquoteledger.apiimportcreate_apppytest.fixturedefclient():withTestClient(create_app())asvalue:yieldvaluepytest.mark.parametrize(quantity,total,[(1,1090),(2,2080),(100,99100)])deftest_total_and_terminal_state(client,quantity,total):responseclient.post(/quotes,json{sku:A,supplier:S,quantity:quantity,unit_cents:990,shipping_cents:100,},)assertresponse.status_code201quoteresponse.json()assertquote[total_cents]totalassertresponse.headers[location]/quotes/quote[id]assert(client.post(/quotes/quote[id]/confirm).json()[status]confirmed)assertclient.post(/quotes/quote[id]/cancel).status_code409assertclient.post(/quotes/quote[id]/confirm).status_code409pytest.mark.parametrize(quantity,[0,101,True,1])deftest_invalid_quantity(client,quantity):assert(client.post(/quotes,json{sku:A,supplier:S,quantity:quantity,unit_cents:1},).status_code422)deftest_factory_isolation_and_missing(client):quoteclient.post(/quotes,json{sku:A,supplier:S,quantity:1,unit_cents:0}).json()withTestClient(create_app())assecond:assertsecond.get(/quotes/quote[id]).status_code404assertclient.get(/quotes/missing).status_code404assertclient.get(/health).json(){status:ok}deftest_extra_fields_rejected(client):responseclient.post(/quotes,json{sku:A,supplier:S,quantity:1,unit_cents:1,admin:True,},)assertresponse.status_code422

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号