本文目录
基本用法
setuptools基本用法的清晰介绍,好文
MANIFEST.in文件的官方讲解
根目录setup.py
, MANIFEST.in
, README.md
等文件会自动加入包里,不需要指定什么。
python setup.py clean --all
清除之前编译时的中间结果(也即build
目录),如果没有清理,修改setup.py
, MANIFEST.in
文件中的内容后重新编译很可能不会生效,程序依然直接根据之前编译的中间结果去打包文件。(后记:不过实测出来光clean
还不够,还要rm -rf *.egg-info/
,否则还是会打包之前指定的py文件)
关于setup
函数的参数,我们一个一个来讲(讲某个参数的时候其他参数忽略不写):
from setuptools import setup, find_packages
# 包里只有根目录下的`setup.py`, `MANIFEST.in`, `README.md`等文件
setup(name="demo", version="1.0")`
# 假设根目录下面有很多包,我们只需要`mod1`跟`mod2`包,那么把它们加进`packages`参数,则这些目录下的所有`.py`文件都会被打包
setup(packages=["mod1", "mod2"])
# 当包越来越多时,手动一个个添加维护成本太大了,可以借助setuptools提供的find_packages函数,
# 一次性递归地将起始目录下的所有模块包进来(有__init__.py才算是模块)
setup(packages=find_packages(where=".", exclude=["test"]))
# 根据`MANIFEST.in`的内容精细化指定需要和不需要打包的文件,只对sdist命令有效,对bdist_wheel命令无效
setup(include_package_data=True)
# 排除`mod1`目录下的`.gitignore`文件,如果`'mod1'`换成`''`表示排除所有目录下的`.gitignore`文件
setup(exclude_package_data={'mod1':['.gitignore']})
# 跟`exclude_package_data`的用法基本一致,需要注意的是它只对在`packages`中的目录进行检查,
# 而不会添加其他目录下的数据文件,即使这里写的`''`看起来像是整个根目录下的所有目录。
# `package_data `感觉没有`MANIFEST.in`用起来方便
setup(package_data={"": ["*.txt", "*.pth"]})
# 用于给`bdist_wheel`编译whl包时添加不在python模块内的数据文件,tuple中的内容是[目标目录, 源文件路径列表],
# 数据文件在安装whl后的存放位置即为<目标目录>,当目标目录以/开头时,
# setuptools将自动在前面加上site-packages的路径,若不以/开头则自动在前面加上/usr/local/
setup(data_files=[("output_dir", ["conf/data.json"])])
# 用于给`bdist_wheel`编译whl包时添加在python模块内的数据文件,key为模块名,
# value为该模块目录下的文件路径列表,安装whl后会保持与源代码同样的目录结构
setup(package_data={"mod1.submod1": ["data/data.json"]})
python setup.py cmd
表示基于setup.py运行某个命令,这个cmd
可以取很多个值,比如install
,develop
等,下面给出一些例子:
# 直接将当前目录下的内容安装成`pip list`里头的一个包,
python setup.py install
# develop指令更适合处于调试阶段的代码,它并没有在`site-packages`目录下生成一个包,
# 而是拉了一条软链到当前目录,这样我们在当前目录下修改的代码都能立即反映上去,不需要重新
# 运行`python setup.py install`
python setup.py develop
# 创建一个源码包
python setup.py sdist
# 创建一个wheel发布包
python setup.py bdist_wheel
# 查看其他支持的cmd字段
python setup.py --help-commands
python setup.py install/develop
会在当前目录下创建一个module_name.egg-info/
目录,后续如果python setup.py sdist && pip install dist/module_name-version.tar.gz
就会报一些奇奇怪怪的错误,这时把module_name.egg-info/
目录删掉可以正常编译安装了。
更进一步:在打包时添加c++文件拓展
首先是自己制作可被python调用的C++动态链接库也就是so文件,有几种常见的技术路径比如ctypes、Boost和pybind11(参考链接1,参考链接2,参考链接3),这里选择pybind11进行讲解。
复习一下编译和链接的关系:看这里。编译是把我们自己写的代码转换成二进制目标文件.o
或.obj
,链接是将所有的目标文件以及系统组件组合成一个可执行文件.exe
。
使用cmake编译纯C++代码可以看我的另一篇文章。
首先安装pybind11,源码安装跟pip install
安装的结果还不太一样,后者可以import pybind11
前者不行,把usr/local/include/pybind11
加入PYTHONPATH
也无济于事,前者可以找到pybind11-config.cmake
文件后者不行,wheel包就是不包含这个文件,所以这里分开讲用法。pip安装:pip install pybind11
然后用C++写一个简单的求和函数,命名为example.cpp
(从这里拷过来的):
#include <pybind11/pybind11.h>
namespace py = pybind11;
int add(int i, int j)
{
return i + j;
}
PYBIND11_MODULE(test, m)
{
// optional module docstring
m.doc() = "pybind11 example plugin";
// expose add function, and add keyword arguments and default arguments
m.def("add", &add, "A function which adds two numbers", py::arg("i")=1, py::arg("j")=2);
// exporting variables
m.attr("the_answer") = 42;
py::object world = py::cast("World");
m.attr("what") = world;
}
然后进行编译:c++ -O3 -Wall -shared -std=c++11 -fPIC $(python -m pybind11 --includes) example.cpp -o test$(python3-config --extension-suffix)
,超长的命令,官网的示例命令就是这么长,离谱。
各参数的含义为(参考链接):
-O3:优化等级设为第3级
-Wall:启用最大程度的警告信息
-shared:生成一个共享库文件;
-fPIC:生成位置无关目标代码,适用于动态连接;
-L path:将path库文件搜索路径列表;
-I path:将path加入头文件搜索路径列表;
-o file:指定输出文件名,也就是so文件名字的前半段,以及python import时候的包名,file必须与PYBIND11_MODULE()的第一个参数相同;
其他参数可以通过c++ -v --help
查看。
python -m pybind11 --includes
会给出pybind11用到的库文件的搜索路径,形如"-I/usr/include/python3.9 -I/usr/local/lib/python3.9/dist-packages/pybind11/include"
python3-config --extension-suffix
给出当前环境下so文件的后缀,形如".cpython-39-x86_64-linux-gnu.so"
所以如果把$()
包着的两个参数解开,整一条命令就是:c++ -O3 -Wall -shared -std=c++11 -fPIC -I/usr/include/python3.9 -I/usr/local/lib/python3.9/dist-packages/pybind11/include example.cpp -o test.cpython-39-x86_64-linux-gnu.so
如果so文件已经生成好了,在python里头import却说找不到,这时可以通过下面的命令看看当前python解释器支持哪些后缀的so文件的导入(参考链接):
import importlib.machinery
print(importlib.machinery.all_suffixes())
如果发现so文件名称不对,例如so文件中的python版本数字本来应该是39结果变成了38,就要去确认一下python3
跟python3-config
命令软链的目标是否跟预期一致。
到这一步,cpp文件目录下就应当有一个test开头的so文件,且可以在python中执行下列命令:
import test
test.add(1, 2)
至此一个基于pybind11和c++
编译命令的python和C++混合用例就完成了~
接下来使用cmake
进行编译,与cpp文件同目录的CMakeLists.txt内容如下:
# 给定cmake版本最低要求
cmake_minimum_required(VERSION 3.10)
# 设置项目名
project(example)
# 添加头文件的搜索路径
include_directories("/usr/include/python3.9")
include_directories("/usr/local/lib/python3.9/dist-packages/pybind11/include")
# 设置变量,这部分与单纯用`c++`命令编译时的参数一致
SET(CMAKE_CXX_FLAGS "-std=c++11 -O3")
# 把一或多个源文件编译成库文件
add_library(test SHARED example.cpp)
对cmake命令的更详细介绍请看这里。
然后执行cmake -B build/ && cmake --build build/
,就会得到一个libtest.so
文件。这里我没有找到自动去掉前面的lib前缀的方法,但是这个so文件里头其实是对应着模块名test,所以还需要手动改回test.so,用其他名字都无法import成功。
mv libtest.so test.so
,最后检验一下导入是否成功:
import test
test.add(1, 2)
至此一个基于pybind11和cmake
的python和C++混合用例就完成了!
接下来讲一下源码安装方式,网上大多都是用这种方式安装pybind11
:
#有一些python跟C++的混合项目编译用的是eigen,也要装一下
git clone git@github.com:pybind/pybind11.git \
&& mkdir pybind11/build && cd pybind11/build \
&& cmake .. \
&& make -j12 \
&& make install \
&& apt-get install libeigen3-dev \
&& ln -s /usr/include/eigen3/Eigen /usr/include/Eigen \
&& ln -s /usr/include/eigen3/unsupported /usr/include/unsupported
&& export PYTHONPATH=$PYTHONPATH:/usr/local/include/pybind11
此时,CMakeLists.txt应该这样写:
# 给定cmake版本最低要求
cmake_minimum_required(VERSION 3.10)
# 设置项目名,此后可以通过 ${PROJECT_NAME} 使用项目名
project(example)
# 寻找pybind11,此时应有 /usr/local/include/pybind11 目录,否则程序无法工作
find_package(pybind11 REQUIRED)
# 将example.cpp文件中的内容添加到输出文件test中,最后将生成一个以test开头的so文件供python导入
# example.cpp中应有PYBIND11_MODULE函数,且它的第一个参数应于这里传给pybind11_add_module的第一个参数一致,
# 否则后续会报错ImportError: dynamic module does not define module export function
pybind11_add_module(test example.cpp)
然后执行cmake -B build/ && cd build/ && make
,就会得到一个test.xxx.so
文件,最后检验一下导入是否成功:
import test
test.add(1, 2)
完成!
(后记:有的时候CMakeList需要添加一行set_target_properties(name PROPERTIES OUTPUT_NAME "name " PREFIX "")
,用于取消编译出的库文件的 ‘lib’ 前缀,这样 python 才能找到这个包,参考链接)
下面展示一个引入c++文件的setup.py:
import os
import subprocess
from setuptools import setup, Extension
from setuptools.command.build_ext import build_ext
project_name = 'projection_render'
class CMakeExtension(Extension):
def __init__(self, name, sourcedir=''):
Extension.__init__(self, name, sources=[])
self.sourcedir = os.path.abspath(sourcedir)
if not os.path.exists(self.sourcedir):
os.makedirs(self.sourcedir)
class CMakeBuild(build_ext):
r"""
During the process of `python setup.py install`, it will automatically call `python setup.py build_ext`
to build C/C++ extensions, so we need to rewrite the event of build_ext command
"""
def run(self):
for ext in self.extensions:
self.build_extension(ext)
def build_extension(self, ext):
if not os.path.exists(self.build_temp):
os.makedirs(self.build_temp)
extdir = self.get_ext_fullpath(ext.name)
if not os.path.exists(extdir):
os.makedirs(extdir)
# This is the temp directory where your build output should go
install_prefix = os.path.abspath(os.path.dirname(extdir))
if not os.path.exists(install_prefix):
os.makedirs(install_prefix)
cmake_list_dir = os.path.join(install_prefix, project_name)
# run cmake to build a .so file according to CMakeLists.txt
subprocess.check_call(['cmake', cmake_list_dir], cwd=self.build_temp)
subprocess.check_call(['cmake', '--build', '.'], cwd=self.build_temp)
setup(
name=project_name,
# 将include_package_data设为True,通过MANIFEST.in文件将非.py的编译所需文件(如.cpp)打包
include_package_data=True,
# 没有写__init__.py但又需要打包的.py文件目录,写在packages里头
packages=[
'projection_render', 'projection_render.include',
'projection_render.src', 'projection_render.cuda_renderer'
],
version="0.1",
ext_modules=[CMakeExtension('projection_render')],
# 含C++包的需要重写python setup.py build_ext命令,在里头编译cpp文件
cmdclass={
"build_ext": CMakeBuild,
},
description='python verison of projection_render',
)
MANIFEST.in
文件的内容为:
include projection_render/README.md
include projection_render/cuda_renderer/*
include projection_render/include/*
graft projection_render/pybind/
include projection_render/src/*
include projection_render/CMakeLists.txt
如果一个顶层目录下有多个C++模块,各有各的CMakeLists.txt
,情况又有不同。
引入C++模块最简单的写法是使用ext_modules
参数,通过sources
指定参与编译的源文件,通过name
指定编译成的文件名(参考链接)。但在需要比较多的源文件参与编译的时候,这个参数就显得有点乏力了。这时我们需要使用更强大的工具,通过cmdclass
参数重写build_ext
函数。
setup(cmdclass={"build_ext": MyCommand})
这个MyCommand
必须继承自distutils.core.Command
类,setuptools
已经包了一层,一般是继承setuptools
包好的类,比如build_ext
命令对应的MyCommand
就继承自setuptools.command.build_ext.build_ext
类,我们如果不做重写,python setup.py build_ext
命令运行的就是它这个类。
重写主要是重写run()
函数,在进入run()
的时候,self.extensions
已经被赋值为输入setup()
的ext_modules
,可以被MyCommand实例使用了,暂时没有搞明白到底是在哪个阶段赋值的,不管它,先用了再说
不管运行的命令是install
, develop
还是build_ext
还是啥,setup()
里头的参数都会在Command
实例get_finalized_command()
的时候被解析出来,其中就包括ext_modules
。
这里再给出一个多模块的setup.py
例子:
import os
import subprocess
from setuptools import setup, Extension
from setuptools.command.build_ext import build_ext
project_name = "algo_utils"
# 因为编译所需的文件由MANIFEST.in指定,具体的操作由CMakeLists.txt指定,所以Extension的sources参数就没有用了,所以这里才直接赋了个固定空列表
# 不加makedirs的操作会报CMake Error: The source directory "xxx" does not exist,原因不明
class CMakeExtension(Extension):
def __init__(self, name, sourcedir=""):
Extension.__init__(self, name, sources=[])
self.sourcedir = os.path.abspath(sourcedir)
if not os.path.exists(self.sourcedir):
os.makedirs(self.sourcedir)
class CMakeBuild(build_ext):
r"""
During the process of `python setup.py install`, it will automatically call `python setup.py build_ext`
to build C/C++ extensions, so we need to rewrite the event of build_ext command
"""
def run(self):
for ext in self.extensions:
self.build_extension(ext)
def build_extension(self, ext):
if not os.path.exists(self.build_temp):
os.makedirs(self.build_temp)
extdir = self.get_ext_fullpath(ext.name)
if not os.path.exists(extdir):
os.makedirs(extdir)
# This is the temp directory where your build output should go
install_prefix = os.path.abspath(os.path.dirname(extdir))
if not os.path.exists(install_prefix):
os.makedirs(install_prefix)
cmake_list_dir = os.path.join(install_prefix, project_name, ext.name)
# run cmake to build a .so file according to CMakeLists.txt
subprocess.check_call(["cmake", cmake_list_dir], cwd=self.build_temp)
subprocess.check_call(["cmake", "--build", "."], cwd=self.build_temp)
setup(
name=project_name,
include_package_data=True,
version="0.1",
# 二级模块名写在这里
# 修改Extension或CMakeExtension构造时的name入参,可以调整编译出的so文件的位置
# 比如"aa.bb.projection_render",则输出的so文件会下放到aa/bb目录下
ext_modules=[
CMakeExtension("projection_render"),
],
cmdclass={
"build_ext": CMakeBuild,
},
description="algorithm utility package containing .cpp file",
)
安装好之后检查一下:
from algo_utils import projection_render
没有问题就完成了。
深入细节
python setup.py install
细致讲解:
在描述具体的行为之前,需要对一些变量做好定义:
首先记包名为name
,版本号为ver
,系统后缀为suffix
,形如py3.9-linux-x86_64
,在后面的表述中直接以package
代指name-ver-py3.9-linux-x86_64
以缩短篇幅,提高可读性;
以setup.py
所在目录为./
目录,记suffix2
与系统版本和python版本相关的变量,形如linux-x86_64-3.9
,则./build
目录存放所有编译输出,具体来说:
./build/lib.suffix2
目录存放.py、C语言源文件以及编译链接最终输出的.so文件,将其记为build_lib
;
./build/temp.suffix2
目录存放C语言源文件编译链接过程中产生的中间文件,将其记为build_temp
,在编译链接后不会自动删除,以提高反复编译时的编译速度;
./build/bdist.suffix2
目录存放最终要安装(拷贝)到dist-packages目录下的所有内容,它是在将build_lib
整个目录的内容拷过来之后,把所有的.py文件通过Cython编译成.pyc文件放在对应目录的__pycache__
目录下面(相比直接运行同名.py文件会更快),另外多了一个EGG-INFO/目录和每个模块一个对应的__bootstrap__描述函数。每次安装后这个目录会被清空
python setup.py cmd
中的每个cmd都对应于setuptools包里的一个类,比如install
类。python setup.py cmd
实际上是去运行这个cmd
类的run()
函数,因此看run()
函数怎么写的就知道这个命令的行为是什么了。
先来看看install
指令对应的行为:
def run(self):
# Explicit request for old-style install? Just do it
if self.old_and_unmanageable or self.single_version_externally_managed:
return orig.install.run(self)
if not self._called_from_setup(inspect.currentframe()):
# Run in backward-compatibility mode to support bdist_* commands.
orig.install.run(self)
else:
self.do_egg_install()
第一个if看起来是对古早版本的支持,不管了;第二个if条件中的self._called_from_setup()
可以判断出这个命令是直接通过python setup.py cmd
调用的,还是在python setup.py
调用其他命令的过程中被其他命令调用的。我们一般都是通过python setup.py
直接调用,所以就是else分支,self.do_egg_install()
:
def do_egg_install(self):
easy_install = self.distribution.get_command_class('easy_install')
cmd = easy_install(
self.distribution, args="x", root=self.root, record=self.record,
)
cmd.ensure_finalized() # finalize before bdist_egg munges install cmd
cmd.always_copy_from = '.' # make sure local-dir eggs get installed
# pick up setup-dir .egg files only: no .egg-info
cmd.package_index.scan(glob.glob('*.egg'))
self.run_command('bdist_egg')
args = [self.distribution.get_command_obj('bdist_egg').egg_output]
if setuptools.bootstrap_install_from:
# Bootstrap self-installation of setuptools
args.insert(0, setuptools.bootstrap_install_from)
cmd.args = args
cmd.run(show_deprecation=False)
setuptools.bootstrap_install_from = None
看起来像是通过easy_install安装,不是很懂。看到一个self.run_command("bdist_egg")
,应该就相当于运行一次python setup.py bdist_egg
了。bdist_egg
类的run()
比较长就不放上来了,它首先进行self.run_command("egg_info")
,这个命令是在生成metadata,它会创建一个name.egg-info
目录,然后往里面写入如下几个文件:
PKG-INFO # 对包的描述信息,在setup()中添加的author_email等参数都在这里
# SOURCES.txt先是写入default的内容,然后读出来再读取MANIFEST.in的内容拼接到一起
# 又写了回去,但目前default没有内容,所以现在还不知道default如果有会是什么东西。
SOURCES.txt # 记录了安装的时候打包了那些文件,一行一个文件,可用于事后查看是否正确打包
dependency_links.txt # 目前编译出来是空的,所以也不知道是什么含义
具体到更细的函数的时候,有许多函数是沿用了distutils里的类方法,无需重写。
self.run_command("egg_info")
结束之后,如果有C语言相关的库文件(通过判断setup()
时libraries
参数是否非空得知),则执行self.run_command('build_clib')
。
接着执行self.call_command('install_lib', warn_dir=0)
(不管有没有C源文件都会执行该指令):
install_lib
类基本沿用了disutils的方法,它的run()
函数如下:
def run(self):
self.build()
outfiles = self.install()
if outfiles is not None:
# always compile, in case we have any extension stubs to deal with
self.byte_compile(outfiles)
self.build()
, self.install()
和self.byte_compile()
都是直接调的distutils包里的类方法:
def build(self):
if not self.skip_build:
# setup()时packages参数非空或py_modules参数非空则has_pure_modules()为True
if self.distribution.has_pure_modules():
self.run_command('build_py')
# setup()时ext_modules参数非空时has_ext_modules()为True
if self.distribution.has_ext_modules():
self.run_command('build_ext')
先看self.run_command('build_py')
,build_py
类对应的run()
函数为:
def run(self):
"""Build modules, packages, and copy data files to build directory"""
if not self.py_modules and not self.packages:
return
# setup()中的py_modulesc参数
if self.py_modules:
self.build_modules() # 沿用distutils原有的方法
if self.packages:
self.build_packages() # 沿用distutils原有的方法
self.build_package_data() # setuptools进行了重写
self.run_2to3(self.__updated_files, False)
self.run_2to3(self.__updated_files, True)
self.run_2to3(self.__doctests_2to3, True)
# Only compile actual .py files, using our base class' idea of what our
# output files are.
self.byte_compile(orig.build_py.get_outputs(self, include_bytecode=0))
build_modules()
用得少就先不研究了,build_packages()
的内容如下:
def build_packages(self):
# 扫描``setup()``时传入的``packages``参数,一一处理之
for package in self.packages:
# 将用.连接的包名转换成用/连接的文件路径
package_dir = self.get_package_dir(package)
# 返回该路径下的所有.py文件(不含子目录),除了启动编译的那个setup.py文件
modules = self.find_package_modules(package, package_dir)
# 把找到的.py文件一一拷贝到"build"域,例如./build/lib.linux-x86_64-3.9目,保持其原有的目录结构
for (package_, module, module_file) in modules:
assert package == package_
self.build_module(module, module_file, package)
对于.py文件来说,"编译"其实就是简单地拷贝,因此build_packages()
的主要工作就是找包和拷贝包。
然后是build_package_data()
,这一操作的目的是把前面通过MANIFEST.in文件以及setup()
时传入的package_data
一并拷贝进来,名为build,其实也是在做拷贝。
最后是byte_compile()
,貌似.py文件也是可以编译的,编译成.pyc?在默认情形下self.compile
跟self.optimize
都是0,是不会对.py文件进行编译的,所以这一步相当于没有。
build_py
结束之后,所有需要的文件都已经在build_lib
(也即./build/lib.linux-x86_64-3.9
)中准备妥当了(这是不是说如果self.distribution.has_pure_modules()为假,也即没有python模块,MANIFEST.in的内容也不会被打包?可能是出于C语言代码只打包编译结果不打包源代码的缘故),下一步就是self.run_command('build_ext')
了:
def run(self):
# 在python setup.py install时如果使用了--inplace参数,则另外拷贝一份至setup.py目录
old_inplace, self.inplace = self.inplace, 0
# 将C语言扩展编译至build_lib
_build_ext.run(self)
self.inplace = old_inplace
if old_inplace:
self.copy_extensions_to_source()
代码段中的_build_ext
可能是Cython.Distutils.build_ext
也可能是distutils.command.build_ext
(默认),目前对Cython基本没有了解,故先研究后者。
_build_ext.run(self)
最终调用了self.build_extensions()
,setuptools对distutils原有的实现做了一点封装,但重点还是在distutils原有的实现:
看到这里发现setuptools的build_extension只支持了最原始的编译方式,不支持CMake,要想支持CMake需要重写build_extension方法,这也是我们现在的处理方式,所以这一块的代码我们是用不到的,这块的解析我们就跳过了。
只要bdist_egg
了,就会把.py文件、C语言源文件连同编译出的so文件一起放到dist-packages目录下的package.egg
目录,同时添加一条记录./package.egg
到dist-packages目录下的easy-install.pth中。这种编译方式理应只在dist-packages目录下生成package.egg
目录,而不会有name
目录,一开始python setup.py install
得到的name
目录忘记是怎么来的了。
总而言之,安装参考链接的写法通过CMake编译,现在可以做到只生成.egg目录,且能正常导入,编译所需的所有文件都在,用户可以自己做调整
build/bdist.linux-x86_64/egg
综上,运行python setup.py install
等价于依次运行如下几条命令:
python setup.py bdist_egg
-- python setup.py egg_info
把算法包的egg目录(形如./name-ver-py3.9-linux-x86_64.egg
)拷贝到/usr/local/lib/python3.9/dist-packages
目录;
往/usr/local/lib/python3.9/dist-packages/easy-install.pth
中添加一行./algo_utils-0.1.1-py3.9-linux-x86_64.egg
egg包的目录结构为:
submoduleA.py
submoduleB.py
EGG-INFO/
PKG-INFO # 对包的描述信息,在setup()中添加的author_email等参数都在这里
SOURCES.txt # 记录了安装的时候打包了那些文件,一行一个文件,可用于事后查看是否正确打包
dependency_links.txt
进阶:多个包共用一个命名空间
随着代码仓库规模扩大,有时候我们会有将某些功能模块独立出来单独进行版本控制的需求,分离出来的模块以一个单独的python包的形式存在,也需要安装,有自己的版本号。但是简单的分离成一个独立的库,在导入时跟之前的写法就不一样了,有很多import
语句需要更改。setuptools
给出了一种解决方案,通过namespace_packages
参数指出命名空间,详情请看参考链接和示例项目(还有一个谷歌的项目也可以参考)。简单地说,就是在setup.py
中的setup()
函数添加一个参数namespace_packages=["顶层模块名"]
,然后按示例项目的写法在对应的__init__.py
中写上__import__("pkg_resources").declare_namespace(__name__)
,这句话的含义是临时导入pkg_resources
包,调用它的declare_namespace()
方法将顶层模块指定为命名空间(参考链接)。需要注意的是原有的主仓库跟新分离出来的子仓库都要对setup()
和__init__.py
做上述处理,如果只是子仓库做了主仓库没做,在python setup.py develop
之后主仓库的命名空间会被子仓库所覆盖,import 顶层模块名
只能找到子仓库,主仓库找不到了,这点一定要注意。如果书写正确,代码分离前后import
命令是不需要改动的。
另一种方案是基于pkgutils
库,跟上面的pkg_resources
方法类似,在对应的__init__.py
里写上__path__ = __import__('pkgutil').extend_path(__path__, __name__)
,然后setup()
函数不需要加namespace_packages
参数。根据文档的说法,这个方案对Python2和Python3的兼容性更强,个人觉得pkg_resources
方法有namespace_packages
参数可以直截了当地看出顶层包名,是比较方便的,在不需要兼容Python2的情况下选用pkg_resources
方案应该更合适。
其他
扩充setup.py支持的命令:需要使用cmdclass参数
setup(cmdclass={"build_ext": CMakeBuild})
新增一个build_ext命令,通过python setup.py build_ext
调用,这里的CMakeBuild是一个父类为setuptools.command.build_ext.build_ext的类,需要重写run方法,在运行python setup.py build_ext
的时候实际上就是在调用CMakeBuild类的run方法。