Как отследить относительный импорт Python? - PullRequest
1 голос
/ 26 июня 2019

Следующее поведение python выглядит для меня как ошибка:

>>> from curses import textpad
>>> from . import textpad # <-- expected to fail?
>>> from . import ascii
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: cannot import name 'ascii'
>>> from curses import ascii
>>> from . import textpad
>>> from . import ascii
>>>

(протестировано на conda-forge python 3.6.7)

В общем, есть ли способ отследить илиотладить процесс импорта python, чтобы понять, где находится cpython и не будет искать модули (и почему)?Особенно когда речь идет об относительном импорте, подпакетах, сценариях внутри пакетов и различных способах их вызова (например, находится ли текущий рабочий каталог внутри или снаружи пакета)?

1 Ответ

0 голосов
/ 26 июня 2019

Такое поведение является ошибкой в ​​cpython.

Каждый оператор python import транслируется в один или несколько вызовов встроенной функции __import__ python. (Это задокументировано и может быть перехвачено.)

В cpython есть две реализации __import__: есть реализация Python reference (в стандартной библиотеке importlib), и есть реализация C (к которой можно получить доступ или перехватить через builtins (стандартная библиотека), которая вызывается по умолчанию.

Вот скрипт, который исследует проблему (примечание curses.ascii и curses.textpad - некоторые модули в стандартной библиотеке Python):

commands = ['from curses import ascii', 
            'from . import ascii', 
            'from . import textpad']

def mock(name, globals=None, locals=None, fromlist=(), level=0):
    print('    __import__ :', repr(name), ':', fromlist, ':', level)
    return alternate(name, globals, locals, fromlist, level)

import builtins
import importlib._bootstrap
original = builtins.__import__
builtins.__import__ = mock

for implementation in ['original', 'importlib._bootstrap.__import__']:
    print(implementation.upper(), '\n')
    alternate = eval(implementation)
    try:    
        for command in commands:
            print(command)
            exec(command)
    except ImportError as err:
        print('   ', repr(err), '\n\n')

Вывод демонстрирует, что встроенный в cpython, в отличие от эталонной реализации, не может проверить родительский пакет перед попыткой относительного импорта:

ORIGINAL 

from curses import ascii
    __import__ : 'curses' : ('ascii',) : 0
    __import__ : '_curses' : ('*',) : 0
    __import__ : 'os' : None : 0
    __import__ : 'sys' : None : 0
from . import ascii
    __import__ : '' : ('ascii',) : 1
from . import textpad
    __import__ : '' : ('textpad',) : 1
    ImportError("cannot import name 'textpad'",) 


IMPORTLIB._BOOTSTRAP.__IMPORT__ 

from curses import ascii
    __import__ : 'curses' : ('ascii',) : 0
from . import ascii
    __import__ : '' : ('ascii',) : 1
    ImportError('attempted relative import with no known parent package',) 

В cpython оператор from [...][X] import Y [as Z] преобразуется в две главные инструкции байт-кода (плюс некоторые служебные инструкции для соответствующей загрузки и сохранения между стеком и списками констант / переменных):

  1. IMPORT_NAME: выполняется вызов builtins.__import__. Аргументами вызова являются аргумент инструкции (имя X возвращаемого модуля), некоторое текущее состояние кадра интерпретатора (globals() и locals()) и два элемента, извлеченные из стека (список Y который может содержать субмодули для импорта, и относительный уровень, то есть число [...]). Ожидается, что вызов вернет объект модуля, который помещен в стек.
  2. IMPORT_FROM: Это проверяет модуль на вершине стека и получает от его атрибута Y объект (который он также оставляет в стеке).

(Они документированы вместе с библиотекой dis и реализованы в ceval.c.)

Если мы попытаемся from . import foo (т. Е. X пусто, а уровень равен 1), тогда IMPORT_NAME попытается вернуть объект модуля для текущего родительского пакета (например, имя, которое указано глобальным __package__) , Если у него нет атрибута с именем foo, то IMPORT_FROM поднимает ImportError.

В интерактивной оболочке интерпретатора или в простом скрипте __package__ равен None. При таких обстоятельствах:

  • importlib.__import__ вызвало бы ImportError (попытка относительного импорта без известного родительского пакета), но
  • builtins.__import__ возвращает модуль __main__ (встроенный), представляющий собой среду сценариев верхнего уровня python.

Это ключевое отличие. Поскольку все глобальные переменные являются атрибутами модуля __main__, это неправильное поведение приводит к:

>>> foo = 'oops'
>>> from . import foo as fubar
>>> fubar
'oops'

Существует также еще одно неправильное поведение: при попытке выполнить более глубокий уровень относительного импорта (помимо пакета верхнего уровня, например, from ..... import foo), тогда builtins.__import__ поднимает ValueError (вместо ожидаемого ImportError).

...