編寫 python package 中的 setup.py 文件

引言

之前我們項目組在使用 python 的過程中戳晌,經(jīng)常遇到用 pip install -e . 的方式安裝 python package 的情況泞辐,這些 package 中都有一個 setup.py 文件笔横,里面指定了 package 的依賴、描述性的信息等咐吼。

但是我們從來沒有為自己的 package 寫過類似的 setup.py 文件吹缔。如果需要在程序中調(diào)用自己寫的 package,一般是設(shè)定環(huán)境變量 PYTHONPATH 或者在 python 程序中設(shè)置 sys.path 來具體指定 package 的搜索路徑锯茄。如果只是在本地調(diào)用自己的 package厢塘,路徑相對固定茶没,這種方式也沒什么問題,如果要將 package 分享給別人甚至分享到 PyPI 上晚碾,這種路徑設(shè)置的方式就有些簡陋了抓半。如果我們?yōu)?package 編寫了 setup.py 文件,那么 package 路徑的問題格嘁、依賴的問題以及描述性的信息管理就可以交給 pip (package installer for Python) 處理笛求。 用戶只需要通過 pip install -e . 的方式安裝即可。

在學(xué)習(xí)編寫 setup.py 文件的過程中糕簿,我們感覺這篇文章講解的非常透徹探入,所以將它的內(nèi)容整理總結(jié)出來,目的是方便以后自己和其他研究人員參考冶伞。

一個簡單的 package 例子

如果還不太了解 module 和 package 的概念和用法新症,可以參考 python 官方的 tutorial步氏。

在這里我們先創(chuàng)建一個文件夾 setuppy_tutorial响禽,后邊所有的操作都在這個文件夾中進(jìn)行。
假設(shè)在這個文件夾中有一個 package 名為 greeting_pkg荚醒,文檔路徑結(jié)構(gòu)如下:

.
└── greeting_pkg
    ├── greeting_module.py
    └── __init__.py

其中 __init__.py 為空文件芋类,greeting_module.py 文件內(nèi)容如下:

def greeting_func(name):
    print("Hello, ", name)

也就是說,這個 greeting_pkg package 包含一個 module 文件 greeting_module.py界阁,其中的函數(shù) greeting_func 可以打印出輸入的名字并問候侯繁。
例如,當(dāng) greeting_pkg package 處于當(dāng)前目錄時泡躯,進(jìn)入 python 環(huán)境贮竟,調(diào)用 module 并執(zhí)行其中的函數(shù):

$ python
>>> from greeting_pkg.greeting_module import greeting_func
>>> greeting_func("De Gang")
Hello,  De Gang

如果 greeting_pkg package 沒有處于當(dāng)前目錄,在調(diào)用 module 時就會出現(xiàn)如下路徑錯誤

>>> from greeting_pkg.greeting_module import greeting_func
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'greeting_pkg'

python 搜索 module 的路徑由 sys.path 決定较剃,可以通過如下命令查看:

>>> import sys
>>> print('\n'.join(sys.path))

           <--- 這里有空字符串
/home/automan/pyenv/testsetuppy/lib/python36.zip
/home/automan/pyenv/testsetuppy/lib/python3.6
/home/automan/pyenv/testsetuppy/lib/python3.6/lib-dynload
/usr/lib/python3.6
/home/automan/pyenv/testsetuppy/lib/python3.6/site-packages

(這里用了虛擬環(huán)境咕别,名稱為 testsetuppy)
顯示的路徑列表中包括
1. 當(dāng)前路徑 (以空字符串表示)
2. PYTHONPATH 中的路徑(如果沒有設(shè)置環(huán)境變量 PYTHONPATH,這一項就不存在)
3. 與 python 安裝位置相關(guān)的其他路徑

可以在 python 程序中擴(kuò)展 sys.path 中的路徑写穴,例如在另一篇介紹 SUMO TraCI 的文章中惰拱,為了調(diào)用 TraCI,我們在程序中將 TraCI 所在的路徑加入到 sys.path 中:

if 'SUMO_HOME' in os.environ:
     tools = os.path.join(os.environ['SUMO_HOME'], 'tools')
     sys.path.append(tools)
else:
     sys.exit("please declare environment variable 'SUMO_HOME'")

import traci  

如果自己編寫的 module 不在上述 sys.path 路徑中,就會出現(xiàn)找不到 module 的錯誤。

setup.py 文件的作用之一是提供了一種通過 pip 標(biāo)準(zhǔn)化安裝 package 的方式袁梗,避免了額外設(shè)置路徑的麻煩粱玲。

添加一個最簡潔的 setup.py 文件

在剛才的 setuppy_tutorial 文件夾中 (即與 my_greetings package 在同一目錄中) 編寫如下的 setup.py 文件:

from setuptools import setup, find_packages

setup(
    name='greeting_pkg',
    packages=find_packages()
)

其中,name 是在 pip 中注冊的名字(如果我們將這個 package 分享到 PyPI备籽,必須保證這個名字與 PyPI 中已有 package 注冊的名字不重復(fù)),與 package 文件夾的名字可以不同,但一般只要不涉及到重名勾怒,最好用與 package 相同的名字,方便用戶記憶。packages 后邊跟的是這個 package 的名字以及它內(nèi)部的 subpackage 的名字控硼,這里我們只有 greeting_pkg 一個 package泽论。

現(xiàn)在文檔路徑結(jié)構(gòu)如下:

.
├── greeting_pkg
│   ├── greeting_module.py
│   └── __init__.py
└── setup.py

用如下命令安裝這個 package

pip install -e .

確認(rèn)一下是否安裝成功:

$ pip list

Package       Version Location                 
------------- ------- -------------------------
greeting-pkg  0.0.0   /home/automan/setuppy_tutorial
pip           19.2.1  
pkg-resources 0.0.0   
setuptools    41.0.1  
wheel         0.33.4  

此時我們的 package greeting-pkg 可以像其他通過 pip 安裝的 package 一樣使用,不必考慮路徑問題卡乾。

這里再回頭說一下 pip install -e . 安裝命令翼悴,其中參數(shù) -e 表示以 editable 方式安裝,這樣對于原 python 文件的改動可以直接反映到安裝的 package 中幔妨,不必重新安裝鹦赎。
例如,將 greeting_module.py 文件改成如下內(nèi)容:

def greeting_func(name):
    print("Cheers, ", name)    # "Cheers" 替換了原本的 "Hello"

再調(diào)用該 module 時效果如下:

$ python
>>> from greeting_pkg.greeting_module import greeting_func
>>> greeting_func("De Gang")
Cheers,  De Gang   # 馬上反映了對原 python 文件的修改

擴(kuò)展 setup.py

添加 vesion

在上邊 pip list 命令返回結(jié)果中 greeting_pkg 對應(yīng)的版本是 0.0.0误堡,這是默認(rèn)的版本號古话。我們可以在 setup.py 文件中設(shè)置 version 參數(shù),更好的反映 package 的開發(fā)進(jìn)度锁施。
修改 setup.py 內(nèi)容如下:

from setuptools import setup, find_packages

setup(
    name='greeting_pkg',
    packages=find_packages(),
    version='0.1.0'
)

pip install -e . 命令重新安裝陪踩,再用 pip list 命令顯示如下:

$ pip list

Package       Version Location                 
------------- ------- -------------------------
greeting-pkg  0.1.0   /home/automan/setuppy_tutorial
List          1.3.0   
pip           19.2.1  
pkg-resources 0.0.0   
setuptools    41.0.1  
wheel         0.33.4  

版本號已更新。

添加 package 依賴

如果自己編寫的 package A 調(diào)用了某個 package B悉抵,但是在其他用戶的機子上沒有安裝 package B肩狂,那么在執(zhí)行 package A 時會報錯,找不到 module姥饰。為了避免這種問題傻谁,可以在 setup.py 文件中設(shè)置好依賴的其他 package,讓別人在用 setup.py 安裝時就將所有依賴的 package 一起安裝了列粪。
例如审磁,我們將 greeting_module.py 修改為如下內(nèi)容:

import pyjokes

def greeting_func(name):
    print("Hello,", name)
    print("Here is a joke for you:\n", pyjokes.get_joke())

這里調(diào)用了一個 module pyjokes,它的功能是隨機產(chǎn)生一個 joke岂座。這個 module 需要額外安裝态蒂。可以將 setup.py 文件修改成如下形式:

from setuptools import setup, find_packages

setup(
    name='greeting_pkg',
    packages=find_packages(),
    version='0.1.0',
    install_requires=[          # 添加了依賴的 package
        'pyjokes'
    ]
)

在設(shè)置依賴 package 時可以指定版本號掺逼,例如

  • pyjokes==0.5.0
  • pyjokes>=0.3.0
  • pyjokes>=0.3.0,<0.5.0

重新用 pip install -e . 方式安裝吃媒,然后在 python 中再次調(diào)用 greeting_func() 函數(shù),效果如下:

$ python

>>> from greeting_pkg.greeting_module import greeting_func
>>> greeting_func("De Gang")
Hello, De Gang
Here is a joke for you:
 How to explain the movie Inception to a programmer? When you run a VM inside another VM, inside another VM ... everything runs real slow!

另外吕喘,還可以根據(jù)使用環(huán)境有選擇的安裝某些依賴 package赘那,例如修改 setup.py 文件如下:

from setuptools import setup, find_packages

setup(
    name='greeting_pkg',
    packages=find_packages(),
    version='0.1.0',
    install_requires=[         
        'pyjokes'
    ],
    extras_require={       # 添加了可選安裝的依賴 package
        'interactive': ['matplotlib>=2.2.0,<3.0.0', 'jupyter']
    }
)

其中 extras_require 部分是可以選擇安裝的 package。
使用 pip install -e . 命令并不會安裝 extras_require 里面的 package氯质。
如果要安裝募舟,需要用如下命令:

pip install -e .[interactive]

在命令行中執(zhí)行 module 中的函數(shù)

如果我們希望 module 中的函數(shù)不僅僅只是被其他 python 程序通過 import 調(diào)用,還可以直接在命令行中執(zhí)行闻察,那么可以做如下修改:

  • 在 greeting_module.py 文件中的修改

    import pyjokes
    
    def greeting_func(name):
        print("Hello,", name)
        print("Here is a joke for you:\n", pyjokes.get_joke())
    
    def main():
        import sys
        arg = sys.argv[1]
        greeting_func(arg)
    
  • 在 setup.py 文件中的修改:

    from setuptools import setup, find_packages
    
    setup(
        name='greeting_pkg',
        packages=find_packages(),
        version='0.1.0',
        install_requires=[         
            'pyjokes'
        ],
        extras_require={
            'interactive': ['matplotlib>=2.2.0,<3.0.0', 'jupyter']
        },
        entry_points={       # 設(shè)置了在命令行中如何使用 greeting_module  中的 main 函數(shù)
            'console_scripts': [
                'greeting=greeting_pkg.greeting_module:main'
            ]
        }
    )
    

這里需要注意的是拱礁,我們的 greeting_func() 是需要送入?yún)?shù)的琢锋,但是在命令行中執(zhí)行函數(shù)不能添加參數(shù),只能以 sys.argv 的形式讀進(jìn)去呢灶,再進(jìn)行后續(xù)的處理吴超。所以當(dāng)作函數(shù)使用和當(dāng)作命令行中的命令使用時,"入口" 是不一樣的鸯乃!

做了以上修改之后鲸阻,再用 pip install -e . 命令安裝一下,然后測試缨睡。在命令行中輸入:

$ greeting "De Gang"
Hello, De Gang
Here is a joke for you:
 3 Database Admins walked into a NoSQL bar. A little while later they walked out because they couldn’t find a table.

添加獨立的 module

除了以 package 的形式存在鸟悴,還可以允許不屬于任何 package 的 module 存在。例如我們在 package 的外邊添加兩個 module奖年,文檔路徑結(jié)構(gòu)如下:

$ tree -L 2
.
├── greeting_pkg
│   ├── greeting_module.py
│   └── __init__.py
├── isolated_greeting_module_1.py
├── isolated_greeting_module_2.py
└── setup.py

1 directory, 5 files

其中

  • isolated_greeting_module_1.py 內(nèi)容如下:
def greeting_func(name):
    print("Hi,", name, ', greetings from isolated_greeting_module_1.')
  • isolated_greeting_module_2.py 內(nèi)容如下:
def greeting_func(name):
    print("Hi,", name, ', greetings from isolated_greeting_module_2.')

對應(yīng)的 setup.py 中的內(nèi)容如下:

from setuptools import setup, find_packages

setup(
    name='greeting_pkg',
    packages=find_packages(),
    py_modules=[     # 在 package 之外添加兩個獨立的 module
        'isolated_greeting_module_1',
        'isolated_greeting_module_2'
    ],
    version='0.1.0',
    install_requires=[         
        'pyjokes'
    ],
    extras_require={
        'interactive': ['matplotlib>=2.2.0,<3.0.0', 'jupyter']
    },
    entry_points={       
        'console_scripts': [
            'greeting=greeting_pkg.greeting_module:main'
        ]
    }
)

pip install -e . 安裝之后细诸,可以像普通的 module 一樣調(diào)用,例如:

$ python

>>> from isolated_greeting_module_1 import greeting_func
>>> greeting_func("De Gang")
Hi, De Gang , greetings from isolated_greeting_module_1.

添加描述性的條目

如果編寫 setup.py 文件的目的是希望將整個 package 分享給其他研究人員或者分享到 PyPI 上陋守,那么還要添加一些描述性的信息震贵,以便別人更好的理解這個 package。常見的描述如下:

from setuptools import setup, find_packages

setup(
   ...
   # metadata to display on PyPI
    author="Me",
    author_email="me@example.com",
    description="This is an Example Package",
    keywords="hello world example examples",
    url="http://example.com/HelloWorld/",   # project home page, if any
    project_urls={
        "Documentation": "https://docs.example.com/HelloWorld/",
        "Source Code": "https://code.example.com/HelloWorld/",
    },
    classifiers=[
        'License :: OSI Approved :: Python Software Foundation License'
    ]
)

以上是基本的 setup.py 編寫方法嗅义。還有其他更多的條目以及對 setup.py 更深入的介紹可以在這里查閱屏歹。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末隐砸,一起剝皮案震驚了整個濱河市之碗,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌季希,老刑警劉巖褪那,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異式塌,居然都是意外死亡博敬,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門峰尝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來偏窝,“玉大人,你說我怎么就攤上這事武学〖劳” “怎么了?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵火窒,是天一觀的道長硼补。 經(jīng)常有香客問我,道長熏矿,這世上最難降的妖魔是什么已骇? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任离钝,我火速辦了婚禮,結(jié)果婚禮上褪储,老公的妹妹穿的比我還像新娘卵渴。我一直安慰自己,他們只是感情好鲤竹,可當(dāng)我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布奖恰。 她就那樣靜靜地躺著,像睡著了一般宛裕。 火紅的嫁衣襯著肌膚如雪瑟啃。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天揩尸,我揣著相機與錄音蛹屿,去河邊找鬼。 笑死岩榆,一個胖子當(dāng)著我的面吹牛错负,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播勇边,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼犹撒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了粒褒?” 一聲冷哼從身側(cè)響起识颊,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎奕坟,沒想到半個月后祥款,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡月杉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年刃跛,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片苛萎。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡桨昙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出腌歉,到底是詐尸還是另有隱情蛙酪,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布究履,位于F島的核電站滤否,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏最仑。R本人自食惡果不足惜藐俺,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一炊甲、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧欲芹,春花似錦卿啡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至浙宜,卻和暖如春官辽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背粟瞬。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工同仆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人裙品。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓俗批,卻偏偏與公主長得像,于是被迫代替她去往敵國和親市怎。 傳聞我的和親對象是個殘疾皇子岁忘,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內(nèi)容