mirror of
https://github.com/ankitects/anki.git
synced 2026-01-13 05:53:53 -05:00
Merge branch 'main' into Add-last_review_time-field-to-card-data
This commit is contained in:
commit
cca414a09f
228 changed files with 3208 additions and 2806 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.06b5
|
||||
25.07.1
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Please do this as a final step before marking a task as completed.
|
|||
During development, you can build/check subsections of our code:
|
||||
|
||||
- Rust: 'cargo check'
|
||||
- Python: './tools/dmypy'
|
||||
- Python: './tools/dmypy', and if wheel-related, './ninja wheels'
|
||||
- TypeScript/Svelte: './ninja check:svelte'
|
||||
|
||||
Be mindful that some changes (such as modifications to .proto files) may
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ Jakub Kaczmarzyk <jakub.kaczmarzyk@gmail.com>
|
|||
Akshara Balachandra <akshara.bala.18@gmail.com>
|
||||
lukkea <github.com/lukkea/>
|
||||
David Allison <davidallisongithub@gmail.com>
|
||||
David Allison <62114487+david-allison@users.noreply.github.com>
|
||||
Tsung-Han Yu <johan456789@gmail.com>
|
||||
Piotr Kubowicz <piotr.kubowicz@gmail.com>
|
||||
RumovZ <gp5glkw78@relay.firefox.com>
|
||||
|
|
@ -232,6 +233,7 @@ Spiritual Father <https://github.com/spiritualfather>
|
|||
Emmanuel Ferdman <https://github.com/emmanuel-ferdman>
|
||||
Sunong2008 <https://github.com/Sunrongguo2008>
|
||||
Marvin Kopf <marvinkopf@outlook.com>
|
||||
Kevin Nakamura <grinkers@grinkers.net>
|
||||
********************
|
||||
|
||||
The text of the 3 clause BSD license follows:
|
||||
|
|
|
|||
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -94,6 +94,7 @@ dependencies = [
|
|||
"axum",
|
||||
"axum-client-ip",
|
||||
"axum-extra",
|
||||
"bitflags 2.9.1",
|
||||
"blake3",
|
||||
"bytes",
|
||||
"chrono",
|
||||
|
|
@ -3543,11 +3544,13 @@ dependencies = [
|
|||
"anki_io",
|
||||
"anki_process",
|
||||
"anyhow",
|
||||
"camino",
|
||||
"dirs 6.0.0",
|
||||
"embed-resource",
|
||||
"libc",
|
||||
"libc-stdhandle",
|
||||
"winapi",
|
||||
"widestring",
|
||||
"windows 0.61.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -7374,6 +7377,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"
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ async-trait = "0.1.88"
|
|||
axum = { version = "0.8.4", features = ["multipart", "macros"] }
|
||||
axum-client-ip = "1.1.3"
|
||||
axum-extra = { version = "0.10.1", features = ["typed-header"] }
|
||||
bitflags = "2.9.1"
|
||||
blake3 = "1.8.2"
|
||||
bytes = "1.10.1"
|
||||
camino = "1.1.10"
|
||||
|
|
@ -138,8 +139,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", "errhandlingapi", "consoleapi"] }
|
||||
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"] }
|
||||
|
|
|
|||
2
LICENSE
2
LICENSE
|
|
@ -6,8 +6,6 @@ The following included source code items use a license other than AGPL3:
|
|||
|
||||
In the pylib folder:
|
||||
|
||||
* The SuperMemo importer: GPL3 and 0BSD.
|
||||
* The Pauker importer: BSD-3.
|
||||
* statsbg.py: CC BY 4.0.
|
||||
|
||||
In the qt folder:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Anki
|
||||
# Anki®
|
||||
|
||||
[](https://buildkite.com/ankitects/anki-ci)
|
||||
|
||||
|
|
|
|||
|
|
@ -342,7 +342,12 @@ fn build_wheel(build: &mut Build) -> Result<()> {
|
|||
name: "aqt",
|
||||
version: anki_version(),
|
||||
platform: None,
|
||||
deps: inputs![":qt:aqt", glob!("qt/aqt/**"), "qt/pyproject.toml"],
|
||||
deps: inputs![
|
||||
":qt:aqt",
|
||||
glob!("qt/aqt/**"),
|
||||
"qt/pyproject.toml",
|
||||
"qt/hatch_build.py"
|
||||
],
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,8 @@ pub fn build_pylib(build: &mut Build) -> Result<()> {
|
|||
deps: inputs![
|
||||
":pylib:anki",
|
||||
glob!("pylib/anki/**"),
|
||||
"pylib/pyproject.toml"
|
||||
"pylib/pyproject.toml",
|
||||
"pylib/hatch_build.py"
|
||||
],
|
||||
},
|
||||
)?;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -35,3 +35,7 @@ path = "src/bin/update_uv.rs"
|
|||
[[bin]]
|
||||
name = "update_protoc"
|
||||
path = "src/bin/update_protoc.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "update_node"
|
||||
path = "src/bin/update_node.rs"
|
||||
|
|
|
|||
268
build/ninja_gen/src/bin/update_node.rs
Normal file
268
build/ninja_gen/src/bin/update_node.rs
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use regex::Regex;
|
||||
use reqwest::blocking::Client;
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct NodeRelease {
|
||||
version: String,
|
||||
files: Vec<NodeFile>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct NodeFile {
|
||||
filename: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let release_info = fetch_node_release_info()?;
|
||||
let new_text = generate_node_archive_function(&release_info)?;
|
||||
update_node_text(&new_text)?;
|
||||
println!("Node.js archive function updated successfully!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fetch_node_release_info() -> Result<NodeRelease, Box<dyn Error>> {
|
||||
let client = Client::new();
|
||||
|
||||
// Get the Node.js release info
|
||||
let response = client
|
||||
.get("https://nodejs.org/dist/index.json")
|
||||
.header("User-Agent", "anki-build-updater")
|
||||
.send()?;
|
||||
|
||||
let releases: Vec<Value> = response.json()?;
|
||||
|
||||
// Find the latest LTS release
|
||||
let latest = releases
|
||||
.iter()
|
||||
.find(|release| {
|
||||
// LTS releases have a non-false "lts" field
|
||||
release["lts"].as_str().is_some() && release["lts"] != false
|
||||
})
|
||||
.ok_or("No LTS releases found")?;
|
||||
|
||||
let version = latest["version"]
|
||||
.as_str()
|
||||
.ok_or("Version not found")?
|
||||
.to_string();
|
||||
|
||||
let files = latest["files"]
|
||||
.as_array()
|
||||
.ok_or("Files array not found")?
|
||||
.iter()
|
||||
.map(|f| f.as_str().unwrap_or(""))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let lts_name = latest["lts"].as_str().unwrap_or("unknown");
|
||||
println!("Found Node.js LTS version: {version} ({lts_name})");
|
||||
|
||||
// Map platforms to their expected file keys and full filenames
|
||||
let platform_mapping = vec![
|
||||
(
|
||||
"linux-x64",
|
||||
"linux-x64",
|
||||
format!("node-{version}-linux-x64.tar.xz"),
|
||||
),
|
||||
(
|
||||
"linux-arm64",
|
||||
"linux-arm64",
|
||||
format!("node-{version}-linux-arm64.tar.xz"),
|
||||
),
|
||||
(
|
||||
"darwin-x64",
|
||||
"osx-x64-tar",
|
||||
format!("node-{version}-darwin-x64.tar.xz"),
|
||||
),
|
||||
(
|
||||
"darwin-arm64",
|
||||
"osx-arm64-tar",
|
||||
format!("node-{version}-darwin-arm64.tar.xz"),
|
||||
),
|
||||
(
|
||||
"win-x64",
|
||||
"win-x64-zip",
|
||||
format!("node-{version}-win-x64.zip"),
|
||||
),
|
||||
(
|
||||
"win-arm64",
|
||||
"win-arm64-zip",
|
||||
format!("node-{version}-win-arm64.zip"),
|
||||
),
|
||||
];
|
||||
|
||||
let mut node_files = Vec::new();
|
||||
|
||||
for (platform, file_key, filename) in platform_mapping {
|
||||
// Check if this file exists in the release
|
||||
if files.contains(&file_key) {
|
||||
let url = format!("https://nodejs.org/dist/{version}/{filename}");
|
||||
node_files.push(NodeFile {
|
||||
filename: filename.clone(),
|
||||
url,
|
||||
});
|
||||
println!("Found file for {platform}: {filename} (key: {file_key})");
|
||||
} else {
|
||||
return Err(
|
||||
format!("File not found for {platform} (key: {file_key}): {filename}").into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(NodeRelease {
|
||||
version,
|
||||
files: node_files,
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_node_archive_function(release: &NodeRelease) -> Result<String, Box<dyn Error>> {
|
||||
let client = Client::new();
|
||||
|
||||
// Fetch the SHASUMS256.txt file once
|
||||
println!("Fetching SHA256 checksums...");
|
||||
let shasums_url = format!("https://nodejs.org/dist/{}/SHASUMS256.txt", release.version);
|
||||
let shasums_response = client
|
||||
.get(&shasums_url)
|
||||
.header("User-Agent", "anki-build-updater")
|
||||
.send()?;
|
||||
let shasums_text = shasums_response.text()?;
|
||||
|
||||
// Create a mapping from filename patterns to platform names - using the exact
|
||||
// patterns we stored in files
|
||||
let platform_mapping = vec![
|
||||
("linux-x64.tar.xz", "LinuxX64"),
|
||||
("linux-arm64.tar.xz", "LinuxArm"),
|
||||
("darwin-x64.tar.xz", "MacX64"),
|
||||
("darwin-arm64.tar.xz", "MacArm"),
|
||||
("win-x64.zip", "WindowsX64"),
|
||||
("win-arm64.zip", "WindowsArm"),
|
||||
];
|
||||
|
||||
let mut platform_blocks = Vec::new();
|
||||
|
||||
for (file_pattern, platform_name) in platform_mapping {
|
||||
// Find the file that ends with this pattern
|
||||
if let Some(file) = release
|
||||
.files
|
||||
.iter()
|
||||
.find(|f| f.filename.ends_with(file_pattern))
|
||||
{
|
||||
// Find the SHA256 for this file
|
||||
let sha256 = shasums_text
|
||||
.lines()
|
||||
.find(|line| line.contains(&file.filename))
|
||||
.and_then(|line| line.split_whitespace().next())
|
||||
.ok_or_else(|| format!("SHA256 not found for {}", file.filename))?;
|
||||
|
||||
println!(
|
||||
"Found SHA256 for {}: {} => {}",
|
||||
platform_name, file.filename, sha256
|
||||
);
|
||||
|
||||
let block = format!(
|
||||
" Platform::{} => OnlineArchive {{\n url: \"{}\",\n sha256: \"{}\",\n }},",
|
||||
platform_name, file.url, sha256
|
||||
);
|
||||
platform_blocks.push(block);
|
||||
} else {
|
||||
return Err(format!(
|
||||
"File not found for platform {platform_name}: no file ending with {file_pattern}"
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
let function = format!(
|
||||
"pub fn node_archive(platform: Platform) -> OnlineArchive {{\n match platform {{\n{}\n }}\n}}",
|
||||
platform_blocks.join("\n")
|
||||
);
|
||||
|
||||
Ok(function)
|
||||
}
|
||||
|
||||
fn update_node_text(new_function: &str) -> Result<(), Box<dyn Error>> {
|
||||
let node_rs_content = read_node_rs()?;
|
||||
|
||||
// Regex to match the entire node_archive function with proper multiline
|
||||
// matching
|
||||
let re = Regex::new(
|
||||
r"(?s)pub fn node_archive\(platform: Platform\) -> OnlineArchive \{.*?\n\s*\}\s*\n\s*\}",
|
||||
)?;
|
||||
|
||||
let updated_content = re.replace(&node_rs_content, new_function);
|
||||
|
||||
write_node_rs(&updated_content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_node_rs() -> Result<String, Box<dyn Error>> {
|
||||
// Use CARGO_MANIFEST_DIR to get the crate root, then find src/node.rs
|
||||
let manifest_dir =
|
||||
std::env::var("CARGO_MANIFEST_DIR").map_err(|_| "CARGO_MANIFEST_DIR not set")?;
|
||||
let path = Path::new(&manifest_dir).join("src").join("node.rs");
|
||||
Ok(fs::read_to_string(path)?)
|
||||
}
|
||||
|
||||
fn write_node_rs(content: &str) -> Result<(), Box<dyn Error>> {
|
||||
// Use CARGO_MANIFEST_DIR to get the crate root, then find src/node.rs
|
||||
let manifest_dir =
|
||||
std::env::var("CARGO_MANIFEST_DIR").map_err(|_| "CARGO_MANIFEST_DIR not set")?;
|
||||
let path = Path::new(&manifest_dir).join("src").join("node.rs");
|
||||
fs::write(path, content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_regex_replacement() {
|
||||
let sample_content = r#"Some other code
|
||||
pub fn node_archive(platform: Platform) -> OnlineArchive {
|
||||
match platform {
|
||||
Platform::LinuxX64 => OnlineArchive {
|
||||
url: "https://nodejs.org/dist/v20.11.0/node-v20.11.0-linux-x64.tar.xz",
|
||||
sha256: "old_hash",
|
||||
},
|
||||
Platform::MacX64 => OnlineArchive {
|
||||
url: "https://nodejs.org/dist/v20.11.0/node-v20.11.0-darwin-x64.tar.xz",
|
||||
sha256: "old_hash",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
More code here"#;
|
||||
|
||||
let new_function = r#"pub fn node_archive(platform: Platform) -> OnlineArchive {
|
||||
match platform {
|
||||
Platform::LinuxX64 => OnlineArchive {
|
||||
url: "https://nodejs.org/dist/v21.0.0/node-v21.0.0-linux-x64.tar.xz",
|
||||
sha256: "new_hash",
|
||||
},
|
||||
Platform::MacX64 => OnlineArchive {
|
||||
url: "https://nodejs.org/dist/v21.0.0/node-v21.0.0-darwin-x64.tar.xz",
|
||||
sha256: "new_hash",
|
||||
},
|
||||
}
|
||||
}"#;
|
||||
|
||||
let re = Regex::new(
|
||||
r"(?s)pub fn node_archive\(platform: Platform\) -> OnlineArchive \{.*?\n\s*\}\s*\n\s*\}"
|
||||
).unwrap();
|
||||
|
||||
let result = re.replace(sample_content, new_function);
|
||||
assert!(result.contains("v21.0.0"));
|
||||
assert!(result.contains("new_hash"));
|
||||
assert!(!result.contains("old_hash"));
|
||||
assert!(result.contains("Some other code"));
|
||||
assert!(result.contains("More code here"));
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,28 +19,28 @@ use crate::input::BuildInput;
|
|||
pub fn node_archive(platform: Platform) -> OnlineArchive {
|
||||
match platform {
|
||||
Platform::LinuxX64 => OnlineArchive {
|
||||
url: "https://nodejs.org/dist/v20.11.0/node-v20.11.0-linux-x64.tar.xz",
|
||||
sha256: "822780369d0ea309e7d218e41debbd1a03f8cdf354ebf8a4420e89f39cc2e612",
|
||||
url: "https://nodejs.org/dist/v22.17.0/node-v22.17.0-linux-x64.tar.xz",
|
||||
sha256: "325c0f1261e0c61bcae369a1274028e9cfb7ab7949c05512c5b1e630f7e80e12",
|
||||
},
|
||||
Platform::LinuxArm => OnlineArchive {
|
||||
url: "https://nodejs.org/dist/v20.11.0/node-v20.11.0-linux-arm64.tar.xz",
|
||||
sha256: "f6df68c6793244071f69023a9b43a0cf0b13d65cbe86d55925c28e4134d9aafb",
|
||||
url: "https://nodejs.org/dist/v22.17.0/node-v22.17.0-linux-arm64.tar.xz",
|
||||
sha256: "140aee84be6774f5fb3f404be72adbe8420b523f824de82daeb5ab218dab7b18",
|
||||
},
|
||||
Platform::MacX64 => OnlineArchive {
|
||||
url: "https://nodejs.org/dist/v20.11.0/node-v20.11.0-darwin-x64.tar.xz",
|
||||
sha256: "d4b4ab81ebf1f7aab09714f834992f27270ad0079600da00c8110f8950ca6c5a",
|
||||
url: "https://nodejs.org/dist/v22.17.0/node-v22.17.0-darwin-x64.tar.xz",
|
||||
sha256: "f79de1f64df4ac68493a344bb5ab7d289d0275271e87b543d1278392c9de778a",
|
||||
},
|
||||
Platform::MacArm => OnlineArchive {
|
||||
url: "https://nodejs.org/dist/v20.11.0/node-v20.11.0-darwin-arm64.tar.xz",
|
||||
sha256: "f18a7438723d48417f5e9be211a2f3c0520ffbf8e02703469e5153137ca0f328",
|
||||
url: "https://nodejs.org/dist/v22.17.0/node-v22.17.0-darwin-arm64.tar.xz",
|
||||
sha256: "cc9cc294eaf782dd93c8c51f460da610cc35753c6a9947411731524d16e97914",
|
||||
},
|
||||
Platform::WindowsX64 => OnlineArchive {
|
||||
url: "https://nodejs.org/dist/v20.11.0/node-v20.11.0-win-x64.zip",
|
||||
sha256: "893115cd92ad27bf178802f15247115e93c0ef0c753b93dca96439240d64feb5",
|
||||
url: "https://nodejs.org/dist/v22.17.0/node-v22.17.0-win-x64.zip",
|
||||
sha256: "721ab118a3aac8584348b132767eadf51379e0616f0db802cc1e66d7f0d98f85",
|
||||
},
|
||||
Platform::WindowsArm => OnlineArchive {
|
||||
url: "https://nodejs.org/dist/v20.11.0/node-v20.11.0-win-arm64.zip",
|
||||
sha256: "89c1f7034dcd6ff5c17f2af61232a96162a1902f862078347dcf274a938b6142",
|
||||
url: "https://nodejs.org/dist/v22.17.0/node-v22.17.0-win-arm64.zip",
|
||||
sha256: "78355dc9ca117bb71d3f081e4b1b281855e2b134f3939bb0ca314f7567b0e621",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -159,6 +159,10 @@ impl BuildAction for PythonEnvironment {
|
|||
}
|
||||
build.add_output_stamp(format!("{}/.stamp", self.venv_folder));
|
||||
}
|
||||
|
||||
fn check_output_timestamps(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PythonTypecheck {
|
||||
|
|
@ -189,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}",
|
||||
|
|
@ -223,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,
|
||||
},
|
||||
)?;
|
||||
|
||||
|
|
@ -238,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 {
|
||||
|
|
|
|||
|
|
@ -67,7 +67,10 @@ pub fn run_build(args: BuildArgs) {
|
|||
"MYPY_CACHE_DIR",
|
||||
build_root.join("tests").join("mypy").into_string(),
|
||||
)
|
||||
.env("PYTHONPYCACHEPREFIX", build_root.join("pycache"))
|
||||
.env(
|
||||
"PYTHONPYCACHEPREFIX",
|
||||
std::path::absolute(build_root.join("pycache")).unwrap(),
|
||||
)
|
||||
// commands will not show colors by default, as we do not provide a tty
|
||||
.env("FORCE_COLOR", "1")
|
||||
.env("MYPY_FORCE_COLOR", "1")
|
||||
|
|
@ -135,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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ pub fn setup_pyenv(args: PyenvArgs) {
|
|||
run_command(
|
||||
Command::new(args.uv_bin)
|
||||
.env("UV_PROJECT_ENVIRONMENT", args.pyenv_folder.clone())
|
||||
.args(["sync", "--frozen"])
|
||||
.args(["sync", "--locked"])
|
||||
.args(args.extra_args),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 2f8c9d9566aef8b86e3326fe9ff007d594b7ec83
|
||||
Subproject commit a9216499ba1fb1538cfd740c698adaaa3410fd4b
|
||||
|
|
@ -60,7 +60,6 @@ card-templates-this-will-create-card-proceed =
|
|||
}
|
||||
card-templates-type-boxes-warning = Only one typing box per card template is supported.
|
||||
card-templates-restore-to-default = Restore to Default
|
||||
card-templates-restore-to-default-confirmation = This will reset all fields and templates in this note type to their default
|
||||
values, removing any extra fields/templates and their content, and any custom styling. Do you wish to proceed?
|
||||
card-templates-restore-to-default-confirmation = This will reset all fields and templates in this note type to their default values, removing any extra fields/templates and their content, and any custom styling. Do you wish to proceed?
|
||||
card-templates-restored-to-default = Note type has been restored to its original state.
|
||||
|
||||
|
|
|
|||
|
|
@ -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 info box 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
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ importing-with-deck-configs-help =
|
|||
If enabled, any deck options that the deck sharer included will also be imported.
|
||||
Otherwise, all decks will be assigned the default preset.
|
||||
importing-packaged-anki-deckcollection-apkg-colpkg-zip = Packaged Anki Deck/Collection (*.apkg *.colpkg *.zip)
|
||||
importing-pauker-18-lesson-paugz = Pauker 1.8 Lesson (*.pau.gz)
|
||||
# the '|' character
|
||||
importing-pipe = Pipe
|
||||
# Warning displayed when the csv import preview table is clipped (some columns were hidden)
|
||||
|
|
@ -78,7 +77,6 @@ importing-rows-had-num1d-fields-expected-num2d = '{ $row }' had { $found } field
|
|||
importing-selected-file-was-not-in-utf8 = Selected file was not in UTF-8 format. Please see the importing section of the manual.
|
||||
importing-semicolon = Semicolon
|
||||
importing-skipped = Skipped
|
||||
importing-supermemo-xml-export-xml = Supermemo XML export (*.xml)
|
||||
importing-tab = Tab
|
||||
importing-tag-modified-notes = Tag modified notes:
|
||||
importing-text-separated-by-tabs-or-semicolons = Text separated by tabs or semicolons (*)
|
||||
|
|
@ -252,3 +250,5 @@ importing-importing-collection = Importing collection...
|
|||
importing-unable-to-import-filename = Unable to import { $filename }: file type not supported
|
||||
importing-notes-that-could-not-be-imported = Notes that could not be imported as note type has changed: { $val }
|
||||
importing-added = Added
|
||||
importing-pauker-18-lesson-paugz = Pauker 1.8 Lesson (*.pau.gz)
|
||||
importing-supermemo-xml-export-xml = Supermemo XML export (*.xml)
|
||||
|
|
|
|||
|
|
@ -99,9 +99,9 @@ statistics-counts-relearning-cards = Relearning
|
|||
statistics-counts-title = Card Counts
|
||||
statistics-counts-separate-suspended-buried-cards = Separate suspended/buried cards
|
||||
|
||||
## True Retention represents your actual retention rate from past reviews, in
|
||||
## comparison to the "desired retention" parameter of FSRS, which forecasts
|
||||
## future retention. True Retention is the percentage of all reviewed cards
|
||||
## Retention rate represents your actual retention rate from past reviews, in
|
||||
## comparison to the "desired retention" setting of FSRS, which forecasts
|
||||
## future retention. Retention rate is the percentage of all reviewed cards
|
||||
## that were marked as "Hard," "Good," or "Easy" within a specific time period.
|
||||
##
|
||||
## Most of these strings are used as column / row headings in a table.
|
||||
|
|
@ -112,9 +112,9 @@ statistics-counts-separate-suspended-buried-cards = Separate suspended/buried ca
|
|||
## N.B. Stats cards may be very small on mobile devices and when the Stats
|
||||
## window is certain sizes.
|
||||
|
||||
statistics-true-retention-title = True Retention
|
||||
statistics-true-retention-title = Retention rate
|
||||
statistics-true-retention-subtitle = Pass rate of cards with an interval ≥ 1 day.
|
||||
statistics-true-retention-tooltip = If you are using FSRS, your true retention is expected to be close to your desired retention. Please keep in mind that data for a single day is noisy, so it's better to look at monthly data.
|
||||
statistics-true-retention-tooltip = If you are using FSRS, your retention rate is expected to be close to your desired retention. Please keep in mind that data for a single day is noisy, so it's better to look at monthly data.
|
||||
statistics-true-retention-range = Range
|
||||
statistics-true-retention-pass = Pass
|
||||
statistics-true-retention-fail = Fail
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 69f2dbaeba6f72ac62da0b35881f320603da5124
|
||||
Subproject commit a1134ab59d3d23468af2968741aa1f21d16ff308
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
qt-accel-about = &About
|
||||
qt-accel-about-mac = About Anki...
|
||||
qt-accel-cards = &Cards
|
||||
qt-accel-check-database = &Check Database
|
||||
qt-accel-check-media = Check &Media
|
||||
|
|
@ -45,3 +46,4 @@ qt-accel-zoom-editor-in = Zoom Editor &In
|
|||
qt-accel-zoom-editor-out = Zoom Editor &Out
|
||||
qt-accel-create-backup = Create &Backup
|
||||
qt-accel-load-backup = &Revert to Backup
|
||||
qt-accel-upgrade-downgrade = Upgrade/Downgrade
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ qt-misc-second =
|
|||
qt-misc-layout-auto-enabled = Responsive layout enabled
|
||||
qt-misc-layout-vertical-enabled = Vertical layout enabled
|
||||
qt-misc-layout-horizontal-enabled = Horizontal layout enabled
|
||||
qt-misc-please-restart-to-update-anki = Please restart Anki to update to the latest version.
|
||||
qt-misc-open-anki-launcher = Change to a different Anki version?
|
||||
|
||||
## deprecated- these strings will be removed in the future, and do not need
|
||||
## to be translated
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
21
package.json
21
package.json
|
|
@ -19,8 +19,8 @@
|
|||
"@poppanator/sveltekit-svg": "^5.0.0",
|
||||
"@sqltools/formatter": "^1.2.2",
|
||||
"@sveltejs/adapter-static": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.20.7",
|
||||
"@sveltejs/vite-plugin-svelte": "4.0.0",
|
||||
"@sveltejs/kit": "^2.22.2",
|
||||
"@sveltejs/vite-plugin-svelte": "5.1",
|
||||
"@types/bootstrap": "^5.0.12",
|
||||
"@types/codemirror": "^5.60.0",
|
||||
"@types/d3": "^7.0.0",
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
"@types/jqueryui": "^1.12.13",
|
||||
"@types/lodash-es": "^4.17.4",
|
||||
"@types/marked": "^5.0.0",
|
||||
"@types/node": "^20",
|
||||
"@types/node": "^22",
|
||||
"@typescript-eslint/eslint-plugin": "^5.60.1",
|
||||
"@typescript-eslint/parser": "^5.60.1",
|
||||
"caniuse-lite": "^1.0.30001431",
|
||||
|
|
@ -48,16 +48,16 @@
|
|||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.2",
|
||||
"sass": "<1.77",
|
||||
"svelte": "^5.17.3",
|
||||
"svelte-check": "^3.4.4",
|
||||
"svelte-preprocess": "^5.0.4",
|
||||
"svelte": "^5.34.9",
|
||||
"svelte-check": "^4.2.2",
|
||||
"svelte-preprocess": "^6.0.3",
|
||||
"svelte-preprocess-esbuild": "^3.0.1",
|
||||
"svgo": "^3.2.0",
|
||||
"tslib": "^2.0.3",
|
||||
"tsx": "^3.12.0",
|
||||
"tsx": "^4.8.1",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "5.4.19",
|
||||
"vitest": "^2"
|
||||
"vite": "6",
|
||||
"vitest": "^3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^1.2.1",
|
||||
|
|
@ -81,7 +81,8 @@
|
|||
},
|
||||
"resolutions": {
|
||||
"canvas": "npm:empty-npm-package@1.0.0",
|
||||
"cookie": "0.7.0"
|
||||
"cookie": "0.7.0",
|
||||
"vite": "6"
|
||||
},
|
||||
"browserslist": [
|
||||
"defaults",
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ message ConfigKey {
|
|||
RENDER_LATEX = 25;
|
||||
LOAD_BALANCER_ENABLED = 26;
|
||||
FSRS_SHORT_TERM_WITH_STEPS_ENABLED = 27;
|
||||
FSRS_LEGACY_EVALUATE = 28;
|
||||
}
|
||||
enum String {
|
||||
SET_DUE_BROWSER = 0;
|
||||
|
|
|
|||
|
|
@ -236,6 +236,7 @@ message DeckConfigsForUpdate {
|
|||
bool new_cards_ignore_review_limit = 7;
|
||||
bool fsrs = 8;
|
||||
bool fsrs_health_check = 11;
|
||||
bool fsrs_legacy_evaluate = 12;
|
||||
bool apply_all_parent_limits = 9;
|
||||
uint32 days_since_last_fsrs_optimize = 10;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ service SchedulerService {
|
|||
rpc SimulateFsrsReview(SimulateFsrsReviewRequest)
|
||||
returns (SimulateFsrsReviewResponse);
|
||||
rpc EvaluateParams(EvaluateParamsRequest) returns (EvaluateParamsResponse);
|
||||
rpc EvaluateParamsLegacy(EvaluateParamsLegacyRequest)
|
||||
returns (EvaluateParamsResponse);
|
||||
rpc ComputeMemoryState(cards.CardId) returns (ComputeMemoryStateResponse);
|
||||
// The number of days the calculated interval was fuzzed by on the previous
|
||||
// review (if any). Utilized by the FSRS add-on.
|
||||
|
|
@ -442,6 +444,12 @@ message EvaluateParamsRequest {
|
|||
uint32 num_of_relearning_steps = 3;
|
||||
}
|
||||
|
||||
message EvaluateParamsLegacyRequest {
|
||||
repeated float params = 1;
|
||||
string search = 2;
|
||||
int64 ignore_revlogs_before_ms = 3;
|
||||
}
|
||||
|
||||
message EvaluateParamsResponse {
|
||||
float log_loss = 1;
|
||||
float rmse_bins = 2;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ from anki.importing.apkg import AnkiPackageImporter
|
|||
from anki.importing.base import Importer
|
||||
from anki.importing.csvfile import TextImporter
|
||||
from anki.importing.mnemo import MnemosyneImporter
|
||||
from anki.importing.pauker import PaukerImporter
|
||||
from anki.importing.supermemo_xml import SupermemoXmlImporter # type: ignore
|
||||
from anki.lang import TR
|
||||
|
||||
|
||||
|
|
@ -24,8 +22,6 @@ def importers(col: Collection) -> Sequence[tuple[str, type[Importer]]]:
|
|||
AnkiPackageImporter,
|
||||
),
|
||||
(col.tr.importing_mnemosyne_20_deck_db(), MnemosyneImporter),
|
||||
(col.tr.importing_supermemo_xml_export_xml(), SupermemoXmlImporter),
|
||||
(col.tr.importing_pauker_18_lesson_paugz(), PaukerImporter),
|
||||
]
|
||||
anki.hooks.importing_importers(importers)
|
||||
return importers
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,94 +0,0 @@
|
|||
# Copyright: Andreas Klauer <Andreas.Klauer@metamorpher.de>
|
||||
# License: BSD-3
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
import gzip
|
||||
import html
|
||||
import math
|
||||
import random
|
||||
import time
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from anki.importing.noteimp import ForeignCard, ForeignNote, NoteImporter
|
||||
from anki.stdmodels import _legacy_add_forward_reverse
|
||||
|
||||
ONE_DAY = 60 * 60 * 24
|
||||
|
||||
|
||||
class PaukerImporter(NoteImporter):
|
||||
"""Import Pauker 1.8 Lesson (*.pau.gz)"""
|
||||
|
||||
needMapper = False
|
||||
allowHTML = True
|
||||
|
||||
def run(self):
|
||||
model = _legacy_add_forward_reverse(self.col)
|
||||
model["name"] = "Pauker"
|
||||
self.col.models.save(model, updateReqs=False)
|
||||
self.col.models.set_current(model)
|
||||
self.model = model
|
||||
self.initMapping()
|
||||
NoteImporter.run(self)
|
||||
|
||||
def fields(self):
|
||||
"""Pauker is Front/Back"""
|
||||
return 2
|
||||
|
||||
def foreignNotes(self):
|
||||
"""Build and return a list of notes."""
|
||||
notes = []
|
||||
|
||||
try:
|
||||
f = gzip.open(self.file)
|
||||
tree = ET.parse(f) # type: ignore
|
||||
lesson = tree.getroot()
|
||||
assert lesson.tag == "Lesson"
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
index = -4
|
||||
|
||||
for batch in lesson.findall("./Batch"):
|
||||
index += 1
|
||||
|
||||
for card in batch.findall("./Card"):
|
||||
# Create a note for this card.
|
||||
front = card.findtext("./FrontSide/Text")
|
||||
back = card.findtext("./ReverseSide/Text")
|
||||
note = ForeignNote()
|
||||
assert front and back
|
||||
note.fields = [
|
||||
html.escape(x.strip())
|
||||
.replace("\n", "<br>")
|
||||
.replace(" ", " ")
|
||||
for x in [front, back]
|
||||
]
|
||||
notes.append(note)
|
||||
|
||||
# Determine due date for cards.
|
||||
frontdue = card.find("./FrontSide[@LearnedTimestamp]")
|
||||
backdue = card.find("./ReverseSide[@Batch][@LearnedTimestamp]")
|
||||
|
||||
if frontdue is not None:
|
||||
note.cards[0] = self._learnedCard(
|
||||
index, int(frontdue.attrib["LearnedTimestamp"])
|
||||
)
|
||||
|
||||
if backdue is not None:
|
||||
note.cards[1] = self._learnedCard(
|
||||
int(backdue.attrib["Batch"]),
|
||||
int(backdue.attrib["LearnedTimestamp"]),
|
||||
)
|
||||
|
||||
return notes
|
||||
|
||||
def _learnedCard(self, batch, timestamp):
|
||||
ivl = math.exp(batch)
|
||||
now = time.time()
|
||||
due = ivl - (now - timestamp / 1000.0) / ONE_DAY
|
||||
fc = ForeignCard()
|
||||
fc.due = self.col.sched.today + int(due + 0.5)
|
||||
fc.ivl = random.randint(int(ivl * 0.90), int(ivl + 0.5))
|
||||
fc.factor = random.randint(1500, 2500)
|
||||
return fc
|
||||
|
|
@ -1,484 +0,0 @@
|
|||
# Copyright: petr.michalec@gmail.com
|
||||
# License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
# pytype: disable=attribute-error
|
||||
# type: ignore
|
||||
# pylint: disable=C
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import unicodedata
|
||||
from string import capwords
|
||||
from xml.dom import minidom
|
||||
from xml.dom.minidom import Element, Text
|
||||
|
||||
from anki.collection import Collection
|
||||
from anki.importing.noteimp import ForeignCard, ForeignNote, NoteImporter
|
||||
from anki.stdmodels import _legacy_add_basic_model
|
||||
|
||||
|
||||
class SmartDict(dict):
|
||||
"""
|
||||
See http://www.peterbe.com/plog/SmartDict
|
||||
Copyright 2005, Peter Bengtsson, peter@fry-it.com
|
||||
0BSD
|
||||
|
||||
A smart dict can be instantiated either from a pythonic dict
|
||||
or an instance object (eg. SQL recordsets) but it ensures that you can
|
||||
do all the convenient lookups such as x.first_name, x['first_name'] or
|
||||
x.get('first_name').
|
||||
"""
|
||||
|
||||
def __init__(self, *a, **kw) -> None:
|
||||
if a:
|
||||
if isinstance(type(a[0]), dict):
|
||||
kw.update(a[0])
|
||||
elif isinstance(type(a[0]), object):
|
||||
kw.update(a[0].__dict__)
|
||||
elif hasattr(a[0], "__class__") and a[0].__class__.__name__ == "SmartDict":
|
||||
kw.update(a[0].__dict__)
|
||||
|
||||
dict.__init__(self, **kw)
|
||||
self.__dict__ = self
|
||||
|
||||
|
||||
class SuperMemoElement(SmartDict):
|
||||
"SmartDict wrapper to store SM Element data"
|
||||
|
||||
def __init__(self, *a, **kw) -> None:
|
||||
SmartDict.__init__(self, *a, **kw)
|
||||
# default content
|
||||
self.__dict__["lTitle"] = None
|
||||
self.__dict__["Title"] = None
|
||||
self.__dict__["Question"] = None
|
||||
self.__dict__["Answer"] = None
|
||||
self.__dict__["Count"] = None
|
||||
self.__dict__["Type"] = None
|
||||
self.__dict__["ID"] = None
|
||||
self.__dict__["Interval"] = None
|
||||
self.__dict__["Lapses"] = None
|
||||
self.__dict__["Repetitions"] = None
|
||||
self.__dict__["LastRepetiton"] = None
|
||||
self.__dict__["AFactor"] = None
|
||||
self.__dict__["UFactor"] = None
|
||||
|
||||
|
||||
# This is an AnkiImporter
|
||||
class SupermemoXmlImporter(NoteImporter):
|
||||
needMapper = False
|
||||
allowHTML = True
|
||||
|
||||
"""
|
||||
Supermemo XML export's to Anki parser.
|
||||
Goes through a SM collection and fetch all elements.
|
||||
|
||||
My SM collection was a big mess where topics and items were mixed.
|
||||
I was unable to parse my content in a regular way like for loop on
|
||||
minidom.getElementsByTagName() etc. My collection had also an
|
||||
limitation, topics were splited into branches with max 100 items
|
||||
on each. Learning themes were in deep structure. I wanted to have
|
||||
full title on each element to be stored in tags.
|
||||
|
||||
Code should be upgrade to support importing of SM2006 exports.
|
||||
"""
|
||||
|
||||
def __init__(self, col: Collection, file: str) -> None:
|
||||
"""Initialize internal variables.
|
||||
Pameters to be exposed to GUI are stored in self.META"""
|
||||
NoteImporter.__init__(self, col, file)
|
||||
m = _legacy_add_basic_model(self.col)
|
||||
m["name"] = "Supermemo"
|
||||
self.col.models.save(m)
|
||||
self.initMapping()
|
||||
|
||||
self.lines = None
|
||||
self.numFields = int(2)
|
||||
|
||||
# SmXmlParse VARIABLES
|
||||
self.xmldoc = None
|
||||
self.pieces = []
|
||||
self.cntBuf = [] # to store last parsed data
|
||||
self.cntElm = [] # to store SM Elements data
|
||||
self.cntCol = [] # to store SM Colections data
|
||||
|
||||
# store some meta info related to parse algorithm
|
||||
# SmartDict works like dict / class wrapper
|
||||
self.cntMeta = SmartDict()
|
||||
self.cntMeta.popTitles = False
|
||||
self.cntMeta.title = []
|
||||
|
||||
# META stores controls of import script, should be
|
||||
# exposed to import dialog. These are default values.
|
||||
self.META = SmartDict()
|
||||
self.META.resetLearningData = False # implemented
|
||||
self.META.onlyMemorizedItems = False # implemented
|
||||
self.META.loggerLevel = 2 # implemented 0no,1info,2error,3debug
|
||||
self.META.tagAllTopics = True
|
||||
self.META.pathsToBeTagged = [
|
||||
"English for beginners",
|
||||
"Advanced English 97",
|
||||
"Phrasal Verbs",
|
||||
] # path patterns to be tagged - in gui entered like 'Advanced English 97|My Vocablary'
|
||||
self.META.tagMemorizedItems = True # implemented
|
||||
self.META.logToStdOutput = False # implemented
|
||||
|
||||
self.notes = []
|
||||
|
||||
## TOOLS
|
||||
|
||||
def _fudgeText(self, text: str) -> str:
|
||||
"Replace sm syntax to Anki syntax"
|
||||
text = text.replace("\n\r", "<br>")
|
||||
text = text.replace("\n", "<br>")
|
||||
return text
|
||||
|
||||
def _unicode2ascii(self, str: str) -> str:
|
||||
"Remove diacritic punctuation from strings (titles)"
|
||||
return "".join(
|
||||
[
|
||||
c
|
||||
for c in unicodedata.normalize("NFKD", str)
|
||||
if not unicodedata.combining(c)
|
||||
]
|
||||
)
|
||||
|
||||
def _decode_htmlescapes(self, html: str) -> str:
|
||||
"""Unescape HTML code."""
|
||||
# In case of bad formatted html you can import MinimalSoup etc.. see BeautifulSoup source code
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
# my sm2004 also ecaped & char in escaped sequences.
|
||||
html = re.sub("&", "&", html)
|
||||
|
||||
# https://anki.tenderapp.com/discussions/ankidesktop/39543-anki-is-replacing-the-character-by-when-i-exit-the-html-edit-mode-ctrlshiftx
|
||||
if html.find(">") < 0:
|
||||
return html
|
||||
|
||||
# unescaped solitary chars < or > that were ok for minidom confuse btfl soup
|
||||
# html = re.sub(u'>',u'>',html)
|
||||
# html = re.sub(u'<',u'<',html)
|
||||
|
||||
return str(BeautifulSoup(html, "html.parser"))
|
||||
|
||||
def _afactor2efactor(self, af: float) -> float:
|
||||
# Adapted from <http://www.supermemo.com/beta/xml/xml-core.htm>
|
||||
|
||||
# Ranges for A-factors and E-factors
|
||||
af_min = 1.2
|
||||
af_max = 6.9
|
||||
ef_min = 1.3
|
||||
ef_max = 3.3
|
||||
|
||||
# Sanity checks for the A-factor
|
||||
if af < af_min:
|
||||
af = af_min
|
||||
elif af > af_max:
|
||||
af = af_max
|
||||
|
||||
# Scale af to the range 0..1
|
||||
af_scaled = (af - af_min) / (af_max - af_min)
|
||||
# Rescale to the interval ef_min..ef_max
|
||||
ef = ef_min + af_scaled * (ef_max - ef_min)
|
||||
|
||||
return ef
|
||||
|
||||
## DEFAULT IMPORTER METHODS
|
||||
|
||||
def foreignNotes(self) -> list[ForeignNote]:
|
||||
# Load file and parse it by minidom
|
||||
self.loadSource(self.file)
|
||||
|
||||
# Migrating content / time consuming part
|
||||
# addItemToCards is called for each sm element
|
||||
self.logger("Parsing started.")
|
||||
self.parse()
|
||||
self.logger("Parsing done.")
|
||||
|
||||
# Return imported cards
|
||||
self.total = len(self.notes)
|
||||
self.log.append("%d cards imported." % self.total)
|
||||
return self.notes
|
||||
|
||||
def fields(self) -> int:
|
||||
return 2
|
||||
|
||||
## PARSER METHODS
|
||||
|
||||
def addItemToCards(self, item: SuperMemoElement) -> None:
|
||||
"This method actually do conversion"
|
||||
|
||||
# new anki card
|
||||
note = ForeignNote()
|
||||
|
||||
# clean Q and A
|
||||
note.fields.append(self._fudgeText(self._decode_htmlescapes(item.Question)))
|
||||
note.fields.append(self._fudgeText(self._decode_htmlescapes(item.Answer)))
|
||||
note.tags = []
|
||||
|
||||
# pre-process scheduling data
|
||||
# convert learning data
|
||||
if (
|
||||
not self.META.resetLearningData
|
||||
and int(item.Interval) >= 1
|
||||
and getattr(item, "LastRepetition", None)
|
||||
):
|
||||
# migration of LearningData algorithm
|
||||
tLastrep = time.mktime(time.strptime(item.LastRepetition, "%d.%m.%Y"))
|
||||
tToday = time.time()
|
||||
card = ForeignCard()
|
||||
card.ivl = int(item.Interval)
|
||||
card.lapses = int(item.Lapses)
|
||||
card.reps = int(item.Repetitions) + int(item.Lapses)
|
||||
nextDue = tLastrep + (float(item.Interval) * 86400.0)
|
||||
remDays = int((nextDue - time.time()) / 86400)
|
||||
card.due = self.col.sched.today + remDays
|
||||
card.factor = int(
|
||||
self._afactor2efactor(float(item.AFactor.replace(",", "."))) * 1000
|
||||
)
|
||||
note.cards[0] = card
|
||||
|
||||
# categories & tags
|
||||
# it's worth to have every theme (tree structure of sm collection) stored in tags, but sometimes not
|
||||
# you can deceide if you are going to tag all toppics or just that containing some pattern
|
||||
tTaggTitle = False
|
||||
for pattern in self.META.pathsToBeTagged:
|
||||
if (
|
||||
item.lTitle is not None
|
||||
and pattern.lower() in " ".join(item.lTitle).lower()
|
||||
):
|
||||
tTaggTitle = True
|
||||
break
|
||||
if tTaggTitle or self.META.tagAllTopics:
|
||||
# normalize - remove diacritic punctuation from unicode chars to ascii
|
||||
item.lTitle = [self._unicode2ascii(topic) for topic in item.lTitle]
|
||||
|
||||
# Transform xyz / aaa / bbb / ccc on Title path to Tag xyzAaaBbbCcc
|
||||
# clean things like [999] or [111-2222] from title path, example: xyz / [1000-1200] zyx / xyz
|
||||
# clean whitespaces
|
||||
# set Capital letters for first char of the word
|
||||
tmp = list(
|
||||
{re.sub(r"(\[[0-9]+\])", " ", i).replace("_", " ") for i in item.lTitle}
|
||||
)
|
||||
tmp = list({re.sub(r"(\W)", " ", i) for i in tmp})
|
||||
tmp = list({re.sub("^[0-9 ]+$", "", i) for i in tmp})
|
||||
tmp = list({capwords(i).replace(" ", "") for i in tmp})
|
||||
tags = [j[0].lower() + j[1:] for j in tmp if j.strip() != ""]
|
||||
|
||||
note.tags += tags
|
||||
|
||||
if self.META.tagMemorizedItems and int(item.Interval) > 0:
|
||||
note.tags.append("Memorized")
|
||||
|
||||
self.logger("Element tags\t- " + repr(note.tags), level=3)
|
||||
|
||||
self.notes.append(note)
|
||||
|
||||
def logger(self, text: str, level: int = 1) -> None:
|
||||
"Wrapper for Anki logger"
|
||||
|
||||
dLevels = {0: "", 1: "Info", 2: "Verbose", 3: "Debug"}
|
||||
if level <= self.META.loggerLevel:
|
||||
# self.deck.updateProgress(_(text))
|
||||
|
||||
if self.META.logToStdOutput:
|
||||
print(
|
||||
self.__class__.__name__
|
||||
+ " - "
|
||||
+ dLevels[level].ljust(9)
|
||||
+ " -\t"
|
||||
+ text
|
||||
)
|
||||
|
||||
# OPEN AND LOAD
|
||||
def openAnything(self, source):
|
||||
"""Open any source / actually only opening of files is used
|
||||
@return an open handle which must be closed after use, i.e., handle.close()"""
|
||||
|
||||
if source == "-":
|
||||
return sys.stdin
|
||||
|
||||
# try to open with urllib (if source is http, ftp, or file URL)
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
try:
|
||||
return urllib.request.urlopen(source)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# try to open with native open function (if source is pathname)
|
||||
try:
|
||||
return open(source, encoding="utf8")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# treat source as string
|
||||
import io
|
||||
|
||||
return io.StringIO(str(source))
|
||||
|
||||
def loadSource(self, source: str) -> None:
|
||||
"""Load source file and parse with xml.dom.minidom"""
|
||||
self.source = source
|
||||
self.logger("Load started...")
|
||||
sock = open(self.source, encoding="utf8")
|
||||
self.xmldoc = minidom.parse(sock).documentElement
|
||||
sock.close()
|
||||
self.logger("Load done.")
|
||||
|
||||
# PARSE
|
||||
def parse(self, node: Text | Element | None = None) -> None:
|
||||
"Parse method - parses document elements"
|
||||
|
||||
if node is None and self.xmldoc is not None:
|
||||
node = self.xmldoc
|
||||
|
||||
_method = "parse_%s" % node.__class__.__name__
|
||||
if hasattr(self, _method):
|
||||
parseMethod = getattr(self, _method)
|
||||
parseMethod(node)
|
||||
else:
|
||||
self.logger("No handler for method %s" % _method, level=3)
|
||||
|
||||
def parse_Document(self, node):
|
||||
"Parse XML document"
|
||||
|
||||
self.parse(node.documentElement)
|
||||
|
||||
def parse_Element(self, node: Element) -> None:
|
||||
"Parse XML element"
|
||||
|
||||
_method = "do_%s" % node.tagName
|
||||
if hasattr(self, _method):
|
||||
handlerMethod = getattr(self, _method)
|
||||
handlerMethod(node)
|
||||
else:
|
||||
self.logger("No handler for method %s" % _method, level=3)
|
||||
# print traceback.print_exc()
|
||||
|
||||
def parse_Text(self, node: Text) -> None:
|
||||
"Parse text inside elements. Text is stored into local buffer."
|
||||
|
||||
text = node.data
|
||||
self.cntBuf.append(text)
|
||||
|
||||
# def parse_Comment(self, node):
|
||||
# """
|
||||
# Source can contain XML comments, but we ignore them
|
||||
# """
|
||||
# pass
|
||||
|
||||
# DO
|
||||
def do_SuperMemoCollection(self, node: Element) -> None:
|
||||
"Process SM Collection"
|
||||
|
||||
for child in node.childNodes:
|
||||
self.parse(child)
|
||||
|
||||
def do_SuperMemoElement(self, node: Element) -> None:
|
||||
"Process SM Element (Type - Title,Topics)"
|
||||
|
||||
self.logger("=" * 45, level=3)
|
||||
|
||||
self.cntElm.append(SuperMemoElement())
|
||||
self.cntElm[-1]["lTitle"] = self.cntMeta["title"]
|
||||
|
||||
# parse all child elements
|
||||
for child in node.childNodes:
|
||||
self.parse(child)
|
||||
|
||||
# strip all saved strings, just for sure
|
||||
for key in list(self.cntElm[-1].keys()):
|
||||
if hasattr(self.cntElm[-1][key], "strip"):
|
||||
self.cntElm[-1][key] = self.cntElm[-1][key].strip()
|
||||
|
||||
# pop current element
|
||||
smel = self.cntElm.pop()
|
||||
|
||||
# Process cntElm if is valid Item (and not an Topic etc..)
|
||||
# if smel.Lapses != None and smel.Interval != None and smel.Question != None and smel.Answer != None:
|
||||
if smel.Title is None and smel.Question is not None and smel.Answer is not None:
|
||||
if smel.Answer.strip() != "" and smel.Question.strip() != "":
|
||||
# migrate only memorized otherway skip/continue
|
||||
if self.META.onlyMemorizedItems and not (int(smel.Interval) > 0):
|
||||
self.logger("Element skipped \t- not memorized ...", level=3)
|
||||
else:
|
||||
# import sm element data to Anki
|
||||
self.addItemToCards(smel)
|
||||
self.logger("Import element \t- " + smel["Question"], level=3)
|
||||
|
||||
# print element
|
||||
self.logger("-" * 45, level=3)
|
||||
for key in list(smel.keys()):
|
||||
self.logger(
|
||||
"\t{} {}".format((key + ":").ljust(15), smel[key]), level=3
|
||||
)
|
||||
else:
|
||||
self.logger("Element skipped \t- no valid Q and A ...", level=3)
|
||||
|
||||
else:
|
||||
# now we know that item was topic
|
||||
# parsing of whole node is now finished
|
||||
|
||||
# test if it's really topic
|
||||
if smel.Title is not None:
|
||||
# remove topic from title list
|
||||
t = self.cntMeta["title"].pop()
|
||||
self.logger("End of topic \t- %s" % (t), level=2)
|
||||
|
||||
def do_Content(self, node: Element) -> None:
|
||||
"Process SM element Content"
|
||||
|
||||
for child in node.childNodes:
|
||||
if hasattr(child, "tagName") and child.firstChild is not None:
|
||||
self.cntElm[-1][child.tagName] = child.firstChild.data
|
||||
|
||||
def do_LearningData(self, node: Element) -> None:
|
||||
"Process SM element LearningData"
|
||||
|
||||
for child in node.childNodes:
|
||||
if hasattr(child, "tagName") and child.firstChild is not None:
|
||||
self.cntElm[-1][child.tagName] = child.firstChild.data
|
||||
|
||||
# It's being processed in do_Content now
|
||||
# def do_Question(self, node):
|
||||
# for child in node.childNodes: self.parse(child)
|
||||
# self.cntElm[-1][node.tagName]=self.cntBuf.pop()
|
||||
|
||||
# It's being processed in do_Content now
|
||||
# def do_Answer(self, node):
|
||||
# for child in node.childNodes: self.parse(child)
|
||||
# self.cntElm[-1][node.tagName]=self.cntBuf.pop()
|
||||
|
||||
def do_Title(self, node: Element) -> None:
|
||||
"Process SM element Title"
|
||||
|
||||
t = self._decode_htmlescapes(node.firstChild.data)
|
||||
self.cntElm[-1][node.tagName] = t
|
||||
self.cntMeta["title"].append(t)
|
||||
self.cntElm[-1]["lTitle"] = self.cntMeta["title"]
|
||||
self.logger("Start of topic \t- " + " / ".join(self.cntMeta["title"]), level=2)
|
||||
|
||||
def do_Type(self, node: Element) -> None:
|
||||
"Process SM element Type"
|
||||
|
||||
if len(self.cntBuf) >= 1:
|
||||
self.cntElm[-1][node.tagName] = self.cntBuf.pop()
|
||||
|
||||
|
||||
# if __name__ == '__main__':
|
||||
|
||||
# for testing you can start it standalone
|
||||
|
||||
# file = u'/home/epcim/hg2g/dev/python/sm2anki/ADVENG2EXP.xxe.esc.zaloha_FINAL.xml'
|
||||
# file = u'/home/epcim/hg2g/dev/python/anki/libanki/tests/importing/supermemo/original_ENGLISHFORBEGGINERS_noOEM.xml'
|
||||
# file = u'/home/epcim/hg2g/dev/python/anki/libanki/tests/importing/supermemo/original_ENGLISHFORBEGGINERS_oem_1250.xml'
|
||||
# file = str(sys.argv[1])
|
||||
# impo = SupermemoXmlImporter(Deck(),file)
|
||||
# impo.foreignCards()
|
||||
|
||||
# sys.exit(1)
|
||||
|
||||
# vim: ts=4 sts=2 ft=python
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -35,8 +35,16 @@ class CustomBuildHook(BuildHookInterface):
|
|||
|
||||
assert generated_root.exists(), "you should build with --wheel"
|
||||
for path in generated_root.rglob("*"):
|
||||
if path.is_file():
|
||||
if path.is_file() and not self._should_exclude(path):
|
||||
relative_path = path.relative_to(generated_root)
|
||||
# Place files under anki/ in the distribution
|
||||
dist_path = "anki" / relative_path
|
||||
force_include[str(path)] = str(dist_path)
|
||||
|
||||
def _should_exclude(self, path: Path) -> bool:
|
||||
"""Check if a file should be excluded from the wheel."""
|
||||
# Exclude __pycache__
|
||||
path_str = str(path)
|
||||
if "/__pycache__/" in path_str:
|
||||
return True
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -4,19 +4,15 @@ dynamic = ["version"]
|
|||
requires-python = ">=3.9"
|
||||
license = "AGPL-3.0-or-later"
|
||||
dependencies = [
|
||||
"beautifulsoup4",
|
||||
"decorator",
|
||||
"markdown",
|
||||
"orjson",
|
||||
"protobuf>=4.21",
|
||||
"requests[socks]",
|
||||
# remove after we update to min python 3.11+
|
||||
"typing_extensions",
|
||||
"types-protobuf",
|
||||
"types-requests",
|
||||
"types-orjson",
|
||||
# platform-specific dependencies
|
||||
"distro; sys_platform != 'darwin' and sys_platform != 'win32'",
|
||||
"psutil; sys_platform == 'win32'",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ from anki.importing import (
|
|||
Anki2Importer,
|
||||
AnkiPackageImporter,
|
||||
MnemosyneImporter,
|
||||
SupermemoXmlImporter,
|
||||
TextImporter,
|
||||
)
|
||||
from tests.shared import getEmptyCol, getUpgradeDeckPath
|
||||
|
|
@ -306,22 +305,6 @@ def test_csv_tag_only_if_modified():
|
|||
col.close()
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:Using or importing the ABCs")
|
||||
def test_supermemo_xml_01_unicode():
|
||||
col = getEmptyCol()
|
||||
file = str(os.path.join(testDir, "support", "supermemo1.xml"))
|
||||
i = SupermemoXmlImporter(col, file)
|
||||
# i.META.logToStdOutput = True
|
||||
i.run()
|
||||
assert i.total == 1
|
||||
cid = col.db.scalar("select id from cards")
|
||||
c = col.get_card(cid)
|
||||
# Applies A Factor-to-E Factor conversion
|
||||
assert c.factor == 2879
|
||||
assert c.reps == 7
|
||||
col.close()
|
||||
|
||||
|
||||
def test_mnemo():
|
||||
col = getEmptyCol()
|
||||
file = str(os.path.join(testDir, "support", "mnemo.db"))
|
||||
|
|
|
|||
|
|
@ -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,16 +7,23 @@ 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",
|
||||
"types-protobuf",
|
||||
"types-requests",
|
||||
"types-orjson",
|
||||
"types-decorator",
|
||||
"types-flask",
|
||||
"types-flask-cors",
|
||||
"types-markdown",
|
||||
"types-waitress",
|
||||
"types-pywin32",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -66,7 +66,8 @@ def show(mw: aqt.AnkiQt) -> QDialog:
|
|||
# WebView contents
|
||||
######################################################################
|
||||
abouttext = "<center><img src='/_anki/imgs/anki-logo-thin.png'></center>"
|
||||
abouttext += f"<p>{tr.about_anki_is_a_friendly_intelligent_spaced()}"
|
||||
lede = tr.about_anki_is_a_friendly_intelligent_spaced().replace("Anki", "Anki®")
|
||||
abouttext += f"<p>{lede}"
|
||||
abouttext += f"<p>{tr.about_anki_is_licensed_under_the_agpl3()}"
|
||||
abouttext += f"<p>{tr.about_version(val=version_with_build())}<br>"
|
||||
abouttext += ("Python %s Qt %s PyQt %s<br>") % (
|
||||
|
|
@ -223,6 +224,7 @@ def show(mw: aqt.AnkiQt) -> QDialog:
|
|||
"Mukunda Madhav Dey",
|
||||
"Adnane Taghi",
|
||||
"Anon_0000",
|
||||
"Bilolbek Normuminov",
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ from anki.hooks import runFilter
|
|||
from anki.httpclient import HttpClient
|
||||
from anki.models import NotetypeDict, NotetypeId, StockNotetype
|
||||
from anki.notes import Note, NoteFieldsCheckResult, NoteId
|
||||
from anki.utils import checksum, is_lin, is_mac, is_win, namedtmp
|
||||
from anki.utils import checksum, is_lin, is_win, namedtmp
|
||||
from aqt import AnkiQt, colors, gui_hooks
|
||||
from aqt.operations import QueryOp
|
||||
from aqt.operations.note import update_note
|
||||
|
|
@ -343,7 +343,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
|
||||
|
|
@ -796,7 +796,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
def accept(file: str) -> None:
|
||||
self.resolve_media(file)
|
||||
|
||||
file = getFile(
|
||||
getFile(
|
||||
parent=self.widget,
|
||||
title=tr.editing_add_media(),
|
||||
cb=cast(Callable[[Any], None], accept),
|
||||
|
|
@ -999,7 +999,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
if html.find(">") < 0:
|
||||
return html
|
||||
|
||||
with warnings.catch_warnings() as w:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", UserWarning)
|
||||
doc = BeautifulSoup(html, "html.parser")
|
||||
|
||||
|
|
@ -1029,15 +1029,14 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
m = re.match(r"http://127.0.0.1:\d+/(.*)$", str(src))
|
||||
if m:
|
||||
tag["src"] = m.group(1)
|
||||
else:
|
||||
# in external pastes, download remote media
|
||||
if isinstance(src, str) and self.isURL(src):
|
||||
fname = self._retrieveURL(src)
|
||||
if fname:
|
||||
tag["src"] = fname
|
||||
elif isinstance(src, str) and src.startswith("data:image/"):
|
||||
# and convert inlined data
|
||||
tag["src"] = self.inlinedImageToFilename(str(src))
|
||||
# in external pastes, download remote media
|
||||
elif isinstance(src, str) and self.isURL(src):
|
||||
fname = self._retrieveURL(src)
|
||||
if fname:
|
||||
tag["src"] = fname
|
||||
elif isinstance(src, str) and src.startswith("data:image/"):
|
||||
# and convert inlined data
|
||||
tag["src"] = self.inlinedImageToFilename(str(src))
|
||||
|
||||
html = str(doc)
|
||||
return html
|
||||
|
|
@ -1102,7 +1101,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
)
|
||||
filter = f"{tr.editing_media()} ({extension_filter})"
|
||||
|
||||
file = getFile(
|
||||
getFile(
|
||||
parent=self.widget,
|
||||
title=tr.editing_add_media(),
|
||||
cb=cast(Callable[[Any], None], self.setup_mask_editor),
|
||||
|
|
@ -1735,10 +1734,9 @@ class EditorWebView(AnkiWebView):
|
|||
assert a is not None
|
||||
qconnect(a.triggered, lambda: openFolder(path))
|
||||
|
||||
if is_win or is_mac:
|
||||
a = menu.addAction(tr.editing_show_in_folder())
|
||||
assert a is not None
|
||||
qconnect(a.triggered, lambda: show_in_folder(path))
|
||||
a = menu.addAction(tr.editing_show_in_folder())
|
||||
assert a is not None
|
||||
qconnect(a.triggered, lambda: show_in_folder(path))
|
||||
|
||||
def _clipboard(self) -> QClipboard:
|
||||
clipboard = self.editor.mw.app.clipboard()
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue