setuptools库:构建自己的python包

本文目录

基本用法

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"]})

bdist_wheel添加数据文件
sdist添加数据文件

python setup.py cmd表示基于setup.py运行某个命令,这个cmd可以取很多个值,比如installdevelop等,下面给出一些例子:

# 直接将当前目录下的内容安装成`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,就要去确认一下python3python3-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.compileself.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方法。

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我知道了。关于Python分发工具setuptools的安装和使用,具体步骤如下: 1. 首先,确保你已经安装了Python3。如果没有安装,可以去Python官网下载并安装。 2. 接着,在命令行中输入以下命令来安装setuptools: ``` pip3 install setuptools ``` 3. 安装完成后,你可以使用以下命令来验证setuptools是否安装成功: ``` python3 -m easy_install --version ``` 如果输出了setuptools的版本号,说明安装成功。 4. 接下来,你可以使用setuptools来打你的Python项目。在你的项目根目录下创建一个名为`setup.py`的文件,内容如下: ``` from setuptools import setup, find_packages setup( name='your-project-name', version='0.1', packages=find_packages(), install_requires=[ 'dependency1', 'dependency2', # ... 其他依赖项 ], entry_points={ 'console_scripts': [ 'your-script-name=your_package_name.module_name:main' ] } ) ``` 其中,`name`是你的项目名称,`version`是你的项目版本号,`packages`是你的项目中含的所有Python模块,`install_requires`是你的项目依赖的所有Python,`entry_points`是你的项目可执行文件的入口。 5. 编写完`setup.py`文件后,你可以使用以下命令来构建一个源码分发: ``` python3 setup.py sdist ``` 6. 构建完成后,你可以使用以下命令来安装你的项目: ``` pip3 install your-project-name-0.1.tar.gz ``` 7. 如果你的项目需要发布到PyPI等Python管理平台上,可以使用以下命令来上传你的项目: ``` python3 setup.py sdist upload ``` 以上就是使用setuptools进行Python分发的基本步骤。希望对你有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值