-
Notifications
You must be signed in to change notification settings - Fork 687
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
Fix type hints in models, qa_loader #5616
Fix type hints in models, qa_loader #5616
Conversation
c53443e
to
184be06
Compare
(rebased on latest develop) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @rmol lint target now passing after rebase and addresses lint failures in develop
. One comment/clarification inline before merging
securedrop/models.py
Outdated
@@ -634,6 +637,8 @@ def verify_token(self, token: 'Optional[str]') -> bool: | |||
# window is 1:30s. | |||
return self.totp.verify(token, valid_window=1) | |||
else: | |||
if not self.hotp_counter: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The database default value for hotp_counter
is 0, why do we manually set to 0 here? Under which circumstances is hotp_counter
is None
? Shouldn't we be returning False
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent question, which made me dig deeper into this. Here's my understanding of this whole house of cards:
Right now, Journalist.hotp_counter
is defined as Column(Integer, default=0)
- without
nullable=False
, the database schema sayshotp_counter
can benull
. (In fact, being SQLite, it can be 'what a wonderfully flexible database', too, but that's beside the point.) - the
default=0
is not the database default value; it's a value that SQLAlchemy will use in the case where no value was provided to the INSERT or UPDATE statement for that column. - Mypy infers
hotp_counter
asbuiltins.int*
-- I thought this might be because the sqlalchemy stubs that were just added were clever enough to notice thedefault
clause, but I get the same result ondevelop
before the stubs were added, and I've found some nullable columns without defaults (first_name
, for instance) that are also not inferred as optional. I'm still looking, but in any case it does not match reality, because the column can benull
.
So: adding # type: Column[Optional[int]]
accurately describes what we can get from our database (as long as we disregard SQLite flexibility), but then if hotp_counter
is not explicitly assigned, mypy knows it might be None
, so catches that that should not be passed to range
:
securedrop/models.py:641: error: Argument 1 to "range" has incompatible type "Optional[int]"; expected "int"
securedrop/models.py:642: error: Unsupported operand types for + ("None" and "int")
securedrop/models.py:642: note: Left operand is of type "Optional[int]"
So that's why it could be None
, at least in mypy's understanding of the world, and why I made sure it was set to the default, but you're right, I was too far down this rabbit hole and not thinking about the actual method. We should treat a null hotp_counter
as an error and return False
from verify_token
.
And as much as I'd like to grumble about the bolt-on type checker necessitating appeasement code, this is turning up valid problems with our ORM and database and the illusions they create. I think many of our columns should have been created with nullable=False
. That would take a difficult data migration to fix. In the meantime, I'd like to propagate the Optional
annotations to them. It doesn't have to be done in this PR, as I really just wanted to fix CI, but I think it's wrong to pretend we can't get null
/None
from the database. If we're going to check typing, let's be as accurate as possible.
Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for digging into this. I agree with your assessment:
- Immediately, to unblock this PR and CI: return false if
hotp_counter
isnull
- Medium term propagate the
Optional
annotation - Long term: Open an issue to add/migrate nullable=False database constrains (which as you say may be a complex migration for long-running instances.
184be06
to
f3ea107
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
Status
Ready for review
Description of Changes
Corrects some type hinting problems in
models.py
andqa_loader.py
.Testing
CI is green. Run
make lint
if you feel like it.Deployment
Dev only, 'cause type hints don't actually do anything.
Checklist
If you made changes to the server application code:
make lint
) and tests (make test
) pass in the development containerIf you made non-trivial code changes:
Choose one of the following: