Flask Mega-Tutorial、パートXV:アプリケーション構造の改善

(2018年版)



ミゲル・グリンバーグ






ここに 戻る







これは、Flask Mega-Tutorialsシリーズの15番目であり、大規模なアプリケーションに適したスタイルを使用してアプリケーションを再構築します。







ネタバレの下には、この2018年シリーズのすべての記事のリストがあります。









注1:このコースの古いバージョンをお探しの場合は、こちらをご覧ください







注2:私(ミゲル)の仕事を支持して突然声をかけたい場合、または1週間記事を待つ忍耐がない場合、私(ミゲルグリーンバーグ)はこのガイドの完全版(英語)を電子書籍またはビデオの形式で提供します。 詳細については、 learn.miguelgrinberg.comをご覧ください







マイクロブログはすでにまともなサイズのアプリです! そして、Flaskアプリケーションが混乱することなく、または管理するにはあまりにも複雑になることなく、どのように成長できるかを議論する時だと思いました。 Flaskは、プロジェクトを任意の方法で整理する機会を提供するように設計されたフレームワークです。この哲学の一環として、アプリケーションが大きくなるにつれて、またはニーズや経験レベルが変化するにつれて、アプリケーションの構造を変更または適合させることができます。







この章では、大規模なアプリケーションに適用されるいくつかのパターンについて説明し、それらを実証するために、コードをより保守しやすく編成しやすくするために、私のマイクロブログプロジェクトの構造にいくつかの変更を加えます。 しかし、もちろん、Flaskの真の精神では、これらの変更を推奨事項、つまり独自のプロジェクトを編成する方法としてのみ受け入れることをお勧めします。







この章のGitHubリンク: BrowseZipDiff







現在の制限



アプリケーションの現在の状態には、主に2つの問題があります。 アプリケーションの構造を見ると、識別できるサブシステムがいくつかあることがわかりますが、それらをサポートするコードは明確な境界なしにすべて混同されています。 これらのサブシステムが何であるか見てみましょう:









私が定義したこれらの3つのサブシステムとそれらがどのように構成されているかを考えると、おそらくパターンに気付くでしょう。 これまで、私が従った組織のロジックは、アプリケーションのさまざまな機能に関するモジュールの可用性に基づいています。 機能を表示するためのモジュール、Webフォーム用、エラー用、レター用、HTMLテンプレート用のディレクトリなどがあります。 これは小規模なプロジェクトには理にかなった構造ですが、プロジェクトが成長し始めると、これらのモジュールのいくつかが本当に大きく汚くなる傾向があります。







問題を明確に見る1つの方法は、最初のプロジェクトから可能な限り再利用して、2番目のプロジェクトを開始する方法を検討することです。 たとえば、ユーザー認証部分は他のアプリケーションでうまく機能するはずです。 ただし、このコードをそのまま使用する場合は、いくつかのモジュールに移動し、適切なセクションをコピーして、新しいプロジェクトの新しいファイルに貼り付ける必要があります。 それがどれほど不便かをご覧ください。 このプロジェクトに、他のアプリケーションとは別に認証関連のファイルがすべて含まれていれば、より良いと思いませんか? Flask のブループリント機能は、コードの再利用を簡素化する、より実用的な組織の実現に役立ちます。







2番目の問題はそれほど明白ではありません。 Flaskアプリケーションインスタンスはapp/__init__.py



グローバル変数として作成され、その後多くのアプリケーションモジュールによってインポートされます。 これ自体は問題ではありませんが、アプリケーションをグローバル変数として使用すると、特にテストに関連するいくつかのシナリオが複雑になる可能性があります。 さまざまな構成でこのアプリケーションをテストするとします。 アプリケーションはグローバル変数として定義されているため、実際には、異なる構成変数を使用して2つのアプリケーションをインスタンス化することはできません。 理想的ではない別の状況は、すべてのテストが同じアプリケーションを使用するため、後で実行される別のテストに影響を及ぼすアプリケーションに変更を加えることができることです。 理想的には、すべてのテストを元のアプリケーションインスタンスで実行する必要があります。







実際、これらすべてはtests.pyモジュールで見ることができます。 ディスクベースのSQLiteデータベース(デフォルト)の代わりにメモリ内のデータベースを使用するようにテストを指示するために、アプリケーションにインストールした後の構成変更のトリックに頼ります。 テストが開始された時点で、アプリケーションは既に作成および構成されているため、構成済みデータベースを変更する他の方法はありません。 この特定の状況では、アプリケーションに適用された後に構成を変更するとうまくいくように見えますが、他の場合には役に立たず、いずれの場合も悪い練習であり、あいまいでエラーを見つけるのが難しくなります。







最善の解決策は、アプリケーションにグローバル変数を使用せず、代わりにアプリケーションファクトリ関数を使用して実行時に関数を作成することです。 これは、構成オブジェクトを引数として受け取り、これらの設定によって構成されたFlaskアプリケーションインスタンスを返す関数になります。 アプリケーションを変更してアプリケーションファクトリ関数を使用できるようにすると、各テストで独自のアプリケーションを作成できるため、特別な構成を必要とするテストの作成が容易になります。







この章では、上記の3つのサブシステムの要素図とアプリケーションファクトリ関数を導入して、アプリケーションを再編成します。 変更の詳細なリストを表示することは実用的ではありません。アプリケーションの一部であるほとんどすべてのファイルに小さな変更があるため、リファクタリングを行うために行った手順について説明します。これらの変更を含むアプリケーションをダウンロードできます







設計図



Flaskでは、プロジェクトはアプリケーションのサブセットである論理構造です。 プロジェクトには、ルート、表示機能、フォーム、テンプレート、静的ファイルなどの要素を含めることができます。 プロジェクトを別のPythonパッケージで記述する場合、特定のアプリケーション機能に関連する要素をカプセル化するコンポーネントがあります。







スキーマ要素の内容は最初は静止しています。 これらの要素をリンクするには、要素図をアプリケーションに登録する必要があります。 登録時に、要素図に追加されたすべての要素がアプリケーションに転送されます。 したがって、要素図をアプリケーションの機能の一時ストレージとして提示することができ、コードの整理に役立ちます。







ブループリントエラー処理



最初に作成した要素図は、エラーハンドラをサポートするためにカプセル化されました。 この概念の構造は次のとおりです。







 app/ errors/ <-- blueprint  __init__.py <-- blueprint  handlers.py <-- error  templates/ errors/ <-- error  404.html 500.html __init__.py <-- blueprint 
      
      





本質的に、 app / errors.pyモジュールapp / errors / handlers.pyに移動し、 app / templates / errorsの2つのエラーパターンを他のパターンから分離しました。 また、両方のエラーハンドラーでrender_template()



呼び出しを変更して、新しいエラーテンプレートのサブディレクトリを使用する必要がありました。 その後、ブループリントをapp/errors/__init__.py



に作成し、アプリケーションインスタンスの作成後にプロジェクトをapp/__init__.py



に登録しました。







Flask要素のスキーマは、テンプレートまたは静的ファイル用の別のディレクトリで構成できることに注意してください。 すべてのテンプレートが同じ階層にあるように、テンプレートをアプリケーションテンプレートディレクトリのサブディレクトリに移動することにしましたが、要素スキームパッケージ内の要素スキームに属するテンプレートを使用する場合は、これがサポートされています。 たとえば、引数template_folder= 'templates'



Blueprint()



コンストラクターに追加すると、要素レイアウトテンプレートをapp / errors / templatesに保存できます。







要素図の作成は、アプリケーションの作成と非常に似ています。 これは、blueprintパッケージの__init__.py



モジュールで行われます。







app/errors/__init__.py



:エラーのブループリント。


 from flask import Blueprint bp = Blueprint('errors', __name__) from app.errors import handlers
      
      





Blueprint



クラスは、要素スキーマの名前、ベースモジュールの名前(通常はFlaskアプリケーションのインスタンスのように__name__



に設定されます)、およびこの場合は必要ないいくつかのオプションの引数を取ります。 要素図オブジェクトを作成したら、 handlers.pyモジュールをインポートして、その中のエラーハンドラーが要素図に登録されるようにします。 循環依存関係を避けるため、このインポートは下部にあります。







handlers.pyモジュールは、 @app.errorhandler



デコレーターを使用してアプリケーションにエラーハンドラーをアタッチする代わりに、ブループリントデコレーター@bp.app_errorhandler



を使用します。 両方のデコレータは同じ最終結果を達成しますが、アイデアは、アプリケーションの独立した要素のレイアウトを作成して、移植性を高めることです。 また、2つのエラーパターンへのパスを変更して、移動先の新しいエラーサブディレクトリに対応する必要があります。







エラーハンドラのリファクタリングを完了する最後の手順は、要素スキーマをアプリケーションに登録することです。







app/__init__.py



:要素スキームをアプリケーションに登録します。


 app = Flask(__name__) # ... from app.errors import bp as errors_bp app.register_blueprint(errors_bp) # ... from app import routes, models # <--     !
      
      





アイテムスキーマを登録するには、Flaskアプリケーションインスタンスのregister_blueprint()



メソッドを使用します。 要素図を登録すると、すべてのプレゼンテーション関数、テンプレート、静的ファイル、エラーハンドラなどがアプリケーションに接続されます。 循環依存関係を避けるために、要素スキーマのインポートをapp.register_blueprint()



すぐ上にapp.register_blueprint()



ます。







ブループリント認証



アプリケーションの認証機能をプロジェクトにリファクタリングするプロセスは、エラーハンドラーを処理するプロセスと非常に似ています。 リファクタリングスキームは次のとおりです。







 app/ auth/ <-- blueprint  __init__.py <-- blueprint  email.py <-- authentication emails forms.py <-- authentication forms routes.py <-- authentication routes templates/ auth/ <-- blueprint  login.html register.html reset_password_request.html reset_password.html __init__.py <-- blueprint 
      
      





このプロジェクトを作成するには、認証に関連するすべての機能を、プロジェクトで作成した新しいモジュールに転送する必要がありました。 これには、いくつかのブラウジング機能、Webフォーム、およびパスワードリセットトークンを電子メールで送信する機能などのサポート機能が含まれます。 エラーページの場合と同様に、テンプレートをサブディレクトリに移動して、アプリケーションの他の部分から分離しました。







要素図でルートを定義するとき、 @bp.route



代わりに@app.route



デコレーターを使用します。 また、URLを作成するには、 url_for()



使用される構文を変更する必要があります。 アプリケーションに直接接続されている通常のビュー関数の場合、 url_for()



最初の引数はビュー関数の名前です。 ルートが要素マップで定義されている場合、この引数には、要素マップの名前とビュー関数の名前をピリオドで区切って含める必要があります。 そのため、たとえば、すべてのurl_for('login')



url_for('auth.')



url_for('auth.')



、残りのプレゼンテーション関数についても同じように置き換える必要がありました。







auth



要素のスキーマをアプリケーションに登録するために、少し異なる形式を使用しました。







app/__init__.py



:認証要素スキームをアプリケーションに登録します。


 # ... from app.auth import bp as auth_bp app.register_blueprint(auth_bp, url_prefix='/auth') # ...
      
      





この場合のregister_blueprint()



の呼び出しには、追加のurl_prefix



引数があります。 これは完全にオプションですが、Flaskは要素マップをURLプレフィックスの下にアタッチする機能を提供するため、要素マップで定義されたルートはすべてURLでこのプレフィックスを取得します。 多くの場合、これは、要素図のすべてのルートをアプリケーションまたは他の要素図の他のルートから分離する一種の「名前空間」として役立ちます。 認証については、すべてのルートが/auth



で始まるのがいいと思ったので、プレフィックスを追加しました。 したがって、ログインURLはhttp:// localhost:5000 / auth / loginになりますurl_for()



を使用してURLを生成するため、すべてのURLに自動的にプレフィックスが含まれます。







アプリケーション要素の基本的な概要



3番目の要素図には、アプリケーションのメインロジックが含まれています。 この要素図をリファクタリングするには、前の2つの要素図と同じプロセスが必要です。 この要素スキームにmain



という名前を付けたので、ビュー関数を参照するすべてのurl_for()



呼び出しにはmain



必要です。 プレフィックス。 これがアプリケーションの主要な機能であるため、テンプレートを同じ場所に残すことにしました。 テンプレートを他の2つの要素スキームからサブディレクトリに移動したため、これは問題ではありません。







アプリケーションファクトリテンプレート



この章の概要で述べたように、アプリケーションをグローバル変数として使用すると、主にいくつかのテストシナリオの制限という形で、いくつかの複雑さが生じます。 要素図を紹介する前に、アプリケーションはグローバル変数でなければなりません@app.route



。なぜなら、すべてのプレゼンテーション関数とエラーハンドラーは、 @app.route



などのapp



関数で装飾する必要があったからapp



。 しかし、すべてのルートとエラーハンドラーが要素スキーマに移動されたため、アプリケーションをグローバルに保つ理由ははるかに少なくなりました。







したがって、Flaskアプリケーションのインスタンスを作成し、グローバル変数を除外するcreate_app()



関数を追加します。 変換は簡単ではありませんでしたが、いくつかの困難を理解する必要がありましたが、最初にアプリケーションファクトリ関数を見てみましょう。







app/__init__.py



:アプリケーションファクトリ関数。


 # ... db = SQLAlchemy() migrate = Migrate() login = LoginManager() login.login_view = 'auth.login' login.login_message = _l('Please log in to access this page.') mail = Mail() bootstrap = Bootstrap() moment = Moment() babel = Babel() def create_app(config_class=Config): app = Flask(__name__) app.config.from_object(config_class) db.init_app(app) migrate.init_app(app, db) login.init_app(app) mail.init_app(app) bootstrap.init_app(app) moment.init_app(app) babel.init_app(app) # ...    blueprint registration if not app.debug and not app.testing: # ...    logging setup return app
      
      





ほとんどのFlask拡張機能は、拡張機能をインスタンス化し、アプリケーションを引数として渡すことで初期化されることがわかりました。 アプリケーションがグローバル変数として存在しない場合、拡張機能が2つのステップで初期化される代替モードがあります。 拡張インスタンスは、以前と同様にグローバルスコープで最初に作成されますが、引数は渡されません。 これにより、アプリケーションに添付されていない拡張機能のインスタンスが作成されます。 ファクトリー関数でアプリケーションインスタンスを作成する場合、拡張インスタンスでinit_app()



メソッドを呼び出して、既知のアプリケーションにバインドする必要があります。







初期化中に実行される他のタスクは変更されませんが、グローバルスコープ内ではなくファクトリ関数に転送されます。 これには、要素スキーマとロギング構成の登録が含まれます。 条件にnot app.testing



条件を追加したことに注意してください。これは、ユニットテスト中にこれらのログがすべてスキップされるように、電子メールとファイルのログを有効または無効にするかどうかを決定します。 構成でTESTING



変数がTrue



に設定されているため、単体テストの実行時にapp.testing



フラグはTrue



になります。







では、誰がアプリケーションファクトリ関数を呼び出すのでしょうか? この機能を使用する明白な場所は、トップレベルスクリプトmicroblog.pyです 。これは、アプリケーションが現在グローバルエリアに存在する唯一のモジュールです。 別の場所はtest.pyで、次のセクションでユニットテストについて詳しく説明します。







上で述べたように、アプリケーションへのリンクのほとんどは要素スキーマの導入とともになくなりましたが、それらのいくつかはまだ考慮すべきコードに残っています。 たとえば、すべてのアプリケーションapp / models.pyapp / translate.pyおよびapp / main / routes.pyにapp.configへのリンクがあります。 幸いなことに、Flask開発者は、これまで行ってきたように、インポートすることなくアプリケーションインスタンスにアクセスするためにブラウジング機能を簡素化しようとしました。 Flaskのcurrent_app



変数は、リクエストを送信する前にアプリケーションを初期化する特別な「コンテキスト」Flask変数です。 以前に別のコンテキスト変数、つまり現在のロケールを保存するg



変数を見てきました。 これら2つは、 current_user



Flask-Loginおよびまだ見たことのない他のいくつかと一緒に、グローバル変数として機能するため条件付きの「マジック」変数ですが、リクエスト処理中およびそれを処理するスレッドでのみ使用可能です。







app



をFlaskのcurrent_app



変数に置き換えると、アプリケーションインスタンスをグローバル変数としてインポートする必要がなくなります。 app.config



すべての記述をcurrent_app.config



簡単な検索と置換で簡単に置き換えることができました。







app / email.pyモジュールは少し大きな問題だったので、ちょっとしたトリックを使わなければなりませんでした。







app / email.py :アプリケーションインスタンスを別のスレッドに転送します。


 from app import current_app def send_async_email(app, msg): with app.app_context(): mail.send(msg) def send_email(subject, sender, recipients, text_body, html_body): msg = Message(subject, sender=sender, recipients=recipients) msg.body = text_body msg.html = html_body Thread(target=send_async_email, args=(current_app._get_current_object(), msg)).start()
      
      





send_email()



関数では、アプリケーションインスタンスが引数としてバックグラウンドスレッドに渡され、メインアプリケーションをブロックせずに電子メールが配信されます。 current_app



はクライアント要求を処理するスレッドにバインドされたコンテキスト変数であるため、バックグラウンドスレッドとして機能するsend_async_email()



関数でcurrent_app



直接使用しても機能しません。 別のスレッドでは、 current_app



は値が割り当てられていません。 current_app



は実際にはアプリケーションインスタンスに動的にマップされるプロキシオブジェクトであるため、 current_app



引数としてストリームオブジェクトに直接渡すことも機能しません。 したがって、プロキシオブジェクトを渡すことは、ストリーム内でcurrent_app



直接使用することと同じになります。 プロキシオブジェクト内に格納されているアプリケーションの実際のインスタンスにアクセスし、それをアプリケーションの引数として渡す必要がありました。 式current_app._get_current_object()



は、プロキシオブジェクトから実際のアプリケーションインスタンスを取得するため、これは引数としてストリームに渡したものです。







もう1つの「ハードケース」はapp / cli.pyモジュールで 、これは言語翻訳を管理するためのいくつかのクイックアクセスコマンドを実装しています。 この場合、変数current_app



は機能しません。これらのコマンドは、リクエストの処理中ではなく、起動時に登録されるためです。これは、 current_app



を使用できる唯一の時間です。 このモジュールでアプリケーションへのリンクを削除するために、別のトリックに頼りました。これらのユーザーコマンドをregister()



関数内に移動し、 app



インスタンスを引数として使用します。







app / cli.py :カスタムアプリケーションコマンドを登録します。


 import os import click def register(app): @app.cli.group() def translate(): """Translation and localization commands.""" pass @translate.command() @click.argument('lang') def init(lang): """Initialize a new language.""" # ... @translate.command() def update(): """Update all languages.""" # ... @translate.command() def compile(): """Compile all languages.""" # ...
      
      





次に、 microblog.pyからこのregister()



関数を呼び出しました。 リファクタリング後の完全なmicroblog.pyは次のとおりです。







microblog.py :リファクタリング後のメインアプリケーションモジュール。


 from app import create_app, db, cli from app.models import User, Post app = create_app() cli.register(app) @app.shell_context_processor def make_shell_context(): return {'db': db, 'User': User, 'Post' :Post}
      
      





単体テストの改善



この章の冒頭で示唆したように、これまでに行った作業のほとんどは、単体テストのワークフローを改善することを目的としています。 単体テストを実行する場合、アプリケーションがデータベースなどの開発リソースに干渉しないように構成されていることを確認する必要があります。







tests.pyの現在のバージョンは、アプリケーションインスタンスに適用された後に構成を変更するというトリックに頼ります。これは、最後の瞬間にすべてのタイプの変更が機能するわけではないため、危険な慣行です。 テスト構成をアプリケーションに追加する前に指定できるようにしたい。







create_app()



関数は、構成クラスを引数としてcreate_app()



ようになりました。 デフォルトでは、 config.pyで定義されたConfig



クラスが使用されますが、新しいクラスをファクトリー関数に渡すだけで、異なる構成を使用するアプリケーションインスタンスを作成できるようになりました。 ユニットテストに使用できる構成クラスの例を次に示します。







tests.py :設定のテスト。


 from config import Config class TestConfig(Config): TESTING = True SQLALCHEMY_DATABASE_URI = 'sqlite://'
      
      





ここでは、アプリケーションのConfig



クラスをサブクラス化し、SQLAlchemy構成をオーバーライドして、メモリ内のSQLiteデータベースを使用します。 また、値TrueのTESTING



属性を追加しましたが、これは現在必要ありませんが、アプリケーションが単体テストで実行されるかどうかを判断する必要がある場合に役立ちます。







思い出していただければ、私のユニットテストはsetUp()



およびtearDown()



メソッドに基づいていました。これらのメソッドは、各テストに適した環境を作成および破棄するためにユニットテストプラットフォームによって自動的に呼び出されます。 :







tests.py : .


 class UserModelCase(unittest.TestCase): def setUp(self): self.app = create_app(TestConfig) self.app_context = self.app.app_context() self.app_context.push() db.create_all() def tearDown(self): db.session.remove() db.drop_all() self.app_context.pop()
      
      





self.app



, , . db.create_all()



, . db



, , URI app.config



, , , . , db



, self.app



, ?







. current_app



, - , ? , , . , , , current_app



. , Python. , python



, flask shell



, , .







 >>> from flask import current_app >>> current_app.config['SQLALCHEMY_DATABASE_URI'] Traceback (most recent call last): ... RuntimeError: Working outside of application context. >>> from app import create_app >>> app = create_app() >>> app.app_context().push() >>> current_app.config['SQLALCHEMY_DATABASE_URI'] 'sqlite:////home/miguel/microblog/app.db'
      
      





! Flask , current_app



g



. , . db.create_all()



setUp()



, db.create_all()



current_app.config



, , . tearDown()



, .







, , Flask. request context , , . , request



session



Flask, current_user



Flask-Login.









, , , , , . , , URL- API Microsoft Translator. , , , , , , .







, , .env



. , .







Python, .env , python-dotenv



. , :







 (venv) $ pip install python-dotenv
      
      





config.py — , , .env Config



, :







config.py : .env .


 import os from dotenv import load_dotenv basedir = os.path.abspath(os.path.dirname(__file__)) load_dotenv(os.path.join(basedir, '.env')) class Config(object): # ...
      
      





, .env , . , .env - . , , .







.env - , FLASK_APP



FLASK_DEBUG



, , , .







.env , , 25- , API Microsoft Translator :







 SECRET_KEY=a-really-long-and-unique-key-that-nobody-knows MAIL_SERVER=localhost MAIL_PORT=25 MS_TRANSLATOR_KEY=<your-translator-key-here>
      
      





Requirements



Python. - , , , requirements.txt . :







 (venv) $ pip freeze > requirements.txt
      
      





pip freeze



, , , requirements.txt . , , , :







 (venv) $ pip install -r requirements.txt
      
      





ここに 戻る








All Articles