Merge branch 'main' into svelte5

This commit is contained in:
Abdo 2024-08-14 14:44:16 +03:00 committed by GitHub
commit add1248cd8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
156 changed files with 813 additions and 731 deletions

View file

@ -22,7 +22,7 @@ echo "--- Ensure libs importable"
SKIP_RUN=1 ./run SKIP_RUN=1 ./run
echo "--- Check Rust libs" echo "--- Check Rust libs"
cargo install cargo-deny --version 0.14.12 cargo install cargo-deny --version 0.14.24
cargo deny check cargo deny check
echo "--- Cleanup" echo "--- Cleanup"

View file

@ -5,8 +5,8 @@
db-path = "~/.cargo/advisory-db" db-path = "~/.cargo/advisory-db"
db-urls = ["https://github.com/rustsec/advisory-db"] db-urls = ["https://github.com/rustsec/advisory-db"]
ignore = [ ignore = [
# safemem only used by makeapp # gix only used by burn-train
"RUSTSEC-2023-0081", "RUSTSEC-2024-0350",
] ]
[licenses] [licenses]
@ -23,6 +23,7 @@ allow = [
"CC0-1.0", "CC0-1.0",
"Unlicense", "Unlicense",
"Zlib", "Zlib",
"Unicode-3.0",
] ]
confidence-threshold = 0.8 confidence-threshold = 0.8
# eg { allow = ["Zlib"], name = "adler32", version = "*" }, # eg { allow = ["Zlib"], name = "adler32", version = "*" },

View file

@ -1,10 +1,5 @@
[settings] [settings]
ensure_newline_before_comments=true py_version=39
force_grid_wrap=0
include_trailing_comma=True
known_first_party=anki,aqt,tests known_first_party=anki,aqt,tests
line_length=88
multi_line_output=3
profile=black profile=black
skip= extend_skip=qt/bundle
use_parentheses=True

2
.ruff.toml Normal file
View file

@ -0,0 +1,2 @@
target-version = "py39"
extend-exclude = ["qt/bundle"]

View file

@ -148,6 +148,7 @@ user1823 <92206575+user1823@users.noreply.github.com>
Gustaf Carefall <https://github.com/Gustaf-C> Gustaf Carefall <https://github.com/Gustaf-C>
virinci <github.com/virinci> virinci <github.com/virinci>
snowtimeglass <snowtimeglass@gmail.com> snowtimeglass <snowtimeglass@gmail.com>
brishtibheja <sorata225yume@gmail.com>
Ben Olson <github.com/grepgrok> Ben Olson <github.com/grepgrok>
Akash Reddy <akashreddy2003@gmail.com> Akash Reddy <akashreddy2003@gmail.com>
Lucio Sauer <watermanpaint@posteo.net> Lucio Sauer <watermanpaint@posteo.net>
@ -177,7 +178,14 @@ RRomeroJr <117.rromero@gmail.com>
Xidorn Quan <me@upsuper.org> Xidorn Quan <me@upsuper.org>
Alexander Bocken <alexander@bocken.org> Alexander Bocken <alexander@bocken.org>
James Elmore <email@jameselmore.org> James Elmore <email@jameselmore.org>
Ian Samir Yep Manzano <https://github.com/isym444>
David Culley <6276049+davidculley@users.noreply.github.com>
Rastislav Kish <rastislav.kish@protonmail.com>
Expertium <https://github.com/Expertium>
Christian Donat <https://github.com/cdonat2>
Asuka Minato <https://asukaminato.eu.org>
Dillon Baldwin <https://github.com/DillBal>
Voczi <https://github.com/voczi>
******************** ********************
The text of the 3 clause BSD license follows: The text of the 3 clause BSD license follows:

12
Cargo.lock generated
View file

@ -1901,9 +1901,9 @@ dependencies = [
[[package]] [[package]]
name = "fsrs" name = "fsrs"
version = "0.6.4" version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70cec685337af48789e58cea6ef59ee9f01289d1083428b03fe14e76b98c817c" checksum = "285d9b275f7d5a276f17006e9d92ea67fa9991187ae88760fa96705fba1f97aa"
dependencies = [ dependencies = [
"burn", "burn",
"itertools 0.12.1", "itertools 0.12.1",
@ -3859,9 +3859,9 @@ dependencies = [
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.64" version = "0.10.66"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"cfg-if", "cfg-if",
@ -3891,9 +3891,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.102" version = "0.9.103"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",

View file

@ -35,7 +35,7 @@ git = "https://github.com/ankitects/linkcheck.git"
rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca" rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca"
[workspace.dependencies.fsrs] [workspace.dependencies.fsrs]
version = "0.6.4" version = "1.1.4"
# git = "https://github.com/open-spaced-repetition/fsrs-rs.git" # git = "https://github.com/open-spaced-repetition/fsrs-rs.git"
# rev = "58ca25ed2bc4bb1dc376208bbcaed7f5a501b941" # rev = "58ca25ed2bc4bb1dc376208bbcaed7f5a501b941"
# path = "../open-spaced-repetition/fsrs-rs" # path = "../open-spaced-repetition/fsrs-rs"

View file

@ -368,8 +368,8 @@ pub trait FilesHandle {
/// different variables. This is a shortcut for calling .expand_inputs() /// different variables. This is a shortcut for calling .expand_inputs()
/// and then .add_inputs_vec() /// and then .add_inputs_vec()
/// - If the variable name is non-empty, a variable of the same name will be /// - If the variable name is non-empty, a variable of the same name will be
/// created so the file list can be accessed in the command. By convention, /// created so the file list can be accessed in the command. By
/// this is often `in`. /// convention, this is often `in`.
fn add_inputs(&mut self, variable: &'static str, inputs: impl AsRef<BuildInput>); fn add_inputs(&mut self, variable: &'static str, inputs: impl AsRef<BuildInput>);
fn add_inputs_vec(&mut self, variable: &'static str, inputs: Vec<String>); fn add_inputs_vec(&mut self, variable: &'static str, inputs: Vec<String>);
fn add_order_only_inputs(&mut self, variable: &'static str, inputs: impl AsRef<BuildInput>); fn add_order_only_inputs(&mut self, variable: &'static str, inputs: impl AsRef<BuildInput>);
@ -394,10 +394,10 @@ pub trait FilesHandle {
/// - Each output automatically has $builddir/ prefixed to it if it does not /// - Each output automatically has $builddir/ prefixed to it if it does not
/// already start with it. /// already start with it.
/// - If the variable name is non-empty, a variable of the same name will be /// - If the variable name is non-empty, a variable of the same name will be
/// created so the file list can be accessed in the command. By convention, /// created so the file list can be accessed in the command. By
/// this is often `out`. /// convention, this is often `out`.
/// - If subgroup is true, the files are also placed in a subgroup. Eg /// - If subgroup is true, the files are also placed in a subgroup. Eg if a
/// if a rule `foo` exists and subgroup `bar` is provided, the files are /// rule `foo` exists and subgroup `bar` is provided, the files are
/// accessible via `:foo:bar`. The variable name must not be empty, or /// accessible via `:foo:bar`. The variable name must not be empty, or
/// called `out`. /// called `out`.
fn add_outputs_ext( fn add_outputs_ext(

View file

@ -223,18 +223,6 @@ impl BuildAction for SvelteCheck {
build.add_inputs("yarn", inputs![":yarn:bin"]); build.add_inputs("yarn", inputs![":yarn:bin"]);
build.add_inputs("", &self.inputs); build.add_inputs("", &self.inputs);
build.add_inputs("", inputs!["yarn.lock"]); build.add_inputs("", inputs!["yarn.lock"]);
build.add_variable(
"compiler_warnings",
[
"a11y-click-events-have-key-events",
"a11y-no-noninteractive-tabindex",
"a11y-no-static-element-interactions",
]
.iter()
.map(|warning| format!("{}$:ignore", warning))
.collect::<Vec<_>>()
.join(","),
);
let hash = simple_hash(&self.tsconfig); let hash = simple_hash(&self.tsconfig);
build.add_output_stamp(format!("tests/svelte-check.{hash}")); build.add_output_stamp(format!("tests/svelte-check.{hash}"));
} }

View file

@ -251,7 +251,7 @@ impl BuildAction for PythonTest {
build.add_variable("folder", self.folder); build.add_variable("folder", self.folder);
build.add_variable( build.add_variable(
"pythonpath", "pythonpath",
&self.python_path.join(if cfg!(windows) { ";" } else { ":" }), self.python_path.join(if cfg!(windows) { ";" } else { ":" }),
); );
build.add_env_var("PYTHONPATH", "$pythonpath"); build.add_env_var("PYTHONPATH", "$pythonpath");
build.add_env_var("ANKI_TEST_MODE", "1"); build.add_env_var("ANKI_TEST_MODE", "1");

View file

@ -1252,7 +1252,7 @@
}, },
{ {
"name": "fsrs", "name": "fsrs",
"version": "0.6.4", "version": "1.1.4",
"authors": "Open Spaced Repetition", "authors": "Open Spaced Repetition",
"repository": "https://github.com/open-spaced-repetition/fsrs-rs", "repository": "https://github.com/open-spaced-repetition/fsrs-rs",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
@ -2521,7 +2521,7 @@
}, },
{ {
"name": "openssl", "name": "openssl",
"version": "0.10.64", "version": "0.10.66",
"authors": "Steven Fackler <sfackler@gmail.com>", "authors": "Steven Fackler <sfackler@gmail.com>",
"repository": "https://github.com/sfackler/rust-openssl", "repository": "https://github.com/sfackler/rust-openssl",
"license": "Apache-2.0", "license": "Apache-2.0",
@ -2548,7 +2548,7 @@
}, },
{ {
"name": "openssl-sys", "name": "openssl-sys",
"version": "0.9.102", "version": "0.9.103",
"authors": "Alex Crichton <alex@alexcrichton.com>|Steven Fackler <sfackler@gmail.com>", "authors": "Alex Crichton <alex@alexcrichton.com>|Steven Fackler <sfackler@gmail.com>",
"repository": "https://github.com/sfackler/rust-openssl", "repository": "https://github.com/sfackler/rust-openssl",
"license": "MIT", "license": "MIT",

View file

@ -3,32 +3,29 @@
For info on contributing things other than code, such as translations, decks For info on contributing things other than code, such as translations, decks
and add-ons, please see https://docs.ankiweb.net/contrib and add-ons, please see https://docs.ankiweb.net/contrib
With most users now on 2.1, the past 2 years have been focused on paying down some
of the technical debt that Anki's codebase has built up over the years, and making
changes that will make future maintenance and refactoring easier. A lot of Anki's
"business logic" has been migrated to Rust, which AnkiMobile and AnkiDroid
can also take advantage of - previously a lot of effort was wasted writing the same
code for each platform, and then debugging differences in the implementations.
Considerable effort has also been put into improving the Python side of things,
with type hints added to the majority of the codebase, automatic linting/formatting,
and refactoring of parts of the code.
The import/export code remains to be done, and this will likely
take a number of months to work through. Until that is complete, new features
will not be the top priority, unless they are easy wins as part of the refactoring
process.
If you are planning to contribute any non-trivial changes, please reach out
on the support site before you begin work. Some areas (primarily pylib/) are
likely to change/conflict with other work, and larger changes will likely need
to wait until the refactoring process is done.
## Help wanted ## Help wanted
If you'd like to contribute but don't know what to work on, please take a look If you'd like to contribute but don't know what to work on, please take a look
at the [issues tab](https://github.com/ankitects/anki/issues) of the Anki repo at the [issues tab](https://github.com/ankitects/anki/issues) of the Anki repo
on GitHub. on GitHub.
## Larger changes
Before starting work on larger changes, especially ones that aren't listed on the
issue tracker, please reach out on the forums before you begin work, so we can let
you know whether they're likely to be accepted or not. When you spent a bunch of time
on a PR that ends up getting rejected, it's no fun for either you or us.
## Refactoring
Please avoid PRs that focus on refactoring. Every PR has a cost to review, and a chance
of introducing accidental regressions, and often these costs are not worth it for
slightly more elegant code.
That's not to say there's no value in refactoring. But such changes are usually better done
in a PR that happens to be working in the same area - for example, making small changes
to the code as part of fixing a bug, or a larger refactor when introducing a new feature.
## Type hints ## Type hints
Most of Anki's Python code now has type hints, which improve code completion, Most of Anki's Python code now has type hints, which improve code completion,

View file

@ -1,14 +1,14 @@
FROM rust:1.76-alpine3.19 AS builder FROM rust:1.79-alpine3.20 AS builder
ARG ANKI_VERSION ARG ANKI_VERSION
RUN apk update && apk add --no-cache build-base protobuf && rm -rf /var/cache/apk/* RUN apk update && apk add --no-cache build-base protobuf && rm -rf /var/cache/apk/*
RUN cargo install --git https://github.com/ankitects/anki.git \ RUN cargo install --git https://github.com/ankitects/anki.git \
--tag ${ANKI_VERSION} \ --tag ${ANKI_VERSION} \
--root /anki-server \ --root /anki-server \
anki-sync-server anki-sync-server
FROM alpine:3.19.1 FROM alpine:3.20
RUN adduser -D -h /home/anki anki RUN adduser -D -h /home/anki anki
@ -25,8 +25,9 @@ EXPOSE ${SYNC_PORT}
CMD ["anki-sync-server"] CMD ["anki-sync-server"]
# TODO - consider exposing endpoint /health to check on health cause currently it will return 404 error # This health check will work for Anki versions 24.06.3 and newer.
# HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ # For older versions, it may incorrectly report an unhealthy status, which should not be the case.
# CMD wget -qO- http://localhost:${SYNC_PORT} || exit 1 HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget -qO- http://localhost:${SYNC_PORT}/health || exit 1
LABEL maintainer="Jean Khawand <jk@jeankhawand.com>" LABEL maintainer="Jean Khawand <jk@jeankhawand.com>"

@ -1 +1 @@
Subproject commit 173f043875bb38607d7d31bfcb0eb9874b8e4f3a Subproject commit ababc05c46db93309ac50e7f80c10d6559314aa1

View file

@ -61,7 +61,7 @@ deck-config-today-only = Today only
deck-config-learning-steps = Learning steps deck-config-learning-steps = Learning steps
# Please don't translate `1m`, `2d` # Please don't translate `1m`, `2d`
-deck-config-delay-hint = Delays are typically minutes (eg `1m`) or days (eg `2d`), but hours (eg `1h`) and seconds (eg `30s`) are also supported. -deck-config-delay-hint = Delays are typically minutes (e.g. `1m`) or days (e.g. `2d`), but hours (e.g. `1h`) and seconds (e.g. `30s`) are also supported.
deck-config-learning-steps-tooltip = deck-config-learning-steps-tooltip =
One or more delays, separated by spaces. The first delay will be used One or more delays, separated by spaces. The first delay will be used
when you press the `Again` button on a new card, and is 1 minute by default. when you press the `Again` button on a new card, and is 1 minute by default.
@ -100,7 +100,7 @@ deck-config-leech-threshold-tooltip =
think of a mnemonic to help you remember it. think of a mnemonic to help you remember it.
# See actions-suspend-card and scheduling-tag-only for the wording # See actions-suspend-card and scheduling-tag-only for the wording
deck-config-leech-action-tooltip = deck-config-leech-action-tooltip =
`Tag Only`: Add a "leech" tag to the note, and display a pop-up. `Tag Only`: Add a 'leech' tag to the note, and display a pop-up.
`Suspend Card`: In addition to tagging the note, hide the card until it is `Suspend Card`: In addition to tagging the note, hide the card until it is
manually unsuspended. manually unsuspended.
@ -148,8 +148,8 @@ deck-config-new-gather-priority-tooltip-2 =
the latest-added first. the latest-added first.
`Random notes`: gathers cards of randomly selected notes. When sibling burying is `Random notes`: gathers cards of randomly selected notes. When sibling burying is
disabled, this allows all cards of a note to be seen in a session (eg. both a front->back disabled, this allows all cards of a note to be seen in a session (e.g. both a front→back
and back->front card) and back→front card).
`Random cards`: gathers cards completely randomly. `Random cards`: gathers cards completely randomly.
deck-config-new-gather-priority-deck = Deck deck-config-new-gather-priority-deck = Deck
@ -234,7 +234,7 @@ deck-config-stop-timer-on-answer-tooltip =
## Auto Advance section ## Auto Advance section
deck-config-seconds-to-show-question = Seconds to show question for deck-config-seconds-to-show-question = Seconds to show question for
deck-config-seconds-to-show-question-tooltip-2 = When auto advance is activated, the number of seconds to wait before revealing the answer. Set to 0 to disable. deck-config-seconds-to-show-question-tooltip-3 = When auto advance is activated, the number of seconds to wait before applying the question action. Set to 0 to disable.
deck-config-seconds-to-show-answer = Seconds to show answer for deck-config-seconds-to-show-answer = Seconds to show answer for
deck-config-seconds-to-show-answer-tooltip-2 = When auto advance is activated, the number of seconds to wait before applying the answer action. Set to 0 to disable. deck-config-seconds-to-show-answer-tooltip-2 = When auto advance is activated, the number of seconds to wait before applying the answer action. Set to 0 to disable.
deck-config-question-action-show-answer = Show Answer deck-config-question-action-show-answer = Show Answer
@ -242,8 +242,8 @@ deck-config-question-action-show-reminder = Show Reminder
deck-config-question-action = Question action deck-config-question-action = Question action
deck-config-question-action-tool-tip = The action to perform after the question is shown, and time has elapsed. deck-config-question-action-tool-tip = The action to perform after the question is shown, and time has elapsed.
deck-config-answer-action = Answer action deck-config-answer-action = Answer action
deck-config-answer-action-tooltip = The action to perform on the current card before automatically advancing to the next one. deck-config-answer-action-tooltip-2 = The action to perform after the answer is shown, and time has elapsed.
deck-config-wait-for-audio-tooltip = Wait for audio to finish before automatically revealing answer or next question. deck-config-wait-for-audio-tooltip-2 = Wait for audio to finish before automatically applying the question action or answer action.
## Audio section ## Audio section
@ -251,7 +251,7 @@ deck-config-audio-title = Audio
deck-config-disable-autoplay = Don't play audio automatically deck-config-disable-autoplay = Don't play audio automatically
deck-config-disable-autoplay-tooltip = deck-config-disable-autoplay-tooltip =
When enabled, Anki will not play audio automatically. When enabled, Anki will not play audio automatically.
It can be played manually by clicking/tapping on an audio icon, or by using the replay audio action. It can be played manually by clicking/tapping on an audio icon, or by using the Replay action.
deck-config-skip-question-when-replaying = Skip question when replaying answer deck-config-skip-question-when-replaying = Skip question when replaying answer
deck-config-always-include-question-audio-tooltip = deck-config-always-include-question-audio-tooltip =
Whether the question audio should be included when the Replay action is Whether the question audio should be included when the Replay action is
@ -350,7 +350,7 @@ deck-config-compute-optimal-weights = Optimize FSRS parameters
deck-config-compute-minimum-recommended-retention = Minimum recommended retention deck-config-compute-minimum-recommended-retention = Minimum recommended retention
deck-config-optimize-button = Optimize deck-config-optimize-button = Optimize
deck-config-compute-button = Compute deck-config-compute-button = Compute
deck-config-ignore-before = Ignore reviews before deck-config-ignore-before = Ignore cards reviewed before
deck-config-optimize-all-tip = You can optimize all presets at once by using the dropdown button next to "Save". deck-config-optimize-all-tip = You can optimize all presets at once by using the dropdown button next to "Save".
deck-config-evaluate-button = Evaluate deck-config-evaluate-button = Evaluate
deck-config-desired-retention = Desired retention deck-config-desired-retention = Desired retention
@ -368,7 +368,7 @@ deck-config-reschedule-cards-on-change = Reschedule cards on change
deck-config-fsrs-tooltip = deck-config-fsrs-tooltip =
Affects the entire collection. Affects the entire collection.
The Free Spaced Repetition Scheduler (FSRS) is an alternative to Anki's legacy SuperMemo 2 (SM2) scheduler. The Free Spaced Repetition Scheduler (FSRS) is an alternative to Anki's legacy SuperMemo 2 (SM-2) scheduler.
By more accurately determining when you are likely to forget, it can help you remember By more accurately determining when you are likely to forget, it can help you remember
more material in the same amount of time. This setting is shared by all deck presets. more material in the same amount of time. This setting is shared by all deck presets.
@ -388,11 +388,11 @@ deck-config-historical-retention-tooltip =
the missing reviews. the missing reviews.
Your review history may be incomplete for two reasons: Your review history may be incomplete for two reasons:
1. Because you've used the 'ignore reviews before' option. 1. Because you're using the 'ignore cards reviewed before' option.
2. Because you previously deleted review logs to free up space, or imported material from a different 2. Because you previously deleted review logs to free up space, or imported material from a different
SRS program. SRS program.
The latter is quite rare, so unless you've used the former option, you probably don't need to adjust The latter is quite rare, so unless you're using the former option, you probably don't need to adjust
this setting. this setting.
deck-config-weights-tooltip2 = deck-config-weights-tooltip2 =
FSRS parameters affect how cards are scheduled. Anki will start with default parameters. You can use FSRS parameters affect how cards are scheduled. Anki will start with default parameters. You can use
@ -406,12 +406,12 @@ deck-config-reschedule-cards-on-change-tooltip =
will be changed. will be changed.
deck-config-reschedule-cards-warning = deck-config-reschedule-cards-warning =
Depending on your desired retention, this can result in a large number of cards becoming Depending on your desired retention, this can result in a large number of cards becoming
due, so is not recommended when first switching from SM2. due, so is not recommended when first switching from SM-2.
Use this option sparingly, as it will add a review entry to each of your cards, and Use this option sparingly, as it will add a review entry to each of your cards, and
increase the size of your collection. increase the size of your collection.
deck-config-ignore-before-tooltip = deck-config-ignore-before-tooltip-2 =
If set, reviews before the provided date will be ignored when optimizing & evaluating FSRS parameters. If set, cards reviewed before the provided date will be ignored when optimizing FSRS parameters.
This can be useful if you imported someone else's scheduling data, or have changed the way you use the answer buttons. This can be useful if you imported someone else's scheduling data, or have changed the way you use the answer buttons.
deck-config-compute-optimal-weights-tooltip2 = deck-config-compute-optimal-weights-tooltip2 =
When you click the Optimize button, FSRS will analyze your review history, and generate parameters that are When you click the Optimize button, FSRS will analyze your review history, and generate parameters that are
@ -469,6 +469,12 @@ deck-config-bury-tooltip =
When using the V3 scheduler, interday learning cards can also be buried. Interday When using the V3 scheduler, interday learning cards can also be buried. Interday
learning cards are cards with a current learning step of one or more days. learning cards are cards with a current learning step of one or more days.
deck-config-seconds-to-show-question-tooltip = When auto advance is activated, the number of seconds to wait before revealing the answer. Set to 0 to disable.
deck-config-answer-action-tooltip = The action to perform on the current card before automatically advancing to the next one.
deck-config-wait-for-audio-tooltip = Wait for audio to finish before automatically revealing answer or next question.
deck-config-ignore-before-tooltip =
If set, reviews before the provided date will be ignored when optimizing & evaluating FSRS parameters.
This can be useful if you imported someone else's scheduling data, or have changed the way you use the answer buttons.
deck-config-compute-optimal-retention-tooltip = deck-config-compute-optimal-retention-tooltip =
This tool assumes you're starting with 0 cards, and will attempt to calculate the amount of material you'll This tool assumes you're starting with 0 cards, and will attempt to calculate the amount of material you'll
be able to retain in the given time frame. The estimated retention will greatly depend on your inputs, and be able to retain in the given time frame. The estimated retention will greatly depend on your inputs, and
@ -505,3 +511,4 @@ deck-config-compute-optimal-retention-tooltip3 =
set your desired retention to. You may wish to choose a higher desired retention, if youre willing to trade more study set your desired retention to. You may wish to choose a higher desired retention, if youre willing to trade more study
time for a greater recall rate. Setting your desired retention lower than the minimum is not recommended, as it will time for a greater recall rate. Setting your desired retention lower than the minimum is not recommended, as it will
lead to a higher workload, because of the high forgetting rate. lead to a higher workload, because of the high forgetting rate.
deck-config-seconds-to-show-question-tooltip-2 = When auto advance is activated, the number of seconds to wait before revealing the answer. Set to 0 to disable.

View file

@ -41,6 +41,7 @@ preferences-theme-follow-system = Follow System
preferences-theme-light = Light preferences-theme-light = Light
preferences-theme-dark = Dark preferences-theme-dark = Dark
preferences-v3-scheduler = V3 scheduler preferences-v3-scheduler = V3 scheduler
preferences-check-for-updates = Check for program updates
preferences-ignore-accents-in-search = Ignore accents in search (slower) preferences-ignore-accents-in-search = Ignore accents in search (slower)
preferences-backup-explanation = preferences-backup-explanation =
Anki periodically backs up your collection. After backups are more than 2 days old, Anki periodically backs up your collection. After backups are more than 2 days old,

View file

@ -152,7 +152,7 @@ statistics-cards-due =
} }
statistics-backlog-checkbox = Backlog statistics-backlog-checkbox = Backlog
statistics-intervals-title = Review Intervals statistics-intervals-title = Review Intervals
statistics-intervals-subtitle = Delays until reviews are shown again. statistics-intervals-subtitle = Delays until review cards are shown again.
statistics-intervals-day-range = statistics-intervals-day-range =
{ $cards -> { $cards ->
[one] { $cards } card with a { $daysStart }~{ $daysEnd } day interval [one] { $cards } card with a { $daysStart }~{ $daysEnd } day interval

@ -1 +1 @@
Subproject commit 1e8db8f33c0da127e67312aeb77f1c3aa901af77 Subproject commit 8cc8e3447cca79a7a8720e118388f6383c830eee

View file

@ -9,8 +9,8 @@
"dev": "cd ts && vite dev", "dev": "cd ts && vite dev",
"build": "cd ts && vite build", "build": "cd ts && vite build",
"preview": "cd ts && vite preview", "preview": "cd ts && vite preview",
"svelte-check:once": "cd ts && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --fail-on-warnings --threshold warning --compiler-warnings a11y-click-events-have-key-events:ignore,a11y-no-noninteractive-tabindex:ignore,a11y-no-static-element-interactions:ignore", "svelte-check:once": "cd ts && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --fail-on-warnings --threshold warning",
"svelte-check": "cd ts && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch --compiler-warnings a11y-click-events-have-key-events:ignore,a11y-no-noninteractive-tabindex:ignore,a11y-no-static-element-interactions:ignore", "svelte-check": "cd ts && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"vitest:once": "cd ts && vitest run", "vitest:once": "cd ts && vitest run",
"vitest": "cd ts && vitest" "vitest": "cd ts && vitest"
}, },

View file

@ -402,27 +402,26 @@ message ComputeOptimalRetentionResponse {
float optimal_retention = 1; float optimal_retention = 1;
} }
message OptimalRetentionParameters {
double recall_secs_hard = 1;
double recall_secs_good = 2;
double recall_secs_easy = 3;
double forget_secs = 4;
double learn_secs = 5;
double first_rating_probability_again = 6;
double first_rating_probability_hard = 7;
double first_rating_probability_good = 8;
double first_rating_probability_easy = 9;
double review_rating_probability_hard = 10;
double review_rating_probability_good = 11;
double review_rating_probability_easy = 12;
}
message GetOptimalRetentionParametersRequest { message GetOptimalRetentionParametersRequest {
string search = 1; string search = 1;
} }
message GetOptimalRetentionParametersResponse { message GetOptimalRetentionParametersResponse {
OptimalRetentionParameters params = 1; uint32 deck_size = 1;
uint32 learn_span = 2;
float max_cost_perday = 3;
float max_ivl = 4;
repeated float learn_costs = 5;
repeated float review_costs = 6;
repeated float first_rating_prob = 7;
repeated float review_rating_prob = 8;
repeated float first_rating_offsets = 9;
repeated float first_session_lens = 10;
float forget_rating_offset = 11;
float forget_session_len = 12;
float loss_aversion = 13;
uint32 learn_limit = 14;
uint32 review_limit = 15;
} }
message EvaluateWeightsRequest { message EvaluateWeightsRequest {

View file

@ -6,8 +6,9 @@ from __future__ import annotations
import sys import sys
import time import time
import traceback import traceback
from collections.abc import Iterable, Sequence
from threading import current_thread, main_thread from threading import current_thread, main_thread
from typing import TYPE_CHECKING, Any, Iterable, Sequence from typing import TYPE_CHECKING, Any
from weakref import ref from weakref import ref
from markdown import markdown from markdown import markdown

View file

@ -8,7 +8,8 @@ import os
import pathlib import pathlib
import sys import sys
import traceback import traceback
from typing import TYPE_CHECKING, Any, Callable, Union from collections.abc import Callable
from typing import TYPE_CHECKING, Any, Union
from anki._vendor import stringcase # type: ignore from anki._vendor import stringcase # type: ignore

View file

@ -1,6 +1,5 @@
from typing import Union from typing import Union
class Backend: class Backend:
@classmethod @classmethod
def command(cls, service: int, method: int, data: bytes) -> bytes: ... def command(cls, service: int, method: int, data: bytes) -> bytes: ...

View file

@ -5,6 +5,7 @@ from __future__ import annotations
import pprint import pprint
import time import time
from typing import NewType
import anki # pylint: disable=unused-import import anki # pylint: disable=unused-import
import anki.collection import anki.collection

View file

@ -3,7 +3,8 @@
from __future__ import annotations from __future__ import annotations
from typing import Any, Generator, Iterable, Literal, Sequence, Union, cast from collections.abc import Generator, Iterable, Sequence
from typing import Any, Literal, Union, cast
from anki import ( from anki import (
ankiweb_pb2, ankiweb_pb2,

View file

@ -4,8 +4,9 @@
from __future__ import annotations from __future__ import annotations
import re import re
from collections.abc import Callable, Iterable, Sequence
from re import Match from re import Match
from typing import TYPE_CHECKING, Any, Callable, Iterable, Sequence, Union from typing import TYPE_CHECKING, Any, Union
if TYPE_CHECKING: if TYPE_CHECKING:
import anki._backend import anki._backend

View file

@ -4,7 +4,8 @@
from __future__ import annotations from __future__ import annotations
import copy import copy
from typing import TYPE_CHECKING, Any, Iterable, NewType, Sequence from collections.abc import Iterable, Sequence
from typing import TYPE_CHECKING, Any, NewType
if TYPE_CHECKING: if TYPE_CHECKING:
import anki import anki

View file

@ -13,8 +13,9 @@ import threading
import time import time
import unicodedata import unicodedata
import zipfile import zipfile
from collections.abc import Sequence
from io import BufferedWriter from io import BufferedWriter
from typing import Any, Optional, Sequence from typing import Any
from zipfile import ZipFile from zipfile import ZipFile
from anki import hooks from anki import hooks
@ -26,16 +27,16 @@ from anki.utils import ids2str, namedtmp, split_fields, strip_html
class Exporter: class Exporter:
includeHTML: bool | None = None includeHTML: bool | None = None
ext: Optional[str] = None ext: str | None = None
includeTags: Optional[bool] = None includeTags: bool | None = None
includeSched: Optional[bool] = None includeSched: bool | None = None
includeMedia: Optional[bool] = None includeMedia: bool | None = None
def __init__( def __init__(
self, self,
col: Collection, col: Collection,
did: Optional[DeckId] = None, did: DeckId | None = None,
cids: Optional[list[CardId]] = None, cids: list[CardId] | None = None,
) -> None: ) -> None:
self.col = col.weakref() self.col = col.weakref()
self.did = did self.did = did

View file

@ -94,8 +94,8 @@ class ForeignCard:
class ForeignNote: class ForeignNote:
fields: list[str] = field(default_factory=list) fields: list[str] = field(default_factory=list)
tags: list[str] = field(default_factory=list) tags: list[str] = field(default_factory=list)
notetype: Union[str, NotetypeId] = "" notetype: str | NotetypeId = ""
deck: Union[str, DeckId] = "" deck: str | DeckId = ""
cards: list[ForeignCard] = field(default_factory=list) cards: list[ForeignCard] = field(default_factory=list)
@ -103,7 +103,7 @@ class ForeignNote:
class ForeignData: class ForeignData:
notes: list[ForeignNote] = field(default_factory=list) notes: list[ForeignNote] = field(default_factory=list)
notetypes: list[ForeignNotetype] = field(default_factory=list) notetypes: list[ForeignNotetype] = field(default_factory=list)
default_deck: Union[str, DeckId] = "" default_deck: str | DeckId = ""
def serialize(self) -> str: def serialize(self) -> str:
return json.dumps(self, cls=ForeignDataEncoder, separators=(",", ":")) return json.dumps(self, cls=ForeignDataEncoder, separators=(",", ":"))

View file

@ -17,7 +17,6 @@ Notetype | Card Type
import re import re
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Tuple, Type
from anki.db import DB from anki.db import DB
from anki.decks import DeckId from anki.decks import DeckId
@ -38,7 +37,7 @@ def serialize(db_path: str, deck_id: DeckId) -> str:
def gather_data(db: DB, deck_id: DeckId) -> ForeignData: def gather_data(db: DB, deck_id: DeckId) -> ForeignData:
facts = gather_facts(db) facts = gather_facts(db)
gather_cards_into_facts(db, facts) gather_cards_into_facts(db, facts)
used_fact_views: dict[Type[MnemoFactView], bool] = {} used_fact_views: dict[type[MnemoFactView], bool] = {}
notes = [fact.foreign_note(used_fact_views) for fact in facts.values()] notes = [fact.foreign_note(used_fact_views) for fact in facts.values()]
notetypes = [fact_view.foreign_notetype() for fact_view in used_fact_views] notetypes = [fact_view.foreign_notetype() for fact_view in used_fact_views]
return ForeignData(notes, notetypes, deck_id) return ForeignData(notes, notetypes, deck_id)
@ -54,7 +53,7 @@ def open_mnemosyne_db(db_path: str) -> DB:
class MnemoFactView(ABC): class MnemoFactView(ABC):
notetype: str notetype: str
field_keys: Tuple[str, ...] field_keys: tuple[str, ...]
@classmethod @classmethod
@abstractmethod @abstractmethod
@ -162,7 +161,7 @@ class MnemoFact:
cards: list[MnemoCard] = field(default_factory=list) cards: list[MnemoCard] = field(default_factory=list)
def foreign_note( def foreign_note(
self, used_fact_views: dict[Type[MnemoFactView], bool] self, used_fact_views: dict[type[MnemoFactView], bool]
) -> ForeignNote: ) -> ForeignNote:
fact_view = self.fact_view() fact_view = self.fact_view()
used_fact_views[fact_view] = True used_fact_views[fact_view] = True
@ -173,7 +172,7 @@ class MnemoFact:
cards=self.foreign_cards(), cards=self.foreign_cards(),
) )
def fact_view(self) -> Type[MnemoFactView]: def fact_view(self) -> type[MnemoFactView]:
try: try:
fact_view = self.cards[0].fact_view_id fact_view = self.cards[0].fact_view_id
except IndexError as err: except IndexError as err:
@ -190,7 +189,7 @@ class MnemoFact:
raise Exception(f"Fact {id} has unknown fact view: {fact_view}") raise Exception(f"Fact {id} has unknown fact view: {fact_view}")
def anki_fields(self, fact_view: Type[MnemoFactView]) -> list[str]: def anki_fields(self, fact_view: type[MnemoFactView]) -> list[str]:
return [munge_field(self.fields.get(k, "")) for k in fact_view.field_keys] return [munge_field(self.fields.get(k, "")) for k in fact_view.field_keys]
def anki_tags(self) -> list[str]: def anki_tags(self) -> list[str]:

View file

@ -14,6 +14,9 @@ modifying it.
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from typing import Any
import decorator import decorator
# You can find the definitions in ../tools/genhooks.py # You can find the definitions in ../tools/genhooks.py
@ -32,7 +35,7 @@ def runHook(hook: str, *args: Any) -> None:
for func in hookFuncs: for func in hookFuncs:
try: try:
func(*args) func(*args)
except: except Exception:
hookFuncs.remove(func) hookFuncs.remove(func)
raise raise
@ -43,7 +46,7 @@ def runFilter(hook: str, arg: Any, *args: Any) -> Any:
for func in hookFuncs: for func in hookFuncs:
try: try:
arg = func(arg, *args) arg = func(arg, *args)
except: except Exception:
hookFuncs.remove(func) hookFuncs.remove(func)
raise raise
return arg return arg

View file

@ -9,7 +9,8 @@ from __future__ import annotations
import io import io
import os import os
from typing import Any, Callable from collections.abc import Callable
from typing import Any
import requests import requests
from requests import Response from requests import Response

View file

@ -1,7 +1,8 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from typing import Any, Callable, Sequence, Type, Union from collections.abc import Callable, Sequence
from typing import Any, Type, Union
import anki import anki
from anki.collection import Collection from anki.collection import Collection

View file

@ -2,10 +2,11 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
# pylint: disable=invalid-name # pylint: disable=invalid-name
from __future__ import annotations
import os import os
import unicodedata import unicodedata
from typing import Optional from typing import Any
from anki.cards import CardId from anki.cards import CardId
from anki.collection import Collection from anki.collection import Collection
@ -31,7 +32,7 @@ class MediaMapInvalid(Exception):
class Anki2Importer(Importer): class Anki2Importer(Importer):
needMapper = False needMapper = False
deckPrefix: Optional[str] = None deckPrefix: str | None = None
allowUpdate = True allowUpdate = True
src: Collection src: Collection
dst: Collection dst: Collection
@ -409,7 +410,7 @@ insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)""",
if fname.startswith("_") and not self.dst.media.have(fname): if fname.startswith("_") and not self.dst.media.have(fname):
self._writeDstMedia(fname, self._srcMediaData(fname)) self._writeDstMedia(fname, self._srcMediaData(fname))
def _mediaData(self, fname: str, dir: Optional[str] = None) -> bytes: def _mediaData(self, fname: str, dir: str | None = None) -> bytes:
if not dir: if not dir:
dir = self.src.media.dir() dir = self.src.media.dir()
path = os.path.join(dir, fname) path = os.path.join(dir, fname)

View file

@ -2,12 +2,13 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
# pylint: disable=invalid-name # pylint: disable=invalid-name
from __future__ import annotations
import json import json
import os import os
import unicodedata import unicodedata
import zipfile import zipfile
from typing import Any, Optional from typing import Any
from anki.importing.anki2 import Anki2Importer, MediaMapInvalid from anki.importing.anki2 import Anki2Importer, MediaMapInvalid
from anki.utils import tmpfile from anki.utils import tmpfile
@ -15,7 +16,7 @@ from anki.utils import tmpfile
class AnkiPackageImporter(Anki2Importer): class AnkiPackageImporter(Anki2Importer):
nameToNum: dict[str, str] nameToNum: dict[str, str]
zip: Optional[zipfile.ZipFile] zip: zipfile.ZipFile | None
def run(self) -> None: # type: ignore def run(self) -> None: # type: ignore
# extract the deck from the zip file # extract the deck from the zip file

View file

@ -2,8 +2,9 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
# pylint: disable=invalid-name # pylint: disable=invalid-name
from __future__ import annotations
from typing import Any, Optional from typing import Any
from anki.collection import Collection from anki.collection import Collection
from anki.utils import max_id from anki.utils import max_id
@ -15,7 +16,7 @@ from anki.utils import max_id
class Importer: class Importer:
needMapper = False needMapper = False
needDelimiter = False needDelimiter = False
dst: Optional[Collection] dst: Collection | None
def __init__(self, col: Collection, file: str) -> None: def __init__(self, col: Collection, file: str) -> None:
self.file = file self.file = file

View file

@ -7,7 +7,7 @@ from __future__ import annotations
import csv import csv
import re import re
from typing import Any, Optional, TextIO from typing import Any, TextIO
from anki.collection import Collection from anki.collection import Collection
from anki.importing.noteimp import ForeignNote, NoteImporter from anki.importing.noteimp import ForeignNote, NoteImporter
@ -20,12 +20,12 @@ class TextImporter(NoteImporter):
def __init__(self, col: Collection, file: str) -> None: def __init__(self, col: Collection, file: str) -> None:
NoteImporter.__init__(self, col, file) NoteImporter.__init__(self, col, file)
self.lines = None self.lines = None
self.fileobj: Optional[TextIO] = None self.fileobj: TextIO | None = None
self.delimiter: Optional[str] = None self.delimiter: str | None = None
self.tagsToAdd: list[str] = [] self.tagsToAdd: list[str] = []
self.numFields = 0 self.numFields = 0
self.dialect: Optional[Any] self.dialect: Any | None
self.data: Optional[str | list[str]] self.data: str | list[str] | None
def foreignNotes(self) -> list[ForeignNote]: def foreignNotes(self) -> list[ForeignNote]:
self.open() self.open()
@ -99,15 +99,15 @@ class TextImporter(NoteImporter):
if not self.delimiter: if not self.delimiter:
try: try:
self.dialect = sniffer.sniff("\n".join(self.data[:10]), self.patterns) self.dialect = sniffer.sniff("\n".join(self.data[:10]), self.patterns)
except: except Exception:
try: try:
self.dialect = sniffer.sniff(self.data[0], self.patterns) self.dialect = sniffer.sniff(self.data[0], self.patterns)
except: except Exception:
pass pass
if self.dialect: if self.dialect:
try: try:
reader = csv.reader(self.data, self.dialect, doublequote=True) reader = csv.reader(self.data, self.dialect, doublequote=True)
except: except Exception:
err() err()
else: else:
if not self.delimiter: if not self.delimiter:
@ -126,7 +126,7 @@ class TextImporter(NoteImporter):
if row: if row:
self.numFields = len(row) self.numFields = len(row)
break break
except: except Exception:
err() err()
self.initMapping() self.initMapping()

View file

@ -7,7 +7,7 @@ from __future__ import annotations
import html import html
import unicodedata import unicodedata
from typing import Optional, Union from typing import Union
from anki.collection import Collection from anki.collection import Collection
from anki.config import Config from anki.config import Config
@ -76,8 +76,8 @@ class NoteImporter(Importer):
needDelimiter = False needDelimiter = False
allowHTML = False allowHTML = False
importMode = UPDATE_MODE importMode = UPDATE_MODE
mapping: Optional[list[str]] mapping: list[str] | None
tagModified: Optional[str] tagModified: str | None
def __init__(self, col: Collection, file: str) -> None: def __init__(self, col: Collection, file: str) -> None:
Importer.__init__(self, col, file) Importer.__init__(self, col, file)
@ -268,7 +268,7 @@ class NoteImporter(Importer):
def updateData( def updateData(
self, n: ForeignNote, id: NoteId, sflds: list[str] self, n: ForeignNote, id: NoteId, sflds: list[str]
) -> Optional[Updates]: ) -> Updates | None:
self._ids.append(id) self._ids.append(id)
self.processFields(n, sflds) self.processFields(n, sflds)
if self._tagsMapped: if self._tagsMapped:
@ -316,9 +316,7 @@ where id = ? and flds != ?""",
changes2 = self.col.db.scalar("select total_changes()") changes2 = self.col.db.scalar("select total_changes()")
self.updateCount = changes2 - changes self.updateCount = changes2 - changes
def processFields( def processFields(self, note: ForeignNote, fields: list[str] | None = None) -> None:
self, note: ForeignNote, fields: Optional[list[str]] = None
) -> None:
if not fields: if not fields:
fields = [""] * len(self.model["flds"]) fields = [""] * len(self.model["flds"])
for c, f in enumerate(self.mapping): for c, f in enumerate(self.mapping):

View file

@ -3,13 +3,13 @@
# pytype: disable=attribute-error # pytype: disable=attribute-error
# type: ignore # type: ignore
# pylint: disable=C # pylint: disable=C
from __future__ import annotations
import re import re
import sys import sys
import time import time
import unicodedata import unicodedata
from string import capwords from string import capwords
from typing import Optional, Union
from xml.dom import minidom from xml.dom import minidom
from xml.dom.minidom import Element, Text from xml.dom.minidom import Element, Text
@ -329,7 +329,7 @@ class SupermemoXmlImporter(NoteImporter):
self.logger("Load done.") self.logger("Load done.")
# PARSE # PARSE
def parse(self, node: Optional[Union[Text, Element]] = None) -> None: def parse(self, node: Text | Element | None = None) -> None:
"Parse method - parses document elements" "Parse method - parses document elements"
if node is None and self.xmldoc is not None: if node is None and self.xmldoc is not None:

View file

@ -180,9 +180,16 @@ def set_lang(lang: str) -> None:
tr_legacyglobal.backend = weakref.ref(current_i18n) tr_legacyglobal.backend = weakref.ref(current_i18n)
def get_def_lang(lang: str | None = None) -> tuple[int, str]: def get_def_lang(user_lang: str | None = None) -> tuple[int, str]:
"""Return lang converted to name used on disk and its index, defaulting to system language """Return user_lang converted to name used on disk and its index, defaulting to system language
or English if not available.""" or English if not available."""
def get_index_of_language(wanted_locale: str) -> int | None:
for i, (_, locale_) in enumerate(langs):
if locale_ == wanted_locale:
return i
return None
try: try:
# getdefaultlocale() is deprecated since Python 3.11, but we need to keep using it as getlocale() behaves differently: https://bugs.python.org/issue38805 # getdefaultlocale() is deprecated since Python 3.11, but we need to keep using it as getlocale() behaves differently: https://bugs.python.org/issue38805
with warnings.catch_warnings(): with warnings.catch_warnings():
@ -192,28 +199,30 @@ def get_def_lang(lang: str | None = None) -> tuple[int, str]:
# this will return a different format on Windows (e.g. Italian_Italy), resulting in us falling back to en_US # this will return a different format on Windows (e.g. Italian_Italy), resulting in us falling back to en_US
# further below # further below
(sys_lang, enc) = locale.getlocale() (sys_lang, enc) = locale.getlocale()
except: except Exception:
# fails on osx # fails on osx
sys_lang = "en_US" sys_lang = "en_US"
user_lang = lang
if user_lang in compatMap: if user_lang in compatMap:
user_lang = compatMap[user_lang] user_lang = compatMap[user_lang]
idx = None idx = None
lang = None lang = None
en_idx = None
for preferred_lang in (user_lang, sys_lang): for preferred_lang in (user_lang, sys_lang):
for lang_idx, (name, code) in enumerate(langs): idx = get_index_of_language(preferred_lang)
if code == "en_US": is_language_supported = idx is not None
en_idx = lang_idx if is_language_supported:
if code == preferred_lang: assert preferred_lang is not None
idx = lang_idx
lang = preferred_lang lang = preferred_lang
if idx is not None:
break break
# if the specified language and the system language aren't available, revert to english # if the specified language and the system language aren't available, revert to english
if idx is None: is_preferred_language_supported = idx is not None
idx = en_idx if not is_preferred_language_supported:
lang = "en_US" lang = "en_US"
idx = get_index_of_language(lang)
is_english_supported = idx is not None
if not is_english_supported:
raise AssertionError("English is supposed to be a supported language.")
assert idx is not None and lang is not None
return (idx, lang) return (idx, lang)

View file

@ -170,7 +170,7 @@ def _err_msg(col: anki.collection.Collection, type: str, texpath: str) -> str:
if not log: if not log:
raise Exception() raise Exception()
msg += f"<small><pre>{html.escape(log)}</pre></small>" msg += f"<small><pre>{html.escape(log)}</pre></small>"
except: except Exception:
msg += col.tr.media_have_you_installed_latex_and_dvipngdvisvgm() msg += col.tr.media_have_you_installed_latex_and_dvipngdvisvgm()
return msg return msg

View file

@ -8,7 +8,7 @@ import pprint
import re import re
import sys import sys
import time import time
from typing import Callable, Sequence from collections.abc import Callable, Sequence
from anki import media_pb2 from anki import media_pb2
from anki._legacy import DeprecatedNamesMixin, deprecated_keywords from anki._legacy import DeprecatedNamesMixin, deprecated_keywords

View file

@ -7,7 +7,8 @@ import copy
import pprint import pprint
import sys import sys
import time import time
from typing import Any, NewType, Sequence, Union from collections.abc import Sequence
from typing import Any, NewType, Union
import anki # pylint: disable=unused-import import anki # pylint: disable=unused-import
import anki.collection import anki.collection
@ -312,7 +313,7 @@ class ModelManager(DeprecatedNamesMixin):
def rename_field( def rename_field(
self, notetype: NotetypeDict, field: FieldDict, new_name: str self, notetype: NotetypeDict, field: FieldDict, new_name: str
) -> None: ) -> None:
if not field in notetype["flds"]: if field not in notetype["flds"]:
raise Exception("invalid field") raise Exception("invalid field")
field["name"] = new_name field["name"] = new_name
@ -388,8 +389,8 @@ and notes.mid = ? and cards.ord = ?""",
To get defaults, use To get defaults, use
input = ChangeNotetypeRequest() info = col.models.change_notetype_info(...)
input.ParseFromString(col.models.change_notetype_info(...)) input = info.input
input.note_ids.extend([...]) input.note_ids.extend([...])
The new_fields and new_templates lists are relative to the new notetype's The new_fields and new_templates lists are relative to the new notetype's

View file

@ -4,7 +4,8 @@
from __future__ import annotations from __future__ import annotations
import copy import copy
from typing import NewType, Sequence from collections.abc import Sequence
from typing import NewType
import anki # pylint: disable=unused-import import anki # pylint: disable=unused-import
import anki.cards import anki.cards
@ -94,21 +95,28 @@ class Note(DeprecatedNamesMixin):
self, self,
ord: int = 0, ord: int = 0,
*, *,
custom_note_type: NotetypeDict = None, custom_note_type: NotetypeDict | None = None,
custom_template: TemplateDict = None, custom_template: TemplateDict | None = None,
fill_empty: bool = False, fill_empty: bool = False,
) -> anki.cards.Card: ) -> anki.cards.Card:
card = anki.cards.Card(self.col) card = anki.cards.Card(self.col)
card.ord = ord card.ord = ord
card.did = anki.decks.DEFAULT_DECK_ID card.did = anki.decks.DEFAULT_DECK_ID
model = custom_note_type or self.note_type() if custom_note_type is None:
template = copy.copy( model = self.note_type()
custom_template else:
or ( model = custom_note_type
model["tmpls"][ord] if model["type"] == MODEL_STD else model["tmpls"][0] if model is None:
) raise NotImplementedError
)
if custom_template is not None:
template = custom_template
elif model["type"] == MODEL_STD:
template = model["tmpls"][ord]
else:
template = model["tmpls"][0]
template = copy.copy(template)
# may differ in cloze case # may differ in cloze case
template["ord"] = card.ord template["ord"] = card.ord
@ -171,10 +179,7 @@ class Note(DeprecatedNamesMixin):
return self.col.tags.in_list(tag, self.tags) return self.col.tags.in_list(tag, self.tags)
def remove_tag(self, tag: str) -> None: def remove_tag(self, tag: str) -> None:
rem = [] rem = [tag_ for tag_ in self.tags if tag_.lower() == tag.lower()]
for tag_ in self.tags:
if tag_.lower() == tag.lower():
rem.append(tag_)
for tag_ in rem: for tag_ in rem:
self.tags.remove(tag_) self.tags.remove(tag_)

View file

@ -22,7 +22,8 @@ ScheduleCardsAsNewDefaults = scheduler_pb2.ScheduleCardsAsNewDefaultsResponse
FilteredDeckForUpdate = decks_pb2.FilteredDeckForUpdate FilteredDeckForUpdate = decks_pb2.FilteredDeckForUpdate
RepositionDefaults = scheduler_pb2.RepositionDefaultsResponse RepositionDefaults = scheduler_pb2.RepositionDefaultsResponse
from typing import Sequence, overload from collections.abc import Sequence
from typing import overload
from anki import config_pb2 from anki import config_pb2
from anki.cards import CardId from anki.cards import CardId

View file

@ -5,8 +5,6 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional
from anki._legacy import deprecated from anki._legacy import deprecated
from anki.cards import Card, CardId from anki.cards import Card, CardId
from anki.consts import ( from anki.consts import (
@ -54,7 +52,7 @@ class SchedulerBaseWithLegacy(SchedulerBase):
print("_nextDueMsg() is obsolete") print("_nextDueMsg() is obsolete")
return "" return ""
def rebuildDyn(self, did: Optional[DeckId] = None) -> Optional[int]: def rebuildDyn(self, did: DeckId | None = None) -> int | None:
did = did or self.col.decks.selected() did = did or self.col.decks.selected()
count = self.rebuild_filtered_deck(did).count or None count = self.rebuild_filtered_deck(did).count or None
if not count: if not count:
@ -63,7 +61,7 @@ class SchedulerBaseWithLegacy(SchedulerBase):
self.col.decks.select(did) self.col.decks.select(did)
return count return count
def emptyDyn(self, did: Optional[DeckId], lim: Optional[str] = None) -> None: def emptyDyn(self, did: DeckId | None, lim: str | None = None) -> None:
if lim is None: if lim is None:
self.empty_filtered_deck(did) self.empty_filtered_deck(did)
return return

View file

@ -14,7 +14,8 @@ as '2' internally.
from __future__ import annotations from __future__ import annotations
from typing import Literal, Optional, Sequence from collections.abc import Sequence
from typing import Any, Literal
from anki import frontend_pb2, scheduler_pb2 from anki import frontend_pb2, scheduler_pb2
from anki._legacy import deprecated from anki._legacy import deprecated
@ -109,7 +110,7 @@ class Scheduler(SchedulerBaseWithLegacy):
# backend automatically resets queues as operations are performed # backend automatically resets queues as operations are performed
pass pass
def getCard(self) -> Optional[Card]: def getCard(self) -> Card | None:
"""Fetch the next card from the queue. None if finished.""" """Fetch the next card from the queue. None if finished."""
try: try:
queued_card = self.get_queued_cards().cards[0] queued_card = self.get_queued_cards().cards[0]
@ -125,7 +126,7 @@ class Scheduler(SchedulerBaseWithLegacy):
"Don't use this, it is a stop-gap until this code is refactored." "Don't use this, it is a stop-gap until this code is refactored."
return not self.get_queued_cards().cards return not self.get_queued_cards().cards
def counts(self, card: Optional[Card] = None) -> tuple[int, int, int]: def counts(self, card: Card | None = None) -> tuple[int, int, int]:
info = self.get_queued_cards() info = self.get_queued_cards()
return (info.new_count, info.learning_count, info.review_count) return (info.new_count, info.learning_count, info.review_count)

View file

@ -8,7 +8,8 @@ from __future__ import annotations
import json import json
import random import random
import time import time
from typing import Sequence from collections.abc import Sequence
from typing import Any
import anki.cards import anki.cards
import anki.collection import anki.collection
@ -721,7 +722,7 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = {QUEUE
tot = bad + good tot = bad + good
try: try:
pct = good / float(tot) * 100 pct = good / float(tot) * 100
except: except Exception:
pct = 0 pct = 0
i.append( i.append(
"Correct: <b>%(pct)0.2f%%</b><br>(%(good)d of %(tot)d)" "Correct: <b>%(pct)0.2f%%</b><br>(%(good)d of %(tot)d)"
@ -973,7 +974,7 @@ from cards where did in %s"""
else: else:
conf["legend"] = {"container": "#%sLegend" % id, "noColumns": 10} conf["legend"] = {"container": "#%sLegend" % id, "noColumns": 10}
conf["series"] = dict(stack=True) conf["series"] = dict(stack=True)
if not "yaxis" in conf: if "yaxis" not in conf:
conf["yaxis"] = {} conf["yaxis"] = {}
conf["yaxis"]["labelWidth"] = 40 conf["yaxis"]["labelWidth"] = 40
if "xaxis" not in conf: if "xaxis" not in conf:

View file

@ -3,7 +3,8 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Any, Callable from collections.abc import Callable
from typing import TYPE_CHECKING, Any
import anki.collection import anki.collection
import anki.models import anki.models

View file

@ -13,7 +13,8 @@ from __future__ import annotations
import pprint import pprint
import re import re
from typing import Collection, Match, Sequence from collections.abc import Collection, Sequence
from typing import Match
import anki # pylint: disable=unused-import import anki # pylint: disable=unused-import
import anki.collection import anki.collection

View file

@ -28,8 +28,9 @@ template_legacy.py file, using the legacy addHook() system.
from __future__ import annotations from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Sequence, Union from typing import Any, Union
import anki import anki
import anki.cards import anki.cards

View file

@ -13,9 +13,10 @@ import subprocess
import sys import sys
import tempfile import tempfile
import time import time
from collections.abc import Callable, Iterable, Iterator
from contextlib import contextmanager from contextlib import contextmanager
from hashlib import sha1 from hashlib import sha1
from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator from typing import TYPE_CHECKING, Any
from anki._legacy import DeprecatedNamesMixinForModule from anki._legacy import DeprecatedNamesMixinForModule
from anki.dbproxy import DBProxy from anki.dbproxy import DBProxy
@ -28,7 +29,7 @@ try:
to_json_bytes: Callable[[Any], bytes] = orjson.dumps to_json_bytes: Callable[[Any], bytes] = orjson.dumps
from_json_bytes = orjson.loads from_json_bytes = orjson.loads
except: except Exception:
print("orjson is missing; DB operations will be slower") print("orjson is missing; DB operations will be slower")
def to_json_bytes(obj: Any) -> bytes: def to_json_bytes(obj: Any) -> bytes:
@ -214,7 +215,7 @@ def call(argv: list[str], wait: bool = True, **kwargs: Any) -> int:
info = subprocess.STARTUPINFO() # type: ignore info = subprocess.STARTUPINFO() # type: ignore
try: try:
info.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore info.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore
except: except Exception:
# pylint: disable=no-member # pylint: disable=no-member
info.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW # type: ignore info.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW # type: ignore
else: else:
@ -285,7 +286,7 @@ def plat_desc() -> str:
else: else:
theos = system theos = system
break break
except: except Exception:
continue continue
return theos return theos

View file

@ -26,7 +26,7 @@ def clear_tempfile(tf):
try: try:
tf.close() tf.close()
os.unlink(tf.name) os.unlink(tf.name)
except: except Exception:
pass pass

View file

@ -4,7 +4,8 @@
import copy import copy
import os import os
import time import time
from typing import Callable, Dict from collections.abc import Callable
from typing import Dict
import pytest import pytest
@ -385,7 +386,7 @@ def test_reviews():
assert "leech" in c.note().tags assert "leech" in c.note().tags
def review_limits_setup() -> tuple[anki.collection.Collection, Dict]: def review_limits_setup() -> tuple[anki.collection.Collection, dict]:
col = getEmptyCol() col = getEmptyCol()
parent = col.decks.get(col.decks.id("parent")) parent = col.decks.get(col.decks.id("parent"))

View file

@ -137,7 +137,9 @@ prefix = """\
from __future__ import annotations from __future__ import annotations
from typing import Any, Callable, Sequence from collections.abc import Callable, Sequence
from typing import Any
import anki import anki
import anki.hooks import anki.hooks
from anki.cards import Card from anki.cards import Card

View file

@ -5,12 +5,13 @@
Code for generating hooks. Code for generating hooks.
""" """
from __future__ import annotations
import os import os
import subprocess import subprocess
import sys import sys
from dataclasses import dataclass from dataclasses import dataclass
from operator import attrgetter from operator import attrgetter
from typing import Optional
sys.path.append("pylib/anki/_vendor") sys.path.append("pylib/anki/_vendor")
@ -23,19 +24,19 @@ class Hook:
name: str name: str
# string of the typed arguments passed to the callback, eg # string of the typed arguments passed to the callback, eg
# ["kind: str", "val: int"] # ["kind: str", "val: int"]
args: list[str] = None args: list[str] | None = None
# string of the return type. if set, hook is a filter. # string of the return type. if set, hook is a filter.
return_type: Optional[str] = None return_type: str | None = None
# if add-ons may be relying on the legacy hook name, add it here # if add-ons may be relying on the legacy hook name, add it here
legacy_hook: Optional[str] = None legacy_hook: str | None = None
# if legacy hook takes no arguments but the new hook does, set this # if legacy hook takes no arguments but the new hook does, set this
legacy_no_args: bool = False legacy_no_args: bool = False
# if the hook replaces a deprecated one, add its name here # if the hook replaces a deprecated one, add its name here
replaces: Optional[str] = None replaces: str | None = None
# arguments that the hook being replaced took # arguments that the hook being replaced took
replaced_hook_args: Optional[list[str]] = None replaced_hook_args: list[str] | None = None
# docstring to add to hook class # docstring to add to hook class
doc: Optional[str] = None doc: str | None = None
def callable(self) -> str: def callable(self) -> str:
"Convert args into a Callable." "Convert args into a Callable."
@ -47,7 +48,7 @@ class Hook:
types_str = ", ".join(types) types_str = ", ".join(types)
return f"Callable[[{types_str}], {self.return_type or 'None'}]" return f"Callable[[{types_str}], {self.return_type or 'None'}]"
def arg_names(self, args: Optional[list[str]]) -> list[str]: def arg_names(self, args: list[str] | None) -> list[str]:
names = [] names = []
for arg in args or []: for arg in args or []:
if not arg: if not arg:
@ -126,7 +127,7 @@ class {self.classname()}:
for hook in self._hooks: for hook in self._hooks:
try: try:
hook({", ".join(arg_names)}) hook({", ".join(arg_names)})
except: except Exception:
# if the hook fails, remove it # if the hook fails, remove it
self._hooks.remove(hook) self._hooks.remove(hook)
raise raise
@ -162,7 +163,7 @@ class {self.classname()}:
for filter in self._hooks: for filter in self._hooks:
try: try:
{arg_names[0]} = filter({", ".join(arg_names)}) {arg_names[0]} = filter({", ".join(arg_names)})
except: except Exception:
# if the hook fails, remove it # if the hook fails, remove it
self._hooks.remove(filter) self._hooks.remove(filter)
raise raise

3
pyproject.toml Normal file
View file

@ -0,0 +1,3 @@
[tool.black]
target-version = ["py39", "py310", "py311", "py312"]
extend-exclude = "qt/bundle"

View file

@ -1,9 +1,5 @@
[settings] [settings]
ensure_newline_before_comments=true py_version=39
force_grid_wrap=0 profile=black
include_trailing_comma=True
known_first_party=anki,aqt known_first_party=anki,aqt
line_length=88 extend_skip=aqt/forms,hooks_gen.py
multi_line_output=3
skip=aqt/forms,hooks_gen.py
use_parentheses=True

View file

@ -5,6 +5,8 @@ from __future__ import annotations
import logging import logging
import sys import sys
from collections.abc import Callable
from typing import TYPE_CHECKING, Any, Union, cast
try: try:
import pip_system_certs.wrapt_requests import pip_system_certs.wrapt_requests
@ -50,7 +52,6 @@ import os
import tempfile import tempfile
import traceback import traceback
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Optional, cast
import anki.lang import anki.lang
from anki._backend import RustBackend from anki._backend import RustBackend
@ -61,6 +62,7 @@ from anki.utils import checksum, is_lin, is_mac
from aqt import gui_hooks from aqt import gui_hooks
from aqt.log import setup_logging from aqt.log import setup_logging
from aqt.qt import * from aqt.qt import *
from aqt.qt import sip
from aqt.utils import TR, tr from aqt.utils import TR, tr
if TYPE_CHECKING: if TYPE_CHECKING:
@ -92,8 +94,8 @@ appHelpSite = HELP_SITE
from aqt.main import AnkiQt # isort:skip from aqt.main import AnkiQt # isort:skip
from aqt.profiles import ProfileManager, VideoDriver # isort:skip from aqt.profiles import ProfileManager, VideoDriver # isort:skip
profiler: Optional[cProfile.Profile] = None profiler: cProfile.Profile | None = None
mw: Optional[AnkiQt] = None # set on init mw: AnkiQt | None = None # set on init
import aqt.forms import aqt.forms
@ -154,7 +156,7 @@ class DialogManager:
def allClosed(self) -> bool: def allClosed(self) -> bool:
return not any(x[1] for x in self._dialogs.values()) return not any(x[1] for x in self._dialogs.values())
def closeAll(self, onsuccess: Callable[[], None]) -> Optional[bool]: def closeAll(self, onsuccess: Callable[[], None]) -> bool | None:
# can we close immediately? # can we close immediately?
if self.allClosed(): if self.allClosed():
onsuccess() onsuccess()
@ -181,7 +183,7 @@ class DialogManager:
return True return True
def register_dialog( def register_dialog(
self, name: str, creator: Union[Callable, type], instance: Optional[Any] = None self, name: str, creator: Callable | type, instance: Any | None = None
) -> None: ) -> None:
"""Allows add-ons to register a custom dialog to be managed by Anki's dialog """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, manager, which ensures that only one copy of the window is open at once,
@ -219,19 +221,19 @@ dialogs = DialogManager()
# A reference to the Qt translator needs to be held to prevent it from # A reference to the Qt translator needs to be held to prevent it from
# being immediately deallocated. # being immediately deallocated.
_qtrans: Optional[QTranslator] = None _qtrans: QTranslator | None = None
def setupLangAndBackend( def setupLangAndBackend(
pm: ProfileManager, pm: ProfileManager,
app: QApplication, app: QApplication,
force: Optional[str] = None, force: str | None = None,
firstTime: bool = False, firstTime: bool = False,
) -> RustBackend: ) -> RustBackend:
global _qtrans global _qtrans
try: try:
locale.setlocale(locale.LC_ALL, "") locale.setlocale(locale.LC_ALL, "")
except: except Exception:
pass pass
# add _ and ngettext globals used by legacy code # add _ and ngettext globals used by legacy code
@ -288,7 +290,7 @@ def setupLangAndBackend(
class NativeEventFilter(QAbstractNativeEventFilter): class NativeEventFilter(QAbstractNativeEventFilter):
def nativeEventFilter( def nativeEventFilter(
self, eventType: Any, message: Any self, eventType: Any, message: Any
) -> tuple[bool, Optional[sip.voidptr]]: ) -> tuple[bool, sip.voidptr | None]:
if eventType == "windows_generic_MSG": if eventType == "windows_generic_MSG":
import ctypes import ctypes
@ -563,7 +565,7 @@ def run() -> None:
) )
def _run(argv: Optional[list[str]] = None, exec: bool = True) -> Optional[AnkiApp]: def _run(argv: list[str] | None = None, exec: bool = True) -> AnkiApp | None:
"""Start AnkiQt application or reuse an existing instance if one exists. """Start AnkiQt application or reuse an existing instance if one exists.
If the function is invoked with exec=False, the AnkiQt will not enter If the function is invoked with exec=False, the AnkiQt will not enter
@ -629,7 +631,7 @@ def _run(argv: Optional[list[str]] = None, exec: bool = True) -> Optional[AnkiAp
pmLoadResult = pm.setupMeta() pmLoadResult = pm.setupMeta()
Collection.initialize_backend_logging() Collection.initialize_backend_logging()
except: except Exception:
# will handle below # will handle below
traceback.print_exc() traceback.print_exc()
pm = None pm = None
@ -671,11 +673,6 @@ def _run(argv: Optional[list[str]] = None, exec: bool = True) -> Optional[AnkiAp
# we've signaled the primary instance, so we should close # we've signaled the primary instance, so we should close
return None return None
setup_logging(
pm.addon_logs(),
level=logging.DEBUG if int(os.getenv("ANKIDEV", "0")) else logging.INFO,
)
if not pm: if not pm:
if i18n_setup: if i18n_setup:
QMessageBox.critical( QMessageBox.critical(
@ -687,6 +684,11 @@ def _run(argv: Optional[list[str]] = None, exec: bool = True) -> Optional[AnkiAp
QMessageBox.critical(None, "Startup Failed", "Unable to create data folder") QMessageBox.critical(None, "Startup Failed", "Unable to create data folder")
return None return None
setup_logging(
pm.addon_logs(),
level=logging.DEBUG if int(os.getenv("ANKIDEV", "0")) else logging.INFO,
)
# disable icons on mac; this must be done before window created # disable icons on mac; this must be done before window created
if is_mac: if is_mac:
app.setAttribute(Qt.ApplicationAttribute.AA_DontShowIconsInMenus) app.setAttribute(Qt.ApplicationAttribute.AA_DontShowIconsInMenus)
@ -719,7 +721,7 @@ def _run(argv: Optional[list[str]] = None, exec: bool = True) -> Optional[AnkiAp
# we must have a usable temp dir # we must have a usable temp dir
try: try:
tempfile.gettempdir() tempfile.gettempdir()
except: except Exception:
QMessageBox.critical( QMessageBox.critical(
None, None,
tr.qt_misc_error(), tr.qt_misc_error(),

View file

@ -5,8 +5,8 @@ from __future__ import annotations
import os import os
import sys import sys
from collections.abc import Callable
from ctypes import CDLL, CFUNCTYPE, c_bool, c_char_p from ctypes import CDLL, CFUNCTYPE, c_bool, c_char_p
from typing import Callable
import aqt import aqt
import aqt.utils import aqt.utils

View file

@ -2,6 +2,7 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import platform import platform
from collections.abc import Callable
import aqt.forms import aqt.forms
from anki.lang import without_unicode_isolation from anki.lang import without_unicode_isolation
@ -86,6 +87,7 @@ def show(mw: aqt.AnkiQt) -> QDialog:
"Christian Krause", "Christian Krause",
"Christian Rusche", "Christian Rusche",
"Dave Druelinger", "Dave Druelinger",
"David Culley",
"David Smith", "David Smith",
"Dmitry Mikheev", "Dmitry Mikheev",
"Dotan Cohen", "Dotan Cohen",
@ -194,6 +196,8 @@ def show(mw: aqt.AnkiQt) -> QDialog:
"Marko Sisovic", "Marko Sisovic",
"Lucas Scharenbroch", "Lucas Scharenbroch",
"Antoine Q.", "Antoine Q.",
"Ian Samir Yep Manzano",
"Asuka Minato",
) )
) )

View file

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Callable, Optional from collections.abc import Callable
import aqt.editor import aqt.editor
import aqt.forms import aqt.forms
@ -54,7 +54,7 @@ class AddCards(QMainWindow):
self.setupButtons() self.setupButtons()
self.col.add_image_occlusion_notetype() self.col.add_image_occlusion_notetype()
self.history: list[NoteId] = [] self.history: list[NoteId] = []
self._last_added_note: Optional[Note] = None self._last_added_note: Note | None = None
gui_hooks.operation_did_execute.append(self.on_operation_did_execute) gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
restoreGeom(self, "add") restoreGeom(self, "add")
gui_hooks.add_cards_did_init(self) gui_hooks.add_cards_did_init(self)
@ -178,7 +178,7 @@ class AddCards(QMainWindow):
break break
# copy non-empty old fields # copy non-empty old fields
if ( if (
not old_field_value in copied_field_names old_field_value not in copied_field_names
and old_note.fields[old_idx] and old_note.fields[old_idx]
): ):
new_note.fields[new_idx] = old_note.fields[old_idx] new_note.fields[new_idx] = old_note.fields[old_idx]
@ -195,7 +195,7 @@ class AddCards(QMainWindow):
self, old_note.note_type(), new_note.note_type() self, old_note.note_type(), new_note.note_type()
) )
def _load_new_note(self, sticky_fields_from: Optional[Note] = None) -> None: def _load_new_note(self, sticky_fields_from: Note | None = None) -> None:
note = self._new_note() note = self._new_note()
if old_note := sticky_fields_from: if old_note := sticky_fields_from:
flds = note.note_type()["flds"] flds = note.note_type()["flds"]
@ -209,7 +209,7 @@ class AddCards(QMainWindow):
self.setAndFocusNote(note) self.setAndFocusNote(note)
def on_operation_did_execute( def on_operation_did_execute(
self, changes: OpChanges, handler: Optional[object] self, changes: OpChanges, handler: object | None
) -> None: ) -> None:
if (changes.notetype or changes.deck) and handler is not self.editor: if (changes.notetype or changes.deck) and handler is not self.editor:
self.on_notetype_change( self.on_notetype_change(

View file

@ -9,13 +9,16 @@ import json
import logging import logging
import os import os
import re import re
import sys
import traceback
import zipfile import zipfile
from collections import defaultdict from collections import defaultdict
from collections.abc import Callable, Iterable, Sequence
from concurrent.futures import Future from concurrent.futures import Future
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import IO, Any, Callable, Iterable, Sequence, Union from typing import IO, Any, Union
from urllib.parse import parse_qs, urlparse from urllib.parse import parse_qs, urlparse
from zipfile import ZipFile from zipfile import ZipFile
@ -247,7 +250,7 @@ class AddonManager:
__import__(addon.dir_name) __import__(addon.dir_name)
except AbortAddonImport: except AbortAddonImport:
pass pass
except: except Exception:
name = html.escape(addon.human_name()) name = html.escape(addon.human_name())
page = addon.page() page = addon.page()
if page: if page:
@ -340,7 +343,7 @@ class AddonManager:
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
print(f"json error in add-on {module}:\n{e}") print(f"json error in add-on {module}:\n{e}")
return dict() return dict()
except: except Exception:
# missing meta file, etc # missing meta file, etc
return dict() return dict()
@ -643,7 +646,7 @@ class AddonManager:
try: try:
with open(path, encoding="utf8") as f: with open(path, encoding="utf8") as f:
return json.load(f) return json.load(f)
except: except Exception:
return None return None
def set_config_help_action(self, module: str, action: Callable[[], str]) -> None: def set_config_help_action(self, module: str, action: Callable[[], str]) -> None:
@ -1083,9 +1086,11 @@ def download_addon(client: HttpClient, id: int) -> DownloadOk | DownloadError:
data = client.stream_content(resp) data = client.stream_content(resp)
fname = re.match( match = re.match(
"attachment; filename=(.+)", resp.headers["content-disposition"] "attachment; filename=(.+)", resp.headers["content-disposition"]
).group(1) )
assert match is not None
fname = match.group(1)
meta = extract_meta_from_download_url(resp.url) meta = extract_meta_from_download_url(resp.url)
@ -1113,11 +1118,14 @@ def extract_meta_from_download_url(url: str) -> ExtractedDownloadMeta:
urlobj = urlparse(url) urlobj = urlparse(url)
query = parse_qs(urlobj.query) query = parse_qs(urlobj.query)
def get_first_element(elements: list[str]) -> int:
return int(elements[0])
meta = ExtractedDownloadMeta( meta = ExtractedDownloadMeta(
mod_time=int(query.get("t")[0]), mod_time=get_first_element(query["t"]),
min_point_version=int(query.get("minpt")[0]), min_point_version=get_first_element(query["minpt"]),
max_point_version=int(query.get("maxpt")[0]), max_point_version=get_first_element(query["maxpt"]),
branch_index=int(query.get("bidx")[0]), branch_index=get_first_element(query["bidx"]),
) )
return meta return meta

View file

@ -6,7 +6,8 @@ from __future__ import annotations
import json import json
import math import math
import re import re
from typing import Callable, Sequence from collections.abc import Callable, Sequence
from typing import Any
import aqt import aqt
import aqt.browser import aqt.browser

View file

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Callable from collections.abc import Callable
import aqt import aqt
from anki.cards import Card, CardId from anki.cards import Card, CardId

View file

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Sequence from collections.abc import Sequence
import aqt import aqt
import aqt.forms import aqt.forms

View file

@ -13,6 +13,7 @@ import aqt.forms
from anki.collection import SearchNode from anki.collection import SearchNode
from anki.notes import NoteId from anki.notes import NoteId
from aqt.qt import * from aqt.qt import *
from aqt.qt import sip
from aqt.webview import AnkiWebViewKind from aqt.webview import AnkiWebViewKind
from ..operations import QueryOp from ..operations import QueryOp

View file

@ -6,7 +6,8 @@ from __future__ import annotations
import json import json
import re import re
import time import time
from typing import Any, Callable from collections.abc import Callable
from typing import Any
import aqt.browser import aqt.browser
from anki.cards import Card from anki.cards import Card

View file

@ -2,8 +2,8 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable, Iterable
from enum import Enum, auto from enum import Enum, auto
from typing import Callable, Iterable
from anki.collection import SearchNode from anki.collection import SearchNode
from aqt.theme import ColoredIcon from aqt.theme import ColoredIcon

View file

@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from enum import Enum, auto from enum import Enum, auto
import aqt import aqt

View file

@ -2,8 +2,9 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable, Iterable
from enum import Enum, auto from enum import Enum, auto
from typing import Iterable, cast from typing import cast
import aqt import aqt
import aqt.browser import aqt.browser
@ -43,6 +44,7 @@ from aqt.operations.tag import (
set_tag_collapsed, set_tag_collapsed,
) )
from aqt.qt import * from aqt.qt import *
from aqt.qt import sip
from aqt.theme import ColoredIcon, theme_manager from aqt.theme import ColoredIcon, theme_manager
from aqt.utils import ( from aqt.utils import (
KeyboardModifiersPressed, KeyboardModifiersPressed,
@ -147,7 +149,7 @@ class SidebarTreeView(QTreeView):
def op_executed( def op_executed(
self, changes: OpChanges, handler: object | None, focused: bool self, changes: OpChanges, handler: object | None, focused: bool
) -> None: ) -> None:
if changes.browser_sidebar and not handler is self: if changes.browser_sidebar and handler is not self:
self._refresh_needed = True self._refresh_needed = True
if focused: if focused:
self.refresh_if_needed() self.refresh_if_needed()
@ -266,7 +268,7 @@ class SidebarTreeView(QTreeView):
def update_search( def update_search(
self, self,
*terms: Union[str, SearchNode], *terms: str | SearchNode,
joiner: SearchJoiner = "AND", joiner: SearchJoiner = "AND",
) -> None: ) -> None:
"""Modify the current search string based on modifier keys, then refresh.""" """Modify the current search string based on modifier keys, then refresh."""
@ -524,7 +526,7 @@ class SidebarTreeView(QTreeView):
*, *,
root: SidebarItem, root: SidebarItem,
name: str, name: str,
icon: Union[str, ColoredIcon], icon: str | ColoredIcon,
collapse_key: Config.Bool.V, collapse_key: Config.Bool.V,
type: SidebarItemType | None = None, type: SidebarItemType | None = None,
) -> SidebarItem: ) -> SidebarItem:

View file

@ -4,8 +4,9 @@ from __future__ import annotations
import copy import copy
import time import time
from collections.abc import Generator, Sequence
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, Generator, Sequence, Union from typing import TYPE_CHECKING, Union
import aqt import aqt
import aqt.browser import aqt.browser

View file

@ -3,7 +3,8 @@
from __future__ import annotations from __future__ import annotations
import time import time
from typing import Any, Callable, Sequence from collections.abc import Callable, Sequence
from typing import Any
import aqt import aqt
import aqt.browser import aqt.browser
@ -242,7 +243,7 @@ class DataModel(QAbstractTableModel):
self._state = self._state.toggle_state() self._state = self._state.toggle_state()
try: try:
self._search_inner(context) self._search_inner(context)
except: except Exception:
# rollback to prevent inconsistent state # rollback to prevent inconsistent state
self._state = self._state.toggle_state() self._state = self._state.toggle_state()
raise raise

View file

@ -3,7 +3,8 @@
from __future__ import annotations from __future__ import annotations
from abc import ABC, abstractmethod, abstractproperty from abc import ABC, abstractmethod, abstractproperty
from typing import Sequence, cast from collections.abc import Sequence
from typing import cast
from anki.browser import BrowserConfig from anki.browser import BrowserConfig
from anki.cards import Card, CardId from anki.cards import Card, CardId

View file

@ -2,7 +2,8 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations from __future__ import annotations
from typing import Any, Callable, Sequence from collections.abc import Callable, Sequence
from typing import Any
import aqt import aqt
import aqt.browser import aqt.browser

View file

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Sequence from collections.abc import Sequence
import aqt import aqt
import aqt.deckconf import aqt.deckconf

View file

@ -5,8 +5,9 @@ from __future__ import annotations
import json import json
import re import re
from collections.abc import Callable
from concurrent.futures import Future from concurrent.futures import Future
from typing import Any, Match, Optional, cast from typing import Any, Match, cast
import aqt import aqt
import aqt.forms import aqt.forms
@ -50,7 +51,7 @@ class CardLayout(QDialog):
mw: AnkiQt, mw: AnkiQt,
note: Note, note: Note,
ord: int = 0, ord: int = 0,
parent: Optional[QWidget] = None, parent: QWidget | None = None,
fill_empty: bool = False, fill_empty: bool = False,
) -> None: ) -> None:
QDialog.__init__(self, parent or mw, Qt.WindowType.Window) QDialog.__init__(self, parent or mw, Qt.WindowType.Window)
@ -509,7 +510,7 @@ class CardLayout(QDialog):
# Preview # Preview
########################################################################## ##########################################################################
_previewTimer: Optional[QTimer] = None _previewTimer: QTimer | None = None
def renderPreview(self) -> None: def renderPreview(self) -> None:
# schedule a preview when timing stops # schedule a preview when timing stops
@ -590,7 +591,7 @@ class CardLayout(QDialog):
return res return res
type_filter = r"\[\[type:.+?\]\]" type_filter = r"\[\[type:.+?\]\]"
repl: Union[str, Callable] repl: str | Callable
if type == "q": if type == "q":
repl = "<input id='typeans' type=text value='example' readonly='readonly'>" repl = "<input id='typeans' type=text value='example' readonly='readonly'>"

View file

@ -3,8 +3,6 @@
from __future__ import annotations from __future__ import annotations
from typing import Tuple
import aqt import aqt
import aqt.forms import aqt.forms
import aqt.operations import aqt.operations
@ -37,12 +35,12 @@ class CustomStudy(QDialog):
def fetch_data_and_show(mw: aqt.AnkiQt) -> None: def fetch_data_and_show(mw: aqt.AnkiQt) -> None:
def fetch_data( def fetch_data(
col: Collection, col: Collection,
) -> Tuple[DeckId, CustomStudyDefaults]: ) -> tuple[DeckId, CustomStudyDefaults]:
deck_id = mw.col.decks.get_current_id() deck_id = mw.col.decks.get_current_id()
defaults = col.sched.custom_study_defaults(deck_id) defaults = col.sched.custom_study_defaults(deck_id)
return (deck_id, defaults) return (deck_id, defaults)
def show_dialog(data: Tuple[DeckId, CustomStudyDefaults]) -> None: def show_dialog(data: tuple[DeckId, CustomStudyDefaults]) -> None:
deck_id, defaults = data deck_id, defaults = data
CustomStudy(mw=mw, deck_id=deck_id, defaults=defaults) CustomStudy(mw=mw, deck_id=deck_id, defaults=defaults)

View file

@ -4,6 +4,8 @@
from __future__ import annotations from __future__ import annotations
import os import os
import sys
from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from functools import partial from functools import partial
from pathlib import Path from pathlib import Path
@ -292,7 +294,7 @@ class DebugConsole(QDialog):
try: try:
# pylint: disable=exec-used # pylint: disable=exec-used
exec(text, vars) exec(text, vars)
except: except Exception:
self._output += traceback.format_exc() self._output += traceback.format_exc()
self._captureOutput(False) self._captureOutput(False)
buf = "" buf = ""

View file

@ -3,10 +3,13 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from anki.collection import OpChanges from anki.collection import OpChanges
from anki.decks import DEFAULT_DECK_ID, DeckId from anki.decks import DEFAULT_DECK_ID, DeckId
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.qt import * from aqt.qt import *
from aqt.qt import sip
from aqt.utils import HelpPage, shortcut, tr from aqt.utils import HelpPage, shortcut, tr

View file

@ -173,8 +173,8 @@ class DeckConf(QDialog):
# Loading # Loading
################################################## ##################################################
def listToUser(self, l: list[Union[int, float]]) -> str: def listToUser(self, l: list[int | float]) -> str:
def num_to_user(n: Union[int, float]) -> str: def num_to_user(n: int | float) -> str:
if n == round(n): if n == round(n):
return str(int(n)) return str(int(n))
else: else:
@ -267,7 +267,7 @@ class DeckConf(QDialog):
if i == int(i): if i == int(i):
i = int(i) i = int(i)
ret.append(i) ret.append(i)
except: except Exception:
# invalid, don't update # invalid, don't update
showWarning(tr.scheduling_steps_must_be_numbers()) showWarning(tr.scheduling_steps_must_be_numbers())
return return

View file

@ -1,6 +1,8 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from typing import Optional from __future__ import annotations
from collections.abc import Callable
import aqt.editor import aqt.editor
from anki.collection import OpChanges from anki.collection import OpChanges
@ -37,7 +39,7 @@ class EditCurrent(QMainWindow):
self.show() self.show()
def on_operation_did_execute( def on_operation_did_execute(
self, changes: OpChanges, handler: Optional[object] self, changes: OpChanges, handler: object | None
) -> None: ) -> None:
if changes.note_text and handler is not self.editor: if changes.note_text and handler is not self.editor:
# reload note # reload note

View file

@ -9,14 +9,16 @@ import html
import itertools import itertools
import json import json
import mimetypes import mimetypes
import os
import re import re
import urllib.error import urllib.error
import urllib.parse import urllib.parse
import urllib.request import urllib.request
import warnings import warnings
from collections.abc import Callable
from enum import Enum from enum import Enum
from random import randrange from random import randrange
from typing import Any, Callable, Match, cast from typing import Any, Match, cast
import bs4 import bs4
import requests import requests
@ -59,7 +61,7 @@ from aqt.utils import (
) )
from aqt.webview import AnkiWebView, AnkiWebViewKind from aqt.webview import AnkiWebView, AnkiWebViewKind
pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg", "webp", "ico", "avif") pics = ("jpg", "jpeg", "png", "gif", "svg", "webp", "ico", "avif")
audio = ( audio = (
"3gp", "3gp",
"aac", "aac",
@ -294,6 +296,8 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
disables: bool = True, disables: bool = True,
rightside: bool = True, rightside: bool = True,
) -> str: ) -> str:
title_attribute = tip
if icon: if icon:
if icon.startswith("qrc:/"): if icon.startswith("qrc:/"):
iconstr = icon iconstr = icon
@ -301,47 +305,35 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
iconstr = self.resourceToData(icon) iconstr = self.resourceToData(icon)
else: else:
iconstr = f"/_anki/imgs/{icon}.png" iconstr = f"/_anki/imgs/{icon}.png"
imgelm = f"""<img class="topbut" src="{iconstr}">""" image_element = f'<img class="topbut" src="{iconstr}">'
else: else:
imgelm = "" image_element = ""
if label or not imgelm:
labelelm = label or cmd if not label and icon:
label_element = ""
elif label:
label_element = label
else: else:
labelelm = "" label_element = cmd
if id:
idstr = f"id={id}" title_attribute = shortcut(title_attribute)
else: cmd_to_toggle_button = "toggleEditorButton(this);" if toggleable else ""
idstr = "" id_attribute_assignment = f"id={id}" if id else ""
if toggleable: class_attribute = "linkb" if rightside else "rounded"
toggleScript = "toggleEditorButton(this);"
else:
toggleScript = ""
tip = shortcut(tip)
if rightside:
class_ = "linkb"
else:
class_ = "rounded"
if not disables: if not disables:
class_ += " perm" class_attribute += " perm"
return """<button tabindex=-1
{id} return f"""<button tabindex=-1
class="{class_}" {id_attribute_assignment}
class="{class_attribute}"
type="button" type="button"
title="{tip}" title="{title_attribute}"
onclick="pycmd('{cmd}');{togglesc}return false;" onclick="pycmd('{cmd}');{cmd_to_toggle_button}return false;"
onmousedown="window.event.preventDefault();" onmousedown="window.event.preventDefault();"
> >
{imgelm} {image_element}
{labelelm} {label_element}
</button>""".format( </button>"""
imgelm=imgelm,
cmd=cmd,
tip=tip,
labelelm=labelelm,
id=idstr,
togglesc=toggleScript,
class_=class_,
)
def setupShortcuts(self) -> None: def setupShortcuts(self) -> None:
# if a third element is provided, enable shortcut even when no field selected # if a third element is provided, enable shortcut even when no field selected

View file

@ -3,15 +3,19 @@
from __future__ import annotations from __future__ import annotations
import os
import re import re
import sys
import time import time
from typing import TYPE_CHECKING, Optional, TextIO, cast import traceback
from typing import TYPE_CHECKING, TextIO, cast
from markdown import markdown from markdown import markdown
import aqt import aqt
from anki.collection import HelpPage from anki.collection import HelpPage
from anki.errors import BackendError, Interrupted from anki.errors import BackendError, Interrupted
from anki.utils import is_win
from aqt.addons import AddonManager, AddonMeta from aqt.addons import AddonManager, AddonMeta
from aqt.qt import * from aqt.qt import *
from aqt.utils import openHelp, showWarning, supportText, tooltip, tr from aqt.utils import openHelp, showWarning, supportText, tooltip, tr
@ -169,7 +173,7 @@ class ErrorHandler(QObject):
def __init__(self, mw: AnkiQt) -> None: def __init__(self, mw: AnkiQt) -> None:
QObject.__init__(self, mw) QObject.__init__(self, mw)
self.mw = mw self.mw = mw
self.timer: Optional[QTimer] = None self.timer: QTimer | None = None
qconnect(self.errorTimer, self._setTimer) qconnect(self.errorTimer, self._setTimer)
self.pool = "" self.pool = ""
self._oldstderr = sys.stderr self._oldstderr = sys.stderr

View file

@ -7,7 +7,6 @@ import os
import re import re
import time import time
from concurrent.futures import Future from concurrent.futures import Future
from typing import Optional
import aqt import aqt
import aqt.forms import aqt.forms
@ -35,7 +34,7 @@ class ExportDialog(QDialog):
mw: aqt.main.AnkiQt, mw: aqt.main.AnkiQt,
did: DeckId | None = None, did: DeckId | None = None,
cids: list[CardId] | None = None, cids: list[CardId] | None = None,
parent: Optional[QWidget] = None, parent: QWidget | None = None,
): ):
QDialog.__init__(self, parent or mw, Qt.WindowType.Window) QDialog.__init__(self, parent or mw, Qt.WindowType.Window)
self.mw = mw self.mw = mw

View file

@ -3,8 +3,6 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional
import aqt import aqt
import aqt.forms import aqt.forms
import aqt.operations import aqt.operations
@ -32,7 +30,7 @@ class FieldDialog(QDialog):
self, self,
mw: AnkiQt, mw: AnkiQt,
nt: NotetypeDict, nt: NotetypeDict,
parent: Optional[QWidget] = None, parent: QWidget | None = None,
open_at: int = 0, open_at: int = 0,
) -> None: ) -> None:
QDialog.__init__(self, parent or mw) QDialog.__init__(self, parent or mw)
@ -62,7 +60,7 @@ class FieldDialog(QDialog):
self.form.buttonBox.button(QDialogButtonBox.StandardButton.Save).setAutoDefault( self.form.buttonBox.button(QDialogButtonBox.StandardButton.Save).setAutoDefault(
False False
) )
self.currentIdx: Optional[int] = None self.currentIdx: int | None = None
self.fillFields() self.fillFields()
self.setupSignals() self.setupSignals()
self.form.fieldList.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) self.form.fieldList.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
@ -125,8 +123,8 @@ class FieldDialog(QDialog):
self.loadField(idx) self.loadField(idx)
def _uniqueName( def _uniqueName(
self, prompt: str, ignoreOrd: Optional[int] = None, old: str = "" self, prompt: str, ignoreOrd: int | None = None, old: str = ""
) -> Optional[str]: ) -> str | None:
txt = getOnlyText(prompt, default=old).replace('"', "").strip() txt = getOnlyText(prompt, default=old).replace('"', "").strip()
if not txt: if not txt:
return None return None

View file

@ -44,6 +44,9 @@
<property name="text"> <property name="text">
<string>preferences_language</string> <string>preferences_language</string>
</property> </property>
<property name="buddy">
<cstring>lang</cstring>
</property>
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item row="0" column="1">
@ -64,6 +67,9 @@
<property name="text"> <property name="text">
<string>preferences_video_driver</string> <string>preferences_video_driver</string>
</property> </property>
<property name="buddy">
<cstring>video_driver</cstring>
</property>
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="1" column="1">
@ -76,6 +82,19 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QCheckBox" name="check_for_updates">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>preferences_check_for_updates</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@ -116,6 +135,9 @@
<property name="text"> <property name="text">
<string>preferences_style</string> <string>preferences_style</string>
</property> </property>
<property name="buddy">
<cstring>styleComboBox</cstring>
</property>
</widget> </widget>
</item> </item>
<item row="0" column="0"> <item row="0" column="0">
@ -123,6 +145,9 @@
<property name="text"> <property name="text">
<string>preferences_theme</string> <string>preferences_theme</string>
</property> </property>
<property name="buddy">
<cstring>theme</cstring>
</property>
</widget> </widget>
</item> </item>
<item row="2" column="0"> <item row="2" column="0">
@ -130,6 +155,9 @@
<property name="text"> <property name="text">
<string>preferences_user_interface_size</string> <string>preferences_user_interface_size</string>
</property> </property>
<property name="buddy">
<cstring>uiScale</cstring>
</property>
</widget> </widget>
</item> </item>
<item row="3" column="0" colspan="2"> <item row="3" column="0" colspan="2">
@ -264,33 +292,25 @@
<widget class="QSpinBox" name="lrnCutoff"> <widget class="QSpinBox" name="lrnCutoff">
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>60</width> <width>16777215</width>
<height>16777215</height> <height>16777215</height>
</size> </size>
</property> </property>
<property name="suffix">
<string>preferences_mins</string>
</property>
<property name="maximum"> <property name="maximum">
<number>999</number> <number>999</number>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="2">
<widget class="QLabel" name="label_29">
<property name="text">
<string>preferences_mins</string>
</property>
</widget>
</item>
<item row="2" column="0"> <item row="2" column="0">
<widget class="QLabel" name="label_30"> <widget class="QLabel" name="label_30">
<property name="text"> <property name="text">
<string>preferences_timebox_time_limit</string> <string>preferences_timebox_time_limit</string>
</property> </property>
</widget> <property name="buddy">
</item> <cstring>timeLimit</cstring>
<item row="0" column="2">
<widget class="QLabel" name="label_40">
<property name="text">
<string>preferences_hours_past_midnight</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -298,10 +318,13 @@
<widget class="QSpinBox" name="dayOffset"> <widget class="QSpinBox" name="dayOffset">
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>60</width> <width>16777215</width>
<height>16777215</height> <height>16777215</height>
</size> </size>
</property> </property>
<property name="suffix">
<string>preferences_hours_past_midnight</string>
</property>
<property name="maximum"> <property name="maximum">
<number>23</number> <number>23</number>
</property> </property>
@ -312,6 +335,9 @@
<property name="text"> <property name="text">
<string>preferences_learn_ahead_limit</string> <string>preferences_learn_ahead_limit</string>
</property> </property>
<property name="buddy">
<cstring>lrnCutoff</cstring>
</property>
</widget> </widget>
</item> </item>
<item row="0" column="0"> <item row="0" column="0">
@ -319,17 +345,16 @@
<property name="text"> <property name="text">
<string>preferences_next_day_starts_at</string> <string>preferences_next_day_starts_at</string>
</property> </property>
</widget> <property name="buddy">
</item> <cstring>dayOffset</cstring>
<item row="2" column="2">
<widget class="QLabel" name="label_39">
<property name="text">
<string>preferences_mins</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="2" column="1">
<widget class="QSpinBox" name="timeLimit"> <widget class="QSpinBox" name="timeLimit">
<property name="suffix">
<string>preferences_mins</string>
</property>
<property name="maximum"> <property name="maximum">
<number>9999</number> <number>9999</number>
</property> </property>
@ -513,6 +538,9 @@
<property name="text"> <property name="text">
<string>preferences_default_deck</string> <string>preferences_default_deck</string>
</property> </property>
<property name="buddy">
<cstring>useCurrent</cstring>
</property>
</widget> </widget>
</item> </item>
<item> <item>
@ -553,6 +581,9 @@
<property name="text"> <property name="text">
<string>preferences_default_search_text</string> <string>preferences_default_search_text</string>
</property> </property>
<property name="buddy">
<cstring>default_search_text</cstring>
</property>
</widget> </widget>
</item> </item>
<item> <item>
@ -705,10 +736,16 @@
<property name="text"> <property name="text">
<string>preferences_network_timeout</string> <string>preferences_network_timeout</string>
</property> </property>
<property name="buddy">
<cstring>network_timeout</cstring>
</property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QSpinBox" name="network_timeout"> <widget class="QSpinBox" name="network_timeout">
<property name="suffix">
<string>scheduling_seconds</string>
</property>
<property name="minimum"> <property name="minimum">
<number>30</number> <number>30</number>
</property> </property>
@ -717,26 +754,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QLabel" name="label_8">
<property name="text">
<string>scheduling_seconds</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout> </layout>
</item> </item>
<item> <item>
@ -853,6 +870,9 @@
<property name="text"> <property name="text">
<string>preferences_custom_sync_url</string> <string>preferences_custom_sync_url</string>
</property> </property>
<property name="buddy">
<cstring>custom_sync_url</cstring>
</property>
</widget> </widget>
</item> </item>
</layout> </layout>
@ -954,6 +974,9 @@
<property name="text"> <property name="text">
<string>preferences_daily_backups</string> <string>preferences_daily_backups</string>
</property> </property>
<property name="buddy">
<cstring>daily_backups</cstring>
</property>
</widget> </widget>
</item> </item>
<item row="4" column="2"> <item row="4" column="2">
@ -968,6 +991,9 @@
<property name="text"> <property name="text">
<string>preferences_monthly_backups</string> <string>preferences_monthly_backups</string>
</property> </property>
<property name="buddy">
<cstring>monthly_backups</cstring>
</property>
</widget> </widget>
</item> </item>
<item row="4" column="0"> <item row="4" column="0">
@ -975,6 +1001,9 @@
<property name="text"> <property name="text">
<string>preferences_weekly_backups</string> <string>preferences_weekly_backups</string>
</property> </property>
<property name="buddy">
<cstring>weekly_backups</cstring>
</property>
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="1" column="0">
@ -982,6 +1011,9 @@
<property name="text"> <property name="text">
<string>preferences_minutes_between_backups</string> <string>preferences_minutes_between_backups</string>
</property> </property>
<property name="buddy">
<cstring>minutes_between_backups</cstring>
</property>
</widget> </widget>
</item> </item>
<item row="2" column="2"> <item row="2" column="2">

View file

@ -7,8 +7,8 @@ import os
import re import re
import time import time
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Sequence
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional, Sequence, Type
import aqt.forms import aqt.forms
import aqt.main import aqt.main
@ -42,7 +42,7 @@ class ExportDialog(QDialog):
mw: aqt.main.AnkiQt, mw: aqt.main.AnkiQt,
did: DeckId | None = None, did: DeckId | None = None,
nids: Sequence[NoteId] | None = None, nids: Sequence[NoteId] | None = None,
parent: Optional[QWidget] = None, parent: QWidget | None = None,
): ):
QDialog.__init__(self, parent or mw, Qt.WindowType.Window) QDialog.__init__(self, parent or mw, Qt.WindowType.Window)
self.mw = mw self.mw = mw
@ -56,7 +56,7 @@ class ExportDialog(QDialog):
self.open() self.open()
def setup(self, did: DeckId | None) -> None: def setup(self, did: DeckId | None) -> None:
self.exporter_classes: list[Type[Exporter]] = [ self.exporter_classes: list[type[Exporter]] = [
ApkgExporter, ApkgExporter,
ColpkgExporter, ColpkgExporter,
NoteCsvExporter, NoteCsvExporter,

View file

@ -3,10 +3,11 @@
from __future__ import annotations from __future__ import annotations
import os
import re import re
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Callable
from itertools import chain from itertools import chain
from typing import Type
import aqt.main import aqt.main
from anki.collection import Collection, Progress from anki.collection import Collection, Progress
@ -124,7 +125,7 @@ class JsonImporter(Importer):
ImportDialog(mw, JsonFileArgs(path=path)) ImportDialog(mw, JsonFileArgs(path=path))
IMPORTERS: list[Type[Importer]] = [ IMPORTERS: list[type[Importer]] = [
ColpkgImporter, ColpkgImporter,
ApkgImporter, ApkgImporter,
MnemosyneImporter, MnemosyneImporter,

View file

@ -1,11 +1,15 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
import os import os
import re import re
import sys
import traceback import traceback
import zipfile import zipfile
from collections.abc import Callable
from concurrent.futures import Future from concurrent.futures import Future
from typing import Any, Optional from typing import Any
import anki.importing as importing import anki.importing as importing
import aqt.deckchooser import aqt.deckchooser
@ -54,7 +58,7 @@ class ChangeMap(QDialog):
self.frm.fields.setCurrentRow(n) self.frm.fields.setCurrentRow(n)
else: else:
self.frm.fields.setCurrentRow(n + 1) self.frm.fields.setCurrentRow(n + 1)
self.field: Optional[str] = None self.field: str | None = None
def getField(self) -> str: def getField(self) -> str:
self.exec() self.exec()
@ -230,13 +234,13 @@ class ImportDialog(QDialog):
self.frm.mappingArea.setWidget(self.frame) self.frm.mappingArea.setWidget(self.frame)
self.mapbox = QVBoxLayout(self.frame) self.mapbox = QVBoxLayout(self.frame)
self.mapbox.setContentsMargins(0, 0, 0, 0) self.mapbox.setContentsMargins(0, 0, 0, 0)
self.mapwidget: Optional[QWidget] = None self.mapwidget: QWidget | None = None
def hideMapping(self) -> None: def hideMapping(self) -> None:
self.frm.mappingGroup.hide() self.frm.mappingGroup.hide()
def showMapping( def showMapping(
self, keepMapping: bool = False, hook: Optional[Callable] = None self, keepMapping: bool = False, hook: Callable | None = None
) -> None: ) -> None:
if hook: if hook:
hook() hook()

View file

@ -8,10 +8,13 @@ import gc
import os import os
import re import re
import signal import signal
import sys
import traceback
import weakref import weakref
from argparse import Namespace from argparse import Namespace
from collections.abc import Callable, Sequence
from concurrent.futures import Future from concurrent.futures import Future
from typing import Any, Literal, Sequence, TypeVar, cast from typing import Any, Literal, TypeVar, cast
import anki import anki
import anki.cards import anki.cards
@ -199,7 +202,7 @@ class AnkiQt(QMainWindow):
self.setupUI() self.setupUI()
self.setupAddons(args) self.setupAddons(args)
self.finish_ui_setup() self.finish_ui_setup()
except: except Exception:
showInfo(tr.qt_misc_error_during_startup(val=traceback.format_exc())) showInfo(tr.qt_misc_error_during_startup(val=traceback.format_exc()))
sys.exit(1) sys.exit(1)
# must call this after ui set up # must call this after ui set up
@ -350,7 +353,7 @@ class AnkiQt(QMainWindow):
f.profiles.addItems(profs) f.profiles.addItems(profs)
try: try:
idx = profs.index(self.pm.name) idx = profs.index(self.pm.name)
except: except Exception:
idx = 0 idx = 0
f.profiles.setCurrentRow(idx) f.profiles.setCurrentRow(idx)
@ -680,7 +683,7 @@ class AnkiQt(QMainWindow):
self.maybeOptimize() self.maybeOptimize()
if not dev_mode: if not dev_mode:
corrupt = self.col.db.scalar("pragma quick_check") != "ok" corrupt = self.col.db.scalar("pragma quick_check") != "ok"
except: except Exception:
corrupt = True corrupt = True
try: try:
@ -692,7 +695,7 @@ class AnkiQt(QMainWindow):
force=False, force=False,
wait_for_completion=False, wait_for_completion=False,
) )
except: except Exception:
print("backup on close failed") print("backup on close failed")
self.col.close(downgrade=False) self.col.close(downgrade=False)
except Exception as e: except Exception as e:
@ -1428,6 +1431,7 @@ title="{}" {}>{}</button>""".format(
def setup_auto_update(self, _log: list[DownloadLogEntry]) -> None: def setup_auto_update(self, _log: list[DownloadLogEntry]) -> None:
from aqt.update import check_for_update from aqt.update import check_for_update
if aqt.mw.pm.check_for_updates():
check_for_update() check_for_update()
# Timers # Timers

View file

@ -5,8 +5,9 @@ from __future__ import annotations
import itertools import itertools
import time import time
from collections.abc import Iterable, Sequence
from concurrent.futures import Future from concurrent.futures import Future
from typing import Iterable, Sequence, TypeVar from typing import TypeVar
import aqt import aqt
import aqt.progress import aqt.progress

View file

@ -11,10 +11,10 @@ import re
import sys import sys
import threading import threading
import traceback import traceback
from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from errno import EPROTOTYPE from errno import EPROTOTYPE
from http import HTTPStatus from http import HTTPStatus
from typing import Callable
import flask import flask
import flask_cors import flask_cors

View file

@ -4,9 +4,10 @@
from __future__ import annotations from __future__ import annotations
import time import time
from collections.abc import Callable
from concurrent.futures import Future from concurrent.futures import Future
from datetime import datetime from datetime import datetime
from typing import Any, Callable from typing import Any
import aqt import aqt
import aqt.forms import aqt.forms

View file

@ -1,7 +1,8 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
from typing import Optional from collections.abc import Callable
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.qt import * from aqt.qt import *
@ -16,7 +17,7 @@ class ModelChooser(QHBoxLayout):
mw: AnkiQt, mw: AnkiQt,
widget: QWidget, widget: QWidget,
label: bool = True, label: bool = True,
on_activated: Optional[Callable[[], None]] = None, on_activated: Callable[[], None] | None = None,
) -> None: ) -> None:
"""If provided, on_activated() will be called when the button is clicked, """If provided, on_activated() will be called when the button is clicked,
and the caller can call .onModelChange() to pull up the dialog when they and the caller can call .onModelChange() to pull up the dialog when they

View file

@ -3,9 +3,10 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable, Sequence
from concurrent.futures import Future from concurrent.futures import Future
from operator import itemgetter from operator import itemgetter
from typing import Any, Optional, Sequence from typing import Any
import aqt.clayout import aqt.clayout
from anki import stdmodels from anki import stdmodels
@ -40,9 +41,9 @@ class Models(QDialog):
def __init__( def __init__(
self, self,
mw: AnkiQt, mw: AnkiQt,
parent: Optional[QWidget] = None, parent: QWidget | None = None,
fromMain: bool = False, fromMain: bool = False,
selected_notetype_id: Optional[NotetypeId] = None, selected_notetype_id: NotetypeId | None = None,
): ):
self.mw = mw self.mw = mw
parent = parent or mw parent = parent or mw
@ -61,6 +62,13 @@ class Models(QDialog):
self.models: Sequence[NotetypeNameIdUseCount] = [] self.models: Sequence[NotetypeNameIdUseCount] = []
self.setupModels() self.setupModels()
restoreGeom(self, "models") restoreGeom(self, "models")
self.setWindowFlags(
self.windowFlags()
| Qt.WindowType.WindowMaximizeButtonHint
| Qt.WindowType.WindowMinimizeButtonHint
)
self.show() self.show()
# Models # Models
@ -231,13 +239,13 @@ class Models(QDialog):
class AddModel(QDialog): class AddModel(QDialog):
model: Optional[NotetypeDict] model: NotetypeDict | None
def __init__( def __init__(
self, self,
mw: AnkiQt, mw: AnkiQt,
on_success: Callable[[NotetypeDict], None], on_success: Callable[[NotetypeDict], None],
parent: Optional[QWidget] = None, parent: QWidget | None = None,
) -> None: ) -> None:
self.parent_ = parent or mw self.parent_ = parent or mw
self.mw = mw self.mw = mw
@ -249,9 +257,7 @@ class AddModel(QDialog):
self.setWindowModality(Qt.WindowModality.ApplicationModal) self.setWindowModality(Qt.WindowModality.ApplicationModal)
disable_help_button(self) disable_help_button(self)
# standard models # standard models
self.notetypes: list[ self.notetypes: list[NotetypeDict | Callable[[Collection], NotetypeDict]] = []
Union[NotetypeDict, Callable[[Collection], NotetypeDict]]
] = []
for name, func in stdmodels.get_stock_notetypes(self.col): for name, func in stdmodels.get_stock_notetypes(self.col):
item = QListWidgetItem(tr.notetypes_add(val=name)) item = QListWidgetItem(tr.notetypes_add(val=name))
self.dialog.models.addItem(item) self.dialog.models.addItem(item)

View file

@ -25,6 +25,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# pylint: disable=raise-missing-from # pylint: disable=raise-missing-from
from __future__ import annotations
import inspect import inspect
import json import json
@ -38,7 +39,6 @@ import threading
import time import time
from queue import Empty, Full, Queue from queue import Empty, Full, Queue
from shutil import which from shutil import which
from typing import Optional
from anki.utils import is_win from anki.utils import is_win
@ -77,7 +77,7 @@ class MPVBase:
""" """
executable = which("mpv") executable = which("mpv")
popenEnv: Optional[dict[str, str]] = None popenEnv: dict[str, str] | None = None
default_argv = [ default_argv = [
"--idle", "--idle",

View file

@ -3,6 +3,8 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from anki.collection import OpChanges from anki.collection import OpChanges
from anki.models import NotetypeId from anki.models import NotetypeId
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks

View file

@ -3,8 +3,9 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from concurrent.futures._base import Future from concurrent.futures._base import Future
from typing import Any, Callable, Generic, Protocol, TypeVar, Union from typing import Any, Generic, Protocol, TypeVar, Union
import aqt import aqt
import aqt.gui_hooks import aqt.gui_hooks

View file

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Sequence from collections.abc import Sequence
from anki.cards import CardId from anki.cards import CardId
from anki.collection import OpChangesWithCount from anki.collection import OpChangesWithCount

Some files were not shown because too many files have changed in this diff Show more