今回の内容は、Python公式のPackaging User Guideで案内されている pyproject.toml、python -m build、twine upload の流れと、PyPI公式ヘルプのAPI token方式、setuptoolsのpackage-dataの考え方を前提にしています。(Python Packaging)
はじめに
Pythonのライブラリを作っていると、普通はPythonだけで完結するものを想像するかもしれません。
しかし実際には、
- Javaで作った処理をPythonから使いたい
- 既存のJavaライブラリをPython向けにラップしたい
- JVM上の資産をPythonユーザーにも届けたい
- Pythonの
pip installだけで使えるようにしたい
というケースもあります。
この記事では、Javaで作ったJARファイルをPythonパッケージに同梱し、PyPIで公開する方法を整理します。
例として、仮想プロジェクト java-greeter-py を作ることにします。
pip install java-greeter-py
でインストールできて、Pythonから次のように使えることを目標にします。
from java_greeter import Greeter
g = Greeter()
print(g.hello("Python"))
出力例:
Hello, Python from Java!
全体像
構成は次のようなイメージです。
java-greeter-py/
├── pyproject.toml
├── README.md
├── LICENSE
├── src/
│ └── java_greeter/
│ ├── __init__.py
│ ├── greeter.py
│ └── jars/
│ └── java-greeter-core.jar
└── README_build.md
ポイントは、Javaで作ったJARファイルを
src/java_greeter/jars/
の下に置き、Pythonパッケージの一部として配布することです。
Python側では、jpype1 などを使ってJVMを起動し、同梱したJARをclasspathに追加してJavaクラスを呼び出します。
前提条件
この記事では、以下はすでに準備済みとします。
- PyPIアカウント
- PyPI API token
- 必要ならTestPyPIアカウント
- 必要ならTestPyPI API token
- Javaのビルド環境
- Pythonのビルド環境
API tokenの作成方法やPyPIアカウントの作成方法はこの記事では省略します。
Java側の例
まずJava側に、次のようなクラスがあるとします。
package com.example.greeter;
public class Greeter {
public String hello(String name) {
return "Hello, " + name + " from Java!";
}
}
これをビルドして、次のJARを作成します。
java-greeter-core.jar
MavenやGradleで作成してもよいですし、小さなプロジェクトであれば手動でJAR化しても構いません。
この記事では、JARの作成そのものは主題ではないため、作成済みのJARをPythonパッケージに同梱する前提にします。
配置先は以下です。
src/java_greeter/jars/java-greeter-core.jar
Python側のラッパーを書く
Python側では jpype1 を使ってJavaクラスを呼び出すことにします。
src/java_greeter/greeter.py を作成します。
from pathlib import Path
import jpype
import jpype.imports
def _start_jvm_if_needed():
if jpype.isJVMStarted():
return
jar_dir = Path(__file__).resolve().parent / "jars"
jar_path = jar_dir / "java-greeter-core.jar"
jpype.startJVM(classpath=[str(jar_path)])
class Greeter:
def __init__(self):
_start_jvm_if_needed()
from com.example.greeter import Greeter as JavaGreeter
self._java_greeter = JavaGreeter()
def hello(self, name: str) -> str:
return str(self._java_greeter.hello(name))
src/java_greeter/__init__.py も作成します。
from .greeter import Greeter
__all__ = ["Greeter"]
これでPython側からは次のように使えます。
from java_greeter import Greeter
g = Greeter()
print(g.hello("Python"))
pyproject.tomlを書く
次に、PyPI配布用の pyproject.toml を作成します。
[build-system]
requires = ["setuptools>=77.0.3", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "java-greeter-py"
version = "0.1.0"
description = "A sample Python package that wraps a Java JAR"
readme = "README.md"
requires-python = ">=3.9"
license = "Apache-2.0"
license-files = ["LICENSE"]
authors = [
{ name = "Your Name" }
]
keywords = ["python", "java", "jpype", "pypi"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Operating System :: OS Independent",
"Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = [
"jpype1>=1.4.0",
]
[project.urls]
Homepage = "https://github.com/example/java-greeter-py"
Repository = "https://github.com/example/java-greeter-py"
Issues = "https://github.com/example/java-greeter-py/issues"
[tool.setuptools.packages.find]
where = ["src"]
[tool.setuptools.package-data]
java_greeter = ["jars/*.jar"]
重要なのはこの部分です。
[tool.setuptools.package-data]
java_greeter = ["jars/*.jar"]
これにより、src/java_greeter/jars/*.jar がPythonパッケージに含まれます。
ライセンス指定について
最近のsetuptoolsでは、ライセンスは次のようにSPDX形式で書くのがよいです。
license = "Apache-2.0"
license-files = ["LICENSE"]
古い書き方として、以下のようなclassifierを書く例を見かけることがあります。
"License :: OSI Approved :: Apache Software License"
しかし、新しいsetuptoolsではエラーになる場合があります。
そのため、ライセンスは license = "Apache-2.0" のように書き、古いライセンスclassifierは入れないほうが安全です。
README.mdを書く
最低限、インストール方法と使い方を書いておきます。
# java-greeter-py
A sample Python package that wraps a Java JAR.
## Installation
```bash
pip install java-greeter-py
Usage
from java_greeter import Greeter
g = Greeter()
print(g.hello("Python"))
PyPIでは `README.md` の内容がプロジェクトページに表示されるので、READMEは意外と重要です。
## ビルドツールをインストールする
リポジトリ直下で実行します。
```bash
python -m pip install --upgrade pip
python -m pip install --upgrade build twine
古いビルド成果物を削除する
ビルド前に古い成果物を消しておくと安全です。
rm -rf dist build *.egg-info src/*.egg-info
パッケージをビルドする
python -m build
成功すると dist/ の下にファイルが作成されます。
dist/
java_greeter_py-0.1.0.tar.gz
java_greeter_py-0.1.0-py3-none-any.whl
配布ファイルをチェックする
python -m twine check dist/*
成功例:
Checking dist/java_greeter_py-0.1.0-py3-none-any.whl: PASSED
Checking dist/java_greeter_py-0.1.0.tar.gz: PASSED
JARが含まれているか確認する
今回のようにJavaのJARを同梱する場合、wheelにJARが入っているか確認します。
unzip -l dist/*.whl | grep jar
期待する結果:
java_greeter/jars/java-greeter-core.jar
もしJARが含まれていない場合は、pyproject.toml の次の指定を確認します。
[tool.setuptools.package-data]
java_greeter = ["jars/*.jar"]
ローカルでインストール確認する
PyPIへアップロードする前に、ローカルのwheelを新しい仮想環境にインストールして確認します。
python -m venv .venv-release-test
source .venv-release-test/bin/activate
python -m pip install --upgrade pip
python -m pip install dist/java_greeter_py-0.1.0-py3-none-any.whl
確認します。
python - <<'PY'
from java_greeter import Greeter
g = Greeter()
print(g.hello("Python"))
PY
期待する出力:
Hello, Python from Java!
確認が終わったら仮想環境を抜けます。
deactivate
rm -rf .venv-release-test
TestPyPIにアップロードする
本番PyPIの前にTestPyPIで確認する場合は、次のようにします。
python -m twine upload --repository testpypi dist/*
入力を求められたら、次のように入力します。
username: __token__
password: TestPyPIのAPI token
注意点として、PyPI本番のAPI tokenはTestPyPIでは使えません。
PyPIとTestPyPIは別サービスなので、TestPyPI用のAPI tokenが必要です。
TestPyPIからインストール確認する
TestPyPIからインストールする場合、依存パッケージがTestPyPI側に存在しないことがあります。
そのため、依存パッケージは通常のPyPIから入れて、対象パッケージだけTestPyPIから入れるのが簡単です。
python -m venv .venv-testpypi
source .venv-testpypi/bin/activate
python -m pip install --upgrade pip
python -m pip install jpype1
python -m pip install \
--index-url https://test.pypi.org/simple/ \
--no-deps \
java-greeter-py==0.1.0
確認します。
python - <<'PY'
from java_greeter import Greeter
g = Greeter()
print(g.hello("Python"))
PY
PyPI本番にアップロードする
TestPyPIで問題なければ、本番PyPIへアップロードします。
python -m twine upload dist/*
入力を求められたら、次のように入力します。
username: __token__
password: PyPIのAPI token
ここでの password はPyPIのログインパスワードではありません。
pypi- で始まるAPI token全体を貼り付けます。
公開後にインストール確認する
PyPIに公開できたら、新しい仮想環境でインストール確認します。
python -m venv .venv-pypi-test
source .venv-pypi-test/bin/activate
python -m pip install --upgrade pip
python -m pip install java-greeter-py==0.1.0
確認します。
python - <<'PY'
from java_greeter import Greeter
g = Greeter()
print(g.hello("Python"))
PY
バージョン番号に注意する
PyPIでは、一度アップロードした同じバージョンのファイルを再アップロードできません。
たとえば、0.1.0 を公開した後に修正が必要になった場合は、pyproject.toml のバージョンを更新します。
version = "0.1.1"
そのうえで、もう一度ビルドしてアップロードします。
rm -rf dist build *.egg-info src/*.egg-info
python -m build
python -m twine check dist/*
python -m twine upload dist/*
よくあるエラー
403 Forbiddenになる
TestPyPIにアップロードしているのに、PyPI本番のAPI tokenを使っている可能性があります。
python -m twine upload --repository testpypi dist/*
の場合は、TestPyPIのAPI tokenを使います。
username: __token__
password: TestPyPIのAPI token
本番PyPIの場合は、本番PyPIのAPI tokenを使います。
python -m twine upload dist/*
username: __token__
password: PyPIのAPI token
File already existsになる
同じバージョンをすでにPyPIへアップロード済みです。
PyPIでは同じバージョンのファイルを上書きできません。
pyproject.toml のバージョン番号を上げてください。
version = "0.1.1"
JARが見つからない
wheelにJARが含まれていない可能性があります。
確認します。
unzip -l dist/*.whl | grep jar
含まれていない場合は、pyproject.toml のpackage-data指定を確認します。
[tool.setuptools.package-data]
java_greeter = ["jars/*.jar"]
また、JARの配置場所が正しいかも確認します。
src/java_greeter/jars/java-greeter-core.jar
JVMが起動できない
利用者の環境にJavaが入っていない可能性があります。
この方式では、PythonパッケージにJARは同梱できますが、Javaランタイムまで同梱するわけではありません。
READMEには、Javaが必要であることを書いておくと親切です。
例:
## Requirements
- Python 3.9+
- Java 11+
Java以外にも応用できる
この記事ではJavaのJARを例にしましたが、考え方としてはJavaに限定されません。
たとえば、次のようなファイルも同じようにPythonパッケージへ含められます。
- 辞書ファイル
- 設定ファイル
- モデルファイル
- SQLファイル
- ルールファイル
- 小さなバイナリデータ
ポイントは、Pythonコード以外のファイルを package-data として含めることです。
ただし、巨大なファイルを同梱するとwheelのサイズが大きくなります。
その場合は、
- PyPIに含める
- 初回実行時にダウンロードする
- 別パッケージに分ける
- Mavenなど別の配布経路を使う
といった設計も検討したほうがよいです。
まとめ
Javaで作った処理でも、Pythonから使いやすい形にラップすれば、PyPIで配布できます。
今回の流れは次の通りです。
# 1. JARをPythonパッケージ内に配置
src/java_greeter/jars/java-greeter-core.jar
# 2. pyproject.tomlでpackage-dataを指定
[tool.setuptools.package-data]
java_greeter = ["jars/*.jar"]
# 3. ビルド
python -m build
# 4. チェック
python -m twine check dist/*
# 5. JAR同梱確認
unzip -l dist/*.whl | grep jar
# 6. PyPIへアップロード
python -m twine upload dist/*
Javaの資産をPythonユーザーに届けたいとき、PyPIはかなり便利な配布経路になります。
個人的には、Javaで作った堅牢な処理を、Pythonから気軽に使えるようにする構成はかなり魅力的だと思っています。
Pythonだけで完結する世界も便利ですが、JavaにはJavaの強みがあります。
その強みをPythonのエコシステムに接続できるのは、なかなか面白い選択肢だと思います。
以上.