Pythonパッケージ作成
- https://docs.python.org/3/tutorial/modules.html
- https://docs.python.org/3/reference/import.html
- https://packaging.python.org/
ファイル構成
GitHubやローカルの開発環境から pip
で直接インストールできる形。
pywtl/
├── LICENSE
├── README.md
├── pyproject.toml
├── src/wtl/
│ ├── __init__.py
│ └── hello.py
└── tests/
リポジトリ名(pywtl
)とパッケージ名(wtl
)は必ずしも一致してなくてもよい。
ソースコードは src
の中に入れる流派と、ルート直下に置く流派がある。
落とし穴が少なくて推奨されているのは前者。
開発向けの -e,--editable
オプションをつけたローカルインストールではコピーが起こらず、
編集後に再インストールしなくてもそのまま反映される。
uv pip install -e .
python3 -m wtl.hello
python3 -m site
手動で作ってもいいけど uv
に任せるのが楽。
いくつかの形式があるけどとりあえず --lib
:
--app
: パッケージとして扱われることを想定しないスクリプトやウェブアプリなど。--package
:src/
レイアウトで[build-system]
も設定される。--lib
: 上記に加えてpy.typed
も作成される。
uv init --lib example-lib
cd example-lib/
uv run python -c 'import example_lib; print(example_lib.hello())'
uv version
See https://docs.astral.sh/uv/concepts/projects/init/.
pyproject.toml
- https://packaging.python.org/en/latest/specifications/pyproject-toml/
- https://packaging.python.org/en/latest/guides/writing-pyproject-toml/
パッケージ作成に関わる全てのメタ情報を書いておくファイル。
setuptools
に依存しない形式として
PEP 517,
PEP 621
で決められた。
過去によく使われていた setup.py
,
setup.cfg
,
MANIFEST.in
などは非推奨になった。
[build-system]
, [project]
, [tool]
という3つのテーブルから成る。後に
PEP 735 で
[dependency-groups]
が追加された。
build-system
必須ではないけど推奨。
PyPA/Flit (setuptools後継?),
PDM,
Poetry,
など後発のツールは早くから対応していて、
setuptools
もようやく61.0から使えるようになった。
とりあえず uv init --lib
初期設定の uv_build
を使い、もし不満を感じたら考える。
[build-system]
requires = ["uv_build>=0.8.24,<0.9.0"]
build-backend = "uv_build"
project
[project]
name = "wtl"
version = "0.1.0"
description = "Personal Python Package"
authors = [
{name = "Watal M. Iwasaki", email = "heavywatal@gmail.com"}
]
license = {file = "LICENSE"}
readme = "README.md"
classifiers = [
"Development Status :: 2 - Pre-Alpha",
"Environment :: Console",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: MIT License",
"Topic :: Scientific/Engineering :: Bio-Informatics",
]
requires-python = ">=3.13"
dependencies = [
"tomli-w",
]
[dependency-groups]
dev = [
"pytest",
"pytest-cov",
"ruff",
]
[project.urls]
Source = "https://github.com/heavywatal/pywtl"
[project.scripts]
"hello.py" = "wtl.hello:main"
project.dynamic
に ["description", "version"]
と指定して
__init__.py
のdocstringや __version__
を参照できるかはbackend次第。
uv_build
は今のところサポートしていないので
(uv#8714)
pyproject.toml
に書いたバージョンを
importlib.metadata
で __init__.py
に取り込む。
import importlib.metadata
assert __package__
__version__ = importlib.metadata.version(__package__)
__doc__ = importlib.metadata.metadata(__package__)["Summary"]
バージョンを比較したいときは
packaging.version.parse()
を利用する。
依存関係を書けるところはいくつかある。 See https://docs.astral.sh/uv/concepts/projects/dependencies/:
project.dependencies
: 普通の依存関係。uv pip install
で自動的にインストールされる。project.optional-dependencies
: 通称"extras"。 ユーザー向けに公開されるけどデフォルトではインストールされない。uv add altair --optional plot
のように追加し、uv pip install polars[plot]
のようにインストールする。dependency-groups
: 開発者向けで[project]
の外にある。 パッケージ化しないプロジェクトの依存関係を記述するのにも使える。uv add --group dev ruff
のようにして追加。 名前は何でもいいけどdev
はuv
で特別扱いされていて、--dev
オプションがあったり、デフォルトでuv sync
対象だったりする。requirements.txt
: インストール過程には関与せず、能動的にpip install -r requirements.txt
のように参照するためのもの。
project.scripts
で設定したものは
${prefix}/bin/
に実行可能ファイルが配置される。
以前は console_scripts
で設定していた。
tool
コード整形やテストのような各種開発ツールの設定を記述する。
linterとしては
pyproject.toml
対応拒否のflake8
を捨てて超高速Rust製ruffを使う。
0.2からはformatterとしても使えるようになり、
blackも不要になった。
[tool.pyright]
typeCheckingMode = "strict"
[tool.ruff.lint]
select = ["ALL"]
ignore = [
"D1", # missing docstring
"D203", # incompatible
"D213", # incompatible
"ANN401", # Any
"T201", # print
"S101", # assert
"DTZ", # timezone
"COM812", # trailing comma
"TD", # todo
"FIX", # todo
]
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]
[tool.coverage.run]
source = ["src"]
[tool.coverage.report]
exclude_also = [
"if __name__ == .__main__.:",
]
ソースコード
wtl/__init__.py
- このディレクトリがひとつのパッケージであることを示すファイル。
空でもいいし、初期化処理やオブジェクトを記述してもよい。
文字列変数
__version__ = "0.1.2"
を定義してwtl.__version__
のように参照できるようにしておくのが慣例。
wtl/hello.py
"""Simple module to say hello."""
import getpass
def main():
print("Hello, " + getpass.getuser() + "!")
if __name__ == "__main__":
main()
ソースツリーの中にあるファイルを参照するには
importlib.resources
が使える。
Pythonスクリプトではない設定ファイルなどを同梱して読み込むのに便利。