Структура проекта Python 3.6 приводит к RuntimeWarning

Я пытаюсь упаковать свой проект для распространения, но я RuntimeWarning когда запускаю модуль.

Я нашел отчет об ошибке в списке рассылки Python, который указывает, что RuntimeWarning является новым поведением, которое было введено в Python 3.5.2.

Прочитав отчет об ошибке, кажется, что происходит двойной импорт, и это RuntimeWarning правильное в том, что RuntimeWarning предупреждения пользователя. Однако я не вижу, какие изменения мне необходимо внести в свою структуру проекта, чтобы избежать этой проблемы.

Это первый проект, который я попытался структурировать "правильно". Я хотел бы иметь аккуратный макет, когда я нажимаю код и структуру проекта, которая может быть клонирована и легко запускаться другими.

Я основывал свою структуру главным образом на http://docs.python-guide.org/en/latest/writing/structure/.

Я добавил детали минимального рабочего примера ниже.

Чтобы реплицировать проблему, я запускаю основной файл с помощью python -m:

(py36) X:\test_proj>python -m proj.proj
C:\Users\Matthew\Anaconda\envs\py36\lib\runpy.py:125: RuntimeWarning: 
'proj.proj' found in sys.modules after import of package 'proj', but prior 
to execution of 'proj.proj'; this may result in unpredictable behaviour
  warn(RuntimeWarning(msg))
This is a test project.'

Выполнение моих тестов в порядке:

(py36) X:\test_proj>python -m unittest tests.test_proj
This is a test project.
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Структура проекта для репликации проблемы выглядит следующим образом:

myproject/
    proj/
        __init__.py
        proj.py
    tests/
        __init__.py
        context.py
        test_proj.py

В файле proj/proj.py:

def main():
    print('This is a test project.')
    raise ValueError

if __name__ == '__main__':
    main()

В proj/__init__.py:

from .proj import main

В tests/context.py:

import os
import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
import proj

Наконец, в tests/test_proj.py:

import unittest

from .context import proj


class SampleTestCase(unittest.TestCase):
    """Test case for this sample project"""
    def test_raise_error(self):
        """Test that we correctly raise an error."""
        with self.assertRaises(ValueError):
            proj.main()


if __name__ == '__main__':
    unittest.main()

Может ли кто-нибудь помочь мне исправить мою структуру проекта, чтобы избежать этого сценария двойного импорта? Любая помощь с этим была бы весьма признательна.

Ответ 1

В этом конкретном случае предупреждение двойного импорта происходит из-за этой строки в proj/__init__.py:

from .proj import main

Что означает эта строка, так это то, что к тому моменту, -m реализация переключателя -m завершает шаг import proj, proj.proj уже импортирован как побочный эффект импорта родительского пакета.

Избегание предупреждения

Чтобы избежать предупреждения, вам нужно найти способ гарантировать, что импорт родительского пакета не подразумевает импорт пакета, выполняемого с -m переключателя -m.

Два основных варианта решения этой проблемы:

  1. Отбросьте from.proj import main линии from.proj import main (как предложил @John Moutafis), предполагая, что это можно сделать без нарушения совместимости API; или
  2. Удалите, if __name__ == "__main__": блок из proj субмодуля и заменить его на отдельный proj/__main__.py файл, который просто делает:

    from .proj import main
    main()
    

Если вы перейдете с опцией 2, то вызов командной строки также изменится, чтобы быть просто python -m proj, а не ссылаться на подмодуль.

Более обратный вариант варианта 2 заключается в том, чтобы добавить __main__.py не удаляя блок CLI из текущего подмодуля, и это может быть особенно хорошим подходом в сочетании с DeprecationWarning:

if __name__ == "__main__":
    import warnings
    warnings.warn("use 'python -m proj', not 'python -m proj.proj'", DeprecationWarning)
    main()

Если proj/__main__.py уже используется для каких-то других целей, вы также можете делать такие вещи, как замена python -m proj.proj на python -m proj.proj_cli, где proj/proj_cli.py выглядит так:

if __name__ != "__main__":
    raise RuntimeError("Only for use with the -m switch, not as a Python API")
from .proj import main
main()

Почему существует предупреждение?

Это предупреждение -m когда реализация коммутатора -m собирается снова запустить и уже импортированный код модуля в модуле __main__, что означает, что у вас будет две разные копии всего, что он определяет, - классы, функции, контейнеры и т.д.

В зависимости от специфики приложения это может работать нормально (именно поэтому это предупреждение, а не ошибка), или это может привести к причудливому поведению, например, изменения состояния уровня модуля, которые не будут разделены, как ожидалось, или даже исключения, которые не были пойманы, обработчик исключений пытался поймать тип исключения из одного экземпляра модуля, в то время как возникшее исключение использовало тип из другого экземпляра.

Следовательно, неопределенное this may cause unpredictable behaviour предупреждение о this may cause unpredictable behaviour - если что-то пойдет не так, как результат выполнения кода верхнего уровня модуля дважды, симптомы могут быть почти что угодно.

Как вы можете отлаживать более сложные случаи?

Хотя в этом конкретном примере импорт побочных эффектов находится непосредственно в proj/__init__.py, там гораздо более тонкий и трудно отлаживаемый вариант, где вместо этого выполняется родительский пакет:

import some_other_module

а затем это some_other_module (или модуль, который он импортирует), который делает:

import proj.proj # or "from proj import proj"

Предполагая, что неправильное поведение воспроизводимо, основным способом отладки этих проблем является запуск python в подробном режиме и проверка последовательности импорта:

$ python -v -c "print('Hello')" 2>&1 | grep '^import'
import zipimport # builtin
import site # precompiled from /usr/lib64/python2.7/site.pyc
import os # precompiled from /usr/lib64/python2.7/os.pyc
import errno # builtin
import posix # builtin
import posixpath # precompiled from /usr/lib64/python2.7/posixpath.pyc
import stat # precompiled from /usr/lib64/python2.7/stat.pyc
import genericpath # precompiled from /usr/lib64/python2.7/genericpath.pyc
import warnings # precompiled from /usr/lib64/python2.7/warnings.pyc
import linecache # precompiled from /usr/lib64/python2.7/linecache.pyc
import types # precompiled from /usr/lib64/python2.7/types.pyc
import UserDict # precompiled from /usr/lib64/python2.7/UserDict.pyc
import _abcoll # precompiled from /usr/lib64/python2.7/_abcoll.pyc
import abc # precompiled from /usr/lib64/python2.7/abc.pyc
import _weakrefset # precompiled from /usr/lib64/python2.7/_weakrefset.pyc
import _weakref # builtin
import copy_reg # precompiled from /usr/lib64/python2.7/copy_reg.pyc
import traceback # precompiled from /usr/lib64/python2.7/traceback.pyc
import sysconfig # precompiled from /usr/lib64/python2.7/sysconfig.pyc
import re # precompiled from /usr/lib64/python2.7/re.pyc
import sre_compile # precompiled from /usr/lib64/python2.7/sre_compile.pyc
import _sre # builtin
import sre_parse # precompiled from /usr/lib64/python2.7/sre_parse.pyc
import sre_constants # precompiled from /usr/lib64/python2.7/sre_constants.pyc
import _locale # dynamically loaded from /usr/lib64/python2.7/lib-dynload/_localemodule.so
import _sysconfigdata # precompiled from /usr/lib64/python2.7/_sysconfigdata.pyc
import abrt_exception_handler # precompiled from /usr/lib64/python2.7/site-packages/abrt_exception_handler.pyc
import encodings # directory /usr/lib64/python2.7/encodings
import encodings # precompiled from /usr/lib64/python2.7/encodings/__init__.pyc
import codecs # precompiled from /usr/lib64/python2.7/codecs.pyc
import _codecs # builtin
import encodings.aliases # precompiled from /usr/lib64/python2.7/encodings/aliases.pyc
import encodings.utf_8 # precompiled from /usr/lib64/python2.7/encodings/utf_8.pyc

В этом конкретном примере показан только базовый набор импорта, который Python 2.7 на Fedora делает при запуске. При отладке двойного импорта RuntimeWarning как в этом вопросе, вы будете искать строки "import proj", а затем "import proj.proj" в подробном выводе, а затем внимательно посмотрите на импорт, непосредственно предшествующий " import proj.proj ".

Ответ 2

Если вы посмотрите на двойную ловушку импорта, вы увидите следующее:

Эта следующая ловушка существует во всех текущих версиях Python, включая 3.3, и может быть суммирована в следующем общем руководстве: "Никогда не добавляйте каталог пакета или любой каталог внутри пакета непосредственно к пути Python".

Причина этого в том, что каждый модуль в этом каталоге теперь потенциально доступен под двумя разными именами: как модуль верхнего уровня (поскольку каталог находится на sys.path) и как подмодуль пакета (если каталог более высокого уровня содержит сам пакет также находится на sys.path).

В tests/context.py

remove: sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

что, вероятно, вызывает проблему, и ваш код по-прежнему работает должным образом.


Изменить из-за комментария:

Вы можете попробовать и изменить некоторые части кода:

  1. proj/__init__.py Может быть полностью пустым
  2. На test_proj.py следует изменить импорт следующим образом:

    import unittest
    
    from proj import proj
    

PS: Я не смог воспроизвести предупреждение в Linux с вашим исходным кодом или с моими предложениями.

Ответ 3

python -m немного сложнее. @ncoghlan уже предоставили подробную информацию. когда мы пытаемся запустить с помощью python -m, по умолчанию все пакеты в sys.path/pythonpath импортируются. если в вашем пакете есть оператор импорта во все каталоги в PATH, это предупреждение появляется. See the Pic

У моего PYTHONPATH уже есть каталог Project. Таким образом, когда я это делаю

from reader.reader import Reader

Система выдает предупреждение. Таким образом, нет необходимости иметь явный импорт, если путь находится в пути python

Ответ 4

Если вы уверены, что предупреждение не относится к вам, легкий способ избежать этого - игнорировать RuntimeWarnings с runpy модуля runpy который реализует логику, стоящую за переключателем -m:

import sys
import warnings

if not sys.warnoptions:  # allow overriding with '-W' option
    warnings.filterwarnings('ignore', category=RuntimeWarning, module='runpy')

Очевидно, это может скрыть соответствующие предупреждения, но по крайней мере на данный момент это единственное RuntimeWarning, runpy использует runpy. В качестве альтернативы, фильтрацию можно сделать более строгим, указав шаблон для сообщения или номера строки, где должно появляться предупреждение, но обе они могут быть повреждены, если runpy отредактирован позже.