From d2485b06cef4e526781ebafc2fd70ccaa6a09285 Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Sat, 12 Mar 2022 08:53:28 -0600 Subject: [PATCH] Upgrade to Flask 2 --- KerbalStuff/app.py | 13 ++++++----- KerbalStuff/blueprints/login_oauth.py | 2 -- KerbalStuff/common.py | 19 ---------------- KerbalStuff/database.py | 3 +-- KerbalStuff/kerbdown.py | 17 +++++++------- KerbalStuff/stubs/jinja2.pyi | 8 ------- .../2020_06_23_17_49_36-85be165bc5dc.py | 3 +-- .../2020_07_20_19_00_00-73c9d707134b.py | 2 +- .../2021_06_28_19_00_00-17fbd4ff8193.py | 2 +- requirements-backend.txt | 4 ++-- requirements-tests.txt | 2 -- spacedock | 4 ++-- tests/fixtures/fake_config.py | 2 ++ tests/test_api_browse.py | 10 ++++----- tests/test_api_errors.py | 8 +++---- tests/test_api_mod.py | 22 +++++++++---------- tests/test_errors.py | 6 ++--- tests/test_version.py | 4 ++-- 18 files changed, 51 insertions(+), 80 deletions(-) delete mode 100644 KerbalStuff/stubs/jinja2.pyi diff --git a/KerbalStuff/app.py b/KerbalStuff/app.py index cceb6aeb..402e6b9a 100644 --- a/KerbalStuff/app.py +++ b/KerbalStuff/app.py @@ -15,7 +15,8 @@ from flask_login import LoginManager, current_user from flaskext.markdown import Markdown from sqlalchemy import desc -from werkzeug.exceptions import HTTPException, InternalServerError +from werkzeug.exceptions import HTTPException, InternalServerError, NotFound +from flask.typing import ResponseReturnValue from jinja2 import ChainableUndefined from .blueprints.accounts import accounts @@ -129,8 +130,8 @@ def load_user(username: str) -> User: # 1 | 1 | 1 | 4XX in API -> jsonified_exception(e) -@app.errorhandler(404) -def handle_404(e: HTTPException) -> Union[Tuple[str, int], werkzeug.wrappers.Response]: +@app.errorhandler(NotFound) +def handle_404(e: NotFound) -> ResponseReturnValue: # Switch out the default message if e.description == werkzeug.exceptions.NotFound.description: e.description = "Requested page not found. Looks like this was deleted, or maybe was never here." @@ -143,7 +144,7 @@ def handle_404(e: HTTPException) -> Union[Tuple[str, int], werkzeug.wrappers.Res # This one handles the remaining 4XX errors. JSONified for XHR requests, otherwise the user gets a nice error screen. @app.errorhandler(HTTPException) -def handle_http_exception(e: HTTPException) -> Union[Tuple[str, int], werkzeug.wrappers.Response]: +def handle_http_exception(e: HTTPException) -> ResponseReturnValue: if e.code and e.code >= 500: return handle_generic_exception(e) if request.path.startswith("/api/") \ @@ -155,7 +156,7 @@ def handle_http_exception(e: HTTPException) -> Union[Tuple[str, int], werkzeug.w # And this one handles everything leftover, that means, real otherwise unhandled exceptions. # https://flask.palletsprojects.com/en/1.1.x/errorhandling/#unhandled-exceptions @app.errorhandler(Exception) -def handle_generic_exception(e: Union[Exception, HTTPException]) -> Union[Tuple[str, int], werkzeug.wrappers.Response]: +def handle_generic_exception(e: Exception) -> ResponseReturnValue: site_logger.exception(e) try: db.rollback() @@ -179,7 +180,7 @@ def handle_generic_exception(e: Union[Exception, HTTPException]) -> Union[Tuple[ @app.teardown_request -def teardown_request(exception: Optional[Exception]) -> None: +def teardown_request(exception: Optional[BaseException]) -> None: db.close() diff --git a/KerbalStuff/blueprints/login_oauth.py b/KerbalStuff/blueprints/login_oauth.py index 108978a3..6b0d4703 100644 --- a/KerbalStuff/blueprints/login_oauth.py +++ b/KerbalStuff/blueprints/login_oauth.py @@ -66,8 +66,6 @@ def get_github_oath() -> Tuple[str, OAuthRemoteApp]: if resp is None: raise Exception( f"Access denied: reason={request.args['error']} error={request.args['error_description']}") - if 'error' in resp: - return jsonify(resp) session['github_token'] = (resp['access_token'], '') gh_info = github.get('user').data return gh_info['login'], github diff --git a/KerbalStuff/common.py b/KerbalStuff/common.py index c2867a91..aa6f6343 100644 --- a/KerbalStuff/common.py +++ b/KerbalStuff/common.py @@ -127,25 +127,6 @@ def wrapper(*args: str, **kwargs: int) -> werkzeug.wrappers.Response: return wrapper -def cors(f: Callable[..., Any]) -> Callable[..., Any]: - @wraps(f) - def wrapper(*args: str, **kwargs: int) -> Tuple[str, int]: - res = f(*args, **kwargs) - if request.headers.get('x-cors-status', False): - if isinstance(res, tuple): - json_text = res[0].data - code = res[1] - else: - json_text = res.data - code = 200 - o = json.loads(json_text) - o['x-status'] = code - return jsonify(o) - return res - - return wrapper - - def paginate_query(query: Query, page_size: int = 30) -> Tuple[List[Mod], int, int]: total_pages = math.ceil(query.count() / page_size) page = get_page() diff --git a/KerbalStuff/database.py b/KerbalStuff/database.py index ede15f36..a66567ec 100644 --- a/KerbalStuff/database.py +++ b/KerbalStuff/database.py @@ -1,6 +1,5 @@ from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy.orm import scoped_session, sessionmaker, declarative_base from .config import _cfg diff --git a/KerbalStuff/kerbdown.py b/KerbalStuff/kerbdown.py index 180f04ef..8e29c033 100644 --- a/KerbalStuff/kerbdown.py +++ b/KerbalStuff/kerbdown.py @@ -1,11 +1,11 @@ import urllib.parse from urllib.parse import parse_qs, urlparse -from typing import Dict, Any, Match, Tuple +from typing import Dict, Any, Match, Tuple, Optional from markdown import Markdown from markdown.extensions import Extension from markdown.inlinepatterns import InlineProcessor -from markdown.util import etree +from xml.etree import ElementTree class EmbedInlineProcessor(InlineProcessor): @@ -23,18 +23,19 @@ def __init__(self, md: Markdown, configs: Dict[str, Any]) -> None: super().__init__(self.EMBED_RE, md) self.config = configs - def handleMatch(self, m: Match[str], data: str) -> Tuple[etree.Element, int, int]: # type: ignore[override] + def handleMatch(self, m: Match[str], data: str) -> Tuple[ElementTree.Element, int, int]: # type: ignore[override] d = m.groupdict() url = d.get('url') + el: Optional[ElementTree.Element] if not url: - el = etree.Element('span') + el = ElementTree.Element('span') el.text = "[[]]" return el, m.start(0), m.end(0) try: link = urlparse(url) host = link.hostname except: - el = etree.Element('span') + el = ElementTree.Element('span') el.text = "[[" + url + "]]" return el, m.start(0), m.end(0) el = None @@ -44,7 +45,7 @@ def handleMatch(self, m: Match[str], data: str) -> Tuple[etree.Element, int, int except: pass if el is None: - el = etree.Element('span') + el = ElementTree.Element('span') el.text = "[[" + url + "]]" return el, m.start(0), m.end(0) @@ -52,8 +53,8 @@ def _get_youtube_id(self, link: urllib.parse.ParseResult) -> str: return (link.path if link.netloc == 'youtu.be' else parse_qs(link.query)['v'][0]) - def _embed_youtube(self, vid_id: str) -> etree.Element: - el = etree.Element('iframe') + def _embed_youtube(self, vid_id: str) -> ElementTree.Element: + el = ElementTree.Element('iframe') el.set('width', '100%') el.set('height', '600') el.set('frameborder', '0') diff --git a/KerbalStuff/stubs/jinja2.pyi b/KerbalStuff/stubs/jinja2.pyi deleted file mode 100644 index dcadd70a..00000000 --- a/KerbalStuff/stubs/jinja2.pyi +++ /dev/null @@ -1,8 +0,0 @@ - -from jinja2.environment import Template as Template - -class Undefined: - ... - -class ChainableUndefined(Undefined): - ... diff --git a/alembic/versions/2020_06_23_17_49_36-85be165bc5dc.py b/alembic/versions/2020_06_23_17_49_36-85be165bc5dc.py index 6f45e79e..0d5ec6bc 100644 --- a/alembic/versions/2020_06_23_17_49_36-85be165bc5dc.py +++ b/alembic/versions/2020_06_23_17_49_36-85be165bc5dc.py @@ -10,8 +10,7 @@ from packaging import version from sqlalchemy import orm, Column, Integer, Unicode, DateTime, String, ForeignKey -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relationship, backref +from sqlalchemy.orm import relationship, backref, declarative_base # revision identifiers, used by Alembic. revision = '85be165bc5dc' diff --git a/alembic/versions/2020_07_20_19_00_00-73c9d707134b.py b/alembic/versions/2020_07_20_19_00_00-73c9d707134b.py index 3119d2e6..b41c5804 100644 --- a/alembic/versions/2020_07_20_19_00_00-73c9d707134b.py +++ b/alembic/versions/2020_07_20_19_00_00-73c9d707134b.py @@ -14,7 +14,7 @@ from alembic import op import sqlalchemy as sa -Base = sa.ext.declarative.declarative_base() +Base = sa.orm.declarative_base() class ModVersion(Base): # type: ignore diff --git a/alembic/versions/2021_06_28_19_00_00-17fbd4ff8193.py b/alembic/versions/2021_06_28_19_00_00-17fbd4ff8193.py index bfd46f40..0e792053 100644 --- a/alembic/versions/2021_06_28_19_00_00-17fbd4ff8193.py +++ b/alembic/versions/2021_06_28_19_00_00-17fbd4ff8193.py @@ -13,7 +13,7 @@ from alembic import op import sqlalchemy as sa -Base = sa.ext.declarative.declarative_base() +Base = sa.orm.declarative_base() class User(Base): # type: ignore diff --git a/requirements-backend.txt b/requirements-backend.txt index 65b15863..e5dd0e0a 100644 --- a/requirements-backend.txt +++ b/requirements-backend.txt @@ -6,14 +6,14 @@ celery click dnspython flameprof -Flask<2 # Needs testing before upgrading +Flask Flask-Login Flask-Markdown Flask-OAuthlib future GitPython gunicorn -Jinja2<3 # See Flask +Jinja2 Markdown MarkupSafe oauthlib diff --git a/requirements-tests.txt b/requirements-tests.txt index 3e4ab57a..491c010e 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -4,8 +4,6 @@ flask-api flask-testing types-Markdown types-bleach -types-Flask -types-Jinja2 types-Markdown types-MarkupSafe types-Werkzeug diff --git a/spacedock b/spacedock index af0062a3..752f479e 100755 --- a/spacedock +++ b/spacedock @@ -33,8 +33,8 @@ def wait_database(): from sqlalchemy.engine.url import URL site_logger.info('Waiting for database to come online...') u = engine.url - pg_engine = create_engine(URL(u.drivername, u.username, u.password, - u.host, u.port)) + pg_engine = create_engine(URL.create(u.drivername, u.username, u.password, + u.host, u.port)) while True: try: connection = pg_engine.connect() diff --git a/tests/fixtures/fake_config.py b/tests/fixtures/fake_config.py index 536846e1..e9e06648 100644 --- a/tests/fixtures/fake_config.py +++ b/tests/fixtures/fake_config.py @@ -10,5 +10,7 @@ config[env]['protocol'] = 'https' config[env]['domain'] = 'tests.spacedock.info' config[env]['ksp-game-id'] = '1' +if 'profile-dir' in config[env]: + del config[env]['profile-dir'] dummy = '' diff --git a/tests/test_api_browse.py b/tests/test_api_browse.py index c8d3b630..e73ace82 100644 --- a/tests/test_api_browse.py +++ b/tests/test_api_browse.py @@ -3,7 +3,7 @@ import pytest from flask.testing import FlaskClient from flask import Response -from flask_api import status +from http import HTTPStatus from .fixtures.client import client from KerbalStuff.objects import Publisher, Game, GameVersion, User, Mod, ModVersion @@ -21,14 +21,14 @@ def test_api_browse(client: 'FlaskClient[Response]') -> None: featured_resp = client.get('/api/browse/featured') # Assert - assert browse_resp.status_code == status.HTTP_200_OK, 'Request should succeed' + assert browse_resp.status_code == HTTPStatus.OK, 'Request should succeed' assert browse_resp.data == b'{"total":0,"count":30,"pages":1,"page":1,"result":[]}', 'Should be a simple empty db' - assert new_resp.status_code == status.HTTP_200_OK, 'Request should succeed' + assert new_resp.status_code == HTTPStatus.OK, 'Request should succeed' assert new_resp.data == b'[]', 'Should return empty list' - assert top_resp.status_code == status.HTTP_200_OK, 'Request should succeed' + assert top_resp.status_code == HTTPStatus.OK, 'Request should succeed' assert top_resp.data == b'[]', 'Should return empty list' - assert featured_resp.status_code == status.HTTP_200_OK, 'Request should succeed' + assert featured_resp.status_code == HTTPStatus.OK, 'Request should succeed' assert featured_resp.data == b'[]', 'Should return empty list' diff --git a/tests/test_api_errors.py b/tests/test_api_errors.py index dec1be2f..e01551f6 100644 --- a/tests/test_api_errors.py +++ b/tests/test_api_errors.py @@ -1,7 +1,7 @@ import pytest from flask.testing import FlaskClient from flask import Response -from flask_api import status +from http import HTTPStatus from .fixtures.client import client @@ -14,8 +14,8 @@ def test_api_bad_url(client: 'FlaskClient[Response]') -> None: bad_url_resp = client.get('/api/something_that_matches_no_routes/69/420') # Assert - assert bad_url_resp.status_code == status.HTTP_404_NOT_FOUND, 'Request should fail' - assert bad_url_resp.json['code'] == status.HTTP_404_NOT_FOUND, 'Code should match' + assert bad_url_resp.status_code == HTTPStatus.NOT_FOUND, 'Request should fail' + assert bad_url_resp.json['code'] == HTTPStatus.NOT_FOUND, 'Code should match' assert bad_url_resp.json['error'] == True, 'Should contain "error" property' assert 'not found' in bad_url_resp.json['reason'], 'Reason should be typical 404 lingo' @@ -28,6 +28,6 @@ def test_api_mod_not_found(client: 'FlaskClient[Response]') -> None: missing_mod_resp = client.get('/api/mod/20000') # Assert - assert missing_mod_resp.status_code == status.HTTP_404_NOT_FOUND, 'Request should fail' + assert missing_mod_resp.status_code == HTTPStatus.NOT_FOUND, 'Request should fail' assert missing_mod_resp.json['error'] == True, 'Should contain "error" property' assert missing_mod_resp.json['reason'] == 'Mod not found.', 'Reason should match' diff --git a/tests/test_api_mod.py b/tests/test_api_mod.py index f9eefaf4..3419ed34 100644 --- a/tests/test_api_mod.py +++ b/tests/test_api_mod.py @@ -4,7 +4,7 @@ import pytest from flask.testing import FlaskClient from flask import Response -from flask_api import status +from http import HTTPStatus from .fixtures.client import client from KerbalStuff.objects import Publisher, Game, GameVersion, User, Mod, ModVersion @@ -65,36 +65,36 @@ def test_api_mod(client: 'FlaskClient[Response]') -> None: search_user_resp = client.get('/api/search/user?query=Test&page=0') # Assert - assert mod_resp.status_code == status.HTTP_200_OK, 'Request should succeed' + assert mod_resp.status_code == HTTPStatus.OK, 'Request should succeed' check_mod(mod_resp.json) # Not returned by all APIs assert mod_resp.json['description'] == 'A mod that we will use to test the API', 'Short description should match' - assert kspversions_resp.status_code == status.HTTP_200_OK, 'Request should succeed' + assert kspversions_resp.status_code == HTTPStatus.OK, 'Request should succeed' check_game_version(kspversions_resp.json[0]) - assert gameversions_resp.status_code == status.HTTP_200_OK, 'Request should succeed' + assert gameversions_resp.status_code == HTTPStatus.OK, 'Request should succeed' check_game_version(gameversions_resp.json[0]) - assert games_resp.status_code == status.HTTP_200_OK, 'Request should succeed' + assert games_resp.status_code == HTTPStatus.OK, 'Request should succeed' check_game(games_resp.json[0]) - assert publishers_resp.status_code == status.HTTP_200_OK, 'Request should succeed' + assert publishers_resp.status_code == HTTPStatus.OK, 'Request should succeed' check_publisher(publishers_resp.json[0]) - assert mod_version_resp.status_code == status.HTTP_200_OK, 'Request should succeed' + assert mod_version_resp.status_code == HTTPStatus.OK, 'Request should succeed' check_mod_version(mod_version_resp.json) - assert user_resp.status_code == status.HTTP_200_OK, 'Request should succeed' + assert user_resp.status_code == HTTPStatus.OK, 'Request should succeed' check_user(user_resp.json) - assert typeahead_resp.status_code == status.HTTP_200_OK, 'Request should succeed' + assert typeahead_resp.status_code == HTTPStatus.OK, 'Request should succeed' check_mod(typeahead_resp.json[0]) - assert search_mod_resp.status_code == status.HTTP_200_OK, 'Request should succeed' + assert search_mod_resp.status_code == HTTPStatus.OK, 'Request should succeed' check_mod(search_mod_resp.json[0]) - assert search_user_resp.status_code == status.HTTP_200_OK, 'Request should succeed' + assert search_user_resp.status_code == HTTPStatus.OK, 'Request should succeed' check_user(search_user_resp.json[0]) diff --git a/tests/test_errors.py b/tests/test_errors.py index d4c3f5ad..fcad43ef 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -1,7 +1,7 @@ import pytest from flask.testing import FlaskClient from flask import Response -from flask_api import status +from http import HTTPStatus from .fixtures.client import client @@ -14,7 +14,7 @@ def test_bad_url(client: 'FlaskClient[Response]') -> None: bad_url_resp = client.get('/something_that_matches_no_routes/69/420') # Assert - assert bad_url_resp.status_code == status.HTTP_404_NOT_FOUND, 'Request should fail' + assert bad_url_resp.status_code == HTTPStatus.NOT_FOUND, 'Request should fail' assert bad_url_resp.json is None, 'Should not be JSON' assert bad_url_resp.mimetype == 'text/html', 'Should be HTML' assert b'Not Found' in bad_url_resp.data, 'Should be a nice web page' @@ -29,7 +29,7 @@ def test_mod_not_found(client: 'FlaskClient[Response]') -> None: missing_mod_resp = client.get('/mod/20000') # Assert - assert missing_mod_resp.status_code == status.HTTP_404_NOT_FOUND, 'Request should fail' + assert missing_mod_resp.status_code == HTTPStatus.NOT_FOUND, 'Request should fail' assert missing_mod_resp.json is None, 'Should not be JSON' assert missing_mod_resp.mimetype == 'text/html', 'Should be HTML' assert b'Not Found' in missing_mod_resp.data, 'Should be a nice web page' diff --git a/tests/test_version.py b/tests/test_version.py index 5c44be69..b1a88a76 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,7 +1,7 @@ import pytest from flask.testing import FlaskClient from flask import Response -from flask_api import status +from http import HTTPStatus from .fixtures.client import client @@ -14,7 +14,7 @@ def test_version(client: 'FlaskClient[Response]') -> None: resp = client.get('/version') # Assert - assert resp.status_code == status.HTTP_200_OK, 'Request should succeed' + assert resp.status_code == HTTPStatus.OK, 'Request should succeed' assert resp.data.startswith(b'commit'), 'Response should start with "commit"' assert b'\nAuthor: ' in resp.data, 'Response should return a Author header' assert b'\nDate: ' in resp.data, 'Response should return a Date header'