본문 바로가기

MLOps/트러블 슈팅

개발자가 사용자와 소통하는 방법(Nonetype object has no attribute write, pyinstaller)

 

 

개발자가 사용자와 소통하는 방법(Nonetype object has no attribute write, pyinstaller)

 

부제 : 오픈마켓 상품 정보 수집기 버그를 고치며..


1. 사건

Pyinstaller를 이용해서 패키징 한 EXE 파일을 지인에게 전달하고, 또 다른 버그가 생겼다

하지만 내가 테스트할 때 발생하지 않은 버그라 당황했다.

 


2. 사용자 환경 문제

"사용자가 프로그램을 실행하는 환경이 뭐가 잘못된 건가?"라고 처음엔 생각했다.

사실상 최악의 접근방식이었다.

실행하는 환경에 상관없이 동작하도록 만들었어야 하는 것이지 사용자는 잘못 없다.


3. 원인 파악

먼저 Pyinstaller 도큐먼트에서 5.7 버전에서 Bugfix 내용을 확인할 수 있다

Pyinstaller Docs

<5.7 버전 Bugfix>


설명

패키징 할 때, windowed/noconsole 옵션을 사용하면, 생성된 실행 파일은 콘솔 창 없이 실행된다.

이전 버전에서는 이러한 모드에서 'sys.stdout', 'sys.stderr'를 사용자 정의 'NullWriter' 객체로 설정했다.

 if sys.stdout is None: 
     sys.stdout = NullWriter() 
 if sys.stderr is None: 
     sys.stderr = NullWriter()

sys.stdout과 sys.stderr는 표준 출력/오류 스트림을 말하고

보통 터미널 또는 콘솔 창으로 데이터를 출력하는 데 사용된다.

 

Pyinstaller의 이전 버전에서는 windowd/noconsole 옵션을 적용했을 때,

이러한 스트림을 NullWriter라는 특수한 객체로 설정해서 콘솔 출력을 모두 흡수해 버렸다.

 

NullWriter를 살펴보면, 아무것도 쓰지 않는 객체이다.  [Github]

class NullWriter:
    softspace = 0
    encoding = 'UTF-8'

    def write(*args):
        pass

    def flush(*args):
        pass

    # Some packages are checking if stdout/stderr is available (e.g., youtube-dl). For details, see #1883.
    def isatty(self):
        return False

하지만 

NullWriter는 'io.IOBase' 인터페이스에서 완전히 자유로워지지 못했다.

즉, 'sys.stdout'과 'sys.stderr'는 아래 'io.IOBase'의 다양한 메서드에 대한 기댓값을 가지고 있지만

NullWriter 가 아래 다양한 메서드들 중 일부만 구현했을 경우, 에러를 발생시킬 수 있는 것이다. 

io.IOBase

파이썬의 입출력 관련 클래스들의 기본 클래스로, 파일이나 다른 스트림을 처리하는 데 사용되는 메서드를 제공하고 있다.

<다양한 메서드들>


따라서

5.7 버전 이후에서는 표준 출력/오류 스트림을 NullWriter로 초기화하지 않고, 

None으로 초기화했다는 내용이다.


4. 원인 해결

버그가 발생한 이유

5.12 버전을 사용 및 noconsole 옵션 사용
  1. 표준 출력/에러 스트림이 None으로 초기화 상태에서 별도의 스트림 객체로 리디렉션 하지않음
  2. 5.6버전을 사용했으면, 'write' 속성이 없다는 에러는 발생하지 않았을 것

그래서

에러가 발생한 'write' 메서드에 대해서 "LoggerWrite" 객체를 선언하고

class LoggerWriter:
    def __init__(self, logger, level):
        self.logger = logger
        self.level = level

    def write(self, message):
        if message != '\n':
            self.logger.log(self.level, message)

    def flush(self):
        pass

표준 입/출력 스트림을 로거 객체를 생성하고 리디렉션 했다

sys.stdout = LoggerWriter(logger, logging.INFO)
sys.stderr = LoggerWriter(logger, logging.ERROR)

 

하지만 다시 전달할 때는 

혹시나 아래 2가지 버전을 전달해 버리는 하남자 행동을 해버렸다

  1. [A 버전] Pyinstaller 5.12 버전
  2. [B 버전] Pyinstaller 5.6 버전 (NullWriter에서 wirte 메서드가 있었기 때문)

5. 개발자가 사용자와 소통하는 방법

돌고 돌아 문제를 단번에 파악하기 위해서 가장 빠른 방법은 에러 메시지를 확인하는 것이다.

따라서 개발자가 사용자와 소통하는 방법은 로그로..

로그 레벨을 Debug로 설정하고 로그를 다운로드할 수 있는 버튼을 빨리 만들어 전달했고

문제가 생길 경우, 로그 파일을 전달받기로 했다


 

6. 결과

다행히 A 버전이 잘됐다.

하지만, 하지만이 남아있다.. 


참고

 

CX_FREEZE Window - AttributeError: 'NoneType' object has no attribute write

I am currently using cx_freeze 6.1 (had some different issues with 6.2 and 6.3) to generate MSI windows installer for an OpenCV python application. Compilation was successful and generated MSI inst...

stackoverflow.com

 

 

PyInstaller

Converts (packages) Python programs into stand-alone executables, under Windows, Linux, Mac OS X, AIX and Solaris. - PyInstaller

github.com