From 06d662982c36dfce745c1320beb3223ba9019f86 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Mon, 28 Jun 2021 20:56:40 -0700 Subject: [PATCH 1/4] Rename lookup functions --- amanuensis/backend/lexicon.py | 10 +++++----- amanuensis/backend/user.py | 28 ++++++++++------------------ amanuensis/cli/lexicon.py | 9 ++++++--- amanuensis/cli/user.py | 4 ++-- amanuensis/server/auth/__init__.py | 4 ++-- tests/backend/test_lexicon.py | 4 ++-- tests/backend/test_user.py | 8 ++++---- tests/conftest.py | 2 +- 8 files changed, 32 insertions(+), 37 deletions(-) diff --git a/amanuensis/backend/lexicon.py b/amanuensis/backend/lexicon.py index 5efd406..073a4cf 100644 --- a/amanuensis/backend/lexicon.py +++ b/amanuensis/backend/lexicon.py @@ -55,11 +55,6 @@ def create( return new_lexicon -def from_name(db: DbContext, name: str) -> Lexicon: - """Get a lexicon by its name.""" - return db(select(Lexicon).where(Lexicon.name == name)).scalar_one() - - def get_all(db: DbContext) -> Sequence[Lexicon]: """Get all lexicons.""" return db(select(Lexicon)).scalars() @@ -75,3 +70,8 @@ def get_joined(db: DbContext, user_id: int) -> Sequence[Lexicon]: def get_public(db: DbContext) -> Sequence[Lexicon]: """Get all publicly visible lexicons.""" return db(select(Lexicon).where(Lexicon.public == True)).scalars() + + +def try_from_name(db: DbContext, name: str) -> Optional[Lexicon]: + """Get a lexicon by its name, or None if no such lexicon was found.""" + return db(select(Lexicon).where(Lexicon.name == name)).scalar_one_or_none() diff --git a/amanuensis/backend/user.py b/amanuensis/backend/user.py index 1283fb2..5542c8c 100644 --- a/amanuensis/backend/user.py +++ b/amanuensis/backend/user.py @@ -71,24 +71,6 @@ def create( return new_user -def from_id(db: DbContext, user_id: int) -> Optional[User]: - """ - Get a user by the user's id. - Returns None if no user was found. - """ - user: User = db(select(User).where(User.id == user_id)).scalar_one_or_none() - return user - - -def from_username(db: DbContext, username: str) -> Optional[User]: - """ - Get a user by the user's username. - Returns None if no user was found. - """ - user: User = db(select(User).where(User.username == username)).scalar_one_or_none() - return user - - def get_all(db: DbContext) -> Sequence[User]: """Get all users.""" return db(select(User)).scalars() @@ -109,6 +91,16 @@ def password_check(db: DbContext, username: str, password: str) -> bool: return check_password_hash(user_password_hash, password) +def try_from_id(db: DbContext, user_id: int) -> Optional[User]: + """Get a user by the user's id, or None is no such user was found.""" + return db(select(User).where(User.id == user_id)).scalar_one_or_none() + + +def try_from_username(db: DbContext, username: str) -> Optional[User]: + """Get a user by the user's username, or None is no such user was found.""" + return db(select(User).where(User.username == username)).scalar_one_or_none() + + def update_logged_in(db: DbContext, username: str) -> None: """Bump the value of the last_login column for a user.""" db( diff --git a/amanuensis/cli/lexicon.py b/amanuensis/cli/lexicon.py index 2d580d2..4419593 100644 --- a/amanuensis/cli/lexicon.py +++ b/amanuensis/cli/lexicon.py @@ -24,9 +24,12 @@ def command_add(args) -> int: Add a user to a lexicon. """ db: DbContext = args.get_db() - lexicon = lexiq.from_name(db, args.lexicon) - user = userq.from_username(db, args.user) - assert user is not None + lexicon = lexiq.try_from_name(db, args.lexicon) + if not lexicon: + raise ValueError("Lexicon does not exist") + user = userq.try_from_username(db, args.user) + if not user: + raise ValueError("User does not exist") memq.create(db, user.id, lexicon.id, args.editor) LOG.info(f"Added {args.user} to lexicon {args.lexicon}") return 0 diff --git a/amanuensis/cli/user.py b/amanuensis/cli/user.py index 79518eb..e28455c 100644 --- a/amanuensis/cli/user.py +++ b/amanuensis/cli/user.py @@ -29,7 +29,7 @@ def command_create(args) -> int: def command_promote(args) -> int: """Make a user a site admin.""" db: DbContext = args.get_db() - user: Optional[User] = userq.from_username(db, args.username) + user: Optional[User] = userq.try_from_username(db, args.username) if user is None: args.parser.error("User not found") return -1 @@ -46,7 +46,7 @@ def command_promote(args) -> int: def command_demote(args): """Revoke a user's site admin status.""" db: DbContext = args.get_db() - user: Optional[User] = userq.from_username(db, args.username) + user: Optional[User] = userq.try_from_username(db, args.username) if user is None: args.parser.error("User not found") return -1 diff --git a/amanuensis/server/auth/__init__.py b/amanuensis/server/auth/__init__.py index f8fc748..b88466f 100644 --- a/amanuensis/server/auth/__init__.py +++ b/amanuensis/server/auth/__init__.py @@ -39,7 +39,7 @@ def get_login_manager() -> LoginManager: user_id = int(user_id_str) except: return None - return userq.from_id(g.db, user_id) + return userq.try_from_id(g.db, user_id) login_manager.user_loader(load_user) @@ -58,7 +58,7 @@ def login(): # POST with valid data username: str = form.username.data password: str = form.password.data - user: User = userq.from_username(g.db, username) + user: User = userq.try_from_username(g.db, username) if not user or not userq.password_check(g.db, username, password): # Bad creds flash("Login not recognized") diff --git a/tests/backend/test_lexicon.py b/tests/backend/test_lexicon.py index b2f07c9..3c73d9e 100644 --- a/tests/backend/test_lexicon.py +++ b/tests/backend/test_lexicon.py @@ -58,8 +58,8 @@ def test_lexicon_from(db: DbContext, make: ObjectFactory): """Test lexiq.from_*.""" lexicon1: Lexicon = make.lexicon() lexicon2: Lexicon = make.lexicon() - assert lexiq.from_name(db, lexicon1.name) == lexicon1 - assert lexiq.from_name(db, lexicon2.name) == lexicon2 + assert lexiq.try_from_name(db, lexicon1.name) == lexicon1 + assert lexiq.try_from_name(db, lexicon2.name) == lexicon2 def test_get_lexicon(db: DbContext, make: ObjectFactory): diff --git a/tests/backend/test_user.py b/tests/backend/test_user.py index e5fc571..f657a33 100644 --- a/tests/backend/test_user.py +++ b/tests/backend/test_user.py @@ -57,10 +57,10 @@ def test_user_from(db: DbContext, make): """Test userq.from_*.""" user1: User = make.user() user2: User = make.user() - assert userq.from_id(db, user1.id) == user1 - assert userq.from_username(db, user1.username) == user1 - assert userq.from_id(db, user2.id) == user2 - assert userq.from_username(db, user2.username) == user2 + assert userq.try_from_id(db, user1.id) == user1 + assert userq.try_from_username(db, user1.username) == user1 + assert userq.try_from_id(db, user2.id) == user2 + assert userq.try_from_username(db, user2.username) == user2 def test_user_password(db: DbContext, make): diff --git a/tests/conftest.py b/tests/conftest.py index 2dccf33..ff601bb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -45,7 +45,7 @@ class UserClient: def login(self, client: FlaskClient): """Log the user in.""" - user: Optional[User] = userq.from_id(self.db, self.user_id) + user: Optional[User] = userq.try_from_id(self.db, self.user_id) assert user is not None # Set the user's password so we know what it is later -- 2.44.1 From 74fe79dbf9c0ac3ceaf201b60ebfb13d2879fdfe Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Mon, 28 Jun 2021 21:13:14 -0700 Subject: [PATCH 2/4] Make importing backends easier --- amanuensis/backend/__init__.py | 9 +++++++++ amanuensis/cli/lexicon.py | 4 +--- amanuensis/cli/user.py | 2 +- amanuensis/server/__init__.py | 5 ++--- amanuensis/server/auth/__init__.py | 2 +- amanuensis/server/home/__init__.py | 3 +-- tests/backend/test_article.py | 6 +++--- tests/backend/test_character.py | 2 +- tests/backend/test_index.py | 4 ++-- tests/backend/test_lexicon.py | 2 +- tests/backend/test_membership.py | 2 +- tests/backend/test_post.py | 2 +- tests/backend/test_user.py | 2 +- tests/conftest.py | 5 +---- 14 files changed, 26 insertions(+), 24 deletions(-) diff --git a/amanuensis/backend/__init__.py b/amanuensis/backend/__init__.py index e69de29..e0c9365 100644 --- a/amanuensis/backend/__init__.py +++ b/amanuensis/backend/__init__.py @@ -0,0 +1,9 @@ +import amanuensis.backend.article as artiq +import amanuensis.backend.character as charq +import amanuensis.backend.index as indq +import amanuensis.backend.lexicon as lexiq +import amanuensis.backend.membership as memq +import amanuensis.backend.post as postq +import amanuensis.backend.user as userq + +__all__ = ["artiq", "charq", "indq", "lexiq", "memq", "postq", "userq"] diff --git a/amanuensis/cli/lexicon.py b/amanuensis/cli/lexicon.py index 4419593..0c0e3c1 100644 --- a/amanuensis/cli/lexicon.py +++ b/amanuensis/cli/lexicon.py @@ -2,9 +2,7 @@ import logging from sqlalchemy import update -import amanuensis.backend.lexicon as lexiq -import amanuensis.backend.membership as memq -import amanuensis.backend.user as userq +from amanuensis.backend import lexiq, memq, userq from amanuensis.db import DbContext, Lexicon from .helpers import add_argument diff --git a/amanuensis/cli/user.py b/amanuensis/cli/user.py index e28455c..9506e09 100644 --- a/amanuensis/cli/user.py +++ b/amanuensis/cli/user.py @@ -1,7 +1,7 @@ import logging from typing import Optional -import amanuensis.backend.user as userq +from amanuensis.backend import userq from amanuensis.db import DbContext, User from .helpers import add_argument diff --git a/amanuensis/server/__init__.py b/amanuensis/server/__init__.py index 2f73815..d14fb5e 100644 --- a/amanuensis/server/__init__.py +++ b/amanuensis/server/__init__.py @@ -4,8 +4,7 @@ import os from flask import Flask, g -import amanuensis.backend.lexicon -import amanuensis.backend.user +from amanuensis.backend import lexiq, userq from amanuensis.config import AmanuensisConfig, CommandLineConfig from amanuensis.db import DbContext import amanuensis.server.auth as auth @@ -61,7 +60,7 @@ def get_app( app.template_filter("date")(date_format) def include_backend(): - return {"db": db, "lexiq": amanuensis.backend.lexicon, "userq": amanuensis.backend.user} + return {"db": db, "lexiq": lexiq, "userq": userq} app.context_processor(include_backend) diff --git a/amanuensis/server/auth/__init__.py b/amanuensis/server/auth/__init__.py index b88466f..4cd049b 100644 --- a/amanuensis/server/auth/__init__.py +++ b/amanuensis/server/auth/__init__.py @@ -17,7 +17,7 @@ from flask_login import ( LoginManager, ) -import amanuensis.backend.user as userq +from amanuensis.backend import userq from amanuensis.db import User from .forms import LoginForm diff --git a/amanuensis/server/home/__init__.py b/amanuensis/server/home/__init__.py index c2608b3..6103e88 100644 --- a/amanuensis/server/home/__init__.py +++ b/amanuensis/server/home/__init__.py @@ -1,7 +1,6 @@ from flask import Blueprint, render_template, g -import amanuensis.backend.user as userq -import amanuensis.backend.lexicon as lexiq +from amanuensis.backend import userq, lexiq # from .forms import LexiconCreateForm diff --git a/tests/backend/test_article.py b/tests/backend/test_article.py index 7dafe02..269a898 100644 --- a/tests/backend/test_article.py +++ b/tests/backend/test_article.py @@ -1,9 +1,9 @@ import pytest import time -from amanuensis.db import DbContext -from amanuensis.db.models import Character, Lexicon, User -import amanuensis.backend.article as artiq +from amanuensis.backend import artiq +from amanuensis.db import DbContext, Character, Lexicon, User + from amanuensis.errors import ArgumentError from tests.conftest import ObjectFactory diff --git a/tests/backend/test_character.py b/tests/backend/test_character.py index dc50302..d6808f4 100644 --- a/tests/backend/test_character.py +++ b/tests/backend/test_character.py @@ -1,7 +1,7 @@ import pytest +from amanuensis.backend import charq from amanuensis.db import * -import amanuensis.backend.character as charq from amanuensis.errors import ArgumentError diff --git a/tests/backend/test_index.py b/tests/backend/test_index.py index c5a2ac7..47128d4 100644 --- a/tests/backend/test_index.py +++ b/tests/backend/test_index.py @@ -1,8 +1,8 @@ from amanuensis.db.models import IndexType import pytest -import amanuensis.backend.index as indq -from amanuensis.db import DbContext, Lexicon, User +from amanuensis.backend import indq +from amanuensis.db import DbContext, Lexicon from amanuensis.errors import ArgumentError diff --git a/tests/backend/test_lexicon.py b/tests/backend/test_lexicon.py index 3c73d9e..8cc4f28 100644 --- a/tests/backend/test_lexicon.py +++ b/tests/backend/test_lexicon.py @@ -3,7 +3,7 @@ import time import pytest -import amanuensis.backend.lexicon as lexiq +from amanuensis.backend import lexiq from amanuensis.db import DbContext, Lexicon, User from amanuensis.errors import ArgumentError from tests.conftest import ObjectFactory diff --git a/tests/backend/test_membership.py b/tests/backend/test_membership.py index d6a0637..f62d6c1 100644 --- a/tests/backend/test_membership.py +++ b/tests/backend/test_membership.py @@ -2,9 +2,9 @@ import pytest from sqlalchemy import select +from amanuensis.backend import memq from amanuensis.db import * from amanuensis.errors import ArgumentError -import amanuensis.backend.membership as memq def test_create_membership(db: DbContext, make): diff --git a/tests/backend/test_post.py b/tests/backend/test_post.py index 773b705..d66426a 100644 --- a/tests/backend/test_post.py +++ b/tests/backend/test_post.py @@ -1,7 +1,7 @@ import pytest +from amanuensis.backend import postq from amanuensis.db import DbContext -import amanuensis.backend.post as postq from amanuensis.errors import ArgumentError diff --git a/tests/backend/test_user.py b/tests/backend/test_user.py index f657a33..43af33f 100644 --- a/tests/backend/test_user.py +++ b/tests/backend/test_user.py @@ -2,7 +2,7 @@ import os import pytest -import amanuensis.backend.user as userq +from amanuensis.backend import userq from amanuensis.db import DbContext, User from amanuensis.errors import ArgumentError diff --git a/tests/conftest.py b/tests/conftest.py index ff601bb..203da40 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,10 +10,7 @@ from bs4 import BeautifulSoup from flask.testing import FlaskClient from sqlalchemy.orm.session import close_all_sessions -import amanuensis.backend.character as charq -import amanuensis.backend.lexicon as lexiq -import amanuensis.backend.membership as memq -import amanuensis.backend.user as userq +from amanuensis.backend import charq, lexiq, memq, userq from amanuensis.config import AmanuensisConfig from amanuensis.db import DbContext, User, Lexicon, Membership, Character from amanuensis.server import get_app -- 2.44.1 From d6f558a92b682357432984890cbc1584d05a9a63 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Thu, 12 Aug 2021 19:08:42 -0700 Subject: [PATCH 3/4] Add backend to jinja context --- amanuensis/server/__init__.py | 38 ++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/amanuensis/server/__init__.py b/amanuensis/server/__init__.py index d14fb5e..5ffd770 100644 --- a/amanuensis/server/__init__.py +++ b/amanuensis/server/__init__.py @@ -2,15 +2,33 @@ from datetime import datetime, timezone import json import os -from flask import Flask, g +from flask import Flask, g, url_for -from amanuensis.backend import lexiq, userq +from amanuensis.backend import lexiq, userq, memq from amanuensis.config import AmanuensisConfig, CommandLineConfig from amanuensis.db import DbContext +from amanuensis.parser import filesafe_title import amanuensis.server.auth as auth import amanuensis.server.home as home +def date_format(dt: datetime, formatstr="%Y-%m-%d %H:%M:%S%z") -> str: + """Convert datetime to human-readable string""" + if dt is None: + return "never" + # Cast db time to UTC, then convert to local timezone + adjusted = dt.replace(tzinfo=timezone.utc).astimezone() + return adjusted.strftime(formatstr) + + +def article_link(title): + """Get the url for a lexicon by its title""" + return url_for( + 'lexicon.article', + name=g.lexicon.name, + title=filesafe_title(title)) + + def get_app( config: AmanuensisConfig, db: DbContext = None, @@ -48,20 +66,12 @@ def get_app( app.teardown_appcontext(db_teardown) # Configure jinja options - app.jinja_options.update(trim_blocks=True, lstrip_blocks=True) - - def date_format(dt: datetime, formatstr="%Y-%m-%d %H:%M:%S%z") -> str: - if dt is None: - return "never" - # Cast db time to UTC, then convert to local timezone - adjusted = dt.replace(tzinfo=timezone.utc).astimezone() - return adjusted.strftime(formatstr) - - app.template_filter("date")(date_format) - def include_backend(): - return {"db": db, "lexiq": lexiq, "userq": userq} + return {"db": db, "lexiq": lexiq, "userq": userq, "memq": memq} + app.jinja_options.update(trim_blocks=True, lstrip_blocks=True) + app.template_filter("date")(date_format) + app.template_filter("articlelink")(article_link) app.context_processor(include_backend) # Set up Flask-Login -- 2.44.1 From 7645c85c9d975858b2976ab4775f88b7857d5268 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Fri, 13 Aug 2021 16:38:47 -0700 Subject: [PATCH 4/4] Make backend argument type errors more specific --- amanuensis/backend/article.py | 8 ++++---- amanuensis/backend/character.py | 10 +++++----- amanuensis/backend/index.py | 14 +++++++------- amanuensis/backend/lexicon.py | 10 +++++----- amanuensis/backend/membership.py | 8 ++++---- amanuensis/backend/post.py | 10 +++++----- amanuensis/backend/user.py | 10 +++++----- amanuensis/errors.py | 17 +++++++++++++++-- tests/backend/test_character.py | 8 ++++---- tests/backend/test_lexicon.py | 4 ++-- tests/backend/test_post.py | 13 +++++-------- tests/backend/test_user.py | 4 ++-- 12 files changed, 63 insertions(+), 53 deletions(-) diff --git a/amanuensis/backend/article.py b/amanuensis/backend/article.py index 973c123..187e9d0 100644 --- a/amanuensis/backend/article.py +++ b/amanuensis/backend/article.py @@ -7,7 +7,7 @@ from typing import Optional from sqlalchemy import select from amanuensis.db import * -from amanuensis.errors import ArgumentError +from amanuensis.errors import ArgumentError, BackendArgumentTypeError def create( @@ -21,11 +21,11 @@ def create( """ # Verify argument types are correct if not isinstance(lexicon_id, int): - raise ArgumentError("lexicon_id") + raise BackendArgumentTypeError(int, lexicon_id=lexicon_id) if not isinstance(user_id, int): - raise ArgumentError("user_id") + raise BackendArgumentTypeError(int, user_id=user_id) if character_id is not None and not isinstance(character_id, int): - raise ArgumentError("character_id") + raise BackendArgumentTypeError(int, character_id=character_id) # Check that the user is a member of this lexicon mem: Membership = db( diff --git a/amanuensis/backend/character.py b/amanuensis/backend/character.py index 1b87b3b..01f5804 100644 --- a/amanuensis/backend/character.py +++ b/amanuensis/backend/character.py @@ -7,7 +7,7 @@ from typing import Optional from sqlalchemy import select, func from amanuensis.db import * -from amanuensis.errors import ArgumentError +from amanuensis.errors import ArgumentError, BackendArgumentTypeError def create( @@ -22,13 +22,13 @@ def create( """ # Verify argument types are correct if not isinstance(lexicon_id, int): - raise ArgumentError("lexicon_id") + raise BackendArgumentTypeError(int, lexicon_id=lexicon_id) if not isinstance(user_id, int): - raise ArgumentError("user_id") + raise BackendArgumentTypeError(int, user_id=user_id) if not isinstance(name, str): - raise ArgumentError("name") + raise BackendArgumentTypeError(str, name=name) if signature is not None and not isinstance(signature, str): - raise ArgumentError("signature") + raise BackendArgumentTypeError(str, signature=signature) # Verify character name is valid if not name.strip(): diff --git a/amanuensis/backend/index.py b/amanuensis/backend/index.py index 7f108ec..bfd259c 100644 --- a/amanuensis/backend/index.py +++ b/amanuensis/backend/index.py @@ -6,7 +6,7 @@ import re from typing import Optional from amanuensis.db import DbContext, ArticleIndex, IndexType -from amanuensis.errors import ArgumentError +from amanuensis.errors import ArgumentError, BackendArgumentTypeError def create( @@ -23,17 +23,17 @@ def create( """ # Verify argument types are correct if not isinstance(lexicon_id, int): - raise ArgumentError("lexicon_id") + raise BackendArgumentTypeError(int, lexicon_id=lexicon_id) if not isinstance(index_type, IndexType): - raise ArgumentError("index_type") + raise BackendArgumentTypeError(IndexType, index_type=index_type) if not isinstance(pattern, str): - raise ArgumentError("pattern") + raise BackendArgumentTypeError(str, pattern=pattern) if not isinstance(logical_order, int): - raise ArgumentError("logical_order") + raise BackendArgumentTypeError(int, logical_order=logical_order) if not isinstance(display_order, int): - raise ArgumentError("display_order") + raise BackendArgumentTypeError(int, display_order=display_order) if capacity is not None and not isinstance(capacity, int): - raise ArgumentError("capacity") + raise BackendArgumentTypeError(int, capacity=capacity) # Verify the pattern is valid for the index type: if index_type == IndexType.CHAR: diff --git a/amanuensis/backend/lexicon.py b/amanuensis/backend/lexicon.py index 073a4cf..ff83b34 100644 --- a/amanuensis/backend/lexicon.py +++ b/amanuensis/backend/lexicon.py @@ -8,7 +8,7 @@ from typing import Sequence, Optional from sqlalchemy import select, func from amanuensis.db import DbContext, Lexicon, Membership -from amanuensis.errors import ArgumentError +from amanuensis.errors import ArgumentError, BackendArgumentTypeError RE_ALPHANUM_DASH_UNDER = re.compile(r"^[A-Za-z0-9-_]*$") @@ -25,7 +25,7 @@ def create( """ # Verify name if not isinstance(name, str): - raise ArgumentError("Lexicon name must be a string") + raise BackendArgumentTypeError(str, name=name) if not name.strip(): raise ArgumentError("Lexicon name must not be blank") if not RE_ALPHANUM_DASH_UNDER.match(name): @@ -34,12 +34,12 @@ def create( ) # Verify title - if title is not None and not isinstance(name, str): - raise ArgumentError("Lexicon name must be a string") + if title is not None and not isinstance(title, str): + raise BackendArgumentTypeError(str, title=title) # Verify prompt if not isinstance(prompt, str): - raise ArgumentError("Lexicon prompt must be a string") + raise BackendArgumentTypeError(str, prompt=prompt) # Query the db to make sure the lexicon name isn't taken if db(select(func.count(Lexicon.id)).where(Lexicon.name == name)).scalar() > 0: diff --git a/amanuensis/backend/membership.py b/amanuensis/backend/membership.py index 5e9af13..baddcdd 100644 --- a/amanuensis/backend/membership.py +++ b/amanuensis/backend/membership.py @@ -6,7 +6,7 @@ from sqlalchemy import select, func from amanuensis.db import DbContext, Membership from amanuensis.db.models import Lexicon -from amanuensis.errors import ArgumentError +from amanuensis.errors import ArgumentError, BackendArgumentTypeError def create( @@ -20,11 +20,11 @@ def create( """ # Verify argument types are correct if not isinstance(user_id, int): - raise ArgumentError("user_id") + raise BackendArgumentTypeError(int, user_id=user_id) if not isinstance(lexicon_id, int): - raise ArgumentError("lexicon_id") + raise BackendArgumentTypeError(int, lexicon_id=lexicon_id) if not isinstance(is_editor, bool): - raise ArgumentError("is_editor") + raise BackendArgumentTypeError(bool, is_editor=is_editor) # Verify user has not already joined lexicon if ( diff --git a/amanuensis/backend/post.py b/amanuensis/backend/post.py index 0a6373f..8a4d59d 100644 --- a/amanuensis/backend/post.py +++ b/amanuensis/backend/post.py @@ -8,7 +8,7 @@ from sqlalchemy import select from amanuensis.db import DbContext, Post from amanuensis.db.models import Lexicon -from amanuensis.errors import ArgumentError +from amanuensis.errors import ArgumentError, BackendArgumentTypeError def create( @@ -23,15 +23,15 @@ def create( # Verify lexicon id if not isinstance(lexicon_id, int): - raise ArgumentError("Lexicon id must be an integer.") + raise BackendArgumentTypeError(int, lexicon_id=lexicon_id) # Verify user_id - if not (isinstance(user_id, int) or user_id is None): - raise ArgumentError("User id must be an integer.") + if user_id is not None and not isinstance(user_id, int): + raise BackendArgumentTypeError(int, user_id=user_id) # Verify body if not isinstance(body, str): - raise ArgumentError("Post body must be a string.") + raise BackendArgumentTypeError(str, body=body) if not body.strip(): raise ArgumentError("Post body cannot be empty.") diff --git a/amanuensis/backend/user.py b/amanuensis/backend/user.py index 5542c8c..e07e315 100644 --- a/amanuensis/backend/user.py +++ b/amanuensis/backend/user.py @@ -10,7 +10,7 @@ from sqlalchemy import select, func, update from werkzeug.security import generate_password_hash, check_password_hash from amanuensis.db import DbContext, User -from amanuensis.errors import ArgumentError +from amanuensis.errors import ArgumentError, BackendArgumentTypeError RE_NO_LETTERS = re.compile(r"^[0-9-_]*$") @@ -30,7 +30,7 @@ def create( """ # Verify username if not isinstance(username, str): - raise ArgumentError("Username must be a string") + raise BackendArgumentTypeError(str, username=username) if len(username) < 3 or len(username) > 32: raise ArgumentError("Username must be between 3 and 32 characters") if RE_NO_LETTERS.match(username): @@ -42,18 +42,18 @@ def create( # Verify password if not isinstance(password, str): - raise ArgumentError("Password must be a string") + raise BackendArgumentTypeError(str, password=password) # Verify display name if display_name is not None and not isinstance(display_name, str): - raise ArgumentError("Display name must be a string") + raise BackendArgumentTypeError(str, display_name=display_name) # If display name is not provided, use the username if not display_name or not display_name.strip(): display_name = username # Verify email if not isinstance(email, str): - raise ArgumentError("Email must be a string") + raise BackendArgumentTypeError(str, email=email) # Query the db to make sure the username isn't taken if db(select(func.count(User.id)).where(User.username == username)).scalar() > 0: diff --git a/amanuensis/errors.py b/amanuensis/errors.py index b6a9145..7c35ee1 100644 --- a/amanuensis/errors.py +++ b/amanuensis/errors.py @@ -4,8 +4,21 @@ Submodule of custom exception types class AmanuensisError(Exception): - """Base class for exceptions in amanuensis""" + """Base class for exceptions in Amanuensis""" class ArgumentError(AmanuensisError): - """An internal call was made with invalid arguments""" + """An internal call was made with invalid arguments.""" + + +class BackendArgumentTypeError(ArgumentError): + """ + A call to a backend function was made with a value of an invalid type for the parameter. + Specify the invalid parameter and value as a kwarg. + """ + def __init__(self, obj_type, **kwarg): + if not kwarg: + raise ValueError("Missing kwarg") + param, value = next(iter(kwarg.items())) + msg = f"Expected {param} of type {obj_type}, got {type(value)}" + super().__init__(msg) diff --git a/tests/backend/test_character.py b/tests/backend/test_character.py index d6808f4..bf0bb95 100644 --- a/tests/backend/test_character.py +++ b/tests/backend/test_character.py @@ -2,7 +2,7 @@ import pytest from amanuensis.backend import charq from amanuensis.db import * -from amanuensis.errors import ArgumentError +from amanuensis.errors import ArgumentError, BackendArgumentTypeError def test_create_character(db: DbContext, lexicon_with_editor, make): @@ -20,13 +20,13 @@ def test_create_character(db: DbContext, lexicon_with_editor, make): kwargs: dict # Bad argument types - with pytest.raises(ArgumentError): + with pytest.raises(BackendArgumentTypeError): kwargs = {**defaults, "name": b"bytestring"} charq.create(**kwargs) - with pytest.raises(ArgumentError): + with pytest.raises(BackendArgumentTypeError): kwargs = {**defaults, "name": None} charq.create(**kwargs) - with pytest.raises(ArgumentError): + with pytest.raises(BackendArgumentTypeError): kwargs = {**defaults, "signature": b"bytestring"} charq.create(**kwargs) diff --git a/tests/backend/test_lexicon.py b/tests/backend/test_lexicon.py index 8cc4f28..dbbb904 100644 --- a/tests/backend/test_lexicon.py +++ b/tests/backend/test_lexicon.py @@ -5,7 +5,7 @@ import pytest from amanuensis.backend import lexiq from amanuensis.db import DbContext, Lexicon, User -from amanuensis.errors import ArgumentError +from amanuensis.errors import ArgumentError, BackendArgumentTypeError from tests.conftest import ObjectFactory @@ -20,7 +20,7 @@ def test_create_lexicon(db: DbContext): kwargs: dict # Test name constraints - with pytest.raises(ArgumentError): + with pytest.raises(BackendArgumentTypeError): kwargs = {**defaults, "name": None} lexiq.create(**kwargs) with pytest.raises(ArgumentError): diff --git a/tests/backend/test_post.py b/tests/backend/test_post.py index d66426a..289e989 100644 --- a/tests/backend/test_post.py +++ b/tests/backend/test_post.py @@ -3,7 +3,7 @@ import pytest from amanuensis.backend import postq from amanuensis.db import DbContext -from amanuensis.errors import ArgumentError +from amanuensis.errors import ArgumentError, BackendArgumentTypeError def test_create_post(db: DbContext, lexicon_with_editor): @@ -20,19 +20,16 @@ def test_create_post(db: DbContext, lexicon_with_editor): kwargs: dict # ids are integers - with pytest.raises(ArgumentError): + with pytest.raises(BackendArgumentTypeError): kwargs = {**defaults, "user_id": "zero"} postq.create(**kwargs) - with pytest.raises(ArgumentError): + with pytest.raises(BackendArgumentTypeError): kwargs = {**defaults, "lexicon_id": "zero"} postq.create(**kwargs) # empty arguments don't work - with pytest.raises(ArgumentError): - kwargs = {**defaults, "lexicon_id": ""} - postq.create(**kwargs) - with pytest.raises(ArgumentError): - kwargs = {**defaults, "user_id": ""} + with pytest.raises(BackendArgumentTypeError): + kwargs = {**defaults, "lexicon_id": None} postq.create(**kwargs) with pytest.raises(ArgumentError): kwargs = {**defaults, "body": ""} diff --git a/tests/backend/test_user.py b/tests/backend/test_user.py index 43af33f..6cf8f88 100644 --- a/tests/backend/test_user.py +++ b/tests/backend/test_user.py @@ -4,7 +4,7 @@ import pytest from amanuensis.backend import userq from amanuensis.db import DbContext, User -from amanuensis.errors import ArgumentError +from amanuensis.errors import ArgumentError, BackendArgumentTypeError def test_create_user(db: DbContext): @@ -33,7 +33,7 @@ def test_create_user(db: DbContext): userq.create(**kwargs) # No password - with pytest.raises(ArgumentError): + with pytest.raises(BackendArgumentTypeError): kwargs = {**defaults, "password": None} userq.create(**kwargs) -- 2.44.1