From 0b40c2d40a5e6dd95c6f7b0bbc1232c0fe509165 Mon Sep 17 00:00:00 2001 From: cary-rowen Date: Sun, 2 Mar 2025 23:54:22 +0800 Subject: [PATCH] Format code using black --- alembic/env.py | 14 +- .../28099038d8d6_initial_migration.py | 276 +++++++++++------- ...3946f1e_added_start_and_end_pos_to_note.py | 13 +- .../85028f013e6d_zero_migration_revision.py | 5 +- bookworm/annotation/__init__.py | 10 +- bookworm/annotation/annotation_dialogs.py | 14 +- bookworm/annotation/annotation_gui.py | 12 +- bookworm/annotation/annotator.py | 32 +- bookworm/app.py | 4 +- .../bookshelf/local_bookshelf/__init__.py | 33 ++- bookworm/database/__init__.py | 21 +- bookworm/database/models.py | 42 ++- bookworm/document/cache_utils.py | 3 +- bookworm/document/elements.py | 12 +- bookworm/document/formats/epub.py | 12 +- bookworm/document/formats/fitz.py | 2 +- bookworm/document/formats/pdf.py | 2 +- bookworm/document/formats/plain_text.py | 4 +- bookworm/document/formats/word.py | 4 +- bookworm/document/serde.py | 6 +- bookworm/gui/components.py | 3 +- bookworm/gui/settings.py | 11 +- bookworm/logger.py | 3 +- bookworm/ocr/ocr_menu.py | 8 +- bookworm/otau.py | 11 +- bookworm/paths.py | 2 +- bookworm/platforms/win32/shell.py | 4 +- .../win32/speech_engines/piper/__init__.py | 2 - bookworm/shell.py | 6 +- .../speechdriver/element/converter/base.py | 48 +-- .../structured_text/structured_html_parser.py | 4 +- setup.py | 2 +- tasks.py | 31 +- tests/conftest.py | 20 +- tests/test_annotator.py | 9 +- tests/test_epub.py | 22 +- tests/test_otau.py | 21 +- 37 files changed, 433 insertions(+), 295 deletions(-) diff --git a/alembic/env.py b/alembic/env.py index 919d3304..536221a0 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -11,14 +11,15 @@ from bookworm.database import * - # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config # Interpret the config file for Python logging. # This line sets up loggers basically. -if config.config_file_name is not None and config.attributes.get('configure_logger', True): +if config.config_file_name is not None and config.attributes.get( + "configure_logger", True +): fileConfig(config.config_file_name, disable_existing_loggers=False) # add your model's MetaData object here @@ -34,13 +35,13 @@ writer = rewriter.Rewriter() + @writer.rewrites(ops.MigrationScript) def add_imports(context, revision, op): op.imports.add("import bookworm") return [op] - def run_migrations_offline() -> None: """Run migrations in 'offline' mode. @@ -73,11 +74,12 @@ def run_migrations_online() -> None: """ connectable = create_engine(config.get_main_option("sqlalchemy.url")) - + with connectable.connect() as connection: context.configure( - connection=connection, target_metadata=target_metadata, - process_revision_directives=writer + connection=connection, + target_metadata=target_metadata, + process_revision_directives=writer, ) with context.begin_transaction(): diff --git a/alembic/versions/28099038d8d6_initial_migration.py b/alembic/versions/28099038d8d6_initial_migration.py index bfe5e83c..127f5ffe 100644 --- a/alembic/versions/28099038d8d6_initial_migration.py +++ b/alembic/versions/28099038d8d6_initial_migration.py @@ -5,6 +5,7 @@ Create Date: 2024-09-23 00:10:33.591108 """ + from typing import Sequence, Union from alembic import op @@ -12,7 +13,7 @@ import bookworm # revision identifiers, used by Alembic. -revision: str = '28099038d8d6' +revision: str = "28099038d8d6" down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -20,127 +21,186 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.create_table('book', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('title', sa.String(length=512), nullable=False), - sa.Column('uri', bookworm.database.models.DocumentUriDBType(length=1024), nullable=False), - sa.PrimaryKeyConstraint('id') + op.create_table( + "book", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("title", sa.String(length=512), nullable=False), + sa.Column( + "uri", + bookworm.database.models.DocumentUriDBType(length=1024), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_book_uri"), "book", ["uri"], unique=True) + op.create_table( + "document_position_info", + sa.Column("last_page", sa.Integer(), nullable=True), + sa.Column("last_position", sa.Integer(), nullable=True), + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("title", sa.String(length=512), nullable=False), + sa.Column( + "uri", + bookworm.database.models.DocumentUriDBType(length=1024), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_document_position_info_uri"), + "document_position_info", + ["uri"], + unique=True, ) - op.create_index(op.f('ix_book_uri'), 'book', ['uri'], unique=True) - op.create_table('document_position_info', - sa.Column('last_page', sa.Integer(), nullable=True), - sa.Column('last_position', sa.Integer(), nullable=True), - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('title', sa.String(length=512), nullable=False), - sa.Column('uri', bookworm.database.models.DocumentUriDBType(length=1024), nullable=False), - sa.PrimaryKeyConstraint('id') + op.create_table( + "note_tag", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("title", sa.String(length=512), nullable=False), + sa.PrimaryKeyConstraint("id"), ) - op.create_index(op.f('ix_document_position_info_uri'), 'document_position_info', ['uri'], unique=True) - op.create_table('note_tag', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('title', sa.String(length=512), nullable=False), - sa.PrimaryKeyConstraint('id') + op.create_index(op.f("ix_note_tag_title"), "note_tag", ["title"], unique=True) + op.create_table( + "pinned_document", + sa.Column("last_opened_on", sa.DateTime(), nullable=True), + sa.Column("is_pinned", sa.Boolean(), nullable=True), + sa.Column("pinning_order", sa.Integer(), nullable=True), + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("title", sa.String(length=512), nullable=False), + sa.Column( + "uri", + bookworm.database.models.DocumentUriDBType(length=1024), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), ) - op.create_index(op.f('ix_note_tag_title'), 'note_tag', ['title'], unique=True) - op.create_table('pinned_document', - sa.Column('last_opened_on', sa.DateTime(), nullable=True), - sa.Column('is_pinned', sa.Boolean(), nullable=True), - sa.Column('pinning_order', sa.Integer(), nullable=True), - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('title', sa.String(length=512), nullable=False), - sa.Column('uri', bookworm.database.models.DocumentUriDBType(length=1024), nullable=False), - sa.PrimaryKeyConstraint('id') + op.create_index( + op.f("ix_pinned_document_uri"), "pinned_document", ["uri"], unique=True ) - op.create_index(op.f('ix_pinned_document_uri'), 'pinned_document', ['uri'], unique=True) - op.create_table('quote_tag', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('title', sa.String(length=512), nullable=False), - sa.PrimaryKeyConstraint('id') + op.create_table( + "quote_tag", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("title", sa.String(length=512), nullable=False), + sa.PrimaryKeyConstraint("id"), ) - op.create_index(op.f('ix_quote_tag_title'), 'quote_tag', ['title'], unique=True) - op.create_table('recent_document', - sa.Column('last_opened_on', sa.DateTime(), nullable=True), - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('title', sa.String(length=512), nullable=False), - sa.Column('uri', bookworm.database.models.DocumentUriDBType(length=1024), nullable=False), - sa.PrimaryKeyConstraint('id') + op.create_index(op.f("ix_quote_tag_title"), "quote_tag", ["title"], unique=True) + op.create_table( + "recent_document", + sa.Column("last_opened_on", sa.DateTime(), nullable=True), + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("title", sa.String(length=512), nullable=False), + sa.Column( + "uri", + bookworm.database.models.DocumentUriDBType(length=1024), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), ) - op.create_index(op.f('ix_recent_document_uri'), 'recent_document', ['uri'], unique=True) - op.create_table('bookmark', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('title', sa.String(length=255), nullable=False), - sa.Column('page_number', sa.Integer(), nullable=False), - sa.Column('position', sa.Integer(), nullable=False), - sa.Column('section_title', sa.String(length=1024), nullable=False), - sa.Column('section_identifier', sa.String(length=1024), nullable=False), - sa.Column('date_created', sa.DateTime(), nullable=True), - sa.Column('date_updated', sa.DateTime(), nullable=True), - sa.Column('book_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['book_id'], ['book.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_index( + op.f("ix_recent_document_uri"), "recent_document", ["uri"], unique=True ) - op.create_table('note', - sa.Column('content', sa.Text(), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('title', sa.String(length=255), nullable=False), - sa.Column('page_number', sa.Integer(), nullable=False), - sa.Column('position', sa.Integer(), nullable=False), - sa.Column('section_title', sa.String(length=1024), nullable=False), - sa.Column('section_identifier', sa.String(length=1024), nullable=False), - sa.Column('date_created', sa.DateTime(), nullable=True), - sa.Column('date_updated', sa.DateTime(), nullable=True), - sa.Column('book_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['book_id'], ['book.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "bookmark", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("title", sa.String(length=255), nullable=False), + sa.Column("page_number", sa.Integer(), nullable=False), + sa.Column("position", sa.Integer(), nullable=False), + sa.Column("section_title", sa.String(length=1024), nullable=False), + sa.Column("section_identifier", sa.String(length=1024), nullable=False), + sa.Column("date_created", sa.DateTime(), nullable=True), + sa.Column("date_updated", sa.DateTime(), nullable=True), + sa.Column("book_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["book_id"], + ["book.id"], + ), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('quote', - sa.Column('start_pos', sa.Integer(), nullable=False), - sa.Column('end_pos', sa.Integer(), nullable=False), - sa.Column('content', sa.Text(), nullable=False), - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('title', sa.String(length=255), nullable=False), - sa.Column('page_number', sa.Integer(), nullable=False), - sa.Column('position', sa.Integer(), nullable=False), - sa.Column('section_title', sa.String(length=1024), nullable=False), - sa.Column('section_identifier', sa.String(length=1024), nullable=False), - sa.Column('date_created', sa.DateTime(), nullable=True), - sa.Column('date_updated', sa.DateTime(), nullable=True), - sa.Column('book_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['book_id'], ['book.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "note", + sa.Column("content", sa.Text(), nullable=False), + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("title", sa.String(length=255), nullable=False), + sa.Column("page_number", sa.Integer(), nullable=False), + sa.Column("position", sa.Integer(), nullable=False), + sa.Column("section_title", sa.String(length=1024), nullable=False), + sa.Column("section_identifier", sa.String(length=1024), nullable=False), + sa.Column("date_created", sa.DateTime(), nullable=True), + sa.Column("date_updated", sa.DateTime(), nullable=True), + sa.Column("book_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["book_id"], + ["book.id"], + ), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('notes_tags', - sa.Column('note_id', sa.Integer(), nullable=True), - sa.Column('note_tag_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['note_id'], ['note.id'], ), - sa.ForeignKeyConstraint(['note_tag_id'], ['note_tag.id'], ) + op.create_table( + "quote", + sa.Column("start_pos", sa.Integer(), nullable=False), + sa.Column("end_pos", sa.Integer(), nullable=False), + sa.Column("content", sa.Text(), nullable=False), + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("title", sa.String(length=255), nullable=False), + sa.Column("page_number", sa.Integer(), nullable=False), + sa.Column("position", sa.Integer(), nullable=False), + sa.Column("section_title", sa.String(length=1024), nullable=False), + sa.Column("section_identifier", sa.String(length=1024), nullable=False), + sa.Column("date_created", sa.DateTime(), nullable=True), + sa.Column("date_updated", sa.DateTime(), nullable=True), + sa.Column("book_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["book_id"], + ["book.id"], + ), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('quotes_tags', - sa.Column('quote_id', sa.Integer(), nullable=True), - sa.Column('quote_tag_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['quote_id'], ['quote.id'], ), - sa.ForeignKeyConstraint(['quote_tag_id'], ['quote_tag.id'], ) + op.create_table( + "notes_tags", + sa.Column("note_id", sa.Integer(), nullable=True), + sa.Column("note_tag_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["note_id"], + ["note.id"], + ), + sa.ForeignKeyConstraint( + ["note_tag_id"], + ["note_tag.id"], + ), + ) + op.create_table( + "quotes_tags", + sa.Column("quote_id", sa.Integer(), nullable=True), + sa.Column("quote_tag_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["quote_id"], + ["quote.id"], + ), + sa.ForeignKeyConstraint( + ["quote_tag_id"], + ["quote_tag.id"], + ), ) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('quotes_tags') - op.drop_table('notes_tags') - op.drop_table('quote') - op.drop_table('note') - op.drop_table('bookmark') - op.drop_index(op.f('ix_recent_document_uri'), table_name='recent_document') - op.drop_table('recent_document') - op.drop_index(op.f('ix_quote_tag_title'), table_name='quote_tag') - op.drop_table('quote_tag') - op.drop_index(op.f('ix_pinned_document_uri'), table_name='pinned_document') - op.drop_table('pinned_document') - op.drop_index(op.f('ix_note_tag_title'), table_name='note_tag') - op.drop_table('note_tag') - op.drop_index(op.f('ix_document_position_info_uri'), table_name='document_position_info') - op.drop_table('document_position_info') - op.drop_index(op.f('ix_book_uri'), table_name='book') - op.drop_table('book') + op.drop_table("quotes_tags") + op.drop_table("notes_tags") + op.drop_table("quote") + op.drop_table("note") + op.drop_table("bookmark") + op.drop_index(op.f("ix_recent_document_uri"), table_name="recent_document") + op.drop_table("recent_document") + op.drop_index(op.f("ix_quote_tag_title"), table_name="quote_tag") + op.drop_table("quote_tag") + op.drop_index(op.f("ix_pinned_document_uri"), table_name="pinned_document") + op.drop_table("pinned_document") + op.drop_index(op.f("ix_note_tag_title"), table_name="note_tag") + op.drop_table("note_tag") + op.drop_index( + op.f("ix_document_position_info_uri"), table_name="document_position_info" + ) + op.drop_table("document_position_info") + op.drop_index(op.f("ix_book_uri"), table_name="book") + op.drop_table("book") # ### end Alembic commands ### diff --git a/alembic/versions/35f453946f1e_added_start_and_end_pos_to_note.py b/alembic/versions/35f453946f1e_added_start_and_end_pos_to_note.py index b7775b2e..fb2df616 100644 --- a/alembic/versions/35f453946f1e_added_start_and_end_pos_to_note.py +++ b/alembic/versions/35f453946f1e_added_start_and_end_pos_to_note.py @@ -5,6 +5,7 @@ Create Date: 2024-12-01 11:46:44.985313 """ + from typing import Sequence, Union from alembic import op @@ -12,21 +13,21 @@ import bookworm # revision identifiers, used by Alembic. -revision: str = '35f453946f1e' -down_revision: Union[str, None] = '85028f013e6d' +revision: str = "35f453946f1e" +down_revision: Union[str, None] = "85028f013e6d" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.add_column('note', sa.Column('start_pos', sa.Integer(), nullable=True)) - op.add_column('note', sa.Column('end_pos', sa.Integer(), nullable=True)) + op.add_column("note", sa.Column("start_pos", sa.Integer(), nullable=True)) + op.add_column("note", sa.Column("end_pos", sa.Integer(), nullable=True)) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('note', 'end_pos') - op.drop_column('note', 'start_pos') + op.drop_column("note", "end_pos") + op.drop_column("note", "start_pos") # ### end Alembic commands ### diff --git a/alembic/versions/85028f013e6d_zero_migration_revision.py b/alembic/versions/85028f013e6d_zero_migration_revision.py index be60ebd2..ec54b9d9 100644 --- a/alembic/versions/85028f013e6d_zero_migration_revision.py +++ b/alembic/versions/85028f013e6d_zero_migration_revision.py @@ -5,6 +5,7 @@ Create Date: 2024-09-30 14:48:49.349114 """ + from typing import Sequence, Union from alembic import op @@ -12,8 +13,8 @@ import bookworm # revision identifiers, used by Alembic. -revision: str = '85028f013e6d' -down_revision: Union[str, None] = '28099038d8d6' +revision: str = "85028f013e6d" +down_revision: Union[str, None] = "28099038d8d6" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None diff --git a/bookworm/annotation/__init__.py b/bookworm/annotation/__init__.py index b785e578..7e410585 100644 --- a/bookworm/annotation/__init__.py +++ b/bookworm/annotation/__init__.py @@ -172,7 +172,9 @@ def onKeyUp(self, event): return start_pos, end_pos = (comment.start_pos, comment.end_pos) is_whole_line = (start_pos, end_pos) == (None, None) - self.reader.go_to_page(comment.page_number, comment.position if is_whole_line else end_pos) + self.reader.go_to_page( + comment.page_number, comment.position if is_whole_line else end_pos + ) if is_whole_line: self.view.select_text(*self.view.get_containing_line(comment.position)) else: @@ -233,7 +235,11 @@ def onCaretMoved(self, event): for comment in NoteTaker(self.reader).get_for_page(): start_pos, end_pos = (comment.start_pos, comment.end_pos) # If the comment has a selection, we check if the caret position is inside the selection. Otherwise, we check that the ocmment position is in the pos_range, typically the whole line. - condition = (start_pos <= position < end_pos) if (start_pos, end_pos) != (None, None) else comment.position in pos_range + condition = ( + (start_pos <= position < end_pos) + if (start_pos, end_pos) != (None, None) + else comment.position in pos_range + ) if condition: evtdata["comment"] = True wx.CallAfter(self._process_caret_move, evtdata) diff --git a/bookworm/annotation/annotation_dialogs.py b/bookworm/annotation/annotation_dialogs.py index 1c69a5f9..7d1ecf30 100644 --- a/bookworm/annotation/annotation_dialogs.py +++ b/bookworm/annotation/annotation_dialogs.py @@ -35,9 +35,9 @@ def create_default(cls, annotator): book_id = annotator.current_book.id if has_book else None return cls( filter_criteria=AnnotationFilterCriteria(book_id=book_id), - sort_criteria=AnnotationSortCriteria.Page - if has_book - else AnnotationSortCriteria.Date, + sort_criteria=( + AnnotationSortCriteria.Page if has_book else AnnotationSortCriteria.Date + ), asc=has_book, ) @@ -298,9 +298,9 @@ def onApplyFilter(self, event): self.filter_callback( book_id=book_id, tag=self.tagsCombo.GetValue().strip(), - section_title=self.sectionChoice.GetValue().strip() - if not self.filter_by_book - else "", + section_title=( + self.sectionChoice.GetValue().strip() if not self.filter_by_book else "" + ), content=self.contentFilterText.GetValue().strip(), ) @@ -580,7 +580,7 @@ def go_to_item(self, item): if (start_pos, end_pos) != (None, None): # We have a selection, let's select the text self.service.view.contentTextCtrl.SetSelection(start_pos, end_pos) - + class QuotesDialog(AnnotationWithContentDialog): def go_to_item(self, item): diff --git a/bookworm/annotation/annotation_gui.py b/bookworm/annotation/annotation_gui.py index 55ef12a0..6ee4d258 100644 --- a/bookworm/annotation/annotation_gui.py +++ b/bookworm/annotation/annotation_gui.py @@ -209,11 +209,13 @@ def onAddNote(self, event): if start_pos == end_pos: start_pos, end_pos = (None, None) comments = NoteTaker(self.reader) - if comments.overlaps(start_pos, end_pos, self.reader.current_page, insertionPoint): + if comments.overlaps( + start_pos, end_pos, self.reader.current_page, insertionPoint + ): return self.view.notify_user( _("Error"), # Translator: Message obtained whenever another note is overlapping the selected position - _("Another note is currently overlapping the selected position.") + _("Another note is currently overlapping the selected position."), ) comment_text = self.view.get_text_from_user( # Translators: the title of a dialog to add a comment @@ -225,7 +227,11 @@ def onAddNote(self, event): if not comment_text: return note = comments.create( - title="", content=comment_text, position=insertionPoint, start_pos=start_pos, end_pos=end_pos + title="", + content=comment_text, + position=insertionPoint, + start_pos=start_pos, + end_pos=end_pos, ) self.service.style_comment(self.view, insertionPoint) diff --git a/bookworm/annotation/annotator.py b/bookworm/annotation/annotator.py index 9dacb365..d52f9c78 100644 --- a/bookworm/annotation/annotator.py +++ b/bookworm/annotation/annotator.py @@ -13,6 +13,7 @@ log = logger.getChild(__name__) # The bakery caches query objects to avoid recompiling them into strings in every call + @dataclass class AnnotationFilterCriteria: book_id: int = 0 @@ -141,7 +142,6 @@ def get_for_section(self, section_ident=None, asc=False): def get(self, item_id): return self.model.query.get(item_id) - def get_first_after(self, page_number, pos): model = self.model clauses = ( @@ -235,10 +235,13 @@ def delete_orphan_tags(cls): session.delete(otag) session.commit() + class PositionedAnnotator(TaggedAnnotator): """Annotations which are positioned on a specific text range""" - def overlaps(self, start: Optional[int], end: Optional[int], page_number: int, position: int) -> bool: + def overlaps( + self, start: Optional[int], end: Optional[int], page_number: int, position: int + ) -> bool: """ Determines whether an annotation overlaps with a given position The criterias used to check for the position are the following: @@ -254,10 +257,7 @@ def overlaps(self, start: Optional[int], end: Optional[int], page_number: int, p model.end_pos == end, model.page_number == page_number, ), - sa.and_( - model.page_number == page_number, - model.position == position - ), + sa.and_(model.page_number == page_number, model.position == position), sa.and_( model.start_pos.is_not(None), model.end_pos.is_not(None), @@ -270,11 +270,17 @@ def overlaps(self, start: Optional[int], end: Optional[int], page_number: int, p sa.and_( model.start_pos <= position, model.end_pos >= position, - ) - ) - ) + ), + ), + ), ] - return self.session.query(model).filter_by(book_id = self.current_book.id).filter(sa.or_(*clauses)).one_or_none() is not None + return ( + self.session.query(model) + .filter_by(book_id=self.current_book.id) + .filter(sa.or_(*clauses)) + .one_or_none() + is not None + ) class NoteTaker(PositionedAnnotator): @@ -282,7 +288,6 @@ class NoteTaker(PositionedAnnotator): model = Note - def get_first_after(self, page_number, pos): model = self.model clauses = ( @@ -327,8 +332,8 @@ def get_first_before(self, page_number, pos): ), sa.and_( model.page_number == page_number, - model.position < pos, - model.start_pos.is_(None) + model.position < pos, + model.start_pos.is_(None), ), model.page_number < page_number, ) @@ -341,6 +346,7 @@ def get_first_before(self, page_number, pos): .first() ) + class Quoter(TaggedAnnotator): """Highlights.""" diff --git a/bookworm/app.py b/bookworm/app.py index ee660e25..9cbf8f95 100644 --- a/bookworm/app.py +++ b/bookworm/app.py @@ -13,7 +13,9 @@ version_ex = "2025.1.0.0" url = "https://github.com/blindpandas/bookworm" website = "https://github.com/blindpandas/bookworm" -update_url = "https://raw.githubusercontent.com/blindpandas/bookworm/main/update_info.json" +update_url = ( + "https://raw.githubusercontent.com/blindpandas/bookworm/main/update_info.json" +) copyright = f"Copyright (c) 2025 {author} and {display_name} contributors." exit_code = 0 is_frozen = getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS") diff --git a/bookworm/bookshelf/local_bookshelf/__init__.py b/bookworm/bookshelf/local_bookshelf/__init__.py index cf580ccc..a202c67f 100644 --- a/bookworm/bookshelf/local_bookshelf/__init__.py +++ b/bookworm/bookshelf/local_bookshelf/__init__.py @@ -577,23 +577,32 @@ def get_item_actions(self, item): ), BookshelfAction( # Translators: label of an item in the context menu of a document in the bookshelf - _("Remove from ¤tly reading") if doc_instance.is_currently_reading - # Translators: label of an item in the context menu of a document in the bookshelf - else _("Add to ¤tly reading"), + ( + _("Remove from ¤tly reading") + if doc_instance.is_currently_reading + # Translators: label of an item in the context menu of a document in the bookshelf + else _("Add to ¤tly reading") + ), func=lambda __: self._do_toggle_currently_reading(doc_instance), ), BookshelfAction( # Translators: label of an item in the context menu of a document in the bookshelf - _("Remove from &want to read") if doc_instance.in_reading_list - # Translators: label of an item in the context menu of a document in the bookshelf - else _("Add to &want to read"), + ( + _("Remove from &want to read") + if doc_instance.in_reading_list + # Translators: label of an item in the context menu of a document in the bookshelf + else _("Add to &want to read") + ), func=lambda __: self._do_toggle_in_reading_list(doc_instance), ), BookshelfAction( # Translators: label of an item in the context menu of a document in the bookshelf - _("Remove from &favorites") if doc_instance.favorited - # Translators: label of an item in the context menu of a document in the bookshelf - else _("Add to &favorites"), + ( + _("Remove from &favorites") + if doc_instance.favorited + # Translators: label of an item in the context menu of a document in the bookshelf + else _("Add to &favorites") + ), func=lambda __: self._do_toggle_favorited(doc_instance), ), BookshelfAction( @@ -639,9 +648,9 @@ def _do_edit_category_and_tags(self, doc_instance): # Translators: title of a dialog to change the reading list or collection for a document title=_("Edit reading list/collections"), categories=[cat.name for cat in Category.get_all()], - given_category=None - if not doc_instance.category - else doc_instance.category.name, + given_category=( + None if not doc_instance.category else doc_instance.category.name + ), tags_names=[ Tag.get_by_id(doc_tag.tag_id).name for doc_tag in DocumentTag.select().where( diff --git a/bookworm/database/__init__.py b/bookworm/database/__init__.py index 9a4ff918..6936362d 100644 --- a/bookworm/database/__init__.py +++ b/bookworm/database/__init__.py @@ -23,11 +23,13 @@ log = logger.getChild(__name__) + def get_db_url() -> str: db_path = os.path.join(get_db_path(), "database.sqlite") return f"sqlite:///{db_path}" -def init_database(engine = None, url: str = None, **kwargs) -> bool: + +def init_database(engine=None, url: str = None, **kwargs) -> bool: if not url: url = get_db_url() if engine == None: @@ -38,7 +40,9 @@ def init_database(engine = None, url: str = None, **kwargs) -> bool: rev = context.get_current_revision() # let's check for the book table # Should it be too ambiguous, we'd have to revisit what tables should be checked to determine whether the DB is at the baseline point - cursor = conn.execute(text("SELECT name FROM sqlite_master WHERE type='table';")) + cursor = conn.execute( + text("SELECT name FROM sqlite_master WHERE type='table';") + ) tables = [row[0] for row in cursor.fetchall()] is_baseline = tables != None and "book" in tables and rev == None log.info(f"Current revision is {rev}") @@ -48,21 +52,22 @@ def init_database(engine = None, url: str = None, **kwargs) -> bool: if app.is_frozen: cfg_file = sys._MEIPASS script_location = paths.app_path("alembic") - + cfg = Config(Path(cfg_file, "alembic.ini")) # we set this attribute in order to prevent alembic from configuring logging if we're running the commands programmatically. # This is because otherwise our loggers would be overridden - cfg.attributes['configure_logger'] = False - cfg.set_main_option('script_location', str(script_location)) + cfg.attributes["configure_logger"] = False + cfg.set_main_option("script_location", str(script_location)) cfg.set_main_option("sqlalchemy.url", url) if rev == None: if is_baseline: - log.info("No revision was found, but the database appears to be at the baseline required to begin tracking.") + log.info( + "No revision was found, but the database appears to be at the baseline required to begin tracking." + ) log.info("Stamping alembic revision") - command.stamp(cfg, "28099038d8d6") + command.stamp(cfg, "28099038d8d6") command.upgrade(cfg, "head") Base.session = scoped_session( sessionmaker(engine, autocommit=False, autoflush=False) ) return engine - diff --git a/bookworm/database/models.py b/bookworm/database/models.py index 1cf0c135..64b05f3f 100644 --- a/bookworm/database/models.py +++ b/bookworm/database/models.py @@ -12,7 +12,16 @@ from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property from sqlalchemy.ext.declarative import declared_attr -from sqlalchemy.orm import deferred, relationship, synonym, class_mapper, mapper, Query, scoped_session, declarative_base +from sqlalchemy.orm import ( + deferred, + relationship, + synonym, + class_mapper, + mapper, + Query, + scoped_session, + declarative_base, +) from bookworm.document.uri import DocumentUri from bookworm.logger import logger @@ -22,6 +31,7 @@ class DocumentUriDBType(types.TypeDecorator): """Provides sqlalchemy custom type for the DocumentUri.""" + cache_ok = True impl = types.Unicode @@ -55,6 +65,7 @@ def __get__(self, obj, type): except UnmappedClassError: return None + class Model: id = sa.Column(sa.Integer, primary_key=True) query = _QueryProperty() @@ -91,7 +102,7 @@ def get_or_create(cls, *args, **kwargs): class Book(DocumentBase): __tablename__ = "book" - + @property def identifier(self): return self.uri.to_uri_string() @@ -113,7 +124,9 @@ def save_position(self, page, pos): class RecentDocument(DocumentBase): __tablename__ = "recent_document" - last_opened_on = sa.Column(sa.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + last_opened_on = sa.Column( + sa.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow + ) def record_open(self): self.last_opened_on = datetime.utcnow() @@ -133,7 +146,9 @@ def clear_all(cls): class PinnedDocument(DocumentBase): __tablename__ = "pinned_document" - last_opened_on = sa.Column(sa.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + last_opened_on = sa.Column( + sa.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow + ) is_pinned = sa.Column(sa.Boolean, default=False) pinning_order = sa.Column(sa.Integer, default=0) @@ -166,8 +181,10 @@ def clear_all(cls): session.delete(item) session.commit() + # annotation models + class TaggedMixin: """Provides a generic many-to-many relationship to a dynamically generated tags table using @@ -195,16 +212,16 @@ def tags(cls): # Create the Tag model tag_attrs = { "id": sa.Column(sa.Integer, primary_key=True), - "title": sa.Column(sa.String(512), nullable=False, unique=True, index=True), + "title": sa.Column( + sa.String(512), nullable=False, unique=True, index=True + ), "items": relationship( cls, secondary=lambda: cls.__tags_association_table__, backref="related_tags", ), } - cls.Tag = type( - f"{cls.__name__}Tag", (GetOrCreateMixin, Base), tag_attrs - ) + cls.Tag = type(f"{cls.__name__}Tag", (GetOrCreateMixin, Base), tag_attrs) # The many-to-many association table cls.__tags_association_table__ = cls._prepare_association_table( table_name=f"{cls.__tablename__}s_tags", @@ -246,6 +263,7 @@ def book(cls): class Bookmark(AnnotationBase): """Represents a user-defined bookmark.""" + __tablename__ = "bookmark" @@ -264,15 +282,17 @@ def text_column(cls): class Note(TaggedContent): """Represents user comments (notes).""" + __tablename__ = "note" # Like Quote, a note can have a start and an end position # difference is that they are allowed to be None, and if so, it means they are targeting the whole line - start_pos = sa.Column(sa.Integer, default = None) - end_pos = sa.Column(sa.Integer, default = None) - + start_pos = sa.Column(sa.Integer, default=None) + end_pos = sa.Column(sa.Integer, default=None) + class Quote(TaggedContent): """Represents a highlight (quote) from the book.""" + __tablename__ = "quote" start_pos = sa.Column(sa.Integer, nullable=False) diff --git a/bookworm/document/cache_utils.py b/bookworm/document/cache_utils.py index 81cfec3a..e47910ef 100644 --- a/bookworm/document/cache_utils.py +++ b/bookworm/document/cache_utils.py @@ -1,4 +1,5 @@ """Caching utilities""" + from pathlib import Path from diskcache import Cache @@ -17,7 +18,7 @@ def is_document_modified(key: str, path: Path, cache: Cache) -> bool: stat_mtime = path.stat().st_mtime return mtime != stat_mtime + def set_document_modified_time(key: str, path: Path, cache: Cache) -> bool: key = f"{key}_meta" cache.set(key, path.stat().st_mtime) - \ No newline at end of file diff --git a/bookworm/document/elements.py b/bookworm/document/elements.py index f5aa531e..d8c4dce4 100644 --- a/bookworm/document/elements.py +++ b/bookworm/document/elements.py @@ -229,12 +229,12 @@ def from_document(cls, document): title=metadata.title, language=document.language, description=metadata.description, - number_of_pages=len(document) - if not document.is_single_page_document() - else None, - number_of_sections=len(document.toc_tree) - if document.has_toc_tree() - else None, + number_of_pages=( + len(document) if not document.is_single_page_document() else None + ), + number_of_sections=( + len(document.toc_tree) if document.has_toc_tree() else None + ), authors=metadata.author, creation_date=metadata.creation_date, publication_date=metadata.publication_year, diff --git a/bookworm/document/formats/epub.py b/bookworm/document/formats/epub.py index 3a55f6f5..ee6c62aa 100644 --- a/bookworm/document/formats/epub.py +++ b/bookworm/document/formats/epub.py @@ -85,7 +85,9 @@ def metadata(self): desc = None date_value = info.get("date", "") if not isinstance(date_value, str): - log.warning(f"Unexpected date format: {type(date_value)}. Converting to string.") + log.warning( + f"Unexpected date format: {type(date_value)}. Converting to string." + ) if isinstance(date_value, (int, float)): date_value = str(date_value) else: @@ -190,13 +192,13 @@ def epub_html_items(self) -> tuple[str]: # However this poses a problem when the chapters do not follow a conventional numeric scheme but rather use something like roman numbers # As reported in issue 243 # We will now sort the items obtained earlier based on the position that the chapter itself occupies in the TOC - spine = [x[0].split('/')[-1] for x in self.epub.spine] + spine = [x[0].split("/")[-1] for x in self.epub.spine] log.debug(spine) try: items = sorted(items, key=lambda x: spine.index(x.id)) except ValueError: log.warn( - 'Failed to order chapters based on the table of content. Order may be inconsistent' + "Failed to order chapters based on the table of content. Order may be inconsistent" ) return items @@ -312,7 +314,9 @@ def html_content(self): ) cache_key = self.uri.to_uri_string() document_path = self.get_file_system_path() - if (cached_html_content := cache.get(cache_key)) and not cache_utils.is_document_modified(cache_key, document_path, cache): + if ( + cached_html_content := cache.get(cache_key) + ) and not cache_utils.is_document_modified(cache_key, document_path, cache): return cached_html_content.decode("utf-8") html_content_gen = ( (item.file_name, item.content) for item in self.epub_html_items diff --git a/bookworm/document/formats/fitz.py b/bookworm/document/formats/fitz.py index f56730a1..90231c4b 100644 --- a/bookworm/document/formats/fitz.py +++ b/bookworm/document/formats/fitz.py @@ -46,7 +46,7 @@ def _text_from_page(self, page: fitz.Page) -> str: fix_character_width=False, uncurl_quotes=False, fix_latin_ligatures=False, - normalization='NFC' + normalization="NFC", ) return ftfy.fix_text(text, config) diff --git a/bookworm/document/formats/pdf.py b/bookworm/document/formats/pdf.py index 4403e2cc..ae589b49 100644 --- a/bookworm/document/formats/pdf.py +++ b/bookworm/document/formats/pdf.py @@ -62,7 +62,7 @@ def normalize_text(self, text): fix_character_width=False, uncurl_quotes=False, fix_latin_ligatures=False, - normalization='NFC' + normalization="NFC", ) text = ftfy.fix_text(text, config) return super().normalize_text(text) diff --git a/bookworm/document/formats/plain_text.py b/bookworm/document/formats/plain_text.py index 244ee495..16a44156 100644 --- a/bookworm/document/formats/plain_text.py +++ b/bookworm/document/formats/plain_text.py @@ -47,10 +47,10 @@ def get_content(self): fix_character_width=False, uncurl_quotes=False, fix_latin_ligatures=False, - normalization='NFC', + normalization="NFC", unescape_html=False, fix_line_breaks=True, - max_decode_length=MAX_NUM_CHARS + max_decode_length=MAX_NUM_CHARS, ) return ftfy.fix_text(text, config) diff --git a/bookworm/document/formats/word.py b/bookworm/document/formats/word.py index af0bb3f2..a5a14bb0 100644 --- a/bookworm/document/formats/word.py +++ b/bookworm/document/formats/word.py @@ -91,7 +91,9 @@ def _get_html_content_from_docx(self, data_buf, is_encrypted_document): self._get_cache_directory(), eviction_policy="least-frequently-used" ) cache_key = self.uri.to_uri_string() - if (cached_html_content := cache.get(cache_key)) and not cache_utils.is_document_modified(cache_key, doc_path, cache): + if ( + cached_html_content := cache.get(cache_key) + ) and not cache_utils.is_document_modified(cache_key, doc_path, cache): return cached_html_content.decode("utf-8") result = mammoth.convert_to_html(data_buf, include_embedded_style_map=False) data_buf.seek(0) diff --git a/bookworm/document/serde.py b/bookworm/document/serde.py index c62f9b46..ff6a3976 100644 --- a/bookworm/document/serde.py +++ b/bookworm/document/serde.py @@ -13,9 +13,9 @@ def section_to_dict(section: Section) -> dict[str, t.Any]: return { "title": section.title, "pager": section.pager.astuple(), - "text_range": None - if (text_range := section.text_range) is None - else text_range.astuple(), + "text_range": ( + None if (text_range := section.text_range) is None else text_range.astuple() + ), "level": section.level, "data": section.data, } diff --git a/bookworm/gui/components.py b/bookworm/gui/components.py index d173fee8..2f568d33 100644 --- a/bookworm/gui/components.py +++ b/bookworm/gui/components.py @@ -489,8 +489,7 @@ def prevent_mutations(self): def onDeleteItem(self, event): self.prevent_mutations() - def onDeleteAllItems(self, event): - ... + def onDeleteAllItems(self, event): ... def onInsertItem(self, event): self.prevent_mutations() diff --git a/bookworm/gui/settings.py b/bookworm/gui/settings.py index 17168b0e..96400abf 100644 --- a/bookworm/gui/settings.py +++ b/bookworm/gui/settings.py @@ -16,7 +16,7 @@ from bookworm.paths import app_path from bookworm.platforms import PLATFORM from bookworm.resources import app_icons -from bookworm.shell import shell_disintegrate, shell_integrate,is_file_type_associated +from bookworm.shell import shell_disintegrate, shell_integrate, is_file_type_associated from bookworm.shellinfo import get_ext_info from bookworm.signals import app_started, config_updated from bookworm.utils import restart_application @@ -39,6 +39,7 @@ class ReconciliationStrategies(IntEnum): load = auto() save = auto() + class FileAssociationDialog(SimpleDialog): """Associate supported file types.""" @@ -103,7 +104,9 @@ def update_button_for_file_type(self, btn, ext, metadata): # Translators: the main label of a button mlbl = _("Dissociate files of type {format}").format(format=metadata[1]) # Translators: the note of a button - nlbl = _("Dissociate files with {ext} extension so they no longer open in Bookworm").format(ext=ext) + nlbl = _( + "Dissociate files with {ext} extension so they no longer open in Bookworm" + ).format(ext=ext) btn.SetLabel(mlbl) btn.SetNote(nlbl) btn.Bind(wx.EVT_BUTTON, lambda e: self.on_disassociate(btn, ext, metadata)) @@ -111,7 +114,9 @@ def update_button_for_file_type(self, btn, ext, metadata): # Translators: the main label of a button mlbl = _("Associate files of type {format}").format(format=metadata[1]) # Translators: the note of a button - nlbl = _("Associate files with {ext} extension so they always open in Bookworm").format(ext=ext) + nlbl = _( + "Associate files with {ext} extension so they always open in Bookworm" + ).format(ext=ext) btn.SetLabel(mlbl) btn.SetNote(nlbl) btn.Bind(wx.EVT_BUTTON, lambda e: self.on_associate(btn, ext, metadata)) diff --git a/bookworm/logger.py b/bookworm/logger.py index 13fdeed0..058f26a0 100644 --- a/bookworm/logger.py +++ b/bookworm/logger.py @@ -35,7 +35,7 @@ def configure_logger(log_file_suffix="", level: int = logging.DEBUG): error_handler.setFormatter(formatter) error_handler.setLevel(logging.ERROR) logger.addHandler(error_handler) - + # we are actually interested in the stream handler only when we are running from source if not app.is_frozen: stream_handler = logging.StreamHandler(sys.stdout) @@ -44,4 +44,3 @@ def configure_logger(log_file_suffix="", level: int = logging.DEBUG): logger.addHandler(stream_handler) logger.setLevel(level) - \ No newline at end of file diff --git a/bookworm/ocr/ocr_menu.py b/bookworm/ocr/ocr_menu.py index 4af07b43..1436c740 100644 --- a/bookworm/ocr/ocr_menu.py +++ b/bookworm/ocr/ocr_menu.py @@ -324,12 +324,12 @@ def _continue_with_text_extraction(self, ocr_opts, output_file, progress_dlg): ) wx.CallAfter( wx.MessageBox, - message = _( + message=_( "Successfully processed {total} pages.\nExtracted text was written to: {file}" ).format(total=total, file=output_file), - caption = _("OCR Completed"), - style = wx.ICON_INFORMATION | wx.OK, - parent = self.view # 设置 parent 确保对话框聚焦 + caption=_("OCR Completed"), + style=wx.ICON_INFORMATION | wx.OK, + parent=self.view, # 设置 parent 确保对话框聚焦 ) finally: progress_dlg.Dismiss() diff --git a/bookworm/otau.py b/bookworm/otau.py index f0456881..d7155fda 100644 --- a/bookworm/otau.py +++ b/bookworm/otau.py @@ -68,16 +68,17 @@ def get_update_info_for_channel(self, channel_identifier: str) -> VersionInfo: channel_identifier = "" return self.root.get(UpdateChannel.model_validate(channel_identifier)) + def is_newer_version(current_version: str, upstream_version: str) -> bool: """Compare two version strings to determine if upstream_version is newer than current_version. - + Args: current_version: The version string of the current installation upstream_version: The version string of the available update - + Returns: bool: True if upstream_version is newer than current_version - + Note: - Returns False if upstream_version is invalid - Returns True if current_version is invalid but upstream_version is valid @@ -91,13 +92,13 @@ def is_newer_version(current_version: str, upstream_version: str) -> bool: f"Failed to parse version strings: current_version='{current_version}', " f"upstream_version='{upstream_version}'" ) - + # Check if upstream version is valid try: version.parse(upstream_version) except Exception: return False # Invalid upstream version is never newer - + # Check if current version is valid try: version.parse(current_version) diff --git a/bookworm/paths.py b/bookworm/paths.py index 01f1484a..42e0c443 100644 --- a/bookworm/paths.py +++ b/bookworm/paths.py @@ -117,4 +117,4 @@ def fonts_path(): @merge_paths def libs_path() -> str: - return app_path() \ No newline at end of file + return app_path() diff --git a/bookworm/platforms/win32/shell.py b/bookworm/platforms/win32/shell.py index 38068ea3..57752bd7 100644 --- a/bookworm/platforms/win32/shell.py +++ b/bookworm/platforms/win32/shell.py @@ -115,5 +115,7 @@ def is_file_type_associated(ext): except PermissionError: return False except Exception as e: - log.exception(f"Unexpected error when checking file association for {ext}: {str(e)}") + log.exception( + f"Unexpected error when checking file association for {ext}: {str(e)}" + ) return False diff --git a/bookworm/platforms/win32/speech_engines/piper/__init__.py b/bookworm/platforms/win32/speech_engines/piper/__init__.py index c1cccf0b..f3672334 100644 --- a/bookworm/platforms/win32/speech_engines/piper/__init__.py +++ b/bookworm/platforms/win32/speech_engines/piper/__init__.py @@ -44,8 +44,6 @@ os.symlink(espeak_ng_dll, espeak_dll_dst) - - from ..utils import _audio_uri_to_filepath from .tts_system import ( PiperTextToSpeechSystem, diff --git a/bookworm/shell.py b/bookworm/shell.py index ddd4c3e9..ec418b85 100644 --- a/bookworm/shell.py +++ b/bookworm/shell.py @@ -3,6 +3,10 @@ from bookworm.platforms import PLATFORM if PLATFORM == "win32": - from bookworm.platforms.win32.shell import shell_disintegrate, shell_integrate, is_file_type_associated + from bookworm.platforms.win32.shell import ( + shell_disintegrate, + shell_integrate, + is_file_type_associated, + ) elif PLATFORM == "linux": from bookworm.platforms.linux.shell import shell_disintegrate, shell_integrate diff --git a/bookworm/speechdriver/element/converter/base.py b/bookworm/speechdriver/element/converter/base.py index b897e521..61b4f4e9 100644 --- a/bookworm/speechdriver/element/converter/base.py +++ b/bookworm/speechdriver/element/converter/base.py @@ -23,64 +23,48 @@ def convert(self, utterance, *, localeinfo: LocaleInfo = None): ) @abstractmethod - def start(self, localeinfo): - ... + def start(self, localeinfo): ... @abstractmethod - def end(self): - ... + def end(self): ... @abstractmethod - def text(self, content): - ... + def text(self, content): ... @abstractmethod - def ssml(self, content): - ... + def ssml(self, content): ... @abstractmethod - def sentence(self, content): - ... + def sentence(self, content): ... @abstractmethod - def bookmark(self, content): - ... + def bookmark(self, content): ... @abstractmethod - def pause(self, content): - ... + def pause(self, content): ... @abstractmethod - def audio(self, content): - ... + def audio(self, content): ... @abstractmethod - def start_paragraph(self, content): - ... + def start_paragraph(self, content): ... @abstractmethod - def end_paragraph(self, content): - ... + def end_paragraph(self, content): ... @abstractmethod - def start_voice(self, content): - ... + def start_voice(self, content): ... @abstractmethod - def end_voice(self, content): - ... + def end_voice(self, content): ... @abstractmethod - def start_emph(self, content): - ... + def start_emph(self, content): ... - def end_emph(self, content): - ... + def end_emph(self, content): ... @abstractmethod - def start_prosody(self, content): - ... + def start_prosody(self, content): ... @abstractmethod - def end_prosody(self, content): - ... + def end_prosody(self, content): ... diff --git a/bookworm/structured_text/structured_html_parser.py b/bookworm/structured_text/structured_html_parser.py index e6a03803..6afd56b7 100644 --- a/bookworm/structured_text/structured_html_parser.py +++ b/bookworm/structured_text/structured_html_parser.py @@ -111,10 +111,10 @@ def normalize_html(html_string): fix_character_width=False, uncurl_quotes=False, fix_latin_ligatures=False, - normalization='NFC', + normalization="NFC", unescape_html=False, fix_line_breaks=True, - max_decode_length=MAX_DECODE_LENGTH + max_decode_length=MAX_DECODE_LENGTH, ) html_string = ftfy.fix_text(html_string, config) return remove_excess_blank_lines(html_string) diff --git a/setup.py b/setup.py index 398fb9f4..28d91ce4 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ LONG_DESCRIPTION = "Bookworm is the universally accessible document reader.\nVisit [the project's home](https://github.com/blindpandas/bookworm) for more information." REQUIREMENTS = [] -with open(CWD / "requirements-app.txt", "r", encoding='utf-8') as reqs: +with open(CWD / "requirements-app.txt", "r", encoding="utf-8") as reqs: for line in reqs: if any(line.startswith(prfx) for prfx in INVALID_PREFIXES): continue diff --git a/tasks.py b/tasks.py index 597e257c..d5af310a 100644 --- a/tasks.py +++ b/tasks.py @@ -459,7 +459,9 @@ def copy_espeak_and_piper_libs(): onnxruntime_notices_src = ( PROJECT_ROOT / "scripts" / "dlls" / "onnxruntime" / "notices" ) - onnxruntime_dst = Path(os.environ["IAPP_FROZEN_DIRECTORY"]) / "_internal" / "onnxruntime" + onnxruntime_dst = ( + Path(os.environ["IAPP_FROZEN_DIRECTORY"]) / "_internal" / "onnxruntime" + ) onnxruntime_dst.mkdir(parents=True, exist_ok=True) print("Copying ONNXRuntime dll and notices...") @@ -562,7 +564,9 @@ def gen_update_info_file(c): channel = version_info.get("pre_type", "") # Stable version if no pre_type # Define base URLs and file paths for x86 and x64 builds - base_url = f"https://github.com/blindpandas/bookworm/releases/download/{app.version}" + base_url = ( + f"https://github.com/blindpandas/bookworm/releases/download/{app.version}" + ) x86_file = f"{app.display_name}-{app.version}-x86-update.bundle" x64_file = f"{app.display_name}-{app.version}-x64-update.bundle" x86_download_url = f"{base_url}/{x86_file}" @@ -573,12 +577,21 @@ def gen_update_info_file(c): x64_bundle_path = artifacts_folder / x64_file # Generate SHA1 hash or use default if file does not exist - x86_sha1hash = generate_sha1hash(x86_bundle_path) if x86_bundle_path.exists() else "example_x86_sha1hash" - x64_sha1hash = generate_sha1hash(x64_bundle_path) if x64_bundle_path.exists() else "example_x64_sha1hash" + x86_sha1hash = ( + generate_sha1hash(x86_bundle_path) + if x86_bundle_path.exists() + else "example_x86_sha1hash" + ) + x64_sha1hash = ( + generate_sha1hash(x64_bundle_path) + if x64_bundle_path.exists() + else "example_x64_sha1hash" + ) # Construct the update info dictionary update_info = { - channel or "": { # Ensure stable version uses an empty string key + channel + or "": { # Ensure stable version uses an empty string key "version": app.version, "x86_download": x86_download_url, "x64_download": x64_download_url, @@ -587,7 +600,7 @@ def gen_update_info_file(c): } } -# update_info_file = artifacts_folder / "update_info.json" + # update_info_file = artifacts_folder / "update_info.json" update_info_file = PROJECT_ROOT / "update_info.json" # Read the existing data if the file exists, otherwise start with an empty dictionary @@ -741,7 +754,7 @@ def freeze(c): ) # This is required because pyxpdf_data looks for a default.xpdf file inside the site-packages folder # TODO: Fix this if at all possible - lib = c['build_folder'] / "Lib" / "site-packages" + lib = c["build_folder"] / "Lib" / "site-packages" os.makedirs(str(lib)) print("App freezed.") @@ -767,7 +780,7 @@ def build(c): """Freeze, package, and prepare the app for distribution.""" # The following fixes a bug on windows where some DLL's are not # deletable due to pyinstaller copying them - # without clearing their read-only status + # without clearing their read-only status if sys.platform == "win32": build_folder = Path(c["build_folder"]) for dll_file in build_folder.glob("*.dll"): @@ -808,7 +821,7 @@ def bench(c, filename="tests/assets/epub30-spec.epub", runs=5): return arch = os.environ["IAPP_ARCH"] c.run( - f'hyperfine -N -r {runs} -w 1 ' + f"hyperfine -N -r {runs} -w 1 " f'--export-json "scripts\\benchmark-{arch}.json" ' f'"python -m bookworm benchmark {filename}"' ) diff --git a/tests/conftest.py b/tests/conftest.py index 95ed2e39..ef0d05b5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,10 +22,11 @@ def asset(): class DummyTextCtrl: def SetFocus(self): pass - + class DummyView: """Represents a mock for the bookworm view""" + def __init__(self): self.title = "" self.toc_tree = None @@ -33,7 +34,7 @@ def __init__(self): self.insertion_point = 0 self.state_on_section_change: Section = None self.contentTextCtrl = DummyTextCtrl() - + def add_toc_tree(self, tree): self.toc_tree = tree @@ -45,7 +46,7 @@ def set_title(self, title): def get_insertion_point(self): return self.insertion_point - + def set_state_on_section_change(self, value: Section) -> None: self.state_on_section_change = value @@ -57,34 +58,35 @@ def set_insertion_point(self, point: int) -> None: def go_to_position(self, start: int, end: int) -> None: self.set_insertion_point(start) - + def go_to_webpage(self, url: str) -> None: pass def show_html_dialog(self, markup: str, title: str) -> None: pass + @pytest.fixture() def text_ctrl(): yield DummyTextCtrl() + @pytest.fixture def view(text_ctrl): v = DummyView() v.contentTextCtrl = text_ctrl yield v + @pytest.fixture() def engine(tmp_path): - temp_file = tmp_path / "test.db" - engine = init_database( - url=f"sqlite:///{temp_file}", - poolclass = NullPool - ) + temp_file = tmp_path / "test.db" + engine = init_database(url=f"sqlite:///{temp_file}", poolclass=NullPool) yield engine close_all_sessions() engine.dispose() + @pytest.fixture() def reader(view, engine): setup_config() diff --git a/tests/test_annotator.py b/tests/test_annotator.py index e2fb064e..db55636f 100644 --- a/tests/test_annotator.py +++ b/tests/test_annotator.py @@ -1,17 +1,20 @@ import pytest from bookworm.annotation import NoteTaker -from bookworm.database.models import * +from bookworm.database.models import * from bookworm.document.uri import DocumentUri from conftest import asset, reader + def test_notes_can_not_overlap(asset, reader): - uri = DocumentUri.from_filename(asset('roman.epub')) + uri = DocumentUri.from_filename(asset("roman.epub")) reader.load(uri) annot = NoteTaker(reader) # This should succeed - comment = annot.create(title='test', content="test", position=0, start_pos=0, end_pos=1) + comment = annot.create( + title="test", content="test", position=0, start_pos=0, end_pos=1 + ) # check if it overlaps at start_pos 0, end_pos 1, page_number 0 and position 0 assert annot.overlaps(0, 1, 0, 0) == True # Check if no selection with position 0 and page_number 0 overlaps with the existing annotation diff --git a/tests/test_epub.py b/tests/test_epub.py index a298692f..e200982a 100644 --- a/tests/test_epub.py +++ b/tests/test_epub.py @@ -6,14 +6,13 @@ from bookworm.document.uri import DocumentUri from bookworm.document.formats.epub import EpubDocument -def temp_book(title: str = 'Sample book') -> epub.EpubBook: + +def temp_book(title: str = "Sample book") -> epub.EpubBook: book = epub.EpubBook() book.set_title("test book") - book.set_language('en') + book.set_language("en") c1 = epub.EpubHtml(title="Intro", file_name="chap_01.xhtml", lang="en") - c1.content = ( - "

This is a test

" - ) + c1.content = "

This is a test

" book.add_item(c1) book.toc = ( epub.Link("chap_01.xhtml", "Introduction", "intro"), @@ -27,30 +26,29 @@ def temp_book(title: str = 'Sample book') -> epub.EpubBook: def test_chapter_order_is_unchanged_with_roman_numbers(asset): - doc = EpubDocument(DocumentUri.from_filename(asset('roman.epub'))) + doc = EpubDocument(DocumentUri.from_filename(asset("roman.epub"))) doc.read() spine = [x[0] for x in doc.epub.spine] - items = [x.file_name.split('/')[-1] for x in doc.epub_html_items] + items = [x.file_name.split("/")[-1] for x in doc.epub_html_items] assert spine == items + def test_modified_epub_modifies_cache(asset): book = temp_book() epub.write_epub(asset("test.epub"), book, {}) - doc = EpubDocument(DocumentUri.from_filename(asset('test.epub'))) + doc = EpubDocument(DocumentUri.from_filename(asset("test.epub"))) doc.read() content = doc.html_content # Let's now add a second chapter, and see whether the document read modifies its cache c2 = epub.EpubHtml(title="Second chapter", file_name="chap_02.xhtml", lang="en") - c2.content = ( - "

This is another test

" - ) + c2.content = "

This is another test

" book.add_item(c2) book.spine.append(c2) epub.write_epub(asset("test.epub"), book, {}) # read the book once more, and verify that the content is different - doc = EpubDocument(DocumentUri.from_filename(asset('test.epub'))) + doc = EpubDocument(DocumentUri.from_filename(asset("test.epub"))) doc.read() new_content = doc.html_content Path(asset("test.epub")).unlink() diff --git a/tests/test_otau.py b/tests/test_otau.py index 67359ef7..7fa9e987 100644 --- a/tests/test_otau.py +++ b/tests/test_otau.py @@ -5,17 +5,17 @@ def test_is_not_valid_identifier(): with pytest.raises(TypeError): - channel = UpdateChannel('test') + channel = UpdateChannel("test") def test_is_valid_identifier(): - valid_identifiers = ('', 'a', 'b', 'rc') + valid_identifiers = ("", "a", "b", "rc") for identifier in valid_identifiers: channel = UpdateChannel(identifier) def test_is_major_version(): - c = UpdateChannel('') + c = UpdateChannel("") assert c.is_major == True @@ -27,22 +27,27 @@ def test_is_major_version(): ("2024.1", "2025.1", True), # Major version increment ("2025.1", "2024.4.2", False), # Current version newer ("2024.1.0", "2024.1.1", True), # Patch version increment - # Pre-release versions ("2024.1rc1", "2024.1", True), # Release candidate to final ("2024.1", "2024.1rc1", False), # Final to release candidate ("2024.1a1", "2024.1b1", True), # Alpha to beta ("2024.1b1", "2024.1rc1", True), # Beta to release candidate - # Complex versions ("2024.1.0.0", "2024.1.0.1", True), # Four-part version ("2024.1.post1", "2024.2", True), # Post-release version ("2024.1.dev1", "2024.1", True), # Development version - # Edge cases ("2024.1", "2024.1", False), # Same version - ("invalid", "2024.1", True), # Invalid current version (fallback to string comparison) - ("2024.1", "invalid", False), # Invalid upstream version (fallback to string comparison) + ( + "invalid", + "2024.1", + True, + ), # Invalid current version (fallback to string comparison) + ( + "2024.1", + "invalid", + False, + ), # Invalid upstream version (fallback to string comparison) ], ) def test_version_comparison(current_version, upstream_version, expected):