diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 0f59c7501..ddacfb416 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -20,5 +20,5 @@ jobs: pip install pylint - name: Analysing the code with pylint run: | - pylint bookwyrm/ --ignore=migrations --disable=E1101,E1136,R0903,R0901,W0707,W0511 + pylint bookwyrm/ --ignore=migrations,__init__ --disable=E1101,E1135,E1136,R0903,R0901,R0902,W0707,W0511,W0406 diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index 5349e1dd0..81762388f 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -37,6 +37,7 @@ class Mention(Link): @dataclass +# pylint: disable=invalid-name class Signature: """public key block""" @@ -56,11 +57,11 @@ def naive_parse(activity_objects, activity_json, serializer=None): activity_type = activity_json.get("type") try: serializer = activity_objects[activity_type] - except KeyError as e: + except KeyError as err: # we know this exists and that we can't handle it if activity_type in ["Question"]: return None - raise ActivitySerializerError(e) + raise ActivitySerializerError(err) return serializer(activity_objects=activity_objects, **activity_json) diff --git a/bookwyrm/activitypub/book.py b/bookwyrm/activitypub/book.py index 1599b408a..bd27c4e60 100644 --- a/bookwyrm/activitypub/book.py +++ b/bookwyrm/activitypub/book.py @@ -6,6 +6,7 @@ from .base_activity import ActivityObject from .image import Document +# pylint: disable=invalid-name @dataclass(init=False) class BookData(ActivityObject): """shared fields for all book data and authors""" @@ -18,6 +19,7 @@ class BookData(ActivityObject): lastEditedBy: str = None +# pylint: disable=invalid-name @dataclass(init=False) class Book(BookData): """serializes an edition or work, abstract""" @@ -40,6 +42,7 @@ class Book(BookData): type: str = "Book" +# pylint: disable=invalid-name @dataclass(init=False) class Edition(Book): """Edition instance of a book object""" @@ -57,6 +60,7 @@ class Edition(Book): type: str = "Edition" +# pylint: disable=invalid-name @dataclass(init=False) class Work(Book): """work instance of a book object""" @@ -66,6 +70,7 @@ class Work(Book): type: str = "Work" +# pylint: disable=invalid-name @dataclass(init=False) class Author(BookData): """author of a book""" diff --git a/bookwyrm/activitypub/verbs.py b/bookwyrm/activitypub/verbs.py index f26936d7b..50a479b71 100644 --- a/bookwyrm/activitypub/verbs.py +++ b/bookwyrm/activitypub/verbs.py @@ -22,6 +22,7 @@ class Verb(ActivityObject): self.object.to_model() +# pylint: disable=invalid-name @dataclass(init=False) class Create(Verb): """Create activity""" @@ -32,6 +33,7 @@ class Create(Verb): type: str = "Create" +# pylint: disable=invalid-name @dataclass(init=False) class Delete(Verb): """Create activity""" @@ -57,6 +59,7 @@ class Delete(Verb): # if we can't find it, we don't need to delete it because we don't have it +# pylint: disable=invalid-name @dataclass(init=False) class Update(Verb): """Update activity""" @@ -192,6 +195,7 @@ class Like(Verb): self.to_model() +# pylint: disable=invalid-name @dataclass(init=False) class Announce(Verb): """boosting a status""" diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py index 22489af29..6c032b830 100644 --- a/bookwyrm/connectors/abstract_connector.py +++ b/bookwyrm/connectors/abstract_connector.py @@ -127,8 +127,8 @@ class AbstractConnector(AbstractMinimalConnector): edition_data = data try: work_data = self.get_work_from_edition_data(data) - except (KeyError, ConnectorException) as e: - logger.exception(e) + except (KeyError, ConnectorException) as err: + logger.exception(err) work_data = data if not work_data or not edition_data: @@ -237,16 +237,16 @@ def get_data(url, params=None, timeout=10): }, timeout=timeout, ) - except (RequestError, SSLError, ConnectionError) as e: - logger.exception(e) + except (RequestError, SSLError, ConnectionError) as err: + logger.exception(err) raise ConnectorException() if not resp.ok: raise ConnectorException() try: data = resp.json() - except ValueError as e: - logger.exception(e) + except ValueError as err: + logger.exception(err) raise ConnectorException() return data @@ -262,8 +262,8 @@ def get_image(url, timeout=10): }, timeout=timeout, ) - except (RequestError, SSLError) as e: - logger.exception(e) + except (RequestError, SSLError) as err: + logger.exception(err) return None if not resp.ok: return None diff --git a/bookwyrm/connectors/inventaire.py b/bookwyrm/connectors/inventaire.py index 102c9d727..116aa5c11 100644 --- a/bookwyrm/connectors/inventaire.py +++ b/bookwyrm/connectors/inventaire.py @@ -74,7 +74,7 @@ class Connector(AbstractConnector): **{k: data.get(k) for k in ["uri", "image", "labels", "sitelinks"]}, } - def search(self, query, min_confidence=None): + def search(self, query, min_confidence=None): # pylint: disable=arguments-differ """overrides default search function with confidence ranking""" results = super().search(query) if min_confidence: diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index 042118029..cb55d229e 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -22,6 +22,7 @@ class CustomForm(ModelForm): css_classes["number"] = "input" css_classes["checkbox"] = "checkbox" css_classes["textarea"] = "textarea" + # pylint: disable=super-with-arguments super(CustomForm, self).__init__(*args, **kwargs) for visible in self.visible_fields(): if hasattr(visible.field.widget, "input_type"): diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 2a5c3382a..bbad5ba9b 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -93,7 +93,8 @@ class ListItem(CollectionItemMixin, BookWyrmModel): ) class Meta: - # A book may only be placed into a list once, and each order in the list may be used only - # once + """A book may only be placed into a list once, + and each order in the list may be used only once""" + unique_together = (("book", "book_list"), ("order", "book_list")) ordering = ("-created_date",) diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index ba97330d2..e328b84bf 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -232,7 +232,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): def save(self, *args, **kwargs): """populate fields for new local users""" created = not bool(self.id) - if not self.local and not re.match(regex.full_username, self.username): + if not self.local and not re.match(regex.FULL_USERNAME, self.username): # generate a username that uses the domain (webfinger format) actor_parts = urlparse(self.remote_id) self.username = "%s@%s" % (self.username, actor_parts.netloc) diff --git a/bookwyrm/signatures.py b/bookwyrm/signatures.py index 5488cf9be..c8c900283 100644 --- a/bookwyrm/signatures.py +++ b/bookwyrm/signatures.py @@ -73,6 +73,7 @@ class Signature: self.headers = headers self.signature = signature + # pylint: disable=invalid-name @classmethod def parse(cls, request): """extract and parse a signature from an http request""" diff --git a/bookwyrm/timezone_middleware.py b/bookwyrm/timezone_middleware.py index 633d6835d..5033397a5 100644 --- a/bookwyrm/timezone_middleware.py +++ b/bookwyrm/timezone_middleware.py @@ -1,9 +1,12 @@ +""" Makes the app aware of the users timezone """ import pytz from django.utils import timezone class TimezoneMiddleware: + """Determine the timezone based on the request""" + def __init__(self, get_response): self.get_response = get_response diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index bfdcd9fc0..15e802b54 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -7,8 +7,8 @@ from django.views.generic.base import TemplateView from bookwyrm import settings, views from bookwyrm.utils import regex -user_path = r"^user/(?P%s)" % regex.username -local_user_path = r"^user/(?P%s)" % regex.localname +USER_PATH = r"^user/(?P%s)" % regex.username +LOCAL_USER_PATH = r"^user/(?P%s)" % regex.localname status_types = [ "status", @@ -19,9 +19,9 @@ status_types = [ "boost", "generatednote", ] -status_path = r"%s/(%s)/(?P\d+)" % (user_path, "|".join(status_types)) +STATUS_PATH = r"%s/(%s)/(?P\d+)" % (USER_PATH, "|".join(status_types)) -book_path = r"^book/(?P\d+)" +BOOK_PATH = r"^book/(?P\d+)" urlpatterns = [ path("admin/", admin.site.urls), @@ -31,8 +31,8 @@ urlpatterns = [ ), # federation endpoints re_path(r"^inbox/?$", views.Inbox.as_view()), - re_path(r"%s/inbox/?$" % local_user_path, views.Inbox.as_view()), - re_path(r"%s/outbox/?$" % local_user_path, views.Outbox.as_view()), + re_path(r"%s/inbox/?$" % LOCAL_USER_PATH, views.Inbox.as_view()), + re_path(r"%s/outbox/?$" % LOCAL_USER_PATH, views.Outbox.as_view()), re_path(r"^\.well-known/webfinger/?$", views.webfinger), re_path(r"^\.well-known/nodeinfo/?$", views.nodeinfo_pointer), re_path(r"^\.well-known/host-meta/?$", views.host_meta), @@ -192,21 +192,21 @@ urlpatterns = [ re_path(r"^import/?$", views.Import.as_view(), name="import"), re_path(r"^import/(\d+)/?$", views.ImportStatus.as_view(), name="import-status"), # users - re_path(r"%s/?$" % user_path, views.User.as_view(), name="user-feed"), - re_path(r"%s\.json$" % user_path, views.User.as_view()), - re_path(r"%s/rss" % user_path, views.rss_feed.RssFeed(), name="user-rss"), + re_path(r"%s/?$" % USER_PATH, views.User.as_view(), name="user-feed"), + re_path(r"%s\.json$" % USER_PATH, views.User.as_view()), + re_path(r"%s/rss" % USER_PATH, views.rss_feed.RssFeed(), name="user-rss"), re_path( - r"%s/followers(.json)?/?$" % user_path, + r"%s/followers(.json)?/?$" % USER_PATH, views.Followers.as_view(), name="user-followers", ), re_path( - r"%s/following(.json)?/?$" % user_path, + r"%s/following(.json)?/?$" % USER_PATH, views.Following.as_view(), name="user-following", ), # lists - re_path(r"%s/lists/?$" % user_path, views.UserLists.as_view(), name="user-lists"), + re_path(r"%s/lists/?$" % USER_PATH, views.UserLists.as_view(), name="user-lists"), re_path(r"^list/?$", views.Lists.as_view(), name="lists"), re_path(r"^list/(?P\d+)(.json)?/?$", views.List.as_view(), name="list"), re_path(r"^list/add-book/?$", views.list.add_book, name="list-add-book"), @@ -224,14 +224,14 @@ urlpatterns = [ r"^list/(?P\d+)/curate/?$", views.Curate.as_view(), name="list-curate" ), # User books - re_path(r"%s/books/?$" % user_path, views.Shelf.as_view(), name="user-shelves"), + re_path(r"%s/books/?$" % USER_PATH, views.Shelf.as_view(), name="user-shelves"), re_path( - r"^%s/(helf|books)/(?P[\w-]+)(.json)?/?$" % user_path, + r"^%s/(helf|books)/(?P[\w-]+)(.json)?/?$" % USER_PATH, views.Shelf.as_view(), name="shelf", ), re_path( - r"^%s/(books|shelf)/(?P[\w-]+)(.json)?/?$" % local_user_path, + r"^%s/(books|shelf)/(?P[\w-]+)(.json)?/?$" % LOCAL_USER_PATH, views.Shelf.as_view(), name="shelf", ), @@ -241,7 +241,7 @@ urlpatterns = [ re_path(r"^unshelve/?$", views.unshelve), # goals re_path( - r"%s/goal/(?P\d{4})/?$" % user_path, + r"%s/goal/(?P\d{4})/?$" % USER_PATH, views.Goal.as_view(), name="user-goal", ), @@ -258,10 +258,10 @@ urlpatterns = [ re_path(r"^block/(?P\d+)/?$", views.Block.as_view()), re_path(r"^unblock/(?P\d+)/?$", views.unblock), # statuses - re_path(r"%s(.json)?/?$" % status_path, views.Status.as_view(), name="status"), - re_path(r"%s/activity/?$" % status_path, views.Status.as_view(), name="status"), + re_path(r"%s(.json)?/?$" % STATUS_PATH, views.Status.as_view(), name="status"), + re_path(r"%s/activity/?$" % STATUS_PATH, views.Status.as_view(), name="status"), re_path( - r"%s/replies(.json)?/?$" % status_path, views.Replies.as_view(), name="replies" + r"%s/replies(.json)?/?$" % STATUS_PATH, views.Replies.as_view(), name="replies" ), re_path( r"^post/?$", @@ -289,17 +289,17 @@ urlpatterns = [ re_path(r"^boost/(?P\d+)/?$", views.Boost.as_view()), re_path(r"^unboost/(?P\d+)/?$", views.Unboost.as_view()), # books - re_path(r"%s(.json)?/?$" % book_path, views.Book.as_view(), name="book"), + re_path(r"%s(.json)?/?$" % BOOK_PATH, views.Book.as_view(), name="book"), re_path( - r"%s/(?Preview|comment|quote)/?$" % book_path, + r"%s/(?Preview|comment|quote)/?$" % BOOK_PATH, views.Book.as_view(), name="book-user-statuses", ), - re_path(r"%s/edit/?$" % book_path, views.EditBook.as_view()), - re_path(r"%s/confirm/?$" % book_path, views.ConfirmEditBook.as_view()), + re_path(r"%s/edit/?$" % BOOK_PATH, views.EditBook.as_view()), + re_path(r"%s/confirm/?$" % BOOK_PATH, views.ConfirmEditBook.as_view()), re_path(r"^create-book/?$", views.EditBook.as_view(), name="create-book"), re_path(r"^create-book/confirm?$", views.ConfirmEditBook.as_view()), - re_path(r"%s/editions(.json)?/?$" % book_path, views.Editions.as_view()), + re_path(r"%s/editions(.json)?/?$" % BOOK_PATH, views.Editions.as_view()), re_path( r"^upload-cover/(?P\d+)/?$", views.upload_cover, name="upload-cover" ), diff --git a/bookwyrm/utils/regex.py b/bookwyrm/utils/regex.py index 6389c35d6..3ac5a0ffd 100644 --- a/bookwyrm/utils/regex.py +++ b/bookwyrm/utils/regex.py @@ -1,10 +1,10 @@ """ defining regexes for regularly used concepts """ -domain = r"[\w_\-\.]+\.[a-z]{2,}" -localname = r"@?[a-zA-Z_\-\.0-9]+" -strict_localname = r"@[a-zA-Z_\-\.0-9]+" -username = r"%s(@%s)?" % (localname, domain) -strict_username = r"\B%s(@%s)?\b" % (strict_localname, domain) -full_username = r"%s@%s\b" % (localname, domain) +DOMAIN = r"[\w_\-\.]+\.[a-z]{2,}" +LOCALNAME = r"@?[a-zA-Z_\-\.0-9]+" +STRICT_LOCALNAME = r"@[a-zA-Z_\-\.0-9]+" +USERNAME = r"%s(@%s)?" % (LOCALNAME, DOMAIN) +STRICT_USERNAME = r"\B%s(@%s)?\b" % (STRICT_LOCALNAME, DOMAIN) +FULL_USERNAME = r"%s@%s\b" % (LOCALNAME, DOMAIN) # should match (BookWyrm/1.0.0; or (BookWyrm/99.1.2; -bookwyrm_user_agent = r"\(BookWyrm/[0-9]+\.[0-9]+\.[0-9]+;" +BOOKWYRM_USER_AGENT = r"\(BookWyrm/[0-9]+\.[0-9]+\.[0-9]+;" diff --git a/bookwyrm/views/helpers.py b/bookwyrm/views/helpers.py index 95a1cbbe3..452d81b9c 100644 --- a/bookwyrm/views/helpers.py +++ b/bookwyrm/views/helpers.py @@ -38,7 +38,7 @@ def is_api_request(request): def is_bookwyrm_request(request): """check if the request is coming from another bookwyrm instance""" user_agent = request.headers.get("User-Agent") - if user_agent is None or re.search(regex.bookwyrm_user_agent, user_agent) is None: + if user_agent is None or re.search(regex.BOOKWYRM_USER_AGENT, user_agent) is None: return False return True diff --git a/bookwyrm/views/inbox.py b/bookwyrm/views/inbox.py index a558c571e..ff5fa46da 100644 --- a/bookwyrm/views/inbox.py +++ b/bookwyrm/views/inbox.py @@ -21,6 +21,7 @@ from bookwyrm.utils import regex class Inbox(View): """requests sent by outside servers""" + # pylint: disable=too-many-return-statements def post(self, request, username=None): """only works as POST request""" # first check if this server is on our shitlist @@ -70,7 +71,7 @@ def is_blocked_user_agent(request): user_agent = request.headers.get("User-Agent") if not user_agent: return False - url = re.search(r"https?://{:s}/?".format(regex.domain), user_agent) + url = re.search(r"https?://{:s}/?".format(regex.DOMAIN), user_agent) if not url: return False url = url.group() diff --git a/bookwyrm/views/isbn.py b/bookwyrm/views/isbn.py index 197088bab..12208a3d7 100644 --- a/bookwyrm/views/isbn.py +++ b/bookwyrm/views/isbn.py @@ -1,13 +1,8 @@ """ isbn search view """ -from django.http import HttpResponseNotFound from django.http import JsonResponse -from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse -from django.utils.decorators import method_decorator from django.views import View -from django.views.decorators.http import require_POST -from bookwyrm import forms, models from bookwyrm.connectors import connector_manager from .helpers import is_api_request @@ -23,7 +18,6 @@ class Isbn(View): return JsonResponse([r.json() for r in book_results], safe=False) data = { - "title": "ISBN Search Results", "results": book_results, "query": isbn, } diff --git a/bookwyrm/views/list.py b/bookwyrm/views/list.py index 75bb5d48c..6e872434c 100644 --- a/bookwyrm/views/list.py +++ b/bookwyrm/views/list.py @@ -314,8 +314,7 @@ def set_book_position(request, list_item_id): Max("order") )["order__max"] - if int_position > order_max: - int_position = order_max + int_position = min(int_position, order_max) if request.user not in (book_list.user, list_item.user): return HttpResponseNotFound() diff --git a/bookwyrm/views/rss_feed.py b/bookwyrm/views/rss_feed.py index f3613a091..0d3a8902e 100644 --- a/bookwyrm/views/rss_feed.py +++ b/bookwyrm/views/rss_feed.py @@ -10,7 +10,7 @@ class RssFeed(Feed): description_template = "rss/content.html" title_template = "rss/title.html" - def get_object(self, request, username): + def get_object(self, request, username): # pylint: disable=arguments-differ """the user who's posts get serialized""" return get_user_from_username(request.user, username) diff --git a/bookwyrm/views/search.py b/bookwyrm/views/search.py index e7b538e47..274a3bc2e 100644 --- a/bookwyrm/views/search.py +++ b/bookwyrm/views/search.py @@ -83,7 +83,7 @@ def user_search(query, viewer, *_): # use webfinger for mastodon style account@domain.com username to load the user if # they don't exist locally (handle_remote_webfinger will check the db) - if re.match(regex.full_username, query): + if re.match(regex.FULL_USERNAME, query): handle_remote_webfinger(query) return ( diff --git a/bookwyrm/views/shelf.py b/bookwyrm/views/shelf.py index 17efec686..540975094 100644 --- a/bookwyrm/views/shelf.py +++ b/bookwyrm/views/shelf.py @@ -17,7 +17,7 @@ from bookwyrm import forms, models from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.settings import PAGE_LENGTH from .helpers import is_api_request, get_edition, get_user_from_username -from .helpers import handle_reading_status, privacy_filter +from .helpers import privacy_filter # pylint: disable=no-self-use diff --git a/bookwyrm/views/status.py b/bookwyrm/views/status.py index 27aeab07c..7ac770b94 100644 --- a/bookwyrm/views/status.py +++ b/bookwyrm/views/status.py @@ -133,7 +133,7 @@ def find_mentions(content): """detect @mentions in raw status content""" if not content: return - for match in re.finditer(regex.strict_username, content): + for match in re.finditer(rhandle_reading_status, egex.STRICT_USERNAME, content): username = match.group().strip().split("@")[1:] if len(username) == 1: # this looks like a local user (@user), fill in the domain @@ -150,7 +150,7 @@ def find_mentions(content): def format_links(content): """detect and format links""" return re.sub( - r'([^(href=")]|^|\()(https?:\/\/(%s([\w\.\-_\/+&\?=:;,])*))' % regex.domain, + r'([^(href=")]|^|\()(https?:\/\/(%s([\w\.\-_\/+&\?=:;,])*))' % regex.DOMAIN, r'\g<1>\g<3>', content, )