93031


自己解決した問題ですが、参考のため投稿します。

# 現象

FastAPI+Uvicornを使用したアプリケーションをPyInstallerでビルドします。  
デバッグのため、PyInstallerでビルドしたバイナリにおいて、console=Trueとしてログ出力を見ながら、実行状況を確認していました。  
テストをパスして、本番リリースが近づいたので、console=Falseにして、PyInstallerでビルドしたところ以下のエラーが発生しました。

[code]

    File "logging\config.py", line 543, in configure
      File "logging\config.py", line 655, in configure_formatter
      File "logging\config.py", line 474, in configure_custom
      File "uvicorn\logging.py", line 47, in __init__
    AttributeError: 'NoneType' object has no attribute 'isatty'
    
    The above exception was the direct cause of the following exception:
    
    Traceback (most recent call last):
      File "multiprocessing\process.py", line 315, in _bootstrap
      File "multiprocessing\process.py", line 108, in run
      File "_app.py", line 75, in run
      File "_app.py", line 51, in __init__
      File "uvicorn\config.py", line 299, in __init__
      File "uvicorn\config.py", line 407, in configure_logging
      File "logging\config.py", line 809, in dictConfig
      File "logging\config.py", line 546, in configure
    ValueError: Unable to configure formatter 'default'
    
    During handling of the above exception, another exception occurred:
    
    Traceback (most recent call last):
      File "_app.py", line 120, in <module>
      File "Lib\site-packages\PyInstaller\hooks\rthooks\pyi_rth_multiprocessing.py", line 49, in _freeze_support
      File "multiprocessing\spawn.py", line 116, in spawn_main
      File "multiprocessing\spawn.py", line 129, in _main
      File "multiprocessing\process.py", line 330, in _bootstrap
    AttributeError: 'NoneType' object has no attribute 'write'
    
    
[/code]

## 参考記事

似たような問題として、以下の記事がありますが、対策しても現象が変わりません。  
場合によっては、以下の記事内容も対応が必要です。  
<https://stackoverflow.com/questions/64281002/pyinstaller-compiled-uvicorn-
server-does-not-start-correctly>


CC BY-SA 4.0
2022-12-27T02:50:45.820
93030
2022-12-27T12:42:16.957
2022-12-27T12:42:16.957
32891
32891
question
python pyinstaller fastapi
Fast API+Uvicornの環境をPyInstallerでビルドしたとき、console=Falseとするとエラーが発生してしまう


# 結論

FastAPIオブジェクト宣言前に以下の一文を入れてください。  
consoleオプションによりエラー有無が変わることから、FastAPIが標準出力しようとすることが問題でした。

[code]

    sys.stdout = open(os.devnull, 'w')
    
[/code]

# サンプルコード

サンプルコードはフォルダ構成に注意してください。  
spec,PS1ファイルは以下のフォルダツリーを前提としています。

[code]

    (project folder)
    +---ENV (venvで作成した仮想環境フォルダ)
    |
    \---tests
       \---pyinstaller_fastAPI
          | _app.py
          | app_release.spec
          | pyinstaller_fastAPI.ps1
          \---app (PyInstaller出力フォルダ)
              +---build
              +---dist
    
    
[/code]

## Pythonスクリプト

[code]

    from multiprocessing import freeze_support, set_start_method, Process
    from multiprocessing import Queue as MP_Queue
    
    from fastapi import FastAPI
    from pydantic import BaseModel
    
    import pathlib
    import os
    import sys
    from threading import Thread
    import time
    import tkinter as tk
    from uvicorn import Config, Server
    
    
    class ApiResult(BaseModel):
        status: str
        result: dict
        error: dict
    
    
    sys.stdout = open(os.devnull, 'w')
    
    api_port = 8080
    
    TERMINATE_SERVER = 'TERMINATE_SERVER'
    
    api_title = 'test Pyinstaller & fast FPI'
    api_version = '0.0'
    
    app = FastAPI(
        title=api_title,
        description=api_title,
        version=api_version
    )
    
    
    @app.get(f"/test")
    def read_api_root():
        result = ApiResult(
            status='SUCCESS',
            result={'Welcome': api_title, 'version': api_version},
            error={}
        )
        return result.dict()
    
    
    class web_server_thread:
        def __init__(
                self, web_app, host: str, port: int, reload: bool = False):
            self.config = Config(
                app=web_app,
                host=host,
                port=port,
                reload=reload,
                # log_level="warning",
            )
            self.server = Server(self.config)
            self.server.install_signal_handlers = lambda: None
            self.proc = None
    
        def start(self):
            self.th = Thread(name="WebServer Thread", target=self.server.run)
            self.th.setDaemon(True)
            self.th.start()
    
        def stop(self):
            if self.th:
                self.th.join(0.25)
    
    
    class ApiServer():
        def run(self,
                recv_queue, host: str = '127.0.0.1', port: int = 8080):
            self.instThread = web_server_thread(
                app, host=host, port=port)
            self.instThread.start()
    
            isLoop = True
    
            while (isLoop):
                time.sleep(0.2)
    
                while (not recv_queue.empty()):
                    item = recv_queue.get()
                    if item == TERMINATE_SERVER:
                        self.instThread.stop()
                        isLoop = False
    
            return
    
    
    class _DebugDialog():
        def __init__(self, send_queue):
            self.isLoop = True
            self.after_interval = 200
    
            self.send_queue = send_queue
    
            self.dialog = tk.Tk()
            self.dialog.protocol("WM_DELETE_WINDOW", self.quit)
            self.dialog.after(self.after_interval, self.loop)
            self.dialog.mainloop()
    
            return
    
        def quit(self):
            self.isLoop = False
            self.send_queue.put(TERMINATE_SERVER)
            self.dialog.destroy()
            return
    
        def loop(self):
            if self.isLoop:
                self.dialog.after(self.after_interval, self.loop)
            return
    
    
    if __name__ == '__main__':
        freeze_support()
        set_start_method('spawn')
    
        send_queue = MP_Queue()
    
        instServer = ApiServer()
    
        instProcess = Process(
            name='FastAPI Server',
            target=instServer.run,
            args=(send_queue, ),
            kwargs={
                'port': api_port
            }
        )
        instProcess.daemon = True
        instProcess.start()
    
        _DebugDialog(send_queue)
    
    
[/code]

## specファイル

[code]

    # -*- mode: python -*-
    import os
    from PyInstaller.utils.hooks import collect_submodules
    
    current_dir = os.getcwd()
    work_dir = current_dir
    source_dir = current_dir +'\\tests\\pyinstaller_fastAPI'
    
    hiddenimports = collect_submodules('uvicorn')
    
    block_cipher = None
    
    a = Analysis(
        [source_dir+'\\_app.py'],
        pathex=[],
        binaries=[],
        datas=[],
        hiddenimports=hiddenimports,
        hookspath=[],
        runtime_hooks=[],
        excludes=[],
        win_no_prefer_redirects=False,
        win_private_assemblies=False,
        cipher=block_cipher,
        noarchive=False
        )
    pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
    options = [('--uac-admin', None, 'OPTION')]
    
    exe = EXE(pyz,
              a.scripts,
              options,
              exclude_binaries=True,
              name='pyinstaller_fastAPI',
              debug=False,
              bootloader_ignore_signals=False,
              strip=False,
              upx=True,
              console=False,
              )
    coll = COLLECT(exe,
                   a.binaries,
                   a.zipfiles,
                   a.datas,
                   strip=False,
                   upx=True,
                   name='pyinstaller_fastAPI')
    
    
[/code]

## PowerShellスクリプト

[code]

    # ターミナルのカレントディレクトリを示す
    
    $PS_FILE_PATH = $MyInvocation.MyCommand.Path
    $PS_FILE_DIR = Split-Path $PS_FILE_PATH -Parent
    $_FILE_NAME_ = Split-Path $PS_FILE_PATH -Leaf
    
    $WORK_DIR = Split-Path $PS_FILE_DIR -Parent
    $WORK_DIR = Split-Path $WORK_DIR -Parent
    Set-Location -Path $WORK_DIR
    
    $CURRENT_DIR = Convert-Path .
    $CURRENT_DIR = Split-Path $CURRENT_DIR -Parent
    
    $PROJECT_NAME = "Test_PyInstaller"
    $PYTHON_PATH = $WORK_DIR + '\ENV\Scripts\python.exe'
    $PYINSTALL_DIR = $WORK_DIR + '\tests\pyinstaller_fastAPI'
    $DIST_PATH = $PYINSTALL_DIR + '\app\dist'
    $BUILD_PATH = $PYINSTALL_DIR + '\app\build'
    $PROJECT_PATH = $DIST_PATH + '\' + $PROJECT_NAME
    $RET_CODE = 0
    
    $LOG_FILE_NAME = [System.IO.Path]::GetFileNameWithoutExtension("$_FILE_NAME_")
    $LOG_FILE_PATH = $PS_FILE_DIR + '\' + $LOG_FILE_NAME + '.log'
    
    Start-Transcript $LOG_FILE_PATH
    
    write-host "実行ファイル名:" $PS_FILE_PATH
    
    write-host "カレントディレクトリ：" $CURRENT_DIR
    write-host "作業ディレクトリ：" $WORK_DIR
    
    write-host -NoNewline "プロジェクト名：" $PROJECT_NAME
    write-host "■スタンドアローンアプリを生成します。"
    
    $ErrorActionPreference = "Stop"
    try {
        Remove-Item -Path $DIST_PATH -Recurse -Force
        write-host -NoNewline "ディレクトリを削除しました。：" $DIST_PATH
    }
    catch {
        write-host -NoNewline "削除ディレクトリが見つかりません。：" $DIST_PATH
    }
    
    try {
        Remove-Item -Path $BUILD_PATH -Recurse -Force
        write-host -NoNewline "ディレクトリを削除しました。：" $BUILD_PATH
    }
    catch {
        write-host -NoNewline "削除ディレクトリが見つかりません。："
        write-host $BUILD_PATH
    }
    
    #スタンドアローン実行形式のビルド
    write-host "■スタンドアローン実行形式のビルドを開始します。"
    $PYINSTALL = $PYTHON_PATH + " -m PyInstaller $PYINSTALL_DIR\app_release.spec --distpath $DIST_PATH --workpath $BUILD_PATH "
    Invoke-Expression $PYINSTALL
    write-host "■スタンドアローン実行形式のビルドが完了しました。"
    
    write-host "何かキーを押してください。"
    [Console]::ReadKey($true) | Out-Null
    
    write-host "終了します。"
    Stop-Transcript
    
    exit $RET_CODE
    
    
[/code]


CC BY-SA 4.0
2022-12-27T02:50:45.820
93031
2022-12-27T02:50:45.820
32891
93030
answer