mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 16:56:36 -04:00
Merge branch 'main' into editor-3830
This commit is contained in:
commit
939d833e12
187 changed files with 1620 additions and 811 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -19,4 +19,4 @@ yarn-error.log
|
|||
ts/.svelte-kit
|
||||
.yarn
|
||||
.claude/settings.local.json
|
||||
CLAUDE.local.md
|
||||
.claude/user.md
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
[settings]
|
||||
py_version=39
|
||||
known_first_party=anki,aqt,tests
|
||||
profile=black
|
48
.pylintrc
48
.pylintrc
|
@ -1,48 +0,0 @@
|
|||
[MASTER]
|
||||
ignore-patterns=.*_pb2.*
|
||||
persistent = no
|
||||
extension-pkg-whitelist=orjson,PyQt6
|
||||
init-hook="import sys; sys.path.extend(['pylib/anki/_vendor', 'out/qt'])"
|
||||
|
||||
[REPORTS]
|
||||
output-format=colorized
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
disable=
|
||||
R,
|
||||
line-too-long,
|
||||
too-many-lines,
|
||||
missing-function-docstring,
|
||||
missing-module-docstring,
|
||||
missing-class-docstring,
|
||||
import-outside-toplevel,
|
||||
wrong-import-position,
|
||||
wrong-import-order,
|
||||
fixme,
|
||||
unused-wildcard-import,
|
||||
attribute-defined-outside-init,
|
||||
redefined-builtin,
|
||||
wildcard-import,
|
||||
broad-except,
|
||||
bare-except,
|
||||
unused-argument,
|
||||
unused-variable,
|
||||
redefined-outer-name,
|
||||
global-statement,
|
||||
protected-access,
|
||||
arguments-differ,
|
||||
arguments-renamed,
|
||||
consider-using-f-string,
|
||||
invalid-name,
|
||||
broad-exception-raised
|
||||
|
||||
[BASIC]
|
||||
good-names =
|
||||
id,
|
||||
tr,
|
||||
db,
|
||||
ok,
|
||||
ip,
|
||||
|
||||
[IMPORTS]
|
||||
ignored-modules = anki.*_pb2, anki.sync_pb2, win32file,pywintypes,socket,win32pipe,pyaudio,anki.scheduler_pb2,anki.notetypes_pb2
|
93
.ruff.toml
93
.ruff.toml
|
@ -1,2 +1,91 @@
|
|||
target-version = "py39"
|
||||
extend-exclude = []
|
||||
lint.select = [
|
||||
"E", # pycodestyle errors
|
||||
"F", # Pyflakes errors
|
||||
"PL", # Pylint rules
|
||||
"I", # Isort rules
|
||||
"ARG",
|
||||
# "UP", # pyupgrade
|
||||
# "B", # flake8-bugbear
|
||||
# "SIM", # flake8-simplify
|
||||
]
|
||||
|
||||
extend-exclude = ["*_pb2.py", "*_pb2.pyi"]
|
||||
|
||||
lint.ignore = [
|
||||
# Docstring rules (missing-*-docstring in pylint)
|
||||
"D100", # Missing docstring in public module
|
||||
"D101", # Missing docstring in public class
|
||||
"D103", # Missing docstring in public function
|
||||
|
||||
# Import rules (wrong-import-* in pylint)
|
||||
"E402", # Module level import not at top of file
|
||||
"E501", # Line too long
|
||||
|
||||
# pycodestyle rules
|
||||
"E741", # ambiguous-variable-name
|
||||
|
||||
# Comment rules (fixme in pylint)
|
||||
"FIX002", # Line contains TODO
|
||||
|
||||
# Pyflakes rules
|
||||
"F402", # import-shadowed-by-loop-var
|
||||
"F403", # undefined-local-with-import-star
|
||||
"F405", # undefined-local-with-import-star-usage
|
||||
|
||||
# Naming rules (invalid-name in pylint)
|
||||
"N801", # Class name should use CapWords convention
|
||||
"N802", # Function name should be lowercase
|
||||
"N803", # Argument name should be lowercase
|
||||
"N806", # Variable in function should be lowercase
|
||||
"N811", # Constant imported as non-constant
|
||||
"N812", # Lowercase imported as non-lowercase
|
||||
"N813", # Camelcase imported as lowercase
|
||||
"N814", # Camelcase imported as constant
|
||||
"N815", # Variable in class scope should not be mixedCase
|
||||
"N816", # Variable in global scope should not be mixedCase
|
||||
"N817", # CamelCase imported as acronym
|
||||
"N818", # Error suffix in exception names
|
||||
|
||||
# Pylint rules
|
||||
"PLW0603", # global-statement
|
||||
"PLW2901", # redefined-loop-name
|
||||
"PLC0415", # import-outside-top-level
|
||||
"PLR2004", # magic-value-comparison
|
||||
|
||||
# Exception handling (broad-except, bare-except in pylint)
|
||||
"BLE001", # Do not catch blind exception
|
||||
|
||||
# Argument rules (unused-argument in pylint)
|
||||
"ARG001", # Unused function argument
|
||||
"ARG002", # Unused method argument
|
||||
"ARG005", # Unused lambda argument
|
||||
|
||||
# Access rules (protected-access in pylint)
|
||||
"SLF001", # Private member accessed
|
||||
|
||||
# String formatting (consider-using-f-string in pylint)
|
||||
"UP032", # Use f-string instead of format call
|
||||
|
||||
# Exception rules (broad-exception-raised in pylint)
|
||||
"TRY301", # Abstract raise to an inner function
|
||||
|
||||
# Builtin shadowing (redefined-builtin in pylint)
|
||||
"A001", # Variable shadows a Python builtin
|
||||
"A002", # Argument shadows a Python builtin
|
||||
"A003", # Class attribute shadows a Python builtin
|
||||
]
|
||||
|
||||
[lint.per-file-ignores]
|
||||
"**/anki/*_pb2.py" = ["ALL"]
|
||||
|
||||
[lint.pep8-naming]
|
||||
ignore-names = ["id", "tr", "db", "ok", "ip"]
|
||||
|
||||
[lint.pylint]
|
||||
max-args = 12
|
||||
max-returns = 10
|
||||
max-branches = 35
|
||||
max-statements = 125
|
||||
|
||||
[lint.isort]
|
||||
known-first-party = ["anki", "aqt", "tests"]
|
||||
|
|
2
.version
2
.version
|
@ -1 +1 @@
|
|||
25.06b6
|
||||
25.06b7
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"recommendations": [
|
||||
"dprint.dprint",
|
||||
"ms-python.python",
|
||||
"ms-python.black-formatter",
|
||||
"charliermarsh.ruff",
|
||||
"rust-lang.rust-analyzer",
|
||||
"svelte.svelte-vscode",
|
||||
"zxh404.vscode-proto3",
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
"out/qt",
|
||||
"qt"
|
||||
],
|
||||
"python.formatting.provider": "black",
|
||||
"python.formatting.provider": "charliermarsh.ruff",
|
||||
"python.linting.mypyEnabled": false,
|
||||
"python.analysis.diagnosticSeverityOverrides": {
|
||||
"reportMissingModuleSource": "none"
|
||||
|
|
|
@ -80,3 +80,7 @@ when possible.
|
|||
|
||||
in rslib, use error/mod.rs's AnkiError/Result and snafu. In our other Rust modules, prefer anyhow + additional context where appropriate. Unwrapping
|
||||
in build scripts/tests is fine.
|
||||
|
||||
## Individual preferences
|
||||
|
||||
See @.claude/user.md
|
||||
|
|
9
Cargo.lock
generated
9
Cargo.lock
generated
|
@ -3548,7 +3548,8 @@ dependencies = [
|
|||
"embed-resource",
|
||||
"libc",
|
||||
"libc-stdhandle",
|
||||
"winapi",
|
||||
"widestring",
|
||||
"windows 0.61.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -7375,6 +7376,12 @@ dependencies = [
|
|||
"winsafe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "widestring"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
|
|
|
@ -138,8 +138,9 @@ unic-ucd-category = "0.9.0"
|
|||
unicode-normalization = "0.1.24"
|
||||
walkdir = "2.5.0"
|
||||
which = "8.0.0"
|
||||
winapi = { version = "0.3", features = ["wincon"] }
|
||||
windows = { version = "0.61.3", features = ["Media_SpeechSynthesis", "Media_Core", "Foundation_Collections", "Storage_Streams"] }
|
||||
widestring = "1.1.0"
|
||||
winapi = { version = "0.3", features = ["wincon", "winreg"] }
|
||||
windows = { version = "0.61.3", features = ["Media_SpeechSynthesis", "Media_Core", "Foundation_Collections", "Storage_Streams", "Win32_System_Console", "Win32_System_Registry", "Win32_Foundation", "Win32_UI_Shell"] }
|
||||
wiremock = "0.6.3"
|
||||
xz2 = "0.1.7"
|
||||
zip = { version = "4.1.0", default-features = false, features = ["deflate", "time"] }
|
||||
|
|
|
@ -7,17 +7,14 @@ use anyhow::Result;
|
|||
use ninja_gen::action::BuildAction;
|
||||
use ninja_gen::archives::Platform;
|
||||
use ninja_gen::build::FilesHandle;
|
||||
use ninja_gen::command::RunCommand;
|
||||
use ninja_gen::copy::CopyFiles;
|
||||
use ninja_gen::glob;
|
||||
use ninja_gen::hashmap;
|
||||
use ninja_gen::input::BuildInput;
|
||||
use ninja_gen::inputs;
|
||||
use ninja_gen::python::python_format;
|
||||
use ninja_gen::python::PythonEnvironment;
|
||||
use ninja_gen::python::PythonLint;
|
||||
use ninja_gen::python::PythonTypecheck;
|
||||
use ninja_gen::rsync::RsyncFiles;
|
||||
use ninja_gen::python::RuffCheck;
|
||||
use ninja_gen::Build;
|
||||
|
||||
/// Normalize version string by removing leading zeros from numeric parts
|
||||
|
@ -51,7 +48,7 @@ fn normalize_version(version: &str) -> String {
|
|||
part.to_string()
|
||||
} else {
|
||||
let normalized_prefix = numeric_prefix.parse::<u32>().unwrap_or(0).to_string();
|
||||
format!("{}{}", normalized_prefix, rest)
|
||||
format!("{normalized_prefix}{rest}")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -60,14 +57,7 @@ fn normalize_version(version: &str) -> String {
|
|||
}
|
||||
|
||||
pub fn setup_venv(build: &mut Build) -> Result<()> {
|
||||
let extra_binary_exports = &[
|
||||
"mypy",
|
||||
"black",
|
||||
"isort",
|
||||
"pylint",
|
||||
"pytest",
|
||||
"protoc-gen-mypy",
|
||||
];
|
||||
let extra_binary_exports = &["mypy", "ruff", "pytest", "protoc-gen-mypy"];
|
||||
build.add_action(
|
||||
"pyenv",
|
||||
PythonEnvironment {
|
||||
|
@ -200,60 +190,26 @@ pub fn check_python(build: &mut Build) -> Result<()> {
|
|||
},
|
||||
)?;
|
||||
|
||||
add_pylint(build)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_pylint(build: &mut Build) -> Result<()> {
|
||||
// pylint does not support PEP420 implicit namespaces split across import paths,
|
||||
// so we need to merge our pylib sources and generated files before invoking it,
|
||||
// and add a top-level __init__.py
|
||||
let ruff_folders = &["qt/aqt", "ftl", "pylib/tools", "tools", "python"];
|
||||
let ruff_deps = inputs![
|
||||
glob!["{pylib,ftl,qt,python,tools}/**/*.py"],
|
||||
":pylib:anki",
|
||||
":qt:aqt"
|
||||
];
|
||||
build.add_action(
|
||||
"check:pylint:copy_pylib",
|
||||
RsyncFiles {
|
||||
inputs: inputs![":pylib:anki"],
|
||||
target_folder: "pylint/anki",
|
||||
strip_prefix: "$builddir/pylib/anki",
|
||||
// avoid copying our large rsbridge binary
|
||||
extra_args: "--links",
|
||||
"check:ruff",
|
||||
RuffCheck {
|
||||
folders: ruff_folders,
|
||||
deps: ruff_deps.clone(),
|
||||
check_only: true,
|
||||
},
|
||||
)?;
|
||||
build.add_action(
|
||||
"check:pylint:copy_pylib",
|
||||
RsyncFiles {
|
||||
inputs: inputs![glob!["pylib/anki/**"]],
|
||||
target_folder: "pylint/anki",
|
||||
strip_prefix: "pylib/anki",
|
||||
extra_args: "",
|
||||
},
|
||||
)?;
|
||||
build.add_action(
|
||||
"check:pylint:copy_pylib",
|
||||
RunCommand {
|
||||
command: ":pyenv:bin",
|
||||
args: "$script $out",
|
||||
inputs: hashmap! { "script" => inputs!["python/mkempty.py"] },
|
||||
outputs: hashmap! { "out" => vec!["pylint/anki/__init__.py"] },
|
||||
},
|
||||
)?;
|
||||
build.add_action(
|
||||
"check:pylint",
|
||||
PythonLint {
|
||||
folders: &[
|
||||
"$builddir/pylint/anki",
|
||||
"qt/aqt",
|
||||
"ftl",
|
||||
"pylib/tools",
|
||||
"tools",
|
||||
"python",
|
||||
],
|
||||
pylint_ini: inputs![".pylintrc"],
|
||||
deps: inputs![
|
||||
":check:pylint:copy_pylib",
|
||||
":qt:aqt",
|
||||
glob!("{pylib/tools,ftl,qt,python,tools}/**/*.py")
|
||||
],
|
||||
"fix:ruff",
|
||||
RuffCheck {
|
||||
folders: ruff_folders,
|
||||
deps: ruff_deps,
|
||||
check_only: false,
|
||||
},
|
||||
)?;
|
||||
|
||||
|
|
|
@ -169,7 +169,7 @@ fn build_rsbridge(build: &mut Build) -> Result<()> {
|
|||
|
||||
pub fn check_rust(build: &mut Build) -> Result<()> {
|
||||
let inputs = inputs![
|
||||
glob!("{rslib/**,pylib/rsbridge/**,ftl/**,build/**,tools/workspace-hack/**}"),
|
||||
glob!("{rslib/**,pylib/rsbridge/**,ftl/**,build/**,qt/launcher/**}"),
|
||||
"Cargo.lock",
|
||||
"Cargo.toml",
|
||||
"rust-toolchain.toml",
|
||||
|
|
|
@ -72,12 +72,11 @@ fn fetch_protoc_release_info() -> Result<String, Box<dyn Error>> {
|
|||
"MacArm" => continue, // Skip MacArm since it's handled with MacX64
|
||||
"WindowsX64" => "Platform::WindowsX64 | Platform::WindowsArm",
|
||||
"WindowsArm" => continue, // Skip WindowsArm since it's handled with WindowsX64
|
||||
_ => &format!("Platform::{}", platform),
|
||||
_ => &format!("Platform::{platform}"),
|
||||
};
|
||||
|
||||
match_blocks.push(format!(
|
||||
" {} => {{\n OnlineArchive {{\n url: \"{}\",\n sha256: \"{}\",\n }}\n }}",
|
||||
match_pattern, download_url, sha256
|
||||
" {match_pattern} => {{\n OnlineArchive {{\n url: \"{download_url}\",\n sha256: \"{sha256}\",\n }}\n }}"
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ fn fetch_uv_release_info() -> Result<String, Box<dyn Error>> {
|
|||
// Find the corresponding .sha256 or .sha256sum asset
|
||||
let sha_asset = assets.iter().find(|a| {
|
||||
let name = a["name"].as_str().unwrap_or("");
|
||||
name == format!("{}.sha256", asset_name) || name == format!("{}.sha256sum", asset_name)
|
||||
name == format!("{asset_name}.sha256") || name == format!("{asset_name}.sha256sum")
|
||||
});
|
||||
if sha_asset.is_none() {
|
||||
eprintln!("No sha256 asset found for {asset_name}");
|
||||
|
@ -71,8 +71,7 @@ fn fetch_uv_release_info() -> Result<String, Box<dyn Error>> {
|
|||
let sha256 = sha_text.split_whitespace().next().unwrap_or("");
|
||||
|
||||
match_blocks.push(format!(
|
||||
" Platform::{} => {{\n OnlineArchive {{\n url: \"{}\",\n sha256: \"{}\",\n }}\n }}",
|
||||
platform, download_url, sha256
|
||||
" Platform::{platform} => {{\n OnlineArchive {{\n url: \"{download_url}\",\n sha256: \"{sha256}\",\n }}\n }}"
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -135,10 +134,7 @@ mod tests {
|
|||
assert_eq!(
|
||||
updated_lines,
|
||||
original_lines - EXPECTED_LINES_REMOVED,
|
||||
"Expected line count to decrease by exactly {} lines (original: {}, updated: {})",
|
||||
EXPECTED_LINES_REMOVED,
|
||||
original_lines,
|
||||
updated_lines
|
||||
"Expected line count to decrease by exactly {EXPECTED_LINES_REMOVED} lines (original: {original_lines}, updated: {updated_lines})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -300,7 +300,7 @@ impl BuildStatement<'_> {
|
|||
|
||||
writeln!(buf, "build {outputs_str}: {action_name} {inputs_str}").unwrap();
|
||||
for (key, value) in self.variables.iter().sorted() {
|
||||
writeln!(buf, " {key} = {}", value).unwrap();
|
||||
writeln!(buf, " {key} = {value}").unwrap();
|
||||
}
|
||||
writeln!(buf).unwrap();
|
||||
|
||||
|
@ -476,7 +476,7 @@ impl FilesHandle for BuildStatement<'_> {
|
|||
let outputs = outputs.into_iter().map(|v| {
|
||||
let v = v.as_ref();
|
||||
let v = if !v.starts_with("$builddir/") && !v.starts_with("$builddir\\") {
|
||||
format!("$builddir/{}", v)
|
||||
format!("$builddir/{v}")
|
||||
} else {
|
||||
v.to_owned()
|
||||
};
|
||||
|
|
|
@ -148,7 +148,7 @@ impl BuildAction for PythonEnvironment {
|
|||
// Add --python flag to extra_args if PYTHON_BINARY is set
|
||||
let mut args = self.extra_args.to_string();
|
||||
if let Ok(python_binary) = env::var("PYTHON_BINARY") {
|
||||
args = format!("--python {} {}", python_binary, args);
|
||||
args = format!("--python {python_binary} {args}");
|
||||
}
|
||||
build.add_variable("extra_args", args);
|
||||
}
|
||||
|
@ -193,31 +193,19 @@ impl BuildAction for PythonTypecheck {
|
|||
struct PythonFormat<'a> {
|
||||
pub inputs: &'a BuildInput,
|
||||
pub check_only: bool,
|
||||
pub isort_ini: &'a BuildInput,
|
||||
}
|
||||
|
||||
impl BuildAction for PythonFormat<'_> {
|
||||
fn command(&self) -> &str {
|
||||
"$black -t py39 -q $check --color $in && $
|
||||
$isort --color --settings-path $isort_ini $check $in"
|
||||
"$ruff format $mode $in && $ruff check --select I --fix $in"
|
||||
}
|
||||
|
||||
fn files(&mut self, build: &mut impl crate::build::FilesHandle) {
|
||||
build.add_inputs("in", self.inputs);
|
||||
build.add_inputs("black", inputs![":pyenv:black"]);
|
||||
build.add_inputs("isort", inputs![":pyenv:isort"]);
|
||||
build.add_inputs("ruff", inputs![":pyenv:ruff"]);
|
||||
|
||||
let hash = simple_hash(self.inputs);
|
||||
build.add_env_var("BLACK_CACHE_DIR", "out/python/black.cache.{hash}");
|
||||
build.add_inputs("isort_ini", self.isort_ini);
|
||||
build.add_variable(
|
||||
"check",
|
||||
if self.check_only {
|
||||
"--diff --check"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
);
|
||||
build.add_variable("mode", if self.check_only { "--check" } else { "" });
|
||||
|
||||
build.add_output_stamp(format!(
|
||||
"tests/python_format.{}.{hash}",
|
||||
|
@ -227,13 +215,11 @@ impl BuildAction for PythonFormat<'_> {
|
|||
}
|
||||
|
||||
pub fn python_format(build: &mut Build, group: &str, inputs: BuildInput) -> Result<()> {
|
||||
let isort_ini = &inputs![".isort.cfg"];
|
||||
build.add_action(
|
||||
format!("check:format:python:{group}"),
|
||||
PythonFormat {
|
||||
inputs: &inputs,
|
||||
check_only: true,
|
||||
isort_ini,
|
||||
},
|
||||
)?;
|
||||
|
||||
|
@ -242,34 +228,39 @@ pub fn python_format(build: &mut Build, group: &str, inputs: BuildInput) -> Resu
|
|||
PythonFormat {
|
||||
inputs: &inputs,
|
||||
check_only: false,
|
||||
isort_ini,
|
||||
},
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct PythonLint {
|
||||
pub struct RuffCheck {
|
||||
pub folders: &'static [&'static str],
|
||||
pub pylint_ini: BuildInput,
|
||||
pub deps: BuildInput,
|
||||
pub check_only: bool,
|
||||
}
|
||||
|
||||
impl BuildAction for PythonLint {
|
||||
impl BuildAction for RuffCheck {
|
||||
fn command(&self) -> &str {
|
||||
"$pylint --rcfile $pylint_ini -sn -j $cpus $folders"
|
||||
"$ruff check $folders $mode"
|
||||
}
|
||||
|
||||
fn files(&mut self, build: &mut impl crate::build::FilesHandle) {
|
||||
build.add_inputs("", &self.deps);
|
||||
build.add_inputs("pylint", inputs![":pyenv:pylint"]);
|
||||
build.add_inputs("pylint_ini", &self.pylint_ini);
|
||||
build.add_inputs("", inputs![".ruff.toml"]);
|
||||
build.add_inputs("ruff", inputs![":pyenv:ruff"]);
|
||||
build.add_variable("folders", self.folders.join(" "));
|
||||
// On a 16 core system, values above 10 do not improve wall clock time,
|
||||
// but waste extra cores that could be working on other tests.
|
||||
build.add_variable("cpus", num_cpus::get().min(10).to_string());
|
||||
build.add_variable(
|
||||
"mode",
|
||||
if self.check_only {
|
||||
""
|
||||
} else {
|
||||
"--fix --unsafe-fixes"
|
||||
},
|
||||
);
|
||||
|
||||
let hash = simple_hash(&self.deps);
|
||||
build.add_output_stamp(format!("tests/python_lint.{hash}"));
|
||||
let kind = if self.check_only { "check" } else { "fix" };
|
||||
build.add_output_stamp(format!("tests/python_ruff.{kind}.{hash}"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,12 +30,12 @@ impl Build {
|
|||
)
|
||||
.unwrap();
|
||||
for (key, value) in &self.variables {
|
||||
writeln!(&mut buf, "{} = {}", key, value).unwrap();
|
||||
writeln!(&mut buf, "{key} = {value}").unwrap();
|
||||
}
|
||||
buf.push('\n');
|
||||
|
||||
for (key, value) in &self.pools {
|
||||
writeln!(&mut buf, "pool {}\n depth = {}", key, value).unwrap();
|
||||
writeln!(&mut buf, "pool {key}\n depth = {value}").unwrap();
|
||||
}
|
||||
buf.push('\n');
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ fn sha2_data(data: &[u8]) -> String {
|
|||
let mut digest = sha2::Sha256::new();
|
||||
digest.update(data);
|
||||
let result = digest.finalize();
|
||||
format!("{:x}", result)
|
||||
format!("{result:x}")
|
||||
}
|
||||
|
||||
enum CompressionKind {
|
||||
|
|
|
@ -138,7 +138,7 @@ fn setup_build_root() -> Utf8PathBuf {
|
|||
true
|
||||
};
|
||||
if create {
|
||||
println!("Switching build root to {}", new_target);
|
||||
println!("Switching build root to {new_target}");
|
||||
std::os::unix::fs::symlink(new_target, build_root).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ fn split_args(args: Vec<String>) -> Vec<Vec<String>> {
|
|||
|
||||
pub fn run_command(command: &mut Command) {
|
||||
if let Err(err) = command.ensure_success() {
|
||||
println!("{}", err);
|
||||
println!("{err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,7 +85,7 @@ When formatting issues are reported, they can be fixed with
|
|||
./ninja format
|
||||
```
|
||||
|
||||
## Fixing eslint/copyright header issues
|
||||
## Fixing ruff/eslint/copyright header issues
|
||||
|
||||
```
|
||||
./ninja fix
|
||||
|
|
|
@ -98,12 +98,6 @@ should preferably be assigned a number between 1 and 15. If a message contains
|
|||
|
||||
Protobuf has an official Python implementation with an extensive [reference](https://developers.google.com/protocol-buffers/docs/reference/python-generated).
|
||||
|
||||
- Every message used in aqt or pylib must be added to the respective `.pylintrc`
|
||||
to avoid failing type checks. The unqualified protobuf message's name must be
|
||||
used, not an alias from `collection.py` for example. This should be taken into
|
||||
account when choosing a message name in order to prevent skipping typechecking
|
||||
a Python class of the same name.
|
||||
|
||||
### Typescript
|
||||
|
||||
Anki uses [protobuf-es](https://github.com/bufbuild/protobuf-es), which offers
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit cc56464ab6354d4f1ad87ab3cc5c071c076b662d
|
||||
Subproject commit 4a65d6012ac022a35f5c80c80b2b665447b6a525
|
|
@ -425,6 +425,8 @@ deck-config-desired-retention-tooltip =
|
|||
less frequently, and you will forget more of them. Be conservative when adjusting this - higher
|
||||
values will greatly increase your workload, and lower values can be demoralizing when you forget
|
||||
a lot of material.
|
||||
deck-config-desired-retention-tooltip2 =
|
||||
The workload values provided by the tooltip are a rough approximation. For a greater level of accuracy, use the simulator.
|
||||
deck-config-historical-retention-tooltip =
|
||||
When some of your review history is missing, FSRS needs to fill in the gaps. By default, it will
|
||||
assume that when you did those old reviews, you remembered 90% of the material. If your old retention
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 5f9a9ceb6e8a9aade26c1ad9f1c936f5cc4d9e2a
|
||||
Subproject commit f42461a6438cbe844150f543128d79a669bc4ef2
|
|
@ -435,7 +435,7 @@ impl TextWriter {
|
|||
item = item.trim_start_matches(' ');
|
||||
}
|
||||
|
||||
write!(self.buffer, "{}", item)
|
||||
write!(self.buffer, "{item}")
|
||||
}
|
||||
|
||||
fn write_char_into_indent(&mut self, ch: char) {
|
||||
|
|
|
@ -67,7 +67,7 @@ fn additional_template_folder(dst_folder: &Utf8Path) -> Option<Utf8PathBuf> {
|
|||
|
||||
fn all_langs(lang_folder: &Utf8Path) -> Result<Vec<Utf8PathBuf>> {
|
||||
std::fs::read_dir(lang_folder)
|
||||
.with_context(|| format!("reading {:?}", lang_folder))?
|
||||
.with_context(|| format!("reading {lang_folder:?}"))?
|
||||
.filter_map(Result::ok)
|
||||
.map(|e| Ok(e.path().utf8()?))
|
||||
.collect()
|
||||
|
|
|
@ -46,7 +46,6 @@ from .errors import (
|
|||
|
||||
# the following comment is required to suppress a warning that only shows up
|
||||
# when there are other pylint failures
|
||||
# pylint: disable=c-extension-no-member
|
||||
if _rsbridge.buildhash() != anki.buildinfo.buildhash:
|
||||
raise Exception(
|
||||
f"""rsbridge and anki build hashes do not match:
|
||||
|
@ -164,7 +163,7 @@ class RustBackend(RustBackendGenerated):
|
|||
finally:
|
||||
elapsed = time.time() - start
|
||||
if current_thread() is main_thread() and elapsed > 0.2:
|
||||
print(f"blocked main thread for {int(elapsed*1000)}ms:")
|
||||
print(f"blocked main thread for {int(elapsed * 1000)}ms:")
|
||||
print("".join(traceback.format_stack()))
|
||||
|
||||
err = backend_pb2.BackendError()
|
||||
|
|
|
@ -7,7 +7,7 @@ import pprint
|
|||
import time
|
||||
from typing import NewType
|
||||
|
||||
import anki # pylint: disable=unused-import
|
||||
import anki
|
||||
import anki.collection
|
||||
import anki.decks
|
||||
import anki.notes
|
||||
|
|
|
@ -158,7 +158,7 @@ class Collection(DeprecatedNamesMixin):
|
|||
self.tags = TagManager(self)
|
||||
self.conf = ConfigManager(self)
|
||||
self._load_scheduler()
|
||||
self._startReps = 0 # pylint: disable=invalid-name
|
||||
self._startReps = 0
|
||||
|
||||
def name(self) -> Any:
|
||||
return os.path.splitext(os.path.basename(self.path))[0]
|
||||
|
@ -511,9 +511,7 @@ class Collection(DeprecatedNamesMixin):
|
|||
# Utils
|
||||
##########################################################################
|
||||
|
||||
def nextID( # pylint: disable=invalid-name
|
||||
self, type: str, inc: bool = True
|
||||
) -> Any:
|
||||
def nextID(self, type: str, inc: bool = True) -> Any:
|
||||
type = f"next{type.capitalize()}"
|
||||
id = self.conf.get(type, 1)
|
||||
if inc:
|
||||
|
@ -849,7 +847,6 @@ class Collection(DeprecatedNamesMixin):
|
|||
)
|
||||
|
||||
def _pb_search_separator(self, operator: SearchJoiner) -> SearchNode.Group.Joiner.V:
|
||||
# pylint: disable=no-member
|
||||
if operator == "AND":
|
||||
return SearchNode.Group.Joiner.AND
|
||||
else:
|
||||
|
@ -867,7 +864,9 @@ class Collection(DeprecatedNamesMixin):
|
|||
return column
|
||||
return None
|
||||
|
||||
def browser_row_for_id(self, id_: int) -> tuple[
|
||||
def browser_row_for_id(
|
||||
self, id_: int
|
||||
) -> tuple[
|
||||
Generator[tuple[str, bool, BrowserRow.Cell.TextElideMode.V], None, None],
|
||||
BrowserRow.Color.V,
|
||||
str,
|
||||
|
@ -1212,8 +1211,6 @@ class Collection(DeprecatedNamesMixin):
|
|||
# the count on things like edits, which we probably could do by checking
|
||||
# the previous state in moveToState.
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
def startTimebox(self) -> None:
|
||||
self._startTime = time.time()
|
||||
self._startReps = self.sched.reps
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
@ -351,7 +350,7 @@ class AnkiPackageExporter(AnkiExporter):
|
|||
colfile = path.replace(".apkg", ".anki2")
|
||||
AnkiExporter.exportInto(self, colfile)
|
||||
# prevent older clients from accessing
|
||||
# pylint: disable=unreachable
|
||||
|
||||
self._addDummyCollection(z)
|
||||
z.write(colfile, "collection.anki21")
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
"""
|
||||
Tools for extending Anki.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
@ -144,7 +143,6 @@ class TextImporter(NoteImporter):
|
|||
self.close()
|
||||
zuper = super()
|
||||
if hasattr(zuper, "__del__"):
|
||||
# pylint: disable=no-member
|
||||
zuper.__del__(self) # type: ignore
|
||||
|
||||
def noteFromFields(self, fields: list[str]) -> ForeignNote:
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
import re
|
||||
import time
|
||||
|
@ -35,7 +34,6 @@ f._id=d._fact_id"""
|
|||
):
|
||||
if id != curid:
|
||||
if note:
|
||||
# pylint: disable=unsubscriptable-object
|
||||
notes[note["_id"]] = note
|
||||
note = {"_id": _id}
|
||||
curid = id
|
||||
|
@ -185,7 +183,6 @@ acq_reps+ret_reps, lapses, card_type_id from cards"""
|
|||
state = dict(n=1)
|
||||
|
||||
def repl(match):
|
||||
# pylint: disable=cell-var-from-loop
|
||||
# replace [...] with cloze refs
|
||||
res = "{{c%d::%s}}" % (state["n"], match.group(1))
|
||||
state["n"] += 1
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
|
@ -157,13 +157,13 @@ def lang_to_disk_lang(lang: str) -> str:
|
|||
|
||||
|
||||
# the currently set interface language
|
||||
current_lang = "en" # pylint: disable=invalid-name
|
||||
current_lang = "en"
|
||||
|
||||
# the current Fluent translation instance. Code in pylib/ should
|
||||
# not reference this, and should use col.tr instead. The global
|
||||
# instance exists for legacy reasons, and as a convenience for the
|
||||
# Qt code.
|
||||
current_i18n: anki._backend.RustBackend | None = None # pylint: disable=invalid-name
|
||||
current_i18n: anki._backend.RustBackend | None = None
|
||||
tr_legacyglobal = anki._backend.Translations(None)
|
||||
|
||||
|
||||
|
@ -178,7 +178,7 @@ def ngettext(single: str, plural: str, num: int) -> str:
|
|||
|
||||
|
||||
def set_lang(lang: str) -> None:
|
||||
global current_lang, current_i18n # pylint: disable=invalid-name
|
||||
global current_lang, current_i18n
|
||||
current_lang = lang
|
||||
current_i18n = anki._backend.RustBackend(langs=[lang])
|
||||
tr_legacyglobal.backend = weakref.ref(current_i18n)
|
||||
|
@ -198,9 +198,7 @@ def get_def_lang(user_lang: str | None = None) -> tuple[int, str]:
|
|||
# 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():
|
||||
warnings.simplefilter("ignore", DeprecationWarning)
|
||||
(sys_lang, enc) = (
|
||||
locale.getdefaultlocale() # pylint: disable=deprecated-method
|
||||
)
|
||||
(sys_lang, enc) = locale.getdefaultlocale()
|
||||
except AttributeError:
|
||||
# this will return a different format on Windows (e.g. Italian_Italy), resulting in us falling back to en_US
|
||||
# further below
|
||||
|
|
|
@ -10,7 +10,7 @@ import time
|
|||
from collections.abc import Sequence
|
||||
from typing import Any, NewType, Union
|
||||
|
||||
import anki # pylint: disable=unused-import
|
||||
import anki
|
||||
import anki.collection
|
||||
import anki.notes
|
||||
from anki import notetypes_pb2
|
||||
|
@ -419,7 +419,7 @@ and notes.mid = ? and cards.ord = ?""",
|
|||
|
||||
# legacy API - used by unit tests and add-ons
|
||||
|
||||
def change( # pylint: disable=invalid-name
|
||||
def change(
|
||||
self,
|
||||
notetype: NotetypeDict,
|
||||
nids: list[anki.notes.NoteId],
|
||||
|
@ -478,8 +478,6 @@ and notes.mid = ? and cards.ord = ?""",
|
|||
# Legacy
|
||||
##########################################################################
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
@deprecated(info="use note.cloze_numbers_in_fields()")
|
||||
def _availClozeOrds(
|
||||
self, notetype: NotetypeDict, flds: str, allow_empty: bool = True
|
||||
|
|
|
@ -7,7 +7,7 @@ import copy
|
|||
from collections.abc import Sequence
|
||||
from typing import NewType
|
||||
|
||||
import anki # pylint: disable=unused-import
|
||||
import anki
|
||||
import anki.cards
|
||||
import anki.collection
|
||||
import anki.decks
|
||||
|
|
|
@ -4,10 +4,8 @@
|
|||
# The backend code has moved into _backend; this file exists only to avoid breaking
|
||||
# some add-ons. They should be updated to point to the correct location in the
|
||||
# future.
|
||||
#
|
||||
# pylint: disable=unused-import
|
||||
# pylint: enable=invalid-name
|
||||
|
||||
# ruff: noqa: F401
|
||||
from anki.decks import DeckTreeNode
|
||||
from anki.errors import InvalidInput, NotFoundError
|
||||
from anki.lang import TR
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
"""
|
||||
The V3/2021 scheduler.
|
||||
|
@ -184,7 +183,7 @@ class Scheduler(SchedulerBaseWithLegacy):
|
|||
return self._interval_for_filtered_state(state.filtered)
|
||||
else:
|
||||
assert_exhaustive(kind)
|
||||
return 0 # pylint: disable=unreachable
|
||||
return 0
|
||||
|
||||
def _interval_for_normal_state(
|
||||
self, normal: scheduler_pb2.SchedulingState.Normal
|
||||
|
@ -200,7 +199,7 @@ class Scheduler(SchedulerBaseWithLegacy):
|
|||
return normal.relearning.learning.scheduled_secs
|
||||
else:
|
||||
assert_exhaustive(kind)
|
||||
return 0 # pylint: disable=unreachable
|
||||
return 0
|
||||
|
||||
def _interval_for_filtered_state(
|
||||
self, filtered: scheduler_pb2.SchedulingState.Filtered
|
||||
|
@ -212,7 +211,7 @@ class Scheduler(SchedulerBaseWithLegacy):
|
|||
return self._interval_for_normal_state(filtered.rescheduling.original_state)
|
||||
else:
|
||||
assert_exhaustive(kind)
|
||||
return 0 # pylint: disable=unreachable
|
||||
return 0
|
||||
|
||||
def nextIvl(self, card: Card, ease: int) -> Any:
|
||||
"Don't use this - it is only required by tests, and will be moved in the future."
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# pylint: disable=C
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
@ -27,7 +26,7 @@ def _legacy_card_stats(
|
|||
col: anki.collection.Collection, card_id: anki.cards.CardId, include_revlog: bool
|
||||
) -> str:
|
||||
"A quick hack to preserve compatibility with the old HTML string API."
|
||||
random_id = f"cardinfo-{base62(random.randint(0, 2 ** 64 - 1))}"
|
||||
random_id = f"cardinfo-{base62(random.randint(0, 2**64 - 1))}"
|
||||
varName = random_id.replace("-", "")
|
||||
return f"""
|
||||
<div id="{random_id}"></div>
|
||||
|
@ -324,7 +323,6 @@ group by day order by day"""
|
|||
yaxes=[dict(min=0), dict(position="right", min=0)],
|
||||
)
|
||||
if days is not None:
|
||||
# pylint: disable=invalid-unary-operand-type
|
||||
conf["xaxis"]["min"] = -days + 0.5
|
||||
|
||||
def plot(id: str, data: Any, ylabel: str, ylabel2: str) -> str:
|
||||
|
@ -359,7 +357,6 @@ group by day order by day"""
|
|||
yaxes=[dict(min=0), dict(position="right", min=0)],
|
||||
)
|
||||
if days is not None:
|
||||
# pylint: disable=invalid-unary-operand-type
|
||||
conf["xaxis"]["min"] = -days + 0.5
|
||||
|
||||
def plot(id: str, data: Any, ylabel: str, ylabel2: str) -> str:
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# pylint: disable=invalid-name
|
||||
|
||||
# from subtlepatterns.com; CC BY 4.0.
|
||||
# by Daniel Beaton
|
||||
# https://www.toptal.com/designers/subtlepatterns/fancy-deboss/
|
||||
|
|
|
@ -12,7 +12,6 @@ from anki import notetypes_pb2
|
|||
from anki._legacy import DeprecatedNamesMixinForModule
|
||||
from anki.utils import from_json_bytes
|
||||
|
||||
# pylint: disable=no-member
|
||||
StockNotetypeKind = notetypes_pb2.StockNotetype.Kind
|
||||
|
||||
# add-on authors can add ("note type name", function)
|
||||
|
|
|
@ -16,7 +16,7 @@ import re
|
|||
from collections.abc import Collection, Sequence
|
||||
from typing import Match
|
||||
|
||||
import anki # pylint: disable=unused-import
|
||||
import anki
|
||||
import anki.collection
|
||||
from anki import tags_pb2
|
||||
from anki._legacy import DeprecatedNamesMixin, deprecated
|
||||
|
|
|
@ -24,7 +24,6 @@ from anki.dbproxy import DBProxy
|
|||
_tmpdir: str | None
|
||||
|
||||
try:
|
||||
# pylint: disable=c-extension-no-member
|
||||
import orjson
|
||||
|
||||
to_json_bytes: Callable[[Any], bytes] = orjson.dumps
|
||||
|
@ -156,12 +155,12 @@ def field_checksum(data: str) -> int:
|
|||
# Temp files
|
||||
##############################################################################
|
||||
|
||||
_tmpdir = None # pylint: disable=invalid-name
|
||||
_tmpdir = None
|
||||
|
||||
|
||||
def tmpdir() -> str:
|
||||
"A reusable temp folder which we clean out on each program invocation."
|
||||
global _tmpdir # pylint: disable=invalid-name
|
||||
global _tmpdir
|
||||
if not _tmpdir:
|
||||
|
||||
def cleanup() -> None:
|
||||
|
@ -216,7 +215,6 @@ def call(argv: list[str], wait: bool = True, **kwargs: Any) -> int:
|
|||
try:
|
||||
info.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore
|
||||
except Exception:
|
||||
# pylint: disable=no-member
|
||||
info.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW # type: ignore
|
||||
else:
|
||||
info = None
|
||||
|
@ -282,7 +280,7 @@ def plat_desc() -> str:
|
|||
elif is_win:
|
||||
theos = f"win:{platform.win32_ver()[0]}"
|
||||
elif system == "Linux":
|
||||
import distro # pytype: disable=import-error # pylint: disable=import-error
|
||||
import distro # pytype: disable=import-error
|
||||
|
||||
dist_id = distro.id()
|
||||
dist_version = distro.version()
|
||||
|
|
|
@ -28,6 +28,6 @@ fn main() {
|
|||
.to_string();
|
||||
|
||||
let libs_path = stdlib_path + "s";
|
||||
println!("cargo:rustc-link-search={}", libs_path);
|
||||
println!("cargo:rustc-link-search={libs_path}");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -169,8 +169,7 @@ def test_find_cards():
|
|||
# properties
|
||||
id = col.db.scalar("select id from cards limit 1")
|
||||
col.db.execute(
|
||||
"update cards set queue=2, ivl=10, reps=20, due=30, factor=2200 "
|
||||
"where id = ?",
|
||||
"update cards set queue=2, ivl=10, reps=20, due=30, factor=2200 where id = ?",
|
||||
id,
|
||||
)
|
||||
assert len(col.find_cards("prop:ivl>5")) == 1
|
||||
|
|
|
@ -551,12 +551,10 @@ def test_bury():
|
|||
col.addNote(note)
|
||||
c2 = note.cards()[0]
|
||||
# burying
|
||||
col.sched.bury_cards([c.id], manual=True) # pylint: disable=unexpected-keyword-arg
|
||||
col.sched.bury_cards([c.id], manual=True)
|
||||
c.load()
|
||||
assert c.queue == QUEUE_TYPE_MANUALLY_BURIED
|
||||
col.sched.bury_cards(
|
||||
[c2.id], manual=False
|
||||
) # pylint: disable=unexpected-keyword-arg
|
||||
col.sched.bury_cards([c2.id], manual=False)
|
||||
c2.load()
|
||||
assert c2.queue == QUEUE_TYPE_SIBLING_BURIED
|
||||
|
||||
|
|
|
@ -15,6 +15,5 @@ with open(buildhash_file, "r", encoding="utf8") as f:
|
|||
|
||||
with open(outpath, "w", encoding="utf8") as f:
|
||||
# if we switch to uppercase we'll need to add legacy aliases
|
||||
f.write("# pylint: disable=invalid-name\n")
|
||||
f.write(f"version = '{version}'\n")
|
||||
f.write(f"buildhash = '{buildhash}'\n")
|
||||
|
|
|
@ -133,7 +133,7 @@ prefix = """\
|
|||
# This file is automatically generated; edit tools/genhooks.py instead.
|
||||
# Please import from anki.hooks instead of this file.
|
||||
|
||||
# pylint: disable=unused-import
|
||||
# ruff: noqa: F401
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ Code for generating hooks.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
|
@ -204,9 +203,6 @@ def write_file(path: str, hooks: list[Hook], prefix: str, suffix: str):
|
|||
|
||||
code += f"\n{suffix}"
|
||||
|
||||
# work around issue with latest black
|
||||
if sys.platform == "win32" and "HOME" in os.environ:
|
||||
os.environ["USERPROFILE"] = os.environ["HOME"]
|
||||
with open(path, "wb") as file:
|
||||
file.write(code.encode("utf8"))
|
||||
subprocess.run([sys.executable, "-m", "black", "-q", path], check=True)
|
||||
subprocess.run([sys.executable, "-m", "ruff", "format", "-q", path], check=True)
|
||||
|
|
|
@ -7,14 +7,11 @@ classifiers = ["Private :: Do Not Upload"]
|
|||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"black",
|
||||
"isort",
|
||||
"mypy",
|
||||
"mypy-protobuf",
|
||||
"pylint",
|
||||
"ruff",
|
||||
"pytest",
|
||||
"PyChromeDevTools",
|
||||
"colorama", # for isort --color
|
||||
"wheel",
|
||||
"hatchling", # for type checking hatch_build.py files
|
||||
"mock",
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
os.environ["REPO_ROOT"] = os.path.abspath(".")
|
||||
subprocess.run(["out/pyenv/bin/sphinx-apidoc", "-o", "out/python/sphinx", "pylib", "qt"], check=True)
|
||||
subprocess.run(["out/pyenv/bin/sphinx-build", "out/python/sphinx", "out/python/sphinx/html"], check=True)
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
[settings]
|
||||
py_version=39
|
||||
profile=black
|
||||
known_first_party=anki,aqt
|
||||
extend_skip=aqt/forms,hooks_gen.py
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
# ruff: noqa: F401
|
||||
import atexit
|
||||
import logging
|
||||
import os
|
||||
|
@ -28,7 +29,7 @@ if sys.version_info[0] < 3 or sys.version_info[1] < 9:
|
|||
# ensure unicode filenames are supported
|
||||
try:
|
||||
"テスト".encode(sys.getfilesystemencoding())
|
||||
except UnicodeEncodeError as exc:
|
||||
except UnicodeEncodeError:
|
||||
print("Anki requires a UTF-8 locale.")
|
||||
print("Please Google 'how to change locale on [your Linux distro]'")
|
||||
sys.exit(1)
|
||||
|
@ -41,6 +42,11 @@ if "--syncserver" in sys.argv:
|
|||
# does not return
|
||||
run_sync_server()
|
||||
|
||||
if sys.platform == "win32":
|
||||
from win32com.shell import shell
|
||||
|
||||
shell.SetCurrentProcessExplicitAppUserModelID("Ankitects.Anki")
|
||||
|
||||
import argparse
|
||||
import builtins
|
||||
import cProfile
|
||||
|
@ -285,7 +291,6 @@ class NativeEventFilter(QAbstractNativeEventFilter):
|
|||
def nativeEventFilter(
|
||||
self, eventType: Any, message: Any
|
||||
) -> tuple[bool, Any | None]:
|
||||
|
||||
if eventType == "windows_generic_MSG":
|
||||
import ctypes.wintypes
|
||||
|
||||
|
@ -558,7 +563,7 @@ def run() -> None:
|
|||
print(f"Starting Anki {_version}...")
|
||||
try:
|
||||
_run()
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
|
|
|
@ -6,8 +6,6 @@ from __future__ import annotations
|
|||
import sys
|
||||
|
||||
if sys.platform == "darwin":
|
||||
from anki_mac_helper import ( # pylint:disable=unused-import,import-error
|
||||
macos_helper,
|
||||
)
|
||||
from anki_mac_helper import macos_helper
|
||||
else:
|
||||
macos_helper = None
|
||||
|
|
|
@ -927,7 +927,6 @@ class AddonsDialog(QDialog):
|
|||
or self.mgr.configAction(addon.dir_name)
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
def _onAddonItemSelected(self, row_int: int) -> None:
|
||||
try:
|
||||
|
@ -1457,7 +1456,9 @@ class ChooseAddonsToUpdateDialog(QDialog):
|
|||
layout.addWidget(addons_list_widget)
|
||||
self.addons_list_widget = addons_list_widget
|
||||
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) # type: ignore
|
||||
button_box = QDialogButtonBox(
|
||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
) # type: ignore
|
||||
qconnect(
|
||||
button_box.button(QDialogButtonBox.StandardButton.Ok).clicked, self.accept
|
||||
)
|
||||
|
|
|
@ -36,7 +36,6 @@ def ankihub_login(
|
|||
username: str = "",
|
||||
password: str = "",
|
||||
) -> None:
|
||||
|
||||
def on_future_done(fut: Future[str], username: str, password: str) -> None:
|
||||
try:
|
||||
token = fut.result()
|
||||
|
@ -73,7 +72,6 @@ def ankihub_logout(
|
|||
on_success: Callable[[], None],
|
||||
token: str,
|
||||
) -> None:
|
||||
|
||||
def logout() -> None:
|
||||
mw.pm.set_ankihub_username(None)
|
||||
mw.pm.set_ankihub_token(None)
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
from __future__ import annotations
|
||||
|
||||
# ruff: noqa: F401
|
||||
import sys
|
||||
|
||||
import aqt
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
# ruff: noqa: F401
|
||||
from anki.utils import is_mac
|
||||
from aqt.theme import theme_manager
|
||||
|
||||
|
|
|
@ -106,7 +106,7 @@ class SidebarTreeView(QTreeView):
|
|||
def _setup_style(self) -> None:
|
||||
# match window background color and tweak style
|
||||
bgcolor = QPalette().window().color().name()
|
||||
border = theme_manager.var(colors.BORDER)
|
||||
theme_manager.var(colors.BORDER)
|
||||
styles = [
|
||||
"padding: 3px",
|
||||
"padding-right: 0px",
|
||||
|
@ -711,7 +711,6 @@ class SidebarTreeView(QTreeView):
|
|||
|
||||
def _flags_tree(self, root: SidebarItem) -> None:
|
||||
icon_off = "icons:flag-variant-off-outline.svg"
|
||||
icon = "icons:flag-variant.svg"
|
||||
icon_outline = "icons:flag-variant-outline.svg"
|
||||
|
||||
root = self._section_root(
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
from __future__ import annotations
|
||||
|
||||
# ruff: noqa: F401
|
||||
import copy
|
||||
import time
|
||||
from collections.abc import Generator, Sequence
|
||||
|
|
|
@ -105,11 +105,11 @@ class DataModel(QAbstractTableModel):
|
|||
row = CellRow(*self.col.browser_row_for_id(item))
|
||||
except BackendError as e:
|
||||
return CellRow.disabled(self.len_columns(), str(e))
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
return CellRow.disabled(
|
||||
self.len_columns(), tr.errors_please_check_database()
|
||||
)
|
||||
except BaseException as e:
|
||||
except BaseException:
|
||||
# fatal error like a panic in the backend - dump it to the
|
||||
# console so it gets picked up by the error handler
|
||||
import traceback
|
||||
|
|
|
@ -59,7 +59,7 @@ class ItemState(ABC):
|
|||
|
||||
# abstractproperty is deprecated but used due to mypy limitations
|
||||
# (https://github.com/python/mypy/issues/1362)
|
||||
@abstractproperty # pylint: disable=deprecated-decorator
|
||||
@abstractproperty
|
||||
def active_columns(self) -> list[str]:
|
||||
"""Return the saved or default columns for the state."""
|
||||
|
||||
|
|
|
@ -361,8 +361,7 @@ class Table:
|
|||
for m in self.col.models.all():
|
||||
for t in m["tmpls"]:
|
||||
bsize = t.get("bsize", 0)
|
||||
if bsize > curmax:
|
||||
curmax = bsize
|
||||
curmax = max(curmax, bsize)
|
||||
|
||||
assert self._view is not None
|
||||
vh = self._view.verticalHeader()
|
||||
|
|
|
@ -221,7 +221,7 @@ class CardLayout(QDialog):
|
|||
)
|
||||
for i in range(min(len(self.cloze_numbers), 9)):
|
||||
QShortcut( # type: ignore
|
||||
QKeySequence(f"Alt+{i+1}"),
|
||||
QKeySequence(f"Alt+{i + 1}"),
|
||||
self,
|
||||
activated=lambda n=i: self.pform.cloze_number_combo.setCurrentIndex(n),
|
||||
)
|
||||
|
@ -790,7 +790,7 @@ class CardLayout(QDialog):
|
|||
assert a is not None
|
||||
qconnect(
|
||||
a.triggered,
|
||||
lambda: self.on_restore_to_default(), # pylint: disable=unnecessary-lambda
|
||||
lambda: self.on_restore_to_default(),
|
||||
)
|
||||
|
||||
if not self._isCloze():
|
||||
|
|
|
@ -294,7 +294,6 @@ class DebugConsole(QDialog):
|
|||
}
|
||||
self._captureOutput(True)
|
||||
try:
|
||||
# pylint: disable=exec-used
|
||||
exec(text, vars)
|
||||
except Exception:
|
||||
self._output += traceback.format_exc()
|
||||
|
|
|
@ -386,9 +386,7 @@ class DeckBrowser:
|
|||
if b[0]:
|
||||
b[0] = tr.actions_shortcut_key(val=shortcut(b[0]))
|
||||
buf += """
|
||||
<button title='%s' onclick='pycmd(\"%s\");'>%s</button>""" % tuple(
|
||||
b
|
||||
)
|
||||
<button title='%s' onclick='pycmd(\"%s\");'>%s</button>""" % tuple(b)
|
||||
self.bottom.draw(
|
||||
buf=buf,
|
||||
link_handler=self._linkHandler,
|
||||
|
|
|
@ -320,7 +320,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
gui_hooks.editor_did_init_shortcuts(cuts, self)
|
||||
for row in cuts:
|
||||
if len(row) == 2:
|
||||
keys, fn = row # pylint: disable=unbalanced-tuple-unpacking
|
||||
keys, fn = row
|
||||
fn = self._addFocusCheck(fn)
|
||||
else:
|
||||
keys, fn, _ = row
|
||||
|
|
|
@ -212,11 +212,10 @@ class ExportDialog(QDialog):
|
|||
if self.isVerbatim:
|
||||
msg = tr.exporting_collection_exported()
|
||||
self.mw.reopen()
|
||||
elif self.isTextNote:
|
||||
msg = tr.exporting_note_exported(count=self.exporter.count)
|
||||
else:
|
||||
if self.isTextNote:
|
||||
msg = tr.exporting_note_exported(count=self.exporter.count)
|
||||
else:
|
||||
msg = tr.exporting_card_exported(count=self.exporter.count)
|
||||
msg = tr.exporting_card_exported(count=self.exporter.count)
|
||||
gui_hooks.legacy_exporter_did_export(self.exporter)
|
||||
tooltip(msg, period=3000)
|
||||
QDialog.reject(self)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# ruff: noqa: F401
|
||||
from . import (
|
||||
about,
|
||||
addcards,
|
||||
|
|
|
@ -134,9 +134,8 @@ IMPORTERS: list[type[Importer]] = [
|
|||
|
||||
|
||||
def legacy_file_endings(col: Collection) -> list[str]:
|
||||
from anki.importing import AnkiPackageImporter
|
||||
from anki.importing import AnkiPackageImporter, TextImporter, importers
|
||||
from anki.importing import MnemosyneImporter as LegacyMnemosyneImporter
|
||||
from anki.importing import TextImporter, importers
|
||||
|
||||
return [
|
||||
ext
|
||||
|
|
|
@ -11,10 +11,10 @@ from collections.abc import Callable
|
|||
from concurrent.futures import Future
|
||||
from typing import Any
|
||||
|
||||
import anki.importing as importing
|
||||
import aqt.deckchooser
|
||||
import aqt.forms
|
||||
import aqt.modelchooser
|
||||
from anki import importing
|
||||
from anki.importing.anki2 import MediaMapInvalid, V2ImportIntoV1
|
||||
from anki.importing.apkg import AnkiPackageImporter
|
||||
from aqt.import_export.importing import ColpkgImporter
|
||||
|
@ -262,7 +262,7 @@ class ImportDialog(QDialog):
|
|||
self.mapwidget.setLayout(self.grid)
|
||||
self.grid.setContentsMargins(3, 3, 3, 3)
|
||||
self.grid.setSpacing(6)
|
||||
for num in range(len(self.mapping)): # pylint: disable=consider-using-enumerate
|
||||
for num in range(len(self.mapping)):
|
||||
text = tr.importing_field_of_file_is(val=num + 1)
|
||||
self.grid.addWidget(QLabel(text), num, 0)
|
||||
if self.mapping[num] == "_tags":
|
||||
|
@ -357,7 +357,7 @@ def importFile(mw: AnkiQt, file: str) -> None:
|
|||
try:
|
||||
importer.open()
|
||||
mw.progress.finish()
|
||||
diag = ImportDialog(mw, importer)
|
||||
ImportDialog(mw, importer)
|
||||
except UnicodeDecodeError:
|
||||
mw.progress.finish()
|
||||
showUnicodeWarning()
|
||||
|
@ -443,3 +443,4 @@ def setupApkgImport(mw: AnkiQt, importer: AnkiPackageImporter) -> bool:
|
|||
return True
|
||||
ColpkgImporter.do_import(mw, importer.file)
|
||||
return False
|
||||
return False
|
||||
|
|
|
@ -376,7 +376,6 @@ class AnkiQt(QMainWindow):
|
|||
def openProfile(self) -> None:
|
||||
name = self.pm.profiles()[self.profileForm.profiles.currentRow()]
|
||||
self.pm.load(name)
|
||||
return
|
||||
|
||||
def onOpenProfile(self, *, callback: Callable[[], None] | None = None) -> None:
|
||||
def on_done() -> None:
|
||||
|
@ -451,7 +450,6 @@ class AnkiQt(QMainWindow):
|
|||
self.loadProfile()
|
||||
|
||||
def onOpenBackup(self) -> None:
|
||||
|
||||
def do_open(path: str) -> None:
|
||||
if not askUser(
|
||||
tr.qt_misc_replace_your_collection_with_an_earlier2(
|
||||
|
@ -677,7 +675,7 @@ class AnkiQt(QMainWindow):
|
|||
gui_hooks.collection_did_load(self.col)
|
||||
self.apply_collection_options()
|
||||
self.moveToState("deckBrowser")
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
# dump error to stderr so it gets picked up by errors.py
|
||||
traceback.print_exc()
|
||||
|
||||
|
@ -774,7 +772,6 @@ class AnkiQt(QMainWindow):
|
|||
oldState = self.state
|
||||
cleanup = getattr(self, f"_{oldState}Cleanup", None)
|
||||
if cleanup:
|
||||
# pylint: disable=not-callable
|
||||
cleanup(state)
|
||||
self.clearStateShortcuts()
|
||||
self.state = state
|
||||
|
@ -821,7 +818,7 @@ class AnkiQt(QMainWindow):
|
|||
self.bottomWeb.hide_timer.start()
|
||||
|
||||
def _reviewCleanup(self, newState: MainWindowState) -> None:
|
||||
if newState != "resetRequired" and newState != "review":
|
||||
if newState not in {"resetRequired", "review"}:
|
||||
self.reviewer.auto_advance_enabled = False
|
||||
self.reviewer.cleanup()
|
||||
self.toolbarWeb.elevate()
|
||||
|
@ -1722,11 +1719,37 @@ title="{}" {}>{}</button>""".format(
|
|||
self.maybeHideAccelerators()
|
||||
self.hideStatusTips()
|
||||
elif is_win:
|
||||
# make sure ctypes is bundled
|
||||
from ctypes import windll, wintypes # type: ignore
|
||||
self._setupWin32()
|
||||
|
||||
_dummy1 = windll
|
||||
_dummy2 = wintypes
|
||||
def _setupWin32(self):
|
||||
"""Fix taskbar display/pinning"""
|
||||
if sys.platform != "win32":
|
||||
return
|
||||
|
||||
launcher_path = os.environ.get("ANKI_LAUNCHER")
|
||||
if not launcher_path:
|
||||
return
|
||||
|
||||
from win32com.propsys import propsys, pscon
|
||||
from win32com.propsys.propsys import PROPVARIANTType
|
||||
|
||||
hwnd = int(self.winId())
|
||||
prop_store = propsys.SHGetPropertyStoreForWindow(hwnd) # type: ignore[call-arg]
|
||||
prop_store.SetValue(
|
||||
pscon.PKEY_AppUserModel_ID, PROPVARIANTType("Ankitects.Anki")
|
||||
)
|
||||
prop_store.SetValue(
|
||||
pscon.PKEY_AppUserModel_RelaunchCommand,
|
||||
PROPVARIANTType(f'"{launcher_path}"'),
|
||||
)
|
||||
prop_store.SetValue(
|
||||
pscon.PKEY_AppUserModel_RelaunchDisplayNameResource, PROPVARIANTType("Anki")
|
||||
)
|
||||
prop_store.SetValue(
|
||||
pscon.PKEY_AppUserModel_RelaunchIconResource,
|
||||
PROPVARIANTType(f"{launcher_path},0"),
|
||||
)
|
||||
prop_store.Commit()
|
||||
|
||||
def maybeHideAccelerators(self, tgt: Any | None = None) -> None:
|
||||
if not self.hideMenuAccels:
|
||||
|
|
|
@ -232,7 +232,11 @@ def _handle_local_file_request(request: LocalFileRequest) -> Response:
|
|||
else:
|
||||
max_age = 60 * 60
|
||||
return flask.send_file(
|
||||
fullpath, mimetype=mimetype, conditional=True, max_age=max_age, download_name="foo" # type: ignore[call-arg]
|
||||
fullpath,
|
||||
mimetype=mimetype,
|
||||
conditional=True,
|
||||
max_age=max_age,
|
||||
download_name="foo", # type: ignore[call-arg]
|
||||
)
|
||||
else:
|
||||
print(f"Not found: {path}")
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
#
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# pylint: disable=raise-missing-from
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
|
@ -66,7 +66,6 @@ class MPVTimeoutError(MPVError):
|
|||
|
||||
|
||||
if is_win:
|
||||
# pylint: disable=import-error
|
||||
import pywintypes
|
||||
import win32file # pytype: disable=import-error
|
||||
import win32job
|
||||
|
@ -138,15 +137,15 @@ class MPVBase:
|
|||
extended_info = win32job.QueryInformationJobObject(
|
||||
self._job, win32job.JobObjectExtendedLimitInformation
|
||||
)
|
||||
extended_info["BasicLimitInformation"][
|
||||
"LimitFlags"
|
||||
] = win32job.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
|
||||
extended_info["BasicLimitInformation"]["LimitFlags"] = (
|
||||
win32job.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
|
||||
)
|
||||
win32job.SetInformationJobObject(
|
||||
self._job,
|
||||
win32job.JobObjectExtendedLimitInformation,
|
||||
extended_info,
|
||||
)
|
||||
handle = self._proc._handle # pylint: disable=no-member
|
||||
handle = self._proc._handle
|
||||
win32job.AssignProcessToJobObject(self._job, handle)
|
||||
|
||||
def _stop_process(self):
|
||||
|
@ -193,7 +192,10 @@ class MPVBase:
|
|||
None,
|
||||
)
|
||||
win32pipe.SetNamedPipeHandleState(
|
||||
self._sock, 1, None, None # PIPE_NOWAIT
|
||||
self._sock,
|
||||
1,
|
||||
None,
|
||||
None, # PIPE_NOWAIT
|
||||
)
|
||||
except pywintypes.error as err:
|
||||
if err.args[0] == winerror.ERROR_FILE_NOT_FOUND:
|
||||
|
@ -394,7 +396,7 @@ class MPVBase:
|
|||
return self._get_response(timeout)
|
||||
except MPVCommandError as e:
|
||||
raise MPVCommandError(f"{message['command']!r}: {e}")
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
if _retry:
|
||||
print("mpv timed out, restarting")
|
||||
self._stop_process()
|
||||
|
@ -501,7 +503,6 @@ class MPV(MPVBase):
|
|||
# Simulate an init event when the process and all callbacks have been
|
||||
# completely set up.
|
||||
if hasattr(self, "on_init"):
|
||||
# pylint: disable=no-member
|
||||
self.on_init()
|
||||
|
||||
#
|
||||
|
|
|
@ -113,7 +113,7 @@ class Overview:
|
|||
self.mw.moveToState("deckBrowser")
|
||||
elif url == "review":
|
||||
openLink(f"{aqt.appShared}info/{self.sid}?v={self.sidVer}")
|
||||
elif url == "studymore" or url == "customStudy":
|
||||
elif url in {"studymore", "customStudy"}:
|
||||
self.onStudyMore()
|
||||
elif url == "unbury":
|
||||
self.on_unbury()
|
||||
|
@ -180,7 +180,6 @@ class Overview:
|
|||
############################################################
|
||||
|
||||
def _renderPage(self) -> None:
|
||||
but = self.mw.button
|
||||
deck = self.mw.col.decks.current()
|
||||
self.sid = deck.get("sharedFrom")
|
||||
if self.sid:
|
||||
|
@ -307,9 +306,7 @@ class Overview:
|
|||
if b[0]:
|
||||
b[0] = tr.actions_shortcut_key(val=shortcut(b[0]))
|
||||
buf += """
|
||||
<button title="%s" onclick='pycmd("%s")'>%s</button>""" % tuple(
|
||||
b
|
||||
)
|
||||
<button title="%s" onclick='pycmd("%s")'>%s</button>""" % tuple(b)
|
||||
self.bottom.draw(
|
||||
buf=buf,
|
||||
link_handler=link_handler,
|
||||
|
|
|
@ -11,7 +11,7 @@ from pathlib import Path
|
|||
from anki.utils import is_mac
|
||||
|
||||
|
||||
# pylint: disable=unused-import,import-error
|
||||
# ruff: noqa: F401
|
||||
def first_run_setup() -> None:
|
||||
"""Code run the first time after install/upgrade.
|
||||
|
||||
|
|
|
@ -128,7 +128,7 @@ class ProfileManager:
|
|||
default_answer_keys = {ease_num: str(ease_num) for ease_num in range(1, 5)}
|
||||
last_run_version: int = 0
|
||||
|
||||
def __init__(self, base: Path) -> None: #
|
||||
def __init__(self, base: Path) -> None:
|
||||
"base should be retrieved via ProfileMangager.get_created_base_folder"
|
||||
## Settings which should be forgotten each Anki restart
|
||||
self.session: dict[str, Any] = {}
|
||||
|
@ -153,7 +153,7 @@ class ProfileManager:
|
|||
else:
|
||||
try:
|
||||
self.load(profile)
|
||||
except Exception as exc:
|
||||
except Exception:
|
||||
self.invalid_profile_provided_on_commandline = True
|
||||
|
||||
# Profile load/save
|
||||
|
@ -483,7 +483,11 @@ create table if not exists profiles
|
|||
code = obj[1]
|
||||
name = obj[0]
|
||||
r = QMessageBox.question(
|
||||
None, "Anki", tr.profiles_confirm_lang_choice(lang=name), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No # type: ignore
|
||||
None,
|
||||
"Anki",
|
||||
tr.profiles_confirm_lang_choice(lang=name),
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No, # type: ignore
|
||||
)
|
||||
if r != QMessageBox.StandardButton.Yes:
|
||||
return self.setDefaultLang(f.lang.currentRow())
|
||||
|
|
|
@ -119,13 +119,12 @@ class ProgressManager:
|
|||
if not self._levels:
|
||||
# no current progress; safe to fire
|
||||
func()
|
||||
elif repeat:
|
||||
# skip this time; we'll fire again
|
||||
pass
|
||||
else:
|
||||
if repeat:
|
||||
# skip this time; we'll fire again
|
||||
pass
|
||||
else:
|
||||
# retry in 100ms
|
||||
self.single_shot(100, func, requires_collection)
|
||||
# retry in 100ms
|
||||
self.single_shot(100, func, requires_collection)
|
||||
|
||||
return handler
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# make sure not to optimize imports on this file
|
||||
# pylint: disable=unused-import
|
||||
# ruff: noqa: F401
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
@ -23,7 +23,7 @@ def debug() -> None:
|
|||
from pdb import set_trace
|
||||
|
||||
pyqtRemoveInputHook()
|
||||
set_trace() # pylint: disable=forgotten-debug-statement
|
||||
set_trace()
|
||||
|
||||
|
||||
if os.environ.get("DEBUG"):
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# make sure not to optimize imports on this file
|
||||
# pylint: disable=unused-import
|
||||
|
||||
# ruff: noqa: F401
|
||||
"""
|
||||
PyQt6 imports
|
||||
"""
|
||||
|
|
|
@ -21,13 +21,11 @@ from anki.scheduler.base import ScheduleCardsAsNew
|
|||
from anki.scheduler.v3 import (
|
||||
CardAnswer,
|
||||
QueuedCards,
|
||||
)
|
||||
from anki.scheduler.v3 import Scheduler as V3Scheduler
|
||||
from anki.scheduler.v3 import (
|
||||
SchedulingContext,
|
||||
SchedulingStates,
|
||||
SetSchedulingStatesRequest,
|
||||
)
|
||||
from anki.scheduler.v3 import Scheduler as V3Scheduler
|
||||
from anki.tags import MARKED_TAG
|
||||
from anki.types import assert_exhaustive
|
||||
from anki.utils import is_mac
|
||||
|
@ -597,10 +595,9 @@ class Reviewer:
|
|||
def _shortcutKeys(
|
||||
self,
|
||||
) -> Sequence[tuple[str, Callable] | tuple[Qt.Key, Callable]]:
|
||||
|
||||
def generate_default_answer_keys() -> (
|
||||
Generator[tuple[str, partial], None, None]
|
||||
):
|
||||
def generate_default_answer_keys() -> Generator[
|
||||
tuple[str, partial], None, None
|
||||
]:
|
||||
for ease in aqt.mw.pm.default_answer_keys:
|
||||
key = aqt.mw.pm.get_answer_key(ease)
|
||||
if not key:
|
||||
|
|
|
@ -101,7 +101,7 @@ def is_audio_file(fname: str) -> bool:
|
|||
return ext in AUDIO_EXTENSIONS
|
||||
|
||||
|
||||
class SoundOrVideoPlayer(Player): # pylint: disable=abstract-method
|
||||
class SoundOrVideoPlayer(Player):
|
||||
default_rank = 0
|
||||
|
||||
def rank_for_tag(self, tag: AVTag) -> int | None:
|
||||
|
@ -111,7 +111,7 @@ class SoundOrVideoPlayer(Player): # pylint: disable=abstract-method
|
|||
return None
|
||||
|
||||
|
||||
class SoundPlayer(Player): # pylint: disable=abstract-method
|
||||
class SoundPlayer(Player):
|
||||
default_rank = 0
|
||||
|
||||
def rank_for_tag(self, tag: AVTag) -> int | None:
|
||||
|
@ -121,7 +121,7 @@ class SoundPlayer(Player): # pylint: disable=abstract-method
|
|||
return None
|
||||
|
||||
|
||||
class VideoPlayer(Player): # pylint: disable=abstract-method
|
||||
class VideoPlayer(Player):
|
||||
default_rank = 0
|
||||
|
||||
def rank_for_tag(self, tag: AVTag) -> int | None:
|
||||
|
@ -324,7 +324,7 @@ def retryWait(proc: subprocess.Popen) -> int:
|
|||
##########################################################################
|
||||
|
||||
|
||||
class SimpleProcessPlayer(Player): # pylint: disable=abstract-method
|
||||
class SimpleProcessPlayer(Player):
|
||||
"A player that invokes a new process for each tag to play."
|
||||
|
||||
args: list[str] = []
|
||||
|
|
|
@ -208,7 +208,7 @@ class CustomStyles:
|
|||
button_pressed_gradient(
|
||||
tm.var(colors.BUTTON_GRADIENT_START),
|
||||
tm.var(colors.BUTTON_GRADIENT_END),
|
||||
tm.var(colors.SHADOW)
|
||||
tm.var(colors.SHADOW),
|
||||
)
|
||||
};
|
||||
}}
|
||||
|
@ -340,7 +340,7 @@ class CustomStyles:
|
|||
}}
|
||||
QTabBar::tab:selected:hover {{
|
||||
background: {
|
||||
button_gradient(
|
||||
button_gradient(
|
||||
tm.var(colors.BUTTON_PRIMARY_GRADIENT_START),
|
||||
tm.var(colors.BUTTON_PRIMARY_GRADIENT_END),
|
||||
)
|
||||
|
@ -391,7 +391,7 @@ class CustomStyles:
|
|||
button_pressed_gradient(
|
||||
tm.var(colors.BUTTON_GRADIENT_START),
|
||||
tm.var(colors.BUTTON_GRADIENT_END),
|
||||
tm.var(colors.SHADOW)
|
||||
tm.var(colors.SHADOW),
|
||||
)
|
||||
}
|
||||
}}
|
||||
|
@ -647,10 +647,12 @@ class CustomStyles:
|
|||
margin: -7px 0;
|
||||
}}
|
||||
QSlider::handle:hover {{
|
||||
background: {button_gradient(
|
||||
tm.var(colors.BUTTON_GRADIENT_START),
|
||||
tm.var(colors.BUTTON_GRADIENT_END),
|
||||
)}
|
||||
background: {
|
||||
button_gradient(
|
||||
tm.var(colors.BUTTON_GRADIENT_START),
|
||||
tm.var(colors.BUTTON_GRADIENT_END),
|
||||
)
|
||||
}
|
||||
}}
|
||||
"""
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ def get_sync_status(
|
|||
) -> None:
|
||||
auth = mw.pm.sync_auth()
|
||||
if not auth:
|
||||
callback(SyncStatus(required=SyncStatus.NO_CHANGES)) # pylint:disable=no-member
|
||||
callback(SyncStatus(required=SyncStatus.NO_CHANGES))
|
||||
return
|
||||
|
||||
def on_future_done(fut: Future[SyncStatus]) -> None:
|
||||
|
@ -302,7 +302,6 @@ def sync_login(
|
|||
username: str = "",
|
||||
password: str = "",
|
||||
) -> None:
|
||||
|
||||
def on_future_done(fut: Future[SyncAuth], username: str, password: str) -> None:
|
||||
try:
|
||||
auth = fut.result()
|
||||
|
@ -374,7 +373,9 @@ def get_id_and_pass_from_user(
|
|||
g.addWidget(passwd, 1, 1)
|
||||
l2.setBuddy(passwd)
|
||||
vbox.addLayout(g)
|
||||
bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) # type: ignore
|
||||
bb = QDialogButtonBox(
|
||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
) # type: ignore
|
||||
ok_button = bb.button(QDialogButtonBox.StandardButton.Ok)
|
||||
assert ok_button is not None
|
||||
ok_button.setAutoDefault(True)
|
||||
|
|
|
@ -187,7 +187,7 @@ class ThemeManager:
|
|||
self, card_ord: int, night_mode: bool | None = None
|
||||
) -> str:
|
||||
"Returns body classes used when showing a card."
|
||||
return f"card card{card_ord+1} {self.body_class(night_mode, reviewer=True)}"
|
||||
return f"card card{card_ord + 1} {self.body_class(night_mode, reviewer=True)}"
|
||||
|
||||
def var(self, vars: dict[str, str]) -> str:
|
||||
"""Given day/night colors/props, return the correct one for the current theme."""
|
||||
|
@ -213,13 +213,12 @@ class ThemeManager:
|
|||
return False
|
||||
elif theme == Theme.DARK:
|
||||
return True
|
||||
elif is_win:
|
||||
return get_windows_dark_mode()
|
||||
elif is_mac:
|
||||
return get_macos_dark_mode()
|
||||
else:
|
||||
if is_win:
|
||||
return get_windows_dark_mode()
|
||||
elif is_mac:
|
||||
return get_macos_dark_mode()
|
||||
else:
|
||||
return get_linux_dark_mode()
|
||||
return get_linux_dark_mode()
|
||||
|
||||
def apply_style(self) -> None:
|
||||
"Apply currently configured style."
|
||||
|
@ -340,7 +339,7 @@ def get_windows_dark_mode() -> bool:
|
|||
if not is_win:
|
||||
return False
|
||||
|
||||
from winreg import ( # type: ignore[attr-defined] # pylint: disable=import-error
|
||||
from winreg import ( # type: ignore[attr-defined]
|
||||
HKEY_CURRENT_USER,
|
||||
OpenKey,
|
||||
QueryValueEx,
|
||||
|
@ -352,7 +351,7 @@ def get_windows_dark_mode() -> bool:
|
|||
r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize",
|
||||
)
|
||||
return not QueryValueEx(key, "AppsUseLightTheme")[0]
|
||||
except Exception as err:
|
||||
except Exception:
|
||||
# key reportedly missing or set to wrong type on some systems
|
||||
return False
|
||||
|
||||
|
@ -416,12 +415,12 @@ def get_linux_dark_mode() -> bool:
|
|||
capture_output=True,
|
||||
encoding="utf8",
|
||||
)
|
||||
except FileNotFoundError as e:
|
||||
except FileNotFoundError:
|
||||
# detection strategy failed, missing program
|
||||
# print(e)
|
||||
continue
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
except subprocess.CalledProcessError:
|
||||
# detection strategy failed, command returned error
|
||||
# print(e)
|
||||
continue
|
||||
|
|
|
@ -166,7 +166,6 @@ class MacVoice(TTSVoice):
|
|||
original_name: str
|
||||
|
||||
|
||||
# pylint: disable=no-member
|
||||
class MacTTSPlayer(TTSProcessPlayer):
|
||||
"Invokes a process to play the audio in the background."
|
||||
|
||||
|
@ -487,7 +486,7 @@ if is_win:
|
|||
class WindowsTTSPlayer(TTSProcessPlayer):
|
||||
default_rank = -1
|
||||
try:
|
||||
import win32com.client # pylint: disable=import-error
|
||||
import win32com.client
|
||||
|
||||
speaker = win32com.client.Dispatch("SAPI.SpVoice")
|
||||
except Exception as exc:
|
||||
|
|
|
@ -15,14 +15,12 @@ from functools import partial, wraps
|
|||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Literal, Union
|
||||
|
||||
import requests
|
||||
from send2trash import send2trash
|
||||
|
||||
import aqt
|
||||
import requests
|
||||
from anki._legacy import DeprecatedNamesMixinForModule
|
||||
from anki.collection import Collection, HelpPage
|
||||
from anki.httpclient import HttpClient
|
||||
from anki.lang import TR, tr_legacyglobal # pylint: disable=unused-import
|
||||
from anki.lang import TR, tr_legacyglobal # noqa: F401
|
||||
from anki.utils import (
|
||||
call,
|
||||
invalid_filename,
|
||||
|
@ -32,9 +30,9 @@ from anki.utils import (
|
|||
version_with_build,
|
||||
)
|
||||
from aqt.qt import *
|
||||
from aqt.qt import QT_VERSION_STR # noqa: F401
|
||||
from aqt.qt import (
|
||||
PYQT_VERSION_STR,
|
||||
QT_VERSION_STR,
|
||||
QAction,
|
||||
QApplication,
|
||||
QCheckBox,
|
||||
|
@ -84,6 +82,7 @@ from aqt.qt import (
|
|||
traceback,
|
||||
)
|
||||
from aqt.theme import theme_manager
|
||||
from send2trash import send2trash
|
||||
|
||||
if TYPE_CHECKING:
|
||||
TextFormat = Literal["plain", "rich", "markdown"]
|
||||
|
@ -340,7 +339,7 @@ def showInfo(
|
|||
icon = QMessageBox.Icon.Critical
|
||||
else:
|
||||
icon = QMessageBox.Icon.Information
|
||||
mb = QMessageBox(parent_widget) #
|
||||
mb = QMessageBox(parent_widget)
|
||||
if textFormat == "plain":
|
||||
mb.setTextFormat(Qt.TextFormat.PlainText)
|
||||
elif textFormat == "rich":
|
||||
|
@ -1013,9 +1012,8 @@ def show_in_folder(path: str) -> None:
|
|||
|
||||
|
||||
def _show_in_folder_win32(path: str) -> None:
|
||||
import win32con # pylint: disable=import-error
|
||||
import win32gui # pylint: disable=import-error
|
||||
|
||||
import win32con
|
||||
import win32gui
|
||||
from aqt import mw
|
||||
|
||||
def focus_explorer():
|
||||
|
@ -1309,12 +1307,12 @@ def opengl_vendor() -> str | None:
|
|||
# Can't use versionFunctions there
|
||||
return None
|
||||
|
||||
vp = QOpenGLVersionProfile() # type: ignore # pylint: disable=undefined-variable
|
||||
vp = QOpenGLVersionProfile() # type: ignore
|
||||
vp.setVersion(2, 0)
|
||||
|
||||
try:
|
||||
vf = ctx.versionFunctions(vp) # type: ignore
|
||||
except ImportError as e:
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
if vf is None:
|
||||
|
|
|
@ -980,7 +980,6 @@ def _create_ankiwebview_subclass(
|
|||
/,
|
||||
**fixed_kwargs: Unpack[_AnkiWebViewKwargs],
|
||||
) -> Type[AnkiWebView]:
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: _AnkiWebViewKwargs) -> None:
|
||||
# user‑supplied kwargs override fixed kwargs
|
||||
merged = cast(_AnkiWebViewKwargs, {**fixed_kwargs, **kwargs})
|
||||
|
|
|
@ -100,7 +100,7 @@ _SHGetFolderPath.restype = _err_unless_zero
|
|||
|
||||
def _get_path_buf(csidl):
|
||||
path_buf = ctypes.create_unicode_buffer(wintypes.MAX_PATH)
|
||||
result = _SHGetFolderPath(0, csidl, 0, 0, path_buf)
|
||||
_SHGetFolderPath(0, csidl, 0, 0, path_buf)
|
||||
return path_buf.value
|
||||
|
||||
|
||||
|
|
|
@ -15,7 +15,8 @@ camino.workspace = true
|
|||
dirs.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winapi.workspace = true
|
||||
windows.workspace = true
|
||||
widestring.workspace = true
|
||||
libc.workspace = true
|
||||
libc-stdhandle.workspace = true
|
||||
|
||||
|
|
135
qt/launcher/addon/__init__.py
Normal file
135
qt/launcher/addon/__init__.py
Normal file
|
@ -0,0 +1,135 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import aqt.sound
|
||||
from anki.utils import pointVersion
|
||||
from aqt import mw
|
||||
from aqt.qt import QAction
|
||||
from aqt.utils import askUser, is_mac, is_win, showInfo
|
||||
|
||||
|
||||
def _anki_launcher_path() -> str | None:
|
||||
return os.getenv("ANKI_LAUNCHER")
|
||||
|
||||
|
||||
def have_launcher() -> bool:
|
||||
return _anki_launcher_path() is not None
|
||||
|
||||
|
||||
def update_and_restart() -> None:
|
||||
from aqt import mw
|
||||
|
||||
launcher = _anki_launcher_path()
|
||||
assert launcher
|
||||
|
||||
_trigger_launcher_run()
|
||||
|
||||
with contextlib.suppress(ResourceWarning):
|
||||
env = os.environ.copy()
|
||||
creationflags = 0
|
||||
if sys.platform == "win32":
|
||||
creationflags = (
|
||||
subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
|
||||
)
|
||||
subprocess.Popen(
|
||||
[launcher],
|
||||
start_new_session=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
env=env,
|
||||
creationflags=creationflags,
|
||||
)
|
||||
|
||||
mw.app.quit()
|
||||
|
||||
|
||||
def _trigger_launcher_run() -> None:
|
||||
"""Bump the mtime on pyproject.toml in the local data directory to trigger an update on next run."""
|
||||
try:
|
||||
# Get the local data directory equivalent to Rust's dirs::data_local_dir()
|
||||
if is_win:
|
||||
from aqt.winpaths import get_local_appdata
|
||||
|
||||
data_dir = Path(get_local_appdata())
|
||||
elif is_mac:
|
||||
data_dir = Path.home() / "Library" / "Application Support"
|
||||
else: # Linux
|
||||
data_dir = Path(
|
||||
os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share")
|
||||
)
|
||||
|
||||
pyproject_path = data_dir / "AnkiProgramFiles" / "pyproject.toml"
|
||||
|
||||
if pyproject_path.exists():
|
||||
# Touch the file to update its mtime
|
||||
pyproject_path.touch()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
|
||||
def confirm_then_upgrade():
|
||||
if not askUser("Change to a different Anki version?"):
|
||||
return
|
||||
update_and_restart()
|
||||
|
||||
|
||||
# return modified command array that points to bundled command, and return
|
||||
# required environment
|
||||
def _packagedCmd(cmd: list[str]) -> tuple[Any, dict[str, str]]:
|
||||
cmd = cmd[:]
|
||||
env = os.environ.copy()
|
||||
# keep LD_LIBRARY_PATH when in snap environment
|
||||
if "LD_LIBRARY_PATH" in env and "SNAP" not in env:
|
||||
del env["LD_LIBRARY_PATH"]
|
||||
|
||||
# Try to find binary in anki-audio package for Windows/Mac
|
||||
if is_win or is_mac:
|
||||
try:
|
||||
import anki_audio
|
||||
|
||||
audio_pkg_path = Path(anki_audio.__file__).parent
|
||||
if is_win:
|
||||
packaged_path = audio_pkg_path / (cmd[0] + ".exe")
|
||||
else: # is_mac
|
||||
packaged_path = audio_pkg_path / cmd[0]
|
||||
|
||||
if packaged_path.exists():
|
||||
cmd[0] = str(packaged_path)
|
||||
return cmd, env
|
||||
except ImportError:
|
||||
# anki-audio not available, fall back to old behavior
|
||||
pass
|
||||
|
||||
packaged_path = Path(sys.prefix) / cmd[0]
|
||||
if packaged_path.exists():
|
||||
cmd[0] = str(packaged_path)
|
||||
|
||||
return cmd, env
|
||||
|
||||
|
||||
def setup():
|
||||
if pointVersion() >= 250600:
|
||||
return
|
||||
if not have_launcher():
|
||||
return
|
||||
|
||||
# Add action to tools menu
|
||||
action = QAction("Upgrade/Downgrade", mw)
|
||||
action.triggered.connect(confirm_then_upgrade)
|
||||
mw.form.menuTools.addAction(action)
|
||||
|
||||
# Monkey-patch audio tools to use anki-audio
|
||||
if is_win or is_mac:
|
||||
aqt.sound._packagedCmd = _packagedCmd
|
||||
|
||||
|
||||
setup()
|
6
qt/launcher/addon/manifest.json
Normal file
6
qt/launcher/addon/manifest.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "Anki Launcher",
|
||||
"package": "anki-launcher",
|
||||
"min_point_version": 50,
|
||||
"max_point_version": 250600
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue