soucis d'import python

importer une classe du modèle

Le problème exposé dans ce sujet a été résolu.

salut les agrumes (et les pas agrumes aussi)!

voilà j’ai un tout petit projet, histoire d’apprendre un peu à programmer des sites web avec Python et Flask, un capteur qui enregistre ses mesures dans une base de données, elle-même lue par Flask pour générer un site.

j’ai réussi à le faire marcher sur mon pc de dev en codant tout en vrac dans un fichier "app.py": modèle, routes, etc…

mais, j’aimerais le rendre plus facile à maintenir, et plus "professionnel", et je me retrouve à galérer devant des problèmes d’import, à tel point que je n’en ai même plus les yeux en face des trous: je souhaite transférer ma base de données qui est contrôlée via SQLAlchemy, et la classe contenant la table (pour l’instant unique). dans un fichier "models.py". sauf que dans mon fichier de routing, il y a une référence à la classe qui contient cette table, pour appeler une fonction de SQLAlchemy qui lit les données dedans. voilà mon code en "spoiler" pour que ce soit moins long dans le message initial:

voici init.py:

#! /usr/bin/env python
from flask import Flask
from .views import app
from . import models

voici views.py:

#! /usr/bin/env python
from flask import Flask, render_template


app = Flask(__name__)
app.config.from_object('config')

@app.route('/stats')
def stats():
    posts = Statement.query.all()
    return render_template('pages/stats.html', posts=posts)

et bien sûr voici mon modèle models.py:

#! /usr/bin/env python
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

from .views import app

db = SQLAlchemy(app)


class Statement(db.Model):
    __tablename__ = "statements"

    id = db.Column(db.Integer, primary_key=True)
    temperature = db.Column(db.Float)
    humidity = db.Column(db.Float)
    stated_at = db.Column(db.DateTime, default=db.func.now())

    def __init__(self, temp, hum):
        self.temperature = temp
        self.humidity = hum
        self.created_at = datetime.now()


db.create_all()

ces 3 fichiers sont dans le même package, (sensor_app). si j’importe mon modèle directement dans la vue, ça fait des imports circulaires, et si je l’importe dans init.py, ma vue ne sait pas ce que c’est qu’un "Statement (nom de ma classe du modèle avec la table qui va contenir les mesures)".

+0 -0

Haha, j’ai eu exactement le même problème que toi, utilisant Flask pour un projet. C’est un peu le « piège » des micro-frameworks : tu as une marge intéressante de libertés mais c’est facile de se tirer une balle dans le pied…

Bref, voici ma solution (qui n’est peut-être pas la meilleure) :

Renomme ton __init__.py en flask.py. Laisse un fichier __init__.py vide.

Dans ce fichier flask.py, pas besoin d’importer les modèles. Voici le mien :

from ._app import app  # Here we define the flask app.
from .api import api
from .backend import backend
from .email_viewer import email_viewer
from .landing import landing
from .payment import payment
from .static_content import static_content


app.register_blueprint(api, url_prefix="/api")
app.register_blueprint(backend, url_prefix="/")
app.register_blueprint(email_viewer, url_prefix="/")
app.register_blueprint(landing, url_prefix="/")
app.register_blueprint(payment, url_prefix="/")
app.register_blueprint(static_content, url_prefix="/")

Comme tu peux le voir, ici j’enregistre mes « blueprint ». De surcroit, c’est pas super élégant, mais ça fonctionne bien.

Dans un fichier _app.py, j’ai paramétré mon application Flask entre autre choses :

from flask import Flask, render_template

from .conf import settings


app = Flask(__name__)
app.secret_key = settings.SECRET_KEY
app.config["SERVER_NAME"] = settings.SERVER_NAME
app.config["SESSION_COOKIE_DOMAIN"] = settings.SERVER_NAME


@app.template_filter()
def get_settings(name):
    return getattr(settings, name)


def render_mail_template(template: str, browser_link: str, **kwargs):
    """Render a mail template."""
    return render_template(template, browser_link=browser_link, **kwargs)

Tu peux voir que mes blueprints correspondent à des modules python. Voici, à titre d’exemple, celui du backend :

import enum

from flask import (
    Blueprint,
    abort,
    flash,
    redirect,
    render_template,
    request,
    url_for,
)

from .conf import settings
from .models import db
from .models.user import User, UserForgottenPasswordToken


backend = Blueprint("backend", __name__)


class Error(enum.Enum):
    success = 0
    passwords_do_not_match = 1
    password_too_short = 2


ERROR_DESCRIPTION = {
    Error.success: "success",
    Error.passwords_do_not_match: "passwords do not match",
    Error.password_too_short: "password too short",
}


@backend.route("/reset_password")
def reset_password():
    email = request.args.get("email")
    token = request.args.get("token")
    if token is None or email is None:
        abort(400)

    user_token = UserForgottenPasswordToken.query.filter(
        User.email == email, UserForgottenPasswordToken.token == token
    ).first()

    if user_token is None:
        abort(400)

    return render_template(
        "backend/reset_password.html", email=email, token=token
    )


@backend.route("/password_reset")
def password_reset():
    return render_template("backend/reset_password_after.html")


@backend.route("/reset_password/post", methods=["POST"])
def reset_password_post():
    email = request.form.get("email")
    token = request.form.get("token")
    password = request.form.get("password")
    password_confirm = request.form.get("password_confirm")

    if (
        email is None
        or token is None
        or password is None
        or password_confirm is None
    ):
        abort(400)

    user = User.query.filter_by(email=email).first()
    if user is None:
        abort(400)

    user_token = UserForgottenPasswordToken.query.filter_by(
        user=user, token=token
    ).first()
    if user_token is None:
        abort(400)

    if password != password_confirm:
        flash(
            f"Error: {ERROR_DESCRIPTION[Error.passwords_do_not_match]}",
            "error",
        )
        return redirect(
            url_for("backend.reset_password", email=email, token=token)
        )

    if len(password) < 6:
        flash(f"Error: {ERROR_DESCRIPTION[Error.password_too_short]}", "error")
        return redirect(
            url_for("backend.reset_password", email=email, token=token)
        )

    # Here, everything is O.K. so we can change the password and delete the
    # token.
    user.hash_password(password)
    db.session.delete(user_token)
    db.session.commit()
    flash("Your password has been reset.", "success")
    return redirect(url_for("backend.password_reset"))

Tu peux notamment remarquer deux choses :

from .models import db
from .models.user import User, UserForgottenPasswordToken

Eh oui ! J’ai un module models qui exporte le symbole db qui est le « lien » vers ma base de données SQLAlchemy, et plusieurs sous-modules pour regrouper mes modèles par fichiers / thêmes (pour ne pas m’y perdre).

models/__init__.py:

import importlib
import os
from pathlib import Path

from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy

from .._app import app
from ..conf import settings


app.config["SQLALCHEMY_DATABASE_URI"] = settings.SQLALCHEMY_DATABASE_URI

db = SQLAlchemy(app)

# Import the python files in this module.
path = Path(os.path.dirname(__file__))
for x in path.iterdir():
    if x.is_file():
        module = str(x.relative_to(os.path.dirname(__file__))).partition(".")[
            0
        ]
        importlib.import_module(
            f".models.{module}", package="flexomatic_backend"
        )

migrate = Migrate(app, db)

Ici, c’est une sorte de « hack » pour importer automatiquement tous les sous-modules (et donc pour enregistrer les modèles). Ça me permet d’éviter d’avoir à faire from .user import User, UserForgottenPassword etc. À toi de voir.

Enfin, une partie du fichier user.py :

import datetime
import enum

from itsdangerous import (
    BadSignature,
    SignatureExpired,
    TimedJSONWebSignatureSerializer as Serializer,
)
from passlib.apps import custom_app_context as pwd_context

from .._app import app
from . import db


class User(db.Model):
    __tablename__ = "users"
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(100), index=True, unique=True)
    phone_number = db.Column(db.String(20), index=True, unique=True)
    password_hash = db.Column(db.String(255))
    is_activated = db.Column(db.Boolean, default=False)
    is_banned = db.Column(db.Boolean, default=False)

    validation_token = db.relationship(
        "UserValidationToken", back_populates="user"
    )
    stripe_user = db.relationship("StripeUser", back_populates="user")
    stripe_source = db.relationship("StripeSource", back_populates="user")
    stripe_charges = db.relationship("StripeCharge", back_populates="user")

    sponsorship_code = db.relationship(
        "UserSponsorshipCode", back_populates="user"
    )
    sponsored_party = db.relationship(
        "UserSponsoredParties",
        back_populates="user",
        foreign_keys="UserSponsoredParties.user_id",
    )
    sponsored_parties = db.relationship(
        "UserSponsoredParties",
        back_populates="sponsor",
        foreign_keys="UserSponsoredParties.sponsor_id",
    )

    connection_logs = db.relationship(
        "UserConnectionLog", back_populates="user"
    )
    forgotten_password_token = db.relationship(
        "UserForgottenPasswordToken", back_populates="user"
    )

    created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
    updated_at = db.Column(
        db.DateTime,
        default=datetime.datetime.utcnow,
        onupdate=datetime.datetime.utcnow,
    )

    def set_email_address(self, email):
        self.email = email.lower()

    def hash_password(self, password):
        self.password_hash = pwd_context.encrypt(password)

    def verify_password(self, password):
        return pwd_context.verify(password, self.password_hash)

    def generate_auth_token(self, expiration=600):
        s = Serializer(app.config["SECRET_KEY"], expires_in=expiration)
        return s.dumps({"id": self.id})

    @staticmethod
    def verify_auth_token(token):
        s = Serializer(app.config["SECRET_KEY"])
        try:
            data = s.loads(token)
        except (BadSignature, SignatureExpired):
            return None  # valid token, but expired
        else:
            return User.query.get(data["id"])



class UserForgottenPasswordToken(db.Model):
    __tablename__ = "user_forgotten_password_tokens"
    user_id = db.Column(
        db.Integer, db.ForeignKey("users.id"), primary_key=True
    )
    user = db.relationship("User", back_populates="forgotten_password_token")
    token = db.Column(db.String(32))
    created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
    updated_at = db.Column(
        db.DateTime,
        default=datetime.datetime.utcnow,
        onupdate=datetime.datetime.utcnow,
    )



class UserSponsorshipCode(db.Model):
    __tablename__ = "user_sponsorship_codes"
    user_id = db.Column(
        db.Integer, db.ForeignKey("users.id"), primary_key=True
    )
    user = db.relationship("User", back_populates="sponsorship_code")
    code = db.Column(db.String(8), unique=True)
    created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
    updated_at = db.Column(
        db.DateTime,
        default=datetime.datetime.utcnow,
        onupdate=datetime.datetime.utcnow,
    )


class UserConnectionLog(db.Model):
    __tablename__ = "user_connection_logs"
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
    user = db.relationship("User", back_populates="connection_logs")
    uuid = db.Column(db.String(36))
    version = db.Column(db.String(10), default="")
    created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
    updated_at = db.Column(
        db.DateTime,
        default=datetime.datetime.utcnow,
        onupdate=datetime.datetime.utcnow,
    )


class UserSponsoredParties(db.Model):
    """UserSponsoredParties model.

    This model describes a sponsorship between two parties.

    The user is the one being sponsored, and sponsor is the sponsor.

    """

    __tablename__ = "user_sponsored_parties"
    user_id = db.Column(
        db.Integer, db.ForeignKey("users.id"), primary_key=True
    )
    user = db.relationship(
        "User", back_populates="sponsored_party", foreign_keys=[user_id]
    )
    sponsor_id = db.Column(db.Integer, db.ForeignKey("users.id"))
    sponsor = db.relationship(
        "User", back_populates="sponsored_parties", foreign_keys=[sponsor_id]
    )

(J’ai enlevé quelques trucs, parce que, tu comprends, l’Open Source c’est bien, mais pas trop ! :lol: )

Voilà, j’ai fait un peu le tour, ça fait beaucoup de choses mais j’espère t’avoir aidé au final.

Ah et dernier truc ! N’oublie pas de mettre sensor_app.flask dans ta variable d’environnement FLASK_APP en ce qui te concerne. :)

À+

+0 -0

Salut,

Je ne connais pas bien l’organisation d’une application flask, mais je pense qu’une solution simple à ton problème serait d’ajouter from . import models dans ton fichier views.py, et d’utiliser models.Statement dans ta vue stats.

Je n’ai pas vérifié, mais a priori l’import paresseux devrait régler le soucis.

merci pour les réponses!

@Ge0: alors pour le coup, vu la simplicité du projet, je pense pas qu’il y ait besoin de ruses de sioux de ce style. surtout que c’est presque du copié-collé d’un tuto "qui fonctionne chez le tuto-maker". je garde quand même la ruse sioux en mémoire, pour un jour où ça serait vraiment bien

@entwanne: si je fais ce que tu me dis, python me sort l’erreur suivante:

ImportError: cannot import name 'app' from partially initialized module 'sensors_app.views’ (most likely due to a circular import)

mais! en bricolant mes imports avec tes instructions (un peu à l’aveugle), ça m’a donné une idée: et si l’ordre des imports était important? (sans blague! en plus c’est le truc dont on te parle tout le temps, quand tu apprends un langage, à faire attention à l’ordre des inclusions, il est benêt ce Remace) du coup j’ai importé mes modèles en premier dans init.py et paf! ça a marché!

je passe en résolu, merci encore pour votre aide les copains!

+0 -0

oui, pour relier le package à la base de données (de ce que j’en ai compris, pour le moins…) et pour ce faire, dans la classe SQLAlchemy, il y a une fonction SQLAlchemy.init_app(app) que j’appelle dans mon fichier __init__.py.mais je l’ai pas mise dans mon post pour ne pas surcharger la lecture de code, étant persuadé que ça ne vienne pas de là.

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte