Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Comparasing between objects causes crash if _eq_ of db model class is "occupied" #2437

Open
stenhardvara opened this issue Jul 6, 2024 · 24 comments
Labels

Comments

@stenhardvara
Copy link

stenhardvara commented Jul 6, 2024

There seems to be an incorrect check for object equality. It works if dunder eq is not used in application since it will probably default to object reference equality.

Look into changing ""==" to "is" in

yield (pk, self.get_label(obj), obj == self.data)

@stenhardvara stenhardvara changed the title Comparasing between objects causes crash if __eq_ of db model class is "occupied" Comparasing between objects causes crash if _eq_ of db model class is "occupied" Jul 6, 2024
@stenhardvara
Copy link
Author

I started to think about this. I think it might be quite hard to solve actually. I couldn't find if the eq has any definition (except for python default) or "==" is used anywhere in sqlalchemy for a model class.

The only reason it crashed for me seems to be that I used the eq for application purposes and it was initialised to a non-callable (for my application to get an exception if "==" was used before equality was defined).

I am not really sure about the purpose for the test for equality in the line above, is to check for model object equality in the sense having the same data and private key? Or is it to check if it actually is the same object by checking memory address?

Looking at the previous implementation is seems that a corresponding check is done towards a list with "in". Which will return true if any of the two cases with either "is" or "==" is true with any element in the list. Maybe the solution here is to write a specific check that never uses "==" and takes in consideration the "get_pk" method when specified. With no get_pk compare keys and that the object is of same type with isinstance,

@ElLorans
Copy link
Contributor

Can you provide a reproducible example causing the error?

@stenhardvara
Copy link
Author

stenhardvara commented Aug 14, 2024

@ElLorans Sure, when editing a scoreboard object here it will crash trying to call the dunder eq for the class result:

File "/home/.venv/lib/python3.11/site-packages/flask_admin/contrib/sqla/fields.py", line 170, in iter_choices
    yield (pk, self.get_label(obj), obj in self.data)
                                    ^^^^^^^^^^^^^^^^^
  File "/home/admin_test.py", line 28, in __eq__
    return Result.score_eq(self, value)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: 'NoneType' object is not callable
from flask import Flask
from flask_admin import Admin
from flask_admin.contrib.sqla import ModelView
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()
app = Flask(__name__, instance_relative_config=True)

class Scoreboard(db.Model):
    id = db.Column(db.Integer, primary_key = True)
    name = db.Column(db.Text)

    results = db.relationship('Result')

    def __str__(self):
        return f'{self.name}'
    
class ScoreboardView(ModelView):
    create_modal = True
    edit_modal = True
    form_columns = ['name','results']

class Result(db.Model):
    score_gt = None
    score_eq = None

    def __eq__(self, value: object) -> bool:
        return Result.score_eq(self, value)
    
    def __gt__(self, value: object) -> bool:
        return Result.score_gt(self, value)
    
    @classmethod
    def set_score_comparison(cls, gt, eq):
        cls.score_gt = gt
        cls.score_eq = eq

    id = db.Column(db.Integer, primary_key = True)
    name = db.Column(db.Text)
    score = db.Column(db.Integer)
    scoreboard_id = db.Column(db.Integer, db.ForeignKey('scoreboard.id'))

    scoreboard = db.relationship('Scoreboard', back_populates='results', lazy='joined')

class ResultView(ModelView):
    create_modal = True
    edit_modal = True
    form_columns = ['name','score','scoreboard']
    

def create_app():
    app.config['SECRET_KEY'] = 'secret_stuff'
    app.config["SQLALCHEMY_DATABASE_URI"] = 'sqlite://'
    app.config['FLASK_ADMIN_SWATCH'] = 'darkly' 

    admin = Admin(template_mode="bootstrap3")

    db.init_app(app)
    admin.init_app(app)

    with app.app_context():
        db.create_all()

    admin.add_view(ScoreboardView(Scoreboard, db.session))
    admin.add_view(ResultView(Result, db.session))

@app.route("/")
def show_scores():
    return "<p>*Not EQ*</p>"


if __name__ == "__main__":
    create_app()

    app.run(debug=True)

@ElLorans
Copy link
Contributor

I believe the error is in your code:

class Result(db.Model):
score_gt = None
score_eq = None # fix this

@stenhardvara
Copy link
Author

@ElLorans No that is not an error! That is a feature on my application code. What I am saying that application code should be able to have any thing or any callable in that line without braking flask-admin. Right now flask-admin as a library is dependent on that the '''eq()''' method is defined and also means some specific form of equality for application specific classes.

I don't think that is suitable since equality of objects (in this case application specific objects) means different things in different applications. Imagine having a class "person" that map to your application database and you add 'eq()' to check is height is equal. Then flask-admin would not provide expected behaviour, but start to list some of the persons as selectable in some forms.

@ElLorans
Copy link
Contributor

it's the 'in' operator failing on your code because your eq calls a None value. This is not a flask-admin issue but a design flaw in your code. You can easily solve it.

@stenhardvara
Copy link
Author

@ElLorans No you don't understand. That fact that is crashes just exposes what I think is a very bad behavoiur of flask-admin. I think it is a fundamental design error of flask-admin to use the == operator on database mapped class instances. The "==" is not defined as the same idea of equality in different applications. Flask-admin cannot check "equality" with "==" of the database table mapped classes instances since it not good assumption that it is equality in the same abstract sense in different applications. Equality can mean different thing in different cases.

@ElLorans
Copy link
Contributor

I understand perfectly. Once again, the issue is not in flask-admin which is calling the 'in' operator. If you believe the 'in' operator should not call eq and you are unwilling to change your code so that your object is able to be compared, you can post an issue on cPython.

@stenhardvara
Copy link
Author

stenhardvara commented Aug 15, 2024

First of all. No, flask admin does not call the in operator where this happens. Flask-admin calls the "==" operator.

Calling the "==" in flask-admin on application defined database table row mapped class makes absolutly no sense. Because equality is application dependent in sqlalchemy table mapped classes .
Database table row equality is in the private key/id.
Object instance equality is in the memory address/reference of the objects.
These are not necessarly the same!
There can be many instances from the same table row. Then the private key/id is the same but the object reference is different.
And on top of that "==" can mean whatever the application want.

@stenhardvara
Copy link
Author

stenhardvara commented Aug 15, 2024

Could you explain how it is supposed to make sense to check for equality on user defined database table mapped classes instances with "=="?

@ElLorans
Copy link
Contributor

First of all. No, flask admin does not call the in operator where this happens. Flask-admin calls the "==" operator.

That's not what the error says. Take this as an example:

class Result:
    score_gt = None
    score_eq = None

    def __eq__(self, value: object) -> bool:
        return Result.score_eq(self, value)
    
    def __gt__(self, value: object) -> bool:
        return Result.score_gt(self, value)
    
    @classmethod
    def set_score_comparison(cls, gt, eq):
        cls.score_gt = gt
        cls.score_eq = eq

    def __init__(self, id, name, score, scoreboard):
        self.id = id
        self.name = name
        self.score = score
        self.scoreboard = scoreboard

a = Result(1, "a", 0, 0)
b_named_a = Result(1, "a", 0, 0)
c = Result(1, "c", 1, 0)
d = Result(1, "d", 0, 1)

"a" in [a, b_named_a, c, d] # this will raise an error

I would expect the in operator to work on a collection of objects, but your design makes it impossible: you would first need to set the score_eq function on each object inside the collection. Is that really what your logic requires you to do? I would say you are not following OOP.

@stenhardvara
Copy link
Author

Sorry I messed up in my exemple, it is not crashing where I intended. Didn't look close enough at the error.

The intention was to fail at the line in my original post where "==" is.

My application code does not look like this and been significantly refactored since July.

I agree that the in operator should work on a collection. So "==" should throw NotImplemented if not implemented and then in will check "is".

However I still thing that "==" in my original post should be either changed to "is" for reference equality or to "a.get_pk() == b.get()" where a and be is being compared. This depends of the purpose for flask-admin to check this equality.

@ElLorans
Copy link
Contributor

ElLorans commented Aug 16, 2024

That you can do it yourself by overriding correctly the eq method

@stenhardvara
Copy link
Author

I think that is an incorrect statement. The eq method is supposed to be used for value equality. Which will cause in some cases sqlachemy to do multiple queries and unexpected results.

Either the private key (table row the same) or the object reference (same object) is what should make sense for flask-admin to consider as equality.

@ElLorans
Copy link
Contributor

fask-admin will happily do that if you tell your object to do it.

You only need to change to this:
'''

def eq(self, value: object) -> bool:
return self.id == value.id
'''

@stenhardvara
Copy link
Author

I know that I can set it to whatever I want in my application context. That is also irrelevant in my specific application at this point since everything relating to this is already refactored away for other reasons.

I still think flask-admin as a library should not be dependent on "==" on sqlalchemy database mapped object instances.

@ElLorans
Copy link
Contributor

your example crashes on the 'in' operator, that calls the == operator. You can run my example or look at the error you provided to verify that.

@stenhardvara
Copy link
Author

Yes I have clearly stated that is a misstake by me when attempted to recreate the problem. Please ignore the example.

Please look at the specific line provided in the first comment when I reported this.

@ElLorans
Copy link
Contributor

okay now I understand what you are talking about. But that boolean is needed to understand what field must be marked as 'selected'. using 'is' would not work.

@stenhardvara
Copy link
Author

If "is" wont work and test is to check if a object match the selected one in seems to me that the primary key equality is perfect.

Then if a user have multiple entries in the database have same values (for other then primary key) not cause any problems. Like two people with the same name or something else.

And no extra queries would automatically be made by sqlalchemy to get unloaded fields/relationships that is not necessary.

So I would suggest doing something like obj.get_pk() == self.data.get_pk() if that doesn't create problems.

@ElLorans
Copy link
Contributor

ElLorans commented Aug 17, 2024

but self.data is the wtf form, it does not have a pk. Comparing a orm table's attribute vs a form value using == makes total sense.

@stenhardvara
Copy link
Author

But then "==" doesn't make sense either because that is a comparison between different objects? A sql mapped class object always have a primary key unless it is comittes to database. If is not yet committed uniqueness is not guaranteed as far as I know.
You could have many objects with the same data (except for primary key) in a database.

I think this is a really hard problem to get right.

@ElLorans
Copy link
Contributor

it really isn't. == compares values, so if the table row column A has value 1, and the form attribute A has value 1, the == will return True.

@stenhardvara
Copy link
Author

Nope, it will be way messier.

If table column A has value 4 and table column B has value "cucumber". Then it will be dependent on how the query looks if both A or B is loaded from database at the same time or not. Lazyloading is also default for relationship loading in sqlalchemy. So there is concurrency issues there as well.

But this doesn't really makes sense here either, since one of the values that should be compared if checking value equality is the primary key. If thats the case then this check should be always be false(the wtform don't have a PK yet). Something is strange here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Development

No branches or pull requests

3 participants