私は、オープンソースのフラッシュカードプログラムである Anki の一連のアドオンに取り組んでいます。 AnkiアドオンはPythonパッケージとして出荷され、基本的なフォルダー構造は次のようになります:
_anki_addons/
addon_name_1/
__init__.py
addon_name_2/
__init__.py
_
_anki_addons
_は、基本アプリによって_sys.path
_に追加され、次に各アドオンを_import <addon_name>
_でインポートします。
私が解決しようとしている問題は、グローバル状態を汚染したり、ベンダーのパッケージの手動編集にフォールバックしたりせずに、パッケージとその依存関係をアドオンと配布する信頼できる方法を見つけることです。
具体的には、このようなアドオン構造を考えると...
_addon_name_1/
__init__.py
_vendor/
__init__.py
library1
library2
dependency_of_library2
...
_
...__vendor
_ディレクトリに含まれている任意のパッケージをインポートできるようにしたい、たとえば:
_from ._vendor import library1
_
このような相対インポートの主な問題は、絶対参照を介してインポートされた他のパッケージにも依存するパッケージでは機能しないことです(例:_import dependency_of_library2
_のソースコードの_library2
_)。
これまでのところ、私は次のオプションを検討しました:
import addon_name_1._vendor.dependency_of_library2
_)内の完全修飾モジュールパスを指すようにします。しかし、これは退屈な作業ではありません。より大きな依存関係ツリーにスケーラブルで、他のパッケージに移植できません。sys.path.insert(1, <path_to_vendor_dir>)
を介して__vendor
_に_sys.path
_を追加します。これは機能しますが、モジュールルックアップパスにグローバルな変更が導入され、他のアドオンやベースアプリ自体にも影響を及ぼします。後で、Pandoraの一連の問題が発生する可能性のあるハックのように見えます(たとえば、同じパッケージの異なるバージョン間の競合など)。私はこれにかなりの時間を費やしてきましたが、これを行う簡単な方法が完全に欠けているか、または私の全体的なアプローチに根本的に何か問題があると考え始めています。
_sys.path
_ハックや問題のパッケージの変更に頼らなくても、サードパーティパッケージの依存関係ツリーをコードと一緒に出荷する方法はありませんか?
編集:
明確にするために:anki_addonsフォルダーからアドオンをインポートする方法を制御することはできません。 anki_addonsは、すべてのアドオンがインストールされる基本アプリによって提供されるディレクトリです。これはsysパスに追加されるため、その中のアドオンパッケージは、Pythonのモジュールルックアップパスにある他のpythonパッケージとほとんど同じように動作します。
まず第一に、私はベンダーに反対することを勧めます。いくつかの主要なパッケージは以前はベンダーを使用していましたが、ベンダーを処理する必要のある苦痛を避けるために切り替えました。そのような例の1つが requests
library です。 _pip install
_を使用してパッケージをインストールするユーザーに依存している場合は、依存関係を使用して、仮想環境についてユーザーに伝えます。依存関係を複雑にしないという負担を負う必要がある、またはグローバルなPython _site-packages
_の場所に依存関係をインストールするのを止める必要がある)必要があると思い込まないでください。
同時に、サードパーティツールのプラグイン環境が何か異なるものであることを感謝しています。また、依存関係をPythonこのツールで使用されるインストールに追加すると、面倒な、または不可能なベンダライズが可能になる場合があります。 Ankiは拡張機能をsetuptoolsサポートなしで_.Zip
_ファイルとして配布しているので、確かにそのような環境です。
したがって、依存関係をベンダーに選択する場合は、スクリプトを使用して依存関係を管理し、それらのインポートを更新します。これはオプション#1ですが、automatedです。
これはpip
プロジェクトが選択したパスです。 tasks
サブディレクトリ を参照して、 invoke
library 。それらのポリシーと根拠については、pipプロジェクト vendoring README を参照してください(これらの中で最も重要なのは、pip
がbootstrapそれ自体、たとえば、何でもインストールできるように依存関係を利用できるようにします)。
他のオプションは使用しないでください。 #2と#3の問題はすでに列挙されています。
カスタムインポーターを使用したオプション#4の問題は、インポートを書き換える必要があることです。言い換えると、setuptools
で使用されるカスタムインポーターフックは、ベンダー化された名前空間の問題をまったく解決しません。代わりに、ベンダー化されたパッケージが欠落している場合、トップレベルパッケージを動的にインポートすることができます(問題 pip
はmanual分離プロセス )で解決します。 setuptools
は実際にはオプション#1を使用し、ベンダー化されたパッケージのソースコードを書き換えます。たとえば、 packaging
ベンダーサブパッケージのsetuptools
プロジェクト のこれらの行を参照してください。 _setuptools.extern
_名前空間はカスタムインポートフックによって処理され、ベンダー化されたパッケージからのインポートが失敗した場合、_setuptools._vendor
_または最上位の名前にリダイレクトされます。
ベンダーパッケージを更新するpip
自動化は、次の手順を実行します。
_vendor/
_ファイル、および要件のテキストファイルを除く、___init__.py
_サブディレクトリ内のすべての削除します。pip
を使用して、_vendor.txt
_という名前の専用要件ファイルを使用し、_.pyc
_バイトキャッシュファイルのコンパイルを回避して一時的な依存関係を無視して、これらのディレクトリにベンダー依存関係をすべてインストールします(これらは、 _vendor.txt
_すでに);使用されるコマンドは_pip install -t pip/_vendor -r pip/_vendor/vendor.txt --no-compile --no-deps
_です。pip
によってインストールされたがベンダー環境では不要なすべてのもの、つまり_*.dist-info
_、_*.Egg-info
_、bin
ディレクトリ、およびインストールされている依存関係からのいくつかの削除pip
は使用しません。.py
_拡張子を収集します(ホワイトリストにないもの)。これは_vendored_libs
_リストです。vendored_lists
_内のすべての名前を使用して_import <name>
_を_import pip._vendor.<name>
_に置き換え、すべてのfrom <name>(.*) import
をfrom pip._vendor.<name>(.*) import
に置き換えます。pip
パッチのみがrequests
のパッチであり、requests
ライブラリの下位互換性が更新されます。 requests
ライブラリが削除したベンダーパッケージのレイヤー。このパッチはかなりメタです!したがって、本質的には、pip
アプローチの最も重要な部分であるベンダーパッケージのインポートの書き換えは非常に簡単です。ロジックを簡略化し、pip
固有の部分を削除するために言い換えると、それは単に次のプロセスです。
_import shutil
import subprocess
import re
from functools import partial
from itertools import chain
from pathlib import Path
WHITELIST = {'README.txt', '__init__.py', 'vendor.txt'}
def delete_all(*paths, whitelist=frozenset()):
for item in paths:
if item.is_dir():
shutil.rmtree(item, ignore_errors=True)
Elif item.is_file() and item.name not in whitelist:
item.unlink()
def iter_subtree(path):
"""Recursively yield all files in a subtree, depth-first"""
if not path.is_dir():
if path.is_file():
yield path
return
for item in path.iterdir():
if item.is_dir():
yield from iter_subtree(item)
Elif item.is_file():
yield item
def patch_vendor_imports(file, replacements):
text = file.read_text('utf8')
for replacement in replacements:
text = replacement(text)
file.write_text(text, 'utf8')
def find_vendored_libs(vendor_dir, whitelist):
vendored_libs = []
paths = []
for item in vendor_dir.iterdir():
if item.is_dir():
vendored_libs.append(item.name)
Elif item.is_file() and item.name not in whitelist:
vendored_libs.append(item.stem) # without extension
else: # not a dir or a file not in the whilelist
continue
paths.append(item)
return vendored_libs, paths
def vendor(vendor_dir):
# target package is <parent>.<vendor_dir>; foo/_vendor -> foo._vendor
pkgname = f'{vendor_dir.parent.name}.{vendor_dir.name}'
# remove everything
delete_all(*vendor_dir.iterdir(), whitelist=WHITELIST)
# install with pip
subprocess.run([
'pip', 'install', '-t', str(vendor_dir),
'-r', str(vendor_dir / 'vendor.txt'),
'--no-compile', '--no-deps'
])
# delete stuff that's not needed
delete_all(
*vendor_dir.glob('*.dist-info'),
*vendor_dir.glob('*.Egg-info'),
vendor_dir / 'bin')
vendored_libs, paths = find_vendored_libs(vendor_dir, WHITELIST)
replacements = []
for lib in vendored_libs:
replacements += (
partial( # import bar -> import foo._vendor.bar
re.compile(r'(^\s*)import {}\n'.format(lib), flags=re.M).sub,
r'\1from {} import {}\n'.format(pkgname, lib)
),
partial( # from bar -> from foo._vendor.bar
re.compile(r'(^\s*)from {}(\.|\s+)'.format(lib), flags=re.M).sub,
r'\1from {}.{}\2'.format(pkgname, lib)
),
)
for file in chain.from_iterable(map(iter_subtree, paths)):
patch_vendor_imports(file, replacements)
if __name__ == '__main__':
# this assumes this is a script in foo next to foo/_vendor
here = Path('__file__').resolve().parent
vendor_dir = here / 'foo' / '_vendor'
assert (vendor_dir / 'vendor.txt').exists(), '_vendor/vendor.txt file not found'
assert (vendor_dir / '__init__.py').exists(), '_vendor/__init__.py file not found'
vendor(vendor_dir)
_
anki_addons
フォルダーをパッケージにして、必要なライブラリーをメインパッケージフォルダーの__init__.py
にインポートしてみませんか。
だからそれは次のようなものになるでしょう
anki/
__init__.py
anki.__init__.py
内:
from anki_addons import library1
anki.anki_addons.__init__.py
内:
from addon_name_1 import *
私はこれが初めてなので、ここで我慢してください。
依存関係をバンドルする最良の方法は、virtualenv
を使用することです。 Anki
プロジェクトは、少なくとも1つのプロジェクト内にインストールできる必要があります。
あなたが求めているのはnamespace packages
だと思います。
https://packaging.python.org/guides/packaging-namespace-packages/
Ankiのメインプロジェクトにはsetup.py
があり、すべてのアドオンには独自のsetup.py
があり、独自のソースディストリビューションからインストールできると思います。その後、アドオンは依存関係を独自のsetup.py
にリストでき、pipはそれらをsite-packages
にインストールします。
名前空間パッケージは問題の一部のみを解決し、あなたが言ったようにanki_addonsフォルダーからアドオンをインポートする方法を制御することはできません。アドオンのインポート方法の設計とそれらのパッケージ化は密接に関連していると思います。
pkgutil
モジュールは、メインプロジェクトがインストールされたアドオンを発見する方法を提供します。 https://packaging.python.org/guides/creating-and-discovering-plugins/
これを広範囲に使用するプロジェクトはZopeです。 http://www.zope.org
ここを見てください: https://github.com/zopefoundation/zope.interface/blob/master/setup.py