小一時間悩んだので備忘録として残しておきます。
結論
ちゃんと環境でサポートされているバージョンを使いましょう。(numpy
なら 1.6.1
)
https://cloud.google.com/appengine/docs/python/tools/built-in-libraries-27
あと dev_appserver.py
の仕組みに少し詳しくなれました。
現象
サポートされているバージョンが 1.6.1
なのはさておき、Docker で構築中の GAE python の開発環境で numpy
を 1.9.1
から 1.10.1
に上げたら以下のエラーが出るようになって困りました。
File "/usr/local/google_appengine/google/appengine/tools/devappserver2/python/sandbox.py", line 705, in load_module
module = self._find_and_load_module(fullname, fullname, [module_path])
File "/usr/local/google_appengine/google/appengine/tools/devappserver2/python/sandbox.py", line 446, in _find_and_load_module
return imp.load_module(fullname, source_file, path_name, description)
File "/root/.pyenv/versions/2.7.12/lib/python2.7/site-packages/numpy/__init__.py", line 180, in <module>
from . import add_newdocs
...
File "/root/.pyenv/versions/2.7.12/lib/python2.7/site-packages/numpy/core/_internal.py", line 14, in <module>
import ctypes
File "/root/.pyenv/versions/2.7.12/lib/python2.7/ctypes/__init__.py", line 7, in <module>
from _ctypes import Union, Structure, Array
File "/usr/local/google_appengine/google/appengine/tools/devappserver2/python/sandbox.py", line 964, in load_module
raise ImportError('No module named %s' % fullname)
ImportError: No module named _ctypes
原因
GAE python では主にセキュリティ的な理由で C
で書かれた python module の大部分は利用することができません。
local 開発環境で利用する dev_appserver.py
でもこの制限を模擬するために(そうでないと local では動くのにデプロイしたら動かなくなる謎現象に悩まされるので)、上のエラーにも出てくる sandbox.py
というモジュールが使われています。(このモジュール以外にもこういった制限を模擬するものがきっとあると思いますがそこまでは調べていないです)
具体的には sandbox.py
の以下のコードで import
が呼ばれた場合の hook を登録しています。
${GAE_HOME}/google/appengine/tools/devappserver2/python/sandbox.py
def _install_import_hooks(config, path_override_hook):
"""Install runtime's import hooks.
These hooks customize the import process as per
https://docs.python.org/2/library/sys.html#sys.meta_path .
Args:
config: An apphosting/tools/devappserver2/runtime_config.proto
for this instance.
path_override_hook: A hook for importing special appengine
versions of select libraries from the libraries
section of the current module's app.yaml file.
"""
if not config.vm:
enabled_library_regexes = [
NAME_TO_CMODULE_WHITELIST_REGEX[lib.name] for lib in config.libraries
if lib.name in NAME_TO_CMODULE_WHITELIST_REGEX]
sys.meta_path = [
StubModuleImportHook(),
ModuleOverrideImportHook(_MODULE_OVERRIDE_POLICIES),
CModuleImportHook(enabled_library_regexes),
path_override_hook,
PyCryptoRandomImportHook,
PathRestrictingImportHook(enabled_library_regexes)]
else:
...
sys.meta_path
というのがこの hook を登録する場所にあたり、
PEP 302 -- New Import Hooks で定義された find_module(fullname, path=None)
というメソッドを持つオブジェクトが登録されます。
この find_module
で module
が発見された場合には loader
が返され、その load_module(fullname)
が呼ばれることで import
が行われます。
注目すべきは CModuleImportHook
で sandbox.py
をさらに見ていくとクラス定義を見つけることができます。
class CModuleImportHook(object):
"""An import hook implementing a C module (builtin or extensions) whitelist.
CModuleImportHook implements the PEP 302 finder protocol where it returns
itself as a loader for any builtin module that isn't whitelisted or part of an
enabled third-party library. The loader implementation always raises
ImportError.
"""
def __init__(self, enabled_regexes):
self._enabled_regexes = enabled_regexes
@staticmethod
def _module_type(fullname, path):
_, _, submodule_name = fullname.rpartition('.')
try:
f, _, description = imp.find_module(submodule_name, path)
_, _, file_type = description
except ImportError:
return None
if f:
f.close()
return file_type
def find_module(self, fullname, path=None):
if (fullname in _WHITE_LIST_C_MODULES or
any(regex.match(fullname) for regex in self._enabled_regexes)):
return None
if self._module_type(fullname, path) in [imp.C_EXTENSION, imp.C_BUILTIN]:
return self
return None
def load_module(self, fullname):
raise ImportError('No module named %s' % fullname)
このクラスは find_module
と load_module
の 2 つのメソッドを持っています。 find_module
で C
の native module だと判定すると self
が返り、 self.load_module()
が必ず ImportError
を raise
するため import
に失敗していました。
※_WHITE_LIST_C_MODULES
など幾つか例外は登録されていますが _ctypes
は例外扱いされていません
numpy
の 1.9.1
は ctypes
を import する際に ImportError
が出るとフォールバックしてくれていたのですが、 1.10.1
に挙げるとその挙動が無くなりこけるようになったという次第でした。