前の関連記事:LibreOffice5(119)コンテナウィンドウとコンポーネントウィンドウのサービスとインターフェイスの比較
(2018.9.12追記。撤廃メソッドを置換しました。LibreOffice5(156)ドキュメントに埋め込んだモジュールをインポートする 撤廃メソッドの排除参照。)vnd.sun.star.tdocプロトコールからのimportを有効にするtdocimport.py
tdocimport.py
import sys import importlib.abc from types import ModuleType def _get_links(simplefileaccess, url): # url内のファイル名とフォルダ名のリストを返す関数。 foldercontents = simplefileaccess.getFolderContents(url, True) # url内のファイルとフォルダをすべて取得。フルパスで返ってくる。 tdocpath = "".join((url, "/")) # 除去するパスの部分を作成。 return [content.replace(tdocpath, "") for content in foldercontents] # ファイル名かフォルダ名だけのリストにして返す。 class UrlMetaFinder(importlib.abc.MetaPathFinder): # meta path finderの実装。 def __init__(self, simplefileaccess, baseurl): self._simplefileaccess = simplefileaccess # LibreOfficeドキュメント内のファイルにアクセスするためのsimplefileaccess self._baseurl = baseurl # モジュールを探すパス self._links = {} # baseurl内のファイル名とフォルダ名のリストのキャッシュにする辞書。。 self._loaders = {baseurl: UrlModuleLoader(simplefileaccess, baseurl)} # ローダーのキャッシュにする辞書。 def find_module(self, fullname, path=None): # find_moduleはPython3.4で撤廃だが、find_spec()にしてもそのままではうまく動かない。 if path is None: baseurl = self._baseurl else: if not path[0].startswith(self._baseurl): return None baseurl = path[0] parts = fullname.split('.') basename = parts[-1] if basename not in self._links: # Check link cache self._links[baseurl] = _get_links(self._simplefileaccess, baseurl) if basename in self._links[baseurl]: # Check if it's a package。パッケージの時。 fullurl = "/".join((self._baseurl, basename)) loader = UrlPackageLoader(self._simplefileaccess, fullurl) try: # Attempt to load the package (which accesses __init__.py) loader.load_module(fullname) self._links[fullurl] = _get_links(self._simplefileaccess, fullurl) self._loaders[fullurl] = UrlModuleLoader(self._simplefileaccess, fullurl) except ImportError: loader = None return loader filename = "".join((basename, '.py')) if filename in self._links[baseurl]: # A normal module return self._loaders[baseurl] else: return None def invalidate_caches(self): self._links.clear() class UrlModuleLoader(importlib.abc.SourceLoader): # Module Loader for a URL def __init__(self, simplefileaccess, baseurl): self._simplefileaccess = simplefileaccess # LibreOfficeドキュメント内のファイルにアクセスするためのsimplefileaccess self._baseurl = baseurl # モジュールを探すパス self._source_cache = {} # ソースのキャッシュの辞書。 def module_repr(self, module): # モジュールを表す文字列を返す。 return '<urlmodule {} from {}>'.format(module.__name__, module.__file__) def load_module(self, fullname): # Required method。引数はimport文で使うフルネーム。 code = self.get_code(fullname) # モジュールのコードオブジェクトを取得。 mod = sys.modules.setdefault(fullname, ModuleType(fullname)) # 辞書sys.modulesにキーfullnameなければ値を代入して値を取得。 mod.__file__ = self.get_filename(fullname) # ソースファイルへのフルパスを取得。 mod.__loader__ = self # ローダーを取得。 mod.__package__ = fullname.rpartition('.')[0] # パッケージ名を取得。.区切りがないときは空文字が入る。 exec(code, mod.__dict__) # コードオブジェクトを実行する。 return mod # モジュールオブジェクトを返す。 def get_code(self, fullname): # モジュールのコードオブジェクトを返す。Optional extensions。引数はimport文で使うフルネーム。 src = self.get_source(fullname) return compile(src, self.get_filename(fullname), 'exec') def get_data(self, path): # バイナリ文字列を返す。 pass def get_filename(self, fullname): # ソースファイルへのフルパスを返す。引数はimport文で使うフルネーム。 return "".join((self._baseurl, '/', fullname.split('.')[-1], '.py')) def get_source(self, fullname): # モジュールのソースをテキストで返す。 filename = self.get_filename(fullname) # ソースファイルへのフルパス。 if filename in self._source_cache: # すでにキャッシュがあればそれを返して終わる。 return self._source_cache[filename] try: inputstream = self._simplefileaccess.openFileRead(filename) # ソースファイルのインプットストリームを取得。 dummy, b = inputstream.readBytes([], inputstream.available()) # simplefileaccess.getSize(module_tdocurl)は0が返る。 source = bytes(b).decode("utf-8") # モジュールのソースファイルをutf-8のテキストで取得。 self._source_cache[filename] = source # ソースをキャッシュに取得。 return source # ソースのテキストを返す。 except: raise ImportError("Can't load {}".format(filename)) def is_package(self, fullname): # パッケージの時はTrueを返す。 return False class UrlPackageLoader(UrlModuleLoader): # Package loader for a URL def load_module(self, fullname): # fullnameはパッケージのときはフォルダ名に該当する。 mod = super().load_module(fullname) # __init__.pyを実行する。 mod.__path__ = [self._baseurl] # パッケージ内の検索パスを指定する文字列のリスト mod.__package__ = fullname # フォルダ名を入れる。 def get_filename(self, fullname): # パッケージの__init__.pyを返す。 return "/".join((self._baseurl, '__init__.py')) def is_package(self, fullname): # パッケージの時はTrueを返す。 return True _installed_meta_cache = {} # meta path finderを入れておくグローバル辞書。重複を防ぐ目的。 def install_meta(simplefileaccess, address): # Utility functions for installing the loader if address not in _installed_meta_cache: # グローバル辞書にないパスの時。 finder = UrlMetaFinder(simplefileaccess, address) # meta path finder。モジュールを探すクラスをインスタンス化。 _installed_meta_cache[address] = finder # グローバル辞書にmeta path finderを登録。 sys.meta_path.append(finder) # meta path finderをsys.meta_pathに登録。 def remove_meta(address): # Utility functions for uninstalling the loader if address in _installed_meta_cache: finder = _installed_meta_cache.pop(address) sys.meta_path.remove(finder)Python Cookbookの10.11.loading_modules_from_a_remote_machine_using_import_hooksのurlimport.pyを改変したものです。
ドキュメントに埋め込んだファイルを読み込むために、20行目でまずSimpleFileAccessのインスタンスを受け取らないといけません。
Python3.3から3.5にかけてimport文周りのモジュールの変更があったようで、tdocimport.pyで使っているメソッドにはdeprecatedになっているものがいくつかあります。
14行目のimportlib.abc.MetaPathFinderのfind_module()メソッドはPython3.4でfind_spec()メソッドに、49行目のimportlib.abc.SourceLoaderのload_module()メソッドはPython3.4でexec_module()メソッドに、置換することが推奨されていますが、どうもうまくいきませんでした。
pydevdでうまくブレークできないところが多いので理解がとても難しいです。
とくにパッケージのインポートの処理がうまくできませんでした。
とりあえずdeprecatedのメソッドを使ったままLibreOffice5.4では動きました。
tdocimport.pyを使った埋め込みマクロ
tdocimport.ods
tdocimport.pyを使った埋め込みマクロを埋め込んだCalcドキュメントの例です。
└── Scripts └── python ├── embeddedmacro.py └── pythonpath ├── consts.py ├── subdir │ ├── __init__.py │ ├── consts2.py │ └── submod.py └── tdocimport.pyドキュメント内のScripts/pythonフォルダの構造はこのようになっています。
マクロを有効化してtdocimport.odsを開いて、マクロセレクターでembeddedmacroのmacroを実行するとA1セルとA4セルに文字列が入力されます。
pythonpathフォルダはマクロセレクターからはみえません。
embeddedmacro.py
import unohelper # オートメーションには必須(必須なのはuno)。 import sys from types import ModuleType global XSCRIPTCONTEXT # PyDevのエラー抑制用。 def macro(documentevent=None): # 引数は文書のイベント駆動用。 doc = XSCRIPTCONTEXT.getDocument() if documentevent is None else documentevent.Source # ドキュメントのモデルを取得。 controller = doc.getCurrentController() # コントローラの取得。 sheet = controller.getActiveSheet() ctx = XSCRIPTCONTEXT.getComponentContext() # コンポーネントコンテクストの取得。 smgr = ctx.getServiceManager() # サービスマネージャーの取得。 simplefileaccess = smgr.createInstanceWithContext("com.sun.star.ucb.SimpleFileAccess", ctx) # SimpleFileAccess modulefolderpath = getModuleFolderPath(ctx, smgr, doc) # 埋め込みpythonpathフォルダのパスを取得。 tdocimport = load_module(simplefileaccess, "/".join((modulefolderpath, "tdocimport.py"))) # import hooks tdocimport.install_meta(simplefileaccess, modulefolderpath) import consts from subdir import submod sheet["A1"].setString(consts.LISTSHEET["name"]) submod.fromsubmod(sheet) tdocimport.remove_meta(modulefolderpath) def load_module(simplefileaccess, modulepath): inputstream = simplefileaccess.openFileRead(modulepath) dummy, b = inputstream.readBytes([], inputstream.available()) # simplefileaccess.getSize(module_tdocurl)は0が返る。 source = bytes(b).decode("utf-8") # モジュールのソースをテキストで取得。 mod = sys.modules.setdefault(modulepath, ModuleType(modulepath)) # 新規モジュールをsys.modulesに挿入。 code = compile(source, modulepath, 'exec') # urlを呼び出し元としてソースコードをコンパイルする。 mod.__file__ = modulepath # モジュールの__file__を設定。 mod.__package__ = '' # モジュールの__package__を設定。 exec(code, mod.__dict__) # モジュールの名前空間を設定する。 return mod def getModuleFolderPath(ctx, smgr, doc): transientdocumentsdocumentcontentfactory = smgr.createInstanceWithContext("com.sun.star.frame.TransientDocumentsDocumentContentFactory", ctx) transientdocumentsdocumentcontent = transientdocumentsdocumentcontentfactory.createDocumentContent(doc) tdocurl = transientdocumentsdocumentcontent.getIdentifier().getContentIdentifier() # ex. vnd.sun.star.tdoc:/1 return "/".join((tdocurl, "Scripts/python/pythonpath")) # 開いているドキュメント内の埋め込みマクロフォルダへのパス。 g_exportedScripts = macro, #マクロセレクターに限定表示させる関数をタプルで指定。embeddedmacro.pyのmacro()を実行すると、13行目でLibreOffice5(116)埋め込みマクロからPythonモジュールをロードする方法の方法で、pythonpath/tdocimport.pyを読み込んでいます。
次の行でドキュメント内のpythonpathフォルダにあるモジュールをimportできるように設定しています。
そこでSimpleFileAccessのインスタンスとドキュメント内のpythonpathフォルダへのパスを渡しています。
16行目はimport subdir.submodでも呼び込めます。
いずれにせよsubdirには__init__.pyファイルがないとエラーになります(つまり名前空間パッケージには未対応)。
submod.pyからは相対インポートでconsts2.pyをインポートしています。
読み込んだモジュールではグローバル変数のXSCRIPTCONTEXTは使えないのでモジュールに渡しておかないといけません。
埋め込みモジュールを編集した後はLibreOfficeをいったん終了させる
Calc(47)埋め込みマクロのためのPyDevプロジェクトを使って動作確認をしていましたが、replaceEmbeddedScripts.pyでドキュメントを開き直すだけでは、インポートしたモジュールのキャッシュが使われていることに気が付きました。
モジュールの作成をしている間の動作確認はLibreOfficeのプロセスを終了してあることを確認してから、開き直さないと、編集前のモジュールが使われている場合があります。
(2018.9.11追記。ドキュメントに埋め込んだとしてもこの方法でインポートしたモジュールはドキュメントごとに区別されているわけではないので、pythonpath以下のパスが同じ埋め込みマクロをもつドキュメントを複数開くと(おそらく)最初に開いたドキュメントのマクロが呼ばれてしまいます。なので、同じパスの埋め込みマクロがあるドキュメントは同時に開かないように気をつけないといけません。)
参考にしたサイト
5. インポートシステム — Python 3.6.4 ドキュメント
カスタムインポートの解説がありますが、理解は難しいです。
Python を支える技術: モジュール・ インポートシステム編 / python-module-import-system - SSSSLIDE
Pythonのインポートシステムの変化の履歴がわかりやすく解説してあるスライドです。
0 件のコメント:
コメントを投稿