본문 바로가기
공부/Python

[python] pyinstaller jinja2.PackageLoader Error

by 병진들 2021. 6. 24.

기존에 만들었던 pyinstaller 패키징에 에 많은 삽질이 있었지만 이 삽질은 거의 3일짜리였다..

 

기존 삽질 로그 : https://bslife.tistory.com/76?category=767584 

 

[python] pyinstaller 일곱번의 삽질 for uvicorn + FastAPI

pyinstaller 란.. python 파일을 패키징해서 executable 형식으로 만들어주는 아주 좋은 도구이다. 사용방법이 굉장히 간단해 보이지만, 실제로 적용하고 세부 설정을 하다보니 너무 할게 많았고 디버깅

bslife.tistory.com

 

에러 발생 과정

기존 패키징 파일은 pyinstaller를 이용하여 FastAPI(uvicorn) 를 빌드했었는데, 이 친구는 h2o AutoML을 배포하기 위한 파일이었다. 

 

하지만  h2o AutoML 을 사용하면서 다음과 같은 세가지 문제가 있었다.

  • pandas.Dataframe을 그대로 사용하지 않고 h2oFrame으로 변환 후 학습을 해야하고 예측을 해야하는 번거로움이 있음
  • h2o는 java를 기반으로 하는 Lib이기 때문에 jdk가 존재해야하며, pyinstaller로 패키징 했을때 이 경로를 인식하지 못하여 일일히 설정해줘야 함. 또한 이 과정에서 패키징 파일의 크기가 상대적으로 커짐
  • 간단한 모델임에도 불구하고 저장된 Model을 Load하고 Predict하는데 시간이 굉장히 오래걸림..! 10초정도..?

 

h2o 를 PyCaret으로 바꾸기로 결심하였다!

 

그리고 터진 Error.....

File "PyInstaller/loader/pyimod03_importers.py", line 540, in exec_module
File "pandas/io/formats/style.py", line 62, in <module>
File "pandas/io/formats/style.py", line 139, in Styler
File "jinja2/loaders.py", line 309, in __init__
ValueError: The 'pandas' package was not installed in a way that PackageLoader understands.

 

jinja2 Lib 의 PackageLoader가 pandas를 이해하지 못한다...

 

해결 과정

처음엔 jinja2/loaders.py lib를 열어봤다. 하지만 이 파일은 그저 파일을 읽어올 뿐, 별 다른 기능을 하지 않았다.

오류를 검색해본것 중에 패키지를 읽어올 때, __name__이랑 __package__ 이런 변수의 문제라는 이야기가 있어서 수정 후 빌드해봤지만 소용 없었고

 

Pyinstaller의 공통적인 문제가 빌드했을때 jinja2가 Package를 제대로 읽지 못하는 것이었는데 내 경우에는  pandas/io/formats/style.py 에서 문제가 발생했다.

 

 

기존 코드

# pandas/io/formats/style.py

loader = jinja2.PackageLoader("pandas", "io/formats/templates")

env = jinja2.Environment(loader=loader, trim_blocks=True)
print(f"###ENV###  {env}  ###")
template = env.get_template("html.tpl")

pandas 라이브러리 Package를 로드할때 위와 같이 불러오는데 Pyinstaller로 빌드하면 제대로 인식하지 못하기 때문에 빌드시에는 인식할 수 있도록 경로를 다르게 설정할 필요가 있다.

 

수정 후

import sys

if getattr(sys, 'frozen', False):
        # we are running in a bundle
        bundle_dir = sys._MEIPASS
        print(f"###bundle_dir###  {bundle_dir}  ###")
        loader = jinja2.FileSystemLoader(bundle_dir)
    else:
        # we are running in a normal Python environment
        loader = jinja2.PackageLoader("pandas", "io/formats/templates")

    env = jinja2.Environment(loader=loader, trim_blocks=True)
    print(f"###ENV###  {env}  ###")
    template = env.get_template("html.tpl")

 

pyinstaller가 빌드할때 사용되는 'frozen'으로 구분을 두고 빌드시 환경과 일반적인 환경에서의 실행과 나누었다.

 

빌드 될 때는 bundle_dir에 sys._MEIPASS를 인식하게 하고 최종적으로 필요한 파일을 참조할때 그 경로에 필요한 파일을 넣어주는 식으로 하면 될거라 생각했기때문에..! 

 

결과적으로 "ValueError: The 'pandas' package was not installed in a way that PackageLoader understands." 문제는 해결이 되었고

 

loader 과정에서 원래는 io/formats/templates 내부에 있는 html.tpl 을 참조해야했지만 해당 파일이 없으므로 bundle_dir  위치를 출력해보고 그 위치에 html.tpl 를 추가해주면 깔끔하게 해결된다.

 

추가하는 방법은 main.spec 에서 datas에 넣어준 후 빌드하면 된다.

아래는 예시이다.

a = Analysis(['main.py'],
             pathex=['/home/kbj/xedm/Scripts/fastapp'],
             binaries=[],
             datas=[
				('./package/html.tpl','./')
                ]

./package/ 내부에 html.tpl을 복사해주었고 './'는 bundle_dir 이다.

 

 

Pyinstaller 하면 할수록 어려운 과제가 자꾸 생긴다.. pandas Lib까지 고쳐야 할 줄이야...

 

 

ps. 개발환경

참고로 작성일 기준 Python 3.9 버전에서는 pycaret이 동작하지 않는다. 강제로 내려야만 했음..ㅠㅠ

# OS
ubuntu == 20.04

# Python
version == 3.8.5

# Lib
pandas == 1.2.4
jinja2 == 3.0.1
pyinstaller == 4.3
pycaret == 2.3.1

 

댓글