Skip to content

Commit

Permalink
Multiple typing fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
jace committed Dec 18, 2023
1 parent ddb7d0f commit 72a714b
Show file tree
Hide file tree
Showing 36 changed files with 152 additions and 132 deletions.
20 changes: 11 additions & 9 deletions funnel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@

#: Main app for hasgeek.com
app = Flask(__name__, instance_relative_config=True)
app.name = 'funnel'
app.name = 'funnel' # pyright: ignore[reportGeneralTypeIssues]
app.config['SITE_TITLE'] = __("Hasgeek")
#: Shortlink app at has.gy
#: Short link app at has.gy
shortlinkapp = Flask(__name__, static_folder=None, instance_relative_config=True)
shortlinkapp.name = 'shortlink'
shortlinkapp.name = 'shortlink' # pyright: ignore[reportGeneralTypeIssues]
#: Unsubscribe app at bye.li
unsubscribeapp = Flask(__name__, static_folder=None, instance_relative_config=True)
unsubscribeapp.name = 'unsubscribe'
unsubscribeapp.name = 'unsubscribe' # pyright: ignore[reportGeneralTypeIssues]

all_apps = [app, shortlinkapp, unsubscribeapp]

Expand Down Expand Up @@ -92,9 +92,6 @@
each_app, ['py', 'env'], env_prefix=['FLASK', f'APP_{each_app.name.upper()}']
)

# Legacy additional config for the main app (pending deprecation)
coaster.app.load_config_from_file(app, 'hasgeekapp.py')

# Force specific config settings, overriding deployment config
shortlinkapp.config['SERVER_NAME'] = app.config['SHORTLINK_DOMAIN']
if app.config.get('UNSUBSCRIBE_DOMAIN'):
Expand Down Expand Up @@ -147,7 +144,12 @@
geoip.geoip.init_app(app)

# Baseframe is required for apps with UI ('funnel' theme is registered above)
baseframe.init_app(app, requires=['funnel'], theme='funnel', error_handlers=False)
baseframe.init_app(
app,
requires=['funnel'],
theme='funnel', # type: ignore[arg-type]
error_handlers=False,
)

# Initialize available login providers from app config
loginproviders.init_app(app)
Expand Down Expand Up @@ -195,7 +197,7 @@

views.siteadmin.init_rq_dashboard()

# --- Serve static files with Whitenoise -----------------------------------------------
# --- Serve static files with WhiteNoise -----------------------------------------------

app.wsgi_app = WhiteNoise( # type: ignore[method-assign]
app.wsgi_app, root=app.static_folder, prefix=app.static_url_path
Expand Down
17 changes: 11 additions & 6 deletions funnel/devtest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Support for development and testing environments."""
# pyright: reportGeneralTypeIssues=false

from __future__ import annotations

Expand All @@ -13,7 +14,7 @@
import weakref
from collections.abc import Callable, Iterable
from secrets import token_urlsafe
from typing import Any, NamedTuple, Protocol
from typing import Any, NamedTuple, Protocol, cast

from flask import Flask

Expand All @@ -29,7 +30,7 @@
# Pytest `live_server` fixture used for end-to-end tests. Fork on macOS is not
# compatible with the Objective C framework. If you have a framework Python build and
# experience crashes, try setting the environment variable
# OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES
# `OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES`
mpcontext = multiprocessing.get_context('fork')

# --- Development and testing app multiplexer ------------------------------------------
Expand Down Expand Up @@ -160,7 +161,7 @@ def install_mock(func: Callable, mock: Callable) -> None:
# Use weakref to dereference func from local namespace
func = weakref.ref(func)
gc.collect()
refs = gc.get_referrers(func()) # type: ignore[misc]
refs = gc.get_referrers(func()) # type: ignore[misc] # Typeshed says not callable
# Recover func from the weakref so we can do an `is` match in referrers
func = func() # type: ignore[misc]
for ref in refs:
Expand All @@ -170,6 +171,10 @@ def install_mock(func: Callable, mock: Callable) -> None:
for key, value in ref.items():
if value is func:
ref[key] = mock
else:
raise RuntimeError(
f"Can't patch {func.__qualname__} in unknown reference type {ref!r}"
)


def _prepare_subprocess(
Expand Down Expand Up @@ -267,9 +272,9 @@ def __init__(
self.mock_transports = mock_transports

manager = mpcontext.Manager()
self.calls: CapturedCalls = manager.Namespace()
self.calls.email = manager.list()
self.calls.sms = manager.list()
self.calls = cast(CapturedCalls, manager.Namespace())
self.calls.email = cast(list[CapturedEmail], manager.list())
self.calls.sms = cast(list[CapturedSms], manager.list())

def start(self) -> None:
"""Start worker in a separate process."""
Expand Down
10 changes: 3 additions & 7 deletions funnel/forms/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class AccountSelectField(forms.AutocompleteField):
def _value(self) -> str:
"""Return value for HTML rendering."""
if self.data is not None:
return self.data.name
return self.data.name or ''
return ''

def process_formdata(self, valuelist: Sequence[str]) -> None:
Expand Down Expand Up @@ -83,9 +83,7 @@ def __init__(self, purpose: Literal['use', 'claim', 'register']) -> None:

def __call__(self, form: forms.Form, field: forms.Field) -> None:
# Get actor (from form, or current_auth.actor)
actor: User | None = None
if hasattr(form, 'edit_user'):
actor = form.edit_user
actor: User | None = getattr(form, 'edit_user', None)
if actor is None:
actor = current_auth.actor

Expand Down Expand Up @@ -174,9 +172,7 @@ def __init__(self, purpose: Literal['use', 'claim', 'register']) -> None:

def __call__(self, form: forms.Form, field: forms.Field) -> None:
# Get actor (from existing obj, or current_auth.actor)
actor: User | None = None
if hasattr(form, 'edit_user'):
actor = form.edit_user
actor: User | None = getattr(form, 'edit_user', None)
if actor is None:
actor = current_auth.actor

Expand Down
12 changes: 5 additions & 7 deletions funnel/forms/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@
from ..models import (
PASSWORD_MAX_LENGTH,
Account,
AccountEmail,
AccountEmailClaim,
AccountPhone,
Anchor,
EmailAddress,
EmailAddressBlockedError,
LoginSession,
Expand Down Expand Up @@ -78,7 +76,7 @@ def __call__(self, form, field) -> None:
if not field.data:
# Use getattr for when :meth:`LoginForm.validate_username` is skipped
if getattr(form, 'anchor', None) is not None:
# If user has an anchor, we can allow login to proceed passwordless
# If user has an anchor, we can allow login to proceed password-less
# using an OTP or email link
raise LoginWithOtp()
if (
Expand Down Expand Up @@ -123,7 +121,7 @@ class LoginForm(forms.RecaptchaForm):

__returns__ = ('user', 'anchor', 'weak_password', 'new_email', 'new_phone')
user: Account | None = None
anchor: AccountEmail | AccountEmailClaim | AccountPhone | None = None
anchor: Anchor | None = None
weak_password: bool | None = None
new_email: str | None = None
new_phone: str | None = None
Expand Down Expand Up @@ -195,7 +193,7 @@ def validate_password(self, field: forms.Field) -> None:
"""Validate password if provided."""
# If there is already an error in the password field, don't bother validating.
# This will be a `Length` validation error, but that one unfortunately does not
# raise `StopValidation`. If the length is off, we can end rightaway.
# raise `StopValidation`. If the length is off, we can end right away.
if field.errors:
return

Expand Down Expand Up @@ -244,7 +242,7 @@ def validate_password(self, field: forms.Field) -> None:
# supports both outcomes.

# `check_password_strength(password).is_weak` is a bool
self.weak_password: bool = check_password_strength(field.data).is_weak
self.weak_password = check_password_strength(field.data).is_weak


@Account.forms('logout')
Expand Down
7 changes: 4 additions & 3 deletions funnel/forms/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,10 @@ class ProfileTransitionForm(forms.Form):

def set_queries(self) -> None:
"""Prepare form for use."""
self.transition.choices = list(
self.edit_obj.profile_state.transitions().items()
)
self.transition.choices = [
(k, v.data['title'])
for k, v in self.edit_obj.profile_state.transitions().items()
]


@Account.forms('logo')
Expand Down
12 changes: 9 additions & 3 deletions funnel/forms/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,11 @@ class ProjectNameForm(forms.Form):
),
validators=[
forms.validators.DataRequired(),
forms.validators.Length(max=Project.__name_length__),
forms.validators.Length(
max=Project.__name_length__
if Project.__name_length__ is not None
else -1
),
forms.validators.ValidName(
__(
"This URL contains unsupported characters. It can contain lowercase"
Expand Down Expand Up @@ -278,7 +282,9 @@ class ProjectTransitionForm(forms.Form):

def set_queries(self) -> None:
"""Prepare form for use."""
self.transition.choices = list(self.edit_obj.state.transitions().items())
self.transition.choices = [
(k, v.data['title']) for k, v in self.edit_obj.state.transitions().items()
]


@Project.forms('cfp_transition')
Expand Down Expand Up @@ -354,7 +360,7 @@ def set_queries(self) -> None:
# options in the form even without an Rsvp instance.
self.transition.choices = [
(transition_name, getattr(Rsvp, transition_name))
for transition_name in Rsvp.state.statemanager.transitions
for transition_name in Rsvp.state.transitions
]


Expand Down
2 changes: 1 addition & 1 deletion funnel/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class GeonameModel(ModelBase, DeclarativeBase):
# This must be set _before_ any of the models using db.Model are imported
TimestampMixin.__with_timezone__ = True

db = SQLAlchemy(query_class=Query, metadata=Model.metadata) # type: ignore[arg-type]
db: SQLAlchemy = SQLAlchemy(query_class=Query, metadata=Model.metadata) # type: ignore[arg-type]
Model.init_flask_sqlalchemy(db)
GeonameModel.init_flask_sqlalchemy(db)

Expand Down
7 changes: 4 additions & 3 deletions funnel/models/account.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Account model with subtypes, and account-linked personal data models."""
# pylint: disable=unnecessary-lambda,invalid-unary-operand-type
# pyright: reportGeneralTypeIssues=false

from __future__ import annotations

Expand All @@ -19,7 +20,7 @@
from sqlalchemy.ext.hybrid import Comparator
from sqlalchemy.sql.expression import ColumnElement
from werkzeug.utils import cached_property
from zbase32 import decode as zbase32_decode, encode as zbase32_encode # type: ignore
from zbase32 import decode as zbase32_decode, encode as zbase32_encode

from baseframe import __
from coaster.sqlalchemy import (
Expand Down Expand Up @@ -2334,8 +2335,8 @@ class AccountEmailClaim(EmailAddressMixin, BaseMixin, Model):
def __init__(self, *, account: Account, email: str, **kwargs) -> None:
kwargs['email_address'] = EmailAddress.add_for(account, email)
super().__init__(account=account, **kwargs)
self.blake2b = hashlib.blake2b( # self.email is not optional, so this ignore:
self.email.lower().encode(), digest_size=16 # type: ignore[union-attr]
self.blake2b = hashlib.blake2b(
self.email.lower().encode(), digest_size=16
).digest()

def __repr__(self) -> str:
Expand Down
2 changes: 1 addition & 1 deletion funnel/models/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ def message(self) -> MessageComposite | MarkdownCompositeBasic:
@message.inplace.setter
def _message_setter(self, value: Any) -> None:
"""Edit the message of a comment."""
self._message = value # type: ignore[assignment]
self._message = value

@message.inplace.expression
@classmethod
Expand Down
2 changes: 1 addition & 1 deletion funnel/models/geoname.py
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,7 @@ class GeoAltName(BaseMixin, GeonameModel):
sa.Unicode, nullable=True, index=True
)
title: Mapped[str] = sa_orm.mapped_column(sa.Unicode, nullable=False)
is_preferred_name: Mapped[str] = sa_orm.mapped_column(sa.Boolean, nullable=False)
is_preferred_name: Mapped[bool] = sa_orm.mapped_column(sa.Boolean, nullable=False)
is_short_name: Mapped[bool] = sa_orm.mapped_column(sa.Boolean, nullable=False)
is_colloquial: Mapped[bool] = sa_orm.mapped_column(sa.Boolean, nullable=False)
is_historic: Mapped[bool] = sa_orm.mapped_column(sa.Boolean, nullable=False)
Expand Down
2 changes: 1 addition & 1 deletion funnel/models/membership_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ class ImmutableMembershipMixin(UuidMixin, BaseMixin[UUID]):
#: subclasses)
parent_id: Mapped[int] | None
#: Parent object
parent: Mapped[Model | None]
parent: Mapped[Model] | None
#: Subject of this membership (subclasses must define)
member: Mapped[Account]

Expand Down
10 changes: 4 additions & 6 deletions funnel/models/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,25 +187,23 @@ class NotificationCategory:
__("Projects I am participating in"),
# Criteria: User has registered or proposed
lambda user: (
db.session.query(user.rsvps.exists()).scalar() # type: ignore[has-type]
or db.session.query( # type: ignore[has-type]
user.proposal_memberships.exists()
).scalar()
db.session.query(user.rsvps.exists()).scalar()
or db.session.query(user.proposal_memberships.exists()).scalar()
),
),
project_crew=NotificationCategory(
4,
__("Projects I am a crew member in"),
# Criteria: user has ever been a project crew member
lambda user: db.session.query( # type: ignore[has-type]
lambda user: db.session.query(
user.projects_as_crew_memberships.exists()
).scalar(),
),
account_admin=NotificationCategory(
5,
__("Accounts I manage"),
# Criteria: user has ever been an organization admin
lambda user: db.session.query( # type: ignore[has-type]
lambda user: db.session.query(
user.organization_admin_memberships.exists()
).scalar(),
),
Expand Down
17 changes: 8 additions & 9 deletions funnel/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ class Project(UuidMixin, BaseScopedNameMixin, Model):
index=True,
)
state = with_roles(
StateManager('_state', PROJECT_STATE, doc="Project state"), call={'all'}
StateManager['Project']('_state', PROJECT_STATE, doc="Project state"),
call={'all'},
)
_cfp_state: Mapped[int] = sa_orm.mapped_column(
'cfp_state',
Expand All @@ -166,7 +167,7 @@ class Project(UuidMixin, BaseScopedNameMixin, Model):
index=True,
)
cfp_state = with_roles(
StateManager('_cfp_state', CFP_STATE, doc="CfP state"), call={'all'}
StateManager['Project']('_cfp_state', CFP_STATE, doc="CfP state"), call={'all'}
)

#: State of RSVPs
Expand Down Expand Up @@ -595,9 +596,7 @@ def rooms(self):
cfp_state.add_conditional_state(
'HAS_PROPOSALS',
cfp_state.ANY,
lambda project: db.session.query( # type: ignore[has-type]
project.proposals.exists()
).scalar(),
lambda project: db.session.query(project.proposals.exists()).scalar(),
label=('has_proposals', __("Has submissions")),
)
cfp_state.add_conditional_state(
Expand Down Expand Up @@ -1607,11 +1606,11 @@ def __repr__(self) -> str:
.where(Session.start_at.is_not(None))
.where(Session.start_at >= sa.func.utcnow())
.where(Session.project_id == Project.id)
.correlate_except(Session) # type: ignore[arg-type]
.correlate_except(Session)
.union(
sa.select(Project.start_at.label('start_at')) # type: ignore[has-type]
.where(Project.start_at.is_not(None)) # type: ignore[has-type]
.where(Project.start_at >= sa.func.utcnow()) # type: ignore[has-type]
sa.select(Project.start_at.label('start_at'))
.where(Project.start_at.is_not(None))
.where(Project.start_at >= sa.func.utcnow())
.correlate(Project)
)
.subquery()
Expand Down
2 changes: 1 addition & 1 deletion funnel/models/project_membership.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ class ProjectMembership(ImmutableUserMembershipMixin, Model):
def __table_args__(cls) -> tuple:
"""Table arguments."""
try:
args = list(super().__table_args__) # type: ignore[misc]
args = list(super().__table_args__)
except AttributeError:
args = []
kwargs = args.pop(-1) if args and isinstance(args[-1], dict) else None
Expand Down
4 changes: 1 addition & 3 deletions funnel/models/reorder_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,7 @@ def reorder_item(self: Reorderable, other: Reorderable, before: bool) -> None:
new_seq_number = self.seq
# Temporarily give self an out-of-bounds number
self.seq = (
sa.select( # type: ignore[assignment]
sa.func.coalesce(sa.func.max(cls.seq) + 1, 1)
)
sa.select(sa.func.coalesce(sa.func.max(cls.seq) + 1, 1))
.where(self.parent_scoped_reorder_query_filter)
.scalar_subquery()
)
Expand Down
Loading

0 comments on commit 72a714b

Please sign in to comment.