PyPIデビュー2015

tell-k
PyCon JP 2015 (2015.10.11)

お前誰よ?

https://pbs.twimg.com/profile_images/1045138776224231425/3GD8eWeG_200x200.jpg _images/vxjmiemo.png

目的/動機

趣味

対象

対象

_images/ljj0l-e4.png

Pythonプロフェッショナルプログラミング 第2版

http://ecx.images-amazon.com/images/I/51ZNlK0%2Bn-L._SL160_.jpg

目次

PyPIとは?

_images/3x4hkmn6.png

PyPiとは?

PyPAとは?

_images/zli4wet3.png

PyPAとは?

Pythonパッケージ作成

Pythonパッケージ作成

  1. アカウント作成(https://pypi.python.org/pypi?%3Aaction=register_form)
  2. setup.pyを書く
  3. パッケージ登録 & アップロード

2. setup.py

from setuptools import setup, find_packages

setup(
    name='sample',
    version='1.0.0',
    url='https://github.com/pypa/sampleproject',
    packages=find_packages(exclude=['tests*']),
)

3. パッケージ登録

# パッケージ登録
$ python setup.py register
# 指示に従って入力

# パッケージアップロード
$ python setup.py sdist upload

終わりです。簡単ですね。

そういうわけにもいきますまい…

_images/wuuz3mx_.png

Pythonパッケージ作成

  1. setup.pyを書く
  2. 開発する
  3. テストする
  4. 配布物を決める
  5. 登録/アップロード

構成

例えば「sample」というパッケージを妄想する

sample
  ├── MANIFEST.in
  ├── README.rst
  ├── sample        <- Pythonパッケージ
  │   └── __init__.py
  ├── setup.cfg
  ├── setup.py
  └── tests         <- テストコード
     ├── __init__.py
     └── test_sample.py

1. setup.pyを書く

setup.pyに書く事

setup.pyに書く事

setup.pyの例

setup(
    name='sample',
    version='1.0.0',
    description='A sample Python project',
    long_description=long_description,
    url='https://github.com/pypa/sampleproject',
    author='The Python Packaging Authority',
    author_email='pypa-dev@googlegroups.com',
    license='MIT',
    classifiers=[
        'Development Status :: 3 - Alpha',
        'Intended Audience :: Developers',
        'Topic :: Software Development :: Build Tools',
        'License :: OSI Approved :: MIT License',
        'Programming Language :: Python :: 2',
        'Programming Language :: Python :: 3',
    ],
    keywords='sample setuptools development',
    packages=find_packages(exclude=['contrib', 'docs', 'tests*']),
    install_requires=['peppercorn'],
 )

参考: https://github.com/pypa/sampleproject/blob/master/setup.py

classifiers is 何?

classifiers

Framework :: Django
Framework :: Django :: 1.4
Framework :: Django :: 1.5
Framework :: Django :: 1.6
Framework :: Django :: 1.7
Framework :: Django :: 1.8
Development Status :: 1 - Planning
Development Status :: 2 - Pre-Alpha
Development Status :: 3 - Alpha
Development Status :: 4 - Beta
Development Status :: 5 - Production/Stable
Development Status :: 6 - Mature
Development Status :: 7 - Inactive

version

1.2.0.dev1  # Development release
1.2.0a1     # Alpha Release
1.2.0b1     # Beta Release
1.2.0rc1    # Release Candidate
1.2.0       # Final Release
1.2.0.post1 # Post Release
15.10       # Date based release
23          # Serial release

version

バージョンナンバーは、メジャー.マイナー.パッチとし、バージョンを上げるには、

- APIの変更に互換性のない場合はメジャーバージョンを、
- 後方互換性があり機能性を追加した場合はマイナーバージョンを、
- 後方互換性を伴うバグ修正をした場合はパッチバージョンを上げます。

プレリリースやビルドナンバーなどのラベルに関しては、メジャー.マイナー.パッチの
形式を拡張する形で利用することができます。

via http://semver.org/lang/ja/

version

def find_version(*file_paths):
    version_file = read(*file_paths)
    version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
                              version_file, re.M)
    if version_match:
        return version_match.group(1)
    raise RuntimeError("Unable to find version string.")

setup(
   ...
   version=find_version("package", "__init__.py")
   ...
)

via Single-sourcing the Project Version

long_description

_images/m5j2qspj.png

long_description

with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
    long_description = f.read()

 setup(
    ...
    long_description=long_description,
    ...
 )

via https://github.com/pypa/sampleproject/blob/master/setup.py

packages

from setuptools import find_packages

setup(
   ...
   packages=find_packages(exclude=['tests*'])
   ...
)

excludeで適切に除外

# × 除外しない
packages=find_packages()

# ○ 除外する
packages=find_packages(exclude=['tests*'])
# 除外しなかった場合
import sample
import tests.test_sample  # トップレベルでテストコードがインストールされてしまう

install_requires

setup(
   ...
   install_requires=[
      'Hoge',
   ]
   ...
)

install_requires VS requirements.txt

$ pip freeze > requirements.txt
# インストール済みパッケージの一覧を出してくれる
# バージョンを固定
Spam==1.0.0
Ham==2.1.0
Egg==1.3.0

install_requiresにこれ使えば?

with open('requiments.txt') as fp:
       requires = fp.readlines()

install_requires=requires

バージョン固定されると単純に困る

$ pip install Spam
$ pip freeze | grep Spam
Spam==1.1.0
# => Spamの最新版 1.1.0 が インストール

$ pip install sample
$ pip freeze | grep Spam
Spam==1.0.0
# => install_requires の バージョン固定により1.0.0になってしまう

_人人人人人人人人人人人人人人人_
> 突然のダウングレード!!! <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^ ̄

# 別パッケージが同様に、別のバージョン番号を固定で指定してると、
# バージョン競合が発生しインストールに失敗するケースも...

とてもつらい

_images/p1fcoye0.png

install_requires の 役割

# そもそもバージョンを指定しない
install_requires=['Ham']

# 最低限必要なバージョンだけを記載
install_requires=['Ham>=1.0.0']

# 範囲を限定的にするとか
install_requires=['Ham>=1.0.0,<2']

# requirements.txtにもバージョン指定なし、範囲指定を記述可能
# install_requiresにバージョンを固定しないというのが大事

開発する

Python2/3両対応

Python2/3両対応

sixの例

import six

d = {'hoge1': 'fuga', 'hoge2': 'fuga', }

# python2
for k, v in d.iteritems():
    print(k, v)

# python3
for k, v in d.items():
    print(k, v)

# python2, 3 両方
for k, v in six.iteritems(d):
    print(k, v)

先達の知見を得る

Python3しか対応しないのもあり

http://cdn-ak.f.st-hatena.com/images/fotolife/n/niguruta/20101015/20101015123332.jpg

開発モード

開発モード

$ pip install -e .

# そのまま動作確認可能
$ python
Python 3.4.3 (default, Mar 23 2015, 04:19:36)
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import sample
>>> sample.main()
Call your main application code here

テスト

tox

$ pip install tox

tox.iniの例

[tox]
envlist=py27,py34,py35,pypy

[testenv]
commands=
    python setup.py test
# 全バージョンでテストを実行
$ tox

# バージョンを指定してテスト実行
$ tox -e py35

どこまでサポートすればいいの?

pytest

assertが賢くなる例

$ py.test test_assert.py

...

=========== FAILURES =============
__________ test_main _____________

    def test_main():
        lst1 = ['test1', 'test2']
        lst2 = ['test1', 'test3']

>       assert lst1 == lst2, 'Not equal lists'

E       AssertionError: Not equal lists
E       assert ['test1', 'test2'] == ['test1', 'test3']
E         At index 1 diff: 'test2' != 'test3'
E         Full diff:
E         - ['test1', 'test2']
E         ?                ^
E         + ['test1', 'test3']
E         ?

setup.py testから呼ぶ

setup.py testから呼ぶ

import sys

from setuptools.command.test import test as TestCommand

class PyTest(TestCommand):
    user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")]

    def initialize_options(self):
        TestCommand.initialize_options(self)
        self.pytest_args = []

    def finalize_options(self):
        TestCommand.finalize_options(self)
        self.test_args = []
        self.test_suite = True

    def run_tests(self):
        #import here, cause outside the eggs aren't loaded
        import pytest
        errno = pytest.main(self.pytest_args)
        sys.exit(errno)

setup.py testから呼ぶ

setup(
    ...
    tests_require = ['pytest'],
    cmdclass = {'test': PyTest},
    ...
    )
# py.testが実行される
$ python setup.py test

coverage

$ python setup.py test -a "--cov sample --cov-report term-missing"
_images/s8kwn9ff.png

配布物を決める

MANIFEST.in

include README.rst
include tox.ini
include setup.cfg
recursive-include docs *
recursive-include tests *
include sample/*.dat

ビルド

sdist

# ビルド
$ python setup.py sdist

# 直下のdistにsdistが生成されます
$ ls dist
sample-0.0.1.tar.gz

wheel

$ pip install wheel

wheel

$ python setup.py bdist_wheel

# python3にしか対応しない
$ python setup.py bdist_wheel --python-tag=py3

wheel

[wheel]
universal = 1
$ python setup bdist_wheel
$ ls
sample-0.0.1-py2.py3-none-any.whl

アップロード

アカウントの作成

_images/ydjsq6a_.png

https://pypi.python.org/pypi?%3Aaction=register_form

パッケージの登録

$ ./setup.py register
running register
running egg_info
writing tesdat.egg-info/PKG-INFO
writing top-level names to sample.egg-info/top_level.txt
writing dependency_links to sample.egg-info/dependency_links.txt
reading manifest file 'sample.egg-info/SOURCES.txt'
writing manifest file 'sample.egg-info/SOURCES.txt'
running check
We need to know who you are, so please choose either:
 1. use your existing login,

...

Registering sample to https://pypi.python.org/pypi
Server response (200): OK
I can store your PyPI login so future submissions will be faster.
(the login will be stored in /home/tell-k/.pypirc)
Save your login (y/N)?y

パッケージの登録

[distutils]
index-servers =
   pypi

[pypi]
username: tell-k
password: xxxxxxxxxxxx

パッケージのアップロード

# 事前に「setup.py」 の記述内容をチェック
$ python setup.py check -r -s

$ python setup.py sdist bdist_wheel upload

# × uplaod単体では利用できない
$ python upload
running upload
error: No dist file created in earlier command

Twineでのアップロード(推奨)

$ pip install twine
$ twine uplaod dist/*

testpypi.python.org の利用

_images/lttwh7en.png

pypircの設定

[distutils]
index-servers=
    pypi
    pypitest

[pypitest]
repository = https://testpypi.python.org/pypi
username = <your user name goes here>
password = <your password goes here>

[pypi]
repository = https://pypi.python.org/pypi
username = <your user name goes here>
password = <your password goes here>

testpypiへの操作

パッケージ登録
$ python setup.py register -r https://testpypi.python.org/pypi

アップロード
$ python setup.py sdist upload -r https://testpypi.python.org/pypi

インストール
$ pip install -i https://testpypi.python.org/pypi <package name>

依存パッケージがあるようのものは「--extra-index-url」を利用すると良い
$ pip install --extra-index-url https://testpypi.python.org/pypi <package name>

その他のTips

ドキュメントのアップロード

setup.cfg:

[upload_docs]
upload-dir = _build/sphinx/html
# Sphinxをビルドしてアップロード
$ python setup.py build_sphinx upload_docs

# アップロード後はpyhthonhosted.orgのURLで確認できる
# pypiからリンクが貼られれます
http://pythonhosted.org/<packge name>/

コマンドラインツール

entry_points = {
    "console_scripts": [
        "sayhello=sample.commands:hello",
    ]
}

# sample.commandsモジュールのhello関数を呼び出す
$ pip install sample

$ sayhello
Hello! my name is hoge.

$ which sayhello
/usr/local/bin/sayhello

Githubのバージョンタグをつける

# リリース時点でタグを切る
$ git tag -am "Version 1.0.0" v1.0.0
$ git push origin --tags

Githubのバージョンタグをつける

_images/x9134jfl.png

Djangoアプリみたいなの

便利サービス

便利サービス

Travis CI

.travis.yml例

language: python
python: 3.4
env:
  matrix:
   - TOXENV=py26
   - TOXENV=py27
   - TOXENV=py33
   - TOXENV=py34
   - TOXENV=pypy
   - TOXENV=flake8
install:
  - pip install tox
  - if test "$TOXENV" = py34 ; then pip install coveralls ; fi
script: tox
after_script:
  - if test "$TOXENV" = py34 ; then coveralls ; fi

Coveralls

_images/6n4sok0b.png

Coveralls

install:
  - pip install tox
  - if test "$TOXENV" = py34 ; then pip install coveralls ; fi
script: tox
after_script:
  - if test "$TOXENV" = py34 ; then coveralls ; fi

Code Climate

_images/phbb_i4h.png

Code Climate

_images/qkkl6cdq.png

Read The Docs

_images/to6rlori.png

Requires.io

Requires.io

_images/vvbm341_.png

Sheilds.io

_images/d1iqgbwa.png

トラブルシューティング

トラブルシューティング

まちがってアップロードした!

_images/by6w58ku.png

一度消したら同じバージョン番号が使えない!

_images/oibkxoxp.png

説明文がレンダリングされていない!

_images/0-myl_qn.png

今日話さなかった事

参考URL

Python Packaging Authority
https://www.pypa.io/en/latest/
PEP Summaries
https://www.pypa.io/en/latest/peps/
Python Packaging User Guide
https://python-packaging-user-guide.readthedocs.org/en/latest/
vinta/awesome-python
https://github.com/vinta/awesome-python
PyPIデビュー
http://www.freia.jp/taka/docs/pyhack4/pypi/
PyPIデビュー
http://note.crohaco.net/2014/pypi-debut/

参考URL

PyPIにパッケージ登録する
http://qiita.com/edvakf@github/items/d82cd7ab77ea2b88506c
How To Package Your Python Code
http://python-packaging.readthedocs.org/en/latest/index.html
Sharing Your Labor of Love: PyPI Quick and Dirty
https://hynek.me/articles/sharing-your-labor-of-love-pypi-quick-and-dirty/
Python: PyPIにパッケージをアップロードする最新の推奨な方法
http://elicon.blog57.fc2.com/blog-entry-422.html
Windows での Python 2.7/3.4 の拡張モジュールビルド環境
http://qiita.com/methane/items/2210712763b91e75fdf0
パッケージングの今
http://www.slideshare.net/aodag/ss-39068785
パッケージングの今と未来
http://www.slideshare.net/aodag/ss-26183017

まとめ

_images/m4naltmy.png

Special Thanks

ご静聴ありがとうとございました

Use the left and right arrow keys or click the left and right edges of the page to navigate between slides.
(Press 'H' or navigate to hide this message.)