0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaとPythonで作ったアプリをPyPIで公開する方法

0
Posted at

今回の内容は、Python公式のPackaging User Guideで案内されている pyproject.tomlpython -m buildtwine 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のエコシステムに接続できるのは、なかなか面白い選択肢だと思います。


以上.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?