Проблема с проверкой с использованием PyInstaller; может получить источник класса, но не функцию - PullRequest
0 голосов
/ 23 апреля 2020

Мой первый пост здесь, потерпите меня .....

Я работаю над сборкой приложения tkinter с помощью PyInstaller, и я столкнулся с проблемой при использовании inspect библиотека. По сути, я могу вызвать исходный код класса, но не функцию, которую я написал. Я сделал этот надуманный пример, чтобы продемонстрировать:

У меня есть структура папок, подобная этой:

experiment/
---experiment.py
---init.py
---resources/
    /---resources.py
    /---init.py
---loader/
    /---loader.py
    /---init.py

resources.py определяет одну функцию в одном классе:

def foo(a):
    print(str(a) + ' is what you entered.')

class Bar:
    def __init__(self, b):
        self.b = b

loader.py импортирует эту функцию и этот класс и определяет функцию для печати их исходного кода:

import inspect

from resources import resources

def testfunc():
    source_Bar = inspect.getsource(resources.Bar)
    print(source_Bar)

    source_foo = inspect.getsource(resources.foo)
    print(source_foo)    

и experiment.py загружает эту функцию печати из loader.py и вызывает ее:

from loader.loader import testfunc

testfunc()

Я могу запустить experiment.py в консоли Python и получить ожидаемый результат (исходный код для foo и Bar).

Затем я использую PyInstaller для создания исполняемого файла из experiment.py (из виртуальной среды с добавлением только PyInstaller). Файл spec не тронут (я могу поделиться им), но я копирую / вставляю каталоги loader и resources в dist/experiment, чтобы исполняемый файл мог их найти. Если я запускаю experiment.exe из командной строки, я получаю следующие выходные данные:

C:\Users\earne\Desktop\experiment\dist\experiment>experiment.exe
class Bar:
    def __init__(self, b):
        self.b = b

Traceback (most recent call last):
  File "experiment\experiment.py", line 3, in <module>
  File "experiment\loader\loader.py", line 9, in testfunc
  File "inspect.py", line 973, in getsource
  File "inspect.py", line 955, in getsourcelines
  File "inspect.py", line 786, in findsource
OSError: could not get source code
[14188] Failed to execute script experiment

Таким образом, код для Bar найден и напечатан, но код для foo не найден ! Обратите внимание, что в строке 9 проверяется foo.

Реальный контекст - это графическая программа, в которой я хочу вернуть код, использованный для построения графика, на случай, если пользователи захотят внести изменения. Так что foo на самом деле много matplotlib кода, loader - это модуль для форматирования кода, а experiment - приложение tkinter. Как и в этом случае, inspect отлично работает из IDE, но ломается после сборки exe.

Подробнее о моей настройке:

  • virtualenv был создан с помощью Anaconda Prompt; Я использовал conda create, чтобы создать новую среду, а затем pip установил PyInstaller
  • Python - это 3.7.7
  • все в этой среде, в основном то, с чем она поставляется:
altgraph       0.17
certifi        2020.4.5.1
future         0.18.2
pefile         2019.4.18
pip            20.0.2
pyinstaller    4.0.dev0+03d42a2a25
pywin32-ctypes 0.2.0
setuptools     46.1.3.post20200330
wheel          0.34.2
wincertstore   0.2
  • Windows 10 64 бит

Что я пробовал:

  • много переключения метода импорта (например, явный импорт функции по имени, используя import *, импортируя только модуль и используя module.function)
  • возиться с файлом .spec, как добавление моих модулей в аргумент datas Analysis , Я видел эту проблему , и я пытался добавить свои модули в a.pure, как прокомментировал htgoebel, но я не уверен, что я делаю это правильно, и кажется, что, возможно, нет проблема root, поскольку код для класса Bar можно найти в одном и том же файле
  • Я использовал как самую последнюю версию PyInstaller (3.6), так и текущую версию разработчика от https://github.com/pyinstaller/pyinstaller/archive/develop.zip

В целом самая странная часть кажется, что исходный код класса может быть найден, но функция не может , когда они находятся в одном файле - но, возможно, PyInstaller делает это более сложным, чем это таким образом, я не понимаю? Пожалуйста, дайте мне знать, если у вас есть какие-либо предложения или хотите, чтобы я попробовал что-нибудь еще. Рад предоставить любую другую информацию / тесты, которые я могу. Ура!

1 Ответ

0 голосов
/ 24 апреля 2020

У меня есть решение после того, как я немного поковыряюсь - хотя и чувствует себя не идеально.

К loader Я добавил операторы для печати модуля и файла как foo, так и Barmymod» не требуется):

import inspect
from resources import resources as mymod

def testfunc():
    print(inspect.getfile(mymod.Bar))
    print(inspect.getmodule(mymod.Bar))
    source_Bar = inspect.getsource(mymod.Bar)
    print(source_Bar)

    print(inspect.getfile(mymod.foo))
    print(inspect.getmodule(mymod.foo))
    source_foo = inspect.getsource(mymod.foo)
    print(source_foo)

При повторном создании программы вывод experiment.exe указывает на несоответствие между foo и Bar. Операторы печати для mymod.Bar:

C:\Users\...\dist\experiment\resources\resources.pyc
<module 'resources.resources' from 'C:\\Users\...\dist\\experiment\\resources\\resources.pyc'>
class Bar:
    def __init__(self, b):
        self.b = b

и для mymod.foo:

experiment\resources\resources.py
<module 'resources.resources' from 'C:\\Users\...\dist\\experiment\\resources\\resources.pyc'>
Traceback (most recent call last):
... #same error as before

Так что, похоже, Python может распознать, что они из одного модуля, но не фактический файл; есть абсолютный путь, указывающий на файл .pyc для Bar, а только относительный путь к файлу .py для foo.

Поэтому я попытался изменить импорт / быть более явным, и получил использование importlib:

from importlib import import_module
mymod = import_module('resources.resources')

Это исправлено experiment.exe, чтобы дать правильный вывод:

C:\Users\...\dist\experiment\resources\resources.py
<module 'resources.resources' from 'C:\\Users\...\dist\\experiment\\resources\\resources.py'>
class Bar:
    def __init__(self, b):
        self.b = b

C:\Users\...\experiment\resources\resources.py
<module 'resources.resources' from 'C:\\Users\...\dist\\experiment\\resources\\resources.py'>
def foo(a):
    print(str(a) + ' is what you entered.')

Так что даже для этого я не понимаю, почему изменение импорт нужен. Более того, это исправление не работало в контексте моего GUI - я все еще не мог получить исходный код функции, но класс продолжал работать. В итоге мне пришлось следовать этому примеру и выполнить:

import importlib.util
homedir = os.path.dirname(os.path.dirname(__file__)
resources_dir = os.path.join(homedir, 'resources/resources.py')
spec = importlib.util.spec_from_file_location("resources.resources", resources_dir)
mymod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mymod)
#I do os stuff to get a relative path to resources from loader

И это работает в приложении - кажется слишком многословным и запутанным, но в конечном итоге это, по-видимому, не влияет на функционирование приложение, либо в виде .exe, либо в виде Python сценария.

Все равно хотелось бы знать, что здесь происходит, если кому-то захочется дать крик. Надеюсь, это поможет кому-то, если нет. ?

...