Heavy Watal

Pythonパッケージ作成

ファイル構成

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:

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

パッケージ作成に関わる全てのメタ情報を書いておくファイル。 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.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スクリプトではない設定ファイルなどを同梱して読み込むのに便利。