Click框架Path类型参数跨脚本导入后行为异常
问题复现
假设编写了简单Python脚本do_stuff.py,功能为列出指定目录下的子文件夹,代码如下:
import click @click.command(help="Do stuff.") @click.argument('datasets', type=click.Path(exists=True, readable=True, writable=True), nargs=-1) def main(datasets): for dataset in datasets: print(dataset) if __name__ == "__main__": main()
直接执行shell命令python3 do_stuff.py ./s3_data/lsc2/landsat_ot_c2_l2/*时,脚本可以正常返回预期的文件夹列表,输出如下:
./s3_data/lsc2/landsat_ot_c2_l2/LC08_L2SP_195027_20220121 ./s3_data/lsc2/landsat_ot_c2_l2/LC08_L2SP_195027_20220206 ./s3_data/lsc2/landsat_ot_c2_l2/LC08_L2SP_195027_20220222 ./s3_data/lsc2/landsat_ot_c2_l2/LC08_L2SP_195027_20220310
在另一个脚本master.py中导入do_stuff.py的相关函数进行相同操作时,第一种写法代码如下:
from do_stuff import main as ds ds('./s3_data/lsc2/landsat_ot_c2_l2/*')
执行python3 master.py后返回报错:
Usage: master.py [OPTIONS] [DATASETS]... Try 'master.py --help' for help. Error: Invalid value for '[DATASETS]...': Path '/' is not writable.
如果将master.py最后一行修改为ds(['./s3_data/lsc2/landsat_ot_c2_l2/*']),则返回另一个报错:
Usage: master.py [OPTIONS] [DATASETS]... Try 'master.py --help' for help. Error: Invalid value for '[DATASETS]...': Path './s3_data/lsc2/landsat_ot_c2_l2/*' does not exist.
根本原因
- 命令行下的通配符
*展开是Shell本身完成的,和Python、Click无关。直接在Shell执行带*的命令时,Shell会先把通配符替换成所有匹配的真实文件/文件夹路径,再把这些路径作为参数传给Python进程,脚本拿到的本身就是合法存在的路径,因此可以正常通过Click的路径校验。 - 被
@click.command装饰的函数不是普通Python函数,装饰器会将其包装为命令行入口,直接传参调用不会按普通函数的参数逻辑解析:传入单个字符串时,Click会将字符串按字符迭代拆分,遍历到/字符时就会触发根目录写权限校验,抛出第一个错误;传入带*的路径列表时,因为没有经过Shell展开,*会被当做路径的普通字符,Click会查找名称为*的路径,自然找不到对应文件,抛出第二个错误。
解决方案
方案1:手动展开通配符,按Click规则传参
使用Python标准库glob完成通配符匹配,拿到真实路径列表后,通过指定args参数传入Click命令,同时关闭standalone_mode避免Click直接接管异常退出流程:
修改后的master.py代码:
import glob from do_stuff import main as ds match_paths = glob.glob('./s3_data/lsc2/landsat_ot_c2_l2/*') ds.main(args=match_paths, standalone_mode=False)
方案2:拆分业务逻辑与命令行入口(推荐)
将核心遍历逻辑抽离为普通Python函数,Click命令仅负责命令行参数解析、调用核心逻辑。这种写法下,命令行执行和跨脚本导入调用都不会出现参数解析问题,也不需要依赖Shell做通配符展开:
修改后的do_stuff.py:
import click import glob # 核心业务逻辑为普通函数,无命令行耦合 def print_target_paths(patterns): for pattern in patterns: for path in glob.glob(pattern): print(path) @click.command(help="Do stuff.") @click.argument('datasets', type=click.Path(exists=True, readable=True, writable=True), nargs=-1) def main(datasets): print_target_paths(datasets) if __name__ == "__main__": main()
修改后的master.py直接调用普通业务函数即可:
from do_stuff import print_target_paths print_target_paths(['./s3_data/lsc2/landsat_ot_c2_l2/*'])
内容的提问来源于stack exchange,提问作者bchate
相关产品推荐
相关产品推荐

