Skip to content

Commit

Permalink
Modernize ClassView and ModelView (#433)
Browse files Browse the repository at this point in the history
ClassView's legacy architecture dating to the Python 2.7 era has been thoroughly revised for Py3, with full static type checking support. Coincidental fixes in other modules that were noticed during the overhaul are also included.
  • Loading branch information
jace committed Dec 12, 2023
1 parent 1cbca26 commit da9a736
Show file tree
Hide file tree
Showing 24 changed files with 1,036 additions and 672 deletions.
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import typing as t

sys.path.insert(0, os.path.abspath('../src'))
from coaster import _version # isort:skip # pylint: disable=wrong-import-position
from coaster import _version # isort:skip

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
Expand Down
15 changes: 7 additions & 8 deletions src/coaster/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
mod_tomli: t.Optional[types.ModuleType] = None
mod_yaml: t.Optional[types.ModuleType] = None

try: # pragma: no cover
try:
import toml as mod_toml # type: ignore[no-redef,unused-ignore]
except ModuleNotFoundError:
try:
Expand All @@ -52,7 +52,7 @@
pass


try: # pragma: no cover
try:
import yaml as mod_yaml
except ModuleNotFoundError:
pass
Expand Down Expand Up @@ -106,7 +106,7 @@ class ConfigLoader(NamedTuple):
# --- Key rotation wrapper -------------------------------------------------------------


class KeyRotationWrapper(t.Generic[_S]): # pylint: disable=too-few-public-methods
class KeyRotationWrapper(t.Generic[_S]):
"""
Wrapper to support multiple secret keys in itsdangerous.
Expand Down Expand Up @@ -193,7 +193,7 @@ class JSONProvider(DefaultJSONProvider):

@staticmethod
def default(o: t.Any) -> t.Any:
"""Expand default support to check for `__json__`."""
"""Expand default support to check for a ``__json__`` method."""
if hasattr(o, '__json__'):
return o.__json__()
if isinstance(o, abc.Mapping):
Expand Down Expand Up @@ -330,11 +330,10 @@ def load_config_from_file(
try:
if load is None:
return app.config.from_pyfile(filepath)
# The `text` parameter requires Flask 2.3. We still support Flask 2.2
# The `text` parameter was introduced in Flask 2.3, but its default value
# may change in a future release, so we only supply a value if we have a bool
if text is not None:
return app.config.from_file( # type: ignore[call-arg]
filepath, load=load, text=text
)
return app.config.from_file(filepath, load=load, text=text)
return app.config.from_file(filepath, load=load)
except OSError:
app.logger.warning(
Expand Down
24 changes: 13 additions & 11 deletions src/coaster/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
from threading import Lock
from typing import cast

from flask import Flask, current_app, g, request
from flask import Flask, current_app, g
from flask.globals import request_ctx
from werkzeug.local import LocalProxy
from werkzeug.wrappers import Response as BaseResponse

Expand Down Expand Up @@ -108,9 +109,9 @@ def request_has_auth() -> bool:
to set cookies or perform other housekeeping functions.
"""
return (
bool(request)
and hasattr(request, '_current_auth')
and 'actor' in request._current_auth.__dict__
bool(request_ctx)
and hasattr(request_ctx, 'current_auth')
and 'actor' in request_ctx.current_auth.__dict__
)


Expand Down Expand Up @@ -162,10 +163,10 @@ def __setattr__(self, attr: str, value: t.Any) -> t.NoReturn:
# This test is used to allow in-place mutations such as:
# current_auth.permissions |= {extra}
return # type: ignore[misc]
raise AttributeError('CurrentAuth is read-only')
raise TypeError('current_auth is read-only')

def __delattr__(self, attr: str) -> t.NoReturn:
raise AttributeError('CurrentAuth is read-only')
raise TypeError('current_auth is read-only')

def __contains__(self, attr: str) -> bool:
"""Check for presence of an attribute."""
Expand Down Expand Up @@ -197,12 +198,12 @@ def __getattr__(self, attr: str) -> t.Any:
def _call_login_manager(self) -> None:
"""Call the app's login manager on first access of user or actor (internal)."""
# Check for an existing user from Flask-Login
if not request:
if not request_ctx:
# There's no request context for a login manager to operate on
return
# If there's no existing user, look for a login manager
if (
request
request_ctx
and hasattr(current_app, 'login_manager')
and hasattr(current_app.login_manager, '_load_user')
):
Expand Down Expand Up @@ -258,13 +259,14 @@ def init_app(app: Flask) -> None:
def _get_current_auth() -> CurrentAuth:
"""Provide current_auth for the request context."""
# 1. Do we have a request context?
if request:
if request_ctx:
with _get_lock:
# 2. Does this request already have current_auth? If so, return it
ca = getattr(request, '_current_auth', None)
ca = getattr(request_ctx, 'current_auth', None)
if ca is None:
# 3. If not, create it
request._current_auth = ca = CurrentAuth() # type: ignore[attr-defined]
ca = CurrentAuth()
request_ctx.current_auth = ca # type: ignore[attr-defined]
# 4. Return current_auth
return ca

Expand Down
4 changes: 2 additions & 2 deletions src/coaster/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@

from .sqlalchemy import Query

try: # pragma: no cover
try:
from psycopg2.extensions import connection as Psycopg2Connection # noqa: N812
except ModuleNotFoundError:
Psycopg2Connection = None

try: # pragma: no cover
try:
from psycopg import Connection as Psycopg3Connection
except ModuleNotFoundError:
Psycopg3Connection = None # type: ignore[assignment,misc]
Expand Down
2 changes: 1 addition & 1 deletion src/coaster/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from flask import g, request, session
from flask.config import Config

try: # Flask >= 3.0 # pragma: no cover
try: # Flask >= 3.0
from flask.sansio.app import App as FlaskApp
except ModuleNotFoundError:
from flask import Flask as FlaskApp
Expand Down
6 changes: 3 additions & 3 deletions src/coaster/sqlalchemy/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,17 @@ def make_timestamp_columns(


@overload
def failsafe_add(__session: session_type, __instance: t.Any) -> None:
def failsafe_add(__session: session_type, __instance: t.Any, /) -> None:
...


@overload
def failsafe_add(__session: session_type, __instance: T, **filters: t.Any) -> T:
def failsafe_add(__session: session_type, __instance: T, /, **filters: t.Any) -> T:
...


def failsafe_add(
__session: session_type, __instance: T, **filters: t.Any
__session: session_type, __instance: T, /, **filters: t.Any
) -> t.Optional[T]:
"""
Add and commit a new instance in a nested transaction (using SQL SAVEPOINT).
Expand Down
4 changes: 0 additions & 4 deletions src/coaster/sqlalchemy/markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,6 @@ def __eq__(self, other: t.Any) -> bool:
self.__composite_values__() == other.__composite_values__()
)

def __ne__(self, other: t.Any) -> bool:
"""Compare for inequality."""
return not self.__eq__(other)

# Pickle support methods implemented as per SQLAlchemy documentation, but not
# tested here as we don't use them.
# https://docs.sqlalchemy.org/en/13/orm/extensions/mutable.html#id1
Expand Down
Loading

0 comments on commit da9a736

Please sign in to comment.