
Python工程化管理:Poetry让项目管理变得优雅
Python工程化管理:Poetry让项目管理变得优雅
在我五年的测试开发经验中,项目的依赖管理一直是一个让人头疼的问题。从最初的pip + requirements.txt,到后来的pipenv,再到现在的Poetry,我见证了Python生态在项目管理方面的不断进步。
Poetry作为现代Python项目管理的佼佼者,不仅解决了依赖管理的痛点,还提供了完整的项目生命周期管理方案。今天,我将基于实际项目经验,为大家详细介绍Poetry的使用方法和最佳实践。
为什么选择Poetry?
在传统的Python项目中,我们经常遇到这些问题:
- 依赖版本冲突难以解决
- requirements.txt维护困难
- 开发环境和生产环境不一致
- 包的构建和发布流程复杂
Poetry优雅地解决了这些问题,让Python项目管理变得简单而可靠。
新建仓库
poetry new xxx
e.g:
poetry new pass_apitest -> Created package pass_apitest in pass_apitest生成的目录结构如下:
tree -L 4
.
├── README.md
├── pass_apitest
│ └── __init__.py
├── pyproject.toml
└── tests
└── __init__.py
2 directories, 4 files注:下面的所有操作,如不特殊提示,均在伤处创建的文件里的根目录下,即pass_apitest/下
添加基础python环境
# 需要本地有python3.*的环境才可以
# 使用如下命令时不能在虚拟环境下,如果要退出,那么使用deactivate这个命令
poetry env use python3.e.g:
poetry env use python3.10
Creating virtualenv pass-apitest in /Users/bytedance/PycharmProjects/my_project/pass_apitest/.venv
Using virtualenv: /Users/bytedance/PycharmProjects/my_project/pass_apitest/.venv添加相关库的依赖
poetry add xxx添加 .gitignore文件,下面这个网站会自己生成
https://www.toptal.com/developers/gitignore
一个标准的项目结构
其实没有一个特别标准的项目结构,只有适合自己的,具体的代码结构可以参考下面这个文章
两种结构我都有使用,接口自动化这种自己的项目,我使用的最原始的项目
自己想要开源的仓库使用的是https://github.com/pypa/sampleproject这种结构,这种的虽然推荐,但我
一般自己写项目,使用的是,最古老的那种,结构一般如下,如果自己的项目想要给其他人使用,记得,https://github.com/pypa/sampleprojec是最为推荐的:
.
├── README.md
├── client ## 发起请求
├── conf ## 配置管理
├── docs ## 说明文档
├── logs ## 日志
├── main.py
├── output ## 输出的文件
├── requirements.txt ## 依赖库
├── test_case
├── test_data
└── utils ## 常用到的库常见需要添加的第三方库
最常见的就是日志和配置管理,当然还有一个诸如pytest request等库就不多说了,给一个我正常情况下会有的库及版本,即poetry的安装方式
下面只会介绍比较常见的这几个库的使用,其他的不作为本文章的重点
先说配置管理,我们一般在conf文件夹下,存放相关管理文件,我们也推荐使用dynaconf
配置管理
conf/contanst.py 根据自己项目的情况,写下如下这段代码:
官方推荐结构目录如下:
官方目前推荐的结构
.
├── README.md
├── pass_apitest
│ ├── client
│ ├── conf
│ │ ├── config.py
│ │ ├── constants.py
│ │ └── settings.yaml
│ ├── logs
│ └── utils
├── poetry.lock
├── pyproject.toml
├── req.txt
├── settings.yaml
└── tests
├── __init__.py
└── mian.py那么使用下面的方式
import os
# dirname(path) 是返回path的父路径
base_project_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# config_dir = os.path.join(base_dir, "config")
# config_file_path = os.path.join(config_dir, "config.yaml")
conf_dir = os.path.join(base_dir, "conf")
settings_yaml = os.path.join(conf_dir, "settings.yaml")
common_dir = os.path.join(base_dir, "common")
utils_dir = os.path.join(base_dir, "utils")
template_dir = os.path.join(utils_dir, "create_test_template")conf/config.py
import os
import sys
base_project_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0,base_project_dir)
from dynaconf import Dynaconf
settingfile = os.path.join(base_project_dir,"settings.yaml")
## 更多配置相关的问题,可以参看官方文档中的相关介绍 https://www.dynaconf.com/configuration/
settings = Dynaconf(
envvar_prefix="DYNACONF",
settings_files=[settingfile, ".secrets.toml"],
# includes=["conf/settings.*.toml"],
environments=True,
env="ONLINE-PREPAY-IPAAS" ## 这块是会从环境变量中读取ENV_FOR_DYNACONF,如果没有读取到,那么默认为online
# env="acHsc_aa1"
)
## 导入默认变量举例
## export ENV_FOR_DYNACONF=dev
## export DYNACONF_ENV=BOE ## 调用settings.env传统目录结构
目录结构应该如下
.
├── README.md
├── client
├── conf
│ ├── config.py
│ └── settings.yaml
├── conftest.py
├── docs
├── logs
├── main.py
├── requirements.txt
├── rqm.txt
├── test.py
├── test_case
│ ├── test_common.py
│ ├── test_future.py
│ ├── test_ipaas
│ └── test_vegame_paas
│ └── single
├── test_data
└── utils
├── PaaS_stable.postman_collection.json
├── cmd_comand.py
├── constants.py
├── data_handle.py
├── date_operation.py
├── decorator.py
├── dict_tools.py
├── file_operation.py
├── json_api.yaml
├── jsonfile_trans_testyaml.py
├── libs
│ └── volcauth
├── log_moudle.pyutils/contanst.py
import os
# dirname(path) 是返回path的父路径
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# config_dir = os.path.join(base_dir, "config")
# config_file_path = os.path.join(config_dir, "config.yaml")
conf_dir = os.path.join(base_dir, "conf")
settings_yaml = os.path.join(conf_dir, "settings.yaml")
common_dir = os.path.join(base_dir, "common")
utils_dir = os.path.join(base_dir, "utils")
template_dir = os.path.join(utils_dir, "create_test_template")
data_dir = os.path.join(base_dir, "data")
demo_dir = os.path.join(base_dir, "demo")
logs_dir = os.path.join(base_dir, "logs")
output_dir = os.path.join(base_dir, "output")
test_case_dir = os.path.join(base_dir, "test_case")
test_data_dir = os.path.join(base_dir, "test_data")conf/settings.py
下面的settings_files自己设置即可
from dynaconf import Dynaconf
## 更多配置相关的问题,可以参看官方文档中的相关介绍 https://www.dynaconf.com/configuration/
settings = Dynaconf(
envvar_prefix="DYNACONF",
settings_files=["conf/settings.yaml", "conf/.secrets.toml"],
includes=["conf/settings.*.toml"],
environments=True,
env="ONLINE-PREPAY-IPAAS" ## 这块是会从环境变量中读取ENV_FOR_DYNACONF,如果没有读取到,那么默认为online
# env="acHsc_aa1"
)
## 导入默认变量举例
## export ENV_FOR_DYNACONF=dev
## export DYNACONF_ENV=BOE ## 调用settings.env日志模块
使用loguru,在其基础上进行了二次封装
"""
该库比Python的原始日志好用,故使用该库进行日志的封装
官方文档: https://github.com/Delgan/loguru
使用说明:
*****
from common.log_moudle import logger
logger.info("this is info message")
logger.debug("this in debug infomation")
*****
日志存储位置
根目录下的log目录
其中,test{_xxx}.log 日志中存储我们通常使用的case
error.log 日志中记录较为关键的崩溃及失败信息,信息包括但不限于以下:调用链路的logID,错误的case名称及错误的相关原因
"""
import os
import sys
from loguru import logger
##日志存储地址
BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
LOG_PATH = os.path.join(BASE_PATH, "logs")
if not os.path.exists(LOG_PATH):
os.mkdir(LOG_PATH)
##测试代码
# logger.add(os.path.join(LOG_PATH,"test_{time}.log"))
# logger.add(os.path.join(LOG_PATH,"test.log"),rotation="1 MB",retention="10 days",encoding="utf-8") ## 目前,我们设置成自动清除存满100MB的文件跟换到下一个文件
# logger.add(os.path.join(LOG_PATH,"test.log"),rotation="",encoding="utf-8")
# logger.add(os.path.join(LOG_PATH,"test.log"),rotation="",encoding="utf-8")
## 按照日志等级记录到不同文件中
# logger.add(os.path.join(LOG_PATH,"test1.log"),level="INFO",filter=lambda x: "INFO" in str(x["level"]).upper())
# logger.add(os.path.join(LOG_PATH,"test2.log"),level=["DEBUG","WARNING"],filter=lambda x: "INFO" in str(x["level"]).upper())
## 封装成类
class MyLogger(object):
def __init__(
self,
log_test_path=os.path.join(LOG_PATH, "test.log"),
log_error_path=os.path.join(LOG_PATH, "error.log"),
):
self.logger = logger
# 清空所有设置
self.logger.remove()
# 添加控制台输出的格式,sys.stdout为输出到屏幕;关于这些配置还需要自定义请移步官网查看相关参数说明
self.logger.add(
sys.stdout,
format="<green>{time:YYYY-MM-DD HH:mm:ss:sss}</green> | " # 颜色>时间
"{process.name} | " # 进程名
"{thread.name} | " # 线程名
"<cyan>{module}</cyan>.<cyan>{function}</cyan>" # 模块名.方法名
":<cyan>{line}</cyan> | " # 行号
"<level>{level}</level>: " # 等级
"<level>{message}</level>", # 日志内容
)
# 输出到文件的格式,注释下面的add',则关闭日志写入
self.logger.add(
log_test_path,
format="{time:YYYYMMDD HH:mm:ss:sss} - " # 时间
"{process.name} | " # 进程名
"{thread.name} | " # 线程名
"{module}.{function}:{line} - {level} -{message}", # 模块名.方法名:行号
rotation="100 MB",
retention="10 days",
encoding="utf-8",
)
self.logger.add(
log_error_path,
level="ERROR",
format="{time:YYYYMMDD HH:mm:ss:sss} - " # 时间
"{process.name} | " # 进程名
"{thread.name} | " # 线程名
"{module}.{function}:{line} - {level} -{message}", # 模块名.方法名:行号
rotation="100 MB",
retention="10 days",
encoding="utf-8",
)
def get_logger(self):
return self.logger
# logger.info("hello world")
logger = MyLogger().get_logger()常用的库的安装
req.txt
allure-pytest==2.9.45
allure-python-commons==2.9.45
arrow==1.2.3
attrs==21.4.0
beautifulsoup4==4.10.0
binaryornot==0.4.4
black==22.12.0
certifi==2022.12.7
cfgv==3.3.1
chardet==5.1.0
charset-normalizer==2.0.12
click==8.1.3
cookiecutter==2.1.1
decorator==5.1.1
distlib==0.3.6
dnspython==1.16.0
dynaconf==3.1.11
execnet==1.9.0
filelock==3.9.0
google==3.0.0
identify==2.5.13
idna==3.3
importlib-metadata==4.11.2
iniconfig==1.1.1
isort==5.11.4
Jinja2==3.1.2
jinja2-time==0.2.0
jmespath==1.0.1
jsonpath-ng==1.5.3
loguru==0.6.0
MarkupSafe==2.1.1
mypy-extensions==0.4.3
nodeenv==1.7.0
numpy==1.24.1
packaging==21.3
pathspec==0.10.3
platformdirs==2.6.2
pluggy==1.0.0
ply==3.11
polling2==0.5.0
pre-commit==2.21.0
protobuf==3.19.4
py==1.11.0
pycryptodome==3.11.0
PyHamcrest==2.0.4
pyparsing==3.0.7
pytest==7.0.1
pytest-assume==2.4.3
pytest-forked==1.4.0
pytest-html==3.1.1
pytest-metadata==1.11.0
pytest-rerunfailures==10.2
pytest-xdist==2.5.0
python-dateutil==2.8.2
python-slugify==7.0.0
pytz==2021.3
PyYAML==6.0
requests==2.28.2
requests-aws4auth==1.1.2
retry==0.9.2
retrying==1.3.4
six==1.16.0
soupsieve==2.2.1
text-unidecode==1.3
tomli==2.0.1
typing_extensions==4.1.1
urllib3==1.26.7
virtualenv==20.17.1
volcengine==1.0.24
zipp==3.7.0如何导入
for i in $(cat req.txt);do poetry add ${i};done书写自己的代码
这部分不过多叙述
这块要说的也就是一些代码规范,我们参考https://zh-google-styleguide.readthedocs.io/en/latest/google-python-styleguide/contents/这篇文章吧
还有两个比较好的库来调整我们的代码,使用如下方式
black .
isort .推送至代码仓库
这部分也不过多赘述了
git init // 初始化版本库
git add . // 添加文件到版本库(只是添加到缓存区),.代表添加文件夹下所有文件
git commit -m "first commit" // 把添加的文件提交到版本库,并填写提交备注
git remote add origin 你的远程库地址 // 把本地库与远程库关联
git push -u origin master // 第一次推送时
git push origin master // 第一次推送后,直接使用该命令即可推送修改别人拿到代码后如何配置环境
推荐使用 poetry 管理依赖和虚拟环境。
Mac/Linux 安装 poetry, 其他更多安装方式参考官方文档 poetry docs
# 检查 python 版本
python3 --version
# 如果没有安装 python3 则先安装。
brew install python3
brew install python@3.7
# 配置私有源 bytedpypi: https://bytedance.feishu.cn/wiki/wikcnApN5RWx8y5oyDl3vXVq6lb
pip3 config set global.index-url https://bytedpypi.byted.org/simple/
pip3 config list
# 安装 poetry
curl -sSL https://install.python-poetry.org | python3 -
# 检查是否安装成功
poetry --version
# 配置在项目内创建虚拟环境
poetry config virtualenvs.in-project true安装 python 依赖
# 制定 python 环境版本
poetry env use python3.7
# 安装依赖
poetry install在调用poetry install时遇到过一次报错 那次是python 3.12的环境
AttributeError: module 'pkgutil' has no attribute 'ImpImporter'. Did you mean: 'zipimporter'?
解决办法:
参考文献:https://stackoverflow.com/questions/77364550/attributeerror-module-pkgutil-has-no-attribute-impimporter-did-you-mean
You just have to manually install pip for Python 3.12
非虚拟环境操作如下:
python -m ensurepip --upgrade
python -m pip install --upgrade setuptools
python -m pip install <module>
虚拟环境操作如下
In your virtual environment:
pip install --upgrade setuptools备注
如果其他人不习惯使用poetry,该如何
导出所有依赖
pip freeze > rqm.txt他人拿到后,创建虚拟环境
使用 venv + pip 安装
# 项目根目录执行
python3.7 -m venv .venv
# 激活虚拟环境
source .venv/bin/activate
# 安装依赖
pip3 install -f requirements.txtModuleNotFoundError No module named ‘xxxx‘
sys的使用,这部分给一个demo,如果使用terminal时,import失败可能需要再相关py文件中加入相关代码
import sys
import os
sys.path.insert(0,os.path.join(BASEDIR,'xxxx')) # 这个是把环境变量加入到第一个位置
E,g:
import os
import sys
# 注意,这个BASE_DIR是一个应该是脚本的根目录,可能不是如下写法,注意切换
BASE_DIR = os.path.dirname(os.path.abspath(os.path.dirname(__file__)))
sys.path.insert(0, BASE_DIR)