update to latest black

This commit is contained in:
Damien Elmes 2020-08-31 13:29:28 +10:00
parent 603210149c
commit a517accee3
24 changed files with 395 additions and 320 deletions

View File

@ -129,9 +129,11 @@ class Card:
self, reload: bool = False, browser: bool = False
) -> anki.template.TemplateRenderOutput:
if not self._render_output or reload:
self._render_output = anki.template.TemplateRenderContext.from_existing_card(
self, browser
).render()
self._render_output = (
anki.template.TemplateRenderContext.from_existing_card(
self, browser
).render()
)
return self._render_output
def set_render_output(self, output: anki.template.TemplateRenderOutput) -> None:

View File

@ -104,7 +104,12 @@ class DeckManager:
# Deck save/load
#############################################################
def id(self, name: str, create: bool = True, type: int = 0,) -> Optional[int]:
def id(
self,
name: str,
create: bool = True,
type: int = 0,
) -> Optional[int]:
"Add a deck with NAME. Reuse deck if already exists. Return id as int."
id = self.id_for_name(name)
if id:

View File

@ -232,9 +232,9 @@ exporters_list_created = _ExportersListCreatedHook()
class _FieldFilterFilter:
"""Allows you to define custom {{filters:..}}
Your add-on can check filter_name to decide whether it should modify
field_text or not before returning it."""
Your add-on can check filter_name to decide whether it should modify
field_text or not before returning it."""
_hooks: List[
Callable[[str, str, str, "anki.template.TemplateRenderContext"], str]
@ -395,7 +395,7 @@ notes_will_be_deleted = _NotesWillBeDeletedHook()
class _SchedulerNewLimitForSingleDeckFilter:
"""Allows changing the number of new card for this deck (without
considering descendants)."""
considering descendants)."""
_hooks: List[Callable[[int, "anki.decks.Deck"], int]] = []
@ -426,7 +426,7 @@ scheduler_new_limit_for_single_deck = _SchedulerNewLimitForSingleDeckFilter()
class _SchedulerReviewLimitForSingleDeckFilter:
"""Allows changing the number of rev card for this deck (without
considering descendants)."""
considering descendants)."""
_hooks: List[Callable[[int, "anki.decks.Deck"], int]] = []

View File

@ -437,7 +437,15 @@ and notes.mid = ? and cards.ord = ?""",
for c in range(nfields):
flds.append(newflds.get(c, ""))
flds = joinFields(flds)
d.append((flds, newModel["id"], intTime(), self.col.usn(), nid,))
d.append(
(
flds,
newModel["id"],
intTime(),
self.col.usn(),
nid,
)
)
self.col.db.executemany(
"update notes set flds=?,mid=?,mod=?,usn=? where id = ?", d
)

View File

@ -209,7 +209,9 @@ class RustBackend(RustBackendGenerated):
langs = [anki.lang.currentLang]
init_msg = pb.BackendInit(
locale_folder_path=ftl_folder, preferred_langs=langs, server=server,
locale_folder_path=ftl_folder,
preferred_langs=langs,
server=server,
)
self._backend = ankirspy.open_backend(init_msg.SerializeToString())

View File

@ -668,7 +668,10 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
return tot + tod * 1000
def _leftToday(
self, delays: List[int], left: int, now: Optional[int] = None,
self,
delays: List[int],
left: int,
now: Optional[int] = None,
) -> int:
"The number of steps that can be completed by the day cutoff."
if not now:
@ -1603,7 +1606,16 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""",
mod = intTime()
for id in ids:
r = random.randint(imin, imax)
d.append((max(1, r), r + t, self.col.usn(), mod, STARTING_FACTOR, id,))
d.append(
(
max(1, r),
r + t,
self.col.usn(),
mod,
STARTING_FACTOR,
id,
)
)
self.remFromDyn(ids)
self.col.db.executemany(
f"""

View File

@ -243,7 +243,9 @@ from revlog where id > ? """
def _dueInfo(self, tot: int, num: int) -> str:
i: List[str] = []
self._line(
i, _("Total"), self.col.tr(TR.STATISTICS_REVIEWS, reviews=tot),
i,
_("Total"),
self.col.tr(TR.STATISTICS_REVIEWS, reviews=tot),
)
self._line(i, _("Average"), self._avgDay(tot, num, _("reviews")))
tomorrow = self.col.db.scalar(
@ -436,7 +438,9 @@ group by day order by day"""
return self._lineTbl(i), int(tot)
def _splitRepData(
self, data: List[Tuple[Any, ...]], spec: Sequence[Tuple[int, str, str]],
self,
data: List[Tuple[Any, ...]],
spec: Sequence[Tuple[int, str, str]],
) -> Tuple[List[Dict[str, Any]], List[Tuple[Any, Any]]]:
sep: Dict[int, Any] = {}
totcnt = {}

View File

@ -1,6 +1,6 @@
wheel
mypy
mypy_protobuf>=1.21
black
black>=20.1b0
pytest>=6.0.1
stringcase==1.2.0

View File

@ -93,10 +93,14 @@ hooks = [
doc="Obsolete, do not use.",
),
Hook(
name="sync_stage_did_change", args=["stage: str"], doc="Obsolete, do not use.",
name="sync_stage_did_change",
args=["stage: str"],
doc="Obsolete, do not use.",
),
Hook(
name="sync_progress_did_change", args=["msg: str"], doc="Obsolete, do not use.",
name="sync_progress_did_change",
args=["msg: str"],
doc="Obsolete, do not use.",
),
]

View File

@ -144,22 +144,22 @@ class DialogManager:
"""Allows add-ons to register a custom dialog to be managed by Anki's dialog
manager, which ensures that only one copy of the window is open at once,
and that the dialog cleans up asynchronously when the collection closes
Please note that dialogs added in this manner need to define a close behavior
by either:
- setting `dialog.silentlyClose = True` to have it close immediately
- define a `dialog.closeWithCallback()` method that is called when closed
by the dialog manager
TODO?: Implement more restrictive type check to ensure these requirements
are met
Arguments:
name {str} -- Name/identifier of the dialog in question
creator {Union[Callable, type]} -- A class or function to create new
dialog instances with
Keyword Arguments:
instance {Optional[Any]} -- An optional existing instance of the dialog
(default: {None})

View File

@ -760,7 +760,9 @@ class AddonsDialog(QDialog):
def should_grey(self, addon: AddonMeta) -> bool:
return not addon.enabled or not addon.compatible()
def redrawAddons(self,) -> None:
def redrawAddons(
self,
) -> None:
addonList = self.form.addonList
mgr = self.mgr
@ -1321,7 +1323,11 @@ class ConfigEditor(QDialog):
def updateText(self, conf: Dict[str, Any]) -> None:
text = json.dumps(
conf, ensure_ascii=False, sort_keys=True, indent=4, separators=(",", ": "),
conf,
ensure_ascii=False,
sort_keys=True,
indent=4,
separators=(",", ": "),
)
text = gui_hooks.addon_config_editor_will_display_json(text)
self.form.editor.setPlainText(text)

View File

@ -166,13 +166,19 @@ class CardLayout(QDialog):
self.tform.back_button.setToolTip(shortcut("Ctrl+2"))
self.tform.style_button.setToolTip(shortcut("Ctrl+3"))
QShortcut( # type: ignore
QKeySequence("Ctrl+1"), self, activated=self.tform.front_button.click,
QKeySequence("Ctrl+1"),
self,
activated=self.tform.front_button.click,
)
QShortcut( # type: ignore
QKeySequence("Ctrl+2"), self, activated=self.tform.back_button.click,
QKeySequence("Ctrl+2"),
self,
activated=self.tform.back_button.click,
)
QShortcut( # type: ignore
QKeySequence("Ctrl+3"), self, activated=self.tform.style_button.click,
QKeySequence("Ctrl+3"),
self,
activated=self.tform.style_button.click,
)
# Main area setup
@ -303,7 +309,10 @@ class CardLayout(QDialog):
"reviewer.js",
]
self.preview_web.stdHtml(
self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc, context=self,
self.mw.reviewer.revHtml(),
css=["reviewer.css"],
js=jsinc,
context=self,
)
self.preview_web.set_bridge_command(self._on_bridge_cmd, self)
@ -764,7 +773,9 @@ Enter deck to place new %s cards in, or leave blank:"""
row = form.fields.currentIndex().row()
if row >= 0:
self._addField(
fields[row], form.font.currentFont().family(), form.size.value(),
fields[row],
form.font.currentFont().family(),
form.size.value(),
)
def _addField(self, field, font, size):

View File

@ -28,7 +28,7 @@ class DeckBrowserBottomBar:
class DeckBrowserContent:
"""Stores sections of HTML content that the deck browser will be
populated with.
Attributes:
tree {str} -- HTML of the deck tree section
stats {str} -- HTML of the stats section
@ -118,7 +118,8 @@ class DeckBrowser:
def __renderPage(self, offset):
content = DeckBrowserContent(
tree=self._renderDeckTree(self._dueTree), stats=self._renderStats(),
tree=self._renderDeckTree(self._dueTree),
stats=self._renderStats(),
)
gui_hooks.deck_browser_will_render_content(self, content)
self.web.stdHtml(

View File

@ -233,7 +233,9 @@ class Editor:
self._links[cmd] = func
if keys:
QShortcut( # type: ignore
QKeySequence(keys), self.widget, activated=lambda s=self: func(s),
QKeySequence(keys),
self.widget,
activated=lambda s=self: func(s),
)
btn = self._addButton(
icon,

View File

@ -120,7 +120,12 @@ class ExportDialog(QDialog):
key_str = self.exporter.key
while 1:
file = getSaveFile(
self, _("Export"), "export", key_str, self.exporter.ext, fname=filename,
self,
_("Export"),
"export",
key_str,
self.exporter.ext,
fname=filename,
)
if not file:
return
@ -175,14 +180,18 @@ class ExportDialog(QDialog):
if self.isTextNote:
msg = (
ngettext(
"%d note exported.", "%d notes exported.", self.exporter.count,
"%d note exported.",
"%d notes exported.",
self.exporter.count,
)
% self.exporter.count
)
else:
msg = (
ngettext(
"%d card exported.", "%d cards exported.", self.exporter.count,
"%d card exported.",
"%d cards exported.",
self.exporter.count,
)
% self.exporter.count
)

View File

@ -84,12 +84,12 @@ add_cards_did_init = _AddCardsDidInitHook()
class _AddCardsWillAddNoteFilter:
"""Decides whether the note should be added to the collection or
not. It is assumed to come from the addCards window.
not. It is assumed to come from the addCards window.
reason_to_already_reject is the first reason to reject that
was found, or None. If your filter wants to reject, it should
replace return the reason to reject. Otherwise return the
input."""
reason_to_already_reject is the first reason to reject that
was found, or None. If your filter wants to reject, it should
replace return the reason to reject. Otherwise return the
input."""
_hooks: List[Callable[[Optional[str], "anki.notes.Note"], Optional[str]]] = []
@ -183,10 +183,10 @@ addcards_will_add_history_entry = _AddcardsWillAddHistoryEntryFilter()
class _AddonConfigEditorWillDisplayJsonFilter:
"""Allows changing the text of the json configuration before actually
displaying it to the user. For example, you can replace "\n" by
some actual new line. Then you can replace the new lines by "\n"
while reading the file and let the user uses real new line in
string instead of its encoding."""
displaying it to the user. For example, you can replace "\n" by
some actual new line. Then you can replace the new lines by "\n"
while reading the file and let the user uses real new line in
string instead of its encoding."""
_hooks: List[Callable[[str], str]] = []
@ -217,8 +217,8 @@ addon_config_editor_will_display_json = _AddonConfigEditorWillDisplayJsonFilter(
class _AddonConfigEditorWillSaveJsonFilter:
"""Allows changing the text of the json configuration that was
received from the user before actually reading it. For
example, you can replace new line in strings by some "\n"."""
received from the user before actually reading it. For
example, you can replace new line in strings by some "\n"."""
_hooks: List[Callable[[str], str]] = []
@ -286,7 +286,7 @@ addons_dialog_did_change_selected_addon = _AddonsDialogDidChangeSelectedAddonHoo
class _AddonsDialogWillShowHook:
"""Allows changing the add-on dialog before it is shown. E.g. add
buttons."""
buttons."""
_hooks: List[Callable[["aqt.addons.AddonsDialog"], None]] = []
@ -542,40 +542,40 @@ browser_menus_did_init = _BrowserMenusDidInitHook()
class _BrowserWillBuildTreeFilter:
"""Used to add or replace items in the browser sidebar tree
'tree' is the root SidebarItem that all other items are added to.
'stage' is an enum describing the different construction stages of
the sidebar tree at which you can interject your changes.
The different values can be inspected by looking at
aqt.browser.SidebarStage.
If you want Anki to proceed with the construction of the tree stage
in question after your have performed your changes or additions,
return the 'handled' boolean unchanged.
On the other hand, if you want to prevent Anki from adding its own
items at a particular construction stage (e.g. in case your add-on
implements its own version of that particular stage), return 'True'.
If you return 'True' at SidebarStage.ROOT, the sidebar will not be
populated by any of the other construction stages. For any other stage
the tree construction will just continue as usual.
For example, if your code wishes to replace the tag tree, you could do:
def on_browser_will_build_tree(handled, root, stage, browser):
if stage != SidebarStage.TAGS:
# not at tag tree building stage, pass on
return handled
# your tag tree construction code
# root.addChild(...)
# your code handled tag tree construction, no need for Anki
# or other add-ons to build the tag tree
return True
"""
'tree' is the root SidebarItem that all other items are added to.
'stage' is an enum describing the different construction stages of
the sidebar tree at which you can interject your changes.
The different values can be inspected by looking at
aqt.browser.SidebarStage.
If you want Anki to proceed with the construction of the tree stage
in question after your have performed your changes or additions,
return the 'handled' boolean unchanged.
On the other hand, if you want to prevent Anki from adding its own
items at a particular construction stage (e.g. in case your add-on
implements its own version of that particular stage), return 'True'.
If you return 'True' at SidebarStage.ROOT, the sidebar will not be
populated by any of the other construction stages. For any other stage
the tree construction will just continue as usual.
For example, if your code wishes to replace the tag tree, you could do:
def on_browser_will_build_tree(handled, root, stage, browser):
if stage != SidebarStage.TAGS:
# not at tag tree building stage, pass on
return handled
# your tag tree construction code
# root.addChild(...)
# your code handled tag tree construction, no need for Anki
# or other add-ons to build the tag tree
return True
"""
_hooks: List[
Callable[
@ -644,16 +644,16 @@ browser_will_build_tree = _BrowserWillBuildTreeFilter()
class _BrowserWillSearchHook:
"""Allows you to modify the search text, or perform your own search.
You can modify context.search to change the text that is sent to the
searching backend.
If you set context.card_ids to a list of ids, the regular search will
not be performed, and the provided ids will be used instead.
Your add-on should check if context.card_ids is not None, and return
without making changes if it has been set.
"""
You can modify context.search to change the text that is sent to the
searching backend.
If you set context.card_ids to a list of ids, the regular search will
not be performed, and the provided ids will be used instead.
Your add-on should check if context.card_ids is not None, and return
without making changes if it has been set.
"""
_hooks: List[Callable[["aqt.browser.SearchContext"], None]] = []
@ -739,7 +739,7 @@ browser_will_show_context_menu = _BrowserWillShowContextMenuHook()
class _CardLayoutWillShowHook:
"""Allow to change the display of the card layout. After most values are
set and before the window is actually shown."""
set and before the window is actually shown."""
_hooks: List[Callable[["aqt.clayout.CardLayout"], None]] = []
@ -859,7 +859,7 @@ current_note_type_did_change = _CurrentNoteTypeDidChangeHook()
class _DebugConsoleDidEvaluatePythonFilter:
"""Allows processing the debug result. E.g. logging queries and
result, saving last query to display it later..."""
result, saving last query to display it later..."""
_hooks: List[Callable[[str, str, QDialog], str]] = []
@ -890,7 +890,7 @@ debug_console_did_evaluate_python = _DebugConsoleDidEvaluatePythonFilter()
class _DebugConsoleWillShowHook:
"""Allows editing the debug window. E.g. setting a default code, or
previous code."""
previous code."""
_hooks: List[Callable[[QDialog], None]] = []
@ -949,19 +949,19 @@ deck_browser_did_render = _DeckBrowserDidRenderHook()
class _DeckBrowserWillRenderContentHook:
"""Used to modify HTML content sections in the deck browser body
'content' contains the sections of HTML content the deck browser body
will be updated with.
When modifying the content of a particular section, please make sure your
changes only perform the minimum required edits to make your add-on work.
You should avoid overwriting or interfering with existing data as much
as possible, instead opting to append your own changes, e.g.:
def on_deck_browser_will_render_content(deck_browser, content):
content.stats += "
<div>my html</div>"
"""
'content' contains the sections of HTML content the deck browser body
will be updated with.
When modifying the content of a particular section, please make sure your
changes only perform the minimum required edits to make your add-on work.
You should avoid overwriting or interfering with existing data as much
as possible, instead opting to append your own changes, e.g.:
def on_deck_browser_will_render_content(deck_browser, content):
content.stats += "
<div>my html</div>"
"""
_hooks: List[
Callable[
@ -1039,14 +1039,14 @@ deck_browser_will_show_options_menu = _DeckBrowserWillShowOptionsMenuHook()
class _DeckConfDidAddConfigHook:
"""Allows modification of a newly created config group
This hook is called after the config group was created, but
before initializing the widget state.
This hook is called after the config group was created, but
before initializing the widget state.
`deck_conf` will point to the old config group, `new_conf_id` will
point to the newly created config group.
`deck_conf` will point to the old config group, `new_conf_id` will
point to the newly created config group.
Config groups are created as clones of the current one.
"""
Config groups are created as clones of the current one.
"""
_hooks: List[
Callable[["aqt.deckconf.DeckConf", Deck, DeckConfig, str, int], None]
@ -1549,7 +1549,7 @@ editor_web_view_did_init = _EditorWebViewDidInitHook()
class _EditorWillLoadNoteFilter:
"""Allows changing the javascript commands to load note before
executing it and do change in the QT editor."""
executing it and do change in the QT editor."""
_hooks: List[Callable[[str, "anki.notes.Note", "aqt.editor.Editor"], str]] = []
@ -1704,12 +1704,12 @@ empty_cards_will_show = _EmptyCardsWillShowHook()
class _MainWindowDidInitHook:
"""Executed after the main window is fully initialized
A sample use case for this hook would be to delay actions until Anki objects
like the profile or collection are fully initialized. In contrast to
`profile_did_open`, this hook will only fire once per Anki session and
is thus suitable for single-shot subscribers.
"""
A sample use case for this hook would be to delay actions until Anki objects
like the profile or collection are fully initialized. In contrast to
`profile_did_open`, this hook will only fire once per Anki session and
is thus suitable for single-shot subscribers.
"""
_hooks: List[Callable[[], None]] = []
@ -1740,13 +1740,13 @@ main_window_did_init = _MainWindowDidInitHook()
class _MainWindowShouldRequireResetFilter:
"""Executed before the main window will require a reset
This hook can be used to change the behavior of the main window,
when other dialogs, like the AddCards or Browser, require a reset
from the main window.
If you decide to use this hook, make you sure you check the reason for the reset.
Some reasons require more attention than others, and skipping important ones might
put the main window into an invalid state (e.g. display a deleted note).
"""
This hook can be used to change the behavior of the main window,
when other dialogs, like the AddCards or Browser, require a reset
from the main window.
If you decide to use this hook, make you sure you check the reason for the reset.
Some reasons require more attention than others, and skipping important ones might
put the main window into an invalid state (e.g. display a deleted note).
"""
_hooks: List[
Callable[[bool, "Union[aqt.main.ResetReason, str]", Optional[Any]], bool]
@ -1871,7 +1871,7 @@ models_advanced_will_show = _ModelsAdvancedWillShowHook()
class _OverviewDidRefreshHook:
"""Allow to update the overview window. E.g. add the deck name in the
title."""
title."""
_hooks: List[Callable[["aqt.overview.Overview"], None]] = []
@ -1902,18 +1902,18 @@ overview_did_refresh = _OverviewDidRefreshHook()
class _OverviewWillRenderContentHook:
"""Used to modify HTML content sections in the overview body
'content' contains the sections of HTML content the overview body
will be updated with.
'content' contains the sections of HTML content the overview body
will be updated with.
When modifying the content of a particular section, please make sure your
changes only perform the minimum required edits to make your add-on work.
You should avoid overwriting or interfering with existing data as much
as possible, instead opting to append your own changes, e.g.:
When modifying the content of a particular section, please make sure your
changes only perform the minimum required edits to make your add-on work.
You should avoid overwriting or interfering with existing data as much
as possible, instead opting to append your own changes, e.g.:
def on_overview_will_render_content(overview, content):
content.table += "
<div>my html</div>"
"""
def on_overview_will_render_content(overview, content):
content.table += "
<div>my html</div>"
"""
_hooks: List[
Callable[["aqt.overview.Overview", "aqt.overview.OverviewContent"], None]
@ -1953,11 +1953,11 @@ overview_will_render_content = _OverviewWillRenderContentHook()
class _ProfileDidOpenHook:
"""Executed whenever a user profile has been opened
Please note that this hook will also be called on profile switches, so if you
are looking to simply delay an add-on action in a single-shot manner,
`main_window_did_init` is likely the more suitable choice.
"""
Please note that this hook will also be called on profile switches, so if you
are looking to simply delay an add-on action in a single-shot manner,
`main_window_did_init` is likely the more suitable choice.
"""
_hooks: List[Callable[[], None]] = []
@ -2132,14 +2132,14 @@ reviewer_did_show_question = _ReviewerDidShowQuestionHook()
class _ReviewerWillAnswerCardFilter:
"""Used to modify the ease at which a card is rated or to bypass
rating the card completely.
ease_tuple is a tuple consisting of a boolean expressing whether the reviewer
should continue with rating the card, and an integer expressing the ease at
which the card should be rated.
If your code just needs to be notified of the card rating event, you should use
the reviewer_did_answer_card hook instead."""
rating the card completely.
ease_tuple is a tuple consisting of a boolean expressing whether the reviewer
should continue with rating the card, and an integer expressing the ease at
which the card should be rated.
If your code just needs to be notified of the card rating event, you should use
the reviewer_did_answer_card hook instead."""
_hooks: List[
Callable[[Tuple[bool, int], "aqt.reviewer.Reviewer", Card], Tuple[bool, int]]
@ -2216,16 +2216,16 @@ reviewer_will_end = _ReviewerWillEndHook()
class _ReviewerWillInitAnswerButtonsFilter:
"""Used to modify list of answer buttons
buttons_tuple is a tuple of buttons, with each button represented by a
tuple containing an int for the button's ease and a string for the
button's label.
buttons_tuple is a tuple of buttons, with each button represented by a
tuple containing an int for the button's ease and a string for the
button's label.
Return a tuple of the form ((int, str), ...), e.g.:
((1, "Label1"), (2, "Label2"), ...)
Return a tuple of the form ((int, str), ...), e.g.:
((1, "Label1"), (2, "Label2"), ...)
Note: import _ from anki.lang to support translation, using, e.g.,
((1, _("Label1")), ...)
"""
Note: import _ from anki.lang to support translation, using, e.g.,
((1, _("Label1")), ...)
"""
_hooks: List[
Callable[
@ -2279,15 +2279,15 @@ reviewer_will_init_answer_buttons = _ReviewerWillInitAnswerButtonsFilter()
class _ReviewerWillPlayAnswerSoundsHook:
"""Called before showing the answer/back side.
`tags` can be used to inspect and manipulate the sounds
that will be played (if any).
`tags` can be used to inspect and manipulate the sounds
that will be played (if any).
This won't be called when the user manually plays sounds
using `Replay Audio`.
This won't be called when the user manually plays sounds
using `Replay Audio`.
Note that this hook is called even when the `Automatically play audio`
option is unchecked; This is so as to allow playing custom
sounds regardless of that option."""
Note that this hook is called even when the `Automatically play audio`
option is unchecked; This is so as to allow playing custom
sounds regardless of that option."""
_hooks: List[Callable[[Card, "List[anki.sound.AVTag]"], None]] = []
@ -2318,15 +2318,15 @@ reviewer_will_play_answer_sounds = _ReviewerWillPlayAnswerSoundsHook()
class _ReviewerWillPlayQuestionSoundsHook:
"""Called before showing the question/front side.
`tags` can be used to inspect and manipulate the sounds
that will be played (if any).
`tags` can be used to inspect and manipulate the sounds
that will be played (if any).
This won't be called when the user manually plays sounds
using `Replay Audio`.
This won't be called when the user manually plays sounds
using `Replay Audio`.
Note that this hook is called even when the `Automatically play audio`
option is unchecked; This is so as to allow playing custom
sounds regardless of that option."""
Note that this hook is called even when the `Automatically play audio`
option is unchecked; This is so as to allow playing custom
sounds regardless of that option."""
_hooks: List[Callable[[Card, "List[anki.sound.AVTag]"], None]] = []
@ -2701,15 +2701,15 @@ tag_editor_did_process_key = _TagEditorDidProcessKeyHook()
class _TopToolbarDidInitLinksHook:
"""Used to modify or add links in the top toolbar of Anki's main window
'links' is a list of HTML link elements. Add-ons can generate their own links
by using aqt.toolbar.Toolbar.create_link. Links created in that way can then be
appended to the link list, e.g.:
def on_top_toolbar_did_init_links(links, toolbar):
my_link = toolbar.create_link(...)
links.append(my_link)
"""
'links' is a list of HTML link elements. Add-ons can generate their own links
by using aqt.toolbar.Toolbar.create_link. Links created in that way can then be
appended to the link list, e.g.:
def on_top_toolbar_did_init_links(links, toolbar):
my_link = toolbar.create_link(...)
links.append(my_link)
"""
_hooks: List[Callable[[List[str], "aqt.toolbar.Toolbar"], None]] = []
@ -2797,35 +2797,35 @@ undo_state_did_change = _UndoStateDidChangeHook()
class _WebviewDidReceiveJsMessageFilter:
"""Used to handle pycmd() messages sent from Javascript.
Message is the string passed to pycmd().
For messages you don't want to handle, return 'handled' unchanged.
If you handle a message and don't want it passed to the original
bridge command handler, return (True, None).
If you want to pass a value to pycmd's result callback, you can
return it with (True, some_value).
Context is the instance that was passed to set_bridge_command().
It can be inspected to check which screen this hook is firing
in, and to get a reference to the screen. For example, if your
code wishes to function only in the review screen, you could do:
Message is the string passed to pycmd().
if not isinstance(context, aqt.reviewer.Reviewer):
# not reviewer, pass on message
return handled
if message == "my-mark-action":
# our message, call onMark() on the reviewer instance
context.onMark()
# and don't pass message to other handlers
return (True, None)
else:
# some other command, pass it on
return handled
"""
For messages you don't want to handle, return 'handled' unchanged.
If you handle a message and don't want it passed to the original
bridge command handler, return (True, None).
If you want to pass a value to pycmd's result callback, you can
return it with (True, some_value).
Context is the instance that was passed to set_bridge_command().
It can be inspected to check which screen this hook is firing
in, and to get a reference to the screen. For example, if your
code wishes to function only in the review screen, you could do:
if not isinstance(context, aqt.reviewer.Reviewer):
# not reviewer, pass on message
return handled
if message == "my-mark-action":
# our message, call onMark() on the reviewer instance
context.onMark()
# and don't pass message to other handlers
return (True, None)
else:
# some other command, pass it on
return handled
"""
_hooks: List[Callable[[Tuple[bool, Any], str, Any], Tuple[bool, Any]]] = []
@ -2863,34 +2863,34 @@ webview_did_receive_js_message = _WebviewDidReceiveJsMessageFilter()
class _WebviewWillSetContentHook:
"""Used to modify web content before it is rendered.
Web_content contains the HTML, JS, and CSS the web view will be
populated with.
Web_content contains the HTML, JS, and CSS the web view will be
populated with.
Context is the instance that was passed to stdHtml().
It can be inspected to check which screen this hook is firing
in, and to get a reference to the screen. For example, if your
code wishes to function only in the review screen, you could do:
Context is the instance that was passed to stdHtml().
It can be inspected to check which screen this hook is firing
in, and to get a reference to the screen. For example, if your
code wishes to function only in the review screen, you could do:
def on_webview_will_set_content(web_content: WebContent, context):
if not isinstance(context, aqt.reviewer.Reviewer):
# not reviewer, do not modify content
return
# reviewer, perform changes to content
context: aqt.reviewer.Reviewer
addon_package = mw.addonManager.addonFromModule(__name__)
web_content.css.append(
f"/_addons/{addon_package}/web/my-addon.css")
web_content.js.append(
f"/_addons/{addon_package}/web/my-addon.js")
def on_webview_will_set_content(web_content: WebContent, context):
web_content.head += "<script>console.log('my-addon')</script>"
web_content.body += "<div id='my-addon'></div>"
"""
if not isinstance(context, aqt.reviewer.Reviewer):
# not reviewer, do not modify content
return
# reviewer, perform changes to content
context: aqt.reviewer.Reviewer
addon_package = mw.addonManager.addonFromModule(__name__)
web_content.css.append(
f"/_addons/{addon_package}/web/my-addon.css")
web_content.js.append(
f"/_addons/{addon_package}/web/my-addon.js")
web_content.head += "<script>console.log('my-addon')</script>"
web_content.body += "<div id='my-addon'></div>"
"""
_hooks: List[Callable[["aqt.webview.WebContent", Optional[Any]], None]] = []

View File

@ -92,7 +92,10 @@ def allroutes(pathin):
try:
directory, path = _redirectWebExports(pathin)
except TypeError:
return flask.make_response(f"Invalid path: {pathin}", HTTPStatus.FORBIDDEN,)
return flask.make_response(
f"Invalid path: {pathin}",
HTTPStatus.FORBIDDEN,
)
try:
isdir = os.path.isdir(os.path.join(directory, path))
@ -153,7 +156,10 @@ def allroutes(pathin):
return flask.send_file(fullpath, mimetype=mimetype, conditional=True)
else:
print(f"Not found: {ascii(pathin)}")
return flask.make_response(f"Invalid path: {pathin}", HTTPStatus.NOT_FOUND,)
return flask.make_response(
f"Invalid path: {pathin}",
HTTPStatus.NOT_FOUND,
)
except Exception as error:
if devMode:
@ -165,7 +171,10 @@ def allroutes(pathin):
# swallow it - user likely surfed away from
# review screen before an image had finished
# downloading
return flask.make_response(str(error), HTTPStatus.INTERNAL_SERVER_ERROR,)
return flask.make_response(
str(error),
HTTPStatus.INTERNAL_SERVER_ERROR,
)
def _redirectWebExports(path):

View File

@ -76,7 +76,7 @@ if isWin:
class MPVBase:
"""Base class for communication with the mpv media player via unix socket
based JSON IPC.
based JSON IPC.
"""
executable = find_executable("mpv")
@ -116,8 +116,7 @@ class MPVBase:
# Process
#
def _prepare_process(self):
"""Prepare the argument list for the mpv process.
"""
"""Prepare the argument list for the mpv process."""
self.argv = [self.executable]
self.argv += self.default_argv
self.argv += ["--input-ipc-server=" + self._sock_filename]
@ -125,13 +124,11 @@ class MPVBase:
self.argv += ["--wid=" + str(self.window_id)]
def _start_process(self):
"""Start the mpv process.
"""
"""Start the mpv process."""
self._proc = subprocess.Popen(self.argv, env=self.popenEnv)
def _stop_process(self):
"""Stop the mpv process.
"""
"""Stop the mpv process."""
if hasattr(self, "_proc"):
try:
self._proc.terminate()
@ -144,7 +141,7 @@ class MPVBase:
#
def _prepare_socket(self):
"""Create a random socket filename which we pass to mpv with the
--input-unix-socket option.
--input-unix-socket option.
"""
if isWin:
self._sock_filename = "ankimpv"
@ -155,7 +152,7 @@ class MPVBase:
def _start_socket(self):
"""Wait for the mpv process to create the unix socket and finish
startup.
startup.
"""
start = time.time()
while self.is_running() and time.time() < start + 10:
@ -197,8 +194,7 @@ class MPVBase:
raise MPVProcessError("unable to start process")
def _stop_socket(self):
"""Clean up the socket.
"""
"""Clean up the socket."""
if hasattr(self, "_sock"):
self._sock.close()
if hasattr(self, "_sock_filename"):
@ -208,23 +204,20 @@ class MPVBase:
pass
def _prepare_thread(self):
"""Set up the queues for the communication threads.
"""
"""Set up the queues for the communication threads."""
self._request_queue = Queue(1)
self._response_queues = {}
self._event_queue = Queue()
self._stop_event = threading.Event()
def _start_thread(self):
"""Start up the communication threads.
"""
"""Start up the communication threads."""
self._thread = threading.Thread(target=self._reader)
self._thread.daemon = True
self._thread.start()
def _stop_thread(self):
"""Stop the communication threads.
"""
"""Stop the communication threads."""
if hasattr(self, "_stop_event"):
self._stop_event.set()
if hasattr(self, "_thread"):
@ -232,7 +225,7 @@ class MPVBase:
def _reader(self):
"""Read the incoming json messages from the unix socket that is
connected to the mpv process. Pass them on to the message handler.
connected to the mpv process. Pass them on to the message handler.
"""
buf = b""
while not self._stop_event.is_set():
@ -276,22 +269,20 @@ class MPVBase:
# Message handling
#
def _compose_message(self, message):
"""Return a json representation from a message dictionary.
"""
"""Return a json representation from a message dictionary."""
# XXX may be strict is too strict ;-)
data = json.dumps(message)
return data.encode("utf8", "strict") + b"\n"
def _parse_message(self, data):
"""Return a message dictionary from a json representation.
"""
"""Return a message dictionary from a json representation."""
# XXX may be strict is too strict ;-)
data = data.decode("utf8", "strict")
return json.loads(data)
def _handle_message(self, message):
"""Handle different types of incoming messages, i.e. responses to
commands or asynchronous events.
commands or asynchronous events.
"""
if "error" in message:
# This message is a reply to a request.
@ -311,8 +302,8 @@ class MPVBase:
def _send_message(self, message, timeout=None):
"""Send a message/command to the mpv process, message must be a
dictionary of the form {"command": ["arg1", "arg2", ...]}. Responses
from the mpv process must be collected using _get_response().
dictionary of the form {"command": ["arg1", "arg2", ...]}. Responses
from the mpv process must be collected using _get_response().
"""
data = self._compose_message(message)
@ -348,8 +339,8 @@ class MPVBase:
def _get_response(self, timeout=None):
"""Collect the response message to a previous request. If there was an
error a MPVCommandError exception is raised, otherwise the command
specific data is returned.
error a MPVCommandError exception is raised, otherwise the command
specific data is returned.
"""
try:
message = self._response_queues[self._thread_id()].get(
@ -365,8 +356,8 @@ class MPVBase:
def _get_event(self, timeout=None):
"""Collect a single event message that has been received out-of-band
from the mpv process. If a timeout is specified and there have not
been any events during that period, None is returned.
from the mpv process. If a timeout is specified and there have not
been any events during that period, None is returned.
"""
try:
return self._event_queue.get(block=timeout is not None, timeout=timeout)
@ -374,8 +365,7 @@ class MPVBase:
return None
def _send_request(self, message, timeout=None, _retry=1):
"""Send a command to the mpv process and collect the result.
"""
"""Send a command to the mpv process and collect the result."""
self.ensure_running()
try:
self._send_message(message, timeout)
@ -392,15 +382,14 @@ class MPVBase:
def _register_callbacks(self):
"""Will be called after mpv restart to reinitialize callbacks
defined in MPV subclass
defined in MPV subclass
"""
#
# Public API
#
def is_running(self):
"""Return True if the mpv process is still active.
"""
"""Return True if the mpv process is still active."""
return self._proc.poll() is None
def ensure_running(self):
@ -417,8 +406,7 @@ class MPVBase:
self._register_callbacks()
def close(self):
"""Shutdown the mpv process and our communication setup.
"""
"""Shutdown the mpv process and our communication setup."""
if self.is_running():
self._send_request({"command": ["quit"]}, timeout=1)
self._stop_process()
@ -429,22 +417,22 @@ class MPVBase:
class MPV(MPVBase):
"""Class for communication with the mpv media player via unix socket
based JSON IPC. It adds a few usable methods and a callback API.
based JSON IPC. It adds a few usable methods and a callback API.
To automatically register methods as event callbacks, subclass this
class and define specially named methods as follows:
To automatically register methods as event callbacks, subclass this
class and define specially named methods as follows:
def on_file_loaded(self):
# This is called for every 'file-loaded' event.
...
def on_file_loaded(self):
# This is called for every 'file-loaded' event.
...
def on_property_time_pos(self, position):
# This is called whenever the 'time-pos' property is updated.
...
def on_property_time_pos(self, position):
# This is called whenever the 'time-pos' property is updated.
...
Please note that callbacks are executed inside a separate thread. The
MPV class itself is completely thread-safe. Requests from different
threads to the same MPV instance are synchronized.
Please note that callbacks are executed inside a separate thread. The
MPV class itself is completely thread-safe. Requests from different
threads to the same MPV instance are synchronized.
"""
def __init__(self, *args, **kwargs):
@ -498,8 +486,7 @@ class MPV(MPVBase):
# Socket communication
#
def _start_thread(self):
"""Start up the communication threads.
"""
"""Start up the communication threads."""
super()._start_thread()
if not hasattr(self, "_event_thread"):
self._event_thread = threading.Thread(target=self._event_reader)
@ -510,8 +497,7 @@ class MPV(MPVBase):
# Event/callback API
#
def _event_reader(self):
"""Collect incoming event messages and call the event handler.
"""
"""Collect incoming event messages and call the event handler."""
while True:
message = self._get_event(timeout=1)
if message is None:
@ -520,8 +506,7 @@ class MPV(MPVBase):
self._handle_event(message)
def _handle_event(self, message):
"""Lookup and call the callbacks for a particular event message.
"""
"""Lookup and call the callbacks for a particular event message."""
if not self._callbacks_initialized:
self._callbacks_queue.put(message)
return
@ -538,8 +523,7 @@ class MPV(MPVBase):
callback()
def register_callback(self, name, callback):
"""Register a function `callback` for the event `name`.
"""
"""Register a function `callback` for the event `name`."""
try:
self.command("enable_event", name)
except MPVCommandError:
@ -549,7 +533,7 @@ class MPV(MPVBase):
def unregister_callback(self, name, callback):
"""Unregister a previously registered function `callback` for the event
`name`.
`name`.
"""
try:
callbacks = self._callbacks[name]
@ -563,7 +547,7 @@ class MPV(MPVBase):
def register_property_callback(self, name, callback):
"""Register a function `callback` for the property-change event on
property `name`.
property `name`.
"""
# Property changes are normally not sent over the connection unless they
# are requested using the 'observe_property' command.
@ -585,7 +569,7 @@ class MPV(MPVBase):
def unregister_property_callback(self, name, callback):
"""Unregister a previously registered function `callback` for the
property-change event on property `name`.
property-change event on property `name`.
"""
try:
callbacks = self._callbacks["property-" + name]
@ -606,18 +590,15 @@ class MPV(MPVBase):
# Public API
#
def command(self, *args, timeout=1):
"""Execute a single command on the mpv process and return the result.
"""
"""Execute a single command on the mpv process and return the result."""
return self._send_request({"command": list(args)}, timeout=timeout)
def get_property(self, name):
"""Return the value of property `name`.
"""
"""Return the value of property `name`."""
return self.command("get_property", name)
def set_property(self, name, value):
"""Set the value of property `name`.
"""
"""Set the value of property `name`."""
return self.command("set_property", name, value)

View File

@ -119,7 +119,10 @@ class Previewer(QDialog):
"reviewer.js",
]
self._web.stdHtml(
self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc, context=self,
self.mw.reviewer.revHtml(),
css=["reviewer.css"],
js=jsinc,
context=self,
)
self._web.set_bridge_command(self._on_bridge_cmd, self)

View File

@ -146,7 +146,9 @@ class ProfileManager:
app = QtWidgets.QApplication([])
icon = QtGui.QIcon()
icon.addPixmap(
QtGui.QPixmap(":/icons/anki.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off,
QtGui.QPixmap(":/icons/anki.png"),
QtGui.QIcon.Normal,
QtGui.QIcon.Off,
)
window_title = "Data Folder Migration"
migration_directories = f"\n\n {oldBase}\n\nto\n\n {self.base}"

View File

@ -77,7 +77,8 @@ def on_normal_sync_timer(mw: aqt.main.AnkiQt) -> None:
assert isinstance(progress.val, NormalSyncProgress)
mw.progress.update(
label=f"{progress.val.added}\n{progress.val.removed}", process=False,
label=f"{progress.val.added}\n{progress.val.removed}",
process=False,
)
mw.progress.set_title(progress.val.stage)

View File

@ -71,18 +71,18 @@ class Toolbar:
id: Optional[str] = None,
) -> str:
"""Generates HTML link element and registers link handler
Arguments:
cmd {str} -- Command name used for the JS Python bridge
label {str} -- Display label of the link
func {Callable} -- Callable to be called on clicking the link
Keyword Arguments:
tip {Optional[str]} -- Optional tooltip text to show on hovering
over the link (default: {None})
id: {Optional[str]} -- Optional id attribute to supply the link with
(default: {None})
Returns:
str -- HTML link element
"""

View File

@ -148,7 +148,7 @@ class WebContent:
changes only perform the minimum requried edits to make your add-on work.
You should avoid overwriting or interfering with existing data as much
as possible, instead opting to append your own changes, e.g.:
def on_webview_will_set_content(web_content: WebContent, context):
web_content.body += "<my_html>"
web_content.head += "<my_head>"
@ -157,28 +157,28 @@ class WebContent:
media server. All list members without a specified subpath are assumed
to be located under `/_anki`, which is the media server subpath used
for all web assets shipped with Anki.
Add-ons may expose their own web assets by utilizing
aqt.addons.AddonManager.setWebExports(). Web exports registered
in this manner may then be accessed under the `/_addons` subpath.
E.g., to allow access to a `my-addon.js` and `my-addon.css` residing
in a "web" subfolder in your add-on package, first register the
corresponding web export:
> from aqt import mw
> mw.addonManager.setWebExports(__name__, r"web/.*(css|js)")
Then append the subpaths to the corresponding web_content fields
within a function subscribing to gui_hooks.webview_will_set_content:
def on_webview_will_set_content(web_content: WebContent, context):
addon_package = mw.addonManager.addonFromModule(__name__)
web_content.css.append(
f"/_addons/{addon_package}/web/my-addon.css")
web_content.js.append(
f"/_addons/{addon_package}/web/my-addon.js")
Note that '/' will also match the os specific path separator.
"""

View File

@ -412,7 +412,10 @@ hooks = [
),
Hook(
name="webview_will_set_content",
args=["web_content: aqt.webview.WebContent", "context: Optional[Any]",],
args=[
"web_content: aqt.webview.WebContent",
"context: Optional[Any]",
],
doc="""Used to modify web content before it is rendered.
Web_content contains the HTML, JS, and CSS the web view will be
@ -526,7 +529,8 @@ hooks = [
doc="""Executed when the top toolbar is redrawn""",
),
Hook(
name="media_sync_did_progress", args=["entry: aqt.mediasync.LogEntryWithTime"],
name="media_sync_did_progress",
args=["entry: aqt.mediasync.LogEntryWithTime"],
),
Hook(name="media_sync_did_start_or_stop", args=["running: bool"]),
Hook(
@ -541,7 +545,10 @@ hooks = [
args=["addcards: aqt.addcards.AddCards", "menu: QMenu"],
legacy_hook="AddCards.onHistory",
),
Hook(name="add_cards_did_init", args=["addcards: aqt.addcards.AddCards"],),
Hook(
name="add_cards_did_init",
args=["addcards: aqt.addcards.AddCards"],
),
Hook(
name="add_cards_did_add_note",
args=["note: anki.notes.Note"],
@ -623,7 +630,10 @@ hooks = [
name="editor_web_view_did_init",
args=["editor_web_view: aqt.editor.EditorWebView"],
),
Hook(name="editor_did_init", args=["editor: aqt.editor.Editor"],),
Hook(
name="editor_did_init",
args=["editor: aqt.editor.Editor"],
),
Hook(
name="editor_will_load_note",
args=["js: str", "note: anki.notes.Note", "editor: aqt.editor.Editor"],
@ -675,7 +685,10 @@ hooks = [
),
# Model
###################
Hook(name="models_advanced_will_show", args=["advanced: QDialog"],),
Hook(
name="models_advanced_will_show",
args=["advanced: QDialog"],
),
# Stats
###################
Hook(