Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor code with Pyright and Ruff #152

Merged
merged 11 commits into from
May 15, 2023
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.9.15
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Life Drain [![CodeFactor](https://www.codefactor.io/repository/github/yutsuten/anki-lifedrain/badge)](https://www.codefactor.io/repository/github/yutsuten/anki-lifedrain)
# Life Drain

MAINTENANCE NOTICE:

Expand Down
23 changes: 23 additions & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Rules
line-length = 100
select = ["ALL"]
ignore = [
"D100", # Missing docstring in public module
"D104", # Missing docstring in public package
"D107", # Missing docstring in `__init__`
"ANN002", # Missing type annotation for `*args`
"ANN003", # Missing type annotation for `**kwargs`
"ANN101", # Missing type annotation for `self` in method
"ANN204", # Missing return type annotation for special method
"ANN401", # Disallow typing.Any (qt modules aren't discovered by pyright)
"UP007", # Use X | Y for type annotations (only available in python 3.10+)
"UP035", # Deprecated import
]

[pydocstyle]
convention = "google"

[flake8-quotes]
docstring-quotes = "double"
inline-quotes = "single"
multiline-quotes = "single"
6 changes: 2 additions & 4 deletions src/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""
Copyright (c) Yutsuten <https://github.com/Yutsuten>. Licensed under AGPL-3.0.
See the LICENCE file in the repository root for full licence text.
"""
# Copyright (c) Yutsuten <https://github.com/Yutsuten>. Licensed under AGPL-3.0.
# See the LICENCE file in the repository root for full licence text.

from . import main

Expand Down
61 changes: 38 additions & 23 deletions src/database.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
Copyright (c) Yutsuten <https://github.com/Yutsuten>. Licensed under AGPL-3.0.
See the LICENCE file in the repository root for full licence text.
"""
# Copyright (c) Yutsuten <https://github.com/Yutsuten>. Licensed under AGPL-3.0.
# See the LICENCE file in the repository root for full licence text.

from aqt.main import AnkiQt

from .defaults import DEFAULTS

Expand All @@ -14,16 +14,16 @@ class GlobalConf:
'globalSettingsShortcut', 'deckSettingsShortcut',
'pauseShortcut', 'recoverShortcut', 'behavUndo', 'behavBury',
'behavSuspend', 'stopOnLostFocus', 'shareDrain'}
_main_window = None

def __init__(self, mw):
self._main_window = mw
def __init__(self, mw: AnkiQt):
self._mw = mw

def get(self):
def get(self) -> dict:
"""Get global configuration from Anki's database."""
conf = self._main_window.addonManager.getConfig(__name__)
conf = self._mw.addonManager.getConfig(__name__)
if not conf:
conf = self._main_window.col.get_config('lifedrain', {})
raise RuntimeError

for field in self.fields:
if field not in conf:
conf[field] = DEFAULTS[field]
Expand All @@ -32,34 +32,41 @@ def get(self):
conf[field] = DEFAULTS[field]
return conf

def set(self, new_conf):
def update(self, new_conf: dict) -> None:
"""Saves global configuration into Anki's database."""
conf = self._main_window.addonManager.getConfig(__name__)
conf = self._mw.addonManager.getConfig(__name__)
if not conf:
raise RuntimeError

for field in self.fields:
if field in new_conf:
conf[field] = new_conf[field]
for field in DeckConf.fields:
conf[field] = new_conf[field]
self._main_window.addonManager.writeConfig(__name__, conf)
self._mw.addonManager.writeConfig(__name__, conf)

# Cleanup old configuration saved in mw.col.conf
if self._mw.col is not None and 'lifedrain' in self._mw.col.conf:
self._mw.col.conf.remove('lifedrain')


class DeckConf:
"""Manages each lifedrain's deck configuration."""
fields = {'maxLife', 'recover', 'damage', 'damageNew', 'damageLearning'}
_main_window = None

def __init__(self, mw):
self._main_window = mw
def __init__(self, mw: AnkiQt):
self._mw = mw
self._global_conf = GlobalConf(mw)

def get(self):
def get(self) -> dict:
"""Get current deck configuration from Anki's database."""
if self._mw.col is None:
raise RuntimeError

conf = self._global_conf.get()
deck = self._main_window.col.decks.current()
deck = self._mw.col.decks.current()
decks = conf.get('decks', {})
deck_conf = decks.get(str(deck['id']), {})
if not deck_conf:
deck_conf = deck.get('lifedrain', {})
conf_dict = {
'id': deck['id'],
'name': deck['name'],
Expand All @@ -68,14 +75,22 @@ def get(self):
conf_dict[field] = deck_conf.get(field, conf[field])
return conf_dict

def set(self, new_conf):
def update(self, new_conf: dict) -> None:
"""Saves deck configuration into Anki's database."""
if self._mw.col is None:
raise RuntimeError

conf = self._global_conf.get()
deck = self._main_window.col.decks.current()
deck = self._mw.col.decks.current()
if 'decks' not in conf:
conf['decks'] = {}
deck_conf = {}
for field in self.fields:
deck_conf[field] = new_conf[field]
conf['decks'][str(deck['id'])] = deck_conf
self._main_window.addonManager.writeConfig(__name__, conf)
self._mw.addonManager.writeConfig(__name__, conf)

# Cleanup old configuration saved in mw.col.decks
if 'lifedrain' in deck:
del deck['lifedrain']
self._mw.col.decks.save(deck)
130 changes: 64 additions & 66 deletions src/deck_manager.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
"""
Copyright (c) Yutsuten <https://github.com/Yutsuten>. Licensed under AGPL-3.0.
See the LICENCE file in the repository root for full licence text.
"""
# Copyright (c) Yutsuten <https://github.com/Yutsuten>. Licensed under AGPL-3.0.
# See the LICENCE file in the repository root for full licence text.

from typing import Any, Literal, Optional, Union

from anki.consts import CardType
from anki.hooks import runHook
from aqt.main import AnkiQt

from .database import DeckConf, GlobalConf
from .defaults import BEHAVIORS
from .progress_bar import ProgressBar


Expand All @@ -13,34 +17,27 @@ class DeckManager:

Users may configure each deck with different settings, and the current
status of the life bar (e.g. current life) will likely differ for each deck.

Attributes:
bar_visible: Function that toggles the Progress Bar visibility.
"""

bar_visible = None

_bar_info = {}
_conf = None
_global_conf = None
_deck_conf = None
_game_over = False
_progress_bar = None
_cur_deck_id = None

def __init__(self, mw, qt, global_conf, deck_conf):
def __init__(self, mw: AnkiQt, qt: Any, global_conf: GlobalConf, deck_conf: DeckConf):
"""Initializes a Progress Bar, and keeps Anki's main window reference.

Args:
mw: Anki's main window.
qt: The PyQt library.
global_conf: An instance of GlobalConf.
deck_conf: An instance of DeckConf.
"""
self._progress_bar = ProgressBar(mw, qt)
self._global_conf = global_conf
self._deck_conf = deck_conf
self.bar_visible = self._progress_bar.set_visible

def update(self):
def update(self) -> None:
"""Updates the current deck's life bar."""
deck_id = self._get_deck_id()
self._cur_deck_id = deck_id
Expand All @@ -56,19 +53,20 @@ def update(self):
history = bar_info['history']
history[bar_info['currentReview']] = bar_info['currentValue']

def get_current_life(self):
def get_current_life(self) -> Union[int, float]:
"""Get the current deck's current life."""
deck_id = self._get_deck_id()
self._cur_deck_id = deck_id
if deck_id not in self._bar_info:
self._add_deck(deck_id)
return self._bar_info[deck_id]['currentValue']

def set_deck_conf(self, conf, update_life=True):
def set_deck_conf(self, conf: dict, *, update_life: bool) -> None:
"""Updates a deck's current settings and state.

Args:
conf: A dictionary with the deck's configuration and state.
update_life: Update the current life?
"""
current_value = conf.get('currentValue', conf['maxLife'])
if current_value > conf['maxLife']:
Expand All @@ -85,54 +83,53 @@ def set_deck_conf(self, conf, update_life=True):
if update_life:
self._bar_info[deck_id]['currentValue'] = current_value

def recover_life(self, increment=True, value=None, damage=False,
card_type=None):
def drain(self) -> None:
"""Life loss due to drain."""
self._update_life(-0.1)

def recover(self, value:Optional[Union[int, float]]=None, *, increment:bool=True) -> None:
"""Recover life of the currently active deck.

Args:
increment: Optional. A flag that indicates increment or decrement.
value: Optional. The value used to increment or decrement.
damage: Optional. If this flag is ON, uses the default damage value.
"""
bar_info = self._bar_info[self._cur_deck_id]

multiplier = 1
if not increment:
multiplier = -1
multiplier = 1 if increment else -1
if value is None:
if damage and bar_info['damageValue'] is not None:
multiplier = -1
value = self._calculate_damage(card_type)
else:
value = bar_info['recoverValue']
value = int(self._bar_info[self._cur_deck_id]['recoverValue'])
self._update_life(multiplier * value)

self._progress_bar.inc_current_value(multiplier * value)
def damage(self, card_type: CardType) -> None:
"""Apply damage.

life = self._progress_bar.get_current_value()
bar_info['currentValue'] = life
if life > 0:
self._game_over = False
elif not self._game_over:
self._game_over = True
runHook('LifeDrain.gameOver')
Args:
card_type: Optional. Applies different damage depending on card type.
"""
bar_info = self._bar_info[self._cur_deck_id]
damage = bar_info['damageValue']
if card_type == 0 and bar_info['damageNew'] is not None:
damage = bar_info['damageNew']
elif card_type == 1 and bar_info['damageLearning'] is not None:
damage = bar_info['damageLearning']
self._update_life(-1 * damage)

def answer(self, review_response, card_type):
def answer(self, review_response: Literal[1, 2, 3, 4], card_type: CardType) -> None:
"""Restores or drains life after an answer."""
if review_response == 1:
self.recover_life(damage=True, card_type=card_type)
if review_response == 1 and self._bar_info[self._cur_deck_id]['damageValue'] is not None:
self.damage(card_type=card_type)
else:
self.recover_life()
self.recover()
self._next()

def action(self, behavior_index):
def action(self, behavior_index: Literal[0, 1, 2]) -> None:
"""Bury/suspend handling."""
if behavior_index == 0:
self.recover_life(False)
elif behavior_index == 2:
self.recover_life(True)
if behavior_index == BEHAVIORS.index('Drain life'):
self.recover(increment=False)
elif behavior_index == BEHAVIORS.index('Recover life'):
self.recover(increment=True)
self._next()

def undo(self):
def undo(self) -> None:
"""Restore the life to how it was in the previous card."""
bar_info = self._bar_info[self._cur_deck_id]
history = bar_info['history']
Expand All @@ -142,7 +139,22 @@ def undo(self):
bar_info['currentValue'] = history[bar_info['currentReview']]
self._progress_bar.set_current_value(bar_info['currentValue'])

def _next(self):
def _update_life(self, difference: Union[int, float]) -> None:
"""Apply recover/damage/drain.

Args:
difference: The amount to increase or decrease.
"""
self._progress_bar.inc_current_value(difference)
life = self._progress_bar.get_current_value()
self._bar_info[self._cur_deck_id]['currentValue'] = life
if life > 0:
self._game_over = False
elif not self._game_over:
self._game_over = True
runHook('LifeDrain.gameOver')

def _next(self) -> None:
"""Remembers the current life and advances to the next card."""
bar_info = self._bar_info[self._cur_deck_id]
bar_info['currentReview'] += 1
Expand All @@ -152,28 +164,14 @@ def _next(self):
else:
history[bar_info['currentReview']] = bar_info['currentValue']

def _get_deck_id(self):
def _get_deck_id(self) -> str:
global_conf = self._global_conf.get()
if global_conf['shareDrain']:
return 'shared'
conf = self._deck_conf.get()
return conf['id']

def _calculate_damage(self, card_type):
"""Calculate damage depending on card type.

Args:
card_type: 0 = New, 1 = Learning, 2 = Review.
"""
bar_info = self._bar_info[self._cur_deck_id]
damage = bar_info['damageValue']
if card_type == 0 and bar_info['damageNew'] is not None:
damage = bar_info['damageNew']
elif card_type == 1 and bar_info['damageLearning'] is not None:
damage = bar_info['damageLearning']
return damage

def _add_deck(self, deck_id):
def _add_deck(self, deck_id:str) -> None:
"""Adds a deck to the list of decks that are being managed.

Args:
Expand All @@ -193,7 +191,7 @@ def _add_deck(self, deck_id):
'currentReview': 0,
}

def _update_progress_bar_style(self):
def _update_progress_bar_style(self) -> None:
"""Synchronizes the Progress Bar styling with the Global Settings."""
conf = self._global_conf.get()
self._progress_bar.dock_at(conf['barPosition'])
Expand Down
Loading