لود داینامیک فایلها و اپلیکیشن‌ها در پایتون و فلسک

سلام.

در فلسک یه کانسپت بخصوص به نام بلوپرینت(Blueprint) وجود داره که اجازه میده ما بتونیم بخش‌های مختلف اپلیکیشن رو، از همدیگه تفکیک بکنیم. اگر با جنگو آشنا باشید، بلوپرینت‌ها همون ‍`اپ` های جنگو هستن.

حالا، اخیرا نیاز شده بود که به صورت داینامیک یه اپلیکیشن به پروژه اضافه بشه یا ازش برداشته بشه و میخواستیم این کار به ساده‌ترین صورت ممکن قابل انجام باشه. از بین کل‌ راه‌های موجود برای این‌کار، کپی کردن فایل به محلی مشخص و لود شدن بلو‌پرینت و ریجستر شدنش به صورت اتوماتیک رو انتخاب کردیم.

با فلسک، پایتونی کد میزنیم!

نکتهٔ خیلی مهم در مورد فلسک، این هستش که بر خلاف جنگو، شما پایتون کد می‌زنید. در واقع، برای اینکه بتونید با جنگو کار کنید، باید یاد بگیرید که هر کاری رو توی جنگو به چه صورت انجام می‌دن. ولی این در مورد فلسک صدق نمی‌کنه. فلسک اونقدر کوچیکه و بدون پیش‌فرض در مورد کد شما، که برای کار کردن باهاش، کافیه بلد باشید اون کار رو توی پایتون چطوری باید انجام داد. باقی قضیه دیزاین و توانایی خودتون هست. این قضیه در عین حال که سادگی بیش‌از اندازه فلسک رو به همراه داره، ساختن اپلیکیشن پروداکشن رو باهاش به شدت سخت می‌کنه. شما مسئول همه چیز هستید و هیچ کاری برای شما انجام نمیشه.

انجام نرمال کارها

اول، میگم‌که ساختار پروژه‌ام چطوری هستش و فایل‌ها رو چطوری کنار هم می‌چینم و بلو‌پرینت درست می‌کنم، بعدش لود داینامیک اونها رو بررسی می‌کنم.من نیاز دارم که یه پروژه فلسک که بلوپرینت ساپورت می‌کنه بسازم. کل فایل‌هایی که باید بسازم به صورت زیر هستش

├── develop.py
├── dloader
│   ├── application.py
│   ├── apps
│   │   ├── __init__.py
│   │   └── test_app
│   │       ├── __init__.py
│   │       └── views.py
│   ├── __init__.py
└── README.md

کدهایی که باید داخل این فایل‌ها بره رو از توی گیت میتونید بردارید. من تمام فایل‌ها رو بررسی نمی‌کنم!

کد‌های فایل application.py به صورت زیر هستن


from flask import Flask

def create_app():
    app = Flask(__name__)
    _load_blueprints(app)
    return app
    
def _load_blueprints(app):
     from .apps.test_app import test
      app.register_blueprint(test)

همچنین برای ساختن بلو‌پرینت‌ در فایل init.py به صورت زیر عمل کردم(فایل داخل پوشه apps)

from flask import Blueprint
test = Blueprint('test', __name__)

در صورت اجرا، این‌ کدها به درستی عمل میکنند. حالا میخوایم به صورت داینامیک بلو‌پرینت‌ها رو لود کنیم. تغییرات چندان زیادی لازم نیست. همچنین میشه به صورت های مختلفی این‌کار رو انجام داد. من در نهایت از روشی استفاده کردم که در ادامه توضیحش می‌دم اما کاملا به فکر هستم که سیستم‌اش رو عوض کنم.

من فایل application.py رو به صورت زیر ویرایش میکنم

import importlib
import os

 def _load_blueprints(app):
    folder_names =  os.listdir("dloader/apps/")
    for name in folder_names:
        if name.endswith("_app"):
            tmp_blueprint = importlib.import_module(f".{name}", "dloader.apps")
            app.register_blueprint(tmp_blueprint.blueprint)

اول از همه کل‌پوشه‌های داخل مسیر رو به دست اوردم، بعد از اون روی لیست پوشه‌ها لوپ زدم. در مرحله بعد بررسی کردم که اگر اسم پوشه به app_ ختم میشه، بنابراین جزو بلوپرینت های من هستش. اون رو ایمپورت کردم و بعد هم ریجستر.

این وسط یه اتفاقی افتاده و اون خط اخر هستش. همینجوری که می‌بینید من از ماژول ایمپورت شده، دارم متغییری به نام blueprint رو استفاده می‌کنم. دلیل این قضیه این هستش که من با ایمپورتی که در خط قبلی انجام دادم، نیاز دارم به خود ابجکت بلوپرینت اشاره کنم تا بتونم ریجسترش کنم.

حالا برای اینکه اسم بلوپرینت هم داینامیک باشه، من همیشه ابجکت بلوپرینت رو باید داخل یه متغییر به اسم blueprint گذاشته باشم:)

فایل init ام به صورت زیر تغییر میکنه بنابراین

test = Blueprint('test', __name__)
blueprint = test

همینجور که می‌بینید، من در نهایت ابجکت blueprint رو برای ریجستر کردن استفاده کردم.

همچنین در صورتی که بخوام url_prefix استفاده کنم هم می‌تونم به همین صورت یه متغییر بهش بایند کنم و استفاده‌اش کنم.


این importlib چیکار میکنه

ماژول importlib برای ایمپورت کردن پکیج‌ها و ماژول‌های مختلف به صورت داینامیک و از داخل کد هستش. همینجور که می‌بینید ما از یه کد کاملا پایتونی برای انجام این‌کار در فلسک استفاده کردیم.

این تابع دوتا ارگومنت می‌گیره که البته دومی الزامی نیست. بر اساس داکیومنت پایتون

The 'package' argument is required when performing a relative import. It specifies the package to use as the anchor point from which to resolve the relative import to an absolute import.

بنابراین فقط در صورتی که داریم رلتیو ایمپورت انجام می‌دیم، نیاز هستش که از package استفاده کنیم. همچنین بهتره که با یه . شروع کنیم اسم ماژولی که باید ایمپورت بشه تا به مشکل نخوریم! :)


این ماژول واقعا کار راه انداز هستش و مشخصا بهتون کمک می‌کنه که به صورت داینامیک و راحت، با استفاده از کد ماژول‌های مختلف رو ایمپورت کنین و نه اینکه همیشه همه چیز رو ایمپورت کنین. همچنین میشه به صورت کلی به عنوان یه پلاگین سیستم و لودر اتوماتیک پلاگین‌ها هم بهش نگاه کرد.

کد کامل پروژه که کار میکنه رو براتون اینجا اپلود کردم که میتونید استفاده کنین.