Skip to content

Commit

Permalink
Multiple typing fixes and a migration for a missed non-nullable column
Browse files Browse the repository at this point in the history
  • Loading branch information
jace committed Dec 27, 2023
1 parent d668f35 commit 760103e
Show file tree
Hide file tree
Showing 74 changed files with 546 additions and 340 deletions.
10 changes: 7 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,18 +102,22 @@ repos:
- id: black
# Mypy is temporarily disabled until the SQLAlchemy 2.0 migration is complete
# - repo: https://github.com/pre-commit/mirrors-mypy
# rev: v1.3.0
# rev: v1.8.0
# hooks:
# - id: mypy
# # warn-unused-ignores is unsafe with pre-commit, see
# # https://github.com/python/mypy/issues/2960
# args: ['--no-warn-unused-ignores', '--ignore-missing-imports']
# args:
# [
# '--no-warn-unused-ignores',
# '--no-warn-redundant-casts',
# '--ignore-missing-imports',
# ]
# additional_dependencies:
# - flask
# - lxml-stubs
# - sqlalchemy
# - toml
# - tomli
# - types-chevron
# - types-geoip2
# - types-python-dateutil
Expand Down
41 changes: 41 additions & 0 deletions funnel/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Auth proxy."""

from coaster.auth import (
CurrentAuth as CurrentAuthBase,
GetCurrentAuth,
add_auth_anchor,
add_auth_attribute,
request_has_auth,
)

from . import all_apps
from .models import User

__all__ = [
'CurrentAuth',
'add_auth_attribute',
'add_auth_anchor',
'current_auth',
'request_has_auth',
]


class CurrentAuth(CurrentAuthBase):
"""CurrentAuth for Funnel."""

# These attrs are typed as not-optional because they're typically accessed in a view
# that is already gated with the `@requires_login` or related decorator, so they're
# guaranteed to be present within the view. However, this will require a type-ignore
# for any code that tests `if current_auth.actor`, so those will need a rewrite to
# `if current_auth`. When auth clients become supported actors, this may need some
# form of PEP 647 typeguard to identify the actor's exact type.

user: User
actor: User


current_auth = GetCurrentAuth.proxy(CurrentAuth)

# Install this proxy in all apps, overriding the proxy provided by coaster.app.init_app
for _app in all_apps:
_app.jinja_env.globals['current_auth'] = current_auth
6 changes: 3 additions & 3 deletions funnel/devtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from flask import Flask

from . import app as main_app, shortlinkapp, transports, unsubscribeapp
from . import all_apps, app as main_app, transports
from .models import db
from .typing import ReturnView

Expand Down Expand Up @@ -100,7 +100,7 @@ def __call__(self, environ: Any, start_response: Any) -> Iterable[bytes]:
return use_app(environ, start_response)


devtest_app = AppByHostWsgi(main_app, shortlinkapp, unsubscribeapp)
devtest_app = AppByHostWsgi(*all_apps)

# --- Background worker ----------------------------------------------------------------

Expand Down Expand Up @@ -312,7 +312,7 @@ def start(self) -> None:
raise RuntimeError(f"Server exited with code {self._process.exitcode}")

def _is_ready(self) -> bool:
"""Probe for readyness with a socket connection."""
"""Probe for readiness with a socket connection."""
if not self.probe_at:
return False
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Expand Down
7 changes: 4 additions & 3 deletions funnel/forms/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ class PasswordPolicyForm(forms.Form):
)

def validate_password(self, field: forms.Field) -> None:
"""Test password strength and save resuls (no errors raised)."""
"""Test password strength and save results (no errors raised)."""
user_inputs = []

if self.edit_user:
Expand Down Expand Up @@ -283,6 +283,7 @@ class PasswordResetForm(forms.Form):

__returns__ = ('password_strength',)
password_strength: int | None = None
edit_user: User

# TODO: This form has been deprecated with OTP-based reset as that doesn't need
# username and now uses :class:`PasswordCreateForm`. This form is retained in the
Expand Down Expand Up @@ -502,7 +503,7 @@ class EnableNotificationsDescriptionProtoMixin:

enable_notifications: forms.Field

def set_queries(self) -> None:
def __post_init__(self) -> None:
"""Change the description to include a link."""
self.enable_notifications.description = Markup(
_(
Expand Down Expand Up @@ -614,7 +615,7 @@ class ModeratorReportForm(forms.Form):
__("Report type"), coerce=int, validators=[forms.validators.InputRequired()]
)

def set_queries(self) -> None:
def __post_init__(self) -> None:
"""Prepare form for use."""
self.report_type.choices = [
(idx, report_type.title)
Expand Down
2 changes: 1 addition & 1 deletion funnel/forms/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
from flask import flash

from baseframe import _, __, forms
from coaster.auth import current_auth

from .. import app
from ..auth import current_auth
from ..models import (
Account,
AccountEmailClaim,
Expand Down
3 changes: 1 addition & 2 deletions funnel/forms/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,7 @@ def validate_otp(self, field: forms.Field) -> None:
class EmailOtpForm(OtpForm):
"""Verify an OTP sent to email."""

def set_queries(self) -> None:
super().set_queries()
def __post_init__(self) -> None:
self.otp.description = _("One-time password sent to your email address")


Expand Down
6 changes: 3 additions & 3 deletions funnel/forms/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ class UnsubscribeForm(forms.Form):
__("Unsubscribe token type"), validators=[forms.validators.DataRequired()]
)

def set_queries(self) -> None:
def __post_init__(self) -> None:
"""Prepare form for use."""
# Populate choices with all notification types that the user has a preference
# row for.
Expand Down Expand Up @@ -188,7 +188,7 @@ def set_types(self, obj) -> None:
# self.types.data will only contain the enabled preferences. Therefore, iterate
# through all choices and toggle true or false based on whether it's in the
# enabled list. This uses dict access instead of .get because the rows are known
# to exist (set_queries loaded from this source).
# to exist (`__post_init__` loaded from this source).
for ntype, _title in self.types.choices:
obj.notification_preferences[ntype].set_transport(
self.transport, ntype in self.types.data
Expand All @@ -205,7 +205,7 @@ class SetNotificationPreferenceForm(forms.Form):
)
enabled = forms.BooleanField(__("Enable this transport"))

def set_queries(self) -> None:
def __post_init__(self) -> None:
"""Prepare form for use."""
# The main switch is special-cased with an empty string for notification type
self.notification_type.choices = [('', __("Main switch"))] + [
Expand Down
8 changes: 4 additions & 4 deletions funnel/forms/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class ProfileForm(OrganizationForm):
filters=nullable_strip_filters,
)

def set_queries(self) -> None:
def __post_init__(self) -> None:
"""Prepare form for use."""
self.logo_url.profile = self.account.name or self.account.buid

Expand Down Expand Up @@ -89,7 +89,7 @@ class ProfileTransitionForm(forms.Form):
__("Account visibility"), validators=[forms.validators.DataRequired()]
)

def set_queries(self) -> None:
def __post_init__(self) -> None:
"""Prepare form for use."""
self.transition.choices = list(
self.edit_obj.profile_state.transitions().items()
Expand Down Expand Up @@ -117,7 +117,7 @@ class ProfileLogoForm(forms.Form):
filters=nullable_strip_filters,
)

def set_queries(self) -> None:
def __post_init__(self) -> None:
"""Prepare form for use."""
self.logo_url.widget_type = 'modal'
self.logo_url.profile = self.account.name or self.account.buid
Expand All @@ -144,7 +144,7 @@ class ProfileBannerForm(forms.Form):
filters=nullable_strip_filters,
)

def set_queries(self) -> None:
def __post_init__(self) -> None:
"""Prepare form for use."""
self.banner_image_url.widget_type = 'modal'
self.banner_image_url.profile = self.account.name or self.account.buid
10 changes: 5 additions & 5 deletions funnel/forms/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def validate_location(self, field: forms.Field) -> None:
__("Quotes are not necessary in the location name")
)

def set_queries(self) -> None:
def __post_init__(self) -> None:
self.bg_image.profile = self.account.name or self.account.buid
if self.edit_obj is not None and self.edit_obj.schedule_start_at:
# Don't allow user to directly manipulate timestamps when it's done via
Expand Down Expand Up @@ -186,7 +186,7 @@ class ProjectNameForm(forms.Form):
"""Form to change the URL name of a project."""

# TODO: Add validators for `account` and unique name here instead of delegating to
# the view. Also add `set_queries` method to change ``name.prefix``
# the view. Also add `__post_init__` method to change ``name.prefix``

name = forms.AnnotatedTextField(
__("Custom URL"),
Expand Down Expand Up @@ -236,7 +236,7 @@ class ProjectBannerForm(forms.Form):
filters=nullable_strip_filters,
)

def set_queries(self) -> None:
def __post_init__(self) -> None:
"""Prepare form for use."""
self.bg_image.widget_type = 'modal'
self.bg_image.profile = self.account.name or self.account.buid
Expand Down Expand Up @@ -280,7 +280,7 @@ class ProjectTransitionForm(forms.Form):
__("Status"), validators=[forms.validators.DataRequired()]
)

def set_queries(self) -> None:
def __post_init__(self) -> None:
"""Prepare form for use."""
self.transition.choices = list(self.edit_obj.state.transitions().items())

Expand Down Expand Up @@ -349,7 +349,7 @@ class RsvpTransitionForm(forms.Form):
__("Status"), validators=[forms.validators.DataRequired()]
)

def set_queries(self) -> None:
def __post_init__(self) -> None:
"""Prepare form for use."""
# Usually you need to use an instance's state.transitions to find
# all the valid transitions for the current state of the instance.
Expand Down
10 changes: 5 additions & 5 deletions funnel/forms/proposal.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ class ProposalLabelsForm(forms.Form):
edit_parent: Project
formlabels = forms.FormField(forms.Form, __("Labels"))

def set_queries(self) -> None:
def __post_init__(self) -> None:
"""Prepare form for use."""
self.formlabels.form = proposal_label_form(
project=self.edit_parent, proposal=self.edit_obj
Expand All @@ -139,7 +139,7 @@ class ProposalLabelsAdminForm(forms.Form):
edit_parent: Project
formlabels = forms.FormField(forms.Form, __("Labels"))

def set_queries(self) -> None:
def __post_init__(self) -> None:
"""Prepare form for use."""
self.formlabels.form = proposal_label_admin_form(
project=self.edit_parent, proposal=self.edit_obj
Expand Down Expand Up @@ -175,7 +175,7 @@ class ProposalForm(forms.Form):
)
formlabels = forms.FormField(forms.Form, __("Labels"))

def set_queries(self) -> None:
def __post_init__(self) -> None:
"""Prepare form for use."""
label_form = proposal_label_form(
project=self.edit_parent, proposal=self.edit_obj
Expand Down Expand Up @@ -229,7 +229,7 @@ class ProposalTransitionForm(forms.Form):
__("Status"), validators=[forms.validators.DataRequired()]
)

def set_queries(self) -> None:
def __post_init__(self) -> None:
"""Prepare form for use."""
# value: transition method name
# label: transition object itself
Expand All @@ -251,6 +251,6 @@ class ProposalMoveForm(forms.Form):
get_label='title',
)

def set_queries(self) -> None:
def __post_init__(self) -> None:
"""Prepare form for use."""
self.target.query = self.user.projects_as_editor
4 changes: 2 additions & 2 deletions funnel/forms/sync_ticket.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ class ProjectBoxofficeForm(forms.Form):
validators=[forms.validators.Optional(), validate_and_convert_json],
)

def set_queries(self):
def __post_init__(self):
"""Set form schema description."""
self.register_form_schema.description = Markup(
'<p>{description}</p><pre><code>{schema}</code></pre>'
Expand Down Expand Up @@ -214,7 +214,7 @@ class TicketParticipantForm(forms.Form):
validators=[forms.validators.DataRequired("Select at least one event")],
)

def set_queries(self) -> None:
def __post_init__(self) -> None:
"""Prepare form for use."""
self.ticket_events.query = self.edit_parent.ticket_events

Expand Down
4 changes: 2 additions & 2 deletions funnel/forms/venue.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class VenueForm(forms.Form):
validators=[forms.validators.Optional(), forms.validators.ValidCoordinates()],
)

def set_queries(self) -> None:
def __post_init__(self) -> None:
"""Prepare form for use."""
pycountry_locale = gettext.translation(
'iso3166-2', pycountry.LOCALES_DIR, languages=[str(get_locale()), 'en']
Expand Down Expand Up @@ -124,6 +124,6 @@ class VenuePrimaryForm(forms.Form):
render_kw={'autocorrect': 'off', 'autocapitalize': 'off'},
)

def set_queries(self) -> None:
def __post_init__(self) -> None:
"""Prepare form for use."""
self.venue.query = self.edit_parent.venues
5 changes: 3 additions & 2 deletions funnel/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import sqlalchemy.exc as sa_exc
import sqlalchemy.orm as sa_orm
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Table
from sqlalchemy.dialects import postgresql
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import DeclarativeBase, Mapped, declarative_mixin, declared_attr
Expand Down Expand Up @@ -42,14 +43,14 @@
class Model(ModelBase, DeclarativeBase):
"""Base for all models."""

__table__: ClassVar[sa.Table]
__table__: ClassVar[Table]
__with_timezone__ = True


class GeonameModel(ModelBase, DeclarativeBase):
"""Base for geoname models."""

__table__: ClassVar[sa.Table]
__table__: ClassVar[Table]
__bind_key__ = 'geoname'
__with_timezone__ = True

Expand Down
Loading

0 comments on commit 760103e

Please sign in to comment.